6 Commits
Author SHA1 Message Date
esilva 047deb65e7 Rewrote the function comments to be more in line with go.dev/doc/comments 2026-07-06 23:13:43 +01:00
esilva f1728c11da Fixed an issue where the Pitspeed method wasn't checking the right flag 2026-07-06 23:13:14 +01:00
esilva 245d6ac0e1 deleted some dead code 2026-07-01 14:49:30 +01:00
esilva c3ee9c0f44 Memory usage improvements
Improved the memory usage of the ReadData function. Pretty low footprint
now
2026-07-01 14:39:39 +01:00
esilva 31140b5df4 API fix. Copied something without looking twice and broke it. Fixed
Docstring fixes
2026-06-30 23:52:14 +01:00
esilva 3857f5983f added the API for the outgauge flag fields 2026-06-30 23:47:02 +01:00
4 changed files with 203 additions and 115 deletions
+30 -2
View File
@@ -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
```
+112 -87
View File
@@ -1,11 +1,10 @@
// Package bngsdk defines an API to interact with the BeamNG outgauge data in
// Go
// Package bngsdk defines an API to interact with the BeamNG outgauge data in Go
package bngsdk
import (
"bytes"
"encoding/binary"
"fmt"
"math"
"net"
)
@@ -17,11 +16,10 @@ 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
}
func createUDPConnection(ip string, port int) (*net.UDPConn, *net.UDPAddr, error) {
@@ -40,6 +38,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 +74,18 @@ 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 {
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 +108,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
}
@@ -101,80 +116,59 @@ func Init(ip string, port int) (BeamNGSDK, error) {
// ShowLights - functions to check if a given dash light is on [START]
// ShiftLight returns:
// true if the shift light is on
// false if the shift light is off
// ShiftLight reports whether the shift light is on
func (sdk *BeamNGSDK) ShiftLight() bool {
return sdk.Data.ShowLights&DL_SHIFT != 0
}
// HighBeam returns:
// true if the high beams are on
// false if the high beams are off
// HighBeam reports whether high beams are on
func (sdk *BeamNGSDK) HighBeam() bool {
return sdk.Data.ShowLights&DL_FULLBEAM != 0
}
// Handbrake returns:
// true if the handbrake is pulled
// false if the handbrake is down
// Handbrake reports whether the handbrake is pulled
func (sdk *BeamNGSDK) Handbrake() bool {
return sdk.Data.ShowLights&DL_HANDBRAKE != 0
}
// Pitspeed returns:
// true if the pit speed limiter is on
// false if the pit speed limiter is off
// Pitspeed reports whether the pit speed limiter is engaged
//
// NOTE: this may not be used in BeamNG.drive, haven't checked yet
func (sdk *BeamNGSDK) Pitspeed() bool {
return sdk.Data.ShowLights&DL_HANDBRAKE != 0
return sdk.Data.ShowLights&DL_PITSPEED != 0
}
// TractionControl returns:
// true if traction control is on
// false if traction control is off
// TractionControl reports wheter TC is engaged
func (sdk *BeamNGSDK) TractionControl() bool {
return sdk.Data.ShowLights&DL_TC != 0
}
// LeftIndicator returns:
// true if the left indicator is on
// false if the left indicator is off
// LeftIndicator reports whether the left indicator is on
func (sdk *BeamNGSDK) LeftIndicator() bool {
return sdk.Data.ShowLights&DL_SIGNAL_L != 0
}
// RightIndicator returns:
// true if the right indicator is on
// false if the right indicator is off
// RightIndicator reports wheter the right indicator is on
func (sdk *BeamNGSDK) RightIndicator() bool {
return sdk.Data.ShowLights&DL_SIGNAL_R != 0
}
// AnyIndicator returns:
// true if the any indicator is on
// false if the all indicators are off
// AnyIndicator reports whether any indicator is on
func (sdk *BeamNGSDK) AnyIndicator() bool {
return sdk.Data.ShowLights&DL_SIGNAL_ANY != 0
}
// OilLight returns
// true if the oil light is on
// false if the oil light is off
// OilLight reports whether the oil warning light is on
func (sdk *BeamNGSDK) OilLight() bool {
return sdk.Data.ShowLights&DL_OILWARN != 0
}
// BatteryLight returns
// true if the battery light is on
// false if the battery light is off
// BatteryLight reports whether the battery light is on
func (sdk *BeamNGSDK) BatteryLight() bool {
return sdk.Data.ShowLights&DL_BATTERY != 0
}
// ABS returns
// true if the ABS is engaged
// false if the ABS isn't engaged
// ABS reports whether the ABS light is on
func (sdk *BeamNGSDK) ABS() bool {
return sdk.Data.ShowLights&DL_ABS != 0
}
@@ -183,82 +177,113 @@ func (sdk *BeamNGSDK) ABS() bool {
// DashLights - functions to check if a given dash light is provided [START]
// HasShiftLight returns:
// true if its available
// false if its unavailable
// HasShiftLight reports whether a shift light is available
func (sdk *BeamNGSDK) HasShiftLight() bool {
return sdk.Data.DashLights&DL_SHIFT != 0
}
// HasHighBeamLight returns:
// true if its available
// false if its unavailable
// HasHighBeamLight reports whether a high beam light is available
func (sdk *BeamNGSDK) HasHighBeamLight() bool {
return sdk.Data.DashLights&DL_FULLBEAM != 0
}
// HasHandbrakeLight returns:
// true if its available
// false if its unavailable
// HasHandbrakeLight reports wheter a handbrake light is available
func (sdk *BeamNGSDK) HasHandbrakeLight() bool {
return sdk.Data.DashLights&DL_HANDBRAKE != 0
}
// HasPitspeed returns:
// true if its available
// false if its unavailable
// HasPitspeed reports whether a pit speed limitr is available
// NOTE: this may not be used in BeamNG.drive, haven't checked yet
func (sdk *BeamNGSDK) HasPitspeed() bool {
return sdk.Data.DashLights&DL_HANDBRAKE != 0
}
// HasTractionControlLight returns:
// true if its available
// false if its unavailable
// HasTractionControlLight reports whether a traction control light is available
func (sdk *BeamNGSDK) HasTractionControlLight() bool {
return sdk.Data.DashLights&DL_TC != 0
}
// HasLeftIndicatorLight returns:
// true if its available
// false if its unavailable
// HasLeftIndicatorLight reports whether a left indicator is available
func (sdk *BeamNGSDK) HasLeftIndicatorLight() bool {
return sdk.Data.DashLights&DL_SIGNAL_L != 0
}
// HasRightIndicatorLight returns:
// true if its available
// false if its unavailable
// HasRightIndicatorLight reports whether a right indicator is available
func (sdk *BeamNGSDK) HasRightIndicatorLight() bool {
return sdk.Data.DashLights&DL_SIGNAL_R != 0
}
// HasAnyIndicatorLight returns:
// true if its available
// false if its unavailable
// HasAnyIndicatorLight reports whether an any indicator light is available
func (sdk *BeamNGSDK) HasAnyIndicatorLight() bool {
return sdk.Data.DashLights&DL_SIGNAL_ANY != 0
}
// HasOilLight returns:
// true if its available
// false if its unavailable
// HasOilLight reports whether a oil light is available
func (sdk *BeamNGSDK) HasOilLight() bool {
return sdk.Data.DashLights&DL_OILWARN != 0
}
// HasBatteryLight returns:
// true if its available
// false if its unavailable
// HasBatteryLight reports whether a battery light is available
func (sdk *BeamNGSDK) HasBatteryLight() bool {
return sdk.Data.DashLights&DL_BATTERY != 0
}
// HasABSLight returns:
// true if its available
// false if its unavailable
// HasABSLight reports whether an ABS light is available
func (sdk *BeamNGSDK) HasABSLight() bool {
return sdk.Data.DashLights&DL_ABS != 0
}
// DashLights - functions to check if a given dash light is provided [END]
// Flags - functions to check if a given flag is ON [START]
// HasTurbo reports whether there's a turbo
func (sdk *BeamNGSDK) HasTurbo() bool {
return sdk.Data.Flags&OG_TURBO != 0
}
// PrefersKm reports whether the user prefers kilometers:
// - true is prefers Km
// - false is prefers Mi
func (sdk *BeamNGSDK) PrefersKm() bool {
return sdk.Data.Flags&OG_KM != 0
}
// PrefersBAR reports whether the user prefers BAR:
// - true is prefers BAR
// - false is prefers PSI
func (sdk *BeamNGSDK) PrefersBAR() bool {
return sdk.Data.Flags&OG_BAR != 0
}
// 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]
+57
View File
@@ -0,0 +1,57 @@
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()
b.ResetTimer()
b.ReportAllocs()
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)
}
}
}
+4 -26
View File
@@ -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
}
}