// Package iracing is in the providers and provides telemety to our desktop // application. It needs to establish a relationship between the desktop data // structure of our app and the data iRacing provides package iracing import ( "context" "fmt" "log/slog" "os" "strconv" "sync" "time" conv "esdi/conversions" telem "esdi/telemetry" "github.com/ESilva15/goirsdk" ) const ( NAME = "iRacing" ) // IRacing is our iRacing telemetry data provider - its a TelemetryProvider interface type IRacing struct { logger *slog.Logger SDK *goirsdk.IBT // Data Handling mut sync.Mutex data *telem.TelemetryData // Timing information ticker *time.Ticker // ticker will keep polling intervals constant // Stream streamCh chan telem.TelemetryData streamCancel context.CancelFunc } func NewIRacingProvider( logger *slog.Logger, source string, telemOut string, yamlOut string, ) (*IRacing, error) { var err error // Open the input file if provided - otherwise live telemetry was requested // Maybe this can be changed so we don't have to run it with these ifs but by configuring our // provider var file goirsdk.Reader = nil if source != "" { file, err = os.Open(source) if err != nil { return &IRacing{}, err // log.Fatalf("Failed to open IBT file: %v", err) } } sdk, err := goirsdk.Init(file, telemOut, yamlOut) if err != nil { logger.Error("failed to open the IRSDK instance") return &IRacing{}, err } return &IRacing{ logger: logger, SDK: sdk, data: telem.NewTelemetryData(), streamCh: make(chan telem.TelemetryData), // NOTE: This is because I stupidly recorded a test IBT file in 240 ticker: time.NewTicker(time.Second / 240), }, nil } func (i *IRacing) isDataAvailable() bool { // Its offline telemetry, data must be available if i.SDK.File != nil { return true } // Check if live telemetry is on if i.SDK.IsConnected() { return true } return false } func (i *IRacing) stream(ctx context.Context) { i.data.InitialTime = time.Now() go func() { for { // We start by checking if we do or do not have data available if !i.isDataAvailable() { continue } select { case <-ctx.Done(): return case <-i.ticker.C: i.readData() // Publish data select { case i.streamCh <- *i.data: default: // skip this data, don't allow publishers to lag behind } } } }() } func (i *IRacing) readData() { i.mut.Lock() defer i.mut.Unlock() var err error _, err = i.SDK.Update(time.Millisecond * 16) if err != nil { return } // Read 1 to 1 data for _, b := range i.data.ActiveBinds { v := i.SDK.Vars.Vars[b.Key].Value b.Transform(v, &i.data.Values[b.ID]) } // Set up virtual binds i.logger.Debug("Entering virtual binds loop") for _, vBind := range i.data.VirtualBinds { // NOTE: delete the logs here, they are really bad i.logger.Debug("Processing virtual binds") vBind.Process(i.data) } i.data.PenultimateDataPoll = i.data.LastDataPoll i.data.LastDataPoll = time.Now() } // Telemetry Provider Interface // Stream returns a channel that we will use to funnel the telemetry data back to the // UI, which then should broadcast it to the devices func (i *IRacing) Stream() (<-chan telem.TelemetryData, error) { var ctx context.Context ctx, i.streamCancel = context.WithCancel(context.Background()) // Start the stream i.stream(ctx) return i.streamCh, nil } func (i *IRacing) StopStream() { i.streamCancel() } func (i *IRacing) Subscribe(requestFields map[int16]telem.FieldID) { i.logger.Debug(fmt.Sprintf("Len Req: %d\n", len(requestFields))) i.data.ActiveBinds = make([]telem.BoundField, 0, len(requestFields)) // First we must add the virtual fields // we will add their dependencies and the primitives to a slice pendingBinds := make([]telem.FieldID, telem.MaxFields) for winID, id := range requestFields { i.data.Values[id].IDs = append(i.data.Values[id].IDs, winID) switch id { case telem.RPMStateColour: i.data.VirtualBinds = append(i.data.VirtualBinds, telem.NewRPMLights()) case telem.FCCurrentLap: i.data.VirtualBinds = append(i.data.VirtualBinds, telem.NewFuelCalculator(i.logger.WithGroup("FUEL CALC"))) default: // primitive telemetry field pendingBinds = append(pendingBinds, id) } } boundCheck := make(map[telem.FieldID]bool) // Now that we know all the fields we need to bind we follow the binding procedure for _, id := range pendingBinds { // Check if we already bound this FieldID if boundCheck[id] { continue } // Translate the UI FieldIDs to this provider's field names sdkKey, ok := internalToSDKFieldNames[id] if !ok { i.logger.Debug("failed to get internal id") // Need to find a way to pass a message saying something wasn't right continue } binding := telem.BoundField{ Key: sdkKey, ID: id, } switch id { case telem.Speed: binding.Transform = func(v any, out *telem.TelemetryField) { out.Type = telem.DataTypeUINT16 out.Raw = uint64(conv.MsToKph(v.(float32))) } case telem.Gear: binding.Transform = func(v any, out *telem.TelemetryField) { out.Type = telem.DataTypeCHAR // out.Raw = uint64(v.(int)) gear := 0 if val, ok := v.(int32); ok { gear = int(val) } else if val, ok := v.(int); ok { gear = val } switch { case gear == 0: out.Raw = uint64('N') // ASCII 78 case gear < 0: out.Raw = uint64('R') // ASCII 82 case gear > 0 && gear < 10: // Quickest way to turn 1 into '1', 2 into '2', etc. // ASCII '0' is 48, so 48 + 1 = 49 ('1') out.Raw = uint64('0' + gear) default: out.Raw = uint64('?') // Fallback } } case telem.RPM: binding.Transform = func(v any, out *telem.TelemetryField) { out.Type = telem.DataTypeUINT16 out.Raw = uint64(uint16(v.(float32))) } case telem.FuelLevel: binding.Transform = FloatToStringTransform // Engine Data case telem.OilPress: binding.Transform = FloatToStringTransform case telem.OilTemp: binding.Transform = FloatToStringTransform case telem.WaterTemp: binding.Transform = FloatToStringTransform // Something else case telem.PitSpeedLimiter: binding.Transform = PitSpeedLimiterTransform // Adjustements case telem.BrakeBias: binding.Transform = FloatToStringTransform case telem.ABSSetting: binding.Transform = FloatToUInt8Transform case telem.TCSetting: binding.Transform = FloatToUInt8Transform case telem.ThrottleSetting: binding.Transform = FloatToUInt8Transform case telem.LFtempM: binding.Transform = func(v any, out *telem.TelemetryField) { out.Type = telem.DataTypeSTRING out.Str = strconv.FormatFloat(float64(v.(float32)), 'f', 1, 32) } case telem.SessionTime: binding.Transform = func(v any, out *telem.TelemetryField) { out.Type = telem.DataTypeSTRING out.Str = strconv.FormatFloat(v.(float64), 'f', 1, 32) } case telem.ReplaySessionTime: binding.Transform = func(v any, out *telem.TelemetryField) { out.Type = telem.DataTypeSTRING out.Str = strconv.FormatFloat(v.(float64), 'f', 1, 32) } case telem.Empty: binding.Transform = EmptyTransform case telem.LapLastLapTime: binding.Transform = LapTimeTransform case telem.LapNumber: binding.Transform = UInt8Transform } i.data.ActiveBinds = append(i.data.ActiveBinds, binding) boundCheck[id] = true } i.logger.Debug(fmt.Sprintf("Subscribed: %+v\n", i.data.ActiveBinds)) }