33 Commits
Author SHA1 Message Date
esilva 66012b6904 Merge pull request 'remove old logger. We use actual loggers now' (#10) from remove-old-logger into master
Reviewed-on: #10
2026-09-17 23:00:17 +01:00
esilva a807eb3782 remove old logger. We use actual loggers now 2026-09-17 23:00:02 +01:00
esilva fe86993c6d update the irsdk version 2026-08-24 16:54:36 +01:00
esilva 5ae0542149 Another README edit 2026-07-23 10:47:27 +01:00
esilva 4088e4d8f8 Edited the README 2026-07-23 10:45:25 +01:00
esilva ce7aabfa09 Instead of panicing on unset updaters, set them as unused
This might have impact on different types of data other than a window
set for STRING, but I will worry about that later on
2026-07-08 09:47:45 +01:00
esilva ca57cbd9f0 Small validation for the updaters array in BeamNG 2026-07-07 18:16:52 +01:00
esilva 0887fbfd75 simplified a bit how the TelemetryField updaters work
- now we only use one function to update the TelemetryField instead
of having to fetch data and then update it.

Still need to update the iRacing provider to use this pattern. Way
better and creates better units.

Got rid of that stupid long ass switch statement too
2026-07-07 17:52:47 +01:00
esilva 94ba0e2539 window title reporting 2026-07-07 00:06:59 +01:00
esilva c431c389e3 added a TODO so I won't forget it 2026-07-06 23:47:12 +01:00
esilva 16e6a336b0 Added fields for the BeamNG provider and am working on some changes for the telem
I need to simplify the way telemetry is fetched and updated on the app
side.
Reduce the use of any for example and so on
2026-07-06 23:31:41 +01:00
esilva 8def8cac68 Bumped the BeamNG SDK version 2026-07-06 23:30:40 +01:00
esilva 4e91edc036 small change to the gear transform for BeamNG 2026-07-06 14:43:45 +01:00
esilva abc60a81d3 added an important TODO 2026-06-26 17:47:38 +01:00
esilva 20dbf15adf Support for BeamNG as a data provider 2026-06-25 15:35:57 +01:00
esilva 6dd4db0358 pprof stuff for debugging 2026-06-24 17:05:41 +01:00
esilva 2ae29af199 reducing the channel sizes so data doesn't pile up and frames are dropped instead 2026-06-24 16:05:03 +01:00
esilva 5b77b5b941 broadcaster setup 2026-06-24 12:00:02 +01:00
esilva 9c7c6d528f moved the service layer to its own package 2026-06-23 17:54:01 +01:00
esilva 2146dc8396 adding the updateStream method and the models for the form 2026-06-23 17:47:10 +01:00
esilva a28f996330 start and stop behaviour 2026-06-23 10:23:14 +01:00
esilva 1614675e9d Added the default sim config variable 2026-06-23 10:02:17 +01:00
esilva 0f3a6ceeba update beamng.drives provider name 2026-06-23 10:01:47 +01:00
esilva aa3fa88044 init application routines 2026-06-22 22:07:52 +01:00
esilva 5d8552e9a2 add the metrics server flag to the config for now 2026-06-22 22:07:43 +01:00
esilva ac187c9292 adding a very basig config 2026-06-22 21:37:16 +01:00
esilva f2e49f1ba0 removing some unused code 2026-06-19 16:29:27 +01:00
esilva a980640e40 working on a global config 2026-06-18 17:05:42 +01:00
esilva 3e92467beb adding the options form for the stream view 2026-06-18 17:05:27 +01:00
esilva 74d57a6ece preparing the providers package 2026-06-18 17:05:08 +01:00
esilva 24fea285c3 very bare bones layout selectable menu 2026-06-09 16:39:13 +01:00
esilva d3c4464aa0 organizing the code a bit better 2026-06-09 14:51:11 +01:00
esilva 99a4f794c5 removing unused code 2026-06-09 14:42:58 +01:00
43 changed files with 1636 additions and 532 deletions
+41 -11
View File
@@ -21,27 +21,57 @@ or to view the live data:
Games implemented so far: Games implemented so far:
- [iRacing](https://www.iracing.com/) using the [goirsdk](https://github.com/ESilva15/goirsdk) - [iRacing](https://www.iracing.com/) using the [goirsdk](https://github.com/ESilva15/goirsdk)
Games being implemented: <!-- Games being implemented: -->
- [BeamNG.drive](https://www.beamng.com/game/) using the [gobngsdk](https://github.com/ESilva15/gobngsdk) <!-- - [BeamNG.drive](https://www.beamng.com/game/) using the [gobngsdk](https://github.com/ESilva15/gobngsdk) -->
<!---->
Games to be implemented: <!-- Games to be implemented: -->
- [Assetto Corsa](https://assettocorsa.gg/) <!-- - [Assetto Corsa](https://assettocorsa.gg/) -->
## Roadmap ## 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 - [ ] Configuration of the peripherals via ESDI
- [ ] Detection of the display - [ ] Detection of the display
- [ ] Fuel Calculator
- [ ] LapTime Calculator - [ ] LapTime Calculator
- [ ] A very long list useful stuff like flags, position, more info about - [ ] A very long list useful stuff like flags, position, more info about
other drivers, track conditions and so on so forth other drivers, track conditions and so on so forth
- [ ] Dynamic data packets
- [ ] More roadmap entries
- [ ] Telemetry analysis tool - [ ] Telemetry analysis tool
- [ ] Better user interface - [ ] Better user interface
### Shameless begging
## 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 <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`
- Type `continue` onto Terminal2 and go to Terminal1 to use the application
normally until it hangs.
- Go back to Terminal2 to and do a `Ctrl+c` to capture the state and then check
what went wrong by looking at the `goroutines` for example.
pprof:
- `go tool pprof http://localhost:8001/debug/pprof/mutex`
- type `top` to view the summary
- type `web` to view a graph version
## Shameless begging
Hey, doesn't hurt to try, its free either way: Hey, doesn't hurt to try, its free either way:
[Buy me a coffee!](buymeacoffee.com/ESilva_15) [Buy me a coffee!](buymeacoffee.com/ESilva_15)
+23 -1
View File
@@ -8,6 +8,7 @@ import (
"log/slog" "log/slog"
"os" "os"
"path" "path"
"time"
helper "esdi/helpers" helper "esdi/helpers"
"esdi/peripheral/communication" "esdi/peripheral/communication"
@@ -298,7 +299,7 @@ func (d *CDashDisplay) ResizeWindow(wID int16, delta *helper.Vector) error {
func (d *CDashDisplay) SaveLayout(outputPath string) error { func (d *CDashDisplay) SaveLayout(outputPath string) error {
file, err := os.OpenFile(path.Join(layoutsDir, outputPath), file, err := os.OpenFile(path.Join(layoutsDir, outputPath),
os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755) os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o755)
if err != nil { if err != nil {
return err return err
} }
@@ -339,6 +340,27 @@ func (d *CDashDisplay) LoadLayout(layoutName string) error {
return nil return nil
} }
func (d *CDashDisplay) UnloadLayout() error {
var err error
for _, w := range d.State.Layout.Windows {
pLogger.Debug(fmt.Sprintf("= Removing %d ==============================================",
w.UIData.IDX))
err = d.DestroyWindow(w.UIData.IDX)
time.Sleep(75 * time.Millisecond)
if err != nil {
pLogger.Error(fmt.Sprintf("failed to destroy window: %+v", err))
// NOTE: Add a way to handle multiple errors ?
return err
}
pLogger.Debug(fmt.Sprintf("= Removing %d ==============================================",
w.UIData.IDX))
}
return nil
}
func (d *CDashDisplay) SendData(data *telemetry.TelemetryData) { func (d *CDashDisplay) SendData(data *telemetry.TelemetryData) {
packet := data.Pack() packet := data.Pack()
+1 -6
View File
@@ -2,8 +2,6 @@
package cmd package cmd
import ( import (
"context"
"log/slog"
"os" "os"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@@ -20,10 +18,7 @@ type loggerKey struct{}
// Execute adds all child commands to the root command and sets flags appropriately. // Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd. // This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute(log *slog.Logger) { func Execute() {
ctx := context.WithValue(context.Background(), loggerKey{}, log)
rootCmd.SetContext(ctx)
err := rootCmd.Execute() err := rootCmd.Execute()
if err != nil { if err != nil {
os.Exit(1) os.Exit(1)
+3 -8
View File
@@ -1,21 +1,16 @@
package cmd package cmd
import ( import (
"esdi/tui"
"fmt" "fmt"
"log/slog" "log/slog"
"esdi/tui"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
func tuiCmdAction(cmd *cobra.Command, args []string) { func tuiCmdAction(cmd *cobra.Command, args []string) {
logger, ok := cmd.Context().Value(loggerKey{}).(*slog.Logger) err := tui.Run(slog.Default())
if !ok {
fmt.Printf("Error loading logger from context: %s\n", logger)
return
}
err := tui.Run(logger)
if err != nil { if err != nil {
fmt.Printf("Error running TUI: %s\n", err.Error()) fmt.Printf("Error running TUI: %s\n", err.Error())
return return
+51
View File
@@ -0,0 +1,51 @@
// Package config is responsible for the configuration of the application
package config
// NOTE: actually make a package out of this if possible - lets try
import (
"os"
"gopkg.in/yaml.v3"
)
var instance *ESDICfg
type ESDICfg struct {
DefaultSim string `yaml:"default_sim"`
DefaultLayout string `yaml:"default_layout"`
MetricsServer bool `yaml:"metrics_server"`
}
func (cfg *ESDICfg) loadConfiguration(path string) error {
file, err := os.ReadFile(path)
if err != nil {
return err
}
err = yaml.Unmarshal(file, &instance)
if err != nil {
return err
}
return nil
}
func Setup(path string) error {
instance = &ESDICfg{}
err := instance.loadConfiguration(path)
if err != nil {
return err
}
return nil
}
func GetCfg() *ESDICfg {
if instance == nil {
panic("configuration instance wasn't initialized")
}
return instance
}
+3
View File
@@ -0,0 +1,3 @@
default_sim: "iRacing"
default_layout: "layout.yaml"
metrics_server: true
+2 -2
View File
@@ -4,8 +4,8 @@ go 1.25.5
require ( require (
github.com/ESilva15/ESgoRepl v0.1.0 github.com/ESilva15/ESgoRepl v0.1.0
github.com/ESilva15/gobngsdk v0.0.2 github.com/ESilva15/gobngsdk v1.1.3
github.com/ESilva15/goirsdk v0.2.11 github.com/ESilva15/goirsdk v0.3.0
github.com/arl/statsviz v0.8.0 github.com/arl/statsviz v0.8.0
github.com/gdamore/tcell/v2 v2.8.1 github.com/gdamore/tcell/v2 v2.8.1
github.com/rivo/tview v0.42.0 github.com/rivo/tview v0.42.0
+4 -4
View File
@@ -1,9 +1,9 @@
github.com/ESilva15/ESgoRepl v0.1.0 h1:hOVcRBfdP8Yw1kLr2tFoM3SetV7vxFswKjzfAtq8UgM= github.com/ESilva15/ESgoRepl v0.1.0 h1:hOVcRBfdP8Yw1kLr2tFoM3SetV7vxFswKjzfAtq8UgM=
github.com/ESilva15/ESgoRepl v0.1.0/go.mod h1:O2JQMyEgHy0zf9UvzvLNvp2w95T2uazc3torrzRmG9Y= github.com/ESilva15/ESgoRepl v0.1.0/go.mod h1:O2JQMyEgHy0zf9UvzvLNvp2w95T2uazc3torrzRmG9Y=
github.com/ESilva15/gobngsdk v0.0.2 h1:N6stNOE14Wg80BAZMFu/Qd9Qa/J0DmExCfdPoDf7lco= github.com/ESilva15/gobngsdk v1.1.3 h1:CFaz2KjHKnBLrQuEN1QHOd1761cWM/rmTwLpSLrgbe4=
github.com/ESilva15/gobngsdk v0.0.2/go.mod h1:cKLaZRgM0tGGXDvosaSjHnxeyC5Y6rg7zCLqEUJnOzw= github.com/ESilva15/gobngsdk v1.1.3/go.mod h1:cKLaZRgM0tGGXDvosaSjHnxeyC5Y6rg7zCLqEUJnOzw=
github.com/ESilva15/goirsdk v0.2.11 h1:C/HuO9xGmRJ01vMR0KRjrhcMEfq1Mea1rQK0CICaOU4= github.com/ESilva15/goirsdk v0.3.0 h1:6D95Avq7chikGHwEUKh7Y/HaLHOWQ75Qxaz8oMDSfrw=
github.com/ESilva15/goirsdk v0.2.11/go.mod h1:5borQbw+L4fe9b58JFHRHZwrzz0sMVAteuYbRnFOc0Q= github.com/ESilva15/goirsdk v0.3.0/go.mod h1:5borQbw+L4fe9b58JFHRHZwrzz0sMVAteuYbRnFOc0Q=
github.com/arl/statsviz v0.8.0 h1:O6GjjVxEDxcByAucOSl29HaGYLXsuwA3ujJw8H9E7/U= github.com/arl/statsviz v0.8.0 h1:O6GjjVxEDxcByAucOSl29HaGYLXsuwA3ujJw8H9E7/U=
github.com/arl/statsviz v0.8.0/go.mod h1:XlrbiT7xYT03xaW9JMMfD8KFUhBOESJwfyNJu83PbB0= github.com/arl/statsviz v0.8.0/go.mod h1:XlrbiT7xYT03xaW9JMMfD8KFUhBOESJwfyNJu83PbB0=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+337
View File
@@ -0,0 +1,337 @@
Windows:
1:
uiwindow:
Dims:
X0: 170
Y0: 136
Width: 40
Height: 55
Decor:
BGColour: 4161
FGColour: 65535
TitleColour: 65535
BorderColour: 63488
TitleSize: 2
TextSize: 4
HasBorder: 1
Padding: 0
Opts:
ShowID: 0
WinType: 2
PreviewValue: '>'
Title: ->
uidata:
WID: 1
TelemetryField: Right Indicator
2:
uiwindow:
Dims:
X0: 341
Y0: 417
Width: 40
Height: 55
Decor:
BGColour: 4161
FGColour: 65535
TitleColour: 65535
BorderColour: 63488
TitleSize: 2
TextSize: 4
HasBorder: 1
Padding: 0
Opts:
ShowID: 0
WinType: 2
PreviewValue: A
Title: ABS
uidata:
WID: 2
TelemetryField: ABS Dash Light
3:
uiwindow:
Dims:
X0: 470
Y0: 71
Width: 322
Height: 57
Decor:
BGColour: 4161
FGColour: 65535
TitleColour: 65535
BorderColour: 63488
TitleSize: 2
TextSize: 4
HasBorder: 1
Padding: 0
Opts:
ShowID: 0
WinType: 2
PreviewValue: "9999"
Title: Dig Tacho
uidata:
WID: 3
TelemetryField: RPM
4:
uiwindow:
Dims:
X0: 321
Y0: 73
Width: 134
Height: 111
Decor:
BGColour: 4161
FGColour: 65535
TitleColour: 65535
BorderColour: 63488
TitleSize: 2
TextSize: 12
HasBorder: 1
Padding: 0
Opts:
ShowID: 0
WinType: 2
PreviewValue: R
Title: Box
uidata:
WID: 4
TelemetryField: Gear
5:
uiwindow:
Dims:
X0: 80
Y0: 136
Width: 40
Height: 55
Decor:
BGColour: 4161
FGColour: 65535
TitleColour: 65535
BorderColour: 63488
TitleSize: 2
TextSize: 4
HasBorder: 1
Padding: 0
Opts:
ShowID: 0
WinType: 2
PreviewValue: <
Title: <-
uidata:
WID: 5
TelemetryField: Left Indicator
6:
uiwindow:
Dims:
X0: 694
Y0: 367
Width: 100
Height: 50
Decor:
BGColour: 4161
FGColour: 65535
TitleColour: 65535
BorderColour: 63488
TitleSize: 2
TextSize: 3
HasBorder: 1
Padding: 0
Opts:
ShowID: 0
WinType: 2
PreviewValue: "101.4"
Title: Oil T
uidata:
WID: 6
TelemetryField: Oil Temperature
7:
uiwindow:
Dims:
X0: 386
Y0: 417
Width: 40
Height: 55
Decor:
BGColour: 4161
FGColour: 65535
TitleColour: 65535
BorderColour: 63488
TitleSize: 2
TextSize: 4
HasBorder: 1
Padding: 0
Opts:
ShowID: 0
WinType: 2
PreviewValue: T
Title: TC
uidata:
WID: 7
TelemetryField: Traction Control Light
8:
uiwindow:
Dims:
X0: 431
Y0: 417
Width: 40
Height: 55
Decor:
BGColour: 4161
FGColour: 65535
TitleColour: 65535
BorderColour: 63488
TitleSize: 2
TextSize: 4
HasBorder: 1
Padding: 0
Opts:
ShowID: 0
WinType: 2
PreviewValue: B
Title: Bat
uidata:
WID: 8
TelemetryField: Battery Light
9:
uiwindow:
Dims:
X0: 321
Y0: 195
Width: 134
Height: 80
Decor:
BGColour: 4161
FGColour: 65535
TitleColour: 65535
BorderColour: 63488
TitleSize: 2
TextSize: 7
HasBorder: 1
Padding: 0
Opts:
ShowID: 0
WinType: 2
PreviewValue: "234"
Title: SPEEDO
uidata:
WID: 9
TelemetryField: Speed
10:
uiwindow:
Dims:
X0: 590
Y0: 367
Width: 100
Height: 50
Decor:
BGColour: 4161
FGColour: 65535
TitleColour: 65535
BorderColour: 63488
TitleSize: 2
TextSize: 3
HasBorder: 1
Padding: 0
Opts:
ShowID: 0
WinType: 2
PreviewValue: "101.5"
Title: Water T
uidata:
WID: 10
TelemetryField: Water Temperature
11:
uiwindow:
Dims:
X0: 470
Y0: 132
Width: 169
Height: 50
Decor:
BGColour: 4161
FGColour: 65535
TitleColour: 65535
BorderColour: 63488
TitleSize: 2
TextSize: 3
HasBorder: 1
Padding: 0
Opts:
ShowID: 0
WinType: 2
PreviewValue: "123.4"
Title: Fuel Level
uidata:
WID: 11
TelemetryField: Fuel Level
12:
uiwindow:
Dims:
X0: 694
Y0: 423
Width: 100
Height: 50
Decor:
BGColour: 4161
FGColour: 65535
TitleColour: 65535
BorderColour: 63488
TitleSize: 2
TextSize: 3
HasBorder: 1
Padding: 0
Opts:
ShowID: 0
WinType: 2
PreviewValue: "4.56"
Title: Oil P
uidata:
WID: 12
TelemetryField: Oil Pressure
13:
uiwindow:
Dims:
X0: 296
Y0: 417
Width: 40
Height: 55
Decor:
BGColour: 4161
FGColour: 65535
TitleColour: 65535
BorderColour: 63488
TitleSize: 2
TextSize: 4
HasBorder: 1
Padding: 0
Opts:
ShowID: 0
WinType: 2
PreviewValue: P
Title: HB
uidata:
WID: 13
TelemetryField: Parking Brake Dash Light
14:
uiwindow:
Dims:
X0: 142
Y0: 19
Width: 500
Height: 20
Decor:
BGColour: 4161
FGColour: 65535
TitleColour: 65535
BorderColour: 63488
TitleSize: 1
TextSize: 2
HasBorder: 1
Padding: 0
Opts:
ShowID: 0
WinType: 1
PreviewValue: "5678"
Title: TACHO
uidata:
WID: 14
TelemetryField: RPM
-22
View File
@@ -1,22 +0,0 @@
package logger
import (
"log"
"os"
"sync"
)
var l *log.Logger
var once sync.Once
func createLogger() {
l = log.New(os.Stdout, "[esdi] ", log.LstdFlags | log.Lshortfile)
}
func GetInstance() *log.Logger {
once.Do(func() {
createLogger()
})
return l
}
+66 -8
View File
@@ -1,39 +1,97 @@
package main package main
import ( import (
"fmt"
"log/slog" "log/slog"
"net/http" "net/http"
"net/http/pprof"
"os" "os"
"runtime"
"esdi/cmd" "esdi/cmd"
"esdi/config"
"esdi/telemetry" "esdi/telemetry"
"github.com/arl/statsviz" "github.com/arl/statsviz"
) )
func main() { // NOTE: in go we have the init() function. Its a function that runs before
output, err := os.OpenFile("./output.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0755) // everything else in a package. Make use of that
func initApplication() {
fmt.Fprint(os.Stdout, "\x1b]0;ESDI\x07")
// 1. This is the first initialization setup we do so we can log
err := setupLogger()
if err != nil { if err != nil {
panic("failed to open logging file: " + err.Error()) panic("failed to open logging file: " + err.Error())
} }
// 2. This is the second thing we setup because we need some stuff to run
// other things
err = config.Setup("./config/config.yaml")
if err != nil {
panic("failed to setup config: " + err.Error())
}
// Performance analysis
if config.GetCfg().MetricsServer {
slog.Info("Setting up metrics server...")
err = runStatviz()
if err != nil {
slog.Error(fmt.Sprintf("failed to setup metrics server: %+v", err))
}
}
// Setting up some internal data structures
telemetry.Init()
}
func setupLogger() error {
output, err := os.OpenFile("./output.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o755)
if err != nil {
return err
}
logger := slog.New( logger := slog.New(
slog.NewTextHandler(output, &slog.HandlerOptions{ slog.NewTextHandler(output, &slog.HandlerOptions{
Level: slog.LevelDebug, Level: slog.LevelDebug,
}), }),
) )
// Performance analysis slog.SetDefault(logger)
return nil
}
func runStatviz() error {
mux := http.NewServeMux() mux := http.NewServeMux()
statsviz.Register(mux) err := statsviz.Register(mux, statsviz.Root("/"))
if err != nil {
return err
}
runtime.SetBlockProfileRate(1)
runtime.SetMutexProfileFraction(1)
mux.HandleFunc("/debug/pprof/", pprof.Index)
mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
go func() { go func() {
http.ListenAndServe("localhost:8001", mux) err := http.ListenAndServe("localhost:8001", mux)
if err != nil {
slog.Error("Failed to set up server for statsviz", "err", err)
}
}() }()
// Setting up some internal data structures return nil
telemetry.Init() }
func main() {
initApplication()
// Launches the cobra package stuff // Launches the cobra package stuff
cmd.Execute(logger) cmd.Execute()
} }
+1 -1
View File
@@ -54,7 +54,7 @@ func RunLiveTelemetry(port string, output string, session string) {
// log.Fatalf("Failed to create iRacing interface: %v", err) // log.Fatalf("Failed to create iRacing interface: %v", err)
// } // }
irsdk, err := goirsdk.Init(nil, output, session) irsdk, err := goirsdk.Init(goirsdk.Options{})
if err != nil { if err != nil {
log.Fatalf("Failed to create irsdk instance: %v\n", err) log.Fatalf("Failed to create irsdk instance: %v\n", err)
} }
+188 -21
View File
@@ -1,44 +1,211 @@
// Package beamng is the BeamNG.drive data provider
package beamng package beamng
import ( import (
"context"
"fmt" "fmt"
"log/slog"
"sync"
"time"
"github.com/ESilva15/gobngsdk" "esdi/telemetry"
bngsdk "github.com/ESilva15/gobngsdk"
) )
// This will implement the GameSink interface from the main package // BeamNG is the concrete implementation of the TelemetryProvider interface
// for BeamNG.drive
// NOTE: document this please. What is a TelemetryData????
type BeamNG struct { type BeamNG struct {
SDK *bngsdk.BNGSDK SDK *bngsdk.BeamNGSDK
// data handling
mut sync.Mutex
data *telemetry.TelemetryData
updaters [telemetry.MaxFields]func(*telemetry.TelemetryField)
// stream control
streamCh chan telemetry.TelemetryData
streamCancel context.CancelFunc
// timing
ticker *time.Ticker
} }
const ( const (
NAME = "BeamNG.Drive" NAME = "BeamNG.drive"
) )
func Init(ip string, port int) (BeamNG, error) { func NewBeamNGProvider(ip string, port int) (*BeamNG, error) {
var err error beam, err := bngsdk.Init(ip, port)
sdk, err := bngsdk.Init(ip, port)
if err != nil { if err != nil {
return BeamNG{}, err return &BeamNG{}, err
} }
return BeamNG{SDK: &sdk}, nil provider := &BeamNG{
} streamCh: make(chan telemetry.TelemetryData, 1),
data: telemetry.NewTelemetryData(),
// GetData will retrieve a given field by its name from the OutGauge data SDK: &beam,
func (b *BeamNG) GetData(fieldName string) (interface{}, error) { ticker: time.NewTicker(time.Second / 60),
if val, ok := b.SDK.DataDict[fieldName]; ok {
return val, nil
} }
return nil, fmt.Errorf("key `%s` doesn't exist", fieldName) provider.updaters = [telemetry.MaxFields]func(*telemetry.TelemetryField){
telemetry.Speed: provider.updateSpeed,
telemetry.Gear: provider.updateGear,
telemetry.RPM: provider.updateRPM,
telemetry.FuelLevel: provider.fuelLevel,
// Engine Data
telemetry.OilPress: provider.oilPressure,
telemetry.OilTemp: provider.oilTemp,
telemetry.WaterTemp: provider.engTemp,
// Electrics (dash lights and so on)
telemetry.PitSpeedLimiter: provider.pitSpeedLimiter,
telemetry.LeftIndicator: provider.leftIndicator,
telemetry.RightIndicator: provider.rightIndicator,
telemetry.Hazards: provider.unused,
telemetry.ABSWarningLight: provider.absLight,
telemetry.ParkingBrakeLight: provider.handbrakeLight,
telemetry.TCLight: provider.tcLight,
telemetry.BatteryLight: provider.batteryLight,
}
// Set the unset telemetry fields on the updaters as unused fields
for k := range int(telemetry.MaxFields) {
if provider.updaters[k] == nil {
provider.updaters[k] = provider.unused
}
}
return provider, nil
} }
func (b *BeamNG) UpdateData() error { func (b *BeamNG) StopStream() {
return b.SDK.ReadData() if b.streamCancel == nil {
return
}
b.streamCancel()
} }
func (b *BeamNG) GetSessionInfo() (interface{}, error) { func (b *BeamNG) Stream() (<-chan telemetry.TelemetryData, error) {
return nil, fmt.Errorf("Not implemented") var ctx context.Context
ctx, b.streamCancel = context.WithCancel(context.Background())
// Start the stream
b.stream(ctx)
return b.streamCh, nil
}
func (b *BeamNG) Subscribe(requestFields map[int16]telemetry.FieldID) {
// NOTE: document how the Subscribe funtion works
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
}
binding := telemetry.BoundField{
ID: id,
}
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 {
slog.Debug("Current bind: ", "id", bind.ID)
b.updaters[bind.ID](&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:
}
// NOTE: add a method to check if there's data available, or make this happen
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
}
}
}
}()
} }
+113
View File
@@ -0,0 +1,113 @@
package beamng
import (
"strconv"
conv "esdi/conversions"
"esdi/telemetry"
)
const (
LapTimeFormatStr = "04:05.000"
)
func (b *BeamNG) unused(out *telemetry.TelemetryField) {
out.Unused()
}
func (b *BeamNG) updateSpeed(out *telemetry.TelemetryField) {
out.Type = telemetry.DataTypeUINT16
out.Raw = uint64(conv.MsToKph(b.SDK.Data.Speed))
}
func (b *BeamNG) updateGear(out *telemetry.TelemetryField) {
out.Type = telemetry.DataTypeSTRING
// NOTE: stupid idea but we can cache these values
out.Str = strconv.Itoa(int(b.SDK.Data.Gear))
}
func (b *BeamNG) updateRPM(out *telemetry.TelemetryField) {
out.Type = telemetry.DataTypeUINT16
out.Raw = uint64(uint16(b.SDK.Data.RPM))
}
func (b *BeamNG) fuelLevel(out *telemetry.TelemetryField) {
telemetry.FloatToStringTransform(b.SDK.Data.Fuel, out)
}
func (b *BeamNG) oilPressure(out *telemetry.TelemetryField) {
telemetry.FloatToStringTransform(b.SDK.Data.OilPressure, out)
}
func (b *BeamNG) oilTemp(out *telemetry.TelemetryField) {
telemetry.FloatToStringTransform(b.SDK.Data.OilTemp, out)
}
func (b *BeamNG) engTemp(out *telemetry.TelemetryField) {
telemetry.FloatToStringTransform(b.SDK.Data.EngTemp, out)
}
// NOTE: find how to empty this
func (b *BeamNG) pitSpeedLimiter(out *telemetry.TelemetryField) {
}
func (b *BeamNG) leftIndicator(out *telemetry.TelemetryField) {
chr := ' '
if b.SDK.LeftIndicator() {
chr = '<'
}
out.Type = telemetry.DataTypeCHAR
out.Raw = uint64(chr)
}
func (b *BeamNG) rightIndicator(out *telemetry.TelemetryField) {
chr := ' '
if b.SDK.RightIndicator() {
chr = '>'
}
out.Type = telemetry.DataTypeCHAR
out.Raw = uint64(chr)
}
func (b *BeamNG) absLight(out *telemetry.TelemetryField) {
chr := ' '
if b.SDK.ABS() {
chr = 'A'
}
out.Type = telemetry.DataTypeCHAR
out.Raw = uint64(chr)
}
func (b *BeamNG) handbrakeLight(out *telemetry.TelemetryField) {
chr := ' '
if b.SDK.Handbrake() {
chr = 'P'
}
out.Type = telemetry.DataTypeCHAR
out.Raw = uint64(chr)
}
func (b *BeamNG) tcLight(out *telemetry.TelemetryField) {
chr := ' '
if b.SDK.TractionControl() {
chr = 'T'
}
out.Type = telemetry.DataTypeCHAR
out.Raw = uint64(chr)
}
func (b *BeamNG) batteryLight(out *telemetry.TelemetryField) {
chr := ' '
if b.SDK.BatteryLight() {
chr = 'B'
}
out.Type = telemetry.DataTypeCHAR
out.Raw = uint64(chr)
}
-72
View File
@@ -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))
}
+85 -75
View File
@@ -7,68 +7,62 @@ import (
"context" "context"
"fmt" "fmt"
"log/slog" "log/slog"
"os"
"strconv" "strconv"
"sync" "sync"
"time" "time"
conv "esdi/conversions" conv "esdi/conversions"
telem "esdi/telemetry" "esdi/telemetry"
"github.com/ESilva15/goirsdk" "github.com/ESilva15/goirsdk"
) )
const (
NAME = "iRacing"
)
// IRacing is our iRacing telemetry data provider - its a TelemetryProvider interface // IRacing is our iRacing telemetry data provider - its a TelemetryProvider interface
type IRacing struct { type IRacing struct {
logger *slog.Logger logger *slog.Logger
SDK *goirsdk.IBT SDK *goirsdk.IBT
// Data Handling // Data Handling
mut sync.Mutex mut sync.Mutex
data *telem.TelemetryData data *telemetry.TelemetryData
updaters [telemetry.MaxFields]func(*telemetry.TelemetryField)
// Timing information // Timing information
ticker *time.Ticker // ticker will keep polling intervals constant ticker *time.Ticker // ticker will keep polling intervals constant
// Stream // Stream
streamCh chan telem.TelemetryData streamCh chan telemetry.TelemetryData
streamCancel context.CancelFunc streamCancel context.CancelFunc
} }
func NewIRacingProvider( func NewIRacingProvider(
logger *slog.Logger, logger *slog.Logger,
source string, opts goirsdk.Options,
telemOut string,
yamlOut string,
) (*IRacing, error) { ) (*IRacing, error) {
var err error var err error
// Open the input file if provided - otherwise live telemetry was requested sdk, err := goirsdk.Init(opts)
// Maybe this can be changed so we don't have to run it with these ifs but by configuring our
// provider
var file goirsdk.Reader = nil
if source != "" {
file, err = os.Open(source)
if err != nil {
return &IRacing{}, err
// log.Fatalf("Failed to open IBT file: %v", err)
}
}
sdk, err := goirsdk.Init(file, telemOut, yamlOut)
if err != nil { if err != nil {
logger.Error("failed to open the IRSDK instance") logger.Error("failed to open the IRSDK instance")
return &IRacing{}, err return &IRacing{}, err
} }
return &IRacing{ provider := &IRacing{
logger: logger, logger: logger,
SDK: sdk, SDK: sdk,
data: telem.NewTelemetryData(), data: telemetry.NewTelemetryData(),
streamCh: make(chan telem.TelemetryData), streamCh: make(chan telemetry.TelemetryData, 1),
// NOTE: This is because I stupidly recorded a test IBT file in 240 // NOTE: This is because I stupidly recorded a test IBT file in 240
ticker: time.NewTicker(time.Second / 240), ticker: time.NewTicker(time.Second / 240),
}, nil }
// provider.updaters = [telemetry.MaxFields]func(*telemetry.TelemetryField){}
return provider, nil
} }
func (i *IRacing) isDataAvailable() bool { func (i *IRacing) isDataAvailable() bool {
@@ -90,6 +84,13 @@ func (i *IRacing) stream(ctx context.Context) {
go func() { go func() {
for { for {
// Explicitly intercpt cancellation
select {
case <-ctx.Done():
return
default:
}
// We start by checking if we do or do not have data available // We start by checking if we do or do not have data available
if !i.isDataAvailable() { if !i.isDataAvailable() {
continue continue
@@ -146,7 +147,7 @@ func (i *IRacing) readData() {
// Stream returns a channel that we will use to funnel the telemetry data back to the // 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 // 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 var ctx context.Context
ctx, i.streamCancel = context.WithCancel(context.Background()) ctx, i.streamCancel = context.WithCancel(context.Background())
@@ -156,31 +157,40 @@ func (i *IRacing) Stream() (<-chan telem.TelemetryData, error) {
return i.streamCh, nil return i.streamCh, nil
} }
func (i *IRacing) Subscribe(requestFields map[int16]telem.FieldID) { 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.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 // First we must add the virtual fields
// we will add their dependencies and the primitives to a slice // 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 { for winID, id := range requestFields {
i.data.Values[id].IDs = append(i.data.Values[id].IDs, winID) i.data.Values[id].IDs = append(i.data.Values[id].IDs, winID)
switch id { switch id {
case telem.RPMStateColour: case telemetry.RPMStateColour:
i.data.VirtualBinds = append(i.data.VirtualBinds, telem.NewRPMLights()) i.data.VirtualBinds = append(i.data.VirtualBinds, telemetry.NewRPMLights())
case telem.FCCurrentLap: case telemetry.FCCurrentLap:
i.data.VirtualBinds = append(i.data.VirtualBinds, i.data.VirtualBinds = append(i.data.VirtualBinds,
telem.NewFuelCalculator(i.logger.WithGroup("FUEL CALC"))) telemetry.NewFuelCalculator(i.logger.WithGroup("FUEL CALC")))
default: default:
// primitive telemetry field // primitive telemetry field
pendingBinds = append(pendingBinds, id) 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 // Now that we know all the fields we need to bind we follow the binding procedure
for _, id := range pendingBinds { for _, id := range pendingBinds {
@@ -197,20 +207,20 @@ func (i *IRacing) Subscribe(requestFields map[int16]telem.FieldID) {
continue continue
} }
binding := telem.BoundField{ binding := telemetry.BoundField{
Key: sdkKey, Key: sdkKey,
ID: id, ID: id,
} }
switch id { switch id {
case telem.Speed: case telemetry.Speed:
binding.Transform = func(v any, out *telem.TelemetryField) { binding.Transform = func(v any, out *telemetry.TelemetryField) {
out.Type = telem.DataTypeUINT16 out.Type = telemetry.DataTypeUINT16
out.Raw = uint64(conv.MsToKph(v.(float32))) out.Raw = uint64(conv.MsToKph(v.(float32)))
} }
case telem.Gear: case telemetry.Gear:
binding.Transform = func(v any, out *telem.TelemetryField) { binding.Transform = func(v any, out *telemetry.TelemetryField) {
out.Type = telem.DataTypeCHAR out.Type = telemetry.DataTypeCHAR
// out.Raw = uint64(v.(int)) // out.Raw = uint64(v.(int))
gear := 0 gear := 0
@@ -233,53 +243,53 @@ func (i *IRacing) Subscribe(requestFields map[int16]telem.FieldID) {
out.Raw = uint64('?') // Fallback out.Raw = uint64('?') // Fallback
} }
} }
case telem.RPM: case telemetry.RPM:
binding.Transform = func(v any, out *telem.TelemetryField) { binding.Transform = func(v any, out *telemetry.TelemetryField) {
out.Type = telem.DataTypeUINT16 out.Type = telemetry.DataTypeUINT16
out.Raw = uint64(uint16(v.(float32))) out.Raw = uint64(uint16(v.(float32)))
} }
case telem.FuelLevel: case telemetry.FuelLevel:
binding.Transform = FloatToStringTransform binding.Transform = telemetry.FloatToStringTransformDEPRECATE
// Engine Data // Engine Data
case telem.OilPress: case telemetry.OilPress:
binding.Transform = FloatToStringTransform binding.Transform = telemetry.FloatToStringTransformDEPRECATE
case telem.OilTemp: case telemetry.OilTemp:
binding.Transform = FloatToStringTransform binding.Transform = telemetry.FloatToStringTransformDEPRECATE
case telem.WaterTemp: case telemetry.WaterTemp:
binding.Transform = FloatToStringTransform binding.Transform = telemetry.FloatToStringTransformDEPRECATE
// Something else // Something else
case telem.PitSpeedLimiter: case telemetry.PitSpeedLimiter:
binding.Transform = PitSpeedLimiterTransform binding.Transform = PitSpeedLimiterTransform
// Adjustements // Adjustements
case telem.BrakeBias: case telemetry.BrakeBias:
binding.Transform = FloatToStringTransform binding.Transform = telemetry.FloatToStringTransformDEPRECATE
case telem.ABSSetting: case telemetry.ABSSetting:
binding.Transform = FloatToUInt8Transform binding.Transform = telemetry.FloatToUInt8TransformDEPRECATE
case telem.TCSetting: case telemetry.TCSetting:
binding.Transform = FloatToUInt8Transform binding.Transform = telemetry.FloatToUInt8TransformDEPRECATE
case telem.ThrottleSetting: case telemetry.ThrottleSetting:
binding.Transform = FloatToUInt8Transform binding.Transform = telemetry.FloatToUInt8TransformDEPRECATE
case telem.LFtempM: case telemetry.LFtempM:
binding.Transform = func(v any, out *telem.TelemetryField) { binding.Transform = func(v any, out *telemetry.TelemetryField) {
out.Type = telem.DataTypeSTRING out.Type = telemetry.DataTypeSTRING
out.Str = strconv.FormatFloat(float64(v.(float32)), 'f', 1, 32) out.Str = strconv.FormatFloat(float64(v.(float32)), 'f', 1, 32)
} }
case telem.SessionTime: case telemetry.SessionTime:
binding.Transform = func(v any, out *telem.TelemetryField) { binding.Transform = func(v any, out *telemetry.TelemetryField) {
out.Type = telem.DataTypeSTRING out.Type = telemetry.DataTypeSTRING
out.Str = strconv.FormatFloat(v.(float64), 'f', 1, 32) out.Str = strconv.FormatFloat(v.(float64), 'f', 1, 32)
} }
case telem.ReplaySessionTime: case telemetry.ReplaySessionTime:
binding.Transform = func(v any, out *telem.TelemetryField) { binding.Transform = func(v any, out *telemetry.TelemetryField) {
out.Type = telem.DataTypeSTRING out.Type = telemetry.DataTypeSTRING
out.Str = strconv.FormatFloat(v.(float64), 'f', 1, 32) out.Str = strconv.FormatFloat(v.(float64), 'f', 1, 32)
} }
case telem.Empty: case telemetry.Empty:
binding.Transform = EmptyTransform binding.Transform = telemetry.EmptyTransform
case telem.LapLastLapTime: case telemetry.LapLastLapTime:
binding.Transform = LapTimeTransform binding.Transform = LapTimeTransform
case telem.LapNumber: case telemetry.LapNumber:
binding.Transform = UInt8Transform binding.Transform = telemetry.UInt8Transform
} }
i.data.ActiveBinds = append(i.data.ActiveBinds, binding) i.data.ActiveBinds = append(i.data.ActiveBinds, binding)
+34
View File
@@ -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 = " "
}
}
+41
View File
@@ -0,0 +1,41 @@
// Package providers
package providers
import (
"log/slog"
"esdi/providers/beamng"
"esdi/providers/iracing"
"esdi/telemetry"
"github.com/ESilva15/goirsdk"
)
// Make this be some kind of struct where we can access a function that returns
// the selected provider by its name
type Provider struct {
Name string
Provider telemetry.TelemetryProvider
}
var Providers = map[string]Provider{
beamng.NAME: {
Name: beamng.NAME,
},
iracing.NAME: {
Name: iracing.NAME,
},
}
func NewIRacingProvider(logger *slog.Logger, opts goirsdk.Options,
) telemetry.TelemetryProvider {
provider, _ := iracing.NewIRacingProvider(logger, opts)
return provider
}
func NewBeamNGProvider(ip string, port int) telemetry.TelemetryProvider {
provider, _ := beamng.NewBeamNGProvider(ip, port)
return provider
}
@@ -1,21 +1,25 @@
package services package services
import ( import (
"context"
"fmt"
"log/slog"
"sync/atomic"
"esdi/cdashdisplay" "esdi/cdashdisplay"
helper "esdi/helpers" helper "esdi/helpers"
"esdi/peripheral" "esdi/peripheral"
"esdi/telemetry" "esdi/telemetry"
"fmt"
"log/slog"
"sync/atomic"
) )
type CDashService struct { type CDashService struct {
Logger *slog.Logger Logger *slog.Logger
CDash *cdashdisplay.CDashDisplay CDash *cdashdisplay.CDashDisplay
// iRacingTelemetry *IRacingService
DevClerk *peripheral.PeripheralDeviceClerk DevClerk *peripheral.PeripheralDeviceClerk
Messages chan string Messages chan string
// Telemetry Channel
streamCancel context.CancelFunc
TelemCh <-chan telemetry.TelemetryData
} }
func NewCDashService(logger *slog.Logger) *CDashService { func NewCDashService(logger *slog.Logger) *CDashService {
@@ -65,6 +69,10 @@ func (cds *CDashService) SaveLayout(layoutPath string) error {
return cds.CDash.SaveLayout(layoutPath) return cds.CDash.SaveLayout(layoutPath)
} }
func (cds *CDashService) UnloadLayout() error {
return cds.CDash.UnloadLayout()
}
func (cds *CDashService) UpdateWindow(win *cdashdisplay.DesktopUIWindow) error { func (cds *CDashService) UpdateWindow(win *cdashdisplay.DesktopUIWindow) error {
cds.Messages <- fmt.Sprintf("Updating a window:\n%+v\n", win) cds.Messages <- fmt.Sprintf("Updating a window:\n%+v\n", win)
return cds.CDash.UpdateWindow(win) return cds.CDash.UpdateWindow(win)
@@ -92,19 +100,49 @@ func (cds *CDashService) MoveWindow(idx int16, vec *helper.Vector) error {
return nil return nil
} }
func (cds *CDashService) StreamData(stream <-chan telemetry.TelemetryData) { func (cds *CDashService) SetTelemetryChannel(ch <-chan telemetry.TelemetryData) {
cds.TelemCh = ch
}
func (cds *CDashService) StartStream() {
// NOTE: i'm using this pattern a whole lot. Maybe I can create a struct to handle this
var ctx context.Context
ctx, cds.streamCancel = context.WithCancel(context.Background())
go cds.transmit(ctx)
}
func (cds *CDashService) StopStream() {
if cds.streamCancel == nil {
return
}
cds.streamCancel()
cds.streamCancel = nil
}
// INTERNAL
func (cds *CDashService) transmit(ctx context.Context) {
var isSending atomic.Bool var isSending atomic.Bool
go func() { for {
for msg := range stream { select {
case <-ctx.Done():
return
case data, ok := <-cds.TelemCh:
if !ok {
return
}
if isSending.Load() { if isSending.Load() {
continue continue
} }
isSending.Store(true) isSending.Store(true)
cds.CDash.SendData(&msg) cds.CDash.SendData(&data)
isSending.Store(false) isSending.Store(false)
} }
}() }
} }
+152
View File
@@ -0,0 +1,152 @@
package services
import (
"context"
"log/slog"
"sync"
"esdi/providers"
telem "esdi/telemetry"
"github.com/ESilva15/goirsdk"
)
// TelemetryService will be our base struct to handle telemetry data
// 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
// Concurrency protection
mut sync.RWMutex
ativeProvider telem.TelemetryProvider
// Channel for the UI
listeners map[string]chan telem.TelemetryData
cancelForward context.CancelFunc
}
func NewTelemetryService(logger *slog.Logger, cdash *CDashService) *TelemetryService {
newService := &TelemetryService{
logger: logger,
cdash: cdash,
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(), goirsdk.Options{
SourceType: goirsdk.SharedMemoryFile,
})
// firstProvider := providers.NewBeamNGProvider("127.0.0.1", 4443)
newService.SwitchProvider(firstProvider)
return newService
}
func (t *TelemetryService) SwitchProvider(newProvider telem.TelemetryProvider) error {
t.mut.Lock()
defer t.mut.Unlock()
// Clean up the current to be old provider
if t.ativeProvider != nil {
t.dropActiveProvider()
}
// Assign the new provider
t.ativeProvider = newProvider
return nil
}
func (t *TelemetryService) multiplexData(ctx context.Context, dataCh <-chan telem.TelemetryData) {
for {
select {
case <-ctx.Done():
return
case data, ok := <-dataCh:
if !ok {
return
}
t.mut.RLock()
for _, ch := range t.listeners {
select {
case ch <- data:
// Sends data to the subscriber
default:
// Subscriber is full, we just skip ahead. Maybe find a how to add metrics here
}
}
t.mut.RUnlock()
}
}
}
func (t *TelemetryService) dropActiveProvider() {
if t.cancelForward != nil {
t.cancelForward()
}
t.ativeProvider.StopStream()
}
func (t *TelemetryService) SubscribeListener(id string, bufferSize int) <-chan telem.TelemetryData {
t.mut.Lock()
defer t.mut.Unlock()
// NOTE: is this truly necessary?
// return the channel if it already exists
if ch, exists := t.listeners[id]; exists {
return ch
}
ch := make(chan telem.TelemetryData, bufferSize)
t.listeners[id] = ch
t.logger.Info("New stream subscriber registered", "id", id)
return ch
}
func (t *TelemetryService) UnsubscribeListener(id string) {
t.mut.Lock()
defer t.mut.Unlock()
if ch, exists := t.listeners[id]; exists {
close(ch)
delete(t.listeners, id)
t.logger.Info("Stream subscriber removed", "id", id)
}
}
func (t *TelemetryService) SubscribeToFields(fields map[int16]telem.FieldID) {
t.ativeProvider.Subscribe(fields)
}
func (t *TelemetryService) StartStream() {
slog.Debug("Stream started")
// Start the new stream
simInCh, _ := t.ativeProvider.Stream()
// if err != nil {
// // NOTE
// }
// Create the context so we can control the lifecycle
ctx, cancel := context.WithCancel(context.Background())
t.cancelForward = cancel
// Multiplex this data
go t.multiplexData(ctx, simInCh)
}
func (t *TelemetryService) StopStream() {
t.mut.Lock()
defer t.mut.Unlock()
if t.cancelForward != nil {
t.cancelForward()
t.cancelForward = nil
}
t.ativeProvider.StopStream()
}
+46
View File
@@ -0,0 +1,46 @@
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 float32, out *TelemetryField) {
out.Type = DataTypeSTRING
out.Str = strconv.FormatFloat(float64(v), 'f', 1, 32)
}
func FloatToStringTransformDEPRECATE(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 FloatToUInt8TransformDEPRECATE(v any, out *TelemetryField) {
out.Type = DataTypeUINT8
if v == nil {
out.Raw = uint64(0)
return
}
out.Raw = uint64(v.(float32))
}
+27 -2
View File
@@ -27,10 +27,14 @@ type VirtualField interface {
// NOTE: Update the iracing SDK to write data to the same map ALWAYS, then // 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 // I can bind that address and read directly from there on the transform
// BoundField is the data structure we use to bind the telemetry provider's data
// to our internal telemetry fields
type BoundField struct { type BoundField struct {
Key string Key string // NOTE: to be deprecated
ID FieldID ID FieldID
Transform func(any, *TelemetryField) Fetch func() any // NOTE: to be deprecated
Transform func(any, *TelemetryField) // NOTE: to be deprecated
Update func(out *TelemetryField)
} }
var bufferPool = sync.Pool{ var bufferPool = sync.Pool{
@@ -69,6 +73,11 @@ type TelemetryField struct {
Str string // Only to be used with DataTypeSTRING Str string // Only to be used with DataTypeSTRING
} }
func (tf *TelemetryField) Unused() {
tf.Type = DataTypeCHAR
tf.Raw = uint64('-')
}
// Pack will pack this current TelemetryField into bytes to send over the wire // Pack will pack this current TelemetryField into bytes to send over the wire
// Format: // Format:
// 0x00 - Field ID // 0x00 - Field ID
@@ -143,6 +152,14 @@ const (
WaterTemp WaterTemp
// Engine Warnings // Engine Warnings
PitSpeedLimiter PitSpeedLimiter
// Electrics (dash lights and so on)
LeftIndicator
RightIndicator
Hazards
ABSWarningLight
ParkingBrakeLight
TCLight
BatteryLight
// Adjustements // Adjustements
BrakeBias BrakeBias
ABSSetting ABSSetting
@@ -191,6 +208,14 @@ var FieldNames = [MaxFields]string{
WaterTemp: "Water Temperature", WaterTemp: "Water Temperature",
// Engine Warnings // Engine Warnings
PitSpeedLimiter: "Pit Speed Limiter", PitSpeedLimiter: "Pit Speed Limiter",
// Electrics (dash lights and so on)
LeftIndicator: "Left Indicator",
RightIndicator: "Right Indicator",
Hazards: "Hazards",
ABSWarningLight: "ABS Dash Light",
ParkingBrakeLight: "Parking Brake Dash Light",
TCLight: "Traction Control Light",
BatteryLight: "Battery Light",
// Ajustments // Ajustments
BrakeBias: "BrakeBias", BrakeBias: "BrakeBias",
ABSSetting: "ABS Control", ABSSetting: "ABS Control",
+1
View File
@@ -2,6 +2,7 @@
package telemetry package telemetry
type TelemetryProvider interface { type TelemetryProvider interface {
StopStream()
Stream() (<-chan TelemetryData, error) Stream() (<-chan TelemetryData, error)
Subscribe(map[int16]FieldID) Subscribe(map[int16]FieldID)
} }
+3
View File
@@ -24,6 +24,9 @@ func ListFormButtonLabels(form *tview.Form) []string {
} }
func SetFormButtonCallback(form *tview.Form, btnLabel string, fn func()) error { func SetFormButtonCallback(form *tview.Form, btnLabel string, fn func()) error {
// NOTE: is there a better way to do this ???
// Find out
btnIndex := form.GetButtonIndex(btnLabel) btnIndex := form.GetButtonIndex(btnLabel)
if btnIndex == -1 { if btnIndex == -1 {
availableButtons := ListFormButtonLabels(form) availableButtons := ListFormButtonLabels(form)
+5 -4
View File
@@ -2,10 +2,11 @@
package controllers package controllers
import ( import (
serv "esdi/tui/internal/services"
"esdi/tui/internal/views"
"fmt" "fmt"
serv "esdi/services"
"esdi/tui/internal/views"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
) )
@@ -76,12 +77,12 @@ func (mc *DeviceController) AddDeviceAPIListItems() {
AddItem("stream", "stream data to the display", func() { AddItem("stream", "stream data to the display", func() {
views.AddAndShowPage(mc.DeviceAPIView.DevAPIToolView.Pages, views.AddAndShowPage(mc.DeviceAPIView.DevAPIToolView.Pages,
"streaming-tool", "streaming-tool",
mc.StreamCtrl.StreamView.TextView, mc.StreamCtrl.StreamView.Flex,
) )
mc.StreamCtrl.SetInternalState() mc.StreamCtrl.SetInternalState()
mc.App.SetFocus(mc.StreamCtrl.StreamView.TextView) mc.App.SetFocus(mc.StreamCtrl.StreamView.Options.Form)
}) })
} }
+69 -3
View File
@@ -2,11 +2,12 @@ package controllers
import ( import (
"fmt" "fmt"
"os"
"strconv" "strconv"
"esdi/cdashdisplay" "esdi/cdashdisplay"
helper "esdi/helpers" helper "esdi/helpers"
"esdi/tui/internal/services" "esdi/services"
"esdi/tui/internal/views" "esdi/tui/internal/views"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
@@ -20,6 +21,7 @@ type LayoutController struct {
Messages chan string Messages chan string
DevService *services.CDashService DevService *services.CDashService
MoveToolState *windowManipState MoveToolState *windowManipState
SelectedLayout string // NOTE: This should be a struct to handle its own things
} }
func NewLayoutController(base *Controller, service *services.CDashService) *LayoutController { func NewLayoutController(base *Controller, service *services.CDashService) *LayoutController {
@@ -29,6 +31,8 @@ func NewLayoutController(base *Controller, service *services.CDashService) *Layo
Messages: make(chan string, 10), Messages: make(chan string, 10),
DevService: service, DevService: service,
MoveToolState: &windowManipState{Mode: moveMode}, MoveToolState: &windowManipState{Mode: moveMode},
// SelectedLayout: "beamng.yaml",
SelectedLayout: "layout.yaml",
} }
lc.registerHooks() lc.registerHooks()
@@ -56,6 +60,9 @@ func (lc *LayoutController) registerHooks() {
case 'm': case 'm':
lc.Messages <- "calling window manipulation tool\n" lc.Messages <- "calling window manipulation tool\n"
lc.moveWindow() lc.moveWindow()
case 'c':
lc.Messages <- "calling change selected layout tool\n"
lc.changeSelectedLoadout()
case 's': case 's':
// Save the current layout // Save the current layout
lc.Messages <- "calling save layout\n" lc.Messages <- "calling save layout\n"
@@ -64,6 +71,10 @@ func (lc *LayoutController) registerHooks() {
// Load the layout // Load the layout
lc.Messages <- "calling load layout\n" lc.Messages <- "calling load layout\n"
lc.loadLayout() lc.loadLayout()
case 'u':
// Unload the layout
lc.Messages <- "calling unload layout\n"
lc.unloadLayout()
case 'g': case 'g':
// Go -> launches the current set up source // Go -> launches the current set up source
// streamingWindow(bus, doc) // streamingWindow(bus, doc)
@@ -339,7 +350,7 @@ func (lc *LayoutController) getCurrentTreeNodeModel() (*tview.TreeNode, int16, e
func (lc *LayoutController) loadLayout() { func (lc *LayoutController) loadLayout() {
// We would get the layout path from somewhere but for nots its layout.yaml // We would get the layout path from somewhere but for nots its layout.yaml
err := lc.DevService.LoadLayout("layout.yaml") err := lc.DevService.LoadLayout(lc.SelectedLayout)
if err != nil { if err != nil {
lc.Messages <- "failed to load layout: " + err.Error() lc.Messages <- "failed to load layout: " + err.Error()
return return
@@ -348,8 +359,16 @@ func (lc *LayoutController) loadLayout() {
lc.displayLoadedLayouts() lc.displayLoadedLayouts()
} }
func (lc *LayoutController) unloadLayout() {
err := lc.DevService.UnloadLayout()
if err != nil {
lc.Logger.Error(fmt.Sprintf("Failed to unload layout: %+v", err))
return
}
}
func (lc *LayoutController) saveLayout() { func (lc *LayoutController) saveLayout() {
err := lc.DevService.SaveLayout("layout.yaml") err := lc.DevService.SaveLayout(lc.SelectedLayout)
if err != nil { if err != nil {
lc.Messages <- "failed to save layout: " + err.Error() lc.Messages <- "failed to save layout: " + err.Error()
return return
@@ -423,3 +442,50 @@ func (lc *LayoutController) moveWindow() {
lc.App.SetFocus(formView.Form) lc.App.SetFocus(formView.Form)
} }
// changeSelectedLoadout is used to change the currently selected loadout
// its a very bare bones utility to aid development
// NOTE: FOR THE LOVE OF WHATS HOLY ACTUALLY MAKE THIS GOOD BEFORE ADDING MORE FEATURES
func (lc *LayoutController) changeSelectedLoadout() {
// First we get the files in the layouts directory
entries, err := os.ReadDir("./layouts")
if err != nil {
lc.Logger.Error(fmt.Sprintf("Failed to read directory: %v", err))
return
}
// Then we open a modal and pass that list there
lc.Logger.Debug(fmt.Sprintf("Found layouts: %+v", entries))
layoutSelection := views.NewLayoutToolLayoutsList()
layoutSelection.AddEntries(entries)
layoutSelection.Tree.SetInputCapture(func(ev *tcell.EventKey) *tcell.EventKey {
switch ev.Key() {
case tcell.KeyESC:
// We have to destroy this window, but first we tell the pages to show something else
// NOTE: will this be left hanging and then given to GC? Can we improve that?
lc.LayoutToolView.LayoutActions.LayoutSelection = nil
lc.LayoutToolView.DeleteLayoutList()
lc.App.SetFocus(lc.LayoutToolView.LayoutTree.Tree)
}
return ev
})
layoutSelection.Tree.SetSelectedFunc(func(node *tview.TreeNode) {
ref := node.GetReference()
if ref == nil {
return
}
lc.Messages <- "User selected " + fmt.Sprintf("%+v", ref) + "\n"
lc.SelectedLayout = ref.(string)
lc.LayoutToolView.LayoutTree.Tree.SetTitle(fmt.Sprintf("%s [%s]",
views.LayoutToolTreeViewTitle, ref.(string)))
})
lc.LayoutToolView.LayoutActions.LayoutSelection = layoutSelection
lc.LayoutToolView.ShowLayoutList()
lc.App.SetFocus(lc.LayoutToolView.LayoutActions.LayoutSelection.Tree)
}
+120 -41
View File
@@ -1,25 +1,33 @@
package controllers package controllers
import ( import (
"esdi/telemetry"
"esdi/tui/internal/services"
"esdi/tui/internal/views"
"fmt" "fmt"
"log/slog"
"sync/atomic" "sync/atomic"
"esdi/config"
"esdi/providers"
"esdi/services"
"esdi/telemetry"
"esdi/tui/internal/models"
"esdi/tui/internal/views"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
) )
type StreamingCtrl struct { type StreamingCtrl struct {
*Controller *Controller
Service *services.CDashService Service *services.CDashService
StreamView *views.StreamView StreamView *views.StreamToolView
Messages chan string Messages chan string
Internal chan string Internal chan string
Run bool TelemetryCh <-chan telemetry.TelemetryData
OnExit func() Run bool
isRunning bool OnExit func()
TelemServ *services.TelemetryService TelemServ *services.TelemetryService
// Stream State
isRunning bool
} }
func NewStreamingCtrl( func NewStreamingCtrl(
@@ -27,24 +35,38 @@ func NewStreamingCtrl(
serCDash *services.CDashService, serCDash *services.CDashService,
serTelem *services.TelemetryService, serTelem *services.TelemetryService,
) *StreamingCtrl { ) *StreamingCtrl {
// NOTE: looks sus, put this somewhere also. Not very good in here
providerList := []providers.Provider{}
for _, item := range providers.Providers {
providerList = append(providerList, item)
}
streamView := views.NewStreamToolView(providerList, config.GetCfg().DefaultSim)
ctrl := &StreamingCtrl{ ctrl := &StreamingCtrl{
Controller: base, Controller: base,
Service: serCDash, Service: serCDash,
TelemServ: serTelem, TelemServ: serTelem,
Messages: make(chan string, 10), Messages: make(chan string, 10),
Internal: make(chan string, 10), Internal: make(chan string, 10),
Run: false, TelemetryCh: make(chan telemetry.TelemetryData, 1),
StreamView: views.NewStreamView(), Run: false,
isRunning: false, StreamView: streamView,
isRunning: false,
} }
ctrl.registerHooks() ctrl.registerHooks()
ctrl.subscribeListeners()
go ctrl.listenToUIStream()
return ctrl return ctrl
} }
func (sc *StreamingCtrl) subscribeListeners() {
sc.TelemetryCh = sc.TelemServ.SubscribeListener("UI", 1)
}
func (sc *StreamingCtrl) registerHooks() { func (sc *StreamingCtrl) registerHooks() {
sc.StreamView.TextView.SetInputCapture(func(ev *tcell.EventKey) *tcell.EventKey { sc.StreamView.Options.Form.SetInputCapture(func(ev *tcell.EventKey) *tcell.EventKey {
switch ev.Key() { switch ev.Key() {
case tcell.KeyEsc: case tcell.KeyEsc:
sc.OnExit() sc.OnExit()
@@ -52,36 +74,77 @@ func (sc *StreamingCtrl) registerHooks() {
switch ev.Rune() { switch ev.Rune() {
case 's': case 's':
// Start // Start - stop
sc.Start() sc.StartStop()
case 'p': case 'u':
// Pause // Update
// sc.Stop() sc.updateStream()
} }
return ev return ev
}) })
// Set the options form callbacks
err := SetFormButtonCallback(sc.StreamView.Options.Form, "Update", func() {
sc.updateStream()
})
if err != nil {
// NOTE: what do we do here?
panic("Failed to set callback for streaming controller form update button")
}
} }
func (sc *StreamingCtrl) Start() { func (sc *StreamingCtrl) StartStop() {
stream := sc.TelemServ.StartStream() if sc.isRunning {
slog.Info("stopping stream")
var isDrawing atomic.Bool sc.TelemServ.StopStream()
sc.Service.StopStream()
go func() { sc.isRunning = false
for msg := range stream { return
if isDrawing.Load() { }
continue
}
isDrawing.Store(true) // stream is not running, we have to start it now
// NOTE:
// Subscribe the only existing device - needs to be discovered by now
slog.Debug("setting the data stream for cdash")
sc.Service.SetTelemetryChannel(sc.TelemServ.SubscribeListener("cdash", 1))
sc.App.QueueUpdateDraw(func() { slog.Debug("starting to stream data again")
sc.StreamView.Update(&msg) sc.Service.StartStream()
isDrawing.Store(false)
}) slog.Debug("starting the stream")
} sc.TelemServ.StartStream()
}()
slog.Debug("setting local control variables")
sc.isRunning = true
slog.Debug("starting stream")
}
func (sc *StreamingCtrl) parseStreamUpdateForm(form *views.StreamOptionsView) (*models.StreamOptions, error) {
_, sim := form.SimDropdown.GetCurrentOption()
return &models.StreamOptions{
Sim: sim,
}, nil
}
// updateStream will get the new options from the form and update the state accordingly
func (sc *StreamingCtrl) updateStream() {
sc.Messages <- "Request to update stream received - parsing form"
formData, err := sc.parseStreamUpdateForm(sc.StreamView.Options)
if err != nil {
slog.Error(fmt.Sprintf("failed to parse form data: %+v", err))
return
}
// NOTE: find a way to hold state so we can compare this new state to the old
// state. Need a streamctrl state that holds the form model, for example
slog.Debug(fmt.Sprintf("Parsed form data: %+v", formData))
} }
// SetInternalState is used to update the stuff in here, for example, the user // SetInternalState is used to update the stuff in here, for example, the user
@@ -99,7 +162,23 @@ func (sc *StreamingCtrl) SetInternalState() {
fields[w.UIData.IDX] = fieldID fields[w.UIData.IDX] = fieldID
} }
sc.TelemServ.ActiveProvider.Subscribe(fields) sc.TelemServ.SubscribeToFields(fields)
sc.Messages <- fmt.Sprintf("Subscribed Fields: %+v [%d]\n", fields, len(fields)) sc.Messages <- fmt.Sprintf("Subscribed Fields: %+v [%d]\n", fields, len(fields))
} }
func (sc *StreamingCtrl) listenToUIStream() {
var isDrawing atomic.Bool
for msg := range sc.TelemetryCh {
if isDrawing.Load() {
continue
}
isDrawing.Store(true)
sc.App.QueueUpdateDraw(func() {
sc.StreamView.Visualizer.Update(&msg)
isDrawing.Store(false)
})
}
}
+5
View File
@@ -0,0 +1,5 @@
package models
type StreamOptions struct {
Sim string
}
-55
View File
@@ -1,55 +0,0 @@
package services
import (
providerir "esdi/providers/iracing"
"log/slog"
telemetry "esdi/telemetry"
)
// TelemetryService will be our base struct to handle telemetry data
// 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, cdash *CDashService) *TelemetryService {
return &TelemetryService{
logger: logger,
cdash: cdash,
}
}
func (t *TelemetryService) setIRacingProvider() {
path := "/home/esilva/Desktop/projetos/simracing_peripherals/testTelemetry/gt3_mustang_bathurst.ibt"
provider, _ := providerir.NewIRacingProvider(t.logger, path, "", "")
t.ActiveProvider = provider
}
func (t *TelemetryService) SetProvider(provider string) *TelemetryService {
if t.ActiveProvider != nil {
// Gotta do something here to clean up before switching
}
switch provider {
case "iRacing":
// Set up iRacing
t.setIRacingProvider()
default:
return nil
}
return t
}
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
}
+23
View File
@@ -0,0 +1,23 @@
package tviewhelpers
import "github.com/rivo/tview"
func FindNodeByID(node *tview.TreeNode, id int16) *tview.TreeNode {
if node == nil {
return nil
}
if ref, ok := node.GetReference().(int16); ok {
if ref == id {
return node
}
}
for _, child := range node.GetChildren() {
if found := FindNodeByID(child, id); found != nil {
return found
}
}
return nil
}
-10
View File
@@ -1,10 +0,0 @@
// Package ui
package ui
import "github.com/rivo/tview"
type UIContext struct {
Redraw func()
Log func(formatter string, args ...any)
ChangeFocus func(p tview.Primitive)
}
-114
View File
@@ -1,114 +0,0 @@
package ui
import (
"esdi/cdashdisplay"
helper "esdi/helpers"
"esdi/tui/internal/models"
"github.com/rivo/tview"
)
//
type TUILoaded struct{}
//
// Main controller events
// Find a way to remove this
type ForceRedraw struct{}
type LogEv struct {
Log string
}
type PrintLogEv struct {
Log string
}
type RedrawEv struct {
Fn func()
}
type ChangeFocusEv struct {
Target tview.Primitive
}
// ----------------------------------------------------------------------------
// Layout events
type SaveLayoutEv struct{}
type LoadLayoutEv struct{}
type RegisterLoadedLayout struct {
Layout cdashdisplay.LayoutTree
}
type UpdateWindowEv struct {
ID int16
Window models.WindowForm
}
type CreateWindowEv struct {
Window models.WindowForm
}
type DestroyWindowEv struct {
ID int16
}
type LayoutRegisterWindowEv struct {
Window models.WindowForm
}
type MoveWindowEv struct {
WindowID int16
Delta helper.Vector
}
type ResizeWindowEv struct {
WindowID int16
Delta helper.Vector
}
type WindowCreatedEv struct {
ID int16
Win cdashdisplay.CDashWin
}
type WindowMovedEv struct {
ID int16
Dims cdashdisplay.UIDimensions
}
type WindowDestroyedEv struct {
ID int16
}
type ErrorCreateWindowEv struct {
Error error
}
type ErrorFormParseEv struct {
Error error
}
// Layout events --------------------------------------------------------------
// Device events
type FindCDashDisplay struct{}
// Device events --------------------------------------------------------------
// + Stream events ------------------------------------------------------------
type StartStreamingReqEv struct{}
type StopStreamingReqEv struct{}
type StreamDataEv struct {
Str string
}
// - Stream events ------------------------------------------------------------
+17 -22
View File
@@ -5,38 +5,20 @@ import (
"log/slog" "log/slog"
"esdi/cdashdisplay" "esdi/cdashdisplay"
tviewh "esdi/tui/internal/tview_helpers"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
"github.com/rivo/tview" "github.com/rivo/tview"
) )
const ( const (
LayoutToolTreeViewTitle = "Layout Tree"
LayoutToolFlexID = "layout-tool-flex" LayoutToolFlexID = "layout-tool-flex"
LayoutToolTreeID = "layout-tool-tree" LayoutToolTreeID = "layout-tool-tree"
LayoutToolActionPagesID = "layout-tool-action-pages" LayoutToolActionPagesID = "layout-tool-action-pages"
LayoutToolNewWindowID = "new-window-action-form" LayoutToolNewWindowID = "new-window-action-form"
) )
func FindNodeByID(node *tview.TreeNode, id int16) *tview.TreeNode {
if node == nil {
return nil
}
if ref, ok := node.GetReference().(int16); ok {
if ref == id {
return node
}
}
for _, child := range node.GetChildren() {
if found := FindNodeByID(child, id); found != nil {
return found
}
}
return nil
}
type LayoutTreeView struct { type LayoutTreeView struct {
Tree *tview.TreeView Tree *tview.TreeView
InputCapture func(*tcell.EventKey) *tcell.EventKey InputCapture func(*tcell.EventKey) *tcell.EventKey
@@ -49,7 +31,7 @@ func NewLayoutTreeView() *LayoutTreeView {
view.Tree = tview.NewTreeView() view.Tree = tview.NewTreeView()
view.Tree.SetBorder(true).SetTitle("Layout Tree") view.Tree.SetBorder(true).SetTitle(LayoutToolTreeViewTitle)
view.Tree.SetInputCapture(view.InputCapture) view.Tree.SetInputCapture(view.InputCapture)
// Inject ChangedFunc // Inject ChangedFunc
// Inject SelectedFunc // Inject SelectedFunc
@@ -80,6 +62,7 @@ func (lt *LayoutTreeView) AddWindow(win *cdashdisplay.DesktopUIWindow) error {
type LayoutToolActionView struct { type LayoutToolActionView struct {
Pages *tview.Pages Pages *tview.Pages
CreateWindowView *CreateWindowFormView CreateWindowView *CreateWindowFormView
LayoutSelection *LayoutToolLayoutsList
} }
func NewLayoutToolActionView() *LayoutToolActionView { func NewLayoutToolActionView() *LayoutToolActionView {
@@ -173,6 +156,18 @@ func (ltv *LayoutToolView) ShowWindowFormByID(idx int16) {
) )
} }
func (ltv *LayoutToolView) ShowLayoutList() {
AddAndShowPage(
ltv.LayoutActions.Pages,
"available-layouts-list",
ltv.LayoutActions.LayoutSelection.Tree,
)
}
func (ltv *LayoutToolView) DeleteLayoutList() {
ltv.LayoutActions.Pages.RemovePage("available-layouts-list")
}
func (ltv *LayoutToolView) DeleteWindowByNode(node *tview.TreeNode) { func (ltv *LayoutToolView) DeleteWindowByNode(node *tview.TreeNode) {
root := ltv.LayoutTree.Tree.GetRoot() root := ltv.LayoutTree.Tree.GetRoot()
if root == nil { if root == nil {
@@ -199,7 +194,7 @@ func (ltv *LayoutToolView) UpdateFormView(idx int16, win *cdashdisplay.DesktopUI
return return
} }
node := FindNodeByID(root, idx) node := tviewh.FindNodeByID(root, idx)
if node == nil { if node == nil {
return return
} }
+1
View File
@@ -87,6 +87,7 @@ func NewCreateWindowFormView() *CreateWindowFormView {
Form: NewCDashDisplayWindowFormView(), Form: NewCDashDisplayWindowFormView(),
} }
// NOTE: is this variable actually used anywhere
view.CreateBtn = tview.NewButton("Create") view.CreateBtn = tview.NewButton("Create")
// Need to inject the button functionality later // Need to inject the button functionality later
@@ -0,0 +1,45 @@
package views
import (
"os"
"github.com/rivo/tview"
)
type LayoutToolLayoutsList struct {
Tree *tview.TreeView
}
func NewLayoutToolLayoutsList() *LayoutToolLayoutsList {
view := &LayoutToolLayoutsList{}
view.Tree = tview.NewTreeView()
view.Tree.SetBorder(true).SetTitle("Layout List")
// Inject InputCapture
// Inject ChangedFunc
// Inject SelectedFunc
// Create a root element
rootElem := tview.NewTreeNode(".")
view.Tree.SetRoot(rootElem).SetCurrentNode(rootElem)
return view
}
// AddEntries adds the list of files we wish the user to select from
// For now we will send just a list of []string but in the future this should
// receive (or have an alternative to receive) a directory structure
func (ltll *LayoutToolLayoutsList) AddEntries(entries []os.DirEntry) {
root := ltll.Tree.GetRoot()
if root == nil {
// NOTE: shouldn't happen at all
return
}
for _, e := range entries {
node := tview.NewTreeNode(e.Name()).
SetReference(e.Name())
root.AddChild(node)
}
}
-25
View File
@@ -51,9 +51,6 @@ type DeviceAPIListView struct {
func NewDeviceAPIListView() *DeviceAPIListView { func NewDeviceAPIListView() *DeviceAPIListView {
deviceAPIList := tview.NewList() deviceAPIList := tview.NewList()
// AddItem("layout", "build a layout for CDashDisplay", 0, func() {
// layoutToolUIOnSelect(bus, doc)
// })
deviceAPIList.SetBorder(true).SetTitle("list") deviceAPIList.SetBorder(true).SetTitle("list")
return &DeviceAPIListView{ return &DeviceAPIListView{
@@ -75,40 +72,18 @@ func (dl *DeviceAPIListView) AddItem(name, description string, onSelect func())
} }
func NewDeviceAPIView() (*DeviceAPIView, error) { func NewDeviceAPIView() (*DeviceAPIView, error) {
// To build the main view we must set the DOM root
mainFlex := tview.NewFlex().SetDirection(tview.FlexColumn) mainFlex := tview.NewFlex().SetDirection(tview.FlexColumn)
// mainFlexUINode, err := doc.NewUINode(MainFlexID, nil, mainFlex)
// if err != nil {
// return nil, err
// }
//
// doc.SetRoot(mainFlexUINode)
deviceAPIList := NewDeviceAPIListView() deviceAPIList := NewDeviceAPIListView()
// apiListWindowUINode, err := doc.NewUINode(DeviceAPIListID, doc.GetRootElem(),
// deviceAPIList.List)
// if err != nil {
// return nil, err
// }
apiToolPages := NewDeviceAPIToolView() apiToolPages := NewDeviceAPIToolView()
// apiToolPagesNode, err := doc.NewUINode(APIToolPagesID, doc.GetElemByID(RightFlexID),
// apiToolPages.Pages)
// if err != nil {
// return nil, err
// }
mainFlex. mainFlex.
AddItem(deviceAPIList.List, 0, 1, false). AddItem(deviceAPIList.List, 0, 1, false).
AddItem(apiToolPages.Pages, 0, 4, false) AddItem(apiToolPages.Pages, 0, 4, false)
// Output window // Output window
outputWin := NewOutputWinView() outputWin := NewOutputWinView()
// _, err = doc.NewUINode("output-window", nil, outputWin.TextArea)
// if err != nil {
// return nil, err
// }
// Flex with debug window // Flex with debug window
// -------------------------------------------------------------------------- // --------------------------------------------------------------------------
+75 -6
View File
@@ -1,11 +1,13 @@
package views package views
import ( import (
telem "esdi/telemetry"
"fmt" "fmt"
"strings" "strings"
"time" "time"
"esdi/providers"
telem "esdi/telemetry"
"github.com/rivo/tview" "github.com/rivo/tview"
) )
@@ -17,20 +19,58 @@ const (
BrakeBiasLen = 6 BrakeBiasLen = 6
) )
type StreamView struct { // Form to select the game and whatnot ↓↓↓↓
// StreamOptionsView will be our view element to show the stream data
type StreamOptionsView struct {
Form *tview.Form
SimDropdown *tview.DropDown
UpdateBtn *tview.Button
}
func NewStreamOptionsView(providerList []providers.Provider, defaultProvider string,
) *StreamOptionsView {
sov := &StreamOptionsView{}
sov.Form = tview.NewForm()
sov.Form.SetTitle("Stream Options").SetBorder(true)
defaultIdx := 0
sov.SimDropdown = tview.NewDropDown().SetLabel("SIM").SetCurrentOption(0)
for k, prov := range providerList {
sov.SimDropdown.AddOption(prov.Name, func() {})
if prov.Name == defaultProvider {
defaultIdx = k
}
}
sov.SimDropdown.SetCurrentOption(defaultIdx)
sov.Form.AddFormItem(sov.SimDropdown)
// Inject callback on the controller
sov.Form.AddButton("Update", func() {})
return sov
}
// Form to select the game and whatnot ↑↑↑↑
// Area to visualize what data is being passed to the game and whatnot ↓↓↓↓
type StreamVisualizerView struct {
TextView *tview.TextView TextView *tview.TextView
} }
func NewStreamView() *StreamView { func NewStreamVisualizerView() *StreamVisualizerView {
tv := tview.NewTextView() tv := tview.NewTextView()
tv.SetTitle("Streaming Tool").SetBorder(true) tv.SetTitle("Streaming Visualizer").SetBorder(true)
return &StreamView{ return &StreamVisualizerView{
TextView: tv, TextView: tv,
} }
} }
func (sv *StreamView) Update(data *telem.TelemetryData) { func (sv *StreamVisualizerView) Update(data *telem.TelemetryData) {
sv.TextView.SetText(stringify(data)) sv.TextView.SetText(stringify(data))
} }
@@ -49,3 +89,32 @@ func stringify(data *telem.TelemetryData) string {
return buffer.String() return buffer.String()
} }
// Area to visualize what data is being passed to the game and whatnot ↑↑↑↑
// Stream Tool ↓↓↓↓
type StreamToolView struct {
Flex *tview.Flex
Options *StreamOptionsView
Visualizer *StreamVisualizerView
}
func NewStreamToolView(providerList []providers.Provider, defaultProvider string) *StreamToolView {
optionsView := NewStreamOptionsView(providerList, defaultProvider)
visualizerView := NewStreamVisualizerView()
flex := tview.NewFlex().SetDirection(tview.FlexColumn)
flex.SetTitle("Streaming Tool")
flex.
AddItem(optionsView.Form, 0, 2, true).
AddItem(visualizerView.TextView, 0, 5, true)
return &StreamToolView{
Flex: flex,
Options: optionsView,
Visualizer: visualizerView,
}
}
// Stream Tool ↑↑↑↑
+5 -4
View File
@@ -2,10 +2,11 @@
package tui package tui
import ( import (
"esdi/tui/internal/controllers"
"esdi/tui/internal/services"
"log/slog" "log/slog"
"esdi/services"
"esdi/tui/internal/controllers"
"github.com/rivo/tview" "github.com/rivo/tview"
) )
@@ -16,14 +17,14 @@ type ControlPanel struct {
} }
func NewControlPanel(logger *slog.Logger) *ControlPanel { func NewControlPanel(logger *slog.Logger) *ControlPanel {
// NOTE: given the services should not be only for the TUI should this be here?
baseController := &controllers.Controller{ baseController := &controllers.Controller{
Logger: logger, Logger: logger,
App: tview.NewApplication(), App: tview.NewApplication(),
} }
devService := services.NewCDashService(logger) devService := services.NewCDashService(logger)
telemService := services.NewTelemetryService(logger, devService). telemService := services.NewTelemetryService(logger, devService)
SetProvider("iRacing")
if telemService == nil { if telemService == nil {
panic("failed to create the telemetry service") panic("failed to create the telemetry service")
} }
View File
-4
View File
@@ -1,4 +0,0 @@
// Package virtualdata will have cool features like:
// - fuel calculator
// - wheel lockup and whatever lights features
package virtualdata
View File