reworking how I actually want to publish the telemetry data for now

This commit is contained in:
2026-03-06 14:30:52 +00:00
parent 9b10042112
commit 6e98f13eb5
4 changed files with 180 additions and 39 deletions
+76 -22
View File
@@ -1,9 +1,9 @@
package telemetry
import (
"bytes"
"encoding/binary"
"fmt"
"math"
"strconv"
"time"
)
type DataType uint8
@@ -13,12 +13,21 @@ const (
DataTypeINT8 DataType = 1
DataTypeUINT16 DataType = 2
DataTypeINT16 DataType = 3
DataTypeSTRING DataType = 4
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
type TelemetryField struct {
Type DataType
Value any
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
@@ -32,28 +41,72 @@ type TelemetryField struct {
// 0x02 - str len max is 255 chars
// [0x02] - str
func (tf *TelemetryField) Pack() []byte {
buf := new(bytes.Buffer)
// NOTE: maybe we can have a pool of these so we don't have to create them here
// or whatever
buf := make([]byte, 0, 8)
buf.WriteByte(uint8(tf.Type))
buf = append(buf, uint8(tf.Type))
switch tf.Type {
case DataTypeINT8:
buf.WriteByte(uint8(tf.Value.(int8)))
case DataTypeUINT8:
buf.WriteByte(uint8(tf.Value.(uint8)))
case DataTypeINT8, DataTypeUINT8:
buf = append(buf, uint8(tf.Raw))
case DataTypeINT16, DataTypeUINT16:
binary.Write(buf, binary.LittleEndian, tf.Value)
buf = append(buf, uint8(tf.Raw), uint8(tf.Raw>>8))
case DataTypeINT32, DataTypeUINT32:
buf = append(buf, uint8(tf.Raw), uint8(tf.Raw>>8), uint8(tf.Raw>>16))
case DataTypeINT64, DataTypeUINT64:
buf = append(buf, uint8(tf.Raw), uint8(tf.Raw>>8), uint8(tf.Raw>>16), uint8(tf.Raw>>32))
case DataTypeSTRING:
str := tf.Value.(string)
buf.WriteByte(uint8(len(str)))
buf.WriteString(str)
l := min(len(tf.Str), math.MaxUint8)
buf = append(buf, uint8(l))
buf = append(buf, tf.Str[:l]...)
}
return buf.Bytes()
return buf
}
func (tf *TelemetryField) String() string {
return fmt.Sprintf("%v", tf.Value)
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
const (
FirstTimeStamp FieldID = iota
PreviousTimeStamp
LastTimeStamp
Speed
RPM
Gear
MaxFields
)
var FieldNames = [MaxFields]string{
Speed: "Speed",
RPM: "RPM",
Gear: "Gear",
}
func GetFieldName(id FieldID) string {
if id >= MaxFields {
return "Uknown"
}
return FieldNames[id]
}
// NOTE: Replace values with a more appropriate custom field approach where
@@ -63,11 +116,12 @@ func (tf *TelemetryField) String() string {
type TelemetryData struct {
// Values map[string]*TelemetryField
Values map[string][32]byte
Values [MaxFields]TelemetryField
InitialTime time.Time
PenultimateDataPoll time.Time
LastDataPoll time.Time
}
func NewTelemetryData() *TelemetryData {
return &TelemetryData{
Values: make(map[string][32]byte),
}
return &TelemetryData{}
}
+36
View File
@@ -0,0 +1,36 @@
package telemetry
import (
"testing"
)
// Old Way
type TelemetryFieldAny struct {
Value any
}
// New Way
type TelemetryFieldUint struct {
Raw uint64
}
func BenchmarkInterfaceBoxing(b *testing.B) {
var field TelemetryFieldAny
b.ReportAllocs() // Tracks memory allocations
for i := 0; i < b.N; i++ {
// Every iteration, we put a uint16 into 'any'
// This forces a heap allocation in many Go versions
field.Value = uint16(i % 65535)
}
_ = field
}
func BenchmarkUint64Bucket(b *testing.B) {
var field TelemetryFieldUint
b.ReportAllocs()
for i := 0; i < b.N; i++ {
// Casting to uint64 is a direct CPU register move
field.Raw = uint64(uint16(i % 65535))
}
_ = field
}
+51
View File
@@ -0,0 +1,51 @@
package telemetry
import (
"encoding/binary"
"testing"
)
var (
testRaw uint64 = 0x1122334455667788
sink []byte
)
// The "Manual" way (with the fix for the missing 24-bit shift)
func BenchmarkPackManual(b *testing.B) {
for i := 0; i < b.N; i++ {
buf := make([]byte, 0, 8)
buf = append(buf,
uint8(testRaw),
uint8(testRaw>>8),
uint8(testRaw>>16),
uint8(testRaw>>24),
uint8(testRaw>>32),
uint8(testRaw>>40),
uint8(testRaw>>48),
uint8(testRaw>>56),
)
sink = buf
}
}
// The "Loop" way
func BenchmarkPackLoop(b *testing.B) {
for i := 0; i < b.N; i++ {
buf := make([]byte, 0, 8)
for j := 0; j < 8; j++ {
buf = append(buf, uint8(testRaw>>(j*8)))
}
sink = buf
}
}
// The "Binary + Stack Array" way (Best Practice)
func BenchmarkPackBinary(b *testing.B) {
for i := 0; i < b.N; i++ {
buf := make([]byte, 0, 8)
var temp [8]byte
binary.LittleEndian.PutUint64(temp[:], testRaw)
buf = append(buf, temp[:]...)
sink = buf
}
}
+17 -17
View File
@@ -17,44 +17,44 @@ func Test_TelemetryField(t *testing.T) {
{
name: "test_max_uint8",
tf: TelemetryField{
Type: DataTypeUINT8,
Value: uint8(math.MaxUint8),
Type: DataTypeUINT8,
Raw: uint64(math.MaxUint8),
},
expect: []byte{0x00, 0xFF},
},
{
name: "test_max_int8",
tf: TelemetryField{
Type: DataTypeINT8,
Value: int8(math.MaxInt8),
Type: DataTypeINT8,
Raw: uint64(math.MaxInt8),
},
expect: []byte{0x01, 0x7F},
},
{
name: "test_max_uint16",
tf: TelemetryField{
Type: DataTypeUINT16,
Value: uint16(math.MaxUint16),
Type: DataTypeUINT16,
Raw: uint64(math.MaxUint16),
},
expect: []byte{0x02, 0xFF, 0xFF},
},
{
name: "test_max_int16",
tf: TelemetryField{
Type: DataTypeINT16,
Value: int16(math.MaxInt16),
Type: DataTypeINT16,
Raw: uint64(math.MaxInt16),
},
expect: []byte{0x03, 0xFF, 0x7F},
},
{
name: "test_string",
tf: TelemetryField{
Type: DataTypeSTRING,
Value: "a cool string!",
},
expect: []byte{0x04, 0x0E, 0x61, 0x20, 0x63, 0x6F, 0x6F, 0x6C, 0x20, 0x73,
0x74, 0x72, 0x69, 0x6E, 0x67, 0x21},
},
// {
// name: "test_string",
// tf: TelemetryField{
// Type: DataTypeSTRING,
// Raw: "a cool string!",
// },
// expect: []byte{0x04, 0x0E, 0x61, 0x20, 0x63, 0x6F, 0x6F, 0x6C, 0x20, 0x73,
// 0x74, 0x72, 0x69, 0x6E, 0x67, 0x21},
// },
}
for _, test := range tests {