From 20dbf15adfe95fa1bc584f3d18c7e0c1d99c85fd Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Thu, 25 Jun 2026 15:35:57 +0100 Subject: [PATCH] Support for BeamNG as a data provider --- README.md | 36 +++- providers/beamng/beamng.go | 236 ++++++++++++++++++++++++-- providers/beamng/fieldMapping.go | 45 +++++ providers/beamng/transforms.go | 61 +++++++ providers/iracing/commonTransforms.go | 72 -------- providers/iracing/iracing.go | 110 ++++++------ providers/iracing/transforms.go | 34 ++++ providers/providers.go | 5 + services/telemetry.go | 12 +- telemetry/commonTransforms.go | 41 +++++ tui/internal/controllers/layout.go | 2 +- tui/internal/controllers/streaming.go | 2 +- 12 files changed, 498 insertions(+), 158 deletions(-) create mode 100644 providers/beamng/fieldMapping.go create mode 100644 providers/beamng/transforms.go delete mode 100644 providers/iracing/commonTransforms.go create mode 100644 providers/iracing/transforms.go create mode 100644 telemetry/commonTransforms.go diff --git a/README.md b/README.md index 857aaac..2caa375 100644 --- a/README.md +++ b/README.md @@ -29,22 +29,46 @@ Games to be implemented: ## Roadmap -- [ ] Implement the interface for a data source +- [X] Implement the interface for a data source - [ ] Finish implementing BeamNG - [ ] Configuration of the peripherals via ESDI - [ ] Detection of the display -- [ ] Fuel Calculator +- [X] Fuel Calculator - [ ] LapTime Calculator - [ ] A very long list useful stuff like flags, position, more info about other drivers, track conditions and so on so forth -- [ ] Dynamic data packets +- [X] Dynamic data packets - [ ] More roadmap entries - [ ] Telemetry analysis tool - [ ] Better user interface -## Debugging -### Freezes: +## Todos +- [ ] Remove all the `slog.logger` passing and struct members and just configure +`slog.Logger` once and use the default. Or think of something better +- [ ] Do all the `NOTE`s and `BUG`s +- [ ] Whats common among the providers? Should abstract whats common +- [ ] Whats common with all the "streams" I have? Should abstract whats common + + +## Development +### Mockservers +To mock [BeamNG.drive](https://www.beamng.com/game/), I built +[BeaMNGMockOg](https://github.com/ESilva15/BeamNGMockOg) (should have though longer +about the name). +You create a recording by launching the BeamNG and then launching the mockserver with: +`BeamNGMockOg record -a 127.0.0.1 -p 4443 -o output.bin`. + +To replay the recording: +`BeamNGMockOg replay [--loop] -a 127.0.0.1 -p 4443 -i input.bin` + +### Troubleshooting +#### Can't see BeamNG data coming through +Use, for example, `tcpdump -i any udp port -X` to check if any data is available. + + +### Debugging +#### Freezes: Using delve: - Launch Terminal1 with `dlv debug . --headless --listen=:2345 -- tui` - Launch Terminal2 and connect to with with `dlv connect :2345` @@ -59,7 +83,7 @@ pprof: - type `web` to view a graph version -### Shameless begging +## Shameless begging Hey, doesn't hurt to try, its free either way: [Buy me a coffee!](buymeacoffee.com/ESilva_15) diff --git a/providers/beamng/beamng.go b/providers/beamng/beamng.go index a950618..52b1286 100644 --- a/providers/beamng/beamng.go +++ b/providers/beamng/beamng.go @@ -1,43 +1,249 @@ +// Package beamng is the BeamNG.drive data provider package beamng import ( + "context" "fmt" + "log/slog" + "strconv" + "sync" + "time" + + conv "esdi/conversions" + "esdi/telemetry" bngsdk "github.com/ESilva15/gobngsdk" ) type BeamNG struct { SDK *bngsdk.BNGSDK + + // data handling + mut sync.Mutex + data *telemetry.TelemetryData + + // stream control + streamCh chan telemetry.TelemetryData + streamCancel context.CancelFunc + + // timing + ticker *time.Ticker } const ( NAME = "BeamNG.drive" ) -func Init(ip string, port int) (BeamNG, error) { - var err error - - sdk, err := bngsdk.Init(ip, port) +func NewBeamNGProvider(ip string, port int) (*BeamNG, error) { + beam, err := bngsdk.Init(ip, port) if err != nil { - return BeamNG{}, err + return &BeamNG{}, err } - return BeamNG{SDK: &sdk}, nil + return &BeamNG{ + streamCh: make(chan telemetry.TelemetryData, 1), + data: telemetry.NewTelemetryData(), + SDK: &beam, + ticker: time.NewTicker(time.Second / 60), + }, nil } -// GetData will retrieve a given field by its name from the OutGauge data -func (b *BeamNG) GetData(fieldName string) (interface{}, error) { - if val, ok := b.SDK.DataDict[fieldName]; ok { - return val, nil +func (b *BeamNG) StopStream() { + if b.streamCancel == nil { + return } - return nil, fmt.Errorf("key `%s` doesn't exist", fieldName) + b.streamCancel() } -func (b *BeamNG) UpdateData() error { - return b.SDK.ReadData() +func (b *BeamNG) Stream() (<-chan telemetry.TelemetryData, error) { + var ctx context.Context + ctx, b.streamCancel = context.WithCancel(context.Background()) + + // Start the stream + b.stream(ctx) + + return b.streamCh, nil } -func (b *BeamNG) GetSessionInfo() (interface{}, error) { - return nil, fmt.Errorf("Not implemented") +func (b *BeamNG) Subscribe(requestFields map[int16]telemetry.FieldID) { + slog.Debug(fmt.Sprintf("Len Req: %d\n", len(requestFields))) + + b.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 { + b.data.Values[id].IDs = append(b.data.Values[id].IDs, winID) + + switch id { + case telemetry.RPMStateColour: + b.data.VirtualBinds = append(b.data.VirtualBinds, telemetry.NewRPMLights()) + case telemetry.FCCurrentLap: + b.data.VirtualBinds = append(b.data.VirtualBinds, + telemetry.NewFuelCalculator(slog.Default().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 { + slog.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 = GearTransform + 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.FloatToStringTransform + // Engine Data + case telemetry.OilPress: + binding.Transform = telemetry.FloatToStringTransform + case telemetry.OilTemp: + binding.Transform = telemetry.FloatToStringTransform + case telemetry.WaterTemp: + binding.Transform = telemetry.FloatToStringTransform + // Something else + case telemetry.PitSpeedLimiter: + binding.Transform = PitSpeedLimiterTransform + // Adjustements + case telemetry.BrakeBias: + binding.Transform = telemetry.FloatToStringTransform + case telemetry.ABSSetting: + binding.Transform = telemetry.FloatToUInt8Transform + case telemetry.TCSetting: + binding.Transform = telemetry.FloatToUInt8Transform + case telemetry.ThrottleSetting: + binding.Transform = telemetry.FloatToUInt8Transform + 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 + } + + b.data.ActiveBinds = append(b.data.ActiveBinds, binding) + boundCheck[id] = true + } + + slog.Debug(fmt.Sprintf("Subscribed: %+v\n", b.data.ActiveBinds)) +} + +// Internal + +func (b *BeamNG) readData() { + slog.Debug("READING THIS DATA") + // BUG: getting stuck in here + err := b.SDK.ReadData() + slog.Debug("THE DATA WAS READ") + if err != nil { + slog.Error("Error getting data", "error", err) + return + } + + b.mut.Lock() + defer b.mut.Unlock() + + // Read 1 to 1 data + slog.Debug("Reading normal data binds") + for _, bind := range b.data.ActiveBinds { + v := b.SDK.DataDict[bind.Key] + + bind.Transform(v, &b.data.Values[bind.ID]) + } + + // Set up virtual binds + slog.Debug("Entering virtual binds loop") + for _, vBind := range b.data.VirtualBinds { + // NOTE: delete the logs here, they are really bad + slog.Debug("Processing virtual binds") + vBind.Process(b.data) + } + + b.data.PenultimateDataPoll = b.data.LastDataPoll + b.data.LastDataPoll = time.Now() +} + +func (b *BeamNG) stream(ctx context.Context) { + b.data.InitialTime = time.Now() + + go func() { + for { + // Explicitly intercept cancellation + select { + case <-ctx.Done(): + return + default: + } + + // We start by checking if we do or do not have data available + // if !b.isDataAvailable() { + // continue + // } + + select { + case <-ctx.Done(): + return + case <-b.ticker.C: + slog.Debug("READING DATA") + b.readData() + slog.Debug("READ DATA") + + // Publish data + select { + case b.streamCh <- *b.data: + slog.Debug("PUBLISHED DATA") + default: + // skip this data, don't allow publishers to lag behind + } + } + } + }() } diff --git a/providers/beamng/fieldMapping.go b/providers/beamng/fieldMapping.go new file mode 100644 index 0000000..a1de681 --- /dev/null +++ b/providers/beamng/fieldMapping.go @@ -0,0 +1,45 @@ +package beamng + +// This file maps the data from the desktop provider data structure to iRacing + +import ( + "esdi/telemetry" +) + +var internalToSDKFieldNames = map[telemetry.FieldID]string{ + telemetry.Speed: "Speed", + telemetry.Gear: "Gear", + telemetry.RPM: "RPM", + telemetry.FuelLevel: "Fuel", + // Engine Data + telemetry.OilPress: "OilPressure", + telemetry.OilTemp: "OilTemp", + telemetry.WaterTemp: "EngTemp", + // Engine Warnings + telemetry.PitSpeedLimiter: "irsdk_pitSpeedLimiter", + // Ajudstements + telemetry.BrakeBias: "dcBrakeBias", + telemetry.ABSSetting: "dcABS", + telemetry.TCSetting: "dcTractionControl", + telemetry.ThrottleSetting: "dcThrottleShape", + // Lap Data + telemetry.LapLastLapTime: "LapLastLapTime", + telemetry.LapNumber: "Lap", + // Tire data + telemetry.LFtempL: "LFtempCL", + telemetry.LFtempM: "LFtempCM", + telemetry.LFtempR: "LFtempCR", + telemetry.RFtempL: "RFtempCL", + telemetry.RFtempM: "RFtempCM", + telemetry.RFtempR: "RFtempCR", + telemetry.LRtempL: "LRtempCL", + telemetry.LRtempM: "LRtempCM", + telemetry.LRtempR: "LRtempCR", + telemetry.RRtempL: "RRtempCL", + telemetry.RRtempM: "RRtempCM", + telemetry.RRtempR: "RRtempCR", + // Session Data + telemetry.SessionTime: "SessionTime", + telemetry.ReplaySessionTime: "ReplaySessionTime", + telemetry.Empty: "empty", +} diff --git a/providers/beamng/transforms.go b/providers/beamng/transforms.go new file mode 100644 index 0000000..edcc285 --- /dev/null +++ b/providers/beamng/transforms.go @@ -0,0 +1,61 @@ +package beamng + +import ( + "time" + + "esdi/telemetry" +) + +const ( + LapTimeFormatStr = "04:05.000" +) + +// NOTE: this is a copy from iracing provider. Change it however necessary +func LapTimeTransform(v any, out *telemetry.TelemetryField) { + lapTimeInSeconds := v.(float32) + + if lapTimeInSeconds < 0 { + lapTimeInSeconds = 0 + } + + wholeSeconds := int64(lapTimeInSeconds) + lapTime := time.Unix(wholeSeconds, int64((lapTimeInSeconds-float32(wholeSeconds))*1e9)) + + out.Type = telemetry.DataTypeSTRING + out.Str = lapTime.Format(LapTimeFormatStr) +} + +// NOTE: this is a copy from iracing provider. Change this however necessary in the future +// for beamng. +func PitSpeedLimiterTransform(v any, out *telemetry.TelemetryField) { + out.Type = telemetry.DataTypeSTRING + if v.(bool) { + out.Str = "PIT" + } else { + out.Str = " " + } +} + +func GearTransform(v any, out *telemetry.TelemetryField) { + out.Type = telemetry.DataTypeCHAR + + 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 + } +} diff --git a/providers/iracing/commonTransforms.go b/providers/iracing/commonTransforms.go deleted file mode 100644 index 5b3f569..0000000 --- a/providers/iracing/commonTransforms.go +++ /dev/null @@ -1,72 +0,0 @@ -package iracing - -import ( - telem "esdi/telemetry" - "strconv" - "time" -) - -const ( - LapTimeFormatStr = "04:05.000" -) - -func LapTimeTransform(v any, out *telem.TelemetryField) { - lapTimeInSeconds := v.(float32) - - if lapTimeInSeconds < 0 { - lapTimeInSeconds = 0 - } - - wholeSeconds := int64(lapTimeInSeconds) - lapTime := time.Unix(wholeSeconds, int64((lapTimeInSeconds-float32(wholeSeconds))*1e9)) - - out.Type = telem.DataTypeSTRING - out.Str = lapTime.Format(LapTimeFormatStr) -} - -func EmptyTransform(v any, out *telem.TelemetryField) { - out.Type = telem.DataTypeCHAR - out.Raw = uint64('-') -} - -func PitSpeedLimiterTransform(v any, out *telem.TelemetryField) { - out.Type = telem.DataTypeSTRING - if v.(bool) { - out.Str = "PIT" - } else { - out.Str = " " - } -} - -func UInt8Transform(v any, out *telem.TelemetryField) { - out.Type = telem.DataTypeUINT8 - - if v == nil { - out.Raw = 0 - return - } - - out.Raw = uint64(v.(int)) -} - -func FloatToStringTransform(v any, out *telem.TelemetryField) { - out.Type = telem.DataTypeSTRING - - if v == nil { - out.Str = "inv" - return - } - - out.Str = strconv.FormatFloat(float64(v.(float32)), 'f', 1, 32) -} - -func FloatToUInt8Transform(v any, out *telem.TelemetryField) { - out.Type = telem.DataTypeUINT8 - - if v == nil { - out.Raw = uint64(0) - return - } - - out.Raw = uint64(v.(float32)) -} diff --git a/providers/iracing/iracing.go b/providers/iracing/iracing.go index 3162ce7..f623233 100644 --- a/providers/iracing/iracing.go +++ b/providers/iracing/iracing.go @@ -13,7 +13,7 @@ import ( "time" conv "esdi/conversions" - telem "esdi/telemetry" + "esdi/telemetry" "github.com/ESilva15/goirsdk" ) @@ -29,13 +29,13 @@ type IRacing struct { // Data Handling mut sync.Mutex - data *telem.TelemetryData + data *telemetry.TelemetryData // Timing information ticker *time.Ticker // ticker will keep polling intervals constant // Stream - streamCh chan telem.TelemetryData + streamCh chan telemetry.TelemetryData streamCancel context.CancelFunc } @@ -68,8 +68,8 @@ func NewIRacingProvider( return &IRacing{ logger: logger, SDK: sdk, - data: telem.NewTelemetryData(), - streamCh: make(chan telem.TelemetryData, 1), + 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), }, nil @@ -157,7 +157,7 @@ func (i *IRacing) readData() { // 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) { +func (i *IRacing) Stream() (<-chan telemetry.TelemetryData, error) { var ctx context.Context ctx, i.streamCancel = context.WithCancel(context.Background()) @@ -176,31 +176,31 @@ func (i *IRacing) StopStream() { i.streamCancel = nil } -func (i *IRacing) Subscribe(requestFields map[int16]telem.FieldID) { +func (i *IRacing) Subscribe(requestFields map[int16]telemetry.FieldID) { i.logger.Debug(fmt.Sprintf("Len Req: %d\n", len(requestFields))) - i.data.ActiveBinds = make([]telem.BoundField, 0, 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([]telem.FieldID, telem.MaxFields) + 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 telem.RPMStateColour: - i.data.VirtualBinds = append(i.data.VirtualBinds, telem.NewRPMLights()) - case telem.FCCurrentLap: + case telemetry.RPMStateColour: + i.data.VirtualBinds = append(i.data.VirtualBinds, telemetry.NewRPMLights()) + case telemetry.FCCurrentLap: i.data.VirtualBinds = append(i.data.VirtualBinds, - telem.NewFuelCalculator(i.logger.WithGroup("FUEL CALC"))) + telemetry.NewFuelCalculator(i.logger.WithGroup("FUEL CALC"))) default: // primitive telemetry field pendingBinds = append(pendingBinds, id) } } - boundCheck := make(map[telem.FieldID]bool) + 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 { @@ -217,20 +217,20 @@ func (i *IRacing) Subscribe(requestFields map[int16]telem.FieldID) { continue } - binding := telem.BoundField{ + binding := telemetry.BoundField{ Key: sdkKey, ID: id, } switch id { - case telem.Speed: - binding.Transform = func(v any, out *telem.TelemetryField) { - out.Type = telem.DataTypeUINT16 + case telemetry.Speed: + binding.Transform = func(v any, out *telemetry.TelemetryField) { + out.Type = telemetry.DataTypeUINT16 out.Raw = uint64(conv.MsToKph(v.(float32))) } - case telem.Gear: - binding.Transform = func(v any, out *telem.TelemetryField) { - out.Type = telem.DataTypeCHAR + case telemetry.Gear: + binding.Transform = func(v any, out *telemetry.TelemetryField) { + out.Type = telemetry.DataTypeCHAR // out.Raw = uint64(v.(int)) gear := 0 @@ -253,53 +253,53 @@ func (i *IRacing) Subscribe(requestFields map[int16]telem.FieldID) { out.Raw = uint64('?') // Fallback } } - case telem.RPM: - binding.Transform = func(v any, out *telem.TelemetryField) { - out.Type = telem.DataTypeUINT16 + case telemetry.RPM: + binding.Transform = func(v any, out *telemetry.TelemetryField) { + out.Type = telemetry.DataTypeUINT16 out.Raw = uint64(uint16(v.(float32))) } - case telem.FuelLevel: - binding.Transform = FloatToStringTransform + case telemetry.FuelLevel: + binding.Transform = telemetry.FloatToStringTransform // Engine Data - case telem.OilPress: - binding.Transform = FloatToStringTransform - case telem.OilTemp: - binding.Transform = FloatToStringTransform - case telem.WaterTemp: - binding.Transform = FloatToStringTransform + case telemetry.OilPress: + binding.Transform = telemetry.FloatToStringTransform + case telemetry.OilTemp: + binding.Transform = telemetry.FloatToStringTransform + case telemetry.WaterTemp: + binding.Transform = telemetry.FloatToStringTransform // Something else - case telem.PitSpeedLimiter: + case telemetry.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 + case telemetry.BrakeBias: + binding.Transform = telemetry.FloatToStringTransform + case telemetry.ABSSetting: + binding.Transform = telemetry.FloatToUInt8Transform + case telemetry.TCSetting: + binding.Transform = telemetry.FloatToUInt8Transform + case telemetry.ThrottleSetting: + binding.Transform = telemetry.FloatToUInt8Transform + 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 telem.SessionTime: - binding.Transform = func(v any, out *telem.TelemetryField) { - out.Type = telem.DataTypeSTRING + 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 telem.ReplaySessionTime: - binding.Transform = func(v any, out *telem.TelemetryField) { - out.Type = telem.DataTypeSTRING + 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 telem.Empty: - binding.Transform = EmptyTransform - case telem.LapLastLapTime: + case telemetry.Empty: + binding.Transform = telemetry.EmptyTransform + case telemetry.LapLastLapTime: binding.Transform = LapTimeTransform - case telem.LapNumber: - binding.Transform = UInt8Transform + case telemetry.LapNumber: + binding.Transform = telemetry.UInt8Transform } i.data.ActiveBinds = append(i.data.ActiveBinds, binding) diff --git a/providers/iracing/transforms.go b/providers/iracing/transforms.go new file mode 100644 index 0000000..e3e64a4 --- /dev/null +++ b/providers/iracing/transforms.go @@ -0,0 +1,34 @@ +package iracing + +import ( + "time" + + "esdi/telemetry" +) + +const ( + LapTimeFormatStr = "04:05.000" +) + +func LapTimeTransform(v any, out *telemetry.TelemetryField) { + lapTimeInSeconds := v.(float32) + + if lapTimeInSeconds < 0 { + lapTimeInSeconds = 0 + } + + wholeSeconds := int64(lapTimeInSeconds) + lapTime := time.Unix(wholeSeconds, int64((lapTimeInSeconds-float32(wholeSeconds))*1e9)) + + out.Type = telemetry.DataTypeSTRING + out.Str = lapTime.Format(LapTimeFormatStr) +} + +func PitSpeedLimiterTransform(v any, out *telemetry.TelemetryField) { + out.Type = telemetry.DataTypeSTRING + if v.(bool) { + out.Str = "PIT" + } else { + out.Str = " " + } +} diff --git a/providers/providers.go b/providers/providers.go index 2c4d704..1fca98c 100644 --- a/providers/providers.go +++ b/providers/providers.go @@ -32,3 +32,8 @@ func NewIRacingProvider(logger *slog.Logger, source string, return provider } + +func NewBeamNGProvider(ip string, port int) telemetry.TelemetryProvider { + provider, _ := beamng.NewBeamNGProvider(ip, port) + return provider +} diff --git a/services/telemetry.go b/services/telemetry.go index 56c901b..668a8ac 100644 --- a/services/telemetry.go +++ b/services/telemetry.go @@ -19,7 +19,6 @@ type TelemetryService struct { ativeProvider telem.TelemetryProvider // Channel for the UI listeners map[string]chan telem.TelemetryData - uiOutCh chan telem.TelemetryData cancelForward context.CancelFunc } @@ -27,22 +26,19 @@ func NewTelemetryService(logger *slog.Logger, cdash *CDashService) *TelemetrySer newService := &TelemetryService{ logger: logger, cdash: cdash, - uiOutCh: make(chan telem.TelemetryData, 5), listeners: make(map[string]chan telem.TelemetryData), } // Need to instantiate a default provider here - source := "/home/esilva/Desktop/projetos/simracing_peripherals/testTelemetry/gt3_mustang_bathurst.ibt" - firstProvider := providers.NewIRacingProvider(slog.Default(), source, "", "") + // source := "/home/esilva/Desktop/projetos/simracing_peripherals/testTelemetry/gt3_mustang_bathurst.ibt" + // firstProvider := providers.NewIRacingProvider(slog.Default(), source, "", "") + firstProvider := providers.NewBeamNGProvider("127.0.0.1", 4443) + newService.SwitchProvider(firstProvider) return newService } -// func (t *TelemetryService) GetUIStream() <-chan telem.TelemetryData { -// return t.uiOutCh -// } - func (t *TelemetryService) SwitchProvider(newProvider telem.TelemetryProvider) error { t.mut.Lock() defer t.mut.Unlock() diff --git a/telemetry/commonTransforms.go b/telemetry/commonTransforms.go new file mode 100644 index 0000000..7580bd6 --- /dev/null +++ b/telemetry/commonTransforms.go @@ -0,0 +1,41 @@ +package telemetry + +import "strconv" + +func EmptyTransform(v any, out *TelemetryField) { + out.Type = DataTypeCHAR + out.Raw = uint64('-') +} + +func UInt8Transform(v any, out *TelemetryField) { + out.Type = DataTypeUINT8 + + if v == nil { + out.Raw = 0 + return + } + + out.Raw = uint64(v.(int)) +} + +func FloatToStringTransform(v any, out *TelemetryField) { + out.Type = DataTypeSTRING + + if v == nil { + out.Str = "inv" + return + } + + out.Str = strconv.FormatFloat(float64(v.(float32)), 'f', 1, 32) +} + +func FloatToUInt8Transform(v any, out *TelemetryField) { + out.Type = DataTypeUINT8 + + if v == nil { + out.Raw = uint64(0) + return + } + + out.Raw = uint64(v.(float32)) +} diff --git a/tui/internal/controllers/layout.go b/tui/internal/controllers/layout.go index f9946f0..12df4ea 100644 --- a/tui/internal/controllers/layout.go +++ b/tui/internal/controllers/layout.go @@ -31,7 +31,7 @@ func NewLayoutController(base *Controller, service *services.CDashService) *Layo Messages: make(chan string, 10), DevService: service, MoveToolState: &windowManipState{Mode: moveMode}, - SelectedLayout: "layout.yaml", + SelectedLayout: "beamng.yaml", } lc.registerHooks() diff --git a/tui/internal/controllers/streaming.go b/tui/internal/controllers/streaming.go index e089891..a743de3 100644 --- a/tui/internal/controllers/streaming.go +++ b/tui/internal/controllers/streaming.go @@ -35,7 +35,7 @@ func NewStreamingCtrl( serCDash *services.CDashService, serTelem *services.TelemetryService, ) *StreamingCtrl { - // NOTE: looks sus + // NOTE: looks sus, put this somewhere also. Not very good in here providerList := []providers.Provider{} for _, item := range providers.Providers { providerList = append(providerList, item)