Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
c01ffbc
Flex-like-Senso: extract common websocket connection handling
yfyf Nov 3, 2025
484aa17
Move Driver protocol to separate file
yfyf Nov 3, 2025
1686c72
Flex-like-Senso: implement "hand-shake" commands in Flex
yfyf Nov 4, 2025
83a5724
Bump go and replace serial package to forked one
yfyf Nov 7, 2025
96b07de
Add support Flex device info, refine auto-connect, add broadcasts
yfyf Nov 7, 2025
d8caf08
Move serial chunking outside and add Sensitronics support
yfyf Nov 10, 2025
93cbe82
Add bcdDevice to UsbDeviceInfo for identifying V4 vs V5 Flex
yfyf Nov 10, 2025
08d87b8
Remove unused const
yfyf Nov 10, 2025
03acce7
Simplify context/cancelation management in ReadFromSerial
yfyf Nov 10, 2025
7a5d931
Ensure auto-connect is always disabled
yfyf Nov 10, 2025
00290b4
Move out Flex device enumeration to separate package
yfyf Nov 12, 2025
508fb2c
Support creating/destroying mock devices via HTTP POST/DELETE
yfyf Nov 12, 2025
e7e4ce8
Announce testMode in main and louder
yfyf Nov 13, 2025
6f8df05
Fix: actually use the Broadcast messages
yfyf Dec 23, 2025
f81271a
Add a passthru Flex device for testing purposes
yfyf Dec 23, 2025
509c958
Add TODO note about manual-connect header
yfyf Dec 23, 2025
b653f7f
Initial Flex unit tests using Passthru
yfyf Dec 23, 2025
114bed5
Use Websocket subprotocol to control auto/manual-connect in Flex
yfyf Dec 23, 2025
d00d0b5
Refactor test expects with helpers
yfyf Jan 5, 2026
40e8d3d
Upgrade broadcast test to multi-client
yfyf Jan 5, 2026
d884a2e
Add a multi-device discover test (and fix a bug in impl!)
yfyf Jan 5, 2026
062b926
Enable changing replay speed in VirtualDevice
yfyf Jan 6, 2026
000a8ba
Upgrade replay-flex to use a mocked VirtualDevice
yfyf Jan 6, 2026
4a4bb4a
Replace minimist with commander for CLI parsing
yfyf Jan 6, 2026
8bbefa8
Allow specifying the device type
yfyf Jan 6, 2026
1d5cdf4
Add a way to record raw serial data and to replay both raw and WS data
yfyf Jan 7, 2026
90027aa
Move and rename v4 recordings
yfyf Jan 7, 2026
458cc88
Fixup: simplify TrimPrefix
yfyf Jan 7, 2026
deed5f3
Add Flex v5 recordings
yfyf Jan 7, 2026
892d984
Add a basic test for v4
yfyf Jan 8, 2026
5b994db
Flex v4 and v5 tests using synthetic data
yfyf Jan 8, 2026
adbef42
Update Sensitronics to new simplified protocol
yfyf Jan 8, 2026
1634f78
Add test for Sensitronics
yfyf Jan 8, 2026
096b947
Tidy up the tests: re-group, re-order
yfyf Jan 8, 2026
f14d3cf
Further test tidy up and some generalizations
yfyf Jan 8, 2026
8ad9514
Avoid a temporary disconnect if two clients race
yfyf Jan 8, 2026
9cddde4
Make logs clearer
yfyf Jan 8, 2026
250147e
Fixup: gofmt
yfyf Jan 8, 2026
fd2b777
Remove optional TODO
yfyf Jan 9, 2026
2a6a725
Add Sensitronics recordings
yfyf Jan 9, 2026
1b1040e
Fixup: add missing test helper file
yfyf Jan 12, 2026
c353954
Use context.AfterFunc to handle context cleanup
yfyf Jan 12, 2026
c12278c
Remove TODO, keep background scan
yfyf Jan 12, 2026
a0ef96a
Fixup: remove noTx TODO
yfyf Jan 12, 2026
26fbcf2
Remove protocol note TODOs
yfyf Jan 12, 2026
6b358ab
Update Readme with recording naming convention
yfyf Jan 12, 2026
f815ecf
Update docs on how to use replay-flex
yfyf Jan 12, 2026
a17ed7f
Expand explanations of functions
yfyf Jan 12, 2026
1d82ece
Print actual error if port detail parsing fails
yfyf Jan 12, 2026
77c27ed
Bump go-serial to version with bcdDevice support on macOS
yfyf Jan 13, 2026
ad30a30
Mark .rec and .dat files as binary to prevent text diffs
yfyf Jan 14, 2026
da22cdf
Revert "Move and rename v4 recordings"
yfyf Jan 14, 2026
c6942fe
Revert "Add Flex v5 recordings"
yfyf Jan 14, 2026
e4a2150
Revert "Add Sensitronics recordings"
yfyf Jan 14, 2026
2287ebb
Change to dividat fork of go-serial
yfyf Jan 19, 2026
cdbab9c
Use a tagged reference to dividat/go-serial
yfyf Jan 20, 2026
7b48814
Name PASSTHRU- stripping more clearly
yfyf Jan 23, 2026
544b17e
Use a more idiomatic alias for serial/enumerator
yfyf Jan 23, 2026
66087b4
Use a more specific name for sendGeneric
yfyf Jan 23, 2026
241ab6f
Use a more idiomatic alias for flex.enumerator
yfyf Jan 23, 2026
4d0bd71
Move devices to a common module
yfyf Jan 23, 2026
ffb149a
Move detailed Flex recording/replay docs to separate file
yfyf Jan 23, 2026
ec74088
Ensure correct DeviceInfo creation and serialization
yfyf Jan 23, 2026
8e31859
Move websocket/command.go to a 'protocol' module
yfyf Jan 23, 2026
2eab988
Make websocket a top-level module
yfyf Jan 23, 2026
83b5fdd
Combine device enumeration with device family identification
yfyf Jan 26, 2026
eb763ce
Fix typo
yfyf Jan 26, 2026
7481284
Move vibe-coding disclaimers to git history
yfyf Jan 28, 2026
6b93fca
Identify V6 devices according to the agreed new USB descriptors
yfyf Jan 28, 2026
bf41097
Drop flex- prefix from pubsub topic names
yfyf Jan 28, 2026
644d4e9
Sleep before writing sample data in replays
yfyf Jan 28, 2026
e9ca559
Replace runtime -test-mode flag with a 'debug' build tag
yfyf Jan 28, 2026
c91c8fd
Use a better name for serial device family handlers
yfyf Jan 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*.dat binary
*.rec binary
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ VERSION := $(shell git describe --always HEAD)
CHECKSUM_SIGNING_CERT ?= ./keys/checksumsign.private.pem


