Skip to content

Conversation

@yfyf
Copy link
Contributor

@yfyf yfyf commented Jan 13, 2026

This PR materializes the idea of unifying Senso and Flex backends to have the same Driver command protocol, adds support for the new Sensitronics protocol/devices and revamps the testing workflow by introducing mocked serial devices (using socat).

Added functionality:

  • /flex endpoint WS clients can issue the same Driver commands as for the /senso backend. All commands except for firmware update are supported.
  • the Driver command protocol (see websocket/command.go) is updated to optionally include USB device metadata in DeviceInfo JSONs (in addition to discovered mDNS service metadata).
  • the Driver gains support for sending Broadcast messages to all connected endpoint clients. Currently /flex endpoint uses this to inform clients about change in device connectivity status. This eliminates the need to poll for changes from the client side. Not used in /senso.
  • A /flex client can now indicate it wants to manually control the device connection by passing an WS subprotocol manual-connect. This disables automatic scan+connect, requiring the client to manually issue Discover and Connect commands. Not used in Play, so can be thrown out, but this is a parity check that we can control Flex and Senso devices "in the same way" from Play.
  • Raw device serial data can now be recorded using tools/record-flex-serial (relies on strace, so Linux-only)
  • Driver can now be run in test mode (-test-mode flag), which enables vitrual/mock serial device registration via a POST /flex/mock request (used in tests and below)
  • tools/replay-flex now uses the Driver for replays and can replay both raw serial data (emulating a serial device via socat) and WS binary data (by asking the Driver to run with a "passthru" reader, see below)
  • Driver has two new Flex device (serial) readers: Sensitronics (for the new devices) and a special Passthru reader (for various dev/testing purposes)
  • Proper unit tests for the /flex endpoint
  • Added raw serial and WS data recordings for all current Flex devices

Changes:

  • go-serial is updated to a forked version that enables extra USB descriptors: bcdDevice, product and manufacturer. Tested to work on Linux and macOS. Windows support blindly implemented, not tested.
  • Driver used to (implicitly) sleep for 2 seconds on serial device disconnect/error. This is now removed, if a client aggressively attempts to reconnect without any backoff, so will the driver.
  • Avoid a temporary disconnect due to a race condition when two clients connect to the same /flex device at once

