added auto measuraments
This commit is contained in:
1 parent
44fc4cdf36
commit
0eceaf537b
7 files changed
+2182
-55
No files matched your search
@@ -58,6 +58,68 @@ frame and the GUI thread takes once per frame around binding the traces.
|
||||
Samples cross the boundary through `plot::data::RingBuffer` (single producer,
|
||||
single consumer), never through the lock.
|
||||
|
||||
## Measurements
|
||||
|
||||
**Measurements** (overflow menu) reveals a pane with a `[+]` and one row per measurement:
|
||||
`[badge] <type> <value> [edit]`. `[+]` opens a dialog with the signal, the type, the options
|
||||
that type actually uses, and "show on plot"; `edit` opens the same dialog on that
|
||||
measurement, with a `Delete` beside `Save`.
|
||||
|
||||
The signal list is the signals the **plots are showing**, in signal-list order: a
|
||||
measurement reads the window a plot displays and its marks go on the row that plots the
|
||||
signal, so the candidates are the traces on screen. That is also what keeps everything else
|
||||
out of it — a signal nobody acquires, and a stream's own timestamp channel, which is acquired
|
||||
but is not something to measure. A measurement whose signal is no longer plotted keeps it in
|
||||
the list while it is being edited, so saving does not silently re-point it at whatever the
|
||||
dropdown happened to be showing. The badge is the measurement's own colour, and
|
||||
the marks it draws on the plot use it, so a readout and its ink are recognisably the same
|
||||
measurement.
|
||||
|
||||
| type | what it is | options | shown on the plot as |
|
||||
|---|---|---|---|
|
||||
| `average` | the mean of the visible samples | — | a ruler at the mean |
|
||||
| `top` / `bottom` | the high / low level: the mean of the samples above / below the split | split % | a ruler at the level |
|
||||
| `max` / `min` | the extremes | — | a ruler at the value **and** a marker where it happened |
|
||||
| `pulse duration` | the mean width of the pulse-side runs | split %, which side is the pulse | markers at the pulse's two edges |
|
||||
| `period` / `frequency` | the spacing of the threshold crossings / its reciprocal, in Hz | — | markers at two edges of a cycle |
|
||||
|
||||
`period` and `frequency` deliberately take **no options**: the engine counts *rising*
|
||||
crossings of the *mid* level, one edge per cycle, and a split or a side to choose from could
|
||||
only make that count wrong — a split near either extreme counts noise as crossings, and
|
||||
counting both edges halves the period.
|
||||
|
||||
The split is a share of the **window's own** min..max, not volts: a measurement is about this
|
||||
signal in this window, and a level in volts would have to be typed for every signal. A
|
||||
crossing has to be *strict* — a sample exactly at the level is on neither side — so a signal
|
||||
that merely touches the level does not count every sample as an event.
|
||||
|
||||
Each measurement's row carries **two badges**: its own colour (the same one its marks use on
|
||||
the plot) and the signal's chip, in the palette colour every other view names that signal
|
||||
with. The dialog's fields and their wording follow the type — the split row is absent for
|
||||
`average`, `min` and `max`, the side row only for `pulse duration`, and the "show on plot"
|
||||
line says what would be drawn.
|
||||
|
||||
The values are **window statistics over exactly the visible range, at the stored rate**: the
|
||||
same cut the export makes, so a measurement describes the data on screen rather than the ring
|
||||
behind it — and the marks it draws belong to that window, so they stay in the viewport.
|
||||
|
||||
They are recomputed when the **window moves**, not on a timer: a timer decouples the two, and
|
||||
with a scrolling window the stored marks then describe a range the plot has already left (the
|
||||
min/max marker ends up outside the viewport, and a dragged marker survives until the next
|
||||
tick). The trigger is 2 % of the span, so the numbers track the window within a few pixels
|
||||
while the cost stays bounded by the window's motion rather than by the frame rate; one pass
|
||||
copies the visible samples of each *channel* once, however many measurements read it.
|
||||
|
||||
**Show on plot** marks the result on the row that plots the signal: a ruler at the level
|
||||
(`top`/`bottom`), both rulers at the extremes (`min`/`max`), the pulse's or the cycle's two
|
||||
edge times as vertical markers. The marks are **readouts, not instruments**: a fixed pool of
|
||||
cursors per row, created **unselectable** (`Plot::set_cursor_selectable()`, added for this), so
|
||||
the controller's hit test never offers the pointer one — it can neither hover nor drag a
|
||||
measurement's ruler. They are also re-placed every frame, because a mark is only correct for
|
||||
the window its measurement was computed over. Re-placing alone was not enough: a drag is held
|
||||
for the gesture's life, so the marker followed the pointer *between* frames and the readout
|
||||
looked draggable.
|
||||
|
||||
## The session layout file
|
||||
|
||||
What you arranged is remembered per data source, in
|
||||
@@ -409,8 +471,12 @@ figure so the legend name is right (there is one widget, so the row a drop lands
|
||||
on is `PlotArea::row_at()`). The status line reports the outcome of a drop
|
||||
(`plotting 'sine' on plot 1`, `plot 1 already holds four signals`), so an action
|
||||
that changes nothing still says why. Each plot's y axis has a right-click menu
|
||||
with auto scale, a fixed range, and one **Remove '<signal>' from plot N** entry per
|
||||
occupied trace slot (a plot with nothing on it says so instead).
|
||||
with auto scale, a fixed range, **Fit X axis**, **Fit all**, and one
|
||||
**Remove '<signal>' from plot N** entry per occupied trace slot (a plot with nothing on
|
||||
it says so instead). The two fits are the same pair the overflow menu and the `f` key
|
||||
offer, split by what they touch: **Fit X axis** frames the window on the data the ring
|
||||
holds and leaves every y scale exactly as the user set it, **Fit all** does that and
|
||||
auto-scales the y axes too.
|
||||
|
||||
**The plot does not twitch.** Two independent causes were fixed, both of which
|
||||
showed up as the x axis moving a few pixels every frame — including while paused,
|
||||
@@ -518,6 +584,55 @@ switches them on as well, because a reset nobody can see reads as a dead button.
|
||||
The two panels collapse vertically (`SLIDE_DOWN`), so hiding one gives its space
|
||||
back instead of leaving an invisible gap in the column.
|
||||
|
||||
With the cursors on, a **readout panel** appears in the side column, and a dropdown
|
||||
chooses what the instruments are:
|
||||
|
||||
| mode | instruments | the pane reads |
|
||||
|---|---|---|
|
||||
| `vertical` | two x markers, one pair for the whole figure | `T0`, `T1`, `dT` |
|
||||
| `horizontal` | two y rulers on **each** row | `Y1`, `Y2`, `dY` per row |
|
||||
| `both` | both of the above | the times, then a line per row |
|
||||
| `mix` | the x pair, plus where each marker **crosses each trace** | the times, then `Y1`, `Y2`, `dY` per plotted signal |
|
||||
|
||||
The vertical pair is one stream time shared by every row, so the markers line up down
|
||||
the column and dragging one on any row moves them all — the app reads the position back
|
||||
out of the plot each frame, because a drag writes the *plot* and that is the only place
|
||||
it is visible. A ruler is per row: each row has its own y axis, so two rows' rulers are
|
||||
two different values, and the pane names the row.
|
||||
|
||||
`mix` needs no rulers: it reports the value the trace actually has at each marker, read
|
||||
through the library's `Plot::crosshair_value()` — the two bracketing samples,
|
||||
interpolated — so it is the answer to "what is this signal at these two times" rather
|
||||
than "drag a ruler until it crosses the trace". A readout is `--` when the trace has
|
||||
nothing at that time and `partial` when only one of the two markers is over samples.
|
||||
|
||||
The readouts are a **table**: a header naming the columns, then one row per instrument —
|
||||
`| who | A | B | delta |`. The first row is the two times and their difference, and the
|
||||
rows below it are the rulers (one per row, in `horizontal`/`both`) or the traces (in
|
||||
`mix`). The time row is there only where the time cursors are: in `horizontal` the pair is
|
||||
parked, and a row of times nothing on the plot can move would be a readout of nothing. Columns size to their widest cell, so a column of numbers is a column; the two
|
||||
cursors are `A` and `B` and their difference is `delta`, whichever of the three the mode
|
||||
puts in them.
|
||||
|
||||
Every readout is a **measurement**, not a raw number: at most two decimals, scaled by an
|
||||
SI prefix so the digits sit in `1..999`, and the unit the stream declared appended —
|
||||
`10.52 mV`, `-9.8 V`, `dT -398.67 ms`, `1.06 pV`. Two decimals is the resolution the
|
||||
instruments can claim (a marker lands on a sample, and the sample is an `f32`), and a
|
||||
trailing zero is dropped, so `1.50 V` reads `1.5 V`. A row's rulers use its signals' unit
|
||||
when they agree on one and print bare digits when they do not: a ruler is on the row's y
|
||||
axis, and two units on one axis make "the" unit a guess. A trace's line carries the
|
||||
signal's **colour chip** instead of its name — the chip identifies it exactly as the signal
|
||||
panel does — and the values are bare: `● 616.49 mV -24.57 mV -641.06 mV`, because the
|
||||
pane is a readout beside a plot and `Y1:` labels there are noise.
|
||||
|
||||
`mix` also *draws* the crossings: a mark on each trace where each marker meets it, in the
|
||||
marker's own colour, placed with the same call the pane reads (so the mark and the number
|
||||
cannot disagree), and shown only where the trace actually has samples at that time. The
|
||||
marks are annotations the plot holds from construction and that are moved, never added:
|
||||
`Plot` appends annotations only, so a host that re-added them would grow the plot every
|
||||
frame — which is why the library gained `Plot::set_annotation()` for this. Nothing marks
|
||||
the ruler modes: a ruler *is* the mark.
|
||||
|
||||
**The time window is typed, not picked off a ladder.** The header bar's `window`
|
||||
button reads the current span and opens onto `÷10` · a number · a unit (µs, ms, s,
|
||||
min) · `×10`. A ladder of round numbers could only ever offer the spans someone had
|
||||
@@ -573,9 +688,11 @@ logged with both rates and the new anti-alias corner. A window change that leave
|
||||
`time_stride` alone rebuilds nothing. Per-channel `stride` is still always 1 (see the
|
||||
limitations below); only the shared clock is decimated.
|
||||
|
||||
**Fit to data** (overflow menu) does what the plot's own `f` key does: auto-scales every row's y
|
||||
axis and re-frames the x window on the data the ring holds, propagated to every
|
||||
row. The status line acknowledges it (`fitted to the buffered data (1.25 s)`).
|
||||
**Fit to data** (overflow menu, the `f` key, or **Fit all** in a plot's right-click menu)
|
||||
auto-scales every row's y axis and re-frames the x window on the data the ring holds,
|
||||
propagated to every row. **Fit X axis** is the same re-framing without the y scaling, for
|
||||
the common case where the window should be the data's but the levels were placed by hand.
|
||||
The status line acknowledges either (`x fitted to the buffered data (1.25 s)`).
|
||||
|
||||
**Shared x axis.** The rows are the rows of **one `plot::Figure`**, drawn by one
|
||||
`plot::gtk4::PlotArea` in figure mode, and they all join a single x link group.
|
||||
@@ -607,6 +724,11 @@ removes it, a `+` on each boundary inserts one there — so the row an action na
|
||||
row you are pointing at. `--plots` is now only the count to start with, and `RowSet::kMaxRows`
|
||||
(8) is a ceiling rather than a menu: past about that a row is chrome and no plot.
|
||||
|
||||
The editor also carries its own way out: a **`Done`** button in the plot area's top-right
|
||||
corner while it is up, because the toggle that opened it lives in the overflow menu — a
|
||||
long way from the plot being edited. It turns that toggle off, so there is still exactly
|
||||
one piece of state, and the buttons (its own included) go with it.
|
||||
|
||||
An insert or a remove **moves the rows' contents with them**: what row 1 plotted, in the
|
||||
colours it plotted it in and with its cursors where they were, is on row 2 after inserting
|
||||
above row 1. Only that state moves; the figure's `Plot`s are rebuilt and re-dealt, which is
|
||||
@@ -711,6 +833,19 @@ now — they are listed so they are not mistaken for anything else:
|
||||
hundred samples every fifth of a second made every frame span microseconds while
|
||||
the frames were 100 ms apart).
|
||||
|
||||
**One source, one client, over unicast.** A UDPStreamer over unicast keeps a single
|
||||
session: a second `udpscope`'s CONNECT displaces the first, the displaced client sees its
|
||||
`silence_timeout_s` elapse, reconnects and displaces the other, and the two take turns
|
||||
being served — which reads as "the second UI gets no data" (and both windows look alike
|
||||
because they share the saved layout). Nothing on the client side can give both of them the
|
||||
stream: the wire has one destination. Watching one source from several UIs is what
|
||||
`--multicast <group> [--interface <local ip>]` is for — every instance joins the group and
|
||||
the kernel hands each of them a copy. When the app sees the fight (a reconnect with no
|
||||
frames delivered in the same second — the client's own retry counter is also shown in the
|
||||
status line as `N reconnects` once it is nonzero) it says so in the status line rather than
|
||||
leaving an empty plot to be puzzled over, and prints a `starved:` line to stderr while the
|
||||
state holds.
|
||||
|
||||
## Limitations
|
||||
|
||||
- **Multi-element (vector/matrix) signals are acquired with a zero-order hold.**
|
||||
@@ -727,11 +862,22 @@ now — they are listed so they are not mistaken for anything else:
|
||||
- **Calibration is applied at ingest**, so changing gain or offset rebuilds the
|
||||
buffers (and drops the history). Applying it at read time would need the LOD
|
||||
pyramid to be gain-aware.
|
||||
- **Cursors are per row** and not synchronised with each other; show/hide parks
|
||||
them outside the axis range rather than removing them, because `Plot` has no
|
||||
`remove_cursor`. The trigger marker is the one exception: it has its own cursor
|
||||
and is independent of the cursor toggle.
|
||||
- **The horizontal rulers are per row**, since a y value means nothing without the
|
||||
y axis it is on; the vertical pair is shared, so the same two times are on every
|
||||
row. Show/hide *parks* every instrument outside the axis range rather than removing
|
||||
it, because `Plot` has no `remove_cursor` — which is why switching mode re-places
|
||||
rather than rebuilds, and why the frame tick reads the markers back: a parked
|
||||
cursor's snapped position is a real number, so nothing may be inferred from it. The
|
||||
trigger marker is separate: its own cursor, independent of the cursor toggle.
|
||||
- **Verified headless only.** Acquisition, partitioning, the trigger state
|
||||
machine and its integration with the buffers, and rendering under Xvfb
|
||||
(llvmpipe) were exercised; the interactive paths (drag and drop, the dialogs,
|
||||
the right-click menu) were not driven by an automated test.
|
||||
|
||||
**A debug-build assertion in the library's suite.** `deps/plotter`'s debug build prints
|
||||
`TextBatch::add_cell`'s `glyph_is_cell` assertion once while running its tests — from a
|
||||
forked child of the text/atlas cases (`a_backend_without_an_atlas_uploads_nothing` is the
|
||||
one that precedes it), not from a failing test: the suite still reports `355 cases,
|
||||
0 failed`, and this is pre-existing rather than something the cursors work introduced. The
|
||||
new ruler test asserts the *strokes* (`figure_rows_draw_their_own_horizontal_rulers`) with
|
||||
the harness's no-atlas backend, so it neither needs nor trips the text path.
|
||||
Reference in new issue
Block a user