From 9adc0a945652b8c0f56d353fe71ee33e758a0809 Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Sun, 23 Aug 2026 22:56:14 +0100 Subject: [PATCH 1/3] upgrade to support writing to a memmap file --- constants.go | 9 +- diskSubHeader.go | 14 +-- examples/example.go | 19 ++-- headers.go | 49 ++++------ irsdk.go | 179 +++++++++++++++++++++++------------ irsdk_test.go | 6 +- sessionInfo.go | 8 +- variables.go | 57 +++++++---- winutils/winutils_linux.go | 113 +++++++++++++++++----- winutils/winutils_windows.go | 15 ++- 10 files changed, 300 insertions(+), 169 deletions(-) diff --git a/constants.go b/constants.go index 02dd816..67fbde3 100644 --- a/constants.go +++ b/constants.go @@ -8,9 +8,10 @@ type Msg struct { } const ( + MEMMAPFILENAME = "IRSDKMemMapFileName" SimStatusUrl string = "http://127.0.0.1:32034/get_sim_status?object=simStatus" IRSDK_DATAVALIDEVENTNAME string = "Local\\IRSDKDataValidEvent" - IRSDK_MEMMAPFILENAME string = "Local\\IRSDKMemMapFileName" + IRSDK_MEMMAPFILENAME string = "Local\\" + MEMMAPFILENAME IRSDK_BROADCASTMSGNAME string = "IRSDK_BROADCASTMSG" fileMapSize uint32 = 1164 * 1024 connTimeout int64 = 30 @@ -133,9 +134,11 @@ var ( irsdkPitSpeedLimiter = bitfieldValue{0x10, "irsdk_pitSpeedLimiter"} irsdkRevLimiterActive = bitfieldValue{0x20, "irsdk_revLimiterActive"} irsdkAbsActive = bitfieldValue{0x100, "irsdk_absActive"} - irsdkEngineWarnings = []bitfieldValue{irsdkWaterTempWarning, irsdkFuelPressureWarning, + irsdkEngineWarnings = []bitfieldValue{ + irsdkWaterTempWarning, irsdkFuelPressureWarning, irsdkOilPressureWarning, irsdkEngineStalled, irsdkPitSpeedLimiter, irsdkRevLimiterActive, - irsdkAbsActive} + irsdkAbsActive, + } ) // enum irsdk_EngineWarnings diff --git a/diskSubHeader.go b/diskSubHeader.go index 1b42d24..5ff94c4 100644 --- a/diskSubHeader.go +++ b/diskSubHeader.go @@ -1,11 +1,11 @@ package goirsdk import ( - "github.com/ESilva15/goirsdk/logger" - "bytes" "encoding/binary" "fmt" + + "github.com/ESilva15/goirsdk/logger" ) const ( @@ -28,18 +28,18 @@ func (i *IBT) readSubheader() error { var subheaderRaw [SubHeaderSize]byte _, err := i.File.ReadAt(subheaderRaw[:], HeaderSize) if err != nil { - return fmt.Errorf("Failed to read disk subheaders from file: %v", err) + return fmt.Errorf("failed to read disk subheaders from file: %v", err) } i.SubHeaders, err = parseTelemetrySubHeader(subheaderRaw) if err != nil { - return fmt.Errorf("Unable to parse disk subheaders from file: %v", err) + return fmt.Errorf("unable to parse disk subheaders from file: %v", err) } // Write to the output file - TODO add the check - if i.IBTExport != nil { - err = i.exportIBT(subheaderRaw[:], HeaderSize) + if i.Opts.IBTExport { + err = i.exportIBT(subheaderRaw[:], HeaderSize) if err != nil { - log.Printf("Failed to export subheaders: %v\n", err) + log.Printf("failed to export subheaders: %v\n", err) } } diff --git a/examples/example.go b/examples/example.go index 58c0853..caaa9b2 100644 --- a/examples/example.go +++ b/examples/example.go @@ -3,7 +3,6 @@ package main import ( "fmt" "log" - "os" "time" "github.com/ESilva15/goirsdk" @@ -14,21 +13,23 @@ func msToKph(v float32) int { } func main() { - // Open the data source file - file, err := os.Open("/path/to/ibtFile") - if err != nil { - log.Fatalf("Failed to open IBT file: %v", err) - } - // Instantiate our iRacing SDK instance - irsdk, err := goirsdk.Init(file, "", "") + irsdk, err := goirsdk.Init(goirsdk.Options{ + SourceType: goirsdk.IBTFile, + SourcePath: "../../../testTelemetry/gt3_mustang_bathurst.ibt", + // SourcePath: "./exported.ibt", + IBTExportType: goirsdk.SharedMemoryFile, + // IBTExportPath: "./exported.ibt", + IBTExport: true, + }) if err != nil { log.Fatalf("Failed to create iRacing interface: %v", err) } defer irsdk.Close() // Set up a loop to iterate our data - mainLoopTicker := time.NewTicker(time.Second / 60) + // TODO: revert this 240 back to 60 because i recorded the thing wrong or whatever + mainLoopTicker := time.NewTicker(time.Second / 240) defer mainLoopTicker.Stop() for { diff --git a/headers.go b/headers.go index 803effc..91c3ec0 100644 --- a/headers.go +++ b/headers.go @@ -1,11 +1,11 @@ package goirsdk import ( - "github.com/ESilva15/goirsdk/logger" - "bytes" "encoding/binary" "fmt" + + "github.com/ESilva15/goirsdk/logger" ) const ( @@ -15,30 +15,19 @@ const ( // 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 + Version int32 + Status int32 // Status of 1 indicates a completed session and status of 0 a live session + TickRate int32 // TickRate indicates the frequency of writes (usually 60) + SessionInfoUpdate 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 + SessionInfoLength int32 // SessionInfoLength is the length of the session info buffer + SessionInfoOffset int32 // SessionInfoOffset is the offset of the session info in the buffer + NumVars int32 // NumVars is the number of variables in each input + VarHeaderOffset int32 // VarHeaderOffset is the offset of the VarHeader + NumBuf int32 // NumBuf will be 1 for static files and 3 for live telemetry files + BufLen int32 // BufLen is the length for parsing VarHeader values + Padding [12]byte // Padding + BufOffset int32 // I still don't know what this is: } // readHeader will read the header out of the telemetry data @@ -48,18 +37,18 @@ func (i *IBT) readHeader() error { var headerRaw [FileHeaderSize]byte _, err := i.File.ReadAt(headerRaw[:], 0) if err != nil { - return fmt.Errorf("Failed to read headers from file: %v", err) + return fmt.Errorf("failed to read headers from file: %v", err) } i.Headers, err = parseTelemetryHeader(headerRaw) if err != nil { - return fmt.Errorf("Unable to read headers from file: %v", err) + return fmt.Errorf("unable to read headers from file: %v", err) } // Write to the output file - TODO: this should only write if necessary - if i.IBTExport != nil { - err = i.exportIBT(headerRaw[:], 0) + if i.Opts.IBTExport { + err = i.exportIBT(headerRaw[:], 0) if err != nil { - log.Printf("Failed to export headers: %v\n", err) + log.Printf("Failed to export headers: %v\n", err) } } diff --git a/irsdk.go b/irsdk.go index e2e13cb..97be156 100644 --- a/irsdk.go +++ b/irsdk.go @@ -3,23 +3,15 @@ package goirsdk import ( "fmt" + "io" "os" - "io" - "github.com/ESilva15/goirsdk/logger" + "github.com/ESilva15/goirsdk/sharedMem" "github.com/ESilva15/goirsdk/winutils" "gopkg.in/yaml.v3" ) -const ( - ibtFile = "./telemetryFiles/mx5_2016Okayama_full_2024_10_19_22_02_12.ibt" -) - -func msToKph(v float32) int { - return int((3600 * v) / 1000) -} - // Reader is an interface to represent the readable data that can be either // a .ibt file (or live data, hopefully) type Reader interface { @@ -28,18 +20,47 @@ type Reader interface { io.ReadCloser } +type Writer interface { + io.WriterAt + io.Closer +} + +type TelemetryContainer int + +const ( + IBTFile TelemetryContainer = iota + SharedMemoryFile TelemetryContainer = iota +) + +type Options struct { + SourceType TelemetryContainer // type of source data + SourcePath string // Path to source + IBTExportType TelemetryContainer // export type of telemetry: store .ibt or replay in shm + IBTExportPath string // path where to export the data + IBTExport bool // whether to export the telemetry data + SessionInfoExport bool // whether to export the session info data + SessionInfoExportPath string // path where to export the session info +} + // IBT struct will hold the relevant data for a given IBT file type IBT struct { - File Reader // Source of the data - IBTExport *os.File // If set, it will export the IBT data to the file - IBTExportPath string // Path for IBT export - YAMLExport *os.File // If set, it will export the session YAML to the file - YAMLExportPath string // Path for YAML export - 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 + File Reader // Source of the data + Opts Options + // TODO: IBTExporter should be an interface because we need to support shm too + IBTExporter Writer + // IBTExport *os.File // If set, it will export the IBT data to the file + // IBTExportPath string // Path for IBT export + // YAMLExport *os.File // If set, it will export the session YAML to the file + // YAMLExportPath string // Path for YAML export + winUtils *winutils.IRacingWinUtils // WinUtils gives access to the system utilities + + // TODO: fragment this struct a little bit, for now I want to actually get + // stuff done so its enough to work as is + // Actual FILE + 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 } func (i *IBT) IsConnected() bool { @@ -58,7 +79,7 @@ func (i *IBT) IsConnected() bool { func (i *IBT) exportYAML() error { log := logger.GetInstance() - file, err := os.OpenFile(i.YAMLExportPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) + file, err := os.OpenFile(i.Opts.SessionInfoExportPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o644) if err != nil { log.Printf("Failed to open file for YAML export: %v\n", err) return fmt.Errorf("failed to open output file for YAML: %v", err) @@ -79,11 +100,10 @@ func (i *IBT) exportYAML() error { func (i *IBT) exportIBT(data []byte, offset int64) error { log := logger.GetInstance() - _, err := i.IBTExport.WriteAt(data, offset) - + _, err := i.IBTExporter.WriteAt(data, offset) if err != nil { - i.IBTExport.Close() - i.IBTExport = nil + i.IBTExporter.Close() + i.IBTExporter = nil log.Println("Won't attempt to export anymore") return err } @@ -91,61 +111,96 @@ func (i *IBT) exportIBT(data []byte, offset int64) error { return nil } -// Init serves to initialize and get a hold of a IBT struct -// f -> is the source data, pass nil for the SDK to read live data or a -// *os.File to read from a file -// exportTelem -> is a string with the path to export the telemetry data, pass -// an empty string to not export any data -// exportTelem -> is a string with the path to export the session info data, pass -// an empty string to not export any data -func Init(f Reader, exportTelem string, exportYAML string) (*IBT, error) { - // log := logger.GetInstance() - - // Read the header of the file +func (i *IBT) openSource() error { var err error - ibt := IBT{ - File: f, - IBTExport: nil, - IBTExportPath: exportTelem, - YAMLExport: nil, - YAMLExportPath: exportYAML, - Vars: &TelemetryVars{}, - winUtils: nil, - } - // If requested to output to a telemetry file - if exportTelem != "" { - ibt.IBTExport, err = os.OpenFile(exportTelem, os.O_CREATE|os.O_RDWR, 0644) - if err != nil { - return nil, fmt.Errorf("failed to open ibt export file: %v", err) - } - } - - if ibt.File == nil { + switch i.Opts.SourceType { + case SharedMemoryFile: // User is requesting us to read live data - present in the mem map file - ibt.File, err = winutils.OpenMemMap(IRSDK_MEMMAPFILENAME, fileMapSize) + i.File, err = winutils.OpenMemMap(IRSDK_MEMMAPFILENAME, fileMapSize) if err != nil { - return nil, fmt.Errorf("Failed to open memory mapped file: %v", err) + return fmt.Errorf("failed to open memory mapped file: %v", err) } // 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() + i.winUtils, err = winutils.Init() if err != nil { - return nil, err + return err } // We need to open the windows event thing - err = ibt.winUtils.OpenWinEvent(IRSDK_DATAVALIDEVENTNAME) + err = i.winUtils.OpenWinEvent(IRSDK_DATAVALIDEVENTNAME) if err != nil { - return nil, err + return err } // We need to open the broadcast channel - err = ibt.winUtils.OpenBroadcastChannel(IRSDK_BROADCASTMSGNAME) + err = i.winUtils.OpenBroadcastChannel(IRSDK_BROADCASTMSGNAME) if err != nil { - return nil, err + return err + } + case IBTFile: + i.File, err = os.Open(i.Opts.SourcePath) + if err != nil { + return fmt.Errorf("failed to open file `%s`: %+v", i.Opts.SourcePath, err) + } + default: + return fmt.Errorf("a source type must be specified") + } + + return nil +} + +func (i *IBT) openExporter() error { + var err error + + switch i.Opts.IBTExportType { + case SharedMemoryFile: + // Lets create a shared memory file! + shm, err := sharedMem.Create(MEMMAPFILENAME, fileMapSize) + if err != nil { + return fmt.Errorf("unable to create memory map file: %+v", err) + } + + i.IBTExporter = shm + case IBTFile: + i.IBTExporter, err = os.OpenFile(i.Opts.IBTExportPath, os.O_CREATE|os.O_RDWR, 0o644) + if err != nil { + return fmt.Errorf("failed to open ibt export file: %v", err) + } + } + + return nil +} + +// Init serves to initialize and get a hold of a IBT struct +// Receives an Options struct with the required configurations +func Init(opts Options) (*IBT, error) { + // log := logger.GetInstance() + + // Create our irsdk instance + var err error + ibt := IBT{ + Opts: opts, + Vars: &TelemetryVars{}, + winUtils: nil, + } + + // Setup the source + err = ibt.openSource() + if err != nil { + return nil, err + } + + // Setup the IBT data export - can be either shared memory or data file + if opts.IBTExport { + err = ibt.openExporter() + if err != nil { + // We log this only, or return some type of message + // Set the option to false so we won't export + ibt.Opts.IBTExport = false } } diff --git a/irsdk_test.go b/irsdk_test.go index e989645..d4ac41c 100644 --- a/irsdk_test.go +++ b/irsdk_test.go @@ -30,7 +30,7 @@ func lapTimeRepresentation(t float32) string { } func TestFunctionality(t *testing.T) { - input, err := os.Open("../testTelemetry/supercars_race_watkins_glenn.ibt") + input, err := os.Open("../../testTelemetry/supercars_race_watkins_glenn.ibt") if err != nil { t.Fatal("Was unable to prepare telemetry file for testing.") } @@ -62,7 +62,7 @@ func TestFunctionality(t *testing.T) { // driversBehind := i.Vars.Vars["CarIdxF2Time"].Value.([]float32) drivers := i.SessionInfo.DriverInfo.Drivers - myIdx := i.SessionInfo.DriverInfo.DriverCarIdx + myIdx := i.SessionInfo.DriverInfo.DriverCarIdx standings := make([]StandingsLine, len(drivers)) @@ -90,7 +90,7 @@ func TestFunctionality(t *testing.T) { return standings[i].LapPct >= standings[j].LapPct }) - fmt.Printf("%v\n", driversEstTime) + fmt.Printf("%v\n", driversEstTime) // for p, v := range standings { // fmt.Printf("[%2d] %-30s %13f %13f\n", // p+1, v.DriverName, v.LapPct, driversEstTime[p] - driversEstTime[myIdx]) diff --git a/sessionInfo.go b/sessionInfo.go index d177503..2695b65 100644 --- a/sessionInfo.go +++ b/sessionInfo.go @@ -1,13 +1,13 @@ package goirsdk import ( - "github.com/ESilva15/goirsdk/logger" - "encoding/json" "fmt" "log" "strings" + "github.com/ESilva15/goirsdk/logger" + "golang.org/x/text/encoding/charmap" "gopkg.in/yaml.v3" ) @@ -321,7 +321,7 @@ func (i *IBT) readSessionInfo() error { } // Write to the output file - if i.IBTExport != nil { + if i.Opts.IBTExport { err := i.exportIBT(sessionInfoStringRaw[:], int64(i.Headers.SessionInfoOffset)) if err != nil { log.Printf("Failed to export offline telemetry data: %v", err) @@ -334,7 +334,7 @@ func (i *IBT) readSessionInfo() error { } // Write to YAML output file - if i.YAMLExportPath != "" { + if i.Opts.SessionInfoExport { err := i.exportYAML() if err != nil { log.Printf("Failed to export YAML string: %v\n", err) diff --git a/variables.go b/variables.go index 32fdc92..3d3061c 100644 --- a/variables.go +++ b/variables.go @@ -40,11 +40,13 @@ 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 -} +type ( + IRacingState int + 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 @@ -106,9 +108,10 @@ type varBuffer struct { } type TelemetryVars struct { - Tick int32 // Keeps track of the current data buffer tick - RecorderTick int32 // Counts from 0 when creating a telemetry file from a replay or live data - Vars map[string]Var // Variables content + Tick int32 // Keeps track of the current data buffer tick + RecorderTick int32 // Counts from 0 when creating a telemetry file from a + // replay or live data + Vars map[string]Var // Variables content } func (i *IBT) readVariablerHeaders() error { @@ -123,7 +126,7 @@ func (i *IBT) readVariablerHeaders() error { return err } - if i.IBTExport != nil { + if i.Opts.IBTExport { err = i.exportIBT(rbuf, int64(i.Headers.VarHeaderOffset+k*VarHeaderSize)) if err != nil { // Don't outright kill it here - maybe nowhere else @@ -306,6 +309,7 @@ func (i *IBT) readData(buf []byte) error { // Update will read the next data chunk from the telemetry data, works for both the // live and offline data func (i *IBT) Update(timeout time.Duration) (IRacingState, error) { + // This is what happens if we are reading live data 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") @@ -344,10 +348,19 @@ func (i *IBT) Update(timeout time.Duration) (IRacingState, error) { return Failed, err } - if i.IBTExport != nil { - err = i.exportIBT(buf, int64(i.Headers.BufOffset+i.Vars.RecorderTick*i.Headers.BufLen)) - if err != nil { - log.Printf("Failed to export live telemetry data: %v", err) + if i.Opts.IBTExport { + // Dirty attempt at getting this to work to write to a memory mapped file + switch i.Opts.IBTExportType { + case IBTFile: + err = i.exportIBT(buf, int64(i.Headers.BufOffset+i.Vars.RecorderTick*i.Headers.BufLen)) + if err != nil { + log.Printf("Failed to export live telemetry data: %v", err) + } + case SharedMemoryFile: + err = i.exportIBT(buf, int64(i.Headers.BufOffset)) + if err != nil { + log.Printf("Failed to export live telemetry data: %v", err) + } } } @@ -363,6 +376,7 @@ func (i *IBT) Update(timeout time.Duration) (IRacingState, error) { // Document why this is here, I don't remember the exact words right now i.Vars.RecorderTick++ } else { + // This is what happens if we are reading from an .ibt file // This will get the dataframe corresponding to a given tick start := i.Headers.BufOffset + i.Vars.Tick*i.Headers.BufLen buf := make([]byte, i.Headers.BufLen) @@ -370,10 +384,19 @@ func (i *IBT) Update(timeout time.Duration) (IRacingState, error) { // Make this happen in a different thread, or have this send to a queue that has a thread // writing to a file - if i.IBTExport != nil { - err = i.exportIBT(buf, int64(start)) - if err != nil { - log.Printf("Failed to export offline telemetry data: %v", err) + if i.Opts.IBTExport { + // Dirty attempt at getting this to work to write to a memory mapped file + switch i.Opts.IBTExportType { + case IBTFile: + err = i.exportIBT(buf, int64(start)) + if err != nil { + log.Printf("Failed to export offline telemetry data: %v", err) + } + case SharedMemoryFile: + err = i.exportIBT(buf, int64(i.Headers.BufOffset)) + if err != nil { + log.Printf("Failed to export live telemetry data: %v", err) + } } } diff --git a/winutils/winutils_linux.go b/winutils/winutils_linux.go index bb914a4..ff6002c 100644 --- a/winutils/winutils_linux.go +++ b/winutils/winutils_linux.go @@ -1,53 +1,116 @@ //go:build (linux && cgo) || (darwin && cgo) -// +build linux,cgo darwin,cgo package winutils import ( - "errors" - "sync" + "fmt" + "net" + "os" "time" ) -const ( - WAIT_OBJECT_0 = 0 - WAIT_TIMEOUT = 258 -) - -var ( - once sync.Once - ErrUnsupportedOS = errors.New("not found") -) - type utils struct { + socketPath string + listener *net.UnixConn } -// INITIALIZATION func newUtils() (*utils, error) { - return nil, ErrUnsupportedOS + return &utils{}, nil } func (u *utils) Close() { + if u.listener != nil { + u.listener.Close() + os.Remove(u.socketPath) + } } -// openEvent opens a windows.Handle for a given event +// OpenEvent creates or connects to a Unix socket for event signaling on Linux func (u *utils) OpenEvent(eventName string) error { - return ErrUnsupportedOS + u.socketPath = fmt.Sprintf("/tmp/iracing_%s.sock", eventName) + _ = os.Remove(u.socketPath) + + addr, err := net.ResolveUnixAddr("unixgram", u.socketPath) + if err != nil { + return err + } + + l, err := net.ListenUnixgram("unixgram", addr) + if err != nil { + return err + } + u.listener = l + return nil } -// OpenBroadcastChannel opens up a broadcast channel to send commands to iracing func (u *utils) OpenBroadcastChannel(name string) error { - return ErrUnsupportedOS + // No-op or log stub on Linux + return nil } -// INITIALIZATION - -// openEvent waits for a good response for some given time +// CheckValidDataEvent waits for a pulse byte sent over the socket func (u *utils) CheckValidDataEvent(timeout time.Duration) bool { - return false + if u.listener == nil { + return false + } + + _ = u.listener.SetReadDeadline(time.Now().Add(timeout)) + buf := make([]byte, 1) + _, err := u.listener.Read(buf) + return err == nil } -// SendBroadcastMessage sends a message trough the broadcast channel func (u *utils) SendBroadcastMessage(id, p1, p2 uintptr) error { - return ErrUnsupportedOS + return nil } + +// package winutils +// +// import ( +// "errors" +// "sync" +// "time" +// ) +// +// const ( +// WAIT_OBJECT_0 = 0 +// WAIT_TIMEOUT = 258 +// ) +// +// var ( +// once sync.Once +// ErrUnsupportedOS = errors.New("not found") +// ) +// +// type utils struct { +// } +// +// // INITIALIZATION +// func newUtils() (*utils, error) { +// return nil, ErrUnsupportedOS +// } +// +// func (u *utils) Close() { +// } +// +// // openEvent opens a windows.Handle for a given event +// func (u *utils) OpenEvent(eventName string) error { +// return ErrUnsupportedOS +// } +// +// // OpenBroadcastChannel opens up a broadcast channel to send commands to iracing +// func (u *utils) OpenBroadcastChannel(name string) error { +// return ErrUnsupportedOS +// } +// +// // INITIALIZATION +// +// // openEvent waits for a good response for some given time +// func (u *utils) CheckValidDataEvent(timeout time.Duration) bool { +// return false +// } +// +// // SendBroadcastMessage sends a message trough the broadcast channel +// func (u *utils) SendBroadcastMessage(id, p1, p2 uintptr) error { +// return ErrUnsupportedOS +// } diff --git a/winutils/winutils_windows.go b/winutils/winutils_windows.go index 3163681..55c12bc 100644 --- a/winutils/winutils_windows.go +++ b/winutils/winutils_windows.go @@ -1,5 +1,4 @@ -//go:build windows && cgo -// +build windows,cgo +// go:build windows package winutils @@ -16,13 +15,11 @@ const ( WAIT_TIMEOUT = 258 ) -var ( - once sync.Once -) +var once sync.Once type utils struct { user32DLL *windows.LazyDLL - wEvent *windows.Handle + wEvent windows.Handle wBroadcastChn uintptr } @@ -34,7 +31,7 @@ func newUtils() (*utils, error) { } func (u *utils) Close() { - closeEvent(u.wEvent) + closeEvent(&u.wEvent) // Do we need to unload the user32DLL ??? // Do we need to close the broadcast channel ??? } @@ -50,7 +47,7 @@ func (u *utils) OpenEvent(eventName string) error { if err != nil { return err } - u.wEvent = &event + u.wEvent = event return nil } @@ -90,7 +87,7 @@ func (u *utils) CheckValidDataEvent(timeout time.Duration) bool { t0 := time.Now().UnixNano() timeoutInt := uint32(timeout / time.Millisecond) - result, err := windows.WaitForSingleObject(*u.wEvent, timeoutInt) + result, err := windows.WaitForSingleObject(u.wEvent, timeoutInt) if err != nil { remaining := timeoutInt - uint32((time.Now().UnixNano()-t0)/1000000) if remaining > 0 { -- 2.54.0 From a449ced75f2ceb8edbd20106b1d6198521437abe Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Sun, 23 Aug 2026 23:44:36 +0100 Subject: [PATCH 2/3] Linux can now read from a shared memory map and the sdk can create it --- irsdk.go | 27 ++++++++++--------- .../winutils.go => mmaputils/mmaputils.go | 16 ++--------- .../mmaputils_linux.go | 15 ++++++++++- .../mmaputils_windows.go | 14 +++++++++- 4 files changed, 43 insertions(+), 29 deletions(-) rename winutils/winutils.go => mmaputils/mmaputils.go (69%) rename winutils/winutils_linux.go => mmaputils/mmaputils_linux.go (82%) rename winutils/winutils_windows.go => mmaputils/mmaputils_windows.go (84%) diff --git a/irsdk.go b/irsdk.go index 97be156..7422bab 100644 --- a/irsdk.go +++ b/irsdk.go @@ -7,8 +7,8 @@ import ( "os" "github.com/ESilva15/goirsdk/logger" + "github.com/ESilva15/goirsdk/mmaputils" "github.com/ESilva15/goirsdk/sharedMem" - "github.com/ESilva15/goirsdk/winutils" "gopkg.in/yaml.v3" ) @@ -52,7 +52,7 @@ type IBT struct { // IBTExportPath string // Path for IBT export // YAMLExport *os.File // If set, it will export the session YAML to the file // YAMLExportPath string // Path for YAML export - winUtils *winutils.IRacingWinUtils // WinUtils gives access to the system utilities + winUtils *mmaputils.IRacingWinUtils // WinUtils gives access to the system utilities // TODO: fragment this struct a little bit, for now I want to actually get // stuff done so its enough to work as is @@ -117,30 +117,31 @@ func (i *IBT) openSource() error { switch i.Opts.SourceType { case SharedMemoryFile: // User is requesting us to read live data - present in the mem map file - i.File, err = winutils.OpenMemMap(IRSDK_MEMMAPFILENAME, fileMapSize) + i.File, err = mmaputils.OpenMemMap(MEMMAPFILENAME, fileMapSize) if err != nil { - return fmt.Errorf("failed to open memory mapped file: %v", err) + return fmt.Errorf("failed to open memory mapped file: %+v", err) } // 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 - i.winUtils, err = winutils.Init() + i.winUtils, err = mmaputils.Init() if err != nil { return err } + // I don't believe we need this on windows either, but I'll have to check // We need to open the windows event thing - err = i.winUtils.OpenWinEvent(IRSDK_DATAVALIDEVENTNAME) - if err != nil { - return err - } + // err = i.winUtils.OpenWinEvent(IRSDK_DATAVALIDEVENTNAME) + // if err != nil { + // return err + // } // We need to open the broadcast channel - err = i.winUtils.OpenBroadcastChannel(IRSDK_BROADCASTMSGNAME) - if err != nil { - return err - } + // err = i.winUtils.OpenBroadcastChannel(IRSDK_BROADCASTMSGNAME) + // if err != nil { + // return err + // } case IBTFile: i.File, err = os.Open(i.Opts.SourcePath) if err != nil { diff --git a/winutils/winutils.go b/mmaputils/mmaputils.go similarity index 69% rename from winutils/winutils.go rename to mmaputils/mmaputils.go index 6f829fc..d810893 100644 --- a/winutils/winutils.go +++ b/mmaputils/mmaputils.go @@ -2,10 +2,9 @@ // is interface some windows stuff that we need for the: // - Broadcast Channel // - Valid Data Event windows thing -package winutils +package mmaputils import ( - "github.com/ESilva15/goirsdk/sharedMem" "io" "time" ) @@ -32,23 +31,12 @@ 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 nil, err - } - - return file, nil -} - // OpenWinEvent will open the named windows event func (u *IRacingWinUtils) OpenWinEvent(name string) error { return u.Utils.OpenEvent(name) } -// OpenWinEvent will open the broadcast channel +// OpenBroadcastChannel will open the broadcast channel func (u *IRacingWinUtils) OpenBroadcastChannel(name string) error { return u.Utils.OpenBroadcastChannel(name) } diff --git a/winutils/winutils_linux.go b/mmaputils/mmaputils_linux.go similarity index 82% rename from winutils/winutils_linux.go rename to mmaputils/mmaputils_linux.go index ff6002c..e88f4ee 100644 --- a/winutils/winutils_linux.go +++ b/mmaputils/mmaputils_linux.go @@ -1,12 +1,14 @@ //go:build (linux && cgo) || (darwin && cgo) -package winutils +package mmaputils import ( "fmt" "net" "os" "time" + + "github.com/ESilva15/goirsdk/sharedMem" ) type utils struct { @@ -25,6 +27,17 @@ func (u *utils) Close() { } } +// OpenMemMap returns a Reader interface that can be used to read the data +// No need to encapsulate it +func OpenMemMap(name string, size uint32) (Reader, error) { + file, err := sharedMem.Open(name, size) + if err != nil { + return nil, fmt.Errorf("failed to open `%s` with err: %+v", name, err) + } + + return file, nil +} + // OpenEvent creates or connects to a Unix socket for event signaling on Linux func (u *utils) OpenEvent(eventName string) error { u.socketPath = fmt.Sprintf("/tmp/iracing_%s.sock", eventName) diff --git a/winutils/winutils_windows.go b/mmaputils/mmaputils_windows.go similarity index 84% rename from winutils/winutils_windows.go rename to mmaputils/mmaputils_windows.go index 55c12bc..b9c89a7 100644 --- a/winutils/winutils_windows.go +++ b/mmaputils/mmaputils_windows.go @@ -1,12 +1,13 @@ // go:build windows -package winutils +package mmaputils import ( "sync" "time" "unsafe" + "github.com/ESilva15/goirsdk/sharedMem" "golang.org/x/sys/windows" ) @@ -36,6 +37,17 @@ func (u *utils) Close() { // Do we need to close the broadcast channel ??? } +// OpenMemMap returns a Reader interface that can be used to read the data +// No need to encapsulate it +func OpenMemMap(name string, size uint32) (Reader, error) { + file, err := sharedMem.Open("Local\\"+name, size) + if err != nil { + return nil, err + } + + return file, nil +} + // openEvent opens a windows.Handle for a given event func (u *utils) OpenEvent(eventName string) error { name, err := windows.UTF16PtrFromString(eventName) -- 2.54.0 From 6b0283b20612aba4cc5aaca9f5b78919fec2c05d Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Sun, 23 Aug 2026 23:53:14 +0100 Subject: [PATCH 3/3] added these examples I'm using them for development... --- .../read_from_ibt_file.go} | 9 ++- .../read_from_mmap_file.go | 62 +++++++++++++++++++ 2 files changed, 66 insertions(+), 5 deletions(-) rename examples/{example.go => read_from_ibt_file/read_from_ibt_file.go} (88%) create mode 100644 examples/read_from_mmap_file/read_from_mmap_file.go diff --git a/examples/example.go b/examples/read_from_ibt_file/read_from_ibt_file.go similarity index 88% rename from examples/example.go rename to examples/read_from_ibt_file/read_from_ibt_file.go index caaa9b2..5694d65 100644 --- a/examples/example.go +++ b/examples/read_from_ibt_file/read_from_ibt_file.go @@ -15,12 +15,11 @@ func msToKph(v float32) int { func main() { // Instantiate our iRacing SDK instance irsdk, err := goirsdk.Init(goirsdk.Options{ - SourceType: goirsdk.IBTFile, - SourcePath: "../../../testTelemetry/gt3_mustang_bathurst.ibt", - // SourcePath: "./exported.ibt", + SourceType: goirsdk.IBTFile, + SourcePath: "../../../../testTelemetry/gt3_mustang_bathurst.ibt", IBTExportType: goirsdk.SharedMemoryFile, - // IBTExportPath: "./exported.ibt", - IBTExport: true, + IBTExportPath: "./exported.ibt", + IBTExport: true, }) if err != nil { log.Fatalf("Failed to create iRacing interface: %v", err) diff --git a/examples/read_from_mmap_file/read_from_mmap_file.go b/examples/read_from_mmap_file/read_from_mmap_file.go new file mode 100644 index 0000000..71df02e --- /dev/null +++ b/examples/read_from_mmap_file/read_from_mmap_file.go @@ -0,0 +1,62 @@ +package main + +import ( + "fmt" + "log" + "time" + + "github.com/ESilva15/goirsdk" +) + +func msToKph(v float32) int { + return int((3600 * v) / 1000) +} + +func main() { + // Instantiate our iRacing SDK instance + irsdk, err := goirsdk.Init(goirsdk.Options{ + SourceType: goirsdk.SharedMemoryFile, + }) + if err != nil { + log.Fatalf("Failed to create iRacing interface: %v", err) + } + defer irsdk.Close() + + // Set up a loop to iterate our data + // TODO: revert this 240 back to 60 because i recorded the thing wrong or whatever + mainLoopTicker := time.NewTicker(time.Second / 240) + defer mainLoopTicker.Stop() + + for { + // Update the data that the SDK is holding with the next tick + _, err := irsdk.Update(100 * time.Millisecond) + if err != nil { + log.Printf("could not update data: %v", err) + continue + } + + // Vehicle Movement data gathered from the names we can find on the + // telemetry_docs.pdf file + // - I wish to make this less verbose if possible + if _, ok := irsdk.Vars.Vars["Gear"]; !ok { + log.Fatal("Field `Gear` doesn't exist") + } + + if _, ok := irsdk.Vars.Vars["RPM"]; !ok { + log.Fatal("Field `RPM` doesn't exist") + } + + if _, ok := irsdk.Vars.Vars["Speed"]; !ok { + log.Fatal("Field `Speed` doesn't exist") + } + + gear := int32(irsdk.Vars.Vars["Gear"].Value.(int)) + rpm := int32(irsdk.Vars.Vars["RPM"].Value.(float32)) + speed := int32(msToKph(irsdk.Vars.Vars["Speed"].Value.(float32))) + + fmt.Printf("\033[?25l\033[2J\033[H") + fmt.Printf("Gear: %d, RPM: %d, Speed: %d", gear, rpm, speed) + + <-mainLoopTicker.C + } +} -- 2.54.0