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:
Martino Ferrari
2026-08-17 07:27:25 +02:00
co-authored by Claude Sonnet 4.6
parent e37eec0276
commit d26b78b7f6
4 changed files with 171 additions and 5 deletions
+109 -3
View File
@@ -46,8 +46,57 @@ Reply (unicast): `{"type":"pong"}`.
```json
{"type":"saveSources"}
```
Persists the current dynamically-added source list to the hub's `SourcesFile`
(JSON array of `{label,addr,multicastGroup,dataPort}`); it is reloaded at startup.
Writes the hub's `SourcesFile`: the current dynamically-added source list **and**
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`
@@ -219,6 +268,37 @@ If history is not enabled: `{"type":"historyZoom","error":"history not enabled"}
{"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)
@@ -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 |
|-------|-------|