-
Notifications
You must be signed in to change notification settings - Fork 5
Refactor to enable Driver commands for /flex, new Flex device support, testing via socat/VirtualDevice
#162
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
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
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
|
I removed all the Flex recordings for now to avoid repo/diff bloat. We can either:
I vote option 1 or 2. Option 3 is inconvenient, because you can't easily deal with the files while developing. |
|
Added |
I browsed the upstream repo, and while it's maintained, it seems the maintainer is burnt out on/uninterested in bringing back the metadata. What about the sysfs-ignore, is that worth trying to upstream or a hack? If we stick with the fork, shall we move it to the |
| @@ -0,0 +1,143 @@ | |||
| // Disclaimer: this is 90% vibe-coded | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's your code and I will review it as your code. :-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not trying to disown it, just hinting at the fact that since VirtualDevice and VirtualSerialPort modules are purely testing tools, it does matter much how they are implemented, as long as they satisfy the testing needs and do not have strange loopholes.
Yeah, my impression is also that any extra development is "on hold" in go-serial. The Driver code and tests no longer rely on the sysfs-ignore actually, that is a left-over from an earlier approach where I was trying to create a "real" serial device that could be discovered by I'll move the fork to |
|
Changed to dividat fork, but will need a version/ref bump once dividat/go-serial#1 is merged. EDIT: bumped.
|
knuton
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very good work, still working through it, but submitting some feedback mostly about code structure.
Will perform manual testing with physical Senso next.
| } else if message.Discovered != nil { | ||
| serviceEntry := message.Discovered.TcpDeviceInfo | ||
| msg := struct { | ||
| Type string `json:"type"` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we consider adding an additional device tag field that explicitly encodes which kind of device was discovered?
It took me a while to understand what's happening here.
Maybe branch on which of TCP/USB is defined and don't "mix" definitions, even if a little more verbose?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 for adding the extra device tag that the client can use to simplify parsing.
But not entirely sure how you propose to branch/not "mix", while still being backwards compatible.
Currently Play receives Discover replies (for the Senso) that look like:
{ "type: "Discovered", "service": <TcpDeviceInfo>, "ip": [ "1.1.1.1", ... ] }
do you propose to re-use the service field, i.e.:
{ "type: "Discovered", "device": "senso", "service": <TcpDeviceInfo>, "ip": [ "1.1.1.1", ... ] }
{ "type: "Discovered", "device": "flex", "service": <UsbDeviceInfo> }
This would still "mix" things, as e.g. the IP is present for Senso, but not Flex devices.
Or do you mean moving service/ip to a sub-field, i.e. making things not backwards compatible? I really wanted to avoid having to make any kind of changes to the Senso part in Play to minimize the surface. I think it's better to tackle these rougher corners if/once we attempt to fully unify the device backends.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thinking to keep the format backwards compatible and flat as you made it, but use the device tag to make explicit what it should be parsed as.
- No
devicetag: Implicitly parse only for TCP device = "senso": Parse only for TCPdevice = "flex"(ordevice = "serial"?): Parse only for USB
If there are superfluous fields for a given device tag, they can simply be ignored (this is the default behaviour in most JSON parsing).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I went a slightly different way and duplicated the DeviceInfo into a separate device field of the Status reply. Otherwise, it would mean we have two different serialization paths and hence two different parsing paths for Status vs Discovered messages. Also enforced safe/correct creation of DeviceInfo for the different device types.
Re naming: went for flex instead of serial, since otherwise it's a strange taxonomical mix in the enum ("senso" and "serial").
More details in the commit: ec74088
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P.S. This breaks the PoC implementation in Play, will fix it after I'm done with other parts.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: "util" is underselling the importance of these modules, and util/websocket is very vague. "device"/"devices" or "protocol"/"client protocol"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True, was thinking about this as well. Maybe transport/websocket? Or.. just websocket/?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Something emblematic of why I think websocket is not a very helpful name is code like this:
func (backend *DeviceBackend) GetStatus() websocket.Status {
return websocket.Status{
Address: backend.address,
}
}This is really not a WebSocket status at all, and also not a WebSocket address. So one needs to override intuitive reading with background knowledge completely in this function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right. I think the protocol part deserves it's own namespace, so maybe it can even become two modules.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Converted websocket/command.go to a protocol module (8e31859) and moved websocket to the top-level (2eab988). At first I did transport/websocket, but I don't think it makes much sense to have another layer since 1) there's only one transport currently; 2) the module does more than just handle transport; and 3) the top-level is anyway messy with modules that should not probably be top-level (e.g. service, logging and firmware).
| reader := deviceToReader(device) | ||
| // should not happen | ||
| if reader == nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I might have missed something, but to me it looks like the enumerator only gave us a list of all Teensys, so this could very well happen?
Could we "parse, don't validate" and look up the reader earlier when checking for supported devices?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, good point. Will maybe just move the reader-choice closer to enumeration, but need to think a bit.
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 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:
/flexendpoint WS clients can issue the same Driver commands as for the/sensobackend. All commands except for firmware update are supported.websocket/command.go) is updated to optionally include USB device metadata inDeviceInfoJSONs (in addition to discovered mDNS service metadata).Broadcastmessages to all connected endpoint clients. Currently/flexendpoint 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./flexclient can now indicate it wants to manually control the device connection by passing an WS subprotocolmanual-connect. This disables automatic scan+connect, requiring the client to manually issueDiscoverandConnectcommands. 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.tools/record-flex-serial(relies on strace, so Linux-only)-test-modeflag), which enables vitrual/mock serial device registration via aPOST /flex/mockrequest (used in tests and below)tools/replay-flexnow 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)/flexendpointChanges:
go-serialis updated to a forked version that enables extra USB descriptors:bcdDevice,productandmanufacturer. Tested to work on Linux and macOS. Windows support blindly implemented, not tested./flexdevice at onceNot implemented, but considered (sorted by most->least important):
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.replay-flexdoes not know what device to emulate, it has to be specified manually, which can be quite error proneudevorfsnotifyon Linux at least, instead of manual sleep+polling. TBD in a separate PR.Discovercommands 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.Potential issues:
go-serialis untested, as mentioned aboveChanges should be 100% backwards compatible for both
/sensoand/flexclients, 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
/flexdevice metadata: https://github.com/yfyf/diviapps/tree/flex-discovery (fully working, but not yet rebased / cleaned up).Checklist
manufacturer == Sensitronics, which is maybe good enough, but need to set it in stone + document.