feat: the manual is a Mallard set opened in Yelp, not a GResource in the binary

The help was 14 Pango-markup `.txt` bodies in a second GResource, drawn into a paged `GtkWindow`
built in `help_window.cpp`. That made F1 a viewer of ours with no search index, no cross-references
and no translation, and it kept the manual in the *binary* when every other desktop application's is
a document on disk. It is now a real Mallard set:

  help/C/org.udpscope.app/  index.page + 14 topic pages + figures/*.png

and `show_help()` hands `help:org.udpscope.app` to whatever the desktop registered for the `help:`
scheme -- Yelp on GNOME -- through a `GtkUriLauncher`, reporting a launch failure with a
`GtkAlertDialog` rather than a click that silently does nothing. The paged window's builder, the
page table and `help_self_check()`'s markup validation are gone with the resource.

The document identifier is the desktop file's own basename (its desktop-file id), derived in
CMakeLists from `data/org.udpscope.app.desktop.in`, so the same string is the `.desktop` name, the
GApplication id and the install directory at once. The set is installed by a new `install()` rule to
`<datadir>/help/C/org.udpscope.app/` -- where the freedesktop Help System Specification's `help:`
URI resolves `help:<id>` -- so the manual is read from a datadir by another process even though the
binary has no install step. `UDPSCOPE_HELP_NAME` and `UDPSCOPE_HELP_DIR` are compiled in from that
one place.

The 14 pages were migrated fact for fact, but restructured the Mallard way: a `<desc>` per page
(what a search result shows), `<steps>` for procedures, `<terms>` for option lists,
`<gui>`/`<key>`/`<keyseq>` for interface words and keys, `<note>` for the asides, and a topic graph
via `<link type="guide" xref="index#main"/>`. The figures are annotated screenshots produced by
`tools/make-help-shots.sh`, which drives the app headlessly against `tools/mock_udpstreamer.py` and
draws the boxes with ImageMagick; the side panel's box is measured from the app's own `side=NNNx`
dump reading, the header and status bands come from a documented table, and a crop whose standard
deviation is 0 fails the script.

`cmake --build build --target test-help` checks the set the way `test-ui` checks the `.ui` files:
`yelp-check`'s offline subcommands (links, media, orphans, ids) and a `yelp-build html` render.
`yelp-check validate` is deliberately absent -- it fetches the Mallard RelaxNG schema over the
network and fails offline for that reason, not for a fault in the pages.

The `[help]` probe reading changes with the resource it was written for. `parsed == pages` over
in-binary markup cannot exist now, so `help_self_check()` scans `UDPSCOPE_HELP_DIR` and the line
reads `topics=14 index=1 figures=3`; `topics=0 index=0` is the manual missing from the tree, which
is the finding the old count named.

Verified: clean build with the documented three warnings (the help bundle's `-Woverlength-strings`
is gone, so the baseline moves from four to three); test-ui 14/14; test-help clean (the four
subcommands and 15 pages rendered); a temp-prefix install lands 15 `.page` files and 3 figures at
`share/help/C/org.udpscope.app/`; the headless run reads `[help] topics=14 index=1 figures=3 dir=...`
with 0 Gtk-CRITICAL. Yelp's own window could not be exercised on this box -- it starts under Xvfb,
activates its portals and never maps, the same WebKit/portal limitation that stops a grab -- so its
rendering and its search index are unverified here; the content pipeline was (`yelp-build` renders
the whole set).

Docs: the manual's build/install/open story in `build-and-run.md` and README; the warning baseline
in `verification.md`/CLAUDE.md; the help bullets in `invariants-gtk-and-chrome.md`; the `[help]`
line in `probe-readings.md`.
This commit is contained in:
Martino Ferrari committed 2026-10-14 16:49:30 +02:00
1 parent 620473a5be
commit c1460a6977
47 files changed
+1910 -1056

No files matched your search

+87
View File
@@ -0,0 +1,87 @@
<page xmlns="http://projectmallard.org/1.0/"
type="topic" style="task"
id="cursors">
<info>
<link type="guide" xref="index#analysis"/>
<link type="seealso" xref="measurements"/>
<desc>The shared vertical cursor pair and the per-row horizontal rulers, their four modes,
and how a reading is formatted.</desc>
<credit type="author copyright">
<name>Martino Ferrari</name>
</credit>
</info>
<title>Cursors</title>
<p>Cursors read values off the traces. There are two kinds: one <em>shared vertical
pair</em> that every row carries at the same time, and <em>horizontal rulers</em>, one pair
per row, that belong to that row alone.</p>
<section id="reach">
<title>How to reach it</title>
<steps>
<item>
<p>Press the cursors button in the toolbar, or <key>c</key>.</p>
</item>
<item>
<p>The pane below the plot shows the readings and owns a <gui>Reset</gui> button.
Drag a marker to move it.</p>
</item>
<item>
<p>The mode dropdown offers four modes: <gui>vertical</gui> (the shared time pair),
<gui>horizontal</gui> (the per-row rulers), <gui>both</gui>, and <gui>mix</gui>.</p>
</item>
</steps>
</section>
<section id="knowing">
<title>Worth knowing</title>
<list>
<item><p>The vertical pair is <em>one</em> time for the whole figure, so moving it on
any row moves it on all of them. That is what makes <code>dt</code> meaningful across
rows.</p></item>
<item><p>A ruler belongs to its row and nothing shares it, so two rows' rulers are
independent.</p></item>
<item><p><gui>mix</gui> is the reading mode: one line per <em>bound</em> trace, with the
trace's own values at each marker and their difference. A slot with a name but no
channel behind it is not a trace and gets no line.</p></item>
<item><p>A value is the <em>nearest stored sample's</em>, never a number between two
samples. Zoom in past the sample spacing and the line between two samples is something
the plot drew, not data — so a reading off it would be the drawing. The hover crosshair
reads the same way.</p></item>
<item><p>Every reading carries a <em>metric prefix</em> so the digits sit in
<code>1..999</code>: <code>10.52 mV</code>, not <code>0.01 V</code>. Two decimals is all
an instrument landing on an <code>f32</code> sample can claim, so without the prefix a
small value would read <code>0.00</code> and the reading would be gone. A stream that
declares no unit gets the prefix anyway, written tight against the number
(<code>585.24m</code>) — the space separates the number from its unit, and a spaced
<code>m</code> with nothing after it would read as metres.</p></item>
<item><p>A marker past the newest sample reads <code>--</code>. With only one marker
over data the row reads <code>partial</code>: a difference between a number and nothing
is not a number.</p></item>
<item><p>In <gui>mix</gui> each marker also places a crossing annotation per trace,
which is what lets you see on the plot which sample a number came from. It is <em>on
that sample</em>, so when the marker falls between two it sits a little off its own
line: the line is where you put the marker, the dot is the datum being quoted. The hover
crosshair's mark snaps the same way, and is drawn larger than a trace's sample dots so
it does not read as one more sample.</p></item>
<item><p>The time row exists only in the modes that show the vertical pair.
<gui>horizontal</gui> has no time row, by design.</p></item>
<item><p>A cursor <em>stays where you put it</em> while a live view rolls under it,
which is what a bench scope does: its <code>T0</code> is an age, so a marker left at
<code>-1.5s</code> keeps reading the sample a second and a half old as the stream
advances. A measurement's marks do the opposite — they travel with the sample they
measured — because a cursor is a question you asked and a mark is an answer about one
particular sample.</p></item>
</list>
</section>
<section id="example">
<title>Example</title>
<p>Three traces in <gui>mix</gui> mode with both markers over data: 3 lines and 6 crossing
marks, each line reading like <code>sine 0.31 V -0.84 V -1.15 V</code>. Move the second
marker past the newest sample and the third column reads <code>--</code>.</p>
</section>
</page>
+78
View File
@@ -0,0 +1,78 @@
<page xmlns="http://projectmallard.org/1.0/"
type="topic" style="task"
id="export">
<info>
<link type="guide" xref="index#output"/>
<link type="seealso" xref="sessions"/>
<desc>Writing the visible range of every plotted trace to NumPy, CSV or MDF 3.30, and what
each format carries.</desc>
<credit type="author copyright">
<name>Martino Ferrari</name>
</credit>
</info>
<title>Saving and export</title>
<p>Export writes the <em>visible range</em> of every plotted trace to a file: NumPy
<file>.npy</file>, <file>.csv</file>, or <gui>MDF 3.30</gui> (<file>.mf3</file> or
<file>.mdf</file>), chosen by the extension you type.</p>
<section id="reach">
<title>How to reach it</title>
<steps>
<item>
<p>Pick <gui>Export visible range…</gui> in the overflow menu, or press
<keyseq><key>Ctrl</key><key>E</key></keyseq>.</p>
</item>
<item>
<p>A file chooser asks where; the format follows the path's extension.</p>
</item>
</steps>
</section>
<section id="knowing">
<title>Worth knowing</title>
<list>
<item><p>The table is collected at the moment you click, not when the chooser returns. A
live view keeps scrolling while a dialog is up, so a late collection would write a window
you never saw.</p></item>
<item><p>The rows are the <gui>stored samples</gui>, never the LOD buckets the plot draws
from. A row count far below the span times the stored rate means something cut the table
from the pyramid.</p></item>
<item><p>The stored rate is not always the wire rate. A window long enough to need
decimation is stored averaged at a lower rate, and the export carries <em>that</em> —
read the status bar's ⓘ popover for the figure before trusting a sample count.</p></item>
<item><p>Two time columns. <code>time</code> is the display timeline — negative ages
ending at 0 while streaming, offsets from the trigger edge while an event is held — so it
is the x you read a value at on screen. <code>abs_seconds</code> is the source's own
clock for the same sample, which is what lets two exports taken in different modes line
up. Both are strictly increasing and evenly spaced; duplicate or backwards stamps would
mean the file held two clocks.</p></item>
<item><p>An export taken <em>mid-capture</em> writes the partly filled window that is on
screen, which is shorter than the event span. Export outside a capture if you want the
whole event.</p></item>
<item><p><gui>MDF</gui> exists for handing a window to another tool, and its shape follows
the contract that reader uses: one channel group, every channel record-interleaved, the
master channel as the x axis and the unit in the channel metadata. So there the
<em>master is <code>abs_seconds</code></em>, named <code>time</code> — the clock that is
the same in every display mode — and the display axis goes out beside it as an ordinary
channel, <code>time_relative</code>. Both are seconds; each signal carries the source's
own unit. That is also what <gui>ScryLab</gui> reads: channel name → signal name, master
→ x axis, unit → the signal's unit.</p></item>
</list>
</section>
<section id="example">
<title>Example</title>
<p>A 2 s window at 10 kS/s over 3 traces writes 20001 rows and 5 columns
(<code>time</code>, <code>abs_seconds</code>, three signals). The <file>.npy</file> is
<code>64·⌈(10+len(header)+1)/64⌉ + rows·(16 + 4·cols)</code> bytes. Read it back with
<code>pd.DataFrame(np.load(path))</code>. The <file>.mf3</file> is bigger — a block per
channel plus a comment — <code>272 + comment + rows·record + 28 + channels·274 + 30</code>
bytes, with <code>record = 16 + 4·cols</code> and <code>channels = cols + 2</code> for the
master and the relative axis. Read that one back with <code>asammdf.MDF(path)</code> or in
ScryLab.</p>
</section>
</page>
Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

+67
View File
@@ -0,0 +1,67 @@
<page xmlns="http://projectmallard.org/1.0/"
type="guide" style="task"
id="index">
<info>
<desc>udpscope plots a live UDPStreamer source as one figure of up to eight stacked
plot rows sharing a single time axis, with a trigger, cursors, measurements, math
channels and an export. This manual covers connecting, the window and its panels,
navigating and pausing the plot, the trigger and its event cache, analysis, and
saving and exporting.</desc>
</info>
<title>udpscope</title>
<section id="main">
<title>Getting started</title>
<list>
<item><p><link xref="overview">Overview</link> — what udpscope is, how it reaches a
source, the window and its panels, and a first five minutes.</p></item>
<item><p><link xref="source">Choosing a source</link> — the command line, the
chooser, and opening an exported waveform instead of a live stream.</p></item>
<item><p><link xref="signals">Signals and calibration</link> — how a signal becomes
a channel, what starts acquisition, and how a scale and offset are applied.</p></item>
<item><p><link xref="layout">Rows, layout and drag &amp; drop</link> — up to eight
rows, four traces each, and the layout editor.</p></item>
</list>
</section>
<section id="viewing">
<title>Viewing</title>
<list>
<item><p><link xref="window">Time window</link> — the one control that changes the
window, and the decimation a long window causes.</p></item>
<item><p><link xref="navigate">Navigating the plot</link> — panning, zooming and
fitting with the mouse and the keyboard.</p></item>
<item><p><link xref="pause">Pause and play</link> — freezing the display without
stopping the stream, and how long you have been looking.</p></item>
<item><p><link xref="trigger">Trigger</link> — the four trigger shapes, arming,
hold-off, the event cache, and searching the buffer.</p></item>
</list>
</section>
<section id="analysis">
<title>Analysis</title>
<list>
<item><p><link xref="cursors">Cursors</link> — the shared vertical pair and the
per-row horizontal rulers.</p></item>
<item><p><link xref="measurements">Measurements</link> — a statistic of the visible
window, its on-plot marks, and its statistics line.</p></item>
<item><p><link xref="math">Math channels</link> — building a derived channel from a
graph of source nodes, operations and one output.</p></item>
</list>
</section>
<section id="output">
<title>Output and state</title>
<list>
<item><p><link xref="export">Saving and export</link> — the visible range to
<file>.npy</file>, <file>.csv</file> or <file>.mf3</file>.</p></item>
<item><p><link xref="sessions">Sessions and settings</link> — the per-source
configuration files and what they carry.</p></item>
<item><p><link xref="status">Status bar and diagnostics</link> — the glance line, the
ⓘ popover, and the activity badge.</p></item>
</list>
</section>
</page>
+71
View File
@@ -0,0 +1,71 @@
<page xmlns="http://projectmallard.org/1.0/"
type="topic" style="task"
id="layout">
<info>
<link type="guide" xref="index#main"/>
<link type="seealso" xref="signals"/>
<desc>How the figure's rows and traces are arranged, how to drop a signal onto a row, and
how the layout editor adds and removes rows.</desc>
<credit type="author copyright">
<name>Martino Ferrari</name>
</credit>
</info>
<title>Rows, layout and drag &amp; drop</title>
<p>The figure is a stack of rows, 1 to 8 of them, each holding up to 4 traces. Rows share
one time axis and one pair of side margins, so their y labels line up, their data areas
start and end on the same two columns, and their windows cannot drift apart. The left
margin holds the y labels; the right one is empty, and is there so the newest tick's label
and the newest sample's ink have somewhere to land instead of sitting on the frame.</p>
<section id="reach">
<title>How to reach it</title>
<steps>
<item>
<p>Drag a signal's chip out of the side panel (<key>s</key>) and drop it on a row to
add a trace.</p>
</item>
<item>
<p>Press <key>l</key>, or pick <gui>Layout editor</gui> from the overflow menu, to add
and remove rows: a <gui>×</gui> appears at the top left of each row and a <gui>+</gui>
on each boundary between rows, including above the first and below the last.</p>
</item>
<item>
<p>The editor's own button in the top-right corner leaves it.</p>
</item>
</steps>
</section>
<section id="knowing">
<title>Worth knowing</title>
<list>
<item><p>A plot's own <gui>right-click menu</gui> (the same compact rows as the app's
menu) holds the y-axis scale, the two fits, and <gui>Remove signal</gui>, which swaps
the menu to that plot's traces plus <gui>All traces on this plot</gui> — with a
<gui>Back</gui> row to return. That is how a trace comes off a row without going through
the layout editor.</p></item>
<item><p>A drop <em>adds</em> a trace to the row's next free slot; it does not replace
one. With four traces already on a row the drop is refused.</p></item>
<item><p>Inserting a row moves the rows below it <em>with their contents</em>. Inserting
above row 1 puts what row 1 plotted onto row 2 and leaves the new row empty — the rows
are not renumbered under you.</p></item>
<item><p>A trace is remembered by the signal's <em>name</em>, not by its channel number,
because a new CONFIG or an enable renumbers the channels and an index would silently
re-point the trace.</p></item>
<item><p>Adding or removing a row rebuilds the figure, which is cheap, but it also
re-deals every trace. A frozen trigger event survives the rebuild: the pinned window is
restated rather than re-framed on the live edge.</p></item>
</list>
</section>
<section id="example">
<title>Example</title>
<p>With three traces on row 1, pressing <key>l</key> shows 3 buttons (one <gui>×</gui>,
two <gui>+</gui>). Clicking the <gui>+</gui> above row 1 gives 2 rows and 5 buttons, with
the three traces now on row 2 — the layout probe reports exactly this as
<code>rows=2 buttons=5 traces=[0:-|1:sine,ramp,noise]</code>.</p>
</section>
</page>
+109
View File
@@ -0,0 +1,109 @@
<page xmlns="http://projectmallard.org/1.0/"
type="topic" style="task"
id="math">
<info>
<link type="guide" xref="index#analysis"/>
<link type="seealso" xref="signals"/>
<desc>Building a derived channel from a graph of source nodes, operations and one output,
and how the graph is evaluated.</desc>
<credit type="author copyright">
<name>Martino Ferrari</name>
</credit>
</info>
<title>Math channels</title>
<p>A math channel is a signal computed from other signals. You build it as a small graph —
source nodes, operations, one output — and udpscope then treats the result as an ordinary
channel.</p>
<section id="reach">
<title>How to reach it</title>
<steps>
<item>
<p>Press the math button in the toolbar, or <key>g</key>.</p>
</item>
<item>
<p>The pane's <gui>+</gui> adds a channel, and opening one shows the graph editor:
nodes as boxes, click a box to select it and edit its parameters beside the canvas.</p>
</item>
<item>
<p>Inside the editor, <gui>add node</gui> is a menu button whose popover picks the op,
so the node arrives as the operation you asked for.</p>
</item>
</steps>
</section>
<section id="gestures">
<title>Graph gestures</title>
<p><gui>drag</gui> from an output port to an input port to wire them; drag a wired input
port off its connection to detach it; click a wire and press <key>Delete</key> to remove
it; drag a box to move it. <gui>Wheel</gui> zooms about the pointer. <gui>Drag the
background</gui> to pan. <key>f</key> fits all nodes into view;
<keyseq><key>Ctrl</key><key>0</key></keyseq> resets the view. <gui>tidy</gui>, the button
beside <gui>fit</gui> in the editor, lays the graph out in topological columns and frames
it. The orange dot marks the output node; click <gui>set as output</gui> to move it.</p>
</section>
<section id="knowing">
<title>Worth knowing</title>
<list>
<item><p>The graph is evaluated as an ordered list, so a node may only read ones
<em>above</em> it. A wire drawn right to left re-sorts the list silently so the graph
stays valid. A cycle is unrepresentable: the connection is refused rather than created,
and the line beneath the form says why — a loop, or a node asked to read itself.</p></item>
<item><p>A derived channel <em>is</em> a channel. It appears in the signal panel, can be
plotted, measured, exported, and used as a source for another math channel.</p></item>
<item><p>The math runs on the <em>stored</em> samples — decimated and averaged — so a
cutoff you type in Hz means the rate the filter will actually run at. Change the window
across a decimation boundary and the coefficients are rebuilt for the new rate.</p></item>
<item><p>A source may only name an <em>acquired</em> channel. The stream's timestamp
signal is not one: a graph sourced from it validates and then reads NaN for ever, so the
dropdown does not offer it.</p></item>
<item><p>The output defaults to the <em>last</em> node, and a node added to a linear
graph becomes the output. Choosing an earlier node explicitly is respected.</p></item>
<item><p>Every numeric field starts at its own identity value — gain 1, offset 0 — not
at the bottom of its range.</p></item>
</list>
</section>
<section id="why">
<title>Why an operation sometimes does nothing</title>
<p>A graph's <em>output</em> is the node whose value becomes the channel, and it is drawn
as the orange dot. A new graph is built source, then operation, and the last node added
becomes the output — but a graph whose output is left on the source is valid and simply
passes its input through, which is the usual reason a gain or a filter appears to have no
effect. <gui>Set as output</gui> moves the dot.</p>
<p><gui>Apply</gui> is insensitive while the graph is incomplete. A line beneath the form
says why: an input that is not connected, no output chosen, or a <em>channel</em> node
naming a signal the scope is not acquiring. Only the channels being acquired are offered,
because a source that is not acquired reads as no data for ever — and a graph that reads
NaN is harder to diagnose than one that would not save.</p>
<p>The filters are real filters. A math channel's samples are produced as the stream
arrives, once per stored slot, so a low pass carries the history a filter needs. A cutoff
typed in hertz is against the <em>stored</em> rate, which is the wire rate until a wide
window makes the buffer decimate — the status line's rate is that number, and the filters
are rebuilt whenever it changes.</p>
<figure>
<title>The math channel editor: nodes as boxes on a canvas, the orange dot marking the
output, and the selected node's parameters beside it.</title>
<media type="image" mime="image/png" src="figures/math-editor.png" width="938" height="420">
<p>A sine source wired through a gain into a filter, with the output on the last
node.</p>
</media>
</figure>
</section>
<section id="example">
<title>Example</title>
<p><code>[sine] → [gain x2]</code> gives a channel reading <code>y=[-2, 2]</code>. A
100 Hz low pass on the same source passes 50 Hz at 0.97 and 2 kHz at 0.002; a 1 kHz band
pass with Q=2 passes 200 Hz at 0.05, 1 kHz at 0.95 and 4 kHz at 0.05. Each derived channel
must hold exactly as many samples as the shared clock has time slots.</p>
</section>
</page>
+97
View File
@@ -0,0 +1,97 @@
<page xmlns="http://projectmallard.org/1.0/"
type="topic" style="task"
id="measurements">
<info>
<link type="guide" xref="index#analysis"/>
<link type="seealso" xref="cursors"/>
<desc>A measurement is a statistic of the visible window of one signal, with optional
on-plot marks and a per-measurement statistics line.</desc>
<credit type="author copyright">
<name>Martino Ferrari</name>
</credit>
</info>
<title>Measurements</title>
<p>A measurement is a statistic of the <em>visible window</em> of one signal, recomputed as
that window moves. The pane lists them with their values; each can also draw its result on
the plot.</p>
<section id="reach">
<title>How to reach it</title>
<steps>
<item>
<p>Press the measurements button in the toolbar, or <key>m</key>.</p>
</item>
<item>
<p>The pane's <gui>+</gui> adds one; clicking a row opens the dialog where the signal,
the type, the threshold share and the on-plot marks are set, with <gui>Save</gui> and
<gui>Delete</gui>.</p>
</item>
</steps>
</section>
<section id="knowing">
<title>Worth knowing</title>
<list>
<item><p>The types are average, top, bottom, min, max, peak to peak, amplitude, pulse
duration, period and frequency. <em>top</em> and <em>bottom</em> are the means of the
upper and lower halves, which is why a unit sine reads 0.637 (2/π) rather than 1.</p></item>
<item><p>The on-plot marks come in two kinds and the mix is the reading: a <em>level</em>
mark is a ruler at a value, a <em>time</em> mark is a marker at a position. An average
draws one level mark; a max draws a level mark <em>and</em> a time mark at the first
sample that reached it.</p></item>
<item><p>Marks cannot be dragged. A measurement mark says what the measurement measured,
so it is created unselectable — as is the trigger marker.</p></item>
<item><p>A pass is throttled to a cadence rather than run every frame, so a ruler over a
live view is <em>stepped</em>. The number and the ruler come from the same pass, so they
always agree.</p></item>
<item><p>An invalid pass keeps its <em>level</em> marks and drops its <em>time</em>
marks. A level is a property of the signal and outlives the window; a position inside
the window does not. That is what stops the rulers blinking off at a mode change.</p></item>
<item><p>A crossing is counted with <em>hysteresis</em>, not on a bare level: the
thresholds are a tenth of the window's peak to peak either side of the share, and an
edge is counted only once the signal has passed the far one. Period, frequency and pulse
duration all depend on it. The mid level is exactly where a waveform moves fastest, so
any interferer faster than the fundamental recrosses a single level several times per
real crossing — a 5 Hz sine with a 500 Hz tone at a tenth of the amplitude reads 39 Hz
without the band and 5 Hz with it. The band holds to an interferer a third of the
waveform's own size.</p></item>
<item><p>The band is clamped to stay inside the data, which is what keeps a threshold set
near an extreme usable: a fixed band there would sit above every sample and the
measurement would read nothing. A share near an extreme still leaves the band little
room, so it rejects less — which is one reason period and frequency offer no share to
set.</p></item>
<item><p>A window with too few crossings reads <code>invalid</code> / <code>--</code>
rather than a plausible invented number. Frequency over a window holding one edge is the
usual case.</p></item>
<item><p><gui>frequency</gui> is reported in Hz and <gui>period</gui>/<gui>pulse
duration</gui> in seconds; every other type carries the channel's own unit. A value is
written with a <em>metric prefix</em> so its digits sit in <code>1..999</code> —
<code>636.62 mV</code>, not <code>0.64 V</code> — including when the stream declares no
unit at all, which is always the case for a math channel: there the prefix goes tight
against the number (<code>636.62m</code>), since two decimals of an unprefixed small
value is <code>0.00</code>.</p></item>
<item><p>Cost is bounded by reading the summary pyramid the producer already maintains,
not the raw window. The values are still exact: a bucket's sum is a sum.</p></item>
<item><p><gui>Statistics</gui> adds a second line to every measurement showing
<code>n</code>, the mean, the standard deviation and the range. Each observation is one
<em>recompute</em> of the pane, so <code>n</code> counts windows measured, not samples —
a mean frequency over many windows is what that claims to be. <gui>Reset</gui> clears
every accumulator and starts fresh. A change of display window, a CONFIG or a
measurement edit also resets; a pause, a resume, a zoom, a pan and the toggle itself do
not, so turning the line off to read the plot and back on again keeps the history.</p></item>
</list>
</section>
<section id="example">
<title>Example</title>
<p>On a 1 Hz ±1 sine: <code>average=0</code>, <code>top=0.636620</code>,
<code>bottom=-0.636620</code>, <code>max=1</code>, <code>min=-1</code>,
<code>peak to peak=2</code>, <code>amplitude=1</code>, <code>pulse duration=0.5 s</code>
(duty 0.5), <code>period=1 s</code>, <code>frequency=1 Hz</code>. Over a 2 M-sample window
those two passes read about 0.1% of the samples.</p>
</section>
</page>
+104
View File
@@ -0,0 +1,104 @@
<page xmlns="http://projectmallard.org/1.0/"
type="topic" style="task"
id="navigate">
<info>
<link type="guide" xref="index#viewing"/>
<link type="seealso" xref="window"/>
<desc>Panning, zooming and fitting the plot with the mouse and the keyboard, and what a
pinned view refuses to do.</desc>
<credit type="author copyright">
<name>Martino Ferrari</name>
</credit>
</info>
<title>Navigating the plot</title>
<p>The plot pans and zooms with the mouse and with the keyboard. Every gesture acts on the
row the pointer is over, and because the rows share one time axis, an x change moves all of
them.</p>
<section id="reach">
<title>How to reach it</title>
<terms>
<item>
<title>Wheel</title>
<p><gui>Wheel</gui> zooms both axes · <gui>Shift</gui>+<gui>Wheel</gui> zooms x only ·
<gui>Ctrl</gui>+<gui>Wheel</gui> zooms y only.</p>
</item>
<item>
<title>Drag</title>
<p><gui>Drag</gui> pans · <gui>Shift</gui>+<gui>Drag</gui> is a box zoom on both axes ·
<gui>Ctrl</gui>+<gui>Drag</gui> on x only · <gui>Alt</gui>+<gui>Drag</gui> on values
only. The area being chosen is framed while you drag it, so you can see what the
release will apply.</p>
</item>
<item>
<title>Keyboard</title>
<p><key>+</key> and <key>-</key> zoom both axes; <key>Ctrl</key> with them restricts
to x, <key>Alt</key> to y. <gui>Arrow keys</gui> pan, <key>f</key> fits,
<key>Escape</key> cancels a gesture in progress.</p>
</item>
</terms>
<p>The full table is in the shortcut overview (<keyseq><key>Ctrl</key><key>/</key></keyseq>).</p>
</section>
<section id="knowing">
<title>Worth knowing</title>
<list>
<item><p>The keyboard zoom's modifiers are <em>not</em> the wheel's, and that is
deliberate: on common layouts <key>+</key> <em>is</em> <keyseq><key>Shift</key><key>=</key></keyseq>,
so a bare zoom-in already arrives with Shift set and Shift could not mean anything.
Ctrl/Alt mirror the box-zoom drags instead.</p></item>
<item><p>A <em>pinned</em> view is navigable within its own data but cannot leave it.
While an event is held, a capture is filling, or the view is paused, a zoom <em>in</em>
works normally — "not outside the event" is not "not readable" — but a pan or a zoom out
is clamped to the data.</p></item>
<item><p>Nothing you do on the plot changes the <em>time window</em> — only the header
control does. A zoom or a pan moves the view inside the window; <key>f</key> (and
<gui>Fit X axis</gui>) is the <em>undo</em> of a zoom, so a 2 s window zoomed into reads
2 s again. The window is more than the axis: it is also the trigger's event span and how
the samples are stored, so a wheel notch that redefined it would re-plan the buffer and
drop your history.</p></item>
<item><p>The axis does not scroll — the <em>data</em> does. A live axis is a fixed
<code>[-window .. 0]</code> with the newest sample at 0, so the labels stand still and
the trace rolls under them. The cursors stand still with the labels (they are positions
on screen, so a cursor you placed stays where you put it, over newer data each frame),
while a measurement's marks travel with the sample they measured and leave the view when
it does.</p></item>
<item><p>A live view is clamped to the window you asked for, not to what the ring
happens to hold. That is why a buffer that has only just started filling leaves its
left-hand side empty instead of squeezing the window down to the first few
samples.</p></item>
<item><p>Zooming hard into a held capture is exact. The renderer finds the visible
samples by binary search over the real timestamps, never over a nominal sample grid, so
a capture whose clock wandered does not drift away from its axis as you zoom.</p></item>
<item><p>A trace holding far more samples than the plot has pixels is drawn <em>one
pixel column at a time</em>: each column is a single vertical line from the lowest to
the highest value that falls in it. Drawn as a polyline instead, such a window puts
three or four near-vertical lines in every column at slightly different sub-pixel
positions, and how they overlap beats against the pixel grid — which reads as bands of
light and dark that <em>crawl</em> as you pan. One line per column removes the beat, and
it is also much faster: a 1 MS/s view costs a couple of thousand line segments a frame
instead of a couple of hundred thousand.</p></item>
<item><p>Those columns are <em>graded by brightness</em>, the way a digital-phosphor
scope reads: bright where the signal spent its time, faint where it only passed through.
A column's highest and lowest value are the same whether the samples in it are spread
evenly or piled at its two ends, so the grading is what tells the two apart — the flat
parts of a square wave come out solid and the edges between them faint. It is read from
a finer division of each column rather than from the line that is drawn, and it costs
about a millisecond a frame.</p></item>
</list>
</section>
<section id="example">
<title>Example</title>
<p>Pause a live view, then press <keyseq><key>Ctrl</key><key>+</key></keyseq>: x narrows
and y does not move. Press <keyseq><key>Alt</key><key>+</key></keyseq>: y narrows and x
does not. Eight doublings into a held 2 s capture, the trace still reaches both margins —
the zoom probe reports <code>gap_px</code> and <code>end_px</code> within a pixel or two
at every step.</p>
</section>
</page>
+169
View File
@@ -0,0 +1,169 @@
<page xmlns="http://projectmallard.org/1.0/"
type="topic" style="task"
id="overview">
<info>
<link type="guide" xref="index#main"/>
<desc>What udpscope plots, the four parts of the window, and one pass through the
application from connecting to saving.</desc>
<credit type="author copyright">
<name>Martino Ferrari</name>
</credit>
</info>
<title>Overview</title>
<section id="what">
<title>What udpscope is</title>
<p>udpscope plots a live UDPStreamer source. One window holds up to eight rows of plots,
each with up to four traces, and every row shares one time axis, so the whole figure
scrolls together. Acquisition runs on its own thread and writes a ring buffer sized from
<cmd>--buffer-mb</cmd>; the drawing thread reads whatever that ring holds when a frame
starts, which is why a trace never makes the window stutter.</p>
<p>The stream is the clock. Every interval you read on screen is measured between
timestamps the source sent, never your machine's, so a stalled source freezes the view
instead of scrolling it — and a source whose clock steps still gets the window you asked
for.</p>
<p>The time axis is <em>relative to now</em>. While streaming it is a fixed
<code>[-window .. 0]</code> with the newest sample at 0, so x reads as an age:
<code>-1.5s</code> is a sample a second and a half old. In trigger mode 0 is the event's
edge instead, and a paused view keeps the ages it was frozen with. The source's own clock
is still there when you need it — an export writes it as a second column.</p>
<figure>
<title>The udpscope window on a live source: the header bar above the plots, the side
panel on the right, and the status bar along the bottom.</title>
<media type="image" mime="image/png" src="figures/main-window.png" width="1280" height="800">
<p>One figure of three rows with four signals enabled, the Measure tab open in the
side panel, and the status bar reading <gui>live</gui>.</p>
</media>
</figure>
</section>
<section id="reach">
<title>How to reach it</title>
<p>Start the app with a source
(<cmd>udpscope --server 127.0.0.1 --port 8000</cmd>), or start it with no
<cmd>--server</cmd> and pick one from the chooser, which lists the sessions you have
opened before. A unicast source serves one client at a time: if a second viewer connects,
they take turns and the status bar says so. To watch one source from several instances,
use <cmd>--multicast</cmd> with the group and <cmd>--interface</cmd> with the local
address.</p>
<p>The signals arrive <em>disabled</em>. Nothing is acquired until you turn one on in the
signal list, and enabling is what starts acquisition — so a freshly opened scope with an
idle side panel is working correctly, not failing to connect.</p>
<note style="tip">
<p>See <link xref="source">Choosing a source</link> for the chooser and the multicast
options in full.</p>
</note>
</section>
<section id="window">
<title>The window</title>
<terms>
<item>
<title>The header bar</title>
<p>Left to right: the time window (<gui>2 s</gui>), the trigger and the pause, the
events, cursors and side-panel switches, and the menu, which holds
<gui>Fit to data</gui>, the <gui>Layout editor</gui>, <gui>Export</gui>,
<gui>Keyboard shortcuts</gui>, <gui>Help</gui> and <gui>About</gui>.</p>
</item>
<item>
<title>The side panel</title>
<p>Two tabs. <gui>Measure</gui> holds the cursors readout and the measurements
derived from it; <gui>Signals</gui> holds the trigger and the channel list, where a
math channel is a row of that one list under a <gui>MATH</gui> heading whose
<gui>+</gui> creates one. One row per channel: its colour, an enable switch, its name
and its unit. Channels are dragged from here onto a plot row.</p>
</item>
<item>
<title>The plot</title>
<p>Up to eight rows, each with its own y axis and up to four traces, all on one shared
time axis. Right-click a row for its own menu (fit, add a signal, remove a trace).</p>
</item>
<item>
<title>The status bar</title>
<p>Directly above the plot's bottom edge — the state word, the stored sample rate and
the window, and the ⓘ beside it for packets, gaps and the buffer's own figures. The
second screenshot below is what a held capture looks like: <gui>event held</gui>, the
axis pinned to the event, and the trigger marker at the edge.</p>
</item>
</terms>
<figure>
<title>A held capture: the trigger has latched, the state reads
<gui>event held</gui>, and the axis is pinned to the event rather than to the live
edge.</title>
<media type="image" mime="image/png" src="figures/trigger-capture.png" width="1280" height="800">
<p>The trigger marker sits at x = 0 and the pre-trigger share of the window is drawn
to its left.</p>
</media>
</figure>
</section>
<section id="first">
<title>The first five minutes</title>
<steps>
<item>
<p><em>Connect.</em> With no <cmd>--server</cmd>, pick a stored session or type a host
and port.</p>
</item>
<item>
<p><em>Enable a signal</em> in the signal list. The plot stays empty for the first
fraction of a second while the first window fills — the buffer holds only what has
arrived.</p>
</item>
<item>
<p><em>Drag it onto a row.</em> Drop a second signal on another row, or the same
signal again for a second view of it. The colour is per signal, so it is the same one
wherever it appears.</p>
</item>
<item>
<p><em>Set the window</em> with the control in the header bar: type a number, pick a
unit, or step a decade. The trigger's event window is a share of this span, so a
window change moves it.</p>
</item>
<item>
<p><em>Pause</em> (<key>space</key>) to look at a still picture; the producer keeps
filling the buffer behind it, and resuming shows what arrived meanwhile rather than a
hole.</p>
</item>
<item>
<p><em>Arm the trigger</em> (<key>a</key>) and wait for a latch: the view freezes on
one event, and you can zoom in on it. Disarming mid-capture keeps what the capture
got.</p>
</item>
<item>
<p><em>Measure it</em> — the measurements pane computes a statistic of the visible
window, and <gui>show on plot</gui> marks it as a ruler or a pair of markers.</p>
</item>
<item>
<p><em>Save the data</em> with <gui>Export visible range</gui>, which writes the
plotted signals over the window at the stored rate to <file>.npy</file>,
<file>.csv</file> or <file>.mf3</file> (MDF 3.30).</p>
</item>
</steps>
</section>
<section id="knowing">
<title>Worth knowing</title>
<list>
<item><p>A signal nobody enables costs nothing — no buffer, no socket work, no
measurements.</p></item>
<item><p>The window remembers itself. Rows, traces, the time window, calibration, which
signals were enabled, the math channels with their graphs, and the measurements are
written per host and port, and reloaded the next time you open that source.</p></item>
<item><p>A very wide window on a fast stream is drawn from averaged groups of samples,
so the rate in the status bar falls: that is the anti-alias filter, not a fault —
striding would fold a fast signal back as a slow one, which looks entirely plausible on
screen.</p></item>
<item><p>Every instrument the app computed — the trigger marker, the measurement marks
— is locked to the pointer's reach. The cursors are yours to drag; those are not.</p></item>
</list>
</section>
</page>
+61
View File
@@ -0,0 +1,61 @@
<page xmlns="http://projectmallard.org/1.0/"
type="topic" style="task"
id="pause">
<info>
<link type="guide" xref="index#viewing"/>
<link type="seealso" xref="trigger"/>
<desc>Pausing freezes the display without stopping the stream, and the status bar says how
long you have been paused.</desc>
<credit type="author copyright">
<name>Martino Ferrari</name>
</credit>
</info>
<title>Pause and play</title>
<p>Pause freezes the <em>display</em>, not the acquisition. The producer keeps filling the
ring at full rate; the view binds an immutable snapshot of the window that was on screen,
and the axis is pinned to it.</p>
<section id="reach">
<title>How to reach it</title>
<steps>
<item>
<p>Press the pause button in the toolbar, or <key>Space</key>.</p>
</item>
<item>
<p>Press it again to resume, which rebinds the live ring and puts the window back on
the newest data.</p>
</item>
</steps>
</section>
<section id="knowing">
<title>Worth knowing</title>
<list>
<item><p>Nothing is lost while paused. Because the producer never stops, resuming shows
current data with no hole in the history behind it.</p></item>
<item><p>The axis is pinned to the <em>snapshot's own stamps</em>, not to the window that
was requested. A window reaching back past the oldest retained sample yields a shorter
snapshot, and the pinned axis is that shorter span — so you see a narrower view rather
than an empty margin.</p></item>
<item><p>A paused view is pinned exactly as a held trigger event is: zoom in freely, but
a pan or a zoom out is clamped to the snapshot's data.</p></item>
<item><p>Pausing is the easiest way to measure a layout or read a cursor precisely,
because nothing under the pointer moves between frames.</p></item>
<item><p>The status bar says how long you have been paused — wall time, since the stream
itself never stopped. It is there so a frozen picture cannot be mistaken for a live one
that went quiet.</p></item>
</list>
</section>
<section id="example">
<title>Example</title>
<p>Pause with a 2 s window over a ring holding 1.4 s: the frozen axis reads
<code>[-1.40 .. 0.00]</code> rather than <code>[-2.00 .. 0.00]</code> with 0.6 s of blank.
The pause probe reports <code>ring_produced</code> still growing across the pause and
<code>event_size</code> non-zero exactly while paused.</p>
</section>
</page>
+93
View File
@@ -0,0 +1,93 @@
<page xmlns="http://projectmallard.org/1.0/"
type="topic" style="task"
id="sessions">
<info>
<link type="guide" xref="index#output"/>
<link type="seealso" xref="source"/>
<desc>Per-source configuration files: where they live, what they persist, and how the
chooser and the Configurations dialog use them.</desc>
<credit type="author copyright">
<name>Martino Ferrari</name>
</credit>
</info>
<title>Sessions and settings</title>
<p>udpscope stores named sessions — <em>configurations</em> — per source, so reopening a
source gives you the scope you closed, and one source can hold several arrangements for
different experiments. There is no global preferences file: the session <em>is</em> the
settings.</p>
<section id="reach">
<title>How to reach it</title>
<p>Nothing to open — the file is written as you work. Its path is in the About dialog, and
the directory is <file>$XDG_CONFIG_HOME/udpscope</file>, or <file>~/.config/udpscope</file>
when that is unset. Each source has a directory <file>&lt;host&gt;_&lt;port&gt;/</file>
holding one <file>&lt;name&gt;.toml</file> per configuration and an <file>active</file>
file naming the one it is on.</p>
</section>
<section id="configs">
<title>Configurations</title>
<p><gui>Configurations…</gui> in the menu lists the running source's configurations: click
a row to open it, <gui>×</gui> to delete it, or type a name and a note and press
<gui>Save this session</gui> to keep the current arrangement under a new one. Opening a
configuration saves the current session first, so switching never loses what it switches
from.</p>
<p>The startup chooser (start with no <cmd>--server</cmd>) lists one row per configuration,
with its note and its settings, so two arrangements of one source are told apart before
they open.</p>
<p>A configuration also carries the settings that can only be decided when the buffers are
allocated: the <gui>buffer size</gui> (<cmd>--buffer-mb</cmd>), relative time, and the
packet capture's ring size. Opening a configuration from the chooser applies all three;
switching inside a running app applies the rest and says in the status line that these take
effect on the next start.</p>
<p>The chooser's form sets the first two for a new host, and its pencil button edits them
on an existing configuration — along with the name and the note. Renaming a configuration,
or editing the address it points at, moves it: the old file is removed rather than left
behind.</p>
</section>
<section id="knowing">
<title>Worth knowing</title>
<list>
<item><p>What a configuration persists: the time window, the rows and each row's slot
signals, the calibration, the trace colours, which signals were enabled, the math
channels with their graphs, the measurements pane — and the startup settings: buffer
size, relative time and the packet capture's ring size.</p></item>
<item><p>Signal identity is the <em>name</em> throughout, because a new CONFIG renumbers
the channels.</p></item>
<item><p>A file written by a run against a source whose streamer was down would name no
signals at all, so such a run does <em>not</em> write: a configured session is never
overwritten by a run that configured nothing. (The chooser hides such a file for the same
reason.)</p></item>
<item><p>The multicast group and the interface live <em>inside</em> the file, because its
name cannot carry them. A file from an older build has neither key and loads as unicast
on the default route.</p></item>
<item><p>The chooser lists only configurations that name at least one signal, which is
what keeps it the sessions you configured rather than every port you have ever tested
against.</p></item>
<item><p>Deleting a configuration is a supported way to start over; the chooser's
<gui>×</gui> and the dialog's do exactly that.</p></item>
<item><p>A session from before configurations existed is imported once as the
configuration <code>default</code>, and the old file is kept beside the directory as
<file>&lt;host&gt;_&lt;port&gt;.toml.imported</file>.</p></item>
</list>
</section>
<section id="example">
<title>Example</title>
<p>Connect to <code>127.0.0.1:8000</code>, drop three signals on row 1, set a 2 s window,
and save the session as <code>bench</code> with a note.
<file>~/.config/udpscope/127.0.0.1_8000/bench.toml</file> now holds
<code>window_s=2 comment="…"</code> with
<code>signals = ["sine", "ramp", "noise", ""]</code>, and the next run reports
<code>layout loaded from …/127.0.0.1_8000/bench.toml</code> with those signals already
acquired.</p>
</section>
</page>
+70
View File
@@ -0,0 +1,70 @@
<page xmlns="http://projectmallard.org/1.0/"
type="topic" style="task"
id="signals">
<info>
<link type="guide" xref="index#main"/>
<link type="seealso" xref="layout"/>
<desc>How the source's CONFIG becomes channels, what enabling a signal starts, and how a
signal's scale and offset are calibrated.</desc>
<credit type="author copyright">
<name>Martino Ferrari</name>
</credit>
</info>
<title>Signals and calibration</title>
<p>The source's CONFIG message describes its signals: a name, a unit and a type. udpscope
turns each enabled signal into a <em>channel</em> — a ring of values beside the shared time
ring — and a channel is what a trace plots. Calibration is a scale and an offset applied at
ingest.</p>
<section id="reach">
<title>How to reach it</title>
<steps>
<item>
<p>Open the side panel (<key>s</key>), which lists every signal with a switch.</p>
</item>
<item>
<p>The <gui>Signal list</gui> item in the overflow menu opens the editor where the
unit and the calibration live.</p>
</item>
</steps>
</section>
<section id="knowing">
<title>Worth knowing</title>
<list>
<item><p>Signals start disabled, and enabling one is what starts acquiring it. A trigger
on a signal nobody acquires can never fire, so arming a trigger enables its signal for
you.</p></item>
<item><p><gui>Calibrate</gui> in a signal's row menu opens the gain and offset it is
<em>currently</em> drawn through, so a trace already scaled by a gain of 2 shows 2 —
pressing <gui>Apply</gui> confirms it rather than resetting it. A signal nobody
calibrated shows the identity, 1 and 0.</p></item>
<item><p>Calibration is applied <em>at ingest</em>, so changing it re-plans the buffer
and drops the history. So does enabling or disabling a signal, and so does a new
CONFIG. An empty plot for a second after one of those is the ring refilling, not a
fault.</p></item>
<item><p>The stream's first signal is usually its <em>timestamp</em>, and that one is
never acquired. It cannot be plotted, measured or used as a math source, and udpscope
refuses to put it in a row rather than binding a trace that would read nothing for
ever.</p></item>
<item><p>A signal's colour follows the <em>signal</em>, not the slot, so the same signal
on two rows is one colour and the side panel's chip identifies it unambiguously. A
signal on no row gets a grey chip rather than none, so the panel's rows keep their
height.</p></item>
<item><p><em>Click a coloured chip to change that signal's colour.</em> A grey chip
names no trace, so it is not clickable. The colour is saved with the session;
<gui>Default colour</gui> in the signal's ⋯ menu gives the palette entry back.</p></item>
</list>
</section>
<section id="example">
<title>Example</title>
<p>A source declaring <code>time</code>, <code>sine</code>, <code>ramp</code> and
<code>noise</code> yields three plottable signals. Enable all three and the exit dump
reads <code>3 signals, 3 channels</code>; <code>time</code> is not among them.</p>
</section>
</page>
+75
View File
@@ -0,0 +1,75 @@
<page xmlns="http://projectmallard.org/1.0/"
type="topic" style="task"
id="source">
<info>
<link type="guide" xref="index#main"/>
<link type="seealso" xref="sessions"/>
<desc>How udpscope is pointed at a source: the command line, the startup chooser, and
opening an exported waveform instead of a live stream.</desc>
<credit type="author copyright">
<name>Martino Ferrari</name>
</credit>
</info>
<title>Choosing a source</title>
<p>A source is a host and a port, optionally reached through a multicast group on a named
interface. Pass it on the command line, or let udpscope ask.</p>
<section id="reach">
<title>How to reach it</title>
<steps>
<item>
<p>Run with <cmd>--server</cmd> and the app connects straight away.</p>
</item>
<item>
<p>Run <em>without</em> <cmd>--server</cmd> and a chooser appears first: one row per
stored <em>configuration</em> (<code>host:port · name</code>, its note, and the
settings it carries), newest first, plus a <gui>New host…</gui> form that takes the
address <em>and</em> the configuration it belongs to — its name and note, the
<gui>buffer size</gui>, and whether time is relative or absolute. A row's pencil
button opens the same form on that configuration, pre-filled, and changes to its name
or address move it (the old file is deleted).</p>
</item>
<item>
<p>The second page, <gui>Waveforms</gui>, opens an <em>exported</em> run instead of a
live source: the files opened before (newest first; one that has since been deleted is
marked <em>missing</em>) and an <gui>Open…</gui> button for a new one. The same file
can be opened straight from the command line with <cmd>--open FILE</cmd>. A waveform
is a source that never arrives — no socket, no acquisition, no starvation notice — and
every instrument works on it: cursors, measurements, math channels and another export.
Only <file>.npy</file> files are read.</p>
</item>
</steps>
<p>The chooser runs before any buffer is allocated and before any socket is opened, so
declining it costs nothing.</p>
</section>
<section id="knowing">
<title>Worth knowing</title>
<list>
<item><p>The list is the sessions that <em>named a signal</em>. A session whose streamer
was down stores empty slot names and is filtered out, which is what keeps the list the
sources you actually configured rather than every port you have ever typed.</p></item>
<item><p>A stored row reconnects the way it was reached. The multicast group and the
interface are written inside the session file, because the file's name
(<file>host_port.toml</file>) cannot carry them.</p></item>
<item><p><cmd>--multicast</cmd> still needs <cmd>--server</cmd>: the CONFIG message is
fetched from the server, not from the group.</p></item>
<item><p>The <gui>×</gui> beside a row deletes that session file. Nothing else is
removed.</p></item>
</list>
</section>
<section id="example">
<title>Example</title>
<p><cmd>udpscope</cmd> with two stored sessions lists
<code>127.0.0.1:8000 plots=1 signals=3</code> and
<code>10.0.0.5:8001 plots=2 signals=4 multicast=239.1.2.3 interface=10.0.0.9</code>.
Activating the first reports <code>[sources] picked 127.0.0.1:8000</code> and continues
into the scope with that session's layout already loaded.</p>
</section>
</page>
+72
View File
@@ -0,0 +1,72 @@
<page xmlns="http://projectmallard.org/1.0/"
type="topic" style="task"
id="status">
<info>
<link type="guide" xref="index#output"/>
<desc>The status bar's two tiers, the activity badge, the fixed state words, and the ⓘ
detail popover.</desc>
<credit type="author copyright">
<name>Martino Ferrari</name>
</credit>
</info>
<title>Status bar and diagnostics</title>
<p>The status bar is two tiers. The bar itself is one glance line — the state, the rate and
the window — and everything else is behind the ⓘ button at its right-hand end.</p>
<section id="reach">
<title>How to reach it</title>
<p>The bar is always visible under the plot. Click <gui>ⓘ</gui> for the detail popover.</p>
</section>
<section id="knowing">
<title>Worth knowing</title>
<list>
<item><p>The badge is one glyph in four colours: green <em>alternating</em> lit and dim
means data is arriving, and a steady colour means it is not. A badge that never blinks is
not a working badge. Of the two steady colours, <em>grey</em> is no stream at all or a
pipeline you stopped (the tooltip says which), and <em>yellow</em> is a source that
answered but has sent no configuration.</p></item>
<item><p>The state field is one word from a fixed set: <code>connecting</code>,
<code>live</code>, <code>paused</code>, <code>armed</code>, <code>capturing</code>,
<code>event held</code>, <code>disarmed</code>. Two states add one field each: in trigger
mode, how long ago the last event was; while paused, how long you have been paused.</p></item>
<item><p><code>connecting</code> means there is no <em>signal list</em> yet — either no
session or a session with no configuration behind it. Every other word claims a signal
list, so until one arrives there is nothing for them to describe: nothing can be enabled
and no data is owed.</p></item>
<item><p>The paused figure is <em>wall</em> time, not stream time — the producer never
stops while paused, so it answers "how long have I been looking at this picture".</p></item>
<item><p>Nothing on the bar may change width with the stream, because the bar sits
directly above the plot and a wider label would relayout it. That is why the rate
collapses to one figure or a range rather than listing per channel, and why an elapsed
time steps to a coarser unit instead of growing digits.</p></item>
<item><p><code>gaps</code> counts packets the receiver knows it missed. It is the number
that matters: a non-zero count means samples are absent from the buffer, not merely
late.</p></item>
<item><p><code>reconnects</code> counts sessions re-established with the source. Over
unicast a climbing count usually means a second client is taking the source's single
session.</p></item>
<item><p>The starvation notice — <em>connected, but no data is arriving</em> — is that
same situation seen from the other side: the signal list is here, so something took the
stream away. A source that has sent <em>no</em> configuration gets a different notice,
<em>trying to connect</em>, because nothing was taken away — pointing the app at a port
where nothing answers is that case, and the usual cause is a streamer that is not running
or is serving another port.</p></item>
<item><p><code>stored</code> versus <code>wire</code> in the popover is the decimation.
They differ exactly when the window is too long to store at full rate.</p></item>
</list>
</section>
<section id="example">
<title>Example</title>
<p>The bar reads <code>● live · 10 kS/s · 2 s window</code>. Behind ⓘ:
<code>Stream connected, configured / packets 717 · frames 716 · gaps 0 / 3 signals · 3
channels</code>, <code>Buffer 200.0 MB · 1677 s span · time x1 / stored 10 kS/s (wire
10 kS/s)</code>, <code>Plot 1 3 traces · 195000 samples</code>,
<code>Reconnects 0</code>.</p>
</section>
</page>
+155
View File
@@ -0,0 +1,155 @@
<page xmlns="http://projectmallard.org/1.0/"
type="topic" style="task"
id="trigger">
<info>
<link type="guide" xref="index#viewing"/>
<link type="seealso" xref="pause"/>
<desc>The trigger's four shapes, arming and hold-off, the event cache with its replay,
compare and accumulate, and searching the buffer for a condition.</desc>
<credit type="author copyright">
<name>Martino Ferrari</name>
</credit>
</info>
<title>Trigger</title>
<p>The trigger watches one signal for a condition and captures a window around it. The
<gui>Shape</gui> dropdown chooses what counts as an event; the arming, hold-off, pre/post
window and Normal/Single mode are the same for all four shapes. The captured window is
frozen on screen until you re-arm.</p>
<section id="reach">
<title>How to reach it</title>
<steps>
<item>
<p>The trigger button in the toolbar (<key>t</key>) reveals the trigger panel: the
signal, the level, the shape, the edge (rising, falling, either), the mode (Normal or
Single) and the pre-trigger share.</p>
</item>
<item>
<p><gui>Arm</gui> starts watching; <key>a</key> arms and disarms from the keyboard.
When the pane is locked with its <gui>[edit]</gui> button — so that it states what the
trigger is set to instead of showing the controls — <gui>Arm/Disarm</gui> is in its
heading, and the trigger can be armed or disarmed without unfolding the panel. It is
offered only while trigger mode is on, because a trigger that is off has no engine to
arm.</p>
</item>
<item>
<p><gui>Search the buffer</gui> is at the foot of the same panel.</p>
</item>
</steps>
<figure>
<title>A held capture: <gui>event held</gui> in the status bar, the axis pinned to the
event, and the trigger marker at x = 0.</title>
<media type="image" mime="image/png" src="figures/trigger-capture.png" width="1280" height="800">
<p>The pre-trigger share is drawn to the left of the marker.</p>
</media>
</figure>
</section>
<section id="shapes">
<title>The four shapes</title>
<terms>
<item>
<title>Edge</title>
<p>A crossing of the level in the chosen direction, the classic behaviour.</p>
</item>
<item>
<title>Pulse width</title>
<p>A crossing and its return, qualified by how long the excursion lasted: less than,
greater than, within or outside a pair of widths; the event is timed at the closing
edge.</p>
</item>
<item>
<title>Window</title>
<p>The signal entering or leaving a band between two levels; the panel keeps the two
levels at least one increment apart, because a zero-width band is not a band: the
condition becomes an exact float-equality test against the level, which an ordinary
signal never satisfies and a signal sitting at the level satisfies on every
transition.</p>
</item>
<item>
<title>Dropout</title>
<p>The absence of a crossing for longer than a timeout; one event per outage, re-arms
when a crossing returns.</p>
</item>
</terms>
<p>The shape's own rows are the only ones shown; the rest are hidden, so the panel never
offers a pulse width to a window trigger.</p>
</section>
<section id="cache">
<title>The event cache, replay and compare</title>
<p><em>Every triggered acquisition is kept.</em> Latches are written to the event cache
(<file>~/.cache/udpscope/events/&lt;source&gt;/</file>) as a <file>.npy</file> of the
event, a <file>.txt</file> of what it was, and a <file>.bmp</file> picture drawn from the
samples. The <gui>Events</gui> pane on the left — opened from the toolbar's
event-history button, which appears while recording is enabled, and collapsed until you
ask for it — lists the last 32 newest first (time, duration, signals, rate), each row with
its own picture beside the numbers.</p>
<p>Clicking a row <em>replays</em> it: the stored event is loaded exactly like an exported
file, <em>without changing the layout</em> — the rows, the colours, the window, the
cursors and the measurements stay where they are and read the stored data instead. A
replay leaves trigger mode, because a file has no edge to wait for, and the pane stays
open. Drag the pane's edge to resize it. <gui>Live</gui> goes back to the stream; the
status bar names the file until you do.</p>
<p>Select two or more events in the list and the two buttons under the header act on them:
<gui>Compare</gui> puts two events in two plots side by side, keeping the layout you have
— one plot per event, the axes synchronised, so zooming a row in one zooms the same row in
the other — and <gui>Accumulate</gui> overlays up to four of them in the same traces,
translucent, so where they agree the trace is brighter. Your own traces stay where they
are in both. They need events with the same signals and the same length. A single click
selects, a <em>double</em> click replays.</p>
<p><gui>Record events</gui>, in the trigger panel with the trigger's other options, is
what decides whether anything is kept. It is <em>off by default</em> — nothing is written
to disk until you ask — and with it off the history button goes with it.</p>
</section>
<section id="search">
<title>Search the buffer</title>
<p><gui>Search the buffer</gui> runs the same condition over the whole retained ring and
lists every place it holds. The arrows step through hits; each step pauses on the hit,
centred, exactly as the Pause button does. Up to 16 hits in the visible window are marked
on every row; when there are more the panel says how many and suggests zooming in. The
search uses the trigger's own condition, set in one place, so a hit is by construction an
instant the trigger would have fired.</p>
</section>
<section id="knowing">
<title>Worth knowing</title>
<list>
<item><p>Arming <em>enables</em> the signal if it was off, because a trigger on a signal
nobody acquires can never fire.</p></item>
<item><p>The pre-trigger share is a share of the <em>display window</em>, not of the
ring. A 2 s window at 50% captures 1 s before the edge and 1 s after.</p></item>
<item><p>There are four display states, and the status bar names each: <gui>armed</gui>
(watching, nothing drawn yet), <gui>capturing</gui> (the edge was accepted and the post
window is filling <em>in view</em>, with the marker at x = 0), <gui>event held</gui>
(the frozen capture) and <gui>disarmed</gui>. A held capture is never quietly replaced
by live data.</p></item>
<item><p>The first edge after arming is only accepted once the history reaches back a
full pre-trigger window, so a capture may legitimately be shorter than you asked for.
The axis is pinned to what the capture actually got.</p></item>
<item><p>Disarming <em>during</em> a capture publishes what the capture got to, so the
picture on screen at that moment stays on screen. No event fired, so the event count
does not move.</p></item>
<item><p>The trigger reads the <em>stored</em> signal — decimated and averaged — not the
wire signal. A spike narrower than one decimation group is filtered away rather than
triggering on data the buffer does not contain.</p></item>
</list>
</section>
<section id="example">
<title>Example</title>
<p>A 1 Hz sine, level 0, rising, 2 s window at 50%, Single. The capture spans
<code>[-1.000 .. 1.000]</code> with <code>n=20001</code> samples at 10 kS/s — the span
times the real rate, plus one — and the marker sits at x = 0 throughout.</p>
</section>
</page>
+66
View File
@@ -0,0 +1,66 @@
<page xmlns="http://projectmallard.org/1.0/"
type="topic" style="task"
id="window">
<info>
<link type="guide" xref="index#viewing"/>
<link type="seealso" xref="navigate"/>
<desc>The time window control: how to set it, and why a long window is decimated rather
than refused.</desc>
<credit type="author copyright">
<name>Martino Ferrari</name>
</credit>
</info>
<title>Time window</title>
<p>The window is how much time the plot shows. Every row shares one window: the figure has
a single x link group, so the rows cannot drift apart. The trigger's event span follows it,
and the pre-trigger share is a share of <em>this</em> window.</p>
<section id="reach">
<title>How to reach it</title>
<steps>
<item>
<p>The header bar's window button opens a popover: type a number, pick a unit under
it, or step a decade with the arrows.</p>
</item>
<item>
<p>The button's own label is a reading of the current window in a natural unit,
width-locked so a change of window cannot widen the header bar.</p>
</item>
</steps>
</section>
<section id="knowing">
<title>Worth knowing</title>
<list>
<item><p>A window too long to store at the wire rate is <em>decimated, not refused</em>.
The shared clock takes a power-of-two stride, and each group of samples is
<em>averaged</em> rather than strided, so a tone above the stored Nyquist is attenuated
instead of aliasing to a false low frequency.</p></item>
<item><p>The stamp stored for a group is the group's <em>mean</em>, which is where the
averaging puts the value. Stamping at the end of a group would shift every trace half a
stored sample late.</p></item>
<item><p>Moving the window across a decimation boundary rebuilds the buffer and drops
the history. Walking it inside one decade is free.</p></item>
<item><p><em>This control is the only thing that changes the window.</em> A zoom, a pan
or a fit moves the view <em>inside</em> it and leaves it alone — which is the point of
the previous bullet: a wheel notch that redefined the window could re-plan the buffer
and throw your samples away.</p></item>
<item><p>The trigger sees the <em>stored</em> (filtered) signal, not the wire signal,
because an edge found on a sample that is not in the buffer could not be cut out of
it.</p></item>
</list>
</section>
<section id="example">
<title>Example</title>
<p>1 MS/s with 200 MB of buffer. A 2 s window stores every sample
(<code>time x1</code>). Widen it to 600 s and the stride becomes <code>x16</code>: 625 S/s
stored, anti-aliased at 312.5 Hz. A 2 kHz sine seen through that window reads
<code>y=[-0.059, 0.079]</code> — attenuated to about 6%, not folded down to a
full-amplitude 125 Hz one.</p>
</section>
</page>