Mid stream peripheral connection #16

Merged
esilva merged 2 commits from mid-stream-peripheral-connection into auto-detect-devices 2026-09-23 17:42:19 +01:00
14 changed files with 194 additions and 104 deletions
+7
View File
@@ -1,6 +1,13 @@
package constants package constants
// Provider Names
const ( const (
IRacingProviderName = "iRacing" IRacingProviderName = "iRacing"
BeamNGProviderName = "BeamNG.drive" BeamNGProviderName = "BeamNG.drive"
) )
// Virtual Field Names
const (
FuelCalculatorName = "FuelCalculator"
RPMLightsName = "RPMLights"
)
+59 -33
View File
@@ -28,6 +28,9 @@ type BeamNG struct {
data *telemetry.TelemetryData data *telemetry.TelemetryData
updaters [telemetry.MaxFields]func(*telemetry.TelemetryField) updaters [telemetry.MaxFields]func(*telemetry.TelemetryField)
// Field Subscription management
boundFields map[telemetry.FieldID]bool
// stream control // stream control
wg sync.WaitGroup wg sync.WaitGroup
streamCancel context.CancelFunc streamCancel context.CancelFunc
@@ -51,7 +54,9 @@ func NewBeamNGProvider(logger *slog.Logger, opts *bngsdk.Options) (*BeamNG, erro
data: telemetry.NewTelemetryData(), data: telemetry.NewTelemetryData(),
SDK: beam, SDK: beam,
og: &bngsdk.Outgauge{}, og: &bngsdk.Outgauge{},
ticker: time.NewTicker(time.Second / 60), // Field subscription management
boundFields: make(map[telemetry.FieldID]bool, telemetry.MaxFields),
ticker: time.NewTicker(time.Second / 60),
} }
provider.updaters = [telemetry.MaxFields]func(*telemetry.TelemetryField){ provider.updaters = [telemetry.MaxFields]func(*telemetry.TelemetryField){
@@ -115,47 +120,68 @@ func (b *BeamNG) Stream() (<-chan telemetry.TelemetryData, error) {
return ch, nil return ch, nil
} }
func (b *BeamNG) Subscribe(requestFields []telemetry.FieldID) { // TODO: this function is exactly the same in BeamNG drive now, and I reckon it will be the same
// NOTE: document how the Subscribe funtion works // In plenty other things. I should make it TelemetryData method
slog.Debug(fmt.Sprintf("Len Req: %d\n", len(requestFields))) func (i *BeamNG) subscribe(fields []telemetry.FieldID) []string {
newSubscriptions := make([]string, 0, telemetry.MaxFields)
b.data.ActiveBinds = make([]telemetry.BoundField, 0, len(requestFields)) i.mut.Lock()
defer i.mut.Unlock()
// First we must add the virtual fields for _, id := range fields {
// we will add their dependencies and the primitives to a slice if i.boundFields[id] {
pendingBinds := make([]telemetry.FieldID, telemetry.MaxFields)
for _, id := range requestFields {
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 continue
} }
binding := telemetry.BoundField{ i.data.ActiveBinds[id] = telemetry.BoundField{
ID: id, ID: id,
} }
i.boundFields[id] = true
b.data.ActiveBinds = append(b.data.ActiveBinds, binding) newSubscriptions = append(newSubscriptions, telemetry.FieldNames[id])
boundCheck[id] = true
} }
slog.Debug(fmt.Sprintf("Subscribed: %+v\n", b.data.ActiveBinds)) // unsubscribe from fields we many not need anymore
for key, bound := range i.boundFields {
if _, ok := i.data.ActiveBinds[key]; bound && !ok {
delete(i.data.ActiveBinds, key)
i.boundFields[key] = false
}
}
return newSubscriptions
}
func (b *BeamNG) Subscribe(requestFields []telemetry.FieldID) []string {
// NOTE: document how the Subscribe funtion works
slog.Debug(fmt.Sprintf("Len Req: %d\n", len(requestFields)))
// First we must add the virtual fields
// we will add their dependencies and the primitives to a slice
toBind := make([]telemetry.FieldID, telemetry.MaxFields)
b.mut.Lock()
for _, id := range requestFields {
switch id {
case telemetry.RPMStateColour:
rpmLights := telemetry.NewRPMLights()
b.data.VirtualBinds[rpmLights.Name()] = rpmLights
toBind = append(toBind, rpmLights.EnsureSubscribed()...)
case telemetry.FCCurrentLap:
fuelCalc := telemetry.NewFuelCalculator(b.logger.WithGroup("FUEL CALC"))
b.data.VirtualBinds[fuelCalc.Name()] = fuelCalc
toBind = append(toBind, fuelCalc.EnsureSubscribed()...)
default:
// primitive telemetry field
toBind = append(toBind, id)
}
}
b.mut.Unlock()
newSubs := b.subscribe(toBind)
slog.Debug(fmt.Sprintf("Subscribed: %+v\n", toBind))
return newSubs
} }
// Internal // Internal
+56 -32
View File
@@ -29,13 +29,14 @@ type IRacing struct {
mut sync.Mutex mut sync.Mutex
data *telemetry.TelemetryData data *telemetry.TelemetryData
updaters [telemetry.MaxFields]func(*telemetry.TelemetryField) updaters [telemetry.MaxFields]func(*telemetry.TelemetryField)
// Field Subscription management
boundFields map[telemetry.FieldID]bool
// Timing information // Timing information
ticker *time.Ticker // ticker will keep polling intervals constant ticker *time.Ticker // ticker will keep polling intervals constant
// Stream // Stream
wg sync.WaitGroup wg sync.WaitGroup
// streamCh chan telemetry.TelemetryData
streamCancel context.CancelFunc streamCancel context.CancelFunc
} }
@@ -55,6 +56,8 @@ func NewIRacingProvider(
logger: logger, logger: logger,
SDK: sdk, SDK: sdk,
data: telemetry.NewTelemetryData(), data: telemetry.NewTelemetryData(),
// Field subscription management
boundFields: make(map[telemetry.FieldID]bool, telemetry.MaxFields),
// TODO: make this configurable from the user side // TODO: make this configurable from the user side
ticker: time.NewTicker(time.Second / 60), ticker: time.NewTicker(time.Second / 60),
} }
@@ -225,46 +228,67 @@ func (i *IRacing) StopStream() {
i.streamCancel = nil i.streamCancel = nil
} }
func (i *IRacing) Subscribe(requestFields []telemetry.FieldID) { // TODO: this function is exactly the same in BeamNG drive now, and I reckon it will be the same
i.logger.Debug(fmt.Sprintf("Len Req: %d\n", len(requestFields))) // In plenty other things. I should make it TelemetryData method
func (i *IRacing) subscribe(fields []telemetry.FieldID) []string {
newSubscriptions := make([]string, 0, telemetry.MaxFields)
i.data.ActiveBinds = make([]telemetry.BoundField, 0, len(requestFields)) i.mut.Lock()
defer i.mut.Unlock()
// First we must add the virtual fields for _, id := range fields {
// we will add their dependencies and the primitives to a slice if i.boundFields[id] {
pendingBinds := make([]telemetry.FieldID, 0, telemetry.MaxFields)
for _, id := range requestFields {
switch id {
case telemetry.RPMStateColour:
i.data.VirtualBinds = append(i.data.VirtualBinds, telemetry.NewRPMLights())
case telemetry.FCCurrentLap:
i.data.VirtualBinds = append(i.data.VirtualBinds,
telemetry.NewFuelCalculator(i.logger.WithGroup("FUEL CALC")))
default:
// primitive telemetry field
pendingBinds = append(pendingBinds, id)
}
}
boundCheck := make(map[telemetry.FieldID]bool)
// Now that we know all the fields we need to bind we follow the binding procedure
for _, id := range pendingBinds {
// Check if we already bound this FieldID
if boundCheck[id] {
continue continue
} }
binding := telemetry.BoundField{ i.data.ActiveBinds[id] = telemetry.BoundField{
ID: id, ID: id,
} }
i.boundFields[id] = true
i.data.ActiveBinds = append(i.data.ActiveBinds, binding) newSubscriptions = append(newSubscriptions, telemetry.FieldNames[id])
boundCheck[id] = true
} }
// unsubscribe from fields we many not need anymore
for key, bound := range i.boundFields {
if _, ok := i.data.ActiveBinds[key]; bound && !ok {
delete(i.data.ActiveBinds, key)
i.boundFields[key] = false
}
}
return newSubscriptions
}
func (i *IRacing) Subscribe(requestFields []telemetry.FieldID) []string {
i.logger.Debug(fmt.Sprintf("Len Req: %d\n", len(requestFields)))
// First we must add the virtual fields
// we will add their dependencies and the primitives to a slice
toBind := make([]telemetry.FieldID, 0, telemetry.MaxFields)
i.mut.Lock()
for _, id := range requestFields {
switch id {
case telemetry.RPMStateColour:
rpmLights := telemetry.NewRPMLights()
i.data.VirtualBinds[rpmLights.Name()] = rpmLights
toBind = append(toBind, rpmLights.EnsureSubscribed()...)
case telemetry.FCCurrentLap:
fuelCalc := telemetry.NewFuelCalculator(i.logger.WithGroup("FUEL CALC"))
i.data.VirtualBinds[fuelCalc.Name()] = fuelCalc
toBind = append(toBind, fuelCalc.EnsureSubscribed()...)
default:
// primitive telemetry field
toBind = append(toBind, id)
}
}
i.mut.Unlock()
newSubs := i.subscribe(toBind)
i.logger.Debug(fmt.Sprintf("Subscribed: %+v\n", i.data.ActiveBinds)) i.logger.Debug(fmt.Sprintf("Subscribed: %+v\n", i.data.ActiveBinds))
return newSubs
} }
func (i *IRacing) Name() string { func (i *IRacing) Name() string {
+12 -2
View File
@@ -2,6 +2,7 @@ package services
import ( import (
"context" "context"
"fmt"
"log/slog" "log/slog"
"sync/atomic" "sync/atomic"
@@ -24,7 +25,8 @@ type DeviceService struct {
Messages chan string Messages chan string
// Callbacks // Callbacks
// Telemetry service data fetchers // Telemetry service data fetchers
telemetryProvider func() (string, error) telemetryProvider func() (string, error)
triggerFieldSubscription func([]telemetry.FieldID) []string
} }
func NewDeviceService(logger *slog.Logger, msg chan string) *DeviceService { func NewDeviceService(logger *slog.Logger, msg chan string) *DeviceService {
@@ -43,6 +45,7 @@ func NewDeviceService(logger *slog.Logger, msg chan string) *DeviceService {
// Set the callbacks for PSS // Set the callbacks for PSS
dev.PSS.telemetryProvider = dev.getTelemetryProvider dev.PSS.telemetryProvider = dev.getTelemetryProvider
dev.PSS.onPeripheralConfigured = dev.peripheralConfigured
return dev return dev
} }
@@ -59,7 +62,7 @@ func (ds *DeviceService) GetDevices() []peripheral.Peripheral {
peripherals := make([]peripheral.Peripheral, 0, len(snapshot)) peripherals := make([]peripheral.Peripheral, 0, len(snapshot))
for _, state := range snapshot { for _, state := range snapshot {
if state.State < DeviceIsConnected { if state.State < DeviceIsConfigured {
continue continue
} }
peripherals = append(peripherals, state.Peripheral) peripherals = append(peripherals, state.Peripheral)
@@ -95,6 +98,8 @@ func (ds *DeviceService) StopStream() {
return return
} }
ds.PSS.OnStopStream()
ds.streamCancel() ds.streamCancel()
ds.streamCancel = nil ds.streamCancel = nil
} }
@@ -145,5 +150,10 @@ func (ds *DeviceService) transmit(ctx context.Context) {
} }
// Callbacks [START] ----------------------------------------------------------- // Callbacks [START] -----------------------------------------------------------
func (ds *DeviceService) peripheralConfigured(pname string, fields []telemetry.FieldID) {
// We need to retrigger field subscription here
subscribedTo := ds.triggerFieldSubscription(fields)
ds.Messages <- fmt.Sprintf("subscribed to fields: %q\n", subscribedTo)
}
// Callbacks [END] ------------------------------------------------------------- // Callbacks [END] -------------------------------------------------------------
+21 -3
View File
@@ -10,6 +10,7 @@ import (
"esdi/devices" "esdi/devices"
"esdi/peripheral" "esdi/peripheral"
"esdi/telemetry"
) )
var ( var (
@@ -85,7 +86,8 @@ type PeripheralStateStore struct {
// Messaging for UI and stuff // Messaging for UI and stuff
Messages chan string Messages chan string
// Callbacks // Callbacks
telemetryProvider func() (string, error) telemetryProvider func() (string, error)
onPeripheralConfigured func(string, []telemetry.FieldID)
// Internal State // Internal State
isStreaming bool isStreaming bool
} }
@@ -183,6 +185,15 @@ func (pss *PeripheralStateStore) GetStreamingState() bool {
return pss.isStreaming return pss.isStreaming
} }
func (pss *PeripheralStateStore) GetPeripheralFields(pname string) []telemetry.FieldID {
per, err := pss.GetState(pname)
if err != nil {
return nil
}
return per.Peripheral.RequiredFields()
}
// "Events" [START] ------------------------------------------------------------ // "Events" [START] ------------------------------------------------------------
func (ds *DeviceService) onDeviceTimedOut(pname string) { func (ds *DeviceService) onDeviceTimedOut(pname string) {
ds.PSS.setDeviceTimedOut(pname) ds.PSS.setDeviceTimedOut(pname)
@@ -204,6 +215,12 @@ func (pss *PeripheralStateStore) OnStopStream() {
pss.mu.Lock() pss.mu.Lock()
pss.isStreaming = false pss.isStreaming = false
pss.mu.Unlock() pss.mu.Unlock()
for _, state := range pss.GetStates() {
if state.State == DeviceIsStreaming {
pss.setDeviceConfigured(state.device.Name)
}
}
} }
// "Events" [END] -------------------------------------------------------------- // "Events" [END] --------------------------------------------------------------
@@ -270,8 +287,10 @@ func (pss *PeripheralStateStore) setDeviceConfigured(pname string) {
pss.Logger.Info("device is configured and ready for data", "device", pname) pss.Logger.Info("device is configured and ready for data", "device", pname)
pss.mu.Lock() pss.mu.Lock()
defer pss.mu.Unlock()
pss.store[pname].State = DeviceIsConfigured pss.store[pname].State = DeviceIsConfigured
pss.mu.Unlock()
pss.onPeripheralConfigured(pname, pss.GetPeripheralFields(pname))
} }
func (pss *PeripheralStateStore) setDeviceIsStreaming(pname string) { func (pss *PeripheralStateStore) setDeviceIsStreaming(pname string) {
@@ -338,7 +357,6 @@ func (pss *PeripheralStateStore) configurePeripheral(
} }
onSuccess(pname) onSuccess(pname)
pss.setDeviceConfigured(pname)
}() }()
} }
+2 -1
View File
@@ -26,9 +26,10 @@ func NewOrchestrator(logger *slog.Logger) (*Orchestrator, error) {
// Setup device service callbacks // Setup device service callbacks
devService.telemetryProvider = telemService.GetTelemetryProviderName devService.telemetryProvider = telemService.GetTelemetryProviderName
devService.triggerFieldSubscription = telemService.SubscribeToFields
// Setup telemetry service callbacks // Setup telemetry service callbacks
telemService.peripheralProvider = devService.GetDevices // telemService.getRequiredFields = devService.GetRequiredFields
return &Orchestrator{ return &Orchestrator{
DeviceService: devService, DeviceService: devService,
+6 -18
View File
@@ -7,7 +7,6 @@ import (
"sync" "sync"
"time" "time"
"esdi/peripheral"
"esdi/providers" "esdi/providers"
"esdi/telemetry" "esdi/telemetry"
telem "esdi/telemetry" telem "esdi/telemetry"
@@ -37,7 +36,8 @@ type TelemetryService struct {
healthCheckCancel context.CancelFunc healthCheckCancel context.CancelFunc
// Callbacks // Callbacks
// Devices data request // Devices data request
peripheralProvider func() []peripheral.Peripheral // peripheralProvider func() []peripheral.Peripheral
// getRequiredFields func() []telemetry.FieldID
} }
func NewTelemetryService( func NewTelemetryService(
@@ -114,23 +114,11 @@ func (t *TelemetryService) UnsubscribeListener(id string) {
} }
} }
func (t *TelemetryService) SubscribeToFields() []telem.FieldID { func (t *TelemetryService) SubscribeToFields(fields []telemetry.FieldID) []string {
seen := make(map[telemetry.FieldID]struct{}) t.logger.Debug("requested fields", "fields", fields)
var allFields []telemetry.FieldID subscribed := t.activeProvider.Subscribe(fields)
for _, dev := range t.peripheralProvider() { return subscribed
for _, field := range dev.RequiredFields() {
if _, exists := seen[field]; !exists {
seen[field] = struct{}{}
allFields = append(allFields, field)
}
}
}
t.logger.Debug("requested fields", "fields", allFields)
t.activeProvider.Subscribe(allFields)
return allFields
} }
// Listener Control [END] ------------------------------------------------------ // Listener Control [END] ------------------------------------------------------
+6 -11
View File
@@ -23,13 +23,6 @@ type FieldMapper struct {
Transform func(any) uint64 Transform func(any) uint64
} }
// VirtualField will derive data from telemetry primitives
// So fuel per lap predictions, compound gauge lights and so on
type VirtualField interface {
Process(td *TelemetryData)
EnsureSubscribed() []FieldID
}
// 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
@@ -228,16 +221,18 @@ func GetFieldID(name string) (FieldID, bool) {
} }
// TelemetryData is // TelemetryData is
// I need to find a way of having the values be per window or some other
type TelemetryData struct { type TelemetryData struct {
Values [MaxFields]TelemetryField Values [MaxFields]TelemetryField
ActiveBinds []BoundField ActiveBinds map[FieldID]BoundField
VirtualBinds []VirtualField VirtualBinds map[string]VirtualField
InitialTime time.Time InitialTime time.Time
PenultimateDataPoll time.Time PenultimateDataPoll time.Time
LastDataPoll time.Time LastDataPoll time.Time
} }
func NewTelemetryData() *TelemetryData { func NewTelemetryData() *TelemetryData {
return &TelemetryData{} return &TelemetryData{
ActiveBinds: make(map[FieldID]BoundField, MaxFields),
VirtualBinds: make(map[string]VirtualField),
}
} }
+6
View File
@@ -5,6 +5,8 @@ import (
"log/slog" "log/slog"
"strconv" "strconv"
"sync" "sync"
"esdi/constants"
) )
// NOTE: for managing fuel consumption and predictions we need to filter out // NOTE: for managing fuel consumption and predictions we need to filter out
@@ -264,3 +266,7 @@ func (fc *FuelCalculator) resetHistory() {
func (fc *FuelCalculator) EnsureSubscribed() []FieldID { func (fc *FuelCalculator) EnsureSubscribed() []FieldID {
return []FieldID{FuelLevel, LapNumber} return []FieldID{FuelLevel, LapNumber}
} }
func (fc *FuelCalculator) Name() string {
return constants.FuelCalculatorName
}
+1 -1
View File
@@ -6,7 +6,7 @@ import "time"
type TelemetryProvider interface { type TelemetryProvider interface {
StopStream() StopStream()
Stream() (<-chan TelemetryData, error) Stream() (<-chan TelemetryData, error)
Subscribe([]FieldID) Subscribe([]FieldID) []string
IsAlive(time.Duration) bool IsAlive(time.Duration) bool
Name() string Name() string
Close() Close()
+6
View File
@@ -1,5 +1,7 @@
package telemetry package telemetry
import "esdi/constants"
type RPMLights struct { type RPMLights struct {
State string State string
} }
@@ -28,3 +30,7 @@ func (rl *RPMLights) Process(td *TelemetryData) {
func (rl *RPMLights) EnsureSubscribed() []FieldID { func (rl *RPMLights) EnsureSubscribed() []FieldID {
return []FieldID{RPM} return []FieldID{RPM}
} }
func (rl *RPMLights) Name() string {
return constants.RPMLightsName
}
+9
View File
@@ -0,0 +1,9 @@
package telemetry
// VirtualField will derive data from telemetry primitives
// So fuel per lap predictions, compound gauge lights and so on
type VirtualField interface {
Name() string
Process(td *TelemetryData)
EnsureSubscribed() []FieldID
}
+1 -1
View File
@@ -93,7 +93,7 @@ func (mc *DeviceController) AddDeviceAPIListItems() {
mc.StreamCtrl.StreamView.Flex, mc.StreamCtrl.StreamView.Flex,
) )
mc.StreamCtrl.SetInternalState() // mc.StreamCtrl.SetInternalState()
mc.App.SetFocus(mc.StreamCtrl.StreamView.Options.Form) mc.App.SetFocus(mc.StreamCtrl.StreamView.Options.Form)
}) })
+2 -2
View File
@@ -154,8 +154,8 @@ func (sc *StreamingCtrl) updateStream() {
// Performance reasoning: this is not used during the high frequency data transmission // Performance reasoning: this is not used during the high frequency data transmission
// so we can get away with using a map for convenience here // so we can get away with using a map for convenience here
func (sc *StreamingCtrl) SetInternalState() { func (sc *StreamingCtrl) SetInternalState() {
fields := sc.TelemServ.SubscribeToFields() // fields := sc.TelemServ.SubscribeToAllFields()
sc.Messages <- fmt.Sprintf("Subscribed to fields: %+v\n", fields) // sc.Messages <- fmt.Sprintf("Subscribed to fields: %+v\n", fields)
} }
func (sc *StreamingCtrl) listenToUIStream() { func (sc *StreamingCtrl) listenToUIStream() {