WebUI: resizable plots, digital/mixed modes, buffer fix, zoom brackets, updated docs

- Resizable plot grid: drag handles between plots adjust column/row fr sizes
- Plot configuration toolbar (click title): edit title, select Normal/Mixed/Digital mode
- Digital mode: logic-analyzer banded layout, signals quantized to hi/lo per band
- Mixed mode: banded layout where each signal is independently analog or digital
- Per-signal vscale toolbar embedded inline below plot header (badge click to open)
- Active signal highlighted in foreground with increased line width
- Signal offset markers draggable on Y axis; per-plot vscale state isolation
- Buffer sizing based on signal sampling rate (up to 60s @ configured rate)
- growBuffer: live buffer expansion without data loss on window/rate change
- Zoom bracket lines: nearest out-of-range points included for continuity
- Updated Docs/WebUI.md to reflect current uPlot-based implementation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino Ferrari
2026-05-27 16:29:09 +02:00
parent cf174edde8
commit 915c6fc126
4 changed files with 604 additions and 80 deletions
+159 -47
View File
@@ -3,16 +3,16 @@
The WebUI is a Go binary that acts as a bridge between UDPStreamer and any web browser.
It receives UDP packets from UDPStreamer, reassembles fragmented data, and re-publishes
decoded signal values over a WebSocket to the browser. The browser renders live plots
using [Plotly.js](https://plotly.com/javascript/).
using [uPlot](https://github.com/leeoniya/uPlot).
```
MARTe2 RT app
│ UDP (binary protocol)
udpstreamer-webui (Go)
│ WebSocket (JSON)
│ WebSocket (binary frames)
Browser (index.html + Plotly.js)
Browser (index.html + uPlot)
```
---
@@ -55,18 +55,19 @@ Open `http://localhost:8080` in any modern browser.
### Layout
```
┌─────────────────────────────────────────────────────────┐
MARTe2 UDP Streamer ● Streaming Window: [5 s▾] ⏸ │ ← top bar
├────────────────────────────────────────────────────────┤
│ Signals │ Plots [1×1][2×1][2×2]… [+Add]
│──────────│──────────────────────────────────────────────│
│ Counter │ ┌─────────────────┐ ┌──────────────────┐
│ Time │ │ Plot 1 │ │ Plot 2 │
│ Sine1 │ │ (drop signals) │ │ (drop signals) │
│ Sine2 │ │ │ │ │
│ Ch1[1000]│ └─────────────────┘ └──────────────────┘
│ Ch2[1000]│ │
└──────────┴──────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────────────────────
☰ UDP Scope │ ⊞ 1×1 ▾ │ A: — B: — ΔT: — │ Window: [5s▾] Fit ⬇ CSV ⚡ Trigger ⏸ Pause │
├──────────────────┬────────────────────────────────────────────────────────────┤
│ Signals [Plot title] ○ ○ ○ ← signal badges
│──────────────────│──────────────────────────────────────────────────────────
│ Counter · u32 │ ┌─────────────────────┐ ┌─────────────────────────────┐ │
│ Time · f64 │ │ Plot 1 │ │ Plot 2 │ │
│ Sine1 · f32 │ │ (drop signals here) │ │ (drop signals here) │
│ Sine2 · f32 │ │ │ │ │ │
│ Ch1 · [1000] f32 │ └─────────────────────┘ └─────────────────────────────┘ │
└──────────────────┴────────────────────────────────────────────────────────────┘
│ ● Streaming [📊 Stats] v1.0.0 │
└───────────────────────────────────────────────────────────────────────────────┘
```
### Signal Sidebar
@@ -79,12 +80,73 @@ Signals received in the CONFIG packet are listed in the sidebar:
- **Spatial arrays** — `TimeMode = PacketTime` arrays are shown as an expandable
group; individual elements (`Ch1[0]`, `Ch1[1]`, …) can be dragged independently.
Click the sidebar toggle button (☰) to collapse/expand the signal list.
### Adding Plots
1. Click **+ Add Plot** in the toolbar.
1. Select a layout from the layout menu (⊞ button in the top bar).
2. Drag a signal from the sidebar onto a plot panel.
3. Multiple signals can be overlaid on the same plot.
4. Click the **×** badge to remove a trace.
4. Click the **×** inside a signal badge to remove a trace.
### Plot Layout
The plot grid supports multiple layouts selectable from the layout menu (⊞):
| Layout | Description |
|--------|-------------|
| 1×1 | Single plot |
| 2×1 | Two columns |
| 1×2 | Two rows |
| 2×2 | Four plots |
| 3×1 | Three columns |
| … | More layouts available |
**Resizing plots**: When multiple plots are shown, drag the dividers between them
to resize. Vertical dividers resize column widths; horizontal dividers resize row
heights. Sizes are stored as fractional grid units (fr).
### Plot Configuration
Click the **plot title** to open the configuration toolbar:
- **Title** — edit the plot's display name.
- **Mode** — select the plot display mode:
- **Normal** — standard time-series oscilloscope view (default).
- **Mixed** — signals are arranged in horizontal bands (like digital mode), but
each signal can independently be displayed as analog (auto-scaled within its band)
or digital (quantized to high/low within its band).
- **Digital** — logic-analyzer style; each signal occupies a fixed horizontal band
and is quantized to high/low based on its data range midpoint as threshold.
### Signal Badges and Selection
Each signal assigned to a plot appears as a colored badge in the plot header.
- **Click a badge** — selects the signal and opens the V-Scale toolbar for that
signal. The selected signal is drawn on top with increased line width.
- **Click again** — deselects the signal and closes the V-Scale toolbar.
- **Right-click a badge** — opens the style context menu (color, line width, dash
style, markers).
- **Click ×** on a badge — removes the signal from the plot.
### V-Scale Toolbar
When a signal is selected (via badge click), an inline toolbar appears below the
plot header showing per-signal vertical scale controls:
| Control | Description |
|---------|-------------|
| **Auto** | Automatically fits the signal vertically |
| **Range** | Shows V/div and Pos controls for manual positioning |
| **Manual** | Fixed V/div with free positioning |
| **V/div** | Volts (or units) per division |
| **Pos (div)** | Screen position in divisions (draggable offset marker on Y axis) |
| **Type** (Mixed mode only) | Toggle between **Analog** and **Digital** for this signal |
| **✕** | Close the toolbar and deselect the signal |
Offset markers (small triangles on the Y axis) show each signal's position and can
be dragged to reposition signals without opening the toolbar.
### Plot Controls
@@ -93,18 +155,76 @@ Signals received in the CONFIG packet are listed in the sidebar:
| **⏸ / ▶** (per plot) | Pause / resume that plot; paused plots allow zoom and pan |
| **⬇** (per plot) | Export all visible traces to CSV |
| **🗑** (per plot) | Delete the plot |
| **⏸ Pause All** (top bar) | Pause all plots simultaneously |
| **⏸ Pause** (top bar) | Pause all plots simultaneously |
| **↺ Auto** (top bar) | Resume all paused plots and snap back to live view |
| **Window** (top bar) | Adjust the rolling time window (1 s 60 s) |
| Layout buttons | Switch between 1×1, 2×1, 1×2, 2×2, 3×1, … grid layouts |
| Sidebar **←** | Collapse the signal list to maximise plot area |
| **Fit** (top bar) | Fit all plots to their current data range |
| **⬇ CSV** (top bar) | Export all signals from all plots to CSV |
| Layout button | Switch between grid layouts |
| Sidebar **☰** | Toggle the signal list panel |
### Status LED
### Cursor System
| Colour | Meaning |
|--------|---------|
| Red (solid) | No WebSocket connection to browser |
| Orange (pulsing) | WebSocket connected but no data received in > 1 s |
| Green (pulsing) | Data is being received normally |
Two vertical cursors (A and B) can be placed on plots:
- Enable with the **Cursor** button (shown when a plot is paused/zoomed).
- The top bar readout shows **A**, **B**, and **ΔT** (time between cursors).
- Cursors follow the mouse within the plot area.
### Zoom
- **Drag** left or right on a paused plot to zoom into a time range.
- **← Back** button steps back through zoom history.
- **Fit** returns to the full data view.
- When zooming into a region with few or no data points, the nearest data points
outside the zoom window are included so that connecting lines are drawn across
the view rather than showing blank space.
### Trigger System
Click **⚡ Trigger** in the top bar to open the trigger bar:
| Control | Description |
|---------|-------------|
| **Signal** | Select the trigger source signal |
| **Edge** | Rising ↑, Falling ↓, or Both ↕ |
| **Threshold** | Trigger level |
| **Window** | Capture duration after trigger (100 µs 10 s) |
| **Pre** | Pre-trigger buffer percentage (0100%) |
| **Mode** | **Normal** (re-arms automatically) or **Single** (fires once) |
| **Rearm** | Manually re-arm the trigger |
| **Stop** | Cancel waiting trigger |
For array signals, clicking the trigger signal selector prompts for the element
index to use as the trigger source.
### Status Bar
The status bar at the bottom shows:
- **LED indicator**: red (disconnected), orange pulsing (connected, no data), green pulsing (streaming).
- **Status text**: connection state and data age.
- **📊 Stats** button: opens the source statistics panel (packet counts, rates, errors).
- **Build version**: server build tag.
---
## Data Buffering
Signal buffers are sized based on the signal's configured sampling rate from the
CONFIG packet:
```
capacity = min(600 000, ceil(samplingRate × 60 s × 1.5))
```
This ensures up to 60 seconds of history is retained for any signal, regardless of
sample rate. If a signal's sampling rate is not configured, a default capacity of
100 000 samples is used. Temporal arrays (burst signals) use a fixed capacity of
500 000 samples.
Buffers grow automatically during streaming if a higher effective sample rate is
detected. Existing data is preserved during growth.
---
@@ -117,15 +237,15 @@ main()
├── hub.Run() ← event loop: register/unregister WS clients, batch data at 30 Hz
├── udpClient.Run() ← reconnects on silence; parses UDP packets; feeds hub.dataCh
└── http.ListenAndServe()
├── GET / ← serves embedded index.html
├── GET / ← serves embedded static files
└── GET /ws ← upgrades to WebSocket; launches per-client read/write pumps
```
### WebSocket Message Format
All messages are JSON. Two types are sent from server to browser:
Two message types are sent from server to browser.
#### `config` message
#### `config` message (JSON)
Sent immediately when a browser client connects (if a CONFIG has been received from
UDPStreamer) and whenever UDPStreamer sends a new CONFIG packet.
@@ -144,7 +264,7 @@ UDPStreamer) and whenever UDPStreamer sends a new CONFIG packet.
"rangeMin": -10.0,
"rangeMax": 10.0,
"timeMode": 0,
"samplingRate": 0.0,
"samplingRate": 1000.0,
"timeSignalIdx": 4294967295,
"unit": "V"
}
@@ -152,27 +272,19 @@ UDPStreamer) and whenever UDPStreamer sends a new CONFIG packet.
}
```
#### `data` message
#### `data` message (binary)
Sent at ≤ 30 Hz, batching all UDP packets received since the last tick.
Each signal carries its own time axis to support mixed scalar + temporal-array signals.
Uses a compact binary format: a fixed header followed by per-signal blocks.
```json
{
"type": "data",
"signals": {
"Sine1": { "t": [1747123456.001, 1747123456.002], "v": [3.14, 2.71] },
"Counter": { "t": [1747123456.001, 1747123456.002], "v": [12340, 12341] },
"Ch1": { "t": [1747123456.000, 1747123456.0001, ...], "v": [0.12, 0.13, ...] }
}
}
```
Each signal block contains:
- Signal name (length-prefixed)
- Number of samples
- Timestamp array (float64 LE, Unix seconds)
- Value array (float64 LE)
- `t` — Unix timestamp in seconds (float64) for each sample.
- `v` — physical value (after dequantization if applicable).
- For **temporal arrays**, `t` and `v` have `N × batchSize` entries
(up to 2000 per 30 Hz tick after server-side decimation).
- For **spatial arrays** the keys are `"Ch1[0]"`, `"Ch1[1]"`, etc.
For **temporal arrays**, each packet contributes `N` samples (one per array element).
For **spatial arrays**, keys are `"Ch1[0]"`, `"Ch1[1]"`, etc.
---