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:
1 parent
620473a5be
commit
c1460a6977
47 files changed
+1910
-1056
No files matched your search
@@ -1,79 +0,0 @@
|
||||
<b>What udpscope is</b>
|
||||
|
||||
udpscope plots a live UDPStreamer source. One window holds up to 8 rows of plots, each with up
|
||||
to 4 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 <tt>--buffer-mb</tt>;
|
||||
the drawing thread reads whatever that ring holds when a frame starts, which is why a trace
|
||||
never makes the window stutter.
|
||||
|
||||
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.
|
||||
|
||||
The time axis is <i>relative to now</i>. While streaming it is a fixed <tt>[-window .. 0]</tt>
|
||||
with the newest sample at 0, so x reads as an age: <tt>-1.5s</tt> 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.
|
||||
|
||||
<b>How to reach it</b>
|
||||
|
||||
Start the app with a source (<tt>udpscope --server 127.0.0.1 --port 8000</tt>), or start it with
|
||||
no <tt>--server</tt> 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
|
||||
<tt>--multicast</tt> with the group and <tt>--interface</tt> with the local address.
|
||||
|
||||
The signals arrive <i>disabled</i>. 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.
|
||||
|
||||
<b>The window</b>
|
||||
|
||||
• <b>The header bar</b> -- left to right: the time window (<tt>2 s</tt>), the trigger and the
|
||||
pause, the events, cursors and side-panel switches, and the menu, which holds Fit to data, the
|
||||
layout editor, Export, Keyboard shortcuts, Help and About.
|
||||
|
||||
• <b>The side panel</b> -- two tabs. <b>Measure</b> holds the cursors readout and the
|
||||
measurements derived from it; <b>Signals</b> holds the trigger and the channel list, where a
|
||||
math channel is a row of that one list under a <tt>MATH</tt> heading whose <tt>+</tt> 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.
|
||||
|
||||
• <b>The plot</b> -- up to 8 rows, each with its own y axis and up to 4 traces, all on one
|
||||
shared time axis. Right-click a row for its own menu (fit, add a signal, remove a trace).
|
||||
|
||||
• <b>The status bar</b>, 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 above is what a held capture looks like: <tt>event held</tt>,
|
||||
the axis pinned to the event, and the trigger marker at the edge.
|
||||
|
||||
<b>The first five minutes</b>
|
||||
|
||||
1. <b>Connect.</b> With no <tt>--server</tt>, pick a stored session or type a host and port.
|
||||
2. <b>Enable a signal</b> 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.
|
||||
3. <b>Drag it onto a row.</b> 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.
|
||||
4. <b>Set the window</b> 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.
|
||||
5. <b>Pause</b> (<tt>space</tt>) to look at a still picture; the producer keeps filling the
|
||||
buffer behind it, and resuming shows what arrived meanwhile rather than a hole.
|
||||
6. <b>Arm the trigger</b> (<tt>a</tt>) 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.
|
||||
7. <b>Measure it</b> -- the measurements pane computes a statistic of the visible window, and
|
||||
<b>show on plot</b> marks it as a ruler or a pair of markers.
|
||||
8. <b>Save the data</b> with Export visible range, which writes the plotted signals over the
|
||||
window at the stored rate to <tt>.npy</tt>, <tt>.csv</tt> or <tt>.mf3</tt> (MDF 3.30).
|
||||
|
||||
<b>Worth knowing</b>
|
||||
|
||||
• A signal nobody enables costs nothing -- no buffer, no socket work, no measurements.
|
||||
• 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.
|
||||
• 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.
|
||||
• 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.
|
||||
@@ -1,42 +0,0 @@
|
||||
<b>Choosing a source</b>
|
||||
|
||||
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.
|
||||
|
||||
<b>How to reach it</b>
|
||||
|
||||
Run with <tt>--server</tt> and the app connects straight away. Run <i>without</i>
|
||||
<tt>--server</tt> and a chooser appears first: one row per stored <i>configuration</i>
|
||||
(<tt>host:port · name</tt>, its note, and the settings it carries), newest first, plus a
|
||||
<b>New host…</b> form that takes the address <i>and</i> the configuration it belongs to — its name
|
||||
and note, the <b>buffer size</b>, 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).
|
||||
|
||||
The second page, <b>Waveforms</b>, opens an <i>exported</i> run instead of a live source: the files
|
||||
opened before (newest first; one that has since been deleted is marked <i>missing</i>) and an
|
||||
<b>Open…</b> button for a new one. The same file can be opened straight from the command line with
|
||||
<tt>--open FILE</tt>. 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 <tt>.npy</tt> files are read. form for one it has not seen. The chooser runs before any
|
||||
buffer is allocated and before any socket is opened, so declining it costs nothing.
|
||||
|
||||
<b>Worth knowing</b>
|
||||
|
||||
• The list is the sessions that <i>named a signal</i>. 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.
|
||||
• 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 (<tt>host_port.toml</tt>) cannot
|
||||
carry them.
|
||||
• <tt>--multicast</tt> still needs <tt>--server</tt>: the CONFIG message is fetched from the
|
||||
server, not from the group.
|
||||
• The <tt>×</tt> beside a row deletes that session file. Nothing else is removed.
|
||||
|
||||
<b>Example</b>
|
||||
|
||||
<tt>udpscope</tt> with two stored sessions lists
|
||||
<tt>127.0.0.1:8000 plots=1 signals=3</tt> and
|
||||
<tt>10.0.0.5:8001 plots=2 signals=4 multicast=239.1.2.3 interface=10.0.0.9</tt>.
|
||||
Activating the first reports <tt>[sources] picked 127.0.0.1:8000</tt> and continues into
|
||||
the scope with that session's layout already loaded.
|
||||
@@ -1,36 +0,0 @@
|
||||
<b>Signals and calibration</b>
|
||||
|
||||
The source's CONFIG message describes its signals: a name, a unit and a type. udpscope turns
|
||||
each enabled signal into a <i>channel</i> — 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.
|
||||
|
||||
<b>How to reach it</b>
|
||||
|
||||
The side panel (<tt>s</tt>) lists every signal with a switch. The <b>Signal list</b> item in
|
||||
the overflow menu opens the editor where the unit and the calibration live.
|
||||
|
||||
<b>Worth knowing</b>
|
||||
|
||||
• 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.
|
||||
• <b>Calibrate</b> in a signal's row menu opens the gain and offset it is <i>currently</i> drawn
|
||||
through, so a trace already scaled by a gain of 2 shows 2 — pressing <i>Apply</i> confirms it
|
||||
rather than resetting it. A signal nobody calibrated shows the identity, 1 and 0.
|
||||
• Calibration is applied <i>at ingest</i>, 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.
|
||||
• The stream's first signal is usually its <i>timestamp</i>, 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.
|
||||
• A signal's colour follows the <i>signal</i>, 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.
|
||||
• <b>Click a coloured chip to change that signal's colour.</b> A grey chip names no trace, so
|
||||
it is not clickable. The colour is saved with the session; <b>Default colour</b> in the
|
||||
signal's ⋯ menu gives the palette entry back.
|
||||
|
||||
<b>Example</b>
|
||||
|
||||
A source declaring <tt>time</tt>, <tt>sine</tt>, <tt>ramp</tt> and <tt>noise</tt> yields three
|
||||
plottable signals. Enable all three and the exit dump reads <tt>3 signals, 3 channels</tt>;
|
||||
<tt>time</tt> is not among them.
|
||||
@@ -1,39 +0,0 @@
|
||||
<b>Rows, layout and drag & drop</b>
|
||||
|
||||
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.
|
||||
|
||||
<b>How to reach it</b>
|
||||
|
||||
Drag a signal's chip out of the side panel (<tt>s</tt>) and drop it on a row to add a trace.
|
||||
Press <tt>l</tt>, or pick <b>Layout editor</b> from the overflow menu, to add and remove rows:
|
||||
a <tt>×</tt> appears at the top left of each row and a <tt>+</tt> on each boundary between
|
||||
rows, including above the first and below the last. The editor's own button in the top-right
|
||||
corner leaves it.
|
||||
|
||||
<b>Worth knowing</b>
|
||||
|
||||
• A plot's own <b>right-click menu</b> (the same compact rows as the app's menu) holds the
|
||||
y-axis scale, the two fits, and <b>Remove signal</b>, which swaps the menu to that plot's traces
|
||||
plus <i>All traces on this plot</i> — with a <b>Back</b> row to return. That is how a trace comes
|
||||
off a row without going through the layout editor.
|
||||
• A drop <i>adds</i> 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.
|
||||
• Inserting a row moves the rows below it <i>with their contents</i>. 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.
|
||||
• A trace is remembered by the signal's <i>name</i>, not by its channel number, because a new
|
||||
CONFIG or an enable renumbers the channels and an index would silently re-point the trace.
|
||||
• 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.
|
||||
|
||||
<b>Example</b>
|
||||
|
||||
With three traces on row 1, pressing <tt>l</tt> shows 3 buttons (one <tt>×</tt>, two
|
||||
<tt>+</tt>). Clicking the <tt>+</tt> above row 1 gives 2 rows and 5 buttons, with the three
|
||||
traces now on row 2 — the layout probe reports exactly this as
|
||||
<tt>rows=2 buttons=5 traces=[0:-|1:sine,ramp,noise]</tt>.
|
||||
@@ -1,33 +0,0 @@
|
||||
<b>Time window</b>
|
||||
|
||||
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 <i>this</i> window.
|
||||
|
||||
<b>How to reach it</b>
|
||||
|
||||
The header bar's window button opens a popover: type a number, pick a unit under it, or step a
|
||||
decade with the arrows. 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.
|
||||
|
||||
<b>Worth knowing</b>
|
||||
|
||||
• A window too long to store at the wire rate is <i>decimated, not refused</i>. The shared clock
|
||||
takes a power-of-two stride, and each group of samples is <b>averaged</b> rather than strided,
|
||||
so a tone above the stored Nyquist is attenuated instead of aliasing to a false low frequency.
|
||||
• The stamp stored for a group is the group's <i>mean</i>, which is where the averaging puts the
|
||||
value. Stamping at the end of a group would shift every trace half a stored sample late.
|
||||
• Moving the window across a decimation boundary rebuilds the buffer and drops the history.
|
||||
Walking it inside one decade is free.
|
||||
• <b>This control is the only thing that changes the window.</b> A zoom, a pan or a fit moves the
|
||||
view <i>inside</i> 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.
|
||||
• The trigger sees the <i>stored</i> (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.
|
||||
|
||||
<b>Example</b>
|
||||
|
||||
1 MS/s with 200 MB of buffer. A 2 s window stores every sample (<tt>time x1</tt>). Widen it to
|
||||
600 s and the stride becomes <tt>x16</tt>: 625 S/s stored, anti-aliased at 312.5 Hz. A 2 kHz
|
||||
sine seen through that window reads <tt>y=[-0.059, 0.079]</tt> — attenuated to about 6%, not
|
||||
folded down to a full-amplitude 125 Hz one.
|
||||
@@ -1,59 +0,0 @@
|
||||
<b>Navigating the plot</b>
|
||||
|
||||
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.
|
||||
|
||||
<b>How to reach it</b>
|
||||
|
||||
<b>Wheel</b> zooms both axes · <b>Shift+wheel</b> zooms x only · <b>Ctrl+wheel</b> zooms y only.
|
||||
<b>Drag</b> pans · <b>Shift+drag</b> is a box zoom on both axes · <b>Ctrl+drag</b> on x only ·
|
||||
<b>Alt+drag</b> on values only.
|
||||
The area being chosen is framed while you drag it, so you can see what the release will apply.
|
||||
<b>+</b> and <b>-</b> zoom both axes; <b>Ctrl</b> with them restricts to x, <b>Alt</b> to y.
|
||||
<b>Arrow keys</b> pan, <b>f</b> fits, <b>Escape</b> cancels a gesture in progress.
|
||||
The full table is in the shortcut overview (<tt>Ctrl+/</tt>).
|
||||
|
||||
<b>Worth knowing</b>
|
||||
|
||||
• The keyboard zoom's modifiers are <i>not</i> the wheel's, and that is deliberate: on common
|
||||
layouts <tt>+</tt> <i>is</i> shift+<tt>=</tt>, so a bare zoom-in already arrives with Shift
|
||||
set and Shift could not mean anything. Ctrl/Alt mirror the box-zoom drags instead.
|
||||
• A <i>pinned</i> 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 <i>in</i> works normally — "not
|
||||
outside the event" is not "not readable" — but a pan or a zoom out is clamped to the data.
|
||||
• Nothing you do on the plot changes the <i>time window</i> — only the header control does. A
|
||||
zoom or a pan moves the view inside the window; <b>f</b> (and Fit X axis) is the <i>undo</i> 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.
|
||||
• The axis does not scroll — the <i>data</i> does. A live axis is a fixed <tt>[-window .. 0]</tt>
|
||||
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.
|
||||
• 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.
|
||||
• 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.
|
||||
• A trace holding far more samples than the plot has pixels is drawn <i>one pixel column at a
|
||||
time</i>: 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 <i>crawl</i> 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.
|
||||
• Those columns are <i>graded by brightness</i>, 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.
|
||||
|
||||
<b>Example</b>
|
||||
|
||||
Pause a live view, then press <tt>Ctrl++</tt>: x narrows and y does not move. Press
|
||||
<tt>Alt++</tt>: y narrows and x does not. Eight doublings into a held 2 s capture, the trace
|
||||
still reaches both margins — the zoom probe reports <tt>gap_px</tt> and <tt>end_px</tt> within
|
||||
a pixel or two at every step.
|
||||
@@ -1,31 +0,0 @@
|
||||
<b>Pause and play</b>
|
||||
|
||||
Pause freezes the <i>display</i>, 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.
|
||||
|
||||
<b>How to reach it</b>
|
||||
|
||||
The pause button in the toolbar, or <tt>Space</tt>. Press it again to resume, which rebinds the
|
||||
live ring and puts the window back on the newest data.
|
||||
|
||||
<b>Worth knowing</b>
|
||||
|
||||
• Nothing is lost while paused. Because the producer never stops, resuming shows current data
|
||||
with no hole in the history behind it.
|
||||
• The axis is pinned to the <i>snapshot's own stamps</i>, 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.
|
||||
• 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.
|
||||
• Pausing is the easiest way to measure a layout or read a cursor precisely, because nothing
|
||||
under the pointer moves between frames.
|
||||
• 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.
|
||||
|
||||
<b>Example</b>
|
||||
|
||||
Pause with a 2 s window over a ring holding 1.4 s: the frozen axis reads
|
||||
<tt>[-1.40 .. 0.00]</tt> rather than <tt>[-2.00 .. 0.00]</tt> with 0.6 s of blank. The pause
|
||||
probe reports <tt>ring_produced</tt> still growing across the pause and
|
||||
<tt>event_size</tt> non-zero exactly while paused.
|
||||
@@ -1,80 +0,0 @@
|
||||
<b>Trigger</b>
|
||||
|
||||
The trigger watches one signal for a condition and captures a window around it. The
|
||||
<b>Shape</b> 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.
|
||||
|
||||
<b>How to reach it</b>
|
||||
|
||||
The trigger button in the toolbar (<tt>t</tt>) 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. <b>Arm</b> starts watching; <tt>a</tt> arms and disarms from the keyboard. When the pane is
|
||||
locked with its <tt>[edit]</tt> button — so that it states what the trigger is set to instead of showing
|
||||
the controls — <b>Arm/Disarm</b> 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.
|
||||
<b>Search the buffer</b> is at the foot of the same panel.
|
||||
|
||||
<b>Worth knowing</b>
|
||||
|
||||
• <b>Every triggered acquisition is kept.</b> Latches are written to the event cache
|
||||
(<tt>~/.cache/udpscope/events/<source>/</tt>) as a <tt>.npy</tt> of the event, a
|
||||
<tt>.txt</tt> of what it was, and a <tt>.bmp</tt> picture drawn from the samples. The
|
||||
<b>Events</b> 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. Clicking a row
|
||||
<i>replays</i> it: the stored event is loaded exactly like an exported file, <b>without changing the
|
||||
layout</b> -- 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. <b>Live</b> goes back to the
|
||||
stream; the status bar names the file until you do.
|
||||
Select two or more events in the list and the two buttons under the header act on them:
|
||||
<b>Compare</b> 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
|
||||
<b>Accumulate</b> 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 <i>double</i> click replays.
|
||||
<b>Record events</b>, in the trigger panel with the trigger's other options, is what decides whether
|
||||
anything is kept. It is <b>off by default</b> -- nothing is written to disk until you ask -- and
|
||||
with it off the history button goes with it.
|
||||
|
||||
• Arming <i>enables</i> the signal if it was off, because a trigger on a signal nobody acquires
|
||||
can never fire.
|
||||
• The pre-trigger share is a share of the <i>display window</i>, not of the ring. A 2 s window
|
||||
at 50% captures 1 s before the edge and 1 s after.
|
||||
• There are four display states, and the status bar names each: <tt>armed</tt> (watching,
|
||||
nothing drawn yet), <tt>capturing</tt> (the edge was accepted and the post window is filling
|
||||
<i>in view</i>, with the marker at x = 0), <tt>event held</tt> (the frozen capture) and
|
||||
<tt>disarmed</tt>. A held capture is never quietly replaced by live data.
|
||||
• 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.
|
||||
• Disarming <i>during</i> 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.
|
||||
• The trigger reads the <b>stored</b> 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.
|
||||
• The four shapes: <b>edge</b> — a crossing of the level in the chosen direction, the classic
|
||||
behaviour. <b>Pulse width</b> — 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. <b>Window</b> — 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.
|
||||
<b>Dropout</b> —
|
||||
the absence of a crossing for longer than a timeout; one event per outage, re-arms when a
|
||||
crossing returns.
|
||||
• 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.
|
||||
• <b>Search the buffer</b> 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.
|
||||
|
||||
<b>Example</b>
|
||||
|
||||
A 1 Hz sine, level 0, rising, 2 s window at 50%, Single. The capture spans
|
||||
<tt>[-1.000 .. 1.000]</tt> with <tt>n=20001</tt> samples at 10 kS/s — the span times the real
|
||||
rate, plus one — and the marker sits at x = 0 throughout.
|
||||
@@ -1,50 +0,0 @@
|
||||
<b>Cursors</b>
|
||||
|
||||
Cursors read values off the traces. There are two kinds: one <i>shared vertical pair</i> that
|
||||
every row carries at the same time, and <i>horizontal rulers</i>, one pair per row, that belong
|
||||
to that row alone.
|
||||
|
||||
<b>How to reach it</b>
|
||||
|
||||
The cursors button in the toolbar, or <tt>c</tt>. The pane below the plot shows the readings and
|
||||
owns a <b>Reset</b> button. Drag a marker to move it. The mode dropdown offers four modes:
|
||||
<tt>vertical</tt> (the shared time pair), <tt>horizontal</tt> (the per-row rulers),
|
||||
<tt>both</tt>, and <tt>mix</tt>.
|
||||
|
||||
<b>Worth knowing</b>
|
||||
|
||||
• The vertical pair is <i>one</i> time for the whole figure, so moving it on any row moves it on
|
||||
all of them. That is what makes <tt>dt</tt> meaningful across rows.
|
||||
• A ruler belongs to its row and nothing shares it, so two rows' rulers are independent.
|
||||
• <tt>mix</tt> is the reading mode: one line per <i>bound</i> 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.
|
||||
• A value is the <i>nearest stored sample's</i>, 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.
|
||||
• Every reading carries a <i>metric prefix</i> so the digits sit in <tt>1..999</tt>:
|
||||
<tt>10.52 mV</tt>, not <tt>0.01 V</tt>. Two decimals is all an instrument landing on an
|
||||
<tt>f32</tt> sample can claim, so without the prefix a small value would read <tt>0.00</tt>
|
||||
and the reading would be gone. A stream that declares no unit gets the prefix anyway, written
|
||||
tight against the number (<tt>585.24m</tt>) — the space separates the number from its unit,
|
||||
and a spaced <tt>m</tt> with nothing after it would read as metres.
|
||||
• A marker past the newest sample reads <tt>--</tt>. With only one marker over data the row
|
||||
reads <tt>partial</tt>: a difference between a number and nothing is not a number.
|
||||
• In <tt>mix</tt> 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 <i>on that sample</i>, 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.
|
||||
• The time row exists only in the modes that show the vertical pair. <tt>horizontal</tt> has no
|
||||
time row, by design.
|
||||
• A cursor <i>stays where you put it</i> while a live view rolls under it, which is what a bench
|
||||
scope does: its <tt>T0</tt> is an age, so a marker left at <tt>-1.5s</tt> 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.
|
||||
|
||||
<b>Example</b>
|
||||
|
||||
Three traces in <tt>mix</tt> mode with both markers over data: 3 lines and 6 crossing marks,
|
||||
each line reading like <tt>sine 0.31 V -0.84 V -1.15 V</tt>. Move the second marker past the
|
||||
newest sample and the third column reads <tt>--</tt>.
|
||||
@@ -1,60 +0,0 @@
|
||||
<b>Measurements</b>
|
||||
|
||||
A measurement is a statistic of the <i>visible window</i> of one signal, recomputed as that
|
||||
window moves. The pane lists them with their values; each can also draw its result on the plot.
|
||||
|
||||
<b>How to reach it</b>
|
||||
|
||||
The measurements button in the toolbar, or <tt>m</tt>. The pane's <tt>+</tt> adds one; clicking
|
||||
a row opens the dialog where the signal, the type, the threshold share and the on-plot marks are
|
||||
set, with <b>Save</b> and <b>Delete</b>.
|
||||
|
||||
<b>Worth knowing</b>
|
||||
|
||||
• The types are average, top, bottom, min, max, peak to peak, amplitude, pulse duration, period
|
||||
and frequency. <i>top</i> and <i>bottom</i> are the means of the upper and lower halves, which
|
||||
is why a unit sine reads 0.637 (2/π) rather than 1.
|
||||
• The on-plot marks come in two kinds and the mix is the reading: a <i>level</i> mark is a ruler
|
||||
at a value, a <i>time</i> mark is a marker at a position. An average draws one level mark; a
|
||||
max draws a level mark <i>and</i> a time mark at the first sample that reached it.
|
||||
• Marks cannot be dragged. A measurement mark says what the measurement measured, so it is
|
||||
created unselectable — as is the trigger marker.
|
||||
• A pass is throttled to a cadence rather than run every frame, so a ruler over a live view is
|
||||
<i>stepped</i>. The number and the ruler come from the same pass, so they always agree.
|
||||
• An invalid pass keeps its <i>level</i> marks and drops its <i>time</i> 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.
|
||||
• A crossing is counted with <i>hysteresis</i>, 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.
|
||||
• 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.
|
||||
• A window with too few crossings reads <tt>invalid</tt> / <tt>--</tt> rather than a plausible
|
||||
invented number. Frequency over a window holding one edge is the usual case.
|
||||
• <tt>frequency</tt> is reported in Hz and <tt>period</tt>/<tt>pulse duration</tt> in seconds;
|
||||
every other type carries the channel's own unit. A value is written with a <i>metric prefix</i>
|
||||
so its digits sit in <tt>1..999</tt> — <tt>636.62 mV</tt>, not <tt>0.64 V</tt> — 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 (<tt>636.62m</tt>), since two decimals of an unprefixed
|
||||
small value is <tt>0.00</tt>.
|
||||
• 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.
|
||||
• <b>Statistics</b> adds a second line to every measurement showing <tt>n</tt>, the mean, the
|
||||
standard deviation and the range. Each observation is one <i>recompute</i> of the pane, so
|
||||
<tt>n</tt> counts windows measured, not samples — a mean frequency over many windows is what
|
||||
that claims to be. <b>Reset</b> 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.
|
||||
|
||||
<b>Example</b>
|
||||
|
||||
On a 1 Hz ±1 sine: <tt>average=0</tt>, <tt>top=0.636620</tt>, <tt>bottom=-0.636620</tt>,
|
||||
<tt>max=1</tt>, <tt>min=-1</tt>, <tt>peak to peak=2</tt>, <tt>amplitude=1</tt>,
|
||||
<tt>pulse duration=0.5 s</tt> (duty 0.5), <tt>period=1 s</tt>, <tt>frequency=1 Hz</tt>. Over a
|
||||
2 M-sample window those two passes read about 0.1% of the samples.
|
||||
@@ -1,61 +0,0 @@
|
||||
<b>Math channels</b>
|
||||
|
||||
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.
|
||||
|
||||
<b>How to reach it</b>
|
||||
|
||||
The math button in the toolbar, or <tt>g</tt>. The pane's <tt>+</tt> 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. Inside the editor, <i>add node</i> is a menu button whose popover picks the op, so the
|
||||
node arrives as the operation you asked for.
|
||||
|
||||
<b>Graph gestures:</b> <b>drag</b> 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 <tt>Delete</tt> to remove
|
||||
it; drag a box to move it. <b>Wheel</b> zooms about the pointer. <b>Drag the background</b> to
|
||||
pan. <tt>f</tt> fits all nodes into view; <tt>Ctrl+0</tt> resets the view. <i>tidy</i>, the button
|
||||
beside <i>fit</i> in the editor, lays the graph out in topological columns and frames it. The orange
|
||||
dot marks the output node; click <i>set as output</i> to move it.
|
||||
|
||||
<b>Worth knowing</b>
|
||||
|
||||
• The graph is evaluated as an ordered list, so a node may only read ones <i>above</i> 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.
|
||||
• A derived channel <i>is</i> a channel. It appears in the signal panel, can be plotted,
|
||||
measured, exported, and used as a source for another math channel.
|
||||
• The math runs on the <b>stored</b> 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.
|
||||
• A source may only name an <i>acquired</i> 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.
|
||||
• The output defaults to the <i>last</i> node, and a node added to a linear graph becomes the
|
||||
output. Choosing an earlier node explicitly is respected.
|
||||
• Every numeric field starts at its own identity value — gain 1, offset 0 — not at the bottom
|
||||
of its range.
|
||||
|
||||
<b>Example</b>
|
||||
|
||||
<tt>[sine] → [gain x2]</tt> gives a channel reading <tt>y=[-2, 2]</tt>. 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.
|
||||
|
||||
<b>Why an operation sometimes does nothing</b>. A graph's <i>output</i> 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. <i>Set as output</i> moves the dot.
|
||||
|
||||
<b>Apply is insensitive while the graph is incomplete.</b> A line beneath the form says why:
|
||||
an input that is not connected, no output chosen, or a <i>channel</i> 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.
|
||||
|
||||
<b>The filters are real filters.</b> 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 <i>stored</i> 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.
|
||||
@@ -1,44 +0,0 @@
|
||||
<b>Saving and export</b>
|
||||
|
||||
Export writes the <i>visible range</i> of every plotted trace to a file: NumPy <tt>.npy</tt>,
|
||||
<tt>.csv</tt>, or <b>MDF 3.30</b> (<tt>.mf3</tt> or <tt>.mdf</tt>), chosen by the extension you
|
||||
type.
|
||||
|
||||
<b>How to reach it</b>
|
||||
|
||||
<b>Export visible range…</b> in the overflow menu, or <tt>Ctrl+E</tt>. A file chooser asks where;
|
||||
the format follows the path's extension.
|
||||
|
||||
<b>Worth knowing</b>
|
||||
|
||||
• 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.
|
||||
• The rows are the <b>stored samples</b>, 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.
|
||||
• 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 <i>that</i> — read the status bar's ⓘ popover
|
||||
for the figure before trusting a sample count.
|
||||
• Two time columns. <tt>time</tt> 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. <tt>abs_seconds</tt> 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.
|
||||
• An export taken <i>mid-capture</i> 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.
|
||||
• <b>MDF</b> 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 <b>master is <tt>abs_seconds</tt></b>,
|
||||
named <tt>time</tt> — the clock that is the same in every display mode — and the display axis
|
||||
goes out beside it as an ordinary channel, <tt>time_relative</tt>. Both are seconds; each signal
|
||||
carries the source's own unit. That is also what <b>ScryLab</b> reads: channel name → signal
|
||||
name, master → x axis, unit → the signal's unit.
|
||||
|
||||
<b>Example</b>
|
||||
|
||||
A 2 s window at 10 kS/s over 3 traces writes 20001 rows and 5 columns
|
||||
(<tt>time</tt>, <tt>abs_seconds</tt>, three signals). The <tt>.npy</tt> is
|
||||
<tt>64·⌈(10+len(header)+1)/64⌉ + rows·(16 + 4·cols)</tt> bytes. Read it back with
|
||||
<tt>pd.DataFrame(np.load(path))</tt>. The <tt>.mf3</tt> is bigger — a block per channel plus a
|
||||
comment — <tt>272 + comment + rows·record + 28 + channels·274 + 30</tt> bytes, with
|
||||
<tt>record = 16 + 4·cols</tt> and <tt>channels = cols + 2</tt> for the master and the relative
|
||||
axis. Read that one back with <tt>asammdf.MDF(path)</tt> or in ScryLab.
|
||||
@@ -1,60 +0,0 @@
|
||||
<b>Sessions and settings</b>
|
||||
|
||||
udpscope stores named sessions — <i>configurations</i> — 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 <i>is</i> the settings.
|
||||
|
||||
<b>How to reach it</b>
|
||||
|
||||
Nothing to open — the file is written as you work. Its path is in the About dialog, and the
|
||||
directory is <tt>$XDG_CONFIG_HOME/udpscope</tt>, or <tt>~/.config/udpscope</tt> when that is
|
||||
unset. Each source has a directory <tt><host>_<port>/</tt> holding one
|
||||
<tt><name>.toml</tt> per configuration and an <tt>active</tt> file naming the one it is on.
|
||||
|
||||
<b>Configurations</b>
|
||||
|
||||
<b>Configurations…</b> in the menu lists the running source's configurations: click a row to open
|
||||
it, <tt>×</tt> to delete it, or type a name and a note and press <i>Save this session</i> 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.
|
||||
|
||||
The startup chooser (start with no <tt>--server</tt>) lists one row per configuration, with its
|
||||
note and its settings, so two arrangements of one source are told apart before they open.
|
||||
|
||||
A configuration also carries the settings that can only be decided when the buffers are
|
||||
allocated: the <b>buffer size</b> (<tt>--buffer-mb</tt>), 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.
|
||||
|
||||
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.
|
||||
|
||||
<b>Worth knowing</b>
|
||||
|
||||
• What a configuration persists: the time window, the rows and each row's slot signals, the
|
||||
calibration, the trace colours, which signals are 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.
|
||||
• Signal identity is the <i>name</i> throughout, because a new CONFIG renumbers the channels.
|
||||
• A file written by a run against a source whose streamer was down would name no signals at all,
|
||||
so such a run does <b>not</b> write: a configured session is never overwritten by a run that
|
||||
configured nothing. (The chooser hides such a file for the same reason.)
|
||||
• The multicast group and the interface live <i>inside</i> 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.
|
||||
• 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.
|
||||
• Deleting a configuration is a supported way to start over; the chooser's <tt>×</tt> and the
|
||||
dialog's do exactly that.
|
||||
• A session from before configurations existed is imported once as the configuration
|
||||
<tt>default</tt>, and the old file is kept beside the directory as
|
||||
<tt><host>_<port>.toml.imported</tt>.
|
||||
|
||||
<b>Example</b>
|
||||
|
||||
Connect to <tt>127.0.0.1:8000</tt>, drop three signals on row 1, set a 2 s window, and save the
|
||||
session as <tt>bench</tt> with a note. <tt>~/.config/udpscope/127.0.0.1_8000/bench.toml</tt> now
|
||||
holds <tt>window_s=2 comment="…"</tt> with <tt>signals = ["sine", "ramp", "noise", ""]</tt>, and
|
||||
the next run reports <tt>layout loaded from …/127.0.0.1_8000/bench.toml</tt> with those signals
|
||||
already acquired.
|
||||
@@ -1,46 +0,0 @@
|
||||
<b>Status bar and diagnostics</b>
|
||||
|
||||
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.
|
||||
|
||||
<b>How to reach it</b>
|
||||
|
||||
The bar is always visible under the plot. Click <b>ⓘ</b> for the detail popover.
|
||||
|
||||
<b>Worth knowing</b>
|
||||
|
||||
• The badge is one glyph in four colours: green <i>alternating</i> 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, <i>grey</i> is no stream at all or a pipeline you stopped (the
|
||||
tooltip says which), and <i>yellow</i> is a source that answered but has sent no configuration.
|
||||
• The state field is one word from a fixed set: <tt>connecting</tt>, <tt>live</tt>,
|
||||
<tt>paused</tt>, <tt>armed</tt>, <tt>capturing</tt>, <tt>event held</tt>, <tt>disarmed</tt>. Two
|
||||
states add one field each: in trigger mode, how long ago the last event was; while paused, how
|
||||
long you have been paused.
|
||||
• <tt>connecting</tt> means there is no <i>signal list</i> 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.
|
||||
• The paused figure is <i>wall</i> time, not stream time — the producer never stops while paused,
|
||||
so it answers "how long have I been looking at this picture".
|
||||
• 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.
|
||||
• <tt>gaps</tt> 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.
|
||||
• <tt>reconnects</tt> counts sessions re-established with the source. Over unicast a climbing
|
||||
count usually means a second client is taking the source's single session.
|
||||
• The starvation notice — <i>connected, but no data is arriving</i> — 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 <i>no</i> configuration gets a different notice, <i>trying to connect</i>, 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.
|
||||
• <tt>stored</tt> versus <tt>wire</tt> in the popover is the decimation. They differ exactly
|
||||
when the window is too long to store at full rate.
|
||||
|
||||
<b>Example</b>
|
||||
|
||||
The bar reads <tt>● live · 10 kS/s · 2 s window</tt>. Behind ⓘ:
|
||||
<tt>Stream connected, configured / packets 717 · frames 716 · gaps 0 / 3 signals · 3 channels</tt>,
|
||||
<tt>Buffer 200.0 MB · 1677 s span · time x1 / stored 10 kS/s (wire 10 kS/s)</tt>,
|
||||
<tt>Plot 1 3 traces · 195000 samples</tt>, <tt>Reconnects 0</tt>.
|
||||
@@ -1,34 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
SPDX-License-Identifier: MIT
|
||||
|
||||
The help pages, compiled into the binary for the same reason the icons are: udpscope is
|
||||
run straight out of the build tree and has no install step, so a search path would have
|
||||
to be located at runtime and the failure mode of not locating it is a help window full
|
||||
of missing pages.
|
||||
-->
|
||||
<gresources>
|
||||
<gresource prefix="/org/udpscope/help">
|
||||
<file>01-overview.txt</file>
|
||||
<file>02-source.txt</file>
|
||||
<file>03-signals.txt</file>
|
||||
<file>04-layout.txt</file>
|
||||
<file>05-window.txt</file>
|
||||
<file>06-navigate.txt</file>
|
||||
<file>07-pause.txt</file>
|
||||
<file>08-trigger.txt</file>
|
||||
<file>09-cursors.txt</file>
|
||||
<file>10-measurements.txt</file>
|
||||
<file>11-math.txt</file>
|
||||
<file>12-export.txt</file>
|
||||
<file>13-sessions.txt</file>
|
||||
<file>14-status.txt</file>
|
||||
|
||||
<!-- The screenshots live with the user documentation (docs/); the resource aliases
|
||||
them under `img/` so a help page can name one by resource path. The path is relative
|
||||
to this manifest's directory. -->
|
||||
<file alias="img/main-window.png">../../docs/img/main-window.png</file>
|
||||
<file alias="img/trigger-capture.png">../../docs/img/trigger-capture.png</file>
|
||||
<file alias="img/math-editor.png">../../docs/img/math-editor.png</file>
|
||||
</gresource>
|
||||
</gresources>
|
||||
+76
-203
@@ -3,48 +3,34 @@
|
||||
|
||||
#include <adwaita.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
#include "build_info.hpp"
|
||||
#include "layout_store.hpp"
|
||||
#include "shortcuts.hpp"
|
||||
|
||||
// Both come from CMakeLists: the document identifier is derived from the desktop file's
|
||||
// basename (its desktop-file id), and the directory is where the Mallard set lives in the
|
||||
// source tree, which is what the self-check scans. Yelp resolves `help:<identifier>` to
|
||||
// `<datadir>/help/<lang>/<identifier>/` on its own (freedesktop Help System Specification),
|
||||
// so the identifier and the installed directory have to name the same set -- which is why
|
||||
// neither is written out again here.
|
||||
static_assert(sizeof(UDPSCOPE_HELP_NAME) > 1, "UDPSCOPE_HELP_NAME must come from the build");
|
||||
static_assert(sizeof(UDPSCOPE_HELP_DIR) > 1, "UDPSCOPE_HELP_DIR must come from the build");
|
||||
|
||||
namespace udpscope {
|
||||
namespace {
|
||||
|
||||
/// Nulled by each window's own close handler, so a second open cannot present a dead widget.
|
||||
/// One pointer per window, file-scope because there is one application.
|
||||
///
|
||||
/// The help window and the shortcut overview are `GtkWindow`s. The About dialog is an
|
||||
/// `AdwDialog`, which is **not** a `GtkWindow`: it has no `::close-request`, so it clears on
|
||||
/// `::closed` and closes with `adw_dialog_close()`.
|
||||
GtkWidget* g_help = nullptr;
|
||||
/// The help window is a `.ui` file too, so its builder, the label one page's markup goes into and the
|
||||
/// column its screenshots go into are all kept beside the window's own pointer. All four are cleared by
|
||||
/// the window's own close handler: the page switch reads them, and it cannot run after the window is
|
||||
/// gone.
|
||||
GtkBuilder* g_help_ui = nullptr;
|
||||
GtkWidget* g_help_body = nullptr;
|
||||
GtkWidget* g_help_shots = nullptr;
|
||||
/// Nulled by each dialog's own close handler, so a second open cannot present a dead widget.
|
||||
/// One pointer per dialog, file-scope because there is one application. The help manual is no
|
||||
/// longer one of these: it opens in a separate viewer process through the `help:` URI, and a
|
||||
/// process with no window of ours has nothing to null.
|
||||
AdwDialog* g_about = nullptr;
|
||||
/// The overview is the one window here built from a `.ui` file, so its builder is held too: the window
|
||||
/// belongs to the builder, and a *destroyed* `GtkWindow` cannot be presented again -- which is why the
|
||||
/// The overview is built from a `.ui` file, so its builder is held too: the window belongs to
|
||||
/// the builder, and a *destroyed* `GtkWindow` cannot be presented again -- which is why the
|
||||
/// builder is replaced on the next opening rather than reused.
|
||||
GtkWidget* g_shortcuts = nullptr;
|
||||
GtkBuilder* g_shortcuts_ui = nullptr;
|
||||
|
||||
/// The help window's own close handler, rather than the shared one: this window also owns the label a
|
||||
/// page is written into and the column its screenshots are built in, and a page switch that ran after
|
||||
/// the window was gone would read both.
|
||||
gboolean on_help_close(GtkWindow* window, gpointer) {
|
||||
(void)window;
|
||||
g_help = nullptr;
|
||||
g_help_body = nullptr;
|
||||
g_help_shots = nullptr;
|
||||
return FALSE; // let the default handler destroy it
|
||||
}
|
||||
|
||||
gboolean on_window_close(GtkWindow* window, gpointer slot) {
|
||||
(void)window;
|
||||
*static_cast<GtkWidget**>(slot) = nullptr;
|
||||
@@ -58,100 +44,27 @@ void on_dialog_closed(AdwDialog* dialog, gpointer slot) {
|
||||
*static_cast<AdwDialog**>(slot) = nullptr;
|
||||
}
|
||||
|
||||
struct HelpPage {
|
||||
const char* section; ///< the group heading in the sidebar
|
||||
const char* title; ///< the row's label and the page's heading
|
||||
const char* resource; ///< a path under /org/udpscope/help
|
||||
/// The screenshots shown above the text, as resource paths and a null terminator. A page
|
||||
/// with none is the common case and the reason this is a list rather than one field: the
|
||||
/// overview carries two, the window and a held capture.
|
||||
const char* const* images = nullptr;
|
||||
};
|
||||
|
||||
/// Adding a page is one .txt file, one <file> line in the manifest and one row here.
|
||||
/// The order here is the order the sidebar shows; `section` groups consecutive rows.
|
||||
/// How tall a page's screenshots are drawn. They are scaled to fit this *height* and centre
|
||||
/// themselves in it, so a 1500x950 capture is readable without a horizontal scrollbar.
|
||||
constexpr int kHelpShotHeight = 260;
|
||||
|
||||
/// The screenshots each page carries, in the order it shows them.
|
||||
constexpr const char* kOverviewShots[] = {"/org/udpscope/help/img/main-window.png",
|
||||
"/org/udpscope/help/img/trigger-capture.png",
|
||||
nullptr};
|
||||
constexpr const char* kTriggerShots[] = {"/org/udpscope/help/img/trigger-capture.png",
|
||||
nullptr};
|
||||
constexpr const char* kMathShots[] = {"/org/udpscope/help/img/math-editor.png", nullptr};
|
||||
|
||||
constexpr HelpPage kPages[] = {
|
||||
{"Getting started", "Overview", "/org/udpscope/help/01-overview.txt", kOverviewShots},
|
||||
{"Getting started", "Choosing a source", "/org/udpscope/help/02-source.txt"},
|
||||
{"Getting started", "Signals and calibration", "/org/udpscope/help/03-signals.txt"},
|
||||
{"Getting started", "Rows, layout and drag & drop", "/org/udpscope/help/04-layout.txt"},
|
||||
{"Viewing", "Time window", "/org/udpscope/help/05-window.txt"},
|
||||
{"Viewing", "Navigating the plot", "/org/udpscope/help/06-navigate.txt"},
|
||||
{"Viewing", "Pause and play", "/org/udpscope/help/07-pause.txt"},
|
||||
{"Viewing", "Trigger", "/org/udpscope/help/08-trigger.txt", kTriggerShots},
|
||||
{"Analysis", "Cursors", "/org/udpscope/help/09-cursors.txt"},
|
||||
{"Analysis", "Measurements", "/org/udpscope/help/10-measurements.txt"},
|
||||
{"Analysis", "Math channels", "/org/udpscope/help/11-math.txt", kMathShots},
|
||||
{"Output and state", "Saving and export", "/org/udpscope/help/12-export.txt"},
|
||||
{"Output and state", "Sessions and settings", "/org/udpscope/help/13-sessions.txt"},
|
||||
{"Output and state", "Status bar and diagnostics", "/org/udpscope/help/14-status.txt"},
|
||||
};
|
||||
constexpr int kPageCount = static_cast<int>(sizeof(kPages) / sizeof(kPages[0]));
|
||||
|
||||
/// The page's markup, or a one-line stand-in. `ok` reports whether the real body loaded
|
||||
/// *and* parsed: a missing resource and markup Pango rejects both render the stand-in,
|
||||
/// because a help window that fails to open is worse than one page that says so.
|
||||
std::string load_page(const HelpPage& page, bool* ok) {
|
||||
*ok = false;
|
||||
/// The launch is asynchronous and can fail -- no help viewer installed, no handler for the
|
||||
/// `help:` scheme, or a document no viewer can find -- and it runs in another process, so
|
||||
/// nothing raises a window of ours. Report the failure here rather than leaving a click that
|
||||
/// silently did nothing; the detail names the identifier a viewer looks for.
|
||||
void on_help_launched(GObject* source, GAsyncResult* result, gpointer user) {
|
||||
GtkWindow* parent = user != nullptr ? GTK_WINDOW(user) : nullptr;
|
||||
GError* error = nullptr;
|
||||
GBytes* bytes = g_resources_lookup_data(page.resource, G_RESOURCE_LOOKUP_FLAGS_NONE,
|
||||
&error);
|
||||
if (bytes == nullptr) {
|
||||
if (error != nullptr) g_error_free(error);
|
||||
return "<i>This page is missing from the build.</i>";
|
||||
}
|
||||
gsize size = 0;
|
||||
const char* data = static_cast<const char*>(g_bytes_get_data(bytes, &size));
|
||||
std::string body(data, size);
|
||||
g_bytes_unref(bytes);
|
||||
|
||||
if (!pango_parse_markup(body.c_str(), -1, 0, nullptr, nullptr, nullptr, &error)) {
|
||||
if (error != nullptr) g_error_free(error);
|
||||
return "<i>This page could not be displayed: its markup is malformed.</i>";
|
||||
}
|
||||
*ok = true;
|
||||
return body;
|
||||
}
|
||||
|
||||
void on_help_row_selected(GtkListBox*, GtkListBoxRow* row, gpointer) {
|
||||
if (row == nullptr || g_help_body == nullptr) return;
|
||||
const int idx = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(row), "help-page"));
|
||||
if (idx < 0 || idx >= kPageCount) return;
|
||||
bool ok = false;
|
||||
const std::string body = load_page(kPages[idx], &ok);
|
||||
gtk_label_set_markup(GTK_LABEL(g_help_body), body.c_str());
|
||||
// The screenshots above it, if this page has any. The column comes from the `.ui` by id rather than
|
||||
// being found as the label's previous sibling: refilling it is a page *switch*, not a frame, so
|
||||
// rebuilding it here is cheap and is also the only way to be sure the pictures are this page's and
|
||||
// not the last one's.
|
||||
GtkWidget* shots = g_help_shots;
|
||||
if (shots != nullptr && GTK_IS_BOX(shots)) {
|
||||
while (GtkWidget* old = gtk_widget_get_first_child(shots)) {
|
||||
gtk_box_remove(GTK_BOX(shots), old);
|
||||
}
|
||||
for (const char* const* img = kPages[idx].images; img != nullptr && *img != nullptr; ++img) {
|
||||
GtkWidget* shot = gtk_picture_new_for_resource(*img);
|
||||
gtk_picture_set_can_shrink(GTK_PICTURE(shot), TRUE);
|
||||
// A height, and `CONTAIN` (the default) to fit the image inside it: a picture that may
|
||||
// shrink asks for no height at all in a box, so without this it is allocated nothing and
|
||||
// the page shows its text with a gap where the screenshots should be.
|
||||
gtk_widget_set_size_request(shot, -1, kHelpShotHeight);
|
||||
gtk_box_append(GTK_BOX(shots), shot);
|
||||
}
|
||||
gtk_widget_set_visible(shots, gtk_widget_get_first_child(shots) != nullptr);
|
||||
if (!gtk_uri_launcher_launch_finish(GTK_URI_LAUNCHER(source), result, &error)) {
|
||||
GtkAlertDialog* alert = gtk_alert_dialog_new("Could not open the help manual");
|
||||
const std::string detail =
|
||||
std::string(error->message) +
|
||||
"\n\nThe manual is installed under $XDG_DATA_DIRS/help/C/help:" UDPSCOPE_HELP_NAME
|
||||
"; a desktop help viewer (for example `yelp`) that handles the `help:` URI is needed "
|
||||
"to read it.";
|
||||
gtk_alert_dialog_set_detail(alert, detail.c_str());
|
||||
gtk_alert_dialog_show(alert, parent);
|
||||
g_object_unref(alert);
|
||||
g_error_free(error);
|
||||
}
|
||||
// `parent` was referenced for the duration of the async call.
|
||||
if (parent != nullptr) g_object_unref(parent);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -197,84 +110,46 @@ const char* about_icon_name() {
|
||||
|
||||
HelpReport help_self_check() {
|
||||
HelpReport report;
|
||||
report.pages = kPageCount;
|
||||
for (const HelpPage& page : kPages) {
|
||||
bool ok = false;
|
||||
const std::string body = load_page(page, &ok);
|
||||
if (ok && !body.empty()) ++report.parsed;
|
||||
if (report.shortest_title.empty() || body.size() < report.shortest_bytes) {
|
||||
report.shortest_title = page.title;
|
||||
report.shortest_bytes = body.size();
|
||||
|
||||
// The manual is a set of files on disk now, so the reading is a file-system one: a missing
|
||||
// directory leaves `topics=0 index=false`, which is a manual that is not where the build
|
||||
// says it is -- and Yelp would show nothing for `help:<identifier>`.
|
||||
GError* error = nullptr;
|
||||
GDir* dir = g_dir_open(UDPSCOPE_HELP_DIR, 0, &error);
|
||||
if (dir == nullptr) {
|
||||
if (error != nullptr) g_error_free(error);
|
||||
return report;
|
||||
}
|
||||
while (const char* name = g_dir_read_name(dir)) {
|
||||
if (!g_str_has_suffix(name, ".page")) continue;
|
||||
if (g_strcmp0(name, "index.page") == 0) report.index = true;
|
||||
else ++report.topics;
|
||||
}
|
||||
g_dir_close(dir);
|
||||
|
||||
// The figures the pages reference, counted for the same reason: an image that went missing
|
||||
// is a broken figure in the viewer rather than a failed build.
|
||||
const std::string figures = std::string(UDPSCOPE_HELP_DIR) + "/figures";
|
||||
if (GDir* figdir = g_dir_open(figures.c_str(), 0, &error)) {
|
||||
while (const char* name = g_dir_read_name(figdir)) {
|
||||
if (g_str_has_suffix(name, ".png") || g_str_has_suffix(name, ".jpg")) ++report.figures;
|
||||
}
|
||||
g_dir_close(figdir);
|
||||
} else if (error != nullptr) {
|
||||
g_error_free(error);
|
||||
}
|
||||
return report;
|
||||
}
|
||||
|
||||
void show_help(GtkWindow* parent) {
|
||||
if (g_help != nullptr) {
|
||||
gtk_window_present(GTK_WINDOW(g_help));
|
||||
return;
|
||||
}
|
||||
|
||||
// A builder per opening, as with every other `.ui` here: the window belongs to its object list and
|
||||
// `close-request` destroys the window, so the resource is parsed again rather than a dead window
|
||||
// fetched. This is where the previous one goes, and it is safe because that window has been closed
|
||||
// for as long as it takes someone to ask for help again.
|
||||
g_clear_object(&g_help_ui);
|
||||
g_help_ui = gtk_builder_new_from_resource("/org/udpscope/ui/help.ui");
|
||||
if (g_help_ui == nullptr) return;
|
||||
GObject* window = gtk_builder_get_object(g_help_ui, "help_window");
|
||||
GObject* list = gtk_builder_get_object(g_help_ui, "page_list");
|
||||
GObject* body = gtk_builder_get_object(g_help_ui, "body");
|
||||
GObject* shots = gtk_builder_get_object(g_help_ui, "shot_box");
|
||||
// A missing id is the one mistake the build and `GtkBuilder` both miss; every reader below would be a
|
||||
// null widget on the first page switch.
|
||||
if (window == nullptr || list == nullptr || body == nullptr || shots == nullptr) return;
|
||||
g_help = GTK_WIDGET(window);
|
||||
g_help_body = GTK_WIDGET(body);
|
||||
g_help_shots = GTK_WIDGET(shots);
|
||||
gtk_window_set_transient_for(GTK_WINDOW(g_help), parent);
|
||||
|
||||
// The rows are the pages, which is data and not template: a heading row per section, then one row per
|
||||
// page carrying its own index. A template cannot know the fourteen pages, and a sectioned list is not
|
||||
// what a GtkListView with one item factory can draw.
|
||||
const char* current_section = nullptr;
|
||||
for (int i = 0; i < kPageCount; ++i) {
|
||||
if (current_section == nullptr || std::string(current_section) != kPages[i].section) {
|
||||
current_section = kPages[i].section;
|
||||
GtkWidget* heading = gtk_label_new(nullptr);
|
||||
char markup[128];
|
||||
std::snprintf(markup, sizeof(markup), "<b><small>%s</small></b>", current_section);
|
||||
gtk_label_set_markup(GTK_LABEL(heading), markup);
|
||||
gtk_label_set_xalign(GTK_LABEL(heading), 0.0f);
|
||||
gtk_widget_set_margin_start(heading, 10);
|
||||
gtk_widget_set_margin_top(heading, 10);
|
||||
gtk_widget_set_margin_bottom(heading, 2);
|
||||
GtkWidget* heading_row = gtk_list_box_row_new();
|
||||
gtk_list_box_row_set_selectable(GTK_LIST_BOX_ROW(heading_row), FALSE);
|
||||
gtk_list_box_row_set_activatable(GTK_LIST_BOX_ROW(heading_row), FALSE);
|
||||
gtk_list_box_row_set_child(GTK_LIST_BOX_ROW(heading_row), heading);
|
||||
gtk_list_box_append(GTK_LIST_BOX(list), heading_row);
|
||||
}
|
||||
GtkWidget* label = gtk_label_new(kPages[i].title);
|
||||
gtk_label_set_xalign(GTK_LABEL(label), 0.0f);
|
||||
gtk_widget_set_margin_start(label, 18);
|
||||
gtk_widget_set_margin_end(label, 10);
|
||||
gtk_widget_set_margin_top(label, 4);
|
||||
gtk_widget_set_margin_bottom(label, 4);
|
||||
GtkWidget* row = gtk_list_box_row_new();
|
||||
gtk_list_box_row_set_child(GTK_LIST_BOX_ROW(row), label);
|
||||
g_object_set_data(G_OBJECT(row), "help-page", GINT_TO_POINTER(i));
|
||||
gtk_list_box_append(GTK_LIST_BOX(list), row);
|
||||
}
|
||||
g_signal_connect(list, "row-selected", G_CALLBACK(on_help_row_selected), nullptr);
|
||||
|
||||
// Select the first page so the window never opens blank.
|
||||
GtkListBoxRow* first = gtk_list_box_get_row_at_index(GTK_LIST_BOX(list), 1);
|
||||
if (first != nullptr) gtk_list_box_select_row(GTK_LIST_BOX(list), first);
|
||||
|
||||
g_signal_connect(g_help, "close-request", G_CALLBACK(on_help_close), &g_help);
|
||||
gtk_window_present(GTK_WINDOW(g_help));
|
||||
// The freedesktop `help:` URI, handed to whatever the desktop registered for it -- Yelp on
|
||||
// GNOME. The launch is asynchronous, so the parent is referenced across it and released in
|
||||
// the callback; `parent` may legitimately be nullptr (the chooser runs before any window
|
||||
// exists).
|
||||
GtkUriLauncher* launcher = gtk_uri_launcher_new("help:" UDPSCOPE_HELP_NAME);
|
||||
gtk_uri_launcher_launch(launcher, parent, nullptr, on_help_launched,
|
||||
parent != nullptr ? g_object_ref(parent) : nullptr);
|
||||
g_object_unref(launcher);
|
||||
}
|
||||
|
||||
void show_shortcuts(GtkWindow* parent) {
|
||||
@@ -284,17 +159,16 @@ void show_shortcuts(GtkWindow* parent) {
|
||||
}
|
||||
|
||||
// A `GtkShortcutsWindow` from `src/ui/shortcuts.ui` -- gedit's arrangement, and GTK's own: one
|
||||
// section per page, with the sidebar and the search box drawn by the window itself.
|
||||
//
|
||||
// A UI file rather than widget construction because of the mouse rows. An accelerator cannot spell
|
||||
// "Ctrl and drag", and libadwaita's `AdwShortcutsItem` -- the modern replacement for this widget --
|
||||
// has no icon property at all, so its cell reads "No Shortcut". `GtkShortcutsShortcut`'s one generic
|
||||
// gesture type draws the `icon` the row names, which is what puts a mouse glyph where the keycaps
|
||||
// would be. (The widget is deprecated since 4.20, but this uses none of its functions: the file is
|
||||
// loaded as data and presented as a `GtkWindow`, so the build stays at its four warnings.)
|
||||
// section per page, with the sidebar and the search box drawn by the window itself. A UI file
|
||||
// rather than widget construction because of the mouse rows: an accelerator cannot spell "Ctrl
|
||||
// and drag", and libadwaita's `AdwShortcutsItem` -- the modern replacement -- has no icon
|
||||
// property, so its cell reads "No Shortcut". `GtkShortcutsShortcut`'s one generic gesture type
|
||||
// draws the `icon` the row names. (The widget is deprecated since 4.20, but this uses none of
|
||||
// its functions: the file is loaded as data and presented as a `GtkWindow`.)
|
||||
//
|
||||
// A builder per opening, not one reused: the window belongs to the builder and `close-request`
|
||||
// destroys the window, so the resource is parsed again rather than an already-dead window fetched.
|
||||
// destroys the window, so the resource is parsed again rather than an already-dead window
|
||||
// fetched.
|
||||
g_clear_object(&g_shortcuts_ui);
|
||||
g_shortcuts_ui = gtk_builder_new_from_resource("/org/udpscope/ui/shortcuts.ui");
|
||||
if (g_shortcuts_ui == nullptr) return;
|
||||
@@ -307,7 +181,6 @@ void show_shortcuts(GtkWindow* parent) {
|
||||
}
|
||||
|
||||
void close_help_windows() {
|
||||
if (g_help != nullptr) gtk_window_close(GTK_WINDOW(g_help));
|
||||
if (g_about != nullptr) adw_dialog_close(g_about);
|
||||
if (g_shortcuts != nullptr) gtk_window_close(GTK_WINDOW(g_shortcuts));
|
||||
}
|
||||
|
||||
+20
-14
@@ -18,20 +18,26 @@ void show_about(GtkWindow* parent, const std::string& session_path);
|
||||
/// so the dialog is asserted to *show* the app icon rather than merely to open.
|
||||
const char* about_icon_name();
|
||||
|
||||
/// The paged help window. Modal on nothing — it is a reference, so it stays usable while
|
||||
/// the scope runs. Already open: presented.
|
||||
/// Opens the installed Mallard manual in the desktop help viewer — Yelp, through the
|
||||
/// `help:` URI the freedesktop Help System Specification defines — rather than drawing a
|
||||
/// window of our own. `parent` is the window the launch is attributed to (and the one an
|
||||
/// error is reported on); the viewer is a separate process, so nothing here can be closed
|
||||
/// with `close_help_windows()`.
|
||||
void show_help(GtkWindow* parent);
|
||||
|
||||
/// What UDPSCOPE_PROBE_HELP_MS reports. `parsed` counts pages whose markup
|
||||
/// pango_parse_markup() accepted and whose body is non-empty; it must equal `pages`.
|
||||
/// What UDPSCOPE_PROBE_HELP_MS reports about the manual *in the tree*. `topics` is the
|
||||
/// number of `<id>.page` files other than `index.page`, and `index` says whether
|
||||
/// `index.page` is present: the manual is now a set of files on disk (installed by the
|
||||
/// `install()` rule, and read by Yelp), not a GResource in the binary, so the self-check
|
||||
/// asserts the set is *there* and must be read as a file-system finding — `topics=0
|
||||
/// index=0` means the manual is missing from `UDPSCOPE_HELP_DIR`.
|
||||
struct HelpReport {
|
||||
int pages = 0;
|
||||
int parsed = 0;
|
||||
std::string shortest_title;
|
||||
std::size_t shortest_bytes = 0;
|
||||
int topics = 0;
|
||||
bool index = false;
|
||||
int figures = 0;
|
||||
};
|
||||
|
||||
/// Loads and validates every page without building a window. Safe to call headless.
|
||||
/// Scans the manual's directory without opening a viewer. Safe to call headless.
|
||||
HelpReport help_self_check();
|
||||
|
||||
/// The shortcut overview: the application accelerators and the plot's own interactions,
|
||||
@@ -39,11 +45,11 @@ HelpReport help_self_check();
|
||||
/// deprecated as of GTK 4.18 and this project builds warning-free.
|
||||
void show_shortcuts(GtkWindow* parent);
|
||||
|
||||
/// Closes whichever of the three windows are open. One of them is a GtkWindow -- the help
|
||||
/// window, closed with gtk_window_close(), which emits close-request so its handler runs. The
|
||||
/// other two are AdwDialogs, which are not GtkWindows: they close with adw_dialog_close(),
|
||||
/// which emits ::closed. Either way the file-scope pointer is nulled by the handler, which is
|
||||
/// what prevents a second open presenting a dead widget. Used by the probe.
|
||||
/// Closes whichever of the two dialogs are open. Both are `AdwDialog`s, which are not
|
||||
/// `GtkWindow`s: they close with `adw_dialog_close()`, which emits `::closed`, and the
|
||||
/// file-scope pointer is nulled by the handler that prevents a second open presenting a dead
|
||||
/// widget. There is no help window left to close -- the manual opens in a separate viewer
|
||||
/// process. Used by the probe.
|
||||
void close_help_windows();
|
||||
|
||||
} // namespace udpscope
|
||||
+5
-3
@@ -5461,9 +5461,11 @@ void collect_spins(GtkWidget* w, std::vector<double>* out) {
|
||||
/// checks the GtkEditable refusal. It goes through run_app_action() for the actions and
|
||||
/// through the real handler for the refusal, because the refusal lives in the handler.
|
||||
|
||||
/// Loads and validates every help page, opens all three windows, closes them, and reports.
|
||||
/// `parsed` must equal `pages` and no page may be empty; `closed ok` with no GTK critical
|
||||
/// is the crash check for three windows built and destroyed in one run.
|
||||
/// Checks that the manual is present in the tree where the build says it is (`topics` and
|
||||
/// `index`, both zero if it is missing), opens the About and shortcut dialogs, reads them and
|
||||
/// closes them. The manual itself opens in a separate viewer process, so there is no help
|
||||
/// window here to build; `closed ok` with no GTK critical is the crash check for the two
|
||||
/// dialogs built and destroyed in one run.
|
||||
|
||||
|
||||
int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisition& acq,
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
<!-- SPDX-License-Identifier: MIT -->
|
||||
<!-- SPDX-FileCopyrightText: 2026 Martino Ferrari -->
|
||||
<!--
|
||||
The application's UI files, compiled into the binary beside the icons and the help pages, so there
|
||||
is still exactly one file to run. `preprocess="xml-stripblanks"` is what catches a malformed file:
|
||||
The application's UI files, compiled into the binary beside the icons, so there is still exactly
|
||||
one file to run. (The manual is no longer a bundle here: it is a Mallard set installed for Yelp.)
|
||||
`preprocess="xml-stripblanks"` is what catches a malformed file:
|
||||
gtk_builder_new_from_resource() on a broken .ui returns nothing usable and the window simply does
|
||||
not open, which is a silent failure at runtime and a loud one here.
|
||||
-->
|
||||
|
||||
+7
-4
@@ -4447,7 +4447,10 @@ gboolean help_probe(gpointer user) {
|
||||
const std::string session =
|
||||
layout_path(a->model->config().server, a->model->config().port);
|
||||
|
||||
show_help(GTK_WINDOW(a->window));
|
||||
// The manual is not opened here: it lives in another process (Yelp, through the `help:` URI)
|
||||
// and neither it nor this box can draw a window for it, so there is nothing to open, read
|
||||
// back or close. `help_self_check()` above is the assertion -- the set is on disk where the
|
||||
// build says it is. The two AdwDialogs below are still built, read and closed for real.
|
||||
show_about(GTK_WINDOW(a->window), session);
|
||||
show_shortcuts(GTK_WINDOW(a->window));
|
||||
// Read while it is open: the icon is the point of the dialog carrying one, and
|
||||
@@ -4464,11 +4467,11 @@ gboolean help_probe(gpointer user) {
|
||||
// works and is not documented, or is documented and does nothing.
|
||||
const UiShortcuts ui = ui_shortcut_rows("/org/udpscope/ui/shortcuts.ui");
|
||||
std::fprintf(stderr,
|
||||
"[help] pages=%d parsed=%d shortest='%s' %zu B; shortcuts: app=%d plot=%d tables=%d "
|
||||
"[help] topics=%d index=%d figures=%d dir=%s; shortcuts: app=%d plot=%d tables=%d "
|
||||
"ui=%d ui_accel=%d ui_icon=%d ui_bare=%d; "
|
||||
"about version=%s commit=%s built=%s settings=%s icon=%s; closed ok\n",
|
||||
report.pages, report.parsed, report.shortest_title.c_str(),
|
||||
report.shortest_bytes, accel_count, gesture_count, accel_count + gesture_count,
|
||||
report.topics, report.index ? 1 : 0, report.figures, UDPSCOPE_HELP_DIR,
|
||||
accel_count, gesture_count, accel_count + gesture_count,
|
||||
ui.rows, ui.with_accel, ui.with_icon, ui.bare, build_info::kVersion,
|
||||
build_info::kCommit, build_info::kTimestamp, config_dir().c_str(),
|
||||
about_icon.c_str());
|
||||
|
||||
Reference in new issue
Block a user