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
309 lines
7.5 KiB
Go
309 lines
7.5 KiB
Go
package telemetry
|
|
|
|
import (
|
|
"math"
|
|
"strconv"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// NOTE: allow the user to create custom data things. For example, iRacing provides
|
|
// multiple surface temps, but I guess the user doesn't want all of them at once.
|
|
// allow him to make something that allows some data transformation to occur.
|
|
|
|
type FieldMapper struct {
|
|
SDKKey string
|
|
DataType DataType
|
|
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
|
|
// 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
|
|
// NOTE: we must deprecate the `Key` member. We used it to map the internal
|
|
// telemetry fields to the providers name. But we shouldn't be using `string` keys.
|
|
type BoundField struct {
|
|
Key string
|
|
ID FieldID
|
|
Fetch func() any
|
|
Transform func(any, *TelemetryField)
|
|
}
|
|
|
|
var bufferPool = sync.Pool{
|
|
New: func() any {
|
|
b := make([]byte, 0, MaxFields*8)
|
|
return &b
|
|
},
|
|
}
|
|
|
|
type DataType uint8
|
|
|
|
const (
|
|
DataTypeUINT8 DataType = 0
|
|
DataTypeINT8 DataType = 1
|
|
DataTypeUINT16 DataType = 2
|
|
DataTypeINT16 DataType = 3
|
|
DataTypeUINT32 DataType = 4
|
|
DataTypeINT32 DataType = 5
|
|
DataTypeUINT64 DataType = 6
|
|
DataTypeINT64 DataType = 7
|
|
DataTypeSTRING DataType = 8
|
|
DataTypeCHAR DataType = 9
|
|
)
|
|
|
|
// TelemetryField will be the basic unit to hold telemetry data values in our
|
|
// application.
|
|
// From my testing, using an uint64 bucket is around 50x faster than any/interface{}
|
|
// The IDs member is a list so that we can have multiple items on the target device
|
|
// consuming the same piece of data
|
|
// NOTE: we can optimize this via a special command that says a given piece of data
|
|
// is for multiple targets
|
|
type TelemetryField struct {
|
|
IDs []int16 // Identification for the serial device
|
|
Type DataType
|
|
Raw uint64
|
|
Str string // Only to be used with DataTypeSTRING
|
|
}
|
|
|
|
// Pack will pack this current TelemetryField into bytes to send over the wire
|
|
// Format:
|
|
// 0x00 - Field ID
|
|
// 0x00 |
|
|
// 0x01 - DataType
|
|
// 0x02 - if its a (u)int8
|
|
// or
|
|
// 0x02 - if its a (u)int16 - first byte
|
|
// 0x02 - if its a (u)int16 - second byte
|
|
// or
|
|
// 0x02 - str len max is 255 chars
|
|
// [0x02] - str
|
|
func (tf *TelemetryField) Pack(dest []byte) []byte {
|
|
// NOTE: maybe we can have a pool of these so we don't have to create them here
|
|
// or whatever
|
|
for _, id := range tf.IDs {
|
|
dest = append(dest, uint8(id), uint8(id>>8))
|
|
dest = append(dest, uint8(tf.Type))
|
|
|
|
switch tf.Type {
|
|
case DataTypeINT8, DataTypeUINT8, DataTypeCHAR:
|
|
dest = append(dest, uint8(tf.Raw))
|
|
case DataTypeINT16, DataTypeUINT16:
|
|
dest = append(dest, uint8(tf.Raw), uint8(tf.Raw>>8))
|
|
case DataTypeINT32, DataTypeUINT32:
|
|
dest = append(dest, uint8(tf.Raw), uint8(tf.Raw>>8), uint8(tf.Raw>>16), uint8(tf.Raw>>24))
|
|
case DataTypeINT64, DataTypeUINT64:
|
|
dest = append(dest, uint8(tf.Raw), uint8(tf.Raw>>8), uint8(tf.Raw>>16),
|
|
uint8(tf.Raw>>24), uint8(tf.Raw>>32), uint8(tf.Raw>>40), uint8(tf.Raw>>48),
|
|
uint8(tf.Raw>>56),
|
|
)
|
|
case DataTypeSTRING:
|
|
l := min(len(tf.Str), math.MaxUint8)
|
|
|
|
dest = append(dest, uint8(l))
|
|
dest = append(dest, tf.Str[:l]...)
|
|
}
|
|
}
|
|
|
|
return dest
|
|
}
|
|
|
|
func (tf *TelemetryField) String() string {
|
|
switch tf.Type {
|
|
case DataTypeSTRING:
|
|
return tf.Str
|
|
case DataTypeCHAR:
|
|
return string([]byte{byte(tf.Raw)})
|
|
case DataTypeUINT16, DataTypeUINT8:
|
|
return strconv.FormatUint(tf.Raw, 10)
|
|
case DataTypeINT8:
|
|
return strconv.FormatInt(int64(int16(tf.Raw)), 10)
|
|
case DataTypeINT16:
|
|
return strconv.FormatInt(int64(int(tf.Raw)), 10)
|
|
}
|
|
|
|
return "NaN"
|
|
}
|
|
|
|
type FieldID uint16
|
|
|
|
// We use FirstField to start the count on the fields the user can select
|
|
// the first three will be for internal use
|
|
const (
|
|
Speed FieldID = iota
|
|
RPM
|
|
Gear
|
|
FuelLevel
|
|
// Engine Data
|
|
OilPress
|
|
OilTemp
|
|
WaterTemp
|
|
// Engine Warnings
|
|
PitSpeedLimiter
|
|
// Electrics (dash lights and so on)
|
|
LeftIndicator
|
|
RightIndicator
|
|
Hazards
|
|
ABSWarningLight
|
|
ParkingBrakeLight
|
|
TCLight
|
|
BatteryLight
|
|
// Adjustements
|
|
BrakeBias
|
|
ABSSetting
|
|
TCSetting
|
|
ThrottleSetting
|
|
// Lap Data
|
|
LapLastLapTime
|
|
LapNumber
|
|
// Tire Data
|
|
LFtempL
|
|
LFtempM
|
|
LFtempR
|
|
RFtempL
|
|
RFtempM
|
|
RFtempR
|
|
LRtempL
|
|
LRtempM
|
|
LRtempR
|
|
RRtempL
|
|
RRtempM
|
|
RRtempR
|
|
// Session Data
|
|
SessionTime
|
|
ReplaySessionTime
|
|
Empty
|
|
// Virtual Fields -- fields derived from primitive fields
|
|
// RPM Dash Lights
|
|
RPMStateColour
|
|
// Fuel Calculator
|
|
FCCurrentLap
|
|
FCLastLap
|
|
FCAverage
|
|
FCExpectedLaps
|
|
MaxFields
|
|
)
|
|
const FirstField = Speed
|
|
|
|
var FieldNames = [MaxFields]string{
|
|
Speed: "Speed",
|
|
RPM: "RPM",
|
|
Gear: "Gear",
|
|
FuelLevel: "Fuel Level",
|
|
// Engine Data
|
|
OilPress: "Oil Pressure",
|
|
OilTemp: "Oil Temperature",
|
|
WaterTemp: "Water Temperature",
|
|
// Engine Warnings
|
|
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
|
|
BrakeBias: "BrakeBias",
|
|
ABSSetting: "ABS Control",
|
|
TCSetting: "TC Control",
|
|
ThrottleSetting: "Throttle Control",
|
|
// Lap Data
|
|
LapLastLapTime: "Last Lap Time",
|
|
LapNumber: "Lap Number",
|
|
// Tire Data
|
|
LFtempL: "LF Surface Temp Left",
|
|
LFtempM: "LF Surface Temp Mid",
|
|
LFtempR: "LF Surface Temp Right",
|
|
RFtempL: "RF Surface Temp Left",
|
|
RFtempM: "RF Surface Temp Mid",
|
|
RFtempR: "RF Surface Temp Right",
|
|
LRtempL: "LR Surface Temp Left",
|
|
LRtempM: "LR Surface Temp Mid",
|
|
LRtempR: "LR Surface Temp Right",
|
|
RRtempL: "RR Surface Temp Left",
|
|
RRtempM: "RR Surface Temp Mid",
|
|
RRtempR: "RR Surface Temp Right",
|
|
// Session Data
|
|
SessionTime: "SessionTime",
|
|
ReplaySessionTime: "ReplaySessionTime",
|
|
// Virtual Fields
|
|
// RPM Dash Lights
|
|
RPMStateColour: "RPM State Colour",
|
|
// Fuel Calculator
|
|
FCCurrentLap: "Fuel Current Lap",
|
|
FCLastLap: "Fuel Last Lap",
|
|
FCAverage: "Fuel Average Usage",
|
|
FCExpectedLaps: "Fuel Expected Laps",
|
|
Empty: "Emtpy",
|
|
}
|
|
|
|
func GetFieldName(id FieldID) string {
|
|
if id >= MaxFields {
|
|
return "Unknown"
|
|
}
|
|
|
|
return FieldNames[id]
|
|
}
|
|
|
|
var fieldNameToID map[string]FieldID
|
|
|
|
func initFieldNamesMap() {
|
|
fieldNameToID = make(map[string]FieldID, MaxFields)
|
|
for id, name := range FieldNames {
|
|
fieldNameToID[name] = FieldID(id)
|
|
}
|
|
}
|
|
|
|
func GetFieldID(name string) (FieldID, bool) {
|
|
id, ok := fieldNameToID[name]
|
|
return id, ok
|
|
}
|
|
|
|
// TelemetryData is
|
|
// I need to find a way of having the values be per window or some other
|
|
type TelemetryData struct {
|
|
Values [MaxFields]TelemetryField
|
|
ActiveBinds []BoundField
|
|
VirtualBinds []VirtualField
|
|
InitialTime time.Time
|
|
PenultimateDataPoll time.Time
|
|
LastDataPoll time.Time
|
|
}
|
|
|
|
func NewTelemetryData() *TelemetryData {
|
|
return &TelemetryData{}
|
|
}
|
|
|
|
func (td *TelemetryData) Pack() []byte {
|
|
bufPtr := bufferPool.Get().(*[]byte)
|
|
buf := (*bufPtr)[:0]
|
|
|
|
// for _, bind := range td.ActiveBinds {
|
|
// buf = td.Values[bind.ID].Pack(buf)
|
|
// }
|
|
|
|
for k := range td.Values {
|
|
if len(td.Values[k].IDs) > 0 {
|
|
buf = td.Values[k].Pack(buf)
|
|
}
|
|
}
|
|
|
|
// We have to copy here because we have to return the buffer
|
|
result := make([]byte, len(buf))
|
|
copy(result, buf)
|
|
|
|
bufferPool.Put(&buf)
|
|
|
|
return result
|
|
}
|