Replies: 6 comments 3 replies
|
I think the main goal here should be that Decaid provides enough generic platform capabilities that people can build integrations without requiring the core team to babysit every device and every implementation step. So I would not optimize #749 purely for the smallest possible API. At the same time, I also would not try to implement every possible transport and device class in #749 itself. I think the right target is a broad platform contract with an incremental implementation. If the extension surface is too narrow, every new grinder, scale, sensor, transport, or protocol eventually needs another Decaid core change. That defeats much of the purpose of having plugins/extensions. The important distinction for me is between broad platform primitives and device-specific implementations. I would like an extension to be able to declare what it contributes, for example:
and separately request the privileged native transports it needs:
Those are two different concepts. A driver declaration tells Decaid what the extension contributes to the application. A transport permission tells Decaid which privileged native capability the extension is allowed to use. Conceptually a manifest could look something like: {
"permissions": [
"transport.ble"
],
"drivers": [
{
"id": "bookoo.motto80",
"type": "grinder",
"match": {
"names": ["BOOKOO MT80", "MOTTO80 BLE"],
"services": [
"4d543830-0001-4b80-8f00-424f4f4b4f4f"
]
}
}
]
}I would keep the driver declaration out of the permission namespace. I would also avoid giving every extension its own unmanaged BLE scanner. Decaid already has to coordinate BLE scanning and connection ownership for the DE1, scales and other devices. The extension should describe what it is interested in, while Decaid owns the actual scanning/lifecycle and hands matching candidates to the extension. Something along these lines: export async function attach(device) {
await host.ble.subscribe(
device,
SERVICE_UUID,
STATUS_UUID,
onStatus
);
await host.ble.write(
device,
SERVICE_UUID,
COMMAND_UUID,
handshake
);
}The I also think matching should probably be treated as a two-stage operation rather than assuming advertisement metadata is always sufficient. The manifest can cheaply pre-filter candidates using things such as:
Then the extension could optionally perform a bounded That gives Decaid a place to arbitrate cases where multiple drivers match the same candidate, and avoids forcing all device identity logic into the central matcher. The same general driver model can then work for other transports. A serial grinder extension should not require us to invent another grinder architecture; it requests A network device may request TCP/TLS or WebSocket. A BLE sensor requests BLE. The protocol implementation changes, but Decaid's device model does not have to. I would define transport capabilities according to their actual programming semantics rather than only physical medium. BLE/GATT, serial streams, TCP streams and WebSocket messages are meaningfully different APIs. Something like generic The host should own the difficult shared parts:
The extension should own the protocol:
There is one lifecycle distinction I think is important here: transport connected is not necessarily driver ready. For example, BLE may report that a MOTTO80 connection is established, but the extension may still need to subscribe, perform a handshake, query initial state and verify the device before Decaid should consider the grinder usable. So the generic driver lifecycle probably needs some equivalent of: rather than treating an open transport connection as the end of initialization. I think we should also go one step further and give drivers a generic way to describe the functionality they expose, rather than forcing every new product to grow another product-specific Dart interface. I would not make this completely free-form, though. Decaid should still define stable semantics for known device classes. A grinder should have a set of well-known grinder capabilities with documented meaning, units and behavior, while still allowing namespaced extension-specific capabilities for unusual hardware. For example a grinder could expose capabilities roughly like: {
"properties": {
"grindSetting": {
"type": "number",
"min": 0,
"max": 1000,
"writable": true
},
"grindRpm": {
"type": "number",
"unit": "rpm",
"min": 0,
"max": 1050,
"writable": true
},
"feedingRpm": {
"type": "number",
"unit": "rpm",
"min": 0,
"max": 65,
"writable": true
},
"humidity": {
"type": "number",
"unit": "%",
"writable": false
}
},
"actions": {
"start": {},
"stop": {},
"loadPreset": {
"parameters": {
"presetId": {
"type": "string"
}
}
}
}
}I would separate properties/state, actions, and potentially events rather than trying to represent everything as a writable property.
Then Decaid can provide generic plumbing around those capabilities:
without knowing anything about the MOTTO80 wire protocol. The important point is not that Decaid should have no domain model. I think Decaid should understand that something is a grinder, scale or sensor, and should define stable semantics for the common operations it wants to integrate with. What I would avoid is making the core understand every product. In other words: stable device-class semantics in core; product-specific implementations in extensions. That is where I think a broader contract in #749 is justified, even if the implementation is delivered incrementally. We should build enough of the platform once so that the next contributor can implement a device largely inside an extension rather than opening another PR that adds a new controller, matcher, settings path, lifecycle path, REST handler, WebSocket handler and debug screen to Decaid core. The MOTTO80 work is actually a useful test case for this. It currently touches a lot of core areas because Decaid does not yet provide these extension points. Instead of treating each of those changes as a reason to permanently add a first-class MOTTO80/grinder stack to core, I would use them as a checklist for what the extension platform is missing. For example:
This also gives us a clearer security boundary. An extension contributing a grinder driver should not automatically gain Bluetooth, filesystem, arbitrary TCP, etc. It only receives the transports explicitly requested and approved. Likewise, transport handles should belong to a particular plugin/extension ID and generation and be automatically closed when that extension is unloaded. Late callbacks from a previous generation should be discarded. So the test I would use for #749 is: Could somebody implement the next BLE grinder without changing Decaid core? And then, once the model exists: Could somebody implement a serial grinder, BLE sensor, or network-connected peripheral using the same driver model and only requesting a different transport? I do not think #749 has to implement all of those transports immediately in order to pass that test. It does need to establish a contract that does not prevent them. A reasonable implementation sequence could be:
If the second grinder or sensor requires no new product-specific Decaid architecture, then we know the abstraction is doing useful work. If every new integration still requires us to add another In short, I would deliberately make the extension toolbox broad, while keeping the implementation rollout incremental and the device implementations outside core: Decaid owns safe native capabilities, lifecycle and generic device integration. Extensions own protocols and products. That gives contributors enough power to build things independently without turning Decaid core into the place where every grinder, scale and sensor protocol has to live. |
|
Two more points from my side:
|
|
A suggestion to throw into the mix: you might get everything this thread is after without moving drivers into JS at all - keep them in Dart, in core, but make them cheap. Basically the Linux kernel model: drivers live in-tree, and the driver API is what makes them small. The MOTTO80 checklist above actually makes the case for this. Almost none of that cost was protocol code - it was wiring. The matcher entry, the controller, the settings path, the REST/WS endpoint family, the debug screen. That wiring is exactly what the generic device model described here would eliminate. And once it's gone, a native driver PR shrinks to roughly what a JS driver would have contained anyway: the protocol, plus a registration saying what the device is and how to match it. class BookooGrinder extends Grinder {
static const registration = DriverRegistration(
id: 'bookoo.motto80',
type: DeviceType.grinder,
match: DeviceMatch(names: ['BOOKOO MT80'], services: [SERVICE_UUID]),
capabilities: mottoCapabilities,
);
// protocol only: framing, handshake, quirks
}Everything else - API exposure, state plumbing, settings, debug UI — comes from the generic model. And staying native keeps a lot of things you'd otherwise have to rebuild or give up: one language, real stack traces in crash reporting, CI against simulated devices, no bridge latency to worry about when a 10Hz weight stream is feeding grind-by-weight, and code review as the quality gate instead of a sandbox and a permission model. The costs are real and worth naming honestly: contributors need core review, and users need an app release to get a new device. But two things make that trade better than it looks. The espresso-hardware universe is maybe 20–50 devices total, not Home Assistant's thousands — at that scale, in-tree drivers on a good abstraction may simply be cheaper in aggregate than building and forever maintaining a driver platform, its schema versioning, and its distribution story. And the boilerplate cost of a Dart driver has dropped a lot now that this repo is deliberately agent-friendly; the tedious part of a driver PR mostly writes itself these days. None of this argues against #146, to be clear. MQTT-style integrations are where JS plugins genuinely shine — outbound, latency-tolerant, and able to reuse existing JS libraries instead of reimplementing protocols in Dart. The boundary that falls out feels clean: plugins observe and integrate, core drives hardware. And nothing gets foreclosed. If in-tree contribution ever becomes the real bottleneck — review queue, release latency, contributors drifting away — the JS bridge can still be added later, on top of a device model that's proven and versioned by then. Building the model first and deciding on the runtime later is the reversible order; building both at once, validated by a single device, is the risky one. |
|
One uncovered part IMO is the plan regarding existing devices, what stays in core, what moves to extensions and are there extensions just maintained with the core and shipped by default (which would make the most sense for me) |
|
I think we have enough signal now to turn this into a staged implementation rather than trying to define the entire future driver system upfront. So far we have proven two useful pieces:
I propose we proceed in the following order. 1. Generic BLE-backed plugin driversThe next transport should be BLE, but I think we should refine the contract before implementing it. The important boundary would be: In particular, I don't think plugins should get arbitrary The matcher should also be independent of the domain device type. The same BLE mechanism should work for: This immediately gives us another useful validation path: a Bluetooth sensor should be able to use the existing plugin-backed 2. Add plugin-backed
|
Uh oh!
There was an error while loading. Please reload this page.
Generic device drivers through the JavaScript extension system
The brief
We want Decaid to be able to support new devices as they become available, without requiring every device integration to become part of Decaid core.
The idea is to expose a generic device API to JavaScript extensions/plugins and let the extension implement the device-specific protocol and behavior.
An extension could, for example, declare capabilities such as:
driver.grinderdriver.sensorand request the transport it needs:
transport.bletransport.usbtransport.wifitransport.websocketDecaid would provide the generic transport, lifecycle and device primitives, while the extension would contain the actual device-specific implementation.
The main challenge is defining an API that is generic enough to accommodate devices with very different requirements without gradually turning into a collection of device-specific exceptions.
This is already showing up in several places
There are a few existing discussions/issues that point toward the same architectural boundary:
I think these are really different instances of the same question:
Where should Decaid stop, and where should an extension begin?
My preference would be:
This is very similar to the direction already emerging in #146 for networking: Decaid owns the safe native capability, while JavaScript owns the protocol-specific behavior.
Questions to answer
Some things worth discussing:
driver.grinder, with a standard set of grinder capabilities exposed back to Decaid?The transport itself probably should not dictate the device abstraction. BLE/GATT, USB and WebSockets are quite different underneath, but a grinder should still look like a grinder to the rest of Decaid.
Why now?
The current MOTTO80 work is a useful example.
The implementation itself is valuable, but I would prefer that we ultimately implement the MOTTO80 protocol through this mechanism rather than maintaining both a native device implementation and a JavaScript one.
That gives us a good first real-world driver with which to validate the abstraction.
Similarly, the grinder discussion in #701 should ideally not result in every supported grinder getting its own Dart implementation in core. If we can define the grinder contract once, individual device integrations could live as extensions.
The same principle applies beyond grinders. Sensors, scales, other coffee hardware and network services should be able to reuse the same plugin-host architecture.
The long-term goal would be for adding support for a new device to mean:
write a JavaScript driver, declare its capabilities and required transports, and install it — rather than modify and release Decaid itself.
So the question for this discussion is:
What is the smallest useful generic device + transport API Decaid needs to expose for JavaScript extensions to reliably drive arbitrary external devices?
All reactions