18 Commits
Author SHA1 Message Date
esilva 312d979be1 added some new API calls
mainly around getting the file size and data read for convenience for
other tools
2026-09-16 23:47:35 +01:00
esilva 137b96fae6 Merge pull request 'Update api' (#2) from update-api into master
Reviewed-on: #2
2026-09-16 21:39:30 +01:00
esilva d9b7c185ed update the examples 2026-09-16 21:38:59 +01:00
esilva 6e915ea1e0 return the outgauge for access 2026-09-16 21:36:33 +01:00
esilva d0eb54e6c3 Fixed a bug with the HasPitspeed endpoint 2026-09-16 19:42:35 +01:00
esilva 861a76164b New gitignore 2026-09-16 19:09:06 +01:00
esilva 88370b25be Merge pull request 'Better sdk' (#1) from better-sdk into master
Reviewed-on: #1
2026-09-16 19:07:40 +01:00
esilva ba6b2c700b reduced allocations 2026-09-16 15:06:27 +01:00
esilva ec2ed17f43 made the mechanism resilient enough to detect stalled data 2026-09-16 14:36:04 +01:00
esilva 67b9cf617c added Close() methods to the structs 2026-09-15 17:41:11 +01:00
esilva a7fbf74f0a A better SDK with more features and more consistency 2026-09-15 13:19:48 +01:00
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
esilva 88f50218a4 Function doc fix 2026-06-30 23:42:59 +01:00
14 changed files with 1020 additions and 240 deletions
+4
View File
@@ -0,0 +1,4 @@
*.log
*.test
*.pprof
*.bin
+29 -2
View File
@@ -1,4 +1,31 @@
# 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=BenchmarkUpdate -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 362514 3188 ns/op 4 B/op 1 allocs/op
PASS
ok github.com/ESilva15/gobngsdk 1.194s
# New footprint
goos: linux
goarch: amd64
pkg: github.com/ESilva15/gobngsdk
cpu: AMD Ryzen 7 5800X3D 8-Core Processor
BenchmarkUpdate-16 159170 7287 ns/op 4 B/op 1 allocs/op
PASS
ok github.com/ESilva15/gobngsdk 1.241s
# Pretty good enough. I can finally go be productive instead of "procrastinating" here
```
+140 -199
View File
@@ -1,12 +1,9 @@
// 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"
"net"
"io"
"log/slog"
)
const (
@@ -16,249 +13,193 @@ const (
DefaultUDPPort = 4444
)
type TelemetryContainer int
const (
BinaryFile TelemetryContainer = iota
UDPData TelemetryContainer = iota
)
type Options struct {
Logger *slog.Logger
// Import settings
SourceType TelemetryContainer // type of source data
BinSourcePath string // Path to source binary
ImportUDPAddress string
ImportUDPPort int
Loop bool // Wheter to loop when we reach the end of file
// Export settings
ExportUDPAddress string
ExportUDPPort int
ExportDataType TelemetryContainer // export type of telemetry: store .bin or replay in UDP
ExportDataPath string // path where to export the data
ExportData bool // whether to export the telemetry data
}
type BeamNGSDK struct {
Addr *net.UDPAddr
Conn *net.UDPConn
Buffer []byte
Data *Outgauge
DataDict map[string]any
Opts Options
reader BngImporter
writer BngExporter
data Outgauge
buffer []byte
}
func createUDPConnection(ip string, port int) (*net.UDPConn, *net.UDPAddr, error) {
// Define the IP address and port to listen on
addr := &net.UDPAddr{
IP: net.ParseIP(ip),
Port: port,
func NewBngSDK(opts Options) (*BeamNGSDK, error) {
sdk := BeamNGSDK{
Opts: opts,
buffer: make([]byte, OutgaugeSize),
}
// Create a UDP socket
conn, err := net.ListenUDP("udp", addr)
var err error
err = sdk.openReader()
if err != nil {
return nil, nil, err
slog.Error("failed to open reader", "err", err)
return nil, err
}
return conn, addr, nil
}
// ReadData will read new data from the UDP server
func (sdk *BeamNGSDK) ReadData() error {
// Receive data from the socket
n, _, err := sdk.Conn.ReadFromUDP(sdk.Buffer)
err = sdk.openWriter()
if err != nil {
fmt.Println("Error reading from UDP:", err)
return err
slog.Error("failed to open writer", "err", err)
return nil, err
}
// Check if enough data was received to fill our struct
if n < binary.Size(Outgauge{}) {
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 {
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
return &sdk, nil
}
func (sdk *BeamNGSDK) Close() error {
return sdk.Conn.Close()
sdk.buffer = nil
if sdk.writer != nil {
sdk.writer.Close()
}
// Init initializes a BeamNG SDK struct
// NOTE: Change this to output a *BeamNGSDK
func Init(ip string, port int) (BeamNGSDK, error) {
var err error
sdk := BeamNGSDK{}
if sdk.reader != nil {
sdk.reader.Close()
}
// Create the connection to the OutGauge server
sdk.Conn, sdk.Addr, err = createUDPConnection(ip, port)
return nil
}
func (sdk *BeamNGSDK) openReader() error {
switch sdk.Opts.SourceType {
case BinaryFile:
reader, err := NewBinaryImporter(sdk.Opts.BinSourcePath)
if err != nil {
return BeamNGSDK{}, err
slog.Error("failed to create BinaryImporter",
"path", sdk.Opts.BinSourcePath, "err", err)
return err
}
// Initiate the data variables
sdk.Buffer = make([]byte, 1024)
sdk.Data = &Outgauge{}
sdk.DataDict = sdk.Data.ToMap()
return sdk, nil
slog.Info(
"created BinaryImporter",
"path", sdk.Opts.BinSourcePath,
)
sdk.reader = reader
case UDPData:
reader, err := NewSocketImporter(sdk.Opts.ImportUDPAddress, sdk.Opts.ImportUDPPort)
if err != nil {
slog.Error(
"failed to create SocketImporter",
"address", sdk.Opts.ImportUDPAddress, "port", sdk.Opts.ImportUDPPort, "err", err,
)
return err
}
// SDK utilities
// 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
func (sdk *BeamNGSDK) ShiftLight() bool {
return sdk.Data.ShowLights&DL_SHIFT != 0
slog.Info(
"created SocketImporter",
"address", sdk.Opts.ImportUDPAddress, "port", sdk.Opts.ImportUDPPort,
)
sdk.reader = reader
}
// HighBeam returns:
// true if the high beams are on
// false if the high beams are off
func (sdk *BeamNGSDK) HighBeam() bool {
return sdk.Data.ShowLights&DL_FULLBEAM != 0
return nil
}
// Handbrake returns:
// true if the handbrake is pulled
// false if the handbrake is down
func (sdk *BeamNGSDK) Handbrake() bool {
return sdk.Data.ShowLights&DL_HANDBRAKE != 0
func (sdk *BeamNGSDK) openWriter() error {
// if the user didn't request data export we don't need a writer
if !sdk.Opts.ExportData {
return nil
}
// Pitspeed returns:
// true if the pit speed limiter is on
// false if the pit speed limiter is off
// 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
switch sdk.Opts.ExportDataType {
case BinaryFile:
writer, err := NewBinaryExporter(sdk.Opts.ExportDataPath)
if err != nil {
slog.Error("failed to create BinaryExporter",
"path", sdk.Opts.ExportDataPath, "err", err)
return err
}
// TractionControl returns:
// true if traction control is on
// false if traction control is off
func (sdk *BeamNGSDK) TractionControl() bool {
return sdk.Data.ShowLights&DL_TC != 0
slog.Info(
"created BinaryExporter",
"path", sdk.Opts.ExportDataPath,
)
sdk.writer = writer
case UDPData:
writer, err := NewSocketExporter(sdk.Opts.ExportUDPAddress, sdk.Opts.ExportUDPPort)
if err != nil {
slog.Error(
"failed to create SocketExporter",
"address", sdk.Opts.ExportUDPAddress, "port", sdk.Opts.ExportUDPPort, "err", err,
)
return err
}
// LeftIndicator returns:
// true if the left indicator is on
// false if the left indicator is off
func (sdk *BeamNGSDK) LeftIndicator() bool {
return sdk.Data.ShowLights&DL_SIGNAL_L != 0
slog.Info(
"created SocketExporter",
"address", sdk.Opts.ExportUDPAddress, "port", sdk.Opts.ExportUDPPort,
)
sdk.writer = writer
}
// RightIndicator returns:
// true if the right indicator is on
// false if the right indicator is off
func (sdk *BeamNGSDK) RightIndicator() bool {
return sdk.Data.ShowLights&DL_SIGNAL_R != 0
return nil
}
// AnyIndicator returns:
// true if the any indicator is on
// false if the all indicators are off
func (sdk *BeamNGSDK) AnyIndicator() bool {
return sdk.Data.ShowLights&DL_SIGNAL_ANY != 0
func (sdk *BeamNGSDK) Update() (*Outgauge, error) {
var err error
_, err = sdk.reader.Next(sdk.buffer)
// We check this first because we want to know if we need to loop
if err == io.EOF {
if sdk.Opts.Loop {
err = sdk.reader.Reset()
if err != nil {
return nil, err
}
}
// OilLight returns
// true if the oil light is on
// false if the oil light is off
func (sdk *BeamNGSDK) OilLight() bool {
return sdk.Data.ShowLights&DL_OILWARN != 0
// NOTE: could we make the reset return the next piece of data?
// We update to the start of the file since we had to reset
_, err = sdk.reader.Next(sdk.buffer)
}
if err != nil {
return nil, err
}
// BatteryLight returns
// true if the battery light is on
// false if the battery light is off
func (sdk *BeamNGSDK) BatteryLight() bool {
return sdk.Data.ShowLights&DL_BATTERY != 0
if sdk.Opts.ExportData {
sdk.writer.Write(sdk.buffer)
}
// ABS returns
// true if the ABS is engaged
// false if the ABS isn't engaged
func (sdk *BeamNGSDK) ABS() bool {
return sdk.Data.ShowLights&DL_ABS != 0
return &sdk.data, sdk.parseData(sdk.buffer)
}
// ShowLights - functions to check if a given dash light is on [END]
// DashLights - functions to check if a given dash light is provided [START]
// HasShiftLight returns:
// true if its available
// false if its unavailable
func (sdk *BeamNGSDK) HasShiftLight() bool {
return sdk.Data.DashLights&DL_SHIFT != 0
func (sdk *BeamNGSDK) GetTotalRead() int64 {
return sdk.reader.GetTotalRead()
}
// HasHighBeamLight returns:
// true if its available
// false if its unavailable
func (sdk *BeamNGSDK) HasHighBeamLight() bool {
return sdk.Data.DashLights&DL_FULLBEAM != 0
func (sdk *BeamNGSDK) GetTotalWritten() int64 {
return sdk.writer.GetTotalWritten()
}
// HasHandbrakeLight returns:
// true if its available
// false if its unavailable
func (sdk *BeamNGSDK) HasHandbrakeLight() bool {
return sdk.Data.DashLights&DL_HANDBRAKE != 0
func (sdk *BeamNGSDK) parseData(buffer []byte) error {
return sdk.data.ParseData(buffer)
}
// HasPitspeed returns:
// true if its available
// false if its unavailable
// 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
func (sdk *BeamNGSDK) GetSourceSize() int64 {
if sizer, ok := sdk.reader.(Sizer); ok {
return sizer.Size()
}
// HasTractionControlLight returns:
// true if its available
// false if its unavailable
func (sdk *BeamNGSDK) HasTractionControlLight() bool {
return sdk.Data.DashLights&DL_TC != 0
return 0
}
// HasLeftIndicatorLight returns:
// true if its available
// false if its unavailable
func (sdk *BeamNGSDK) HasLeftIndicatorLight() bool {
return sdk.Data.DashLights&DL_SIGNAL_L != 0
}
// HasRightIndicatorLight returns:
// true if its available
// false if its unavailable
func (sdk *BeamNGSDK) HasRightIndicatorLight() bool {
return sdk.Data.DashLights&DL_SIGNAL_R != 0
}
// AnyIndicator returns:
// true if its available
// false if its unavailable
func (sdk *BeamNGSDK) HasAnyIndicatorLight() bool {
return sdk.Data.DashLights&DL_SIGNAL_ANY != 0
}
// HasOilLight returns:
// true if its available
// false if its unavailable
func (sdk *BeamNGSDK) HasOilLight() bool {
return sdk.Data.DashLights&DL_OILWARN != 0
}
// HasBatteryLight returns:
// true if its available
// false if its unavailable
func (sdk *BeamNGSDK) HasBatteryLight() bool {
return sdk.Data.DashLights&DL_BATTERY != 0
}
// HasABSLight returns:
// true if its available
// false if its unavailable
func (sdk *BeamNGSDK) HasABSLight() bool {
return sdk.Data.DashLights&DL_ABS != 0
}
// DashLights - functions to check if a given dash light is provided [END]
+73
View File
@@ -0,0 +1,73 @@
package bngsdk
import (
"bytes"
"encoding/binary"
"io"
"log/slog"
"net"
"testing"
)
func BenchmarkUpdate(b *testing.B) {
// Silence logging output so slog calls don't pollute benchmark stats
slogger := slog.New(slog.NewTextHandler(io.Discard, nil))
// Initialize the SDK with port 0 to bind to an OS-assigned ephemeral port
sdk, err := NewBngSDK(Options{
Logger: slogger,
SourceType: UDPData,
ImportUDPAddress: "127.0.0.1",
ImportUDPPort: 0,
})
if err != nil {
b.Fatalf("Failed to initialize SDK: %v", err)
}
defer sdk.Close()
// Access the underlying reader connection to determine the dynamically bound port
ogReader, ok := sdk.reader.(*OgUDPReader)
if !ok || ogReader.udpConnection == nil || ogReader.udpConnection.connection == nil {
b.Fatalf("Failed to retrieve underlying UDP connection")
}
serverAddr := ogReader.udpConnection.connection.LocalAddr().(*net.UDPAddr)
// Dial the UDP socket as a client to send test data
clientConn, err := net.DialUDP("udp", nil, serverAddr)
if err != nil {
b.Fatalf("Failed to dial UDP server: %v", err)
}
defer clientConn.Close()
// Pre-serialize a dummy Outgauge struct matching the required byte layout
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 transport socket
_, err := clientConn.Write(packetBytes)
if err != nil {
b.Fatalf("Failed to write to UDP socket: %v", err)
}
// Run the main API loop method
_, err = sdk.Update()
if err != nil {
b.Fatalf("Update failed at iteration %d: %v", i, err)
}
}
}
+60
View File
@@ -0,0 +1,60 @@
package main
import (
"fmt"
"log"
"log/slog"
"os"
"time"
bngsdk "github.com/ESilva15/gobngsdk"
)
func main() {
fmt.Println("Example of how to read a binary file with the SDK")
output, err := os.OpenFile("./output.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o755)
if err != nil {
log.Fatalf("Failed to open log file: %+v", err)
}
logger := slog.New(
slog.NewTextHandler(output, &slog.HandlerOptions{
Level: slog.LevelDebug,
}),
)
sdk, err := bngsdk.NewBngSDK(bngsdk.Options{
Logger: logger.With("service", "bngsdk"),
SourceType: bngsdk.BinaryFile,
BinSourcePath: "../../sunburstManual.bin",
ExportData: true,
ExportDataType: bngsdk.UDPData,
ExportUDPAddress: "127.0.0.1",
ExportUDPPort: 4444,
Loop: true,
})
if err != nil {
log.Fatalf("failed to open the sdk: %+v", err)
}
defer sdk.Close()
ticker := time.NewTicker(time.Second / 60)
defer ticker.Stop()
for {
select {
case <-ticker.C:
og, err := sdk.Update()
if err != nil {
log.Fatalf("failed to update data: %+v", err)
}
fmt.Printf("\033[?25l\033[2J\033[H")
fmt.Printf(
"Gear: %d, RPM: %f, Speed: %f\n",
og.Gear, og.RPM, og.Speed,
)
}
}
}
+56
View File
@@ -0,0 +1,56 @@
package main
import (
"fmt"
"log"
"log/slog"
"os"
"time"
bngsdk "github.com/ESilva15/gobngsdk"
)
func main() {
fmt.Println("Example of how to read from the socket with the SDK")
output, err := os.OpenFile("./output.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o755)
if err != nil {
log.Fatalf("Failed to open log file: %+v", err)
}
logger := slog.New(
slog.NewTextHandler(output, &slog.HandlerOptions{
Level: slog.LevelDebug,
}),
)
sdk, err := bngsdk.NewBngSDK(bngsdk.Options{
Logger: logger.With("service", "bngsdk"),
SourceType: bngsdk.UDPData,
ImportUDPAddress: "127.0.0.1",
ImportUDPPort: 4444,
})
if err != nil {
log.Fatalf("failed to open the sdk: %+v", err)
}
defer sdk.Close()
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
og, err := sdk.Update()
if err != nil {
log.Fatalf("failed to update data: %+v", err)
}
fmt.Printf("\033[?25l\033[2J\033[H")
fmt.Printf(
"Gear: %d, RPM: %f, Speed: %f\n",
og.Gear, og.RPM, og.Speed,
)
}
}
}
+240 -21
View File
@@ -1,5 +1,16 @@
package bngsdk
import (
"encoding/binary"
"errors"
"math"
)
var (
OutgaugeSize = int64(binary.Size(Outgauge{}))
ErrNotEnoughData = errors.New("buffer len is too short for Outgauge unpacking")
)
// More documentation at https://go.beamng.com/protocols.
// Or at BeamNG/lua/vehicle/protocols/outgauge.lua
// OG_x buts for flags
@@ -29,6 +40,7 @@ const (
// Outgauge describes the data served by the BeamNG outgauge server
type Outgauge struct {
// data []byte
Time uint32 // time in milliseconds (to check order)
Car [4]byte // Car name
Flags uint16 // Info (see OG_x below)
@@ -51,28 +63,235 @@ type Outgauge struct {
ID int32 // optional - only if OutGauge ID is specified
}
func (og *Outgauge) ParseData(buffer []byte) error {
if int64(len(buffer)) < OutgaugeSize {
return ErrNotEnoughData
}
og.Time = binary.LittleEndian.Uint32(buffer[0:4])
copy(og.Car[:], buffer[4:8])
og.Flags = binary.LittleEndian.Uint16(buffer[8:10])
og.Gear = int8(buffer[10])
og.Plid = int8(buffer[11])
og.Speed = math.Float32frombits(binary.LittleEndian.Uint32(buffer[12:16]))
og.RPM = math.Float32frombits(binary.LittleEndian.Uint32(buffer[16:20]))
og.Turbo = math.Float32frombits(binary.LittleEndian.Uint32(buffer[20:24]))
og.EngTemp = math.Float32frombits(binary.LittleEndian.Uint32(buffer[24:28]))
og.Fuel = math.Float32frombits(binary.LittleEndian.Uint32(buffer[28:32]))
og.OilPressure = math.Float32frombits(binary.LittleEndian.Uint32(buffer[32:36]))
og.OilTemp = math.Float32frombits(binary.LittleEndian.Uint32(buffer[36:40]))
og.DashLights = binary.LittleEndian.Uint32(buffer[40:44])
og.ShowLights = binary.LittleEndian.Uint32(buffer[44:48])
og.Throttle = math.Float32frombits(binary.LittleEndian.Uint32(buffer[48:52]))
og.Brake = math.Float32frombits(binary.LittleEndian.Uint32(buffer[52:56]))
og.Clutch = math.Float32frombits(binary.LittleEndian.Uint32(buffer[56:60]))
copy(og.Display1[:], buffer[60:76])
copy(og.Display2[:], buffer[76:92])
og.ID = int32(binary.LittleEndian.Uint32(buffer[92:96]))
return nil
}
// TODO: think about having direct memory access for the fields instead of
// parsing everything
// func NewOutgauge(buffer []byte) (*Outgauge, error) {
// if len(buffer) < outgaugeSize {
// return nil, ErrNotEnoughData
// }
//
// og := &Outgauge{
// data: make([]byte, outgaugeSize),
// }
//
// return og, nil
// }
// SDK utilities
// Values Getters [START]
// Values Getters [END]
// ShowLights - functions to check if a given dash light is on [START]
// ShiftLight reports whether the shift light is on
func (og *Outgauge) ShiftLight() bool {
return og.ShowLights&DL_SHIFT != 0
}
// HighBeam reports whether high beams are on
func (og *Outgauge) HighBeam() bool {
return og.ShowLights&DL_FULLBEAM != 0
}
// Handbrake reports whether the handbrake is pulled
func (og *Outgauge) Handbrake() bool {
return og.ShowLights&DL_HANDBRAKE != 0
}
// Pitspeed reports whether the pit speed limiter is engaged
//
// NOTE: this may not be used in BeamNG.drive, haven't checked yet
func (og *Outgauge) Pitspeed() bool {
return og.ShowLights&DL_PITSPEED != 0
}
// TractionControl reports wheter TC is engaged
func (og *Outgauge) TractionControl() bool {
return og.ShowLights&DL_TC != 0
}
// LeftIndicator reports whether the left indicator is on
func (og *Outgauge) LeftIndicator() bool {
return og.ShowLights&DL_SIGNAL_L != 0
}
// RightIndicator reports wheter the right indicator is on
func (og *Outgauge) RightIndicator() bool {
return og.ShowLights&DL_SIGNAL_R != 0
}
// AnyIndicator reports whether any indicator is on
func (og *Outgauge) AnyIndicator() bool {
return og.ShowLights&DL_SIGNAL_ANY != 0
}
// OilLight reports whether the oil warning light is on
func (og *Outgauge) OilLight() bool {
return og.ShowLights&DL_OILWARN != 0
}
// BatteryLight reports whether the battery light is on
func (og *Outgauge) BatteryLight() bool {
return og.ShowLights&DL_BATTERY != 0
}
// ABS reports whether the ABS light is on
func (og *Outgauge) ABS() bool {
return og.ShowLights&DL_ABS != 0
}
// Spare reports whether spare was flipped
func (og *Outgauge) Spare() bool {
return og.ShowLights&DL_SPARE != 0
}
// ShowLights - functions to check if a given dash light is on [END]
// DashLights - functions to check if a given dash light is provided [START]
// HasShiftLight reports whether a shift light is available
func (og *Outgauge) HasShiftLight() bool {
return og.DashLights&DL_SHIFT != 0
}
// HasHighBeamLight reports whether a high beam light is available
func (og *Outgauge) HasHighBeamLight() bool {
return og.DashLights&DL_FULLBEAM != 0
}
// HasHandbrakeLight reports wheter a handbrake light is available
func (og *Outgauge) HasHandbrakeLight() bool {
return og.DashLights&DL_HANDBRAKE != 0
}
// HasPitspeed reports whether a pit speed limitr is available
// NOTE: this may not be used in BeamNG.drive, haven't checked yet
func (og *Outgauge) HasPitspeed() bool {
return og.DashLights&DL_PITSPEED != 0
}
// HasTractionControlLight reports whether a traction control light is available
func (og *Outgauge) HasTractionControlLight() bool {
return og.DashLights&DL_TC != 0
}
// HasLeftIndicatorLight reports whether a left indicator is available
func (og *Outgauge) HasLeftIndicatorLight() bool {
return og.DashLights&DL_SIGNAL_L != 0
}
// HasRightIndicatorLight reports whether a right indicator is available
func (og *Outgauge) HasRightIndicatorLight() bool {
return og.DashLights&DL_SIGNAL_R != 0
}
// HasAnyIndicatorLight reports whether an any indicator light is available
func (og *Outgauge) HasAnyIndicatorLight() bool {
return og.DashLights&DL_SIGNAL_ANY != 0
}
// HasOilLight reports whether a oil light is available
func (og *Outgauge) HasOilLight() bool {
return og.DashLights&DL_OILWARN != 0
}
// HasBatteryLight reports whether a battery light is available
func (og *Outgauge) HasBatteryLight() bool {
return og.DashLights&DL_BATTERY != 0
}
// HasABSLight reports whether an ABS light is available
func (og *Outgauge) HasABSLight() bool {
return og.DashLights&DL_ABS != 0
}
// HasSpare reports whether spare was flipped
func (og *Outgauge) HasSpare() bool {
return og.DashLights&DL_SPARE != 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 (og *Outgauge) HasTurbo() bool {
return og.Flags&OG_TURBO != 0
}
// PrefersKm reports whether the user prefers kilometers:
// - true is prefers Km
// - false is prefers Mi
func (og *Outgauge) PrefersKm() bool {
return og.Flags&OG_KM != 0
}
// PrefersBAR reports whether the user prefers BAR:
// - true is prefers BAR
// - false is prefers PSI
func (og *Outgauge) PrefersBAR() bool {
return og.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 (o *Outgauge) ToMap() map[string]any {
func (sdk *BeamNGSDK) 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
"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]
+21
View File
@@ -0,0 +1,21 @@
package bngsdk
type BngImporter interface {
Reset() error
Next([]byte) (int, error)
GetTotalRead() int64
Close() error
}
func NewBinaryImporter(file string) (BngImporter, error) {
return NewOgBinReader(file), nil
}
func NewSocketImporter(address string, port int) (BngImporter, error) {
reader, err := NewOgUDPReader(address, port)
if err != nil {
return nil, err
}
return reader, nil
}
+76
View File
@@ -0,0 +1,76 @@
package bngsdk
import (
"io"
"os"
"unsafe"
)
type Sizer interface {
Size() int64
}
type GobReader struct {
TotalRead int64
File *os.File
Buf []byte
size int64
}
func NewOgBinReader(fp string) *GobReader {
bin, err := os.Open(fp)
if err != nil {
return nil
}
info, err := bin.Stat()
if err != nil {
return nil
}
return &GobReader{
TotalRead: 0,
File: bin,
Buf: make([]byte, unsafe.Sizeof(Outgauge{})),
size: info.Size(),
}
}
func (g *GobReader) Close() error {
if g.File != nil {
return g.File.Close()
}
return nil
}
func (g *GobReader) Reset() error {
_, err := g.File.Seek(0, io.SeekStart)
if err != nil {
return err
}
g.TotalRead = 0
return nil
}
func (g *GobReader) Next(buffer []byte) (int, error) {
nBytes, err := io.ReadFull(g.File, buffer)
if err != nil {
return 0, err
}
pos, _ := g.File.Seek(0, io.SeekCurrent)
g.TotalRead = pos
return nBytes, nil
}
func (g *GobReader) GetTotalRead() int64 {
return g.TotalRead
}
func (g *GobReader) Size() int64 {
return g.size
}
+53
View File
@@ -0,0 +1,53 @@
package bngsdk
import (
"errors"
)
type OgUDPReader struct {
udpConnection *UDPTransport
}
var ErrInvalidOutgaugeData = errors.New("data is of different size than outgauge")
func NewOgUDPReader(ip string, port int) (*OgUDPReader, error) {
conn, err := NewUDPReader(ip, port)
if err != nil {
return nil, err
}
return &OgUDPReader{
udpConnection: conn,
}, nil
}
func (ogr *OgUDPReader) Close() error {
if ogr.udpConnection != nil {
return ogr.udpConnection.Close()
}
return nil
}
func (ogr *OgUDPReader) Reset() error {
// NOTE: what to implement here?
return nil
}
func (ogr *OgUDPReader) Next(buffer []byte) (int, error) {
nBytes, err := ogr.udpConnection.Read(buffer)
if err != nil {
return 0, err
}
// Check if enough data was received to fill our struct
if int64(nBytes) != OutgaugeSize {
return 0, ErrInvalidOutgaugeData
}
return nBytes, nil
}
func (ogr *OgUDPReader) GetTotalRead() int64 {
return ogr.udpConnection.GetTotalBytes()
}
+146
View File
@@ -0,0 +1,146 @@
package bngsdk
import (
"errors"
"log/slog"
"net"
"sync"
"time"
"unsafe"
)
// ErrReaderDisabled = errors.New("reader is currently turned off")
var (
ErrNoData = errors.New("no new data available")
readTimeout = (time.Second / 60) * 5 // N missed frames at 60fps
packetPool = sync.Pool{
New: func() any {
var b packetBuffer
return &b
},
}
)
type packetBuffer [unsafe.Sizeof(Outgauge{})]byte
type frame struct {
Buf *packetBuffer
Len int
}
type UDPTransport struct {
address *net.UDPAddr
connection *net.UDPConn
dataChan chan frame
totalBytes int64 // returns total bytes read in case of reader and written in case of writer
}
func NewUDPReader(ip string, port int) (*UDPTransport, error) {
// Define the IP address and port to listen on
addr := &net.UDPAddr{
IP: net.ParseIP(ip),
Port: port,
}
conn, err := net.ListenUDP("udp", addr)
if err != nil {
return nil, err
}
udpT := UDPTransport{
address: addr,
connection: conn,
dataChan: make(chan frame, 1),
}
go udpT.udpSink()
return &udpT, nil
}
func NewUDPWriter(ip string, port int) (*UDPTransport, error) {
// Define the IP address and port to listen on
addr := &net.UDPAddr{
IP: net.ParseIP(ip),
Port: port,
}
conn, err := net.DialUDP("udp", nil, addr)
if err != nil {
return nil, err
}
return &UDPTransport{
address: addr,
connection: conn,
}, nil
}
// udpSink is a loop that will consume the most recent packets on the port
// instead of letting them pile up
func (ut *UDPTransport) udpSink() {
defer close(ut.dataChan)
for {
ut.connection.SetReadDeadline(time.Now().Add(readTimeout))
bufPtr := packetPool.Get().(*packetBuffer)
nBytes, _, err := ut.connection.ReadFromUDP(bufPtr[:])
if err != nil {
return
}
frame := frame{
Buf: bufPtr,
Len: nBytes,
}
select {
case ut.dataChan <- frame:
// Packet sent successfuly
default:
select {
case oldFrame := <-ut.dataChan:
packetPool.Put(oldFrame.Buf)
default:
}
ut.dataChan <- frame
}
}
}
func (ut *UDPTransport) Write(data []byte) (int, error) {
nBytes, err := ut.connection.Write(data)
ut.totalBytes += int64(nBytes)
return nBytes, err
}
func (ut *UDPTransport) Read(buffer []byte) (int, error) {
latestFrame, ok := <-ut.dataChan
if !ok {
slog.Error(ErrNoData.Error())
return 0, ErrNoData
}
nBytes := copy(buffer, latestFrame.Buf[:latestFrame.Len])
packetPool.Put(latestFrame.Buf)
ut.totalBytes += int64(nBytes)
return nBytes, nil
}
func (ut *UDPTransport) Close() error {
if ut.connection != nil {
return ut.connection.Close()
}
return nil
}
func (ut *UDPTransport) GetTotalBytes() int64 {
return ut.totalBytes
}
+25
View File
@@ -0,0 +1,25 @@
package bngsdk
type BngExporter interface {
Write([]byte) (int, error)
GetTotalWritten() int64
Close() error
}
func NewSocketExporter(address string, port int) (BngExporter, error) {
writer, err := NewSocketWriter(address, port)
if err != nil {
return nil, err
}
return writer, nil
}
func NewBinaryExporter(path string) (BngExporter, error) {
writer, err := NewOgBinWriter(path)
if err != nil {
return nil, err
}
return writer, nil
}
+46
View File
@@ -0,0 +1,46 @@
package bngsdk
import (
"encoding/binary"
"os"
)
type OgBinWriter struct {
file *os.File
totalBytes int64
}
func NewOgBinWriter(path string) (*OgBinWriter, error) {
outputFile, err := os.Create(path)
if err != nil {
return nil, err
}
return &OgBinWriter{
file: outputFile,
}, nil
}
func (ogw *OgBinWriter) Close() error {
if ogw.file != nil {
return ogw.file.Close()
}
return nil
}
func (ogw *OgBinWriter) Write(data []byte) (int, error) {
err := binary.Write(ogw.file, binary.LittleEndian, data)
if err != nil {
return 0, err
}
nBytes := len(data)
ogw.totalBytes += int64(nBytes)
return nBytes, nil
}
func (ogw *OgBinWriter) GetTotalWritten() int64 {
return ogw.totalBytes
}
+33
View File
@@ -0,0 +1,33 @@
package bngsdk
type SocketWriter struct {
udpConnection *UDPTransport
}
func NewSocketWriter(ip string, port int) (*SocketWriter, error) {
conn, err := NewUDPWriter(ip, port)
if err != nil {
return nil, err
}
return &SocketWriter{
udpConnection: conn,
}, nil
}
func (sw *SocketWriter) Close() error {
if sw.udpConnection != nil {
return sw.udpConnection.Close()
}
return nil
}
func (sw *SocketWriter) Write(data []byte) (int, error) {
// slog.Debug("Writing", "data", data)
return sw.udpConnection.Write(data)
}
func (sw *SocketWriter) GetTotalWritten() int64 {
return sw.udpConnection.totalBytes
}