From da315b1c7b855e589bb2d920e85dddd078d692b8 Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Tue, 3 Mar 2026 23:06:19 +0000 Subject: [PATCH] So, we got the telemetry service going and it polls back to the UI --- tui/internal/controllers/streaming.go | 205 ++--------------- .../services/{device.go => cdashdisplay.go} | 31 ++- tui/internal/services/iracingTelemetry.go | 211 ++++++++++++++++++ tui/internal/services/services.go | 2 + 4 files changed, 258 insertions(+), 191 deletions(-) rename tui/internal/services/{device.go => cdashdisplay.go} (73%) create mode 100644 tui/internal/services/iracingTelemetry.go create mode 100644 tui/internal/services/services.go diff --git a/tui/internal/controllers/streaming.go b/tui/internal/controllers/streaming.go index 98c18da..7a019be 100644 --- a/tui/internal/controllers/streaming.go +++ b/tui/internal/controllers/streaming.go @@ -1,59 +1,32 @@ package controllers import ( - esdi "esdi/oldEsdi" "esdi/tui/internal/services" "esdi/tui/internal/views" - "fmt" - "log" - "os" - "strings" - "sync" - "time" - "github.com/ESilva15/goirsdk" "github.com/gdamore/tcell/v2" ) -// Car data lengths -const ( - SpeedLen = 5 - GearLen = 3 - RpmLen = 6 - BrakeBiasLen = 6 -) - type StreamingCtrl struct { *Controller - Service *services.CDashService - LastMessageTime time.Time - Data *esdi.SimulationData - DataView *esdi.DataPacket - InitialTime time.Time - LastTime time.Time - Mut sync.Mutex - Irsdk *goirsdk.IBT - StreamView *views.StreamView - Messages chan string - Internal chan string - Run bool - OnExit func() - isRunning bool + Service *services.CDashService + StreamView *views.StreamView + Messages chan string + Internal chan string + Run bool + OnExit func() + isRunning bool } func NewStreamingCtrl(base *Controller, ser *services.CDashService) *StreamingCtrl { ctrl := &StreamingCtrl{ - Controller: base, - Service: ser, - Data: &esdi.SimulationData{}, - DataView: &esdi.DataPacket{}, - InitialTime: time.Now(), - LastTime: time.Now(), - Messages: make(chan string, 10), - Internal: make(chan string, 10), - Run: false, - StreamView: views.NewStreamView(), - isRunning: false, + Controller: base, + Service: ser, + Messages: make(chan string, 10), + Internal: make(chan string, 10), + Run: false, + StreamView: views.NewStreamView(), + isRunning: false, } ctrl.registerHooks() @@ -82,156 +55,22 @@ func (sc *StreamingCtrl) registerHooks() { } func (sc *StreamingCtrl) Stop() { - sc.Run = false + sc.Service.StopStream() } func (sc *StreamingCtrl) Start() { - // Open the telemetry file - file, err := os.Open("/home/esilva/Desktop/projetos/simracing_peripherals/testTelemetry/supercars_indianapolis.ibt") - if err != nil { - log.Fatalf("Failed to open IBT file: %v", err) - } + sc.Service.StartStream() + sc.Messages <- "started stream\n" - sc.Irsdk, err = goirsdk.Init(file, "./out.ibt", "./out.yaml") - if err != nil { - log.Fatalf("Failed to load iRacing data") - } + stream := sc.Service.GetStream() + sc.Messages <- "got stream\n" - startTime := time.Now() - - go sc.ReadData() - - sc.Run = true - - // global irsdk is now here go func() { - ticker := time.NewTicker(time.Second / 60) - defer ticker.Stop() - - counter := 0 - for t := range ticker.C { - if !sc.Run { - break - } - // whatever - currentTime := time.Now() - - delta := currentTime.Sub(startTime) - - str := sc.Stringified() + fmt.Sprintf("%v - %7d\n%5f - %5f", - t.UTC(), counter, delta.Seconds(), delta.Seconds()/float64(counter)) - + for msg := range stream { + sc.Messages <- "received message\n" sc.App.QueueUpdateDraw(func() { - sc.StreamView.Update(str) + sc.StreamView.Update(msg) }) - - counter++ } }() } - -func (sc *StreamingCtrl) ReadData() { - dataReaderTicker := time.NewTicker(time.Second / 60) - defer dataReaderTicker.Stop() - - sc.InitialTime = time.Now() - for _ = range dataReaderTicker.C { - var err error - - sc.Mut.Lock() - _, err = sc.Irsdk.Update(time.Millisecond * 100) - if err != nil { - fmt.Printf("could not update data: %v", err) - continue - } - sc.Mut.Unlock() - - sc.getVehicleData() - - // Test the actual dataPacket we are sending over the wire - copyBytes(sc.DataView.Speed[:], SpeedLen, fmt.Sprintf("%3d", sc.Data.Speed)) - copyBytes(sc.DataView.Gear[:], GearLen, fmt.Sprintf("%2d", sc.Data.Gear)) - copyBytes(sc.DataView.RPM[:], RpmLen, fmt.Sprintf("%3d", sc.Data.RPM)) - - sc.LastMessageTime = time.Now() - } -} - -func (sc *StreamingCtrl) getVehicleData() { - sc.Mut.Lock() - curGear := sc.Irsdk.Vars.Vars["Gear"].Value - curRPM := sc.Irsdk.Vars.Vars["RPM"].Value - curSpeed := sc.Irsdk.Vars.Vars["Speed"].Value - - sc.Data.Gear = int32(curGear.(int)) - sc.Data.RPM = int32(curRPM.(float32)) - sc.Data.Speed = int32(msToKph(curSpeed.(float32))) - sc.Mut.Unlock() -} - -func copyBytes(dest []byte, destSize int, src string) { - copy(dest[:], []byte(src)) - dest[min(destSize-1, len(src))] = '\x00' -} - -func msToKph(v float32) int { - return int((3600 * v) / 1000) -} - -// Move this function to the views -func (sc *StreamingCtrl) Stringified() string { - var buffer strings.Builder - - sc.Mut.Lock() - sessionTimeR := sc.Irsdk.Vars.Vars["SessionTime"].Value - sessionTime := float64(sessionTimeR.(float64)) - - currTime := time.Now() - delta := currTime.Sub(sc.LastTime) - buffer.WriteString(fmt.Sprintf("[%s]\n", currTime.Format("2006/01/02 15:04:05.000"))) - buffer.WriteString(fmt.Sprintf("Delta: %d [%f]\n\n", delta.Milliseconds(), 1000.0/60.0)) - sc.LastTime = currTime - - elapsed := currTime.Sub(sc.InitialTime) - softwareElapsed := time.Unix(0, 0).Add(elapsed).Format("04:05.000") - sessionElapsed := time.Unix(0, 0). - Add(time.Duration(sessionTime * float64(time.Second))). - Format("04:05.000") - - buffer.WriteString(fmt.Sprintf("Elapsed (software): %s\n", - softwareElapsed)) - buffer.WriteString(fmt.Sprintf("Elapsed (session): %s\n\n", - sessionElapsed)) - - // buffer.WriteString("Car data:\n") - buffer.WriteString(fmt.Sprintf("Gear: %d, RPM: %d, Speed: %d\n\n", - sc.Data.Gear, sc.Data.RPM, sc.Data.Speed)) - - // buffer.WriteString("Fuel data:\n") - // buffer.WriteString(fmt.Sprintf("Fuel Est: %s\n\n", e.dataPacket.FuelEst)) - - // buffer.WriteString("Lap data:\n") - // buffer.WriteString(fmt.Sprintf("Delta: [%s] [%f] [%s]\n", e.dataPacket.DeltaToBestLap, - // e.data.LapDeltaFloat, lapTimeDeltaRepresentation(e.data.LapDeltaFloat))) - // buffer.WriteString(fmt.Sprintf("LapTime: %s\n", e.dataPacket.CurrLapTime)) - // buffer.WriteString(fmt.Sprintf("Best Lap Time: %s\n", e.dataPacket.BestLapTime)) - // buffer.WriteString(fmt.Sprintf("Last Lap Time: %s\n", e.dataPacket.LastLapTime)) - // buffer.WriteString(fmt.Sprintf("LapBestNLapTi: %f\n\n", e.data.LapBestNLapTime)) - - // buffer.WriteString("Position data:\n") - // buffer.WriteString(fmt.Sprintf("Pos: %d\n", e.dataPacket.Position)) - - // for p, v := range e.dataPacket.Standings { - // s := fmt.Sprintf("[%2d] %s %-16s %-16s\n", - // p+1, v.Lap, string(bytes.Trim(v.DriverName[:], "\x00")), v.TimeBehindString) - // buffer.WriteString(s) - // } - - // buffer.WriteString(fmt.Sprintf("Size: %v\n", binary.Size(DataPacket{}))) - // buffer.WriteString(fmt.Sprintf("Recv: %d\n", e.data.Recv)) - // buffer.WriteString(fmt.Sprintf("Recv Err: %v\n", e.data.ReadError)) - - sc.Mut.Unlock() - - return buffer.String() -} diff --git a/tui/internal/services/device.go b/tui/internal/services/cdashdisplay.go similarity index 73% rename from tui/internal/services/device.go rename to tui/internal/services/cdashdisplay.go index 137dd35..71889e2 100644 --- a/tui/internal/services/device.go +++ b/tui/internal/services/cdashdisplay.go @@ -9,18 +9,21 @@ import ( ) type CDashService struct { - Logger *slog.Logger - CDash *cdashdisplay.CDashDisplay - DevClerk *peripheral.PeripheralDeviceClerk - Messages chan string + Logger *slog.Logger + CDash *cdashdisplay.CDashDisplay + iRacingTelemetry *IRacingService + DevClerk *peripheral.PeripheralDeviceClerk + Messages chan string } func NewCDashService(logger *slog.Logger) *CDashService { + sharedChannel := make(chan string, 10) return &CDashService{ - Logger: logger, - CDash: nil, - DevClerk: peripheral.NewPeripheralDeviceClerk(), - Messages: make(chan string, 10), + Logger: logger, + CDash: nil, + DevClerk: peripheral.NewPeripheralDeviceClerk(), + Messages: sharedChannel, + iRacingTelemetry: NewIRacingService(sharedChannel), } } @@ -90,3 +93,15 @@ func (cds *CDashService) MoveWindow(win *models.UIWindow, vec *helper.Vector) er return nil } + +func (cds *CDashService) StartStream() { + cds.iRacingTelemetry.StartStream() +} + +func (cds *CDashService) GetStream() <-chan string { + return cds.iRacingTelemetry.GetStream() +} + +func (cds *CDashService) StopStream() { + cds.iRacingTelemetry.StopStream() +} diff --git a/tui/internal/services/iracingTelemetry.go b/tui/internal/services/iracingTelemetry.go new file mode 100644 index 0000000..0574188 --- /dev/null +++ b/tui/internal/services/iracingTelemetry.go @@ -0,0 +1,211 @@ +package services + +// NOTE: I have to think about how to implement this, for now I'm bruteforcing +// iracing support only. But I should have a generic service that can gather +// telemetry from multiples sims and just publish it in an internal format + +import ( + "context" + esdi "esdi/oldEsdi" + "fmt" + "log" + "os" + "strings" + "sync" + "time" + + "github.com/ESilva15/goirsdk" +) + +// Car data lengths +const ( + SpeedLen = 5 + GearLen = 3 + RpmLen = 6 + BrakeBiasLen = 6 +) + +type StreamState int + +const ( + StreamStatePaused StreamState = 0 + StreamStateRunning StreamState = 1 + StreamStateOff StreamState = 2 +) + +type IRacingService struct { + Message chan string + LastMessageTime time.Time + Data *esdi.SimulationData + DataView *esdi.DataPacket + InitialTime time.Time + LastTime time.Time + Mut sync.Mutex + ticker *time.Ticker + Irsdk *goirsdk.IBT + // + isRunning bool + Stream chan string + StreamCancel context.CancelFunc +} + +func NewIRacingService(msg chan string) *IRacingService { + // Open the telemetry file + file, err := os.Open("/home/esilva/Desktop/projetos/simracing_peripherals/testTelemetry/supercars_indianapolis.ibt") + if err != nil { + log.Fatalf("Failed to open IBT file: %v", err) + } + + irsdk, err := goirsdk.Init(file, "./out.ibt", "./out.yaml") + if err != nil { + log.Fatalf("Failed to load iRacing data") + } + + return &IRacingService{ + Message: msg, + ticker: time.NewTicker(time.Second / 60), + Irsdk: irsdk, + Stream: make(chan string, 10), + Data: &esdi.SimulationData{}, + DataView: &esdi.DataPacket{}, + isRunning: false, + } +} + +func (irs *IRacingService) GetStream() <-chan string { + return irs.Stream +} + +func (irs *IRacingService) StartStream() { + if irs.isRunning { + return + } + + var ctx context.Context + ctx, irs.StreamCancel = context.WithCancel(context.Background()) + + irs.startStream(ctx) + irs.isRunning = true +} + +func (irs *IRacingService) StopStream() { + if irs.StreamCancel != nil { + irs.StreamCancel() + } +} + +func (irs *IRacingService) startStream(ctx context.Context) { + irs.Message <- "STARTING THIS\n" + irs.InitialTime = time.Now() + go func() { + for { + select { + case <-ctx.Done(): + irs.Message <- "CONTEXT SAID WE ARE DONE\n" + return + case <-irs.ticker.C: + irs.Message <- "GOT A TICK\n" + irs.ReadData(ctx) + irs.Stream <- irs.Stringified() + } + irs.Message <- "we are we going???\n" + } + }() +} + +func (irs *IRacingService) ReadData(ctx context.Context) { + irs.Mut.Lock() + defer irs.Mut.Unlock() + + var err error + + _, err = irs.Irsdk.Update(time.Millisecond * 100) + if err != nil { + return + } + + irs.getVehicleData() + + // Test the actual dataPacket we are sending over the wire + copyBytes(irs.DataView.Speed[:], SpeedLen, fmt.Sprintf("%3d", irs.Data.Speed)) + copyBytes(irs.DataView.Gear[:], GearLen, fmt.Sprintf("%2d", irs.Data.Gear)) + copyBytes(irs.DataView.RPM[:], RpmLen, fmt.Sprintf("%3d", irs.Data.RPM)) + + irs.LastMessageTime = time.Now() +} + +func (irs *IRacingService) getVehicleData() { + curGear := irs.Irsdk.Vars.Vars["Gear"].Value + curRPM := irs.Irsdk.Vars.Vars["RPM"].Value + curSpeed := irs.Irsdk.Vars.Vars["Speed"].Value + + irs.Data.Gear = int32(curGear.(int)) + irs.Data.RPM = int32(curRPM.(float32)) + irs.Data.Speed = int32(msToKph(curSpeed.(float32))) +} + +func copyBytes(dest []byte, destSize int, src string) { + copy(dest[:], []byte(src)) + dest[min(destSize-1, len(src))] = '\x00' +} + +func msToKph(v float32) int { + return int((3600 * v) / 1000) +} + +func (irs *IRacingService) Stringified() string { + var buffer strings.Builder + + irs.Mut.Lock() + sessionTimeR := irs.Irsdk.Vars.Vars["SessionTime"].Value + sessionTime := float64(sessionTimeR.(float64)) + + currTime := time.Now() + delta := currTime.Sub(irs.LastTime) + buffer.WriteString(fmt.Sprintf("[%s]\n", currTime.Format("2006/01/02 15:04:05.000"))) + buffer.WriteString(fmt.Sprintf("Delta: %d [%f]\n\n", delta.Milliseconds(), 1000.0/60.0)) + irs.LastTime = currTime + + elapsed := currTime.Sub(irs.InitialTime) + softwareElapsed := time.Unix(0, 0).Add(elapsed).Format("04:05.000") + sessionElapsed := time.Unix(0, 0). + Add(time.Duration(sessionTime * float64(time.Second))). + Format("04:05.000") + + buffer.WriteString(fmt.Sprintf("Elapsed (software): %s\n", + softwareElapsed)) + buffer.WriteString(fmt.Sprintf("Elapsed (session): %s\n\n", + sessionElapsed)) + + // buffer.WriteString("Car data:\n") + buffer.WriteString(fmt.Sprintf("Gear: %d, RPM: %d, Speed: %d\n\n", + irs.Data.Gear, irs.Data.RPM, irs.Data.Speed)) + + // buffer.WriteString("Fuel data:\n") + // buffer.WriteString(fmt.Sprintf("Fuel Est: %s\n\n", e.dataPacket.FuelEst)) + + // buffer.WriteString("Lap data:\n") + // buffer.WriteString(fmt.Sprintf("Delta: [%s] [%f] [%s]\n", e.dataPacket.DeltaToBestLap, + // e.data.LapDeltaFloat, lapTimeDeltaRepresentation(e.data.LapDeltaFloat))) + // buffer.WriteString(fmt.Sprintf("LapTime: %s\n", e.dataPacket.CurrLapTime)) + // buffer.WriteString(fmt.Sprintf("Best Lap Time: %s\n", e.dataPacket.BestLapTime)) + // buffer.WriteString(fmt.Sprintf("Last Lap Time: %s\n", e.dataPacket.LastLapTime)) + // buffer.WriteString(fmt.Sprintf("LapBestNLapTi: %f\n\n", e.data.LapBestNLapTime)) + + // buffer.WriteString("Position data:\n") + // buffer.WriteString(fmt.Sprintf("Pos: %d\n", e.dataPacket.Position)) + + // for p, v := range e.dataPacket.Standings { + // s := fmt.Sprintf("[%2d] %s %-16s %-16s\n", + // p+1, v.Lap, string(bytes.Trim(v.DriverName[:], "\x00")), v.TimeBehindString) + // buffer.WriteString(s) + // } + + // buffer.WriteString(fmt.Sprintf("Size: %v\n", binary.Size(DataPacket{}))) + // buffer.WriteString(fmt.Sprintf("Recv: %d\n", e.data.Recv)) + // buffer.WriteString(fmt.Sprintf("Recv Err: %v\n", e.data.ReadError)) + + irs.Mut.Unlock() + + return buffer.String() +} diff --git a/tui/internal/services/services.go b/tui/internal/services/services.go new file mode 100644 index 0000000..bb02e66 --- /dev/null +++ b/tui/internal/services/services.go @@ -0,0 +1,2 @@ +// Package services interacts with the other libraries required for this UI +package services