### Simple build ##########################################
### Simple debug build ####################################
.PHONY: build
build:
@./build.sh -i $(SRC) -o $(OUT) -v $(VERSION)
@./build.sh -i $(SRC) -o $(OUT) -v $(VERSION) -t debug


### Test suite ############################################
Expand Down
49 changes: 44 additions & 5 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,30 @@ The default nix shell (defined in `nix/devShell.nix`) provides all necessary dep

- Enter the nix development shell: `nix develop`
- Build the driver: `make`
- Run the driver: `./bin/dividat-driver`
- Run the driver: `make run` or `./bin/dividat-driver`


### Build tags

By default, all the development-related make targets (`make build`, and those
that depend on it, e.g. `make test`, `make run`) build the driver with `debug`
tag.

The `debug` tag currently enables mock device registration for the Flex backend.

Cross-build targets and plain `go build` invocations do not pass the `debug`
tag and produce "release" builds.

If you are using IDEs/other tools to build the Driver, make sure they pass the
`debug` tag for dev builds.

### Tests

Run the test suite with: `make test`.

Make sure actual Senso/Flex devices are unplugged, since their presence can
cause test assumptions about available devices to fail.

### Go modules

To install a module, use `go get github.com/owner/repo`.
Expand Down Expand Up @@ -120,7 +138,20 @@ Data from Senso can be recorded using the [`recorder`](src/dividat-driver/record

#### Senso Flex data

Like Senso data, but with `make record-flex`.
On Linux, you can record the raw serial data using:

./tools/record-flex-serial -o recording.serial.dat

On macOS, you can only record the WebSocket binary data from the `/flex`
endpoint as output by the Driver, using:

make record-flex > recording.ws.dat

For Flex V6 (Sensitronics) devices, the two methods should be equivalent.

The Driver must be running and connected to the device in both cases.

For more details, see the [Flex recording and replay](tools/flex-recording-and-replay.md) docs.

### Data replayer

Expand All @@ -140,8 +171,16 @@ The Senso replayer will appear as a Senso network device, so both driver and rep

#### Senso Flex replay

The Senso Flex replayer (`npm run replay-flex`) supports the same parameters as the Senso replayer.
The Senso Flex replayer (`npm run replay-flex`) supports the same parameters as the Senso replayer and also allows to fake device metadata.

Driver must be running and built with the `debug` tag, which is the default if
you run `make build` and/or `make run`.

You can then replay a recording using:

node tools/replay-flex -d <device> recording.dat

It mocks the driver with respect to the `/flex` WebSocket resource and the `/` metadata HTTP route, so the real driver can not be running at the same time.
If you are using a WebSocket binary stream recording, you should also specify
`--passthru` mode.

You can control the mocked driver version via the `--driver-version` flag.
For more details, see the [Flex recording and replay](tools/flex-recording-and-replay.md) docs.
11 changes: 8 additions & 3 deletions build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,18 @@ set -euo pipefail
IN=""
OUT=""
VERSION=""
BUILD_TAGS=""

while getopts "i:o:v:" opt; do
while getopts "i:o:v:t:" opt; do
case $opt in
i) IN="$OPTARG"
;;
o) OUT="$OPTARG"
;;
v) VERSION="$OPTARG"
;;
t) BUILD_TAGS="$OPTARG"
;;
\?) echo "Invalid option: -$OPTARG" >&2; exit 1 ;;
esac
done
Expand All @@ -60,7 +63,7 @@ ensure_flag_set "-i" "$IN"
ensure_flag_set "-o" "$OUT"

