commit e1a478d9b52f75b2942b00f43455531e26a753cb Author: Eduardo Silva Date: Sat Oct 26 22:56:42 2024 +0100 First commit This project shall offer a iRacing telemetry parser with as little dependencies as possible diff --git a/diskSubHeader.go b/diskSubHeader.go new file mode 100644 index 0000000..8ce721f --- /dev/null +++ b/diskSubHeader.go @@ -0,0 +1,47 @@ +package main + +import ( + "bytes" + "encoding/binary" + "fmt" +) + +const ( + SubHeaderSize = 32 // SubHeaderSize is the size of the subheader +) + +// DiskSubHeader represents the IBT sub headers +type DiskSubHeader struct { + StartDate float64 // StartDate represents the start data of the telemetry + StartTime float64 // StartTime ... + EndTime float64 // EndTime ... + LapCount int32 // LapCount represents the total number laps + RecordCount int32 // RecordCount ... +} + +// parseTelemetrySubHeader will return a pointer to a DiskSubHeader variable +// or nil if an error occurs. In which case the error return value is more +// valuable +func parseTelemetrySubHeader(buf [SubHeaderSize]byte) (*DiskSubHeader, error) { + dst := DiskSubHeader{} + err := binary.Read(bytes.NewBuffer(buf[:]), binary.LittleEndian, &dst) + if err != nil { + return nil, err + } + + return &dst, nil +} + +// ToString renders a string showing the values of the struct +func (d *DiskSubHeader) ToString() string { + return fmt.Sprintf( + "StartDate: %13f (0x%04x)\n" + + "StartTime: %13f (0x%08x)\n" + + "EndTime: %13f (0x%08x)\n" + + "LapCount: %13d (0x%04x)\n" + + "RecordCount: %13d (0x%04x)\n", + d.StartDate, d.StartDate, d.StartTime, d.StartTime, + d.EndTime, d.EndTime, d.LapCount, d.LapCount, + d.RecordCount, d.RecordCount, + ) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8471af3 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module ibtReader + +go 1.23.2 + +require gopkg.in/yaml.v3 v3.0.1 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a62c313 --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/headers.go b/headers.go new file mode 100644 index 0000000..b28b4ea --- /dev/null +++ b/headers.go @@ -0,0 +1,80 @@ +package main + +import ( + "bytes" + "encoding/binary" + "fmt" +) + +const ( + FileHeaderSize = 56 // FileHeaderSize is the size of the headers + HeaderSize = 4 // HeaderSize is the size of a single header +) + +// TelemetryHeaders struct to hold an IBT file's headers +type TelemetryHeaders struct { + Version int32 + // Status of 1 indicates a completed session and status of 0 a live session + Status int32 + // TickRate indicates the frequency of writes (usually 60) + TickRate int32 + // SessionInfoUpdate indicates the number of times the SessionInfo was + // updated. 0 for finished sessions and >1 for active sessions + SessionInfoUpdate int32 + // SessionInfoLength is the length of the session info buffer + SessionInfoLength int32 + // SessionInfoOffset is the offset of the session info in the buffer + SessionInfoOffset int32 + // NumVars is the number of variables in each input + NumVars int32 + // VarHeaderOffset is the offset of the VarHeader + VarHeaderOffset int32 + // NumBuf will be 1 for static files and 3 for live telemetry files + NumBuf int32 + // BufLen is the length for parsing VarHeader values + BufLen int32 + // Padding + Padding [12]byte + // I still don't know what this is: + BufOffset int32 +} + +// ToString renders a string showing the values of the struct +func (th *TelemetryHeaders) ToString() string { + return fmt.Sprintf( + "Version: %5d (0x%04x)\n"+ + "Status: %5d (0x%04x)\n"+ + "TickRate: %5d (0x%04x)\n"+ + "SIUpdate: %5d (0x%04x)\n"+ + "SILength: %5d (0x%04x)\n"+ + "SIOffset: %5d (0x%04x)\n"+ + "NumVars: %5d (0x%04x)\n"+ + "VarHeaderOffset: %5d (0x%04x)\n"+ + "NumBuf: %5d (0x%04x)\n"+ + "BufLen: %5d (0x%04x)\n"+ + "BufOffset: %5d (0x%04x)\n", + th.Version, th.Version, th.Status, th.Status, th.TickRate, th.TickRate, + th.SessionInfoUpdate, th.SessionInfoUpdate, + th.SessionInfoLength, th.SessionInfoLength, + th.SessionInfoOffset, th.SessionInfoOffset, + th.NumVars, th.NumVars, th.VarHeaderOffset, th.VarHeaderOffset, + th.NumBuf, th.NumBuf, th.BufLen, th.BufLen, th.BufOffset, th.BufOffset, + ) +} + +// parseTelemetryHeader will read the IBT file headers from a correctly sized +// buffer. +// You need to pass a the first FILE_HEADER_SIZE bytes of the buffer +func parseTelemetryHeader(buf [FileHeaderSize]byte) (*TelemetryHeaders, error) { + if len(buf)%HeaderSize != 0 { + return nil, fmt.Errorf("buffer must be multiple of size: %d", HeaderSize) + } + + dst := TelemetryHeaders{} + err := binary.Read(bytes.NewBuffer(buf[:]), binary.LittleEndian, &dst) + if err != nil { + return nil, fmt.Errorf("unable to unpack data: %v", err) + } + + return &dst, nil +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..04aca0e --- /dev/null +++ b/main.go @@ -0,0 +1,210 @@ +// Package IbtParser is all you need for you iRacing telemetry parsing +package main + +import ( + "bytes" + "encoding/binary" + "fmt" + "io" + "log" + "math" + "os" + "strings" + "time" +) + +const ( + ibtFile = "./telemetryFiles/mx5_2016Okayama_full_2024_10_19_22_02_12.ibt" +) + +// Reader is an interface (??? very useful) +type Reader interface { + io.Reader + io.ReaderAt + io.ReadCloser +} + +// IBT struct will hold the relevant data for a given IBT file +type IBT struct { + File Reader // Source of the data + Headers *TelemetryHeaders // IBT file Headers + SubHeaders *DiskSubHeader // IBT file Sub Headers + SessionInfo *SessionInfoYAML // IBT file Session Info + Vars *TelemetryVars + LastValidData int64 + Tick int32 +} + +// Init serves to initialize and get a hold of a IBT struct +func Init(f Reader) (*IBT, error) { + // Read the header of the file + var err error + ibt := IBT{ + File: f, + Vars: &TelemetryVars{}, + } + + // Read the file headers + var headerRaw [FileHeaderSize]byte + _, err = ibt.File.ReadAt(headerRaw[:], 0) + if err != nil { + return nil, fmt.Errorf("Failed to read from file: %v", err) + } + ibt.Headers, err = parseTelemetryHeader(headerRaw) + if err != nil { + return nil, fmt.Errorf("Unable to read headers from file: %v", err) + } + + // Read the disk sub headers + var subheaderRaw [SubHeaderSize]byte + _, err = ibt.File.ReadAt(subheaderRaw[:], FileHeaderSize) + if err != nil { + return nil, fmt.Errorf("Failed to read subheader from file: %v", err) + } + ibt.SubHeaders, err = parseTelemetrySubHeader(subheaderRaw) + if err != nil { + return nil, fmt.Errorf("Unable to parse subheaders from file: %v", err) + } + + // Read session info string + sessionInfoStringRaw := make([]byte, ibt.Headers.SessionInfoLength) + _, err = ibt.File.ReadAt(sessionInfoStringRaw, int64(ibt.Headers.SessionInfoOffset)) + if err != nil { + return nil, fmt.Errorf("Failed to read sessionInfoString from file: %v", err) + } + ibt.SessionInfo, err = parseSessionInfo(sessionInfoStringRaw) + if err != nil { + return nil, fmt.Errorf("Unable to parse Session Info from file: %v", err) + } + + return &ibt, nil +} + +func (i *IBT) readVariablerHeaders() { + i.Vars = &TelemetryVars{Vars: make(map[string]Var, i.Headers.NumVars)} + + var k int32 + for k = 0; k < i.Headers.NumVars; k++ { + rbuf := make([]byte, VarSize) + + _, err := i.File.ReadAt(rbuf, int64(i.Headers.VarHeaderOffset+k*VarSize)) + if err != nil { + log.Fatal(err) + } + + var dst IBTVar + err = binary.Read(bytes.NewBuffer(rbuf[:]), binary.LittleEndian, &dst) + if err != nil { + log.Fatal(err) + } + + v := Var{ + Type: dst.Type, + Offset: dst.Offset, + Count: dst.Count, + CountAsTime: dst.CountAsTime, + Name: strings.TrimLeft(strings.TrimRight(string(dst.Name[:]), "\x00"), "\x00"), + Description: strings.TrimLeft(strings.TrimRight(string(dst.Description[:]), "\x00"), "\x00"), + Unit: strings.TrimLeft(strings.TrimRight(string(dst.Unit[:]), "\x00"), "\x00"), + Value: nil, + } + + // fmt.Println(dst.Name) + // fmt.Println(v.Name) + + i.Vars.Vars[v.Name] = v + } +} + +func (i *IBT) readData() error { + start := i.Headers.BufOffset + i.Tick*i.Headers.BufLen + buf := make([]byte, i.Headers.BufLen) + _, err := i.File.ReadAt(buf, int64(start)) + if err != nil { + return err + } + + for k, v := range i.Vars.Vars { + rbuf := buf[v.Offset : v.Offset+int32(VarTypes[int(v.Type)].Size)] + + // Read the value + switch v.Type { + case IRSDK_char: + v.Value = string(rbuf[0]) + case IRSDK_bool: + v.Value = int(rbuf[0]) > 0 + case IRSDK_int: + v.Value = int(binary.LittleEndian.Uint32(rbuf)) + case IRSDK_bitField: + v.Value = fmt.Sprintf("0x%x", int(binary.LittleEndian.Uint32(rbuf))) + case IRSDK_float: + v.Value = math.Float32frombits(uint32(binary.LittleEndian.Uint32(rbuf))) + case IRSDK_double: + v.Value = math.Float64frombits(uint64(binary.LittleEndian.Uint64(rbuf))) + } + // -------------- + + i.Vars.Vars[k] = v + } + + i.Tick++ + + return nil +} + +func (i *IBT) Update() bool { + err := i.readData() + if err != nil && err != io.EOF { + log.Fatalf("What happened?\n%v\n", err) + } + + if err == io.EOF { + return false + } + + return true +} + +func msToKph(v float32) int { + return int((3600 * v) / 1000) +} + +func main() { + fmt.Println("================== IBT FILE PARSER ==================") + + file, err := os.Open(ibtFile) + if err != nil { + log.Fatalf("Failed to open IBT file: %v", err) + } + + ibt, err := Init(file) + fmt.Printf("%s\n", ibt.Headers.ToString()) + fmt.Printf("%s", ibt.SubHeaders.ToString()) + // fmt.Println(ibt.SessionInfo.ToString()) + + // last := time.Now().Unix() + ibt.readVariablerHeaders() + ibt.Update() + + last := time.Now().UnixMilli() + for { + time.Sleep(time.Second / 60) + res := ibt.Update() + + curTime := time.Now().UnixMilli() + + if curTime - last > 250{ + fmt.Printf(" \r") + if val, ok := ibt.Vars.Vars["Speed"]; ok { + fmt.Printf("\r%d %d", ibt.Tick/60, msToKph(val.Value.(float32))) + } else { + fmt.Printf("\r%d %s", ibt.Tick/60, "KEY DOESN'T EXIST") + } + } + + if !res { + fmt.Println("\nEnd of file found...") + break + } + } +} diff --git a/sessionInfo.go b/sessionInfo.go new file mode 100644 index 0000000..12263db --- /dev/null +++ b/sessionInfo.go @@ -0,0 +1,324 @@ +package main + +import ( + "encoding/json" + + "gopkg.in/yaml.v3" +) + +// SessionInfoYAML is a string with session info in the IBT file +type SessionInfoYAML struct { + WeekendInfo struct { + TrackName string `yaml:"TrackName"` + TrackID int `yaml:"TrackID"` + TrackLength string `yaml:"TrackLength"` + TrackDisplayName string `yaml:"TrackDisplayName"` + TrackDisplayShortName string `yaml:"TrackDisplayShortName"` + TrackConfigName string `yaml:"TrackConfigName"` + TrackCity string `yaml:"TrackCity"` + TrackCountry string `yaml:"TrackCountry"` + TrackAltitude string `yaml:"TrackAltitude"` + TrackLatitude string `yaml:"TrackLatitude"` + TrackLongitude string `yaml:"TrackLongitude"` + TrackNorthOffset string `yaml:"TrackNorthOffset"` + TrackNumTurns int `yaml:"TrackNumTurns"` + TrackPitSpeedLimit string `yaml:"TrackPitSpeedLimit"` + TrackType string `yaml:"TrackType"` + TrackDirection string `yaml:"TrackDirection"` + TrackWeatherType string `yaml:"TrackWeatherType"` + TrackSkies string `yaml:"TrackSkies"` + TrackSurfaceTemp string `yaml:"TrackSurfaceTemp"` + TrackAirTemp string `yaml:"TrackAirTemp"` + TrackAirPressure string `yaml:"TrackAirPressure"` + TrackWindVel string `yaml:"TrackWindVel"` + TrackWindDir string `yaml:"TrackWindDir"` + TrackRelativeHumidity string `yaml:"TrackRelativeHumidity"` + TrackFogLevel string `yaml:"TrackFogLevel"` + TrackCleanup int `yaml:"TrackCleanup"` + TrackDynamicTrack int `yaml:"TrackDynamicTrack"` + TrackVersion string `yaml:"TrackVersion"` + SeriesID int `yaml:"SeriesID"` + SeasonID int `yaml:"SeasonID"` + SessionID int `yaml:"SessionID"` + SubSessionID int `yaml:"SubSessionID"` + LeagueID int `yaml:"LeagueID"` + Official int `yaml:"Official"` + RaceWeek int `yaml:"RaceWeek"` + EventType string `yaml:"EventType"` + Category string `yaml:"Category"` + SimMode string `yaml:"SimMode"` + TeamRacing int `yaml:"TeamRacing"` + MinDrivers int `yaml:"MinDrivers"` + MaxDrivers int `yaml:"MaxDrivers"` + DCRuleSet string `yaml:"DCRuleSet"` + QualifierMustStartRace int `yaml:"QualifierMustStartRace"` + NumCarClasses int `yaml:"NumCarClasses"` + NumCarTypes int `yaml:"NumCarTypes"` + HeatRacing int `yaml:"HeatRacing"` + BuildType string `yaml:"BuildType"` + BuildTarget string `yaml:"BuildTarget"` + BuildVersion string `yaml:"BuildVersion"` + WeekendOptions struct { + NumStarters int `yaml:"NumStarters"` + StartingGrid string `yaml:"StartingGrid"` + QualifyScoring string `yaml:"QualifyScoring"` + CourseCautions string `yaml:"CourseCautions"` + StandingStart int `yaml:"StandingStart"` + ShortParadeLap int `yaml:"ShortParadeLap"` + Restarts string `yaml:"Restarts"` + WeatherType string `yaml:"WeatherType"` + Skies string `yaml:"Skies"` + WindDirection string `yaml:"WindDirection"` + WindSpeed string `yaml:"WindSpeed"` + WeatherTemp string `yaml:"WeatherTemp"` + RelativeHumidity string `yaml:"RelativeHumidity"` + FogLevel string `yaml:"FogLevel"` + TimeOfDay string `yaml:"TimeOfDay"` + Date string `yaml:"Date"` + EarthRotationSpeedupFactor int `yaml:"EarthRotationSpeedupFactor"` + Unofficial int `yaml:"Unofficial"` + CommercialMode string `yaml:"CommercialMode"` + NightMode string `yaml:"NightMode"` + IsFixedSetup int `yaml:"IsFixedSetup"` + StrictLapsChecking string `yaml:"StrictLapsChecking"` + HasOpenRegistration int `yaml:"HasOpenRegistration"` + HardcoreLevel int `yaml:"HardcoreLevel"` + NumJokerLaps int `yaml:"NumJokerLaps"` + IncidentLimit string `yaml:"IncidentLimit"` + FastRepairsLimit string `yaml:"FastRepairsLimit"` + GreenWhiteCheckeredLimit int `yaml:"GreenWhiteCheckeredLimit"` + } `yaml:"WeekendOptions"` + TelemetryOptions struct { + TelemetryDiskFile string `yaml:"TelemetryDiskFile"` + } `yaml:"TelemetryOptions"` + } `yaml:"WeekendInfo"` + SessionInfo struct { + Sessions []struct { + SessionNum int `yaml:"SessionNum"` + SessionLaps string `yaml:"SessionLaps"` + SessionTime string `yaml:"SessionTime"` + SessionNumLapsToAvg int `yaml:"SessionNumLapsToAvg"` + SessionType string `yaml:"SessionType"` + SessionTrackRubberState string `yaml:"SessionTrackRubberState"` + SessionName string `yaml:"SessionName"` + SessionSubType interface{} `yaml:"SessionSubType"` + SessionSkipped int `yaml:"SessionSkipped"` + SessionRunGroupsUsed int `yaml:"SessionRunGroupsUsed"` + ResultsPositions interface{} `yaml:"ResultsPositions"` + ResultsFastestLap []struct { + CarIdx int `yaml:"CarIdx"` + FastestLap int `yaml:"FastestLap"` + FastestTime int `yaml:"FastestTime"` + } `yaml:"ResultsFastestLap"` + ResultsAverageLapTime int `yaml:"ResultsAverageLapTime"` + ResultsNumCautionFlags int `yaml:"ResultsNumCautionFlags"` + ResultsNumCautionLaps int `yaml:"ResultsNumCautionLaps"` + ResultsNumLeadChanges int `yaml:"ResultsNumLeadChanges"` + ResultsLapsComplete int `yaml:"ResultsLapsComplete"` + ResultsOfficial int `yaml:"ResultsOfficial"` + } `yaml:"Sessions"` + } `yaml:"SessionInfo"` + CameraInfo struct { + Groups []struct { + GroupNum int `yaml:"GroupNum"` + GroupName string `yaml:"GroupName"` + Cameras []struct { + CameraNum int `yaml:"CameraNum"` + CameraName string `yaml:"CameraName"` + } `yaml:"Cameras"` + IsScenic bool `yaml:"IsScenic,omitempty"` + } `yaml:"Groups"` + } `yaml:"CameraInfo"` + RadioInfo struct { + SelectedRadioNum int `yaml:"SelectedRadioNum"` + Radios []struct { + RadioNum int `yaml:"RadioNum"` + HopCount int `yaml:"HopCount"` + NumFrequencies int `yaml:"NumFrequencies"` + TunedToFrequencyNum int `yaml:"TunedToFrequencyNum"` + ScanningIsOn int `yaml:"ScanningIsOn"` + Frequencies []struct { + FrequencyNum int `yaml:"FrequencyNum"` + FrequencyName string `yaml:"FrequencyName"` + Priority int `yaml:"Priority"` + CarIdx int `yaml:"CarIdx"` + EntryIdx int `yaml:"EntryIdx"` + ClubID int `yaml:"ClubID"` + CanScan int `yaml:"CanScan"` + CanSquawk int `yaml:"CanSquawk"` + Muted int `yaml:"Muted"` + IsMutable int `yaml:"IsMutable"` + IsDeletable int `yaml:"IsDeletable"` + } `yaml:"Frequencies"` + } `yaml:"Radios"` + } `yaml:"RadioInfo"` + DriverInfo struct { + DriverCarIdx int `yaml:"DriverCarIdx"` + DriverUserID int `yaml:"DriverUserID"` + PaceCarIdx int `yaml:"PaceCarIdx"` + DriverHeadPosX float64 `yaml:"DriverHeadPosX"` + DriverHeadPosY float64 `yaml:"DriverHeadPosY"` + DriverHeadPosZ float64 `yaml:"DriverHeadPosZ"` + DriverCarIdleRPM float64 `yaml:"DriverCarIdleRPM"` + DriverCarRedLine float64 `yaml:"DriverCarRedLine"` + DriverCarEngCylinderCount int `yaml:"DriverCarEngCylinderCount"` + DriverCarFuelKgPerLtr float64 `yaml:"DriverCarFuelKgPerLtr"` + DriverCarFuelMaxLtr float64 `yaml:"DriverCarFuelMaxLtr"` + DriverCarMaxFuelPct float64 `yaml:"DriverCarMaxFuelPct"` + DriverCarGearNumForward int `yaml:"DriverCarGearNumForward"` + DriverCarGearNeutral int `yaml:"DriverCarGearNeutral"` + DriverCarGearReverse int `yaml:"DriverCarGearReverse"` + DriverCarSLFirstRPM float64 `yaml:"DriverCarSLFirstRPM"` + DriverCarSLShiftRPM float64 `yaml:"DriverCarSLShiftRPM"` + DriverCarSLLastRPM float64 `yaml:"DriverCarSLLastRPM"` + DriverCarSLBlinkRPM float64 `yaml:"DriverCarSLBlinkRPM"` + DriverCarVersion string `yaml:"DriverCarVersion"` + DriverPitTrkPct float64 `yaml:"DriverPitTrkPct"` + DriverCarEstLapTime float64 `yaml:"DriverCarEstLapTime"` + DriverSetupName string `yaml:"DriverSetupName"` + DriverSetupIsModified int `yaml:"DriverSetupIsModified"` + DriverSetupLoadTypeName string `yaml:"DriverSetupLoadTypeName"` + DriverSetupPassedTech int `yaml:"DriverSetupPassedTech"` + DriverIncidentCount int `yaml:"DriverIncidentCount"` + Drivers []Driver `yaml:"Drivers"` + } `yaml:"DriverInfo"` + SplitTimeInfo struct { + Sectors []struct { + SectorNum int `yaml:"SectorNum"` + SectorStartPct float64 `yaml:"SectorStartPct"` + } `yaml:"Sectors"` + } `yaml:"SplitTimeInfo"` + CarSetup struct { + UpdateCount int `yaml:"UpdateCount"` + TiresAero struct { + LeftFront struct { + StartingPressure string `yaml:"StartingPressure"` + LastHotPressure string `yaml:"LastHotPressure"` + LastTempsOMI string `yaml:"LastTempsOMI"` + TreadRemaining string `yaml:"TreadRemaining"` + } `yaml:"LeftFront"` + LeftRear struct { + StartingPressure string `yaml:"StartingPressure"` + LastHotPressure string `yaml:"LastHotPressure"` + LastTempsOMI string `yaml:"LastTempsOMI"` + TreadRemaining string `yaml:"TreadRemaining"` + } `yaml:"LeftRear"` + RightFront struct { + StartingPressure string `yaml:"StartingPressure"` + LastHotPressure string `yaml:"LastHotPressure"` + LastTempsIMO string `yaml:"LastTempsIMO"` + TreadRemaining string `yaml:"TreadRemaining"` + } `yaml:"RightFront"` + RightRear struct { + StartingPressure string `yaml:"StartingPressure"` + LastHotPressure string `yaml:"LastHotPressure"` + LastTempsIMO string `yaml:"LastTempsIMO"` + TreadRemaining string `yaml:"TreadRemaining"` + } `yaml:"RightRear"` + } `yaml:"TiresAero"` + Chassis struct { + Front struct { + ArbSetting int `yaml:"ArbSetting"` + ToeIn string `yaml:"ToeIn"` + FuelLevel string `yaml:"FuelLevel"` + CrossWeight string `yaml:"CrossWeight"` + } `yaml:"Front"` + LeftFront struct { + CornerWeight string `yaml:"CornerWeight"` + RideHeight string `yaml:"RideHeight"` + SpringPerchOffset string `yaml:"SpringPerchOffset"` + Camber string `yaml:"Camber"` + } `yaml:"LeftFront"` + LeftRear struct { + CornerWeight string `yaml:"CornerWeight"` + RideHeight string `yaml:"RideHeight"` + SpringPerchOffset string `yaml:"SpringPerchOffset"` + Camber string `yaml:"Camber"` + ToeIn string `yaml:"ToeIn"` + } `yaml:"LeftRear"` + InCarDials struct { + DisplayPage string `yaml:"DisplayPage"` + BrakePressureBias string `yaml:"BrakePressureBias"` + } `yaml:"InCarDials"` + RightFront struct { + CornerWeight string `yaml:"CornerWeight"` + RideHeight string `yaml:"RideHeight"` + SpringPerchOffset string `yaml:"SpringPerchOffset"` + Camber string `yaml:"Camber"` + } `yaml:"RightFront"` + RightRear struct { + CornerWeight string `yaml:"CornerWeight"` + RideHeight string `yaml:"RideHeight"` + SpringPerchOffset string `yaml:"SpringPerchOffset"` + Camber string `yaml:"Camber"` + ToeIn string `yaml:"ToeIn"` + } `yaml:"RightRear"` + Rear struct { + ArbSetting int `yaml:"ArbSetting"` + WingSetting int `yaml:"WingSetting"` + } `yaml:"Rear"` + } `yaml:"Chassis"` + } `yaml:"CarSetup"` +} + +// Driver ... +type Driver struct { + CarIdx int `yaml:"CarIdx"` + UserName string `yaml:"UserName"` + AbbrevName string `yaml:"AbbrevName"` + Initials string `yaml:"Initials"` + UserID int `yaml:"UserID"` + TeamID int `yaml:"TeamID"` + TeamName string `yaml:"TeamName"` + CarNumber string `yaml:"CarNumber"` + CarNumberRaw int `yaml:"CarNumberRaw"` + CarPath string `yaml:"CarPath"` + CarClassID int `yaml:"CarClassID"` + CarID int `yaml:"CarID"` + CarIsPaceCar int `yaml:"CarIsPaceCar"` + CarIsAI int `yaml:"CarIsAI"` + CarScreenName string `yaml:"CarScreenName"` + CarScreenNameShort string `yaml:"CarScreenNameShort"` + CarClassShortName string `yaml:"CarClassShortName"` + CarClassRelSpeed int `yaml:"CarClassRelSpeed"` + CarClassLicenseLevel int `yaml:"CarClassLicenseLevel"` + CarClassMaxFuelPct string `yaml:"CarClassMaxFuelPct"` + CarClassWeightPenalty string `yaml:"CarClassWeightPenalty"` + CarClassPowerAdjust string `yaml:"CarClassPowerAdjust"` + CarClassDryTireSetLimit string `yaml:"CarClassDryTireSetLimit"` + CarClassColor int `yaml:"CarClassColor"` + CarClassEstLapTime float64 `yaml:"CarClassEstLapTime"` + IRating int `yaml:"IRating"` + LicLevel int `yaml:"LicLevel"` + LicSubLevel int `yaml:"LicSubLevel"` + LicString string `yaml:"LicString"` + LicColor string `yaml:"LicColor"` + IsSpectator int `yaml:"IsSpectator"` + CarDesignStr string `yaml:"CarDesignStr"` + HelmetDesignStr string `yaml:"HelmetDesignStr"` + SuitDesignStr string `yaml:"SuitDesignStr"` + CarNumberDesignStr string `yaml:"CarNumberDesignStr"` + CarSponsor1 int `yaml:"CarSponsor_1"` + CarSponsor2 int `yaml:"CarSponsor_2"` + CurDriverIncidentCount int `yaml:"CurDriverIncidentCount"` + TeamIncidentCount int `yaml:"TeamIncidentCount"` +} + +// parseSessionInfo will parse the sessionInfo buffer into the SessionInfoYAML +// struct +func parseSessionInfo(data []byte) (*SessionInfoYAML, error) { + var sessionInfo SessionInfoYAML + err := yaml.Unmarshal(data, &sessionInfo) + if err != nil { + return nil, err + } + + return &sessionInfo, nil +} + +// ToString will return a readable string of the struct +func (s *SessionInfoYAML) ToString() string { + stringified, _ := json.MarshalIndent(s, "", " ") + return string(stringified) +} + diff --git a/variables.go b/variables.go new file mode 100644 index 0000000..01a8d82 --- /dev/null +++ b/variables.go @@ -0,0 +1,167 @@ +package main + +import ( + "fmt" +) + +const ( + VarSize = 144 + IRSDK_char = 0 + IRSDK_bool = 1 + IRSDK_int = 2 + IRSDK_bitField = 3 + IRSDK_float = 4 + IRSDK_double = 5 +) + +// I think I can make an interface if IRSDK types with available types and +// that they need a parser (reads and type coerces I guess) +var ( + VarTypes = map[int]VarType{ + IRSDK_char: {1, "irsdk_char"}, + IRSDK_bool: {1, "irsdk_bool"}, + IRSDK_int: {4, "irsdk_int"}, + IRSDK_bitField: {4, "irsdk_bitField"}, + IRSDK_float: {4, "irsdk_float"}, + IRSDK_double: {8, "irsdk_double"}, + } +) + +type VarType struct { + Size int // Size is the var type size in bytes + Name string // Name is the irsdk var name +} + +type IBTVar struct { + Type int32 + Offset int32 + Count int32 + CountAsTime bool + Padding [0]byte + Name [32]byte + Description [64]byte + Unit [32]byte +} + +type Var struct { + Type int32 + Offset int32 + Count int32 + CountAsTime bool + Name string + Description string + Unit string + // TODO + // Create an interface for this value + // Represent the IRSDK var types with a struct each that implements the Parse + // method or something like that I guess + Value interface{} +} + +func (v *IBTVar) ToString() string { + return fmt.Sprintf( + "Type: %5d (0x%08x)\n"+ + "Offset: %5d (0x%08x)\n"+ + "Count: %5d (0x%08x)\n"+ + "CountAsTime: %5t\n"+ + "Name: %s\n"+ + "Description: %s\n"+ + "Unit: %s", + v.Type, v.Type, v.Offset, v.Offset, v.Count, v.Count, + v.CountAsTime, v.Name, v.Description, v.Unit, + ) +} + +type varBuffer struct { + tickCount int + bufOffset int +} + +type TelemetryVars struct { + LastVersion int + Vars map[string]Var +} + +// func findLatestBuffer(i *IBT) varBuffer { +// var vb varBuffer +// foundTickCount := 0 +// for k := 0; k < int(i.Headers.NumBuf); k++ { +// rbuf := make([]byte, 16) +// _, err := i.File.ReadAt(rbuf, int64(48+k*16)) +// if err != nil { +// log.Fatal(err) +// } +// +// currentVb := varBuffer{ +// int(binary.LittleEndian.Uint32(rbuf[0:4])), +// int(binary.LittleEndian.Uint32(rbuf[4:8])), +// } +// +// if foundTickCount < currentVb.tickCount { +// foundTickCount = currentVb.tickCount +// vb = currentVb +// } +// } +// +// return vb +// } + +// func (i *IBT) parseVariableHeaders(offset int32) (int32, error) { +// if i.Vars.Vars == nil { +// i.Vars.Vars = make(map[string]*Variable, i.Headers.NumVars) +// } +// +// var size int32 = 0 +// for k := range i.Headers.NumVars { +// start := k * VarSize + offset +// size += start +// +// buf := make([]byte, VarSize) +// _, err := i.File.ReadAt(buf, int64(start)) +// if err != nil { +// return 0, err +// } +// +// newVar, err := parseVariable(buf) +// i.Vars.Vars[string(newVar.Name[:])] = newVar +// } +// +// return size, nil +// } + +// This function will read a single variable +// func parseVariable(buf []byte) (*Variable, error) { +// if len(buf)%VarSize != 0 { +// return nil, fmt.Errorf("buffer must be multiple of size: %d", VarSize) +// } +// +// dst := Variable{} +// err := binary.Read(bytes.NewBuffer(buf[:]), binary.LittleEndian, &dst) +// if err != nil { +// return nil, err +// } +// +// return &dst, nil +// } + +// this function will read the variables +// func parseVariables(i *IBT) error { +// i.parseVariableHeaders(0) +// vb := findLatestBuffer(i) +// fmt.Printf("%+v\n", vb) +// if i.Vars.LastVersion < vb.tickCount { +// // Then we have new data +// i.Vars.LastVersion = vb.tickCount +// i.LastValidData = time.Now().Unix() +// for _, v := range i.Vars.Vars { +// // fmt.Printf("%s\n", v.ToString()) +// rbuf := make([]byte, VarTypes[int(v.Type)].Size) +// _, err := i.File.ReadAt(rbuf, int64(vb.bufOffset + int(v.Offset))) +// if err != nil { +// log.Fatalf("Reading values: %v\n", err) +// } +// } +// } +// +// return nil +// }