diff --git a/cdashdisplay/display.go b/cdashdisplay/display.go index 6bbd279..9cce39c 100644 --- a/cdashdisplay/display.go +++ b/cdashdisplay/display.go @@ -13,6 +13,7 @@ import ( "esdi/peripheral/communication" "esdi/peripheral/communication/packets" "esdi/peripheral/types" + "esdi/telemetry" "gopkg.in/yaml.v3" ) @@ -33,7 +34,8 @@ const ( destroyWindowCMDID types.Command = 4 updateWindowDimsCMDID types.Command = 5 updateWindowCMDID types.Command = 6 // Change this to a move cmd instead - newLayoutCMDID types.Command = 7 + sendDataCMDID types.Command = 7 + newLayoutCMDID types.Command = 8 ) const ( @@ -336,3 +338,31 @@ func (d *CDashDisplay) LoadLayout(layoutName string) error { return nil } + +func (d *CDashDisplay) SendData(data *telemetry.TelemetryData) { + packet := data.Pack() + + bytes, err := helper.StructToBytes(packet) + if err != nil { + return + } + + curStr := "" + byteCount := 0 + for _, byte := range bytes { + byteCount++ + curStr += fmt.Sprintf("%02x ", byte) + + if byteCount == 8 { + pLogger.Debug(curStr) + curStr = "" + byteCount = 0 + } + } + + // var ack packets.AckPacket + err = d.WT.SendCommand(sendDataCMDID, bytes, nil) + if err != nil && err != io.EOF { + return + } +} diff --git a/providers/iracing/fieldMapping.go b/providers/iracing/fieldMapping.go index 4486ab4..c899f85 100644 --- a/providers/iracing/fieldMapping.go +++ b/providers/iracing/fieldMapping.go @@ -4,20 +4,6 @@ import ( "esdi/telemetry" ) -type FieldMapper struct { - SDKKey string - DataType telemetry.DataType - Transform func(any) uint64 -} - -// NOTE: Update the iracing SDK to write data to the same map ALWAYS, then -// I can bind that address and read directly from there on the transform -type boundField struct { - Key string - ID telemetry.FieldID - Transform func(any, *telemetry.TelemetryField) -} - var internalToSDKFieldNames = map[telemetry.FieldID]string{ telemetry.Speed: "Speed", telemetry.Gear: "Gear", diff --git a/providers/iracing/iracing.go b/providers/iracing/iracing.go index a4489ee..5424e83 100644 --- a/providers/iracing/iracing.go +++ b/providers/iracing/iracing.go @@ -21,9 +21,8 @@ type IRacing struct { SDK *goirsdk.IBT // Data Handling - mut sync.Mutex - data *telem.TelemetryData - activeBindings []boundField + mut sync.Mutex + data *telem.TelemetryData // Timing information ticker *time.Ticker // ticker will keep polling intervals constant @@ -98,7 +97,7 @@ func (i *IRacing) readData() { } // Read binded data - for _, b := range i.activeBindings { + for _, b := range i.data.ActiveBinds { v := i.SDK.Vars.Vars[b.Key].Value // NOTE: for the love of god, find a way of avoiding this shit b.Transform(v, &i.data.Values[b.ID]) @@ -125,7 +124,7 @@ func (i *IRacing) Stream() (<-chan telem.TelemetryData, error) { func (i *IRacing) Subscribe(requestFields []telem.FieldID) { i.logger.Debug(fmt.Sprintf("Len Req: %d\n", len(requestFields))) - i.activeBindings = make([]boundField, 0, len(requestFields)) + i.data.ActiveBinds = make([]telem.BoundField, 0, len(requestFields)) for _, id := range requestFields { // Translate the UI FieldIDs to this provider's field names @@ -136,7 +135,7 @@ func (i *IRacing) Subscribe(requestFields []telem.FieldID) { continue } - binding := boundField{ + binding := telem.BoundField{ Key: sdkKey, ID: id, } @@ -159,8 +158,8 @@ func (i *IRacing) Subscribe(requestFields []telem.FieldID) { } } - i.activeBindings = append(i.activeBindings, binding) + i.data.ActiveBinds = append(i.data.ActiveBinds, binding) } - i.logger.Debug(fmt.Sprintf("Subscribed: %+v\n", i.activeBindings)) + i.logger.Debug(fmt.Sprintf("Subscribed: %+v\n", i.data.ActiveBinds)) } diff --git a/telemetry/data.go b/telemetry/data.go index 4988e49..62f8e4e 100644 --- a/telemetry/data.go +++ b/telemetry/data.go @@ -3,9 +3,31 @@ package telemetry import ( "math" "strconv" + "sync" "time" ) +type FieldMapper struct { + SDKKey string + DataType DataType + Transform func(any) uint64 +} + +// NOTE: Update the iracing SDK to write data to the same map ALWAYS, then +// I can bind that address and read directly from there on the transform +type BoundField struct { + Key string + ID FieldID + Transform func(any, *TelemetryField) +} + +var bufferPool = sync.Pool{ + New: func() any { + b := make([]byte, 0, MaxFields*8) + return &b + }, +} + type DataType uint8 const ( @@ -40,33 +62,33 @@ type TelemetryField struct { // or // 0x02 - str len max is 255 chars // [0x02] - str -func (tf *TelemetryField) Pack() []byte { +func (tf *TelemetryField) Pack(dest []byte) []byte { // NOTE: maybe we can have a pool of these so we don't have to create them here // or whatever - buf := make([]byte, 0, 8) - - buf = append(buf, uint8(tf.Type)) + dest = append(dest, uint8(tf.Type)) switch tf.Type { case DataTypeINT8, DataTypeUINT8: - buf = append(buf, uint8(tf.Raw)) + dest = append(dest, uint8(tf.Raw)) case DataTypeINT16, DataTypeUINT16: - buf = append(buf, uint8(tf.Raw), uint8(tf.Raw>>8)) + dest = append(dest, uint8(tf.Raw), uint8(tf.Raw>>8)) case DataTypeINT32, DataTypeUINT32: - buf = append(buf, uint8(tf.Raw), uint8(tf.Raw>>8), uint8(tf.Raw>>16)) + dest = append(dest, uint8(tf.Raw), uint8(tf.Raw>>8), uint8(tf.Raw>>16)) case DataTypeINT64, DataTypeUINT64: - buf = append(buf, uint8(tf.Raw), uint8(tf.Raw>>8), uint8(tf.Raw>>16), + dest = append(dest, uint8(tf.Raw), uint8(tf.Raw>>8), uint8(tf.Raw>>16), uint8(tf.Raw>>24), uint8(tf.Raw>>32), uint8(tf.Raw>>40), uint8(tf.Raw>>48), uint8(tf.Raw>>56), ) case DataTypeSTRING: l := min(len(tf.Str), math.MaxUint8) - buf = append(buf, uint8(l)) - buf = append(buf, tf.Str[:l]...) + dest = append(dest, uint8(l)) + dest = append(dest, tf.Str[:l]...) + case DataTypeCHAR: + dest = append(dest, uint8(tf.Raw)) } - return buf + return dest } func (tf *TelemetryField) String() string { @@ -134,6 +156,7 @@ func GetFieldID(name string) (FieldID, bool) { type TelemetryData struct { // Values map[string]*TelemetryField Values [MaxFields]TelemetryField + ActiveBinds []BoundField InitialTime time.Time PenultimateDataPoll time.Time LastDataPoll time.Time @@ -142,3 +165,20 @@ type TelemetryData struct { func NewTelemetryData() *TelemetryData { return &TelemetryData{} } + +func (td *TelemetryData) Pack() []byte { + bufPtr := bufferPool.Get().(*[]byte) + buf := (*bufPtr)[:0] + + for _, bind := range td.ActiveBinds { + buf = td.Values[bind.ID].Pack(buf) + } + + // We have to copy here because we have to return the buffer + result := make([]byte, len(buf)) + copy(result, buf) + + bufferPool.Put(&buf) + + return result +} diff --git a/telemetry/data_test.go b/telemetry/data_test.go index 07d1642..95993c1 100644 --- a/telemetry/data_test.go +++ b/telemetry/data_test.go @@ -55,13 +55,56 @@ func Test_TelemetryField(t *testing.T) { expect: []byte{0x08, 0x0E, 0x61, 0x20, 0x63, 0x6F, 0x6F, 0x6C, 0x20, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x21}, }, + { + name: "test_char", + tf: TelemetryField{ + Type: DataTypeCHAR, + Raw: uint64('R'), + }, + expect: []byte{0x09, 0x52}, + }, } + bufPtr := bufferPool.Get().(*[]byte) + buf := (*bufPtr)[:0] + for _, test := range tests { - result := test.tf.Pack() + result := test.tf.Pack(buf) if !bytes.Equal(result, test.expect) { t.Errorf("\nTest: %s\nExpected: %v\nGot: %v\n", test.name, test.expect, result) } } } + +func Test_TelemetryData(t *testing.T) { + data := TelemetryData{ + ActiveBinds: []BoundField{ + { + Key: "Speed", + ID: Speed, + }, + { + Key: "Gear", + ID: Gear, + }, + }, + } + + data.Values[Speed] = TelemetryField{ + Type: DataTypeUINT16, + Raw: uint64(254), + } + + data.Values[Gear] = TelemetryField{ + Type: DataTypeCHAR, + Raw: uint64('R'), + } + + expect := []byte{0x02, 0xFE, 0x00, 0x09, 0x52} + + result := data.Pack() + if !bytes.Equal(result, expect) { + t.Errorf("\nExpected: %v\nGot: %v\n", expect, result) + } +} diff --git a/tui/internal/services/cdashdisplay.go b/tui/internal/services/cdashdisplay.go index 3c5eb7a..5f50fcb 100644 --- a/tui/internal/services/cdashdisplay.go +++ b/tui/internal/services/cdashdisplay.go @@ -4,8 +4,10 @@ import ( "esdi/cdashdisplay" helper "esdi/helpers" "esdi/peripheral" + "esdi/telemetry" "fmt" "log/slog" + "sync/atomic" ) type CDashService struct { @@ -89,3 +91,21 @@ func (cds *CDashService) MoveWindow(idx int16, vec *helper.Vector) error { return nil } + +func (cds *CDashService) StreamData(stream <-chan telemetry.TelemetryData) { + var isSending atomic.Bool + + go func() { + + for msg := range stream { + if isSending.Load() { + continue + } + + isSending.Store(true) + + cds.CDash.SendData(&msg) + isSending.Store(false) + } + }() +} diff --git a/tui/internal/services/telemetry.go b/tui/internal/services/telemetry.go index 5fb7821..63b5c7b 100644 --- a/tui/internal/services/telemetry.go +++ b/tui/internal/services/telemetry.go @@ -11,12 +11,14 @@ import ( // It should hook to a data sink and handle it like iRacing, BeamNG, AC and so on type TelemetryService struct { logger *slog.Logger + cdash *CDashService ActiveProvider telemetry.TelemetryProvider } -func NewTelemetryService(logger *slog.Logger) *TelemetryService { +func NewTelemetryService(logger *slog.Logger, cdash *CDashService) *TelemetryService { return &TelemetryService{ logger: logger, + cdash: cdash, } } @@ -45,5 +47,9 @@ func (t *TelemetryService) SetProvider(provider string) *TelemetryService { func (t *TelemetryService) StartStream() <-chan telemetry.TelemetryData { stream, _ := t.ActiveProvider.Stream() + + // Somewhere around here we have to tell the device service to also send data + t.cdash.StreamData(stream) + return stream } diff --git a/tui/tui.go b/tui/tui.go index 4691f8d..3587cd1 100644 --- a/tui/tui.go +++ b/tui/tui.go @@ -22,7 +22,8 @@ func NewControlPanel(logger *slog.Logger) *ControlPanel { } devService := services.NewCDashService(logger) - telemService := services.NewTelemetryService(logger).SetProvider("iRacing") + telemService := services.NewTelemetryService(logger, devService). + SetProvider("iRacing") if telemService == nil { panic("failed to create the telemetry service") }