Device node: a device's ports are wired in flows

- flow/nodes/device.py: `device` node with params `device` and `config`;
  provides the device's `in` ports, requires its `out` ports without
  waking anything, never run (Node.passive), down "Offline" from presence
- DeviceDef.io and DevicePort go; GET /devices/{id} computes `io` and
  `bindings` from the device nodes of published flows, in a fixed order,
  cached by FlowStore.revision; the credential's scope comes from them
- issues `unknown_device` and `device_port` (wired twice; against a report
  the wrong direction or dtype, or unlisted as advisory), rechecked when a
  device is created or deleted; ValidationIssue.advisory is a field
- a device publishing on a bound port is credited to its node by the
  server; the panel endpoint kind is gone from `_endpoints` and the canvas
- a rebuild that moves a device's bindings sends `dashboard_changed` with
  an empty name, which the firmware refetches on
- startup moves each device's `io` rows into device nodes in the flows
  declaring their messages, draft and published, one commit; rows no
  flow declares are logged and left out
- frontend: Device node settings (device, Publishes, Follows, a port
  picker from a report), palette shortcuts per reported port, DeviceDetail
  lists bindings with "Use in flow" in place of the I/O editor
- docs: devices, flow editor, node types, issues, API
This commit is contained in:
Melvin Strobl
2026-10-16 16:40:31 +02:00
parent 91e3b703f9
commit 29d3ec467f
35 changed files with 2195 additions and 494 deletions
+31 -14
View File
@@ -468,9 +468,9 @@ device first, the save is refused and the page shows their version.
A device credential reads its assigned dashboards, their bound messages and the
artifacts they show. It writes only the targets of their controls and query
charts, and the `in` ports of its own [I/O](#the-devices-own-io). A message its
tiles only draw, or a dashboard setting is bound to, stays read only. On a
locked dashboard only the query charts may still publish.
charts, and the messages of its own `in` [ports](#the-devices-own-ports). A
message its tiles only draw, or a dashboard setting is bound to, stays read
only. On a locked dashboard only the query charts may still publish.
**Unpair screen** revokes a locally paired credential and keeps the device and
its dashboards. Deleting the device removes the assignment and its access, and
@@ -495,30 +495,47 @@ work), and a chart of runs, which a device credential may not read. Media tiles
play MJPEG streams, pushed frames, images, and H.264 Baseline or MJPEG clips up
to 640×360. Re-encode other clips with the [clip recipe](../code/nodes.md#making-clips-playable-on-an-lvgl-panel).
The device decides what its [I/O](#the-devices-own-io) port names
The device decides what its [port](#the-devices-own-ports) names
mean. openpanel serves `gpio.<n>`, `backlight`, `touch`, `mic` and `speaker`,
lists each one's `config` in its README and ignores any other name. A browser
screen serves none of them. The [speech recipe](../code/nodes.md#speech-to-and-from-a-panel)
turns `mic` into text and text into `speaker`.
### The device's own I/O
### The device's own ports
A device can also act as a node. Under **I/O**, wire a port on the device to a
message. A new port saves once it has a port name and a message:
A device can also act as a node, with a button on its GPIO, its backlight or
its microphone. Its ports are wired in a flow with a **Device** node: pick the
device, then add each port under **Publishes** (`in`, the device sends to the
message) or **Follows** (`out`, the device reads it). **Use in flow** on the
device's page puts its node in a flow you pick, and **Ports** lists what the
published flows wire, each linked to its node.
| Field | Meaning |
|---|---|
| `port` | The device's name for it, such as `gpio.4`, `backlight`, `touch`, `mic` or `speaker`. Lowercase letters, digits, `_` and `.` |
| `direction` | `in`: the device publishes to the message. `out`: the device follows it |
| `message` | A qualified `flow.name`, a lowercase flow name, a dot, then letters, digits and underscores. A flow must declare it, as for a control |
| `config` | A JSON object passed to the device unread, such as `{"pull": "up", "debounce_ms": 50}` |
| port | The device's name for it, such as `gpio.4`, `backlight`, `touch`, `mic` or `speaker`. Lowercase letters, digits, `_` and `.` |
| message | The message the port publishes or follows, named like any port's |
| type | The payload type, checked like any message's |
| config | A JSON object passed to the device unread, such as `{"pull": "up", "debounce_ms": 50}` |
Only published flows count. A port wired in two of them, or a device node
naming a device that does not exist, is an error on the canvas. Until a device
reports what it has, a port name is free text; once it has, a port it does not
list is a warning and one wired the wrong way round an error.
The device reads its ports as `io` on `GET /devices/{id}`, in a fixed order,
and is told to read them again when publishing or deleting a flow changes them.
The credential may publish to its `in` messages even when a dashboard is
locked, and may upload an artifact named after one
(`PUT /artifacts?name=<message>`), which is how a microphone sends a recording.
`out` messages are read only. The flow canvas draws each port as an endpoint,
an `in` port feeding the nodes that consume its message and an `out` port fed
by the nodes that produce it.
A value it publishes there is credited to its device node, so the node's edge
lights up on the canvas. `out` messages are read only. The node is shown down,
*Offline*, while the device is not connected.
Before device nodes a device carried its ports itself. On its first start an
instance moves each one into the flow that declares its message, one device
node per device and flow, in the published flow and its draft. A port whose
message no flow declares is left out and named in the startup log, so wire it
again by hand.
!!! note "If the local link is wrong"
+11 -5
View File
@@ -13,14 +13,14 @@ below, so a large flow grows downwards in one column.
- **Edges** are routed around the nodes, a loop back to an earlier node
included, and pulse when values pass through. Click one to inspect its
payload.
- **Endpoints** show flow inputs, dashboard widgets, device I/O ports and nodes
in other flows that exchange messages with this flow. A value passing lights
- **Endpoints** show flow inputs, dashboard widgets and nodes in other flows
that exchange messages with this flow. A value passing lights
one up in a sweep: left to right when it sends into the flow, right to left
when it reads out of it.
- **Clicking an endpoint** shows a flow input's type, starting value and
current value, with a pencil that opens the flow settings to edit it. A device
port opens the device in the Devices tab. A node in another flow opens that
flow on the label leading back here, with its edges highlighted.
current value, with a pencil that opens the flow settings to edit it. A node
in another flow opens that flow on the label leading back here, with its
edges highlighted.
Values update over a websocket. A connection banner tells you when the editor
cannot reach the engine.
@@ -37,6 +37,12 @@ and open its settings.
payload type, bound message and delivery interval. Outputs also have a
streaming toggle.
A **Device** node stands for a paired device instead: pick the device, then
its ports under **Publishes** and **Follows** (see
[the device's own ports](dashboards.md#the-devices-own-ports)). A device that
reports its ports offers them in a list with their type filled in, and the
command palette offers each one as a shortcut, such as *Hallway camera*.
Message fields suggest existing names. Matching an output's message is how
you connect an input.