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
@@ -38,7 +38,7 @@ The detail lives in `docs/claude/`. Read the files that touch the area you are e
|
|||||||
| File | What it covers |
|
| File | What it covers |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| [build-and-run.md](docs/claude/build-and-run.md) | Configuring and building, the desktop-entry target, the command-line options, and the two vendored dependencies (`deps/plotter`, `deps/udpsclient`) including what was changed upstream and why. |
|
| [build-and-run.md](docs/claude/build-and-run.md) | Configuring and building, the desktop-entry target, the command-line options, and the two vendored dependencies (`deps/plotter`, `deps/udpsclient`) including what was changed upstream and why. |
|
||||||
| [verification.md](docs/claude/verification.md) | How to run the headless self-check, the **two baselines every run is read against** (exactly four build warnings; no `Gtk-CRITICAL`), and the environment variable that schedules each probe. |
|
| [verification.md](docs/claude/verification.md) | How to run the headless self-check, the **two baselines every run is read against** (exactly three build warnings; no `Gtk-CRITICAL`), and the environment variable that schedules each probe. |
|
||||||
| [probe-readings.md](docs/claude/probe-readings.md) | The per-line reference for the self-check's output: what every probe and dump line means, the value it must read, and the defect a wrong reading names. |
|
| [probe-readings.md](docs/claude/probe-readings.md) | The per-line reference for the self-check's output: what every probe and dump line means, the value it must read, and the defect a wrong reading names. |
|
||||||
|
|
||||||
### How the application is put together
|
### How the application is put together
|
||||||
@@ -60,4 +60,4 @@ The detail lives in `docs/claude/`. Read the files that touch the area you are e
|
|||||||
| [invariants-measurements.md](docs/claude/invariants-measurements.md) | Why the pane is refreshed on a cadence, why neither pass may read its window raw, and how a crossing is counted with hysteresis. |
|
| [invariants-measurements.md](docs/claude/invariants-measurements.md) | Why the pane is refreshed on a cadence, why neither pass may read its window raw, and how a crossing is counted with hysteresis. |
|
||||||
| [invariants-math-channels.md](docs/claude/invariants-math-channels.md) | A math channel *is* a channel; the engine runs on the producer thread, so a regression there costs packets and not frames; the node editor's own rules. |
|
| [invariants-math-channels.md](docs/claude/invariants-math-channels.md) | A math channel *is* a channel; the engine runs on the producer thread, so a regression there costs packets and not frames; the node editor's own rules. |
|
||||||
| [invariants-session-and-export.md](docs/claude/invariants-session-and-export.md) | What a session file persists and in what order, when a save is refused, and when an export is collected. |
|
| [invariants-session-and-export.md](docs/claude/invariants-session-and-export.md) | What a session file persists and in what order, when a save is refused, and when an export is collected. |
|
||||||
| [invariants-gtk-and-chrome.md](docs/claude/invariants-gtk-and-chrome.md) | The GTK signal-signature and dialog-lifetime traps this app has already fallen into, the status bar's two tiers, the six bundled icons and their authoring rules, and the help pages. |
|
| [invariants-gtk-and-chrome.md](docs/claude/invariants-gtk-and-chrome.md) | The GTK signal-signature and dialog-lifetime traps this app has already fallen into, the status bar's two tiers, the six bundled icons and their authoring rules, and how the Mallard manual is opened. |
|
||||||
+62
-19
@@ -11,6 +11,8 @@ if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
find_package(Threads REQUIRED)
|
find_package(Threads REQUIRED)
|
||||||
|
# `CMAKE_INSTALL_DATADIR` (the `share` under the prefix) for the help set's install path.
|
||||||
|
include(GNUInstallDirs)
|
||||||
|
|
||||||
# --- libadwaita: the shortcut overview ---------------------------------------
|
# --- libadwaita: the shortcut overview ---------------------------------------
|
||||||
# GTK4's own `GtkShortcutsWindow` is deprecated as of 4.18 and libadwaita's
|
# GTK4's own `GtkShortcutsWindow` is deprecated as of 4.18 and libadwaita's
|
||||||
@@ -61,22 +63,35 @@ add_custom_command(
|
|||||||
COMMENT "Compiling udpscope icon resources"
|
COMMENT "Compiling udpscope icon resources"
|
||||||
VERBATIM)
|
VERBATIM)
|
||||||
|
|
||||||
# --- help pages: Pango markup in a GResource ----------------------------------
|
# --- the manual: a Mallard help set, installed for the `help:` URI ------------
|
||||||
# A second bundle beside the icons'. --c-name is given explicitly so the two generated
|
# The manual is no longer a GResource compiled into the binary. It is a Mallard set that the
|
||||||
# sources cannot collide on symbol names.
|
# desktop help viewer (Yelp on GNOME) reads off disk, because that is the only way `F1` can
|
||||||
set(UDPSCOPE_HELP_DIR ${CMAKE_CURRENT_SOURCE_DIR}/src/help)
|
# open a real, searchable, translatable document and the only way the freedesktop Help System
|
||||||
file(GLOB UDPSCOPE_HELP_FILES CONFIGURE_DEPENDS ${UDPSCOPE_HELP_DIR}/*.txt)
|
# Specification's `help:` URI resolves it. The user documentation is then *files*, checked and
|
||||||
add_custom_command(
|
# installed rather than compiled in.
|
||||||
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/udpscope_help_resources.c
|
#
|
||||||
COMMAND ${GLIB_COMPILE_RESOURCES}
|
# The document identifier -- the `<name>` in `help:<name>` -- is derived from the desktop
|
||||||
--target=${CMAKE_CURRENT_BINARY_DIR}/udpscope_help_resources.c
|
# file's own basename (its desktop-file id), not written out twice: the same string is the
|
||||||
--sourcedir=${UDPSCOPE_HELP_DIR}
|
# `.desktop` file, the GApplication id and the installed directory. The specification installs
|
||||||
--c-name=udpscope_help
|
# a document at `<datadir>/help/<lang>/<document>/` and resolves `help:<document>` to the first
|
||||||
--generate-source
|
# match under $XDG_DATA_HOME/$XDG_DATA_DIRS, so the directory name *is* the identifier.
|
||||||
${UDPSCOPE_HELP_DIR}/udpscope-help.gresource.xml
|
# NAME_WE is not used: it strips the *longest* extension, so it would cut `org.udpscope.app`
|
||||||
DEPENDS ${UDPSCOPE_HELP_DIR}/udpscope-help.gresource.xml ${UDPSCOPE_HELP_FILES}
|
# down to `org`. Strip the two suffixes this one file actually carries, in order.
|
||||||
COMMENT "Compiling udpscope help resources"
|
get_filename_component(_udpscope_desktop
|
||||||
VERBATIM)
|
${CMAKE_CURRENT_SOURCE_DIR}/data/org.udpscope.app.desktop.in NAME)
|
||||||
|
string(REGEX REPLACE "\\.desktop\\.in$" "" UDPSCOPE_HELP_NAME "${_udpscope_desktop}")
|
||||||
|
unset(_udpscope_desktop)
|
||||||
|
set(UDPSCOPE_HELP_SRC ${CMAKE_CURRENT_SOURCE_DIR}/help/C/${UDPSCOPE_HELP_NAME})
|
||||||
|
file(GLOB UDPSCOPE_HELP_FILES CONFIGURE_DEPENDS
|
||||||
|
${UDPSCOPE_HELP_SRC}/*.page ${UDPSCOPE_HELP_SRC}/figures/*.png)
|
||||||
|
|
||||||
|
# The install rule the specification asks for. `CMAKE_INSTALL_DATADIR` is `share` by default,
|
||||||
|
# so the set lands at `<prefix>/share/help/C/<document>/`, which is exactly where a viewer
|
||||||
|
# looks. There is no install step for the binary itself (see the desktop-entry target below),
|
||||||
|
# but the manual is *read from a datadir* by another process, so it has to be installable.
|
||||||
|
install(DIRECTORY ${UDPSCOPE_HELP_SRC}/
|
||||||
|
DESTINATION ${CMAKE_INSTALL_DATADIR}/help/C/${UDPSCOPE_HELP_NAME}
|
||||||
|
FILES_MATCHING PATTERN "*.page" PATTERN "*.png" PATTERN "*.jpg")
|
||||||
|
|
||||||
# --- UI files: GtkBuilder XML in a GResource ----------------------------------
|
# --- UI files: GtkBuilder XML in a GResource ----------------------------------
|
||||||
# The third bundle, for the same reason as the other two: `.ui` files are data, so they are compiled
|
# The third bundle, for the same reason as the other two: `.ui` files are data, so they are compiled
|
||||||
@@ -97,8 +112,9 @@ add_custom_command(
|
|||||||
VERBATIM)
|
VERBATIM)
|
||||||
# The generated source carries each file as one C string literal, and a `.ui` is big enough to pass the
|
# The generated source carries each file as one C string literal, and a `.ui` is big enough to pass the
|
||||||
# 4095-character minimum the standard requires a compiler to support: `-Woverlength-strings` fires on
|
# 4095-character minimum the standard requires a compiler to support: `-Woverlength-strings` fires on
|
||||||
# generated *data*, not on anything anyone wrote. Silenced for this one generated file (the icon and
|
# generated *data*, not on anything anyone wrote. Silenced for this one generated file (the icon bundle
|
||||||
# help bundles are small enough per file never to need it), so the build stays at its four warnings.
|
# is small enough per file never to need it), so the build stays at its three warnings. The manual is
|
||||||
|
# no longer compiled in, so the help bundle's own warning is gone with it.
|
||||||
set_source_files_properties(${CMAKE_CURRENT_BINARY_DIR}/udpscope_ui_resources.c
|
set_source_files_properties(${CMAKE_CURRENT_BINARY_DIR}/udpscope_ui_resources.c
|
||||||
PROPERTIES COMPILE_OPTIONS "-Wno-overlength-strings")
|
PROPERTIES COMPILE_OPTIONS "-Wno-overlength-strings")
|
||||||
|
|
||||||
@@ -127,6 +143,28 @@ else()
|
|||||||
message(WARNING "python3 was not found: the test-ui target is not available")
|
message(WARNING "python3 was not found: the test-ui target is not available")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# --- the manual: an opt-in check, never part of `cmake --build` -----------------
|
||||||
|
# The manual is hand-written Mallard, and `cmake --build` never reads it -- a broken xref or a
|
||||||
|
# missing figure is a page in the viewer with no content or no image, not a build failure.
|
||||||
|
# `tools/check_help.sh` runs yelp-check's four offline subcommands over the set (links, media,
|
||||||
|
# orphans, ids) and renders the whole set to HTML with yelp-build, so the manual is checked the
|
||||||
|
# way the `.ui` files are. `yelp-check validate` is *not* used: it fetches the Mallard RelaxNG
|
||||||
|
# schema over the network, which is not available here and would make the check depend on it.
|
||||||
|
#
|
||||||
|
# No ALL: this runs only when it is named, so `cmake --build build` is unaffected and
|
||||||
|
# yelp-tools is not a build dependency.
|
||||||
|
find_program(UDPSCOPE_YELP_CHECK NAMES yelp-check)
|
||||||
|
find_program(UDPSCOPE_YELP_BUILD NAMES yelp-build)
|
||||||
|
if(UDPSCOPE_YELP_CHECK AND UDPSCOPE_YELP_BUILD)
|
||||||
|
add_custom_target(test-help
|
||||||
|
COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/tools/check_help.sh ${UDPSCOPE_HELP_SRC}
|
||||||
|
DEPENDS ${UDPSCOPE_HELP_FILES} ${CMAKE_CURRENT_SOURCE_DIR}/tools/check_help.sh
|
||||||
|
COMMENT "Checking the Mallard help set with yelp-check and yelp-build"
|
||||||
|
VERBATIM)
|
||||||
|
else()
|
||||||
|
message(WARNING "yelp-check/yelp-build were not found: the test-help target is not available")
|
||||||
|
endif()
|
||||||
|
|
||||||
# --- build info: version, commit and timestamp compiled into the binary -------
|
# --- build info: version, commit and timestamp compiled into the binary -------
|
||||||
# Written at configure time so add_executable can name the file, and again on every
|
# Written at configure time so add_executable can name the file, and again on every
|
||||||
# build so the commit and the timestamp belong to the binary that is running rather
|
# build so the commit and the timestamp belong to the binary that is running rather
|
||||||
@@ -148,7 +186,6 @@ set_source_files_properties(${UDPSCOPE_BUILD_INFO} PROPERTIES GENERATED TRUE)
|
|||||||
# --- the application ----------------------------------------------------------
|
# --- the application ----------------------------------------------------------
|
||||||
add_executable(udpscope
|
add_executable(udpscope
|
||||||
${CMAKE_CURRENT_BINARY_DIR}/udpscope_resources.c
|
${CMAKE_CURRENT_BINARY_DIR}/udpscope_resources.c
|
||||||
${CMAKE_CURRENT_BINARY_DIR}/udpscope_help_resources.c
|
|
||||||
${CMAKE_CURRENT_BINARY_DIR}/udpscope_ui_resources.c
|
${CMAKE_CURRENT_BINARY_DIR}/udpscope_ui_resources.c
|
||||||
${UDPSCOPE_BUILD_INFO}
|
${UDPSCOPE_BUILD_INFO}
|
||||||
src/main.cpp
|
src/main.cpp
|
||||||
@@ -187,6 +224,12 @@ add_executable(udpscope
|
|||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(udpscope PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
|
target_include_directories(udpscope PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
|
||||||
|
# The manual's identifier (`show_help()` builds `help:<id>` from it) and its directory in the
|
||||||
|
# source tree (what `help_self_check()` scans). Both come from the block near the top, so the
|
||||||
|
# identifier is the desktop file's basename and nothing is written out twice.
|
||||||
|
target_compile_definitions(udpscope PRIVATE
|
||||||
|
UDPSCOPE_HELP_NAME="${UDPSCOPE_HELP_NAME}"
|
||||||
|
UDPSCOPE_HELP_DIR="${UDPSCOPE_HELP_SRC}")
|
||||||
target_link_libraries(udpscope PRIVATE plot::gtk4 udpsclient Threads::Threads
|
target_link_libraries(udpscope PRIVATE plot::gtk4 udpsclient Threads::Threads
|
||||||
PkgConfig::ADWAITA)
|
PkgConfig::ADWAITA)
|
||||||
add_dependencies(udpscope udpscope_build_info)
|
add_dependencies(udpscope udpscope_build_info)
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ Every size it prints is read from a value the frame tick captured while the wind
|
|||||||
from the widget: the dump runs from the destroy handler, where the widget pointer is already freed
|
from the widget: the dump runs from the destroy handler, where the widget pointer is already freed
|
||||||
memory. It used to ask two of them directly, which is where the long-standing "an exit-dump run
|
memory. It used to ask two of them directly, which is where the long-standing "an exit-dump run
|
||||||
emits exactly two `Gtk-CRITICAL`s" baseline came from — and a critical was only the lucky reading.
|
emits exactly two `Gtk-CRITICAL`s" baseline came from — and a critical was only the lucky reading.
|
||||||
`UDPSCOPE_PROBE_HELP_MS` builds and destroys three windows of its own before the dump, so by then
|
`UDPSCOPE_PROBE_HELP_MS` builds and destroys the About and shortcut dialogs before the dump, so by then
|
||||||
the memory had been reused and the same two calls **crashed in the middle of the dump**, which
|
the memory had been reused and the same two calls **crashed in the middle of the dump**, which
|
||||||
looks like a truncated log rather than like a segfault in a debug line. The dump now makes no
|
looks like a truncated log rather than like a segfault in a debug line. The dump now makes no
|
||||||
`gtk_widget_*` call at all and a clean run carries no critical.
|
`gtk_widget_*` call at all and a clean run carries no critical.
|
||||||
@@ -844,9 +844,10 @@ reporting a problem had to retype. And the string it shows is a `std::string`, w
|
|||||||
buffer that no one notices until the path matters.
|
buffer that no one notices until the path matters.
|
||||||
|
|
||||||
What it cost: an `AdwAboutDialog` is an `AdwDialog` and **not** a `GtkWindow`, so its close path
|
What it cost: an `AdwAboutDialog` is an `AdwDialog` and **not** a `GtkWindow`, so its close path
|
||||||
differs from the help window sitting beside it -- `adw_dialog_close()` and `::closed`, rather than
|
differs from the shortcut overview's -- `adw_dialog_close()` and `::closed`, rather than
|
||||||
`gtk_window_close()` and `::close-request`. That is the same trap the shortcut overview already
|
`gtk_window_close()` and `::close-request`. That is the same trap the shortcut overview already
|
||||||
had, and the two now share one `::closed` handler keyed by the pointer it must null. What was
|
had; each keeps a close handler that nulls its own pointer -- the About dialog on `::closed`, the
|
||||||
|
overview on `::close-request` -- so neither can be presented again after it is gone. What was
|
||||||
deliberately left out: credits (one author, already named), release notes (there are no releases)
|
deliberately left out: credits (one author, already named), release notes (there are no releases)
|
||||||
and an issue-tracker URL (the website link reaches the repository, which carries the tracker).
|
and an issue-tracker URL (the website link reaches the repository, which carries the tracker).
|
||||||
|
|
||||||
@@ -1657,11 +1658,12 @@ status bar opens its detail **above** the button: below is off the window's bott
|
|||||||
bubble whose body is clipped away while its top edge stays at the button is what "the arrow is
|
bubble whose body is clipped away while its top edge stays at the button is what "the arrow is
|
||||||
detached from the body" looks like.
|
detached from the body" looks like.
|
||||||
|
|
||||||
The **help window** (`F1`) carries the same screenshots as `docs/`: the overview page shows the
|
The **manual** (`F1`, the *Documentation* menu row) opens in the desktop help viewer — Yelp,
|
||||||
main window and a held capture above its text, and the trigger and math pages carry theirs. A
|
through the freedesktop `help:` URI — rather than in a window of udpscope's own: it is a Mallard
|
||||||
page's images are `GtkPicture`s **with a height request** — a shrinkable one asks for no height at
|
set installed under `<datadir>/help/C/org.udpscope.app/`, not a scroll view compiled into the
|
||||||
all inside a box, so without it the page renders its text with a silent gap where the screenshots
|
binary. Its overview page shows the main window and a held capture, and the trigger and math pages
|
||||||
should be. The side column and the two panel toggles stay hidden until a
|
carry their own figures, all produced by `tools/make-help-shots.sh` and embedded as `<figure>`s. The
|
||||||
|
side column and the two panel toggles stay hidden until a
|
||||||
CONFIG arrives — there is nothing to configure before the stream describes itself —
|
CONFIG arrives — there is nothing to configure before the stream describes itself —
|
||||||
and the first CONFIG reveals the signal panel (the trigger panel stays behind its
|
and the first CONFIG reveals the signal panel (the trigger panel stays behind its
|
||||||
own toggle).
|
own toggle).
|
||||||
@@ -2332,7 +2334,7 @@ reaches every widget). The full key table:
|
|||||||
| `s` | show / hide the side panel |
|
| `s` | show / hide the side panel |
|
||||||
| `f` | fit to data (the plot's own key, not an application accelerator) |
|
| `f` | fit to data (the plot's own key, not an application accelerator) |
|
||||||
| `Ctrl+E` | export visible range |
|
| `Ctrl+E` | export visible range |
|
||||||
| `F1` | open the help window |
|
| `F1` | open the manual (`help:org.udpscope.app`) in the desktop help viewer |
|
||||||
| `Ctrl+/` | open the shortcut overview |
|
| `Ctrl+/` | open the shortcut overview |
|
||||||
| `Ctrl+Q` | quit |
|
| `Ctrl+Q` | quit |
|
||||||
| `+` / `-` | zoom in / out (both axes) |
|
| `+` / `-` | zoom in / out (both axes) |
|
||||||
@@ -2349,15 +2351,22 @@ The six "Plot mouse" rows (wheel, drag, right-click) carry no accelerator key
|
|||||||
syntax has no member for pointer gestures — and put the prose in the `subtitle` field instead,
|
syntax has no member for pointer gestures — and put the prose in the `subtitle` field instead,
|
||||||
which `AdwShortcutsItem` renders as free text.
|
which `AdwShortcutsItem` renders as free text.
|
||||||
|
|
||||||
**The 14-page help window** (overflow menu or F1) is a scrollable text view that opens on the
|
**The manual** (the *Documentation* overflow-menu row, or `F1`) is a Mallard help set opened in
|
||||||
Overview page, so it never comes up blank: the help pages
|
Yelp through the `help:` URI, *not* a scroll view compiled into the binary. `help/C/org.udpscope.app/`
|
||||||
are compiled into the binary as a GResource (`src/help/udpscope_help.gresource.xml`,
|
holds `index.page`, one page per subject, and the annotated screenshots the pages embed; the document
|
||||||
`--c-name=udpscope_help`) for the same reason the icons are — udpscope is run straight out of the
|
identifier `org.udpscope.app` is the desktop file's own basename, so it is the `.desktop` name, the
|
||||||
build tree with no install step, so a search path would have to be found at runtime and a missing
|
GApplication id and the install directory at once. The set is installed by an `install()` rule to
|
||||||
file fails silently. The pages are Pango markup (`.txt` bodies), validated at load; a page that
|
`share/help/C/org.udpscope.app/` — the path the freedesktop Help System Specification's `help:` URI
|
||||||
fails validation renders a one-line stand-in so the window always opens rather than crashing.
|
resolves — and `show_help()` hands `help:org.udpscope.app` to whatever the desktop registered for the
|
||||||
`UDPSCOPE_PROBE_HELP_MS` verifies that `parsed == pages` and that the `closed ok` line carries
|
scheme (Yelp on GNOME), reporting a launch failure with an alert rather than failing silently.
|
||||||
no `Gtk-CRITICAL`.
|
`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 left out because it fetches the Mallard RelaxNG schema over the network.
|
||||||
|
`tools/make-help-shots.sh` regenerates the figures by driving the app headlessly against the mock
|
||||||
|
(GTK4 has to be forced to the Cairo renderer or X11 reads the surface back blank).
|
||||||
|
`UDPSCOPE_PROBE_HELP_MS` asserts the set is *present in the tree* — its
|
||||||
|
`[help] topics=… index=… figures=…` reading — rather than parsing pages, since they are files now
|
||||||
|
and not resources. Mallard is translatable with `itstool`, but only the `C` set is authored here.
|
||||||
|
|
||||||
**The status bar is two tiers.** The bar itself shows one glance line: the activity badge, the
|
**The status bar is two tiers.** The bar itself shows one glance line: the activity badge, the
|
||||||
display state (one word from a fixed set — `connecting`, `live`, `paused`, `armed`, `capturing`,
|
display state (one word from a fixed set — `connecting`, `live`, `paused`, `armed`, `capturing`,
|
||||||
|
|||||||
@@ -52,6 +52,28 @@ here is needed to *build* the application. `blueprint-compiler` and `python3` ar
|
|||||||
time; without them the target is absent rather than silently passing. `tools/check_ui.py --lint` also
|
time; without them the target is absent rather than silently passing. `tools/check_ui.py --lint` also
|
||||||
prints the linter's advice, which is advice and deliberately not a gate.
|
prints the linter's advice, which is advice and deliberately not a gate.
|
||||||
|
|
||||||
|
The manual has its own check, the same shape:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cmake --build build --target test-help
|
||||||
|
```
|
||||||
|
|
||||||
|
That runs `yelp-check`'s four offline subcommands over `help/C/<id>/` (links, media, orphans, ids)
|
||||||
|
and renders the whole set with `yelp-build html` into a temp directory, so the manual is checked the
|
||||||
|
way the `.ui` files are. **`yelp-check validate` is deliberately not used**: it fetches the Mallard
|
||||||
|
RelaxNG schema over the network, which is absent on a build box, so it would fail for that reason and
|
||||||
|
not for a fault in the pages. `yelp-check`/`yelp-build` are found at configure time; without them the
|
||||||
|
target is absent rather than silently passing.
|
||||||
|
|
||||||
|
The manual is a Mallard set under `help/C/<id>/`, installed by an `install()` rule to
|
||||||
|
`<datadir>/help/C/<id>/` — where the freedesktop Help System Specification's `help:` URI resolves
|
||||||
|
`help:<id>` — so it is *read from a datadir by another process* even though the binary itself has no
|
||||||
|
install step. `F1` opens it in Yelp through that URI. The document identifier is the desktop file's
|
||||||
|
basename (`data/org.udpscope.app.desktop.in` → `org.udpscope.app`), so it is the `.desktop` name, the
|
||||||
|
GApplication id and the install directory at once. The figures are regenerated by
|
||||||
|
`tools/make-help-shots.sh`, which drives the app headlessly against the mock and draws the annotation
|
||||||
|
boxes with ImageMagick; Mallard is translatable with `itstool`, but only the `C` set is authored.
|
||||||
|
|
||||||
Options: `--server`, `--port`, `--multicast`, `--interface`, `--buffer-mb` (per
|
Options: `--server`, `--port`, `--multicast`, `--interface`, `--buffer-mb` (per
|
||||||
buffer, default 200), `--max-window`, `--holdoff`, `--recv-buffer`, `--plots` (the row
|
buffer, default 200), `--max-window`, `--holdoff`, `--recv-buffer`, `--plots` (the row
|
||||||
count to *start* with, 1..`RowSet::kMaxRows`; the layout editor owns it afterwards),
|
count to *start* with, 1..`RowSet::kMaxRows`; the layout editor owns it afterwards),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Invariants: GTK traps, the status bar, the icons and the help
|
# Invariants: GTK traps, the status bar, the icons and the manual
|
||||||
|
|
||||||
The GTK signal-signature and dialog-lifetime traps this app has already fallen into, plus the status bar's two tiers, the bundled icons and the help pages.
|
The GTK signal-signature and dialog-lifetime traps this app has already fallen into, plus the status bar's two tiers, the bundled icons and how the Mallard manual is installed and opened.
|
||||||
|
|
||||||
Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to learn; read the ones touching the area you are editing before you edit it.
|
Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to learn; read the ones touching the area you are editing before you edit it.
|
||||||
|
|
||||||
@@ -111,19 +111,23 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
|
|||||||
tick updates it, so a rebuild is a per-frame relayout of a widget on screen.
|
tick updates it, so a rebuild is a per-frame relayout of a widget on screen.
|
||||||
`update_status_popover()` only ever calls `gtk_label_set_text()`, and `popover_rebuilds` in
|
`update_status_popover()` only ever calls `gtk_label_set_text()`, and `popover_rebuilds` in
|
||||||
the dump is the assertion.
|
the dump is the assertion.
|
||||||
- **The About dialog, the help window and the shortcut overview are opened by `show_about()`,
|
- **The About dialog and the shortcut overview are opened by `show_about()` and `show_shortcuts()`,
|
||||||
`show_help()` and `show_shortcuts()`, and each closes with the matching call.** There is
|
and each closes with the matching call; the manual is *not* one of these.** `show_help()` hands
|
||||||
exactly **one `GtkWindow`** among the three — the paged help window. It closes with
|
the `help:<document>` URI to the desktop help viewer (`GtkUriLauncher`, i.e. Yelp) — a process of
|
||||||
|
its own that cannot be closed from here — because the manual is a Mallard set installed for the
|
||||||
|
freedesktop `help:` URI, not a GResource in this binary (see the help bullet below). So there is
|
||||||
|
exactly **one `GtkWindow`** among what the app opens: the shortcut overview. It closes with
|
||||||
`gtk_window_close()`, which emits `close-request` so the handler that nulls the file-scope
|
`gtk_window_close()`, which emits `close-request` so the handler that nulls the file-scope
|
||||||
pointer runs. `gtk_window_destroy()` does **not** emit that signal and must not be used — the
|
pointer runs. `gtk_window_destroy()` does **not** emit that signal and must not be used — the
|
||||||
math dialog fell into exactly this trap, and the pointer then points at a dead widget until the
|
math dialog fell into exactly this trap, and the pointer then points at a dead widget until the
|
||||||
next attempt to open the same window. The other two — the About dialog (an `AdwAboutDialog`)
|
next attempt to open the same window. The About dialog (an `AdwAboutDialog`) is an **`AdwDialog`
|
||||||
and the shortcut overview (an `AdwShortcutsDialog`) — are **`AdwDialog`s and not `GtkWindow`s**:
|
and not a `GtkWindow`**: it closes with `adw_dialog_close()` and clears on `::closed`, there being
|
||||||
they close with `adw_dialog_close()` and clear on `::closed`, there being no `::close-request`
|
no `::close-request` on an `AdwDialog`. It shares one `on_dialog_closed()` handler with the About
|
||||||
on an `AdwDialog`. Both share one `on_dialog_closed()` handler keyed by a **pointer to the
|
pointer, keyed by a **pointer to the pointer** it must null (`&g_about`), which keeps the handler
|
||||||
pointer** it must null (`&g_about`, `&g_shortcuts`), which is what keeps one handler honest for
|
honest without knowing which dialog it is. One `GtkWidget*` (or `AdwDialog*`) per window, nulled
|
||||||
two dialogs. One `GtkWidget*` (or `AdwDialog*`) per window, nulled in its own close handler, is
|
in its own close handler, is the shape that prevents the double-open defect. `close_help_windows()`
|
||||||
the shape that prevents the double-open defect across all three.
|
(misnamed now) closes just the two dialogs; a failed *launch* is reported by the launcher's own
|
||||||
|
callback with a `GtkAlertDialog`, so a click that cannot open the manual says so.
|
||||||
- **`build_info.cpp` is generated, never committed.** `cmake/write_build_info.cmake` runs at
|
- **`build_info.cpp` is generated, never committed.** `cmake/write_build_info.cmake` runs at
|
||||||
configure time (so `add_executable` can name the file) and again on every build through
|
configure time (so `add_executable` can name the file) and again on every build through
|
||||||
`udpscope_build_info` (so the commit and the timestamp belong to the binary that is running).
|
`udpscope_build_info` (so the commit and the timestamp belong to the binary that is running).
|
||||||
@@ -137,15 +141,28 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
|
|||||||
arrives with `Mod::Shift` set. Ctrl+Alt together zooms neither axis, which needs no special
|
arrives with `Mod::Shift` set. Ctrl+Alt together zooms neither axis, which needs no special
|
||||||
case. The guards are `deps/plotter`'s own `ctrl_plus_zooms_only_x` and `alt_plus_zooms_only_y`
|
case. The guards are `deps/plotter`'s own `ctrl_plus_zooms_only_x` and `alt_plus_zooms_only_y`
|
||||||
tests, plus `UDPSCOPE_PROBE_ZOOMKEYS_MS`.
|
tests, plus `UDPSCOPE_PROBE_ZOOMKEYS_MS`.
|
||||||
- **A help page is one `.txt`, one `<file>` line in the GResource manifest, and one `kPages`
|
- **A help page is a Mallard file, and the manual is *not* in the binary.** `help/C/<id>/` holds
|
||||||
row.** The bodies are Pango markup in a second GResource (`--c-name=udpscope_help`, so its
|
`index.page`, one `<topic>.page` per subject, and the `figures/*.png` the pages link, and the
|
||||||
generated symbols cannot collide with the icons'), validated with `pango_parse_markup()` at
|
whole set is installed (the `install()` rule in CMakeLists) to `<datadir>/help/C/<id>/` — the
|
||||||
load; a page that fails either lookup or parse renders a one-line stand-in and is excluded from
|
path the freedesktop Help System Specification's `help:` URI resolves `help:<id>` to. The
|
||||||
`parsed`. `UDPSCOPE_PROBE_HELP_MS`'s `parsed` must equal `pages`.
|
document identifier is *derived from the desktop file's basename* (`org.udpscope.app.desktop.in`
|
||||||
A page may also carry screenshots (`HelpPage::images`, a null-terminated list of resources).
|
→ `org.udpscope.app`), so it is the `.desktop` name, the GApplication id and the install
|
||||||
They are `GtkPicture`s **with a height request**: one created `can_shrink` asks for *no* height
|
directory at once; `UDPSCOPE_HELP_NAME` and `UDPSCOPE_HELP_DIR` are compiled in from that one
|
||||||
at all inside a box, so without the request it is allocated nothing and the page renders its text
|
place. `show_help()` opens `help:<id>` with `GtkUriLauncher` (Yelp runs in its own process, so
|
||||||
with a silent gap where its screenshots should be — which is what the first version did.
|
nothing here holds a pointer to it), and its failure callback shows a `GtkAlertDialog`.
|
||||||
|
|
||||||
|
`UDPSCOPE_PROBE_HELP_MS` no longer parses pages — they are files, not markup in a resource — so
|
||||||
|
its `[help]` line asserts the set is *there*: `topics` (the `.page` files other than
|
||||||
|
`index.page`), `index`, and `figures`, scanned from `UDPSCOPE_HELP_DIR`. `topics=0 index=0` means
|
||||||
|
the manual is missing from the tree, which is the finding the old `parsed != pages` used to name.
|
||||||
|
`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 figures come from
|
||||||
|
`tools/make-help-shots.sh`, which drives the app headlessly against the mock and annotates a grab
|
||||||
|
(GTK4 must be forced to the Cairo renderer, `GSK_RENDERER=cairo`, or the X11 surface reads back
|
||||||
|
blank). Mallard is translatable with `itstool`, but there is no i18n set-up here: only the `C`
|
||||||
|
set is authored.
|
||||||
- **A `GtkPaned` hands its start child at least its minimum unless the position says otherwise.**
|
- **A `GtkPaned` hands its start child at least its minimum unless the position says otherwise.**
|
||||||
Putting the events pane in a paned is what makes its width the user's to drag -- but with the
|
Putting the events pane in a paned is what makes its width the user's to drag -- but with the
|
||||||
position left alone, `shrink-start-child` or not, the pane's minimum is reserved as an empty column
|
position left alone, `shrink-start-child` or not, the pane's minimum is reserved as an empty column
|
||||||
|
|||||||
@@ -610,7 +610,7 @@ Two lines of the self-check output are assertions in disguise:
|
|||||||
so the panes the tick rewrites are measured directly. The `[width-probe]` lines are the
|
so the panes the tick rewrites are measured directly. The `[width-probe]` lines are the
|
||||||
per-change detail behind the `axes.w changes` count, and a step over 100 px dumps the widget
|
per-change detail behind the `axes.w changes` count, and a step over 100 px dumps the widget
|
||||||
tree's wide subtrees — that is what the documented, intermittent ~320 px step wants read.
|
tree's wide subtrees — that is what the documented, intermittent ~320 px step wants read.
|
||||||
- `[help] pages=14 parsed=14 shortest='Pause and play' 1627 B; shortcuts: app=12 plot=14 tables=26 ui=26 ui_accel=19 ui_icon=7 ui_bare=0; about version=… commit=… built=… settings=… icon=org.udpscope.app; closed ok` (stderr, `UDPSCOPE_PROBE_HELP_MS`) — **`parsed` must equal `pages`** and no page may be empty: a missing resource or markup Pango rejects renders a one-line stand-in instead of failing, so the window *looks* fine and only this count notices. The `shortcuts:` numbers are the shortcut overview four ways.
|
- `[help] topics=14 index=1 figures=3 dir=/…/help/C/org.udpscope.app; shortcuts: app=14 plot=14 tables=28 ui=28 ui_accel=21 ui_icon=7 ui_bare=0; about version=… commit=… built=… settings=… icon=org.udpscope.app; closed ok` (stderr, `UDPSCOPE_PROBE_HELP_MS`) — the manual is a Mallard set on disk now, not a GResource in the binary, so this is a **file-system reading**: `topics` is the `.page` files other than `index.page`, `index` must be 1, and `figures` counts the screenshots under `figures/`. **`topics=0 index=0` means the manual is missing** from `dir` (the source directory the build was configured from, `UDPSCOPE_HELP_DIR`) — the finding the old `parsed != pages` used to name. The manual itself opens in Yelp, another process, which this probe does not and cannot exercise. The `shortcuts:` numbers are the shortcut overview four ways.
|
||||||
`app` and `plot` are the rows in the C tables -- what the application actually *handles* -- and `tables`
|
`app` and `plot` are the rows in the C tables -- what the application actually *handles* -- and `tables`
|
||||||
is their sum. `ui`, `ui_accel`, `ui_icon` and `ui_bare` are what `src/ui/shortcuts.ui` *declares*, read
|
is their sum. `ui`, `ui_accel`, `ui_icon` and `ui_bare` are what `src/ui/shortcuts.ui` *declares*, read
|
||||||
out of the resource with a markup parse rather than off the built widgets, because a tree walk counts
|
out of the resource with a markup parse rather than off the built widgets, because a tree walk counts
|
||||||
@@ -618,7 +618,7 @@ every row twice: `GtkShortcutsWindow` reaches each one from more than one place
|
|||||||
**`ui` must equal `tables`** -- two lists of one thing, and a row in one but not the other is a gesture
|
**`ui` must equal `tables`** -- two lists of one thing, and a row in one but not the other is a gesture
|
||||||
that works and is not documented, or is documented and does nothing -- and **`ui_bare` must be zero**: a
|
that works and is not documented, or is documented and does nothing -- and **`ui_bare` must be zero**: a
|
||||||
row with no accelerator and no icon is a cell reading "No Shortcut". `ui_accel + ui_icon == ui`, so those
|
row with no accelerator and no icon is a cell reading "No Shortcut". `ui_accel + ui_icon == ui`, so those
|
||||||
two also catch a row whose accelerator went missing. `settings=` must be a real path (the same one the About dialog shows). **`icon=` must read `org.udpscope.app`** — it is read back through `adw_about_dialog_get_application_icon()` while the dialog is open, so the dialog is asserted to *carry* the icon rather than merely to open (a `set_application_icon()` lost in an edit leaves a dialog that opens, looks almost right, and shows a generic placeholder). `closed ok` with **no `Gtk-CRITICAL`** is as much a crash test of three windows built, presented and closed in one run as it is a state check, and a stronger one than it was: the three are now closed by **two different calls**, so a mismatched pair crashes or leaks here. One of the three is a `GtkWindow` (the help window, `gtk_window_close()`); the About dialog and the shortcut overview are `AdwDialog`s and close with `adw_dialog_close()`, cleared on `::closed`. `shortest=` is the page title — a page whose body was never compiled into the binary is absent rather than empty, and a title alone is the stand-in's shape.
|
two also catch a row whose accelerator went missing. `settings=` must be a real path (the same one the About dialog shows). **`icon=` must read `org.udpscope.app`** — it is read back through `adw_about_dialog_get_application_icon()` while the dialog is open, so the dialog is asserted to *carry* the icon rather than merely to open (a `set_application_icon()` lost in an edit leaves a dialog that opens, looks almost right, and shows a generic placeholder). `closed ok` with **no `Gtk-CRITICAL`** is as much a crash test of the two dialogs built, presented and closed in one run as it is a state check: they are closed by **two different calls**, so a mismatched pair crashes or leaks here. One is a `GtkWindow` (the shortcut overview, `gtk_window_close()`/`::close-request`); the About dialog is an `AdwDialog` and closes with `adw_dialog_close()`, cleared on `::closed`. The manual is *not* one of them — it opens in Yelp, a separate process, so there is nothing here to open, read back or close.
|
||||||
- `[menu] N 'label' accel='key' h=P label_x=X accel_right=R/W order=D ok=O`, one line per row,
|
- `[menu] N 'label' accel='key' h=P label_x=X accel_right=R/W order=D ok=O`, one line per row,
|
||||||
then `[menu] rows=R expected=E order=D rows_bad=B popover_visible=V mapped=M button_mapped=B`
|
then `[menu] rows=R expected=E order=D rows_bad=B popover_visible=V mapped=M button_mapped=B`
|
||||||
and `[menu] status pos=top bubble=WxH button_top=T button_right=R fits=F` (stderr,
|
and `[menu] status pos=top bubble=WxH button_top=T button_right=R fits=F` (stderr,
|
||||||
|
|||||||
+15
-14
@@ -13,21 +13,21 @@ xvfb-run -a env UDPSCOPE_EXIT_AFTER_MS=8000 UDPSCOPE_ENABLE_ALL_MS=1000 \
|
|||||||
./build/udpscope --server 127.0.0.1 --port 8000
|
./build/udpscope --server 127.0.0.1 --port 8000
|
||||||
```
|
```
|
||||||
|
|
||||||
Two baselines every run is read against. A clean build emits **four** warnings and no more: two
|
Two baselines every run is read against. A clean build emits **three** warnings and no more: two
|
||||||
`-Wmissing-field-initializers` on `MathNode::source` and `-Woverlength-strings` on **both**
|
`-Wmissing-field-initializers` on `MathNode::source` and one `-Woverlength-strings` on the icons'
|
||||||
generated GResource `.c` files — the help pages' (**516044** bytes) and the icons', whose blob is
|
generated GResource `.c`, whose blob is **12352** against C99's 4095-byte string-literal limit with
|
||||||
**12352** against C99's 4095-byte string-literal limit with the seven action icons and the two app icons
|
the seven action icons and the two app icons that remain (a note about a generated file, which GCC
|
||||||
that remain (a note about a generated file, which GCC itself supports) -- re-measured when
|
itself supports). The *fourth* was the help pages' generated `.c` (**516044** bytes); the manual is
|
||||||
the tab work changed the set (that icon left, the two tab glyphs arrived, and this file's
|
now a Mallard set installed for Yelp rather than a GResource compiled into the binary, so that
|
||||||
help page was rewritten), which did not move the count; re-measure by touching each `*.gresource.xml`
|
bundle — and its warning — is gone, and the baseline moved from four to **three**. Re-measure by
|
||||||
under `src/` and rebuilding. The fifth warning that used to appear here was an unused
|
touching each `*.gresource.xml` under `src/` and rebuilding; a third `-Wmissing-field-initializers`
|
||||||
|
or any other warning is a finding. (The fifth warning that used to appear here was an unused
|
||||||
`output_built` in `math_probe()`; it was **deleted** as dead code — redundant with the
|
`output_built` in `math_probe()`; it was **deleted** as dead code — redundant with the
|
||||||
`output_after_add` already printed on the same line — and that deletion is what dropped the baseline
|
`output_after_add` already printed on the same line — which dropped the baseline from five to
|
||||||
from five to four. Nothing started reading it. And an exit-dump run emits **no** `Gtk-CRITICAL` at
|
four.) And an exit-dump run emits **no** `Gtk-CRITICAL` at all.
|
||||||
all. A fifth warning, or any critical, is a finding.
|
|
||||||
|
|
||||||
An *incremental* build reads **two** of those four: the generated GResource `.c` files are not
|
An *incremental* build reads **one** of those three: the generated GResource `.c` is not recompiled
|
||||||
recompiled when only `src/` changed, so their `-Woverlength-strings` do not reappear.
|
when only `src/` changed, so its `-Woverlength-strings` does not reappear.
|
||||||
|
|
||||||
A third reading every run offers for free: **how many `[udpscope] buffer allocated:` lines it
|
A third reading every run offers for free: **how many `[udpscope] buffer allocated:` lines it
|
||||||
printed.** The line is not behind a probe — it is in every build — and one line is one *real* change
|
printed.** The line is not behind a probe — it is in every build — and one line is one *real* change
|
||||||
@@ -39,7 +39,8 @@ That second baseline used to be "exactly two, `gtk_widget_get_height` and `gtk_w
|
|||||||
from the dump asking a destroyed widget for its size", and tolerating them was a mistake: the dump
|
from the dump asking a destroyed widget for its size", and tolerating them was a mistake: the dump
|
||||||
runs from the destroy handler, so the widget pointer is **freed memory**, and a critical is only
|
runs from the destroy handler, so the widget pointer is **freed memory**, and a critical is only
|
||||||
the lucky reading of it. A run that had built and destroyed other windows first —
|
the lucky reading of it. A run that had built and destroyed other windows first —
|
||||||
`UDPSCOPE_PROBE_HELP_MS`, which opens three — has had that memory reused, and the same two calls
|
`UDPSCOPE_PROBE_HELP_MS`, which opens the About and shortcut dialogs and then closes them — has had
|
||||||
|
that memory reused, and the same two calls
|
||||||
then **SIGSEGV in the middle of the dump**, which reads as a truncated dump rather than as a
|
then **SIGSEGV in the middle of the dump**, which reads as a truncated dump rather than as a
|
||||||
crash in a debug line. Every size the dump prints is therefore captured on the *tick*, while the
|
crash in a debug line. Every size the dump prints is therefore captured on the *tick*, while the
|
||||||
window is alive (`App::dbg_*`), and **no `gtk_widget_*` call may appear in the dump** — the
|
window is alive (`App::dbg_*`), and **no `gtk_widget_*` call may appear in the dump** — the
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
<page xmlns="http://projectmallard.org/1.0/"
|
||||||
|
type="topic" style="task"
|
||||||
|
id="cursors">
|
||||||
|
|
||||||
|
<info>
|
||||||
|
<link type="guide" xref="index#analysis"/>
|
||||||
|
<link type="seealso" xref="measurements"/>
|
||||||
|
<desc>The shared vertical cursor pair and the per-row horizontal rulers, their four modes,
|
||||||
|
and how a reading is formatted.</desc>
|
||||||
|
<credit type="author copyright">
|
||||||
|
<name>Martino Ferrari</name>
|
||||||
|
</credit>
|
||||||
|
</info>
|
||||||
|
|
||||||
|
<title>Cursors</title>
|
||||||
|
|
||||||
|
<p>Cursors read values off the traces. There are two kinds: one <em>shared vertical
|
||||||
|
pair</em> that every row carries at the same time, and <em>horizontal rulers</em>, one pair
|
||||||
|
per row, that belong to that row alone.</p>
|
||||||
|
|
||||||
|
<section id="reach">
|
||||||
|
<title>How to reach it</title>
|
||||||
|
<steps>
|
||||||
|
<item>
|
||||||
|
<p>Press the cursors button in the toolbar, or <key>c</key>.</p>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<p>The pane below the plot shows the readings and owns a <gui>Reset</gui> button.
|
||||||
|
Drag a marker to move it.</p>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<p>The mode dropdown offers four modes: <gui>vertical</gui> (the shared time pair),
|
||||||
|
<gui>horizontal</gui> (the per-row rulers), <gui>both</gui>, and <gui>mix</gui>.</p>
|
||||||
|
</item>
|
||||||
|
</steps>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="knowing">
|
||||||
|
<title>Worth knowing</title>
|
||||||
|
<list>
|
||||||
|
<item><p>The vertical pair is <em>one</em> time for the whole figure, so moving it on
|
||||||
|
any row moves it on all of them. That is what makes <code>dt</code> meaningful across
|
||||||
|
rows.</p></item>
|
||||||
|
<item><p>A ruler belongs to its row and nothing shares it, so two rows' rulers are
|
||||||
|
independent.</p></item>
|
||||||
|
<item><p><gui>mix</gui> is the reading mode: one line per <em>bound</em> trace, with the
|
||||||
|
trace's own values at each marker and their difference. A slot with a name but no
|
||||||
|
channel behind it is not a trace and gets no line.</p></item>
|
||||||
|
<item><p>A value is the <em>nearest stored sample's</em>, never a number between two
|
||||||
|
samples. Zoom in past the sample spacing and the line between two samples is something
|
||||||
|
the plot drew, not data — so a reading off it would be the drawing. The hover crosshair
|
||||||
|
reads the same way.</p></item>
|
||||||
|
<item><p>Every reading carries a <em>metric prefix</em> so the digits sit in
|
||||||
|
<code>1..999</code>: <code>10.52 mV</code>, not <code>0.01 V</code>. Two decimals is all
|
||||||
|
an instrument landing on an <code>f32</code> sample can claim, so without the prefix a
|
||||||
|
small value would read <code>0.00</code> and the reading would be gone. A stream that
|
||||||
|
declares no unit gets the prefix anyway, written tight against the number
|
||||||
|
(<code>585.24m</code>) — the space separates the number from its unit, and a spaced
|
||||||
|
<code>m</code> with nothing after it would read as metres.</p></item>
|
||||||
|
<item><p>A marker past the newest sample reads <code>--</code>. With only one marker
|
||||||
|
over data the row reads <code>partial</code>: a difference between a number and nothing
|
||||||
|
is not a number.</p></item>
|
||||||
|
<item><p>In <gui>mix</gui> each marker also places a crossing annotation per trace,
|
||||||
|
which is what lets you see on the plot which sample a number came from. It is <em>on
|
||||||
|
that sample</em>, so when the marker falls between two it sits a little off its own
|
||||||
|
line: the line is where you put the marker, the dot is the datum being quoted. The hover
|
||||||
|
crosshair's mark snaps the same way, and is drawn larger than a trace's sample dots so
|
||||||
|
it does not read as one more sample.</p></item>
|
||||||
|
<item><p>The time row exists only in the modes that show the vertical pair.
|
||||||
|
<gui>horizontal</gui> has no time row, by design.</p></item>
|
||||||
|
<item><p>A cursor <em>stays where you put it</em> while a live view rolls under it,
|
||||||
|
which is what a bench scope does: its <code>T0</code> is an age, so a marker left at
|
||||||
|
<code>-1.5s</code> keeps reading the sample a second and a half old as the stream
|
||||||
|
advances. A measurement's marks do the opposite — they travel with the sample they
|
||||||
|
measured — because a cursor is a question you asked and a mark is an answer about one
|
||||||
|
particular sample.</p></item>
|
||||||
|
</list>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="example">
|
||||||
|
<title>Example</title>
|
||||||
|
<p>Three traces in <gui>mix</gui> mode with both markers over data: 3 lines and 6 crossing
|
||||||
|
marks, each line reading like <code>sine 0.31 V -0.84 V -1.15 V</code>. Move the second
|
||||||
|
marker past the newest sample and the third column reads <code>--</code>.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</page>
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
<page xmlns="http://projectmallard.org/1.0/"
|
||||||
|
type="topic" style="task"
|
||||||
|
id="export">
|
||||||
|
|
||||||
|
<info>
|
||||||
|
<link type="guide" xref="index#output"/>
|
||||||
|
<link type="seealso" xref="sessions"/>
|
||||||
|
<desc>Writing the visible range of every plotted trace to NumPy, CSV or MDF 3.30, and what
|
||||||
|
each format carries.</desc>
|
||||||
|
<credit type="author copyright">
|
||||||
|
<name>Martino Ferrari</name>
|
||||||
|
</credit>
|
||||||
|
</info>
|
||||||
|
|
||||||
|
<title>Saving and export</title>
|
||||||
|
|
||||||
|
<p>Export writes the <em>visible range</em> of every plotted trace to a file: NumPy
|
||||||
|
<file>.npy</file>, <file>.csv</file>, or <gui>MDF 3.30</gui> (<file>.mf3</file> or
|
||||||
|
<file>.mdf</file>), chosen by the extension you type.</p>
|
||||||
|
|
||||||
|
<section id="reach">
|
||||||
|
<title>How to reach it</title>
|
||||||
|
<steps>
|
||||||
|
<item>
|
||||||
|
<p>Pick <gui>Export visible range…</gui> in the overflow menu, or press
|
||||||
|
<keyseq><key>Ctrl</key><key>E</key></keyseq>.</p>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<p>A file chooser asks where; the format follows the path's extension.</p>
|
||||||
|
</item>
|
||||||
|
</steps>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="knowing">
|
||||||
|
<title>Worth knowing</title>
|
||||||
|
<list>
|
||||||
|
<item><p>The table is collected at the moment you click, not when the chooser returns. A
|
||||||
|
live view keeps scrolling while a dialog is up, so a late collection would write a window
|
||||||
|
you never saw.</p></item>
|
||||||
|
<item><p>The rows are the <gui>stored samples</gui>, never the LOD buckets the plot draws
|
||||||
|
from. A row count far below the span times the stored rate means something cut the table
|
||||||
|
from the pyramid.</p></item>
|
||||||
|
<item><p>The stored rate is not always the wire rate. A window long enough to need
|
||||||
|
decimation is stored averaged at a lower rate, and the export carries <em>that</em> —
|
||||||
|
read the status bar's ⓘ popover for the figure before trusting a sample count.</p></item>
|
||||||
|
<item><p>Two time columns. <code>time</code> is the display timeline — negative ages
|
||||||
|
ending at 0 while streaming, offsets from the trigger edge while an event is held — so it
|
||||||
|
is the x you read a value at on screen. <code>abs_seconds</code> is the source's own
|
||||||
|
clock for the same sample, which is what lets two exports taken in different modes line
|
||||||
|
up. Both are strictly increasing and evenly spaced; duplicate or backwards stamps would
|
||||||
|
mean the file held two clocks.</p></item>
|
||||||
|
<item><p>An export taken <em>mid-capture</em> writes the partly filled window that is on
|
||||||
|
screen, which is shorter than the event span. Export outside a capture if you want the
|
||||||
|
whole event.</p></item>
|
||||||
|
<item><p><gui>MDF</gui> exists for handing a window to another tool, and its shape follows
|
||||||
|
the contract that reader uses: one channel group, every channel record-interleaved, the
|
||||||
|
master channel as the x axis and the unit in the channel metadata. So there the
|
||||||
|
<em>master is <code>abs_seconds</code></em>, named <code>time</code> — the clock that is
|
||||||
|
the same in every display mode — and the display axis goes out beside it as an ordinary
|
||||||
|
channel, <code>time_relative</code>. Both are seconds; each signal carries the source's
|
||||||
|
own unit. That is also what <gui>ScryLab</gui> reads: channel name → signal name, master
|
||||||
|
→ x axis, unit → the signal's unit.</p></item>
|
||||||
|
</list>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="example">
|
||||||
|
<title>Example</title>
|
||||||
|
<p>A 2 s window at 10 kS/s over 3 traces writes 20001 rows and 5 columns
|
||||||
|
(<code>time</code>, <code>abs_seconds</code>, three signals). The <file>.npy</file> is
|
||||||
|
<code>64·⌈(10+len(header)+1)/64⌉ + rows·(16 + 4·cols)</code> bytes. Read it back with
|
||||||
|
<code>pd.DataFrame(np.load(path))</code>. The <file>.mf3</file> is bigger — a block per
|
||||||
|
channel plus a comment — <code>272 + comment + rows·record + 28 + channels·274 + 30</code>
|
||||||
|
bytes, with <code>record = 16 + 4·cols</code> and <code>channels = cols + 2</code> for the
|
||||||
|
master and the relative axis. Read that one back with <code>asammdf.MDF(path)</code> or in
|
||||||
|
ScryLab.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</page>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 152 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 129 KiB |
@@ -0,0 +1,67 @@
|
|||||||
|
<page xmlns="http://projectmallard.org/1.0/"
|
||||||
|
type="guide" style="task"
|
||||||
|
id="index">
|
||||||
|
|
||||||
|
<info>
|
||||||
|
<desc>udpscope plots a live UDPStreamer source as one figure of up to eight stacked
|
||||||
|
plot rows sharing a single time axis, with a trigger, cursors, measurements, math
|
||||||
|
channels and an export. This manual covers connecting, the window and its panels,
|
||||||
|
navigating and pausing the plot, the trigger and its event cache, analysis, and
|
||||||
|
saving and exporting.</desc>
|
||||||
|
</info>
|
||||||
|
|
||||||
|
<title>udpscope</title>
|
||||||
|
|
||||||
|
<section id="main">
|
||||||
|
<title>Getting started</title>
|
||||||
|
<list>
|
||||||
|
<item><p><link xref="overview">Overview</link> — what udpscope is, how it reaches a
|
||||||
|
source, the window and its panels, and a first five minutes.</p></item>
|
||||||
|
<item><p><link xref="source">Choosing a source</link> — the command line, the
|
||||||
|
chooser, and opening an exported waveform instead of a live stream.</p></item>
|
||||||
|
<item><p><link xref="signals">Signals and calibration</link> — how a signal becomes
|
||||||
|
a channel, what starts acquisition, and how a scale and offset are applied.</p></item>
|
||||||
|
<item><p><link xref="layout">Rows, layout and drag & drop</link> — up to eight
|
||||||
|
rows, four traces each, and the layout editor.</p></item>
|
||||||
|
</list>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="viewing">
|
||||||
|
<title>Viewing</title>
|
||||||
|
<list>
|
||||||
|
<item><p><link xref="window">Time window</link> — the one control that changes the
|
||||||
|
window, and the decimation a long window causes.</p></item>
|
||||||
|
<item><p><link xref="navigate">Navigating the plot</link> — panning, zooming and
|
||||||
|
fitting with the mouse and the keyboard.</p></item>
|
||||||
|
<item><p><link xref="pause">Pause and play</link> — freezing the display without
|
||||||
|
stopping the stream, and how long you have been looking.</p></item>
|
||||||
|
<item><p><link xref="trigger">Trigger</link> — the four trigger shapes, arming,
|
||||||
|
hold-off, the event cache, and searching the buffer.</p></item>
|
||||||
|
</list>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="analysis">
|
||||||
|
<title>Analysis</title>
|
||||||
|
<list>
|
||||||
|
<item><p><link xref="cursors">Cursors</link> — the shared vertical pair and the
|
||||||
|
per-row horizontal rulers.</p></item>
|
||||||
|
<item><p><link xref="measurements">Measurements</link> — a statistic of the visible
|
||||||
|
window, its on-plot marks, and its statistics line.</p></item>
|
||||||
|
<item><p><link xref="math">Math channels</link> — building a derived channel from a
|
||||||
|
graph of source nodes, operations and one output.</p></item>
|
||||||
|
</list>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="output">
|
||||||
|
<title>Output and state</title>
|
||||||
|
<list>
|
||||||
|
<item><p><link xref="export">Saving and export</link> — the visible range to
|
||||||
|
<file>.npy</file>, <file>.csv</file> or <file>.mf3</file>.</p></item>
|
||||||
|
<item><p><link xref="sessions">Sessions and settings</link> — the per-source
|
||||||
|
configuration files and what they carry.</p></item>
|
||||||
|
<item><p><link xref="status">Status bar and diagnostics</link> — the glance line, the
|
||||||
|
ⓘ popover, and the activity badge.</p></item>
|
||||||
|
</list>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</page>
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<page xmlns="http://projectmallard.org/1.0/"
|
||||||
|
type="topic" style="task"
|
||||||
|
id="layout">
|
||||||
|
|
||||||
|
<info>
|
||||||
|
<link type="guide" xref="index#main"/>
|
||||||
|
<link type="seealso" xref="signals"/>
|
||||||
|
<desc>How the figure's rows and traces are arranged, how to drop a signal onto a row, and
|
||||||
|
how the layout editor adds and removes rows.</desc>
|
||||||
|
<credit type="author copyright">
|
||||||
|
<name>Martino Ferrari</name>
|
||||||
|
</credit>
|
||||||
|
</info>
|
||||||
|
|
||||||
|
<title>Rows, layout and drag & drop</title>
|
||||||
|
|
||||||
|
<p>The figure is a stack of rows, 1 to 8 of them, each holding up to 4 traces. Rows share
|
||||||
|
one time axis and one pair of side margins, so their y labels line up, their data areas
|
||||||
|
start and end on the same two columns, and their windows cannot drift apart. The left
|
||||||
|
margin holds the y labels; the right one is empty, and is there so the newest tick's label
|
||||||
|
and the newest sample's ink have somewhere to land instead of sitting on the frame.</p>
|
||||||
|
|
||||||
|
<section id="reach">
|
||||||
|
<title>How to reach it</title>
|
||||||
|
<steps>
|
||||||
|
<item>
|
||||||
|
<p>Drag a signal's chip out of the side panel (<key>s</key>) and drop it on a row to
|
||||||
|
add a trace.</p>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<p>Press <key>l</key>, or pick <gui>Layout editor</gui> from the overflow menu, to add
|
||||||
|
and remove rows: a <gui>×</gui> appears at the top left of each row and a <gui>+</gui>
|
||||||
|
on each boundary between rows, including above the first and below the last.</p>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<p>The editor's own button in the top-right corner leaves it.</p>
|
||||||
|
</item>
|
||||||
|
</steps>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="knowing">
|
||||||
|
<title>Worth knowing</title>
|
||||||
|
<list>
|
||||||
|
<item><p>A plot's own <gui>right-click menu</gui> (the same compact rows as the app's
|
||||||
|
menu) holds the y-axis scale, the two fits, and <gui>Remove signal</gui>, which swaps
|
||||||
|
the menu to that plot's traces plus <gui>All traces on this plot</gui> — with a
|
||||||
|
<gui>Back</gui> row to return. That is how a trace comes off a row without going through
|
||||||
|
the layout editor.</p></item>
|
||||||
|
<item><p>A drop <em>adds</em> a trace to the row's next free slot; it does not replace
|
||||||
|
one. With four traces already on a row the drop is refused.</p></item>
|
||||||
|
<item><p>Inserting a row moves the rows below it <em>with their contents</em>. Inserting
|
||||||
|
above row 1 puts what row 1 plotted onto row 2 and leaves the new row empty — the rows
|
||||||
|
are not renumbered under you.</p></item>
|
||||||
|
<item><p>A trace is remembered by the signal's <em>name</em>, not by its channel number,
|
||||||
|
because a new CONFIG or an enable renumbers the channels and an index would silently
|
||||||
|
re-point the trace.</p></item>
|
||||||
|
<item><p>Adding or removing a row rebuilds the figure, which is cheap, but it also
|
||||||
|
re-deals every trace. A frozen trigger event survives the rebuild: the pinned window is
|
||||||
|
restated rather than re-framed on the live edge.</p></item>
|
||||||
|
</list>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="example">
|
||||||
|
<title>Example</title>
|
||||||
|
<p>With three traces on row 1, pressing <key>l</key> shows 3 buttons (one <gui>×</gui>,
|
||||||
|
two <gui>+</gui>). Clicking the <gui>+</gui> above row 1 gives 2 rows and 5 buttons, with
|
||||||
|
the three traces now on row 2 — the layout probe reports exactly this as
|
||||||
|
<code>rows=2 buttons=5 traces=[0:-|1:sine,ramp,noise]</code>.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</page>
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
<page xmlns="http://projectmallard.org/1.0/"
|
||||||
|
type="topic" style="task"
|
||||||
|
id="math">
|
||||||
|
|
||||||
|
<info>
|
||||||
|
<link type="guide" xref="index#analysis"/>
|
||||||
|
<link type="seealso" xref="signals"/>
|
||||||
|
<desc>Building a derived channel from a graph of source nodes, operations and one output,
|
||||||
|
and how the graph is evaluated.</desc>
|
||||||
|
<credit type="author copyright">
|
||||||
|
<name>Martino Ferrari</name>
|
||||||
|
</credit>
|
||||||
|
</info>
|
||||||
|
|
||||||
|
<title>Math channels</title>
|
||||||
|
|
||||||
|
<p>A math channel is a signal computed from other signals. You build it as a small graph —
|
||||||
|
source nodes, operations, one output — and udpscope then treats the result as an ordinary
|
||||||
|
channel.</p>
|
||||||
|
|
||||||
|
<section id="reach">
|
||||||
|
<title>How to reach it</title>
|
||||||
|
<steps>
|
||||||
|
<item>
|
||||||
|
<p>Press the math button in the toolbar, or <key>g</key>.</p>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<p>The pane's <gui>+</gui> adds a channel, and opening one shows the graph editor:
|
||||||
|
nodes as boxes, click a box to select it and edit its parameters beside the canvas.</p>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<p>Inside the editor, <gui>add node</gui> is a menu button whose popover picks the op,
|
||||||
|
so the node arrives as the operation you asked for.</p>
|
||||||
|
</item>
|
||||||
|
</steps>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="gestures">
|
||||||
|
<title>Graph gestures</title>
|
||||||
|
<p><gui>drag</gui> from an output port to an input port to wire them; drag a wired input
|
||||||
|
port off its connection to detach it; click a wire and press <key>Delete</key> to remove
|
||||||
|
it; drag a box to move it. <gui>Wheel</gui> zooms about the pointer. <gui>Drag the
|
||||||
|
background</gui> to pan. <key>f</key> fits all nodes into view;
|
||||||
|
<keyseq><key>Ctrl</key><key>0</key></keyseq> resets the view. <gui>tidy</gui>, the button
|
||||||
|
beside <gui>fit</gui> in the editor, lays the graph out in topological columns and frames
|
||||||
|
it. The orange dot marks the output node; click <gui>set as output</gui> to move it.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="knowing">
|
||||||
|
<title>Worth knowing</title>
|
||||||
|
<list>
|
||||||
|
<item><p>The graph is evaluated as an ordered list, so a node may only read ones
|
||||||
|
<em>above</em> it. A wire drawn right to left re-sorts the list silently so the graph
|
||||||
|
stays valid. A cycle is unrepresentable: the connection is refused rather than created,
|
||||||
|
and the line beneath the form says why — a loop, or a node asked to read itself.</p></item>
|
||||||
|
<item><p>A derived channel <em>is</em> a channel. It appears in the signal panel, can be
|
||||||
|
plotted, measured, exported, and used as a source for another math channel.</p></item>
|
||||||
|
<item><p>The math runs on the <em>stored</em> samples — decimated and averaged — so a
|
||||||
|
cutoff you type in Hz means the rate the filter will actually run at. Change the window
|
||||||
|
across a decimation boundary and the coefficients are rebuilt for the new rate.</p></item>
|
||||||
|
<item><p>A source may only name an <em>acquired</em> channel. The stream's timestamp
|
||||||
|
signal is not one: a graph sourced from it validates and then reads NaN for ever, so the
|
||||||
|
dropdown does not offer it.</p></item>
|
||||||
|
<item><p>The output defaults to the <em>last</em> node, and a node added to a linear
|
||||||
|
graph becomes the output. Choosing an earlier node explicitly is respected.</p></item>
|
||||||
|
<item><p>Every numeric field starts at its own identity value — gain 1, offset 0 — not
|
||||||
|
at the bottom of its range.</p></item>
|
||||||
|
</list>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="why">
|
||||||
|
<title>Why an operation sometimes does nothing</title>
|
||||||
|
<p>A graph's <em>output</em> is the node whose value becomes the channel, and it is drawn
|
||||||
|
as the orange dot. A new graph is built source, then operation, and the last node added
|
||||||
|
becomes the output — but a graph whose output is left on the source is valid and simply
|
||||||
|
passes its input through, which is the usual reason a gain or a filter appears to have no
|
||||||
|
effect. <gui>Set as output</gui> moves the dot.</p>
|
||||||
|
|
||||||
|
<p><gui>Apply</gui> is insensitive while the graph is incomplete. A line beneath the form
|
||||||
|
says why: an input that is not connected, no output chosen, or a <em>channel</em> node
|
||||||
|
naming a signal the scope is not acquiring. Only the channels being acquired are offered,
|
||||||
|
because a source that is not acquired reads as no data for ever — and a graph that reads
|
||||||
|
NaN is harder to diagnose than one that would not save.</p>
|
||||||
|
|
||||||
|
<p>The filters are real filters. A math channel's samples are produced as the stream
|
||||||
|
arrives, once per stored slot, so a low pass carries the history a filter needs. A cutoff
|
||||||
|
typed in hertz is against the <em>stored</em> rate, which is the wire rate until a wide
|
||||||
|
window makes the buffer decimate — the status line's rate is that number, and the filters
|
||||||
|
are rebuilt whenever it changes.</p>
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<title>The math channel editor: nodes as boxes on a canvas, the orange dot marking the
|
||||||
|
output, and the selected node's parameters beside it.</title>
|
||||||
|
<media type="image" mime="image/png" src="figures/math-editor.png" width="938" height="420">
|
||||||
|
<p>A sine source wired through a gain into a filter, with the output on the last
|
||||||
|
node.</p>
|
||||||
|
</media>
|
||||||
|
</figure>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="example">
|
||||||
|
<title>Example</title>
|
||||||
|
<p><code>[sine] → [gain x2]</code> gives a channel reading <code>y=[-2, 2]</code>. A
|
||||||
|
100 Hz low pass on the same source passes 50 Hz at 0.97 and 2 kHz at 0.002; a 1 kHz band
|
||||||
|
pass with Q=2 passes 200 Hz at 0.05, 1 kHz at 0.95 and 4 kHz at 0.05. Each derived channel
|
||||||
|
must hold exactly as many samples as the shared clock has time slots.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</page>
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
<page xmlns="http://projectmallard.org/1.0/"
|
||||||
|
type="topic" style="task"
|
||||||
|
id="measurements">
|
||||||
|
|
||||||
|
<info>
|
||||||
|
<link type="guide" xref="index#analysis"/>
|
||||||
|
<link type="seealso" xref="cursors"/>
|
||||||
|
<desc>A measurement is a statistic of the visible window of one signal, with optional
|
||||||
|
on-plot marks and a per-measurement statistics line.</desc>
|
||||||
|
<credit type="author copyright">
|
||||||
|
<name>Martino Ferrari</name>
|
||||||
|
</credit>
|
||||||
|
</info>
|
||||||
|
|
||||||
|
<title>Measurements</title>
|
||||||
|
|
||||||
|
<p>A measurement is a statistic of the <em>visible window</em> of one signal, recomputed as
|
||||||
|
that window moves. The pane lists them with their values; each can also draw its result on
|
||||||
|
the plot.</p>
|
||||||
|
|
||||||
|
<section id="reach">
|
||||||
|
<title>How to reach it</title>
|
||||||
|
<steps>
|
||||||
|
<item>
|
||||||
|
<p>Press the measurements button in the toolbar, or <key>m</key>.</p>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<p>The pane's <gui>+</gui> adds one; clicking a row opens the dialog where the signal,
|
||||||
|
the type, the threshold share and the on-plot marks are set, with <gui>Save</gui> and
|
||||||
|
<gui>Delete</gui>.</p>
|
||||||
|
</item>
|
||||||
|
</steps>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="knowing">
|
||||||
|
<title>Worth knowing</title>
|
||||||
|
<list>
|
||||||
|
<item><p>The types are average, top, bottom, min, max, peak to peak, amplitude, pulse
|
||||||
|
duration, period and frequency. <em>top</em> and <em>bottom</em> are the means of the
|
||||||
|
upper and lower halves, which is why a unit sine reads 0.637 (2/π) rather than 1.</p></item>
|
||||||
|
<item><p>The on-plot marks come in two kinds and the mix is the reading: a <em>level</em>
|
||||||
|
mark is a ruler at a value, a <em>time</em> mark is a marker at a position. An average
|
||||||
|
draws one level mark; a max draws a level mark <em>and</em> a time mark at the first
|
||||||
|
sample that reached it.</p></item>
|
||||||
|
<item><p>Marks cannot be dragged. A measurement mark says what the measurement measured,
|
||||||
|
so it is created unselectable — as is the trigger marker.</p></item>
|
||||||
|
<item><p>A pass is throttled to a cadence rather than run every frame, so a ruler over a
|
||||||
|
live view is <em>stepped</em>. The number and the ruler come from the same pass, so they
|
||||||
|
always agree.</p></item>
|
||||||
|
<item><p>An invalid pass keeps its <em>level</em> marks and drops its <em>time</em>
|
||||||
|
marks. A level is a property of the signal and outlives the window; a position inside
|
||||||
|
the window does not. That is what stops the rulers blinking off at a mode change.</p></item>
|
||||||
|
<item><p>A crossing is counted with <em>hysteresis</em>, not on a bare level: the
|
||||||
|
thresholds are a tenth of the window's peak to peak either side of the share, and an
|
||||||
|
edge is counted only once the signal has passed the far one. Period, frequency and pulse
|
||||||
|
duration all depend on it. The mid level is exactly where a waveform moves fastest, so
|
||||||
|
any interferer faster than the fundamental recrosses a single level several times per
|
||||||
|
real crossing — a 5 Hz sine with a 500 Hz tone at a tenth of the amplitude reads 39 Hz
|
||||||
|
without the band and 5 Hz with it. The band holds to an interferer a third of the
|
||||||
|
waveform's own size.</p></item>
|
||||||
|
<item><p>The band is clamped to stay inside the data, which is what keeps a threshold set
|
||||||
|
near an extreme usable: a fixed band there would sit above every sample and the
|
||||||
|
measurement would read nothing. A share near an extreme still leaves the band little
|
||||||
|
room, so it rejects less — which is one reason period and frequency offer no share to
|
||||||
|
set.</p></item>
|
||||||
|
<item><p>A window with too few crossings reads <code>invalid</code> / <code>--</code>
|
||||||
|
rather than a plausible invented number. Frequency over a window holding one edge is the
|
||||||
|
usual case.</p></item>
|
||||||
|
<item><p><gui>frequency</gui> is reported in Hz and <gui>period</gui>/<gui>pulse
|
||||||
|
duration</gui> in seconds; every other type carries the channel's own unit. A value is
|
||||||
|
written with a <em>metric prefix</em> so its digits sit in <code>1..999</code> —
|
||||||
|
<code>636.62 mV</code>, not <code>0.64 V</code> — including when the stream declares no
|
||||||
|
unit at all, which is always the case for a math channel: there the prefix goes tight
|
||||||
|
against the number (<code>636.62m</code>), since two decimals of an unprefixed small
|
||||||
|
value is <code>0.00</code>.</p></item>
|
||||||
|
<item><p>Cost is bounded by reading the summary pyramid the producer already maintains,
|
||||||
|
not the raw window. The values are still exact: a bucket's sum is a sum.</p></item>
|
||||||
|
<item><p><gui>Statistics</gui> adds a second line to every measurement showing
|
||||||
|
<code>n</code>, the mean, the standard deviation and the range. Each observation is one
|
||||||
|
<em>recompute</em> of the pane, so <code>n</code> counts windows measured, not samples —
|
||||||
|
a mean frequency over many windows is what that claims to be. <gui>Reset</gui> clears
|
||||||
|
every accumulator and starts fresh. A change of display window, a CONFIG or a
|
||||||
|
measurement edit also resets; a pause, a resume, a zoom, a pan and the toggle itself do
|
||||||
|
not, so turning the line off to read the plot and back on again keeps the history.</p></item>
|
||||||
|
</list>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="example">
|
||||||
|
<title>Example</title>
|
||||||
|
<p>On a 1 Hz ±1 sine: <code>average=0</code>, <code>top=0.636620</code>,
|
||||||
|
<code>bottom=-0.636620</code>, <code>max=1</code>, <code>min=-1</code>,
|
||||||
|
<code>peak to peak=2</code>, <code>amplitude=1</code>, <code>pulse duration=0.5 s</code>
|
||||||
|
(duty 0.5), <code>period=1 s</code>, <code>frequency=1 Hz</code>. Over a 2 M-sample window
|
||||||
|
those two passes read about 0.1% of the samples.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</page>
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
<page xmlns="http://projectmallard.org/1.0/"
|
||||||
|
type="topic" style="task"
|
||||||
|
id="navigate">
|
||||||
|
|
||||||
|
<info>
|
||||||
|
<link type="guide" xref="index#viewing"/>
|
||||||
|
<link type="seealso" xref="window"/>
|
||||||
|
<desc>Panning, zooming and fitting the plot with the mouse and the keyboard, and what a
|
||||||
|
pinned view refuses to do.</desc>
|
||||||
|
<credit type="author copyright">
|
||||||
|
<name>Martino Ferrari</name>
|
||||||
|
</credit>
|
||||||
|
</info>
|
||||||
|
|
||||||
|
<title>Navigating the plot</title>
|
||||||
|
|
||||||
|
<p>The plot pans and zooms with the mouse and with the keyboard. Every gesture acts on the
|
||||||
|
row the pointer is over, and because the rows share one time axis, an x change moves all of
|
||||||
|
them.</p>
|
||||||
|
|
||||||
|
<section id="reach">
|
||||||
|
<title>How to reach it</title>
|
||||||
|
<terms>
|
||||||
|
<item>
|
||||||
|
<title>Wheel</title>
|
||||||
|
<p><gui>Wheel</gui> zooms both axes · <gui>Shift</gui>+<gui>Wheel</gui> zooms x only ·
|
||||||
|
<gui>Ctrl</gui>+<gui>Wheel</gui> zooms y only.</p>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title>Drag</title>
|
||||||
|
<p><gui>Drag</gui> pans · <gui>Shift</gui>+<gui>Drag</gui> is a box zoom on both axes ·
|
||||||
|
<gui>Ctrl</gui>+<gui>Drag</gui> on x only · <gui>Alt</gui>+<gui>Drag</gui> on values
|
||||||
|
only. The area being chosen is framed while you drag it, so you can see what the
|
||||||
|
release will apply.</p>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title>Keyboard</title>
|
||||||
|
<p><key>+</key> and <key>-</key> zoom both axes; <key>Ctrl</key> with them restricts
|
||||||
|
to x, <key>Alt</key> to y. <gui>Arrow keys</gui> pan, <key>f</key> fits,
|
||||||
|
<key>Escape</key> cancels a gesture in progress.</p>
|
||||||
|
</item>
|
||||||
|
</terms>
|
||||||
|
|
||||||
|
<p>The full table is in the shortcut overview (<keyseq><key>Ctrl</key><key>/</key></keyseq>).</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="knowing">
|
||||||
|
<title>Worth knowing</title>
|
||||||
|
<list>
|
||||||
|
<item><p>The keyboard zoom's modifiers are <em>not</em> the wheel's, and that is
|
||||||
|
deliberate: on common layouts <key>+</key> <em>is</em> <keyseq><key>Shift</key><key>=</key></keyseq>,
|
||||||
|
so a bare zoom-in already arrives with Shift set and Shift could not mean anything.
|
||||||
|
Ctrl/Alt mirror the box-zoom drags instead.</p></item>
|
||||||
|
<item><p>A <em>pinned</em> view is navigable within its own data but cannot leave it.
|
||||||
|
While an event is held, a capture is filling, or the view is paused, a zoom <em>in</em>
|
||||||
|
works normally — "not outside the event" is not "not readable" — but a pan or a zoom out
|
||||||
|
is clamped to the data.</p></item>
|
||||||
|
<item><p>Nothing you do on the plot changes the <em>time window</em> — only the header
|
||||||
|
control does. A zoom or a pan moves the view inside the window; <key>f</key> (and
|
||||||
|
<gui>Fit X axis</gui>) is the <em>undo</em> of a zoom, so a 2 s window zoomed into reads
|
||||||
|
2 s again. The window is more than the axis: it is also the trigger's event span and how
|
||||||
|
the samples are stored, so a wheel notch that redefined it would re-plan the buffer and
|
||||||
|
drop your history.</p></item>
|
||||||
|
<item><p>The axis does not scroll — the <em>data</em> does. A live axis is a fixed
|
||||||
|
<code>[-window .. 0]</code> with the newest sample at 0, so the labels stand still and
|
||||||
|
the trace rolls under them. The cursors stand still with the labels (they are positions
|
||||||
|
on screen, so a cursor you placed stays where you put it, over newer data each frame),
|
||||||
|
while a measurement's marks travel with the sample they measured and leave the view when
|
||||||
|
it does.</p></item>
|
||||||
|
<item><p>A live view is clamped to the window you asked for, not to what the ring
|
||||||
|
happens to hold. That is why a buffer that has only just started filling leaves its
|
||||||
|
left-hand side empty instead of squeezing the window down to the first few
|
||||||
|
samples.</p></item>
|
||||||
|
<item><p>Zooming hard into a held capture is exact. The renderer finds the visible
|
||||||
|
samples by binary search over the real timestamps, never over a nominal sample grid, so
|
||||||
|
a capture whose clock wandered does not drift away from its axis as you zoom.</p></item>
|
||||||
|
<item><p>A trace holding far more samples than the plot has pixels is drawn <em>one
|
||||||
|
pixel column at a time</em>: each column is a single vertical line from the lowest to
|
||||||
|
the highest value that falls in it. Drawn as a polyline instead, such a window puts
|
||||||
|
three or four near-vertical lines in every column at slightly different sub-pixel
|
||||||
|
positions, and how they overlap beats against the pixel grid — which reads as bands of
|
||||||
|
light and dark that <em>crawl</em> as you pan. One line per column removes the beat, and
|
||||||
|
it is also much faster: a 1 MS/s view costs a couple of thousand line segments a frame
|
||||||
|
instead of a couple of hundred thousand.</p></item>
|
||||||
|
<item><p>Those columns are <em>graded by brightness</em>, the way a digital-phosphor
|
||||||
|
scope reads: bright where the signal spent its time, faint where it only passed through.
|
||||||
|
A column's highest and lowest value are the same whether the samples in it are spread
|
||||||
|
evenly or piled at its two ends, so the grading is what tells the two apart — the flat
|
||||||
|
parts of a square wave come out solid and the edges between them faint. It is read from
|
||||||
|
a finer division of each column rather than from the line that is drawn, and it costs
|
||||||
|
about a millisecond a frame.</p></item>
|
||||||
|
</list>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="example">
|
||||||
|
<title>Example</title>
|
||||||
|
<p>Pause a live view, then press <keyseq><key>Ctrl</key><key>+</key></keyseq>: x narrows
|
||||||
|
and y does not move. Press <keyseq><key>Alt</key><key>+</key></keyseq>: y narrows and x
|
||||||
|
does not. Eight doublings into a held 2 s capture, the trace still reaches both margins —
|
||||||
|
the zoom probe reports <code>gap_px</code> and <code>end_px</code> within a pixel or two
|
||||||
|
at every step.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</page>
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
<page xmlns="http://projectmallard.org/1.0/"
|
||||||
|
type="topic" style="task"
|
||||||
|
id="overview">
|
||||||
|
|
||||||
|
<info>
|
||||||
|
<link type="guide" xref="index#main"/>
|
||||||
|
<desc>What udpscope plots, the four parts of the window, and one pass through the
|
||||||
|
application from connecting to saving.</desc>
|
||||||
|
<credit type="author copyright">
|
||||||
|
<name>Martino Ferrari</name>
|
||||||
|
</credit>
|
||||||
|
</info>
|
||||||
|
|
||||||
|
<title>Overview</title>
|
||||||
|
|
||||||
|
<section id="what">
|
||||||
|
<title>What udpscope is</title>
|
||||||
|
<p>udpscope plots a live UDPStreamer source. One window holds up to eight rows of plots,
|
||||||
|
each with up to four traces, and every row shares one time axis, so the whole figure
|
||||||
|
scrolls together. Acquisition runs on its own thread and writes a ring buffer sized from
|
||||||
|
<cmd>--buffer-mb</cmd>; the drawing thread reads whatever that ring holds when a frame
|
||||||
|
starts, which is why a trace never makes the window stutter.</p>
|
||||||
|
|
||||||
|
<p>The stream is the clock. Every interval you read on screen is measured between
|
||||||
|
timestamps the source sent, never your machine's, so a stalled source freezes the view
|
||||||
|
instead of scrolling it — and a source whose clock steps still gets the window you asked
|
||||||
|
for.</p>
|
||||||
|
|
||||||
|
<p>The time axis is <em>relative to now</em>. While streaming it is a fixed
|
||||||
|
<code>[-window .. 0]</code> with the newest sample at 0, so x reads as an age:
|
||||||
|
<code>-1.5s</code> is a sample a second and a half old. In trigger mode 0 is the event's
|
||||||
|
edge instead, and a paused view keeps the ages it was frozen with. The source's own clock
|
||||||
|
is still there when you need it — an export writes it as a second column.</p>
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<title>The udpscope window on a live source: the header bar above the plots, the side
|
||||||
|
panel on the right, and the status bar along the bottom.</title>
|
||||||
|
<media type="image" mime="image/png" src="figures/main-window.png" width="1280" height="800">
|
||||||
|
<p>One figure of three rows with four signals enabled, the Measure tab open in the
|
||||||
|
side panel, and the status bar reading <gui>live</gui>.</p>
|
||||||
|
</media>
|
||||||
|
</figure>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="reach">
|
||||||
|
<title>How to reach it</title>
|
||||||
|
<p>Start the app with a source
|
||||||
|
(<cmd>udpscope --server 127.0.0.1 --port 8000</cmd>), or start it with no
|
||||||
|
<cmd>--server</cmd> and pick one from the chooser, which lists the sessions you have
|
||||||
|
opened before. A unicast source serves one client at a time: if a second viewer connects,
|
||||||
|
they take turns and the status bar says so. To watch one source from several instances,
|
||||||
|
use <cmd>--multicast</cmd> with the group and <cmd>--interface</cmd> with the local
|
||||||
|
address.</p>
|
||||||
|
|
||||||
|
<p>The signals arrive <em>disabled</em>. Nothing is acquired until you turn one on in the
|
||||||
|
signal list, and enabling is what starts acquisition — so a freshly opened scope with an
|
||||||
|
idle side panel is working correctly, not failing to connect.</p>
|
||||||
|
|
||||||
|
<note style="tip">
|
||||||
|
<p>See <link xref="source">Choosing a source</link> for the chooser and the multicast
|
||||||
|
options in full.</p>
|
||||||
|
</note>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="window">
|
||||||
|
<title>The window</title>
|
||||||
|
<terms>
|
||||||
|
<item>
|
||||||
|
<title>The header bar</title>
|
||||||
|
<p>Left to right: the time window (<gui>2 s</gui>), the trigger and the pause, the
|
||||||
|
events, cursors and side-panel switches, and the menu, which holds
|
||||||
|
<gui>Fit to data</gui>, the <gui>Layout editor</gui>, <gui>Export</gui>,
|
||||||
|
<gui>Keyboard shortcuts</gui>, <gui>Help</gui> and <gui>About</gui>.</p>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title>The side panel</title>
|
||||||
|
<p>Two tabs. <gui>Measure</gui> holds the cursors readout and the measurements
|
||||||
|
derived from it; <gui>Signals</gui> holds the trigger and the channel list, where a
|
||||||
|
math channel is a row of that one list under a <gui>MATH</gui> heading whose
|
||||||
|
<gui>+</gui> creates one. One row per channel: its colour, an enable switch, its name
|
||||||
|
and its unit. Channels are dragged from here onto a plot row.</p>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title>The plot</title>
|
||||||
|
<p>Up to eight rows, each with its own y axis and up to four traces, all on one shared
|
||||||
|
time axis. Right-click a row for its own menu (fit, add a signal, remove a trace).</p>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title>The status bar</title>
|
||||||
|
<p>Directly above the plot's bottom edge — the state word, the stored sample rate and
|
||||||
|
the window, and the ⓘ beside it for packets, gaps and the buffer's own figures. The
|
||||||
|
second screenshot below is what a held capture looks like: <gui>event held</gui>, the
|
||||||
|
axis pinned to the event, and the trigger marker at the edge.</p>
|
||||||
|
</item>
|
||||||
|
</terms>
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<title>A held capture: the trigger has latched, the state reads
|
||||||
|
<gui>event held</gui>, and the axis is pinned to the event rather than to the live
|
||||||
|
edge.</title>
|
||||||
|
<media type="image" mime="image/png" src="figures/trigger-capture.png" width="1280" height="800">
|
||||||
|
<p>The trigger marker sits at x = 0 and the pre-trigger share of the window is drawn
|
||||||
|
to its left.</p>
|
||||||
|
</media>
|
||||||
|
</figure>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="first">
|
||||||
|
<title>The first five minutes</title>
|
||||||
|
<steps>
|
||||||
|
<item>
|
||||||
|
<p><em>Connect.</em> With no <cmd>--server</cmd>, pick a stored session or type a host
|
||||||
|
and port.</p>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<p><em>Enable a signal</em> in the signal list. The plot stays empty for the first
|
||||||
|
fraction of a second while the first window fills — the buffer holds only what has
|
||||||
|
arrived.</p>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<p><em>Drag it onto a row.</em> Drop a second signal on another row, or the same
|
||||||
|
signal again for a second view of it. The colour is per signal, so it is the same one
|
||||||
|
wherever it appears.</p>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<p><em>Set the window</em> with the control in the header bar: type a number, pick a
|
||||||
|
unit, or step a decade. The trigger's event window is a share of this span, so a
|
||||||
|
window change moves it.</p>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<p><em>Pause</em> (<key>space</key>) to look at a still picture; the producer keeps
|
||||||
|
filling the buffer behind it, and resuming shows what arrived meanwhile rather than a
|
||||||
|
hole.</p>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<p><em>Arm the trigger</em> (<key>a</key>) and wait for a latch: the view freezes on
|
||||||
|
one event, and you can zoom in on it. Disarming mid-capture keeps what the capture
|
||||||
|
got.</p>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<p><em>Measure it</em> — the measurements pane computes a statistic of the visible
|
||||||
|
window, and <gui>show on plot</gui> marks it as a ruler or a pair of markers.</p>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<p><em>Save the data</em> with <gui>Export visible range</gui>, which writes the
|
||||||
|
plotted signals over the window at the stored rate to <file>.npy</file>,
|
||||||
|
<file>.csv</file> or <file>.mf3</file> (MDF 3.30).</p>
|
||||||
|
</item>
|
||||||
|
</steps>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="knowing">
|
||||||
|
<title>Worth knowing</title>
|
||||||
|
<list>
|
||||||
|
<item><p>A signal nobody enables costs nothing — no buffer, no socket work, no
|
||||||
|
measurements.</p></item>
|
||||||
|
<item><p>The window remembers itself. Rows, traces, the time window, calibration, which
|
||||||
|
signals were enabled, the math channels with their graphs, and the measurements are
|
||||||
|
written per host and port, and reloaded the next time you open that source.</p></item>
|
||||||
|
<item><p>A very wide window on a fast stream is drawn from averaged groups of samples,
|
||||||
|
so the rate in the status bar falls: that is the anti-alias filter, not a fault —
|
||||||
|
striding would fold a fast signal back as a slow one, which looks entirely plausible on
|
||||||
|
screen.</p></item>
|
||||||
|
<item><p>Every instrument the app computed — the trigger marker, the measurement marks
|
||||||
|
— is locked to the pointer's reach. The cursors are yours to drag; those are not.</p></item>
|
||||||
|
</list>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</page>
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<page xmlns="http://projectmallard.org/1.0/"
|
||||||
|
type="topic" style="task"
|
||||||
|
id="pause">
|
||||||
|
|
||||||
|
<info>
|
||||||
|
<link type="guide" xref="index#viewing"/>
|
||||||
|
<link type="seealso" xref="trigger"/>
|
||||||
|
<desc>Pausing freezes the display without stopping the stream, and the status bar says how
|
||||||
|
long you have been paused.</desc>
|
||||||
|
<credit type="author copyright">
|
||||||
|
<name>Martino Ferrari</name>
|
||||||
|
</credit>
|
||||||
|
</info>
|
||||||
|
|
||||||
|
<title>Pause and play</title>
|
||||||
|
|
||||||
|
<p>Pause freezes the <em>display</em>, not the acquisition. The producer keeps filling the
|
||||||
|
ring at full rate; the view binds an immutable snapshot of the window that was on screen,
|
||||||
|
and the axis is pinned to it.</p>
|
||||||
|
|
||||||
|
<section id="reach">
|
||||||
|
<title>How to reach it</title>
|
||||||
|
<steps>
|
||||||
|
<item>
|
||||||
|
<p>Press the pause button in the toolbar, or <key>Space</key>.</p>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<p>Press it again to resume, which rebinds the live ring and puts the window back on
|
||||||
|
the newest data.</p>
|
||||||
|
</item>
|
||||||
|
</steps>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="knowing">
|
||||||
|
<title>Worth knowing</title>
|
||||||
|
<list>
|
||||||
|
<item><p>Nothing is lost while paused. Because the producer never stops, resuming shows
|
||||||
|
current data with no hole in the history behind it.</p></item>
|
||||||
|
<item><p>The axis is pinned to the <em>snapshot's own stamps</em>, not to the window that
|
||||||
|
was requested. A window reaching back past the oldest retained sample yields a shorter
|
||||||
|
snapshot, and the pinned axis is that shorter span — so you see a narrower view rather
|
||||||
|
than an empty margin.</p></item>
|
||||||
|
<item><p>A paused view is pinned exactly as a held trigger event is: zoom in freely, but
|
||||||
|
a pan or a zoom out is clamped to the snapshot's data.</p></item>
|
||||||
|
<item><p>Pausing is the easiest way to measure a layout or read a cursor precisely,
|
||||||
|
because nothing under the pointer moves between frames.</p></item>
|
||||||
|
<item><p>The status bar says how long you have been paused — wall time, since the stream
|
||||||
|
itself never stopped. It is there so a frozen picture cannot be mistaken for a live one
|
||||||
|
that went quiet.</p></item>
|
||||||
|
</list>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="example">
|
||||||
|
<title>Example</title>
|
||||||
|
<p>Pause with a 2 s window over a ring holding 1.4 s: the frozen axis reads
|
||||||
|
<code>[-1.40 .. 0.00]</code> rather than <code>[-2.00 .. 0.00]</code> with 0.6 s of blank.
|
||||||
|
The pause probe reports <code>ring_produced</code> still growing across the pause and
|
||||||
|
<code>event_size</code> non-zero exactly while paused.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</page>
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
<page xmlns="http://projectmallard.org/1.0/"
|
||||||
|
type="topic" style="task"
|
||||||
|
id="sessions">
|
||||||
|
|
||||||
|
<info>
|
||||||
|
<link type="guide" xref="index#output"/>
|
||||||
|
<link type="seealso" xref="source"/>
|
||||||
|
<desc>Per-source configuration files: where they live, what they persist, and how the
|
||||||
|
chooser and the Configurations dialog use them.</desc>
|
||||||
|
<credit type="author copyright">
|
||||||
|
<name>Martino Ferrari</name>
|
||||||
|
</credit>
|
||||||
|
</info>
|
||||||
|
|
||||||
|
<title>Sessions and settings</title>
|
||||||
|
|
||||||
|
<p>udpscope stores named sessions — <em>configurations</em> — per source, so reopening a
|
||||||
|
source gives you the scope you closed, and one source can hold several arrangements for
|
||||||
|
different experiments. There is no global preferences file: the session <em>is</em> the
|
||||||
|
settings.</p>
|
||||||
|
|
||||||
|
<section id="reach">
|
||||||
|
<title>How to reach it</title>
|
||||||
|
<p>Nothing to open — the file is written as you work. Its path is in the About dialog, and
|
||||||
|
the directory is <file>$XDG_CONFIG_HOME/udpscope</file>, or <file>~/.config/udpscope</file>
|
||||||
|
when that is unset. Each source has a directory <file><host>_<port>/</file>
|
||||||
|
holding one <file><name>.toml</file> per configuration and an <file>active</file>
|
||||||
|
file naming the one it is on.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="configs">
|
||||||
|
<title>Configurations</title>
|
||||||
|
<p><gui>Configurations…</gui> in the menu lists the running source's configurations: click
|
||||||
|
a row to open it, <gui>×</gui> to delete it, or type a name and a note and press
|
||||||
|
<gui>Save this session</gui> to keep the current arrangement under a new one. Opening a
|
||||||
|
configuration saves the current session first, so switching never loses what it switches
|
||||||
|
from.</p>
|
||||||
|
|
||||||
|
<p>The startup chooser (start with no <cmd>--server</cmd>) lists one row per configuration,
|
||||||
|
with its note and its settings, so two arrangements of one source are told apart before
|
||||||
|
they open.</p>
|
||||||
|
|
||||||
|
<p>A configuration also carries the settings that can only be decided when the buffers are
|
||||||
|
allocated: the <gui>buffer size</gui> (<cmd>--buffer-mb</cmd>), relative time, and the
|
||||||
|
packet capture's ring size. Opening a configuration from the chooser applies all three;
|
||||||
|
switching inside a running app applies the rest and says in the status line that these take
|
||||||
|
effect on the next start.</p>
|
||||||
|
|
||||||
|
<p>The chooser's form sets the first two for a new host, and its pencil button edits them
|
||||||
|
on an existing configuration — along with the name and the note. Renaming a configuration,
|
||||||
|
or editing the address it points at, moves it: the old file is removed rather than left
|
||||||
|
behind.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="knowing">
|
||||||
|
<title>Worth knowing</title>
|
||||||
|
<list>
|
||||||
|
<item><p>What a configuration persists: the time window, the rows and each row's slot
|
||||||
|
signals, the calibration, the trace colours, which signals were enabled, the math
|
||||||
|
channels with their graphs, the measurements pane — and the startup settings: buffer
|
||||||
|
size, relative time and the packet capture's ring size.</p></item>
|
||||||
|
<item><p>Signal identity is the <em>name</em> throughout, because a new CONFIG renumbers
|
||||||
|
the channels.</p></item>
|
||||||
|
<item><p>A file written by a run against a source whose streamer was down would name no
|
||||||
|
signals at all, so such a run does <em>not</em> write: a configured session is never
|
||||||
|
overwritten by a run that configured nothing. (The chooser hides such a file for the same
|
||||||
|
reason.)</p></item>
|
||||||
|
<item><p>The multicast group and the interface live <em>inside</em> the file, because its
|
||||||
|
name cannot carry them. A file from an older build has neither key and loads as unicast
|
||||||
|
on the default route.</p></item>
|
||||||
|
<item><p>The chooser lists only configurations that name at least one signal, which is
|
||||||
|
what keeps it the sessions you configured rather than every port you have ever tested
|
||||||
|
against.</p></item>
|
||||||
|
<item><p>Deleting a configuration is a supported way to start over; the chooser's
|
||||||
|
<gui>×</gui> and the dialog's do exactly that.</p></item>
|
||||||
|
<item><p>A session from before configurations existed is imported once as the
|
||||||
|
configuration <code>default</code>, and the old file is kept beside the directory as
|
||||||
|
<file><host>_<port>.toml.imported</file>.</p></item>
|
||||||
|
</list>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="example">
|
||||||
|
<title>Example</title>
|
||||||
|
<p>Connect to <code>127.0.0.1:8000</code>, drop three signals on row 1, set a 2 s window,
|
||||||
|
and save the session as <code>bench</code> with a note.
|
||||||
|
<file>~/.config/udpscope/127.0.0.1_8000/bench.toml</file> now holds
|
||||||
|
<code>window_s=2 comment="…"</code> with
|
||||||
|
<code>signals = ["sine", "ramp", "noise", ""]</code>, and the next run reports
|
||||||
|
<code>layout loaded from …/127.0.0.1_8000/bench.toml</code> with those signals already
|
||||||
|
acquired.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</page>
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<page xmlns="http://projectmallard.org/1.0/"
|
||||||
|
type="topic" style="task"
|
||||||
|
id="signals">
|
||||||
|
|
||||||
|
<info>
|
||||||
|
<link type="guide" xref="index#main"/>
|
||||||
|
<link type="seealso" xref="layout"/>
|
||||||
|
<desc>How the source's CONFIG becomes channels, what enabling a signal starts, and how a
|
||||||
|
signal's scale and offset are calibrated.</desc>
|
||||||
|
<credit type="author copyright">
|
||||||
|
<name>Martino Ferrari</name>
|
||||||
|
</credit>
|
||||||
|
</info>
|
||||||
|
|
||||||
|
<title>Signals and calibration</title>
|
||||||
|
|
||||||
|
<p>The source's CONFIG message describes its signals: a name, a unit and a type. udpscope
|
||||||
|
turns each enabled signal into a <em>channel</em> — a ring of values beside the shared time
|
||||||
|
ring — and a channel is what a trace plots. Calibration is a scale and an offset applied at
|
||||||
|
ingest.</p>
|
||||||
|
|
||||||
|
<section id="reach">
|
||||||
|
<title>How to reach it</title>
|
||||||
|
<steps>
|
||||||
|
<item>
|
||||||
|
<p>Open the side panel (<key>s</key>), which lists every signal with a switch.</p>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<p>The <gui>Signal list</gui> item in the overflow menu opens the editor where the
|
||||||
|
unit and the calibration live.</p>
|
||||||
|
</item>
|
||||||
|
</steps>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="knowing">
|
||||||
|
<title>Worth knowing</title>
|
||||||
|
<list>
|
||||||
|
<item><p>Signals start disabled, and enabling one is what starts acquiring it. A trigger
|
||||||
|
on a signal nobody acquires can never fire, so arming a trigger enables its signal for
|
||||||
|
you.</p></item>
|
||||||
|
<item><p><gui>Calibrate</gui> in a signal's row menu opens the gain and offset it is
|
||||||
|
<em>currently</em> drawn through, so a trace already scaled by a gain of 2 shows 2 —
|
||||||
|
pressing <gui>Apply</gui> confirms it rather than resetting it. A signal nobody
|
||||||
|
calibrated shows the identity, 1 and 0.</p></item>
|
||||||
|
<item><p>Calibration is applied <em>at ingest</em>, so changing it re-plans the buffer
|
||||||
|
and drops the history. So does enabling or disabling a signal, and so does a new
|
||||||
|
CONFIG. An empty plot for a second after one of those is the ring refilling, not a
|
||||||
|
fault.</p></item>
|
||||||
|
<item><p>The stream's first signal is usually its <em>timestamp</em>, and that one is
|
||||||
|
never acquired. It cannot be plotted, measured or used as a math source, and udpscope
|
||||||
|
refuses to put it in a row rather than binding a trace that would read nothing for
|
||||||
|
ever.</p></item>
|
||||||
|
<item><p>A signal's colour follows the <em>signal</em>, not the slot, so the same signal
|
||||||
|
on two rows is one colour and the side panel's chip identifies it unambiguously. A
|
||||||
|
signal on no row gets a grey chip rather than none, so the panel's rows keep their
|
||||||
|
height.</p></item>
|
||||||
|
<item><p><em>Click a coloured chip to change that signal's colour.</em> A grey chip
|
||||||
|
names no trace, so it is not clickable. The colour is saved with the session;
|
||||||
|
<gui>Default colour</gui> in the signal's ⋯ menu gives the palette entry back.</p></item>
|
||||||
|
</list>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="example">
|
||||||
|
<title>Example</title>
|
||||||
|
<p>A source declaring <code>time</code>, <code>sine</code>, <code>ramp</code> and
|
||||||
|
<code>noise</code> yields three plottable signals. Enable all three and the exit dump
|
||||||
|
reads <code>3 signals, 3 channels</code>; <code>time</code> is not among them.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</page>
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<page xmlns="http://projectmallard.org/1.0/"
|
||||||
|
type="topic" style="task"
|
||||||
|
id="source">
|
||||||
|
|
||||||
|
<info>
|
||||||
|
<link type="guide" xref="index#main"/>
|
||||||
|
<link type="seealso" xref="sessions"/>
|
||||||
|
<desc>How udpscope is pointed at a source: the command line, the startup chooser, and
|
||||||
|
opening an exported waveform instead of a live stream.</desc>
|
||||||
|
<credit type="author copyright">
|
||||||
|
<name>Martino Ferrari</name>
|
||||||
|
</credit>
|
||||||
|
</info>
|
||||||
|
|
||||||
|
<title>Choosing a source</title>
|
||||||
|
|
||||||
|
<p>A source is a host and a port, optionally reached through a multicast group on a named
|
||||||
|
interface. Pass it on the command line, or let udpscope ask.</p>
|
||||||
|
|
||||||
|
<section id="reach">
|
||||||
|
<title>How to reach it</title>
|
||||||
|
<steps>
|
||||||
|
<item>
|
||||||
|
<p>Run with <cmd>--server</cmd> and the app connects straight away.</p>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<p>Run <em>without</em> <cmd>--server</cmd> and a chooser appears first: one row per
|
||||||
|
stored <em>configuration</em> (<code>host:port · name</code>, its note, and the
|
||||||
|
settings it carries), newest first, plus a <gui>New host…</gui> form that takes the
|
||||||
|
address <em>and</em> the configuration it belongs to — its name and note, the
|
||||||
|
<gui>buffer size</gui>, and whether time is relative or absolute. A row's pencil
|
||||||
|
button opens the same form on that configuration, pre-filled, and changes to its name
|
||||||
|
or address move it (the old file is deleted).</p>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<p>The second page, <gui>Waveforms</gui>, opens an <em>exported</em> run instead of a
|
||||||
|
live source: the files opened before (newest first; one that has since been deleted is
|
||||||
|
marked <em>missing</em>) and an <gui>Open…</gui> button for a new one. The same file
|
||||||
|
can be opened straight from the command line with <cmd>--open FILE</cmd>. A waveform
|
||||||
|
is a source that never arrives — no socket, no acquisition, no starvation notice — and
|
||||||
|
every instrument works on it: cursors, measurements, math channels and another export.
|
||||||
|
Only <file>.npy</file> files are read.</p>
|
||||||
|
</item>
|
||||||
|
</steps>
|
||||||
|
|
||||||
|
<p>The chooser runs before any buffer is allocated and before any socket is opened, so
|
||||||
|
declining it costs nothing.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="knowing">
|
||||||
|
<title>Worth knowing</title>
|
||||||
|
<list>
|
||||||
|
<item><p>The list is the sessions that <em>named a signal</em>. A session whose streamer
|
||||||
|
was down stores empty slot names and is filtered out, which is what keeps the list the
|
||||||
|
sources you actually configured rather than every port you have ever typed.</p></item>
|
||||||
|
<item><p>A stored row reconnects the way it was reached. The multicast group and the
|
||||||
|
interface are written inside the session file, because the file's name
|
||||||
|
(<file>host_port.toml</file>) cannot carry them.</p></item>
|
||||||
|
<item><p><cmd>--multicast</cmd> still needs <cmd>--server</cmd>: the CONFIG message is
|
||||||
|
fetched from the server, not from the group.</p></item>
|
||||||
|
<item><p>The <gui>×</gui> beside a row deletes that session file. Nothing else is
|
||||||
|
removed.</p></item>
|
||||||
|
</list>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="example">
|
||||||
|
<title>Example</title>
|
||||||
|
<p><cmd>udpscope</cmd> with two stored sessions lists
|
||||||
|
<code>127.0.0.1:8000 plots=1 signals=3</code> and
|
||||||
|
<code>10.0.0.5:8001 plots=2 signals=4 multicast=239.1.2.3 interface=10.0.0.9</code>.
|
||||||
|
Activating the first reports <code>[sources] picked 127.0.0.1:8000</code> and continues
|
||||||
|
into the scope with that session's layout already loaded.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</page>
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<page xmlns="http://projectmallard.org/1.0/"
|
||||||
|
type="topic" style="task"
|
||||||
|
id="status">
|
||||||
|
|
||||||
|
<info>
|
||||||
|
<link type="guide" xref="index#output"/>
|
||||||
|
<desc>The status bar's two tiers, the activity badge, the fixed state words, and the ⓘ
|
||||||
|
detail popover.</desc>
|
||||||
|
<credit type="author copyright">
|
||||||
|
<name>Martino Ferrari</name>
|
||||||
|
</credit>
|
||||||
|
</info>
|
||||||
|
|
||||||
|
<title>Status bar and diagnostics</title>
|
||||||
|
|
||||||
|
<p>The status bar is two tiers. The bar itself is one glance line — the state, the rate and
|
||||||
|
the window — and everything else is behind the ⓘ button at its right-hand end.</p>
|
||||||
|
|
||||||
|
<section id="reach">
|
||||||
|
<title>How to reach it</title>
|
||||||
|
<p>The bar is always visible under the plot. Click <gui>ⓘ</gui> for the detail popover.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="knowing">
|
||||||
|
<title>Worth knowing</title>
|
||||||
|
<list>
|
||||||
|
<item><p>The badge is one glyph in four colours: green <em>alternating</em> lit and dim
|
||||||
|
means data is arriving, and a steady colour means it is not. A badge that never blinks is
|
||||||
|
not a working badge. Of the two steady colours, <em>grey</em> is no stream at all or a
|
||||||
|
pipeline you stopped (the tooltip says which), and <em>yellow</em> is a source that
|
||||||
|
answered but has sent no configuration.</p></item>
|
||||||
|
<item><p>The state field is one word from a fixed set: <code>connecting</code>,
|
||||||
|
<code>live</code>, <code>paused</code>, <code>armed</code>, <code>capturing</code>,
|
||||||
|
<code>event held</code>, <code>disarmed</code>. Two states add one field each: in trigger
|
||||||
|
mode, how long ago the last event was; while paused, how long you have been paused.</p></item>
|
||||||
|
<item><p><code>connecting</code> means there is no <em>signal list</em> yet — either no
|
||||||
|
session or a session with no configuration behind it. Every other word claims a signal
|
||||||
|
list, so until one arrives there is nothing for them to describe: nothing can be enabled
|
||||||
|
and no data is owed.</p></item>
|
||||||
|
<item><p>The paused figure is <em>wall</em> time, not stream time — the producer never
|
||||||
|
stops while paused, so it answers "how long have I been looking at this picture".</p></item>
|
||||||
|
<item><p>Nothing on the bar may change width with the stream, because the bar sits
|
||||||
|
directly above the plot and a wider label would relayout it. That is why the rate
|
||||||
|
collapses to one figure or a range rather than listing per channel, and why an elapsed
|
||||||
|
time steps to a coarser unit instead of growing digits.</p></item>
|
||||||
|
<item><p><code>gaps</code> counts packets the receiver knows it missed. It is the number
|
||||||
|
that matters: a non-zero count means samples are absent from the buffer, not merely
|
||||||
|
late.</p></item>
|
||||||
|
<item><p><code>reconnects</code> counts sessions re-established with the source. Over
|
||||||
|
unicast a climbing count usually means a second client is taking the source's single
|
||||||
|
session.</p></item>
|
||||||
|
<item><p>The starvation notice — <em>connected, but no data is arriving</em> — is that
|
||||||
|
same situation seen from the other side: the signal list is here, so something took the
|
||||||
|
stream away. A source that has sent <em>no</em> configuration gets a different notice,
|
||||||
|
<em>trying to connect</em>, because nothing was taken away — pointing the app at a port
|
||||||
|
where nothing answers is that case, and the usual cause is a streamer that is not running
|
||||||
|
or is serving another port.</p></item>
|
||||||
|
<item><p><code>stored</code> versus <code>wire</code> in the popover is the decimation.
|
||||||
|
They differ exactly when the window is too long to store at full rate.</p></item>
|
||||||
|
</list>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="example">
|
||||||
|
<title>Example</title>
|
||||||
|
<p>The bar reads <code>● live · 10 kS/s · 2 s window</code>. Behind ⓘ:
|
||||||
|
<code>Stream connected, configured / packets 717 · frames 716 · gaps 0 / 3 signals · 3
|
||||||
|
channels</code>, <code>Buffer 200.0 MB · 1677 s span · time x1 / stored 10 kS/s (wire
|
||||||
|
10 kS/s)</code>, <code>Plot 1 3 traces · 195000 samples</code>,
|
||||||
|
<code>Reconnects 0</code>.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</page>
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
<page xmlns="http://projectmallard.org/1.0/"
|
||||||
|
type="topic" style="task"
|
||||||
|
id="trigger">
|
||||||
|
|
||||||
|
<info>
|
||||||
|
<link type="guide" xref="index#viewing"/>
|
||||||
|
<link type="seealso" xref="pause"/>
|
||||||
|
<desc>The trigger's four shapes, arming and hold-off, the event cache with its replay,
|
||||||
|
compare and accumulate, and searching the buffer for a condition.</desc>
|
||||||
|
<credit type="author copyright">
|
||||||
|
<name>Martino Ferrari</name>
|
||||||
|
</credit>
|
||||||
|
</info>
|
||||||
|
|
||||||
|
<title>Trigger</title>
|
||||||
|
|
||||||
|
<p>The trigger watches one signal for a condition and captures a window around it. The
|
||||||
|
<gui>Shape</gui> dropdown chooses what counts as an event; the arming, hold-off, pre/post
|
||||||
|
window and Normal/Single mode are the same for all four shapes. The captured window is
|
||||||
|
frozen on screen until you re-arm.</p>
|
||||||
|
|
||||||
|
<section id="reach">
|
||||||
|
<title>How to reach it</title>
|
||||||
|
<steps>
|
||||||
|
<item>
|
||||||
|
<p>The trigger button in the toolbar (<key>t</key>) reveals the trigger panel: the
|
||||||
|
signal, the level, the shape, the edge (rising, falling, either), the mode (Normal or
|
||||||
|
Single) and the pre-trigger share.</p>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<p><gui>Arm</gui> starts watching; <key>a</key> arms and disarms from the keyboard.
|
||||||
|
When the pane is locked with its <gui>[edit]</gui> button — so that it states what the
|
||||||
|
trigger is set to instead of showing the controls — <gui>Arm/Disarm</gui> is in its
|
||||||
|
heading, and the trigger can be armed or disarmed without unfolding the panel. It is
|
||||||
|
offered only while trigger mode is on, because a trigger that is off has no engine to
|
||||||
|
arm.</p>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<p><gui>Search the buffer</gui> is at the foot of the same panel.</p>
|
||||||
|
</item>
|
||||||
|
</steps>
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<title>A held capture: <gui>event held</gui> in the status bar, the axis pinned to the
|
||||||
|
event, and the trigger marker at x = 0.</title>
|
||||||
|
<media type="image" mime="image/png" src="figures/trigger-capture.png" width="1280" height="800">
|
||||||
|
<p>The pre-trigger share is drawn to the left of the marker.</p>
|
||||||
|
</media>
|
||||||
|
</figure>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="shapes">
|
||||||
|
<title>The four shapes</title>
|
||||||
|
<terms>
|
||||||
|
<item>
|
||||||
|
<title>Edge</title>
|
||||||
|
<p>A crossing of the level in the chosen direction, the classic behaviour.</p>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title>Pulse width</title>
|
||||||
|
<p>A crossing and its return, qualified by how long the excursion lasted: less than,
|
||||||
|
greater than, within or outside a pair of widths; the event is timed at the closing
|
||||||
|
edge.</p>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title>Window</title>
|
||||||
|
<p>The signal entering or leaving a band between two levels; the panel keeps the two
|
||||||
|
levels at least one increment apart, because a zero-width band is not a band: the
|
||||||
|
condition becomes an exact float-equality test against the level, which an ordinary
|
||||||
|
signal never satisfies and a signal sitting at the level satisfies on every
|
||||||
|
transition.</p>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title>Dropout</title>
|
||||||
|
<p>The absence of a crossing for longer than a timeout; one event per outage, re-arms
|
||||||
|
when a crossing returns.</p>
|
||||||
|
</item>
|
||||||
|
</terms>
|
||||||
|
|
||||||
|
<p>The shape's own rows are the only ones shown; the rest are hidden, so the panel never
|
||||||
|
offers a pulse width to a window trigger.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="cache">
|
||||||
|
<title>The event cache, replay and compare</title>
|
||||||
|
<p><em>Every triggered acquisition is kept.</em> Latches are written to the event cache
|
||||||
|
(<file>~/.cache/udpscope/events/<source>/</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>
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<page xmlns="http://projectmallard.org/1.0/"
|
||||||
|
type="topic" style="task"
|
||||||
|
id="window">
|
||||||
|
|
||||||
|
<info>
|
||||||
|
<link type="guide" xref="index#viewing"/>
|
||||||
|
<link type="seealso" xref="navigate"/>
|
||||||
|
<desc>The time window control: how to set it, and why a long window is decimated rather
|
||||||
|
than refused.</desc>
|
||||||
|
<credit type="author copyright">
|
||||||
|
<name>Martino Ferrari</name>
|
||||||
|
</credit>
|
||||||
|
</info>
|
||||||
|
|
||||||
|
<title>Time window</title>
|
||||||
|
|
||||||
|
<p>The window is how much time the plot shows. Every row shares one window: the figure has
|
||||||
|
a single x link group, so the rows cannot drift apart. The trigger's event span follows it,
|
||||||
|
and the pre-trigger share is a share of <em>this</em> window.</p>
|
||||||
|
|
||||||
|
<section id="reach">
|
||||||
|
<title>How to reach it</title>
|
||||||
|
<steps>
|
||||||
|
<item>
|
||||||
|
<p>The header bar's window button opens a popover: type a number, pick a unit under
|
||||||
|
it, or step a decade with the arrows.</p>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<p>The button's own label is a reading of the current window in a natural unit,
|
||||||
|
width-locked so a change of window cannot widen the header bar.</p>
|
||||||
|
</item>
|
||||||
|
</steps>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="knowing">
|
||||||
|
<title>Worth knowing</title>
|
||||||
|
<list>
|
||||||
|
<item><p>A window too long to store at the wire rate is <em>decimated, not refused</em>.
|
||||||
|
The shared clock takes a power-of-two stride, and each group of samples is
|
||||||
|
<em>averaged</em> rather than strided, so a tone above the stored Nyquist is attenuated
|
||||||
|
instead of aliasing to a false low frequency.</p></item>
|
||||||
|
<item><p>The stamp stored for a group is the group's <em>mean</em>, which is where the
|
||||||
|
averaging puts the value. Stamping at the end of a group would shift every trace half a
|
||||||
|
stored sample late.</p></item>
|
||||||
|
<item><p>Moving the window across a decimation boundary rebuilds the buffer and drops
|
||||||
|
the history. Walking it inside one decade is free.</p></item>
|
||||||
|
<item><p><em>This control is the only thing that changes the window.</em> A zoom, a pan
|
||||||
|
or a fit moves the view <em>inside</em> it and leaves it alone — which is the point of
|
||||||
|
the previous bullet: a wheel notch that redefined the window could re-plan the buffer
|
||||||
|
and throw your samples away.</p></item>
|
||||||
|
<item><p>The trigger sees the <em>stored</em> (filtered) signal, not the wire signal,
|
||||||
|
because an edge found on a sample that is not in the buffer could not be cut out of
|
||||||
|
it.</p></item>
|
||||||
|
</list>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="example">
|
||||||
|
<title>Example</title>
|
||||||
|
<p>1 MS/s with 200 MB of buffer. A 2 s window stores every sample
|
||||||
|
(<code>time x1</code>). Widen it to 600 s and the stride becomes <code>x16</code>: 625 S/s
|
||||||
|
stored, anti-aliased at 312.5 Hz. A 2 kHz sine seen through that window reads
|
||||||
|
<code>y=[-0.059, 0.079]</code> — attenuated to about 6%, not folded down to a
|
||||||
|
full-amplitude 125 Hz one.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</page>
|
||||||
@@ -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 <adwaita.h>
|
||||||
|
|
||||||
#include <cstdio>
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include "build_info.hpp"
|
#include "build_info.hpp"
|
||||||
#include "layout_store.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 udpscope {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
/// Nulled by each window's own close handler, so a second open cannot present a dead widget.
|
/// Nulled by each dialog's own close handler, so a second open cannot present a dead widget.
|
||||||
/// One pointer per window, file-scope because there is one application.
|
/// 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
|
||||||
/// The help window and the shortcut overview are `GtkWindow`s. The About dialog is an
|
/// process with no window of ours has nothing to null.
|
||||||
/// `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;
|
|
||||||
AdwDialog* g_about = nullptr;
|
AdwDialog* g_about = nullptr;
|
||||||
/// The overview is the one window here built from a `.ui` file, so its builder is held too: the window
|
/// The overview is built from a `.ui` file, so its builder is held too: the window belongs to
|
||||||
/// belongs to the builder, and a *destroyed* `GtkWindow` cannot be presented again -- which is why the
|
/// the builder, and a *destroyed* `GtkWindow` cannot be presented again -- which is why the
|
||||||
/// builder is replaced on the next opening rather than reused.
|
/// builder is replaced on the next opening rather than reused.
|
||||||
GtkWidget* g_shortcuts = nullptr;
|
GtkWidget* g_shortcuts = nullptr;
|
||||||
GtkBuilder* g_shortcuts_ui = 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) {
|
gboolean on_window_close(GtkWindow* window, gpointer slot) {
|
||||||
(void)window;
|
(void)window;
|
||||||
*static_cast<GtkWidget**>(slot) = nullptr;
|
*static_cast<GtkWidget**>(slot) = nullptr;
|
||||||
@@ -58,100 +44,27 @@ void on_dialog_closed(AdwDialog* dialog, gpointer slot) {
|
|||||||
*static_cast<AdwDialog**>(slot) = nullptr;
|
*static_cast<AdwDialog**>(slot) = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct HelpPage {
|
/// The launch is asynchronous and can fail -- no help viewer installed, no handler for the
|
||||||
const char* section; ///< the group heading in the sidebar
|
/// `help:` scheme, or a document no viewer can find -- and it runs in another process, so
|
||||||
const char* title; ///< the row's label and the page's heading
|
/// nothing raises a window of ours. Report the failure here rather than leaving a click that
|
||||||
const char* resource; ///< a path under /org/udpscope/help
|
/// silently did nothing; the detail names the identifier a viewer looks for.
|
||||||
/// The screenshots shown above the text, as resource paths and a null terminator. A page
|
void on_help_launched(GObject* source, GAsyncResult* result, gpointer user) {
|
||||||
/// with none is the common case and the reason this is a list rather than one field: the
|
GtkWindow* parent = user != nullptr ? GTK_WINDOW(user) : nullptr;
|
||||||
/// 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;
|
|
||||||
GError* error = nullptr;
|
GError* error = nullptr;
|
||||||
GBytes* bytes = g_resources_lookup_data(page.resource, G_RESOURCE_LOOKUP_FLAGS_NONE,
|
if (!gtk_uri_launcher_launch_finish(GTK_URI_LAUNCHER(source), result, &error)) {
|
||||||
&error);
|
GtkAlertDialog* alert = gtk_alert_dialog_new("Could not open the help manual");
|
||||||
if (bytes == nullptr) {
|
const std::string detail =
|
||||||
if (error != nullptr) g_error_free(error);
|
std::string(error->message) +
|
||||||
return "<i>This page is missing from the build.</i>";
|
"\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 "
|
||||||
gsize size = 0;
|
"to read it.";
|
||||||
const char* data = static_cast<const char*>(g_bytes_get_data(bytes, &size));
|
gtk_alert_dialog_set_detail(alert, detail.c_str());
|
||||||
std::string body(data, size);
|
gtk_alert_dialog_show(alert, parent);
|
||||||
g_bytes_unref(bytes);
|
g_object_unref(alert);
|
||||||
|
g_error_free(error);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
// `parent` was referenced for the duration of the async call.
|
||||||
|
if (parent != nullptr) g_object_unref(parent);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
@@ -197,84 +110,46 @@ const char* about_icon_name() {
|
|||||||
|
|
||||||
HelpReport help_self_check() {
|
HelpReport help_self_check() {
|
||||||
HelpReport report;
|
HelpReport report;
|
||||||
report.pages = kPageCount;
|
|
||||||
for (const HelpPage& page : kPages) {
|
// The manual is a set of files on disk now, so the reading is a file-system one: a missing
|
||||||
bool ok = false;
|
// directory leaves `topics=0 index=false`, which is a manual that is not where the build
|
||||||
const std::string body = load_page(page, &ok);
|
// says it is -- and Yelp would show nothing for `help:<identifier>`.
|
||||||
if (ok && !body.empty()) ++report.parsed;
|
GError* error = nullptr;
|
||||||
if (report.shortest_title.empty() || body.size() < report.shortest_bytes) {
|
GDir* dir = g_dir_open(UDPSCOPE_HELP_DIR, 0, &error);
|
||||||
report.shortest_title = page.title;
|
if (dir == nullptr) {
|
||||||
report.shortest_bytes = body.size();
|
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;
|
return report;
|
||||||
}
|
}
|
||||||
|
|
||||||
void show_help(GtkWindow* parent) {
|
void show_help(GtkWindow* parent) {
|
||||||
if (g_help != nullptr) {
|
// The freedesktop `help:` URI, handed to whatever the desktop registered for it -- Yelp on
|
||||||
gtk_window_present(GTK_WINDOW(g_help));
|
// GNOME. The launch is asynchronous, so the parent is referenced across it and released in
|
||||||
return;
|
// the callback; `parent` may legitimately be nullptr (the chooser runs before any window
|
||||||
}
|
// exists).
|
||||||
|
GtkUriLauncher* launcher = gtk_uri_launcher_new("help:" UDPSCOPE_HELP_NAME);
|
||||||
// A builder per opening, as with every other `.ui` here: the window belongs to its object list and
|
gtk_uri_launcher_launch(launcher, parent, nullptr, on_help_launched,
|
||||||
// `close-request` destroys the window, so the resource is parsed again rather than a dead window
|
parent != nullptr ? g_object_ref(parent) : nullptr);
|
||||||
// fetched. This is where the previous one goes, and it is safe because that window has been closed
|
g_object_unref(launcher);
|
||||||
// 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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void show_shortcuts(GtkWindow* parent) {
|
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
|
// 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.
|
// 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
|
||||||
// A UI file rather than widget construction because of the mouse rows. An accelerator cannot spell
|
// and drag", and libadwaita's `AdwShortcutsItem` -- the modern replacement -- has no icon
|
||||||
// "Ctrl and drag", and libadwaita's `AdwShortcutsItem` -- the modern replacement for this widget --
|
// property, so its cell reads "No Shortcut". `GtkShortcutsShortcut`'s one generic gesture type
|
||||||
// has no icon property at all, so its cell reads "No Shortcut". `GtkShortcutsShortcut`'s one generic
|
// draws the `icon` the row names. (The widget is deprecated since 4.20, but this uses none of
|
||||||
// gesture type draws the `icon` the row names, which is what puts a mouse glyph where the keycaps
|
// its functions: the file is loaded as data and presented as a `GtkWindow`.)
|
||||||
// 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.)
|
|
||||||
//
|
//
|
||||||
// A builder per opening, not one reused: the window belongs to the builder and `close-request`
|
// 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_clear_object(&g_shortcuts_ui);
|
||||||
g_shortcuts_ui = gtk_builder_new_from_resource("/org/udpscope/ui/shortcuts.ui");
|
g_shortcuts_ui = gtk_builder_new_from_resource("/org/udpscope/ui/shortcuts.ui");
|
||||||
if (g_shortcuts_ui == nullptr) return;
|
if (g_shortcuts_ui == nullptr) return;
|
||||||
@@ -307,7 +181,6 @@ void show_shortcuts(GtkWindow* parent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void close_help_windows() {
|
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_about != nullptr) adw_dialog_close(g_about);
|
||||||
if (g_shortcuts != nullptr) gtk_window_close(GTK_WINDOW(g_shortcuts));
|
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.
|
/// so the dialog is asserted to *show* the app icon rather than merely to open.
|
||||||
const char* about_icon_name();
|
const char* about_icon_name();
|
||||||
|
|
||||||
/// The paged help window. Modal on nothing — it is a reference, so it stays usable while
|
/// Opens the installed Mallard manual in the desktop help viewer — Yelp, through the
|
||||||
/// the scope runs. Already open: presented.
|
/// `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);
|
void show_help(GtkWindow* parent);
|
||||||
|
|
||||||
/// What UDPSCOPE_PROBE_HELP_MS reports. `parsed` counts pages whose markup
|
/// What UDPSCOPE_PROBE_HELP_MS reports about the manual *in the tree*. `topics` is the
|
||||||
/// pango_parse_markup() accepted and whose body is non-empty; it must equal `pages`.
|
/// 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 {
|
struct HelpReport {
|
||||||
int pages = 0;
|
int topics = 0;
|
||||||
int parsed = 0;
|
bool index = false;
|
||||||
std::string shortest_title;
|
int figures = 0;
|
||||||
std::size_t shortest_bytes = 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();
|
HelpReport help_self_check();
|
||||||
|
|
||||||
/// The shortcut overview: the application accelerators and the plot's own interactions,
|
/// 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.
|
/// deprecated as of GTK 4.18 and this project builds warning-free.
|
||||||
void show_shortcuts(GtkWindow* parent);
|
void show_shortcuts(GtkWindow* parent);
|
||||||
|
|
||||||
/// Closes whichever of the three windows are open. One of them is a GtkWindow -- the help
|
/// Closes whichever of the two dialogs are open. Both are `AdwDialog`s, which are not
|
||||||
/// window, closed with gtk_window_close(), which emits close-request so its handler runs. The
|
/// `GtkWindow`s: they close with `adw_dialog_close()`, which emits `::closed`, and the
|
||||||
/// other two are AdwDialogs, which are not GtkWindows: they close with adw_dialog_close(),
|
/// file-scope pointer is nulled by the handler that prevents a second open presenting a dead
|
||||||
/// which emits ::closed. Either way the file-scope pointer is nulled by the handler, which is
|
/// widget. There is no help window left to close -- the manual opens in a separate viewer
|
||||||
/// what prevents a second open presenting a dead widget. Used by the probe.
|
/// process. Used by the probe.
|
||||||
void close_help_windows();
|
void close_help_windows();
|
||||||
|
|
||||||
} // namespace udpscope
|
} // 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
|
/// 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.
|
/// 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.
|
/// Checks that the manual is present in the tree where the build says it is (`topics` and
|
||||||
/// `parsed` must equal `pages` and no page may be empty; `closed ok` with no GTK critical
|
/// `index`, both zero if it is missing), opens the About and shortcut dialogs, reads them and
|
||||||
/// is the crash check for three windows built and destroyed in one run.
|
/// 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,
|
int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisition& acq,
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
<!-- SPDX-License-Identifier: MIT -->
|
<!-- SPDX-License-Identifier: MIT -->
|
||||||
<!-- SPDX-FileCopyrightText: 2026 Martino Ferrari -->
|
<!-- SPDX-FileCopyrightText: 2026 Martino Ferrari -->
|
||||||
<!--
|
<!--
|
||||||
The application's UI files, compiled into the binary beside the icons and the help pages, so there
|
The application's UI files, compiled into the binary beside the icons, so there is still exactly
|
||||||
is still exactly one file to run. `preprocess="xml-stripblanks"` is what catches a malformed file:
|
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
|
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.
|
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 =
|
const std::string session =
|
||||||
layout_path(a->model->config().server, a->model->config().port);
|
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_about(GTK_WINDOW(a->window), session);
|
||||||
show_shortcuts(GTK_WINDOW(a->window));
|
show_shortcuts(GTK_WINDOW(a->window));
|
||||||
// Read while it is open: the icon is the point of the dialog carrying one, and
|
// 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.
|
// works and is not documented, or is documented and does nothing.
|
||||||
const UiShortcuts ui = ui_shortcut_rows("/org/udpscope/ui/shortcuts.ui");
|
const UiShortcuts ui = ui_shortcut_rows("/org/udpscope/ui/shortcuts.ui");
|
||||||
std::fprintf(stderr,
|
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; "
|
"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",
|
"about version=%s commit=%s built=%s settings=%s icon=%s; closed ok\n",
|
||||||
report.pages, report.parsed, report.shortest_title.c_str(),
|
report.topics, report.index ? 1 : 0, report.figures, UDPSCOPE_HELP_DIR,
|
||||||
report.shortest_bytes, accel_count, gesture_count, accel_count + gesture_count,
|
accel_count, gesture_count, accel_count + gesture_count,
|
||||||
ui.rows, ui.with_accel, ui.with_icon, ui.bare, build_info::kVersion,
|
ui.rows, ui.with_accel, ui.with_icon, ui.bare, build_info::kVersion,
|
||||||
build_info::kCommit, build_info::kTimestamp, config_dir().c_str(),
|
build_info::kCommit, build_info::kTimestamp, config_dir().c_str(),
|
||||||
about_icon.c_str());
|
about_icon.c_str());
|
||||||
|
|||||||
Executable
+33
@@ -0,0 +1,33 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# SPDX-FileCopyrightText: 2026 Martino Ferrari
|
||||||
|
#
|
||||||
|
# The manual's check, the way tools/check_ui.py is the .ui files' check: it runs the four
|
||||||
|
# yelp-check subcommands that need no network, then renders the whole set to HTML with
|
||||||
|
# yelp-build so a page that only fails at the transform step is caught too.
|
||||||
|
#
|
||||||
|
# `yelp-check validate` is deliberately absent. It fetches the Mallard RelaxNG schema over
|
||||||
|
# the network and fails on an offline machine for that reason rather than for a fault in the
|
||||||
|
# pages -- links/media/orphans/ids are the offline checks.
|
||||||
|
#
|
||||||
|
# Exit status is nonzero on the first finding.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
dir=${1:?usage: check_help.sh HELP_DIR}
|
||||||
|
if [ ! -f "$dir/index.page" ]; then
|
||||||
|
echo "check_help: no index.page under $dir" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
tmp=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$tmp"' EXIT
|
||||||
|
|
||||||
|
for sub in links media orphans ids; do
|
||||||
|
printf 'yelp-check %s %s\n' "$sub" "$dir"
|
||||||
|
yelp-check "$sub" "$dir" >/dev/null
|
||||||
|
done
|
||||||
|
|
||||||
|
mkdir -p "$tmp/html"
|
||||||
|
yelp-build html -o "$tmp/html" "$dir"
|
||||||
|
pages=$(find "$tmp/html" -maxdepth 1 -name '*.html' | wc -l)
|
||||||
|
printf 'yelp-build html: %s page(s) rendered\n' "$pages"
|
||||||
Executable
+224
@@ -0,0 +1,224 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# SPDX-FileCopyrightText: 2026 Martino Ferrari
|
||||||
|
#
|
||||||
|
# Regenerates the screenshots the Mallard manual carries (help/C/<id>/figures/*.png).
|
||||||
|
#
|
||||||
|
# It runs udpscope headlessly under its own Xvfb, against tools/mock_udpstreamer.py, drives it
|
||||||
|
# into a state with the same `UDPSCOPE_*` probes the self-check uses, grabs the window with
|
||||||
|
# ImageMagick's `import`, and draws numbered annotation boxes on a borderless crop with
|
||||||
|
# `magick`.
|
||||||
|
#
|
||||||
|
# Two things about a headless box, both measured rather than assumed:
|
||||||
|
#
|
||||||
|
# * GTK4 picks the Vulkan renderer by default, and under Xvfb that surface is not readable
|
||||||
|
# back through X11 -- `import` sees a window with no pixels (standard deviation 0). Forcing
|
||||||
|
# the Cairo renderer (`GSK_RENDERER=cairo`, with `LIBGL_ALWAYS_SOFTWARE=1`) makes GTK paint
|
||||||
|
# with XPutImage, which `import` can read.
|
||||||
|
# * `xdotool search --name udpscope` finds nothing: GTK4 sets no WM_NAME on the X window it
|
||||||
|
# creates, so windows are found by their geometry, not their name.
|
||||||
|
#
|
||||||
|
# Box provenance: the sidebar width is read from the app's own probe dump (`side=NNNx`), so the
|
||||||
|
# side-panel box tracks the real widget; the header and status bands are the table in
|
||||||
|
# `window_boxes` below, each named where it is used, because the dump reports neither.
|
||||||
|
#
|
||||||
|
# Usage: tools/make-help-shots.sh [OUTPUT_DIR] (default help/C/<id>/figures)
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
here=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)
|
||||||
|
root=$(dirname -- "$here")
|
||||||
|
app=${UDPSCOPE_SHOTS_APP:-$root/build/udpscope}
|
||||||
|
mock=$here/mock_udpstreamer.py
|
||||||
|
help_id=org.udpscope.app
|
||||||
|
out=${1:-$root/help/C/$help_id/figures}
|
||||||
|
port=18391
|
||||||
|
|
||||||
|
for tool in Xvfb import magick xdotool python3; do
|
||||||
|
command -v "$tool" >/dev/null || { echo "make-help-shots: $tool is required" >&2; exit 1; }
|
||||||
|
done
|
||||||
|
[ -x "$app" ] || { echo "make-help-shots: $app is missing; build it first" >&2; exit 1; }
|
||||||
|
[ -f "$mock" ] || { echo "make-help-shots: $mock is missing" >&2; exit 1; }
|
||||||
|
mkdir -p "$out"
|
||||||
|
|
||||||
|
# --- the window's own geometry, and the boxes it is annotated with -------------
|
||||||
|
# main-window.ui asks for 1280x800, and the sidebar is a fixed 320 px
|
||||||
|
# (sidebar-width-unit=px, min=max=320). The header bar is the window's titlebar and the status
|
||||||
|
# row sits at the foot of the plot column; neither reports a pixel height, so their bands are
|
||||||
|
# the third measurement -- a pixel scan of a grab, 51 px and 43 px on the current theme -- and
|
||||||
|
# are refreshed by re-measuring if the styling changes.
|
||||||
|
WIN_W=1280
|
||||||
|
WIN_H=800
|
||||||
|
HEADER_H=51
|
||||||
|
STATUS_H=43
|
||||||
|
PLOT_W=952
|
||||||
|
|
||||||
|
boxes_main="header bar:0:0:$WIN_W:$HEADER_H
|
||||||
|
side panel:SIDE_X:$HEADER_H:SIDE_W:706
|
||||||
|
plot:0:$HEADER_H:$PLOT_W:706
|
||||||
|
status bar:0:757:$PLOT_W:$STATUS_H"
|
||||||
|
boxes_trigger="event held, axis pinned to the event:0:757:$PLOT_W:$STATUS_H
|
||||||
|
the trigger marker at x = 0:0:$HEADER_H:$PLOT_W:706"
|
||||||
|
boxes_math="the graph editor:8:8:EW-24:EH-24"
|
||||||
|
|
||||||
|
display=
|
||||||
|
xvfb_pid=
|
||||||
|
app_pid=
|
||||||
|
mock_pid=
|
||||||
|
cleanup() {
|
||||||
|
[ -n "$app_pid" ] && kill "$app_pid" 2>/dev/null || true
|
||||||
|
[ -n "$mock_pid" ] && kill "$mock_pid" 2>/dev/null || true
|
||||||
|
[ -n "$xvfb_pid" ] && kill "$xvfb_pid" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# A display of our own, so nothing on the developer's screen is grabbed or disturbed.
|
||||||
|
for n in $(seq 210 240); do
|
||||||
|
if Xvfb ":$n" -screen 0 1600x1000x24 -nolisten tcp >/dev/null 2>&1 & then
|
||||||
|
xvfb_pid=$!
|
||||||
|
sleep 1
|
||||||
|
if kill -0 "$xvfb_pid" 2>/dev/null; then display=":$n"; break; fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
[ -n "$display" ] || { echo "make-help-shots: no free Xvfb display in :210..:240" >&2; exit 1; }
|
||||||
|
export DISPLAY="$display"
|
||||||
|
export GDK_BACKEND=x11 GSK_RENDERER=cairo LIBGL_ALWAYS_SOFTWARE=1
|
||||||
|
|
||||||
|
# A throwaway config dir: the app restores the last session for a source, and a restored
|
||||||
|
# layout would change the very arrangement the screenshot is meant to show.
|
||||||
|
tmp=$(mktemp -d)
|
||||||
|
trap 'cleanup; rm -rf "$tmp"' EXIT
|
||||||
|
export XDG_CONFIG_HOME="$tmp/config" XDG_CACHE_HOME="$tmp/cache" XDG_DATA_HOME="$tmp/data"
|
||||||
|
|
||||||
|
python3 "$mock" --port "$port" --rate 10000 --batch 100 >"$tmp/mock.log" 2>&1 &
|
||||||
|
mock_pid=$!
|
||||||
|
sleep 0.5
|
||||||
|
|
||||||
|
# The largest window that is not the 1600x1000 X root; that is the app's 1280x800 one (a math
|
||||||
|
# editor opened on top is smaller, and X may also expose unnamed GTK helper windows).
|
||||||
|
pick_window() {
|
||||||
|
local id geo w best= best_w=0
|
||||||
|
for id in $(xdotool search --onlyvisible "" 2>/dev/null); do
|
||||||
|
geo=$(xdotool getwindowgeometry "$id" 2>/dev/null | awk '/Geometry/{print $2}')
|
||||||
|
[ "$geo" = "1600x1000" ] && continue
|
||||||
|
w=${geo%x*}
|
||||||
|
case "$w" in ''|*[!0-9]*) continue;; esac
|
||||||
|
if [ "$w" -gt "$best_w" ]; then best=$id; best_w=$w; fi
|
||||||
|
done
|
||||||
|
[ -n "$best" ] && echo "$best"
|
||||||
|
}
|
||||||
|
|
||||||
|
# The math editor is a window of its own (a `GtkWindow`, presented on the main one), so it is
|
||||||
|
# the largest window that is neither the X root nor the app's 1280x800 one and that has a real
|
||||||
|
# size (X exposes 1x1 helper windows too).
|
||||||
|
pick_other_window() {
|
||||||
|
local id geo w best= best_w=0
|
||||||
|
for id in $(xdotool search --onlyvisible "" 2>/dev/null); do
|
||||||
|
geo=$(xdotool getwindowgeometry "$id" 2>/dev/null | awk '/Geometry/{print $2}')
|
||||||
|
case "$geo" in 1600x1000|"${WIN_W}x${WIN_H}") continue;; esac
|
||||||
|
w=${geo%x*}
|
||||||
|
case "$w" in ''|*[!0-9]*) continue;; esac
|
||||||
|
[ "$w" -le 50 ] && continue
|
||||||
|
if [ "$w" -gt "$best_w" ]; then best=$id; best_w=$w; fi
|
||||||
|
done
|
||||||
|
[ -n "$best" ] && echo "$best"
|
||||||
|
}
|
||||||
|
|
||||||
|
start_app() { # LOGFILE ENVS... ; extra args via APP_ARGS
|
||||||
|
local log=$1; shift
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
env "$@" "$app" --server 127.0.0.1 --port "$port" --buffer-mb 16 ${APP_ARGS:-} >"$log" 2>&1 &
|
||||||
|
app_pid=$!
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_app() {
|
||||||
|
[ -n "$app_pid" ] && kill "$app_pid" 2>/dev/null || true
|
||||||
|
wait "$app_pid" 2>/dev/null || true
|
||||||
|
app_pid=
|
||||||
|
}
|
||||||
|
|
||||||
|
side_of_dump() { # LOGFILE -> the sidebar width the app reports, else the table's 320
|
||||||
|
local s
|
||||||
|
s=$(grep -o 'side=[0-9]*x' "$1" 2>/dev/null | head -1 | grep -o '[0-9]*' || true)
|
||||||
|
echo "${s:-320}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# annotate NAME RAW LOG BOXES W H -> the figure, with a numbered box per line of BOXES.
|
||||||
|
# A box is `label:x:y:w:h`; `SIDE_X`/`SIDE_W` are the sidebar's own numbers, `EW`/`EH` the
|
||||||
|
# grabbed window's width and height.
|
||||||
|
annotate() {
|
||||||
|
local name=$1 raw=$2 log=$3 boxes=$4 win_w=$5 win_h=$6
|
||||||
|
local side_w side_x i=0 draw=() line label x y w h
|
||||||
|
side_w=$(side_of_dump "$log")
|
||||||
|
side_x=$((win_w - side_w))
|
||||||
|
while IFS= read -r line; do
|
||||||
|
[ -n "$line" ] || continue
|
||||||
|
IFS=: read -r label x y w h <<<"$line"
|
||||||
|
x=${x/SIDE_X/$side_x}; w=${w/SIDE_W/$side_w}
|
||||||
|
w=${w/EW/$win_w}; h=${h/EH/$win_h}
|
||||||
|
i=$((i + 1))
|
||||||
|
draw+=(-stroke '#e01b24' -strokewidth 3 -fill none
|
||||||
|
-draw "rectangle $x,$y $((x + w)),$((y + h))"
|
||||||
|
-stroke none -fill '#e01b24'
|
||||||
|
-draw "circle $((x + 17)),$((y + 17)) $((x + 17)),$((y + 33))"
|
||||||
|
-fill white -stroke none -pointsize 22
|
||||||
|
-draw "text $((x + 10)),$((y + 25)) '$i'")
|
||||||
|
done <<<"$boxes"
|
||||||
|
magick "$raw" -crop "${win_w}x${win_h}+0+0" +repage "${draw[@]}" "$out/$name.png"
|
||||||
|
local sd
|
||||||
|
sd=$(magick identify -format '%[standard-deviation]' "$out/$name.png")
|
||||||
|
awk -v s="$sd" 'BEGIN{exit !(s+0 > 0)}' ||
|
||||||
|
{ echo "make-help-shots: the '$name' figure is blank (stddev=$sd)" >&2; return 1; }
|
||||||
|
echo "make-help-shots: wrote $out/$name.png ($(magick identify -format '%wx%h' "$out/$name.png"))"
|
||||||
|
|
||||||
|
# Print what each number marks, so the script is self-documenting in CI.
|
||||||
|
local n=0
|
||||||
|
while IFS= read -r line; do
|
||||||
|
[ -n "$line" ] || continue
|
||||||
|
n=$((n + 1))
|
||||||
|
printf ' %s = %s\n' "$n" "${line%%:*}"
|
||||||
|
done <<<"$boxes"
|
||||||
|
}
|
||||||
|
|
||||||
|
# grab NAME LOG BOXES [PICKER] -- grab the picked window, fail if it is blank, stop the app,
|
||||||
|
# annotate it at its own size.
|
||||||
|
grab() {
|
||||||
|
local name=$1 log=$2 boxes=$3 picker=${4:-pick_window} win geo win_w win_h raw sd
|
||||||
|
win=$($picker)
|
||||||
|
[ -n "$win" ] || { echo "make-help-shots: no window for '$name'" >&2; stop_app; return 1; }
|
||||||
|
geo=$(xdotool getwindowgeometry "$win" 2>/dev/null | awk '/Geometry/{print $2}')
|
||||||
|
win_w=${geo%x*}; win_h=${geo#*x}
|
||||||
|
raw="$tmp/$name.raw.png"
|
||||||
|
import -window "$win" "$raw"
|
||||||
|
sd=$(magick identify -format '%[standard-deviation]' "$raw")
|
||||||
|
awk -v s="$sd" 'BEGIN{exit !(s+0 > 0)}' ||
|
||||||
|
{ echo "make-help-shots: '$name' grabbed a blank window (stddev=$sd)" >&2; stop_app; return 1; }
|
||||||
|
stop_app
|
||||||
|
annotate "$name" "$raw" "$log" "$boxes" "$win_w" "$win_h"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- scene 1: the live window -------------------------------------------------
|
||||||
|
echo "make-help-shots: scene 'main-window'"
|
||||||
|
APP_ARGS="--plots 3" start_app "$tmp/main-window.log" UDPSCOPE_ENABLE_ALL_MS=2500
|
||||||
|
sleep 8
|
||||||
|
grab main-window "$tmp/main-window.log" "$boxes_main"
|
||||||
|
|
||||||
|
# --- scene 2: a held capture --------------------------------------------------
|
||||||
|
echo "make-help-shots: scene 'trigger-capture'"
|
||||||
|
APP_ARGS="--plots 2" start_app "$tmp/trigger-capture.log" \
|
||||||
|
UDPSCOPE_ENABLE_ALL_MS=1200 UDPSCOPE_ARM_TRIGGER_MS=1500
|
||||||
|
sleep 9
|
||||||
|
grab trigger-capture "$tmp/trigger-capture.log" "$boxes_trigger"
|
||||||
|
|
||||||
|
# --- scene 3: the math editor -------------------------------------------------
|
||||||
|
# The editor is a window of its own, presented on the main one, and `math_probe`
|
||||||
|
# (`UDPSCOPE_PROBE_MATH_MS`) opens it -- "editor opened" in its log is the reading. No clicks
|
||||||
|
# are needed: the probe presents it and reopens it after a structural edit, so by the time the
|
||||||
|
# wait ends it is up. It is grabbed on its own rather than through the main window, which is
|
||||||
|
# the whole point of the figure.
|
||||||
|
echo "make-help-shots: scene 'math-editor'"
|
||||||
|
APP_ARGS="--plots 1" start_app "$tmp/math-editor.log" \
|
||||||
|
UDPSCOPE_ENABLE_ALL_MS=1500 UDPSCOPE_PROBE_MATH_MS=2500
|
||||||
|
sleep 10
|
||||||
|
grab math-editor "$tmp/math-editor.log" "$boxes_math" pick_other_window
|
||||||
|
|
||||||
|
echo "make-help-shots: done"
|
||||||
Reference in new issue
Block a user