if [ "$all_flags_set" = false ]; then
echo "Usage: build-driver -v <version> -i <input> -o <output>"
echo "Usage: build-driver -v <version> -i <input> -o <output> [-t <tags>]"
exit 1
fi

Expand All @@ -81,7 +84,9 @@ if [ $VERBOSE = "1" ]; then
echo "GCO_ENABLED=${CGO_ENABLED:=}"
echo "CC=${CC:=}"
echo "LD_FLAGS=$LD_FLAGS"
echo "BUILD_TAGS=$BUILD_TAGS"
fi

go build -ldflags "$LD_FLAGS" -o "$OUT" "$IN"
go build -tags "$BUILD_TAGS" -ldflags "$LD_FLAGS" -o "$OUT" "$IN"

echo "Built $OUT"
12 changes: 10 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/dividat/driver

go 1.12
go 1.18

require (
github.com/cenkalti/backoff v2.2.1+incompatible
Expand All @@ -17,8 +17,16 @@ require (
// Both projects are dormant at the moment, but we might want to re-evaluate this
// dependency choice as these projects evolve in the future.
github.com/libp2p/zeroconf/v2 v2.2.0

github.com/pin/tftp v2.1.0+incompatible
github.com/sirupsen/logrus v1.8.1
go.bug.st/serial v1.6.1
)

require (
github.com/creack/goselect v0.1.2 // indirect
github.com/miekg/dns v1.1.43 // indirect
golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6 // indirect
golang.org/x/sys v0.19.0 // indirect
)

replace go.bug.st/serial => github.com/dividat/go-serial v1.6.4-usbmetadata
18 changes: 6 additions & 12 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0
github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY=
github.com/cskr/pubsub v1.0.2 h1:vlOzMhl6PFn60gRlTQQsIfVwaPB/B/8MziK8FhEPt/0=
github.com/cskr/pubsub v1.0.2/go.mod h1:/8MzYXk/NJAz782G8RPkFzXTZVu63VotefPnR9TIRis=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ=
github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI=
github.com/dividat/go-serial v1.6.4-usbmetadata h1:U8b1GaZ1roUtf1Jve6sg+yuHdLe7FsBvZxnm1W7bipo=
github.com/dividat/go-serial v1.6.4-usbmetadata/go.mod h1:nofMJxTeNVny/m6+KaafC6vJGj3miwQZ6vW4BZUGJPI=
github.com/ebfe/scard v0.0.0-20190212122703-c3d1b1916a95 h1:OM0MnUcXBysj7ZtXvThVWHMoahuKQ8FuwIdeSLcNdP4=
github.com/ebfe/scard v0.0.0-20190212122703-c3d1b1916a95/go.mod h1:8hHvF8DlEq5kE3KWOsZQezdWq1OTOVxZArZMscS954E=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
Expand All @@ -25,12 +26,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
go.bug.st/serial v1.6.1 h1:VSSWmUxlj1T/YlRo2J104Zv3wJFrjHIl/T3NeruWAHY=
go.bug.st/serial v1.6.1/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6 h1:0PC75Fz/kyMGhL0e1QnypqK2kQMqKt9csD1GnMJR+Zk=
golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
Expand All @@ -42,13 +39,10 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210426080607-c94f62235c83/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 h1:v6hYoSR9T5oet+pMXwUWkbiVqx/63mlHjefrHmxwfeY=
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
3 changes: 2 additions & 1 deletion nix/devShell.nix
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ mkShell
go
gcc

# node for tests
# test dependencies
nodejs
socat

# Required for building go dependencies
autoconf
Expand Down
49 changes: 28 additions & 21 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@
"bonjour": "^3.5.0",
"chai": "^4.1.1",
"mocha": "^10.8.2",
"ws": "^5.2.4"
"ws": "^8.18.3"
},
"devDependencies": {
"binary-split": "^1.0.5",
"minimist": "^1.2.8"
"commander": "^12.1.0"
}
}
86 changes: 86 additions & 0 deletions src/dividat-driver/flex/device/passthru/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Passthru device for sending chunked serial bytes "as is"
// Useful for:
// - recording raw serial data from a device
// - replaying recoreded raw serial data
//
// Currently used in unit tests.
package passthru

