Files
udpscope/help/C/org.udpscope.app/trigger.page
T
Martino Ferrari c1460a6977 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`.
2026-10-14 16:49:30 +02:00

156 lines
7.8 KiB
Plaintext

<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>