diff --git a/diskSubHeader.go b/diskSubHeader.go index a230908..85c24cd 100644 --- a/diskSubHeader.go +++ b/diskSubHeader.go @@ -4,7 +4,6 @@ import ( "bytes" "encoding/binary" "fmt" - "ibtReader/utils" ) const ( @@ -24,7 +23,7 @@ type DiskSubHeader struct { // or nil if an error occurs. In which case the error return value is more // valuable func parseTelemetrySubHeader(buf [SubHeaderSize]byte) (*DiskSubHeader, error) { - utils.HexDump(buf[:]) + // utils.HexDump(buf[:]) dst := DiskSubHeader{} err := binary.Read(bytes.NewBuffer(buf[:]), binary.LittleEndian, &dst) diff --git a/headers.go b/headers.go index f89b110..28f383b 100644 --- a/headers.go +++ b/headers.go @@ -66,6 +66,9 @@ func (th *TelemetryHeaders) ToString() string { // buffer. // You need to pass a the first FILE_HEADER_SIZE bytes of the buffer func parseTelemetryHeader(buf [FileHeaderSize]byte) (*TelemetryHeaders, error) { + // utils.HexDump(buf[:]) + // fmt.Printf("Len: %d\n", len(buf)) + if len(buf)%HeaderSize != 0 { return nil, fmt.Errorf("buffer must be multiple of size: %d", HeaderSize) } diff --git a/irsdk.go b/irsdk.go index 234bc45..a74f7c3 100644 --- a/irsdk.go +++ b/irsdk.go @@ -5,10 +5,9 @@ import ( "fmt" "io" "log" - "os" "time" - "ibtReader/sharedMem" + "ibtReader/winutils" ) const ( @@ -25,13 +24,25 @@ type Reader interface { // 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 - LiveData bool + 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 + winUtils *winutils.IRacingWinUtils // WinUtils gives access to the system utilities +} + +func (i *IBT) IsConnected() bool { + if i.Headers != nil { + if sessionStatusOK(int(i.Headers.Status)) { + return true + } + // if sessionStatusOK(int(i.Headers.Status)) && (sdk.lastValidData+connTimeout > time.Now().Unix()) { + // return true + // } + } + + return false } // Init serves to initialize and get a hold of a IBT struct @@ -41,19 +52,35 @@ func Init(f Reader) (*IBT, error) { ibt := IBT{ File: f, Vars: &TelemetryVars{}, - LiveData: false, + winUtils: nil, } if ibt.File == nil { // User is requesting us to read live data - present in the mem map file - ibt.File, err = sharedMem.Open(IRSDK_MEMMAPFILENAME, fileMapSize) + ibt.File, err = winutils.OpenMemMap(IRSDK_MEMMAPFILENAME, fileMapSize) if err != nil { return nil, fmt.Errorf("Failed to open memory mapped file: %v", err) } - ibt.LiveData = true - // Here we also need to open the broadcast message thing and the - // thing to check if the game is open + // To use our windows interface we need to initialize it first + // it will return a struct with a pointer to the windows handles + // if, for some reason, we need to stub out this to run in on Linux its easier + ibt.winUtils, err = winutils.Init() + if err != nil { + return nil, err + } + + // We need to open the windows event thing + err = ibt.winUtils.OpenWinEvent(IRSDK_DATAVALIDEVENTNAME) + if err != nil { + return nil, err + } + + // We need to open the broadcast channel + err = ibt.winUtils.OpenBroadcastChannel(IRSDK_BROADCASTMSGNAME) + if err != nil { + return nil, err + } } // Read the file headers @@ -66,7 +93,6 @@ func Init(f Reader) (*IBT, error) { if err != nil { return nil, fmt.Errorf("Unable to read headers from file: %v", err) } - fmt.Println(ibt.Headers.ToString()) // Read the disk sub headers var subheaderRaw [SubHeaderSize]byte @@ -78,7 +104,6 @@ func Init(f Reader) (*IBT, error) { if err != nil { return nil, fmt.Errorf("Unable to parse disk subheaders from file: %v", err) } - fmt.Println(ibt.SubHeaders.ToString()) // Read session info string sessionInfoStringRaw := make([]byte, ibt.Headers.SessionInfoLength) @@ -100,6 +125,14 @@ func Init(f Reader) (*IBT, error) { return &ibt, nil } +func (i *IBT) Close() { + if i.winUtils != nil { + // If its not live data, the user is the one with ownership of the handle + i.File.Close() + i.winUtils.Close() + } +} + func msToKph(v float32) int { return int((3600 * v) / 1000) } @@ -107,55 +140,65 @@ func msToKph(v float32) int { func main() { fmt.Println("================== IBT FILE PARSER ==================") - file, err := os.Open(ibtFile) - if err != nil { - log.Fatalf("Failed to open IBT file: %v", err) - } + // file, err := os.Open(ibtFile) + // if err != nil { + // log.Fatalf("Failed to open IBT file: %v", err) + // } - ibt, err := Init(file) + ibt, err := Init(nil) if err != nil { log.Fatalf("Failed to create irsdk instance: %v", err) } - fmt.Printf("%s\n", ibt.Headers.ToString()) - fmt.Printf("%s\n", ibt.SubHeaders.ToString()) - fmt.Printf("%s\n", ibt.SessionInfo.ToString()) + // fmt.Printf("%s\n", ibt.Headers.ToString()) + // fmt.Printf("%s\n", ibt.SubHeaders.ToString()) + // fmt.Printf("%s\n", ibt.SessionInfo.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) + // 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) + // 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) + // 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() + res, err := ibt.Update(100 * time.Millisecond) + if res == Unknown { + log.Fatalf("Some unknown error occurred: %v\n", err) + } + + if res == Paused { + fmt.Printf("\r \r") + fmt.Println("GAME IS PAUSED") + continue + } 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))) + fmt.Printf("\r%d %d", ibt.Vars.Tick/60, msToKph(val.Value.(float32))) } else { - fmt.Printf("\r%d %s", ibt.Tick/60, "KEY DOESN'T EXIST") + fmt.Printf("\r%d %s", ibt.Vars.Tick/60, "KEY DOESN'T EXIST") } } - if !res { + if res == Ended { fmt.Println("\nEnd of file found...") break } } + fmt.Printf("%d\n", ibt.Vars.Tick) - fmt.Printf("%d\n", ibt.Tick) + ibt.Close() } diff --git a/sessionInfo.go b/sessionInfo.go index 38f92f7..5054f54 100644 --- a/sessionInfo.go +++ b/sessionInfo.go @@ -333,6 +333,11 @@ func parseSessionInfo(buf []byte, len int32) (*SessionInfoYAML, error) { return &sessionInfo, nil } +// sessionStatusOK will tell us if we are connected to the live data +func sessionStatusOK(status int) bool { + return (status & stConnected) > 0 +} + // ToString will return a readable string of the struct func (s *SessionInfoYAML) ToString() string { stringified, _ := json.MarshalIndent(s, "", " ") diff --git a/variables.go b/variables.go index 8db21d7..2bdf80d 100644 --- a/variables.go +++ b/variables.go @@ -8,16 +8,22 @@ import ( "log" "math" "strings" + "time" ) const ( - VarHeaderSize = 144 - IRSDK_char = 0 - IRSDK_bool = 1 - IRSDK_int = 2 - IRSDK_bitField = 3 - IRSDK_float = 4 - IRSDK_double = 5 + VarHeaderSize = 144 + IRSDK_char = 0 + IRSDK_bool = 1 + IRSDK_int = 2 + IRSDK_bitField = 3 + IRSDK_float = 4 + IRSDK_double = 5 + Running IRacingState = iota + Paused + Ended + Failed + Unknown ) // I think I can make an interface if IRSDK types with available types and @@ -33,6 +39,7 @@ var ( } ) +type IRacingState int type VarType struct { Size int // Size is the var type size in bytes Name string // Name is the irsdk var name @@ -79,13 +86,13 @@ func (v *IBTVar) ToString() string { } type varBuffer struct { - tickCount int - bufOffset int + TickCount int32 + BufOffset int32 } type TelemetryVars struct { - LastVersion int - Vars map[string]Var + Tick int32 + Vars map[string]Var } func (i *IBT) readVariablerHeaders() error { @@ -123,19 +130,9 @@ func (i *IBT) readVariablerHeaders() error { return nil } -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 - } - +func (i *IBT) readData(buf []byte) error { for k, v := range i.Vars.Vars { + // Slice of the variable value in the buffer rbuf := buf[v.Offset : v.Offset+int32(VarTypes[int(v.Type)].Size)] // Read the value @@ -158,20 +155,77 @@ func (i *IBT) readData() error { 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) +func (i *IBT) Update(timeout time.Duration) (IRacingState, error) { + if i.winUtils != nil { + // Put a way to check if the sim is active here + // fmt.Println("NOT CHECKING IF SIM IS ACTIVE - ADD ME") + + // WORKING HERE + // Need to figure out how to grab the latest buffer with data + var vb varBuffer + foundTickCount := 0 + for k := 0; k < int(i.Headers.NumBuf); k++ { + rbuf := make([]byte, 16) + // Read 16 bytes, I don't know why, but do need to understand this + _, err := i.File.ReadAt(rbuf, int64(48+k*16)) + if err != nil { + return Failed, err + } + + var curVb varBuffer + err = binary.Read(bytes.NewBuffer(rbuf[:]), binary.LittleEndian, &curVb) + if err != nil { + return Failed, err + } + + if foundTickCount < int(curVb.TickCount) { + foundTickCount = int(curVb.TickCount) + vb = curVb + } + } + + i.Vars.Tick = vb.TickCount + + start := vb.BufOffset + buf := make([]byte, i.Headers.BufLen) + + _, err := i.File.ReadAt(buf, int64(start)) + if err != nil { + return Failed, err + } + + err = i.readData(buf) + if err != nil && err != io.EOF { + return Unknown, err + } + + if err == io.EOF { + return Ended, nil + } + } else { + // This will get the dataframe corresponding to a give tick + start := i.Headers.BufOffset + i.Vars.Tick*i.Headers.BufLen + buf := make([]byte, i.Headers.BufLen) + _, err := i.File.ReadAt(buf, int64(start)) + if err != nil { + return Unknown, err + } + + err = i.readData(buf) + if err != nil && err != io.EOF { + log.Fatalf("What happened?\n%v\n", err) + } + + if err == io.EOF { + return Ended, nil + } + + // This was previously in the read data method, but it probably fits here better + i.Vars.Tick++ } - if err == io.EOF { - return false - } - - return true + return Running, nil } diff --git a/winutils/winutils.go b/winutils/winutils.go index 6938c6f..c37d200 100644 --- a/winutils/winutils.go +++ b/winutils/winutils.go @@ -1,108 +1,59 @@ -//go:build windows && cgo -// +build windows,cgo - +// I should rename winutils to something else but what this package does +// is interface some windows stuff that we need for the: +// - Broadcast Channel +// - Valid Data Event windows thing package winutils import ( - "fmt" - "sync" + "ibtReader/sharedMem" + "io" "time" - "unsafe" - - "golang.org/x/sys/windows" ) -const ( - WAIT_OBJECT_0 = 0 - WAIT_TIMEOUT = 258 -) +type Reader interface { + io.Reader + io.ReaderAt + io.ReadCloser +} -var ( - once sync.Once - user32DLL *windows.LazyDLL = nil -) +type IRacingWinUtils struct { + Utils *utils +} -// openEvent opens a windows.Handle for a given event -func OpenEvent(eventName string) (windows.Handle, error) { - name, err := windows.UTF16PtrFromString(eventName) +func Init() (*IRacingWinUtils, error) { + u, err := newUtils() if err != nil { - return 0, err + return nil, err } + return &IRacingWinUtils{u}, nil +} - handle, err := windows.OpenEvent(windows.SYNCHRONIZE, false, name) +func (u *IRacingWinUtils) Close() { + u.Utils.Close() +} + +// OpenMemMap returns a Reader interface that can be used to read the data +// No need to encapsulate it +func OpenMemMap(path string, size uint32) (Reader, error) { + file, err := sharedMem.Open(path, size) if err != nil { - return 0, fmt.Errorf("error opening event %s: %w", eventName, err) + return nil, err } - return handle, nil + return file, nil } -// closeEvent closes a given windows.Handle -func CloseEvent(h windows.Handle) { - windows.CloseHandle(h) +// OpenWinEvent will open the named windows event +func (u *IRacingWinUtils) OpenWinEvent(name string) error { + return u.Utils.OpenEvent(name) } -// openEvent waits for a good response for some given time -func CheckValidDataEvent(handle windows.Handle, timeout time.Duration) bool { - t0 := time.Now().UnixNano() - timeoutInt := uint32(timeout / time.Millisecond) - - result, err := windows.WaitForSingleObject(handle, timeoutInt) - if err != nil { - remaining := timeoutInt - uint32((time.Now().UnixNano()-t0)/1000000) - if remaining > 0 { - time.Sleep(time.Duration(remaining) * time.Millisecond) - } - return false - } - - // Check the result of the wait - if result == WAIT_OBJECT_0 { - return true - } else if result == WAIT_TIMEOUT { - return false - } - - return false +// OpenWinEvent will open the broadcast channel +func (u *IRacingWinUtils) OpenBroadcastChannel(name string) error { + return u.Utils.OpenBroadcastChannel(name) } -// loadUser32DLL loads the user32.dll which is used to create some processes -func loadUser32DLL() { - user32DLL = windows.NewLazyDLL("user32.dll") -} - -// OpenBroadcastChannel opens up a broadcast channel to send commands to iracing -func OpenBroadcastChannel(name string) (uintptr, error) { - if user32DLL == nil { - once.Do(loadUser32DLL) - } - registerWindowsMessageW := user32DLL.NewProc("RegisterWindowMessageW") - - msgPtr, err := windows.UTF16PtrFromString(name) - if err != nil { - return 0, err - } - - ret, _, err := registerWindowsMessageW.Call(uintptr(unsafe.Pointer(msgPtr))) - if ret == 0 { - return 0, err - } - - return ret, nil -} - -// SendBroadcastMessage sends a message trough the broadcast channel -func SendBroadcastMessage(id, p1, p2 uintptr) error { - if user32DLL == nil { - once.Do(loadUser32DLL) - } - - sendMsg := user32DLL.NewProc("SendNotifyMessageW") - ret, _, err := sendMsg.Call(0xffff, id, p1, p2) - - if ret == 1 { - return nil - } else { - return err - } +// CheckValidDataEvent checks if our windows even is telling us we are good to go +func (u *IRacingWinUtils) CheckValidDataEvent(timeout time.Duration) bool { + return u.Utils.CheckValidDataEvent(timeout) } diff --git a/winutils/winutils_windows.go b/winutils/winutils_windows.go new file mode 100644 index 0000000..3163681 --- /dev/null +++ b/winutils/winutils_windows.go @@ -0,0 +1,122 @@ +//go:build windows && cgo +// +build windows,cgo + +package winutils + +import ( + "sync" + "time" + "unsafe" + + "golang.org/x/sys/windows" +) + +const ( + WAIT_OBJECT_0 = 0 + WAIT_TIMEOUT = 258 +) + +var ( + once sync.Once +) + +type utils struct { + user32DLL *windows.LazyDLL + wEvent *windows.Handle + wBroadcastChn uintptr +} + +// INITIALIZATION +func newUtils() (*utils, error) { + return &utils{ + user32DLL: openUser32DLL(), + }, nil +} + +func (u *utils) Close() { + closeEvent(u.wEvent) + // Do we need to unload the user32DLL ??? + // Do we need to close the broadcast channel ??? +} + +// openEvent opens a windows.Handle for a given event +func (u *utils) OpenEvent(eventName string) error { + name, err := windows.UTF16PtrFromString(eventName) + if err != nil { + return err + } + + event, err := windows.OpenEvent(windows.SYNCHRONIZE, false, name) + if err != nil { + return err + } + u.wEvent = &event + + return nil +} + +// loadUser32DLL loads the user32.dll which is used to create some processes +func openUser32DLL() *windows.LazyDLL { + return windows.NewLazyDLL("user32.dll") +} + +// OpenBroadcastChannel opens up a broadcast channel to send commands to iracing +func (u *utils) OpenBroadcastChannel(name string) error { + registerWindowsMessageW := u.user32DLL.NewProc("RegisterWindowMessageW") + + msgPtr, err := windows.UTF16PtrFromString(name) + if err != nil { + return err + } + + ret, _, err := registerWindowsMessageW.Call(uintptr(unsafe.Pointer(msgPtr))) + if ret == 0 { + return err + } + u.wBroadcastChn = ret + + return nil +} + +// INITIALIZATION + +// closeEvent closes a given windows.Handle +func closeEvent(h *windows.Handle) { + windows.CloseHandle(*h) +} + +// openEvent waits for a good response for some given time +func (u *utils) CheckValidDataEvent(timeout time.Duration) bool { + t0 := time.Now().UnixNano() + timeoutInt := uint32(timeout / time.Millisecond) + + result, err := windows.WaitForSingleObject(*u.wEvent, timeoutInt) + if err != nil { + remaining := timeoutInt - uint32((time.Now().UnixNano()-t0)/1000000) + if remaining > 0 { + time.Sleep(time.Duration(remaining) * time.Millisecond) + } + return false + } + + // Check the result of the wait + if result == WAIT_OBJECT_0 { + return true + } else if result == WAIT_TIMEOUT { + return false + } + + return false +} + +// SendBroadcastMessage sends a message trough the broadcast channel +func (u *utils) SendBroadcastMessage(id, p1, p2 uintptr) error { + sendMsg := u.user32DLL.NewProc("SendNotifyMessageW") + ret, _, err := sendMsg.Call(0xffff, id, p1, p2) + + if ret == 1 { + return nil + } else { + return err + } +}