288 lines
6.7 KiB
Go
288 lines
6.7 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
|
|
|
|
type BoundField struct {
|
|
Key string
|
|
ID FieldID
|
|
Transform func(any, *TelemetryField)
|
|
}
|
|
|
|
var bufferPool = sync.Pool{
|
|
New: func() any {
|
|
b := make([]byte, 0, MaxFields*8)
|
|
return &b
|
|
},
|
|
}
|
|
|
|
type DataType uint8
|
|
|
|
const (
|
|
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
|
|
// 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",
|
|
// 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
|
|
}
|