Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
312d979be1 | ||
|
|
137b96fae6 | ||
|
|
d9b7c185ed | ||
|
|
6e915ea1e0 | ||
|
|
d0eb54e6c3 | ||
|
|
861a76164b | ||
|
|
88370b25be | ||
|
|
ba6b2c700b | ||
|
|
ec2ed17f43 | ||
|
|
67b9cf617c | ||
|
|
a7fbf74f0a | ||
|
|
047deb65e7 | ||
|
|
f1728c11da | ||
|
|
245d6ac0e1 |
@@ -0,0 +1,4 @@
|
|||||||
|
*.log
|
||||||
|
*.test
|
||||||
|
*.pprof
|
||||||
|
*.bin
|
||||||
@@ -3,7 +3,7 @@ Simple SDK to interact with BeamNG.drive OutGauge data.
|
|||||||
|
|
||||||
## Development
|
## Development
|
||||||
### Performance
|
### Performance
|
||||||
`go test -bench=BenchmarkReadData -benchmem -memprofile=mem.pprof`
|
`go test -bench=BenchmarkUpdate -benchmem -memprofile=mem.pprof`
|
||||||
replace the function to be tested
|
replace the function to be tested
|
||||||
|
|
||||||
Use `go tool pprof` to analyze the results
|
Use `go tool pprof` to analyze the results
|
||||||
@@ -15,18 +15,17 @@ goos: linux
|
|||||||
goarch: amd64
|
goarch: amd64
|
||||||
pkg: github.com/ESilva15/gobngsdk
|
pkg: github.com/ESilva15/gobngsdk
|
||||||
cpu: AMD Ryzen 7 5800X3D 8-Core Processor
|
cpu: AMD Ryzen 7 5800X3D 8-Core Processor
|
||||||
BenchmarkReadData-16 320931 4058 ns/op 100 B/op 2 allocs/op
|
BenchmarkReadData-16 362514 3188 ns/op 4 B/op 1 allocs/op
|
||||||
PASS
|
PASS
|
||||||
ok github.com/ESilva15/gobngsdk 1.345s
|
ok github.com/ESilva15/gobngsdk 1.194s
|
||||||
|
|
||||||
# New footprint
|
# New footprint
|
||||||
goos: linux
|
goos: linux
|
||||||
goarch: amd64
|
goarch: amd64
|
||||||
pkg: github.com/ESilva15/gobngsdk
|
pkg: github.com/ESilva15/gobngsdk
|
||||||
cpu: AMD Ryzen 7 5800X3D 8-Core Processor
|
cpu: AMD Ryzen 7 5800X3D 8-Core Processor
|
||||||
BenchmarkReadData-16 362514 3188 ns/op 4 B/op 1 allocs/op
|
BenchmarkUpdate-16 159170 7287 ns/op 4 B/op 1 allocs/op
|
||||||
PASS
|
PASS
|
||||||
ok github.com/ESilva15/gobngsdk 1.194s
|
ok github.com/ESilva15/gobngsdk 1.241s
|
||||||
|
|
||||||
# Pretty good enough. I can finally go be productive instead of "procrastinating" here
|
# Pretty good enough. I can finally go be productive instead of "procrastinating" here
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
// Package bngsdk defines an API to interact with the BeamNG outgauge data in
|
// Package bngsdk defines an API to interact with the BeamNG outgauge data in Go
|
||||||
// Go
|
|
||||||
package bngsdk
|
package bngsdk
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"io"
|
||||||
"encoding/binary"
|
"log/slog"
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
"net"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -17,325 +13,193 @@ const (
|
|||||||
DefaultUDPPort = 4444
|
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 {
|
type BeamNGSDK struct {
|
||||||
Addr *net.UDPAddr
|
Opts Options
|
||||||
Conn *net.UDPConn
|
reader BngImporter
|
||||||
Buffer []byte
|
writer BngExporter
|
||||||
Data Outgauge
|
data Outgauge
|
||||||
// Internal data
|
buffer []byte
|
||||||
reader bytes.Reader
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func createUDPConnection(ip string, port int) (*net.UDPConn, *net.UDPAddr, error) {
|
func NewBngSDK(opts Options) (*BeamNGSDK, error) {
|
||||||
// Define the IP address and port to listen on
|
sdk := BeamNGSDK{
|
||||||
addr := &net.UDPAddr{
|
Opts: opts,
|
||||||
IP: net.ParseIP(ip),
|
buffer: make([]byte, OutgaugeSize),
|
||||||
Port: port,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a UDP socket
|
var err error
|
||||||
conn, err := net.ListenUDP("udp", addr)
|
|
||||||
|
err = sdk.openReader()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
slog.Error("failed to open reader", "err", err)
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return conn, addr, nil
|
err = sdk.openWriter()
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
n, _, err := sdk.Conn.ReadFromUDP(sdk.Buffer)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Error reading from UDP:", err)
|
slog.Error("failed to open writer", "err", err)
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if enough data was received to fill our struct
|
return &sdk, nil
|
||||||
if n < outgaugeSize {
|
|
||||||
fmt.Println("Received packet too small for Outgauge struct")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read the binary data into the struct
|
|
||||||
sdk.reader.Reset(sdk.Buffer[:n])
|
|
||||||
err = sdk.parseData()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Error decoding UDP packet:", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// this means there's new data
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sdk *BeamNGSDK) Close() error {
|
func (sdk *BeamNGSDK) Close() error {
|
||||||
return sdk.Conn.Close()
|
sdk.buffer = nil
|
||||||
|
|
||||||
|
if sdk.writer != nil {
|
||||||
|
sdk.writer.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
if sdk.reader != nil {
|
||||||
|
sdk.reader.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init initializes a BeamNG SDK struct
|
func (sdk *BeamNGSDK) openReader() error {
|
||||||
// NOTE: Change this to output a *BeamNGSDK
|
switch sdk.Opts.SourceType {
|
||||||
func Init(ip string, port int) (BeamNGSDK, error) {
|
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() (*Outgauge, error) {
|
||||||
var err error
|
var err error
|
||||||
sdk := BeamNGSDK{}
|
|
||||||
|
|
||||||
// Create the connection to the OutGauge server
|
_, err = sdk.reader.Next(sdk.buffer)
|
||||||
sdk.Conn, sdk.Addr, err = createUDPConnection(ip, port)
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
if err != nil {
|
||||||
return BeamNGSDK{}, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initiate the data variables
|
if sdk.Opts.ExportData {
|
||||||
sdk.Buffer = make([]byte, 1024)
|
sdk.writer.Write(sdk.buffer)
|
||||||
|
|
||||||
return sdk, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// HasHighBeamLight returns:
|
|
||||||
// true if its available
|
|
||||||
// false if its unavailable
|
|
||||||
func (sdk *BeamNGSDK) HasHighBeamLight() bool {
|
|
||||||
return sdk.Data.DashLights&DL_FULLBEAM != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// HasHandbrakeLight returns:
|
|
||||||
// true if its available
|
|
||||||
// false if its unavailable
|
|
||||||
func (sdk *BeamNGSDK) HasHandbrakeLight() bool {
|
|
||||||
return sdk.Data.DashLights&DL_HANDBRAKE != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// HasTractionControlLight returns:
|
|
||||||
// true if its available
|
|
||||||
// false if its unavailable
|
|
||||||
func (sdk *BeamNGSDK) HasTractionControlLight() bool {
|
|
||||||
return sdk.Data.DashLights&DL_TC != 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// HasAnyIndicatorLight 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]
|
|
||||||
|
|
||||||
// Flags - functions to check if a given flag is ON [START]
|
|
||||||
|
|
||||||
// HasTurbo returns:
|
|
||||||
// true if the car has a turbo
|
|
||||||
// false if it doesn't
|
|
||||||
func (sdk *BeamNGSDK) HasTurbo() bool {
|
|
||||||
return sdk.Data.Flags&OG_TURBO != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// PrefersKm returns:
|
|
||||||
// true if true
|
|
||||||
// false if false
|
|
||||||
// wow
|
|
||||||
func (sdk *BeamNGSDK) PrefersKm() bool {
|
|
||||||
return sdk.Data.Flags&OG_KM != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// PrefersBAR returns:
|
|
||||||
// true if true
|
|
||||||
// false if false
|
|
||||||
// wow
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return &sdk.data, sdk.parseData(sdk.buffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Data Retrieval [END]
|
func (sdk *BeamNGSDK) GetTotalRead() int64 {
|
||||||
|
return sdk.reader.GetTotalRead()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sdk *BeamNGSDK) GetTotalWritten() int64 {
|
||||||
|
return sdk.writer.GetTotalWritten()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sdk *BeamNGSDK) parseData(buffer []byte) error {
|
||||||
|
return sdk.data.ParseData(buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sdk *BeamNGSDK) GetSourceSize() int64 {
|
||||||
|
if sizer, ok := sdk.reader.(Sizer); ok {
|
||||||
|
return sizer.Size()
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|||||||
+30
-15
@@ -3,56 +3,71 @@ package bngsdk
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
"net"
|
"net"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func BenchmarkReadData(b *testing.B) {
|
func BenchmarkUpdate(b *testing.B) {
|
||||||
// Spin up an UDP server
|
// Silence logging output so slog calls don't pollute benchmark stats
|
||||||
sdk, err := Init("127.0.0.1", 0)
|
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 {
|
if err != nil {
|
||||||
b.Fatalf("Failed to initialize SDK: %v", err)
|
b.Fatalf("Failed to initialize SDK: %v", err)
|
||||||
}
|
}
|
||||||
defer sdk.Close()
|
defer sdk.Close()
|
||||||
|
|
||||||
// Retrieve the actual assigned UDP address
|
// Access the underlying reader connection to determine the dynamically bound port
|
||||||
localAddr := sdk.Conn.LocalAddr().(*net.UDPAddr)
|
ogReader, ok := sdk.reader.(*OgUDPReader)
|
||||||
|
if !ok || ogReader.udpConnection == nil || ogReader.udpConnection.connection == nil {
|
||||||
|
b.Fatalf("Failed to retrieve underlying UDP connection")
|
||||||
|
}
|
||||||
|
|
||||||
// Start a client to stream data
|
serverAddr := ogReader.udpConnection.connection.LocalAddr().(*net.UDPAddr)
|
||||||
clientConn, err := net.DialUDP("udp", nil, localAddr)
|
|
||||||
|
// Dial the UDP socket as a client to send test data
|
||||||
|
clientConn, err := net.DialUDP("udp", nil, serverAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Fatalf("Failed to dial local UDP socket: %v", err)
|
b.Fatalf("Failed to dial UDP server: %v", err)
|
||||||
}
|
}
|
||||||
defer clientConn.Close()
|
defer clientConn.Close()
|
||||||
|
|
||||||
// Pre serialize some data
|
// Pre-serialize a dummy Outgauge struct matching the required byte layout
|
||||||
dummyOutgauge := Outgauge{
|
dummyOutgauge := Outgauge{
|
||||||
Time: 424242,
|
Time: 424242,
|
||||||
Car: [4]byte{'P', 'E', 'R', 'F'},
|
Car: [4]byte{'P', 'E', 'R', 'F'},
|
||||||
Speed: 45.2,
|
Speed: 45.2,
|
||||||
RPM: 3500.0,
|
RPM: 3500.0,
|
||||||
}
|
}
|
||||||
|
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
if err := binary.Write(&buf, binary.LittleEndian, dummyOutgauge); err != nil {
|
if err := binary.Write(&buf, binary.LittleEndian, dummyOutgauge); err != nil {
|
||||||
b.Fatalf("Failed to serialize dummy struct: %v", err)
|
b.Fatalf("Failed to serialize dummy struct: %v", err)
|
||||||
}
|
}
|
||||||
packetBytes := buf.Bytes()
|
packetBytes := buf.Bytes()
|
||||||
|
|
||||||
// 4. Reset the timer to isolate the setup allocations from the loop metrics
|
|
||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
b.ReportAllocs() // Tracks memory allocations per operation (B/op and allocs/op)
|
b.ReportAllocs()
|
||||||
|
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
// Feed a packet into the network buffer right before reading it
|
// Feed a packet into the network transport socket
|
||||||
_, err := clientConn.Write(packetBytes)
|
_, err := clientConn.Write(packetBytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Fatalf("Failed to write to UDP socket: %v", err)
|
b.Fatalf("Failed to write to UDP socket: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute the target function
|
// Run the main API loop method
|
||||||
err = sdk.ReadData()
|
_, err = sdk.Update()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Fatalf("ReadData failed at iteration %d: %v", i, err)
|
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:
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+243
-2
@@ -1,8 +1,15 @@
|
|||||||
package bngsdk
|
package bngsdk
|
||||||
|
|
||||||
import "encoding/binary"
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
var outgaugeSize = binary.Size(Outgauge{})
|
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.
|
// More documentation at https://go.beamng.com/protocols.
|
||||||
// Or at BeamNG/lua/vehicle/protocols/outgauge.lua
|
// Or at BeamNG/lua/vehicle/protocols/outgauge.lua
|
||||||
@@ -33,6 +40,7 @@ const (
|
|||||||
|
|
||||||
// Outgauge describes the data served by the BeamNG outgauge server
|
// Outgauge describes the data served by the BeamNG outgauge server
|
||||||
type Outgauge struct {
|
type Outgauge struct {
|
||||||
|
// data []byte
|
||||||
Time uint32 // time in milliseconds (to check order)
|
Time uint32 // time in milliseconds (to check order)
|
||||||
Car [4]byte // Car name
|
Car [4]byte // Car name
|
||||||
Flags uint16 // Info (see OG_x below)
|
Flags uint16 // Info (see OG_x below)
|
||||||
@@ -54,3 +62,236 @@ type Outgauge struct {
|
|||||||
Display2 [16]byte // Usually Settings
|
Display2 [16]byte // Usually Settings
|
||||||
ID int32 // optional - only if OutGauge ID is specified
|
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 (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,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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user