301 lines
7.9 KiB
Go
301 lines
7.9 KiB
Go
// 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))
|
|
}
|