Not implemented, but considered (sorted by most->least important):

  • Driver does not send "derived" device metadata (e.g. deviceType=Flex, gen=v5) and so clients have to roll their own device identification logic. Mixed feelings about this. On the one hand, this is aligned with my (future) vision of the driver as merely a "dumb" chunking serial<->WS proxy, so that device firmware/protocol changes do not necessitate driver updates (e.g. if we add a new message type, the driver does not care). On the other hand, this requires duplicating the (USB -> device type/gen) identification logic in the client.
  • As before, Driver does not attempt to distinguish between Flex v4 and v5 and relies on client commands to switch between 8bit vs 12bit. This logic could be moved to the Driver in a backwards compatible way.
  • Recordings have no metadata, so replay-flex does not know what device to emulate, it has to be specified manually, which can be quite error prone
  • Recordings do not capture commands sent to the device (hence there's no way to know how the device was configured during the recording)
  • Would be good to replace USB device detection with udev or fsnotify on Linux at least, instead of manual sleep+polling. TBD in a separate PR.
  • the Discover commands remains awkwardly asynchronous in /flex (same as in in /senso) - the client has no way of knowing when the discovery is "finished", it can merely wait for the specified timeout to happen.
  • the Driver command protocol could be revised further and the implementation generalized/refactored even more, but this is PR is already too big.

Potential issues:

  • Windows support for the extra USB descriptors in go-serial is untested, as mentioned above
  • No way to record raw serial data in macOS, diminished dev experience. I see no easy/good way to achieve this, see Readme and 1d5cdf4 for extra details. Should not be a practical blocker, since for Sensitronics serial data ~= WS binary data.
  • Variance in sample timing in the serial recordings and replays, due to strace and more layers being involved. This can probably be adjusted by chunking the recording instead of replaying every read. I would say it's not a problem right now, since we don't rely on precise timing during replays.

Changes should be 100% backwards compatible for both /senso and /flex clients, i.e. Play should be able to run with this Driver "as is". Older Play versions will simply not be able to use Driver commands/events for Flex or understand the Sensitronics protocol.

Commits unsquashed, since they contain explanations about some of the choices. History is messy after the initial ~5 commits,so probably best to squash before merging.

Apologies for the huge diff.

Related PRs/code

Checklist

  • Changelog updated
  • Code documented
  • Decide on a way to identify Flex/Sensitronics devices going forward. Currently Driver checks manufacturer == Sensitronics, which is maybe good enough, but need to set it in stone + document.

yfyf added 30 commits January 12, 2026 13:31
This is step 1 in introducing Driver commands to Flex: simply extracting
the common (i.e. literally mostly identical) websocket handling code and
encapsulating the device specific commands into a DeviceBackend
interface.

Adding some TODOs for trickier areas.

Purposefully not introducing any new structure to keep the diff
readable/minimal.

At this stage this should be backwards compatible: Driver command
handling is unchanged for the Senso, while Flex can now receive and
respond to some of them, but keeps the backwards-compatible behaviour of
auto-connecting on first websocket connection.
This adds proper Discover/Connect/Disconnect command handling to the Flex
backend, while keeping the /flex endpoint backwards compatible with
"auto-connect" behaviour. The auto-connect backwards compatibility is
useful both for a seamless transition in Play and for allowing tools
like `record-flex` to continue to "just work".

By default, the Flex backend auto-connects to the first Flex-like serial
device when a new WS connection is opened, as before. However, the
client can pass the header `manual-connect: 1`, to indicate that it will
set up the connection via Driver commands itself. If a previous client
has already set up the connection, the header has no effect.

Implementation change: this removes the 2 second sleep after serial
connection closure (e.g. due to error). Blocking the thread seems like a
strange way to implement backoff?
The fork provides extra USB device details on Linux.

The go version bump is needed for basic generics support.

The rest is due to `go mod tidy`
This extends the Driver to:
- Support Flex device metadata in Driver message replies
- Enable broadcasting messages to all connected clients

Using it, the Flex backend implements a flow where it auto-connects to
devices and informs about device changes by broadcasting a Status
update. This eliminates the need for the client to periodically poll the
Driver.

The protocol changes are backwards compatible for the /senso endpoint.
- Handle port closure in the same context where it is opened
- Move context cancelation to the caller
- Remove explicit reader cancelation - it will automatically canceled
  when parent context is canceled
Even if device connection is not established, we still need to cancel
the auto-connect if there are no subscribers remaining.

Note: Disconnect already internally checks if `cancelCurrentConnection != nil`
Bumping node_modules/ws to be able to identify binary messages.
Also split up tests for checking different aspects.
The strace recording approach is a bit hacky, but the alternatives are
even messier. If we want to both interact and observe, we have two
choices:
- multiplex the serial stream before it reaches the driver. This would
  mean circumventing the serial enumeration, registering a mock device
  just to record it, etc. Messy.
- multiplex the serial stream once it reaches the driver, i.e. expose
  yet another binary stream from the driver. Complex and potentially
  dangerous code to have in production.

Philosophical digression: with the new Flex protocol, Driver really
becomes just a glorified chunker + WS<->USB/serial proxy.

TODO: store metadata or at least a file suffix convention (, .v4.ws.dat,
.v5.serial.dat) to help distinguish what type of data was recorded.

Disclaimer: partially vibe-coded
yfyf added 10 commits January 23, 2026 10:45
This modifies the protocol to serialize DeviceInfo in both Status and
Discovered messages in an identical way. This simplifies the parsing
logic for the clients, since they can:
1) Rely on the `deviceType` field to determine what fields are present
2) Re-use the same parsing logic in Status and Discovered messages

