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
+1 -1
View File
@@ -191,7 +191,7 @@ on its own, so this is for reclaiming space sooner.
| `POST` | `/dashboards/{name}/publish`, `/dashboards/{name}/discard`, `/dashboards/{name}/rename` | publish, discard, or rename |
| `POST` | `/dashboards/{name}/duplicate` | copy into an unpublished `{name}_copy` |
| `GET` | `/devices/` | list devices, with `online` and `last_seen` |
| `POST`, `GET`, `PUT`, `DELETE` | `/devices/{id}` | create, read, save or delete one; a save sends the `version` it read and gets 409 when someone saved first |
| `POST`, `GET`, `PUT`, `DELETE` | `/devices/{id}` | create, read, save or delete one; a save sends the `version` it read and gets 409 when someone saved first. A read adds `io` and `bindings`, its ports as the device nodes of published flows wire them |
| `POST` | `/devices/pair` | start device pairing, optionally with `{name, board, firmware}`; unauthenticated |
| `GET` | `/devices/pair/{code}` | poll pairing with its `secret`; answers `device` (and `panel` for older firmware); unauthenticated |
| `GET` | `/devices/pending` | devices waiting for a code, as they describe themselves |
+2 -2
View File
@@ -256,8 +256,8 @@ To wire the loop in a flow `home`:
1. Declare a [flow input](../concepts/flows.md#flow-inputs) `heard` of type
`audio` with origin `panel`.
2. Under the panel's [I/O](../interface/dashboards.md#the-devices-own-io), wire
`mic` as `in` to `home.heard` and `speaker` as `out` to `home.reply`.
2. Add a [Device node](../interface/dashboards.md#the-devices-own-ports) for
the panel, publishing `mic` to `heard` and following `reply` on `speaker`.
3. Bind the listening node's `audio` to `heard` and its `text` to `said`.
4. Have your own nodes read `said` and publish an answer as a `str`, such as
`answer`.
+3 -1
View File
@@ -138,7 +138,9 @@ The canvas reports issues on the affected nodes:
| `node_error` | code or configuration could not load |
| `unauthenticated_hook` | a webhook or served websocket has no shared secret; advisory |
| `ignored_device` | a node other than a Function node names a device, which nothing reads; advisory |
| `node_unhealthy` | a loaded node reports a runtime problem, such as a lost connection |
| `unknown_device` | a Device node names no device, or one that does not exist |
| `device_port` | a device port is wired in two places, or against the device's report the wrong way or type; a port the report does not list is advisory |
| `node_unhealthy` | a loaded node reports a runtime problem, such as a lost connection, or a device being offline |
Build issues prevent the affected flow from running. The last three are runtime
or advisory issues: other work continues, and node health clears when the
+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.
+18
View File
@@ -386,6 +386,24 @@ They are reported but cannot receive writes unless wired.
The chat's [Control tab](../code/agents.md#control) sends commands by hand.
## Devices
### Device
**`device`** — a paired device's own ports: what it publishes, what it follows.
The engine never runs it. Its outputs are the device's `in` ports, published by
the device itself, and its inputs the `out` ports the device follows, which
never wake anything. Each port is named by the device, such as `gpio.4` or
`backlight`. See [the device's own ports](../interface/dashboards.md#the-devices-own-ports).
| Setting | Default | Notes |
|---|---|---|
| `device` | | the device's id |
| `config` | `{}` | per port, a JSON object handed to the device unread |
The node is down, *Offline*, while the device is not connected.
## Connectors
Installed connector packages add types through the `fluksio.node_types`