diff --git a/diskSubHeader.go b/diskSubHeader.go index 8ce721f..7d17898 100644 --- a/diskSubHeader.go +++ b/diskSubHeader.go @@ -7,23 +7,23 @@ import ( ) const ( - SubHeaderSize = 32 // SubHeaderSize is the size of the subheader + 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 ... + StartDate int64 // StartDate represents the start date of the telemetry + StartTime float64 // StartTime of file relative to start of session + EndTime float64 // EndTime of file relative to start of session LapCount int32 // LapCount represents the total number laps - RecordCount int32 // RecordCount ... + RecordCount int32 // RecordCount holds the number of data frames } // 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{} + dst := DiskSubHeader{} err := binary.Read(bytes.NewBuffer(buf[:]), binary.LittleEndian, &dst) if err != nil { return nil, err @@ -34,14 +34,14 @@ func parseTelemetrySubHeader(buf [SubHeaderSize]byte) (*DiskSubHeader, error) { // 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, - ) + return fmt.Sprintf( + "StartDate: %13d (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/headers.go b/headers.go index b28b4ea..2980c2d 100644 --- a/headers.go +++ b/headers.go @@ -7,7 +7,7 @@ import ( ) const ( - FileHeaderSize = 56 // FileHeaderSize is the size of the headers + FileHeaderSize = 112 // FileHeaderSize is the size of the headers HeaderSize = 4 // HeaderSize is the size of a single header ) @@ -42,17 +42,17 @@ type TelemetryHeaders struct { // 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", + "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, diff --git a/irsdk.go b/irsdk.go new file mode 100644 index 0000000..7d53650 --- /dev/null +++ b/irsdk.go @@ -0,0 +1,137 @@ +// Package IbtParser is all you need for you iRacing telemetry parsing +package main + +import ( + "fmt" + "io" + "log" + "os" + "time" +) + +const ( + ibtFile = "./telemetryFiles/mx5_2016Okayama_full_2024_10_19_22_02_12.ibt" + // ibtFile = "./telemetryFiles/sample.ibt" +) + +// Reader is an interface to represent the readable data that can be either +// a .ibt file (or live data, hopefully) +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 // Vars will hold the telemetry data + Tick int32 // Tick holds the cound of the reads +} + +// 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[:], 112) + 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) + } + + // Read the telemetry vars info + ibt.readVariablerHeaders() + + return &ibt, nil +} + +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\n", ibt.SubHeaders.ToString()) + + // Display the human readable start date + unixStartDate := time.Unix(ibt.SubHeaders.StartDate, 0) + startDate := unixStartDate.Format("2006/01/02 15:04:05 -0700 MST") + fmt.Println("StartDate:", startDate) + + // Display the human readable version of start time + unixStartTime := time.Unix(ibt.SubHeaders.StartDate + int64(ibt.SubHeaders.StartTime), 0) + startTime := unixStartTime.Format("2006/01/02 15:04:05 -0700 MST") + fmt.Println("StartTime:", startTime) + + // Display the human readable version of end time + unixEndTime := time.Unix(ibt.SubHeaders.StartDate + int64(ibt.SubHeaders.EndTime), 0) + endTime := unixEndTime.Format("2006/01/02 15:04:05 -0700 MST") + fmt.Println("EndTime: ", endTime) + + 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 + } + } + + fmt.Printf("%d\n", ibt.Tick) +} diff --git a/main.go b/main.go deleted file mode 100644 index 04aca0e..0000000 --- a/main.go +++ /dev/null @@ -1,210 +0,0 @@ -// 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/variables.go b/variables.go index 01a8d82..cef6f69 100644 --- a/variables.go +++ b/variables.go @@ -1,7 +1,13 @@ package main import ( + "bytes" + "encoding/binary" "fmt" + "log" + "strings" + "math" + "io" ) const ( @@ -37,7 +43,7 @@ type IBTVar struct { Offset int32 Count int32 CountAsTime bool - Padding [0]byte + Padding [3]byte Name [32]byte Description [64]byte Unit [32]byte @@ -51,11 +57,11 @@ type Var struct { 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{} + // 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 { @@ -82,86 +88,90 @@ type TelemetryVars struct { 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) readVariablerHeaders() { + i.Vars = &TelemetryVars{Vars: make(map[string]Var, i.Headers.NumVars)} -// 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 -// } + var k int32 + for k = 0; k < i.Headers.NumVars; k++ { + rbuf := make([]byte, VarSize) -// 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 -// } + _, err := i.File.ReadAt(rbuf, int64(i.Headers.VarHeaderOffset+k*VarSize)) + if err != nil { + log.Fatal(err) + } -// 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 -// } + var dst IBTVar + err = binary.Read(bytes.NewBuffer(rbuf[:]), binary.LittleEndian, &dst) + if err != nil { + log.Fatal(err) + } + + fmt.Println(dst.Name) + + v := Var{ + Type: dst.Type, + Offset: dst.Offset, + Count: dst.Count, + CountAsTime: dst.CountAsTime, + Name: strings.TrimRight(string(dst.Name[:]), "\x00"), + Description: strings.TrimRight(string(dst.Description[:]), "\x00"), + Unit: strings.TrimRight(string(dst.Unit[:]), "\x00"), + Value: nil, + } + + i.Vars.Vars[v.Name] = v + } +} + +func (i *IBT) readData() error { + // I think that we can add one extra check or verification here + // The file headers tells us how many data frames there are, we can probably + // cap it at that instead of waiting for the read to fail + // Probably wouldn't work on live data tho + 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 +}