To prevent incorrectly constructed DeviceInfo structs, all the fields
are made private and helper functions are provided for initialization.

To maintain backwards compatibility, Senso device data is duplicated in
the top-level fields in Status messages. Since this data is used at
a low-volume, this should not pose any problems.
This ensures that devices returned by ListMatchingDevices() are all
supported devices that we know how to connect to. Previously, the
enumerator would only check the Teensy Vendor ID.

Also adjusted the tests and extended to verify unknown devices are not
Discovered.

Note: purposefully not providing DeviceFamily to the client, since it
would break the "passthru" concealment.
@yfyf
Copy link
Contributor Author

yfyf commented Jan 26, 2026

Off-topic: the "CORS and PNA test" is flaky: https://github.com/dividat/driver/actions/runs/21350862315/attempts/1

I think I've seen it fail a long time ago too.

@yfyf
Copy link
Contributor Author

yfyf commented Jan 26, 2026

@knuton addressed all review comments as of now. Let me know if you want me to patch Play side for testing. I'd prefer to wait for feedback, finalize here, then return to Play side.

Copy link
Member

@knuton knuton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very good!

I confirmed compatibility with an unaltered main-branch version of Play, including discovery, commands, firmware update.

I was also able to replay v5 recordings via passthru with the same version of Play.

Has flex replay also been tested on macOS?

@@ -0,0 +1,143 @@
// Disclaimer: this is 90% vibe-coded
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I agree that testing tools are under different constraints than the production code.

I am not sure what to practically make of the comment going forward -- is it somehow like generated code, do I need to keep vibing when I want to edit it? Probably not? Origin is good to document (commit message?), but from now on it's just code.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't really have codified JS code style in this repo, and you are adding a much more recent dialect than most of the existing files are written in (which is fine).

But maybe just drop semicolons, which is most obvious stylistic difference?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropping the semicolons actually breaks a bunch of tests in mysterious ways. Is it really a good idea to default to no semicolons?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, it's very possible and nowadays I think the norm to write JS that way ;-)

It's a bit unclear to me how things break, and I don't think it should be too hard. But I am OK with merging as is and standardizing JS in this repo separately.

@knuton knuton added changes requested Changes to be made before next round of reviewing and removed reviewable Ready for initial or iterative review. labels Jan 27, 2026
@yfyf
Copy link
Contributor Author

yfyf commented Jan 28, 2026

Has flex replay also been tested on macOS?

No yet, should be done. Is make test known to work on macOS? Could add a test workflow for macOS in the CI.

yfyf added 6 commits January 28, 2026 11:24
Dropping the `sensitronics` alias for the `v6`, but keeping the
possibility to identify the prototype devices when Manufacturer ==
Sensitronics.
Since each DeviceBackend uses a separate broker instances, there is no
need to use globally unique names.
This ensures that samples are produced with the same delays they were
recorded (previously it was shifted-by-one).
Dev builds use a `debug` tag by default.

When the build is tagged with `debug`, a functional MockDeviceRegistry
is created. Otherwise, a noop stub is used.
@yfyf
Copy link
Contributor Author

yfyf commented Jan 28, 2026

Has flex replay also been tested on macOS?

No yet, should be done. Is make test known to work on macOS? Could add a test workflow for macOS in the CI.

I changed CI to run the tests on macOS and they seem to pass for Flex, but one Senso test fails (or is flaky):
https://github.com/yfyf/driver/actions/runs/21441751480/job/61746447633

EDIT: it's the "Can discover mock Senso" test, so seems to be related to mDNS/bonjour, could be CI specific. Removing the CI change for now, we can add it / fix it in a separate PR.

EDIT 2: confirmed by @terezka to work on her dev macOS machine locally, so it seems to be a CI / GH runner thing.

@knuton knuton removed the changes requested Changes to be made before next round of reviewing label Jan 30, 2026
@knuton knuton merged commit 4102ab6 into dividat:main Jan 30, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants