docs: document per-signal calibration and config save/reload
Update Docs/StreamHub-API.md (new setCalibration/reloadConfig commands, calibration/configSaved/configReloaded events, §4 config file format), ARCHITECTURE.md §6 (updated command/event tables and Config File Format subsection), Docs/WebUI.md (Cal row in V-Scale Toolbar, Sources & Config sidebar section). Also corrects the spec's Validation sentence to match the shipped cal-invalid border behaviour instead of silent field revert. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
e37eec0276
commit
d26b78b7f6
+25
-1
@@ -380,7 +380,9 @@ binary frames carry data push payloads.
|
|||||||
| `ping` | — | Hub replies `{"type":"pong"}` |
|
| `ping` | — | Hub replies `{"type":"pong"}` |
|
||||||
| `addSource` | `label`, `addr` (`"host:port"`), `multicastGroup?`, `dataPort?` | Connect to a new UDPS source; hub assigns id `s1, s2, …` |
|
| `addSource` | `label`, `addr` (`"host:port"`), `multicastGroup?`, `dataPort?` | Connect to a new UDPS source; hub assigns id `s1, s2, …` |
|
||||||
| `removeSource` | `id` | Disconnect and remove a source |
|
| `removeSource` | `id` | Disconnect and remove a source |
|
||||||
| `saveSources` | — | Persist the current dynamic source list to `SourcesFile` (JSON) |
|
| `saveSources` | — | Persist the dynamic source list **and** the calibration table to `SourcesFile`; replies `configSaved` |
|
||||||
|
| `setCalibration` | `source` (label), `signal` (base name), `scale`, `offset`, `unit` | Record `value = raw × scale + offset` for one signal; metadata only, the hub never applies it. Identity entries are deleted. Replies with a `calibration` broadcast |
|
||||||
|
| `reloadConfig` | — | Re-read `SourcesFile`: calibration replaced wholesale, missing sources added, live sources never touched; replies `configReloaded` |
|
||||||
| `getSources` | — | Trigger `sources` broadcast |
|
| `getSources` | — | Trigger `sources` broadcast |
|
||||||
| `getConfig` | `sourceId` | Trigger `config` broadcast for one source |
|
| `getConfig` | `sourceId` | Trigger `config` broadcast for one source |
|
||||||
| `getStats` | — | Trigger `stats` broadcast |
|
| `getStats` | — | Trigger `stats` broadcast |
|
||||||
@@ -402,8 +404,30 @@ binary frames carry data push payloads.
|
|||||||
| `triggerState` | `state` (`"idle"`\|`"armed"`\|`"collecting"`\|`"triggered"`), `mode`, `stopped`, `trigTime?` | On any trigger FSM transition |
|
| `triggerState` | `state` (`"idle"`\|`"armed"`\|`"collecting"`\|`"triggered"`), `mode`, `stopped`, `trigTime?` | On any trigger FSM transition |
|
||||||
| `zoom` | `reqId`, `signals:{"src:sig":{t:[…], v:[…]}}` (`t` printed `%.17g`, `v` `%.9g`) | Unicast reply to `zoom` |
|
| `zoom` | `reqId`, `signals:{"src:sig":{t:[…], v:[…]}}` (`t` printed `%.17g`, `v` `%.9g`) | Unicast reply to `zoom` |
|
||||||
| `maxPointsUpdated` | `maxPoints` | After ring buffer resize |
|
| `maxPointsUpdated` | `maxPoints` | After ring buffer resize |
|
||||||
|
| `calibration` | `cal:[{source, signal, scale, offset, unit}]` | On connect; after an accepted `setCalibration`; after a successful `reloadConfig` |
|
||||||
|
| `configSaved` | `ok`, `path`, `error?` | In reply to `saveSources` |
|
||||||
|
| `configReloaded` | `ok`, `path`, `error?` | In reply to `reloadConfig` |
|
||||||
| `pong` | — | In reply to `ping` |
|
| `pong` | — | In reply to `ping` |
|
||||||
|
|
||||||
|
### Config File Format
|
||||||
|
|
||||||
|
`SourcesFile` is a flat JSON array of flat objects; `addr` marks a source,
|
||||||
|
`signal` marks a calibration entry.
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{"label": "wave", "addr": "127.0.0.1:44500"},
|
||||||
|
{"source": "wave", "signal": "Adc", "scale": 0.00030518, "offset": -1.25, "unit": "V"}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Flatness is a hard constraint: `StreamHub::LoadSourcesFile` scans from each `{`
|
||||||
|
to the next `}`, so a nested object would truncate the parse. Both hubs read and
|
||||||
|
write this format identically, and pre-calibration files load unchanged.
|
||||||
|
|
||||||
|
Calibration is applied **client-side only**. Rings, history, `zoom` replies, both
|
||||||
|
binary frames and the trigger comparator are all in raw units.
|
||||||
|
|
||||||
### Binary Push Frame (version 1, hub → client, binary WS frame)
|
### Binary Push Frame (version 1, hub → client, binary WS frame)
|
||||||
|
|
||||||
Little-endian throughout. Sent at `PushRate` Hz per source; contains **only
|
Little-endian throughout. Sent at `PushRate` Hz per source; contains **only
|
||||||
|
|||||||
+109
-3
@@ -46,8 +46,57 @@ Reply (unicast): `{"type":"pong"}`.
|
|||||||
```json
|
```json
|
||||||
{"type":"saveSources"}
|
{"type":"saveSources"}
|
||||||
```
|
```
|
||||||
Persists the current dynamically-added source list to the hub's `SourcesFile`
|
Writes the hub's `SourcesFile`: the current dynamically-added source list **and**
|
||||||
(JSON array of `{label,addr,multicastGroup,dataPort}`); it is reloaded at startup.
|
the calibration table, as one flat JSON array (see [§4](#4-config-file-format)).
|
||||||
|
The hub replies with [`configSaved`](#configsaved). Despite the name, this
|
||||||
|
command persists the whole config, not just the sources.
|
||||||
|
|
||||||
|
### `setCalibration`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"type":"setCalibration","source":"wave","signal":"Adc","scale":0.00030518,"offset":-1.25,"unit":"V"}
|
||||||
|
```
|
||||||
|
Records an affine calibration `value = raw × scale + offset` for one signal,
|
||||||
|
keyed by the source's **label** (not its runtime id) and the **base** signal
|
||||||
|
name — one entry covers every element of an array signal.
|
||||||
|
|
||||||
|
| Field | Type | Default | Validation |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `source` | string | — | non-empty after trimming |
|
||||||
|
| `signal` | string | — | non-empty after trimming; any trailing `[i]` is stripped |
|
||||||
|
| `scale` | number | `1` | finite and non-zero |
|
||||||
|
| `offset` | number | `0` | finite |
|
||||||
|
| `unit` | string | `""` | trimmed, truncated to 16 chars; empty = use the streamer's own unit |
|
||||||
|
|
||||||
|
Calibration is **metadata only**: the hub stores and redistributes it but never
|
||||||
|
applies it. Ring buffers, recorded history, the `zoom` reply, both binary frames
|
||||||
|
and the trigger comparator all stay in raw units — a client that ignores
|
||||||
|
calibration behaves exactly as before.
|
||||||
|
|
||||||
|
An entry that reduces to the identity (`scale = 1`, `offset = 0`, `unit = ""`) is
|
||||||
|
**deleted** rather than stored, so a reset leaves no residue in the config file.
|
||||||
|
|
||||||
|
On acceptance the hub broadcasts [`calibration`](#calibration) to every client. A
|
||||||
|
rejected entry produces **no** broadcast, so the offending client reverts to the
|
||||||
|
last value it was told.
|
||||||
|
|
||||||
|
### `reloadConfig`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"type":"reloadConfig"}
|
||||||
|
```
|
||||||
|
Re-reads `SourcesFile` and then:
|
||||||
|
|
||||||
|
- **replaces** the calibration table wholesale with the file's contents;
|
||||||
|
- **adds** any source in the file that is not already active;
|
||||||
|
- **never** removes, restarts or reconnects a live source.
|
||||||
|
|
||||||
|
The asymmetry is deliberate: calibration is cheap to reapply, whereas a source is
|
||||||
|
a live UDP session that must not be interrupted. An unsaved source the user added
|
||||||
|
keeps streaming.
|
||||||
|
|
||||||
|
The hub replies with [`configReloaded`](#configreloaded), followed on success by a
|
||||||
|
`calibration` broadcast and a `sources` broadcast.
|
||||||
|
|
||||||
### `getSources` / `getConfig` / `getStats`
|
### `getSources` / `getConfig` / `getStats`
|
||||||
|
|
||||||
@@ -219,6 +268,37 @@ If history is not enabled: `{"type":"historyZoom","error":"history not enabled"}
|
|||||||
{"type":"maxPointsUpdated","maxPoints":50000}
|
{"type":"maxPointsUpdated","maxPoints":50000}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### `calibration`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"type":"calibration","cal":[
|
||||||
|
{"source":"wave","signal":"Adc","scale":0.00030518,"offset":-1.25,"unit":"V"}
|
||||||
|
]}
|
||||||
|
```
|
||||||
|
The complete calibration table. Broadcast when a client connects (as an empty
|
||||||
|
array when nothing is calibrated), after every accepted `setCalibration`, and
|
||||||
|
after a successful `reloadConfig`. It is a separate frame rather than a field on
|
||||||
|
`sources` because `sources` is serialised into a fixed 4 KiB buffer.
|
||||||
|
|
||||||
|
### `configSaved`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"type":"configSaved","ok":true,"path":"/etc/streamhub/sources.json"}
|
||||||
|
{"type":"configSaved","ok":false,"path":"","error":"no SourcesFile configured"}
|
||||||
|
```
|
||||||
|
Broadcast in reply to `saveSources`. `path` is always present (empty when the hub
|
||||||
|
has no config file configured); `error` only when `ok` is false.
|
||||||
|
|
||||||
|
### `configReloaded`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"type":"configReloaded","ok":true,"path":"/etc/streamhub/sources.json"}
|
||||||
|
{"type":"configReloaded","ok":false,"path":"/etc/streamhub/sources.json","error":"cannot read sources file"}
|
||||||
|
```
|
||||||
|
Broadcast in reply to `reloadConfig`; same shape as `configSaved`. On success it
|
||||||
|
is followed by a `calibration` broadcast and, if the file added any source, a
|
||||||
|
`sources` broadcast.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Binary frames (hub → client)
|
## 3. Binary frames (hub → client)
|
||||||
@@ -270,7 +350,33 @@ per signal:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. Limits
|
## 4. Config file format
|
||||||
|
|
||||||
|
`SourcesFile` (C++ `SourcesFile` config key, Go `-sources-file` flag) is a flat
|
||||||
|
JSON array of flat objects. A block containing `addr` is a source; a block
|
||||||
|
containing `signal` is a calibration entry; anything else is skipped with a
|
||||||
|
warning.
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{"label": "wave", "addr": "127.0.0.1:44500"},
|
||||||
|
{"label": "mc", "addr": "127.0.0.1:44501", "multicastGroup": "239.0.0.1", "dataPort": 44502},
|
||||||
|
{"source": "wave", "signal": "Adc", "scale": 0.00030518, "offset": -1.25, "unit": "V"}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Every object must stay flat.** The C++ `StreamHub::LoadSourcesFile` parser
|
||||||
|
takes each `{` up to the next `}` as one object, so a nested object anywhere in
|
||||||
|
the file would truncate the parse at the inner brace. A nested
|
||||||
|
`"calibration": {…}` inside a source entry is therefore not an option, and this
|
||||||
|
is why calibration entries are siblings of sources rather than children.
|
||||||
|
|
||||||
|
Files written by hub versions predating calibration load unchanged, and a file
|
||||||
|
written by either hub loads in the other.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Limits
|
||||||
|
|
||||||
| Limit | Value |
|
| Limit | Value |
|
||||||
|-------|-------|
|
|-------|-------|
|
||||||
|
|||||||
@@ -80,6 +80,21 @@ Signals received in the CONFIG packet are listed in the sidebar:
|
|||||||
- **Spatial arrays** — `TimeMode = PacketTime` arrays are shown as an expandable
|
- **Spatial arrays** — `TimeMode = PacketTime` arrays are shown as an expandable
|
||||||
group; individual elements (`Ch1[0]`, `Ch1[1]`, …) can be dragged independently.
|
group; individual elements (`Ch1[0]`, `Ch1[1]`, …) can be dragged independently.
|
||||||
|
|
||||||
|
The unit badge next to each signal shows the calibration's unit override when one
|
||||||
|
is set, and the streamer's own unit otherwise.
|
||||||
|
|
||||||
|
At the bottom of the sidebar, the collapsible **Sources & Config** section holds:
|
||||||
|
|
||||||
|
- the `host:port`, label, multicast group and data port inputs plus **Connect**,
|
||||||
|
which adds a source at runtime;
|
||||||
|
- **Save** — writes the source list and the whole calibration table to the hub's
|
||||||
|
config file;
|
||||||
|
- **Reload** — re-reads that file. Calibration is replaced wholesale (so unsaved
|
||||||
|
edits are discarded), sources present in the file but not running are added,
|
||||||
|
and no running source is stopped or reconnected;
|
||||||
|
- a status line showing the written path on success or the hub's error text on
|
||||||
|
failure.
|
||||||
|
|
||||||
Click the sidebar toggle button (☰) to collapse/expand the signal list.
|
Click the sidebar toggle button (☰) to collapse/expand the signal list.
|
||||||
|
|
||||||
### Adding Plots
|
### Adding Plots
|
||||||
@@ -143,11 +158,31 @@ plot header showing per-signal vertical scale controls:
|
|||||||
| **V/div** | Volts (or units) per division |
|
| **V/div** | Volts (or units) per division |
|
||||||
| **Pos (div)** | Screen position in divisions (draggable offset marker on Y axis) |
|
| **Pos (div)** | Screen position in divisions (draggable offset marker on Y axis) |
|
||||||
| **Type** (Mixed mode only) | Toggle between **Analog** and **Digital** for this signal |
|
| **Type** (Mixed mode only) | Toggle between **Analog** and **Digital** for this signal |
|
||||||
|
| **Cal · Scale** | Data calibration gain. `value = raw × Scale + Offset` |
|
||||||
|
| **Cal · Offset** | Data calibration bias, in calibrated units |
|
||||||
|
| **Cal · Unit** | Overrides the unit reported by the streamer (max 16 chars) |
|
||||||
|
| **Reset** | Clears this signal's calibration (`Scale = 1`, `Offset = 0`, no unit override) |
|
||||||
| **✕** | Close the toolbar and deselect the signal |
|
| **✕** | Close the toolbar and deselect the signal |
|
||||||
|
|
||||||
Offset markers (small triangles on the Y axis) show each signal's position and can
|
Offset markers (small triangles on the Y axis) show each signal's position and can
|
||||||
be dragged to reposition signals without opening the toolbar.
|
be dragged to reposition signals without opening the toolbar.
|
||||||
|
|
||||||
|
**Calibration vs. V/div and Offset.** They are different things. V/div and Offset
|
||||||
|
are a *display* transform: they move and stretch the trace on screen. Calibration
|
||||||
|
changes *the value itself* — the plot, the Y-axis tick labels, the cursor and
|
||||||
|
hover readouts, the CSV export and the trigger threshold all report
|
||||||
|
`raw × Scale + Offset` in the calibrated unit. V/div is then read as "calibrated
|
||||||
|
units per division" and Offset as "the calibrated value at screen centre".
|
||||||
|
|
||||||
|
The calibration header names the **base** signal and its element count, because
|
||||||
|
one entry covers every element of an array — opening the toolbar on `Adc[3]` and
|
||||||
|
editing the calibration moves all of `Adc`.
|
||||||
|
|
||||||
|
Calibration is keyed by the source's **label**, is shared with every other
|
||||||
|
browser connected to the same hub, and is not persisted until you press **Save**
|
||||||
|
in the Sources & Config section. It is mirrored to `localStorage` so it survives
|
||||||
|
a page reload even against a hub with no config file.
|
||||||
|
|
||||||
### Plot Controls
|
### Plot Controls
|
||||||
|
|
||||||
| Control | Action |
|
| Control | Action |
|
||||||
|
|||||||
+2
-1
@@ -193,7 +193,8 @@ change never sends `calibration`, so the mirror simply remains authoritative;
|
|||||||
the same holds for a hub started without a config file.
|
the same holds for a hub started without a config file.
|
||||||
|
|
||||||
**Validation.** The same rules as the hub are enforced in the input handlers: a
|
**Validation.** The same rules as the hub are enforced in the input handlers: a
|
||||||
non-finite or zero `scale` reverts the field to its last accepted value.
|
non-finite or zero `scale` keeps the rejected text in the field and marks it
|
||||||
|
with a red `cal-invalid` border, so the user can see what was wrong.
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user