diff --git a/README.md b/README.md index 61816d0..80cce45 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,32 @@ # BeamNG SDK Simple SDK to interact with BeamNG.drive OutGauge data. -There's some features missing listed in a comment in the -`outgauge.go` file. I will implement them as necessary. + +## Development +### Performance +`go test -bench=BenchmarkReadData -benchmem -memprofile=mem.pprof` +replace the function to be tested + +Use `go tool pprof` to analyze the results + +#### Results +```bash +# Previous footprint +goos: linux +goarch: amd64 +pkg: github.com/ESilva15/gobngsdk +cpu: AMD Ryzen 7 5800X3D 8-Core Processor +BenchmarkReadData-16 320931 4058 ns/op 100 B/op 2 allocs/op +PASS +ok github.com/ESilva15/gobngsdk 1.345s + +# New footprint +goos: linux +goarch: amd64 +pkg: github.com/ESilva15/gobngsdk +cpu: AMD Ryzen 7 5800X3D 8-Core Processor +BenchmarkReadData-16 362514 3188 ns/op 4 B/op 1 allocs/op +PASS +ok github.com/ESilva15/gobngsdk 1.194s + +# Pretty good enough. I can finally go be productive instead of "procrastinating" here +``` diff --git a/bngsdk.go b/bngsdk.go index 947c410..92b463e 100644 --- a/bngsdk.go +++ b/bngsdk.go @@ -6,6 +6,7 @@ import ( "bytes" "encoding/binary" "fmt" + "math" "net" ) @@ -17,11 +18,12 @@ const ( ) type BeamNGSDK struct { - Addr *net.UDPAddr - Conn *net.UDPConn - Buffer []byte - Data *Outgauge - DataDict map[string]any + Addr *net.UDPAddr + Conn *net.UDPConn + Buffer []byte + Data Outgauge + // Internal data + reader bytes.Reader } func createUDPConnection(ip string, port int) (*net.UDPConn, *net.UDPAddr, error) { @@ -40,6 +42,32 @@ func createUDPConnection(ip string, port int) (*net.UDPConn, *net.UDPAddr, error return conn, addr, nil } +// parseData will parse the bytes read from the socket into the Outgauge struct +func (sdk *BeamNGSDK) parseData() error { + sdk.Data.Time = binary.LittleEndian.Uint32(sdk.Buffer[0:4]) + copy(sdk.Data.Car[:], sdk.Buffer[4:8]) + sdk.Data.Flags = binary.LittleEndian.Uint16(sdk.Buffer[8:10]) + sdk.Data.Gear = int8(sdk.Buffer[10]) + sdk.Data.Plid = int8(sdk.Buffer[11]) + sdk.Data.Speed = math.Float32frombits(binary.LittleEndian.Uint32(sdk.Buffer[12:16])) + sdk.Data.RPM = math.Float32frombits(binary.LittleEndian.Uint32(sdk.Buffer[16:20])) + sdk.Data.Turbo = math.Float32frombits(binary.LittleEndian.Uint32(sdk.Buffer[20:24])) + sdk.Data.EngTemp = math.Float32frombits(binary.LittleEndian.Uint32(sdk.Buffer[24:28])) + sdk.Data.Fuel = math.Float32frombits(binary.LittleEndian.Uint32(sdk.Buffer[28:32])) + sdk.Data.OilPressure = math.Float32frombits(binary.LittleEndian.Uint32(sdk.Buffer[32:36])) + sdk.Data.OilTemp = math.Float32frombits(binary.LittleEndian.Uint32(sdk.Buffer[36:40])) + sdk.Data.DashLights = binary.LittleEndian.Uint32(sdk.Buffer[40:44]) + sdk.Data.ShowLights = binary.LittleEndian.Uint32(sdk.Buffer[44:48]) + sdk.Data.Throttle = math.Float32frombits(binary.LittleEndian.Uint32(sdk.Buffer[48:52])) + sdk.Data.Brake = math.Float32frombits(binary.LittleEndian.Uint32(sdk.Buffer[52:56])) + sdk.Data.Throttle = math.Float32frombits(binary.LittleEndian.Uint32(sdk.Buffer[56:60])) + copy(sdk.Data.Display1[:], sdk.Buffer[60:76]) + copy(sdk.Data.Display2[:], sdk.Buffer[76:92]) + sdk.Data.ID = int32(binary.LittleEndian.Uint32(sdk.Buffer[92:96])) + + return nil +} + // ReadData will read new data from the UDP server func (sdk *BeamNGSDK) ReadData() error { // Receive data from the socket @@ -50,25 +78,19 @@ func (sdk *BeamNGSDK) ReadData() error { } // Check if enough data was received to fill our struct - if n < binary.Size(Outgauge{}) { + if n < outgaugeSize { fmt.Println("Received packet too small for Outgauge struct") return err } // Read the binary data into the struct - // NOTE: create a reader for this struct and then reset it with the data from here - // instead of creating this one everytime - reader := bytes.NewReader(sdk.Buffer[:n]) - if err := binary.Read(reader, binary.LittleEndian, sdk.Data); err != nil { + sdk.reader.Reset(sdk.Buffer[:n]) + err = sdk.parseData() + if err != nil { fmt.Println("Error decoding UDP packet:", err) return err } - // Update the local map - // NOTE: maybe stop doing this here and make the user request this when he - // explicitly wants it - sdk.DataDict = sdk.Data.ToMap() - // this means there's new data return nil } @@ -91,8 +113,6 @@ func Init(ip string, port int) (BeamNGSDK, error) { // Initiate the data variables sdk.Buffer = make([]byte, 1024) - sdk.Data = &Outgauge{} - sdk.DataDict = sdk.Data.ToMap() return sdk, nil } @@ -289,3 +309,33 @@ func (sdk *BeamNGSDK) PrefersBAR() bool { } // Flags - functions to check if a given flag is ON [END] + +// Data Retrieval [START] + +// ToMap creates a map with the data in the Outgauge struct +func (sdk *BeamNGSDK) ToMap() map[string]any { + return map[string]any{ + "Time": sdk.Data.Time, // time in milliseconds (to check order) + "Car": sdk.Data.Car, // Car name + "Flags": sdk.Data.Flags, // Info (see OG_x below) + "Gear": sdk.Data.Gear, // Reverse:0, Neutral:1, First:2... + "Plid": sdk.Data.Plid, // Unique ID of viewed player (0 = none) + "Speed": sdk.Data.Speed, // M/S + "RPM": sdk.Data.RPM, // RPM + "Turbo": sdk.Data.Turbo, // BAR + "EngTemp": sdk.Data.EngTemp, // C + "Fuel": sdk.Data.Fuel, // 0 to 1 + "OilPressure": sdk.Data.OilPressure, // BAR + "OilTemp": sdk.Data.OilTemp, // C + "DashLights": sdk.Data.DashLights, // Dash lights available (see DL_x below) + "ShowLights": sdk.Data.ShowLights, // Dash lights currently switched on + "Throttle": sdk.Data.Throttle, // 0 to 1 + "Brake": sdk.Data.Brake, // 0 to 1 + "Clutch": sdk.Data.Clutch, // 0 to 1 + "Display1": sdk.Data.Display1, // Usually Fuel + "Display2": sdk.Data.Display2, // Usually Settings + "ID": sdk.Data.ID, // optional - only if OutGauge ID is specified + } +} + +// Data Retrieval [END] diff --git a/bngsdk_test.go b/bngsdk_test.go new file mode 100644 index 0000000..854fe45 --- /dev/null +++ b/bngsdk_test.go @@ -0,0 +1,58 @@ +package bngsdk + +import ( + "bytes" + "encoding/binary" + "net" + "testing" +) + +func BenchmarkReadData(b *testing.B) { + // Spin up an UDP server + sdk, err := Init("127.0.0.1", 0) + if err != nil { + b.Fatalf("Failed to initialize SDK: %v", err) + } + defer sdk.Close() + + // Retrieve the actual assigned UDP address + localAddr := sdk.Conn.LocalAddr().(*net.UDPAddr) + + // Start a client to stream data + clientConn, err := net.DialUDP("udp", nil, localAddr) + if err != nil { + b.Fatalf("Failed to dial local UDP socket: %v", err) + } + defer clientConn.Close() + + // Pre serialize some data + dummyOutgauge := Outgauge{ + Time: 424242, + Car: [4]byte{'P', 'E', 'R', 'F'}, + Speed: 45.2, + RPM: 3500.0, + } + var buf bytes.Buffer + if err := binary.Write(&buf, binary.LittleEndian, dummyOutgauge); err != nil { + b.Fatalf("Failed to serialize dummy struct: %v", err) + } + packetBytes := buf.Bytes() + + // 4. Reset the timer to isolate the setup allocations from the loop metrics + b.ResetTimer() + b.ReportAllocs() // Tracks memory allocations per operation (B/op and allocs/op) + + for i := 0; i < b.N; i++ { + // Feed a packet into the network buffer right before reading it + _, err := clientConn.Write(packetBytes) + if err != nil { + b.Fatalf("Failed to write to UDP socket: %v", err) + } + + // Execute the target function + err = sdk.ReadData() + if err != nil { + b.Fatalf("ReadData failed at iteration %d: %v", i, err) + } + } +} diff --git a/outgauge.go b/outgauge.go index 0a882ae..e2dcbab 100644 --- a/outgauge.go +++ b/outgauge.go @@ -1,5 +1,9 @@ package bngsdk +import "encoding/binary" + +var outgaugeSize = binary.Size(Outgauge{}) + // More documentation at https://go.beamng.com/protocols. // Or at BeamNG/lua/vehicle/protocols/outgauge.lua // OG_x buts for flags @@ -50,29 +54,3 @@ type Outgauge struct { Display2 [16]byte // Usually Settings ID int32 // optional - only if OutGauge ID is specified } - -// ToMap creates a map with the data in the Outgauge struct -func (o *Outgauge) ToMap() map[string]any { - return map[string]any{ - "Time": o.Time, // time in milliseconds (to check order) - "Car": o.Car, // Car name - "Flags": o.Flags, // Info (see OG_x below) - "Gear": o.Gear, // Reverse:0, Neutral:1, First:2... - "Plid": o.Plid, // Unique ID of viewed player (0 = none) - "Speed": o.Speed, // M/S - "RPM": o.RPM, // RPM - "Turbo": o.Turbo, // BAR - "EngTemp": o.EngTemp, // C - "Fuel": o.Fuel, // 0 to 1 - "OilPressure": o.OilPressure, // BAR - "OilTemp": o.OilTemp, // C - "DashLights": o.DashLights, // Dash lights available (see DL_x below) - "ShowLights": o.ShowLights, // Dash lights currently switched on - "Throttle": o.Throttle, // 0 to 1 - "Brake": o.Brake, // 0 to 1 - "Clutch": o.Clutch, // 0 to 1 - "Display1": o.Display1, // Usually Fuel - "Display2": o.Display2, // Usually Settings - "ID": o.ID, // optional - only if OutGauge ID is specified - } -}