Files
TurboSu/server/telemetry/parsers/__init__.py
T
AskaEth 1649f91318 fix: correct Forza FH5/FH6 UDP packet parser offsets per official spec
- Speed: offset 256 (m/s * 3.6 = km/h), was incorrectly at 244 (PositionX)
- Gear: offset 319 (U8), was at 264/298
- Throttle/Brake: offsets 315/316 (U8, 0-255)
- Steering: offset 320 (S8, -127~127)
- RPM: offset 16 ✓, MaxRPM: offset 8 ✓
- Lap times: BestLap=296, LastLap=300, CurrentLap=304
- LapNumber: offset 312 (U16)
- Boost: 284, Fuel: 288, Position: 244/248/252
- Updated all Forza parsers (FH4, FH5) and fallback parser
- Ref: Forza Data Out official format (324 bytes)
2026-07-25 16:19:53 +08:00

231 lines
8.3 KiB
Python

from __future__ import annotations
import struct
import time
from server.telemetry.data import TelemetryData
from server.telemetry.parsers.base import BaseParser
from utils.logger import get_logger
logger = get_logger(__name__)
class ForzaParser(BaseParser):
FORZA_FORMATS = {
"fh4": "Forza Horizon 4",
"fh5": "Forza Horizon 5",
"fm8": "Forza Motorsport",
}
def __init__(self, format_id: str = "fh5"):
self._format = format_id
def game_id(self) -> str:
return self._format
def parse(self, data: bytes, addr: tuple[str, int]) -> TelemetryData:
td = TelemetryData(game_id=self._format, timestamp=time.time())
td.raw = {"raw_hex": data.hex(), "length": len(data), "addr": f"{addr[0]}:{addr[1]}"}
if len(data) < 312:
logger.warning("Forza data too short: %d bytes", len(data))
return td
try:
td.raw["is_race_on"] = struct.unpack_from("<i", data, 0)[0]
td.rpm = struct.unpack_from("<f", data, 16)[0]
td.max_rpm = struct.unpack_from("<f", data, 8)[0]
td.horsepower = struct.unpack_from("<f", data, 260)[0] / 745.7
td.torque = struct.unpack_from("<f", data, 264)[0]
td.boost = struct.unpack_from("<f", data, 284)[0]
td.fuel = struct.unpack_from("<f", data, 288)[0]
td.speed_kmh = struct.unpack_from("<f", data, 256)[0] * 3.6
td.speed_mph = td.speed_kmh * 0.621371
td.gear = struct.unpack_from("<B", data, 319)[0]
td.best_lap = struct.unpack_from("<f", data, 296)[0]
td.last_lap = struct.unpack_from("<f", data, 300)[0]
td.lap_time = struct.unpack_from("<f", data, 304)[0]
td.lap_number = struct.unpack_from("<H", data, 312)[0]
td.position_x = struct.unpack_from("<f", data, 244)[0]
td.position_y = struct.unpack_from("<f", data, 248)[0]
td.position_z = struct.unpack_from("<f", data, 252)[0]
td.acceleration_x = struct.unpack_from("<f", data, 20)[0]
td.acceleration_y = struct.unpack_from("<f", data, 24)[0]
td.acceleration_z = struct.unpack_from("<f", data, 28)[0]
td.throttle = struct.unpack_from("<B", data, 315)[0] / 255.0
td.brake = struct.unpack_from("<B", data, 316)[0] / 255.0
td.steering = max(-1.0, min(1.0, struct.unpack_from("<b", data, 320)[0] / 127.0))
td.clutch = struct.unpack_from("<B", data, 317)[0] / 255.0
td.handbrake = struct.unpack_from("<B", data, 318)[0] / 255.0
td.raw.update({
"speed_mph": td.speed_mph,
"speed_kmh": td.speed_kmh,
"rpm": td.rpm,
"max_rpm": td.max_rpm,
"gear": td.gear,
"throttle": td.throttle,
"brake": td.brake,
"steering": td.steering,
"boost": td.boost,
"horsepower": td.horsepower,
"torque": td.torque,
})
except struct.error as e:
logger.error("Forza parse error: %s", e)
return td
class ACCParser(BaseParser):
def game_id(self) -> str:
return "acc"
def parse(self, data: bytes, addr: tuple[str, int]) -> TelemetryData:
td = TelemetryData(game_id="acc", timestamp=time.time())
td.raw = {"raw_hex": data.hex(), "length": len(data), "addr": f"{addr[0]}:{addr[1]}"}
if len(data) < 200:
logger.warning("ACC data too short: %d bytes", len(data))
return td
try:
td.speed_kmh = struct.unpack_from("<f", data, 0)[0]
td.speed_mph = td.speed_kmh * 0.621371
td.rpm = struct.unpack_from("<f", data, 4)[0]
td.max_rpm = struct.unpack_from("<f", data, 8)[0]
td.gear = struct.unpack_from("<B", data, 12)[0]
td.throttle = struct.unpack_from("<f", data, 16)[0]
td.brake = struct.unpack_from("<f", data, 20)[0]
td.steering = struct.unpack_from("<f", data, 24)[0]
td.fuel = struct.unpack_from("<f", data, 28)[0]
td.raw.update({
"speed_kmh": td.speed_kmh,
"rpm": td.rpm,
"gear": td.gear,
"throttle": td.throttle,
"brake": td.brake,
})
except struct.error as e:
logger.error("ACC parse error: %s", e)
return td
class F1Parser(BaseParser):
def game_id(self) -> str:
return "f1"
def parse(self, data: bytes, addr: tuple[str, int]) -> TelemetryData:
td = TelemetryData(game_id="f1", timestamp=time.time())
td.raw = {"raw_hex": data.hex(), "length": len(data), "addr": f"{addr[0]}:{addr[1]}"}
if len(data) < 1289:
logger.warning("F1 data too short: %d bytes", len(data))
return td
try:
td.speed_kmh = struct.unpack_from("<f", data, 37)[0]
td.speed_mph = td.speed_kmh * 0.621371
td.rpm = struct.unpack_from("<H", data, 41)[0]
td.max_rpm = struct.unpack_from("<H", data, 43)[0]
td.gear = struct.unpack_from("<B", data, 46)[0] & 0x0F
td.throttle = struct.unpack_from("<f", data, 47)[0]
td.brake = struct.unpack_from("<f", data, 55)[0]
td.steering = struct.unpack_from("<B", data, 45)[0] / 127.0
td.lap_number = struct.unpack_from("<B", data, 262)[0]
td.lap_time = struct.unpack_from("<f", data, 63)[0]
td.best_lap = struct.unpack_from("<f", data, 268)[0]
td.fuel = struct.unpack_from("<f", data, 51)[0]
td.raw.update({
"speed_kmh": td.speed_kmh,
"rpm": td.rpm,
"gear": td.gear,
"throttle": td.throttle,
"brake": td.brake,
"lap_time": td.lap_time,
})
except struct.error as e:
logger.error("F1 parse error: %s", e)
return td
class IRacingParser(BaseParser):
def game_id(self) -> str:
return "iracing"
def parse(self, data: bytes, addr: tuple[str, int]) -> TelemetryData:
td = TelemetryData(game_id="iracing", timestamp=time.time())
td.raw = {"raw_hex": data.hex(), "length": len(data), "addr": f"{addr[0]}:{addr[1]}"}
if len(data) < 100:
logger.warning("iRacing data too short: %d bytes", len(data))
return td
try:
td.speed_mph = struct.unpack_from("<f", data, 36)[0]
td.speed_kmh = td.speed_mph * 1.60934
td.rpm = struct.unpack_from("<f", data, 48)[0]
td.gear = struct.unpack_from("<i", data, 56)[0]
td.throttle = struct.unpack_from("<f", data, 4)[0]
td.brake = struct.unpack_from("<f", data, 8)[0]
td.steering = struct.unpack_from("<f", data, 0)[0]
td.raw.update({
"speed_mph": td.speed_mph,
"rpm": td.rpm,
"gear": td.gear,
"throttle": td.throttle,
"brake": td.brake,
})
except struct.error as e:
logger.error("iRacing parse error: %s", e)
return td
class ACParser(BaseParser):
def game_id(self) -> str:
return "ac"
def parse(self, data: bytes, addr: tuple[str, int]) -> TelemetryData:
td = TelemetryData(game_id="ac", timestamp=time.time())
td.raw = {"raw_hex": data.hex(), "length": len(data), "addr": f"{addr[0]}:{addr[1]}"}
try:
parts = data.decode("utf-8", errors="replace").rstrip("\r\n").split("\t")
if len(parts) < 10:
return td
td.speed_kmh = float(parts[0])
td.speed_mph = td.speed_kmh * 0.621371
td.rpm = float(parts[1])
td.gear = int(float(parts[2]))
td.throttle = float(parts[3])
td.brake = float(parts[4])
td.steering = float(parts[5])
td.fuel = float(parts[6])
td.raw.update({
"speed_kmh": td.speed_kmh,
"rpm": td.rpm,
"gear": td.gear,
})
except Exception as e:
logger.error("AC parse error: %s", e)
return td
PARSER_MAP: dict[str, type[BaseParser]] = {
"forza": ForzaParser,
"ac": ACParser,
"acc": ACCParser,
"f1": F1Parser,
"iracing": IRacingParser,
}