Files
app/docs/interface/dashboards.md
T
Melvin StroblandClaude Opus 5.5 aca1ea7971
Docs / docs (push) Successful in 24s
Playwright Tests / test-playwright (1, 2) (push) Failing after 4m10s
Playwright Tests / test-playwright (2, 2) (push) Successful in 2m7s
pre-commit / pre-commit (push) Failing after 2m23s
Test Backend / test-backend (3.12) (push) Successful in 4m48s
Test Backend / test-backend (3.14) (push) Successful in 4m32s
Compose Smoke Test / test-compose (push) Successful in 29s
Playwright Tests / merge-reports (push) Successful in 1m47s
Docs: a camera's snapshot_min_s, hold_s at least 2, person on connect
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138N7k5PT4dcR1iEqXaaQ9k
2026-10-17 10:44:14 +02:00

721 lines
43 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Dashboards and devices
A **dashboard** is a grid of widgets bound to message names. It can show values
and send controls across several flows. A **device** is a paired screen, such
as a wall tablet or a knob, and shows an ordered set of published dashboards.
## Building one
1. Open **Dashboards → New dashboard**.
2. Enter edit mode, choose **Add widget** and select a type.
3. Bind the widget to a message in its settings. On desktop, drag and resize
tiles on the grid.
4. **Publish** when the dashboard is ready.
Edits stay in a draft; paired screens show the published version. On phones,
tiles stack in one column over the dashboard's ground (its background image,
or the Glass look's drifting colours) and dragging is disabled, but settings
remain editable.
In edit mode, ⌘Z / Ctrl-Z undoes an edit and ⌘⇧Z / Ctrl-Shift-Z redoes it.
**Copy widget** in a widget's settings, or ⌘C / Ctrl-C with it selected, keeps
a copy in the browser until the next copy replaces it. Paste it on this or any
other dashboard with the paste button in the bar or ⌘V / Ctrl-V. It lands below
the other widgets with the same title, bindings and size.
A widget can end up below the canvas, for example after the canvas was made
smaller. In edit mode the canvas then grows to reach it, under a dashed line
where the panel ends. Scroll down and drag it back, or use **Reflow**.
Controls can publish real values while you edit, so choose their targets with
care. Your new Switch may already be flipping something!
## The widgets
### Display
| Widget | Binds to | Shows |
|---|---|---|
| **Value** | anything | formatted reading, unit and precision |
| **Gauge** | `float`, `int` | reading within a min/max range |
| **Chart** | numeric messages, a query response or run metrics | up to five lines |
| **Bar** | `float`, `int` | up to eight readings with separate scales |
| **Icon** | numbers, booleans or strings | a glyph selected from the value |
| **Text** | — | Markdown |
| **Agenda** | `list` | upcoming items |
| **Forecast** | `list` | an outlook strip |
| **Notification** | `record` | title, body and severity |
| **Media** | `image`, `audio`, `video`, `artifact` | image, audio or video |
| **Player** | `record` | playback status and transport controls |
| **Clock** | — | current time |
| **Website** | — | embedded page or image |
Every widget has a title. **Show title** controls its visibility; the title
still names the widget for accessibility and published values.
### Input
| Widget | Publishes | Settings |
|---|---|---|
| **Button** | fixed value | one-shot action |
| **Buttons** | a value per button | grid of actions |
| **Switch** | `bool` | on/off |
| **Slider** | `float`, `int` | min, max, step; horizontal or vertical; a reading beside it |
| **Input** | text or number | free entry |
| **Selector** | selected value | menu or row of choices |
| **Colour** | list or string | HSV, RGB or hex |
| **Counter** | `int`, `float` | one number set by up to three linked counters, such as hours and minutes; see [A counter](#a-counter) |
Colour defaults to `[h, s, v]`: hue 0–360°, saturation and brightness 0–100%.
**Sends** can select RGB (`[r, g, b]`, 0–255) or hex (`"#rrggbb"`).
HSV and RGB bind to `list`; hex binds to `str`.
Controls publish to their bound messages. The flow canvas shows them as
endpoints feeding the nodes that consume those messages.
Bindings are checked in the editor and on the server. See
[Payload types](../reference/payload-types.md).
### A slider's reading
A Slider can also show what it sets against, such as the room's temperature
beside a setpoint: **Beside it** binds `reading: {message, dtype, label}` to a
`float` or `int`. The reading is only shown, never published. It is written in
the slider's unit and to its step's decimals (a step of 0.5 writes 21 as
`21.0`), as the slider writes its own value, so the two always match; `—`
stands in for the value until one arrives. On a round face it takes the middle
(see [Round dashboards](#round-dashboards)); in a tile it is a line under the
slider's value.
It is also marked where it sits on the slider's scale, as a thermostat marks
the room's temperature: the handle is what is set, the dot what is. The dot is
at the reading's fraction of the scale, (reading − min) / (max − min) held to
0–1, so a reading outside the range sits at that end. It takes no pointer, and
there is none until a value arrives. See [Dots on the track](#dots-on-the-track).
Sizes are CSS pixels of the canvas, as in the round geometry below. The
slider's own value is at the tile's text size (14; 16 on a touch-friendly
panel), centred on a line as tall as the control (32; 48 touch-friendly).
| Part | Value |
|---|---|
| Line | 16 high, text 12: the label (muted, at most 96 wide, cut with an ellipsis), 6, the value (text colour), 6, the unit (muted). Without a label, the value and unit alone |
| Row | directly under the slider's value, right-aligned with it, in the column right of the track (12 apart). With ticks the line shares the ticks' row, so the tile grows no taller; without them it adds 16 |
| Row width | the value column is as wide as its wider line, but at most half the row; the track takes the rest. What does not fit is cut from the label; the value and unit never are |
| Fader | directly under the slider's value, both centred under the track, 8 below it; the line no wider than the tile inside its padding, cut from the label first |
### Dots on the track
A slider's ticks, a selector's options on a round face and a slider's reading
are dots inside the track, on its midline — on a round face the ring's centre
line, radius c − 20. Each is drawn in the colour that reads on what covers it:
the track, the value's fill, or the handle.
| Dot | Size, tile / ring | On the track | On the fill | On the handle |
|---|---|---|---|---|
| Stop: a tick, an option | half the track's thickness (4 on the 8 track) / 6 | `--muted-foreground` | `--primary-foreground` at 70 % | hidden under it |
| Reading | the track's thickness (8) / 12 | `--foreground` | `--primary-foreground` | Fluksio `--foreground`, Material `--primary-foreground`, Glass black at 75 % |
The reading is a filled circle like the handle, smaller and in the text colour
rather than the handle's paint, so it reads as what the room is beside what it
is set to. It is larger than a stop and stronger in colour, and it is the one
dot that shows on the handle, so a reading at the setpoint is still visible.
| Part | Value |
|---|---|
| Place (tile) | the dot's centre on the handle's travel: handle ÷ 2 + fraction × (track length − handle) from the track's left end (from its bottom end on a fader), so a dot and the handle line up at the same value |
| Place (ring) | the dot's centre at the fraction of the sweep, as the handle |
| Ticks | with `ticks` on (the default), n intervals of the scale — the steps when there are 5 or fewer, else the first of 4, 3 or 2 that divides the steps evenly, else 4 — and a dot at each i/n, both ends included. A row keeps its labels under the track; a fader has none |
| Options | a dot per option at i/(n − 1) of the sweep (one option: the middle). The selector's ring has a value arc from the start to the selected option, so the options up to it are on the fill |
| Colour change | where the fill or the handle covers a dot, that part of it takes the colour for the fill or the handle, so a dot changes colour as the fill's end or the handle passes over it, and follows them while they move. A device may instead switch a whole dot when the fill's end or the handle's centre passes the dot's centre |
| Order | track, dots, fill, the dots again over the fill, handle, the reading again over the handle |
| Locked | in a tile, the dots at 0.45 opacity, as the handle; on the ring, the whole ring at 0.45 |
### A counter
A Counter sets one whole number with up to three linked counters. They are the
digits of that number, counted in the unit of the finest, and the widget
publishes the number on its one message: hours and minutes publish 07:30 as
450, and 07:55 plus five minutes is 08:00.
| Field | Meaning |
|---|---|
| `counters` | 1 to 3 `{label, step, size, min, max}`, coarsest first. Absent or empty reads as the time below |
| `label` | written under the counter's digits |
| `step` | a press or a notch, in the counter's own unit: 1 hour moves the number by 60. Default 1, at least 1 |
| `size` | how many of it make one of the next coarser counter, 60 for minutes. Every counter but the coarsest needs one |
| `min`, `max` | the range, on the coarsest only and in its unit. `min` defaults to 0; no `max` is no upper bound |
| `wrap` | at either end, go round to the other (the default) or stop there |
A new counter is a time of day: `[{label: "h", step: 1, min: 0, max: 23},
{label: "min", step: 5, size: 60}]`, wrapping.
For counters 0 to n − 1, coarsest first, and the number v (`ui/core/counter.ts`,
which the firmware ports as it stands):
| Quantity | Formula |
|---|---|
| Weight wᵢ | the finest 1; each coarser one wᵢ = wᵢ₊₁ × sizeᵢ₊₁ |
| Digit dᵢ | d₀ = ⌊v / w₀⌋; dᵢ = ⌊v / wᵢ⌋ mod sizeᵢ, a remainder that is never negative |
| A step | counter i by k steps (k < 0 down): v + k × stepᵢ × wᵢ, so carry and borrow follow |
| Range | lo = min₀ × w₀. With a `max`, period = (max₀ − min₀ + 1) × w₀ and top = lo + ⌊(period − 1) / s⌋ × s, s the finest counter's step: 0–23 in 5-minute steps tops out at 1435 (23:55), one counter of step 5 up to 100 at 100 |
| Wrap | v = lo + ((v − lo) mod span), span = top − lo + s, so a wrap stays on the step grid; where s divides the period the span is the period. 23:55 + 5 min is 00:00, 00:00 − 1 h is 23:00; one counter of step 5 up to 100 goes from 100 + 5 to 0 and from 0 − 5 to 100 |
| Stop | v held to lo … top, as a slider is |
| No `max` | no top and nothing to wrap at: v is only held to lo or above |
Written, the digits are joined by `:`, each counter's label under its own.
Every digit but the coarsest is zero-padded to the width of size − 1 (05); the
coarsest to the width of `max` when it has one (07), and a negative one keeps
its sign in front of the padding. The number shown is the message's, held as
sent until it comes back, as a slider's is, and lo until one arrives.
In a tile each counter is a column: a press up, its digits, a press down and
its label, at the control sizes, so touch-friendly where the panel is. A press
moves the number at once and one publish follows 300 ms after the last press,
as a ring's does; holding a press does not repeat it. On a round face see
[Round dashboards](#round-dashboards).
## Charts
Choose the chart's data source:
- **Live:** recent numeric message history, updated as values arrive.
- **Query:** publishes a `record` request containing `range_s` and
`interval_s`, then draws a flow's `series` response. Include those fields in
the answer so the chart can reject responses for a different window.
- **Runs:** compares a numeric metric from named runs, a sweep or the latest
runs of a flow. See [Runs](../concepts/runs.md).
Multi-line charts show a legend as well as different colours.
!!! tip "Skip the unfinished bucket"
If your query aggregates into time buckets, leave out the bucket still
filling. Otherwise an incomplete total can look like a sudden drop.
## Media tiles
Media messages contain artifact references; the widget loads the bytes they
name. **Crop or fit** controls image sizing. **Play as it arrives** enables
autoplay, subject to the browser's rules for sound.
For local streams, `volatile=True` artifacts can be pushed over the websocket
with their message. Stored artifacts are fetched individually, as are frames
viewed through a portal. Use occasional snapshots for remote screens.
A tile asks for frames only while it is on screen in a tab that is being looked
at, and stops when it scrolls away, the tab is hidden or the tile closes. A
clip keeps playing either way. One page is pushed frames for at most 8
messages at once. A ninth tile fetches its frames one by one and does not turn
a camera on. A [device's camera](#a-devices-camera) sends only while a tile like
this wants it. A tile on a device's camera that streams live shows **Live**:
tap it for [live video](#live-view).
For continuous video, set **Live stream** to a browser-compatible camera URL.
The browser must be able to reach that URL; enrollment does not expose the
camera itself. See [Media payloads](../reference/payload-types.md#image-audio-video).
## Player tiles
A Player reads a status `record` and publishes commands to a separate `str`
message.
| Status field | Meaning |
|---|---|
| `title`, `artist`, `album` | track details |
| `status` | `play`, `pause`, `stop`, `load` or `off` |
| `position`, `duration` | seconds |
Controls send `toggle`, `next`, `prev` and `seek:<seconds>`. Your receiving
node translates those into device actions. The position advances in the
browser between readings and resets to each fresh reading.
## Website tiles
Set **Address** to a page to embed, or choose **Image** for a snapshot or MJPEG
URL. A website may refuse embedding.
Embedded pages run in a sandbox without cookies or storage by default.
**Trusted site** permits the site's own origin, cookies and storage.
Use it only for a trusted destination. The app blocks trusted embeds of its
own origin, including the portal origin, to protect your session.
## Dashboard settings
Open **Dashboard settings** from the dock. Each setting can hold a fixed value
or follow a message chosen in **Driven by**. The fixed value is the fallback
when no usable message value is available.
| Setting | Choices | Message type |
|---|---|---|
| **Look** | `fluksio`, `material`, `glass` | `str` |
| **Theme** | `system`, `light`, `dark` | `str` |
| **Palette** | ordered hex colours | `list` |
| **Background** | image URL | `str` |
| **Lock** | read-only on/off | `bool` |
To change one on a schedule, have a scheduled node publish its bound message.
### Look and theme
**Fluksio** is the default look. **Material** uses flat tonal cards; **Glass**
uses translucent tiles. Widgets work the same in all three.
Theme follows the chosen preference unless a palette sets the background
colour.
### Palette
Paste a colour list or a [Coolors](https://coolors.co) palette link. Colours
have roles by position:
| Position | Role |
|---|---|
| 1 | background |
| 2 | widget surface |
| 3 | primary controls and first chart line |
| 4 | accent |
| 5 | text |
| 6+ | additional chart lines |
Missing roles are derived, so three colours are enough to get started.
**Rotate** shifts their order. Text contrast is corrected automatically;
failure and success indicators keep their status colours.
### Background and lock
Background draws an image beneath the widgets. Lock disables publishing from
controls and shows **Read-only** when viewing the dashboard. It does not
restrict API permissions and does not disable controls in edit mode.
## Round dashboards
For a round screen such as a rotary knob, set the canvas to **Round** in the
dashboard settings, or pick the **Knob** preset (360×360). A round canvas is
square (`shape: "round"` forces `canvas_height` to `canvas_width`), and nothing
outside its circle is drawn. It fills the screen at any width: a phone neither
stacks nor pads it.
A round dashboard has no grid. It shows one widget at a time, each drawn as the
whole face, in the order of `widgets`; `layout` is not read. In the editor, the
faces are a row above the dock: press one to show it and open its settings,
drag it along the row (or Alt+←/→) to move it, and add more with **Add widget**.
A Slider, Selector, Colour, Counter or Gauge draws on the **ring** along the
edge, which a knob's own ring turns. In a browser, drag along the ring or use
the mouse wheel (or the arrow keys once it has focus): one notch is one step,
one option (no wrap), one hue step (`hue_step`, default 5°, wrapping) or one
step of the counter selected. The value moves at once and is published once,
300 ms after the last notch or when the pointer is released. A Gauge only
shows. On any other face, turning a knob only wakes the screen.
A Counter's ring turns one of its counters, with carry: the finest, until a
press on another's digits selects that one instead. The selection holds while
the face is shown and is the finest again once the face is left. Dragging
along its ring counts notches rather than picking a place: one per step's
share of the sweep, 300° × step / its turn (size, or max − min + 1 for the
coarsest), so the arc stays under the finger, and one per 15° for a coarsest
without `max`. The angle is counted from the last move the shorter way round,
so a drag carries on through the gap. The face has no up or down presses.
A Slider's reading (**Beside it**) is written large in the middle, under its
label; the slider's own title and value move to the line in the ring's gap.
Value, Bar, Switch, Button, Clock, Icon, Notification and Text have faces of
their own. Every other type is drawn as its rectangular body in the middle of the
face, and the editor marks it **Poor fit for round**.
### Moving between faces
| Gesture | Effect |
|---|---|
| Swipe left / right | next / previous face of this dashboard; stops at either end |
| Swipe up / down | next / previous dashboard of the device (`/panel/{id}` only); stops at either end |
| Page dot | that face |
| PageDown / PageUp | next / previous face |
| Shift+PageDown / Shift+PageUp | next / previous dashboard of the device |
| Arrow keys | turn the ring, not the page |
A swipe carries the page with the finger ([Swiping](#swiping)); a drag that
starts on the ring turns the ring instead. A dashboard reopens on the face it
was last left on in this session, otherwise on its first. Landing on another
dashboard shows its title and icon in the middle of the face for one second; a
round panel has no rail.
### Geometry
Devices that draw dashboards natively use these numbers. They are canvas
pixels, scaled with the canvas; S is the canvas width and c = S/2. Angles are
degrees clockwise from three o'clock. Text sizes are font sizes; muted is the
look's muted text colour.
| Part | Value |
|---|---|
| Face | circle of diameter S at (c, c) |
| Title | one line centred at y = c − 80, 18, muted, at most 200 wide; the widget's title, or the label of a slider's reading |
| Middle | a 240 × 128 box centred at (c, c); what a face draws in its middle is centred in it and clipped to it |
| Centre reading | centred at (c, c), line height 1, its unit after it at 0.4 em, muted. Size: 72, or 240 ÷ (0.6 × the characters written) rounded down where that is smaller, never below 24 |
| Ring band | 40 along the edge |
| Ring arc | start 120°, sweep 300° (ends at 60°), centre-line radius c − 20, stroke 16 with round caps |
| Slider | track over the sweep, value arc from the start to the value (none at the minimum), handle 28 across, its ticks as [dots](#dots-on-the-track); the value as the centre reading |
| Slider with a reading | the reading as the centre reading; the gap line holds the title (muted) and the slider's value. The reading as a dot 12 across on the centre line at its fraction of the sweep ([Dots on the track](#dots-on-the-track)) |
| Selector | a value arc from the start to the selected option (none at the first), a dot 6 across per option at i/(n−1) of the sweep (one option: the middle), the selected one under the handle; its label as the centre reading, at most 36 |
| Colour | hue h at h/360 of the sweep in 150 strokes of 2°, red at both ends; the handle filled with the colour; a swatch 96 across in the middle, with a hairline border |
| Gauge | track and value arc, no handle; the value as the centre reading |
| Counter | the digits, joined by `:`, sized as a centre reading of the whole line (07:30 is five characters: 72), each counter's label under its digits at 17, muted, 4 apart; the block centred at (c, c). With two or more counters, the one the ring turns is in `--primary`, digits and label, and pressing a counter's digits selects it. A value arc from the start to where the selected counter stands in its turn: dᵢ / sizeᵢ, as a minute hand stands, the coarsest (d₀ − min) / (max − min + 1); no arc for a coarsest without `max`. No handle and no dots |
| Gap line | centred at y = S − 28, 15, at most 140 wide, clear of the ring's end caps |
| Value | the reading as the centre reading, to its precision; a word shrinks like any other |
| Bar | an arc per row, the first outermost, each over the ring's sweep (start 120°, 300°) with round caps: a track in the look's track paint and the row's value arc in its data colour, on its own scale (none at the minimum). n rows share a band 64 thick inward from c − 12, 3 apart: stroke s = min(16, (64 − 3(n − 1)) / n), centre-line radius rᵢ = c − 12 − s/2 − i(s + 3), so up to three rows are 16 thick and the first is on the ring (c − 20). The middle is the legend, a line per row in order: a dot 8 across in the row's colour, 8, the label (muted, cut with an ellipsis), 8, the value and unit to the row's precision (— for none). Lines of 22 at 17 for up to 5 rows, 18 at 15 for 6–7, 16 at 13 for 8; the block centred at (c, c), as wide as its widest line but at most 240 and at most the width of the disc inside the innermost arc (radius c − 12 − n·s − 3(n − 1)) at the block's top and bottom, less 16. The title is at most that disc's width at the title's line (y = c − 89), less 8 |
| Switch | a disc 128 across centred at (c, c): the look's button, filled when on and tonal when off, with a power glyph 40 over the word On or Off at 17; a press toggles |
| Button | a disc 128 across centred at (c, c), tonal, its label at 17 over at most three lines; a press sends its value |
| Clock | the time (hours and minutes, the device's locale) sized as a centre reading, over the date (weekday, day, month) at 17, muted, 4 apart; the pair centred at (c, c) |
| Icon | the glyph 96 across over its label at 17, muted, 4 apart; the pair centred at (c, c). Nothing matched: a dash at 72, muted |
| Notification | the title at 17, medium (an error in the destructive colour, led by ⚠), over the body at 17, muted; centred lines in the middle |
| Text | centred lines at 17: `#` at 1.35 em and `##` medium, `###` muted, list items led by a bullet; in the middle |
| Not wired | the problem, at 17, muted, in the middle; a ring type draws no ring |
| Any other type | its rectangular body, without a card, from y = c − 64 to c + S/(2√2), x = c ± S/(2√2) (Knob: 53–307 × 116–307) |
| Page dots | one per face, from two faces: dots 6 across on targets 16 apart (closer when more than 128 wide would be needed), centred at y = S − 12; the current one in the text colour, the others muted at half opacity |
| Dashboard name | a pill centred at (c, c), 56 high and fully round, 20 padding either side: the icon 24, 10, and the title at 24, medium, cut with an ellipsis; at most 280 wide, on the look's card colour, opaque, with a hairline border in `--border` (no blur), the face visible around it; for 1000 ms |
| Paging | faces slide sideways, dashboards up and down; see [Motion](#motion) |
| Locked | controls do nothing and read as disabled; the ring at 0.45 opacity. **Read-only** in the gap line's place (centred at y = S − 28): a lock glyph 15 and the word at 15, 6 apart, muted. A face whose gap line is in use leads that line with the lock glyph instead. Nothing is drawn outside the circle |
Paint follows the look: Fluksio draws a muted track, a primary value and a
handle of the ground colour ringed in primary; Material a tonal track and a
primary handle; Glass a translucent track, a glowing value and a white handle.
A Switch or Button face wears the look's own button.
## Motion
How a dashboard moves, for devices that draw it natively. Distances are canvas
pixels. Each look moves at its own spring; a device without springs runs the
spring's closed form for the duration given and then sets the end value, or
uses the cubic-bezier that stands in for it.
| Look | Spring (stiffness, damping, mass) | ζ, ω₀ | Duration (within 1 %) | Overshoot | Cubic-bezier stand-in |
|---|---|---|---|---|---|
| Fluksio | 420, 34, 1 | 0.830, 20.49 s⁻¹ | 210 ms | 0.9 % | (0.26, 0.12, 0.25, 1.01) over 250 ms, at most 1.5 % off |
| Material | 550, 38, 1 | 0.810, 23.45 s⁻¹ | 270 ms | 1.3 % | (0.20, 0.11, 0.27, 1.19) over 260 ms, at most 1.7 % off |
| Glass | 320, 22, 0.8 | 0.688, 20.00 s⁻¹ | 330 ms | 5.1 % | (0.26, 0.20, 0.20, 1.33) over 320 ms, at most 1.7 % off |
From rest, a spring's progress is p(t) = 1 − e^(−ζω₀t)·(cos ω_d t +
(ζω₀/ω_d)·sin ω_d t), with ω_d = ω₀·√(1 − ζ²). "Spring" below is the
dashboard's look's.
| What | Moves | From → to | Timing | Input during it |
|---|---|---|---|---|
| Face paging (round) | the faces, side by side on a track | the track slides one face width S, the new face coming in from the side it lies on (the next from the right). A dot that skips faces brings its face in beside the one shown, so a turn is always one width | spring, from where the track is (a swipe's release speed carried on) | a new turn while one runs: the running one lands at once and the next starts |
| Dashboard paging (round panel) | the dashboard, and beside it the next one | slides S up (the next, from below) or down (the previous, from above), the next one drawn on the face it opens on; once landed, it is the dashboard shown, and its name pill shows for a moment. Each one on the track wears its own look, theme and palette. The ground is the dashboard shown's and changes over as the turn begins, without a fade. One opened by its address replaces the last at once | spring | further paging waits for the turn to land |
| Page dot | the current dot and the last | muted at 0.5 opacity ↔ text colour at 1 | 200 ms, cubic-bezier(0.4, 0, 0.2, 1) | retargets |
| Dashboard name | the pill, on landing on another dashboard | opacity 0 → 1, held 1000 ms, 1 → 0 | in 200 ms, out 150 ms, cubic-bezier(0.4, 0, 0.2, 1) | — |
| Ring value set by a notch, a key or the engine | the value arc's end, the handle (slider, selector, colour) | from where it is drawn to the new fraction of the sweep | spring | each notch retargets from where it is, at the speed it has |
| Ring value set by a drag | the same | follows the pointer exactly | none | — |
| Hue across red | the colour handle | a change of more than half the sweep jumps to the other end | none | — |
| Counter across its turn | the value arc | a change of more than half the sweep (a carry, a wrap, another counter selected) jumps | none | — |
| Slider's reading on the ring, gauge on the ring | the reading's dot; the value arc | from where it is drawn to the new fraction; a dot's colour follows the fill and the handle as they pass | spring | retargets |
| Numbers on the ring | centre reading, gap line | switch at once, with the notch | none | — |
| Value face, Value and Gauge tiles | the number | counts to the new value, written to its precision | spring | retargets |
| Slider tile set elsewhere | the fill's end, the handle, the reading's dot | from where it is drawn to the new fraction | spring | a drag or a key moves it at once |
| Gauge and Bar tiles | the value arc, each bar's fill | from where it is drawn to the new fraction | spring | retargets |
| Dashboard change (rectangular panel) | every tile | the old dashboard goes at once. The new tiles come in row by row, 30 ms apart, each opacity 0 → 1 and y +8 → 0 (Glass: +12, and scale 0.96 → 1): from below, top row first, for a dashboard further down the rail, and from above (y −8 → 0), bottom row first, for one further up | spring | — |
| Rail | the current entry's marker | slides to the new entry | spring | retargets |
With reduced motion asked for, nothing travels on its own: a page under the
finger still follows it, but every turn, return and value lands at once. The
dot and the name pill, which only change colour or opacity, keep their timing.
A device may simplify where its hardware is slow, keeping the direction and
the end state: a turn a key or a dot asked for may cut; a retarget may restart
the curve from where it is without carrying the speed; a counting number may
switch; and the value arc may be redrawn each frame as an arc to the eased
angle, which is what the browser's reveal amounts to.
### Swiping
Faces are swiped sideways and a device's dashboards up and down, with the page
under the finger, as a tile view moves.
| Part | Value |
|---|---|
| Start | a press anywhere on the face but the ring band, which turns the ring instead |
| Axis | none until the drag has gone 8 from where it began along either axis; then the longer of the two, kept until release |
| Follow | the page moves with the drag one to one, and the page that way is drawn beside it, edge to edge, S away: the next face, or the next dashboard on the face it would open on (the one last shown there, else its first), with its live values |
| Neighbours | the dashboards before and after the one shown are built in advance — loaded, drawn and fed live values — so the one a swipe reaches is complete from the first move. They take no input until they are the one shown |
| Past the last | nothing beside it; the page moves 0.3 of the drag, at most 48 |
| Release | turns if the drag went at least S/4 (90 on the Knob), or went at least 8 and was moving at least 500 a second towards the next page over its last 80 ms; otherwise goes back |
| Turn, go back | the look's spring from where the page is, carrying the release speed; a device may use the look's cubic-bezier over its duration from where it is |
| Press during a turn | lands the turn at once; the new drag starts from a page at rest |
| A swipe that began on a control | is not a press of it |
## Showing one
Open `/view/{name}` for a dashboard-only view with your normal session.
For a dedicated screen, pair a device.
## Devices: hanging a screen on a wall
As a superuser:
1. Open **Devices → Add device** and name it after where it hangs. The name
sets its address, `/panel/{id}`, and a later change of title keeps it.
2. Under **Dashboards**, add the ones it shows. The first is what it opens
with, and the arrows set the order of its rail.
3. Enable **Touch friendly** for larger controls.
4. Open the pairing link on the device. It shows a six-character code.
5. Enter that code under **Pairing**, check the device description, then press
**Pair**. A device asking from your own network also appears in the list
below, with what it says about itself and a two-character check. The device
shows the same check beside its code. When the two match, **Pair this one**
adopts it without the code.
The screen collects its credential and shows the assigned published dashboards.
Several dashboards get a navigation rail. The page says whether the device is
online, which it is while its live socket is open, and when it was last seen
since the instance started.
Each save carries the version it was read at. When someone else saved the
device first, the save is refused and the page shows their version.
On your network one address may ask for five codes a minute, and the portal
limits the requests that come through it. At most 50 requests wait from each,
and a new one pushes out the oldest that came the same way.
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 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.
A device acts as itself and not as the superuser who paired it, so deleting or
deactivating that account leaves the device paired.
**Unpair screen** revokes the device's credential and keeps the device and its
dashboards. Deleting the device removes the assignment, its access and its
report, and a device added again under the same name does not bring the old
credential back.
The device definitions are stored in the flow repository (`_devices/<id>.json`),
so their history is in git with the flows. The credential generation lives in
`DEVICES_FILE` outside it, so reverting the repository never undoes an unpair.
An instance from before devices moves its `panels.json` there on its first
start and renames the file `panels.json.migrated`.
### A screen without a browser
The openpanel firmware, kept in its own repository, draws dashboards as native
widgets on an ESP32-P4 wall panel (Waveshare ESP32-P4-WIFI6-Touch-LCD-7B,
1024×600). Once its config page points it at this instance (the device's link
works too, the device keeps only its address), it shows the pairing code itself; enter it as in step 5. It reads the same published
dashboards and live values and draws every widget type with two exceptions,
both shown as a placeholder: a website tile that embeds a page (image tiles
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 [port](#the-devices-own-ports) names
mean. openpanel [reports](#what-a-device-reports) the ones it serves, such as
`gpio.<n>`, `backlight`, `touch`, `mic` and `speaker`, with the settings each
one reads, 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 ports
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 **Capabilities** lists the
ports it reports and 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 `.` |
| 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 enabled published flows count, so stopping a flow takes its ports out of
the device's `io` and its credential's reach. 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 it is fixed the device follows the first binding of the port, by message
name. 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, or to another type, an
error. `int` and `float` pass for each other.
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.
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
degraded, *Offline*, in amber while the device is not connected: a warning on
Home, not a fault, since a panel switched off or asleep breaks nothing.
A `bool` the device publishes, such as a camera's `person`, says something is
so right now, and a device that went away cannot say it no longer is. Once the
device has been away for 60 seconds, the same wait as before its node shows
*Offline*, each of its `bool` messages that reads true is set to false by its
device node. A device back within that wait keeps its values, so a restart of
either side changes nothing. Its other values keep the last one it sent.
### What a device reports
A paired device says what it has with `PUT /devices/{id}/report`, from its own
credential only. openpanel sends it after pairing and once per boot. The report
replaces the one before and is at most 16 KB, and 64 KB as kept, once named
`config_schemas` are put in place. It is read leniently, so a newer firmware
still reports to an older instance:
- keys this version does not know are dropped
- a value it does not know for a display `shape` or a port `direction` reads as
`unknown`, and a `features` value that is not true or false is left out
- a port it cannot read, such as one with an unknown `dtype`, is left out on its
own and named in the server log
Only a report that is not `schema` 1, or not an object at all, is refused with
422.
```json
{
"schema": 1,
"platform": "esp32p4",
"board": "p4",
"firmware": "0.1.2",
"display": {"w": 1024, "h": 600, "shape": "rect", "touch": true},
"ports": [
{"port": "backlight", "direction": "out", "dtype": "int",
"config": {"type": "object", "properties": {"wake_s": {"type": "integer", "default": 30}}}},
{"port": "gpio.4", "direction": "in", "modes": ["in", "out"], "dtype": "bool"}
],
"camera": {"sensor": "IMX415", "w": 1920, "h": 1080, "fps": 15},
"features": {"display": true}
}
```
`config` is a JSON schema of the settings the device reads for that port, which
the Device node turns into a form. It may instead name an entry of a top-level
`config_schemas` object, so pins that share a schema send it once:
`"config_schemas": {"pin": {...}}` and `"config": "pin"` on each port. The
schema is put in place when the report is read. `modes` lists the directions a
port may be wired in when it may be either, as a GPIO pin. `on_demand` marks a
port that sends only while somebody wants it.
The device's page shows the report under **Capabilities**, with each port's
binding or **Use in flow** for it. A reported screen becomes a canvas preset in
the dashboard settings, named after the device, and the first report of a
touch screen turns **Touch friendly** on. A device paired before this version
keeps the switch as it was set.
### A device's camera
A camera device (openpanel on a NANO) reports these ports:
| Port | Type | Settings | What it sends |
|---|---|---|---|
| `camera` | `image`, on demand | `width` (640), `fps` (5), `idle_fps` (0, off), `quality` (65) | JPEG frames, held in memory and never stored |
| `person` | `bool` | `hold_s` (10, at least 2) | whether someone is in view, held for `hold_s` after they leave, and sent again on each connect |
| `detections` | `json` | | `{count, boxes: [{score, x0, y0, x1, y1}]}`, boxes scaled 0 to 1, on change and at most once a second |
| `snapshot` | `image` | `snapshot_min_s` (30) | one stored frame each time `person` turns true, none within `snapshot_min_s` of the last |
| `live` | `video`, on demand | | H.264 over WebRTC while a [live view](#live-view) runs; never wired in a flow |
`camera` sends only while it has demand: while a page wants its frames (a
dashboard tile, the preview on the device's page, a screen through the
portal), or a node of a running flow reads its message. Once its flow wires
`camera`, the device's page shows a **Camera** preview. A flow that reads
`camera` keeps it sending all the time, about 300 GB a month through a cloud
instance, so the Device node warns when a node in its flow reads a port that
sends on demand. Have flows read `snapshot` or `person` instead. To keep the
picture of whoever came by, a node reading `snapshot` can hand it on to a
message of its own, which a media tile shows until the next one:
```python
def process(snapshot):
return {"visitor": snapshot}
```
The device is told which of its ports have demand on its live socket, and sends
its frames there too, as described under [Live events](../code/api.md#live-events).
Through the portal the socket carries text only, so a device paired there
uploads each frame over HTTP instead.
A device may fetch the last 8 artifacts each of its messages pointed at, not
only the current one, so a screen fetching a camera frame by frame through the
portal is not refused a frame the camera has just replaced.
#### Who may watch a camera
**Who may watch** in the Camera section of the device's page decides who sees
what its camera sends, its `image` and `video` messages: **Superusers**, the
default, or **All members**. A member of the portal is a member here. For
anyone it leaves out:
- the messages' values are left out of the live socket, `GET /messages/` and a
flow's state, and their history answers 403
- the frames those messages hold, the last 8 included, answer 403, and a
socket asking for their bytes gets none
- a live view is refused with `forbidden`, and so are TURN credentials
- a dashboard tile or the device's preview says *No access to this camera*
A wall panel shows the camera when one of its dashboards carries it, whoever
watches the wall: a superuser assigned it the dashboards.
The setting covers the camera's own messages, not what a flow makes of them.
Anyone who edits flows can wire a node that passes a picture on under another
name, and node code reads whatever it is handed. How long a kept `snapshot`
stays and a light on the device that shows the camera is on are not part of
it yet.
#### Live view
A media tile showing a device's `camera`, and the preview on the device's
page, offer **Live** while the device is online and reports `live`. Tap the
tile and the browser connects to the device with WebRTC, without a plugin,
and plays its video in the tile under a **Live** badge. Tap again to stop. It
stops by itself when the tile scrolls away, the tab is hidden or the page
closes. While it plays the tile asks for no frames, so the camera encodes one
picture rather than two, and frames come back when it ends.
A device serves 2 live viewers. A third tile, a failed connection or no
picture within 10 seconds stays on frames and says so briefly. On one network
the two ends find each other directly. Between networks they need STUN and,
behind strict NATs, a TURN relay: set `STUN_URLS`, `TURN_URLS` and
`TURN_SECRET` (see [Configuration](../reference/configuration.md#live-view)).
The server only passes on the few messages that set a connection up, the
video never crosses it. A wall panel's own screen does not offer live view.
### Before device nodes
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"
Pairing links use `FRONTEND_HOST`. Set it to the app address reachable
from the screen's network. `make dev-lan` sets it to the published port on
the host's LAN address.
### A screen somewhere you cannot reach
An [enrolled instance](portal.md) also shows a portal pairing link. Use that
link on the remote screen, then approve its code on the device's page. The
device description says *via portal*. Anyone can ask the portal for a code, so
a request that came through it, or from outside your network, is adopted by
its code only and never from the list.
The instance enforces the same dashboard and message limits. Remote
credentials are issued by the portal with the device's credential generation
in them, so **Unpair screen** and deleting the device revoke them as they do a
local one. A credential from a portal older than this carries none and keeps
working only while the device was never unpaired or deleted. Pair such a
screen again once the portal is updated.
## See also
- [Payload types](../reference/payload-types.md)
- [Flows, nodes and messages](../concepts/flows.md)
- [Accounts and the portal](portal.md)