import (
"bufio"
"context"
"io"

"github.com/sirupsen/logrus"
"go.bug.st/serial"
)

// Serial communication
type PassthruHandler struct{}

func (PassthruHandler) Run(ctx context.Context, logger *logrus.Entry, port serial.Port, tx chan interface{}, onReceive func([]byte)) {
logger.Info("PassthruReader started")
readerCtx := context.WithoutCancel(ctx)

// Channel to receive ack that reader is done
readerDoneChan := make(chan struct{})

// Start the initial reader goroutine
go readFromPort(readerCtx, logger, port, onReceive, readerDoneChan)

// Forward WebSocket commands to device
for {
select {
case <-ctx.Done():
return

case <-readerDoneChan:
return

case i := <-tx:
data, _ := i.([]byte)
_, err := port.Write(data)
if err != nil {
logger.WithField("error", err).Info("Failed to write command to serial port.")
return
}
logger.WithField("bytes", data).Debug("Wrote binary command to serial out: " + string(data))
}
}
}

// Infinite loop for requesting and reading serial data.
// Stops (returns) upon any error or ctx cancel.
func readFromPort(
ctx context.Context,
logger *logrus.Entry,
port serial.Port,
onReceive func([]byte),
doneChan chan<- struct{},
) {
defer func() {
// Signal that the reader has completed
close(doneChan)
}()

// max estimated frame size in bytes:
// 10 (header) + 24*24*4 (samples) + 20 (misc extras) = 2380 bytes, so ~2 kilobytes
reader := bufio.NewReaderSize(port, 2048)

var message []byte = make([]byte, 2048)
// Start signal acquisition
for {
// Terminate if we were cancelled
if ctx.Err() != nil {
logger.Debug("Stopping reader: context cancelled")
return
}

readBytes, err := io.ReadAtLeast(reader, message, 1)
if err != nil {
logger.WithField("err", err).Error("Error reading from serial port")
return
}
onReceive(message[:readBytes])
}
}
Loading