// 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" "strconv" "sync" "time" conv "esdi/conversions" "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 *telemetry.TelemetryData updaters [telemetry.MaxFields]func(*telemetry.TelemetryField) // Timing information ticker *time.Ticker // ticker will keep polling intervals constant // Stream streamCh chan telemetry.TelemetryData streamCancel context.CancelFunc } func NewIRacingProvider( logger *slog.Logger, opts goirsdk.Options, ) (*IRacing, error) { var err error sdk, err := goirsdk.Init(opts) if err != nil { logger.Error("failed to open the IRSDK instance") return &IRacing{}, err } provider := &IRacing{ logger: logger, SDK: sdk, data: telemetry.NewTelemetryData(), streamCh: make(chan telemetry.TelemetryData, 1), // NOTE: This is because I stupidly recorded a test IBT file in 240 ticker: time.NewTicker(time.Second / 240), } // provider.updaters = [telemetry.MaxFields]func(*telemetry.TelemetryField){} return provider, 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 { // Explicitly intercpt cancellation select { case <-ctx.Done(): return default: } // 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 telemetry.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() { if i.streamCancel == nil { return } i.streamCancel() i.streamCancel = nil } func (i *IRacing) Subscribe(requestFields map[int16]telemetry.FieldID) { i.logger.Debug(fmt.Sprintf("Len Req: %d\n", len(requestFields))) i.data.ActiveBinds = make([]telemetry.BoundField, 0, len(requestFields)) // First we must add the virtual fields // we will add their dependencies and the primitives to a slice pendingBinds := make([]telemetry.FieldID, telemetry.MaxFields) for winID, id := range requestFields { i.data.Values[id].IDs = append(i.data.Values[id].IDs, winID) switch id { case telemetry.RPMStateColour: i.data.VirtualBinds = append(i.data.VirtualBinds, telemetry.NewRPMLights()) case telemetry.FCCurrentLap: i.data.VirtualBinds = append(i.data.VirtualBinds, telemetry.NewFuelCalculator(i.logger.WithGroup("FUEL CALC"))) default: // primitive telemetry field pendingBinds = append(pendingBinds, id) } } boundCheck := make(map[telemetry.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 := telemetry.BoundField{ Key: sdkKey, ID: id, } switch id { case telemetry.Speed: binding.Transform = func(v any, out *telemetry.TelemetryField) { out.Type = telemetry.DataTypeUINT16 out.Raw = uint64(conv.MsToKph(v.(float32))) } case telemetry.Gear: binding.Transform = func(v any, out *telemetry.TelemetryField) { out.Type = telemetry.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 telemetry.RPM: binding.Transform = func(v any, out *telemetry.TelemetryField) { out.Type = telemetry.DataTypeUINT16 out.Raw = uint64(uint16(v.(float32))) } case telemetry.FuelLevel: binding.Transform = telemetry.FloatToStringTransformDEPRECATE // Engine Data case telemetry.OilPress: binding.Transform = telemetry.FloatToStringTransformDEPRECATE case telemetry.OilTemp: binding.Transform = telemetry.FloatToStringTransformDEPRECATE case telemetry.WaterTemp: binding.Transform = telemetry.FloatToStringTransformDEPRECATE // Something else case telemetry.PitSpeedLimiter: binding.Transform = PitSpeedLimiterTransform // Adjustements case telemetry.BrakeBias: binding.Transform = telemetry.FloatToStringTransformDEPRECATE case telemetry.ABSSetting: binding.Transform = telemetry.FloatToUInt8TransformDEPRECATE case telemetry.TCSetting: binding.Transform = telemetry.FloatToUInt8TransformDEPRECATE case telemetry.ThrottleSetting: binding.Transform = telemetry.FloatToUInt8TransformDEPRECATE case telemetry.LFtempM: binding.Transform = func(v any, out *telemetry.TelemetryField) { out.Type = telemetry.DataTypeSTRING out.Str = strconv.FormatFloat(float64(v.(float32)), 'f', 1, 32) } case telemetry.SessionTime: binding.Transform = func(v any, out *telemetry.TelemetryField) { out.Type = telemetry.DataTypeSTRING out.Str = strconv.FormatFloat(v.(float64), 'f', 1, 32) } case telemetry.ReplaySessionTime: binding.Transform = func(v any, out *telemetry.TelemetryField) { out.Type = telemetry.DataTypeSTRING out.Str = strconv.FormatFloat(v.(float64), 'f', 1, 32) } case telemetry.Empty: binding.Transform = telemetry.EmptyTransform case telemetry.LapLastLapTime: binding.Transform = LapTimeTransform case telemetry.LapNumber: binding.Transform = telemetry.UInt8Transform } i.data.ActiveBinds = append(i.data.ActiveBinds, binding) boundCheck[id] = true } i.logger.Debug(fmt.Sprintf("Subscribed: %+v\n", i.data.ActiveBinds)) }