Compare commits
11
Commits
v1.0.0
...
better-sdk
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba6b2c700b | ||
|
|
ec2ed17f43 | ||
|
|
67b9cf617c | ||
|
|
a7fbf74f0a | ||
|
|
047deb65e7 | ||
|
|
f1728c11da | ||
|
|
245d6ac0e1 | ||
|
|
c3ee9c0f44 | ||
|
|
31140b5df4 | ||
|
|
3857f5983f | ||
|
|
88f50218a4 |
@@ -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
|
||||
```
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// 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"
|
||||
"math"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -16,165 +16,267 @@ 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, unsafe.Sizeof(Outgauge{})),
|
||||
}
|
||||
|
||||
// Create a UDP socket
|
||||
conn, err := net.ListenUDP("udp", addr)
|
||||
// Set the passed logger as the default logger
|
||||
slog.SetDefault(sdk.Opts.Logger)
|
||||
|
||||
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
|
||||
|
||||
// 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{}
|
||||
|
||||
// Create the connection to the OutGauge server
|
||||
sdk.Conn, sdk.Addr, err = createUDPConnection(ip, port)
|
||||
if err != nil {
|
||||
return BeamNGSDK{}, err
|
||||
if sdk.writer != nil {
|
||||
sdk.writer.Close()
|
||||
}
|
||||
|
||||
// Initiate the data variables
|
||||
sdk.Buffer = make([]byte, 1024)
|
||||
sdk.Data = &Outgauge{}
|
||||
sdk.DataDict = sdk.Data.ToMap()
|
||||
if sdk.reader != nil {
|
||||
sdk.reader.Close()
|
||||
}
|
||||
|
||||
return sdk, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sdk *BeamNGSDK) openReader() error {
|
||||
switch sdk.Opts.SourceType {
|
||||
case BinaryFile:
|
||||
reader, err := NewBinaryImporter(sdk.Opts.BinSourcePath)
|
||||
if err != nil {
|
||||
slog.Error("failed to create BinaryImporter",
|
||||
"path", sdk.Opts.BinSourcePath, "err", err)
|
||||
return err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
slog.Info(
|
||||
"created SocketImporter",
|
||||
"address", sdk.Opts.ImportUDPAddress, "port", sdk.Opts.ImportUDPPort,
|
||||
)
|
||||
sdk.reader = reader
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
slog.Info(
|
||||
"created SocketExporter",
|
||||
"address", sdk.Opts.ExportUDPAddress, "port", sdk.Opts.ExportUDPPort,
|
||||
)
|
||||
sdk.writer = writer
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sdk *BeamNGSDK) Update() (int, error) {
|
||||
var nBytes int
|
||||
var err error
|
||||
|
||||
nBytes, 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 0, err
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
nBytes, err = sdk.reader.Next(sdk.buffer)
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if sdk.Opts.ExportData {
|
||||
sdk.writer.Write(sdk.buffer)
|
||||
}
|
||||
|
||||
return nBytes, sdk.parseData(sdk.buffer)
|
||||
}
|
||||
|
||||
func ParseData(ogData *Outgauge, buffer []byte) error {
|
||||
ogData.Time = binary.LittleEndian.Uint32(buffer[0:4])
|
||||
copy(ogData.Car[:], buffer[4:8])
|
||||
ogData.Flags = binary.LittleEndian.Uint16(buffer[8:10])
|
||||
ogData.Gear = int8(buffer[10])
|
||||
ogData.Plid = int8(buffer[11])
|
||||
ogData.Speed = math.Float32frombits(binary.LittleEndian.Uint32(buffer[12:16]))
|
||||
ogData.RPM = math.Float32frombits(binary.LittleEndian.Uint32(buffer[16:20]))
|
||||
ogData.Turbo = math.Float32frombits(binary.LittleEndian.Uint32(buffer[20:24]))
|
||||
ogData.EngTemp = math.Float32frombits(binary.LittleEndian.Uint32(buffer[24:28]))
|
||||
ogData.Fuel = math.Float32frombits(binary.LittleEndian.Uint32(buffer[28:32]))
|
||||
ogData.OilPressure = math.Float32frombits(binary.LittleEndian.Uint32(buffer[32:36]))
|
||||
ogData.OilTemp = math.Float32frombits(binary.LittleEndian.Uint32(buffer[36:40]))
|
||||
ogData.DashLights = binary.LittleEndian.Uint32(buffer[40:44])
|
||||
ogData.ShowLights = binary.LittleEndian.Uint32(buffer[44:48])
|
||||
ogData.Throttle = math.Float32frombits(binary.LittleEndian.Uint32(buffer[48:52]))
|
||||
ogData.Brake = math.Float32frombits(binary.LittleEndian.Uint32(buffer[52:56]))
|
||||
ogData.Clutch = math.Float32frombits(binary.LittleEndian.Uint32(buffer[56:60]))
|
||||
copy(ogData.Display1[:], buffer[60:76])
|
||||
copy(ogData.Display2[:], buffer[76:92])
|
||||
ogData.ID = int32(binary.LittleEndian.Uint32(buffer[92:96]))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sdk *BeamNGSDK) parseData(buffer []byte) error {
|
||||
return ParseData(&sdk.Data, buffer)
|
||||
}
|
||||
|
||||
// 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
|
||||
// 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 +285,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
|
||||
}
|
||||
|
||||
// AnyIndicator 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]
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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:
|
||||
_, 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",
|
||||
sdk.Data.Gear, sdk.Data.RPM, sdk.Data.Speed,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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:
|
||||
_, 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",
|
||||
sdk.Data.Gear, sdk.Data.RPM, sdk.Data.Speed,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-26
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package bngsdk
|
||||
|
||||
type BngImporter interface {
|
||||
Reset() error
|
||||
Next([]byte) (int, error)
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package bngsdk
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
type GobReader struct {
|
||||
TotalRead int64
|
||||
File *os.File
|
||||
Buf []byte
|
||||
}
|
||||
|
||||
func NewOgBinReader(fp string) *GobReader {
|
||||
bin, err := os.Open(fp)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &GobReader{
|
||||
TotalRead: 0,
|
||||
File: bin,
|
||||
Buf: make([]byte, unsafe.Sizeof(Outgauge{})),
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
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 nBytes != outgaugeSize {
|
||||
return 0, ErrInvalidOutgaugeData
|
||||
}
|
||||
|
||||
return nBytes, nil
|
||||
}
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
return ut.connection.Write(data)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
return nBytes, nil
|
||||
}
|
||||
|
||||
func (ut *UDPTransport) Close() error {
|
||||
if ut.connection != nil {
|
||||
return ut.connection.Close()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package bngsdk
|
||||
|
||||
type BngExporter interface {
|
||||
Write([]byte) (int, error)
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package bngsdk
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"os"
|
||||
)
|
||||
|
||||
type OgBinWriter struct {
|
||||
file *os.File
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
return len(data), nil
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user