6 Commits
Author SHA1 Message Date
esilva 52056a7da3 updated it to use the latest and greatest API change on bngsdk
May have introduce some stupid things, will look at them after
2026-07-01 00:20:12 +01:00
esilva 31a2f85407 added a view to the recording utility of BeamNG too 2026-06-29 11:27:27 +01:00
esilva b8d915af4f print the error when it throws an error
it might not even matter because it will just flash too fast
2026-06-29 11:23:12 +01:00
esilva 99b3891c60 Moved the outgauge data prints to a standalone function 2026-06-29 00:19:57 +01:00
esilva 988124ef60 Alignement 2026-06-26 18:00:40 +01:00
esilva 422e8fcdc2 very simple way of visualizing the data flowing 2026-06-26 17:49:15 +01:00
8 changed files with 261 additions and 48 deletions
+1
View File
@@ -1 +1,2 @@
*.bin
*.work*
+10 -1
View File
@@ -1,6 +1,7 @@
package cmd
import (
"context"
"fmt"
"github.com/ESilva15/BeamNGMockOg/mockserver"
@@ -13,7 +14,15 @@ func recordAction(cmd *cobra.Command, args []string) {
address, _ := cmd.Flags().GetString("address")
port, _ := cmd.Flags().GetInt("port")
if err := mockserver.Record(address, port, outputFile); err != nil {
recorder, err := mockserver.NewRecorder(outputFile, address, port)
if err != nil {
panic(fmt.Sprintf("failed to create new BeamNG recorder: %+v", err))
}
defer recorder.Close()
// NOTE: is this doing anything at all??
ctx := context.Background()
if err := recorder.Record(ctx); err != nil {
fmt.Printf("Something went wrong while recording the file: %v", err)
}
}
+1
View File
@@ -21,6 +21,7 @@ func replayAction(cmd *cobra.Command, args []string) {
return
}
// NOTE: is this doing anything at all??
ctx := context.Background()
if err := replayer.Replay(ctx, loop); err != nil {
fmt.Printf("Something went wrong while playing the file: %v", err)
+1 -3
View File
@@ -1,11 +1,9 @@
module github.com/ESilva15/BeamNGMockOg
replace github.com/ESilva15/gobngsdk => ../pkg/bngsdk
go 1.23.2
require (
github.com/ESilva15/gobngsdk v0.0.0-00010101000000-000000000000
github.com/ESilva15/gobngsdk v1.1.1
github.com/spf13/cobra v1.10.1
)
+2 -2
View File
@@ -1,5 +1,5 @@
github.com/ESilva15/gobngsdk v0.0.2 h1:N6stNOE14Wg80BAZMFu/Qd9Qa/J0DmExCfdPoDf7lco=
github.com/ESilva15/gobngsdk v0.0.2/go.mod h1:cKLaZRgM0tGGXDvosaSjHnxeyC5Y6rg7zCLqEUJnOzw=
github.com/ESilva15/gobngsdk v1.1.1 h1:MyKUjIqG77lYcUY6cnZpnnxPoxS/FdAYwGoi6InXan0=
github.com/ESilva15/gobngsdk v1.1.1/go.mod h1:cKLaZRgM0tGGXDvosaSjHnxeyC5Y6rg7zCLqEUJnOzw=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+120 -26
View File
@@ -1,47 +1,141 @@
package mockserver
import (
"context"
"encoding/binary"
"log"
"fmt"
"os"
"strings"
"sync"
"time"
bngsdk "github.com/ESilva15/gobngsdk"
)
// NOTE: add some visual feedback of whats happening.
// Maybe reuse the replay view function
type recorderViewData struct {
TotalBytes int
SDK *bngsdk.BeamNGSDK
}
type Recorder struct {
SDK bngsdk.BeamNGSDK
OutputFile *os.File
TotalBytes int
// Views
mut sync.RWMutex
viewDataMut sync.RWMutex
viewData recorderViewData
viewCh chan *recorderViewData
recorderCh chan []byte
}
func NewRecorder(fp string, address string, port int) (*Recorder, error) {
var recorder Recorder
var err error
recorder.SDK, err = bngsdk.Init(address, port)
if err != nil {
return &Recorder{}, err
}
recorder.OutputFile, err = os.Create(fp)
if err != nil {
return &Recorder{}, err
}
recorder.viewData = recorderViewData{}
recorder.viewCh = make(chan *recorderViewData, 1)
recorder.recorderCh = make(chan []byte, 1)
return &recorder, nil
}
func (r *Recorder) Close() {
r.SDK.Close()
if r.OutputFile != nil {
r.OutputFile.Close()
}
}
func (r *Recorder) record(ctx context.Context) {
for {
select {
case <-ctx.Done():
return
case data := <-r.recorderCh:
r.mut.RLock()
err := binary.Write(r.OutputFile, binary.LittleEndian, data)
r.TotalBytes += len(data)
r.mut.RUnlock()
if err != nil {
// NOTE: find a way of logging this somehow
}
}
}
}
func (r *Recorder) view(ctx context.Context) {
var s strings.Builder
var nBytes int
for {
select {
case <-ctx.Done():
return
case viewData := <-r.viewCh:
s.Reset()
fmt.Fprintf(os.Stdout, "\x1b[2J\x1b[H")
stringifyRecordingProgress(&s, nBytes)
fmt.Fprintf(&s, "\n\n")
r.viewDataMut.RLock()
nBytes = viewData.TotalBytes
stringifyOutgaugeData(&s, &r.SDK)
r.viewDataMut.RUnlock()
fmt.Fprint(os.Stdout, s.String())
}
}
}
// Record records data from the UDP connection created by address and port
func Record(address string, port int, filePath string) error {
func (r *Recorder) Record(ctx context.Context) error {
ticker := time.NewTicker(time.Second / 60)
defer ticker.Stop()
// Create the output file
bin, err := os.Create(filePath)
if err != nil {
return err
}
defer bin.Close()
// Create the BeamNGSDK instance
beam, err := bngsdk.Init(address, port)
if err != nil {
return err
}
defer beam.Close()
go r.record(ctx)
go r.view(ctx)
for {
err := beam.ReadData()
if err != nil {
return err
}
select {
case <-ctx.Done():
return nil
case <-ticker.C:
err := r.SDK.ReadData()
if err != nil {
return err
}
err = binary.Write(bin, binary.LittleEndian, beam.Data)
if err != nil {
log.Fatal(err)
}
r.viewData.TotalBytes = r.TotalBytes
<-ticker.C
// Send the data to the view
select {
case r.viewCh <- &r.viewData:
// Sent the data
default:
// Dropped the frame!
}
// Send the data to the UDP socket
select {
case r.recorderCh <- r.SDK.Buffer:
// Sent the data
default:
// Dropped the frame!
}
}
}
}
+37 -16
View File
@@ -3,10 +3,13 @@
package mockserver
import (
"bytes"
"context"
"encoding/binary"
"fmt"
"io"
"os"
"strings"
"sync"
"time"
"unsafe"
@@ -15,13 +18,14 @@ import (
)
type ViewData struct {
Data []byte
SDK *bngsdk.BeamNGSDK
SizeRead int64
}
// Replayer does the replaying
// Should we make a "player" struct that can record and replay?
type Replayer struct {
SDK bngsdk.BeamNGSDK
DataSourcePath string
Socket *UDPTransport
@@ -30,8 +34,7 @@ type Replayer struct {
socketCh chan []byte
// Mut
mut sync.RWMutex
data []byte
mut sync.RWMutex
// View
viewData ViewData
@@ -45,11 +48,14 @@ func NewReplayer(address string, port int, fp string) (*Replayer, error) {
replayer := &Replayer{
DataSourcePath: fp,
Socket: udp,
data: make([]byte, unsafe.Sizeof(bngsdk.Outgauge{})),
viewData: ViewData{},
dataViewCh: make(chan ViewData, 1),
socketCh: make(chan []byte, 1),
SDK: bngsdk.BeamNGSDK{
Data: &bngsdk.Outgauge{},
Buffer: make([]byte, unsafe.Sizeof(bngsdk.Outgauge{})),
},
Socket: udp,
viewData: ViewData{},
dataViewCh: make(chan ViewData, 1),
socketCh: make(chan []byte, 1),
}
return replayer, nil
@@ -64,18 +70,32 @@ func (r *Replayer) renderToTerminal(ctx context.Context) {
}
// NOTE: temporary until I make a better view
lastPercent := -1
var s strings.Builder
var bytesReader bytes.Reader
for {
select {
case <-ctx.Done():
return
case data := <-r.dataViewCh:
percent := int(float64(data.SizeRead) / float64(fileInfo.Size()) * 100)
if percent != lastPercent {
fmt.Printf("\rReplayed: %d%%", percent)
lastPercent = percent
// Reset to the start of the terminal
s.Reset()
fmt.Fprintf(os.Stdout, "\x1b[2J\x1b[H")
bytesReader.Reset(data.SDK.Buffer)
err := binary.Read(&bytesReader, binary.LittleEndian, r.SDK.Data)
if err != nil {
fmt.Fprintf(&s, "FAILED TO PARSE DATA\nError: %+v", err)
fmt.Fprint(os.Stdout, s.String())
continue
}
percent := int(float64(data.SizeRead) / float64(fileInfo.Size()) * 100)
fmt.Fprintf(&s, "Replayed: %d%%\n", percent)
stringifyOutgaugeData(&s, data.SDK)
fmt.Fprint(os.Stdout, s.String())
}
}
}
@@ -121,7 +141,7 @@ func (r *Replayer) Replay(ctx context.Context, loop bool) error {
case <-ticker.C:
r.mut.Lock()
err := reader.Next(r.data)
err := reader.Next(r.SDK.Buffer)
r.mut.Unlock()
if err == io.EOF {
@@ -141,7 +161,8 @@ func (r *Replayer) Replay(ctx context.Context, loop bool) error {
return err
}
r.viewData.Data = r.data
// NOTE: Really like this???
r.viewData.SDK = &r.SDK
r.viewData.SizeRead = reader.TotalRead
// Send the data to the view
@@ -154,7 +175,7 @@ func (r *Replayer) Replay(ctx context.Context, loop bool) error {
// Send the data to the UDP socket
select {
case r.socketCh <- r.data:
case r.socketCh <- r.SDK.Buffer:
// Sent the data
default:
// Dropped the frame!
+89
View File
@@ -0,0 +1,89 @@
package mockserver
import (
"fmt"
"strings"
bngsdk "github.com/ESilva15/gobngsdk"
)
const (
_ = iota
KiB = 1 << (10 * iota)
MiB
GiB
)
func stringifyRecordingProgress(s *strings.Builder, nBytes int) {
if nBytes < KiB {
fmt.Fprintf(s, "%d B", nBytes)
} else if nBytes < MiB {
fmt.Fprintf(s, "%.2f KiB", float64(nBytes)/float64(KiB))
} else if nBytes < GiB {
fmt.Fprintf(s, "%.2f MiB", float64(nBytes)/float64(MiB))
} else {
fmt.Fprintf(s, "%.2f GiB", float64(nBytes)/float64(GiB))
}
}
func stringifyOutgaugeData(s *strings.Builder, sdk *bngsdk.BeamNGSDK) {
// NOTE: write a string serialization function on the SDK itself
fmt.Fprint(s, "Outgauge {\n")
fmt.Fprintf(s, " Time: %d ms\n", sdk.Data.Time)
fmt.Fprintf(s, " Car: %s\n", sdk.Data.Car)
fmt.Fprintf(s, " Flags: %b\n", sdk.Data.Flags)
fmt.Fprintf(s, " Gear: %d\n", sdk.Data.Gear)
fmt.Fprintf(s, " Plid: %d\n", sdk.Data.Plid)
fmt.Fprintf(s, " Speed: %f m/s\n", sdk.Data.Speed)
fmt.Fprintf(s, " RPM: %f RPM\n", sdk.Data.RPM)
fmt.Fprintf(s, " Turbo: %f Bar\n", sdk.Data.Turbo)
fmt.Fprintf(s, " EngTemp: %f °C\n", sdk.Data.EngTemp)
fmt.Fprintf(s, " Fuel: %f\n", sdk.Data.Fuel)
fmt.Fprintf(s, " OilPressure: %f Bar\n", sdk.Data.OilPressure)
fmt.Fprintf(s, " OilTemp: %f °C\n", sdk.Data.OilTemp)
fmt.Fprintf(s, " DashLights: %b\n", sdk.Data.DashLights)
fmt.Fprintf(s, " ShowLights: %b\n", sdk.Data.ShowLights)
fmt.Fprintf(s, " Throttle: %f\n", sdk.Data.Throttle)
fmt.Fprintf(s, " Brakes: %f\n", sdk.Data.Brake)
fmt.Fprintf(s, " Clutch: %f\n", sdk.Data.Clutch)
fmt.Fprintf(s, " Display1: %s\n", sdk.Data.Display1)
fmt.Fprintf(s, " Display2: %s\n", sdk.Data.Display2)
fmt.Fprintf(s, " ID: %d\n", sdk.Data.ID)
fmt.Fprint(s, "}\n\n")
fmt.Fprint(s, "DashLights {\n")
fmt.Fprintf(s, " DL_SHIFT: %t\n", sdk.HasShiftLight())
fmt.Fprintf(s, " DL_FULLBEAM: %t\n", sdk.HasHighBeamLight())
fmt.Fprintf(s, " DL_HANDBRAKE: %t\n", sdk.HasHandbrakeLight())
fmt.Fprintf(s, " DL_PITSPEED: %t\n", sdk.HasPitspeed())
fmt.Fprintf(s, " DL_TC: %t\n", sdk.HasTractionControlLight())
fmt.Fprintf(s, " DL_SIGNAL_L: %t\n", sdk.HasLeftIndicatorLight())
fmt.Fprintf(s, " DL_SIGNAL_R: %t\n", sdk.HasRightIndicatorLight())
fmt.Fprintf(s, " DL_SIGNAL_ANY: %t\n", sdk.HasAnyIndicatorLight())
fmt.Fprintf(s, " DL_OILWARN: %t\n", sdk.HasOilLight())
fmt.Fprintf(s, " DL_BATTERY: %t\n", sdk.HasBatteryLight())
fmt.Fprintf(s, " DL_ABS: %t\n", sdk.HasABSLight())
fmt.Fprintf(s, " DL_SPARE: %t\n", sdk.Data.DashLights&bngsdk.DL_SPARE != 0)
fmt.Fprint(s, "}\n\n")
fmt.Fprint(s, "ShowLights {\n") // Fixed typo "ShowLigths"
fmt.Fprintf(s, " DL_SHIFT: %t\n", sdk.ShiftLight())
fmt.Fprintf(s, " DL_FULLBEAM: %t\n", sdk.HighBeam())
fmt.Fprintf(s, " DL_HANDBRAKE: %t\n", sdk.Handbrake())
fmt.Fprintf(s, " DL_PITSPEED: %t\n", sdk.Pitspeed())
fmt.Fprintf(s, " DL_TC: %t\n", sdk.TractionControl())
fmt.Fprintf(s, " DL_SIGNAL_L: %t\n", sdk.LeftIndicator())
fmt.Fprintf(s, " DL_SIGNAL_R: %t\n", sdk.RightIndicator())
fmt.Fprintf(s, " DL_SIGNAL_ANY: %t\n", sdk.AnyIndicator())
fmt.Fprintf(s, " DL_OILWARN: %t\n", sdk.OilLight())
fmt.Fprintf(s, " DL_BATTERY: %t\n", sdk.BatteryLight())
fmt.Fprintf(s, " DL_ABS: %t\n", sdk.ABS())
fmt.Fprintf(s, " DL_SPARE: %t\n", sdk.Data.ShowLights&bngsdk.DL_SPARE != 0)
fmt.Fprint(s, "}\n\n")
fmt.Fprint(s, "Flags {\n")
fmt.Fprintf(s, " OG_TURBO (Has Turbo): %t\n", sdk.HasTurbo())
fmt.Fprintf(s, " OG_KM (Is Metric): %t\n", sdk.PrefersKm())
fmt.Fprintf(s, " OG_BAR (Pressure): %t\n", sdk.PrefersBAR())
fmt.Fprint(s, "}")
}