working on todo.md features

This commit is contained in:
Martino Ferrari committed 2026-10-10 22:52:12 +02:00
1 parent 210d124814
commit 8f54841c2c
110 files changed
+15932 -6576

No files matched your search

+13
View File
@@ -107,6 +107,9 @@ add_executable(udpscope
src/model.cpp
src/condition.cpp
src/trigger.cpp
src/capture.cpp
src/waveform.cpp
src/event_store.cpp
src/acquisition.cpp
src/plot_host.cpp
src/export.cpp
@@ -119,6 +122,16 @@ add_executable(udpscope
src/shortcuts.cpp
src/help_window.cpp
src/ui.cpp
src/ui_probe.cpp
src/ui_widgets.cpp
src/ui_events.cpp
src/ui_status.cpp
src/ui_measure.cpp
src/ui_cursors.cpp
src/ui_math.cpp
src/ui_layout.cpp
src/ui_trigger.cpp
src/ui_signals.cpp
)
target_include_directories(udpscope PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
+225 -9
View File
@@ -26,7 +26,8 @@ Options: `--server`, `--port`, `--multicast`, `--interface`, `--buffer-mb`
(size of the sample buffer, default 200), `--max-window` (largest time
window the toolbar offers, default 600), `--holdoff` (default trigger hold-off),
`--recv-buffer`, `--plots` (the number of stacked plots to start with, 1..8; rows are
added and removed from the **Layout editor** afterwards), `--relative-time` (see below).
added and removed from the **Layout editor** afterwards), `--relative-time` (see below),
`--capture-packets N` (see "Packet capture" below).
Every CONFIG is dumped to stderr once -- publish mode and, per signal, its name,
element count, rate, time mode and time-signal index -- because a signal that
@@ -407,11 +408,201 @@ floored to a **power of two** (`src/model.cpp:83`, `:110`), so a third more weig
third of the span; it costs a whole halving. The measured figures above are the correct ones; a
per-byte computation gives the wrong answer for this reason.
## Packet capture
**`--capture-packets N`** stores the last N raw datagrams on disk for forensic
analysis — the bytes as `recv()` returned them, before the client validates the
magic, reassembles fragments or resequences counters. It is a debugging aid for
*wire* behaviours (a counter that skips, a datagram that arrives twice, a fragment
that never completes, a CONFIG whose header does not parse), not a feature of the
instrument: it is off unless the flag is given, and it adds nothing to the status
bar's glance tier.
The ring lives at `~/.cache/udpscope/<host>-<port>/capture.bin`, the previous
capture is kept one deep as `capture-prev.bin`, and it can be toggled at any time
from the status bar's detail popover (the **Capture packets** button, insensitive
until connected). At the default 1000 slots the file is **~62.6 MiB**, reserved up
front, which is why the size is stated in the tooltip and in the popover row.
Three things it deliberately is not, and the reasons are in
`docs/superpowers/specs/2026-09-29`'s sibling design note:
- **It cannot cost a packet.** The capture is an `mmap` and a `memcpy` on the
producer thread between two `recv()` calls; there is no queue, no lock and no
second copy. The one exception is the toggle itself: `open()` — a rename, a
create, a `posix_fallocate` and an `mmap` — runs on the producer thread when you
click, and measured ~1–3 ms here. The popover row shows the measured `open` cost
in the probe line; a reading far above that is a filesystem finding.
- **It is not interpreted.** `tools/capture_to_pcap.py` reads the file back and
writes a classic pcap, so wireshark does the interpreting:
```sh
tools/capture_to_pcap.py ~/.cache/udpscope/127.0.0.1-8000/capture.bin out.pcap
# 1000 slots, 16 stored, 0 torn, 0 truncated, 0 empty -> 16 packets, 1.0 MiB, ...
```
The counts it prints are a diagnostic in their own right: `torn` above zero means
the capture died mid-copy (a crash is exactly what the on-disk ring exists to
survive), and `truncated` means a datagram longer than the client's receive
buffer, which should be impossible.
- **It does not see the multicast CONFIG channel.** In multicast mode CONFIG
arrives over TCP, and the capture is of the UDP socket only. A malformed
*multicast* CONFIG is out of reach.
## Captured events
Trigger mode keeps the last **32 triggered acquisitions** in a cache, so an event you did not have
time to look at is still there:
```
~/.cache/udpscope/192.168.1.40-8000/events/
20261009-123250-0001.npy # the event, in the export format
20261009-123250-0001.txt # when, how long, how many signals, the rate, the edge
20261009-123250-0001.bmp # a picture of it, drawn from the samples
```
The **left pane** is opened on demand from the toolbar's event-history button, is collapsed by
default, and its **divider can be dragged**: it starts at 240 pixels and remembers the width you last
chose, and while it is closed it takes no room at all. Both the button and the pane exist while **recording is on**, or while an event is on
screen: with the cache disabled and nothing being examined there is nothing to show, and a visible
empty column beside the plot is what *off* must not look like. Recording is switched in the **trigger
panel**, with the trigger's other options rather than in the pane whose contents it decides, and it is
**off by default** -- writing every latch to disk is the user's decision about their disk.
Each row carries **its own picture** beside the numbers -- time, duration, signal count, rate -- in a
fixed 126x42 cell (the preview's own 3:1), so the picture is the same size in every row whatever the
pane's width. Clicking a row **replays** it: the same loader `--open` uses, on the event's file. **The layout does
not change** -- the rows, which signal sits in which slot, the colours, the row count, the time window,
the cursors and the measurements all stay exactly where they are, and now read the stored event instead
of the stream; that is what "show me that event" asks for, and rebinding the rows to the file would
rearrange the user's work under a click. What changes is the data, and the trigger is left, because a
file has no edge to wait for. Cursors, measurements, math channels and another export all work on a
stored event. **Live** takes you back to the stream.
Two buttons under the pane's header read the **selection**:
- *Compare* puts the two selected events in **two plots side by side**, each with **the layout you
already have** -- the same rows, the same signals in the same slots, the same colours -- one showing
one event and one the other. Their axes are **synchronised**: zoom or pan a row in either plot and the
same row of the other follows.
- *Accumulate* overlays up to four selected events in the **same traces**, translucent, so where they
agree the trace is brighter. The events' copies take the free slots of the row that already holds
their signal (a row holds eight traces), so your own traces never move.
Both need events with the same signals and the same length, which is what makes a shared axis honest;
the build checks it and says so in the status bar when it does not hold.
A group is a file view like a replay: the acquisition is stopped (a live producer would push into a
model whose channel count no longer matches the stream), the trigger is left, and *Live* returns.
A replay leaves trigger mode, because a file has no edge to wait for -- an armed trigger keeps a
*frozen capture* on screen (`display_mode()` stays `Held` while the model holds a snapshot), and a
held display draws that snapshot rather than the file that was just loaded. The events pane stays
open through it: it is available while a stored event is on screen.
Three things worth knowing:
- **The samples are the export format**, so an event is readable by everything that reads an
export: `--open`, the chooser's Waveforms page, and `numpy`/`pandas`.
- **The picture is drawn from the data**, one strip per signal with min/max per pixel column and the
trigger edge as a brighter line -- not captured from the window: the plot is a `GtkGLArea`, and a
GL area's ink is not in the widget's paint tree, so a snapshot of it is empty. A data-drawn
preview is also the honest one: it shows the samples, not the zoom the window happened to be at.
- **The picture is decoded synchronously** (`gdk_texture_new_from_filename()` into
`gtk_picture_set_paintable()`), not through `gtk_picture_set_filename()`: that path uses the image
loader's sandboxed, asynchronous decoder, which here fails silently -- the picture keeps a
paint-able and an allocation, so a probe reports success while the pane shows an empty rectangle.
It is then **scaled once to the row's size** and cached by file: a `GtkPicture`'s natural size is
its content, so the stored 480-pixel preview given straight to a row claims the row's whole width
and squeezes the text out of it -- a size request on the picture is a floor, not a cap.
- **A replay stops the acquisition** (a live producer would overwrite the loaded samples within a
frame), so the status bar names the file until *Live* starts a fresh client and thread. The
replayed samples age out of the ring as the stream refills it.
- The switch in the pane's header stops the recording; the cache is only useful if it was already
on when the event you wanted happened, which is why it defaults to on.
## Opening an exported waveform
An export can be opened again — to look at a run you no longer have the source of, and to analyse it
with everything the scope offers:
```sh
udpscope --open ~/udpscope-20261009-113200.npy
```
or start udpscope with no `--server` and use the chooser's **Waveforms** page, which lists the files
opened before (newest first, with the ones that have since been deleted marked *missing* rather than
left to fail on the click) and has an **Open…** button for a new one.
A waveform is a *source that never arrives*, and the design follows from that: there is no socket, no
acquisition thread, and no starvation notice — the status bar's rate is the file's own stored rate and
its Stream row names the file. Everything else is the same machinery a live run uses: one signal and
channel per column, **one row per column** (so a four-signal file opens as four stacked traces, with
three free slots on every row to drag more onto), the samples pushed into the same ring, the display
epoch pinned to the file's last stamp so the axis reads `[-window, 0]` exactly as the export did — which means cursors, measurements,
math channels, the trigger's search and another export all work on it unchanged. The signal names come
from the file: a `.npy` written by `write_npy()` is a structured array whose *dtype* names its columns
(`time`, `abs_seconds`, then one field per signal), so there is no sidecar to lose.
Only `.npy` is read: it is the format the item asked for and the one that carries names. A `.csv`
export is for other tools.
## Named configurations
What you arranged is remembered per data source, and a source can hold **several named
configurations** — different experiments on the same streamer want different plots, different
measurements, and sometimes a different buffer. Each is one file under the source's own
directory:
```
~/.config/udpscope/192.168.1.40_8000/
active # the configuration this source is on, one line
bench.toml # a configuration, plus its note
emi-test.toml
```
A configuration is the whole session: the window, the rows and their signals, the calibration,
the trace colours, which signals were enabled, the math channels with their graphs, the
measurements — **and the settings that can only be decided at startup**, which is why they live
here rather than only on the command line:
```toml
comment = "5 kHz sine, trigger on Ch1, 8 MB is plenty"
buffer_mb = 8
relative_time = false
capture_packets = 0
```
The **buffer size** is the one that matters most: two sources on one machine rarely want the same
ring, and the value is applied before the model exists, which is the only moment it can be — by
whichever of the chooser's rows opened the session, **and by `apply_stored_settings()` when the
source was named on the command line**. A saved host's settings are read either way: opening
`udpscope --server 10.0.0.5 --port 8000` gets that source's stored buffer size, clock and capture
ring, and a flag the user actually typed wins over the file (`--buffer-mb`, `--relative-time`,
`--capture-packets`) because it is the more recent statement of intent. Switching configurations *inside* a running
app applies everything that can change live (layout, plots, math, measurements, enables) and says
so in the status line about the rest (buffer, clock, packet capture): they take effect on the next
start.
The **chooser** (start with no `--server`) lists one row per configuration, with its note and its
settings, so two configurations of one source are told apart before they are opened. Its **New
host…** form takes the address *and* those settings — the configuration's name and note, the buffer
size, and relative time — so a host can be set up before it has ever streamed; the same form opens
from a row's pencil button to **edit** an existing configuration, pre-filled from its file, and
editing the name or the address is a *move* (the old file is deleted, so the list never shows the
same session twice). Connector settings are stored even for a configuration with no signals yet,
which is why those rows exist at all. The app's
menu has **Configurations…**: the same list for the running source, with *Save this session* —
a name and a note — and a delete button per row. Opening one saves the current session first, so
switching is never the gesture that loses what it switches from.
A session written before configurations existed is imported on first use as the configuration
named `default`: copied into the directory, remembered as active, and the old file renamed to
`<host>_<port>.toml.imported` rather than deleted, so the import can be undone by hand.
## The session layout file
What you arranged is remembered per data source, in
`~/.config/udpscope/<host>_<port>.toml` (`$XDG_CONFIG_HOME` is honoured; the
host's unsafe characters are replaced with `_`). The file holds the display
A configuration's file holds the display
window, the row layout and which signal each of a row's slots plots, and the
per-signal gain and offset:
@@ -861,6 +1052,15 @@ another math channel can be its source. The operations are `channel` (a source),
stored rate), `sum`, `difference`, `product` and `ratio`; the arithmetic ops take two inputs, which
is what makes branching — the sum of a raw and a filtered channel — expressible at all.
**A gap is a gap in a filtered trace, never the end of it.** A hole the resequencer reserved is
NaN in the source, and the filters deliberately do not integrate it: the hole shows as a hole in
the filtered trace, the filter's history stays the last real samples it saw, and the trace picks
up again after it. (Integrating it poisoned the biquad's state for ever — every later sample
NaN — which is what a dropped packet used to do to every low pass, systematically in relative
time, where a counter gap reserves NaN slots by construction.) The exit dump's
`math '<name>': finite=N/M` line is where that reads: `finite` must be about `M` minus the hole
share, and `finite=0` beside a non-zero `hole_slots` is the defect back.
The editor is a compact node graph: each node is a box, its inputs are the squares on its left edge
and its output the square on its right. Click a box to select it and its parameters appear in the
form beside the canvas; **drag from an output square to an input square to wire them** (the old
@@ -1389,10 +1589,17 @@ signal if needed, binds it to that row's first free trace slot, and rebuilds the
figure so the legend name is right (there is one widget, so the row a drop lands
on is `PlotArea::row_at()`). The status line reports the outcome of a drop
(`plotting 'sine' on plot 1`, `plot 1 already holds four signals`), so an action
that changes nothing still says why. Each plot's y axis has a right-click menu
with auto scale, a fixed range, **Fit X axis**, **Fit all**, and one
**Remove '<signal>' from plot N** entry per occupied trace slot (a plot with nothing on
it says so instead). The two fits are the same pair the overflow menu and the `f` key
that changes nothing still says why. Each plot has a right-click menu in the same
compact row style as the app's own menu — the label at one left column, a shortcut flush right,
18 px rows — holding **Y axis: auto scale**, **Y axis: fixed range...**, **Fit X axis**, **Fit
all** (the `f` key's action, so it shows `F`), and **Remove signal**, which swaps the menu to a
second view — one entry per trace on that plot, plus **All traces on this plot** — with a **Back**
row to return (a plot with nothing on it says so instead). The removals are a view of their own
rather than a flat list because as a flat list they were the same long sentence repeated once per
trace, which is most of the menu spent on one verb; and they *replace* the menu instead of floating
beside it as a submenu, because a second popover is a second thing to dismiss and a `GtkMenuButton`
that owns one toggles itself out of sync as soon as anything else closes it — its row stopped
opening at all on the second click. The two fits are the same pair the overflow menu and the `f` key
offer, split by what they touch: **Fit X axis** puts the x view back on the whole time
window and leaves every y scale exactly as the user set it, **Fit all** does that and
auto-scales the y axes too. Neither changes the time window itself — see below.
@@ -1985,7 +2192,7 @@ now — they are listed so they are not mistaken for anything else:
progress. What the spec was protecting still holds — a *held* event is bound to its
snapshot alone, and the axis is the event's own window from the edge onwards rather
than a sliding live view.
- **A plot holds at most four signals** (`RowSet::kSlotsPerRow`), because a `Plot`
- **A plot holds at most eight signals** (`RowSet::kSlotsPerRow`), because a `Plot`
cannot give up a trace and the slots are therefore created up front.
- **One toggle drives both cursor orientations.** The vertical markers and the
horizontal ruler exist separately but share one show/hide and one reset.
@@ -2036,6 +2243,15 @@ stall test. Calling that one "connected, but no data is arriving" blamed a secon
that does not exist; the usual cause is a streamer that is not running, or one serving a
different port.
**A link loss does not cost the acquisition.** When the stream comes back and its CONFIG
names the same signals as before — the common case, because whatever knocked the link out
did not reconfigure the producer — the reconnect keeps the buffer, its history, the
trigger's state and any held event, and the outage appears as the gap in the data that it
was. Only a CONFIG that actually *changes* the stream re-plans the buffer, which is the
one case where dropping history is honest. The mock can reproduce the loss itself:
`tools/mock_udpstreamer.py --blackout-at 5 --blackout 3.5` goes silent past the client's
silence timeout and then resumes, which is the EMI case end to end.
## Limitations
- **Multi-element (vector/matrix) signals are acquired with a zero-order hold.**
+18 -1
View File
@@ -48,7 +48,7 @@
#define UDPS_FRAG_MASK_BYTES (UDPS_MAX_FRAGMENTS / 8)
/** Largest datagram we can receive, header included. */
#define UDPS_RX_BUF_BYTES (65535 + 17)
#define UDPS_RX_BUF_BYTES UDPS_MAX_DATAGRAM_BYTES
/** An incomplete reassembly older than this is abandoned. */
#define UDPS_SLOT_STALE_S 2.0
@@ -209,6 +209,9 @@ struct udps_client {
udps_stats_t stats;
char err[256];
udps_datagram_cb datagram_cb; /**< NULL until set; fires inside poll(). */
void *datagram_user;
udps_config_cb on_config;
udps_data_cb on_data;
udps_event_cb on_event;
@@ -1245,6 +1248,15 @@ static void copy_str(char *dst, size_t cap, const char *src) {
dst[cap - 1u] = '\0';
}
void udps_client_set_datagram_cb(udps_client_t *client,
udps_datagram_cb cb, void *user) {
if (client == NULL) {
return;
}
client->datagram_cb = cb;
client->datagram_user = user;
}
udps_client_t *udps_client_create(const udps_client_config_t *cfg) {
udps_client_t *c;
@@ -1468,6 +1480,11 @@ int udps_client_poll(udps_client_t *c, int timeout_ms) {
}
drained++;
c->last_data = now_mono();
/* The raw datagram, before anything reads out of rxbuf: this is the
* only place a malformed or duplicate datagram is still visible. */
if (c->datagram_cb != NULL) {
c->datagram_cb(c->rxbuf, (size_t)n, now_wall(), c->datagram_user);
}
if ((size_t)n >= UDPS_HEADER_SIZE) {
handle_packet(c, c->rxbuf, (size_t)n);
}
+20
View File
@@ -237,6 +237,20 @@ typedef void (*udps_data_cb)(const udps_frame_t *frame, void *user);
/** Called on connection state changes and on recoverable errors. */
typedef void (*udps_event_cb)(udps_event_t event, const char *detail, void *user);
/**
* Largest datagram recv() can deliver on the client's socket, and therefore the
* most a raw-datagram observer ever sees in one call.
*/
#define UDPS_MAX_DATAGRAM_BYTES (65535 + 17)
/** Called for every datagram as recv() returned it, before the magic check,
* reassembly or resequencing: the bytes a capture has to store, and the only
* place malformed or duplicate datagrams are still visible. Runs on the polling
* thread, inside udps_client_poll(), like every other callback. `bytes` is
* valid until the callback returns. */
typedef void (*udps_datagram_cb)(const uint8_t *bytes, size_t len,
double recv_time, void *user);
/**
* @brief Transport configuration.
*
@@ -286,6 +300,12 @@ void udps_client_set_callbacks(udps_client_t *client,
udps_event_cb on_event,
void *user);
/** Installs the raw-datagram observer. Separate from
* udps_client_set_callbacks(), whose signature has call sites and which a
* capture hook is no reason to change. May be NULL. */
void udps_client_set_datagram_cb(udps_client_t *client,
udps_datagram_cb cb, void *user);
/**
* @brief Drives the client: connects if needed, then waits for and processes
* packets for at most @p timeout_ms milliseconds.
+6
View File
@@ -65,6 +65,12 @@ window change moves the trigger with it.
- **Ctrl+drag** frames x to a box, **Alt+drag** y.
- **Fit to data** (menu) auto-scales both axes and frames the whole buffered range.
Each plot has its own **right-click menu**, in the same compact row style as the app's menu:
**Y axis: auto scale**, **Y axis: fixed range...**, **Fit X axis**, **Fit all** (`f`), and
**Remove signal** — which swaps the menu to one entry per trace on that plot plus *All traces on
this plot*, with a **Back** row to return. That is how a trace comes off a row without going through
the layout editor.
The view is bounded by the **time window**, not by the buffer: zooming out stops at the span you set,
and panning stops where the data starts. When the buffer holds *less* than the window — right after a
signal is enabled — the window is left empty on its left, which is what a scope that has just started
+12 -2
View File
@@ -36,14 +36,24 @@ file's basename is that same string, which is what associates a running window w
Options: `--server`, `--port`, `--multicast`, `--interface`, `--buffer-mb` (per
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),
`--capture-packets N` (the raw-datagram ring of README's "Packet capture"; 1..100000,
the value is required, and the popover button uses 1000 when the flag was absent).
The flag is **not** persisted in the session file: persistence of the enable state
belongs with `todo.md` item 3, alongside the relative-time toggle.
Run with no `--server` and udpscope asks which source to open: the sessions it has stored, plus a
form for a new host. Every probe below passes `--server`, so none of them sees it.
`deps/plotter` is built from source with only the GTK4 adapter enabled
(`PLOT_BUILD_GTK4`); its tests and examples are forced off. `deps/udpsclient` is a
vendored C99 source pair compiled into a static lib.
vendored C99 source pair compiled into a static lib. `deps/udpsclient` gained one
additive entry point for the packet capture — `udps_client_set_datagram_cb()`, firing
inside `udps_client_poll()` immediately after the UDP `recv()` returns, before the
magic check or reassembly — plus the public `UDPS_MAX_DATAGRAM_BYTES` the capture
sizes its slots from. A separate setter, not a fourth parameter on
`udps_client_set_callbacks()`: that signature has call sites, and a capture hook is no
reason to change them.
**libadwaita ≥ 1.8 is a system dependency** (`pkg_check_modules(ADWAITA ... libadwaita-1>=1.8)`),
for two widgets: the shortcut overview (`AdwShortcutsDialog`) and the About dialog
+78
View File
@@ -174,3 +174,81 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
— the oldest frame or so of samples — but there its observable is a *prefix plateau*, which is
exactly what the renderer's increasing-suffix filter was written to absorb, so it is a different
symptom and deliberately not fixed here.
## The packet capture sits at `recv()`, and its file is reserved, not sparse
Every wire behaviour a forensic capture exists to answer — a counter that skips, a datagram that
arrives twice, a fragment that never completes, a CONFIG whose header does not parse — is
invisible by the time a `udps_frame_t` reaches `Acquisition::on_data`: the client has already
validated, reassembled, resequenced and dropped. The capture therefore hooks
`udps_client_set_datagram_cb()`, which the client fires immediately after the UDP `recv()`
returns and before anything reads out of `rxbuf`, and it stores the bytes without interpreting
them. The capture is of the *UDP socket*: in multicast mode CONFIG arrives over TCP and is out
of reach.
The ring file is reserved with `posix_fallocate` rather than left sparse because the stores are
`memcpy`s into an `mmap`: a store into a page the filesystem cannot back raises **SIGBUS**, which
kills udpscope outright. An out-of-space condition has to be an error at the moment the user
toggles — reported in the popover row, the button back off — not a crash ten minutes into the
run the capture was switched on for. Within the reserved file, a slot's trailing `seq` word is
published with a release store *after* its bytes are down, so a slot carrying a non-zero `seq`
is complete and no `fsync` is needed for the file to be readable after a crash: the kernel's
dirty pages are the file, and a half-copied slot still reads `seq == 0`.
## A reconnect with an unchanged CONFIG keeps the buffer
A disconnect -- EMI makes it a silence timeout, not a polite DISCONNECT -- must not cost the
acquisition. What historically did was the reconnect's CONFIG: `ScopeModel::set_signals()` rebuilt
the store on *every* CONFIG, and a rebuild is what drops history, resets `tick_`, retires the
holes and invalidates a held event. Now the incoming CONFIG is compared against the stream the
model already has -- names, shapes, rates, ranges, time modes, time-signal indices -- and an
**identical CONFIG returns early**: the ring keeps its history, the trigger keeps its state and
its held event, and the outage shows as the gap in the data that it was. Only a CONFIG that
*changes* the stream re-plans.
The producer's own state is per *connection*, and carrying it across a drop makes recovery read
as failure: `Acquisition::reset_session_state()` (on DISCONNECTED, and again on the next CONFIG)
clears the counter mark, the stream-time anchor and the smoothed arrival interval, so the first
frame of the new session re-anchors instead of reading as late, as a glitch or as an outage. The
arrival clock's *anchor* deliberately stays -- stamps are small seconds the ring's timeline is
already scaled against, and re-anchoring mid-stream would move x. The clock *choice* is re-derived
per connection (`time_source_ = 0` on CONFIG) for the same reason: a session that dropped may
come back with a different answer.
Relative time's `slot_base_` also survives: it is samples-since-the-stream-began, the user's
chosen origin, and must not jump backwards because the link blipped.
## A waveform is a source that never arrives
`--open FILE` (and the chooser's Waveforms page) puts an exported `.npy` into the model instead of a
stream: one signal and channel per column, the samples pushed through `push_loaded_slot()` — the
producer's own order (values, the shared clock, the absolute clock, then the derived channels), under
the model's lock — and the display epoch pinned to the file's last stamp so the axis reads
`[-window, 0]` exactly as the export did. Everything downstream is unchanged, which is the point:
cursors, measurements, math channels and another export work on a file because they work on the
rings, not on the wire.
Three things follow, and each is a place the live assumptions would otherwise leak:
- **Nothing is ever starved.** There is no socket and no source, so the starvation notice is skipped
while a waveform is open, and the popover's Stream row names the file rather than reporting a
connection that has not answered.
- **The loader must not hold the model's mutex.** Every setter it needs (`set_signals`,
`set_signal_enabled`, `set_target_span`) takes that same non-recursive lock; the first version
wrapped the whole load in it and deadlocked on the first call. The per-slot push is a model method
for the same reason.
- **The signal names come from the file.** A `.npy` written by `write_npy()` is a structured array
whose dtype names its fields, so an export is self-describing and there is no sidecar to lose.
`.csv` is not read: it carries the names too, but as text, and the item asked for the binary one.
- **A waveform has no source, and the app must not invent one.** The address the command line
*defaults* to (`127.0.0.1:8000`) is not one: leaving it in place made a file-mode run load — and
then *save* — that source's session, which overwrote the arrangement the file was opened with
(every drop then refused, the row full) and could clobber a real session. `App::source_port` is 0
in file mode, which is what makes `save_layout_now()` refuse and the first-CONFIG layout load skip
— the same two things that make a source-less run safe.
- **One row per column.** Four columns to one row fills the default single row and then refuses
every drop ("plot 1 already holds four signals"), which is a loaded file the user cannot plot
anything *else* on or rearrange. One per row (sized in `main.cpp`, before the UI exists, as
`--plots` does) leaves three free slots each, shows a four-signal file as four stacked traces, and
keeps the rest of the columns draggable. Sizing the rows from inside the activation instead leaves
the panels built for the old count and the app hangs on the first relayout.
+54 -3
View File
@@ -42,12 +42,12 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
runs after the window is destroyed and asking GTK about a widget then is four criticals, which is
how the first version of that line earned them. What stays in the menu is what acts on the *data*
(Fit, Export) and the two editors that are not panes (Layout, Signal list). The Arm/Disarm button in the trigger panel still has text.
- **Six of this app's icons are its own, and they are a `GResource` compiled into the
- **Seven of this app's icons are its own, and they are a `GResource` compiled into the
binary.** No icon theme has a glyph for an oscilloscope trigger, a pair of cursors or a
measurement ruler, and the stock names those buttons used to carry said something else
entirely: a *record dot* for the trigger and three different **magnifiers** for cursors,
measurements and math. `src/icons/scalable/actions/udpscope-{trigger,cursors,measure,math}-symbolic.svg`
are the four action icons; beside them in `src/icons/scalable/apps/` are the two
measurements and math. `src/icons/scalable/actions/udpscope-{trigger,cursors,measure,math,events}-symbolic.svg`
are the five action icons; beside them in `src/icons/scalable/apps/` are the two
**application** icons, `org.udpscope.app.svg` (128px colour artwork) and
`org.udpscope.app-symbolic.svg` (16px). The `apps` context is where an icon theme keeps an
application's own icon and is where the **in-process** window icon resolves from; it is also
@@ -156,6 +156,57 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
They are `GtkPicture`s **with a height request**: one created `can_shrink` asks for *no* height
at all inside a box, so without the request it is allocated nothing and the page renders its text
with a silent gap where its screenshots should be — which is what the first version did.
- **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
position left alone, `shrink-start-child` or not, the pane's minimum is reserved as an empty column
while it is *closed*, and the plot pays for a pane nobody opened. The position is the only knob that
says "no room": store the width when it is closed (reading `gtk_paned_get_position()` *before*
zeroing it) and set 0, restoring it when the pane opens. `gtk_paned_set_shrink_start_child(TRUE)` is
still needed, or the minimum would clamp even a zero position.
- **An unrevealed `GtkRevealer` measures 0x0 and is still given the leftover width.** `reveal_child
= FALSE` makes both the revealer's minimum and its natural size zero -- and its box then hands it
everything the expanding sibling beside it did not claim, so a *collapsed* pane reserved a
310-pixel empty column between the window edge and a plot drawn 630 pixels wide in a 1270-pixel
window. Every vertical revealer in the side panel hides this, because there the leftover goes down
instead of across. `gtk_widget_set_visible()` is what a layout honours, so anything that must not
take room has to be hidden and not merely unrevealed; the revealer keeps the slide while the pane
*is* open. The allocated width is how this was found -- the revealer's own measure said 0.
- **A list box outlives the children of the pane it sits in, and the teardown order is not yours to
choose.** Tearing the window down finalizes the events pane's buttons, then the list box, and the
list's own `selected-rows-changed` then reaches a handler holding the freed pointers: four
`Gtk-CRITICAL`s from `gtk_widget_set_sensitive` and a read of memory nobody owns. `close-request` is
*not* the hook -- `UDPSCOPE_EXIT_AFTER_MS` ends the self-check with `gtk_window_destroy()`, which does
not emit it. The hook that does work is `App::window`: every teardown path clears it *before* the
widgets go (the exit probe says so in a comment: "stop the tick before the widgets go"), so a handler
that can fire during teardown checks that first, rather than asking GTK whether a pointer is still a
widget.
- **A `GtkPicture`'s natural size *is* its content, so a size request on it is a floor, not a cap.**
A 480x160 texture in a row of a list claims the row's whole width and squeezes the text beside it
out of the row -- the box gives a non-expanding child its natural size and grows it further when
the expanding child has nothing left to take. Give the picture a **frame** whose size request equals
the **scaled texture's** size (natural == minimum, which is what stops the box growing it), and
scale the texture once at decode: it is also what keeps a per-row cache at 12 KB per picture rather
than 300.
- **A pointer to a widget inside a list survives the list being rebuilt.** `events_thumb` named a row's
picture across `refresh_events_pane()`, which removes every row first: the next reader of that
pointer got a `Gtk-CRITICAL` from `gtk_picture_get_paintable` on freed memory (the four criticals
read as "the pane is empty"). A rebuild has to null what it is about to destroy, not only rebuild it.
- **`gtk_picture_set_filename()` can render nothing, silently.** It hands the file to the image
loader's sandboxed, asynchronous decoder, which on this machine fails without a message: the
picture keeps a *paint-able* and takes an allocation, so a probe that checks
`gtk_picture_get_paintable()` reads success while the pane shows an empty rectangle (the event
pane's preview did exactly that). `gdk_texture_new_from_filename()` decodes the same file on this
thread and `gtk_picture_set_paintable()` hands over the result -- and a NULL texture at least says
so. A picture that must be on screen is also a picture with a **height request**: `can_shrink`
alone asks for no height at all in a box.
- **Unparenting a popover inside its own `closed` emission is a use-after-free.** Dismissals go
through `gtk_popover_popdown()`, which emits `closed`; a handler that unparents the popover there
drops the reference the parent held, the widget dies, and GTK's own popdown path then runs on freed
memory — `gtk_popover_get_autohide: assertion 'GTK_IS_POPOVER (popover)' failed`, on every menu
entry that dismisses its popover, which is every entry there is. `on_popover_closed()` therefore
defers the unparent by one idle and holds a reference across it
(`g_idle_add_full(..., g_object_ref(popover), g_object_unref)`), so the popover outlives the
emission that dismissed it.
- **`find_accel()` masks the modifiers before comparing.** Lock, Num-Lock and the pointer buttons
all arrive in `GdkModifierType`, so an unmasked compare matches no table row at all once
Caps Lock is on.
+16
View File
@@ -233,3 +233,19 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
`MathNodeRt` was measured and bought **nothing** -- at six nodes the whole runtime is
L1-resident either way. It was kept for the shape, not for a number; do not expect a cache
argument to pay at this size.
## A gap is a gap in a filter's output, never the end of its state
A reserved hole is NaN in the source channel, and a biquad that integrates it poisons `z1`/`z2`
for ever: every later sample comes out NaN, so a filtered trace died at the first drop and never
came back. Relative time made it systematic — a counter gap reserves NaN slots by construction,
where a stream-time gap is often absorbed by the stamps — but EMI caused it in both modes, and
the reading was unambiguous: the memoryless channels held 95% finite samples while every
`LowPass`/`HighPass`/`BandPass` channel held `finite=0`.
`biquad_step()` therefore returns NaN **without touching the state words** when its input is not
finite. The hole stays visible in the filtered trace (the gap is the truth about the data), the
filter's history remains the last *real* samples it saw, and the trace resumes after the hole
with at most the transient a re-start costs. The exit dump's `math '<name>': finite=N/M` line is
the assertion: `finite` must be about `M` minus the hole share, and `finite=0` beside non-zero
`hole_slots` is this defect returned.
@@ -52,3 +52,37 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
out — a source whose streamer was merely down during one run would silently vanish from the
only list it can be picked from, for ever. Keeping the old file loses nothing: a run that
configured nothing had nothing worth persisting.
## A configuration is a named session, and a source may hold several
One source, several arrangements: the same streamer serves different experiments, and retyping a
layout to look at the second one is how a session gets lost. Storage is therefore a **directory
per source** — `~/.config/udpscope/<host>_<port>/` — holding one `<name>.toml` per configuration
plus an `active` file naming the one the source is on. `configuration_path()` is the only
expression of the scheme, and the name is `sanitize()`d exactly as the host is, so a name typed
with a slash cannot escape the directory.
A configuration carries the whole session *and* the settings that can only be decided before the
first socket: `buffer_mb`, `relative_time`, `capture_packets`. The startup chooser applies them
(`apply_source()`), which works because `choose_source()` runs before `ScopeModel` is constructed
— the one moment a buffer size can be applied at all. Switching configurations inside a running
app applies what can change live and says in the status line that the rest takes effect on the
next start; pretending otherwise would be a configuration that half-applies.
A configuration's *startup* settings reach the model from two directions, and both are needed:
`apply_source()` for a source picked in the chooser, and `apply_stored_settings()` for a source
named with `--server`, which never reaches the chooser at all. Without the second, a saved host's
buffer size, clock and capture ring were silently ignored — the layout still applied, because the
app loads that separately once a CONFIG names the signals — so an edit to any of the three read as
one that never took effect. **A flag the user typed wins over the file** (`--buffer-mb`,
`--relative-time`, `--capture-packets`): the file fills in what the command line did not say, and
`Config::buffer_mb_specified` is what tells "not asked" from "asked for the default".
The `active` file is written on every save and by the chooser's pick, so "which configuration was
I on" survives a restart. A session written before configurations existed is **imported once** as
the configuration named `default`, and the old file is renamed to `<host>_<port>.toml.imported`
rather than deleted: an import that cannot be undone by hand is a migration nobody can back out
of.
Nothing is lost by switching: the dialog saves the current session *before* loading the picked
one, which is the only ordering in which a switch cannot lose what it switches from.
@@ -144,3 +144,134 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
`process()` uses that for `last_trigger_s_`, `pending_start_s_` **and** the pre-window gate —
all three, or the marker, the cut and the gate would disagree with each other by the width of
a pulse.
## A latch is also a file
The pane that lists the cache is **on demand**: the toolbar's event-history button and the pane are
collapsed until it is asked for, because a column of stale events beside a live plot is what a user
who has stopped looking at events does not want. What makes it *available* -- (recording is on) or
(an event is on screen) -- is decided in one place, `sync_events_availability()`, so the button and
the pane cannot disagree; a disabled cache with nothing being examined takes the button away *and*
collapses the pane, because a visible empty column is what "off" must not look like. Trigger mode is
deliberately **not** one of the conditions: a stored event being examined is a file, which has no
trigger, and tying the pane to the trigger would close the list under the click that opened an event.
The recording switch lives in the trigger panel, not in the pane it empties.
## A comparison is two plots of one model
*Compare* and *accumulate* are still one model -- the buffers cannot hold two independent loads -- but a
comparison is **two plots** over it, and which plot draws what is only a matter of which channel *names*
its rows bind. `App::rows` is the primary (`RowSet`, area, gestures); `App::rows_b` is the second, with
its own `RowSet` and its own hidden `PlotArea` in a paned, deliberately without the drop target, the
right-click menu or the frame counter: those act on the primary's rows, and a menu on the second plot
that rearranged the first plot's rows would be a menu lying about its subject.
Rules that cost a defect each:
- **The layout is the user's and is mirrored, never reinvented.** The group build must not call
`set_row_count()`; it reads the rows' slot names and re-names the same slots for each layer
(`… (A)`, `… (B)` for a comparison; a bare name for layer 0 and `… #2`, `… #3` for the extra layers of
an accumulation). An *accumulation* puts its extra layers into free slots -- the row that holds the
signal first, then any row with room -- so the user's traces never move. The default layout is four
signals in one row, which is why `kSlotsPerRow` is 8: with four, "accumulate these two events" had
nowhere to put the second copy and silently did nothing.
- **A layer is not a signal.** A comparison's model holds one channel per (signal, layer), and the
*panel* must not treat the copies as signals of their own: the signal list shows one row per signal
(`layer_base_name()`, the shared rule that takes `(A)`, `(B)` and `#N` back off), `signal_color_index()`
counts *signals* and not channels -- counting channels spread one signal's colour across the palette
-- and both plots of a comparison bind their signal's layers to the *same* colour index, because two
colours would say they were different signals. The reading is `list=4` in `[events] compare plots`
for a two-event comparison of four signals.
- **A slot name carries the last group's suffix, so it must be stripped before it is re-derived**:
after a comparison the rows are named `sine (A)`, and a following accumulation that mapped that name
again bound to no channel at all (`traces0=0`, a plot with nothing in it). `base_name()` takes
`(A)`, `(B)` and `#N` back off.
- **The axes are mirrored from the ranges, once per frame.** The plotter owns its window and a gesture
reaches it through the plot's own setter with nothing announcing it, so the frame compares each row's
signature with the previous frame's: whichever side moved is the side the user touched, and the other
is set to match. The signature is `{span, y.min, y.max}` when the x axis has a **live edge**
(`x_live_edge()`, not merely an armed follow: an armed axis with nothing streaming keeps a *stored*
window, whose edges a pan moves and whose span never changes) and `{x.min, x.max, y.min, y.max}`
otherwise. Comparing the edges under a live edge would read as "both sides moved" on every frame --
the window is recomputed as a distance behind the newest sample -- and a zoom in one plot would be
overwritten by the other's drift. When both moved the first plot is the reference, which is also what
makes the frame after a build converge.
- **`set_y_range()` clears that axis' auto-scale**, by design: a mirrored zoom is a user decision, and
*Fit* re-arms it.
## A group of events is one model, built once
*Compare* and *accumulate* are the same machinery: several event files in **one** model, because the
buffers cannot hold two independent loads -- `load_waveform_into_model` calls `set_signals()`, which
rebuilds them, so a second load takes the first with it. So the group is built by hand: one channel per
(signal, layer) pair, `set_target_span()` to cover the whole group *before* `set_signals()` (or the
oldest samples are written past the ring's end), then all of the events pushed in one pass. A row is
one **signal** and its slots are the events, which is what makes a comparison like for like and an
accumulation a density; the trace alpha (0.35 while accumulating, 1 otherwise) is what turns agreement
into brightness.
Two things every group must do, both of which cost a defect to learn:
- **Stop the acquisition first.** A live producer keeps pushing into the model it was started with --
and it pushes *four* stream signals into the group's *eight* channels, so the group's own samples are
the ones that go missing. The reading that catches it is the samples the busy channel holds
(`samples0` in the `[events] compare` line): it must be exactly the number of stamps pushed, not
that plus whatever the stream added.
- **Free the slots the group does not fill.** A row carries the previous view's binding, and a trace
left in slot 2 of a two-layer row is drawn as if it belonged to the group.
*Compare* shifts the first event so its span *ends* where the second's *begins*, and fills the half a
sample does not belong to with **NaN** -- the plotter excludes non-finite samples from every series and
from every pyramid bucket, so a NaN is a gap and not a repeat, and no line is drawn across the join.
**A replay changes the data and frames it once; it never rearranges the layout.** A row plots a signal
*name*, and an event's export carries the same names the stream does, so loading the event into the
model leaves every row, slot, colour and the row count pointing at the right signal by itself. So
`replay_event()` must **not** rebind the rows (`bind_channels_to_rows()`, which is what a freshly opened
*file* needs) -- that is the user's work, and a click on a list is not a request to rearrange it. The
window is restated **once, on open** (`apply_time_window(span)`, the only path allowed to change the
scale), so the whole event is framed the moment it is opened and the user's own zooms are left alone
afterwards; never per frame. The measurements, the cursors and the readouts then report the stored
event, because they read the model. What a replay *does* change is the model's contents and the trigger:
it leaves trigger mode, without which the display stays `Held` and draws the old snapshot.
**A file view has nothing to acquire.** The signal list's acquire tick is *hidden* -- not disabled --
whenever a file is on screen (`App::waveform_name` non-empty: a replay, a comparison, a `--open`run).
The samples are stored, "acquire this signal" is a stream's question, and a greyed tick invites the user
to look for a reason there is none for. Going live rebuilds the panel, so the ticks come back.
**"The same length" is the same count at the same rate, not the same span to the sample.** Two events
this app recorded itself differ in span by the fraction of a sample the acquisition's window happened to
fill to (0.9999 s of a 1 s pre-window is normal). Demanding one sample interval made a comparison of two
just-recorded events refuse to open; the check allows four intervals or a millisecond, whichever is
larger, and the merged stamp grid carries the difference as a gap.
**A replay must leave trigger mode.** `display_mode()` answers `Held` whenever the model holds a
snapshot, and a held display draws *the snapshot*, not the live channels -- so replaying an event with
trigger mode still armed loaded the file into the model and left the old capture on screen: a click
that looks like it did nothing, with the event sitting in the ring unread. The replay also frames the
view with `apply_time_window(report.span_s)`, so the click shows the whole event rather than the part
of it that fell inside the live window.
The switch that decides recording belongs to the **trigger panel**, not to the pane it empties: it
is a trigger option (it decides whether a latch is written at all), and a pane must not be the only
place to reach the control that stops it.
Every published snapshot is written to the event cache while recording is on: its table (the *export*
format, so `--open` reads it back), a sidecar of what the list shows, and a preview drawn from the
samples. Three rules keep that honest:
- **The record happens on the GTK thread, at the end of the tick that framed the snapshot**, not on
the producer. The snapshot is taken by `shared_ptr` (a latch landing mid-write cannot pull the data
away), and a `record_pending` flag defers the write to the end of the frame so the rows are already
bound to the event when the picture is drawn. Recording from the producer would put file I/O
between two `recv()` calls, which is the one thing the packet capture's design also refuses.
- **Nothing a latch does may be expensive.** The write is a `write_npy` of the event's own window and
a 480x160 raster; a 2 s, four-channel event is ~230 kB of picture and ~600 kB of samples. The pane
is refreshed, not rebuilt, and a failure to write is a line on stderr -- a cache that cannot be
written must not stop the scope.
- **The preview is drawn from the data, never captured from the window.** The plot is a `GtkGLArea`:
its ink is not in the widget's paint tree, so `gtk_snapshot_free_to_node()` on a widget paintable of
it returns NULL and the "screenshot" would be an empty file. Drawing it in the store also keeps that
class GTK-free, which is what makes the cache testable without a window.
+179 -1
View File
@@ -334,7 +334,7 @@ Two lines of the self-check output are assertions in disguise:
floor are the two refusals, and because the probe emits `::clicked` straight at the
widget it reaches the handler even when the button is insensitive, so `add above 0` there
must leave `rows=` and `traces=` untouched rather than growing a ninth row.
- `header: window_button_w=W label_w=L bar_w=B` (stderr, once a second) — **`W` must be
- `header: window_button_w=W bar_w=B` (stderr, once a second) — **`W` must be
a small constant and must not track `B`**: the window control is a header-bar child,
and an expanding one is measured into the title's centre box, so it stretched from the
label to the title (516 px at a 1270 px bar before the fix, 121 px after). A `W` that
@@ -801,3 +801,181 @@ Measured output (port 8419):
disarmed the trigger — so `placed=0` there is legitimate and asserts nothing about the search.
The load-bearing `placed=` is the `[search-probe] marks` line, emitted a frame after the jump,
which quotes the window the scope has actually moved to.
## `[capture-probe]`
`UDPSCOPE_PROBE_CAPTURE_MS` toggles the packet capture through the status-bar button's own
handler, then reads the ring file back off disk and prints one line:
```
[capture-probe] enabled=1 read_ok=1 path=/home/m/.cache/udpscope/127.0.0.1-9102/capture.bin slots=1000 stored=253 resident=253 cursor=253 torn=0 truncated=0 header_ok=1 first_seq=1 last_seq=253 payload=2031337 file=65576064 open_ms=2.9 t0=1791524811.925
```
| field | value it must read | the defect a wrong reading names |
| --- | --- | --- |
| `enabled` | `1` after the probe's toggle; `0` when the open failed, with the reason on stderr | a toggle that sets a flag nobody acts on, or an open that failed silently |
| `read_ok`, `header_ok` | `1` | a file whose header does not round-trip — the format changed without the version bump |
| `stored` | datagrams since the toggle; equal to the exit dump's datagram count over the same interval | the producer storing more or fewer datagrams than the client handed it — a copy in the capture path, which the design forbids |
| `resident` | `min(stored, slots)`; equal to `slots` once wrapped | the ring reusing a slot before its `seq` was published — a torn slot that reads as a hole in history |
| `cursor` | `stored mod slots` | a cursor maintained out of step with `seq`; the file's hint would mislead the `xxd` user it exists for |
| `torn` | `0` | a slot whose bytes landed but whose `seq` never did *without* a crash — the release-store ordering is wrong |
| `truncated` | `0` | a datagram longer than `UDPS_MAX_DATAGRAM_BYTES` — the slot sizing disagrees with the client's receive buffer |
| `first_seq` | `1` on a fresh ring | the ordinal not starting where the file starts — a leftover file being appended to instead of renamed |
| `last_seq - first_seq` | `slots - 1` on a wrapped ring | ordering lost across the wrap: the newest slot is not the largest `seq` |
| `payload` | the bytes the wire delivered over the interval | a capture storing headers and not payloads, or truncating silently |
| `file` | `64 + slots * slot_bytes`, a constant for a given N | the reservation is short — which with `mmap` is the SIGBUS the design forbids |
| `open_ms` | single-digit ms on a local filesystem | a blocking filesystem between two `recv()` calls — the producer stall the popover's toggle costs |
With a **failing** open (the cache path colliding with a plain file, a full disk), the line reads
`enabled=0 read_ok=0` with every count 0, the toggle returns to off on the next tick, and the
popover row reads `failed: <strerror>` — one stderr line, no retry storm against a full disk.
## `[defaults]` and `[configs]`
```
[defaults] calibrate: 1 0 model=1 0 | trigger: 0.001 0.002 0 0.001 0 50 0.01
[defaults] calibrated: 2.5 -1.25 model=2.5 -1.25
[configs] before=default active=default
[configs] save-as=1 names=probe,default active=probe
[configs] round trip read=1 comment='written by the configs probe' buffer_mb=8 relative=0 capture=0 plots=1 signals=4 measures=0 math=0
[configs] after forget names=default
```
| field | value it must read | the defect a wrong reading names |
| --- | --- | --- |
| `[defaults] calibrate:` | `1 0 model=1 0` | an unseeded spin button: `gtk_spin_button_new_with_range()` leaves the value at the range minimum, so the offset opened at -1e12 and Apply wrote it |
| `[defaults] calibrated:` | `2.5 -1.25 model=2.5 -1.25` — the dialog and the model agreeing after a calibration was set | a dialog that opens at the identity while the trace is drawn through a gain: pressing Apply "confirms" the calibration away. The two halves are printed so the assertion is that they *match*, not that either is a particular number |
| `[defaults] trigger:` | `0.001 0.002 0 0.001 0 50 0.01` | the same, per trigger field; a level of `-1e12` is a number nobody chose |
| `[configs] names` | gains `probe`, and loses it after `forget` | a save that writes somewhere the listing does not read, or a delete that leaves the file |
| `[configs] active` | `probe` after the save-as | the `active` file not written, so the next start opens the wrong configuration |
| `[configs] round trip` | `read=1`, the note verbatim, `buffer_mb` non-zero | the note or the startup settings not persisted — a configuration that carries a layout but not the buffer size it was tuned with |
## `[rowmenu]`
```
[rowmenu] main 4 'Remove signal' h=18 label_x=26 accel_right=-1/174 ok=1
[rowmenu] main rows=5 bad=0 labels=[Y axis: auto scale,Y axis: fixed range...,Fit X axis,Fit all,Remove signal]
[rowmenu] remove 0 'Back' h=18 label_x=52 accel_right=-1/178 ok=1
[rowmenu] remove rows=6 bad=0 labels=[Back,sine,square,ramp,noise,All traces on this plot]
[rowmenu] summary main_rows=5 main_bad=0 remove_rows=6 remove_bad=0 after_back_rows=5 after_back_bad=0 second_rows=6 second_bad=0 remove_row_in_main=1 remove_row_gone=1 can_open_again=1
[rowmenu] remove-clicked 'sine' trace_freed=1
```
| field | value it must read | the defect a wrong reading names |
| --- | --- | --- |
| `h` | 18 for every row of both views | the plot menu reverting to Adwaita's 34 px buttons — the row style not applied (`popover.udpscope-menu`) |
| `label_x` | 26 for every row, 52 for **Back** | labels not sharing one column; the Back row's 52 is its leading icon, which every menu indents for |
| `accel_right`/`W` | equal when the row has a shortcut (`Fit all` → `F`), `-1` when it has none | the shortcut not flush right, or shown where the action has none |
| `remove_rows` | 1 per occupied trace slot, plus `All traces on this plot` when there is more than one, plus the **Back** row | a remove view built from the wrong row, or missing its way out |
| `remove_row_gone`, `remove_row_in_main` | 1 | a submenu that floats beside the menu instead of replacing it |
| `can_open_again` | 1 | the second click not reopening: the `GtkMenuButton`-owned submenu that collapsed and left its row dead |
| `bad` | 0 for every view | any row outside the compact style. Each view is measured a step after the click that swapped it in — in the same call the new child is unallocated (`h=0`), which reads like a style defect and is not one |
| `trace_freed` | 1 | a remove entry that dismisses the menu without taking the trace off the row |
## `[sources]`
```
[sources] form: name='probe-host' note='made by the new-host probe' buffer=32 relative=1 connect_sensitive=1
[sources] configuration created: 9.9.9.9:7000/probe-host note='made by the new-host probe' buffer=32 relative=1
[sources] picked 9.9.9.9:7000 multicast= interface= buffer=32 relative=1 capture=0 config=probe-host note='made by the new-host probe'
[sources] edit form: name='probe-host' note='made by the new-host probe' buffer=32 relative=1 button='Save'
[sources] configuration saved: 9.9.9.9:7000/bench note='edited by the probe' buffer=16 relative=0
[sources] 1 known: 9.9.9.9:7000/bench plots=0 signals=0 buffer=16 relative=0 capture=0 note='edited by the probe' |
```
| field | value it must read | the defect a wrong reading names |
| --- | --- | --- |
| `form:` | the name, note, buffer and clock the form was given | a form field that is not wired: the four things a host could not be given before |
| `connect_sensitive` | 1 once the server is typed | the button's validation not following the entry |
| `configuration created`/`saved` | the identity the form held, with the settings | a save that writes somewhere other than what the form shows |
| `edit form:` … `button='Save'` | the **existing** configuration's values, pre-filled | the edit path not reading the file — an edit form that opens empty edits nothing |
| `known:` after a rename | one row, under the new name | a rename that leaves the old file, so the chooser lists the same session twice |
| `known:` for a settings-only configuration | a row, though `signals=0` | the listing filter treating "no signals yet" as "nobody configured this", which hides every host the form set up |
## `[sources] settings from configuration`
```
[sources] settings from configuration 'bench': buffer=64 relative=1 capture=500
```
Printed by `apply_stored_settings()` when a source is named on the command line: the stored
configuration's startup settings, filled in where the command line was silent. A `buffer=` here
that disagrees with `--buffer-mb` is a flag being overruled by the file; a *missing* line, with a
configuration that carries settings, is the defect this exists for — a saved host opened with
`--server` ran on the defaults, so an edit to its buffer or clock never took effect.
## `[sources] waveforms` and the waveform load
```
[sources] waveforms: 1 recent /tmp/rt.npy(ok) page=1
[sources] picked waveform /tmp/rt.npy
[udpscope] waveform loaded: /tmp/rt.npy signals=4 samples=20001 span=2.000000s rate=10000 time=[-2.000000..0.000000]
```
| field | value it must read | the defect a wrong reading names |
| --- | --- | --- |
| `waveforms:` | one entry per remembered export, with `(ok)` or `(missing)`, and `page=1` | the Waveforms page not built, or a stale path offered as if it were openable |
| `picked waveform` | the selected path | a pick that carries no file: `--open` is what the mode reads |
| `signals=`, `samples=` | the export's `cols=` and `rows=` | a reader mis-slicing its records, or the columns bound to the wrong channels |
| `span=`, `rate=`, `time=[…]` | the exported window and rate, `[-window, 0]` | the plan not covering the whole file (the tail arrives as the whole picture) or the display epoch not pinned to the file's last stamp |
| `frames=0 packets=0` beside `channels=N` | 0 — no acquisition | a socket opened for a file, which would also make the starvation notice live |
## `[drop-probe]`
```
[drop-probe] signals=4 channels=4 rows=4 slots0=[sine|||]
[drop-probe] dropped signal 0 -> 1 slots=[sine|sine||]
[drop-probe] dropped signal 1 -> 1 slots=[sine|sine|square|]
```
| field | value it must read | the defect a wrong reading names |
| --- | --- | --- |
| `rows` | the column count, capped at `RowSet::kMaxRows` — one row per trace | rows sized four signals to one row: the only row fills and *every* drop is refused ("plot 1 already holds four signals"), which reads as a scope that cannot plot anything |
| `dropped … -> 1` | 1, and the row gains that trace | a drop that cannot find a free slot, or a signal with no channel to bind |
| `channels` | one per signal | a file whose columns did not become channels, so nothing can be plotted at all |
## `[events]`
```
[events] live, recording off: button_visible=0 pane=0 pane_w=0 tick=1 recording=0
[events] recording on, no trigger: button_visible=1 pane=0 pane_w=0 tick=1 recording=1
[events] after trigger on: button_visible=1 pane=0 pane_w=0 tick=1 recording=1
[events] dir=/tmp/k5/cache/udpscope/127.0.0.1-21023/events n=1 newest: dur=2.000000 pre=1.000000 post=1.000000 edge=1.000100 signals=4 samples=20001 rate=10000 seq=1 thumb_bytes=230454 recorded=1 pane=1
[events] activated row0: replaying=1 pane=1 pane_w=240 tick=0 window=2.000 thumbnail=1 thumb_size=126x42 thumb_visible=1
[events] replayed: channels=4 samples=20001 replaying=1 waveform=1
[events] live again: replaying=0 waveform=0
[events] recording off, trigger off: button_visible=0 pane=0 pane_w=0 tick=1 recording=0
[events] recording off, trigger on: button_visible=0 pane=0 pane_w=0 tick=1 recording=0
[events] compare buttons: cached=2 selected=2 compare=1 accumulate=1
[events] compare: kind=1 rows=1 channels=8 samples0=32964 traces0=4 traces1=-1 window=2.000 recording=1 pane=1
[events] compare plots: area_b=1 rows_a=1 rows_b=1 list=4 x_a=[-2.000,0.000] x_b=[-2.000,0.000] same_x=1
[events] compare sync: y_a=-3.500 y_b=-3.500 mirrored=1
[events] accumulate: kind=2 rows=1 channels=8 samples0=32964 selected=2 traces0=8 window=2.000
[events] group left: replaying=0 kind=0 waveform=0
```
`thumb_size` is the built size of the row's picture (96x32 logical, plus the CSS border), so it moves with
the font and the display's scale factor; what it must never be is `0x0`.
| field | value it must read | the defect a wrong reading names |
| --- | --- | --- |
| `n`, `stem`, `seq` | one entry per latch, named by its time and the run's counter | two events in one second overwriting each other, or a cache that is never written |
| `dur` | `pre + post` for the event's window | the snapshot's own window not recorded, so the list disagrees with the picture |
| `signals`, `samples` | the event's channels and rows | a table built from the *rows* instead of the snapshot: only what happened to be plotted |
| `thumb_bytes` | a real 24-bit BMP (`54 + 480*160*3`) | a preview that is empty or missing -- the widget snapshot of a `GtkGLArea`, which is NULL |
| `recorded` | 1 while the trigger panel's switch is on | the switch not wired, or recording defaulting off (it must be on: the cache is for the event you did not expect) |
| `pane` | 1 once the history button has been clicked | the button's handler not revealing the pane |
| `live` | `button_visible=0 pane=0** | a pane or a button on screen with no trigger mode: a column of nothing beside a live plot |
| `after trigger on` | `button_visible=1 pane=0` | the pane revealed by trigger mode alone (it must be on demand from the toolbar button), or the button missing in trigger mode |
| `recording off, trigger off` and `recording off, trigger on` | `button_visible=0 pane=0 recording=0` in both | the pane surviving its own switch being turned off -- the empty column the item exists to prevent -- or the button coming back with trigger mode while the cache is disabled |
| `thumbnail`, `thumb_size`, `thumb_visible` | 1, a non-zero size, and 1 | the picture loaded through `gtk_picture_set_filename()`, which fails silently (the paint-able is there, the allocation is there, nothing is drawn), or a `GtkPicture` given a size request and no cap -- its natural size is its content, so the row's text is squeezed off. `thumb_size` is the reading that catches both |
| `replaying` (last line) | 0 | a replay with no way back: *Live* must start a fresh client and thread |
| `pane_w` | the divider's position: 0 while the pane is closed, its remembered width (240 by default) while it is open | the pane reserving a column while closed (a paned hands its start child its minimum unless the position says otherwise), or a width that is not remembered |
| `tick` | 1 in live mode, **0** once a file is on screen | the signal list still offering "acquire this signal" for samples that are already stored |
| `window` (in `activated row0`) | the **event's** span -- the probe sets 0.5 s just before the click, so reading the event's span says opening it framed it | a replay that leaves the previous window alone, or one that restates it every frame instead of once |
| `compare buttons` | `selected=2` and both buttons sensitive | the buttons reading something other than the selection, or staying insensitive with a valid pair selected |
| `compare` | `kind=1 rows=1 channels=8 samples0=33045 traces0=4`, `pane=1` | `samples0` above the stamps pushed -- a live producer left running, pushing four stream signals into the group's eight channels; `rows=1` because the layout is the user's and must not be re-derived; `traces0=4` because the four slots the layout holds are renamed, not replaced |
| `compare plots` | `area_b=1 rows_a=1 rows_b=1 list=4 same_x=1` | no second plot on screen, a second plot with a different row count, two plots not on one x window, or `list` above the signal count -- the panel listing each event's copy of a signal as a signal of its own |
| `compare sync` | `mirrored=1`, with `y_a` and `y_b` equal to the range the probe set on the first plot | the mirror not running, or running the wrong way: a y (or x span) change in one plot that the other never sees |
| `accumulate` | `kind=2 rows=1 channels=8 samples0=33045 traces0=8 window=2.000` | the second event's layers not landing: `traces0` stays at the four signals the layout holds when the slots were not free, or the row named after a previous group's suffix so nothing binds |
| `group left` | `replaying=0 kind=0 waveform=0` | the group surviving *Live*: a file view with no file behind it |
+153
View File
@@ -220,6 +220,159 @@ as distinct from the fault injection below, which exercises ingest:
| **re-plan** | `--rate 100000` with the `mixed` flags, and `UDPSCOPE_PROBE_WINDOW_MS=4000 UDPSCOPE_PROBE_RESEQ_MS=6000 UDPSCOPE_EXIT_AFTER_MS=9000` | four `buffer rebuilt` lines before the reading, then `bands=64 bands_repaired=16 nan_slots=7800`: **a band is still only marked repaired when its own slots hold values**. A `bands_repaired == bands` beside a non-zero `nan_slots` here is the hole table and the producer's `tick_` surviving a re-plan that reset `produced()` — a band marked settled over samples that are all still NaN. The non-zero `overflows` is this case too: 100 kS/s with a 50 ms reorder delay keeps about 50 packets in flight against 64 slots |
The re-plan case needs `--rate 100000`, not the other four's 10 000: at 10 kS/s the ring
- `UDPSCOPE_PROBE_CAPTURE_MS` — toggles the packet capture **through the same handler the
status-bar button uses**, waits, then reads the file back off disk and prints one line:
`enabled= path= slots= stored= resident= cursor= torn= truncated= header_ok= first_seq=
last_seq= payload= file= open_ms= t0=`. `stored` is the datagrams-since-open counter and must
equal the dump's datagram count from the toggle's instant; `resident` is what the file holds,
which is the slot count once the ring has wrapped; `file` is the reserved size
`64 + slot_count * slot_bytes` and is a constant for a given N, so a mismatch against the file
on disk is a reading. Two cases, **run each as its own process**:
1. **enable at runtime** — the mock at `--rate 20000 --batch 200`, udpscope with
`UDPSCOPE_ENABLE_ALL_MS=700 UDPSCOPE_PROBE_CAPTURE_MS=1500 UDPSCOPE_EXIT_AFTER_MS=9000`
and no capture flag: `enabled=1 header_ok=1 torn=0 truncated=0 first_seq=1`, `stored` equal
to the datagrams since the toggle, `open_ms` a few ms.
2. **wrap** — the same with `--capture-packets=16` and
`UDPSCOPE_PROBE_CAPTURE_MS=7000 UDPSCOPE_EXIT_AFTER_MS=10000`: `stored` (870 in the
reference run) exceeds `slot_count`, `resident=16`, `last_seq - first_seq == 15`,
`torn=0`, and `cursor` agrees with `seq` (counter mod slot count). A wrap that loses
ordering, or a `cursor` that disagrees, shows here and nowhere else.
Then the converter over the wrap case's file: `tools/capture_to_pcap.py <capture> out.pcap`
must exit 0, print a record count equal to `slot_count` with `torn` 0, and the pcap's first
and last record timestamps must fall inside the run's wall-clock interval — both checked by a
`struct.unpack` of the pcap headers, nothing outside the standard library.
- **A gap must not kill a filtered trace.** `--relative-time` with the mock dropping
(`--drop-every 20`) and `UDPSCOPE_PROBE_MATH_MS=2500 UDPSCOPE_ENABLE_ALL_MS=600
UDPSCOPE_EXIT_AFTER_MS=9000`: every derived channel's dump line
`math '<name>': finite=N/4000` must read about 3800 — the tail minus the reserved holes —
and **`finite=0` beside a non-zero `hole_slots` is the defect** (a biquad that integrated the
hole's NaN, permanently). The filters' `sd` must stay near the no-drop value; run the same
command without `--drop-every` as the control, where every `finite` reads 4000.
- `UDPSCOPE_PROBE_CONFIGS_MS` — saves the running session as a configuration named `probe` with a
note, through `save_configuration_as()` (the same entry point the dialog's button uses), then
reads it back off disk. Reading: `save-as=1`; `names=` gains `probe`; `active=probe`; and the
round trip must carry the note **and** the startup settings —
`comment='written by the configs probe' buffer_mb=8 relative=0 capture=0`. A `buffer_mb=0` there
is the per-source buffer size not being written, which is the one setting a user notices only
after a capture has been sized wrong. Run it with a throwaway `XDG_CONFIG_HOME`; the probe
deletes its own configuration at the end (`after forget names=…`).
- `UDPSCOPE_PROBE_ROWMENU_MS` — gives row 0 a trace, opens the plot's context menu with
`open_row_menu()` (the one entry point `on_right_click()` wraps) and walks its two views one step
at a time (`main` → click **Remove signal** → `remove` → click **Back** → `after-back` → click
**Remove signal** again → `second-open` → click a trace), measuring each view's rows:
`[rowmenu] <view> N '<label>' h=18 label_x=26 accel_right=R/W ok=1` per row and one
`rows=/bad=/labels=` line per view, then `[rowmenu] summary main_rows=5 main_bad=0 remove_rows=6
remove_bad=0 after_back_rows=5 after_back_bad=0 second_rows=6 second_bad=0 remove_row_in_main=1
remove_row_gone=1 can_open_again=1` and `[rowmenu] remove-clicked 'sine' trace_freed=1`.
**Every `bad` must be 0** (the same compact row style as the app's own menu: one label column, a
shortcut flush right, 18 px rows — the **Back** row's icon indents its label, which every menu
does, so its `label_x` is 52 against the others' 26) and the three booleans must all read 1: the
remove view *replaces* the main one (`remove_row_gone`, `remove_row_in_main`), and it can be
entered again after leaving it (`can_open_again`) — that last one is the case a
`GtkMenuButton`-owned submenu failed, its row going dead on the second click. Each view is
measured a step after the click that swapped it in, because a popover allocates its new child on
the next layout pass.
- `UDPSCOPE_PROBE_DEFAULTS_MS` — opens **Calibrate** and reads every spin button in it and in the
trigger panel: `[defaults] calibrate: 1 0 model=1 0 | trigger: 0.001 0.002 0 0.001 0 50 0.01`. A
`GtkSpinButton` starts at its range's *minimum*, so a `-1e12` here is an unseeded button the
dialog's own reader would then apply — which is exactly what the calibration offset did. It then
calibrates signal 0 through `set_calibration()` and reopens the dialog:
`[defaults] calibrated: 2.5 -1.25 model=2.5 -1.25` — **the dialog must show the signal's stored
calibration**, and the two halves must match.
- The **chooser**, display-free: `UDPSCOPE_PROBE_SOURCES=1` prints `[sources] N known:` with one
entry per configuration (`host:port/name … buffer= relative= note='…'`), and
`UDPSCOPE_PROBE_PICK_MS=800` opens the chooser, picks the first row and prints
`[sources] picked host:port … buffer=64 relative=0 capture=0 config=… note='…'` — the settings the
configuration carried, into the `Config` the model is built from. A legacy file
(`<host>_<port>.toml`) produces `layout loaded from …/<host>_<port>/default.toml`, an `active`
holding `default`, and the old file renamed `.imported`.
- **The event cache round-trips, and only where it is available.** `UDPSCOPE_PROBE_EVENTS_MS` reads
the pane's availability first, in the state the app starts in -- `[events] live: button_visible=0
pane=0 recording=1`, **no pane and no button without trigger mode** -- then turns trigger mode on
through the real toggle (`[events] after trigger on: button_visible=1 pane=0`: the button appears,
the pane stays collapsed) and clicks the button, waits for a latch, then reads the cache and replays
the newest event through the pane's own row:
`[events] dir=… n=1 newest: dur=2.000000 pre=1.000000 post=1.000000 edge=… signals=4 samples=20001
rate=10000 seq=1 thumb_bytes=230454 recorded=1 pane=1`, then
`[events] activated row0: replaying=1 thumbnail=1 thumb_size=108x36 thumb_visible=1` (the size is
what says the picture is *on screen*: a paint-able alone is not), `[events] replayed: channels=4
samples=20001 replaying=1 waveform=1` and `[events] live again: replaying=0 waveform=0`. It ends on
the pane's other precondition, through the trigger panel's switch: `[events] recording off, trigger
off` and `[events] recording off, trigger on` must **both** read `button_visible=0 pane=0
recording=0` -- the pane goes with its switch, in either trigger state. **`samples` must equal the
export's row count for the same window** (`20001` for a 2 s event at 10 kS/s), `thumb_bytes` must be
a real BMP (`54 + 480*160*3`-ish, i.e. hundreds of kB, not 0), `thumbnail` must be 1 (the row loaded
the picture) and the last line must read `replaying=0` -- a replay with no way back is a one-way
door. It then **waits for a second latch** (bounded: twelve retries) and drives the two group layouts
through the buttons: `[events] compare buttons: cached=2 selected=2 compare=1 accumulate=1` (the
buttons read the selection), `[events] halve`… `[events] halves a_first=20001 a_second=0 b_first=0
b_second=20001` -- **the two halves of the ring, and the reading that says a comparison's gap is a
gap: the first event's channel is finite only in the first half and the second's only in the second**
-- `[events] compare: kind=1 rows=4 channels=8 samples0=40002 traces0=2 traces1=2 window=4.000
recording=1 pane=1` (`samples0` must be exactly the stamps pushed, **not more**: a live producer left
running pushes its own four signals into the group's eight channels), `[events] compare plots:
area_b=1 rows_a=1 rows_b=1 x_a=[-2.000,0.000] x_b=[-2.000,0.000] same_x=1` (the second plot exists, is
on screen, mirrors the row count, and both are on one x window), then the sync -- the probe sets a y
range nobody would arrive at by accident on the *first* plot and reads the second a few frames later:
`[events] compare sync: y_a=-3.500 y_b=-3.500 mirrored=1` -- and `[events] accumulate: kind=2 rows=1
channels=8 samples0=33045 selected=2 traces0=8 window=2.000` (`traces0=8`: the four signals the layout
holds plus one layer each for the second event, **in the same row**, which is what an accumulation
means). Run it with a throwaway
`XDG_CACHE_HOME` and `UDPSCOPE_EXIT_AFTER_MS` long enough for the second latch (20 s).
- **A waveform round-trips.** Export from a live run (`UDPSCOPE_EXPORT_MS` with
`UDPSCOPE_EXPORT_PATH`, against the mock at 10 kS/s), then open that file with `--open` and an
exit dump. The load must print
`[udpscope] waveform loaded: … signals=4 samples=20001 span=2.000000s rate=10000 time=[-2.000000..0.000000]`
— **the same row count, column count, span and time range the export probe reported**
(`rows=20001 cols=4 time=[-2.000000..0.000000]`) — and the dump must show `channels=4`,
`frames=0 packets=0` (no acquisition) and, per channel,
`20001 samples vs 20001 time slots … y=[-1.0000, 1.0000]`. A file that loads with fewer samples
than it was written with, or with `NaN` slots, is the reader mis-slicing its records.
- **A loaded file can be plotted.** `UDPSCOPE_PROBE_DROP_MS` drives the drop handler the way a drag
from the signal panel does, on a file opened with `--open`: it must read
`[drop-probe] signals=4 channels=4 rows=4 slots0=[sine|||]` — **`rows` equal to the column count**,
one trace per row — and each drop must return `1` and add its trace (`dropped signal 1 -> 1
slots=[sine|sine|square|]`). `rows=1 slots0=[sine|square|ramp|noise]` with every drop returning `0`
is the defect this exists for: rows sized four signals to one row fill the only row, so the app
reads as one that cannot plot anything. A file-mode run must also write **no session** for the
default `127.0.0.1:8000` (`App::source_port` is 0), or it overwrites that source's saved layout.
- **The chooser's Waveforms page**: `UDPSCOPE_PROBE_WAVEFORMS=1` prints
`[sources] waveforms: N recent <path>(ok|missing) … page=1`, and `UDPSCOPE_PROBE_WAVE=<path>` sets
the field the file dialog's callback sets and prints `[sources] picked waveform <path>` — then the
load line above, with no socket involved.
- **A `--server` launch reads the stored configuration.** With a throwaway `XDG_CONFIG_HOME`
holding `127.0.0.1_8101/bench.toml` (`buffer_mb = 64`, `relative_time = true`,
`capture_packets = 500`, `active` = `bench`), `./build/udpscope --server 127.0.0.1 --port 8101`
must print `[sources] settings from configuration 'bench': buffer=64 relative=1 capture=500`, the
`relative time:` line, and a `span=` of about 209 s (64 MB at 10 kS/s) — **not** the 200 MB
default's ~655 s. The same command with `--buffer-mb 8` must read `buffer=8` and `span=26.214s`:
**an explicit flag wins over the file**. Without this, the settings a host was edited to have were
ignored whenever the address came from the command line.
- The chooser's **form**, with `UDPSCOPE_PROBE_PICK_MS` scheduling it:
`UDPSCOPE_PROBE_NEWHOST=host:port` fills it and prints
`[sources] form: name='probe-host' note='…' buffer=32 relative=1 connect_sensitive=1`, then
`[sources] configuration created: host:port/probe-host … buffer=32 relative=1` and a
`picked … config=probe-host`; a following `UDPSCOPE_PROBE_SOURCES=1` must list it
(`buffer=32 relative=1 note='…'`) — a configuration the form wrote that the chooser does not
list cannot be edited next time. `UDPSCOPE_PROBE_EDITHOST=host:port` opens the same form on that
configuration and prints `[sources] edit form: name='…' note='…' buffer=… relative=… button='Save'`
— **the pre-fill is the assertion**: an edit form that opens empty cannot edit anything — then
saves and reports the new values. `UDPSCOPE_PROBE_EDITNAME=new-name` renames it as well: one
`known:` row afterwards, with the old file gone.
- `UDPSCOPE_PROBE_RESEQ_MS`'s neighbour for **reconnect**: the mock gains an EMI case,
`--blackout-at S --blackout D` — silent from second S for D seconds (D must exceed the
client's 2 s silence timeout), resuming from *now* so the link's backlog is what the link
ate. One run (`--blackout-at 4 --blackout 3.5`, `UDPSCOPE_EXIT_AFTER_MS=15000`) has to read:
every reconnect's `CONFIG:` line followed by `CONFIG unchanged: keeping the buffer and its
history` (a reconnect that re-plans wipes history, which is the defect this case exists for),
no `buffer rebuilt` line after the first CONFIG, `frames` climbing again after the blackout
with `gaps=0`, and a trigger armed *after* recovery (`UDPSCOPE_PROBE_KEYS_MS` past the
blackout) latching — the dump's `CAPTURING`/`EVENT HELD` and `publish cost: N events` with
N ≥ 1 are that proof.
already covers 838 s, so no window the control can reach moves `time_stride`,
`set_target_span()` returns without rebuilding, and the window probe reports `decim=1`
throughout — it re-plans nothing. At 100 kS/s the ring covers 83.9 s and the probe's
@@ -0,0 +1,328 @@
<!-- SPDX-License-Identifier: MIT -->
<!-- SPDX-FileCopyrightText: 2026 Martino Ferrari -->
# UDP packet capture: design
`todo.md` item 4: *optional udp frame storage for debugging of strange behaviours (store last
`<N>`, default = 1000, udp packets binary content on disk for forensic analysis).*
The strange behaviours worth forensics on are all *wire* behaviours — a counter that skips, a
datagram that arrives twice, a fragment that never completes, a CONFIG whose header does not parse,
a burst that outruns the reorder ring. Every one of them is invisible by the time a
`udps_frame_t` reaches `Acquisition::on_data`: the client has already validated the magic,
reassembled the fragments, resequenced what it could and dropped what it could not. A capture that
is to answer *what did the wire actually send* must therefore sit at `recv()`, and must store the
bytes without interpreting them.
This is a debugging aid, not a feature of the instrument. It is off by default, it adds nothing to
the glance tier of the status bar, and it must not be able to cost a single packet when it is on.
## The decisions taken, and what each one rules out
| Decision | Alternative rejected |
| --- | --- |
| Always-on rolling ring while enabled; the last N are on disk at all times | Keeping the ring in RAM and flushing on an anomaly or on demand — a crash or a `SIGKILL` then leaves nothing, which is the case a forensic capture exists for |
| A fixed-size file of N slots, plus a converter to pcap in `tools/` | Writing pcap directly (grows, so "last N" needs two rotating files and the newest N is split across both) or one file per datagram (1000 files, an open/write/close per packet in the receive path) |
| `mmap` once, `memcpy` per datagram | A `pwrite` per datagram (a syscall, and on a busy disk a blocking one, between two `recv()` calls) or a third thread with a queue (a second copy of every datagram, and this application's two-thread property is load-bearing) |
| One path per source, previous capture kept one deep | A timestamped file per enable (needs the retention policy `todo.md` item 9 defines and this does not build) or a single path overwritten (re-enabling destroys the capture you enabled for) |
| `--capture-packets[=N]` as well as the popover button | Button only — by the time the window is up and clickable, the first connect and its CONFIG are already history, and those are exactly the "strange behaviours" hardest to reproduce |
## Architecture
Three parts, each with one responsibility and a seam that does not leak the other two.
### 1. The client hands over raw datagrams
`deps/udpsclient` gains **one additive entry point**:
```c
/** Every datagram as recv() returned it, before the magic check, reassembly or
* resequencing: the bytes a capture has to store, and the only place malformed
* or duplicate datagrams are still visible. Runs on the polling thread, inside
* udps_client_poll(), like every other callback. `bytes` is valid until the
* callback returns. */
typedef void (*udps_datagram_cb)(const uint8_t *bytes, size_t len,
double recv_time, void *user);
void udps_client_set_datagram_cb(udps_client_t *client,
udps_datagram_cb cb, void *user);
```
A separate setter, **not** a fourth parameter on `udps_client_set_callbacks`
(`udps_client.h:283`): that signature has call sites, and a capture hook is no reason to change
them.
The call site is `udps_client.c:1458`, immediately after
```c
ssize_t n = recv(c->udp_fd, c->rxbuf, UDPS_RX_BUF_BYTES, 0);
```
returns a positive `n`, and before anything is read out of `rxbuf`. `recv_time` is the same clock
the client already stamps arrivals with, so a capture's times and the stream statistics agree.
**Boundary, stated rather than discovered later:** in multicast mode CONFIG arrives over TCP
(`udps_client.c:1142`) and this hook does not see it. The capture is of the UDP socket. A malformed
*multicast* CONFIG is therefore out of reach, and `README.md` will say so.
### 2. `PacketCapture` owns the file
New unit, `src/capture.hpp` / `src/capture.cpp`. One class, one responsibility — own the ring file
and store a datagram into it. It includes no GTK, no `ScopeModel`, and no client types beyond
`<cstdint>`; its whole interface is:
```cpp
class PacketCapture {
public:
/// Renames an existing capture to `capture-prev.bin`, creates and reserves the new file,
/// maps it. Returns false and leaves the object disabled on any failure, with `error()` set.
bool open(const std::string& path, std::uint32_t slot_count);
void close(); ///< unmap, close; idempotent
bool enabled() const noexcept;
/// One datagram. Called only from the producer thread. Never allocates, never blocks,
/// never fails: an over-long datagram is truncated to the slot and counted.
void store(const std::uint8_t* bytes, std::size_t len, double recv_time) noexcept;
std::uint64_t stored() const noexcept; ///< datagrams since open()
std::uint64_t truncated() const noexcept;
std::uint32_t slot_count() const noexcept;
const std::string& path() const noexcept;
/// Copies the last open() failure out, or writes an empty string. Not a
/// `const std::string&`: see "What crosses between the threads" below.
void error(char* out, std::size_t cap) const noexcept;
};
```
#### What crosses between the threads
The producer thread writes this state and the GTK thread reads it every frame for the popover, so
each field has to be publishable without a lock:
| Field | Type | Why it is safe |
| --- | --- | --- |
| `enabled_`, `stored_`, `truncated_` | `std::atomic<…>`, relaxed | counters and a flag, not a consistent set — the same treatment `StreamStats` already gets |
| `path_` | `std::string` | written **once** by the GTK thread before it ever sets `capture_want_`, and only read afterwards |
| the open failure | `char error_[128]` plus `std::atomic<bool> error_set_` | the producer fills the buffer and *then* sets the flag with a release store; the GTK thread copies the buffer only when the flag reads true |
The error is deliberately **not** a `std::string` accessor. A `std::string` written by the producer
inside `open()` and read by the GTK thread on the next frame is a data race with a reallocating
buffer — the kind that reads correctly for months. The fixed buffer with a flag published after it
is the same release-store trick the slot's `seq` field uses, for the same reason.
#### The file
```
offset 0, 64 bytes — header
magic u8[8] "UDPSCAP\0"
version u32 1
slot_count u32 N
slot_bytes u32 sizeof(record header) + UDPS_RX_BUF_BYTES, 8-byte aligned
cursor u32 next slot index to write (a hint; see below)
t0_realtime f64 CLOCK_REALTIME at open(), the epoch recv_time is relative to
peer_addr u32 the source's IPv4, network order
peer_port u16
local_port u16
reserved to 64 bytes, zeroed
offset 64 + i * slot_bytes, for i in [0, N) — slot i
t_arrival f64 the client's recv_time
len u32 bytes stored (after truncation, if any)
orig_len u32 bytes the wire delivered, so a truncation is visible
bytes u8[len]
seq u64 1-based ordinal since open(); written LAST
```
`seq` is written last, after the bytes, with a release store. Three things follow, and they are the
reason the format is this and not simpler:
- **A slot carrying a non-zero `seq` is complete.** No `fsync` is needed for the file to be
readable after a crash: the kernel's dirty pages are the file, and a half-copied slot still has
`seq == 0`.
- **`seq`, not `cursor`, is the order.** After the ring wraps, the newest slot is the one with the
largest `seq`, and the reader needs no trust in a header field that is updated out of band.
- **A torn write is detectable, not silent.** The reader reports the count of empty and torn slots
rather than averaging over them.
`cursor` is maintained anyway, because a human running `xxd` on the file wants to know where to
look.
#### Why the file is reserved, not sparse
`posix_fallocate` reserves the whole file at `open()`. The obvious alternative — `ftruncate` and
let the file stay sparse, so it costs only the bytes actually written — is wrong with `mmap`: a
store into a page the filesystem cannot back raises **`SIGBUS`**, which kills udpscope outright.
A capture must not be able to take down the instrument it is observing, and an out-of-space
condition must be an error at the moment the user clicks, not a crash ten minutes later.
At the default `N = 1000` and `UDPS_RX_BUF_BYTES = 65552`, the file is **~62.6 MiB**. That is
stated in the help text, in the tooltip and in the popover row, because it is the one cost of
switching the feature on.
### 3. `Acquisition` drives it, and the GTK thread only ever sets an atomic
`Acquisition` owns the `PacketCapture`, installs the datagram callback next to the three it already
installs (`src/acquisition.cpp:62`), and does **every** file operation on the producer thread.
```
GTK thread (the button) producer thread (the datagram callback)
--------------------------- --------------------------------------------
capture_want_.store(true) ──► reads capture_want_ at the top of each datagram
capture_path_ written once rising edge → open() : rename, create, fallocate, mmap
before the first store falling edge → close(): munmap, close
then, if enabled, store(bytes, len, recv_time)
```
No mutex, no queue, no second copy of any datagram, and the application still has exactly two
threads.
The one-off cost of a toggle is the `open()`: a rename, a create, a `posix_fallocate` of 62.6 MiB
and an `mmap`, on the producer thread. It is a user-initiated act that happens once per toggle, and
`README.md` will say that it can cost the packets in flight during it. Moving it off the producer
thread would need the lock or the thread this design exists to avoid, for an event that happens
when a human clicks a button.
The duration is **measured, not asserted**: `open()` records its own wall time and the probe prints
it as `open_ms`. `posix_fallocate` on a local extent-based filesystem is a metadata operation and
should be a handful of milliseconds, but that is a claim about the filesystem, not about this code,
and a reading above ~50 ms is a finding worth putting in `README.md` rather than a number to
predict here.
### Error handling, and only at this boundary
This is a syscall boundary, so it is validated; nothing else here is.
| Condition | Behaviour |
| --- | --- |
| `open`, `posix_fallocate` or `mmap` fails (ENOSPC, EACCES, a read-only cache directory) | capture stays off, `error()` carries `strerror`, one line on stderr, the toggle button returns to off and the popover row shows the reason |
| the cache directory does not exist | created with `g_mkdir_with_parents`, 0700; a failure there is the row above |
| `len > UDPS_RX_BUF_BYTES` | impossible — the slot is sized to the client's own receive buffer — but `store()` truncates to the slot, records `orig_len`, and counts it in `truncated()` rather than trusting the impossibility |
| the previous `capture-prev.bin` cannot be replaced | the capture still opens; the rename failure is reported, the old previous is kept |
| capture enabled with no connection yet | the button is insensitive until connected, because the path is keyed on the source; `--capture-packets` defers `open()` to the first datagram, which is what arms it before the first CONFIG |
## Surfaces
### The popover button and row
The status bar's detail popover (`src/ui.cpp:7619-7672`) is a two-column grid: four bold headings
at rows 0, 3, 5, 6 and seven value labels at rows 0..6, in `app->popover_label[11]`. It gains a
fifth heading, **Capture**, and an eighth value label; the array becomes `[13]` and the
`kHeadings` / `kHeadingRow` tables gain their entry. `update_status_popover`
(`src/ui.cpp:6101`) writes the new label like the others — `gtk_label_set_text` only, no rebuild.
The row reads one of:
```
off
on · 412/1000 slots · 26.1 MiB · ~/.cache/udpscope/192.168.1.40-5000/capture.bin
failed: No space left on device
```
Below the grid, a `GtkToggleButton` labelled **Capture packets** with the
`media-record-symbolic` icon and a tooltip naming the full path and the file's size. `toggled`
sets the atomic and nothing else. Insensitive until connected.
Nothing is added to `format_detail()`'s note string: `src/status_text.cpp:86` is a `char
note[160]` already close to its worst case, and the capture state is not something the exit dump's
one line needs.
### The command line
```
--capture-packets N store the last N raw datagrams on disk for forensics, in a
ring under the cache directory (1000 slots is ~62.6 MiB);
off unless given, and toggleable from the status bar
```
Parsed in `src/config.cpp`'s existing style — both `--capture-packets N` and
`--capture-packets=N`, `N` in 1..100000 — and the value is **required**, not optional.
`--capture-packets` with an optional value cannot be told apart from `--capture-packets` followed
by a value in this parser, and every other option in it takes the space-separated form, so a bare
flag would turn `--capture-packets 500` into `unknown argument: 500`. The 1000 default is the
slot count the **popover button** uses when the flag was not given; with the flag, the button uses
the N the flag named.
`Config::capture_packets` is a `std::uint32_t`: 0 means the flag was absent, and the button then
uses 1000.
The flag arms the ring before the first datagram; it is not persisted in the session file. That
persistence belongs with `todo.md` item 3's work, alongside the relative-time toggle deferred to
the same place.
### `tools/capture_to_pcap.py`
```
$ tools/capture_to_pcap.py ~/.cache/udpscope/192.168.1.40-5000/capture.bin out.pcap
1000 slots, 412 stored, 0 torn, 0 truncated → 412 packets, 26.1 MiB
$ wireshark out.pcap
```
Reads the header, validates the magic and version, walks the slots, orders by `seq`, skips
`seq == 0`, and emits a classic pcap (`LINKTYPE_ETHERNET`, microsecond timestamps from
`t0_realtime + t_arrival`) with a synthesised Ethernet/IPv4/UDP header per record built from the
header's `peer_addr`, `peer_port` and `local_port`. Pure standard library — the mock already
forces a hard `numpy` dependency on `tools/`, and this script has no reason to add to that.
It also prints the counts above to stderr, which is what makes it a diagnostic in its own right:
`torn` or `truncated` above zero is a finding about the capture, before anything about the stream.
## Verification
udpscope has no unit test suite; the capture is verified by the application's own headless
self-check, and the converter by running it.
`UDPSCOPE_PROBE_CAPTURE_MS` toggles capture **through the same handler the button uses**, waits a
scheduled interval, then reads the file back off disk and prints one line:
```
[capture-probe] enabled=1 path=… slots=1000 stored=412 cursor=412 torn=0 truncated=0
header_ok=1 first_seq=1 last_seq=412 payload=1648000 file=65576064
open_ms=3.1 t0=1760000000.123
```
`payload` is the sum of every stored slot's `len` — the bytes the wire actually delivered, which is
what tells you whether the ring is holding what you think. `file` is the reserved size,
`64 + slot_count * slot_bytes`, and is a constant for a given N: it is in the line so that a
mismatch against the file on disk is a reading rather than something to go and check.
What each field has to read, and the defect a wrong reading names, goes in
`docs/claude/probe-readings.md` beside the other probes. Two cases in
`docs/claude/verification.md`:
1. **enable at runtime** — the probe toggles on mid-run; `stored` must equal the datagrams the
exit dump counts from that instant, `torn=0`, and `first_seq=1`.
2. **wrap** — `--capture-packets=16` against the standard mock, run long enough to wrap several
times; `stored` exceeds `slot_count`, `last_seq - first_seq == 15`, `torn=0`. A wrap that loses
ordering, or a `cursor` that disagrees with `seq`, shows here and nowhere else.
Then the converter, run over the wrap case's file: exit 0, a record count equal to `slot_count`,
and the pcap's own first and last record timestamps inside the run's wall-clock interval. The
check is the converter's printed counts plus a `struct.unpack` of the pcap's first and last record
headers — no Wireshark, no `capinfos`, nothing outside the standard library, so it runs wherever
the self-check runs.
The standing baselines apply unchanged: a clean build at exactly four warnings, an incremental at
two, and zero `Gtk-CRITICAL` in any headless run.
## Out of scope
- **Replaying a capture back into udpscope.** That is `todo.md` item 5's territory (opening stored
waveforms) and a different feature with a different shape.
- **Retention beyond one deep.** `capture.bin` and `capture-prev.bin`, nothing more, until item 9
defines the cache-size preferences.
- **Session persistence of the enable state.** Deferred to item 3 with the relative-time toggle,
so one piece of work adds the session fields rather than two.
- **Capturing the multicast TCP CONFIG channel.** Stated as a limitation in `README.md`.
- **Compression, encryption or rotation of the ring.** A fixed file of raw bytes is what a
forensic reader wants, and `gzip` exists.
## Documentation to update
- `README.md` — the feature, the 62.6 MiB cost, the producer stall on toggle, the multicast CONFIG
limitation, the converter's usage.
- `SPECS.md` — nothing: this is a `todo.md` item, not a spec requirement, and not a deviation.
- `todo.md` — item 4 struck, on one line, with the flag's name (GFM strikethrough is inline and
cannot span list items).
- `docs/claude/build-and-run.md` — the new option; `verification.md` and `probe-readings.md` — the
probe and its two cases; `docs/claude/invariants-data-path.md` — one paragraph on why the hook is
at `recv()` and why the file is reserved rather than sparse.
+98 -2
View File
@@ -5,6 +5,13 @@
#include <udps_client.h>
#include "layout_store.hpp"
#include <glib/gstdio.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <algorithm>
#include <cmath>
#include <cstring>
@@ -61,6 +68,13 @@ void Acquisition::start() {
}
udps_client_set_callbacks(client_, &Acquisition::config_trampoline, &Acquisition::data_trampoline,
&Acquisition::event_trampoline, this);
udps_client_set_datagram_cb(client_, &Acquisition::datagram_trampoline, this);
// `--capture-packets`: armed before the first datagram, so the first CONNECT and
// its CONFIG -- the hardest strange behaviours to reproduce -- are in the ring.
// The open itself still happens on the producer thread, at the first datagram.
if (cfg_.capture_packets > 0) {
set_capture_want(true, cfg_.capture_packets);
}
started_ = std::chrono::steady_clock::now();
running_.store(true, std::memory_order_release);
@@ -100,6 +114,60 @@ void Acquisition::run() {
}
}
void Acquisition::set_capture_want(bool want, std::uint32_t slots) {
if (want) {
if (slots > 0) capture_slots_ = slots;
if (capture_path_.empty()) {
// Keyed on the source: one path per source, the previous capture kept one deep.
std::string host = cfg_.server;
for (char& ch : host) {
if (ch == ':' || ch == '/' || ch == ' ') ch = '_';
}
std::string dir = cache_dir() + "/" + host + "-" + std::to_string(cfg_.port);
g_mkdir_with_parents(dir.c_str(), 0700);
capture_path_ = dir + "/capture.bin";
std::uint32_t addr = 0;
struct ::in_addr a;
if (::inet_pton(AF_INET, cfg_.server.c_str(), &a) == 1) {
addr = a.s_addr; // already network order, which is what the header wants
}
capture_.set_peer(addr, cfg_.port);
}
}
capture_want_.store(want, std::memory_order_release);
}
void Acquisition::on_datagram(const std::uint8_t* bytes, std::size_t len, double recv_time) {
const bool want = capture_want_.load(std::memory_order_acquire);
if (want && !capture_.enabled()) {
// A user-initiated act that happens once per toggle, on this thread: a rename, a
// create, a 62.6 MiB reserve and an mmap. It can cost the packets in flight, and
// the measured cost is `capture_.open_ms()`.
if (!capture_.open(capture_path_, capture_slots_)) {
char err[128];
capture_.error(err, sizeof(err));
std::fprintf(stderr, "[udpscope] packet capture failed to open: %s\n", err);
// Stay off rather than retrying every datagram against a full disk; the GTK
// thread reads the flag back and returns the button to off.
capture_want_.store(false, std::memory_order_release);
return;
}
std::fprintf(stderr, "[udpscope] packet capture on: %s (%u slots, %.1f ms to open)\n",
capture_.path().c_str(), capture_.slot_count(), capture_.open_ms());
} else if (!want && capture_.enabled()) {
capture_.close();
std::fprintf(stderr, "[udpscope] packet capture off\n");
}
if (capture_.enabled()) {
capture_.store(bytes, len, recv_time);
}
}
void Acquisition::datagram_trampoline(const std::uint8_t* bytes, std::size_t len,
double recv_time, void* user) {
static_cast<Acquisition*>(user)->on_datagram(bytes, len, recv_time);
}
void Acquisition::on_config(const udps_signal_t* signals, std::uint32_t count,
std::uint8_t publish_mode) {
std::vector<SignalInfo> info;
@@ -129,6 +197,11 @@ void Acquisition::on_config(const udps_signal_t* signals, std::uint32_t count,
warned_outage_ = false;
warned_bridge_ = false;
warned_time_source_ = false;
// The clock choice is per connection too: a reconnect re-derives it from the frames
// the new session actually delivers, rather than trusting a decision made about a
// session that dropped.
time_source_ = 0;
reset_session_state();
model_.stats().have_config.store(true, std::memory_order_relaxed);
model_.set_signals(std::move(info));
}
@@ -139,9 +212,31 @@ void Acquisition::on_event(int event, const char* detail) {
model_.stats().connected.store(true, std::memory_order_relaxed);
} else if (event == UDPS_EVENT_DISCONNECTED) {
model_.stats().connected.store(false, std::memory_order_relaxed);
// The session dropped mid-flight: everything the producer knows about *that*
// session's numbering and clocks is now a lie about the next one. Clearing it here
// is what makes the next frame re-anchor instead of reading as late, as a glitch or
// as an outage -- and nothing here touches the buffer, whose history survives the
// drop (see `ScopeModel::set_signals`'s unchanged-CONFIG path).
reset_session_state();
}
}
void Acquisition::reset_session_state() noexcept {
have_counter_ = false;
last_counter_ = 0;
have_stream_anchor_ = false;
last_stream_anchor_ = 0.0;
stamp_failures_ = 0;
// The arrival clock's *anchor* stays -- stamps are small seconds the ring's timeline
// is already scaled against, and re-anchoring mid-stream would move x -- but the
// smoothed interval and the previous arrival are per-connection measurements, and
// carrying them across a multi-second outage makes the first new frame read as a
// clock fault rather than as the resumption it is.
have_arrival_interval_ = false;
arrival_interval_ = 0.0;
arrival_prev_ = 0.0;
}
std::uint32_t Acquisition::build_stamps(const udps_frame_t* frame, int time_signal) {
// The frame's span in base ticks: a scalar signal carries `num_samples` values
// (one per batched cycle), a block signal (rows*cols > 1) carries its whole
@@ -537,8 +632,9 @@ void Acquisition::on_data(const udps_frame_t* frame) {
// only an absolute slot index while `tick_ / period == produced()`, so the tick counter has
// to restart with the buffer -- otherwise the amend lands outside the ring, repairs nothing,
// and the hole is still marked repaired, which is the band saying the opposite of the truth.
// The generation is the signal rather than the stride/channel-count comparison below: a
// reconnect with an identical CONFIG rebuilds the store leaving both unchanged.
// The generation is the signal rather than the stride/channel-count comparison below: only
// a CONFIG that *changes* the stream re-plans the store -- a reconnect's identical CONFIG
// keeps it, history and all (see `ScopeModel::set_signals`).
// `slot_base_` is deliberately *not* reset -- it is relative mode's clock origin, samples
// since the stream began, and relative time must not jump backwards when the user retypes
// the window.
+31
View File
@@ -3,6 +3,7 @@
#pragma once
#include "capture.hpp"
#include "config.hpp"
#include "model.hpp"
#include "trigger.hpp"
@@ -38,6 +39,19 @@ class Acquisition {
void set_paused(bool paused) noexcept { paused_.store(paused, std::memory_order_relaxed); }
bool paused() const noexcept { return paused_.load(std::memory_order_relaxed); }
/// Turns the packet capture on or off -- the whole GTK side of the feature. The
/// path is built here, once, *before* the flag is published, so the producer reads
/// a complete string whenever it reads a true flag. `slots` is the ring's size;
/// zero means the 1000-slot default. The producer does the open or the close at the
/// top of the next datagram, so a toggle costs no lock and no second thread.
void set_capture_want(bool want, std::uint32_t slots = 0);
bool capture_want() const noexcept {
return capture_want_.load(std::memory_order_acquire);
}
/// The capture, for the popover's row and the probe. Counters and errors are read
/// cross-thread through PacketCapture's own published fields.
const PacketCapture& capture() const noexcept { return capture_; }
private:
void run();
void on_config(const udps_signal_t* signals, std::uint32_t count, std::uint8_t publish_mode);
@@ -48,6 +62,17 @@ class Acquisition {
std::uint8_t publish_mode, void* user);
static void data_trampoline(const udps_frame_t* frame, void* user);
static void event_trampoline(udps_event_t event, const char* detail, void* user);
static void datagram_trampoline(const std::uint8_t* bytes, std::size_t len,
double recv_time, void* user);
/// Drops the producer state that is per *connection*: the counter mark, the time
/// anchors and the clock choice. A reconnect re-sends the CONFIG and the stream may
/// have restarted its numbering; carrying the old session's marks across makes the
/// first new frames read as late, as glitches or as an outage, which is recovery
/// failing exactly when it matters. Called on the producer thread, from on_event().
void reset_session_state() noexcept;
/// The producer side of the toggle: open on a rising edge, close on a falling one,
/// then store if enabled. Every file operation happens here, on this thread.
void on_datagram(const std::uint8_t* bytes, std::size_t len, double recv_time);
/// Fills `stamps_` with this frame's base-tick timestamps, in stream seconds,
/// and returns how many were written (`frame->num_samples` unless the frame is
@@ -73,6 +98,12 @@ class Acquisition {
std::thread thread_;
std::atomic<bool> running_{false};
std::atomic<bool> paused_{false};
PacketCapture capture_;
std::atomic<bool> capture_want_{false};
std::uint32_t capture_slots_ = 1000;
/// Written once, by the GTK thread, before `capture_want_` is ever published true;
/// the producer reads it only after the flag, so no lock is needed.
std::string capture_path_;
TriggerEngine engine_;
std::uint64_t tick_ = 0; ///< base ticks consumed since the epoch
+238
View File
@@ -0,0 +1,238 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 Martino Ferrari
#include "capture.hpp"
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include <cstdio>
#include <cstring>
#include <ctime>
#include <chrono>
#include <vector>
namespace udpscope {
namespace {
/// The file header is one 64-byte block: fixed fields first, zeroed padding after.
constexpr std::uint64_t kHeaderBytes = 64;
/// `seq` lives at the *end* of a slot, at a fixed offset, because the reader must
/// find it without trusting any other field.
constexpr std::uint64_t kSeqBytes = 8;
void put_u32(std::uint8_t* p, std::uint32_t v) { std::memcpy(p, &v, 4); }
void put_u16(std::uint8_t* p, std::uint16_t v) { std::memcpy(p, &v, 2); }
void put_f64(std::uint8_t* p, double v) { std::memcpy(p, &v, 8); }
std::uint32_t get_u32(const std::uint8_t* p) {
std::uint32_t v;
std::memcpy(&v, p, 4);
return v;
}
double get_f64(const std::uint8_t* p) {
double v;
std::memcpy(&v, p, 8);
return v;
}
double wall_seconds() {
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
return double(ts.tv_sec) + double(ts.tv_nsec) * 1e-9;
}
/// `seq` is published with a release store *after* the bytes are down, so a reader
/// that sees a non-zero `seq` sees the whole slot -- the reason no `fsync` is needed
/// for the file to be readable after a crash. Plain __atomic builtins, not
/// `std::atomic_ref`: the target is mmap'd memory the class does not own as an object.
void publish_seq(std::uint8_t* slot, std::uint64_t seq) {
__atomic_store_n(reinterpret_cast<std::uint64_t*>(slot + kSlotBytes - kSeqBytes), seq,
__ATOMIC_RELEASE);
}
std::uint64_t read_seq(const std::uint8_t* slot) {
return __atomic_load_n(reinterpret_cast<const std::uint64_t*>(slot + kSlotBytes - kSeqBytes),
__ATOMIC_ACQUIRE);
}
} // namespace
void PacketCapture::set_error(const char* what) noexcept {
std::snprintf(error_, sizeof(error_), "%s", what);
error_set_.store(true, std::memory_order_release);
}
void PacketCapture::error(char* out, std::size_t cap) const noexcept {
if (cap == 0) return;
out[0] = '\0';
if (!error_set_.load(std::memory_order_acquire)) return;
std::snprintf(out, cap, "%s", error_);
}
bool PacketCapture::open(const std::string& path, std::uint32_t slot_count) {
close();
path_ = path;
slot_count_ = slot_count;
seq_ = 0;
cursor_ = 0;
stored_.store(0, std::memory_order_relaxed);
truncated_.store(0, std::memory_order_relaxed);
error_set_.store(false, std::memory_order_relaxed);
const std::chrono::steady_clock::time_point t0 = std::chrono::steady_clock::now();
// Keep the previous capture one deep: re-enabling must not destroy the capture the
// user enabled *for*. A failure here is reported, and the old previous kept.
std::string prev = path;
const std::size_t slash = prev.rfind('/');
const std::size_t dot = prev.rfind('.');
if (dot != std::string::npos && (slash == std::string::npos || dot > slash)) {
prev.insert(dot, "-prev");
} else {
prev += "-prev";
}
::rename(path.c_str(), prev.c_str());
fd_ = ::open(path.c_str(), O_RDWR | O_CREAT | O_EXCL | O_CLOEXEC, 0600);
if (fd_ < 0) {
set_error(strerror(errno));
return false;
}
// Reserved, not sparse: a store into a page the filesystem cannot back raises
// SIGBUS and kills udpscope outright, and an out-of-space condition must be an
// error at the moment the user clicked, not a crash ten minutes later.
const std::uint64_t total = kHeaderBytes + std::uint64_t(slot_count) * kSlotBytes;
if (::posix_fallocate(fd_, 0, off_t(total)) != 0) {
set_error(strerror(errno));
::close(fd_);
fd_ = -1;
::unlink(path.c_str());
return false;
}
map_ = static_cast<std::uint8_t*>(
::mmap(nullptr, total, PROT_READ | PROT_WRITE, MAP_SHARED, fd_, 0));
if (map_ == MAP_FAILED) {
map_ = nullptr;
set_error(strerror(errno));
::close(fd_);
fd_ = -1;
::unlink(path.c_str());
return false;
}
map_bytes_ = total;
std::uint8_t* h = map_;
std::memset(h, 0, kHeaderBytes);
std::memcpy(h, "UDPSCAP\0", 8);
put_u32(h + 8, 1); // version
put_u32(h + 12, slot_count);
put_u32(h + 16, kSlotBytes);
put_u32(h + 20, 0); // cursor: a hint, `seq` is the order
put_f64(h + 24, wall_seconds()); // the epoch recv_time is relative to
put_u32(h + 32, peer_addr_);
put_u16(h + 36, peer_port_);
put_u16(h + 38, 0); // local_port: the client does not expose its own socket
const double ms =
std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - t0).count();
open_ms_.store(ms, std::memory_order_relaxed);
enabled_.store(true, std::memory_order_release);
return true;
}
void PacketCapture::close() noexcept {
if (map_ != nullptr) {
::munmap(map_, map_bytes_);
map_ = nullptr;
}
if (fd_ >= 0) {
::close(fd_);
fd_ = -1;
}
map_bytes_ = 0;
enabled_.store(false, std::memory_order_relaxed);
}
void PacketCapture::store(const std::uint8_t* bytes, std::size_t len, double recv_time) noexcept {
if (!enabled_.load(std::memory_order_relaxed) || map_ == nullptr) return;
std::uint8_t* slot = map_ + kHeaderBytes + std::uint64_t(cursor_) * kSlotBytes;
std::uint32_t orig = std::uint32_t(len);
std::size_t fit = len;
if (fit > kMaxDatagramBytes) {
fit = kMaxDatagramBytes;
truncated_.fetch_add(1, std::memory_order_relaxed);
}
put_f64(slot, recv_time);
put_u32(slot + 8, std::uint32_t(fit));
put_u32(slot + 12, orig);
std::memcpy(slot + kRecordHeaderBytes, bytes, fit);
++seq_;
publish_seq(slot, seq_); // last: a non-zero `seq` is what makes a slot complete
cursor_ = (cursor_ + 1) % slot_count_;
put_u32(map_ + 20, cursor_); // a hint for a human in xxd; `seq` is the order
stored_.fetch_add(1, std::memory_order_relaxed);
}
bool read_capture(const std::string& path, CaptureReadStats* out) {
if (out == nullptr) return false;
*out = CaptureReadStats{};
FILE* f = std::fopen(path.c_str(), "rb");
if (f == nullptr) return false;
std::fseek(f, 0, SEEK_END);
const long size = std::ftell(f);
std::fseek(f, 0, SEEK_SET);
out->file_bytes = std::uint64_t(size < 0 ? 0 : size);
std::uint8_t h[kHeaderBytes];
if (std::fread(h, 1, sizeof(h), f) != sizeof(h)) {
std::fclose(f);
return false;
}
out->header_ok = std::memcmp(h, "UDPSCAP\0", 8) == 0 && get_u32(h + 8) == 1;
out->slot_count = get_u32(h + 12);
out->slot_bytes = get_u32(h + 16);
out->cursor = get_u32(h + 20);
out->t0_realtime = get_f64(h + 24);
if (!out->header_ok || out->slot_count == 0 || out->slot_bytes < kRecordHeaderBytes + 8) {
std::fclose(f);
return false;
}
const std::uint32_t payload = out->slot_bytes - kRecordHeaderBytes - kSeqBytes;
std::vector<std::uint8_t> slot(out->slot_bytes);
for (std::uint32_t i = 0; i < out->slot_count; ++i) {
if (std::fseek(f, long(kHeaderBytes + std::uint64_t(i) * out->slot_bytes), SEEK_SET) != 0) {
break;
}
if (std::fread(slot.data(), 1, slot.size(), f) != slot.size()) break;
const std::uint64_t seq = read_seq(slot.data());
const std::uint32_t len = get_u32(slot.data() + 8);
const std::uint32_t orig = get_u32(slot.data() + 12);
if (seq == 0 && len == 0 && orig == 0) {
++out->empty;
continue;
}
if (seq == 0) {
++out->torn; // bytes down, ordinal never published: a capture that died mid-copy
continue;
}
++out->stored;
out->payload += len;
if (orig > payload) ++out->truncated;
if (out->first_seq == 0 || seq < out->first_seq) out->first_seq = seq;
if (seq > out->last_seq) out->last_seq = seq;
}
std::fclose(f);
return true;
}
} // namespace udpscope
+124
View File
@@ -0,0 +1,124 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 Martino Ferrari
#pragma once
#include <atomic>
#include <cstdint>
#include <string>
namespace udpscope {
/// The largest datagram the client's socket can deliver, and therefore the payload
/// capacity of one capture slot. Kept equal to the client's own receive buffer: an
/// over-long datagram is impossible by construction, and `store()` counts the
/// impossibility rather than trusting it.
inline constexpr std::uint32_t kMaxDatagramBytes = 65535 + 17;
/// One slot's stride: the fixed record header, the largest datagram, and the `seq`
/// word that is written last. 8-byte aligned.
inline constexpr std::uint32_t kRecordHeaderBytes = 16; ///< t_arrival f64, len u32, orig_len u32
inline constexpr std::uint32_t kSlotBytes =
kRecordHeaderBytes + kMaxDatagramBytes + 8; ///< the trailing 8 are `seq`
/// Owns the ring file a packet capture stores raw datagrams into, and stores a
/// datagram into it. It includes no GTK, no `ScopeModel`, and no client types: its
/// whole world is bytes, a clock reading and one file.
///
/// Everything here runs on the producer thread except `open()`'s caller choice and
/// the GTK thread's reads of the counters and the error, which are publishable
/// without a lock: see the field table in the design note
/// (`docs/superpowers/specs/2026-10-09-udp-packet-capture-design.md`).
class PacketCapture {
public:
PacketCapture() = default;
~PacketCapture() { close(); }
PacketCapture(const PacketCapture&) = delete;
PacketCapture& operator=(const PacketCapture&) = delete;
/// Renames an existing capture to `<path>` minus extension plus `-prev.bin`,
/// creates and reserves the new file, maps it, writes the header. Returns false
/// and leaves the object disabled on any failure, with `error()` set.
bool open(const std::string& path, std::uint32_t slot_count);
/// Unmaps and closes. Idempotent; keeps the counters, which read "since open()".
void close() noexcept;
bool enabled() const noexcept { return enabled_.load(std::memory_order_relaxed); }
/// One datagram. Called only from the producer thread. Never allocates, never
/// blocks, never fails: an over-long datagram is truncated to the slot and counted.
void store(const std::uint8_t* bytes, std::size_t len, double recv_time) noexcept;
std::uint64_t stored() const noexcept { return stored_.load(std::memory_order_relaxed); }
std::uint64_t truncated() const noexcept {
return truncated_.load(std::memory_order_relaxed);
}
std::uint32_t slot_count() const noexcept { return slot_count_; }
const std::string& path() const noexcept { return path_; }
/// The source's IPv4, in network order, and its port: written into the header at
/// open() so the pcap converter can synthesise a UDP header. Call before open(),
/// from the thread that owns the decision -- the same write-once discipline as the
/// path. `local_port` stays zero: the client does not expose its own socket.
void set_peer(std::uint32_t addr_net_order, std::uint16_t port) noexcept {
peer_addr_ = addr_net_order;
peer_port_ = port;
}
/// Wall milliseconds `open()` took, measured rather than asserted: the one cost a
/// toggle has, and a reading above ~50 ms is a finding about the filesystem.
double open_ms() const noexcept { return open_ms_.load(std::memory_order_relaxed); }
/// Copies the last open() failure out, or writes an empty string. Deliberately not
/// a `const std::string&`: the producer writes the fixed buffer and *then* publishes
/// the flag with a release store, and the GTK thread copies only when the flag reads
/// true -- a `std::string` shared like that is a data race with a reallocating
/// buffer, the kind that reads correctly for months.
void error(char* out, std::size_t cap) const noexcept;
private:
void set_error(const char* what) noexcept;
std::string path_;
std::uint32_t slot_count_ = 0;
int fd_ = -1;
std::uint8_t* map_ = nullptr; ///< whole file, header first
std::uint64_t map_bytes_ = 0;
std::uint64_t seq_ = 0; ///< 1-based ordinal since open(); producer-only
std::uint32_t cursor_ = 0; ///< next slot index to write; producer-only
std::uint32_t peer_addr_ = 0; ///< the source's IPv4, network order
std::uint16_t peer_port_ = 0;
std::atomic<bool> enabled_{false};
std::atomic<std::uint64_t> stored_{0};
std::atomic<std::uint64_t> truncated_{0};
std::atomic<bool> error_set_{false};
std::atomic<double> open_ms_{0.0};
mutable char error_[128] = {0};
};
/// What a capture file says when read back off disk. `stored` counts slots with a
/// non-zero `seq`; `torn` counts slots with bytes but no `seq` (a capture that died
/// mid-copy); `empty` counts all-zero slots, which a freshly reserved file is full of.
struct CaptureReadStats {
bool header_ok = false;
std::uint32_t slot_count = 0;
std::uint32_t slot_bytes = 0;
std::uint32_t cursor = 0;
double t0_realtime = 0.0;
std::uint64_t stored = 0;
std::uint64_t torn = 0;
std::uint64_t empty = 0;
std::uint64_t truncated = 0; ///< slots whose `orig_len` exceeds the payload capacity
std::uint64_t payload = 0; ///< sum of stored slots' `len`: the bytes the wire delivered
std::uint64_t first_seq = 0;
std::uint64_t last_seq = 0;
std::uint64_t file_bytes = 0;
};
/// Reads a capture file back and validates it. Pure POSIX plus `<cstdio>`; used by the
/// headless probe, and `tools/capture_to_pcap.py` re-implements the walk in Python.
bool read_capture(const std::string& path, CaptureReadStats* out);
} // namespace udpscope
+12
View File
@@ -54,9 +54,14 @@ void print_usage(std::FILE* out, const char* argv0) {
" --recv-buffer BYTES SO_RCVBUF for the receiver socket (default 8388608)\n"
" --plots N stacked plots to start with, 1..8 (default 1); rows are\n"
" added and removed from the layout editor\n"
" --capture-packets N store the last N raw datagrams on disk for forensics, in a\n"
" ring under the cache directory (1000 slots is ~62.6 MiB);\n"
" off unless given, and toggleable from the status bar\n"
" --relative-time take the plot's time from the packet counter and the\n"
" declared rate, not the stream's time signal; absolute\n"
" time is then kept for export only\n"
" --open FILE open an exported waveform (.npy) instead of a live source:\n"
" signals from its columns, no socket, no acquisition\n"
" -h, --help this text\n",
argv0);
}
@@ -82,6 +87,7 @@ Config parse_args(int argc, char** argv) {
c.interface_addr = value_of(argc, argv, i, "--interface");
} else if (a == "--buffer-mb" || a.rfind("--buffer-mb=", 0) == 0) {
c.buffer_mb = parse_double("--buffer-mb", value_of(argc, argv, i, "--buffer-mb"));
c.buffer_mb_specified = true;
if (c.buffer_mb < 1.0) fail("--buffer-mb must be at least 1");
} else if (a == "--max-window" || a.rfind("--max-window=", 0) == 0) {
c.max_window_s = parse_double("--max-window", value_of(argc, argv, i, "--max-window"));
@@ -101,6 +107,12 @@ Config parse_args(int argc, char** argv) {
fail("--plots must be 1.." + std::to_string(RowSet::kMaxRows) + ": " + v);
}
c.plots = static_cast<int>(n);
} else if (a == "--capture-packets" || a.rfind("--capture-packets=", 0) == 0) {
const long n = std::strtol(value_of(argc, argv, i, "--capture-packets").c_str(), nullptr, 10);
if (n < 1 || n > 100000) fail("--capture-packets must be 1..100000");
c.capture_packets = static_cast<std::uint32_t>(n);
} else if (a == "--open" || a.rfind("--open=", 0) == 0) {
c.open_waveform = value_of(argc, argv, i, "--open");
} else if (a == "--relative-time") {
c.relative_time = true;
} else {
+14
View File
@@ -24,6 +24,10 @@ struct Config {
/// Size of *each* of the two sample buffers, in megabytes. The application
/// always allocates two identical buffers (spec item 1).
double buffer_mb = 200.0;
/// Whether `--buffer-mb` was given. A stored configuration's buffer size fills in only when the
/// flag was *absent*: an explicit flag beats a saved setting, and "not asked" is not the same
/// question as "asked for 200".
bool buffer_mb_specified = false;
/// Largest time window the toolbar offers, in seconds. The buffer partition is
/// sized to hold at least this much time (spec: decimation).
@@ -40,6 +44,16 @@ struct Config {
/// from the stream's own time signal, so arrival and producer-clock jitter cannot move a
/// sample's x. Absolute stream time is still recorded, for export only (`todo.md` item 2).
bool relative_time = false;
/// `--capture-packets N`: the ring size of the raw-datagram capture, 1..100000.
/// 0 means the flag was absent: the capture is off, and the popover button then
/// uses the 1000-slot default when the user toggles it.
std::uint32_t capture_packets = 0;
/// `--open FILE`: a `.npy` an earlier run exported, opened for viewing and analysis instead of a
/// live source. File mode and source mode are exclusive -- there is no acquisition behind a file,
/// and the chooser's Waveforms page sets this rather than the connection fields.
std::string open_waveform;
};
/// Parses argv. On `--help` prints usage to stdout and exits(0); on a bad
+329
View File
@@ -0,0 +1,329 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 Martino Ferrari
#include "event_store.hpp"
#include <dirent.h>
#include <sys/stat.h>
#include <unistd.h>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <ctime>
#include "layout_store.hpp"
namespace udpscope {
namespace {
constexpr const char* kEventsSubdir = "/events";
void mkdirs(const std::string& path) {
std::string cur;
for (std::size_t i = 0; i < path.size(); ++i) {
cur.push_back(path[i]);
if (path[i] != '/' && i + 1 != path.size()) continue;
::mkdir(cur.c_str(), 0755);
}
}
/// `2026-10-09T11:32:00` in local time, plus the sequence number: two events inside one second are
/// ordinary (a 5 Hz trigger latches five times a second), and a stem that collides would overwrite
/// the event it collided with.
std::string event_stem(double wall_time, std::uint64_t seq) {
const std::time_t t = static_cast<std::time_t>(wall_time);
std::tm tm{};
localtime_r(&t, &tm);
char buf[64];
std::strftime(buf, sizeof(buf), "%Y%m%d-%H%M%S", &tm);
char stem[96];
std::snprintf(stem, sizeof(stem), "%s-%04llu", buf,
static_cast<unsigned long long>(seq % 10000ull));
return stem;
}
/// `a.npy` -> `a`; anything else -> "".
std::string stem_of(const std::string& file, const char* ext) {
const std::size_t n = std::strlen(ext);
if (file.size() <= n || file.compare(file.size() - n, n, ext) != 0) return {};
return file.substr(0, file.size() - n);
}
/// The event's picture, drawn from its samples: one horizontal strip per signal, min/max per pixel
/// column, the trigger edge as a brighter line. A 24-bit BMP -- the header is nine words and the rest
/// is the pixels, so it needs no encoder, and a person can open it with anything.
///
/// The palette is fixed rather than the plots': this is navigation, and what has to be told apart at
/// a glance is one *event* from another, not one signal from another (for which the names are on the
/// row above).
bool write_preview(const ExportTable& table, const std::string& path, int width = 480, int height = 160) {
if (table.empty() || table.columns.empty()) return false;
const std::size_t rows = table.rows();
const std::size_t strips = table.columns.size();
const int strip_h = std::max(6, height / static_cast<int>(strips));
const int total_h = strip_h * static_cast<int>(strips);
static const std::uint32_t kPalette[] = {0x4FA3FFu, 0xFF7A4Fu, 0x6EE07Au, 0xE06ED0u,
0xFFD24Fu, 0x6FD8E0u, 0xB9A0FFu, 0xE0E0E0u};
// One byte per pixel per channel, bottom-up (BMP's own order), padded to a 4-byte row.
const int row_bytes = ((width * 3) + 3) & ~3;
std::vector<unsigned char> pixels(std::size_t(row_bytes) * total_h, 0);
const auto put = [&](int x, int y, std::uint32_t rgb, double alpha) {
if (x < 0 || y < 0 || x >= width || y >= total_h) return;
unsigned char* p = &pixels[std::size_t(total_h - 1 - y) * row_bytes + std::size_t(x) * 3];
const double a = std::clamp(alpha, 0.0, 1.0);
p[0] = static_cast<unsigned char>(p[0] * (1.0 - a) + ((rgb >> 16) & 0xFF) * a);
p[1] = static_cast<unsigned char>(p[1] * (1.0 - a) + ((rgb >> 8) & 0xFF) * a);
p[2] = static_cast<unsigned char>(p[2] * (1.0 - a) + (rgb & 0xFF) * a);
};
// The background: a dark field, so the traces read the way they do on screen.
for (int y = 0; y < total_h; ++y) {
for (int x = 0; x < width; ++x) put(x, y, 0x101418u, 1.0);
}
for (std::size_t c = 0; c < strips; ++c) {
const std::vector<f32>& values = table.columns[c].values;
if (values.empty()) continue;
double lo = values[0];
double hi = values[0];
for (f32 v : values) {
if (!std::isfinite(v)) continue;
lo = std::min(lo, double(v));
hi = std::max(hi, double(v));
}
if (!(hi > lo)) {
hi = lo + 1.0;
}
const int base = static_cast<int>(c) * strip_h;
const std::uint32_t colour = kPalette[c % (sizeof(kPalette) / sizeof(kPalette[0]))];
for (int x = 0; x < width; ++x) {
const std::size_t from = rows * std::size_t(x) / std::size_t(width);
const std::size_t to = std::max(from + 1, rows * std::size_t(x + 1) / std::size_t(width));
double vmin = 1e300;
double vmax = -1e300;
for (std::size_t i = from; i < to && i < rows; ++i) {
const double v = values[i];
if (!std::isfinite(v)) continue;
vmin = std::min(vmin, v);
vmax = std::max(vmax, v);
}
if (vmin > vmax) continue;
const auto to_y = [&](double v) {
const double frac = (v - lo) / (hi - lo);
return base + strip_h - 1 - static_cast<int>(frac * (strip_h - 1));
};
const int y0 = std::clamp(to_y(vmax), base, base + strip_h - 1);
const int y1 = std::clamp(to_y(vmin), base, base + strip_h - 1);
for (int y = y0; y <= y1; ++y) put(x, y, colour, 1.0);
}
// The strip's own baseline, so an empty stretch reads as a flat trace and not as a gap.
for (int x = 0; x < width; ++x) {
const int y = base + strip_h - 1;
put(x, y, 0x303840u, 1.0);
}
}
// The trigger edge: the snapshot's x = 0, which is at the same fraction of every event's window.
if (!table.time.empty()) {
const double t0 = table.time.front();
const double t1 = table.time.back();
if (t1 > t0) {
const int x = static_cast<int>((0.0 - t0) / (t1 - t0) * (width - 1));
for (int y = 0; y < total_h; ++y) put(x, y, 0xFFE9A0u, 0.85);
}
}
std::FILE* f = std::fopen(path.c_str(), "wb");
if (f == nullptr) return false;
const std::uint32_t image_bytes = std::uint32_t(row_bytes) * std::uint32_t(total_h);
const std::uint32_t file_bytes = 54u + image_bytes;
unsigned char header[54] = {};
header[0] = 'B';
header[1] = 'M';
const auto put32 = [&](int at, std::uint32_t v) {
header[at] = static_cast<unsigned char>(v & 0xFF);
header[at + 1] = static_cast<unsigned char>((v >> 8) & 0xFF);
header[at + 2] = static_cast<unsigned char>((v >> 16) & 0xFF);
header[at + 3] = static_cast<unsigned char>((v >> 24) & 0xFF);
};
put32(2, file_bytes);
put32(10, 54u);
put32(14, 40u);
put32(18, std::uint32_t(width));
put32(22, std::uint32_t(total_h));
header[26] = 1; // planes
header[28] = 24; // bits per pixel
put32(34, image_bytes);
put32(38, 2835u); // 72 dpi, in pixels per metre
put32(42, 2835u);
std::fwrite(header, 1, sizeof(header), f);
std::fwrite(pixels.data(), 1, pixels.size(), f);
std::fclose(f);
return true;
}
} // namespace
std::string event_meta_text(const EventRecord& r) {
char buf[512];
std::string out;
std::snprintf(buf, sizeof(buf), "wall_time = %.6f\n", r.wall_time);
out += buf;
std::snprintf(buf, sizeof(buf), "duration_s = %.6f\n", r.duration_s);
out += buf;
std::snprintf(buf, sizeof(buf), "pre_s = %.6f\n", r.pre_s);
out += buf;
std::snprintf(buf, sizeof(buf), "post_s = %.6f\n", r.post_s);
out += buf;
std::snprintf(buf, sizeof(buf), "edge = %.6f\n", r.edge);
out += buf;
std::snprintf(buf, sizeof(buf), "rate_hz = %.6g\n", r.rate_hz);
out += buf;
std::snprintf(buf, sizeof(buf), "signals = %zu\n", r.signals);
out += buf;
std::snprintf(buf, sizeof(buf), "samples = %zu\n", r.samples);
out += buf;
std::snprintf(buf, sizeof(buf), "seq = %llu\n", static_cast<unsigned long long>(r.seq));
out += buf;
return out;
}
bool event_meta_parse(const std::string& text, EventRecord* out) {
if (out == nullptr) return false;
bool any = false;
std::size_t i = 0;
while (i < text.size()) {
const std::size_t end = text.find('\n', i);
const std::string line = text.substr(i, end == std::string::npos ? std::string::npos : end - i);
i = end == std::string::npos ? text.size() : end + 1;
const std::size_t eq = line.find('=');
if (eq == std::string::npos) continue;
std::string key = line.substr(0, eq);
std::string value = line.substr(eq + 1);
while (!key.empty() && (key.back() == ' ' || key.back() == '\t')) key.pop_back();
while (!value.empty() && (value.front() == ' ' || value.front() == '\t')) value.erase(0, 1);
const double number = std::atof(value.c_str());
if (key == "wall_time") {
out->wall_time = number;
any = true;
} else if (key == "duration_s") {
out->duration_s = number;
any = true;
} else if (key == "pre_s") {
out->pre_s = number;
any = true;
} else if (key == "post_s") {
out->post_s = number;
any = true;
} else if (key == "edge") {
out->edge = number;
any = true;
} else if (key == "rate_hz") {
out->rate_hz = number;
any = true;
} else if (key == "signals") {
out->signals = static_cast<std::size_t>(std::atoll(value.c_str()));
any = true;
} else if (key == "samples") {
out->samples = static_cast<std::size_t>(std::atoll(value.c_str()));
any = true;
} else if (key == "seq") {
out->seq = static_cast<std::uint64_t>(std::strtoull(value.c_str(), nullptr, 10));
any = true;
}
}
return any;
}
EventStore::EventStore(std::string source) {
if (source.empty()) return;
std::string safe;
for (char c : source) {
const bool ok = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') ||
c == '.' || c == '_' || c == '-';
safe.push_back(ok ? c : '_');
}
dir_ = cache_dir() + "/" + safe + kEventsSubdir;
}
std::string EventStore::save(const ExportTable& table, const EventRecord& meta, std::size_t keep) {
if (dir_.empty() || table.empty()) return {};
mkdirs(dir_);
const std::string stem = event_stem(meta.wall_time > 0.0 ? meta.wall_time : double(std::time(nullptr)),
meta.seq);
const std::string samples = dir_ + "/" + stem + ".npy";
std::string error;
if (!write_npy(samples, table, error)) {
std::fprintf(stderr, "[udpscope] event cache: %s\n", error.c_str());
std::fflush(stderr);
return {};
}
EventRecord record = meta;
record.stem = stem;
std::string text = event_meta_text(record);
if (std::FILE* f = std::fopen((dir_ + "/" + stem + ".txt").c_str(), "wb"); f != nullptr) {
std::fwrite(text.data(), 1, text.size(), f);
std::fclose(f);
}
write_preview(table, dir_ + "/" + stem + ".bmp");
prune(keep);
return samples;
}
std::vector<EventRecord> EventStore::list() const {
std::vector<EventRecord> out;
if (dir_.empty()) return out;
DIR* d = ::opendir(dir_.c_str());
if (d == nullptr) return out;
while (const dirent* e = ::readdir(d)) {
const std::string stem = stem_of(e->d_name, ".txt");
if (stem.empty()) continue;
std::FILE* f = std::fopen((dir_ + "/" + e->d_name).c_str(), "rb");
if (f == nullptr) continue;
std::string text;
char buf[512];
std::size_t n = 0;
while ((n = std::fread(buf, 1, sizeof(buf), f)) > 0) text.append(buf, n);
std::fclose(f);
EventRecord r;
if (!event_meta_parse(text, &r)) continue;
r.stem = stem;
r.file = dir_ + "/" + stem + ".npy";
if (::access(r.file.c_str(), R_OK) != 0) continue; // a sidecar without its samples
struct stat sb {};
if (::stat((dir_ + "/" + stem + ".bmp").c_str(), &sb) == 0) {
r.thumbnail = dir_ + "/" + stem + ".bmp";
}
out.push_back(std::move(r));
}
::closedir(d);
// Newest first by the *record's* time, not the file's: a copy or a touch changes the mtime, and the
// list is about when the event happened.
std::stable_sort(out.begin(), out.end(), [](const EventRecord& a, const EventRecord& b) {
if (a.wall_time != b.wall_time) return a.wall_time > b.wall_time;
return a.seq > b.seq;
});
return out;
}
bool EventStore::forget(const std::string& stem) {
if (dir_.empty() || stem.empty()) return false;
const std::string base = dir_ + "/" + stem;
const bool gone = ::unlink((base + ".npy").c_str()) == 0;
::unlink((base + ".txt").c_str());
::unlink((base + ".bmp").c_str());
return gone;
}
void EventStore::prune(std::size_t keep) const {
const std::vector<EventRecord> all = list();
for (std::size_t i = keep; i < all.size(); ++i) {
const std::string base = dir_ + "/" + all[i].stem;
::unlink((base + ".npy").c_str());
::unlink((base + ".txt").c_str());
::unlink((base + ".bmp").c_str());
}
}
} // namespace udpscope
+82
View File
@@ -0,0 +1,82 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 Martino Ferrari
#pragma once
#include "export.hpp"
#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>
namespace udpscope {
/// What a cached event is, without loading its samples: the sidecar's fields, and what the pane
/// shows in one row.
struct EventRecord {
std::string stem; ///< the file's name without the extension
std::string file; ///< `<stem>.npy`, the event's table in the export format
std::string thumbnail; ///< `<stem>.bmp`, the preview drawn from the samples; else empty
double wall_time = 0.0; ///< CLOCK_REALTIME at the latch, for the row's label
double duration_s = 0.0; ///< pre + post
double pre_s = 0.0;
double post_s = 0.0;
double edge = 0.0; ///< the stream time of the event's x = 0
double rate_hz = 0.0; ///< the stored sample rate at the latch
std::size_t signals = 0; ///< columns in the file
std::size_t samples = 0; ///< rows
std::uint64_t seq = 0; ///< the run's own event counter, so two events in one second differ
};
/// The cache of captured events: `<cache_dir>/<source>/events/`, one `.npy` per event written in
/// the **export format** -- so `--open` and the chooser's Waveforms page read it back with the
/// loader that already exists -- beside a `.txt` sidecar holding what the list shows without
/// loading the samples, and whatever thumbnail the caller rendered.
///
/// No GTK: the caller renders the thumbnail and hands over a path, exactly as it hands over the
/// table. The store owns naming, the sidecar, pruning and listing, which is what makes it testable
/// without a window.
class EventStore {
public:
/// `source` is the `<host>-<port>` pair, the same key the packet capture uses.
explicit EventStore(std::string source = {});
/// How many events to keep, newest first. The oldest beyond it are deleted on every save; item 9's
/// preferences are where this becomes the user's number.
static constexpr std::size_t kDefaultKeep = 32;
const std::string& dir() const noexcept { return dir_; }
bool empty_dir() const noexcept { return dir_.empty(); }
/// Writes `table` as one event, with `meta` in the sidecar and a preview drawn beside it. Returns
/// the samples' path, or an empty string on failure -- a cache that cannot be written must not stop
/// the scope.
///
/// The preview is drawn from the *data* -- min/max per pixel column, one strip per signal -- and not
/// captured from the window: the plot is a `GtkGLArea`, and a GL area's ink is not in the widget's
/// paint tree, so a snapshot of it is empty (`gtk_snapshot_free_to_node()` returns NULL). Drawing it
/// here also keeps this class GTK-free, which is what makes it testable without a window.
std::string save(const ExportTable& table, const EventRecord& meta,
std::size_t keep = kDefaultKeep);
/// The cached events, newest first: the sidecars, never the sample files, so a listing is cheap
/// however large the captures are.
std::vector<EventRecord> list() const;
/// Deletes one event's samples, sidecar and thumbnail.
bool forget(const std::string& stem);
/// Deletes the oldest events beyond `keep`.
void prune(std::size_t keep) const;
private:
std::string dir_;
};
/// The sidecar's text, and back. One `key = value` per line, the project's own TOML subset, so a
/// human can read the cache with `cat` -- which is the point of a forensic directory.
std::string event_meta_text(const EventRecord& record);
bool event_meta_parse(const std::string& text, EventRecord* out);
} // namespace udpscope
+15
View File
@@ -182,6 +182,21 @@ ExportTable collect_export(ScopeModel& model, RowSet& rows) {
return table;
}
ExportTable event_table(ScopeModel& model, const EventSnapshot& snapshot) {
ExportTable table;
table.time = snapshot.stamps;
table.abs_seconds = snapshot.abs_stamps.empty() ? snapshot.stamps : snapshot.abs_stamps;
std::lock_guard<std::mutex> lock(model.mutex());
const std::vector<ChannelInfo>& channels = model.channels();
for (std::size_t c = 0; c < snapshot.values.size() && c < channels.size(); ++c) {
ExportTable::Column column;
column.name = channels[c].name;
column.values = snapshot.values[c];
table.columns.push_back(std::move(column));
}
return table;
}
bool write_npy(const std::string& path, const ExportTable& table, std::string& error) {
if (table.empty()) {
error = "nothing to export: no signal is plotted in the visible range";
+6
View File
@@ -54,6 +54,12 @@ struct ExportTable {
/// model access. In trigger mode nothing is locked: the `EventSnapshot` is immutable.
ExportTable collect_export(ScopeModel& model, RowSet& rows);
/// The held event as a table, built from the **snapshot** rather than from the rows: a cache of the
/// acquisition, every channel the event holds, independent of which traces a row happens to show
/// when the latch lands. The channel names are the model's, in channel order -- the same order the
/// columns come out in, so `--open` restores the same signals.
ExportTable event_table(ScopeModel& model, const EventSnapshot& snapshot);
/// Writes a NumPy `.npy` file holding one structured array: `time` and `abs_seconds` as
/// `<f8`, every signal as `<f4` -- the width the ring actually stores, so the file is
/// about half the size of an all-`f8` one and invents no precision.
+13 -2
View File
@@ -6,8 +6,19 @@ 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: the sessions udpscope has stored, newest
first, plus a <b>New host…</b> form for one it has not seen. The chooser runs before any
<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>
+3
View File
@@ -13,6 +13,9 @@ the overflow menu opens the editor where the unit and the calibration live.
• 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.
+4
View File
@@ -16,6 +16,10 @@ 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
+21
View File
@@ -14,6 +14,27 @@ share. <b>Arm</b> starts watching; <tt>a</tt> arms and disarms from the keyboard
<b>Worth knowing</b>
• <b>Every triggered acquisition is kept.</b> Latches are written to the event cache
(<tt>~/.cache/udpscope/&lt;source&gt;/events/</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
+41 -15
View File
@@ -1,34 +1,60 @@
<b>Sessions and settings</b>
udpscope stores one session file per source, so reopening a source gives you the scope you
closed. There is no global preferences file: the session <i>is</i> the settings.
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 file is named <tt>&lt;host&gt;_&lt;port&gt;.toml</tt>.
unset. Each source has a directory <tt>&lt;host&gt;_&lt;port&gt;/</tt> holding one
<tt>&lt;name&gt;.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 session persists: the time window, the rows and each row's slot signals, the
calibration, which signals are enabled, the math channels with their graphs, and the
measurements pane.
• 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.
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 startup chooser lists only files that name at least one signal, which is what keeps it the
sources you configured rather than every port you have ever tested against.
• Deleting a file is a supported way to start a source over; the chooser's <tt>×</tt> does
exactly that.
• 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>&lt;host&gt;_&lt;port&gt;.toml.imported</tt>.
<b>Example</b>
Connect to <tt>127.0.0.1:8000</tt>, drop three signals on row 1 and set a 2 s window.
<tt>~/.config/udpscope/127.0.0.1_8000.toml</tt> now holds <tt>rows=1 window_s=2</tt> with
<tt>signals = ["sine", "ramp", "noise", ""]</tt>, and the next run reports
<tt>layout loaded from …/127.0.0.1_8000.toml</tt> with those signals already acquired.
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.
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- SPDX-License-Identifier: MIT -->
<!-- SPDX-FileCopyrightText: 2026 Martino Ferrari -->
<!-- The event history: a list of what was latched. A frame with three rows in it, the
last one short, so it reads as a list that ends rather than one that continues. Every
coordinate is a whole pixel of the 16px cell, and every subpath is a clockwise filled
`path`: a stroke, a line or a polyline keeps the colour written here and stays dark on
a dark theme, and the frame's hole needs `evenodd` because the rows sit inside it.
(No double hyphen anywhere in a comment here: xmllint, which preprocess= runs it
through, rejects one and fails the whole build.) -->
<svg height="16px" width="16px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<path fill="#2e3436" fill-rule="evenodd" d="M0 1 H16 V15 H0 Z
M2 3 H14 V13 H2 Z"/>
<path fill="#2e3436" d="M4 4 H12 V6 H4 Z
M4 7 H12 V9 H4 Z
M4 10 H9 V12 H4 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+1
View File
@@ -21,6 +21,7 @@
<file preprocess="xml-stripblanks">scalable/actions/udpscope-cursors-symbolic.svg</file>
<file preprocess="xml-stripblanks">scalable/actions/udpscope-measure-symbolic.svg</file>
<file preprocess="xml-stripblanks">scalable/actions/udpscope-math-symbolic.svg</file>
<file preprocess="xml-stripblanks">scalable/actions/udpscope-events-symbolic.svg</file>
<file preprocess="xml-stripblanks">scalable/apps/org.udpscope.app.svg</file>
<file preprocess="xml-stripblanks">scalable/apps/org.udpscope.app-symbolic.svg</file>
</gresource>
+226 -41
View File
@@ -114,56 +114,223 @@ std::string config_dir() {
return std::string(home) + "/.config/udpscope";
}
std::string cache_dir() {
const char* xdg = std::getenv("XDG_CACHE_HOME");
if (xdg != nullptr && xdg[0] != '\0') return std::string(xdg) + "/udpscope";
const char* home = std::getenv("HOME");
if (home == nullptr || home[0] == '\0') return "/tmp/udpscope-cache";
return std::string(home) + "/.cache/udpscope";
}
std::string layout_path(const std::string& host, std::uint16_t port) {
const std::string dir = config_dir();
mkdirs(dir);
return dir + "/" + sanitize(host) + "_" + std::to_string(port) + ".toml";
}
std::vector<KnownSource> known_sources() {
std::vector<KnownSource> out;
const std::string dir = config_dir();
std::string config_dir_for(const std::string& host, std::uint16_t port) {
return config_dir() + "/" + sanitize(host) + "_" + std::to_string(port);
}
std::string configuration_path(const std::string& host, std::uint16_t port,
const std::string& name) {
const std::string dir = config_dir_for(host, port);
mkdirs(dir);
return dir + "/" + sanitize(name) + ".toml";
}
namespace {
bool file_exists(const std::string& path) {
struct stat sb {};
return ::stat(path.c_str(), &sb) == 0;
}
/// A session written before configurations existed is the configuration named `default`: copied
/// into the per-source directory, remembered as active, and the old file renamed out of the way
/// rather than deleted, so the import can be undone by hand. Runs before every read and write of
/// the per-source directory, so a machine that upgrades keeps its session.
void import_legacy(const std::string& host, std::uint16_t port) {
const std::string dir = config_dir_for(host, port);
if (file_exists(dir)) return;
const std::string legacy = layout_path(host, port);
if (!file_exists(legacy)) return;
mkdirs(dir);
FILE* in = std::fopen(legacy.c_str(), "rb");
if (in == nullptr) return;
const std::string target = dir + "/default.toml";
FILE* out = std::fopen(target.c_str(), "wb");
if (out == nullptr) {
std::fclose(in);
return;
}
char buf[4096];
std::size_t n = 0;
while ((n = std::fread(buf, 1, sizeof(buf), in)) > 0) std::fwrite(buf, 1, n, out);
std::fclose(in);
std::fclose(out);
FILE* active = std::fopen((dir + "/active").c_str(), "wb");
if (active != nullptr) {
std::fputs("default\n", active);
std::fclose(active);
}
::rename(legacy.c_str(), (legacy + ".imported").c_str());
}
/// `a.toml` -> `a`; anything else -> "".
std::string toml_stem(const std::string& file) {
if (file.size() <= 5 || file.compare(file.size() - 5, 5, ".toml") != 0) return {};
return file.substr(0, file.size() - 5);
}
/// `host_port` -> {host, port}, or false. The *last* '_' is the split, because `sanitize()`
/// replaces anything unsafe in a host with one.
bool split_stem(const std::string& stem, std::string* host, std::uint16_t* port) {
const std::size_t cut = stem.rfind('_');
if (cut == std::string::npos || cut + 1 == stem.size()) return false;
char* end = nullptr;
const long p = std::strtol(stem.c_str() + cut + 1, &end, 10);
if (end == nullptr || *end != '\0' || p <= 0 || p > 65535) return false;
*host = stem.substr(0, cut);
*port = static_cast<std::uint16_t>(p);
return true;
}
/// The configurations a source directory holds, newest first, by file name.
std::vector<std::string> names_in_dir(const std::string& dir) {
std::vector<std::pair<std::int64_t, std::string>> found;
DIR* d = ::opendir(dir.c_str());
if (d == nullptr) return {};
while (const dirent* e = ::readdir(d)) {
const std::string stem = toml_stem(e->d_name);
if (stem.empty()) continue;
struct stat sb {};
const std::string path = dir + "/" + e->d_name;
std::int64_t mtime = 0;
if (::stat(path.c_str(), &sb) == 0) mtime = static_cast<std::int64_t>(sb.st_mtime);
found.emplace_back(mtime, stem);
}
::closedir(d);
std::stable_sort(found.begin(), found.end(),
[](const auto& a, const auto& b) { return a.first > b.first; });
std::vector<std::string> out;
out.reserve(found.size());
for (auto& f : found) out.push_back(std::move(f.second));
return out;
}
} // namespace
std::vector<std::string> configuration_names(const std::string& host, std::uint16_t port) {
import_legacy(host, port);
return names_in_dir(config_dir_for(host, port));
}
std::string active_configuration(const std::string& host, std::uint16_t port) {
const std::vector<std::string> names = configuration_names(host, port);
if (names.empty()) return "default";
FILE* f = std::fopen((config_dir_for(host, port) + "/active").c_str(), "rb");
if (f != nullptr) {
char buf[512];
const std::size_t n = std::fread(buf, 1, sizeof(buf) - 1, f);
std::fclose(f);
buf[n] = '\0';
const std::string want = trim(std::string(buf));
if (std::find(names.begin(), names.end(), want) != names.end()) return want;
}
// Newest, which is the one just saved when nothing wrote an `active`.
return names.front();
}
bool set_active_configuration(const std::string& host, std::uint16_t port,
const std::string& name) {
const std::string dir = config_dir_for(host, port);
mkdirs(dir);
FILE* f = std::fopen((dir + "/active").c_str(), "wb");
if (f == nullptr) return false;
std::fputs(sanitize(name).c_str(), f);
std::fputs("\n", f);
std::fclose(f);
return true;
}
bool forget_configuration(const std::string& host, std::uint16_t port, const std::string& name) {
const std::string path = configuration_path(host, port, name);
const bool gone = std::remove(path.c_str()) == 0;
const std::string active = active_configuration(host, port);
if (gone && sanitize(name) == sanitize(active)) {
std::remove((config_dir_for(host, port) + "/active").c_str());
}
return gone;
}
std::vector<KnownSource> known_sources() {
const std::string root = config_dir();
// A legacy single-file session is imported first, so the listing below has one shape: the
// per-source directory and its configuration files.
if (DIR* d = ::opendir(root.c_str())) {
while (const dirent* e = ::readdir(d)) {
const std::string stem = toml_stem(e->d_name);
if (stem.empty()) continue;
std::string host;
std::uint16_t port = 0;
if (!split_stem(stem, &host, &port)) continue;
// The scheme `layout_path()` writes, and nothing else: a stray `.toml` here is not ours.
if (sanitize(host) + "_" + std::to_string(port) != stem) continue;
import_legacy(host, port);
}
::closedir(d);
}
std::vector<KnownSource> out;
DIR* d = ::opendir(root.c_str());
// No directory yet is an empty list, not an error: a first run has never saved anything.
if (d == nullptr) return out;
while (const dirent* e = ::readdir(d)) {
const std::string name = e->d_name;
if (name.size() <= 5 || name.compare(name.size() - 5, 5, ".toml") != 0) continue;
const std::string stem = name.substr(0, name.size() - 5);
// The *last* '_': `sanitize()` replaces anything unsafe in a host with one, so the host part
// may hold them too and a first-match split would return a host cut in half.
const std::size_t cut = stem.rfind('_');
if (cut == std::string::npos || cut + 1 == stem.size()) continue;
char* end = nullptr;
const long port = std::strtol(stem.c_str() + cut + 1, &end, 10);
if (end == nullptr || *end != '\0' || port <= 0 || port > 65535) continue;
const std::string host = stem.substr(0, cut);
// The name must be one `layout_path()` could have written, or the `×` -- which rebuilds it
// through `layout_path()` -- would unlink a different session. `sanitize()` is idempotent on
// any host that already came back out of a filename, so this is also true for hosts whose names
// contain `_` from the sanitizer.
if (sanitize(host) + "_" + std::to_string(port) != stem) continue;
LayoutState st;
const std::string path = dir + "/" + name;
if (!load_layout(path, st)) continue;
KnownSource s;
s.host = host;
s.port = static_cast<std::uint16_t>(port);
s.multicast = st.multicast;
s.interface_addr = st.interface_addr;
s.rows = static_cast<int>(st.rows.size());
for (const LayoutState::Row& row : st.rows) {
for (const std::string& sig : row.signals) {
if (!sig.empty()) ++s.signals;
}
}
if (s.signals == 0) continue; // see the header: a session nobody configured
const std::string dir = root + "/" + e->d_name;
struct stat sb {};
s.mtime = (::stat(path.c_str(), &sb) == 0) ? static_cast<std::int64_t>(sb.st_mtime) : 0;
out.push_back(std::move(s));
if (::stat(dir.c_str(), &sb) != 0 || !S_ISDIR(sb.st_mode)) continue;
std::string host;
std::uint16_t port = 0;
if (!split_stem(e->d_name, &host, &port)) continue;
// The name must be one `config_dir_for()` could have written, or `forget_source()` would walk
// out of the directory the row came from.
if (sanitize(host) + "_" + std::to_string(port) != e->d_name) continue;
// One row per *configuration*: the same source may hold several named sessions, which is the
// point of the feature, and the chooser is where they are told apart.
for (const std::string& name : names_in_dir(dir)) {
const std::string path = dir + "/" + name + ".toml";
LayoutState st;
if (!load_layout(path, st)) continue;
KnownSource s;
s.host = host;
s.port = port;
s.config = name;
s.comment = st.comment;
s.buffer_mb = st.buffer_mb;
s.relative_time = st.relative_time;
s.capture_packets = st.capture_packets;
s.multicast = st.multicast;
s.interface_addr = st.interface_addr;
s.rows = static_cast<int>(st.rows.size());
for (const LayoutState::Row& row : st.rows) {
for (const std::string& sig : row.signals) {
if (!sig.empty()) ++s.signals;
}
}
// A configuration nobody configured is not worth a row: a development machine accumulates one
// session file per probe run and every one holds `signals = ["", "", "", ""]`. One that
// carries a *setting* is worth a row even before it has ever streamed -- that is how a host is
// set up from the chooser's form, and the settings are the whole point of editing one.
const bool configured = s.signals > 0 || s.buffer_mb > 0.0 || !st.comment.empty() ||
st.relative_time || st.capture_packets > 0;
if (!configured) continue;
struct stat fb {};
s.mtime = (::stat(path.c_str(), &fb) == 0) ? static_cast<std::int64_t>(fb.st_mtime) : 0;
out.push_back(std::move(s));
}
}
::closedir(d);
// Stable, so two files sharing an mtime keep the order the directory gave them rather than
@@ -174,10 +341,10 @@ std::vector<KnownSource> known_sources() {
}
bool forget_source(const KnownSource& src) {
// Through `layout_path()` rather than by rebuilding the name here: one expression of the
// Through `configuration_path()` rather than by rebuilding the name here: one expression of the
// scheme. `src.host` came back out of a filename, so it is already sanitized, and `sanitize()`
// keeps alphanumerics, '.', '-' and '_' -- it is idempotent, so the round trip is exact.
return std::remove(layout_path(src.host, src.port).c_str()) == 0;
return forget_configuration(src.host, src.port, src.config);
}
std::string math_node_text(const MathNode& node) {
@@ -315,6 +482,20 @@ bool load_layout(const std::string& path, LayoutState& out) {
st.multicast = strip_quote(value);
} else if (key == "interface") {
st.interface_addr = strip_quote(value);
} else if (key == "comment") {
st.comment = strip_quote(value);
} else if (key == "buffer_mb") {
try {
st.buffer_mb = std::stod(value);
} catch (...) {
}
} else if (key == "relative_time") {
st.relative_time = value == "true";
} else if (key == "capture_packets") {
try {
st.capture_packets = static_cast<std::uint32_t>(std::max(0, std::stoi(value)));
} catch (...) {
}
}
} else if (section == Section::Plot && !st.rows.empty()) {
if (key == "signals") {
@@ -434,6 +615,10 @@ bool save_layout(const std::string& path, const LayoutState& state) {
o << "]\n";
}
o << "measure_stats = " << (state.measure_stats ? "true" : "false") << "\n";
if (!state.comment.empty()) o << "comment = " << toml_escape(state.comment) << "\n";
if (state.buffer_mb > 0.0) o << "buffer_mb = " << state.buffer_mb << "\n";
o << "relative_time = " << (state.relative_time ? "true" : "false") << "\n";
if (state.capture_packets > 0) o << "capture_packets = " << state.capture_packets << "\n";
for (const auto& row : state.rows) {
o << "\n[[plot]]\nsignals = [";
for (std::size_t i = 0; i < row.signals.size(); ++i) {
+56 -4
View File
@@ -65,6 +65,20 @@ struct LayoutState {
/// The measurements pane, by signal *name* and with the type and options a row was built with.
std::vector<Measure> measures;
/// Free text the user attached to this configuration: what it is for, what to look at first.
/// A configuration is a *named* session, and the name alone rarely says what makes it different
/// from its neighbour.
std::string comment;
/// The startup settings the configuration carries, applied when the chooser opens it -- which is
/// why they live here and not only on the command line: two sources on one machine want different
/// buffers and different clocks, and retyping the flags to look at the second one is how a
/// setting gets forgotten. A zero `buffer_mb` and an empty `capture_packets` mean "not stored":
/// the command line's value stands, so an old file cannot silently shrink a buffer.
double buffer_mb = 0.0;
bool relative_time = false;
std::uint32_t capture_packets = 0;
};
/// A data source udpscope has a stored session for: one row of the startup chooser.
@@ -76,13 +90,23 @@ struct KnownSource {
int rows = 0; ///< plots the session had
int signals = 0; ///< named slots across those plots
std::int64_t mtime = 0; ///< orders the list; never shown
/// Which *configuration* of that source this row is. A source may have several, each a named
/// session of its own, so the pair (host, port, config) is what identifies a row.
std::string config;
std::string comment; ///< the configuration's note, shown beside its name
/// The startup settings this configuration carries, so the chooser can apply them.
double buffer_mb = 0.0;
bool relative_time = false;
std::uint32_t capture_packets = 0;
};
/// Every session file that names at least one signal, newest first.
/// Every configuration that names at least one signal *or* carries a setting, newest first.
///
/// The filter is what makes the list usable: a development machine accumulates one session file
/// per probe run on a throwaway port -- 140 of them when this was written -- and every one holds
/// `signals = ["", "", "", ""]`. A listed source is one somebody actually configured.
/// `signals = ["", "", "", ""]`. A listed source is one somebody actually configured — and a
/// buffer size, a note or a clock counts as configuring it, because that is how the chooser's form
/// sets a host up before it has ever streamed.
std::vector<KnownSource> known_sources();
/// Deletes one source's session file; the chooser's `x`. False means the file was already gone or
@@ -98,11 +122,39 @@ bool math_node_from_text(const std::string& text, MathNode& out);
/// The directory session files live in: $XDG_CONFIG_HOME/udpscope, else
/// ~/.config/udpscope, else /tmp/udpscope-config. Not created by this call.
std::string config_dir();
/// `$XDG_CACHE_HOME/udpscope`, or `~/.cache/udpscope`: where bulky, disposable state
/// lives (the packet captures; item 9's event cache). Created on demand by callers.
std::string cache_dir();
/// `~/.config/udpscope/<host>_<port>.toml`, with the directory created.
/// Characters unsafe in a filename are replaced in the host part.
/// The *legacy* single-file path, `~/.config/udpscope/<host>_<port>.toml`, with the directory
/// created. Still the import source for a file written before configurations existed; nothing
/// writes here any more.
std::string layout_path(const std::string& host, std::uint16_t port);
/// The per-source directory a source's configurations live in:
/// `~/.config/udpscope/<host>_<port>/`. Not created by this call.
std::string config_dir_for(const std::string& host, std::uint16_t port);
/// One configuration's file, `<config_dir_for(host, port)>/<name>.toml`, with the directory
/// created. The name is sanitized the way the host is, so a name the user typed with a slash in it
/// cannot escape the directory.
std::string configuration_path(const std::string& host, std::uint16_t port,
const std::string& name);
/// The source's configurations, newest first. A legacy single-file session appears here as the
/// configuration named `default`, and importing it is the first thing this does.
std::vector<std::string> configuration_names(const std::string& host, std::uint16_t port);
/// The configuration the source was last left in: `<dir>/active`, or the newest that exists when
/// nothing wrote one. Never empty for a source that has any configuration at all.
std::string active_configuration(const std::string& host, std::uint16_t port);
/// Remembers which configuration this source is on. Called by every save.
bool set_active_configuration(const std::string& host, std::uint16_t port, const std::string& name);
/// Deletes one configuration's file, and `active` if it named it.
bool forget_configuration(const std::string& host, std::uint16_t port, const std::string& name);
/// Parses the file's TOML subset. Returns false when the file is missing or
/// malformed; `out` is left usable either way (a malformed file loads as much as
/// parsed, which beats refusing a hand-edited file over one stray character).
+41 -6
View File
@@ -3,6 +3,7 @@
#include "acquisition.hpp"
#include "config.hpp"
#include "waveform.hpp"
#include "model.hpp"
#include "plot_host.hpp"
#include "source_dialog.hpp"
@@ -22,17 +23,51 @@ int main(int argc, char** argv) {
// No `--server` means the user has not said which source to open, so ask. Before the model:
// declining must cost no buffer and no socket, and nothing below has to cope with "no source".
if (!cfg.server_specified && !udpscope::choose_source(cfg)) return 0;
// `--open` is the other answer to that question, and the chooser knows it: its Waveforms page
// fills `open_waveform` in instead of the connection fields.
if (!cfg.server_specified && cfg.open_waveform.empty() && !udpscope::choose_source(cfg)) return 0;
// A source named on the command line skips the chooser, but its *stored* configuration still
// applies: the buffer size, the clock and the capture ring are decided here, before the model --
// which is the only moment they can be -- and a saved host whose buffer was ignored because the
// address came from the command line is an edit that looks like it never took effect.
if (cfg.server_specified) udpscope::apply_stored_settings(cfg);
udpscope::ScopeModel model(cfg);
udpscope::RowSet rows;
rows.set_row_count(cfg.plots);
udpscope::Acquisition acquisition(model, cfg);
try {
acquisition.start();
} catch (const std::exception& e) {
std::fprintf(stderr, "udpscope: %s\n", e.what());
return 1;
if (cfg.open_waveform.empty()) {
try {
acquisition.start();
} catch (const std::exception& e) {
std::fprintf(stderr, "udpscope: %s\n", e.what());
return 1;
}
} else {
// No socket and no thread: the file is the source, and there is nothing to arrive afterwards.
// The acquisition object is passed to the UI all the same -- stopped, which is what every
// "paused"/"capturing" question answers as false.
udpscope::WaveformLoadReport report;
std::string error;
if (!udpscope::load_waveform_into_model(model, cfg.open_waveform, &report, &error)) {
std::fprintf(stderr, "udpscope: %s\n", error.c_str());
return 1;
}
// One row per column, so every trace opens where a user can see and rearrange it -- and sized
// *here*, before the UI exists, exactly as `--plots` does: resizing the rows from inside the
// activation leaves the panels built for the old count.
rows.set_row_count(std::min<int>(std::max<int>(cfg.plots, static_cast<int>(report.signals)),
udpscope::RowSet::kMaxRows));
udpscope::remember_waveform(cfg.open_waveform);
// The whole file framed from the start: the window is the recording's own span, and nothing
// afterwards restates it -- a zoom is the user's.
if (report.span_s > 0.0) rows.set_time_window(report.span_s);
std::fprintf(stderr,
"[udpscope] waveform loaded: %s signals=%zu samples=%zu span=%.6fs rate=%.6g "
"time=[%.6f..%.6f]\n",
cfg.open_waveform.c_str(), report.signals, report.samples, report.span_s,
report.rate_hz, report.first_time, report.last_time);
std::fflush(stderr);
}
const int result = udpscope::run_ui(argc, argv, cfg, model, acquisition, rows);
+7
View File
@@ -61,6 +61,13 @@ void biquad(MathOp op, double fc, double q, double dt, double& b0, double& b1, d
/// that actually need carrying between samples.
double biquad_step(double x, double b0, double b1, double b2, double a1, double a2, double& z1,
double& z2) {
// A reserved hole is NaN in the source, and a NaN integrated into the state words is
// permanent: every later sample would come out NaN, so a filtered trace died at the first
// gap and never came back -- which is what a dropped packet did to every low pass in
// relative mode, where a counter gap reserves NaN slots by construction. A gap in the input
// is a gap in the output (the hole stays visible), and the filter's history is the last
// *real* samples it saw, so the state is left alone and the trace resumes after the hole.
if (!std::isfinite(x)) return kNaN;
const double y = b0 * x + z1;
z1 = b1 * x - a1 * y + z2;
z2 = b2 * x - a2 * y;
+60
View File
@@ -170,6 +170,35 @@ ScopeModel::ScopeModel(const Config& cfg) : cfg_(cfg) {
trigger_.holdoff_s = cfg.trigger_holdoff_s;
}
namespace {
/// True when the incoming CONFIG names exactly the stream the model already has: same
/// signals, same shapes, rates, ranges and time modes. A reconnect re-sends the CONFIG,
/// and EMI makes that happen at the worst possible moment; the buffer, its history, the
/// holes, the trigger's held event and every measurement are all still valid and must
/// survive the reconnect untouched. Only a *different* stream is worth a re-plan, and a
/// re-plan is what drops history (see `rebuild_locked`).
bool same_stream_signals(const std::vector<SignalInfo>& incoming,
const std::vector<SignalInfo>& current) {
// `current` is the stream's signals with the math channels' synthetic entries appended,
// so compare the incoming list against current's prefix, skipping nothing on the left.
std::size_t n_stream = 0;
while (n_stream < current.size() && !current[n_stream].math) ++n_stream;
if (incoming.size() != n_stream) return false;
for (std::size_t i = 0; i < incoming.size(); ++i) {
const SignalInfo& a = incoming[i];
const SignalInfo& b = current[i];
if (a.name != b.name || a.unit != b.unit || a.type_code != b.type_code ||
a.quant_type != b.quant_type || a.rows != b.rows || a.cols != b.cols ||
a.range_min != b.range_min || a.range_max != b.range_max ||
a.time_mode != b.time_mode || a.sampling_rate != b.sampling_rate ||
a.time_signal_idx != b.time_signal_idx) {
return false;
}
}
return true;
}
} // namespace
void ScopeModel::set_signals(std::vector<SignalInfo> signals) {
std::lock_guard<std::mutex> lock(mutex_);
// Keep the enable state across CONFIG updates for signals that kept their name. The math
@@ -184,6 +213,14 @@ void ScopeModel::set_signals(std::vector<SignalInfo> signals) {
signals.erase(std::remove_if(signals.begin(), signals.end(),
[](const SignalInfo& s) { return s.math; }),
signals.end());
if (!signals_.empty() && same_stream_signals(signals, signals_)) {
// A reconnect's identical CONFIG: the honest recovery is *nothing* -- the ring keeps
// its history, the trigger keeps its state and its held event, and the outage shows
// as the gap in the data that it was.
std::fprintf(stderr, "[udpscope] CONFIG unchanged: keeping the buffer and its history\n");
std::fflush(stderr);
return;
}
signals_ = std::move(signals);
enabled_.assign(signals_.size(), false);
for (std::size_t i = 0; i < signals_.size(); ++i) {
@@ -257,6 +294,22 @@ void ScopeModel::prepare_math_locked() {
}
}
void ScopeModel::push_loaded_slot(const std::vector<f32>& values, f64 t, f64 abs_t) {
std::lock_guard<std::mutex> lock(mutex_);
if (!store_) return;
BufferStore& store = *store_;
const std::size_t channels = channels_.size();
std::vector<f32> slot(channels, 0.0f);
for (std::size_t i = 0; i < channels; ++i) {
const f32 v = i < values.size() ? values[i] : 0.0f;
store.values(i).push(v);
slot[i] = v;
}
store.time().push(t);
if (auto* abs = store.abs_time()) abs->push(abs_t);
push_math_sample(slot);
}
void ScopeModel::push_math_sample(const std::vector<f32>& sample) {
// The common case is no derived channels at all, and this runs once per *stored slot* -- so the
// early-out is what keeps a stream with no math from paying for the feature.
@@ -441,6 +494,13 @@ void ScopeModel::set_signal_enabled(std::size_t signal, bool on) {
generation_.fetch_add(1, std::memory_order_release);
}
std::pair<double, double> ScopeModel::calibration(std::size_t signal) {
std::lock_guard<std::mutex> lock(mutex_);
if (signal >= signals_.size()) return {1.0, 0.0};
const auto it = calibration_.find(signals_[signal].name);
return it != calibration_.end() ? it->second : std::pair<double, double>{1.0, 0.0};
}
void ScopeModel::set_calibration(std::size_t signal, double gain, double offset) {
std::lock_guard<std::mutex> lock(mutex_);
if (signal < signals_.size()) calibration_[signals_[signal].name] = {gain, offset};
+13
View File
@@ -280,6 +280,13 @@ class ScopeModel {
/// the streamed channels. `sample[c]` is channel `c`'s value for this slot; the math channels are
/// evaluated from those, in order, so a math channel may use another one.
void push_math_sample(const std::vector<f32>& sample);
/// Pushes one stored slot of a **file** load -- the values, the shared clock and the absolute
/// clock -- and then the derived channels, in the producer's own order and under the model's own
/// lock. The loader cannot simply hold the lock and push into `live()` itself: every setter it
/// needs (`set_signals`, `set_signal_enabled`, ...) takes that same non-recursive lock, so the
/// outer one deadlocks the first call.
void push_loaded_slot(const std::vector<f32>& values, f64 t, f64 abs_t);
/// What the derived channels cost the *producer*: every stored slot that went through
/// `push_math_sample()`, and the nanoseconds taken by the `math_timed()` of them that were
/// measured. The producer owns the budget between two DATA packets, so this cost is invisible in
@@ -343,6 +350,12 @@ class ScopeModel {
/// CONFIG, is not silently dropped.
void set_calibration(std::size_t signal, double gain, double offset);
/// The calibration that *will* be applied to that signal, as `{gain, offset}` -- the identity
/// `{1, 0}` for a signal nobody calibrated. The read counterpart of `set_calibration()`, and read
/// from the model's own record rather than from the channels: a signal that is not acquired has
/// no channel to read it from, and the record is what a dialog must show.
std::pair<double, double> calibration(std::size_t signal);
std::size_t channel_count() const noexcept { return channels_.size(); }
const Partition& partition() const noexcept { return part_; }
+21 -1
View File
@@ -70,6 +70,23 @@ constexpr plot::f32 kRowGap = 6.0f;
} // namespace
std::string layer_base_name(const std::string& name) {
const std::size_t a = name.rfind(" (");
if (a != std::string::npos && a + 3 < name.size() && name[a + 3] == ')' &&
name[a + 2] >= 'A' && name[a + 2] <= 'B') {
return name.substr(0, a);
}
const std::size_t h = name.rfind(" #");
if (h != std::string::npos && h + 2 < name.size()) {
bool digits = true;
for (std::size_t i = h + 2; i < name.size(); ++i) {
if (name[i] < '0' || name[i] > '9') digits = false;
}
if (digits) return name.substr(0, h);
}
return name;
}
plot::Color RowSet::palette_color(int index) {
const std::size_t i = static_cast<std::size_t>(index < 0 ? 0 : index) % kPaletteSize;
return plot::Color::hex(kPalette[i]);
@@ -78,7 +95,10 @@ plot::Color RowSet::palette_color(int index) {
plot::Color RowSet::unplotted_color() { return plot::Color::hex(kUnplottedColor); }
plot::Color RowSet::signal_color(const std::string& signal, int palette_index) const {
const auto it = signal_colors_.find(signal);
// The exact name first, then the signal's: a colour the user picked for `sine` is the colour of
// every layer of it, so a comparison's `sine (B)` is not a signal of its own colour.
auto it = signal_colors_.find(signal);
if (it == signal_colors_.end()) it = signal_colors_.find(layer_base_name(signal));
if (it != signal_colors_.end()) return plot::Color::hex(it->second);
return palette_color(palette_index);
}
+20 -1
View File
@@ -41,6 +41,12 @@ namespace udpscope {
/// count is fixed at construction, so there is one path for "a row's traces changed"
/// and "the row count changed", and it costs no GL state -- the area keeps its
/// context and is simply handed the new figure.
/// The signal a channel name belongs to, with a group's layer suffix taken back off: `sine (A)`,
/// `sine (B)` and `sine #2` are all the signal `sine`. A comparison's model holds one channel per
/// (signal, layer), so this is what lets the signal list show each signal once, draw every layer of it
/// in the signal's own colour, and stop a second group from re-suffixing a name a first one left.
std::string layer_base_name(const std::string& name);
class RowSet {
public:
/// The ceiling on rows, not the count: rows are added and removed one at a time
@@ -49,7 +55,12 @@ class RowSet {
/// allocated somewhere, and because the figure keeps a floor height per row -- past
/// about this many a row is chrome and no plot.
static constexpr int kMaxRows = 8;
static constexpr int kSlotsPerRow = 4;
/// Traces one row can hold. Eight rather than the four a hand-arranged scope usually wants, because
/// the *default* layout is four signals in one row and an accumulation needs a slot per extra event:
/// with four, "accumulate these two events" over the layout the app starts with had nowhere to put
/// the second copy and silently did nothing. The free slots cost nothing -- a `Plot` creates its
/// traces up front either way -- and the user's own traces stay where they are.
static constexpr int kSlotsPerRow = 8;
/// Readout marks per row per orientation: enough for a handful of level and time
/// measurements to be shown at once, and a fixed number because a `Plot` fixes its cursor
/// set at construction.
@@ -135,6 +146,13 @@ class RowSet {
};
int row_count() const noexcept { return rows_; }
/// The alpha every trace is drawn with. The *group* views use it: accumulating events wants their
/// traces translucent so that agreement between them reads as brightness, which is the whole point
/// of a persistence display. It is mixed into the palette colour where the trace is created, so it
/// belongs with `assign()` -- set it before the assignments that follow it, or they keep the alpha
/// the previous view left behind.
void set_trace_alpha(float alpha) noexcept { trace_alpha_ = alpha; }
/// Shows `rows` rows. A changed count rebuilds the figure, since a `Figure`'s row
/// count is fixed at construction.
void set_row_count(int rows);
@@ -478,6 +496,7 @@ class RowSet {
/// The one x link group every visible row belongs to. Groups are numbered from 1.
plot::u32 x_group_ = 0;
int rows_ = 1;
float trace_alpha_ = 1.0f;
double window_s_ = 2.0;
double trigger_marker_x_ = 0.0;
bool trigger_marker_on_ = false;
+424 -18
View File
@@ -3,11 +3,15 @@
#include "source_dialog.hpp"
#include "waveform.hpp"
#include "layout_store.hpp"
#include <adwaita.h>
#include <gtk/gtk.h>
#include <unistd.h>
#include <cstdio>
#include <cstdlib>
#include <string>
@@ -24,13 +28,22 @@ void apply_source(Config& cfg, const KnownSource& src) {
cfg.port = src.port;
cfg.multicast = src.multicast;
cfg.interface_addr = src.interface_addr;
// The configuration's own startup settings. They are read *before* the model exists, which is the
// only moment a buffer size can be applied at all -- and the reason they are stored per
// configuration rather than only passed on the command line.
if (src.buffer_mb > 0.0) cfg.buffer_mb = src.buffer_mb;
cfg.relative_time = src.relative_time;
cfg.capture_packets = src.capture_packets;
}
/// One line per known source on stderr, in the style of the probes in `ui.cpp`.
void report_sources(const std::vector<KnownSource>& known) {
std::fprintf(stderr, "[sources] %zu known:", known.size());
for (const KnownSource& s : known) {
std::fprintf(stderr, " %s:%u plots=%d signals=%d", s.host.c_str(), s.port, s.rows, s.signals);
std::fprintf(stderr, " %s:%u/%s plots=%d signals=%d buffer=%.6g relative=%d capture=%u",
s.host.c_str(), s.port, s.config.c_str(), s.rows, s.signals, s.buffer_mb,
s.relative_time ? 1 : 0, s.capture_packets);
if (!s.comment.empty()) std::fprintf(stderr, " note='%s'", s.comment.c_str());
if (!s.multicast.empty()) std::fprintf(stderr, " multicast=%s", s.multicast.c_str());
if (!s.interface_addr.empty()) {
std::fprintf(stderr, " interface=%s", s.interface_addr.c_str());
@@ -51,6 +64,12 @@ struct Chooser {
GtkWidget* window = nullptr;
GtkWidget* list = nullptr;
std::vector<KnownSource> known;
/// The Waveforms page: the exports opened before, newest first, and the notebook that holds the
/// two pages. A waveform is the *other* answer to "which source", so it gets a page of its own
/// rather than a button that hides a file dialog behind one more click.
GtkWidget* notebook = nullptr;
GtkWidget* wave_list = nullptr;
std::vector<std::string> waves;
/// The connection form, created on demand. Members of the chooser rather than a heap struct:
/// the chooser outlives the form by construction, so there is nothing to free on a close path
/// -- and a dialog whose context is freed by a handler is how this project earned a use-after
@@ -61,10 +80,26 @@ struct Chooser {
GtkWidget* form_group = nullptr;
GtkWidget* form_iface = nullptr;
GtkWidget* form_connect = nullptr;
/// The configuration half of the form: what a source's *session* is called and the settings that
/// can only be applied before the model exists. A new host needs all of them, and so does an
/// existing one -- which is why the same form is used for both.
GtkWidget* form_name = nullptr;
GtkWidget* form_note = nullptr;
GtkWidget* form_buffer = nullptr;
GtkWidget* form_relative = nullptr;
/// The configuration the form is editing: empty for a new host. Copied rather than referenced,
/// because `chooser_populate()` rebuilds `known` under the form.
std::string edit_host;
std::uint16_t edit_port = 0;
std::string edit_config;
/// `UDPSCOPE_PROBE_PICK_MS`'s timeout, kept so a pick made before it fires can cancel it. The
/// probe clears it when it runs: `g_source_remove()` on an id whose source already returned
/// `G_SOURCE_REMOVE` is a GLib critical, not a no-op.
guint pick_source = 0;
/// What the *form* last wrote, for `report_pick()`: a save finishes the chooser and destroys the
/// list with it, so the row a report would name is gone by the time the probe reads it.
std::string saved_config;
std::string saved_note;
};
void chooser_finish(Chooser& c, bool chosen) {
@@ -88,15 +123,125 @@ void chooser_finish(Chooser& c, bool chosen) {
}
void chooser_populate(Chooser& c);
void chooser_populate_waves(Chooser& c);
void report_pick(const Chooser& c);
/// Defined below; the row buttons reach it, and the row builder sits above it.
void open_form(Chooser& c, int edit_index);
void on_row_activated(GtkListBox*, GtkListBoxRow* row, gpointer user) {
Chooser& c = *static_cast<Chooser*>(user);
const int idx = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(row), "source-index"));
if (idx < 0 || static_cast<std::size_t>(idx) >= c.known.size()) return;
apply_source(*c.cfg, c.known[static_cast<std::size_t>(idx)]);
const KnownSource& src = c.known[static_cast<std::size_t>(idx)];
// Picking a configuration *is* choosing it as the active one: the app reads that back when it
// loads the session, and the next save goes to the same file. Without this, opening `bench`
// from the chooser would leave the source pointing at whatever was active before.
set_active_configuration(src.host, src.port, src.config);
apply_source(*c.cfg, src);
chooser_finish(c, true);
}
/// One row per waveform opened before, newest first. The name first (that is what a person reads),
/// the directory under it, and a missing file said out loud rather than left to fail on the click.
void chooser_populate_waves(Chooser& c) {
while (GtkWidget* child = gtk_widget_get_first_child(c.wave_list)) {
gtk_list_box_remove(GTK_LIST_BOX(c.wave_list), child);
}
c.waves = recent_waveforms();
if (c.waves.empty()) {
GtkWidget* empty = gtk_label_new("No waveform opened yet -- use Open\xe2\x80\xa6");
gtk_widget_add_css_class(empty, "dim-label");
gtk_widget_set_margin_top(empty, 24);
gtk_widget_set_margin_bottom(empty, 24);
gtk_list_box_append(GTK_LIST_BOX(c.wave_list), empty);
gtk_list_box_set_selection_mode(GTK_LIST_BOX(c.wave_list), GTK_SELECTION_NONE);
return;
}
gtk_list_box_set_selection_mode(GTK_LIST_BOX(c.wave_list), GTK_SELECTION_SINGLE);
for (std::size_t i = 0; i < c.waves.size(); ++i) {
const std::string& path = c.waves[i];
GtkWidget* row = gtk_list_box_row_new();
g_object_set_data(G_OBJECT(row), "wave-index", GINT_TO_POINTER(static_cast<int>(i)));
GtkWidget* line = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 12);
gtk_widget_set_margin_start(line, 12);
gtk_widget_set_margin_end(line, 6);
gtk_widget_set_margin_top(line, 6);
gtk_widget_set_margin_bottom(line, 6);
GtkWidget* text = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2);
gtk_widget_set_hexpand(text, TRUE);
const std::size_t slash = path.rfind('/');
const std::string base = slash == std::string::npos ? path : path.substr(slash + 1);
GtkWidget* title = gtk_label_new(base.c_str());
gtk_widget_set_halign(title, GTK_ALIGN_START);
gtk_box_append(GTK_BOX(text), title);
const bool present = ::access(path.c_str(), R_OK) == 0;
std::string sub = present ? path : path + " (missing)";
GtkWidget* under = gtk_label_new(sub.c_str());
gtk_widget_add_css_class(under, "dim-label");
gtk_widget_set_halign(under, GTK_ALIGN_START);
gtk_label_set_ellipsize(GTK_LABEL(under), PANGO_ELLIPSIZE_MIDDLE);
gtk_box_append(GTK_BOX(text), under);
gtk_box_append(GTK_BOX(line), text);
gtk_list_box_row_set_child(GTK_LIST_BOX_ROW(row), line);
gtk_list_box_append(GTK_LIST_BOX(c.wave_list), row);
}
}
void on_wave_activated(GtkListBox*, GtkListBoxRow* row, gpointer user) {
Chooser& c = *static_cast<Chooser*>(user);
if (row == nullptr) return;
const int idx = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(row), "wave-index"));
if (idx < 0 || static_cast<std::size_t>(idx) >= c.waves.size()) return;
const std::string& path = c.waves[static_cast<std::size_t>(idx)];
if (::access(path.c_str(), R_OK) != 0) {
std::fprintf(stderr, "[sources] waveform is gone: %s\n", path.c_str());
std::fflush(stderr);
return;
}
// The file *is* the source: the chooser's answer is a path, not a socket.
c.cfg->open_waveform = path;
report_pick(c);
chooser_finish(c, true);
}
void on_wave_open_done(GObject* source, GAsyncResult* result, gpointer user) {
Chooser& c = *static_cast<Chooser*>(user);
GFile* file = gtk_file_dialog_open_finish(GTK_FILE_DIALOG(source), result, nullptr);
if (file == nullptr) return; // cancelled
char* path = g_file_get_path(file);
g_object_unref(file);
if (path != nullptr) {
c.cfg->open_waveform = path;
g_free(path);
report_pick(c);
chooser_finish(c, true);
}
}
void on_wave_open_clicked(GtkButton*, gpointer user) {
Chooser& c = *static_cast<Chooser*>(user);
GtkFileDialog* dialog = gtk_file_dialog_new();
gtk_file_dialog_set_title(dialog, "Open an exported waveform");
GtkFileFilter* filter = gtk_file_filter_new();
gtk_file_filter_set_name(filter, "NumPy export (*.npy)");
gtk_file_filter_add_pattern(filter, "*.npy");
GListStore* filters = g_list_store_new(GTK_TYPE_FILE_FILTER);
g_list_store_append(filters, filter);
gtk_file_dialog_set_filters(dialog, G_LIST_MODEL(filters));
gtk_file_dialog_open(dialog, GTK_WINDOW(c.window), nullptr, on_wave_open_done, &c);
g_object_unref(filters);
g_object_unref(filter);
g_object_unref(dialog);
}
void on_edit_clicked(GtkButton* button, gpointer user) {
Chooser& c = *static_cast<Chooser*>(user);
const int idx = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(button), "source-index"));
if (idx < 0 || static_cast<std::size_t>(idx) >= c.known.size()) return;
open_form(c, idx);
}
void on_forget_clicked(GtkButton* button, gpointer user) {
Chooser& c = *static_cast<Chooser*>(user);
const int idx = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(button), "source-index"));
@@ -145,13 +290,29 @@ void chooser_populate(Chooser& c) {
// The *server*, even for a multicast session: that is the host the file is named after and
// the one CONFIG is fetched from. Titling the row by the group would name it after something
// that is not its identity.
const std::string title = s.host + ":" + std::to_string(s.port);
const std::string title = s.host + ":" + std::to_string(s.port) + " \u00b7 " + s.config;
GtkWidget* title_label = gtk_label_new(title.c_str());
gtk_widget_set_halign(title_label, GTK_ALIGN_START);
gtk_box_append(GTK_BOX(text), title_label);
// The note first when there is one: it is what tells two configurations of one source apart,
// and a list that shows only counts makes the user open them to find out.
if (!s.comment.empty()) {
GtkWidget* note = gtk_label_new(s.comment.c_str());
gtk_widget_set_halign(note, GTK_ALIGN_START);
gtk_label_set_ellipsize(GTK_LABEL(note), PANGO_ELLIPSIZE_END);
gtk_box_append(GTK_BOX(text), note);
}
std::string sub = std::to_string(s.rows) + (s.rows == 1 ? " plot, " : " plots, ") +
std::to_string(s.signals) + (s.signals == 1 ? " signal" : " signals");
if (s.buffer_mb > 0.0) {
sub += ", buffer " + std::to_string(static_cast<int>(s.buffer_mb)) + " MB";
}
if (s.relative_time) sub += ", relative time";
if (s.capture_packets > 0) {
sub += ", capture " + std::to_string(s.capture_packets) + " packets";
}
if (!s.multicast.empty()) sub += ", multicast " + s.multicast;
GtkWidget* sub_label = gtk_label_new(sub.c_str());
gtk_widget_add_css_class(sub_label, "dim-label");
@@ -159,8 +320,18 @@ void chooser_populate(Chooser& c) {
gtk_box_append(GTK_BOX(text), sub_label);
gtk_box_append(GTK_BOX(line), text);
GtkWidget* edit = gtk_button_new_from_icon_name("document-edit-symbolic");
gtk_widget_set_tooltip_text(edit,
"Edit this configuration: its name and note, the buffer size and "
"the clock, or the address it points at");
gtk_widget_add_css_class(edit, "flat");
gtk_widget_set_valign(edit, GTK_ALIGN_CENTER);
g_object_set_data(G_OBJECT(edit), "source-index", GINT_TO_POINTER(static_cast<int>(i)));
g_signal_connect(edit, "clicked", G_CALLBACK(on_edit_clicked), &c);
gtk_box_append(GTK_BOX(line), edit);
GtkWidget* forget = gtk_button_new_from_icon_name("window-close-symbolic");
gtk_widget_set_tooltip_text(forget, "Forget this source (deletes its saved session)");
gtk_widget_set_tooltip_text(forget, "Forget this configuration (deletes its saved session)");
gtk_widget_add_css_class(forget, "flat");
gtk_widget_set_valign(forget, GTK_ALIGN_CENTER);
g_object_set_data(G_OBJECT(forget), "source-index", GINT_TO_POINTER(static_cast<int>(i)));
@@ -193,11 +364,58 @@ void on_form_connect(GtkButton*, gpointer user) {
Chooser& c = *static_cast<Chooser*>(user);
const char* server = gtk_editable_get_text(GTK_EDITABLE(c.form_server));
if (server == nullptr || server[0] == '\0') return;
c.cfg->server = server;
c.cfg->port = static_cast<std::uint16_t>(
const std::string host = server;
const std::uint16_t port = static_cast<std::uint16_t>(
gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(c.form_port)));
c.cfg->multicast = gtk_editable_get_text(GTK_EDITABLE(c.form_group));
c.cfg->interface_addr = gtk_editable_get_text(GTK_EDITABLE(c.form_iface));
const std::string group = gtk_editable_get_text(GTK_EDITABLE(c.form_group));
const std::string iface = gtk_editable_get_text(GTK_EDITABLE(c.form_iface));
std::string name = gtk_editable_get_text(GTK_EDITABLE(c.form_name));
const std::string note = gtk_editable_get_text(GTK_EDITABLE(c.form_note));
const double buffer_mb = gtk_spin_button_get_value(GTK_SPIN_BUTTON(c.form_buffer));
const bool relative = gtk_switch_get_active(GTK_SWITCH(c.form_relative));
if (name.empty()) name = "default";
c.cfg->server = host;
c.cfg->port = port;
c.cfg->multicast = group;
c.cfg->interface_addr = iface;
// The startup settings. Applied here because this runs *before* `ScopeModel` exists, which is the
// only moment a buffer size can be applied at all -- and stored, so the next run gets them back.
c.cfg->buffer_mb = buffer_mb;
c.cfg->relative_time = relative;
LayoutState st;
if (!c.edit_config.empty()) {
// An edit keeps everything the configuration already holds -- plots, calibration, math,
// measurements -- and changes only what the form shows.
load_layout(configuration_path(c.edit_host, c.edit_port, c.edit_config), st);
}
st.comment = note;
st.buffer_mb = buffer_mb;
st.relative_time = relative;
st.multicast = group;
st.interface_addr = iface;
const bool editing = !c.edit_config.empty();
const bool moved =
editing && (c.edit_host != host || c.edit_port != port || c.edit_config != name);
if (save_layout(configuration_path(host, port, name), st)) {
set_active_configuration(host, port, name);
// A renamed configuration, or one re-addressed, is a *move*: the old file has no reason to exist
// and leaving it would list the same session twice.
if (moved) forget_configuration(c.edit_host, c.edit_port, c.edit_config);
c.saved_config = name;
c.saved_note = note;
std::fprintf(stderr,
"[sources] configuration %s: %s:%u/%s note='%s' buffer=%.6g relative=%d\n",
editing ? "saved" : "created", host.c_str(), port, name.c_str(), note.c_str(),
buffer_mb, relative ? 1 : 0);
std::fflush(stderr);
} else {
std::fprintf(stderr, "[sources] configuration write failed: %s:%u/%s\n", host.c_str(), port,
name.c_str());
std::fflush(stderr);
}
form_close(c);
chooser_finish(c, true);
}
@@ -212,13 +430,24 @@ GtkWidget* form_field(GtkWidget* grid, int row, const char* label, GtkWidget* wi
return widget;
}
void open_form(Chooser& c) {
/// Opens the connection form: on a new host, or on an existing configuration (`edit_index`). One
/// form for both, because the fields are the same and a configuration *is* a host plus its
/// settings -- the only difference is where the values start and what the button is called.
void open_form(Chooser& c, int edit_index = -1) {
if (c.form != nullptr) {
gtk_window_present(GTK_WINDOW(c.form));
return;
}
const KnownSource* src =
edit_index >= 0 && static_cast<std::size_t>(edit_index) < c.known.size()
? &c.known[static_cast<std::size_t>(edit_index)]
: nullptr;
c.edit_host = src != nullptr ? src->host : std::string();
c.edit_port = src != nullptr ? src->port : 0;
c.edit_config = src != nullptr ? src->config : std::string();
c.form = gtk_window_new();
gtk_window_set_title(GTK_WINDOW(c.form), "New host");
gtk_window_set_title(GTK_WINDOW(c.form), src != nullptr ? "Edit configuration" : "New host");
gtk_window_set_transient_for(GTK_WINDOW(c.form), GTK_WINDOW(c.window));
gtk_window_set_modal(GTK_WINDOW(c.form), TRUE);
@@ -235,15 +464,52 @@ void open_form(Chooser& c) {
gtk_box_append(GTK_BOX(box), grid);
c.form_server = form_field(grid, 0, "Server", gtk_entry_new());
gtk_editable_set_text(GTK_EDITABLE(c.form_server), "127.0.0.1");
gtk_editable_set_text(GTK_EDITABLE(c.form_server),
src != nullptr ? src->host.c_str() : "127.0.0.1");
// A spin button holds no decimals by default, which is right for a port and wrong for every
// other numeric field in this project -- see the math dialog's fields.
c.form_port = form_field(grid, 1, "Port", gtk_spin_button_new_with_range(1, 65535, 1));
gtk_spin_button_set_value(GTK_SPIN_BUTTON(c.form_port), 8000);
gtk_spin_button_set_value(GTK_SPIN_BUTTON(c.form_port), src != nullptr ? src->port : 8000);
c.form_group = form_field(grid, 2, "Multicast group", gtk_entry_new());
gtk_entry_set_placeholder_text(GTK_ENTRY(c.form_group), "empty = unicast");
gtk_editable_set_text(GTK_EDITABLE(c.form_group),
src != nullptr ? src->multicast.c_str() : "");
c.form_iface = form_field(grid, 3, "Interface", gtk_entry_new());
gtk_entry_set_placeholder_text(GTK_ENTRY(c.form_iface), "empty = default route");
gtk_editable_set_text(GTK_EDITABLE(c.form_iface),
src != nullptr ? src->interface_addr.c_str() : "");
// The session half: what this source's configuration is called, and the settings that have to be
// decided before the first socket is opened.
GtkWidget* heading = gtk_label_new(nullptr);
gtk_label_set_markup(GTK_LABEL(heading), "<b>Configuration</b>");
gtk_widget_set_halign(heading, GTK_ALIGN_START);
gtk_widget_set_margin_top(heading, 8);
gtk_grid_attach(GTK_GRID(grid), heading, 0, 4, 2, 1);
c.form_name = form_field(grid, 5, "Name", gtk_entry_new());
gtk_entry_set_placeholder_text(GTK_ENTRY(c.form_name), "default");
gtk_editable_set_text(GTK_EDITABLE(c.form_name),
src != nullptr ? src->config.c_str() : "default");
c.form_note = form_field(grid, 6, "Note", gtk_entry_new());
gtk_entry_set_placeholder_text(GTK_ENTRY(c.form_note), "what this configuration is for");
gtk_editable_set_text(GTK_EDITABLE(c.form_note),
src != nullptr ? src->comment.c_str() : "");
c.form_buffer = form_field(grid, 7, "Buffer MB", gtk_spin_button_new_with_range(1, 65536, 1));
gtk_widget_set_tooltip_text(c.form_buffer,
"sample buffer per source, applied when this source is opened");
gtk_spin_button_set_value(GTK_SPIN_BUTTON(c.form_buffer),
src != nullptr && src->buffer_mb > 0.0
? src->buffer_mb
: std::max(1.0, c.cfg->buffer_mb));
GtkWidget* relative = gtk_switch_new();
gtk_widget_set_halign(relative, GTK_ALIGN_START);
gtk_widget_set_tooltip_text(relative,
"plot time from the packet counter and the declared rate, instead "
"of the stream's own time signal (--relative-time)");
gtk_switch_set_active(GTK_SWITCH(relative),
src != nullptr ? src->relative_time : c.cfg->relative_time);
c.form_relative = form_field(grid, 8, "Relative time", relative);
GtkWidget* actions = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
gtk_widget_set_halign(actions, GTK_ALIGN_END);
@@ -252,7 +518,7 @@ void open_form(Chooser& c) {
form_close(*static_cast<Chooser*>(user));
}),
&c);
c.form_connect = gtk_button_new_with_label("Connect");
c.form_connect = gtk_button_new_with_label(src != nullptr ? "Save" : "Connect");
g_signal_connect(c.form_connect, "clicked", G_CALLBACK(on_form_connect), &c);
gtk_box_append(GTK_BOX(actions), cancel);
gtk_box_append(GTK_BOX(actions), c.form_connect);
@@ -274,8 +540,26 @@ void open_form(Chooser& c) {
/// What the pick probes report: the connection the chooser came back with. One helper rather than
/// the same four lines in each branch, since both probes assert against this exact line.
void report_pick(const Chooser& c) {
std::fprintf(stderr, "[sources] picked %s:%u multicast=%s interface=%s\n", c.cfg->server.c_str(),
c.cfg->port, c.cfg->multicast.c_str(), c.cfg->interface_addr.c_str());
if (!c.cfg->open_waveform.empty()) {
std::fprintf(stderr, "[sources] picked waveform %s\n", c.cfg->open_waveform.c_str());
std::fflush(stderr);
return;
}
// The settings the configuration carried, not just the address: a pick that dropped the buffer
// size would look identical here otherwise, and the buffer is the one of them a user notices
// only after a long capture has been sized wrong.
std::fprintf(stderr,
"[sources] picked %s:%u multicast=%s interface=%s buffer=%.6g relative=%d "
"capture=%u config=%s note='%s'\n",
c.cfg->server.c_str(), c.cfg->port, c.cfg->multicast.c_str(),
c.cfg->interface_addr.c_str(), c.cfg->buffer_mb, c.cfg->relative_time ? 1 : 0,
c.cfg->capture_packets,
(c.saved_config.empty() ? (c.known.empty() ? "-" : c.known.front().config)
: c.saved_config)
.c_str(),
(c.saved_config.empty() ? (c.known.empty() ? "-" : c.known.front().comment)
: c.saved_note)
.c_str());
std::fflush(stderr);
}
@@ -304,7 +588,28 @@ gboolean pick_probe(gpointer user) {
// -probe call would otherwise be a GLib critical on every run that let the probe fire.
c.pick_source = 0;
// `UDPSCOPE_PROBE_NEWHOST=host:port` drives the form instead of a row.
// `UDPSCOPE_PROBE_WAVEFORMS=1` is the waveforms page without a display: the recent list, and the
// page selected the way a click on its tab does. `UDPSCOPE_PROBE_WAVE=<path>` is the file dialog's
// answer without the dialog -- the same field, set the same way.
if (g_getenv("UDPSCOPE_PROBE_WAVEFORMS") != nullptr) {
if (c.notebook != nullptr) gtk_notebook_set_current_page(GTK_NOTEBOOK(c.notebook), 1);
std::fprintf(stderr, "[sources] waveforms: %zu recent", c.waves.size());
for (const std::string& p : c.waves) {
std::fprintf(stderr, " %s(%s)", p.c_str(), ::access(p.c_str(), R_OK) == 0 ? "ok" : "missing");
}
std::fprintf(stderr, " page=%d\n",
static_cast<int>(gtk_notebook_get_current_page(GTK_NOTEBOOK(c.notebook))));
std::fflush(stderr);
}
if (const char* path = g_getenv("UDPSCOPE_PROBE_WAVE")) {
c.cfg->open_waveform = path;
report_pick(c);
chooser_finish(c, true);
return G_SOURCE_REMOVE;
}
// `UDPSCOPE_PROBE_NEWHOST=host:port` drives the form instead of a row: the address, and the four
// things a host could not be given before -- a name, a note, a buffer size and a clock.
if (const char* spec = g_getenv("UDPSCOPE_PROBE_NEWHOST")) {
open_form(c);
const std::string s = spec;
@@ -314,13 +619,63 @@ gboolean pick_probe(gpointer user) {
if (colon != std::string::npos) {
gtk_spin_button_set_value(GTK_SPIN_BUTTON(c.form_port), std::atof(s.c_str() + colon + 1));
}
std::fprintf(stderr, "[sources] form open, connect_sensitive=%d\n",
gtk_editable_set_text(GTK_EDITABLE(c.form_name), "probe-host");
gtk_editable_set_text(GTK_EDITABLE(c.form_note), "made by the new-host probe");
gtk_spin_button_set_value(GTK_SPIN_BUTTON(c.form_buffer), 32);
gtk_switch_set_active(GTK_SWITCH(c.form_relative), TRUE);
std::fprintf(stderr,
"[sources] form: name='%s' note='%s' buffer=%.6g relative=%d "
"connect_sensitive=%d\n",
gtk_editable_get_text(GTK_EDITABLE(c.form_name)),
gtk_editable_get_text(GTK_EDITABLE(c.form_note)),
gtk_spin_button_get_value(GTK_SPIN_BUTTON(c.form_buffer)),
gtk_switch_get_active(GTK_SWITCH(c.form_relative)) ? 1 : 0,
gtk_widget_get_sensitive(c.form_connect) ? 1 : 0);
g_signal_emit_by_name(c.form_connect, "clicked");
report_pick(c);
return G_SOURCE_REMOVE;
}
// `UDPSCOPE_PROBE_EDITHOST=host:port` opens the existing configuration's form, changes its
// settings and saves: the path a user reaches by clicking the row's pencil.
if (const char* spec = g_getenv("UDPSCOPE_PROBE_EDITHOST")) {
const std::string want = spec;
int idx = -1;
for (std::size_t i = 0; i < c.known.size(); ++i) {
if (c.known[i].host + ":" + std::to_string(c.known[i].port) == want) {
idx = static_cast<int>(i);
break;
}
}
if (idx < 0) {
std::fprintf(stderr, "[sources] edit: no configuration for '%s'\n", want.c_str());
std::fflush(stderr);
chooser_finish(c, false);
return G_SOURCE_REMOVE;
}
open_form(c, idx);
// The reading is the *pre-fill*: an edit form that opens empty is the defect this asserts
// against, and the button's label says which mode it is in.
std::fprintf(stderr,
"[sources] edit form: name='%s' note='%s' buffer=%.6g relative=%d button='%s'\n",
gtk_editable_get_text(GTK_EDITABLE(c.form_name)),
gtk_editable_get_text(GTK_EDITABLE(c.form_note)),
gtk_spin_button_get_value(GTK_SPIN_BUTTON(c.form_buffer)),
gtk_switch_get_active(GTK_SWITCH(c.form_relative)) ? 1 : 0,
gtk_button_get_label(GTK_BUTTON(c.form_connect)));
gtk_editable_set_text(GTK_EDITABLE(c.form_note), "edited by the probe");
gtk_spin_button_set_value(GTK_SPIN_BUTTON(c.form_buffer), 16);
gtk_switch_set_active(GTK_SWITCH(c.form_relative), FALSE);
// `UDPSCOPE_PROBE_EDITNAME=new-name` also renames it, which is a *move*: the old file must not
// survive, or the chooser lists the same session twice.
if (const char* rename_to = g_getenv("UDPSCOPE_PROBE_EDITNAME")) {
gtk_editable_set_text(GTK_EDITABLE(c.form_name), rename_to);
}
g_signal_emit_by_name(c.form_connect, "clicked");
report_pick(c);
return G_SOURCE_REMOVE;
}
// Check the source list rather than the row count: `gtk_list_box_append` auto-wraps any
// non-row widget in a `GtkListBoxRow`, so `gtk_list_box_get_row_at_index` returns non-null
// even when the only list child is the empty-state placeholder label.
@@ -341,6 +696,27 @@ gboolean pick_probe(gpointer user) {
} // namespace
void apply_stored_settings(Config& cfg) {
const std::string want = active_configuration(cfg.server, cfg.port);
if (want.empty()) return;
LayoutState st;
if (!load_layout(configuration_path(cfg.server, cfg.port, want), st)) return;
// Only where the command line was silent: a flag the user typed wins, because it is the more
// recent statement of intent -- and the file is not a place to hide a surprise from the ones who
// did type it.
if (!cfg.buffer_mb_specified && st.buffer_mb > 0.0) cfg.buffer_mb = st.buffer_mb;
if (st.relative_time) cfg.relative_time = true;
if (cfg.capture_packets == 0 && st.capture_packets > 0) cfg.capture_packets = st.capture_packets;
const bool applied = (!cfg.buffer_mb_specified && st.buffer_mb > 0.0) || st.relative_time ||
st.capture_packets > 0;
if (applied) {
std::fprintf(stderr,
"[sources] settings from configuration '%s': buffer=%.6g relative=%d capture=%u\n",
want.c_str(), cfg.buffer_mb, cfg.relative_time ? 1 : 0, cfg.capture_packets);
std::fflush(stderr);
}
}
bool choose_source(Config& cfg) {
const std::vector<KnownSource> known = known_sources();
@@ -366,6 +742,13 @@ bool choose_source(Config& cfg) {
GtkWidget* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_window_set_child(GTK_WINDOW(c.window), box);
// Two pages under one window: the sources it can connect to, and the waveforms it can open. A
// waveform is not a source with a file in place of an address -- there is no socket and nothing
// arrives -- so it is a *page* rather than a field, which is also where its history belongs.
c.notebook = gtk_notebook_new();
gtk_widget_set_vexpand(c.notebook, TRUE);
gtk_box_append(GTK_BOX(box), c.notebook);
GtkWidget* scroller = gtk_scrolled_window_new();
gtk_widget_set_vexpand(scroller, TRUE);
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroller), GTK_POLICY_NEVER,
@@ -374,7 +757,30 @@ bool choose_source(Config& cfg) {
gtk_list_box_set_activate_on_single_click(GTK_LIST_BOX(c.list), TRUE);
g_signal_connect(c.list, "row-activated", G_CALLBACK(on_row_activated), &c);
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroller), c.list);
gtk_box_append(GTK_BOX(box), scroller);
gtk_notebook_append_page(GTK_NOTEBOOK(c.notebook), scroller, gtk_label_new("Sources"));
GtkWidget* wave_page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
GtkWidget* wave_scroller = gtk_scrolled_window_new();
gtk_widget_set_vexpand(wave_scroller, TRUE);
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(wave_scroller), GTK_POLICY_NEVER,
GTK_POLICY_AUTOMATIC);
c.wave_list = gtk_list_box_new();
gtk_list_box_set_activate_on_single_click(GTK_LIST_BOX(c.wave_list), TRUE);
g_signal_connect(c.wave_list, "row-activated", G_CALLBACK(on_wave_activated), &c);
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(wave_scroller), c.wave_list);
gtk_box_append(GTK_BOX(wave_page), wave_scroller);
GtkWidget* wave_actions = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
gtk_widget_set_halign(wave_actions, GTK_ALIGN_END);
gtk_widget_set_margin_top(wave_actions, 6);
gtk_widget_set_margin_bottom(wave_actions, 6);
gtk_widget_set_margin_end(wave_actions, 12);
GtkWidget* wave_open = gtk_button_new_with_label("Open\xe2\x80\xa6");
gtk_widget_set_tooltip_text(wave_open, "choose an exported .npy to view and analyse");
g_signal_connect(wave_open, "clicked", G_CALLBACK(on_wave_open_clicked), &c);
gtk_box_append(GTK_BOX(wave_actions), wave_open);
gtk_box_append(GTK_BOX(wave_page), wave_actions);
gtk_notebook_append_page(GTK_NOTEBOOK(c.notebook), wave_page, gtk_label_new("Waveforms"));
chooser_populate_waves(c);
GtkWidget* actions = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
gtk_widget_set_margin_start(actions, 12);
+10
View File
@@ -16,4 +16,14 @@ namespace udpscope {
/// same process, which works because `gtk_init()` is idempotent.
bool choose_source(Config& cfg);
/// Fills in the settings a *stored* configuration carries for `cfg`'s source -- the buffer size,
/// relative time and the packet capture's ring size -- leaving alone anything the command line
/// actually asked for.
///
/// The chooser applies these when it opens a source; a source named with `--server` never reaches
/// the chooser, so without this a saved host's buffer size and clock were silently ignored and the
/// edit that set them read as one that never took effect. The layout is *not* applied here: it
/// names signals, so it is loaded by the app once a CONFIG has described them.
void apply_stored_settings(Config& cfg);
} // namespace udpscope
+767 -6440
View File
File diff suppressed because it is too large. Load diff
+1302
View File
File diff suppressed because it is too large. Load diff
+148
View File
@@ -0,0 +1,148 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 Martino Ferrari
//
// The cursors pane: what the two vertical markers and the per-row rulers read, in the four cursor\n// modes, rebuilt only when the rows or the mode change.
#include "ui_app.hpp"
#include <gtk/gtk.h>
#include <string>
namespace udpscope {
namespace ui_impl {
/// `%.6g s` is exact enough for a cursor readout and short enough for the panel.
void update_cursors_pane(App& app) {
if (app.cursors_body == nullptr || !app.rows->cursors_visible()) return;
if (app.cursors_pane_mode != static_cast<int>(app.rows->cursor_mode()) ||
app.cursors_pane_rows != app.rows->row_count() ||
app.cursors_pane_generation != app.model->generation() ||
app.cursors_pane_bound != bound_slot_mask(app)) {
rebuild_cursors_pane(app);
}
const double t0 = app.rows->cursor_a_x();
const double t1 = app.rows->cursor_b_x();
const auto put = [](GtkWidget* w, const std::string& text) {
if (w != nullptr) gtk_label_set_text(GTK_LABEL(w), text.c_str());
};
const RowSet::CursorMode mode = app.rows->cursor_mode();
const bool rulers = mode == RowSet::CursorMode::Horizontal || mode == RowSet::CursorMode::Both;
const bool crossings = mode == RowSet::CursorMode::Mix;
for (const App::CursorLine& line : app.cursors_lines) {
if (line.a == nullptr) continue;
// One measurement per cell, each with the unit its own axis declares: the pane is a
// readout beside a plot, where the values are the point.
std::string va;
std::string vb;
std::string vd;
if (line.slot == -2) {
va = format_measure(t0, "s");
vb = format_measure(t1, "s");
vd = format_measure(t1 - t0, "s");
} else if (line.slot == -1 && rulers) {
const std::string unit = row_unit(app, line.row);
const double y1 = app.rows->ruler_y(line.row);
const double y2 = app.rows->ruler_b_y(line.row);
va = format_measure(y1, unit);
vb = format_measure(y2, unit);
vd = format_measure(y2 - y1, unit);
} else if (crossings && line.slot >= 0) {
// Where each cursor crosses this trace: the value the trace has at that time,
// read the way a crosshair reads it.
// The same call the crossing markers use, on the same trace: the pane and the
// marks on the plot cannot disagree about what the value is.
const RowSet::Row& row = app.rows->row(line.row);
double y1 = 0.0;
double y2 = 0.0;
const bool a = row.plot != nullptr &&
row.plot->crosshair_value(static_cast<plot::u32>(line.slot), t0, y1);
const bool b = row.plot != nullptr &&
row.plot->crosshair_value(static_cast<plot::u32>(line.slot), t1, y2);
if (a && b) {
const std::string unit = slot_unit(app, line.row, line.slot);
va = format_measure(y1, unit);
vb = format_measure(y2, unit);
vd = format_measure(y2 - y1, unit);
} else {
va = vb = vd = a || b ? "partial" : "--";
}
} else {
va = vb = vd = "--";
}
put(line.a, va);
put(line.b, vb);
put(line.d, vd);
}
}
GtkWidget* build_cursors_panel(App& app) {
GtkWidget* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
gtk_widget_set_margin_top(box, 6);
gtk_widget_set_margin_bottom(box, 6);
gtk_widget_set_margin_start(box, 6);
gtk_widget_set_margin_end(box, 6);
GtkWidget* head = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
GtkWidget* title = gtk_label_new("Cursors");
gtk_widget_set_halign(title, GTK_ALIGN_START);
gtk_widget_set_hexpand(title, true);
gtk_box_append(GTK_BOX(head), title);
const char* names[5] = {kCursorModeNames[0], kCursorModeNames[1], kCursorModeNames[2],
kCursorModeNames[3], nullptr};
app.cursors_mode_drop = gtk_drop_down_new_from_strings(names);
gtk_widget_set_tooltip_text(app.cursors_mode_drop,
"vertical: two times, shared by every row\n"
"horizontal: two rulers per row\n"
"both: the two together\n"
"mix: the two times, read off each trace where they cross");
g_signal_connect(app.cursors_mode_drop, "notify::selected",
G_CALLBACK(on_cursor_mode_changed), &app);
gtk_box_append(GTK_BOX(head), app.cursors_mode_drop);
// Reset lives here, beside the mode it resets, rather than in the overflow menu: it acts on this
// pane's instruments and is only visible while they are on, both of which are this pane's facts.
app.cursors_reset = gtk_button_new_from_icon_name("edit-clear-symbolic");
gtk_widget_add_css_class(app.cursors_reset, "flat");
gtk_widget_set_tooltip_text(app.cursors_reset, "re-place the cursors at the window's edges");
g_signal_connect(app.cursors_reset, "clicked", G_CALLBACK(on_cursors_reset), &app);
gtk_box_append(GTK_BOX(head), app.cursors_reset);
gtk_box_append(GTK_BOX(box), head);
app.cursors_body = gtk_grid_new();
gtk_grid_set_row_spacing(GTK_GRID(app.cursors_body), 2);
gtk_grid_set_column_spacing(GTK_GRID(app.cursors_body), 4);
gtk_box_append(GTK_BOX(box), app.cursors_body);
return box;
}
void on_cursors_toggled(GtkToggleButton* button, gpointer user) {
App* app = static_cast<App*>(user);
const bool on = gtk_toggle_button_get_active(button);
app->rows->set_cursors_visible(on);
// The reset button exists for the cursors: with them hidden there is nothing to
// reset, and a button that does nothing reads as broken.
gtk_widget_set_visible(app->cursors_reset, on);
// The pane is the cursors' readout: with them off there is nothing to read.
if (app->cursors_revealer != nullptr) {
gtk_revealer_set_reveal_child(GTK_REVEALER(app->cursors_revealer), on);
}
if (on) {
app->rows->reset_cursors();
rebuild_cursors_pane(*app);
update_cursors_pane(*app);
}
}
void on_cursors_reset(GtkButton*, gpointer user) {
App* app = static_cast<App*>(user);
// A reset the user cannot see reads as a dead button, so it also switches the
// cursors on.
if (!gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(app->cursors_toggle))) {
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(app->cursors_toggle), true);
}
app->rows->reset_cursors();
}
} // namespace ui_impl
} // namespace udpscope
+814
View File
@@ -0,0 +1,814 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 Martino Ferrari
//
// The event cache, seen from the front end: the pane that lists it, the picture each row carries, the\n// replay, and the two group layouts (side-by-side comparison and accumulation) with the axis mirror\n// that keeps a comparison several plots in step.
#include "ui_app.hpp"
#include <gtk/gtk.h>
#include <string>
namespace udpscope {
namespace ui_impl {
/// The comparison's plot: the same figure machinery, its own `RowSet`, and deliberately *none* of the
/// primary area's gestures -- the drop target, the right-click menu and the frame counter all act on
/// the primary's rows, and a menu on the second plot that rearranged the first plot's rows would be a
/// menu lying about its subject. Hidden until a comparison is up: a hidden child of a paned takes no
/// room and leaves no handle.
GtkWidget* make_compare_area(App& app) {
app.rows_b = std::make_unique<RowSet>();
plot::gtk4::PlotArea* area = plot::gtk4::PlotArea::create(app.placeholder_plot_b);
app.area_b = area;
area->set_figure(&app.rows_b->figure());
GtkWidget* widget = area->widget();
app.area_b_widget = widget;
gtk_widget_set_hexpand(widget, true);
gtk_widget_set_vexpand(widget, true);
gtk_widget_set_size_request(widget, 320, 120);
gtk_widget_set_visible(widget, FALSE);
return widget;
}
/// Whether the event history is reachable at all, and the one place that decides it. It has exactly
/// two preconditions: trigger mode (there are no events without a trigger) and recording (a disabled
/// cache is empty, and an empty column beside the plot is what "off" must not look like). Both the
/// toolbar button and the pane follow from it, so they cannot disagree -- and turning recording off
/// while the pane is open takes the button and the pane with it rather than leaving a stale list.
void sync_events_availability(App& app) {
if (app.events_toggle == nullptr) return;
// What the divider was last dragged to. Read here, on every change of state, because the close that
// follows is about to set the position to 0 and lose it.
if (app.events_divider != nullptr) {
const int at = gtk_paned_get_position(GTK_PANED(app.events_divider));
if (at > 1) app.events_pane_width = at;
}
// Available while the cache can still grow (recording on) *or* while it is being read (an event on
// screen). Not trigger mode: a stored event being examined is a file, and it has no trigger -- if the
// pane's life depended on the trigger, opening one would close the list it was opened from.
const bool available = app.event_recording || app.replaying;
gtk_widget_set_visible(app.events_toggle, available);
if (!available) {
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(app.events_toggle), FALSE);
gtk_revealer_set_reveal_child(GTK_REVEALER(app.events_revealer), FALSE);
}
const bool open =
available && gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(app.events_toggle));
// Visibility, not only `reveal_child`: an unrevealed `GtkRevealer` *measures* 0x0 and its box still
// hands it the leftover width, so the collapsed pane reserved an empty column beside the plot.
gtk_widget_set_visible(app.events_revealer, open);
// The position is what gives the pane its room (or none): a paned hands its start child at least its
// minimum unless the position says otherwise.
if (app.events_divider != nullptr) {
gtk_paned_set_position(GTK_PANED(app.events_divider), open ? app.events_pane_width : 0);
}
}
/// Restates the divider's position, once a frame.
///
/// `gtk_paned_set_position()` is clamped against the allocation the paned has *now*, and a click on
/// the history button flips the toggle in the same instant the pane becomes visible -- so the position
/// is clamped against a child that still measures zero, the pane opens with no width, and only the
/// next call (a replay's, seconds later) gives it one. Restating it per frame is idempotent, and a
/// position the user dragged is *learned* rather than fought: the width is read back whenever the pane
/// is open and the position is not the one we would set.
void sync_events_divider(App& app) {
if (app.events_divider == nullptr) return;
const bool open =
app.events_revealer != nullptr && gtk_widget_get_visible(app.events_revealer);
const int at = gtk_paned_get_position(GTK_PANED(app.events_divider));
if (!open) {
if (at != 0) gtk_paned_set_position(GTK_PANED(app.events_divider), 0);
return;
}
if (at > 1 && at != app.events_pane_width) {
app.events_pane_width = at; // the user dragged it: that is the width now
return;
}
if (at != app.events_pane_width) {
gtk_paned_set_position(GTK_PANED(app.events_divider), app.events_pane_width);
}
}
/// Writes the held event into the cache: its table, a sidecar of what the list shows, and a
/// thumbnail of the plot area. One path for the tick's latch and for the probe, so what the probe
/// asserts is what a latch does.
bool record_event(App& app) {
if (!app.event_recording || app.event_store == nullptr || app.event_store->empty_dir()) {
return false;
}
// By value: a `shared_ptr` to the snapshot, so a latch that lands while this writes cannot pull the
// data out from under it.
const std::shared_ptr<const EventSnapshot> snap = app.model->event();
if (snap == nullptr) return false;
const ExportTable table = event_table(*app.model, *snap);
if (table.empty()) return false;
EventRecord meta;
meta.wall_time = static_cast<double>(g_get_real_time()) / 1.0e6;
meta.pre_s = snap->pre_s;
meta.post_s = snap->post_s;
meta.duration_s = snap->pre_s + snap->post_s;
meta.edge = snap->x_offset;
meta.rate_hz = app.model->partition().stored_rate();
meta.signals = table.columns.size();
meta.samples = table.rows();
meta.seq = app.model->event_seq();
const std::string file = app.event_store->save(table, meta);
if (file.empty()) return false;
// The event is on screen; say it was kept, and where. A notice rather than a dialog: a latch every
// 200 ms must not be a wall of windows.
set_notice(app, "event cached: " + std::to_string(table.columns.size()) + " signals, " +
std::to_string(table.rows()) + " samples",
3);
// Only while the pane is on screen: rebuilding every row per latch for a column nobody opened is
// work the tick owes nothing, and the toggle's own handler refreshes when it is asked for.
if (app.events_revealer != nullptr &&
gtk_revealer_get_child_revealed(GTK_REVEALER(app.events_revealer))) {
refresh_events_pane(app);
}
return true;
}
/// Replays a cached event: the same loader `--open` uses, on the event's file. The acquisition is
/// stopped first, because a live producer overwrites the loaded samples within a frame -- so the
/// scope becomes a file viewer until *Go live* starts it again.
bool replay_event(App& app, const EventRecord& record) {
if (app.acq == nullptr || app.model == nullptr) return false;
app.acq->stop();
WaveformLoadReport report;
std::string error;
if (!load_waveform_into_model(*app.model, record.file, &report, &error)) {
set_notice(app, "cannot replay: " + error, 5);
return false;
}
app.replaying = true;
app.waveform_name = record.file;
// One event is not a group: replaying replaces whatever layout was up, alpha and all.
app.group_kind = EventGroupKind::None;
app.group.clear();
app.comparing = false;
if (app.area_b_widget != nullptr) gtk_widget_set_visible(app.area_b_widget, FALSE);
app.sync_last_a.clear();
app.sync_last_b.clear();
app.rows->set_trace_alpha(1.0f);
app.layout_loaded = true; // the live session's layout must not fight the event's own binding
// The trigger belongs to the *stream*: a file has no edge to wait for. Leaving it on kept the frozen
// capture on screen instead of the file -- `display_mode()` stays `Held` while the model holds a
// snapshot, and a held display draws *the snapshot*, so the click looked like it did nothing at all
// while the event sat in the model unread.
if (app.trigger_toggle != nullptr &&
gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(app.trigger_toggle))) {
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(app.trigger_toggle), FALSE);
}
// The *data* changes; the layout does not. What a row plots is a signal *name*, and an event's export
// carries the same names the stream does, so the rows, their slots, the colours and the row count all
// stay where the user put them and simply read the stored event. Rebinding the rows here (what a
// freshly opened *file* needs) would rearrange the user's work under a click on a list.
app.rows->fit_all();
rebuild_signal_panels(app);
// Fit on *open*, and only then: the whole event framed the moment it is opened, the user's own zooms
// left alone afterwards -- restated here and never per frame.
if (report.span_s > 0.0) apply_time_window(app, report.span_s);
// The pane is available through a replay, and this is what keeps it open.
sync_events_availability(app);
set_notice(app, "replaying " + record.stem + " (" +
std::to_string(record.signals) + " signals, " +
std::to_string(record.samples) + " samples)", 5);
return true;
}
/// Back to the live stream after a replay: a fresh client and thread, and the replayed samples age
/// out of the ring as the stream refills it.
bool go_live_again(App& app) {
if (!app.replaying || app.acq == nullptr) return false;
app.replaying = false;
app.waveform_name.clear();
app.layout_loaded = false; // the session's layout applies to the source again
app.event_held = false;
app.model->set_display_epoch(0.0);
try {
app.acq->start();
} catch (const std::exception& e) {
set_notice(app, std::string("cannot go live: ") + e.what(), 6);
return false;
}
// Leaving a group is leaving a file view: the second plot goes, the traces become opaque, and the
// acquisition's return brings the signal list's acquire ticks back with it.
app.group_kind = EventGroupKind::None;
app.group.clear();
app.comparing = false;
if (app.area_b_widget != nullptr) gtk_widget_set_visible(app.area_b_widget, FALSE);
app.sync_last_a.clear();
app.sync_last_b.clear();
app.rows->set_trace_alpha(1.0f);
rebuild_signal_panels(app);
set_notice(app, "live", 3);
return true;
}
bool read_event_samples(const std::string& path, EventSamples* out, std::string* error) {
WaveformReader reader;
if (!reader.open(path, error)) return false;
if (reader.names().empty() || reader.rows() == 0) {
if (error != nullptr) *error = "'" + path + "' holds no samples";
return false;
}
out->names = reader.names();
out->columns.assign(out->names.size(), {});
for (std::vector<f32>& column : out->columns) column.reserve(reader.rows());
std::vector<f32> values(out->names.size(), 0.0f);
double t = 0.0;
double abs_t = 0.0;
while (reader.next(&t, &abs_t, values.data())) {
out->times.push_back(t);
for (std::size_t i = 0; i < values.size(); ++i) out->columns[i].push_back(values[i]);
}
reader.close();
if (out->times.empty()) {
if (error != nullptr) *error = "'" + path + "' could not be read to the end";
return false;
}
return true;
}
/// The events the list has selected, in list order (newest first).
std::vector<EventRecord> selected_events(App& app) {
std::vector<EventRecord> out;
if (app.events_list == nullptr) return out;
GList* rows = gtk_list_box_get_selected_rows(GTK_LIST_BOX(app.events_list));
for (GList* it = rows; it != nullptr; it = it->next) {
auto* record =
static_cast<EventRecord*>(g_object_get_data(G_OBJECT(it->data), "udpscope-event"));
if (record != nullptr) out.push_back(*record);
}
g_list_free(rows);
return out;
}
/// What the selection can do, as the buttons read it: exactly two events to compare, between two and
/// `kSlotsPerRow` to accumulate -- a row holds eight traces, so a ninth event has nowhere to go.
void update_group_buttons(App& app) {
// The window is gone: a teardown path clears it *before* the widgets go, and the list box outlives
// the buttons inside it -- so a `selected-rows-changed` while they are being finalized arrives here
// with freed pointers. This is the app's own "the widgets are going" flag.
if (app.window == nullptr) return;
const std::size_t selected = selected_events(app).size();
if (app.events_compare != nullptr) {
gtk_widget_set_sensitive(app.events_compare, selected == 2);
}
if (app.events_accumulate != nullptr) {
gtk_widget_set_sensitive(app.events_accumulate,
selected >= 2 && selected <= RowSet::kSlotsPerRow);
}
}
/// Puts `records` in one scope as a group.
///
/// The model is built *once*, from all of them: one channel per (signal, layer) pair, pushed in a
/// single pass, because the buffers below cannot hold two independent loads. A *comparison* is then two
/// plots over that model -- which plot draws which layer is only a matter of which channel *names* its
/// rows bind -- and an *accumulation* is one plot with the extra layers in the free slots of the row
/// that already holds their signal. The user's rows, slots and colours are never re-derived.
///
/// Both layouts stand on the events having the same signals and the same length; the check is here,
/// once, rather than in either layout.
bool build_event_group(App& app, const std::vector<EventRecord>& records, EventGroupKind kind,
std::string* error) {
const auto fail = [&](const std::string& why) {
if (error != nullptr) *error = why;
return false;
};
if (app.acq == nullptr || app.model == nullptr || app.rows == nullptr) return fail("no scope yet");
if (records.size() < 2) return fail("select at least two events");
const bool pair = kind == EventGroupKind::Compare;
if (pair && records.size() != 2) return fail("two events, no more");
if (!pair && records.size() > RowSet::kSlotsPerRow) {
return fail("at most " + std::to_string(RowSet::kSlotsPerRow) +
" events: the rows have no room for another layer");
}
if (pair && app.rows_b == nullptr) return fail("the second plot is not available");
// The acquisition first: a live producer keeps pushing into the model it was started with -- and it
// pushes *four* stream signals into the group's eight channels, so the group's own samples are the
// ones that go missing.
app.acq->stop();
std::vector<EventSamples> events(records.size());
for (std::size_t i = 0; i < records.size(); ++i) {
if (!read_event_samples(records[i].file, &events[i], error)) return false;
}
const std::size_t cols = events[0].names.size();
const std::size_t count = events[0].times.size();
for (std::size_t i = 1; i < events.size(); ++i) {
if (events[i].names != events[0].names) return fail("the events carry different signals");
if (events[i].times.size() != count) return fail("the events are not the same length");
}
const double span = events[0].times.back() - events[0].times.front();
if (!(span > 0.0)) return fail("the events have no duration");
if (pair) {
// The same sample *count* is not enough for the halves to line up: the same recording rate is what
// makes them the same duration. The tolerance is a few sample intervals, because two events this
// app recorded itself differ by the fraction of a sample the acquisition's window filled to.
const double dt = span / double(count > 1 ? count - 1 : 1);
const double other = events[1].times.back() - events[1].times.front();
if (std::abs(other - span) > std::max(4.0 * dt, 1.0e-3)) {
return fail("the events are not the same duration");
}
}
const std::size_t layers = pair ? 2u : events.size();
const auto suffix = [&](std::size_t layer) -> std::string {
if (pair) return layer == 0 ? " (A)" : " (B)";
return layer == 0 ? std::string() : " #" + std::to_string(layer + 1);
};
// A slot's name carries the previous group's suffix; the shared rule takes it back off, so a second
// group over the same slots maps to the same channels instead of to names nothing is called.
const auto base_name = [](const std::string& slot) { return layer_base_name(slot); };
const double rate = count > 1 ? double(count - 1) / span : 0.0;
std::vector<SignalInfo> signals;
signals.reserve(cols * layers);
for (std::size_t c = 0; c < cols; ++c) {
for (std::size_t k = 0; k < layers; ++k) {
SignalInfo sig;
sig.name = events[0].names[c] + suffix(k);
sig.sampling_rate = rate > 0.0 ? rate : 1.0;
signals.push_back(std::move(sig));
}
}
// The stamps: every event's own, merged and sorted. They are the same grid when the events came from
// the same acquisition setup -- each event's `time` column is edge-relative -- and where they differ
// an event simply has no sample at the other's instants and contributes a gap.
std::vector<f64> stamps;
stamps.reserve(count * events.size());
for (const EventSamples& e : events) stamps.insert(stamps.end(), e.times.begin(), e.times.end());
std::sort(stamps.begin(), stamps.end());
stamps.erase(std::unique(stamps.begin(), stamps.end()), stamps.end());
app.model->set_target_span(span);
app.model->set_signals(std::move(signals));
for (std::size_t i = 0; i < cols * layers; ++i) app.model->set_signal_enabled(i, true);
const f32 gap = std::numeric_limits<f32>::quiet_NaN();
std::vector<f32> values(cols * layers, 0.0f);
std::vector<std::size_t> cursor(events.size(), 0);
for (f64 t : stamps) {
for (std::size_t k = 0; k < layers; ++k) {
const EventSamples& e = events[k];
while (cursor[k] < e.times.size() && e.times[cursor[k]] < t) ++cursor[k];
const bool here = cursor[k] < e.times.size() && e.times[cursor[k]] == t;
for (std::size_t c = 0; c < cols; ++c) {
values[c * layers + k] = here ? e.columns[c][cursor[k]] : gap;
}
}
app.model->push_loaded_slot(values, t, t);
}
app.model->set_epoch_if_unset(events[0].times.front());
app.model->set_display_epoch(events[0].times.back());
// --- the layout: the user's, mirrored, never reinvented --------------------
const int rows = app.rows->row_count();
if (pair) {
app.rows_b->set_row_count(rows);
if (app.area_b != nullptr) app.area_b->set_figure(&app.rows_b->figure());
}
app.rows->set_trace_alpha(kind == EventGroupKind::Accumulate ? 0.35f : 1.0f);
if (app.rows_b != nullptr) app.rows_b->set_trace_alpha(1.0f);
for (int r = 0; r < rows; ++r) {
RowSet::Row& row = app.rows->row(r);
const std::array<std::string, RowSet::kSlotsPerRow> names = row.signal_name;
const std::array<std::string, RowSet::kSlotsPerRow> labels = row.name;
const std::array<int, RowSet::kSlotsPerRow> colours = row.color_index;
for (int s = 0; s < RowSet::kSlotsPerRow; ++s) {
const std::size_t at = static_cast<std::size_t>(s);
if (names[at].empty()) continue;
const std::string base = base_name(names[at]);
app.rows->assign(r, s, base + suffix(0), labels[at], colours[at]);
if (pair) {
// The *same* colour index: the two plots show the same signal of two different events, and a
// colour of its own for the second would say they were different signals.
app.rows_b->assign(r, s, base + suffix(1), labels[at] + suffix(1), colours[at]);
continue;
}
for (std::size_t layer = 1; layer < layers; ++layer) {
int found_row = -1;
int found_slot = -1;
for (int pass = 0; pass < 2 && found_row < 0; ++pass) {
const int from = pass == 0 ? r : 0;
const int to = pass == 0 ? r + 1 : rows;
for (int rr = from; rr < to && found_row < 0; ++rr) {
for (int ss = 0; ss < RowSet::kSlotsPerRow; ++ss) {
if (!app.rows->row(rr).signal_name[static_cast<std::size_t>(ss)].empty()) continue;
found_row = rr;
found_slot = ss;
break;
}
}
}
if (found_row < 0) break; // the layout is full: no room for another layer
app.rows->assign(found_row, found_slot, base + suffix(layer), labels[at] + suffix(layer),
colours[at]);
}
}
}
app.rows->fit_all();
if (pair) app.rows_b->fit_all();
app.replaying = true; // a file view: no stream behind it, and no starvation notice
app.waveform_name = pair ? "compare " + records[0].stem + " vs " + records[1].stem
: "accumulate " + records[0].stem + " +" +
std::to_string(records.size() - 1);
app.group_kind = kind;
app.group = records;
app.comparing = pair;
if (app.area_b_widget != nullptr) gtk_widget_set_visible(app.area_b_widget, pair);
app.sync_last_a.clear();
app.sync_last_b.clear();
if (app.trigger_toggle != nullptr &&
gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(app.trigger_toggle))) {
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(app.trigger_toggle), FALSE);
}
rebuild_layout_editor(app);
rebuild_signal_panels(app);
// Fit on open, once: the group's own span, framed.
apply_time_window(app, span);
sync_events_availability(app);
return true;
}
/// One frame's mirror between the two comparison plots.
///
/// The plotter owns its window, and a gesture reaches it through the plot's own setter with nothing
/// telling us it happened. So each frame reads the ranges and compares them with the previous frame's:
/// whichever side moved is the side the user touched, and the other is set to match it. Copying
/// unconditionally would work only until the first gesture, which the copy would undo; copying nothing
/// would let the two plots drift apart, which is the second plot's whole point.
///
/// What is compared is the *span* of an axis with a live edge, not its edges: under the live follow the
/// window is recomputed every frame as a distance behind the newest sample, so both plots' edges move
/// every frame and would read as "both sides moved" -- and then a zoom in one plot would be overwritten
/// by the other's ever-moving window.
void sync_compare_axes(App& app) {
if (!app.comparing || app.rows_b == nullptr) return;
const int rows = std::min(app.rows->row_count(), app.rows_b->row_count());
if (static_cast<int>(app.sync_last_a.size()) != rows) {
app.sync_last_a.assign(static_cast<std::size_t>(rows), {});
app.sync_last_b.assign(static_cast<std::size_t>(rows), {});
}
for (int k = 0; k < rows; ++k) {
RowSet::Row& ra = app.rows->row(k);
RowSet::Row& rb = app.rows_b->row(k);
if (ra.plot == nullptr || rb.plot == nullptr) continue;
const plot::Range ax = ra.plot->x_range(ra.axis_x);
const plot::Range ay = ra.plot->y_range(ra.axis_y);
const plot::Range bx = rb.plot->x_range(rb.axis_x);
const plot::Range by = rb.plot->y_range(rb.axis_y);
// A live *edge*, not merely an armed follow: an armed axis with nothing streaming keeps a stored
// window, whose edges a pan moves and whose span never changes.
f64 newest = 0.0;
const bool live = ra.plot->x_live_edge(ra.axis_x, newest);
const auto signature = [live](const plot::Range& x, const plot::Range& y) {
return std::array<double, 4>{live ? x.max - x.min : x.min, live ? 0.0 : x.max, y.min, y.max};
};
const std::size_t at = static_cast<std::size_t>(k);
const std::array<double, 4> a_now = signature(ax, ay);
const std::array<double, 4> b_now = signature(bx, by);
const bool a_moved = a_now != app.sync_last_a[at];
const bool b_moved = b_now != app.sync_last_b[at];
if (b_moved && !a_moved) {
ra.plot->set_x_range(ra.axis_x, bx);
ra.plot->set_y_range(ra.axis_y, by);
app.sync_last_a[at] = b_now;
app.sync_last_b[at] = b_now;
} else if (a_moved) {
// The first plot is the reference when both moved -- the live edge drifting on both, or the first
// frame after the build (the snapshots start empty and both read as moved).
rb.plot->set_x_range(rb.axis_x, ax);
rb.plot->set_y_range(rb.axis_y, ay);
app.sync_last_a[at] = a_now;
app.sync_last_b[at] = a_now;
} else {
app.sync_last_a[at] = a_now;
app.sync_last_b[at] = b_now;
}
}
}
/// The two group buttons' one handler: which layout it starts is which button it is.
void on_group_clicked(GtkButton* button, gpointer user) {
App& app = *static_cast<App*>(user);
const bool compare = button == GTK_BUTTON(app.events_compare);
const std::vector<EventRecord> records = selected_events(app);
std::string error;
if (!build_event_group(app, records, compare ? EventGroupKind::Compare : EventGroupKind::Accumulate,
&error)) {
set_notice(app, std::string(compare ? "cannot compare: " : "cannot accumulate: ") + error, 5);
return;
}
set_notice(app, compare ? "comparing " + records[0].stem + " with " + records[1].stem
: "accumulating " + std::to_string(records.size()) + " events",
5);
}
EventThumbCache* event_thumb_cache(App& app) {
if (app.events_list == nullptr) return nullptr;
return static_cast<EventThumbCache*>(
g_object_get_data(G_OBJECT(app.events_list), "udpscope-event-thumbs"));
}
/// The stored preview scaled to a row's size. Scaled here, once, for two reasons: a `GtkPicture`'s
/// natural size *is* its content, so a 480-pixel BMP in a row claims the row's whole width and squeezes
/// the text out of it -- a size request on the picture is a floor and not a cap -- and the cache then
/// holds a few KB per event instead of 300.
GdkTexture* scale_texture(GdkTexture* source, int width, int height) {
const int sw = gdk_texture_get_width(source);
const int sh = gdk_texture_get_height(source);
if (sw <= 0 || sh <= 0 || width <= 0 || height <= 0) return nullptr;
const std::size_t src_stride = std::size_t(sw) * 4;
std::vector<unsigned char> pixels(src_stride * std::size_t(sh));
gdk_texture_download(source, pixels.data(), src_stride);
cairo_surface_t* src = cairo_image_surface_create_for_data(pixels.data(), CAIRO_FORMAT_ARGB32, sw,
sh, int(src_stride));
cairo_surface_t* dst = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
if (cairo_surface_status(src) != CAIRO_STATUS_SUCCESS ||
cairo_surface_status(dst) != CAIRO_STATUS_SUCCESS) {
cairo_surface_destroy(src);
cairo_surface_destroy(dst);
return nullptr;
}
cairo_t* cr = cairo_create(dst);
cairo_scale(cr, double(width) / double(sw), double(height) / double(sh));
cairo_set_source_surface(cr, src, 0.0, 0.0);
cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_GOOD);
cairo_paint(cr);
cairo_destroy(cr);
cairo_surface_flush(dst);
const int dst_stride = cairo_image_surface_get_stride(dst);
GBytes* bytes = g_bytes_new(cairo_image_surface_get_data(dst),
std::size_t(dst_stride) * std::size_t(height));
GdkTexture* out = gdk_memory_texture_new(width, height, GDK_MEMORY_DEFAULT, bytes, dst_stride);
g_bytes_unref(bytes);
cairo_surface_destroy(src);
cairo_surface_destroy(dst);
return out;
}
/// The decoded preview for `path`, from the cache or decoded now and kept. NULL when the file cannot
/// be read -- cached as well: the store writes an event's picture once and never rewrites one.
///
/// Decoded here rather than with `gtk_picture_set_filename()`: that goes through the image loader's
/// sandboxed, asynchronous decoder, which on this machine fails silently -- the picture keeps a
/// paint-able and an allocation while nothing is on screen (see the GTK invariants).
GdkTexture* event_preview_texture(App& app, const std::string& path) {
EventThumbCache* cache = event_thumb_cache(app);
if (cache == nullptr || path.empty()) return nullptr;
const auto found = cache->find(path);
if (found != cache->end()) return found->second;
GdkTexture* stored = gdk_texture_new_from_filename(path.c_str(), nullptr);
GdkTexture* texture =
stored != nullptr ? scale_texture(stored, kEventThumbW, kEventThumbH) : nullptr;
if (stored != nullptr) g_object_unref(stored);
cache->emplace(path, texture);
return texture;
}
/// The events pane's rows, newest first: each row is the picture of *that* event beside the
/// sidecar's numbers, because what tells one event from another is its shape, and one picture for
/// the whole pane describes whichever event happens to be newest rather than the one being read. The
/// numbers are one line each: a list that has to load the samples to describe an event is a list that
/// cannot show thirty of them.
void refresh_events_pane(App& app) {
if (app.events_list == nullptr) return;
while (GtkWidget* child = gtk_widget_get_first_child(app.events_list)) {
gtk_list_box_remove(GTK_LIST_BOX(app.events_list), child);
}
// The row it pointed at went with the rest: a rebuild that leaves this dangling is a pointer into
// freed widget memory, and the next reader of it (the probe, or a repaint holding the picture) is
// the one that finds out.
app.events_thumb = nullptr;
if (app.event_store == nullptr) return;
const std::vector<EventRecord> records = app.event_store->list();
if (records.empty()) {
GtkWidget* empty = gtk_label_new("no events yet -- arm the trigger");
gtk_widget_add_css_class(empty, "dim-label");
gtk_label_set_xalign(GTK_LABEL(empty), 0.0f);
gtk_widget_set_margin_start(empty, 10);
gtk_widget_set_margin_top(empty, 12);
gtk_list_box_append(GTK_LIST_BOX(app.events_list), empty);
return;
}
// Only the events still on the list keep a decoded picture: the store trims to the newest few, and
// a latch a second would otherwise grow this session's cache by a dead 300 KB texture each time,
// for as long as the run lasted.
if (EventThumbCache* cache = event_thumb_cache(app)) {
for (auto it = cache->begin(); it != cache->end();) {
const bool listed = std::any_of(records.begin(), records.end(),
[&](const EventRecord& r) { return r.thumbnail == it->first; });
if (listed) {
++it;
continue;
}
if (it->second != nullptr) g_object_unref(it->second);
it = cache->erase(it);
}
}
std::size_t unreadable = 0;
for (const EventRecord& r : records) {
GtkWidget* row = gtk_list_box_row_new();
g_object_set_data_full(G_OBJECT(row), "udpscope-event",
new EventRecord(r), [](gpointer p) { delete static_cast<EventRecord*>(p); });
GtkWidget* line = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
gtk_widget_set_margin_start(line, 6);
gtk_widget_set_margin_end(line, 6);
gtk_widget_set_margin_top(line, 4);
gtk_widget_set_margin_bottom(line, 4);
// The picture of this event, beside its numbers. It sits in a fixed-size frame: a `GtkPicture`
// takes its *natural* size from the paintable -- the whole 480-pixel thumbnail -- so a size
// request on the picture itself is a floor and not a cap, and the row's width would go to the
// image rather than to the text. `can_shrink` is what lets it fit the frame it is given.
GtkWidget* shot = gtk_picture_new();
gtk_picture_set_can_shrink(GTK_PICTURE(shot), TRUE);
gtk_picture_set_content_fit(GTK_PICTURE(shot), GTK_CONTENT_FIT_CONTAIN);
gtk_widget_set_hexpand(shot, TRUE);
gtk_widget_set_vexpand(shot, TRUE);
GtkWidget* frame = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
// Exactly the texture's size: equal to the picture's natural size, so the row's box has nothing
// to grow and the text keeps the rest of the width.
gtk_widget_set_size_request(frame, kEventThumbW, kEventThumbH);
// `halign` START, not the default FILL: a filling widget is given whatever the row has left over,
// which made the picture as wide as the pane and cropped by the height it was pinned to.
gtk_widget_set_halign(frame, GTK_ALIGN_START);
gtk_widget_set_valign(frame, GTK_ALIGN_CENTER);
gtk_widget_add_css_class(frame, "udpscope-event-thumb");
gtk_box_append(GTK_BOX(frame), shot);
GdkTexture* texture = event_preview_texture(app, r.thumbnail);
if (texture != nullptr) {
gtk_picture_set_paintable(GTK_PICTURE(shot), GDK_PAINTABLE(texture));
} else {
++unreadable;
}
gtk_box_append(GTK_BOX(line), frame);
// The newest event is the first row: what the probe measures, and nothing the pane treats
// specially.
if (app.events_thumb == nullptr) app.events_thumb = shot;
GtkWidget* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2);
gtk_widget_set_valign(box, GTK_ALIGN_CENTER);
gtk_widget_set_hexpand(box, TRUE);
const std::time_t t = static_cast<std::time_t>(r.wall_time);
std::tm tm{};
localtime_r(&t, &tm);
char clock[32];
std::strftime(clock, sizeof(clock), "%H:%M:%S", &tm);
char title[160];
std::snprintf(title, sizeof(title), "%s %.3f s%s", clock, r.duration_s,
std::string(app.waveform_name) == r.file ? " (on screen)" : "");
GtkWidget* name = gtk_label_new(title);
gtk_widget_set_halign(name, GTK_ALIGN_START);
gtk_label_set_ellipsize(GTK_LABEL(name), PANGO_ELLIPSIZE_END);
// A cap, not only an ellipsis: `max-width-chars` bounds a label's *natural* width, and the row's
// natural width is the pane's minimum -- which is the floor the divider cannot go below.
gtk_label_set_max_width_chars(GTK_LABEL(name), 22);
gtk_widget_add_css_class(name, "udpscope-event-title");
gtk_box_append(GTK_BOX(box), name);
char sub[160];
std::snprintf(sub, sizeof(sub), "%zu signals · %zu samples · %.4g S/s", r.signals, r.samples,
r.rate_hz);
GtkWidget* detail = gtk_label_new(sub);
gtk_widget_add_css_class(detail, "dim-label");
gtk_widget_set_halign(detail, GTK_ALIGN_START);
gtk_label_set_max_width_chars(GTK_LABEL(detail), 22);
// Both labels ellipsize, so the text gives way to the picture rather than the picture to the
// text: a label's minimum width is its longest word otherwise, and the pane is 250 pixels.
gtk_label_set_ellipsize(GTK_LABEL(detail), PANGO_ELLIPSIZE_END);
gtk_box_append(GTK_BOX(box), detail);
gtk_box_append(GTK_BOX(line), box);
gtk_list_box_row_set_child(GTK_LIST_BOX_ROW(row), line);
gtk_list_box_append(GTK_LIST_BOX(app.events_list), row);
}
if (unreadable > 0) {
std::fprintf(stderr, "[udpscope] %zu event previews could not be read\n", unreadable);
std::fflush(stderr);
}
}
/// Clicking a row replays it: the same loader `--open` uses, on the event's file. The acquisition is
/// stopped by `replay_event()` -- a live producer would overwrite the samples within a frame -- so the
/// pane's own *Live* button is the way back.
void on_event_row_activated(GtkListBox*, GtkListBoxRow* row, gpointer user) {
App& app = *static_cast<App*>(user);
if (row == nullptr) return;
auto* record = static_cast<EventRecord*>(g_object_get_data(G_OBJECT(row), "udpscope-event"));
if (record == nullptr) return;
replay_event(app, *record);
// Rebuilt, not for the picture -- the row already has it -- but for the "on screen" mark the
// replay just changed.
refresh_events_pane(app);
}
/// The pane itself: a collapsible column on the left, a header and the list of events -- each row
/// carrying its own picture. Built once with the rest of the window, and opened on demand from the
/// toolbar's history button, which is itself only there while trigger mode and recording are both
/// on (`sync_events_availability()`).
GtkWidget* build_events_pane(App& app) {
GtkWidget* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
// A floor, not a width: the divider sets the width and this is what the pane needs to stay usable at
// its narrowest.
gtk_widget_set_size_request(box, 170, -1);
gtk_widget_set_margin_start(box, 6);
gtk_widget_set_margin_end(box, 6);
gtk_widget_set_margin_top(box, 6);
GtkWidget* head = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
GtkWidget* title = gtk_label_new(nullptr);
gtk_label_set_markup(GTK_LABEL(title), "<b>Events</b>");
gtk_widget_set_hexpand(title, TRUE);
gtk_widget_set_halign(title, GTK_ALIGN_START);
gtk_box_append(GTK_BOX(head), title);
// The two group actions, under the header and above the list they read: what they act on is the
// selection, so they belong where the selection is.
GtkWidget* actions = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
app.events_compare = gtk_button_new_with_label("Compare");
gtk_widget_set_tooltip_text(app.events_compare,
"the two selected events in two plots side by side, with the axes "
"synchronised");
gtk_widget_set_hexpand(app.events_compare, TRUE);
gtk_widget_set_sensitive(app.events_compare, FALSE);
g_signal_connect(app.events_compare, "clicked", G_CALLBACK(on_group_clicked), &app);
gtk_box_append(GTK_BOX(actions), app.events_compare);
app.events_accumulate = gtk_button_new_with_label("Accumulate");
gtk_widget_set_tooltip_text(app.events_accumulate,
"up to four selected events overlaid in the same traces, translucent, "
"so where they agree the trace is brighter");
gtk_widget_set_hexpand(app.events_accumulate, TRUE);
gtk_widget_set_sensitive(app.events_accumulate, FALSE);
g_signal_connect(app.events_accumulate, "clicked", G_CALLBACK(on_group_clicked), &app);
gtk_box_append(GTK_BOX(actions), app.events_accumulate);
gtk_box_append(GTK_BOX(box), actions);
GtkWidget* live = gtk_button_new_with_label("Live");
gtk_widget_add_css_class(live, "flat");
gtk_widget_set_tooltip_text(live, "go back to the live stream after replaying a stored event");
g_signal_connect(live, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) {
go_live_again(*static_cast<App*>(user));
}),
&app);
gtk_box_append(GTK_BOX(head), live);
gtk_box_append(GTK_BOX(box), head);
app.events_list = gtk_list_box_new();
// The decoded pictures live as long as the list does.
g_object_set_data_full(G_OBJECT(app.events_list), "udpscope-event-thumbs", new EventThumbCache,
[](gpointer p) {
auto* cache = static_cast<EventThumbCache*>(p);
for (const auto& entry : *cache) {
if (entry.second != nullptr) g_object_unref(entry.second);
}
delete cache;
});
// Multiple, and activation on *double* click: the list's other job is choosing which events to
// compare or accumulate, and with single-click activation a plain click -- the gesture that builds a
// selection -- would replay the event instead.
gtk_list_box_set_activate_on_single_click(GTK_LIST_BOX(app.events_list), FALSE);
gtk_list_box_set_selection_mode(GTK_LIST_BOX(app.events_list), GTK_SELECTION_MULTIPLE);
g_signal_connect(app.events_list, "row-activated", G_CALLBACK(on_event_row_activated), &app);
g_signal_connect(app.events_list, "selected-rows-changed", G_CALLBACK(+[](GtkListBox*,
gpointer user) {
update_group_buttons(*static_cast<App*>(user));
}),
&app);
GtkWidget* scroll = gtk_scrolled_window_new();
gtk_widget_set_vexpand(scroll, TRUE);
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll), GTK_POLICY_NEVER,
GTK_POLICY_AUTOMATIC);
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), app.events_list);
gtk_box_append(GTK_BOX(box), scroll);
app.events_revealer = gtk_revealer_new();
gtk_revealer_set_transition_type(GTK_REVEALER(app.events_revealer),
GTK_REVEALER_TRANSITION_TYPE_SLIDE_RIGHT);
gtk_revealer_set_child(GTK_REVEALER(app.events_revealer), box);
return app.events_revealer;
}
/// The recording switch, which lives here because it is a trigger option and not a pane option: it
/// decides whether latches are written at all, and the pane that lists them must not be the only
/// place to reach the control that stops them. Flipping it re-decides the pane's availability.
void on_events_record_toggled(GObject* o, GParamSpec*, gpointer user) {
App& app = *static_cast<App*>(user);
app.event_recording = gtk_switch_get_active(GTK_SWITCH(o));
sync_events_availability(app);
}
} // namespace ui_impl
} // namespace udpscope
+435
View File
@@ -0,0 +1,435 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 Martino Ferrari
//
// The layout editor and the persisted session: the overlay buttons that add and remove rows, the\n// debounced save, and the configurations dialog with the layouts it loads and stores.
#include "ui_app.hpp"
#include <gtk/gtk.h>
#include <string>
namespace udpscope {
namespace ui_impl {
/// Reads the session layout back out of the model and rows: the display window,
/// each row's slot signal names, and each signal's calibration.
LayoutState collect_layout(App& app) {
LayoutState st;
st.window_s = app.rows->time_window();
ScopeModel& model = *app.model;
for (int i = 0; i < app.rows->row_count(); ++i) {
LayoutState::Row row;
const RowSet::Row& r = app.rows->row(i);
for (int s = 0; s < RowSet::kSlotsPerRow; ++s) {
row.signals.push_back(r.signal_name[static_cast<std::size_t>(s)]);
}
st.rows.push_back(std::move(row));
}
for (std::size_t si = 0; si < model.signals().size(); ++si) {
for (std::size_t c = 0; c < model.channels().size(); ++c) {
const ChannelInfo& ch = model.channels()[c];
if (ch.signal != si) continue;
st.calibration.push_back({model.signals()[si].name, ch.gain, ch.offset});
break;
}
}
// The *enable* state, not just what is plotted: a signal can be enabled only as a math channel's
// source, and a session that came back with it off would come back with a NaN trace. "Enabled" is
// read off the channels the model built -- it has no per-signal accessor and does not need one,
// since an enabled signal is exactly one that produced a channel.
std::vector<bool> on(model.signals().size(), false);
for (const ChannelInfo& ch : model.channels()) {
if (ch.math) continue;
if (ch.signal < on.size()) on[ch.signal] = true;
}
for (std::size_t si = 0; si < model.signals().size(); ++si) {
if (model.signals()[si].math || !on[si]) continue;
st.enabled.push_back(model.signals()[si].name);
}
// Only the signals the user actually re-coloured: the rest take the positional palette, and
// writing those out would freeze a colour that is meant to follow the signal list.
for (const auto& [name, rgb] : app.rows->signal_colors()) {
st.colors.push_back({name, rgb});
}
// The app's own lists are the pane's source of truth for both of these.
st.math = app.math;
st.measures = app.measures;
st.measure_stats = app.measure_stats_on;
// The connection is the session's too: the file's name carries only host and port.
st.multicast = app.source_multicast;
st.interface_addr = app.source_interface;
// The configuration's own identity and the startup settings it carries. The buffer size is a
// *startup* parameter -- the rings are allocated before any stream is known -- so a configuration
// is the only place it can be remembered per source, and the chooser applies it on open.
st.comment = app.config_comment;
st.buffer_mb = app.model->config().buffer_mb;
st.relative_time = app.model->config().relative_time;
st.capture_packets = app.model->config().capture_packets;
return st;
}
void on_config_row_activated(GtkListBox*, GtkListBoxRow* row, gpointer user) {
ConfigDialog* d = static_cast<ConfigDialog*>(user);
if (row == nullptr) return;
const char* name = static_cast<const char*>(g_object_get_data(G_OBJECT(row), "config-name"));
if (name == nullptr) return;
App& app = *d->app;
// The session on screen is saved first: switching configurations must not be the one gesture
// that loses the arrangement it is switching *from*.
save_layout_now(app);
LayoutState st;
const std::string path = configuration_path(app.source_host, app.source_port, name);
if (!load_layout(path, st)) {
set_notice(app, std::string("could not read the configuration \"") + name + "\"", 4);
return;
}
app.config_name = name;
app.config_comment = st.comment;
set_active_configuration(app.source_host, app.source_port, name);
apply_layout(app, st);
// A buffer size, a clock and a capture ring are decided before the first socket: saying so beats
// a configuration that appears to half-apply.
const Config& cfg = app.model->config();
std::string later;
if (st.buffer_mb > 0.0 && st.buffer_mb != cfg.buffer_mb) {
later += "buffer " + std::to_string(static_cast<int>(st.buffer_mb)) + " MB";
}
if (st.relative_time != cfg.relative_time) {
later += later.empty() ? "" : ", ";
later += st.relative_time ? "relative time" : "stream time";
}
if (st.capture_packets != cfg.capture_packets) {
later += later.empty() ? "" : ", ";
later += "packet capture";
}
set_notice(app, "configuration \"" + std::string(name) + "\" loaded" +
(later.empty() ? "" : " (" + later + " takes effect on the next start)"),
4);
config_dialog_refresh(*d);
}
void on_config_forget_clicked(GtkButton* button, gpointer user) {
ConfigDialog* d = static_cast<ConfigDialog*>(user);
const char* name = static_cast<const char*>(g_object_get_data(G_OBJECT(button), "config-name"));
if (name == nullptr) return;
App& app = *d->app;
if (app.config_name == name) {
// The active one is being deleted: the next save picks whatever is left, which is what
// `active_configuration()` answers once the name is gone.
app.config_name.clear();
app.config_comment.clear();
}
forget_configuration(app.source_host, app.source_port, name);
config_dialog_refresh(*d);
}
void on_config_save_as(GtkButton*, gpointer user) {
ConfigDialog* d = static_cast<ConfigDialog*>(user);
const std::string name = gtk_editable_get_text(GTK_EDITABLE(d->name));
const std::string comment = gtk_editable_get_text(GTK_EDITABLE(d->comment));
if (name.empty()) {
set_notice(*d->app, "a configuration needs a name", 4);
return;
}
if (!save_configuration_as(*d->app, name, comment)) {
set_notice(*d->app, "could not write the configuration", 4);
return;
}
gtk_editable_set_text(GTK_EDITABLE(d->name), "");
gtk_editable_set_text(GTK_EDITABLE(d->comment), "");
config_dialog_refresh(*d);
}
void config_dialog_refresh(ConfigDialog& d) {
while (GtkWidget* child = gtk_widget_get_first_child(d.list)) {
gtk_list_box_remove(GTK_LIST_BOX(d.list), child);
}
const std::vector<std::string> names =
configuration_names(d.app->source_host, d.app->source_port);
if (names.empty()) {
GtkWidget* empty = gtk_label_new("No configurations yet -- save this session below.");
gtk_widget_add_css_class(empty, "dim-label");
gtk_widget_set_margin_top(empty, 12);
gtk_widget_set_margin_bottom(empty, 12);
gtk_list_box_append(GTK_LIST_BOX(d.list), empty);
return;
}
for (const std::string& name : names) {
const std::string path = configuration_path(d.app->source_host, d.app->source_port, name);
LayoutState st;
if (!load_layout(path, st)) continue;
GtkWidget* row = gtk_list_box_row_new();
g_object_set_data_full(G_OBJECT(row), "config-name", g_strdup(name.c_str()), g_free);
GtkWidget* line = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10);
gtk_widget_set_margin_start(line, 10);
gtk_widget_set_margin_end(line, 4);
gtk_widget_set_margin_top(line, 5);
gtk_widget_set_margin_bottom(line, 5);
GtkWidget* text = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2);
gtk_widget_set_hexpand(text, TRUE);
std::string title = name;
if (name == d.app->config_name) title += " (active)";
GtkWidget* title_label = gtk_label_new(nullptr);
gtk_label_set_markup(GTK_LABEL(title_label), ("<b>" + title + "</b>").c_str());
gtk_widget_set_halign(title_label, GTK_ALIGN_START);
gtk_box_append(GTK_BOX(text), title_label);
if (!st.comment.empty()) {
GtkWidget* note = gtk_label_new(st.comment.c_str());
gtk_widget_add_css_class(note, "dim-label");
gtk_widget_set_halign(note, GTK_ALIGN_START);
gtk_label_set_ellipsize(GTK_LABEL(note), PANGO_ELLIPSIZE_END);
gtk_box_append(GTK_BOX(text), note);
}
int signals = 0;
for (const LayoutState::Row& r : st.rows) {
for (const std::string& sig : r.signals) {
if (!sig.empty()) ++signals;
}
}
std::string sub = std::to_string(st.rows.size()) + " plots, " + std::to_string(signals) +
" signals";
if (st.buffer_mb > 0.0) {
sub += ", buffer " + std::to_string(static_cast<int>(st.buffer_mb)) + " MB";
}
if (st.relative_time) sub += ", relative time";
GtkWidget* counts = gtk_label_new(sub.c_str());
gtk_widget_add_css_class(counts, "dim-label");
gtk_widget_set_halign(counts, GTK_ALIGN_START);
gtk_box_append(GTK_BOX(text), counts);
gtk_box_append(GTK_BOX(line), text);
GtkWidget* forget = gtk_button_new_from_icon_name("window-close-symbolic");
gtk_widget_set_tooltip_text(forget, "Delete this configuration");
gtk_widget_add_css_class(forget, "flat");
gtk_widget_set_valign(forget, GTK_ALIGN_CENTER);
g_object_set_data_full(G_OBJECT(forget), "config-name", g_strdup(name.c_str()), g_free);
g_signal_connect(forget, "clicked", G_CALLBACK(on_config_forget_clicked), &d);
gtk_box_append(GTK_BOX(line), forget);
gtk_list_box_row_set_child(GTK_LIST_BOX_ROW(row), line);
gtk_list_box_append(GTK_LIST_BOX(d.list), row);
}
}
/// Writes the layout file (a debounced schedule lands here once). Errors go to
/// stderr; success is silent -- a save per click must not narrate.
void save_layout_now(App& app) {
if (app.source_port == 0) return;
// Nothing is known about this source yet -- no CONFIG ever arrived -- so every slot name is
// empty and this would overwrite a configured session with `signals = ["", ...]`. The startup
// chooser filters such a file out of its list, so a source whose streamer was down during one
// run would silently vanish from the only place it can be picked. Keeping the old file loses
// nothing: this run configured nothing.
if (app.model->signals().empty()) return;
if (app.config_name.empty()) app.config_name = active_configuration(app.source_host,
app.source_port);
const std::string path =
configuration_path(app.source_host, app.source_port, app.config_name);
if (!save_layout(path, collect_layout(app))) {
std::fprintf(stderr, "[udpscope] failed to write the layout file\n");
std::fflush(stderr);
return;
}
// Which configuration the source is on is part of remembering it: the next run opens this one.
set_active_configuration(app.source_host, app.source_port, app.config_name);
}
/// Writes the current session under a *new* name, with a note, and makes it the source's active
/// configuration. The one entry point for both the dialog's button and the probe: a probe that
/// rolled its own would test a different path than the user's.
bool save_configuration_as(App& app, const std::string& name, const std::string& comment) {
if (name.empty() || app.source_port == 0 || app.model->signals().empty()) return false;
app.config_name = name;
app.config_comment = comment;
const std::string path = configuration_path(app.source_host, app.source_port, name);
if (!save_layout(path, collect_layout(app))) {
std::fprintf(stderr, "[udpscope] failed to write the configuration file\n");
std::fflush(stderr);
return false;
}
set_active_configuration(app.source_host, app.source_port, name);
return true;
}
/// One-shot debounce: a burst of changes (a drop, a row insert, a calibration
/// commit in the same second) writes once, 400 ms after the last one.
gboolean layout_save_tick(gpointer user) {
App* app = static_cast<App*>(user);
app->layout_save_source = 0;
save_layout_now(*app);
return G_SOURCE_REMOVE;
}
void schedule_layout_save(App& app) {
if (app.source_port == 0) return;
if (app.layout_save_source != 0) return;
app.layout_save_source = g_timeout_add(400, layout_save_tick, &app);
}
/// Applies a loaded layout: rows, slot assignments, calibration, window. Names
/// that no longer exist in the stream are skipped rather than freeing their slot
/// (a stream that renamed a signal must not blank the user's layout).
void apply_layout(App& app, const LayoutState& st) {
ScopeModel& model = *app.model;
const auto& signals = model.signals();
// The session's own state first: enabling is what starts acquisition, and a math channel's
// *name* is only a signal once its graph is installed -- a row that holds one would otherwise be
// skipped as a name the stream no longer has.
for (const std::string& name : st.enabled) {
for (std::size_t si = 0; si < signals.size(); ++si) {
if (signals[si].name != name || signals[si].math) continue;
model.set_signal_enabled(si, true);
break;
}
}
app.math = st.math;
apply_math_channels(app);
app.measures = st.measures;
app.measure_stats_on = st.measure_stats;
app.updating_panels = true;
if (app.measures_stats_check != nullptr) {
gtk_check_button_set_active(GTK_CHECK_BUTTON(app.measures_stats_check),
app.measure_stats_on);
}
// The toggled handler is guarded by updating_panels, so the Reset button's sensitivity
// and the stats-label visibility are set directly from the loaded state here; the labels'
// visibility is also set per-row inside rebuild_measures_pane via measures_row().
if (app.measures_stats_reset != nullptr)
gtk_widget_set_sensitive(app.measures_stats_reset, app.measure_stats_on);
app.updating_panels = false;
rebuild_measures_pane(app);
int rows = static_cast<int>(st.rows.size());
if (rows > RowSet::kMaxRows) rows = RowSet::kMaxRows;
if (rows > 0) app.rows->set_row_count(rows);
for (int i = 0; i < rows; ++i) {
int slot = 0;
for (const std::string& name : st.rows[static_cast<std::size_t>(i)].signals) {
if (slot >= RowSet::kSlotsPerRow) break;
if (name.empty()) continue;
for (std::size_t si = 0; si < signals.size(); ++si) {
if (signals[si].name != name) continue;
// A signal the model will never build a channel for must not take a slot. The
// stream's time signal is skipped by `build_channels_locked()` however often it is
// enabled, so such a slot holds a name for ever and data never -- and because
// `collect_layout()` writes the names straight back, one that got in stayed in and
// was re-saved every session. On the plot it drew nothing; in the cursors pane's
// `mix` mode it drew a line that could only read `--`.
if (ScopeModel::is_time_signal(signals, si)) break;
// Enabling is what starts acquisition (a loaded plot nobody acquires would
// come back blank), and it is what a drop does for the signal it places.
app.rows->assign(i, slot, name, name, signal_color_index(signals, si));
model.set_signal_enabled(si, true);
++slot;
break;
}
}
}
for (const auto& cal : st.calibration) {
for (std::size_t si = 0; si < signals.size(); ++si) {
if (signals[si].name != cal.name) continue;
model.set_calibration(si, cal.gain, cal.offset);
break;
}
}
// Skipped for a name the stream no longer carries, like the calibration above: an override the
// signal list cannot show is one `collect_layout()` would write back every session for ever.
for (const auto& color : st.colors) {
for (std::size_t si = 0; si < signals.size(); ++si) {
if (signals[si].name != color.name) continue;
app.rows->set_signal_color(color.name, color.rgb);
break;
}
}
if (st.window_s > 0.0) apply_time_window(app, st.window_s);
// set_row_count() rebuilt the figure (its row count is fixed at construction):
// re-point the area, as every other row-count change must, and refresh the
// signal panel's chips so they name the traces the file brought back.
if (app.area != nullptr) app.area->set_figure(&app.rows->figure());
app.relayout_pending = true;
rebuild_signal_panels(app);
}
void on_layout_row_remove(GtkButton* button, gpointer user) {
App* app = static_cast<App*>(user);
const int row = row_key(GTK_WIDGET(button));
if (!app->rows->remove_row(row)) {
app->notice = "a figure needs at least one row";
app->notice_until_us = g_get_monotonic_time() + 3 * G_TIME_SPAN_SECOND;
return;
}
// `remove_row()` has already built the new figure (a `Figure`'s row count is fixed at
// construction), so the area has to be re-pointed here rather than through
// `relayout_areas()`, which only acts on a *dirty* row.
if (app->area != nullptr) app->area->set_figure(&app->rows->figure());
rebuild_layout_editor(*app);
// The signal list's chips name the trace a signal is drawn as, and a removed row took
// its traces with it.
rebuild_signal_panels(*app);
schedule_layout_save(*app);
}
void on_layout_row_insert(GtkButton* button, gpointer user) {
App* app = static_cast<App*>(user);
const int at = row_key(GTK_WIDGET(button));
if (!app->rows->insert_row(at)) {
app->notice = "at most " + std::to_string(RowSet::kMaxRows) + " rows";
app->notice_until_us = g_get_monotonic_time() + 3 * G_TIME_SPAN_SECOND;
return;
}
if (app->area != nullptr) app->area->set_figure(&app->rows->figure());
rebuild_layout_editor(*app);
rebuild_signal_panels(*app);
schedule_layout_save(*app);
}
/// The `Done` button: the toolbar toggle is the one piece of state that says whether the
/// editor is up, so this turns *that* off and the existing `toggled` handler does the rest
/// (including taking this button down).
void on_layout_editor_done(GtkButton*, gpointer user) {
App* app = static_cast<App*>(user);
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(app->layout_toggle), false);
}
void on_layout_editor_toggled(GtkToggleButton* button, gpointer user) {
App* app = static_cast<App*>(user);
app->layout_editing = gtk_toggle_button_get_active(button);
rebuild_layout_editor(*app);
}
/// The editor button of one kind at one index, or null. The probe clicks the real
/// widgets rather than calling the handlers, so what it drives is what a user reaches.
GtkWidget* layout_button(App& a, bool insert, int index) {
for (GtkWidget* b : a.layout_buttons) {
const bool is_insert = g_object_get_data(G_OBJECT(b), kLayoutKindKey) != nullptr;
if (is_insert == insert && row_key(b) == index) return b;
}
return nullptr;
}
/// Clicks one editor button. `::clicked` directly and not `gtk_widget_activate()`: a
/// `GtkButton` activated that way holds the press for `ACTIVATE_TIMEOUT` (250 ms) and only
/// then emits `::clicked`, so every step of the probe read the *previous* step's result --
/// and the button whose timeout was pending is destroyed by the rebuild the step before it
/// caused, which dropped the click entirely.
void layout_click(GtkWidget* b) {
if (b != nullptr) g_signal_emit_by_name(b, "clicked");
}
} // namespace ui_impl
} // namespace udpscope
+1063
View File
File diff suppressed because it is too large. Load diff
+598
View File
@@ -0,0 +1,598 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 Martino Ferrari
//
// The measurements pane and its dialog: the list of measurements, the window statistics they compute\n// on a throttle, and the marks they place on the plot.
#include "ui_app.hpp"
#include <gtk/gtk.h>
#include <string>
namespace udpscope {
namespace ui_impl {
/// A measurement's own colour, so the pane's badge and the marks it puts on the plot are
/// recognisably the same measurement. Picked from the trace palette's neighbours rather than
/// it: a measurement's mark must not look like one of the signals.
plot::u32 measure_color(std::size_t index) {
static const plot::u32 kPalette[] = {0x9BE8A0u, 0xE8A0C8u, 0xE8D79Bu, 0x9BD7E8u,
0xC8A0E8u, 0xA0E8D7u, 0xE8B9A0u, 0xA0A8E8u};
return kPalette[index % (sizeof(kPalette) / sizeof(kPalette[0]))];
}
/// The channel a measurement's signal resolved to, or -1 while the stream does not carry it.
int measure_channel(App& app, const std::string& signal) {
std::lock_guard<std::mutex> lock(app.model->mutex());
const std::vector<SignalInfo>& signals = app.model->signals();
const std::vector<ChannelInfo>& channels = app.model->channels();
for (std::size_t s = 0; s < signals.size(); ++s) {
if (signals[s].name != signal) continue;
for (std::size_t c = 0; c < channels.size(); ++c) {
if (channels[c].signal == s) return static_cast<int>(c);
}
return -1; // the signal exists but is not acquired
}
return -1;
}
/// Which row plots a signal (the first slot that does), or 0: a measurement's marks belong on
/// the plot showing the signal, and a signal nothing plots still has to go somewhere.
int measure_row(App& app, const std::string& signal) {
for (int r = 0; r < app.rows->row_count(); ++r) {
const RowSet::Row& row = app.rows->row(r);
for (int s = 0; s < RowSet::kSlotsPerRow; ++s) {
if (row.signal_name[static_cast<std::size_t>(s)] == signal) return r;
}
}
return 0;
}
/// The unit of a signal's channel, for the pane's readout.
std::string measure_unit(App& app, const std::string& signal) {
std::lock_guard<std::mutex> lock(app.model->mutex());
for (const ChannelInfo& c : app.model->channels()) {
if (c.name == signal) return c.unit;
}
return "";
}
void rebuild_measures_pane(App& app) {
if (app.measures_list == nullptr) return;
GtkWidget* child = gtk_widget_get_first_child(app.measures_list);
while (child != nullptr) {
GtkWidget* next = gtk_widget_get_next_sibling(child);
gtk_box_remove(GTK_BOX(app.measures_list), child);
child = next;
}
app.measure_labels.clear();
app.measure_stats_labels.clear();
app.measure_text.clear();
// Editing a measurement changes what the figures *mean*, so the history behind them is no
// longer one series. This is the implicit reset for an add, an edit and a delete alike.
app.measure_stats.assign(app.measures.size(), MeasureStatsAccum{});
if (app.measures.empty()) {
GtkWidget* empty = gtk_label_new("no measurements -- [+] to add one");
gtk_widget_add_css_class(empty, "dim-label");
gtk_widget_set_halign(empty, GTK_ALIGN_START);
gtk_box_append(GTK_BOX(app.measures_list), empty);
}
for (std::size_t i = 0; i < app.measures.size(); ++i) {
gtk_box_append(GTK_BOX(app.measures_list), measures_row(app, i, app.measures[i]));
app.measure_text.emplace_back();
}
update_measures(app, /*force=*/true, /*accumulate=*/true);
}
/// Shows the fields a type actually uses: the threshold and the side only where the engine
/// reads them, and the plot option only for a type that can be drawn. A field that does
/// nothing is worse than an absent one -- it invites the user to set it.
void measure_dialog_sync(MeasureDialog* d) {
const int sel = gtk_drop_down_get_selected(GTK_DROP_DOWN(d->type_drop));
const MeasureType type = measure_type_at(sel);
const bool threshold = measure_uses_threshold(type);
const bool side = measure_uses_side(type);
gtk_widget_set_visible(d->threshold_row, threshold);
gtk_widget_set_visible(d->side_row, side);
gtk_widget_set_visible(d->show, measure_can_show_on_plot(type));
// The words follow the type: one row, several meanings.
const std::string signal = d->signals.empty()
? std::string("the signal")
: d->signals[std::min<std::size_t>(
gtk_drop_down_get_selected(GTK_DROP_DOWN(d->signal_drop)),
d->signals.size() - 1)];
switch (type) {
case MeasureType::Average:
gtk_label_set_text(GTK_LABEL(d->threshold_label), "");
gtk_label_set_text(GTK_LABEL(d->side_label), "");
gtk_check_button_set_label(GTK_CHECK_BUTTON(d->show), "show on plot (ruler at the mean)");
break;
case MeasureType::Min:
case MeasureType::Max:
gtk_check_button_set_label(GTK_CHECK_BUTTON(d->show),
"show on plot (ruler at the value, marker where it is)");
break;
case MeasureType::PeakToPeak:
case MeasureType::Amplitude:
gtk_label_set_text(GTK_LABEL(d->threshold_label), "");
gtk_label_set_text(GTK_LABEL(d->side_label), "");
gtk_check_button_set_label(GTK_CHECK_BUTTON(d->show),
"show on plot (rulers at both extremes)");
break;
case MeasureType::Top:
case MeasureType::Bottom:
gtk_label_set_text(GTK_LABEL(d->threshold_label), "split % of the range");
gtk_check_button_set_label(GTK_CHECK_BUTTON(d->show), "show on plot (ruler at the level)");
break;
case MeasureType::PulseDuration:
gtk_label_set_text(GTK_LABEL(d->threshold_label), "split % of the range");
gtk_label_set_text(GTK_LABEL(d->side_label), "pulse above the level");
gtk_check_button_set_label(GTK_CHECK_BUTTON(d->show),
"show on plot (markers at the pulse's edges)");
break;
case MeasureType::Period:
case MeasureType::Frequency:
// No split to choose: the engine counts rising crossings of the mid level (see
// `compute_measure`), and the row that would offer one is not shown at all.
gtk_label_set_text(GTK_LABEL(d->threshold_label), "");
gtk_label_set_text(GTK_LABEL(d->side_label), "");
gtk_check_button_set_label(GTK_CHECK_BUTTON(d->show),
"show on plot (markers at two edges of a cycle)");
break;
}
gtk_widget_set_tooltip_text(
d->show,
("draw what the measurement found on the row plotting " + signal + " -- it follows the "
"window and cannot be dragged").c_str());
}
void on_measure_type_changed(GObject* obj, GParamSpec*, gpointer user) {
(void)obj;
measure_dialog_sync(static_cast<MeasureDialog*>(user));
}
void on_measure_dialog_apply(GtkButton*, gpointer user) {
MeasureDialog* d = static_cast<MeasureDialog*>(user);
App& app = *d->app;
const guint sig = gtk_drop_down_get_selected(GTK_DROP_DOWN(d->signal_drop));
if (sig >= d->signals.size()) {
gtk_window_destroy(GTK_WINDOW(d->window));
return;
}
Measure m;
m.signal = d->signals[sig];
m.type = measure_type_at(gtk_drop_down_get_selected(GTK_DROP_DOWN(d->type_drop)));
m.options.threshold_pct = gtk_spin_button_get_value(GTK_SPIN_BUTTON(d->threshold));
m.options.above = gtk_switch_get_active(GTK_SWITCH(d->above));
m.options.show_on_plot = gtk_check_button_get_active(GTK_CHECK_BUTTON(d->show));
if (d->edit >= 0 && static_cast<std::size_t>(d->edit) < app.measures.size()) {
app.measures[static_cast<std::size_t>(d->edit)] = std::move(m);
} else {
app.measures.push_back(std::move(m));
}
rebuild_measures_pane(app);
gtk_window_destroy(GTK_WINDOW(d->window));
}
void on_measure_dialog_delete(GtkButton*, gpointer user) {
MeasureDialog* d = static_cast<MeasureDialog*>(user);
App& app = *d->app;
if (d->edit >= 0 && static_cast<std::size_t>(d->edit) < app.measures.size()) {
app.measures.erase(app.measures.begin() + d->edit);
rebuild_measures_pane(app);
}
gtk_window_destroy(GTK_WINDOW(d->window));
}
/// Puts `App::measure_marks` on the plots, converting each **time** mark from the stream seconds
/// it is stored in to the x the axis is reading this frame. Called every frame -- that is what
/// makes the marks read-only (a gesture moves one, and this puts it back) and, since a live axis
/// is newest-relative, also what rolls each mark leftwards with the sample it measured.
void place_measure_marks(App& app) {
const double s0 = app.model->stream_at_x0();
app.measure_marks_display = app.measure_marks;
for (RowSet::MeasureMark& mk : app.measure_marks_display) {
if (!mk.level) mk.value -= s0;
}
app.rows->set_measure_marks(app.measure_marks_display);
}
/// Recomputes every measurement over the visible window and writes the results into the pane,
/// collecting the marks the ones asking to be shown need.
///
/// A measurement is a statistic of the window, so it is recomputed when the *window* moves --
/// not on a timer. A timer decouples the two: with the window scrolling under a 5 Hz cadence
/// the stored marks describe a range the plot has already left, which is how a min/max marker
/// ended up outside the viewport, and how a dragged marker survived until the next tick. The
/// threshold is a small fraction of the span, so the marks stay within a few pixels of where
/// the current window would put them, and the cost is bounded by the window's own motion
/// rather than by the frame rate.
void update_measures(App& app, bool force, bool accumulate) {
if (app.measure_labels.size() != app.measures.size()) return; // mid-rebuild
const RowSet::Row& r0 = app.rows->row(0);
if (r0.plot == nullptr) return;
// The window on the **stream's** clock, which is the only space in which "it moved" is a
// question with an answer: a live axis is newest-relative, so `view` itself is a fixed
// `[-window, 0]` that never changes while the data under it does.
const double s0 = app.model->stream_at_x0();
const plot::Range view = r0.plot->x_range(r0.axis_x);
const plot::Range swin{view.min + s0, view.max + s0};
const bool held = app.model->display_mode() == DisplayMode::Held;
const bool moved = app.measure_window_valid && !held &&
std::fabs(swin.min - app.measure_window.min) +
std::fabs(swin.max - app.measure_window.max) >
0.02 * std::max(1.0e-12, swin.span());
// A measurement is a statistic of the visible window, and *a live view moves that window every
// frame* -- so "recompute when the window moves" is "recompute every frame", at O(visible
// samples) per measurement. That is what stalled the frame tick (0.4 ms -> 13.7 ms, a third of
// the run's frames lost) once the window held a few million samples. The pane is a *readout*,
// so it is refreshed on the terms the status line's own fields already use: at most every
// `kMeasureRefreshUs`, plus immediately whenever the window stops moving or the mode changes,
// which is when the number has to be exact. Between those a tenth of a second of lag is
// invisible on a statistic that is itself a window average, and the marks below are re-placed
// from the last result every frame either way.
const gint64 now_us = g_get_monotonic_time();
const gint64 interval =
app.measure_last_pass_us > kMeasureHeavyUs ? 4 * kMeasureRefreshUs : kMeasureRefreshUs;
const bool due = !app.measure_stamp_valid || now_us - app.measure_stamp_us >= interval;
// Two reasons to skip, and they are different questions. `measure_window` is the window the
// *last recompute described*, so no change at all means the pane already says the right thing:
// that is the settled case, and it must recompute once when it settles and then never again --
// getting this backwards is what had a *pinned* window recomputing every single frame (457
// passes in one run) instead of once. The other is a window still scrolling: at most one pass
// per `kMeasureRefreshUs`, because the number is a window average and a tenth of a second of
// lag on it is not readable.
const bool changed = !app.measure_window_valid || held != app.measure_was_held || moved;
if (!force && !changed) {
place_measure_marks(app);
return;
}
if (!force && moved && held == app.measure_was_held && !due) {
// Nothing to recompute -- but the marks are still re-placed below, which is what makes
// them read-only: a gesture moves one on the plot, and this is what puts it back.
place_measure_marks(app);
return;
}
app.measure_window = swin;
app.measure_window_valid = true;
app.measure_was_held = held;
app.measure_stamp_us = now_us;
app.measure_stamp_valid = true;
const gint64 pass_start_us = g_get_monotonic_time();
app.measure_samples.clear();
app.measure_results.assign(app.measures.size(), MeasureResult{});
app.measure_last_valid.resize(app.measures.size());
app.measure_text.assign(app.measures.size(), std::string("--"));
app.measure_marks.clear();
std::vector<RowSet::MeasureMark> marks;
struct PassTimer {
App& app;
gint64 start;
~PassTimer() {
const gint64 took = g_get_monotonic_time() - start;
app.measure_cost_us += took;
app.measure_last_pass_us = took;
++app.measure_passes;
}
} pass_timer{app, pass_start_us};
// Who needs what, before any data is touched: the channel each measurement resolved to, and
// *which* level accumulator answers it. Several measurements on one signal share both walks, and
// several sharing a threshold share one accumulator -- so a pane full of measurements on one
// trace costs the same two walks as a single one. The common case is exactly one level: every
// crossing type reads the mid level and `Top`/`Bottom` default to it.
std::vector<int> chan(app.measures.size(), -1);
std::vector<int> want_level(app.measures.size(), -1);
for (std::size_t i = 0; i < app.measures.size(); ++i) {
const Measure& m = app.measures[i];
const int channel = measure_channel(app, m.signal);
chan[i] = channel;
if (channel < 0) continue; // unresolvable this pass: a rebuild has emptied the channel list
App::MeasureWindow& w = app.measure_samples[channel];
double pct = 0.0;
if (!measure_level_pct(m.type, m.options, pct)) continue;
std::size_t k = 0;
while (k < w.pct.size() && w.pct[k] != pct) ++k;
if (k == w.pct.size()) w.pct.push_back(pct);
want_level[i] = static_cast<int>(k);
}
// The data work: two walks per channel, whatever the pane holds. The first fixes the window's
// extremes (and so every level, which is a share of them); the second is skipped entirely when
// no measurement on this channel reads a level.
//
// Both go through the **LOD pyramid** rather than over the samples. The producer already folds
// every sample into it as it arrives, which is what "evaluate only on the new samples" means
// here: a 64-sample bucket carries its own extremes and its exact sum, so pass one reads 1/64
// of the window, and pass two opens only the buckets that *straddle* its level -- a handful per
// crossing. The raw path is still here, and still exact, for the window's partial edge buckets,
// for the samples produced since this frame's `acquire_all()`, and for those straddling
// buckets; a window shorter than a bucket, or a signal noisy enough to straddle everywhere,
// degrades back to the O(window) cost and nothing else changes.
constexpr std::size_t kChunk = 8192;
std::vector<MeasureBucket>& buckets = app.measure_bucket_scratch;
for (auto& entry : app.measure_samples) {
App::MeasureWindow& w = entry.second;
const gint64 fetch_start_us = g_get_monotonic_time();
MeasureWindowReader reader(app, static_cast<std::size_t>(entry.first));
const std::size_t n = reader.size();
const std::function<double(std::size_t)> time_of = [&reader](std::size_t i) {
return reader.time_of(i);
};
// The window's whole buckets, fetched once and walked by both passes.
u64 kb = 0;
u64 ke = 0;
buckets.clear();
if (reader.bucket_range(kb, ke)) {
buckets.reserve(static_cast<std::size_t>(ke - kb));
for (u64 k = kb; k < ke; ++k) buckets.push_back(reader.bucket(k));
}
const std::size_t bulk_from = buckets.empty() ? n : buckets.front().offset;
const std::size_t bulk_to = buckets.empty() ? n : buckets.back().offset + buckets.back().span;
std::size_t raw_read = 0;
// In window order, always: the first sample to reach an extreme is the one the mark names, so
// a later chunk that merely ties it must not move it.
const auto raw = [&](std::size_t from, std::size_t to, const auto& fn) {
for (std::size_t at = from; at < to; at += kChunk) {
const std::size_t take = std::min(kChunk, to - at);
fn(MeasureChunk{reader.values(at, take), take, at});
raw_read += take;
}
};
const auto scan = [&](const MeasureChunk& c) { measure_scan(c, w.stats); };
raw(0, bulk_from, scan);
if (!buckets.empty()) measure_scan_buckets(buckets.data(), buckets.size(), w.stats);
raw(bulk_to, n, scan);
// A bucket named the 64 samples that hold an extreme, not the sample. One scan of that bucket
// pins it -- two buckets at most, whatever the window's size.
if (w.stats.valid) {
const auto pin = [&](std::size_t& index, std::size_t& search, bool want_hi) {
if (search == 0) return;
MeasureStats one;
measure_scan(MeasureChunk{reader.values(index, search), search, index}, one);
if (one.valid) index = want_hi ? one.hi_i : one.lo_i;
raw_read += search;
search = 0;
};
pin(w.stats.lo_i, w.stats.lo_search, false);
pin(w.stats.hi_i, w.stats.hi_search, true);
// The two stamps the whole scan needed, read now rather than at every chunk that moved an
// extreme: a stamp is a ring read, and an extreme moves many times over a long window.
w.stats.at_lo = reader.time_of(w.stats.lo_i);
w.stats.at_hi = reader.time_of(w.stats.hi_i);
}
w.ok = w.stats.valid;
if (w.ok && !w.pct.empty()) {
w.level.assign(w.pct.size(), MeasureLevel{});
for (std::size_t k = 0; k < w.pct.size(); ++k) {
// The level *and* its hysteresis band, in the engine: how wide the band is is a property of
// how an edge is counted, not of the pane, and two expressions for it is how the walk and
// the bucket fast path end up disagreeing about where a crossing was.
measure_set_level(w.level[k], w.stats, w.pct[k]);
}
const auto accumulate = [&](const MeasureChunk& c) {
measure_accumulate(c, time_of, w.level.data(), w.level.size());
};
raw(0, bulk_from, accumulate);
for (const MeasureBucket& b : buckets) {
const f32* values = nullptr;
for (MeasureLevel& level : w.level) {
if (measure_accumulate_bucket(b, time_of, level)) continue;
// Fetched once for however many levels this bucket straddles, and only then: the usual
// case is one level, and the usual bucket answers without being opened at all.
if (values == nullptr) {
values = reader.values(b.offset, b.span);
raw_read += b.span;
}
measure_accumulate(MeasureChunk{values, b.span, b.offset}, time_of, &level, 1);
}
}
raw(bulk_to, n, accumulate);
}
app.measure_fetch_us += g_get_monotonic_time() - fetch_start_us;
app.measure_covered += static_cast<std::uint64_t>(n);
app.measure_bucketed += static_cast<std::uint64_t>(buckets.size());
app.measure_raw += raw_read;
}
for (std::size_t i = 0; i < app.measures.size(); ++i) {
const Measure& m = app.measures[i];
GtkWidget* label = app.measure_labels[i];
const auto it = chan[i] < 0 ? app.measure_samples.end() : app.measure_samples.find(chan[i]);
if (it == app.measure_samples.end() || !it->second.ok) {
// Its signal is momentarily unresolvable, or the window holds nothing -- so there is nothing
// to measure *this* pass. The marks still fall back (below); `continue`ing out of the marks
// was the other half of the rulers blinking off.
app.measure_text[i] = "--";
if (label != nullptr) gtk_label_set_text(GTK_LABEL(label), "--");
continue;
}
const App::MeasureWindow& w = it->second;
const MeasureLevel* level =
want_level[i] >= 0 && static_cast<std::size_t>(want_level[i]) < w.level.size()
? &w.level[static_cast<std::size_t>(want_level[i])]
: nullptr;
const MeasureResult r = compute_measure(w.stats, level, m.type, m.options);
app.measure_results[i] = r;
if (r.valid) app.measure_last_valid[i] = r;
// Time-valued types carry seconds and a frequency carries hertz whatever the signal's
// own unit is; everything else carries that unit. Hoisted out of the valid branch below
// because the statistics line needs the same unit.
const bool is_time =
m.type == MeasureType::PulseDuration || m.type == MeasureType::Period;
const std::string unit = is_time ? "s"
: m.type == MeasureType::Frequency ? "Hz"
: measure_unit(app, m.signal);
if (label != nullptr) {
std::string text = "--";
if (r.valid) {
text = format_measure(r.primary, unit);
if (r.has_secondary) {
// Pulse duration also reports the duty, which has no unit.
text += " " + format_measure(100.0 * r.secondary, "%");
}
}
app.measure_text[i] = text;
gtk_label_set_text(GTK_LABEL(label), text.c_str());
}
// One observation per recompute, and only when the pass produced one and the caller
// asked for it. An invalid pass contributes nothing -- the marks' `measure_last_valid`
// fallback is a *drawing* convenience. `accumulate=false` is the toggle handler, which
// forces a repaint of the newly-visible labels without counting the window again.
if (accumulate && r.valid && i < app.measure_stats.size()) app.measure_stats[i].add(r.primary);
if (i < app.measure_stats_labels.size() && app.measure_stats_labels[i] != nullptr) {
GtkWidget* sl = app.measure_stats_labels[i];
gtk_widget_set_visible(sl, app.measure_stats_on);
if (app.measure_stats_on && i < app.measure_stats.size()) {
const MeasureStatsAccum& s = app.measure_stats[i];
std::string text = "no passes yet";
if (s.n > 0) {
// The same formatter the reading above uses, so the two lines are comparable at a
// glance; sd and the extremes carry the measurement's unit, n does not.
text = "n " + std::to_string(s.n) + " mean " + format_measure(s.mean, unit) +
" sd " + format_measure(s.sd(), unit) + " " +
format_measure(s.lo, unit) + " .. " + format_measure(s.hi, unit);
}
gtk_label_set_text(GTK_LABEL(sl), text.c_str());
}
}
}
// The marks last, in their own pass over the measurements, so that no `continue` above can skip
// them. That is exactly how the rulers came to blink off -- first at a mode change, then at a
// rebuild where the signal is momentarily unresolvable -- while the plot put them back a few
// frames later, which reads as the lines flickering.
//
// A pass with no answer falls back to the last valid one, and the split is by *kind*: a
// **level** is a property of the signal (a mean, a top, an amplitude) and outlives the window it
// was measured in, so its ruler stays; a **time** mark is a position *inside* the window that
// result describes, so it may not outlive it and is dropped.
for (std::size_t i = 0; i < app.measures.size() && i < app.measure_results.size(); ++i) {
const Measure& m = app.measures[i];
if (!m.options.show_on_plot) continue;
const MeasureResult& r = app.measure_results[i];
const MeasureResult* mk = &r;
if (!r.valid && i < app.measure_last_valid.size() && app.measure_last_valid[i].valid) {
mk = &app.measure_last_valid[i];
}
const int row = measure_row(app, m.signal);
const plot::u32 color = measure_color(i);
for (const MeasureMarkSpec& spec : mk->marks) {
if (!spec.level && !r.valid) continue;
// A time mark is stored on the **stream's** clock -- the walks report display x, so `s0`
// puts it back -- and `place_measure_marks()` converts it again on every frame it draws.
// That is what keeps the mark on its own sample as a live view rolls underneath it; a level
// mark is a y and is stored as it comes.
marks.push_back({row, spec.level, spec.level ? spec.value : spec.value + s0, color});
}
}
app.measure_marks = std::move(marks);
place_measure_marks(app);
}
/// Repeating trace of the pane's numbers beside the *current* window and the marks actually
/// placed: the only way a stale result is visible as a number rather than as a stutter.
gboolean measure_trace(gpointer user) {
App* a = static_cast<App*>(user);
if (a->rows->row_count() <= 0) return G_SOURCE_REMOVE;
RowSet::Row& r0 = a->rows->row(0);
if (r0.plot == nullptr) return G_SOURCE_REMOVE;
const plot::Range view = r0.plot->x_range(r0.axis_x);
std::string line = "[measure-trace] view=[" + std::to_string(view.min) + ".." +
std::to_string(view.max) + "]";
for (std::size_t i = 0; i < a->measures.size() && i < a->measure_results.size(); ++i) {
const MeasureResult& r = a->measure_results[i];
line += " " + std::string(measure_type_name(a->measures[i].type)) + "=" +
(r.valid ? std::to_string(r.primary) : std::string("--"));
}
// The marks as *placed*, not as stored: a time mark is kept on the stream's clock and converted
// for the axis each frame (see `place_measure_marks()`), and it is the converted value that is
// comparable with the `view=` printed beside it.
line += " marks=" + std::to_string(a->measure_marks_display.size());
for (const RowSet::MeasureMark& m : a->measure_marks_display) {
line += " " + std::string(m.level ? "level" : "time") + ":" + std::to_string(m.value);
}
std::fprintf(stderr, "%s\n", line.c_str());
std::fflush(stderr);
return G_SOURCE_CONTINUE;
}
/// Adds one measurement of every type on the longest-cycling scalar signal the stream
/// describes, then reports each result -- through the pane's own list and the tick's own
/// recomputation, so what it prints is what a user would read.
/// Clicks a labelled button inside the measurement dialog, which is a window of its own.
void measure_dialog_click(App& a, const char* label) {
if (a.measure_dialog == nullptr) {
std::fprintf(stderr, "[measures-probe] no dialog to click '%s' in\n", label);
// (the zoom-in is judged on the next frame, after the layout pushes it)
}
GtkWidget* b = find_labelled_button(a.measure_dialog, label);
if (b == nullptr) {
std::fprintf(stderr, "[measures-probe] no '%s' button in the dialog\n", label);
// (the zoom-in is judged on the next frame, after the layout pushes it)
}
g_signal_emit_by_name(b, "clicked");
}
/// What the dialog's signal dropdown holds: the probe's assertion that the list is the
/// *acquired* signals (plus the edited measurement's own, when the stream no longer acquires
/// it), and that every entry is a name rather than the mangled pointer it used to be.
void measure_dialog_report(App& a, const char* what) {
std::string items;
int n = 0;
if (a.measure_dialog != nullptr) {
// The dialog keeps its fields in the context it stores on its own window, which is how a
// probe reaches a dropdown inside a window that is not the main one.
auto* d = static_cast<MeasureDialog*>(
g_object_get_data(G_OBJECT(a.measure_dialog), "udpscope-measure-ctx"));
if (d != nullptr && d->signal_drop != nullptr) {
GListModel* model = gtk_drop_down_get_model(GTK_DROP_DOWN(d->signal_drop));
const guint count = model != nullptr ? g_list_model_get_n_items(model) : 0;
for (guint i = 0; i < count; ++i) {
GObject* obj = G_OBJECT(g_list_model_get_item(model, i));
const char* text = GTK_IS_STRING_OBJECT(obj)
? gtk_string_object_get_string(GTK_STRING_OBJECT(obj))
: "?";
if (i != 0) items += ", ";
items += text;
g_object_unref(obj);
}
n = static_cast<int>(count);
}
}
int drop_h = -1;
char options[192] = "";
if (a.measure_dialog != nullptr) {
auto* dd = static_cast<MeasureDialog*>(
g_object_get_data(G_OBJECT(a.measure_dialog), "udpscope-measure-ctx"));
if (dd != nullptr && dd->signal_drop != nullptr) {
drop_h = gtk_widget_get_height(dd->signal_drop);
}
if (dd != nullptr) {
// What the dialog is offering for the type on screen: the option rows' visibility and
// the words on them, which is the per-type behaviour a user sees.
std::snprintf(options, sizeof(options), "type=%d thr=%d side=%d show=%d '%s'",
static_cast<int>(gtk_drop_down_get_selected(GTK_DROP_DOWN(dd->type_drop))),
dd->threshold_row != nullptr && gtk_widget_get_visible(dd->threshold_row) ? 1 : 0,
dd->side_row != nullptr && gtk_widget_get_visible(dd->side_row) ? 1 : 0,
dd->show != nullptr && gtk_widget_get_visible(dd->show) ? 1 : 0,
dd->show != nullptr && gtk_check_button_get_child(GTK_CHECK_BUTTON(dd->show)) !=
nullptr
? gtk_label_get_text(GTK_LABEL(gtk_check_button_get_child(
GTK_CHECK_BUTTON(dd->show))))
: "");
}
}
std::fprintf(stderr,
"[measures-probe] %-14s measures=%zu dialog=%d signals=%d drop_h=%d [%s] %s\n",
what, a.measures.size(), a.measure_dialog != nullptr ? 1 : 0, n, drop_h,
items.c_str(), options);
std::fflush(stderr);
}
} // namespace ui_impl
} // namespace udpscope
+3113
View File
File diff suppressed because it is too large. Load diff
+431
View File
@@ -0,0 +1,431 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 Martino Ferrari
//
// The signal list: one row per signal with its colour chip and acquire tick, the panel around it,\n// and the drop that puts a signal on a row.
#include "ui_app.hpp"
#include <gtk/gtk.h>
#include <string>
namespace udpscope {
namespace ui_impl {
void on_signal_switch(GObject* obj, GParamSpec*, gpointer user) {
App* app = static_cast<App*>(user);
if (app->updating_panels) return;
const std::size_t index = static_cast<std::size_t>(
GPOINTER_TO_SIZE(g_object_get_data(obj, "udpscope-signal")));
app->model->set_signal_enabled(index, gtk_check_button_get_active(GTK_CHECK_BUTTON(obj)));
// The enable state is part of the session (a signal can be on only as a math channel's source).
schedule_layout_save(*app);
}
/// The palette entry a signal owns: its position among the *plottable* signals, the
/// time base skipped. It is a property of the signal and not of the row/slot a drop
/// lands in, so the same signal is drawn in the same colour on every plot it appears on
/// -- which is what lets one chip in this list name every one of those traces.
///
/// Positional, like the channel list, so a new CONFIG re-colours; that is fine because a
/// CONFIG drops the assignments and the history with them. A name hash would survive a
/// CONFIG but collide inside one, and two traces of the same colour on one row is the
/// failure this exists to prevent.
int signal_color_index(const std::vector<SignalInfo>& signals, std::size_t signal) {
// Counted over *signals*, not channels: a group's model holds each signal once per event, and
// counting the copies would spread one signal's colour across the palette and give its second layer a
// colour of its own.
int index = 0;
std::vector<std::string> seen;
for (std::size_t i = 0; i < signals.size() && i < signal; ++i) {
if (ScopeModel::is_time_signal(signals, i)) continue;
const std::string base = layer_base_name(signals[i].name);
if (std::find(seen.begin(), seen.end(), base) != seen.end()) continue;
seen.push_back(base);
++index;
}
return index;
}
/// Whether any row's slot holds `name` -- i.e. whether the signal has a trace on screen, which is
/// what decides between its own colour and the grey of an unplotted one, and what decides whether
/// there is a colour for the user to change.
bool signal_plotted(App& app, const std::string& name) {
for (int r = 0; r < app.rows->row_count(); ++r) {
const RowSet::Row& row = app.rows->row(r);
for (int s = 0; s < RowSet::kSlotsPerRow; ++s) {
if (row.signal_name[static_cast<std::size_t>(s)] == name) return true;
}
}
return false;
}
/// The chip for a row of the signal list, as Pango markup: the signal's own colour when
/// it is on a plot, grey when it is not (whether that is because it is not acquired or
/// because it is acquired and not placed). Always a filled disc, never a blank: a blank
/// cell is narrower than a disc, so the names beside it used to shift sideways as
/// signals were placed, and "no trace" read as "no chip here" rather than as a state.
std::string signal_chip_markup(App& app, const std::vector<SignalInfo>& signals,
std::size_t signal) {
const std::string& name = signals[signal].name;
if (!signal_plotted(app, name)) return chip_markup(RowSet::unplotted_color());
// Through `RowSet::signal_color()`, which is also what `install()` builds the trace with: the
// chip's whole job is to say which trace this row is about, so it may not resolve a colour by
// its own route. A user's override is exactly the state in which two routes would disagree.
return chip_markup(app.rows->signal_color(name, signal_color_index(signals, signal)));
}
/// Applies a chosen colour, or leaves everything alone if the chooser was dismissed.
void on_signal_color_chosen(GObject* source, GAsyncResult* res, gpointer user) {
std::unique_ptr<ColorPick> pick(static_cast<ColorPick*>(user));
GError* error = nullptr;
GdkRGBA* picked = gtk_color_dialog_choose_rgba_finish(GTK_COLOR_DIALOG(source), res, &error);
if (picked == nullptr) {
// A dismissed chooser arrives here as an error (`GTK_DIALOG_ERROR_DISMISSED`, or a cancelled
// `GError` if the window went away), and a cancel must leave the colour as it was. There is
// nothing to report either way: the user closed a chooser.
if (error != nullptr) g_error_free(error);
return;
}
const auto octet = [](double v) {
return static_cast<plot::u32>(std::clamp(v, 0.0, 1.0) * 255.0 + 0.5);
};
const plot::u32 rgb = (octet(picked->red) << 16) | (octet(picked->green) << 8) |
octet(picked->blue);
gdk_rgba_free(picked);
App& app = *pick->app;
app.rows->set_signal_color(pick->signal, rgb);
// The rebuild is deferred to the frame tick, like a drop's: a trace's colour is fixed at
// `add_trace()`, so a new colour is a new figure, and `relayout_areas()` is the one place that
// builds one and re-points the area at it.
app.relayout_pending = true;
rebuild_signal_panels(app);
schedule_layout_save(app);
}
/// One line per signal: colour chip, checkbox, name, unit, and a menu for the things
/// you touch once (information, calibration). A `GtkSwitch` and two always-visible
/// buttons made every row about twice this tall, which is what turned a four-signal
/// stream into a panel you have to scroll.
void rebuild_signal_list(App& app, const std::vector<SignalInfo>& signals) {
app.updating_panels = true;
GtkWidget* list = app.signal_list;
GtkWidget* child = gtk_widget_get_first_child(list);
while (child != nullptr) {
GtkWidget* next = gtk_widget_get_next_sibling(child);
gtk_list_box_remove(GTK_LIST_BOX(list), child);
child = next;
}
// One line per *signal*, not per channel. A group's model holds each signal once per event; a list
// that showed both would name one "sine (A)" and the other "sine (B)" as if they were different
// things, and give them different colours. What the row toggles, colours and drags is the signal.
std::vector<std::size_t> rows;
for (std::size_t i = 0; i < signals.size(); ++i) {
if (ScopeModel::is_time_signal(signals, i)) continue; // the time base is not plottable
const std::string base = layer_base_name(signals[i].name);
bool seen = false;
for (std::size_t j = 0; j < i && !seen; ++j) {
seen = !ScopeModel::is_time_signal(signals, j) &&
layer_base_name(signals[j].name) == base;
}
if (!seen) rows.push_back(i);
}
for (std::size_t i : rows) {
const SignalInfo& sig = signals[i];
GtkWidget* row = gtk_list_box_row_new();
gtk_widget_add_css_class(row, "udpscope-compact");
GtkWidget* box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
gtk_widget_set_margin_start(box, 6);
gtk_widget_set_margin_end(box, 2);
const bool plotted = signal_plotted(app, sig.name);
GtkWidget* chip = gtk_label_new(nullptr);
gtk_label_set_markup(GTK_LABEL(chip), signal_chip_markup(app, signals, i).c_str());
gtk_widget_set_tooltip_text(
chip, !app.model->signal_enabled(i) ? "not acquired"
: plotted ? "the colour this signal is plotted in -- click to change it"
: "on no plot, so there is no trace to colour");
if (plotted) {
// A gesture on the label rather than a `GtkButton` around it: a button brings its own
// padding, and padding on this row is what made the list twice as tall once already (see
// this function's note). The chip also has to stay exactly one disc wide, or the names
// beside it shift as signals are placed.
gtk_widget_set_cursor_from_name(chip, "pointer");
GtkGesture* pick = gtk_gesture_click_new();
g_object_set_data(G_OBJECT(pick), "udpscope-signal", GSIZE_TO_POINTER(i));
g_signal_connect(pick, "pressed",
G_CALLBACK(+[](GtkGestureClick* g, int, double, double, gpointer user) {
App* a = static_cast<App*>(user);
// Claimed, so the row's `GtkDragSource` cannot take the same sequence
// and turn a click on the chip into the start of a drag.
gtk_gesture_set_state(GTK_GESTURE(g), GTK_EVENT_SEQUENCE_CLAIMED);
open_signal_color(*a, static_cast<std::size_t>(GPOINTER_TO_SIZE(
g_object_get_data(G_OBJECT(g),
"udpscope-signal"))));
}),
&app);
gtk_widget_add_controller(chip, GTK_EVENT_CONTROLLER(pick));
}
gtk_box_append(GTK_BOX(box), chip);
GtkWidget* enable = gtk_check_button_new();
gtk_check_button_set_active(GTK_CHECK_BUTTON(enable), app.model->signal_enabled(i));
gtk_widget_set_tooltip_text(enable, "acquire this signal");
// A file view has nothing to acquire: an event's or a file's samples are already stored, and
// "acquire this signal" is a stream's question. Hidden rather than disabled -- a greyed tick invites
// the user to look for the reason, and the reason is that this mode has no acquisition at all.
gtk_widget_set_visible(enable, app.waveform_name.empty());
g_object_set_data(G_OBJECT(enable), "udpscope-signal", GSIZE_TO_POINTER(i));
g_signal_connect(enable, "notify::active", G_CALLBACK(on_signal_switch), &app);
gtk_box_append(GTK_BOX(box), enable);
GtkWidget* name = gtk_label_new(sig.name.c_str());
gtk_widget_set_hexpand(name, true);
gtk_widget_set_halign(name, GTK_ALIGN_START);
gtk_label_set_ellipsize(GTK_LABEL(name), PANGO_ELLIPSIZE_END);
gtk_box_append(GTK_BOX(box), name);
if (!sig.unit.empty()) {
GtkWidget* unit = gtk_label_new(sig.unit.c_str());
gtk_widget_add_css_class(unit, "dim-label");
gtk_box_append(GTK_BOX(box), unit);
}
// The overflow: information and calibration are set once per session, so they
// cost a click here instead of a button on every row forever.
GtkWidget* menu_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_widget_set_margin_top(menu_box, 4);
gtk_widget_set_margin_bottom(menu_box, 4);
gtk_widget_set_margin_start(menu_box, 4);
gtk_widget_set_margin_end(menu_box, 4);
GtkWidget* info = menu_button("Information", nullptr);
g_object_set_data(G_OBJECT(info), "udpscope-signal", GSIZE_TO_POINTER(i));
g_signal_connect(info, "clicked", G_CALLBACK(on_info_clicked), &app);
gtk_box_append(GTK_BOX(menu_box), info);
// The way back from a chosen colour. The chooser itself cannot carry a "default" button, and a
// setting with no undo is a trap; it lives here rather than on the chip because a second
// gesture on a one-glyph target is not something a user would find.
GtkWidget* recolor = menu_button("Default colour", nullptr);
gtk_widget_set_sensitive(recolor, app.rows->signal_colors().count(sig.name) != 0);
gtk_widget_set_tooltip_text(recolor, "back to the palette colour for this signal's position");
g_object_set_data(G_OBJECT(recolor), "udpscope-signal", GSIZE_TO_POINTER(i));
g_signal_connect(recolor, "clicked",
G_CALLBACK(+[](GtkButton* b, gpointer user) {
App* a = static_cast<App*>(user);
const std::size_t idx = static_cast<std::size_t>(
GPOINTER_TO_SIZE(g_object_get_data(G_OBJECT(b), "udpscope-signal")));
std::string name;
{
std::lock_guard<std::mutex> lock(a->model->mutex());
const std::vector<SignalInfo>& sigs = a->model->signals();
if (idx >= sigs.size()) return;
name = sigs[idx].name;
}
a->rows->clear_signal_color(name);
a->relayout_pending = true;
rebuild_signal_panels(*a);
schedule_layout_save(*a);
}),
&app);
gtk_box_append(GTK_BOX(menu_box), recolor);
GtkWidget* cal = menu_button("Calibrate...", nullptr);
gtk_widget_set_tooltip_text(cal, "gain and offset, applied at ingest");
g_object_set_data(G_OBJECT(cal), "udpscope-signal", GSIZE_TO_POINTER(i));
g_signal_connect(cal, "clicked",
G_CALLBACK(+[](GtkButton* b, gpointer user) {
App* a = static_cast<App*>(user);
const std::size_t idx = static_cast<std::size_t>(
GPOINTER_TO_SIZE(g_object_get_data(G_OBJECT(b), "udpscope-signal")));
open_calibration(*a, GTK_WINDOW(a->window), idx);
}),
&app);
gtk_box_append(GTK_BOX(menu_box), cal);
GtkWidget* popover = gtk_popover_new();
gtk_widget_add_css_class(popover, "udpscope-menu");
gtk_popover_set_child(GTK_POPOVER(popover), menu_box);
GtkWidget* more = gtk_menu_button_new();
gtk_menu_button_set_icon_name(GTK_MENU_BUTTON(more), "view-more-symbolic");
gtk_menu_button_set_popover(GTK_MENU_BUTTON(more), popover);
gtk_widget_add_css_class(more, "flat");
gtk_widget_set_tooltip_text(more, "information and calibration");
gtk_box_append(GTK_BOX(box), more);
gtk_list_box_row_set_child(GTK_LIST_BOX_ROW(row), box);
DragContext* ctx = new DragContext{&app, i};
GtkDragSource* source = gtk_drag_source_new();
gtk_drag_source_set_actions(source, GDK_ACTION_COPY);
g_signal_connect_data(source, "prepare", G_CALLBACK(on_drag_prepare), ctx,
[](gpointer data, GClosure*) { delete static_cast<DragContext*>(data); },
G_CONNECT_DEFAULT);
gtk_widget_add_controller(row, GTK_EVENT_CONTROLLER(source));
gtk_list_box_append(GTK_LIST_BOX(list), row);
}
app.updating_panels = false;
}
void on_signals_toggled(GtkToggleButton* button, gpointer user) {
App* app = static_cast<App*>(user);
gtk_revealer_set_reveal_child(GTK_REVEALER(app->signal_revealer),
gtk_toggle_button_get_active(button));
}
gboolean on_drop_channel(GtkDropTarget*, const GValue* value, double x, double y,
gpointer user) {
App* app = static_cast<App*>(user);
if (!G_VALUE_HOLDS_STRING(value)) return FALSE;
// Where the drop landed decides the row: one area covers them all.
const int row = row_at(*app, x, y);
const char* text = g_value_get_string(value);
const std::size_t signal = static_cast<std::size_t>(std::atoll(text ? text : "0"));
if (signal >= app->model->signals().size()) return FALSE;
const auto notice = [&](const std::string& what) {
app->notice = what;
app->notice_until_us = g_get_monotonic_time() + 4 * G_TIME_SPAN_SECOND;
std::fprintf(stderr, "[udpscope] drop: %s\n", what.c_str());
std::fflush(stderr);
};
// The dropped signal's channels are the ones the model built for it; take the
// first, or the first element if the signal is a vector.
const std::vector<ChannelInfo>& channels = app->model->channels();
int channel = -1;
std::string name;
for (std::size_t i = 0; i < channels.size(); ++i) {
if (channels[i].signal == signal) {
channel = static_cast<int>(i);
name = channels[i].name;
break;
}
}
if (channel < 0) {
// The signal is not enabled: enable it, then take the channel that appears.
app->model->set_signal_enabled(signal, true);
const std::vector<ChannelInfo>& again = app->model->channels();
for (std::size_t i = 0; i < again.size(); ++i) {
if (again[i].signal == signal) {
channel = static_cast<int>(i);
name = again[i].name;
break;
}
}
}
if (channel < 0) {
notice("cannot acquire '" + app->model->signals()[signal].name +
"' (multi-element signals are not acquired)");
return FALSE;
}
int slot = -1;
for (int s = 0; s < RowSet::kSlotsPerRow; ++s) {
if (!app->rows->slot_bound(row, s)) {
slot = s;
break;
}
}
if (slot < 0) {
notice("plot " + std::to_string(row + 1) + " already holds four signals");
return FALSE;
}
// The slot remembers the *signal* (and element), never the channel index: the
// channel list is renumbered whenever a signal is enabled or disabled.
const std::vector<SignalInfo>& signals = app->model->signals();
const SignalInfo& sig = signals[signal];
app->rows->assign(row, slot, sig.name, name, signal_color_index(signals, signal));
schedule_layout_save(*app);
app->relayout_pending = true;
rebuild_signal_panels(*app); // the row's colour chip now has a trace to name
cancel_drag_on(*app, row);
notice("plotting '" + name + "' on plot " + std::to_string(row + 1));
return TRUE;
}
void rebuild_signal_panels(App& app) {
// Snapshot the model's lists under the lock: the acquisition thread replaces
// them on every CONFIG, and the panel build holds these references across many
// GTK calls (a use-after-free of the name strings otherwise).
std::vector<SignalInfo> signals;
bool have_signals = false;
{
std::lock_guard<std::mutex> lock(app.model->mutex());
signals = app.model->signals();
have_signals = !signals.empty();
}
rebuild_signal_list(app, signals);
rebuild_trigger_signals(app);
refresh_window_limits(app);
// Nothing to configure before a CONFIG arrives: hide the whole column and its
// toolbar buttons rather than showing two empty panels. The first CONFIG
// reveals the signal panel (the trigger panel stays behind its own button).
update_side_visibility(app);
if (have_signals && !app.signals_panel_initialised) {
app.signals_panel_initialised = true;
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(app.signal_toggle), TRUE);
}
if (!have_signals) {
app.signals_panel_initialised = false;
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(app.signal_toggle), FALSE);
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(app.trigger_toggle), FALSE);
}
app.signal_generation = app.model->generation();
}
GtkWidget* build_signal_panel(App& app) {
GtkWidget* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
gtk_widget_set_margin_top(box, 8);
gtk_widget_set_margin_bottom(box, 8);
gtk_widget_set_margin_start(box, 8);
gtk_widget_set_margin_end(box, 8);
GtkWidget* heading = gtk_label_new("Signals");
gtk_widget_set_halign(heading, GTK_ALIGN_START);
gtk_box_append(GTK_BOX(box), heading);
GtkWidget* hint = gtk_label_new("drag a signal onto a plot");
gtk_widget_set_halign(hint, GTK_ALIGN_START);
gtk_widget_add_css_class(hint, "dim-label");
gtk_box_append(GTK_BOX(box), hint);
app.signal_list = gtk_list_box_new();
gtk_list_box_set_selection_mode(GTK_LIST_BOX(app.signal_list), GTK_SELECTION_NONE);
// Width only. A height floor here made the list occupy 120 px whatever it held,
// which hid the compact rows' height behind it and wasted the space on an empty
// stream; the scroller above it already caps how tall it can grow.
gtk_widget_set_size_request(app.signal_list, 260, -1);
gtk_box_append(GTK_BOX(box), app.signal_list);
return box;
}
/// The pane's availability as one reading: the history button on the bar, the pane revealed, and the
/// switch that decides half of it. Both the button and the pane follow trigger mode *and* recording,
/// so every step that changes either prints all three.
/// Whether the signal list's first row still carries its acquire tick. A file view has nothing to
/// acquire -- the samples are stored -- so the tick goes; in live mode it is the control that decides
/// what is acquired at all.
int signal_tick_visible(App& app) {
GtkWidget* row =
app.signal_list != nullptr ? gtk_widget_get_first_child(app.signal_list) : nullptr;
if (row == nullptr) return -1;
for (GtkWidget* box = gtk_widget_get_first_child(row); box != nullptr;
box = gtk_widget_get_next_sibling(box)) {
for (GtkWidget* w = gtk_widget_get_first_child(box); w != nullptr;
w = gtk_widget_get_next_sibling(w)) {
if (GTK_IS_CHECK_BUTTON(w)) return gtk_widget_get_visible(w) ? 1 : 0;
}
}
return -1;
}
} // namespace ui_impl
} // namespace udpscope
+187
View File
@@ -0,0 +1,187 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 Martino Ferrari
//
// The status bar and the detail popover: the one-line glance with its activity badge, the popover\n// behind the ⓘ, the short-lived notices, and the starvation detector that says a stream stopped.
#include "ui_app.hpp"
#include <gtk/gtk.h>
#include <string>
namespace udpscope {
namespace ui_impl {
void on_popover_closed(GtkPopover* popover, gpointer) {
// Unparent, one idle later. `gtk_widget_set_parent()` took the reference the popover was created
// with (GTK4 widgets come with a floating reference, sunk by the first parent), so unparenting
// drops it to zero and destroys the popover -- a following `g_object_unref()` would be a
// use-after-free, which is why there is none.
//
// The *deferral* is not tidiness either: this runs inside the `closed` emission, which GTK raises
// from `gtk_popover_popdown()`, and destroying a widget in the middle of its own signal leaves the
// popdown path running on freed memory -- a `Gtk-CRITICAL` from `gtk_popover_get_autohide`, on
// every menu entry that dismisses its popover, which is every entry there is. The idle holds the
// reference (`g_object_ref` in, `g_object_unref` out) so the popover outlives its own emission.
g_idle_add_full(
G_PRIORITY_DEFAULT_IDLE,
+[](gpointer p) -> gboolean {
gtk_widget_unparent(GTK_WIDGET(p));
return G_SOURCE_REMOVE;
},
g_object_ref(popover), g_object_unref);
}
/// What the activity badge is saying. `Lit`/`Dim` are the two halves of the blink, so
/// the badge is only *blinking* while the pipeline is actually running; a steady badge is
/// a stopped one, which is the state worth noticing on a glance. `Connecting` is the
/// half-way state -- the socket answered but the stream has sent no CONFIG, so there is no
/// signal list and nothing to acquire -- and it is steady yellow rather than green,
/// because a blinking green badge over a source that has sent nothing claims a working
/// pipeline.
/// The badge is one glyph in one of four colours. It must stay the *same glyph*: the
/// status row sits above the plot, and a badge that changed width as it blinked would
/// move the label beside it and re-lay out the plot twice a second.
const char* badge_markup(BadgePhase phase) {
switch (phase) {
case BadgePhase::Lit:
return "<span foreground=\"#45E045\">\u25CF</span>";
case BadgePhase::Dim:
return "<span foreground=\"#1C5A1C\">\u25CF</span>";
case BadgePhase::Connecting:
return "<span foreground=\"#E0B020\">\u25CF</span>";
case BadgePhase::Idle:
break;
}
return "<span foreground=\"#5A5A5A\">\u25CF</span>";
}
/// A short-lived line in front of the status text: the outcome of an action that the
/// picture does not show by itself (a drop that changed nothing, an export that wrote a
/// file). `tick()` clears it when it expires.
void set_notice(App& app, std::string what, int seconds) {
app.notice = std::move(what);
app.notice_until_us = g_get_monotonic_time() + seconds * G_TIME_SPAN_SECOND;
}
/// The widest window that is worth offering. `--max-window` only: the buffer no longer
/// sets the ceiling, because a window too long to store at the wire rate is decimated on
/// ingest to fit rather than refused (see `plan_partition()`). It used to be capped to
/// `buffered_span()` as well, which is what made a 600 s view of a 1 MS/s stream
/// unreachable.
double window_limit(const App& app) { return app.model->config().max_window_s; }
/// Re-clamps the window against the current cap. Called once at start-up and again when a
/// CONFIG changes how much history one buffer holds -- never on a mere enable or calibration
/// change, so those cannot silently re-anchor the view.
void refresh_window_limits(App& app) {
const double limit = window_limit(app);
if (std::fabs(limit - app.window_limit_s) < 1.0e-9) return;
app.window_limit_s = limit;
apply_time_window(app, app.rows->time_window());
}
/// Sets the popover's seven value labels. Never creates a widget: `popover_rebuilds` exists to
/// assert that, because the popover is visible when this runs and a rebuild would relayout it.
void update_status_popover(App& app, const udpscope::StatusSnapshot& s) {
if (app.popover_label[4] == nullptr) return;
char buf[256];
if (!app.waveform_name.empty()) {
// Not "waiting, no config": a file is not a source that has not answered yet.
std::snprintf(buf, sizeof(buf), "file: %s", app.waveform_name.c_str());
} else {
std::snprintf(buf, sizeof(buf), "%s, %s", s.connected ? "connected" : "waiting",
s.have_config ? "configured" : "no config");
}
gtk_label_set_text(GTK_LABEL(app.popover_label[4]), buf);
std::snprintf(buf, sizeof(buf), "packets %llu · frames %llu · gaps %llu",
static_cast<unsigned long long>(s.packets),
static_cast<unsigned long long>(s.frames),
static_cast<unsigned long long>(s.gaps));
gtk_label_set_text(GTK_LABEL(app.popover_label[5]), buf);
std::snprintf(buf, sizeof(buf), "%zu signals · %zu channels", s.signals, s.channels);
gtk_label_set_text(GTK_LABEL(app.popover_label[6]), buf);
std::snprintf(buf, sizeof(buf), "%.1f MB · %.0f s span · time x%llu", s.buffer_mb, s.span_s,
static_cast<unsigned long long>(s.time_stride));
gtk_label_set_text(GTK_LABEL(app.popover_label[7]), buf);
const std::string stored = udpscope::format_rate(s.stored_rate_hz);
const std::string wire = udpscope::format_rate(s.declared_rate_hz);
std::snprintf(buf, sizeof(buf), "stored %s (wire %s)", stored.c_str(), wire.c_str());
gtk_label_set_text(GTK_LABEL(app.popover_label[8]), buf);
std::snprintf(buf, sizeof(buf), "%d traces · %zu samples", s.plot1_traces, s.plot1_samples);
gtk_label_set_text(GTK_LABEL(app.popover_label[9]), buf);
std::snprintf(buf, sizeof(buf), "%d", s.reconnects);
gtk_label_set_text(GTK_LABEL(app.popover_label[10]), buf);
}
/// Recognises the "the source is serving someone else" state and says so.
///
/// A UDPStreamer over unicast keeps one session: a second client's CONNECT displaces the
/// first, the first sees `silence_timeout_s` of no data and reconnects, and the two take
/// turns. Nothing in the client can give both of them the stream -- the wire has one
/// destination -- so the useful thing is to name the situation: a reconnect with no frames
/// delivered in the same second is that fight, or a server that keeps dropping us, and
/// neither is visible in an empty plot.
///
/// Called once a second from the frame tick.
void note_starvation(App& app) {
StreamStats& st = app.model->stats();
const std::uint64_t frames = st.frames.load(std::memory_order_relaxed);
const std::uint64_t reconnects = st.reconnects.load(std::memory_order_relaxed);
const bool connected = st.connected.load(std::memory_order_relaxed);
// "Connected, but nothing is arriving" is a *rate* fact, not a reconnect one: a source that
// answers CONNECT and then goes quiet -- the unicast case where another client took the session --
// never reconnects while it is silent, so requiring a reconnect made the notice invisible in
// exactly the state it describes. What it takes is `frames` not moving across three consecutive
// one-second samples: one sample would call a source slower than the sample interval starved, and
// three still clears the moment anything arrives.
const bool idle = app.starve_have_prev && frames == app.starve_frames;
app.starve_still = (connected && !app.acq->paused() && idle) ? app.starve_still + 1 : 0;
app.starve_frames = frames;
app.starve_have_prev = true;
if (app.starve_still < 3) {
app.starve_reported = false;
// **A `return`, not just a branch.** The notice and its line used to sit outside this test, so
// they were re-armed every frame and the status bar carried "no data is arriving" while the
// stream was arriving perfectly well.
return;
}
// Two diagnoses, split on `have_config`, because they are not the same situation. A source
// that sent a CONFIG and then went quiet is the unicast fight: the signal list is here, so
// something took the stream away. One that has sent no CONFIG at all never had the stream to
// lose -- there is no signal list, so nothing can be enabled and no DATA frame is owed -- and
// naming that "connected, but no data is arriving" blamed a second reader that does not exist.
// It is still the same stall test: a CONFIG that arrives promptly leaves no notice at all.
const bool have_config = st.have_config.load(std::memory_order_relaxed);
app.notice =
have_config
? "connected, but no data is arriving: another reader may hold the source (a unicast "
"source serves one client at a time) -- use --multicast to watch from several UIs"
: "trying to connect: the source answered, but has sent no configuration, so there is "
"no signal list yet -- check that the streamer is running and serving this port";
app.notice_until_us = g_get_monotonic_time() + 3 * G_TIME_SPAN_SECOND;
if (!app.starve_reported) {
app.starve_reported = true;
std::fprintf(stderr,
have_config
? "[udpscope] starved: connected (config=1), reconnects=%llu, no new frames "
"in the last second -- a unicast source serves one client at a time\n"
: "[udpscope] connecting: connected (config=0), reconnects=%llu, no "
"configuration from the stream -- there is no signal list to acquire\n",
static_cast<unsigned long long>(reconnects));
std::fflush(stderr);
}
}
} // namespace ui_impl
} // namespace udpscope
+432
View File
@@ -0,0 +1,432 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 Martino Ferrari
//
// The trigger panel and everything the trigger change reaches: the condition rows it shows and hides\n// per shape, the arming, and the switch between the stream and a stored event.
#include "ui_app.hpp"
#include <gtk/gtk.h>
#include <string>
namespace udpscope {
namespace ui_impl {
void on_arm_clicked(GtkButton* button, gpointer user) {
App* app = static_cast<App*>(user);
(void)button;
bool armed = false;
int signal = -1;
{
// The producer reads (and, on a single-shot latch, writes) these fields under
// the same lock; an unguarded read-modify-write here can lose its update.
std::lock_guard<std::mutex> lock(app->model->mutex());
TriggerConfig& tc = app->model->trigger();
tc.armed = !tc.armed;
armed = tc.armed;
signal = tc.channel;
}
if (armed) {
app->event_held = false;
app->capturing = false;
app->model->trigger_time_valid().store(false, std::memory_order_relaxed);
}
// Arming is a statement that this signal should be watched, so its acquisition
// is switched on with it -- otherwise the trigger could never see a sample.
// Outside the lock: set_signal_enabled() takes it.
if (armed && signal >= 0) app->model->set_signal_enabled(static_cast<std::size_t>(signal), true);
gtk_button_set_label(GTK_BUTTON(app->arm_button), armed ? "Disarm" : "Arm");
}
/// The trigger type dropdown's order, as a table rather than a cast: the dropdown lists
/// only the conditions that are implemented, so its index is not `TriggerType`'s value.
TriggerType trigger_type_at(guint i) {
switch (i) {
case 1:
return TriggerType::PulseWidth;
case 2:
return TriggerType::Window;
case 3:
return TriggerType::Dropout;
default:
return TriggerType::Edge;
}
}
void publish_trigger_fields(App* app) {
if (app->updating_panels) return;
// Read the widgets first (they may be touched again by a re-entrant handler if a
// setter ran while the lock was held), then publish the values under the lock the
// producer reads them with.
const double level = gtk_spin_button_get_value(GTK_SPIN_BUTTON(app->trigger_level_spin));
const double pre = gtk_spin_button_get_value(GTK_SPIN_BUTTON(app->trigger_pre_spin));
const double holdoff = gtk_spin_button_get_value(GTK_SPIN_BUTTON(app->trigger_holdoff_spin));
const guint edge = gtk_drop_down_get_selected(GTK_DROP_DOWN(app->trigger_edge_drop));
const guint mode = gtk_drop_down_get_selected(GTK_DROP_DOWN(app->trigger_mode_drop));
const guint type = gtk_drop_down_get_selected(GTK_DROP_DOWN(app->trigger_type_drop));
const guint cmp = gtk_drop_down_get_selected(GTK_DROP_DOWN(app->trigger_cmp_drop));
const double width = gtk_spin_button_get_value(GTK_SPIN_BUTTON(app->trigger_width_spin));
const double width2 = gtk_spin_button_get_value(GTK_SPIN_BUTTON(app->trigger_width2_spin));
const double level2 = gtk_spin_button_get_value(GTK_SPIN_BUTTON(app->trigger_level2_spin));
const double timeout = gtk_spin_button_get_value(GTK_SPIN_BUTTON(app->trigger_timeout_spin));
{
std::lock_guard<std::mutex> lock(app->model->mutex());
TriggerConfig& tc = app->model->trigger();
tc.condition.level = level;
tc.pretrigger_pct = pre;
tc.holdoff_s = holdoff;
tc.condition.edge = edge == 0 ? TriggerEdge::Rising
: edge == 1 ? TriggerEdge::Falling
: TriggerEdge::Both;
tc.mode = mode == 0 ? TriggerMode::Normal : TriggerMode::Single;
tc.condition.type = trigger_type_at(type);
tc.condition.cmp = width_cmp_at(cmp);
tc.condition.width_s = width;
tc.condition.width2_s = width2;
tc.condition.level2 = level2;
tc.condition.timeout_s = timeout;
}
}
/// Shows only the rows the selected condition reads, so the panel never offers a figure the
/// condition ignores. The widgets keep their values while hidden: `EventCondition` carries
/// all eight fields whatever its type, and switching away and back must not lose a setting.
void sync_trigger_rows(App& app) {
if (app.trigger_type_drop == nullptr) return;
const TriggerType type =
trigger_type_at(gtk_drop_down_get_selected(GTK_DROP_DOWN(app.trigger_type_drop)));
const WidthCmp cmp =
width_cmp_at(gtk_drop_down_get_selected(GTK_DROP_DOWN(app.trigger_cmp_drop)));
const bool pulse = type == TriggerType::PulseWidth;
const bool window = type == TriggerType::Window;
gtk_widget_set_visible(app.trigger_cmp_row, pulse);
gtk_widget_set_visible(app.trigger_width_row, pulse);
gtk_widget_set_visible(app.trigger_width2_row,
pulse && (cmp == WidthCmp::Within || cmp == WidthCmp::Outside));
gtk_widget_set_visible(app.trigger_level2_row, window);
gtk_widget_set_visible(app.trigger_timeout_row, type == TriggerType::Dropout);
// A band of zero width reduces the condition to a float-equality test on a real-valued
// signal, which is unusable: it fires on the exact value and nothing else, which in practice
// is never. The panel is where that is refused: the second level is pushed at least one
// increment clear of the first, in whichever direction it already lay.
if (window) {
GtkSpinButton* l2 = GTK_SPIN_BUTTON(app.trigger_level2_spin);
const double level = gtk_spin_button_get_value(GTK_SPIN_BUTTON(app.trigger_level_spin));
double step = 0.0;
gtk_spin_button_get_increments(l2, &step, nullptr);
const double other = gtk_spin_button_get_value(l2);
if (std::fabs(other - level) < step) {
gtk_spin_button_set_value(l2, other < level ? level - step : level + step);
}
}
}
void on_trigger_field_changed(GtkWidget*, gpointer user) {
publish_trigger_fields(static_cast<App*>(user));
}
/// The same publish, for the two dropdowns. It needs its own entry point because
/// `notify::<prop>` passes a `GParamSpec*` ahead of the user data while
/// `GtkSpinButton::value-changed` does not: a two-parameter callback on a `notify::`
/// signal reads the pspec as its `App*` and crashes on the first field it touches.
void on_trigger_drop_changed(GObject*, GParamSpec*, gpointer user) {
publish_trigger_fields(static_cast<App*>(user));
}
/// The two dropdowns that change *which rows exist*, as opposed to a value within them.
/// Same `notify::selected` signature trap as `on_trigger_drop_changed`.
void on_trigger_shape_changed(GObject*, GParamSpec*, gpointer user) {
App* app = static_cast<App*>(user);
publish_trigger_fields(app);
sync_trigger_rows(*app);
}
void on_trigger_signal_changed(GObject* obj, GParamSpec*, gpointer user) {
App* app = static_cast<App*>(user);
if (app->updating_panels) return;
(void)obj;
const guint sel = gtk_drop_down_get_selected(GTK_DROP_DOWN(app->trigger_signal_drop));
if (sel == GTK_INVALID_LIST_POSITION) return;
{
std::lock_guard<std::mutex> lock(app->model->mutex());
if (sel >= app->model->channels().size()) return;
app->model->trigger().channel = static_cast<int>(sel);
}
app->trigger_level_touched = false; // a new signal gets a fresh default level
}
void on_trigger_toggled(GtkToggleButton* button, gpointer user) {
App* app = static_cast<App*>(user);
const bool on = gtk_toggle_button_get_active(button);
int signal = -1;
app->model->set_trigger_enabled(on); // takes the lock and mirrors the atomic
{
std::lock_guard<std::mutex> lock(app->model->mutex());
TriggerConfig& tc = app->model->trigger();
tc.armed = on; // trigger mode means "wait for an event", so it starts armed
signal = tc.channel;
}
app->event_held = false;
app->capturing = false;
if (!on) {
app->model->trigger_time_valid().store(false, std::memory_order_relaxed);
// The epoch is *not* written here: the frame tick owns it and states the live answer -- minus
// the newest acquired sample -- after it has bound and acquired, which is the only moment the
// right number is knowable. This used to set it to 0 ("seconds since the stream started"),
// which is no longer a display coordinate anything uses. Nothing is left stale in the
// meantime: the group follows the live edge again from here, and `Figure::layout()` re-derives
// the range from it on every frame.
app->rows->set_time_window(app->rows->time_window()); // and follow the live edge
}
if (on && signal >= 0) {
app->model->set_signal_enabled(static_cast<std::size_t>(signal), true);
}
gtk_revealer_set_reveal_child(GTK_REVEALER(app->trigger_revealer), on);
// The event-history button is part of trigger mode and the pane it opens is *on demand*; leaving
// trigger mode takes the button away and collapses the pane, so going back to streaming is not a
// column of stale events beside the plot. Recording is the other half of that answer, so this is
// `sync_events_availability()`'s call and not the trigger's alone.
sync_events_availability(*app);
gtk_button_set_label(GTK_BUTTON(app->arm_button), on ? "Disarm" : "Arm");
}
/// The way out of trigger mode, from inside the panel that froze the view: it is
/// the toolbar's trigger switch, so there is exactly one piece of state.
void on_go_live(GtkButton*, gpointer user) {
App* app = static_cast<App*>(user);
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(app->trigger_toggle), FALSE);
}
GtkWidget* build_trigger_panel(App& app) {
GtkWidget* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
gtk_widget_set_margin_top(box, 8);
gtk_widget_set_margin_bottom(box, 8);
gtk_widget_set_margin_start(box, 8);
gtk_widget_set_margin_end(box, 8);
GtkWidget* heading = gtk_label_new("Trigger");
gtk_widget_set_halign(heading, GTK_ALIGN_START);
gtk_box_append(GTK_BOX(box), heading);
app.trigger_signal_drop = gtk_drop_down_new(G_LIST_MODEL(gtk_string_list_new(nullptr)), nullptr);
g_signal_connect(app.trigger_signal_drop, "notify::selected",
G_CALLBACK(on_trigger_signal_changed), &app);
gtk_box_append(GTK_BOX(box), labeled_box("signal", app.trigger_signal_drop));
const char* types[] = {"edge", "pulse width", "window", "dropout", nullptr};
app.trigger_type_drop = gtk_drop_down_new_from_strings(types);
gtk_widget_set_tooltip_text(app.trigger_type_drop,
"what counts as an event; the fields below change to suit it");
gtk_box_append(GTK_BOX(box), labeled_box("condition", app.trigger_type_drop));
const char* edges[] = {"rising", "falling", "both", nullptr};
app.trigger_edge_drop = gtk_drop_down_new_from_strings(edges);
gtk_box_append(GTK_BOX(box), labeled_box("edge", app.trigger_edge_drop));
const char* cmps[] = {"<", ">", "within", "outside", nullptr};
app.trigger_cmp_drop = gtk_drop_down_new_from_strings(cmps);
gtk_widget_set_tooltip_text(app.trigger_cmp_drop,
"how the measured pulse width is compared: '<' is the glitch "
"trigger -- a pulse shorter than it should be");
app.trigger_cmp_row = labeled_box("width is", app.trigger_cmp_drop);
gtk_box_append(GTK_BOX(box), app.trigger_cmp_row);
app.trigger_width_spin = spin(0.0, 3600.0, 0.001, 6, 0.001);
app.trigger_width_row = labeled_box("width s", app.trigger_width_spin);
gtk_box_append(GTK_BOX(box), app.trigger_width_row);
app.trigger_width2_spin = spin(0.0, 3600.0, 0.001, 6, 0.002);
app.trigger_width2_row = labeled_box("and s", app.trigger_width2_spin);
gtk_box_append(GTK_BOX(box), app.trigger_width2_row);
// Zero, not the range's minimum: the level this row reads is unset until a channel is chosen,
// and the panel's sync writes the declared range's middle then -- but a dialog that opens at
// -1e12 is a number nobody typed.
app.trigger_level2_spin = spin(-1.0e12, 1.0e12, 0.01, 4, 0.0);
gtk_widget_set_tooltip_text(app.trigger_level2_spin,
"the band's other edge; it is kept at least one increment away "
"from the level, because a band of zero width 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");
app.trigger_level2_row = labeled_box("to level", app.trigger_level2_spin);
gtk_box_append(GTK_BOX(box), app.trigger_level2_row);
app.trigger_timeout_spin = spin(0.0, 3600.0, 0.001, 6, 0.001);
app.trigger_timeout_row = labeled_box("timeout s", app.trigger_timeout_spin);
gtk_box_append(GTK_BOX(box), app.trigger_timeout_row);
app.trigger_level_spin = spin(-1.0e12, 1.0e12, 0.01, 4, 0.0);
g_signal_connect(app.trigger_level_spin, "value-changed", G_CALLBACK(+[](GtkWidget*, gpointer u) {
static_cast<App*>(u)->trigger_level_touched = true;
}),
&app);
gtk_box_append(GTK_BOX(box), labeled_box("level", app.trigger_level_spin));
app.trigger_pre_spin = spin(0.0, 99.0, 1.0, 0, 50.0);
gtk_box_append(GTK_BOX(box), labeled_box("pre-trigger %", app.trigger_pre_spin));
app.trigger_holdoff_spin = spin(0.0, 3600.0, 0.001, 4,
app.model->config().trigger_holdoff_s);
gtk_box_append(GTK_BOX(box), labeled_box("hold-off s", app.trigger_holdoff_spin));
const char* modes[] = {"normal", "single", nullptr};
app.trigger_mode_drop = gtk_drop_down_new_from_strings(modes);
gtk_box_append(GTK_BOX(box), labeled_box("mode", app.trigger_mode_drop));
app.events_record_switch = gtk_switch_new();
gtk_widget_set_valign(app.events_record_switch, GTK_ALIGN_CENTER);
gtk_switch_set_active(GTK_SWITCH(app.events_record_switch), app.event_recording);
gtk_widget_set_tooltip_text(app.events_record_switch,
"keep every triggered acquisition in the event cache, with a "
"picture of each; the history button goes with it");
g_signal_connect(app.events_record_switch, "notify::active",
G_CALLBACK(on_events_record_toggled), &app);
// Not `labeled_box()`: that expands its child, which is what a spin button wants and what a switch
// must not have -- a switch's natural width *is* its size, so an expanded one stretches into a wide
// pill that reads as a slider. The label takes the slack instead.
{
GtkWidget* row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
GtkWidget* text = gtk_label_new("record events");
gtk_widget_set_halign(text, GTK_ALIGN_START);
gtk_widget_set_hexpand(text, TRUE);
gtk_box_append(GTK_BOX(row), text);
gtk_widget_set_halign(app.events_record_switch, GTK_ALIGN_END);
gtk_widget_set_valign(app.events_record_switch, GTK_ALIGN_CENTER);
gtk_box_append(GTK_BOX(row), app.events_record_switch);
gtk_box_append(GTK_BOX(box), row);
}
app.arm_button = gtk_button_new_with_label("Arm");
g_signal_connect(app.arm_button, "clicked", G_CALLBACK(on_arm_clicked), &app);
gtk_box_append(GTK_BOX(box), app.arm_button);
GtkWidget* live = gtk_button_new_with_label("Go live (trigger off)");
gtk_widget_set_tooltip_text(live, "disable trigger mode and stream continuously");
g_signal_connect(live, "clicked", G_CALLBACK(on_go_live), &app);
gtk_box_append(GTK_BOX(box), live);
for (GtkWidget* w : {app.trigger_level_spin, app.trigger_pre_spin, app.trigger_holdoff_spin,
app.trigger_width_spin, app.trigger_width2_spin,
app.trigger_level2_spin, app.trigger_timeout_spin}) {
g_signal_connect(w, "value-changed", G_CALLBACK(on_trigger_field_changed), &app);
}
g_signal_connect(app.trigger_edge_drop, "notify::selected",
G_CALLBACK(on_trigger_drop_changed), &app);
g_signal_connect(app.trigger_mode_drop, "notify::selected",
G_CALLBACK(on_trigger_drop_changed), &app);
// Searching *is* the trigger's condition applied to history, so it belongs in this panel
// rather than in one of its own: a second place to spell the same condition would be a
// second place to get it wrong. Collapsed by default -- it is not part of running a trigger.
GtkWidget* search_exp = gtk_expander_new("Search the buffer");
gtk_widget_set_tooltip_text(search_exp,
"find every place in the retained buffer where the condition "
"above holds, and step through them");
GtkWidget* sbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
gtk_widget_set_margin_top(sbox, 4);
GtkWidget* srun = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
app.search_run_button = gtk_button_new_with_label("Search");
gtk_widget_set_hexpand(app.search_run_button, true);
g_signal_connect(app.search_run_button, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) {
App* a = static_cast<App*>(user);
// Published, not read back off the widgets: the producer's copy is the
// one the live trigger uses, so the search asks exactly its question.
publish_trigger_fields(a);
EventCondition c;
int channel = -1;
{
std::lock_guard<std::mutex> lock(a->model->mutex());
c = a->model->trigger().condition;
channel = a->model->trigger().channel;
}
a->search.start(c, channel);
update_search_panel(*a);
}),
&app);
gtk_box_append(GTK_BOX(srun), app.search_run_button);
app.search_cancel_button = gtk_button_new_with_label("Stop");
gtk_widget_set_sensitive(app.search_cancel_button, false);
g_signal_connect(app.search_cancel_button, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) {
App* a = static_cast<App*>(user);
a->search.cancel();
update_search_panel(*a);
}),
&app);
gtk_box_append(GTK_BOX(srun), app.search_cancel_button);
gtk_box_append(GTK_BOX(sbox), srun);
app.search_status_label = gtk_label_new("not searched");
gtk_widget_set_halign(app.search_status_label, GTK_ALIGN_START);
gtk_label_set_wrap(GTK_LABEL(app.search_status_label), true);
gtk_widget_add_css_class(app.search_status_label, "dim-label");
gtk_box_append(GTK_BOX(sbox), app.search_status_label);
GtkWidget* snav = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
app.search_prev_button = gtk_button_new_from_icon_name("go-previous-symbolic");
gtk_widget_set_tooltip_text(app.search_prev_button, "the previous hit");
gtk_widget_set_sensitive(app.search_prev_button, false);
g_signal_connect(app.search_prev_button, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) {
App* a = static_cast<App*>(user);
if (a->search.prev()) search_jump(*a, a->search.hits()[static_cast<std::size_t>(
a->search.cursor())]);
update_search_panel(*a);
}),
&app);
gtk_box_append(GTK_BOX(snav), app.search_prev_button);
app.search_pos_label = gtk_label_new("--");
gtk_widget_set_hexpand(app.search_pos_label, true);
gtk_box_append(GTK_BOX(snav), app.search_pos_label);
app.search_next_button = gtk_button_new_from_icon_name("go-next-symbolic");
gtk_widget_set_tooltip_text(app.search_next_button, "the next hit");
gtk_widget_set_sensitive(app.search_next_button, false);
g_signal_connect(app.search_next_button, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) {
App* a = static_cast<App*>(user);
// The first press with no cursor goes to the first hit, not the second.
if (a->search.cursor() < 0) {
a->search.select(0);
} else if (!a->search.next()) {
update_search_panel(*a);
return;
}
search_jump(*a, a->search.hits()[static_cast<std::size_t>(
a->search.cursor())]);
update_search_panel(*a);
}),
&app);
gtk_box_append(GTK_BOX(snav), app.search_next_button);
gtk_box_append(GTK_BOX(sbox), snav);
app.search_marks_label = gtk_label_new(" ");
gtk_widget_set_halign(app.search_marks_label, GTK_ALIGN_START);
gtk_widget_add_css_class(app.search_marks_label, "dim-label");
// Ellipsize so the label's minimum width is bounded: without this, the longest text
// (~58 chars) would widen the side pane just as `search_status_label` would without its own
// wrap — the same fix the statistics label carries at line 2780.
gtk_label_set_ellipsize(GTK_LABEL(app.search_marks_label), PANGO_ELLIPSIZE_END);
gtk_box_append(GTK_BOX(sbox), app.search_marks_label);
gtk_expander_set_child(GTK_EXPANDER(search_exp), sbox);
gtk_box_append(GTK_BOX(box), search_exp);
g_signal_connect(app.trigger_type_drop, "notify::selected",
G_CALLBACK(on_trigger_shape_changed), &app);
g_signal_connect(app.trigger_cmp_drop, "notify::selected",
G_CALLBACK(on_trigger_shape_changed), &app);
// The clamp lives in `sync_trigger_rows()`, so the two level spins have to run it rather
// than the plain field publish.
for (GtkWidget* w : {app.trigger_level_spin, app.trigger_level2_spin}) {
g_signal_connect(w, "value-changed", G_CALLBACK(+[](GtkWidget*, gpointer u) {
sync_trigger_rows(*static_cast<App*>(u));
}),
&app);
}
sync_trigger_rows(app);
return box;
}
} // namespace ui_impl
} // namespace udpscope
+166
View File
@@ -0,0 +1,166 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 Martino Ferrari
//
// The small reusable pieces of GTK chrome: a menu row and the three rows built from it, a labelled\n// panel row, the one stylesheet rule the app needs, and the modal dialog frame every dialog here is\n// built in. They know nothing about this app beyond the widget types they return.
#include "ui_app.hpp"
#include <gtk/gtk.h>
#include <string>
namespace udpscope {
namespace ui_impl {
/// Installs the one style rule the app needs: a signal row that is as short as its
/// contents.
///
/// Adwaita gives a button a 34 px minimum height and a list row its own padding, and
/// those -- not the widgets in it -- are what set a signal row's height, so no amount
/// of trimming margins in C makes the list shorter. A handful of signals then filled
/// the whole side panel. Scoped to `.udpscope-compact` so it cannot reach any other
/// button in the window.
void install_css() {
static const char kCss[] =
"row.udpscope-compact { min-height: 0; padding: 1px 0; }\n"
// A popover menu row: Adwaita gives a button a 34 px minimum height and 8 px of padding,
// which is what made the overflow read as a stack of dialogs rather than a menu. The
// shortcut label is a plain .dim-label at the row's right, so all that is left to trim
// is the row itself.
"popover.udpscope-menu button { min-height: 0; padding: 3px 8px; }\n"
"popover.udpscope-menu button > box { min-height: 0; }\n"
"popover.udpscope-menu button label { min-height: 0; }\n"
"popover.udpscope-menu image { min-width: 12px; min-height: 0; }\n"
"popover.udpscope-menu separator { margin: 4px 0; }\n"
// The events pane: the row's time is what a person scans for, so it is the one thing in the
// column that is not dim; the thumbnail gets an edge, or a dark plot on a dark pane has none.
".udpscope-event-title { font-weight: bold; }\n"
".udpscope-event-thumb { border: 1px solid alpha(currentColor, 0.15); border-radius: 4px; }\n"
"row.udpscope-compact button { min-height: 20px; min-width: 20px; padding: 0 3px; }\n"
"row.udpscope-compact checkbutton { min-height: 20px; }\n";
GtkCssProvider* p = gtk_css_provider_new();
gtk_css_provider_load_from_string(p, kCss);
gtk_style_context_add_provider_for_display(gdk_display_get_default(),
GTK_STYLE_PROVIDER(p),
GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
g_object_unref(p);
}
/// Keeps a menu row's check in step with its toggle. The check keeps its allocation when it is
/// off (opacity, not visibility), so a checked and an unchecked row's labels line up.
void on_menu_row_toggled(GtkToggleButton* button, gpointer) {
GtkWidget* check = GTK_WIDGET(g_object_get_data(G_OBJECT(button), "udpscope-check"));
if (check != nullptr) {
gtk_widget_set_opacity(check, gtk_toggle_button_get_active(button) ? 1.0 : 0.0);
}
}
/// One row of a popover menu, laid out the way a GNOME menu lays one out: the label at the
/// left, the shortcut at the right when the action has one, and -- for a row that toggles
/// something -- a check in its own column. Returned as a box for the caller to hand to
/// `gtk_button_set_child()`; `.udpscope-menu` in install_css() is what trims the height.
GtkWidget* menu_row(const char* label, const char* accel, bool with_check,
GtkWidget** out_check) {
GtkWidget* box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10);
if (with_check) {
GtkWidget* check = gtk_image_new_from_icon_name("object-select-symbolic");
gtk_widget_set_opacity(check, 0.0);
gtk_box_append(GTK_BOX(box), check);
if (out_check != nullptr) *out_check = check;
}
GtkWidget* text = gtk_label_new(label);
gtk_label_set_xalign(GTK_LABEL(text), 0.0f);
gtk_widget_set_hexpand(text, true);
gtk_box_append(GTK_BOX(box), text);
if (accel != nullptr) {
GtkWidget* key = gtk_label_new(accel);
gtk_label_set_xalign(GTK_LABEL(key), 1.0f);
gtk_widget_add_css_class(key, "dim-label");
gtk_box_append(GTK_BOX(box), key);
}
return box;
}
/// A plain menu row. `accel` is what the app binds the action to (see shortcuts.cpp) -- it is
/// shown, never bound here: the key handling stays in the one controller that refuses a key
/// while a text field has focus.
GtkWidget* menu_button(const char* label, const char* accel) {
GtkWidget* button = gtk_button_new();
// The check column is reserved even though this row cannot be checked: a menu whose labels
// start at two different x -- one for the rows that can be checked and one for the rest --
// is what "the text is not left-aligned" looks like.
gtk_button_set_child(GTK_BUTTON(button), menu_row(label, accel, true, nullptr));
gtk_widget_add_css_class(button, "flat");
gtk_widget_set_hexpand(button, true);
gtk_widget_set_halign(button, GTK_ALIGN_FILL);
return button;
}
/// A menu row that toggles something, with the check menu_row() reserves the column for. The
/// initial state is set *before* the handler is connected: a programmatic `set_active()` emits
/// `toggled`, the handler acts on widgets the caller has not built yet (the signal list's
/// revealer is created later in run_ui()), and a row that is created on must not reach it.
GtkWidget* menu_toggle(const char* label, const char* accel, bool active) {
GtkWidget* button = gtk_toggle_button_new();
GtkWidget* check = nullptr;
gtk_button_set_child(GTK_BUTTON(button), menu_row(label, accel, true, &check));
gtk_widget_add_css_class(button, "flat");
gtk_widget_set_hexpand(button, true);
gtk_widget_set_halign(button, GTK_ALIGN_FILL);
g_object_set_data(G_OBJECT(button), "udpscope-check", check);
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(button), active);
on_menu_row_toggled(GTK_TOGGLE_BUTTON(button), nullptr);
g_signal_connect(button, "toggled", G_CALLBACK(on_menu_row_toggled), nullptr);
return button;
}
GtkWidget* menu_separator() {
return gtk_separator_new(GTK_ORIENTATION_HORIZONTAL);
}
GtkWidget* labeled_box(const char* label, GtkWidget* child) {
GtkWidget* box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
GtkWidget* l = gtk_label_new(label);
gtk_widget_set_halign(l, GTK_ALIGN_START);
gtk_box_append(GTK_BOX(box), l);
gtk_widget_set_hexpand(child, true);
gtk_box_append(GTK_BOX(box), child);
return box;
}
/// A modal, transient window with a vertical content box; the caller appends its
/// controls and then a button row. GTK 4.10 deprecated `GtkDialog` and
/// `GtkMessageDialog`, so the dialogs here are built from a plain window.
GtkWidget* dialog_window(GtkWidget* parent, const char* title) {
GtkWidget* w = gtk_window_new();
gtk_window_set_title(GTK_WINDOW(w), title);
if (parent != nullptr) gtk_window_set_transient_for(GTK_WINDOW(w), GTK_WINDOW(parent));
gtk_window_set_modal(GTK_WINDOW(w), TRUE);
gtk_window_set_default_size(GTK_WINDOW(w), 340, -1);
GtkWidget* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
gtk_widget_set_margin_top(box, 12);
gtk_widget_set_margin_bottom(box, 12);
gtk_widget_set_margin_start(box, 12);
gtk_widget_set_margin_end(box, 12);
gtk_window_set_child(GTK_WINDOW(w), box);
return w;
}
/// Appends `button` to a right-aligned row at the end of the dialog's box.
void dialog_add_button(GtkWidget* window, GtkWidget* button) {
GtkWidget* box = gtk_window_get_child(GTK_WINDOW(window));
GtkWidget* last = gtk_widget_get_last_child(box);
GtkWidget* row = nullptr;
if (last != nullptr && g_object_get_data(G_OBJECT(last), "udpscope-dialog-buttons") != nullptr) {
row = last;
} else {
row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
gtk_widget_set_halign(row, GTK_ALIGN_END);
g_object_set_data(G_OBJECT(row), "udpscope-dialog-buttons", GINT_TO_POINTER(1));
gtk_box_append(GTK_BOX(box), row);
}
gtk_box_append(GTK_BOX(row), button);
}
} // namespace ui_impl
} // namespace udpscope
+353
View File
@@ -0,0 +1,353 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 Martino Ferrari
#include "waveform.hpp"
#include <algorithm>
#include <cerrno>
#include <cmath>
#include <cstdlib>
#include <cstring>
#include <sys/stat.h>
#include "layout_store.hpp"
namespace udpscope {
namespace {
/// Reads `'<f4'`, `'<f8'` … out of one dtype entry and returns the bytes per element, 0 for a type
/// this loader does not know. The writer emits little-endian only, and the check that the host is
/// little-endian is the writer's own (`static_assert` there).
std::size_t dtype_bytes(const std::string& type) {
if (type == "<f8" || type == "<i8" || type == "<u8") return 8;
if (type == "<f4" || type == "<i4" || type == "<u4") return 4;
if (type == "<i2" || type == "<u2") return 2;
if (type == "|i1" || type == "|u1" || type == "|b1") return 1;
return 0;
}
/// The field list of a structured dtype: `[('time', '<f8'), ('ch1', '<f4')]` -> {name, type} pairs.
/// Deliberately a scanner rather than a parser: a `.npy` header is a Python dict literal, and the
/// three keys this reader needs are the only thing it may trust.
bool parse_fields(const std::string& descr, std::vector<std::pair<std::string, std::string>>* out) {
std::size_t i = 0;
while (true) {
const std::size_t open = descr.find("('", i);
if (open == std::string::npos) break;
std::size_t cursor = open + 2;
std::string name;
while (cursor < descr.size() && descr[cursor] != '\'') name.push_back(descr[cursor++]);
if (cursor >= descr.size()) return false;
++cursor; // past the name's closing quote, which a comma should follow
const std::size_t type_open = descr.find('\'', cursor);
if (type_open == std::string::npos) return false;
std::string type;
cursor = type_open + 1;
while (cursor < descr.size() && descr[cursor] != '\'') type.push_back(descr[cursor++]);
if (cursor >= descr.size()) return false;
out->emplace_back(name, type);
i = cursor + 1;
}
return !out->empty();
}
} // namespace
WaveformReader::~WaveformReader() { close(); }
void WaveformReader::close() noexcept {
if (f_ != nullptr) {
std::fclose(f_);
f_ = nullptr;
}
}
bool WaveformReader::open(const std::string& path, std::string* error) {
close();
f_ = std::fopen(path.c_str(), "rb");
if (f_ == nullptr) {
if (error != nullptr) *error = "cannot open '" + path + "': " + std::strerror(errno);
return false;
}
char magic[6] = {};
char version[2] = {};
if (std::fread(magic, 1, 6, f_) != 6 || std::fread(version, 1, 2, f_) != 2 ||
std::memcmp(magic, "\x93NUMPY", 6) != 0) {
if (error != nullptr) *error = "'" + path + "' is not a NumPy file";
close();
return false;
}
std::uint8_t len_bytes[4] = {};
std::size_t header_len = 0;
if (version[0] == 1) {
if (std::fread(len_bytes, 1, 2, f_) != 2) {
if (error != nullptr) *error = "'" + path + "' has a truncated header";
close();
return false;
}
header_len = static_cast<std::size_t>(len_bytes[0]) | (std::size_t(len_bytes[1]) << 8);
} else {
// v2.0 (and v3.0, which is UTF-8 and the same layout): a 4-byte little-endian length.
if (std::fread(len_bytes, 1, 4, f_) != 4) {
if (error != nullptr) *error = "'" + path + "' has a truncated header";
close();
return false;
}
header_len = std::size_t(len_bytes[0]) | (std::size_t(len_bytes[1]) << 8) |
(std::size_t(len_bytes[2]) << 16) | (std::size_t(len_bytes[3]) << 24);
}
std::string header(header_len, '\0');
if (header_len == 0 || std::fread(header.data(), 1, header_len, f_) != header_len) {
if (error != nullptr) *error = "'" + path + "' has a truncated header";
close();
return false;
}
// `descr` is either the field list (a structured array, which is what this project writes) or a
// single type string (a plain array, which carries no names and so cannot say what its columns
// are).
std::vector<std::pair<std::string, std::string>> fields;
const std::size_t descr_at = header.find("'descr':");
if (descr_at == std::string::npos) {
if (error != nullptr) *error = "'" + path + "' has no dtype";
close();
return false;
}
const std::size_t descr_end = header.find('\'', descr_at + 9);
const std::size_t bracket = header.find('(', descr_at);
if (bracket != std::string::npos && (descr_end == std::string::npos || bracket < descr_end + 1)) {
if (!parse_fields(header.substr(descr_at), &fields)) {
if (error != nullptr) *error = "'" + path + "': could not read the column names";
close();
return false;
}
} else {
if (error != nullptr) {
*error = "'" + path + "' is a plain array, not a udpscope export: its columns have no names";
}
close();
return false;
}
// The two time columns by name, the rest as signals -- and every one of them must be a type this
// loader can widen.
bool have_time = false;
bool have_abs = false;
record_bytes_ = 0;
rows_read_ = 0;
names_.clear();
field_bytes_.clear();
for (const auto& [name, type] : fields) {
const std::size_t bytes = dtype_bytes(type);
if (bytes == 0) {
if (error != nullptr) *error = "'" + path + "' has a column of an unsupported type '" + type + "'";
close();
return false;
}
record_bytes_ += bytes;
field_bytes_.push_back(bytes);
if (name == "time") {
have_time = true;
} else if (name == "abs_seconds") {
have_abs = true;
} else {
names_.push_back(name);
}
}
if (!have_time || !have_abs) {
if (error != nullptr) {
*error = "'" + path + "' is not a udpscope export: no 'time' and 'abs_seconds' columns";
}
close();
return false;
}
const std::size_t shape_at = header.find("'shape':");
if (shape_at == std::string::npos) {
if (error != nullptr) *error = "'" + path + "' has no shape";
close();
return false;
}
const std::size_t open_paren = header.find('(', shape_at);
if (open_paren == std::string::npos) {
if (error != nullptr) *error = "'" + path + "' has an unreadable shape";
close();
return false;
}
rows_ = static_cast<std::size_t>(std::strtoull(header.c_str() + open_paren + 1, nullptr, 10));
// The rate comes from the first two stamps, and reading them needs the file positioned: peek, then
// rewind. An export's time column is the ring's own stamps, which are uniform -- one dt describes
// all of it.
const long data_at = std::ftell(f_);
if (rows_ >= 2) {
std::vector<char> first(record_bytes_);
if (std::fread(first.data(), 1, record_bytes_, f_) == record_bytes_) {
double t0 = 0.0;
std::memcpy(&t0, first.data(), 8);
if (std::fread(first.data(), 1, record_bytes_, f_) == record_bytes_) {
double t1 = 0.0;
std::memcpy(&t1, first.data(), 8);
sample_dt_ = std::abs(t1 - t0);
}
}
std::fseek(f_, data_at, SEEK_SET);
}
return true;
}
bool WaveformReader::next(double* time, double* abs_seconds, float* values) {
if (f_ == nullptr || record_bytes_ == 0) return false;
if (rows_read_ >= rows_) return false;
std::vector<char> record(record_bytes_);
if (std::fread(record.data(), 1, record_bytes_, f_) != record_bytes_) return false;
const char* p = record.data();
std::memcpy(time, p, 8);
p += 8;
std::memcpy(abs_seconds, p, 8);
p += 8;
// The signals are the fields after the two time columns, each at its own width: `write_npy()`
// emits `<f4`, and a file an external tool widened to `<f8` is read as one rather than mis-sliced.
for (std::size_t i = 0; i < names_.size(); ++i) {
const std::size_t bytes = field_bytes_[2 + i];
switch (bytes) {
case 8: {
double v = 0.0;
std::memcpy(&v, p, 8);
values[i] = static_cast<float>(v);
break;
}
case 4: {
float v = 0.0f;
std::memcpy(&v, p, 4);
values[i] = v;
break;
}
default: {
// A narrower integer column: widen through the integer types rather than reinterpreting.
long long v = 0;
switch (bytes) {
case 2: {
short s = 0;
std::memcpy(&s, p, 2);
v = s;
break;
}
default:
v = static_cast<signed char>(*p);
break;
}
values[i] = static_cast<float>(v);
break;
}
}
p += bytes;
}
++rows_read_;
return true;
}
bool load_waveform_into_model(ScopeModel& model, const std::string& path,
WaveformLoadReport* report, std::string* error) {
WaveformReader reader;
if (!reader.open(path, error)) return false;
if (reader.names().empty() || reader.rows() == 0) {
if (error != nullptr) *error = "'" + path + "' holds no samples";
return false;
}
const double dt = reader.sample_dt();
const double rate = dt > 0.0 ? 1.0 / dt : 0.0;
// No lock held across this: every model call below takes the model's own, and they are the ones
// that must be atomic with each other -- holding it here deadlocked on the first of them.
// The span first: the plan that the enables below build has to cover the whole file, or the oldest
// samples are written past the ring's end and the file arrives as its own tail.
std::vector<SignalInfo> signals;
signals.reserve(reader.names().size());
for (const std::string& name : reader.names()) {
SignalInfo s;
s.name = name;
s.sampling_rate = rate > 0.0 ? rate : 1.0;
signals.push_back(std::move(s));
}
model.set_target_span(reader.rows() * (dt > 0.0 ? dt : 0.001));
model.set_signals(std::move(signals));
for (std::size_t i = 0; i < reader.names().size(); ++i) model.set_signal_enabled(i, true);
const std::size_t channels = model.channels().size();
if (channels == 0) {
if (error != nullptr) *error = "'" + path + "': the model built no channels for its columns";
return false;
}
std::vector<float> values(reader.names().size(), 0.0f);
std::size_t pushed = 0;
double stamp = 0.0;
double abs_stamp = 0.0;
double first = 0.0;
double last = 0.0;
while (reader.next(&stamp, &abs_stamp, values.data())) {
if (pushed == 0) first = stamp;
last = stamp;
model.push_loaded_slot(values, stamp, abs_stamp);
++pushed;
}
reader.close();
model.set_epoch_if_unset(first);
// The newest stamp is "now": the axis then reads `[-window, 0]`, which is the window the export was
// taken from.
model.set_display_epoch(pushed > 0 ? last : 0.0);
if (report != nullptr) {
report->signals = channels;
report->samples = pushed;
report->span_s = pushed > 0 ? std::abs(last - first) : 0.0;
report->rate_hz = rate;
report->first_time = first;
report->last_time = last;
}
if (pushed == 0) {
if (error != nullptr) *error = "'" + path + "' could not be read to the end";
return false;
}
return true;
}
namespace {
std::string waveforms_path() {
const std::string dir = config_dir();
::mkdir(dir.c_str(), 0755); // EEXIST is the common case and not an error
return dir + "/waveforms.txt";
}
} // namespace
std::vector<std::string> recent_waveforms() {
std::vector<std::string> out;
std::FILE* f = std::fopen(waveforms_path().c_str(), "rb");
if (f == nullptr) return out;
char line[4096];
while (std::fgets(line, sizeof(line), f) != nullptr) {
std::string path(line);
while (!path.empty() && (path.back() == '\n' || path.back() == '\r')) path.pop_back();
if (!path.empty()) out.push_back(path);
}
std::fclose(f);
return out;
}
void remember_waveform(const std::string& path) {
std::vector<std::string> list = recent_waveforms();
list.erase(std::remove(list.begin(), list.end(), path), list.end());
list.insert(list.begin(), path);
// A list long enough to be a history, short enough to be a menu: twenty entries is more than a
// session's worth of exports and far less than a directory's.
if (list.size() > 20) list.resize(20);
std::FILE* f = std::fopen(waveforms_path().c_str(), "wb");
if (f == nullptr) return;
for (const std::string& p : list) std::fprintf(f, "%s\n", p.c_str());
std::fclose(f);
}
} // namespace udpscope
+84
View File
@@ -0,0 +1,84 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 Martino Ferrari
#pragma once
#include "model.hpp"
#include <cstdio>
#include <string>
#include <vector>
namespace udpscope {
/// A `.npy` written by `write_npy()`: one packed structured array whose *dtype names the columns*
/// (`time`, `abs_seconds`, then one field per signal). That is what makes an export readable again
/// without a sidecar: the signal names are in the file.
///
/// Read one record at a time, not into a table: an export of a wide window on a fast stream is
/// hundreds of megabytes, and the model's rings are the copy that matters -- a second full copy in
/// a vector would double the peak for nothing.
class WaveformReader {
public:
WaveformReader() = default;
~WaveformReader();
WaveformReader(const WaveformReader&) = delete;
WaveformReader& operator=(const WaveformReader&) = delete;
/// Parses the header and rewinds to the first record. On failure `error` says what the file is
/// (not a NumPy file, not a udpscope export, truncated) and nothing else is usable.
bool open(const std::string& path, std::string* error);
void close() noexcept;
std::size_t rows() const noexcept { return rows_; }
/// The *signal* columns, in file order: `time` and `abs_seconds` are the two time columns and are
/// read through `next()`, not listed here.
const std::vector<std::string>& names() const noexcept { return names_; }
/// Seconds between the first two records: an export's `time` column is the ring's own stamps, which
/// are uniform at the stored rate. 0 when there are fewer than two rows.
double sample_dt() const noexcept { return sample_dt_; }
/// Reads the next record into `time`, `abs_seconds` and `values` (`names().size()` of them).
/// False at the end of the file or on a short read.
bool next(double* time, double* abs_seconds, float* values);
private:
std::FILE* f_ = nullptr;
std::size_t rows_ = 0;
std::size_t rows_read_ = 0;
std::size_t record_bytes_ = 0;
double sample_dt_ = 0.0;
std::vector<std::string> names_;
/// Bytes per *field*, in file order: the two time columns then the signals. Kept so a column an
/// external tool widened to `<f8` is read as one rather than mis-sliced by an assumed width.
std::vector<std::size_t> field_bytes_;
};
/// What a load put into the model, for the log and the status line.
struct WaveformLoadReport {
std::size_t signals = 0;
std::size_t samples = 0;
double span_s = 0.0;
double rate_hz = 0.0;
double first_time = 0.0;
double last_time = 0.0;
};
/// Loads a `.npy` export into `model` as if it had been streamed: one signal and channel per
/// column, every sample pushed into the live ring, and the display epoch pinned to the file's last
/// stamp so the axis reads `[-window, 0]` exactly as the export did.
///
/// Takes `model.mutex()` for the whole load. There is no acquisition behind this -- nothing arrives
/// afterwards -- so the caller must not start one, and the UI must not expect frames.
bool load_waveform_into_model(ScopeModel& model, const std::string& path,
WaveformLoadReport* report, std::string* error);
/// The waveform files opened before, newest first: one path per line in
/// `<config_dir>/waveforms.txt`. Written by `remember_waveform()`, which is called on every
/// successful open -- including the one the command line asked for, so the list fills up from
/// either entry point.
std::vector<std::string> recent_waveforms();
void remember_waveform(const std::string& path);
} // namespace udpscope
+25 -30
View File
@@ -1,30 +1,25 @@
1. fix vertical lines to small to be seen: in some cases (e.g. rect waveform in @mock_upstreamer.py) the vertical lines are to thin to be visible (sometime they glitch in and out of existence):
1. the antialisaing strategy with phosphore effect should be applied with more stregth
2. the lines should be more tick
3. the sample points should be bigger
2. ~~relative timing~~ **done**: `--relative-time`; see README and SPECS.md for the measured history cost.
- in order to avoid problem with time jitter user can decide to go in relative time mode
- in this mode absolute signal time is only use for export
- the plot time depends just on signal sample rate and frame number, e.g.:
- user is acquiring a signal with 1MSps and 100_000 samples per udp frame
- last frame as number <N> and previous <N-2> in this case a 100_000 indexes must be skipped
- time is simply: `((<N_frame> - <N_last>) * <n_samples_per_frame> - <sample_index>) / <sample rate>` (or something like that)
3. named configurations: in order to have multiples configurations for the same sources configuration can also have comments
4. optional udp frame storage for debugging of strange behaviours (store last <N>, default = 1000, udp packets binary content on disk for forensic analyss)
5. open exported binary waveforms to view/analyse, in the source selection dialog there should be the option to open exported waveform (maybe a tab with latest opened file and option to open new ones?)
6. imporve right click menu on the plots:
- use same compact style of main menu
- add a [remove signal] entry that open a submenu to delete signasl
7. in layout edit mode add the option to drag plot border to resize them
8. add option to automatic save the last <N> triggered acquisitions in a cache folder and let the user replay them
- a collapsable pane on the left with the list of the last <N> acqusition with duration number of signals etc
- user select one and immidiatly open it
- this option would only be available in trigger mode
9. preferences dialog:
- user can choose evnt cache directory (default <home>/.cache/udpscope/<source>/events/)
- maximum number of events to cache
- disable completly the events storage option
- enable/disable relative time mode
- interface dark/light/auto color
- defaults
- other parameters
- [x] (1) fix vertical lines to small to be seen: in some cases (e.g. rect waveform in @mock_upstreamer.py) the vertical lines are to thin to be visible (sometime they glitch in and out of existence):
1. the antialisaing strategy with phosphore effect should be applied with more stregth
2. the lines should be more tick
3. the sample points should be bigger
- [x] (2) ~~relative timing~~ **done**: `--relative-time`; see README and SPECS.md for the measured history cost.
- in order to avoid problem with time jitter user can decide to go in relative time mode
- in this mode absolute signal time is only use for export
- the plot time depends just on signal sample rate and frame number, e.g.:
- user is acquiring a signal with 1MSps and 100_000 samples per udp frame
- last frame as number <N> and previous <N-2> in this case a 100_000 indexes must be skipped
- time is simply: `((<N_frame> - <N_last>) * <n_samples_per_frame> - <sample_index>) / <sample rate>` (or something like that)
- [x] (3) ~~named configurations~~ **done**: one directory per source holding `<name>.toml` per configuration plus `active`; a configuration carries a note and the startup settings (`buffer_mb`, `relative_time`, `capture_packets`); the chooser lists them and the menu's _Configurations…_ opens/saves/deletes them.
- [x] (4) ~~optional udp frame storage for debugging of strange behaviours~~ **done**: `--capture-packets N`; see README and the design note in `docs/superpowers/specs/` for the ring file, the popover toggle and `tools/capture_to_pcap.py`.
- [x] (5) ~~open exported binary waveforms to view/analyse~~ **done**: `--open FILE` and the chooser's **Waveforms** page (the files opened before, newest first, plus *Open…*); the `.npy`'s dtype names the columns, so a load restores the signals, the samples and the axis the export had, and every instrument works on it — see README and `docs/claude/invariants-data-path.md`.
- [x] (6) ~~imporve right click menu on the plots~~ **done**: the plot's context menu uses the main menu's row style (`menu_button` + `.udpscope-menu`, asserted by `UDPSCOPE_PROBE_ROWMENU_MS`: uniform `label_x=26`, `h=18`, the shortcut flush right), and deleting a signal is a **Remove signal** submenu listing that plot's traces plus _All traces on this plot_.
- [ ] (7) in layout edit mode add the option to drag plot border to resize them
- [x] (8) ~~add option to automatic save the last <N> triggered acquisitions in a cache folder and let the user replay them~~ **done**: every latch is written to `<cache>/<source>/events/` as a `.npy` (the export format), a `.txt` sidecar and a `.bmp` preview drawn from the samples; the **events pane** (revealed with trigger mode, switchable) lists them with time, duration, signals and rate, shows the preview, replays one through the `--open` loader and has *Live* to go back -- see README and `docs/claude/invariants-trigger-and-pinning.md`.
- [ ] (9) preferences dialog:
- user can choose evnt cache directory (default <home>/.cache/udpscope/<source>/events/)
- maximum number of events to cache
- disable completly the events storage option
- enable/disable relative time mode
- interface dark/light/auto color
- defaults
- other parameters
+11
View File
@@ -0,0 +1,11 @@
// SPDX-License-Identifier: MIT
// Generated by cmake/write_build_info.cmake. Do not edit, do not commit.
#include "build_info.hpp"
namespace udpscope::build_info {
const char* const kVersion = "0.1.0";
const char* const kCommit = "210d124-dirty";
const char* const kTimestamp = "2026-10-09 11:00 UTC";
} // namespace udpscope::build_info
+135
View File
@@ -0,0 +1,135 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
# SPDX-FileCopyrightText: 2026 Martino Ferrari
"""Converts a udpscope packet capture to a classic pcap file.
$ tools/capture_to_pcap.py ~/.cache/udpscope/192.168.1.40-5000/capture.bin out.pcap
1000 slots, 412 stored, 0 torn, 0 truncated -> 412 packets, 26.1 MiB
$ wireshark out.pcap
The capture is udpscope's ring of the last N datagrams exactly as recv() returned
them (see docs/superpowers/specs/2026-10-09-udp-packet-capture-design.md): a 64-byte
header, then fixed-size slots whose trailing `seq` word is written last, so a slot
carrying a non-zero `seq` is complete even after a crash. This script walks the slots,
orders them by `seq`, skips the incomplete ones, and wraps every record in a
synthesised Ethernet/IPv4/UDP header built from the capture header's peer address and
ports -- pure standard library, no numpy, so it runs wherever the self-check runs.
The counts it prints to stderr are a diagnostic in their own right: `torn` or
`truncated` above zero is a finding about the capture before anything about the stream.
"""
import struct
import sys
MAGIC = b"UDPSCAP\x00"
HEADER_BYTES = 64
RECORD_FIXED = 16 # t_arrival f64, len u32, orig_len u32
SEQ_BYTES = 8
PCAP_MAGIC = 0xA1B23C4D # microsecond-resolution, writing the non-nanosecond classic
LINKTYPE_ETHERNET = 1
def fail(msg):
print(f"capture_to_pcap: {msg}", file=sys.stderr)
sys.exit(2)
def main(argv):
if len(argv) != 3:
fail(f"usage: {argv[0]} capture.bin out.pcap")
src, dst = argv[1], argv[2]
with open(src, "rb") as f:
blob = f.read()
if len(blob) < HEADER_BYTES:
fail(f"{src}: {len(blob)} bytes, smaller than a capture header")
magic, version, slot_count, slot_bytes, cursor = struct.unpack_from("<8sIIII", blob, 0)
(t0,) = struct.unpack_from("<d", blob, 24)
peer_addr, peer_port, local_port = struct.unpack_from("<IHH", blob, 32)
if magic != MAGIC:
fail(f"{src}: bad magic {magic!r}")
if version != 1:
fail(f"{src}: unsupported capture version {version}")
payload_bytes = slot_bytes - RECORD_FIXED - SEQ_BYTES
if slot_count == 0 or payload_bytes <= 0:
fail(f"{src}: implausible geometry: {slot_count} slots of {slot_bytes} bytes")
if len(blob) < HEADER_BYTES + slot_count * slot_bytes:
fail(f"{src}: truncated file: {len(blob)} bytes for {slot_count} slots")
records = []
stored = torn = truncated = empty = 0
payload_sum = 0
for i in range(slot_count):
off = HEADER_BYTES + i * slot_bytes
slot = blob[off : off + slot_bytes]
(t_arrival, length, orig_len) = struct.unpack_from("<dII", slot, 0)
(seq,) = struct.unpack_from("<Q", slot, slot_bytes - SEQ_BYTES)
if seq == 0 and length == 0 and orig_len == 0:
empty += 1
continue
if seq == 0:
torn += 1
continue
length = min(length, payload_bytes)
if orig_len > payload_bytes:
truncated += 1
records.append((seq, t_arrival, slot[RECORD_FIXED : RECORD_FIXED + length]))
payload_sum += length
stored += 1
records.sort(key=lambda r: r[0])
ip = bytes([(peer_addr >> 24) & 0xFF, (peer_addr >> 16) & 0xFF, (peer_addr >> 8) & 0xFF,
peer_addr & 0xFF])
zero_ip = b"\x00\x00\x00\x00"
with open(dst, "wb") as out:
out.write(struct.pack("<IHHiIII", PCAP_MAGIC, 0, 0, 0, 0, 0, LINKTYPE_ETHERNET))
for seq, t_arrival, payload in records:
# The client stamps arrivals with CLOCK_REALTIME, so a `t_arrival` is
# already an epoch; `t0_realtime` is the epoch a *relative* stamp would be
# measured from. Take whichever the file carries.
ts = t_arrival if t_arrival > 1e6 else t0 + t_arrival
sec = int(ts)
usec = int(round((ts - sec) * 1e6))
if usec >= 1_000_000:
sec, usec = sec + 1, usec - 1_000_000
udp_len = 8 + len(payload)
udp = struct.pack(">HHHH", peer_port, local_port, udp_len, 0) + payload
total_len = 20 + udp_len
ip_hdr = struct.pack(
">BBHHHBBH4s4s",
0x45, # version 4, IHL 5
0, # DSCP
total_len,
seq & 0xFFFF, # id: the capture's own ordinal, which is what a
# duplicate datagram would show as a repeated id
0, # flags/offset
64, 17, # TTL, proto UDP
0, # checksum: wireshark recomputes and says "incorrect",
# which is honest for a synthesised header
zero_ip, # source: the capture is on the receiver, so the local
# address is unknown to it
ip, # destination: the source we captured from
)
frame = b"\x00" * 6 + b"\x01" * 6 + b"\x08\x00" + ip_hdr + udp
out.write(struct.pack("<IIII", sec, usec, len(frame), len(frame)))
out.write(frame)
mib = len(blob) / 1048576.0
print(
f"{slot_count} slots, {stored} stored, {torn} torn, {truncated} truncated, "
f"{empty} empty -> {len(records)} packets, {mib:.1f} MiB, "
f"payload {payload_sum} bytes",
file=sys.stderr,
)
if __name__ == "__main__":
main(sys.argv)
@@ -0,0 +1,16 @@
# CMAKE generated file: DO NOT EDIT!
# Generated by "Unix Makefiles" Generator, CMake Version 4.4
# Relative path conversion top directories.
set(CMAKE_RELATIVE_PATH_TOP_SOURCE "/home/martino/Projects/tools/udpscope")
set(CMAKE_RELATIVE_PATH_TOP_BINARY "/home/martino/Projects/tools/udpscope/tools")
# Force unix paths in dependencies.
set(CMAKE_FORCE_UNIX_PATHS 1)
# The C and CXX include file regular expressions for this directory.
set(CMAKE_C_INCLUDE_REGEX_SCAN "^.*$")
set(CMAKE_C_INCLUDE_REGEX_COMPLAIN "^$")
set(CMAKE_CXX_INCLUDE_REGEX_SCAN ${CMAKE_C_INCLUDE_REGEX_SCAN})
set(CMAKE_CXX_INCLUDE_REGEX_COMPLAIN ${CMAKE_C_INCLUDE_REGEX_COMPLAIN})
@@ -0,0 +1,47 @@
# Consider dependencies only in project.
set(CMAKE_DEPENDS_IN_PROJECT_ONLY OFF)
# The set of languages for which implicit dependencies are needed:
set(CMAKE_DEPENDS_LANGUAGES
)
# The set of dependency files which are needed:
set(CMAKE_DEPENDS_DEPENDENCY_FILES
"/home/martino/Projects/tools/udpscope/deps/plotter/src/core/status.cpp" "deps/plotter/CMakeFiles/plot_core.dir/src/core/status.cpp.o" "gcc" "deps/plotter/CMakeFiles/plot_core.dir/src/core/status.cpp.o.d"
"/home/martino/Projects/tools/udpscope/deps/plotter/src/data/array_series.cpp" "deps/plotter/CMakeFiles/plot_core.dir/src/data/array_series.cpp.o" "gcc" "deps/plotter/CMakeFiles/plot_core.dir/src/data/array_series.cpp.o.d"
"/home/martino/Projects/tools/udpscope/deps/plotter/src/data/minmax_pyramid.cpp" "deps/plotter/CMakeFiles/plot_core.dir/src/data/minmax_pyramid.cpp.o" "gcc" "deps/plotter/CMakeFiles/plot_core.dir/src/data/minmax_pyramid.cpp.o.d"
"/home/martino/Projects/tools/udpscope/deps/plotter/src/data/series.cpp" "deps/plotter/CMakeFiles/plot_core.dir/src/data/series.cpp.o" "gcc" "deps/plotter/CMakeFiles/plot_core.dir/src/data/series.cpp.o.d"
"/home/martino/Projects/tools/udpscope/deps/plotter/src/data/stream_series.cpp" "deps/plotter/CMakeFiles/plot_core.dir/src/data/stream_series.cpp.o" "gcc" "deps/plotter/CMakeFiles/plot_core.dir/src/data/stream_series.cpp.o.d"
"/home/martino/Projects/tools/udpscope/deps/plotter/src/data/xy_series.cpp" "deps/plotter/CMakeFiles/plot_core.dir/src/data/xy_series.cpp.o" "gcc" "deps/plotter/CMakeFiles/plot_core.dir/src/data/xy_series.cpp.o.d"
"/home/martino/Projects/tools/udpscope/deps/plotter/src/gl/handle.cpp" "deps/plotter/CMakeFiles/plot_core.dir/src/gl/handle.cpp.o" "gcc" "deps/plotter/CMakeFiles/plot_core.dir/src/gl/handle.cpp.o.d"
"/home/martino/Projects/tools/udpscope/deps/plotter/src/gl/loader.cpp" "deps/plotter/CMakeFiles/plot_core.dir/src/gl/loader.cpp.o" "gcc" "deps/plotter/CMakeFiles/plot_core.dir/src/gl/loader.cpp.o.d"
"/home/martino/Projects/tools/udpscope/deps/plotter/src/input/controller.cpp" "deps/plotter/CMakeFiles/plot_core.dir/src/input/controller.cpp.o" "gcc" "deps/plotter/CMakeFiles/plot_core.dir/src/input/controller.cpp.o.d"
"/home/martino/Projects/tools/udpscope/deps/plotter/src/math/transform.cpp" "deps/plotter/CMakeFiles/plot_core.dir/src/math/transform.cpp.o" "gcc" "deps/plotter/CMakeFiles/plot_core.dir/src/math/transform.cpp.o.d"
"/home/martino/Projects/tools/udpscope/deps/plotter/src/plot/axis.cpp" "deps/plotter/CMakeFiles/plot_core.dir/src/plot/axis.cpp.o" "gcc" "deps/plotter/CMakeFiles/plot_core.dir/src/plot/axis.cpp.o.d"
"/home/martino/Projects/tools/udpscope/deps/plotter/src/plot/figure.cpp" "deps/plotter/CMakeFiles/plot_core.dir/src/plot/figure.cpp.o" "gcc" "deps/plotter/CMakeFiles/plot_core.dir/src/plot/figure.cpp.o.d"
"/home/martino/Projects/tools/udpscope/deps/plotter/src/plot/grid.cpp" "deps/plotter/CMakeFiles/plot_core.dir/src/plot/grid.cpp.o" "gcc" "deps/plotter/CMakeFiles/plot_core.dir/src/plot/grid.cpp.o.d"
"/home/martino/Projects/tools/udpscope/deps/plotter/src/plot/measure.cpp" "deps/plotter/CMakeFiles/plot_core.dir/src/plot/measure.cpp.o" "gcc" "deps/plotter/CMakeFiles/plot_core.dir/src/plot/measure.cpp.o.d"
"/home/martino/Projects/tools/udpscope/deps/plotter/src/plot/plot.cpp" "deps/plotter/CMakeFiles/plot_core.dir/src/plot/plot.cpp.o" "gcc" "deps/plotter/CMakeFiles/plot_core.dir/src/plot/plot.cpp.o.d"
"/home/martino/Projects/tools/udpscope/deps/plotter/src/plot/ticks.cpp" "deps/plotter/CMakeFiles/plot_core.dir/src/plot/ticks.cpp.o" "gcc" "deps/plotter/CMakeFiles/plot_core.dir/src/plot/ticks.cpp.o.d"
"/home/martino/Projects/tools/udpscope/deps/plotter/src/render/buffer.cpp" "deps/plotter/CMakeFiles/plot_core.dir/src/render/buffer.cpp.o" "gcc" "deps/plotter/CMakeFiles/plot_core.dir/src/render/buffer.cpp.o.d"
"/home/martino/Projects/tools/udpscope/deps/plotter/src/render/embedded_mono_font.cpp" "deps/plotter/CMakeFiles/plot_core.dir/src/render/embedded_mono_font.cpp.o" "gcc" "deps/plotter/CMakeFiles/plot_core.dir/src/render/embedded_mono_font.cpp.o.d"
"/home/martino/Projects/tools/udpscope/deps/plotter/src/render/primitives.cpp" "deps/plotter/CMakeFiles/plot_core.dir/src/render/primitives.cpp.o" "gcc" "deps/plotter/CMakeFiles/plot_core.dir/src/render/primitives.cpp.o.d"
"/home/martino/Projects/tools/udpscope/deps/plotter/src/render/program.cpp" "deps/plotter/CMakeFiles/plot_core.dir/src/render/program.cpp.o" "gcc" "deps/plotter/CMakeFiles/plot_core.dir/src/render/program.cpp.o.d"
"/home/martino/Projects/tools/udpscope/deps/plotter/src/render/renderer.cpp" "deps/plotter/CMakeFiles/plot_core.dir/src/render/renderer.cpp.o" "gcc" "deps/plotter/CMakeFiles/plot_core.dir/src/render/renderer.cpp.o.d"
"/home/martino/Projects/tools/udpscope/deps/plotter/src/render/shaders.cpp" "deps/plotter/CMakeFiles/plot_core.dir/src/render/shaders.cpp.o" "gcc" "deps/plotter/CMakeFiles/plot_core.dir/src/render/shaders.cpp.o.d"
"/home/martino/Projects/tools/udpscope/deps/plotter/src/render/text.cpp" "deps/plotter/CMakeFiles/plot_core.dir/src/render/text.cpp.o" "gcc" "deps/plotter/CMakeFiles/plot_core.dir/src/render/text.cpp.o.d"
"/home/martino/Projects/tools/udpscope/deps/plotter/src/render/text_atlas.cpp" "deps/plotter/CMakeFiles/plot_core.dir/src/render/text_atlas.cpp.o" "gcc" "deps/plotter/CMakeFiles/plot_core.dir/src/render/text_atlas.cpp.o.d"
"/home/martino/Projects/tools/udpscope/deps/plotter/src/render/vertex_array.cpp" "deps/plotter/CMakeFiles/plot_core.dir/src/render/vertex_array.cpp.o" "gcc" "deps/plotter/CMakeFiles/plot_core.dir/src/render/vertex_array.cpp.o.d"
)
# Targets to which this target links which contain Fortran sources.
set(CMAKE_Fortran_TARGET_LINKED_INFO_FILES
)
# Targets to which this target links which contain Fortran sources.
set(CMAKE_Fortran_TARGET_FORWARD_LINKED_INFO_FILES
)
# Fortran module output directory.
set(CMAKE_Fortran_TARGET_MODULE_DIR "")
@@ -0,0 +1,498 @@
# CMAKE generated file: DO NOT EDIT!
# Generated by "Unix Makefiles" Generator, CMake Version 4.4
# Delete rule output on recipe failure.
.DELETE_ON_ERROR:
#=============================================================================
# Special targets provided by cmake.
# Disable implicit rules so canonical targets will work.
.SUFFIXES:
# Disable VCS-based implicit rules.
% : %,v
# Disable VCS-based implicit rules.
% : RCS/%
# Disable VCS-based implicit rules.
% : RCS/%,v
# Disable VCS-based implicit rules.
% : SCCS/s.%
# Disable VCS-based implicit rules.
% : s.%
.SUFFIXES: .hpux_make_needs_suffix_list
# Command-line flag to silence nested $(MAKE).
$(VERBOSE)MAKESILENT = -s
#Suppress display of executed commands.
$(VERBOSE).SILENT:
# A target that is always out of date.
cmake_force:
.PHONY : cmake_force
#=============================================================================
# Set environment variables for the build.
# The shell in which to execute make rules.
SHELL = /bin/sh
# The CMake executable.
CMAKE_COMMAND = /usr/bin/cmake
# The command to remove a file.
RM = /usr/bin/cmake -E rm -f
# Escaping for special characters.
EQUALS = =
# The top-level source directory on which CMake was run.
CMAKE_SOURCE_DIR = /home/martino/Projects/tools/udpscope
# The top-level build directory on which CMake was run.
CMAKE_BINARY_DIR = /home/martino/Projects/tools/udpscope/tools
# Include any dependencies generated for this target.
include deps/plotter/CMakeFiles/plot_core.dir/depend.make
# Include any dependencies generated by the compiler for this target.
include deps/plotter/CMakeFiles/plot_core.dir/compiler_depend.make
# Include the progress variables for this target.
include deps/plotter/CMakeFiles/plot_core.dir/progress.make
# Include the compile flags for this target's objects.
include deps/plotter/CMakeFiles/plot_core.dir/flags.make
deps/plotter/CMakeFiles/plot_core.dir/codegen:
.PHONY : deps/plotter/CMakeFiles/plot_core.dir/codegen
deps/plotter/CMakeFiles/plot_core.dir/src/core/status.cpp.o: deps/plotter/CMakeFiles/plot_core.dir/flags.make
deps/plotter/CMakeFiles/plot_core.dir/src/core/status.cpp.o: /home/martino/Projects/tools/udpscope/deps/plotter/src/core/status.cpp
deps/plotter/CMakeFiles/plot_core.dir/src/core/status.cpp.o: deps/plotter/CMakeFiles/plot_core.dir/compiler_depend.ts
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green --progress-dir=/home/martino/Projects/tools/udpscope/tools/CMakeFiles --progress-num=$(CMAKE_PROGRESS_1) "Building CXX object deps/plotter/CMakeFiles/plot_core.dir/src/core/status.cpp.o"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -MD -MT deps/plotter/CMakeFiles/plot_core.dir/src/core/status.cpp.o -MF CMakeFiles/plot_core.dir/src/core/status.cpp.o.d -o CMakeFiles/plot_core.dir/src/core/status.cpp.o -c /home/martino/Projects/tools/udpscope/deps/plotter/src/core/status.cpp
deps/plotter/CMakeFiles/plot_core.dir/src/core/status.cpp.i: cmake_force
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green "Preprocessing CXX source to CMakeFiles/plot_core.dir/src/core/status.cpp.i"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -E /home/martino/Projects/tools/udpscope/deps/plotter/src/core/status.cpp > CMakeFiles/plot_core.dir/src/core/status.cpp.i
deps/plotter/CMakeFiles/plot_core.dir/src/core/status.cpp.s: cmake_force
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green "Compiling CXX source to assembly CMakeFiles/plot_core.dir/src/core/status.cpp.s"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -S /home/martino/Projects/tools/udpscope/deps/plotter/src/core/status.cpp -o CMakeFiles/plot_core.dir/src/core/status.cpp.s
deps/plotter/CMakeFiles/plot_core.dir/src/math/transform.cpp.o: deps/plotter/CMakeFiles/plot_core.dir/flags.make
deps/plotter/CMakeFiles/plot_core.dir/src/math/transform.cpp.o: /home/martino/Projects/tools/udpscope/deps/plotter/src/math/transform.cpp
deps/plotter/CMakeFiles/plot_core.dir/src/math/transform.cpp.o: deps/plotter/CMakeFiles/plot_core.dir/compiler_depend.ts
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green --progress-dir=/home/martino/Projects/tools/udpscope/tools/CMakeFiles --progress-num=$(CMAKE_PROGRESS_2) "Building CXX object deps/plotter/CMakeFiles/plot_core.dir/src/math/transform.cpp.o"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -MD -MT deps/plotter/CMakeFiles/plot_core.dir/src/math/transform.cpp.o -MF CMakeFiles/plot_core.dir/src/math/transform.cpp.o.d -o CMakeFiles/plot_core.dir/src/math/transform.cpp.o -c /home/martino/Projects/tools/udpscope/deps/plotter/src/math/transform.cpp
deps/plotter/CMakeFiles/plot_core.dir/src/math/transform.cpp.i: cmake_force
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green "Preprocessing CXX source to CMakeFiles/plot_core.dir/src/math/transform.cpp.i"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -E /home/martino/Projects/tools/udpscope/deps/plotter/src/math/transform.cpp > CMakeFiles/plot_core.dir/src/math/transform.cpp.i
deps/plotter/CMakeFiles/plot_core.dir/src/math/transform.cpp.s: cmake_force
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green "Compiling CXX source to assembly CMakeFiles/plot_core.dir/src/math/transform.cpp.s"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -S /home/martino/Projects/tools/udpscope/deps/plotter/src/math/transform.cpp -o CMakeFiles/plot_core.dir/src/math/transform.cpp.s
deps/plotter/CMakeFiles/plot_core.dir/src/data/minmax_pyramid.cpp.o: deps/plotter/CMakeFiles/plot_core.dir/flags.make
deps/plotter/CMakeFiles/plot_core.dir/src/data/minmax_pyramid.cpp.o: /home/martino/Projects/tools/udpscope/deps/plotter/src/data/minmax_pyramid.cpp
deps/plotter/CMakeFiles/plot_core.dir/src/data/minmax_pyramid.cpp.o: deps/plotter/CMakeFiles/plot_core.dir/compiler_depend.ts
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green --progress-dir=/home/martino/Projects/tools/udpscope/tools/CMakeFiles --progress-num=$(CMAKE_PROGRESS_3) "Building CXX object deps/plotter/CMakeFiles/plot_core.dir/src/data/minmax_pyramid.cpp.o"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -MD -MT deps/plotter/CMakeFiles/plot_core.dir/src/data/minmax_pyramid.cpp.o -MF CMakeFiles/plot_core.dir/src/data/minmax_pyramid.cpp.o.d -o CMakeFiles/plot_core.dir/src/data/minmax_pyramid.cpp.o -c /home/martino/Projects/tools/udpscope/deps/plotter/src/data/minmax_pyramid.cpp
deps/plotter/CMakeFiles/plot_core.dir/src/data/minmax_pyramid.cpp.i: cmake_force
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green "Preprocessing CXX source to CMakeFiles/plot_core.dir/src/data/minmax_pyramid.cpp.i"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -E /home/martino/Projects/tools/udpscope/deps/plotter/src/data/minmax_pyramid.cpp > CMakeFiles/plot_core.dir/src/data/minmax_pyramid.cpp.i
deps/plotter/CMakeFiles/plot_core.dir/src/data/minmax_pyramid.cpp.s: cmake_force
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green "Compiling CXX source to assembly CMakeFiles/plot_core.dir/src/data/minmax_pyramid.cpp.s"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -S /home/martino/Projects/tools/udpscope/deps/plotter/src/data/minmax_pyramid.cpp -o CMakeFiles/plot_core.dir/src/data/minmax_pyramid.cpp.s
deps/plotter/CMakeFiles/plot_core.dir/src/data/series.cpp.o: deps/plotter/CMakeFiles/plot_core.dir/flags.make
deps/plotter/CMakeFiles/plot_core.dir/src/data/series.cpp.o: /home/martino/Projects/tools/udpscope/deps/plotter/src/data/series.cpp
deps/plotter/CMakeFiles/plot_core.dir/src/data/series.cpp.o: deps/plotter/CMakeFiles/plot_core.dir/compiler_depend.ts
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green --progress-dir=/home/martino/Projects/tools/udpscope/tools/CMakeFiles --progress-num=$(CMAKE_PROGRESS_4) "Building CXX object deps/plotter/CMakeFiles/plot_core.dir/src/data/series.cpp.o"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -MD -MT deps/plotter/CMakeFiles/plot_core.dir/src/data/series.cpp.o -MF CMakeFiles/plot_core.dir/src/data/series.cpp.o.d -o CMakeFiles/plot_core.dir/src/data/series.cpp.o -c /home/martino/Projects/tools/udpscope/deps/plotter/src/data/series.cpp
deps/plotter/CMakeFiles/plot_core.dir/src/data/series.cpp.i: cmake_force
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green "Preprocessing CXX source to CMakeFiles/plot_core.dir/src/data/series.cpp.i"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -E /home/martino/Projects/tools/udpscope/deps/plotter/src/data/series.cpp > CMakeFiles/plot_core.dir/src/data/series.cpp.i
deps/plotter/CMakeFiles/plot_core.dir/src/data/series.cpp.s: cmake_force
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green "Compiling CXX source to assembly CMakeFiles/plot_core.dir/src/data/series.cpp.s"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -S /home/martino/Projects/tools/udpscope/deps/plotter/src/data/series.cpp -o CMakeFiles/plot_core.dir/src/data/series.cpp.s
deps/plotter/CMakeFiles/plot_core.dir/src/data/array_series.cpp.o: deps/plotter/CMakeFiles/plot_core.dir/flags.make
deps/plotter/CMakeFiles/plot_core.dir/src/data/array_series.cpp.o: /home/martino/Projects/tools/udpscope/deps/plotter/src/data/array_series.cpp
deps/plotter/CMakeFiles/plot_core.dir/src/data/array_series.cpp.o: deps/plotter/CMakeFiles/plot_core.dir/compiler_depend.ts
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green --progress-dir=/home/martino/Projects/tools/udpscope/tools/CMakeFiles --progress-num=$(CMAKE_PROGRESS_5) "Building CXX object deps/plotter/CMakeFiles/plot_core.dir/src/data/array_series.cpp.o"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -MD -MT deps/plotter/CMakeFiles/plot_core.dir/src/data/array_series.cpp.o -MF CMakeFiles/plot_core.dir/src/data/array_series.cpp.o.d -o CMakeFiles/plot_core.dir/src/data/array_series.cpp.o -c /home/martino/Projects/tools/udpscope/deps/plotter/src/data/array_series.cpp
deps/plotter/CMakeFiles/plot_core.dir/src/data/array_series.cpp.i: cmake_force
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green "Preprocessing CXX source to CMakeFiles/plot_core.dir/src/data/array_series.cpp.i"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -E /home/martino/Projects/tools/udpscope/deps/plotter/src/data/array_series.cpp > CMakeFiles/plot_core.dir/src/data/array_series.cpp.i
deps/plotter/CMakeFiles/plot_core.dir/src/data/array_series.cpp.s: cmake_force
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green "Compiling CXX source to assembly CMakeFiles/plot_core.dir/src/data/array_series.cpp.s"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -S /home/martino/Projects/tools/udpscope/deps/plotter/src/data/array_series.cpp -o CMakeFiles/plot_core.dir/src/data/array_series.cpp.s
deps/plotter/CMakeFiles/plot_core.dir/src/data/stream_series.cpp.o: deps/plotter/CMakeFiles/plot_core.dir/flags.make
deps/plotter/CMakeFiles/plot_core.dir/src/data/stream_series.cpp.o: /home/martino/Projects/tools/udpscope/deps/plotter/src/data/stream_series.cpp
deps/plotter/CMakeFiles/plot_core.dir/src/data/stream_series.cpp.o: deps/plotter/CMakeFiles/plot_core.dir/compiler_depend.ts
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green --progress-dir=/home/martino/Projects/tools/udpscope/tools/CMakeFiles --progress-num=$(CMAKE_PROGRESS_6) "Building CXX object deps/plotter/CMakeFiles/plot_core.dir/src/data/stream_series.cpp.o"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -MD -MT deps/plotter/CMakeFiles/plot_core.dir/src/data/stream_series.cpp.o -MF CMakeFiles/plot_core.dir/src/data/stream_series.cpp.o.d -o CMakeFiles/plot_core.dir/src/data/stream_series.cpp.o -c /home/martino/Projects/tools/udpscope/deps/plotter/src/data/stream_series.cpp
deps/plotter/CMakeFiles/plot_core.dir/src/data/stream_series.cpp.i: cmake_force
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green "Preprocessing CXX source to CMakeFiles/plot_core.dir/src/data/stream_series.cpp.i"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -E /home/martino/Projects/tools/udpscope/deps/plotter/src/data/stream_series.cpp > CMakeFiles/plot_core.dir/src/data/stream_series.cpp.i
deps/plotter/CMakeFiles/plot_core.dir/src/data/stream_series.cpp.s: cmake_force
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green "Compiling CXX source to assembly CMakeFiles/plot_core.dir/src/data/stream_series.cpp.s"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -S /home/martino/Projects/tools/udpscope/deps/plotter/src/data/stream_series.cpp -o CMakeFiles/plot_core.dir/src/data/stream_series.cpp.s
deps/plotter/CMakeFiles/plot_core.dir/src/data/xy_series.cpp.o: deps/plotter/CMakeFiles/plot_core.dir/flags.make
deps/plotter/CMakeFiles/plot_core.dir/src/data/xy_series.cpp.o: /home/martino/Projects/tools/udpscope/deps/plotter/src/data/xy_series.cpp
deps/plotter/CMakeFiles/plot_core.dir/src/data/xy_series.cpp.o: deps/plotter/CMakeFiles/plot_core.dir/compiler_depend.ts
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green --progress-dir=/home/martino/Projects/tools/udpscope/tools/CMakeFiles --progress-num=$(CMAKE_PROGRESS_7) "Building CXX object deps/plotter/CMakeFiles/plot_core.dir/src/data/xy_series.cpp.o"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -MD -MT deps/plotter/CMakeFiles/plot_core.dir/src/data/xy_series.cpp.o -MF CMakeFiles/plot_core.dir/src/data/xy_series.cpp.o.d -o CMakeFiles/plot_core.dir/src/data/xy_series.cpp.o -c /home/martino/Projects/tools/udpscope/deps/plotter/src/data/xy_series.cpp
deps/plotter/CMakeFiles/plot_core.dir/src/data/xy_series.cpp.i: cmake_force
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green "Preprocessing CXX source to CMakeFiles/plot_core.dir/src/data/xy_series.cpp.i"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -E /home/martino/Projects/tools/udpscope/deps/plotter/src/data/xy_series.cpp > CMakeFiles/plot_core.dir/src/data/xy_series.cpp.i
deps/plotter/CMakeFiles/plot_core.dir/src/data/xy_series.cpp.s: cmake_force
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green "Compiling CXX source to assembly CMakeFiles/plot_core.dir/src/data/xy_series.cpp.s"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -S /home/martino/Projects/tools/udpscope/deps/plotter/src/data/xy_series.cpp -o CMakeFiles/plot_core.dir/src/data/xy_series.cpp.s
deps/plotter/CMakeFiles/plot_core.dir/src/gl/loader.cpp.o: deps/plotter/CMakeFiles/plot_core.dir/flags.make
deps/plotter/CMakeFiles/plot_core.dir/src/gl/loader.cpp.o: /home/martino/Projects/tools/udpscope/deps/plotter/src/gl/loader.cpp
deps/plotter/CMakeFiles/plot_core.dir/src/gl/loader.cpp.o: deps/plotter/CMakeFiles/plot_core.dir/compiler_depend.ts
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green --progress-dir=/home/martino/Projects/tools/udpscope/tools/CMakeFiles --progress-num=$(CMAKE_PROGRESS_8) "Building CXX object deps/plotter/CMakeFiles/plot_core.dir/src/gl/loader.cpp.o"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -MD -MT deps/plotter/CMakeFiles/plot_core.dir/src/gl/loader.cpp.o -MF CMakeFiles/plot_core.dir/src/gl/loader.cpp.o.d -o CMakeFiles/plot_core.dir/src/gl/loader.cpp.o -c /home/martino/Projects/tools/udpscope/deps/plotter/src/gl/loader.cpp
deps/plotter/CMakeFiles/plot_core.dir/src/gl/loader.cpp.i: cmake_force
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green "Preprocessing CXX source to CMakeFiles/plot_core.dir/src/gl/loader.cpp.i"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -E /home/martino/Projects/tools/udpscope/deps/plotter/src/gl/loader.cpp > CMakeFiles/plot_core.dir/src/gl/loader.cpp.i
deps/plotter/CMakeFiles/plot_core.dir/src/gl/loader.cpp.s: cmake_force
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green "Compiling CXX source to assembly CMakeFiles/plot_core.dir/src/gl/loader.cpp.s"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -S /home/martino/Projects/tools/udpscope/deps/plotter/src/gl/loader.cpp -o CMakeFiles/plot_core.dir/src/gl/loader.cpp.s
deps/plotter/CMakeFiles/plot_core.dir/src/gl/handle.cpp.o: deps/plotter/CMakeFiles/plot_core.dir/flags.make
deps/plotter/CMakeFiles/plot_core.dir/src/gl/handle.cpp.o: /home/martino/Projects/tools/udpscope/deps/plotter/src/gl/handle.cpp
deps/plotter/CMakeFiles/plot_core.dir/src/gl/handle.cpp.o: deps/plotter/CMakeFiles/plot_core.dir/compiler_depend.ts
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green --progress-dir=/home/martino/Projects/tools/udpscope/tools/CMakeFiles --progress-num=$(CMAKE_PROGRESS_9) "Building CXX object deps/plotter/CMakeFiles/plot_core.dir/src/gl/handle.cpp.o"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -MD -MT deps/plotter/CMakeFiles/plot_core.dir/src/gl/handle.cpp.o -MF CMakeFiles/plot_core.dir/src/gl/handle.cpp.o.d -o CMakeFiles/plot_core.dir/src/gl/handle.cpp.o -c /home/martino/Projects/tools/udpscope/deps/plotter/src/gl/handle.cpp
deps/plotter/CMakeFiles/plot_core.dir/src/gl/handle.cpp.i: cmake_force
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green "Preprocessing CXX source to CMakeFiles/plot_core.dir/src/gl/handle.cpp.i"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -E /home/martino/Projects/tools/udpscope/deps/plotter/src/gl/handle.cpp > CMakeFiles/plot_core.dir/src/gl/handle.cpp.i
deps/plotter/CMakeFiles/plot_core.dir/src/gl/handle.cpp.s: cmake_force
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green "Compiling CXX source to assembly CMakeFiles/plot_core.dir/src/gl/handle.cpp.s"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -S /home/martino/Projects/tools/udpscope/deps/plotter/src/gl/handle.cpp -o CMakeFiles/plot_core.dir/src/gl/handle.cpp.s
deps/plotter/CMakeFiles/plot_core.dir/src/render/shaders.cpp.o: deps/plotter/CMakeFiles/plot_core.dir/flags.make
deps/plotter/CMakeFiles/plot_core.dir/src/render/shaders.cpp.o: /home/martino/Projects/tools/udpscope/deps/plotter/src/render/shaders.cpp
deps/plotter/CMakeFiles/plot_core.dir/src/render/shaders.cpp.o: deps/plotter/CMakeFiles/plot_core.dir/compiler_depend.ts
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green --progress-dir=/home/martino/Projects/tools/udpscope/tools/CMakeFiles --progress-num=$(CMAKE_PROGRESS_10) "Building CXX object deps/plotter/CMakeFiles/plot_core.dir/src/render/shaders.cpp.o"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -MD -MT deps/plotter/CMakeFiles/plot_core.dir/src/render/shaders.cpp.o -MF CMakeFiles/plot_core.dir/src/render/shaders.cpp.o.d -o CMakeFiles/plot_core.dir/src/render/shaders.cpp.o -c /home/martino/Projects/tools/udpscope/deps/plotter/src/render/shaders.cpp
deps/plotter/CMakeFiles/plot_core.dir/src/render/shaders.cpp.i: cmake_force
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green "Preprocessing CXX source to CMakeFiles/plot_core.dir/src/render/shaders.cpp.i"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -E /home/martino/Projects/tools/udpscope/deps/plotter/src/render/shaders.cpp > CMakeFiles/plot_core.dir/src/render/shaders.cpp.i
deps/plotter/CMakeFiles/plot_core.dir/src/render/shaders.cpp.s: cmake_force
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green "Compiling CXX source to assembly CMakeFiles/plot_core.dir/src/render/shaders.cpp.s"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -S /home/martino/Projects/tools/udpscope/deps/plotter/src/render/shaders.cpp -o CMakeFiles/plot_core.dir/src/render/shaders.cpp.s
deps/plotter/CMakeFiles/plot_core.dir/src/render/text.cpp.o: deps/plotter/CMakeFiles/plot_core.dir/flags.make
deps/plotter/CMakeFiles/plot_core.dir/src/render/text.cpp.o: /home/martino/Projects/tools/udpscope/deps/plotter/src/render/text.cpp
deps/plotter/CMakeFiles/plot_core.dir/src/render/text.cpp.o: deps/plotter/CMakeFiles/plot_core.dir/compiler_depend.ts
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green --progress-dir=/home/martino/Projects/tools/udpscope/tools/CMakeFiles --progress-num=$(CMAKE_PROGRESS_11) "Building CXX object deps/plotter/CMakeFiles/plot_core.dir/src/render/text.cpp.o"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -MD -MT deps/plotter/CMakeFiles/plot_core.dir/src/render/text.cpp.o -MF CMakeFiles/plot_core.dir/src/render/text.cpp.o.d -o CMakeFiles/plot_core.dir/src/render/text.cpp.o -c /home/martino/Projects/tools/udpscope/deps/plotter/src/render/text.cpp
deps/plotter/CMakeFiles/plot_core.dir/src/render/text.cpp.i: cmake_force
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green "Preprocessing CXX source to CMakeFiles/plot_core.dir/src/render/text.cpp.i"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -E /home/martino/Projects/tools/udpscope/deps/plotter/src/render/text.cpp > CMakeFiles/plot_core.dir/src/render/text.cpp.i
deps/plotter/CMakeFiles/plot_core.dir/src/render/text.cpp.s: cmake_force
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green "Compiling CXX source to assembly CMakeFiles/plot_core.dir/src/render/text.cpp.s"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -S /home/martino/Projects/tools/udpscope/deps/plotter/src/render/text.cpp -o CMakeFiles/plot_core.dir/src/render/text.cpp.s
deps/plotter/CMakeFiles/plot_core.dir/src/render/text_atlas.cpp.o: deps/plotter/CMakeFiles/plot_core.dir/flags.make
deps/plotter/CMakeFiles/plot_core.dir/src/render/text_atlas.cpp.o: /home/martino/Projects/tools/udpscope/deps/plotter/src/render/text_atlas.cpp
deps/plotter/CMakeFiles/plot_core.dir/src/render/text_atlas.cpp.o: deps/plotter/CMakeFiles/plot_core.dir/compiler_depend.ts
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green --progress-dir=/home/martino/Projects/tools/udpscope/tools/CMakeFiles --progress-num=$(CMAKE_PROGRESS_12) "Building CXX object deps/plotter/CMakeFiles/plot_core.dir/src/render/text_atlas.cpp.o"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -MD -MT deps/plotter/CMakeFiles/plot_core.dir/src/render/text_atlas.cpp.o -MF CMakeFiles/plot_core.dir/src/render/text_atlas.cpp.o.d -o CMakeFiles/plot_core.dir/src/render/text_atlas.cpp.o -c /home/martino/Projects/tools/udpscope/deps/plotter/src/render/text_atlas.cpp
deps/plotter/CMakeFiles/plot_core.dir/src/render/text_atlas.cpp.i: cmake_force
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green "Preprocessing CXX source to CMakeFiles/plot_core.dir/src/render/text_atlas.cpp.i"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -E /home/martino/Projects/tools/udpscope/deps/plotter/src/render/text_atlas.cpp > CMakeFiles/plot_core.dir/src/render/text_atlas.cpp.i
deps/plotter/CMakeFiles/plot_core.dir/src/render/text_atlas.cpp.s: cmake_force
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green "Compiling CXX source to assembly CMakeFiles/plot_core.dir/src/render/text_atlas.cpp.s"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -S /home/martino/Projects/tools/udpscope/deps/plotter/src/render/text_atlas.cpp -o CMakeFiles/plot_core.dir/src/render/text_atlas.cpp.s
deps/plotter/CMakeFiles/plot_core.dir/src/render/embedded_mono_font.cpp.o: deps/plotter/CMakeFiles/plot_core.dir/flags.make
deps/plotter/CMakeFiles/plot_core.dir/src/render/embedded_mono_font.cpp.o: /home/martino/Projects/tools/udpscope/deps/plotter/src/render/embedded_mono_font.cpp
deps/plotter/CMakeFiles/plot_core.dir/src/render/embedded_mono_font.cpp.o: deps/plotter/CMakeFiles/plot_core.dir/compiler_depend.ts
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green --progress-dir=/home/martino/Projects/tools/udpscope/tools/CMakeFiles --progress-num=$(CMAKE_PROGRESS_13) "Building CXX object deps/plotter/CMakeFiles/plot_core.dir/src/render/embedded_mono_font.cpp.o"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -MD -MT deps/plotter/CMakeFiles/plot_core.dir/src/render/embedded_mono_font.cpp.o -MF CMakeFiles/plot_core.dir/src/render/embedded_mono_font.cpp.o.d -o CMakeFiles/plot_core.dir/src/render/embedded_mono_font.cpp.o -c /home/martino/Projects/tools/udpscope/deps/plotter/src/render/embedded_mono_font.cpp
deps/plotter/CMakeFiles/plot_core.dir/src/render/embedded_mono_font.cpp.i: cmake_force
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green "Preprocessing CXX source to CMakeFiles/plot_core.dir/src/render/embedded_mono_font.cpp.i"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -E /home/martino/Projects/tools/udpscope/deps/plotter/src/render/embedded_mono_font.cpp > CMakeFiles/plot_core.dir/src/render/embedded_mono_font.cpp.i
deps/plotter/CMakeFiles/plot_core.dir/src/render/embedded_mono_font.cpp.s: cmake_force
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green "Compiling CXX source to assembly CMakeFiles/plot_core.dir/src/render/embedded_mono_font.cpp.s"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -S /home/martino/Projects/tools/udpscope/deps/plotter/src/render/embedded_mono_font.cpp -o CMakeFiles/plot_core.dir/src/render/embedded_mono_font.cpp.s
deps/plotter/CMakeFiles/plot_core.dir/src/render/primitives.cpp.o: deps/plotter/CMakeFiles/plot_core.dir/flags.make
deps/plotter/CMakeFiles/plot_core.dir/src/render/primitives.cpp.o: /home/martino/Projects/tools/udpscope/deps/plotter/src/render/primitives.cpp
deps/plotter/CMakeFiles/plot_core.dir/src/render/primitives.cpp.o: deps/plotter/CMakeFiles/plot_core.dir/compiler_depend.ts
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green --progress-dir=/home/martino/Projects/tools/udpscope/tools/CMakeFiles --progress-num=$(CMAKE_PROGRESS_14) "Building CXX object deps/plotter/CMakeFiles/plot_core.dir/src/render/primitives.cpp.o"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -MD -MT deps/plotter/CMakeFiles/plot_core.dir/src/render/primitives.cpp.o -MF CMakeFiles/plot_core.dir/src/render/primitives.cpp.o.d -o CMakeFiles/plot_core.dir/src/render/primitives.cpp.o -c /home/martino/Projects/tools/udpscope/deps/plotter/src/render/primitives.cpp
deps/plotter/CMakeFiles/plot_core.dir/src/render/primitives.cpp.i: cmake_force
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green "Preprocessing CXX source to CMakeFiles/plot_core.dir/src/render/primitives.cpp.i"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -E /home/martino/Projects/tools/udpscope/deps/plotter/src/render/primitives.cpp > CMakeFiles/plot_core.dir/src/render/primitives.cpp.i
deps/plotter/CMakeFiles/plot_core.dir/src/render/primitives.cpp.s: cmake_force
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green "Compiling CXX source to assembly CMakeFiles/plot_core.dir/src/render/primitives.cpp.s"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -S /home/martino/Projects/tools/udpscope/deps/plotter/src/render/primitives.cpp -o CMakeFiles/plot_core.dir/src/render/primitives.cpp.s
deps/plotter/CMakeFiles/plot_core.dir/src/render/buffer.cpp.o: deps/plotter/CMakeFiles/plot_core.dir/flags.make
deps/plotter/CMakeFiles/plot_core.dir/src/render/buffer.cpp.o: /home/martino/Projects/tools/udpscope/deps/plotter/src/render/buffer.cpp
deps/plotter/CMakeFiles/plot_core.dir/src/render/buffer.cpp.o: deps/plotter/CMakeFiles/plot_core.dir/compiler_depend.ts
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green --progress-dir=/home/martino/Projects/tools/udpscope/tools/CMakeFiles --progress-num=$(CMAKE_PROGRESS_15) "Building CXX object deps/plotter/CMakeFiles/plot_core.dir/src/render/buffer.cpp.o"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -MD -MT deps/plotter/CMakeFiles/plot_core.dir/src/render/buffer.cpp.o -MF CMakeFiles/plot_core.dir/src/render/buffer.cpp.o.d -o CMakeFiles/plot_core.dir/src/render/buffer.cpp.o -c /home/martino/Projects/tools/udpscope/deps/plotter/src/render/buffer.cpp
deps/plotter/CMakeFiles/plot_core.dir/src/render/buffer.cpp.i: cmake_force
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green "Preprocessing CXX source to CMakeFiles/plot_core.dir/src/render/buffer.cpp.i"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -E /home/martino/Projects/tools/udpscope/deps/plotter/src/render/buffer.cpp > CMakeFiles/plot_core.dir/src/render/buffer.cpp.i
deps/plotter/CMakeFiles/plot_core.dir/src/render/buffer.cpp.s: cmake_force
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green "Compiling CXX source to assembly CMakeFiles/plot_core.dir/src/render/buffer.cpp.s"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -S /home/martino/Projects/tools/udpscope/deps/plotter/src/render/buffer.cpp -o CMakeFiles/plot_core.dir/src/render/buffer.cpp.s
deps/plotter/CMakeFiles/plot_core.dir/src/render/vertex_array.cpp.o: deps/plotter/CMakeFiles/plot_core.dir/flags.make
deps/plotter/CMakeFiles/plot_core.dir/src/render/vertex_array.cpp.o: /home/martino/Projects/tools/udpscope/deps/plotter/src/render/vertex_array.cpp
deps/plotter/CMakeFiles/plot_core.dir/src/render/vertex_array.cpp.o: deps/plotter/CMakeFiles/plot_core.dir/compiler_depend.ts
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green --progress-dir=/home/martino/Projects/tools/udpscope/tools/CMakeFiles --progress-num=$(CMAKE_PROGRESS_16) "Building CXX object deps/plotter/CMakeFiles/plot_core.dir/src/render/vertex_array.cpp.o"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -MD -MT deps/plotter/CMakeFiles/plot_core.dir/src/render/vertex_array.cpp.o -MF CMakeFiles/plot_core.dir/src/render/vertex_array.cpp.o.d -o CMakeFiles/plot_core.dir/src/render/vertex_array.cpp.o -c /home/martino/Projects/tools/udpscope/deps/plotter/src/render/vertex_array.cpp
deps/plotter/CMakeFiles/plot_core.dir/src/render/vertex_array.cpp.i: cmake_force
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green "Preprocessing CXX source to CMakeFiles/plot_core.dir/src/render/vertex_array.cpp.i"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -E /home/martino/Projects/tools/udpscope/deps/plotter/src/render/vertex_array.cpp > CMakeFiles/plot_core.dir/src/render/vertex_array.cpp.i
deps/plotter/CMakeFiles/plot_core.dir/src/render/vertex_array.cpp.s: cmake_force
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green "Compiling CXX source to assembly CMakeFiles/plot_core.dir/src/render/vertex_array.cpp.s"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -S /home/martino/Projects/tools/udpscope/deps/plotter/src/render/vertex_array.cpp -o CMakeFiles/plot_core.dir/src/render/vertex_array.cpp.s
deps/plotter/CMakeFiles/plot_core.dir/src/render/program.cpp.o: deps/plotter/CMakeFiles/plot_core.dir/flags.make
deps/plotter/CMakeFiles/plot_core.dir/src/render/program.cpp.o: /home/martino/Projects/tools/udpscope/deps/plotter/src/render/program.cpp
deps/plotter/CMakeFiles/plot_core.dir/src/render/program.cpp.o: deps/plotter/CMakeFiles/plot_core.dir/compiler_depend.ts
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green --progress-dir=/home/martino/Projects/tools/udpscope/tools/CMakeFiles --progress-num=$(CMAKE_PROGRESS_17) "Building CXX object deps/plotter/CMakeFiles/plot_core.dir/src/render/program.cpp.o"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -MD -MT deps/plotter/CMakeFiles/plot_core.dir/src/render/program.cpp.o -MF CMakeFiles/plot_core.dir/src/render/program.cpp.o.d -o CMakeFiles/plot_core.dir/src/render/program.cpp.o -c /home/martino/Projects/tools/udpscope/deps/plotter/src/render/program.cpp
deps/plotter/CMakeFiles/plot_core.dir/src/render/program.cpp.i: cmake_force
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green "Preprocessing CXX source to CMakeFiles/plot_core.dir/src/render/program.cpp.i"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -E /home/martino/Projects/tools/udpscope/deps/plotter/src/render/program.cpp > CMakeFiles/plot_core.dir/src/render/program.cpp.i
deps/plotter/CMakeFiles/plot_core.dir/src/render/program.cpp.s: cmake_force
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green "Compiling CXX source to assembly CMakeFiles/plot_core.dir/src/render/program.cpp.s"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -S /home/martino/Projects/tools/udpscope/deps/plotter/src/render/program.cpp -o CMakeFiles/plot_core.dir/src/render/program.cpp.s
deps/plotter/CMakeFiles/plot_core.dir/src/render/renderer.cpp.o: deps/plotter/CMakeFiles/plot_core.dir/flags.make
deps/plotter/CMakeFiles/plot_core.dir/src/render/renderer.cpp.o: /home/martino/Projects/tools/udpscope/deps/plotter/src/render/renderer.cpp
deps/plotter/CMakeFiles/plot_core.dir/src/render/renderer.cpp.o: deps/plotter/CMakeFiles/plot_core.dir/compiler_depend.ts
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green --progress-dir=/home/martino/Projects/tools/udpscope/tools/CMakeFiles --progress-num=$(CMAKE_PROGRESS_18) "Building CXX object deps/plotter/CMakeFiles/plot_core.dir/src/render/renderer.cpp.o"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -MD -MT deps/plotter/CMakeFiles/plot_core.dir/src/render/renderer.cpp.o -MF CMakeFiles/plot_core.dir/src/render/renderer.cpp.o.d -o CMakeFiles/plot_core.dir/src/render/renderer.cpp.o -c /home/martino/Projects/tools/udpscope/deps/plotter/src/render/renderer.cpp
deps/plotter/CMakeFiles/plot_core.dir/src/render/renderer.cpp.i: cmake_force
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green "Preprocessing CXX source to CMakeFiles/plot_core.dir/src/render/renderer.cpp.i"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -E /home/martino/Projects/tools/udpscope/deps/plotter/src/render/renderer.cpp > CMakeFiles/plot_core.dir/src/render/renderer.cpp.i
deps/plotter/CMakeFiles/plot_core.dir/src/render/renderer.cpp.s: cmake_force
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green "Compiling CXX source to assembly CMakeFiles/plot_core.dir/src/render/renderer.cpp.s"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -S /home/martino/Projects/tools/udpscope/deps/plotter/src/render/renderer.cpp -o CMakeFiles/plot_core.dir/src/render/renderer.cpp.s
deps/plotter/CMakeFiles/plot_core.dir/src/plot/ticks.cpp.o: deps/plotter/CMakeFiles/plot_core.dir/flags.make
deps/plotter/CMakeFiles/plot_core.dir/src/plot/ticks.cpp.o: /home/martino/Projects/tools/udpscope/deps/plotter/src/plot/ticks.cpp
deps/plotter/CMakeFiles/plot_core.dir/src/plot/ticks.cpp.o: deps/plotter/CMakeFiles/plot_core.dir/compiler_depend.ts
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green --progress-dir=/home/martino/Projects/tools/udpscope/tools/CMakeFiles --progress-num=$(CMAKE_PROGRESS_19) "Building CXX object deps/plotter/CMakeFiles/plot_core.dir/src/plot/ticks.cpp.o"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -MD -MT deps/plotter/CMakeFiles/plot_core.dir/src/plot/ticks.cpp.o -MF CMakeFiles/plot_core.dir/src/plot/ticks.cpp.o.d -o CMakeFiles/plot_core.dir/src/plot/ticks.cpp.o -c /home/martino/Projects/tools/udpscope/deps/plotter/src/plot/ticks.cpp
deps/plotter/CMakeFiles/plot_core.dir/src/plot/ticks.cpp.i: cmake_force
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green "Preprocessing CXX source to CMakeFiles/plot_core.dir/src/plot/ticks.cpp.i"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -E /home/martino/Projects/tools/udpscope/deps/plotter/src/plot/ticks.cpp > CMakeFiles/plot_core.dir/src/plot/ticks.cpp.i
deps/plotter/CMakeFiles/plot_core.dir/src/plot/ticks.cpp.s: cmake_force
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green "Compiling CXX source to assembly CMakeFiles/plot_core.dir/src/plot/ticks.cpp.s"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -S /home/martino/Projects/tools/udpscope/deps/plotter/src/plot/ticks.cpp -o CMakeFiles/plot_core.dir/src/plot/ticks.cpp.s
deps/plotter/CMakeFiles/plot_core.dir/src/plot/axis.cpp.o: deps/plotter/CMakeFiles/plot_core.dir/flags.make
deps/plotter/CMakeFiles/plot_core.dir/src/plot/axis.cpp.o: /home/martino/Projects/tools/udpscope/deps/plotter/src/plot/axis.cpp
deps/plotter/CMakeFiles/plot_core.dir/src/plot/axis.cpp.o: deps/plotter/CMakeFiles/plot_core.dir/compiler_depend.ts
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green --progress-dir=/home/martino/Projects/tools/udpscope/tools/CMakeFiles --progress-num=$(CMAKE_PROGRESS_20) "Building CXX object deps/plotter/CMakeFiles/plot_core.dir/src/plot/axis.cpp.o"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -MD -MT deps/plotter/CMakeFiles/plot_core.dir/src/plot/axis.cpp.o -MF CMakeFiles/plot_core.dir/src/plot/axis.cpp.o.d -o CMakeFiles/plot_core.dir/src/plot/axis.cpp.o -c /home/martino/Projects/tools/udpscope/deps/plotter/src/plot/axis.cpp
deps/plotter/CMakeFiles/plot_core.dir/src/plot/axis.cpp.i: cmake_force
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green "Preprocessing CXX source to CMakeFiles/plot_core.dir/src/plot/axis.cpp.i"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -E /home/martino/Projects/tools/udpscope/deps/plotter/src/plot/axis.cpp > CMakeFiles/plot_core.dir/src/plot/axis.cpp.i
deps/plotter/CMakeFiles/plot_core.dir/src/plot/axis.cpp.s: cmake_force
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green "Compiling CXX source to assembly CMakeFiles/plot_core.dir/src/plot/axis.cpp.s"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -S /home/martino/Projects/tools/udpscope/deps/plotter/src/plot/axis.cpp -o CMakeFiles/plot_core.dir/src/plot/axis.cpp.s
deps/plotter/CMakeFiles/plot_core.dir/src/plot/grid.cpp.o: deps/plotter/CMakeFiles/plot_core.dir/flags.make
deps/plotter/CMakeFiles/plot_core.dir/src/plot/grid.cpp.o: /home/martino/Projects/tools/udpscope/deps/plotter/src/plot/grid.cpp
deps/plotter/CMakeFiles/plot_core.dir/src/plot/grid.cpp.o: deps/plotter/CMakeFiles/plot_core.dir/compiler_depend.ts
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green --progress-dir=/home/martino/Projects/tools/udpscope/tools/CMakeFiles --progress-num=$(CMAKE_PROGRESS_21) "Building CXX object deps/plotter/CMakeFiles/plot_core.dir/src/plot/grid.cpp.o"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -MD -MT deps/plotter/CMakeFiles/plot_core.dir/src/plot/grid.cpp.o -MF CMakeFiles/plot_core.dir/src/plot/grid.cpp.o.d -o CMakeFiles/plot_core.dir/src/plot/grid.cpp.o -c /home/martino/Projects/tools/udpscope/deps/plotter/src/plot/grid.cpp
deps/plotter/CMakeFiles/plot_core.dir/src/plot/grid.cpp.i: cmake_force
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green "Preprocessing CXX source to CMakeFiles/plot_core.dir/src/plot/grid.cpp.i"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -E /home/martino/Projects/tools/udpscope/deps/plotter/src/plot/grid.cpp > CMakeFiles/plot_core.dir/src/plot/grid.cpp.i
deps/plotter/CMakeFiles/plot_core.dir/src/plot/grid.cpp.s: cmake_force
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green "Compiling CXX source to assembly CMakeFiles/plot_core.dir/src/plot/grid.cpp.s"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -S /home/martino/Projects/tools/udpscope/deps/plotter/src/plot/grid.cpp -o CMakeFiles/plot_core.dir/src/plot/grid.cpp.s
deps/plotter/CMakeFiles/plot_core.dir/src/plot/measure.cpp.o: deps/plotter/CMakeFiles/plot_core.dir/flags.make
deps/plotter/CMakeFiles/plot_core.dir/src/plot/measure.cpp.o: /home/martino/Projects/tools/udpscope/deps/plotter/src/plot/measure.cpp
deps/plotter/CMakeFiles/plot_core.dir/src/plot/measure.cpp.o: deps/plotter/CMakeFiles/plot_core.dir/compiler_depend.ts
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green --progress-dir=/home/martino/Projects/tools/udpscope/tools/CMakeFiles --progress-num=$(CMAKE_PROGRESS_22) "Building CXX object deps/plotter/CMakeFiles/plot_core.dir/src/plot/measure.cpp.o"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -MD -MT deps/plotter/CMakeFiles/plot_core.dir/src/plot/measure.cpp.o -MF CMakeFiles/plot_core.dir/src/plot/measure.cpp.o.d -o CMakeFiles/plot_core.dir/src/plot/measure.cpp.o -c /home/martino/Projects/tools/udpscope/deps/plotter/src/plot/measure.cpp
deps/plotter/CMakeFiles/plot_core.dir/src/plot/measure.cpp.i: cmake_force
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green "Preprocessing CXX source to CMakeFiles/plot_core.dir/src/plot/measure.cpp.i"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -E /home/martino/Projects/tools/udpscope/deps/plotter/src/plot/measure.cpp > CMakeFiles/plot_core.dir/src/plot/measure.cpp.i
deps/plotter/CMakeFiles/plot_core.dir/src/plot/measure.cpp.s: cmake_force
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green "Compiling CXX source to assembly CMakeFiles/plot_core.dir/src/plot/measure.cpp.s"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -S /home/martino/Projects/tools/udpscope/deps/plotter/src/plot/measure.cpp -o CMakeFiles/plot_core.dir/src/plot/measure.cpp.s
deps/plotter/CMakeFiles/plot_core.dir/src/plot/plot.cpp.o: deps/plotter/CMakeFiles/plot_core.dir/flags.make
deps/plotter/CMakeFiles/plot_core.dir/src/plot/plot.cpp.o: /home/martino/Projects/tools/udpscope/deps/plotter/src/plot/plot.cpp
deps/plotter/CMakeFiles/plot_core.dir/src/plot/plot.cpp.o: deps/plotter/CMakeFiles/plot_core.dir/compiler_depend.ts
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green --progress-dir=/home/martino/Projects/tools/udpscope/tools/CMakeFiles --progress-num=$(CMAKE_PROGRESS_23) "Building CXX object deps/plotter/CMakeFiles/plot_core.dir/src/plot/plot.cpp.o"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -MD -MT deps/plotter/CMakeFiles/plot_core.dir/src/plot/plot.cpp.o -MF CMakeFiles/plot_core.dir/src/plot/plot.cpp.o.d -o CMakeFiles/plot_core.dir/src/plot/plot.cpp.o -c /home/martino/Projects/tools/udpscope/deps/plotter/src/plot/plot.cpp
deps/plotter/CMakeFiles/plot_core.dir/src/plot/plot.cpp.i: cmake_force
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green "Preprocessing CXX source to CMakeFiles/plot_core.dir/src/plot/plot.cpp.i"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -E /home/martino/Projects/tools/udpscope/deps/plotter/src/plot/plot.cpp > CMakeFiles/plot_core.dir/src/plot/plot.cpp.i
deps/plotter/CMakeFiles/plot_core.dir/src/plot/plot.cpp.s: cmake_force
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green "Compiling CXX source to assembly CMakeFiles/plot_core.dir/src/plot/plot.cpp.s"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -S /home/martino/Projects/tools/udpscope/deps/plotter/src/plot/plot.cpp -o CMakeFiles/plot_core.dir/src/plot/plot.cpp.s
deps/plotter/CMakeFiles/plot_core.dir/src/plot/figure.cpp.o: deps/plotter/CMakeFiles/plot_core.dir/flags.make
deps/plotter/CMakeFiles/plot_core.dir/src/plot/figure.cpp.o: /home/martino/Projects/tools/udpscope/deps/plotter/src/plot/figure.cpp
deps/plotter/CMakeFiles/plot_core.dir/src/plot/figure.cpp.o: deps/plotter/CMakeFiles/plot_core.dir/compiler_depend.ts
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green --progress-dir=/home/martino/Projects/tools/udpscope/tools/CMakeFiles --progress-num=$(CMAKE_PROGRESS_24) "Building CXX object deps/plotter/CMakeFiles/plot_core.dir/src/plot/figure.cpp.o"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -MD -MT deps/plotter/CMakeFiles/plot_core.dir/src/plot/figure.cpp.o -MF CMakeFiles/plot_core.dir/src/plot/figure.cpp.o.d -o CMakeFiles/plot_core.dir/src/plot/figure.cpp.o -c /home/martino/Projects/tools/udpscope/deps/plotter/src/plot/figure.cpp
deps/plotter/CMakeFiles/plot_core.dir/src/plot/figure.cpp.i: cmake_force
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green "Preprocessing CXX source to CMakeFiles/plot_core.dir/src/plot/figure.cpp.i"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -E /home/martino/Projects/tools/udpscope/deps/plotter/src/plot/figure.cpp > CMakeFiles/plot_core.dir/src/plot/figure.cpp.i
deps/plotter/CMakeFiles/plot_core.dir/src/plot/figure.cpp.s: cmake_force
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green "Compiling CXX source to assembly CMakeFiles/plot_core.dir/src/plot/figure.cpp.s"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -S /home/martino/Projects/tools/udpscope/deps/plotter/src/plot/figure.cpp -o CMakeFiles/plot_core.dir/src/plot/figure.cpp.s
deps/plotter/CMakeFiles/plot_core.dir/src/input/controller.cpp.o: deps/plotter/CMakeFiles/plot_core.dir/flags.make
deps/plotter/CMakeFiles/plot_core.dir/src/input/controller.cpp.o: /home/martino/Projects/tools/udpscope/deps/plotter/src/input/controller.cpp
deps/plotter/CMakeFiles/plot_core.dir/src/input/controller.cpp.o: deps/plotter/CMakeFiles/plot_core.dir/compiler_depend.ts
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green --progress-dir=/home/martino/Projects/tools/udpscope/tools/CMakeFiles --progress-num=$(CMAKE_PROGRESS_25) "Building CXX object deps/plotter/CMakeFiles/plot_core.dir/src/input/controller.cpp.o"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -MD -MT deps/plotter/CMakeFiles/plot_core.dir/src/input/controller.cpp.o -MF CMakeFiles/plot_core.dir/src/input/controller.cpp.o.d -o CMakeFiles/plot_core.dir/src/input/controller.cpp.o -c /home/martino/Projects/tools/udpscope/deps/plotter/src/input/controller.cpp
deps/plotter/CMakeFiles/plot_core.dir/src/input/controller.cpp.i: cmake_force
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green "Preprocessing CXX source to CMakeFiles/plot_core.dir/src/input/controller.cpp.i"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -E /home/martino/Projects/tools/udpscope/deps/plotter/src/input/controller.cpp > CMakeFiles/plot_core.dir/src/input/controller.cpp.i
deps/plotter/CMakeFiles/plot_core.dir/src/input/controller.cpp.s: cmake_force
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green "Compiling CXX source to assembly CMakeFiles/plot_core.dir/src/input/controller.cpp.s"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -S /home/martino/Projects/tools/udpscope/deps/plotter/src/input/controller.cpp -o CMakeFiles/plot_core.dir/src/input/controller.cpp.s
# Object files for target plot_core
plot_core_OBJECTS = \
"CMakeFiles/plot_core.dir/src/core/status.cpp.o" \
"CMakeFiles/plot_core.dir/src/math/transform.cpp.o" \
"CMakeFiles/plot_core.dir/src/data/minmax_pyramid.cpp.o" \
"CMakeFiles/plot_core.dir/src/data/series.cpp.o" \
"CMakeFiles/plot_core.dir/src/data/array_series.cpp.o" \
"CMakeFiles/plot_core.dir/src/data/stream_series.cpp.o" \
"CMakeFiles/plot_core.dir/src/data/xy_series.cpp.o" \
"CMakeFiles/plot_core.dir/src/gl/loader.cpp.o" \
"CMakeFiles/plot_core.dir/src/gl/handle.cpp.o" \
"CMakeFiles/plot_core.dir/src/render/shaders.cpp.o" \
"CMakeFiles/plot_core.dir/src/render/text.cpp.o" \
"CMakeFiles/plot_core.dir/src/render/text_atlas.cpp.o" \
"CMakeFiles/plot_core.dir/src/render/embedded_mono_font.cpp.o" \
"CMakeFiles/plot_core.dir/src/render/primitives.cpp.o" \
"CMakeFiles/plot_core.dir/src/render/buffer.cpp.o" \
"CMakeFiles/plot_core.dir/src/render/vertex_array.cpp.o" \
"CMakeFiles/plot_core.dir/src/render/program.cpp.o" \
"CMakeFiles/plot_core.dir/src/render/renderer.cpp.o" \
"CMakeFiles/plot_core.dir/src/plot/ticks.cpp.o" \
"CMakeFiles/plot_core.dir/src/plot/axis.cpp.o" \
"CMakeFiles/plot_core.dir/src/plot/grid.cpp.o" \
"CMakeFiles/plot_core.dir/src/plot/measure.cpp.o" \
"CMakeFiles/plot_core.dir/src/plot/plot.cpp.o" \
"CMakeFiles/plot_core.dir/src/plot/figure.cpp.o" \
"CMakeFiles/plot_core.dir/src/input/controller.cpp.o"
# External object files for target plot_core
plot_core_EXTERNAL_OBJECTS =
deps/plotter/libplot_core.a: deps/plotter/CMakeFiles/plot_core.dir/src/core/status.cpp.o
deps/plotter/libplot_core.a: deps/plotter/CMakeFiles/plot_core.dir/src/math/transform.cpp.o
deps/plotter/libplot_core.a: deps/plotter/CMakeFiles/plot_core.dir/src/data/minmax_pyramid.cpp.o
deps/plotter/libplot_core.a: deps/plotter/CMakeFiles/plot_core.dir/src/data/series.cpp.o
deps/plotter/libplot_core.a: deps/plotter/CMakeFiles/plot_core.dir/src/data/array_series.cpp.o
deps/plotter/libplot_core.a: deps/plotter/CMakeFiles/plot_core.dir/src/data/stream_series.cpp.o
deps/plotter/libplot_core.a: deps/plotter/CMakeFiles/plot_core.dir/src/data/xy_series.cpp.o
deps/plotter/libplot_core.a: deps/plotter/CMakeFiles/plot_core.dir/src/gl/loader.cpp.o
deps/plotter/libplot_core.a: deps/plotter/CMakeFiles/plot_core.dir/src/gl/handle.cpp.o
deps/plotter/libplot_core.a: deps/plotter/CMakeFiles/plot_core.dir/src/render/shaders.cpp.o
deps/plotter/libplot_core.a: deps/plotter/CMakeFiles/plot_core.dir/src/render/text.cpp.o
deps/plotter/libplot_core.a: deps/plotter/CMakeFiles/plot_core.dir/src/render/text_atlas.cpp.o
deps/plotter/libplot_core.a: deps/plotter/CMakeFiles/plot_core.dir/src/render/embedded_mono_font.cpp.o
deps/plotter/libplot_core.a: deps/plotter/CMakeFiles/plot_core.dir/src/render/primitives.cpp.o
deps/plotter/libplot_core.a: deps/plotter/CMakeFiles/plot_core.dir/src/render/buffer.cpp.o
deps/plotter/libplot_core.a: deps/plotter/CMakeFiles/plot_core.dir/src/render/vertex_array.cpp.o
deps/plotter/libplot_core.a: deps/plotter/CMakeFiles/plot_core.dir/src/render/program.cpp.o
deps/plotter/libplot_core.a: deps/plotter/CMakeFiles/plot_core.dir/src/render/renderer.cpp.o
deps/plotter/libplot_core.a: deps/plotter/CMakeFiles/plot_core.dir/src/plot/ticks.cpp.o
deps/plotter/libplot_core.a: deps/plotter/CMakeFiles/plot_core.dir/src/plot/axis.cpp.o
deps/plotter/libplot_core.a: deps/plotter/CMakeFiles/plot_core.dir/src/plot/grid.cpp.o
deps/plotter/libplot_core.a: deps/plotter/CMakeFiles/plot_core.dir/src/plot/measure.cpp.o
deps/plotter/libplot_core.a: deps/plotter/CMakeFiles/plot_core.dir/src/plot/plot.cpp.o
deps/plotter/libplot_core.a: deps/plotter/CMakeFiles/plot_core.dir/src/plot/figure.cpp.o
deps/plotter/libplot_core.a: deps/plotter/CMakeFiles/plot_core.dir/src/input/controller.cpp.o
deps/plotter/libplot_core.a: deps/plotter/CMakeFiles/plot_core.dir/build.make
deps/plotter/libplot_core.a: deps/plotter/CMakeFiles/plot_core.dir/link.txt
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green --bold --progress-dir=/home/martino/Projects/tools/udpscope/tools/CMakeFiles --progress-num=$(CMAKE_PROGRESS_26) "Linking CXX static library libplot_core.a"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && $(CMAKE_COMMAND) -P CMakeFiles/plot_core.dir/cmake_clean_target.cmake
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && $(CMAKE_COMMAND) -E cmake_link_script CMakeFiles/plot_core.dir/link.txt --verbose=$(VERBOSE)
# Rule to build all files generated by this target.
deps/plotter/CMakeFiles/plot_core.dir/build: deps/plotter/libplot_core.a
.PHONY : deps/plotter/CMakeFiles/plot_core.dir/build
deps/plotter/CMakeFiles/plot_core.dir/clean:
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter && $(CMAKE_COMMAND) -P CMakeFiles/plot_core.dir/cmake_clean.cmake
.PHONY : deps/plotter/CMakeFiles/plot_core.dir/clean
deps/plotter/CMakeFiles/plot_core.dir/depend:
cd /home/martino/Projects/tools/udpscope/tools && $(CMAKE_COMMAND) -E cmake_depends "Unix Makefiles" /home/martino/Projects/tools/udpscope /home/martino/Projects/tools/udpscope/deps/plotter /home/martino/Projects/tools/udpscope/tools /home/martino/Projects/tools/udpscope/tools/deps/plotter /home/martino/Projects/tools/udpscope/tools/deps/plotter/CMakeFiles/plot_core.dir/DependInfo.cmake "--color=$(COLOR)" plot_core
.PHONY : deps/plotter/CMakeFiles/plot_core.dir/depend
@@ -0,0 +1,59 @@
file(REMOVE_RECURSE
"CMakeFiles/plot_core.dir/src/core/status.cpp.o"
"CMakeFiles/plot_core.dir/src/core/status.cpp.o.d"
"CMakeFiles/plot_core.dir/src/data/array_series.cpp.o"
"CMakeFiles/plot_core.dir/src/data/array_series.cpp.o.d"
"CMakeFiles/plot_core.dir/src/data/minmax_pyramid.cpp.o"
"CMakeFiles/plot_core.dir/src/data/minmax_pyramid.cpp.o.d"
"CMakeFiles/plot_core.dir/src/data/series.cpp.o"
"CMakeFiles/plot_core.dir/src/data/series.cpp.o.d"
"CMakeFiles/plot_core.dir/src/data/stream_series.cpp.o"
"CMakeFiles/plot_core.dir/src/data/stream_series.cpp.o.d"
"CMakeFiles/plot_core.dir/src/data/xy_series.cpp.o"
"CMakeFiles/plot_core.dir/src/data/xy_series.cpp.o.d"
"CMakeFiles/plot_core.dir/src/gl/handle.cpp.o"
"CMakeFiles/plot_core.dir/src/gl/handle.cpp.o.d"
"CMakeFiles/plot_core.dir/src/gl/loader.cpp.o"
"CMakeFiles/plot_core.dir/src/gl/loader.cpp.o.d"
"CMakeFiles/plot_core.dir/src/input/controller.cpp.o"
"CMakeFiles/plot_core.dir/src/input/controller.cpp.o.d"
"CMakeFiles/plot_core.dir/src/math/transform.cpp.o"
"CMakeFiles/plot_core.dir/src/math/transform.cpp.o.d"
"CMakeFiles/plot_core.dir/src/plot/axis.cpp.o"
"CMakeFiles/plot_core.dir/src/plot/axis.cpp.o.d"
"CMakeFiles/plot_core.dir/src/plot/figure.cpp.o"
"CMakeFiles/plot_core.dir/src/plot/figure.cpp.o.d"
"CMakeFiles/plot_core.dir/src/plot/grid.cpp.o"
"CMakeFiles/plot_core.dir/src/plot/grid.cpp.o.d"
"CMakeFiles/plot_core.dir/src/plot/measure.cpp.o"
"CMakeFiles/plot_core.dir/src/plot/measure.cpp.o.d"
"CMakeFiles/plot_core.dir/src/plot/plot.cpp.o"
"CMakeFiles/plot_core.dir/src/plot/plot.cpp.o.d"
"CMakeFiles/plot_core.dir/src/plot/ticks.cpp.o"
"CMakeFiles/plot_core.dir/src/plot/ticks.cpp.o.d"
"CMakeFiles/plot_core.dir/src/render/buffer.cpp.o"
"CMakeFiles/plot_core.dir/src/render/buffer.cpp.o.d"
"CMakeFiles/plot_core.dir/src/render/embedded_mono_font.cpp.o"
"CMakeFiles/plot_core.dir/src/render/embedded_mono_font.cpp.o.d"
"CMakeFiles/plot_core.dir/src/render/primitives.cpp.o"
"CMakeFiles/plot_core.dir/src/render/primitives.cpp.o.d"
"CMakeFiles/plot_core.dir/src/render/program.cpp.o"
"CMakeFiles/plot_core.dir/src/render/program.cpp.o.d"
"CMakeFiles/plot_core.dir/src/render/renderer.cpp.o"
"CMakeFiles/plot_core.dir/src/render/renderer.cpp.o.d"
"CMakeFiles/plot_core.dir/src/render/shaders.cpp.o"
"CMakeFiles/plot_core.dir/src/render/shaders.cpp.o.d"
"CMakeFiles/plot_core.dir/src/render/text.cpp.o"
"CMakeFiles/plot_core.dir/src/render/text.cpp.o.d"
"CMakeFiles/plot_core.dir/src/render/text_atlas.cpp.o"
"CMakeFiles/plot_core.dir/src/render/text_atlas.cpp.o.d"
"CMakeFiles/plot_core.dir/src/render/vertex_array.cpp.o"
"CMakeFiles/plot_core.dir/src/render/vertex_array.cpp.o.d"
"libplot_core.a"
"libplot_core.pdb"
)
# Per-language clean rules from dependency scanning.
foreach(lang CXX)
include(CMakeFiles/plot_core.dir/cmake_clean_${lang}.cmake OPTIONAL)
endforeach()
@@ -0,0 +1,3 @@
file(REMOVE_RECURSE
"libplot_core.a"
)
@@ -0,0 +1,2 @@
# Empty compiler generated dependencies file for plot_core.
# This may be replaced when dependencies are built.
@@ -0,0 +1,2 @@
# CMAKE generated file: DO NOT EDIT!
# Timestamp file for compiler generated dependencies management for plot_core.
@@ -0,0 +1,2 @@
# Empty dependencies file for plot_core.
# This may be replaced when dependencies are built.
@@ -0,0 +1,10 @@
# CMAKE generated file: DO NOT EDIT!
# Generated by "Unix Makefiles" Generator, CMake Version 4.4
# compile CXX with /usr/bin/c++
CXX_DEFINES =
CXX_INCLUDES = -I/home/martino/Projects/tools/udpscope/deps/plotter/include -I/home/martino/Projects/tools/udpscope/tools/deps/plotter/include
CXX_FLAGS = -O3 -DNDEBUG -std=c++20 -Wall -Wextra -Wpedantic -Wconversion -Wshadow
@@ -0,0 +1,2 @@
/usr/bin/ar qc libplot_core.a CMakeFiles/plot_core.dir/src/core/status.cpp.o CMakeFiles/plot_core.dir/src/math/transform.cpp.o CMakeFiles/plot_core.dir/src/data/minmax_pyramid.cpp.o CMakeFiles/plot_core.dir/src/data/series.cpp.o CMakeFiles/plot_core.dir/src/data/array_series.cpp.o CMakeFiles/plot_core.dir/src/data/stream_series.cpp.o CMakeFiles/plot_core.dir/src/data/xy_series.cpp.o CMakeFiles/plot_core.dir/src/gl/loader.cpp.o CMakeFiles/plot_core.dir/src/gl/handle.cpp.o CMakeFiles/plot_core.dir/src/render/shaders.cpp.o CMakeFiles/plot_core.dir/src/render/text.cpp.o CMakeFiles/plot_core.dir/src/render/text_atlas.cpp.o CMakeFiles/plot_core.dir/src/render/embedded_mono_font.cpp.o CMakeFiles/plot_core.dir/src/render/primitives.cpp.o CMakeFiles/plot_core.dir/src/render/buffer.cpp.o CMakeFiles/plot_core.dir/src/render/vertex_array.cpp.o CMakeFiles/plot_core.dir/src/render/program.cpp.o CMakeFiles/plot_core.dir/src/render/renderer.cpp.o CMakeFiles/plot_core.dir/src/plot/ticks.cpp.o CMakeFiles/plot_core.dir/src/plot/axis.cpp.o CMakeFiles/plot_core.dir/src/plot/grid.cpp.o CMakeFiles/plot_core.dir/src/plot/measure.cpp.o CMakeFiles/plot_core.dir/src/plot/plot.cpp.o CMakeFiles/plot_core.dir/src/plot/figure.cpp.o CMakeFiles/plot_core.dir/src/input/controller.cpp.o
/usr/bin/ranlib libplot_core.a
@@ -0,0 +1,27 @@
CMAKE_PROGRESS_1 = 2
CMAKE_PROGRESS_2 = 3
CMAKE_PROGRESS_3 = 4
CMAKE_PROGRESS_4 = 5
CMAKE_PROGRESS_5 = 6
CMAKE_PROGRESS_6 = 7
CMAKE_PROGRESS_7 = 8
CMAKE_PROGRESS_8 = 9
CMAKE_PROGRESS_9 = 10
CMAKE_PROGRESS_10 = 11
CMAKE_PROGRESS_11 = 12
CMAKE_PROGRESS_12 = 13
CMAKE_PROGRESS_13 = 14
CMAKE_PROGRESS_14 = 15
CMAKE_PROGRESS_15 = 16
CMAKE_PROGRESS_16 = 17
CMAKE_PROGRESS_17 = 18
CMAKE_PROGRESS_18 = 19
CMAKE_PROGRESS_19 = 20
CMAKE_PROGRESS_20 = 21
CMAKE_PROGRESS_21 = 22
CMAKE_PROGRESS_22 = 23
CMAKE_PROGRESS_23 = 24
CMAKE_PROGRESS_24 = 25
CMAKE_PROGRESS_25 = 26
CMAKE_PROGRESS_26 = 27
@@ -0,0 +1 @@
31
+832
View File
@@ -0,0 +1,832 @@
# CMAKE generated file: DO NOT EDIT!
# Generated by "Unix Makefiles" Generator, CMake Version 4.4
# Default target executed when no arguments are given to make.
default_target: all
.PHONY : default_target
# Allow only one "make -f Makefile2" at a time, but pass parallelism.
.NOTPARALLEL:
#=============================================================================
# Special targets provided by cmake.
# Disable implicit rules so canonical targets will work.
.SUFFIXES:
# Disable VCS-based implicit rules.
% : %,v
# Disable VCS-based implicit rules.
% : RCS/%
# Disable VCS-based implicit rules.
% : RCS/%,v
# Disable VCS-based implicit rules.
% : SCCS/s.%
# Disable VCS-based implicit rules.
% : s.%
.SUFFIXES: .hpux_make_needs_suffix_list
# Command-line flag to silence nested $(MAKE).
$(VERBOSE)MAKESILENT = -s
#Suppress display of executed commands.
$(VERBOSE).SILENT:
# A target that is always out of date.
cmake_force:
.PHONY : cmake_force
#=============================================================================
# Set environment variables for the build.
# The shell in which to execute make rules.
SHELL = /bin/sh
# The CMake executable.
CMAKE_COMMAND = /usr/bin/cmake
# The command to remove a file.
RM = /usr/bin/cmake -E rm -f
# Escaping for special characters.
EQUALS = =
# The top-level source directory on which CMake was run.
CMAKE_SOURCE_DIR = /home/martino/Projects/tools/udpscope
# The top-level build directory on which CMake was run.
CMAKE_BINARY_DIR = /home/martino/Projects/tools/udpscope/tools
#=============================================================================
# Targets provided globally by CMake.
# Special rule for the target edit_cache
edit_cache:
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "Running CMake cache editor..."
/usr/bin/ccmake -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR)
.PHONY : edit_cache
# Special rule for the target edit_cache
edit_cache/fast: edit_cache
.PHONY : edit_cache/fast
# Special rule for the target rebuild_cache
rebuild_cache:
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "Running CMake to regenerate build system..."
/usr/bin/cmake --regenerate-during-build -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR)
.PHONY : rebuild_cache
# Special rule for the target rebuild_cache
rebuild_cache/fast: rebuild_cache
.PHONY : rebuild_cache/fast
# The main all target
all: cmake_check_build_system
cd /home/martino/Projects/tools/udpscope/tools && $(CMAKE_COMMAND) -E cmake_progress_start /home/martino/Projects/tools/udpscope/tools/CMakeFiles /home/martino/Projects/tools/udpscope/tools/deps/plotter//CMakeFiles/progress.marks
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 deps/plotter/all
$(CMAKE_COMMAND) -E cmake_progress_start /home/martino/Projects/tools/udpscope/tools/CMakeFiles 0
.PHONY : all
# The main clean target
clean:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 deps/plotter/clean
.PHONY : clean
# The main clean target
clean/fast: clean
.PHONY : clean/fast
# Prepare targets for installation.
preinstall: all
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 deps/plotter/preinstall
.PHONY : preinstall
# Prepare targets for installation.
preinstall/fast:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 deps/plotter/preinstall
.PHONY : preinstall/fast
# clear depends
depend:
cd /home/martino/Projects/tools/udpscope/tools && $(CMAKE_COMMAND) -P /home/martino/Projects/tools/udpscope/tools/CMakeFiles/VerifyGlobs.cmake
cd /home/martino/Projects/tools/udpscope/tools && $(CMAKE_COMMAND) -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR) --check-build-system CMakeFiles/Makefile.cmake 1
.PHONY : depend
# Convenience name for target.
deps/plotter/CMakeFiles/plot_core.dir/rule:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 deps/plotter/CMakeFiles/plot_core.dir/rule
.PHONY : deps/plotter/CMakeFiles/plot_core.dir/rule
# Convenience name for target.
plot_core: deps/plotter/CMakeFiles/plot_core.dir/rule
.PHONY : plot_core
# fast build rule for target.
plot_core/fast:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/build
.PHONY : plot_core/fast
src/core/status.o: src/core/status.cpp.o
.PHONY : src/core/status.o
# target to build an object file
src/core/status.cpp.o:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/core/status.cpp.o
.PHONY : src/core/status.cpp.o
src/core/status.i: src/core/status.cpp.i
.PHONY : src/core/status.i
# target to preprocess a source file
src/core/status.cpp.i:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/core/status.cpp.i
.PHONY : src/core/status.cpp.i
src/core/status.s: src/core/status.cpp.s
.PHONY : src/core/status.s
# target to generate assembly for a file
src/core/status.cpp.s:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/core/status.cpp.s
.PHONY : src/core/status.cpp.s
src/data/array_series.o: src/data/array_series.cpp.o
.PHONY : src/data/array_series.o
# target to build an object file
src/data/array_series.cpp.o:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/data/array_series.cpp.o
.PHONY : src/data/array_series.cpp.o
src/data/array_series.i: src/data/array_series.cpp.i
.PHONY : src/data/array_series.i
# target to preprocess a source file
src/data/array_series.cpp.i:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/data/array_series.cpp.i
.PHONY : src/data/array_series.cpp.i
src/data/array_series.s: src/data/array_series.cpp.s
.PHONY : src/data/array_series.s
# target to generate assembly for a file
src/data/array_series.cpp.s:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/data/array_series.cpp.s
.PHONY : src/data/array_series.cpp.s
src/data/minmax_pyramid.o: src/data/minmax_pyramid.cpp.o
.PHONY : src/data/minmax_pyramid.o
# target to build an object file
src/data/minmax_pyramid.cpp.o:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/data/minmax_pyramid.cpp.o
.PHONY : src/data/minmax_pyramid.cpp.o
src/data/minmax_pyramid.i: src/data/minmax_pyramid.cpp.i
.PHONY : src/data/minmax_pyramid.i
# target to preprocess a source file
src/data/minmax_pyramid.cpp.i:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/data/minmax_pyramid.cpp.i
.PHONY : src/data/minmax_pyramid.cpp.i
src/data/minmax_pyramid.s: src/data/minmax_pyramid.cpp.s
.PHONY : src/data/minmax_pyramid.s
# target to generate assembly for a file
src/data/minmax_pyramid.cpp.s:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/data/minmax_pyramid.cpp.s
.PHONY : src/data/minmax_pyramid.cpp.s
src/data/series.o: src/data/series.cpp.o
.PHONY : src/data/series.o
# target to build an object file
src/data/series.cpp.o:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/data/series.cpp.o
.PHONY : src/data/series.cpp.o
src/data/series.i: src/data/series.cpp.i
.PHONY : src/data/series.i
# target to preprocess a source file
src/data/series.cpp.i:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/data/series.cpp.i
.PHONY : src/data/series.cpp.i
src/data/series.s: src/data/series.cpp.s
.PHONY : src/data/series.s
# target to generate assembly for a file
src/data/series.cpp.s:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/data/series.cpp.s
.PHONY : src/data/series.cpp.s
src/data/stream_series.o: src/data/stream_series.cpp.o
.PHONY : src/data/stream_series.o
# target to build an object file
src/data/stream_series.cpp.o:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/data/stream_series.cpp.o
.PHONY : src/data/stream_series.cpp.o
src/data/stream_series.i: src/data/stream_series.cpp.i
.PHONY : src/data/stream_series.i
# target to preprocess a source file
src/data/stream_series.cpp.i:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/data/stream_series.cpp.i
.PHONY : src/data/stream_series.cpp.i
src/data/stream_series.s: src/data/stream_series.cpp.s
.PHONY : src/data/stream_series.s
# target to generate assembly for a file
src/data/stream_series.cpp.s:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/data/stream_series.cpp.s
.PHONY : src/data/stream_series.cpp.s
src/data/xy_series.o: src/data/xy_series.cpp.o
.PHONY : src/data/xy_series.o
# target to build an object file
src/data/xy_series.cpp.o:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/data/xy_series.cpp.o
.PHONY : src/data/xy_series.cpp.o
src/data/xy_series.i: src/data/xy_series.cpp.i
.PHONY : src/data/xy_series.i
# target to preprocess a source file
src/data/xy_series.cpp.i:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/data/xy_series.cpp.i
.PHONY : src/data/xy_series.cpp.i
src/data/xy_series.s: src/data/xy_series.cpp.s
.PHONY : src/data/xy_series.s
# target to generate assembly for a file
src/data/xy_series.cpp.s:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/data/xy_series.cpp.s
.PHONY : src/data/xy_series.cpp.s
src/gl/handle.o: src/gl/handle.cpp.o
.PHONY : src/gl/handle.o
# target to build an object file
src/gl/handle.cpp.o:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/gl/handle.cpp.o
.PHONY : src/gl/handle.cpp.o
src/gl/handle.i: src/gl/handle.cpp.i
.PHONY : src/gl/handle.i
# target to preprocess a source file
src/gl/handle.cpp.i:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/gl/handle.cpp.i
.PHONY : src/gl/handle.cpp.i
src/gl/handle.s: src/gl/handle.cpp.s
.PHONY : src/gl/handle.s
# target to generate assembly for a file
src/gl/handle.cpp.s:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/gl/handle.cpp.s
.PHONY : src/gl/handle.cpp.s
src/gl/loader.o: src/gl/loader.cpp.o
.PHONY : src/gl/loader.o
# target to build an object file
src/gl/loader.cpp.o:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/gl/loader.cpp.o
.PHONY : src/gl/loader.cpp.o
src/gl/loader.i: src/gl/loader.cpp.i
.PHONY : src/gl/loader.i
# target to preprocess a source file
src/gl/loader.cpp.i:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/gl/loader.cpp.i
.PHONY : src/gl/loader.cpp.i
src/gl/loader.s: src/gl/loader.cpp.s
.PHONY : src/gl/loader.s
# target to generate assembly for a file
src/gl/loader.cpp.s:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/gl/loader.cpp.s
.PHONY : src/gl/loader.cpp.s
src/input/controller.o: src/input/controller.cpp.o
.PHONY : src/input/controller.o
# target to build an object file
src/input/controller.cpp.o:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/input/controller.cpp.o
.PHONY : src/input/controller.cpp.o
src/input/controller.i: src/input/controller.cpp.i
.PHONY : src/input/controller.i
# target to preprocess a source file
src/input/controller.cpp.i:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/input/controller.cpp.i
.PHONY : src/input/controller.cpp.i
src/input/controller.s: src/input/controller.cpp.s
.PHONY : src/input/controller.s
# target to generate assembly for a file
src/input/controller.cpp.s:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/input/controller.cpp.s
.PHONY : src/input/controller.cpp.s
src/math/transform.o: src/math/transform.cpp.o
.PHONY : src/math/transform.o
# target to build an object file
src/math/transform.cpp.o:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/math/transform.cpp.o
.PHONY : src/math/transform.cpp.o
src/math/transform.i: src/math/transform.cpp.i
.PHONY : src/math/transform.i
# target to preprocess a source file
src/math/transform.cpp.i:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/math/transform.cpp.i
.PHONY : src/math/transform.cpp.i
src/math/transform.s: src/math/transform.cpp.s
.PHONY : src/math/transform.s
# target to generate assembly for a file
src/math/transform.cpp.s:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/math/transform.cpp.s
.PHONY : src/math/transform.cpp.s
src/plot/axis.o: src/plot/axis.cpp.o
.PHONY : src/plot/axis.o
# target to build an object file
src/plot/axis.cpp.o:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/plot/axis.cpp.o
.PHONY : src/plot/axis.cpp.o
src/plot/axis.i: src/plot/axis.cpp.i
.PHONY : src/plot/axis.i
# target to preprocess a source file
src/plot/axis.cpp.i:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/plot/axis.cpp.i
.PHONY : src/plot/axis.cpp.i
src/plot/axis.s: src/plot/axis.cpp.s
.PHONY : src/plot/axis.s
# target to generate assembly for a file
src/plot/axis.cpp.s:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/plot/axis.cpp.s
.PHONY : src/plot/axis.cpp.s
src/plot/figure.o: src/plot/figure.cpp.o
.PHONY : src/plot/figure.o
# target to build an object file
src/plot/figure.cpp.o:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/plot/figure.cpp.o
.PHONY : src/plot/figure.cpp.o
src/plot/figure.i: src/plot/figure.cpp.i
.PHONY : src/plot/figure.i
# target to preprocess a source file
src/plot/figure.cpp.i:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/plot/figure.cpp.i
.PHONY : src/plot/figure.cpp.i
src/plot/figure.s: src/plot/figure.cpp.s
.PHONY : src/plot/figure.s
# target to generate assembly for a file
src/plot/figure.cpp.s:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/plot/figure.cpp.s
.PHONY : src/plot/figure.cpp.s
src/plot/grid.o: src/plot/grid.cpp.o
.PHONY : src/plot/grid.o
# target to build an object file
src/plot/grid.cpp.o:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/plot/grid.cpp.o
.PHONY : src/plot/grid.cpp.o
src/plot/grid.i: src/plot/grid.cpp.i
.PHONY : src/plot/grid.i
# target to preprocess a source file
src/plot/grid.cpp.i:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/plot/grid.cpp.i
.PHONY : src/plot/grid.cpp.i
src/plot/grid.s: src/plot/grid.cpp.s
.PHONY : src/plot/grid.s
# target to generate assembly for a file
src/plot/grid.cpp.s:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/plot/grid.cpp.s
.PHONY : src/plot/grid.cpp.s
src/plot/measure.o: src/plot/measure.cpp.o
.PHONY : src/plot/measure.o
# target to build an object file
src/plot/measure.cpp.o:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/plot/measure.cpp.o
.PHONY : src/plot/measure.cpp.o
src/plot/measure.i: src/plot/measure.cpp.i
.PHONY : src/plot/measure.i
# target to preprocess a source file
src/plot/measure.cpp.i:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/plot/measure.cpp.i
.PHONY : src/plot/measure.cpp.i
src/plot/measure.s: src/plot/measure.cpp.s
.PHONY : src/plot/measure.s
# target to generate assembly for a file
src/plot/measure.cpp.s:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/plot/measure.cpp.s
.PHONY : src/plot/measure.cpp.s
src/plot/plot.o: src/plot/plot.cpp.o
.PHONY : src/plot/plot.o
# target to build an object file
src/plot/plot.cpp.o:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/plot/plot.cpp.o
.PHONY : src/plot/plot.cpp.o
src/plot/plot.i: src/plot/plot.cpp.i
.PHONY : src/plot/plot.i
# target to preprocess a source file
src/plot/plot.cpp.i:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/plot/plot.cpp.i
.PHONY : src/plot/plot.cpp.i
src/plot/plot.s: src/plot/plot.cpp.s
.PHONY : src/plot/plot.s
# target to generate assembly for a file
src/plot/plot.cpp.s:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/plot/plot.cpp.s
.PHONY : src/plot/plot.cpp.s
src/plot/ticks.o: src/plot/ticks.cpp.o
.PHONY : src/plot/ticks.o
# target to build an object file
src/plot/ticks.cpp.o:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/plot/ticks.cpp.o
.PHONY : src/plot/ticks.cpp.o
src/plot/ticks.i: src/plot/ticks.cpp.i
.PHONY : src/plot/ticks.i
# target to preprocess a source file
src/plot/ticks.cpp.i:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/plot/ticks.cpp.i
.PHONY : src/plot/ticks.cpp.i
src/plot/ticks.s: src/plot/ticks.cpp.s
.PHONY : src/plot/ticks.s
# target to generate assembly for a file
src/plot/ticks.cpp.s:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/plot/ticks.cpp.s
.PHONY : src/plot/ticks.cpp.s
src/render/buffer.o: src/render/buffer.cpp.o
.PHONY : src/render/buffer.o
# target to build an object file
src/render/buffer.cpp.o:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/render/buffer.cpp.o
.PHONY : src/render/buffer.cpp.o
src/render/buffer.i: src/render/buffer.cpp.i
.PHONY : src/render/buffer.i
# target to preprocess a source file
src/render/buffer.cpp.i:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/render/buffer.cpp.i
.PHONY : src/render/buffer.cpp.i
src/render/buffer.s: src/render/buffer.cpp.s
.PHONY : src/render/buffer.s
# target to generate assembly for a file
src/render/buffer.cpp.s:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/render/buffer.cpp.s
.PHONY : src/render/buffer.cpp.s
src/render/embedded_mono_font.o: src/render/embedded_mono_font.cpp.o
.PHONY : src/render/embedded_mono_font.o
# target to build an object file
src/render/embedded_mono_font.cpp.o:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/render/embedded_mono_font.cpp.o
.PHONY : src/render/embedded_mono_font.cpp.o
src/render/embedded_mono_font.i: src/render/embedded_mono_font.cpp.i
.PHONY : src/render/embedded_mono_font.i
# target to preprocess a source file
src/render/embedded_mono_font.cpp.i:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/render/embedded_mono_font.cpp.i
.PHONY : src/render/embedded_mono_font.cpp.i
src/render/embedded_mono_font.s: src/render/embedded_mono_font.cpp.s
.PHONY : src/render/embedded_mono_font.s
# target to generate assembly for a file
src/render/embedded_mono_font.cpp.s:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/render/embedded_mono_font.cpp.s
.PHONY : src/render/embedded_mono_font.cpp.s
src/render/primitives.o: src/render/primitives.cpp.o
.PHONY : src/render/primitives.o
# target to build an object file
src/render/primitives.cpp.o:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/render/primitives.cpp.o
.PHONY : src/render/primitives.cpp.o
src/render/primitives.i: src/render/primitives.cpp.i
.PHONY : src/render/primitives.i
# target to preprocess a source file
src/render/primitives.cpp.i:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/render/primitives.cpp.i
.PHONY : src/render/primitives.cpp.i
src/render/primitives.s: src/render/primitives.cpp.s
.PHONY : src/render/primitives.s
# target to generate assembly for a file
src/render/primitives.cpp.s:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/render/primitives.cpp.s
.PHONY : src/render/primitives.cpp.s
src/render/program.o: src/render/program.cpp.o
.PHONY : src/render/program.o
# target to build an object file
src/render/program.cpp.o:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/render/program.cpp.o
.PHONY : src/render/program.cpp.o
src/render/program.i: src/render/program.cpp.i
.PHONY : src/render/program.i
# target to preprocess a source file
src/render/program.cpp.i:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/render/program.cpp.i
.PHONY : src/render/program.cpp.i
src/render/program.s: src/render/program.cpp.s
.PHONY : src/render/program.s
# target to generate assembly for a file
src/render/program.cpp.s:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/render/program.cpp.s
.PHONY : src/render/program.cpp.s
src/render/renderer.o: src/render/renderer.cpp.o
.PHONY : src/render/renderer.o
# target to build an object file
src/render/renderer.cpp.o:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/render/renderer.cpp.o
.PHONY : src/render/renderer.cpp.o
src/render/renderer.i: src/render/renderer.cpp.i
.PHONY : src/render/renderer.i
# target to preprocess a source file
src/render/renderer.cpp.i:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/render/renderer.cpp.i
.PHONY : src/render/renderer.cpp.i
src/render/renderer.s: src/render/renderer.cpp.s
.PHONY : src/render/renderer.s
# target to generate assembly for a file
src/render/renderer.cpp.s:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/render/renderer.cpp.s
.PHONY : src/render/renderer.cpp.s
src/render/shaders.o: src/render/shaders.cpp.o
.PHONY : src/render/shaders.o
# target to build an object file
src/render/shaders.cpp.o:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/render/shaders.cpp.o
.PHONY : src/render/shaders.cpp.o
src/render/shaders.i: src/render/shaders.cpp.i
.PHONY : src/render/shaders.i
# target to preprocess a source file
src/render/shaders.cpp.i:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/render/shaders.cpp.i
.PHONY : src/render/shaders.cpp.i
src/render/shaders.s: src/render/shaders.cpp.s
.PHONY : src/render/shaders.s
# target to generate assembly for a file
src/render/shaders.cpp.s:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/render/shaders.cpp.s
.PHONY : src/render/shaders.cpp.s
src/render/text.o: src/render/text.cpp.o
.PHONY : src/render/text.o
# target to build an object file
src/render/text.cpp.o:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/render/text.cpp.o
.PHONY : src/render/text.cpp.o
src/render/text.i: src/render/text.cpp.i
.PHONY : src/render/text.i
# target to preprocess a source file
src/render/text.cpp.i:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/render/text.cpp.i
.PHONY : src/render/text.cpp.i
src/render/text.s: src/render/text.cpp.s
.PHONY : src/render/text.s
# target to generate assembly for a file
src/render/text.cpp.s:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/render/text.cpp.s
.PHONY : src/render/text.cpp.s
src/render/text_atlas.o: src/render/text_atlas.cpp.o
.PHONY : src/render/text_atlas.o
# target to build an object file
src/render/text_atlas.cpp.o:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/render/text_atlas.cpp.o
.PHONY : src/render/text_atlas.cpp.o
src/render/text_atlas.i: src/render/text_atlas.cpp.i
.PHONY : src/render/text_atlas.i
# target to preprocess a source file
src/render/text_atlas.cpp.i:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/render/text_atlas.cpp.i
.PHONY : src/render/text_atlas.cpp.i
src/render/text_atlas.s: src/render/text_atlas.cpp.s
.PHONY : src/render/text_atlas.s
# target to generate assembly for a file
src/render/text_atlas.cpp.s:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/render/text_atlas.cpp.s
.PHONY : src/render/text_atlas.cpp.s
src/render/vertex_array.o: src/render/vertex_array.cpp.o
.PHONY : src/render/vertex_array.o
# target to build an object file
src/render/vertex_array.cpp.o:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/render/vertex_array.cpp.o
.PHONY : src/render/vertex_array.cpp.o
src/render/vertex_array.i: src/render/vertex_array.cpp.i
.PHONY : src/render/vertex_array.i
# target to preprocess a source file
src/render/vertex_array.cpp.i:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/render/vertex_array.cpp.i
.PHONY : src/render/vertex_array.cpp.i
src/render/vertex_array.s: src/render/vertex_array.cpp.s
.PHONY : src/render/vertex_array.s
# target to generate assembly for a file
src/render/vertex_array.cpp.s:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/CMakeFiles/plot_core.dir/build.make deps/plotter/CMakeFiles/plot_core.dir/src/render/vertex_array.cpp.s
.PHONY : src/render/vertex_array.cpp.s
# Help Target
help:
@echo "The following are some of the valid targets for this Makefile:"
@echo "... all (the default if no target is provided)"
@echo "... clean"
@echo "... depend"
@echo "... edit_cache"
@echo "... rebuild_cache"
@echo "... plot_core"
@echo "... src/core/status.o"
@echo "... src/core/status.i"
@echo "... src/core/status.s"
@echo "... src/data/array_series.o"
@echo "... src/data/array_series.i"
@echo "... src/data/array_series.s"
@echo "... src/data/minmax_pyramid.o"
@echo "... src/data/minmax_pyramid.i"
@echo "... src/data/minmax_pyramid.s"
@echo "... src/data/series.o"
@echo "... src/data/series.i"
@echo "... src/data/series.s"
@echo "... src/data/stream_series.o"
@echo "... src/data/stream_series.i"
@echo "... src/data/stream_series.s"
@echo "... src/data/xy_series.o"
@echo "... src/data/xy_series.i"
@echo "... src/data/xy_series.s"
@echo "... src/gl/handle.o"
@echo "... src/gl/handle.i"
@echo "... src/gl/handle.s"
@echo "... src/gl/loader.o"
@echo "... src/gl/loader.i"
@echo "... src/gl/loader.s"
@echo "... src/input/controller.o"
@echo "... src/input/controller.i"
@echo "... src/input/controller.s"
@echo "... src/math/transform.o"
@echo "... src/math/transform.i"
@echo "... src/math/transform.s"
@echo "... src/plot/axis.o"
@echo "... src/plot/axis.i"
@echo "... src/plot/axis.s"
@echo "... src/plot/figure.o"
@echo "... src/plot/figure.i"
@echo "... src/plot/figure.s"
@echo "... src/plot/grid.o"
@echo "... src/plot/grid.i"
@echo "... src/plot/grid.s"
@echo "... src/plot/measure.o"
@echo "... src/plot/measure.i"
@echo "... src/plot/measure.s"
@echo "... src/plot/plot.o"
@echo "... src/plot/plot.i"
@echo "... src/plot/plot.s"
@echo "... src/plot/ticks.o"
@echo "... src/plot/ticks.i"
@echo "... src/plot/ticks.s"
@echo "... src/render/buffer.o"
@echo "... src/render/buffer.i"
@echo "... src/render/buffer.s"
@echo "... src/render/embedded_mono_font.o"
@echo "... src/render/embedded_mono_font.i"
@echo "... src/render/embedded_mono_font.s"
@echo "... src/render/primitives.o"
@echo "... src/render/primitives.i"
@echo "... src/render/primitives.s"
@echo "... src/render/program.o"
@echo "... src/render/program.i"
@echo "... src/render/program.s"
@echo "... src/render/renderer.o"
@echo "... src/render/renderer.i"
@echo "... src/render/renderer.s"
@echo "... src/render/shaders.o"
@echo "... src/render/shaders.i"
@echo "... src/render/shaders.s"
@echo "... src/render/text.o"
@echo "... src/render/text.i"
@echo "... src/render/text.s"
@echo "... src/render/text_atlas.o"
@echo "... src/render/text_atlas.i"
@echo "... src/render/text_atlas.s"
@echo "... src/render/vertex_array.o"
@echo "... src/render/vertex_array.i"
@echo "... src/render/vertex_array.s"
.PHONY : help
#=============================================================================
# Special targets to cleanup operation of make.
# Special rule to run CMake to check the build system integrity.
# No rule that depends on this can have commands that come from listfiles
# because they might be regenerated.
cmake_check_build_system:
cd /home/martino/Projects/tools/udpscope/tools && $(CMAKE_COMMAND) -P /home/martino/Projects/tools/udpscope/tools/CMakeFiles/VerifyGlobs.cmake
cd /home/martino/Projects/tools/udpscope/tools && $(CMAKE_COMMAND) -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR) --check-build-system CMakeFiles/Makefile.cmake 0
.PHONY : cmake_check_build_system
@@ -0,0 +1,16 @@
# CMAKE generated file: DO NOT EDIT!
# Generated by "Unix Makefiles" Generator, CMake Version 4.4
# Relative path conversion top directories.
set(CMAKE_RELATIVE_PATH_TOP_SOURCE "/home/martino/Projects/tools/udpscope")
set(CMAKE_RELATIVE_PATH_TOP_BINARY "/home/martino/Projects/tools/udpscope/tools")
# Force unix paths in dependencies.
set(CMAKE_FORCE_UNIX_PATHS 1)
# The C and CXX include file regular expressions for this directory.
set(CMAKE_C_INCLUDE_REGEX_SCAN "^.*$")
set(CMAKE_C_INCLUDE_REGEX_COMPLAIN "^$")
set(CMAKE_CXX_INCLUDE_REGEX_SCAN ${CMAKE_C_INCLUDE_REGEX_SCAN})
set(CMAKE_CXX_INCLUDE_REGEX_COMPLAIN ${CMAKE_C_INCLUDE_REGEX_COMPLAIN})
@@ -0,0 +1,23 @@
# Consider dependencies only in project.
set(CMAKE_DEPENDS_IN_PROJECT_ONLY OFF)
# The set of languages for which implicit dependencies are needed:
set(CMAKE_DEPENDS_LANGUAGES
)
# The set of dependency files which are needed:
set(CMAKE_DEPENDS_DEPENDENCY_FILES
"/home/martino/Projects/tools/udpscope/deps/plotter/adapters/gtk4/gtk_plot_area.cpp" "deps/plotter/adapters/gtk4/CMakeFiles/plot_gtk4.dir/gtk_plot_area.cpp.o" "gcc" "deps/plotter/adapters/gtk4/CMakeFiles/plot_gtk4.dir/gtk_plot_area.cpp.o.d"
)
# Targets to which this target links which contain Fortran sources.
set(CMAKE_Fortran_TARGET_LINKED_INFO_FILES
)
# Targets to which this target links which contain Fortran sources.
set(CMAKE_Fortran_TARGET_FORWARD_LINKED_INFO_FILES
)
# Fortran module output directory.
set(CMAKE_Fortran_TARGET_MODULE_DIR "")
@@ -0,0 +1,114 @@
# CMAKE generated file: DO NOT EDIT!
# Generated by "Unix Makefiles" Generator, CMake Version 4.4
# Delete rule output on recipe failure.
.DELETE_ON_ERROR:
#=============================================================================
# Special targets provided by cmake.
# Disable implicit rules so canonical targets will work.
.SUFFIXES:
# Disable VCS-based implicit rules.
% : %,v
# Disable VCS-based implicit rules.
% : RCS/%
# Disable VCS-based implicit rules.
% : RCS/%,v
# Disable VCS-based implicit rules.
% : SCCS/s.%
# Disable VCS-based implicit rules.
% : s.%
.SUFFIXES: .hpux_make_needs_suffix_list
# Command-line flag to silence nested $(MAKE).
$(VERBOSE)MAKESILENT = -s
#Suppress display of executed commands.
$(VERBOSE).SILENT:
# A target that is always out of date.
cmake_force:
.PHONY : cmake_force
#=============================================================================
# Set environment variables for the build.
# The shell in which to execute make rules.
SHELL = /bin/sh
# The CMake executable.
CMAKE_COMMAND = /usr/bin/cmake
# The command to remove a file.
RM = /usr/bin/cmake -E rm -f
# Escaping for special characters.
EQUALS = =
# The top-level source directory on which CMake was run.
CMAKE_SOURCE_DIR = /home/martino/Projects/tools/udpscope
# The top-level build directory on which CMake was run.
CMAKE_BINARY_DIR = /home/martino/Projects/tools/udpscope/tools
# Include any dependencies generated for this target.
include deps/plotter/adapters/gtk4/CMakeFiles/plot_gtk4.dir/depend.make
# Include any dependencies generated by the compiler for this target.
include deps/plotter/adapters/gtk4/CMakeFiles/plot_gtk4.dir/compiler_depend.make
# Include the progress variables for this target.
include deps/plotter/adapters/gtk4/CMakeFiles/plot_gtk4.dir/progress.make
# Include the compile flags for this target's objects.
include deps/plotter/adapters/gtk4/CMakeFiles/plot_gtk4.dir/flags.make
deps/plotter/adapters/gtk4/CMakeFiles/plot_gtk4.dir/codegen:
.PHONY : deps/plotter/adapters/gtk4/CMakeFiles/plot_gtk4.dir/codegen
deps/plotter/adapters/gtk4/CMakeFiles/plot_gtk4.dir/gtk_plot_area.cpp.o: deps/plotter/adapters/gtk4/CMakeFiles/plot_gtk4.dir/flags.make
deps/plotter/adapters/gtk4/CMakeFiles/plot_gtk4.dir/gtk_plot_area.cpp.o: /home/martino/Projects/tools/udpscope/deps/plotter/adapters/gtk4/gtk_plot_area.cpp
deps/plotter/adapters/gtk4/CMakeFiles/plot_gtk4.dir/gtk_plot_area.cpp.o: deps/plotter/adapters/gtk4/CMakeFiles/plot_gtk4.dir/compiler_depend.ts
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green --progress-dir=/home/martino/Projects/tools/udpscope/tools/CMakeFiles --progress-num=$(CMAKE_PROGRESS_1) "Building CXX object deps/plotter/adapters/gtk4/CMakeFiles/plot_gtk4.dir/gtk_plot_area.cpp.o"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter/adapters/gtk4 && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -MD -MT deps/plotter/adapters/gtk4/CMakeFiles/plot_gtk4.dir/gtk_plot_area.cpp.o -MF CMakeFiles/plot_gtk4.dir/gtk_plot_area.cpp.o.d -o CMakeFiles/plot_gtk4.dir/gtk_plot_area.cpp.o -c /home/martino/Projects/tools/udpscope/deps/plotter/adapters/gtk4/gtk_plot_area.cpp
deps/plotter/adapters/gtk4/CMakeFiles/plot_gtk4.dir/gtk_plot_area.cpp.i: cmake_force
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green "Preprocessing CXX source to CMakeFiles/plot_gtk4.dir/gtk_plot_area.cpp.i"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter/adapters/gtk4 && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -E /home/martino/Projects/tools/udpscope/deps/plotter/adapters/gtk4/gtk_plot_area.cpp > CMakeFiles/plot_gtk4.dir/gtk_plot_area.cpp.i
deps/plotter/adapters/gtk4/CMakeFiles/plot_gtk4.dir/gtk_plot_area.cpp.s: cmake_force
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green "Compiling CXX source to assembly CMakeFiles/plot_gtk4.dir/gtk_plot_area.cpp.s"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter/adapters/gtk4 && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -S /home/martino/Projects/tools/udpscope/deps/plotter/adapters/gtk4/gtk_plot_area.cpp -o CMakeFiles/plot_gtk4.dir/gtk_plot_area.cpp.s
# Object files for target plot_gtk4
plot_gtk4_OBJECTS = \
"CMakeFiles/plot_gtk4.dir/gtk_plot_area.cpp.o"
# External object files for target plot_gtk4
plot_gtk4_EXTERNAL_OBJECTS =
deps/plotter/adapters/gtk4/libplot_gtk4.a: deps/plotter/adapters/gtk4/CMakeFiles/plot_gtk4.dir/gtk_plot_area.cpp.o
deps/plotter/adapters/gtk4/libplot_gtk4.a: deps/plotter/adapters/gtk4/CMakeFiles/plot_gtk4.dir/build.make
deps/plotter/adapters/gtk4/libplot_gtk4.a: deps/plotter/adapters/gtk4/CMakeFiles/plot_gtk4.dir/link.txt
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green --bold --progress-dir=/home/martino/Projects/tools/udpscope/tools/CMakeFiles --progress-num=$(CMAKE_PROGRESS_2) "Linking CXX static library libplot_gtk4.a"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter/adapters/gtk4 && $(CMAKE_COMMAND) -P CMakeFiles/plot_gtk4.dir/cmake_clean_target.cmake
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter/adapters/gtk4 && $(CMAKE_COMMAND) -E cmake_link_script CMakeFiles/plot_gtk4.dir/link.txt --verbose=$(VERBOSE)
# Rule to build all files generated by this target.
deps/plotter/adapters/gtk4/CMakeFiles/plot_gtk4.dir/build: deps/plotter/adapters/gtk4/libplot_gtk4.a
.PHONY : deps/plotter/adapters/gtk4/CMakeFiles/plot_gtk4.dir/build
deps/plotter/adapters/gtk4/CMakeFiles/plot_gtk4.dir/clean:
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter/adapters/gtk4 && $(CMAKE_COMMAND) -P CMakeFiles/plot_gtk4.dir/cmake_clean.cmake
.PHONY : deps/plotter/adapters/gtk4/CMakeFiles/plot_gtk4.dir/clean
deps/plotter/adapters/gtk4/CMakeFiles/plot_gtk4.dir/depend:
cd /home/martino/Projects/tools/udpscope/tools && $(CMAKE_COMMAND) -E cmake_depends "Unix Makefiles" /home/martino/Projects/tools/udpscope /home/martino/Projects/tools/udpscope/deps/plotter/adapters/gtk4 /home/martino/Projects/tools/udpscope/tools /home/martino/Projects/tools/udpscope/tools/deps/plotter/adapters/gtk4 /home/martino/Projects/tools/udpscope/tools/deps/plotter/adapters/gtk4/CMakeFiles/plot_gtk4.dir/DependInfo.cmake "--color=$(COLOR)" plot_gtk4
.PHONY : deps/plotter/adapters/gtk4/CMakeFiles/plot_gtk4.dir/depend
@@ -0,0 +1,11 @@
file(REMOVE_RECURSE
"CMakeFiles/plot_gtk4.dir/gtk_plot_area.cpp.o"
"CMakeFiles/plot_gtk4.dir/gtk_plot_area.cpp.o.d"
"libplot_gtk4.a"
"libplot_gtk4.pdb"
)
# Per-language clean rules from dependency scanning.
foreach(lang CXX)
include(CMakeFiles/plot_gtk4.dir/cmake_clean_${lang}.cmake OPTIONAL)
endforeach()
@@ -0,0 +1,3 @@
file(REMOVE_RECURSE
"libplot_gtk4.a"
)
@@ -0,0 +1,2 @@
# Empty compiler generated dependencies file for plot_gtk4.
# This may be replaced when dependencies are built.
@@ -0,0 +1,2 @@
# CMAKE generated file: DO NOT EDIT!
# Timestamp file for compiler generated dependencies management for plot_gtk4.
@@ -0,0 +1,2 @@
# Empty dependencies file for plot_gtk4.
# This may be replaced when dependencies are built.
@@ -0,0 +1,10 @@
# CMAKE generated file: DO NOT EDIT!
# Generated by "Unix Makefiles" Generator, CMake Version 4.4
# compile CXX with /usr/bin/c++
CXX_DEFINES =
CXX_INCLUDES = -I/home/martino/Projects/tools/udpscope/deps/plotter/adapters/gtk4/.. -I/home/martino/Projects/tools/udpscope/deps/plotter/include -I/home/martino/Projects/tools/udpscope/tools/deps/plotter/include -isystem /usr/include/gtk-4.0 -isystem /usr/include/pango-1.0 -isystem /usr/include/fribidi -isystem /usr/include/harfbuzz -isystem /usr/include/gdk-pixbuf-2.0 -isystem /usr/include/glycin-2 -isystem /usr/include/cairo -isystem /usr/include/freetype2 -isystem /usr/include/libpng16 -isystem /usr/include/pixman-1 -isystem /usr/include/graphene-1.0 -isystem /usr/lib/graphene-1.0/include -isystem /usr/include/glib-2.0 -isystem /usr/lib/glib-2.0/include -isystem /usr/include/libmount -isystem /usr/include/blkid -isystem /usr/include/sysprof-6
CXX_FLAGS = -O3 -DNDEBUG -std=c++20 -Wall -Wextra -Wpedantic -Wconversion -Wshadow -mfpmath=sse -msse -msse2 -pthread
@@ -0,0 +1,2 @@
/usr/bin/ar qc libplot_gtk4.a CMakeFiles/plot_gtk4.dir/gtk_plot_area.cpp.o
/usr/bin/ranlib libplot_gtk4.a
@@ -0,0 +1,3 @@
CMAKE_PROGRESS_1 = 30
CMAKE_PROGRESS_2 = 31
@@ -0,0 +1 @@
28
+184
View File
@@ -0,0 +1,184 @@
# CMAKE generated file: DO NOT EDIT!
# Generated by "Unix Makefiles" Generator, CMake Version 4.4
# Default target executed when no arguments are given to make.
default_target: all
.PHONY : default_target
# Allow only one "make -f Makefile2" at a time, but pass parallelism.
.NOTPARALLEL:
#=============================================================================
# Special targets provided by cmake.
# Disable implicit rules so canonical targets will work.
.SUFFIXES:
# Disable VCS-based implicit rules.
% : %,v
# Disable VCS-based implicit rules.
% : RCS/%
# Disable VCS-based implicit rules.
% : RCS/%,v
# Disable VCS-based implicit rules.
% : SCCS/s.%
# Disable VCS-based implicit rules.
% : s.%
.SUFFIXES: .hpux_make_needs_suffix_list
# Command-line flag to silence nested $(MAKE).
$(VERBOSE)MAKESILENT = -s
#Suppress display of executed commands.
$(VERBOSE).SILENT:
# A target that is always out of date.
cmake_force:
.PHONY : cmake_force
#=============================================================================
# Set environment variables for the build.
# The shell in which to execute make rules.
SHELL = /bin/sh
# The CMake executable.
CMAKE_COMMAND = /usr/bin/cmake
# The command to remove a file.
RM = /usr/bin/cmake -E rm -f
# Escaping for special characters.
EQUALS = =
# The top-level source directory on which CMake was run.
CMAKE_SOURCE_DIR = /home/martino/Projects/tools/udpscope
# The top-level build directory on which CMake was run.
CMAKE_BINARY_DIR = /home/martino/Projects/tools/udpscope/tools
#=============================================================================
# Targets provided globally by CMake.
# Special rule for the target edit_cache
edit_cache:
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "Running CMake cache editor..."
/usr/bin/ccmake -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR)
.PHONY : edit_cache
# Special rule for the target edit_cache
edit_cache/fast: edit_cache
.PHONY : edit_cache/fast
# Special rule for the target rebuild_cache
rebuild_cache:
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "Running CMake to regenerate build system..."
/usr/bin/cmake --regenerate-during-build -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR)
.PHONY : rebuild_cache
# Special rule for the target rebuild_cache
rebuild_cache/fast: rebuild_cache
.PHONY : rebuild_cache/fast
# The main all target
all: cmake_check_build_system
cd /home/martino/Projects/tools/udpscope/tools && $(CMAKE_COMMAND) -E cmake_progress_start /home/martino/Projects/tools/udpscope/tools/CMakeFiles /home/martino/Projects/tools/udpscope/tools/deps/plotter/adapters/gtk4//CMakeFiles/progress.marks
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 deps/plotter/adapters/gtk4/all
$(CMAKE_COMMAND) -E cmake_progress_start /home/martino/Projects/tools/udpscope/tools/CMakeFiles 0
.PHONY : all
# The main clean target
clean:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 deps/plotter/adapters/gtk4/clean
.PHONY : clean
# The main clean target
clean/fast: clean
.PHONY : clean/fast
# Prepare targets for installation.
preinstall: all
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 deps/plotter/adapters/gtk4/preinstall
.PHONY : preinstall
# Prepare targets for installation.
preinstall/fast:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 deps/plotter/adapters/gtk4/preinstall
.PHONY : preinstall/fast
# clear depends
depend:
cd /home/martino/Projects/tools/udpscope/tools && $(CMAKE_COMMAND) -P /home/martino/Projects/tools/udpscope/tools/CMakeFiles/VerifyGlobs.cmake
cd /home/martino/Projects/tools/udpscope/tools && $(CMAKE_COMMAND) -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR) --check-build-system CMakeFiles/Makefile.cmake 1
.PHONY : depend
# Convenience name for target.
deps/plotter/adapters/gtk4/CMakeFiles/plot_gtk4.dir/rule:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 deps/plotter/adapters/gtk4/CMakeFiles/plot_gtk4.dir/rule
.PHONY : deps/plotter/adapters/gtk4/CMakeFiles/plot_gtk4.dir/rule
# Convenience name for target.
plot_gtk4: deps/plotter/adapters/gtk4/CMakeFiles/plot_gtk4.dir/rule
.PHONY : plot_gtk4
# fast build rule for target.
plot_gtk4/fast:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/adapters/gtk4/CMakeFiles/plot_gtk4.dir/build.make deps/plotter/adapters/gtk4/CMakeFiles/plot_gtk4.dir/build
.PHONY : plot_gtk4/fast
gtk_plot_area.o: gtk_plot_area.cpp.o
.PHONY : gtk_plot_area.o
# target to build an object file
gtk_plot_area.cpp.o:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/adapters/gtk4/CMakeFiles/plot_gtk4.dir/build.make deps/plotter/adapters/gtk4/CMakeFiles/plot_gtk4.dir/gtk_plot_area.cpp.o
.PHONY : gtk_plot_area.cpp.o
gtk_plot_area.i: gtk_plot_area.cpp.i
.PHONY : gtk_plot_area.i
# target to preprocess a source file
gtk_plot_area.cpp.i:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/adapters/gtk4/CMakeFiles/plot_gtk4.dir/build.make deps/plotter/adapters/gtk4/CMakeFiles/plot_gtk4.dir/gtk_plot_area.cpp.i
.PHONY : gtk_plot_area.cpp.i
gtk_plot_area.s: gtk_plot_area.cpp.s
.PHONY : gtk_plot_area.s
# target to generate assembly for a file
gtk_plot_area.cpp.s:
cd /home/martino/Projects/tools/udpscope/tools && $(MAKE) $(MAKESILENT) -f deps/plotter/adapters/gtk4/CMakeFiles/plot_gtk4.dir/build.make deps/plotter/adapters/gtk4/CMakeFiles/plot_gtk4.dir/gtk_plot_area.cpp.s
.PHONY : gtk_plot_area.cpp.s
# Help Target
help:
@echo "The following are some of the valid targets for this Makefile:"
@echo "... all (the default if no target is provided)"
@echo "... clean"
@echo "... depend"
@echo "... edit_cache"
@echo "... rebuild_cache"
@echo "... plot_gtk4"
@echo "... gtk_plot_area.o"
@echo "... gtk_plot_area.i"
@echo "... gtk_plot_area.s"
.PHONY : help
#=============================================================================
# Special targets to cleanup operation of make.
# Special rule to run CMake to check the build system integrity.
# No rule that depends on this can have commands that come from listfiles
# because they might be regenerated.
cmake_check_build_system:
cd /home/martino/Projects/tools/udpscope/tools && $(CMAKE_COMMAND) -P /home/martino/Projects/tools/udpscope/tools/CMakeFiles/VerifyGlobs.cmake
cd /home/martino/Projects/tools/udpscope/tools && $(CMAKE_COMMAND) -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR) --check-build-system CMakeFiles/Makefile.cmake 0
.PHONY : cmake_check_build_system
@@ -0,0 +1,50 @@
# Install script for directory: /home/martino/Projects/tools/udpscope/deps/plotter/adapters/gtk4
# Set the install prefix
if(NOT DEFINED CMAKE_INSTALL_PREFIX)
set(CMAKE_INSTALL_PREFIX "/usr/local")
endif()
string(REGEX REPLACE "/$" "" CMAKE_INSTALL_PREFIX "${CMAKE_INSTALL_PREFIX}")
# Set the install configuration name.
if(NOT DEFINED CMAKE_INSTALL_CONFIG_NAME)
if(BUILD_TYPE)
string(REGEX REPLACE "^[^A-Za-z0-9_]+" ""
CMAKE_INSTALL_CONFIG_NAME "${BUILD_TYPE}")
else()
set(CMAKE_INSTALL_CONFIG_NAME "Release")
endif()
message(STATUS "Install configuration: \"${CMAKE_INSTALL_CONFIG_NAME}\"")
endif()
# Set the component getting installed.
if(NOT CMAKE_INSTALL_COMPONENT)
if(COMPONENT)
message(STATUS "Install component: \"${COMPONENT}\"")
set(CMAKE_INSTALL_COMPONENT "${COMPONENT}")
else()
set(CMAKE_INSTALL_COMPONENT)
endif()
endif()
# Install shared libraries without execute permission?
if(NOT DEFINED CMAKE_INSTALL_SO_NO_EXE)
set(CMAKE_INSTALL_SO_NO_EXE "0")
endif()
# Is this installation the result of a crosscompile?
if(NOT DEFINED CMAKE_CROSSCOMPILING)
set(CMAKE_CROSSCOMPILING "FALSE")
endif()
# Set path to fallback-tool for dependency-resolution.
if(NOT DEFINED CMAKE_OBJDUMP)
set(CMAKE_OBJDUMP "/usr/bin/objdump")
endif()
string(REPLACE ";" "\n" CMAKE_INSTALL_MANIFEST_CONTENT
"${CMAKE_INSTALL_MANIFEST_FILES}")
if(CMAKE_INSTALL_LOCAL_ONLY)
file(WRITE "/home/martino/Projects/tools/udpscope/tools/deps/plotter/adapters/gtk4/install_local_manifest.txt"
"${CMAKE_INSTALL_MANIFEST_CONTENT}")
endif()
+60
View File
@@ -0,0 +1,60 @@
# Install script for directory: /home/martino/Projects/tools/udpscope/deps/plotter
# Set the install prefix
if(NOT DEFINED CMAKE_INSTALL_PREFIX)
set(CMAKE_INSTALL_PREFIX "/usr/local")
endif()
string(REGEX REPLACE "/$" "" CMAKE_INSTALL_PREFIX "${CMAKE_INSTALL_PREFIX}")
# Set the install configuration name.
if(NOT DEFINED CMAKE_INSTALL_CONFIG_NAME)
if(BUILD_TYPE)
string(REGEX REPLACE "^[^A-Za-z0-9_]+" ""
CMAKE_INSTALL_CONFIG_NAME "${BUILD_TYPE}")
else()
set(CMAKE_INSTALL_CONFIG_NAME "Release")
endif()
message(STATUS "Install configuration: \"${CMAKE_INSTALL_CONFIG_NAME}\"")
endif()
# Set the component getting installed.
if(NOT CMAKE_INSTALL_COMPONENT)
if(COMPONENT)
message(STATUS "Install component: \"${COMPONENT}\"")
set(CMAKE_INSTALL_COMPONENT "${COMPONENT}")
else()
set(CMAKE_INSTALL_COMPONENT)
endif()
endif()
# Install shared libraries without execute permission?
if(NOT DEFINED CMAKE_INSTALL_SO_NO_EXE)
set(CMAKE_INSTALL_SO_NO_EXE "0")
endif()
# Is this installation the result of a crosscompile?
if(NOT DEFINED CMAKE_CROSSCOMPILING)
set(CMAKE_CROSSCOMPILING "FALSE")
endif()
# Set path to fallback-tool for dependency-resolution.
if(NOT DEFINED CMAKE_OBJDUMP)
set(CMAKE_OBJDUMP "/usr/bin/objdump")
endif()
if(NOT CMAKE_INSTALL_LOCAL_ONLY)
# Include the install script for the subdirectory.
include("/home/martino/Projects/tools/udpscope/tools/deps/plotter/src/text/cmake_install.cmake")
endif()
if(NOT CMAKE_INSTALL_LOCAL_ONLY)
# Include the install script for the subdirectory.
include("/home/martino/Projects/tools/udpscope/tools/deps/plotter/adapters/gtk4/cmake_install.cmake")
endif()
string(REPLACE ";" "\n" CMAKE_INSTALL_MANIFEST_CONTENT
"${CMAKE_INSTALL_MANIFEST_FILES}")
if(CMAKE_INSTALL_LOCAL_ONLY)
file(WRITE "/home/martino/Projects/tools/udpscope/tools/deps/plotter/install_local_manifest.txt"
"${CMAKE_INSTALL_MANIFEST_CONTENT}")
endif()
@@ -0,0 +1,28 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 Martino Ferrari
// Template for <plot/version.hpp>: configure_file() expands it into
// <build>/include/plot/version.hpp at configure time, and that generated file
// -- not this one -- is a public header. The umbrella names it, and the
// install rules place it under include/plot/ with the rest of the surface, so
// a consumer can ask what it compiled against: the macros for a preprocessor
// test, the plot::version constants for everything else. Both come from the
// project() version, so there is exactly one place a release edits.
#pragma once
#define PLOT_VERSION_MAJOR 0
#define PLOT_VERSION_MINOR 1
#define PLOT_VERSION_PATCH 0
#define PLOT_VERSION_STRING "0.1.0"
namespace plot::version {
/// The library version: one component per constant, and the whole thing as the
/// string the project() declares (e.g. "0.1.0").
inline constexpr int major = PLOT_VERSION_MAJOR;
inline constexpr int minor = PLOT_VERSION_MINOR;
inline constexpr int patch = PLOT_VERSION_PATCH;
inline constexpr const char* string = PLOT_VERSION_STRING;
} // namespace plot::version
@@ -0,0 +1,16 @@
# CMAKE generated file: DO NOT EDIT!
# Generated by "Unix Makefiles" Generator, CMake Version 4.4
# Relative path conversion top directories.
set(CMAKE_RELATIVE_PATH_TOP_SOURCE "/home/martino/Projects/tools/udpscope")
set(CMAKE_RELATIVE_PATH_TOP_BINARY "/home/martino/Projects/tools/udpscope/tools")
# Force unix paths in dependencies.
set(CMAKE_FORCE_UNIX_PATHS 1)
# The C and CXX include file regular expressions for this directory.
set(CMAKE_C_INCLUDE_REGEX_SCAN "^.*$")
set(CMAKE_C_INCLUDE_REGEX_COMPLAIN "^$")
set(CMAKE_CXX_INCLUDE_REGEX_SCAN ${CMAKE_C_INCLUDE_REGEX_SCAN})
set(CMAKE_CXX_INCLUDE_REGEX_COMPLAIN ${CMAKE_C_INCLUDE_REGEX_COMPLAIN})
@@ -0,0 +1,23 @@
# Consider dependencies only in project.
set(CMAKE_DEPENDS_IN_PROJECT_ONLY OFF)
# The set of languages for which implicit dependencies are needed:
set(CMAKE_DEPENDS_LANGUAGES
)
# The set of dependency files which are needed:
set(CMAKE_DEPENDS_DEPENDENCY_FILES
"/home/martino/Projects/tools/udpscope/deps/plotter/src/text/true_type_font.cpp" "deps/plotter/src/text/CMakeFiles/plot_font_ttf.dir/true_type_font.cpp.o" "gcc" "deps/plotter/src/text/CMakeFiles/plot_font_ttf.dir/true_type_font.cpp.o.d"
)
# Targets to which this target links which contain Fortran sources.
set(CMAKE_Fortran_TARGET_LINKED_INFO_FILES
)
# Targets to which this target links which contain Fortran sources.
set(CMAKE_Fortran_TARGET_FORWARD_LINKED_INFO_FILES
)
# Fortran module output directory.
set(CMAKE_Fortran_TARGET_MODULE_DIR "")
@@ -0,0 +1,116 @@
# CMAKE generated file: DO NOT EDIT!
# Generated by "Unix Makefiles" Generator, CMake Version 4.4
# Delete rule output on recipe failure.
.DELETE_ON_ERROR:
#=============================================================================
# Special targets provided by cmake.
# Disable implicit rules so canonical targets will work.
.SUFFIXES:
# Disable VCS-based implicit rules.
% : %,v
# Disable VCS-based implicit rules.
% : RCS/%
# Disable VCS-based implicit rules.
% : RCS/%,v
# Disable VCS-based implicit rules.
% : SCCS/s.%
# Disable VCS-based implicit rules.
% : s.%
.SUFFIXES: .hpux_make_needs_suffix_list
# Command-line flag to silence nested $(MAKE).
$(VERBOSE)MAKESILENT = -s
#Suppress display of executed commands.
$(VERBOSE).SILENT:
# A target that is always out of date.
cmake_force:
.PHONY : cmake_force
#=============================================================================
# Set environment variables for the build.
# The shell in which to execute make rules.
SHELL = /bin/sh
# The CMake executable.
CMAKE_COMMAND = /usr/bin/cmake
# The command to remove a file.
RM = /usr/bin/cmake -E rm -f
# Escaping for special characters.
EQUALS = =
# The top-level source directory on which CMake was run.
CMAKE_SOURCE_DIR = /home/martino/Projects/tools/udpscope
# The top-level build directory on which CMake was run.
CMAKE_BINARY_DIR = /home/martino/Projects/tools/udpscope/tools
# Include any dependencies generated for this target.
include deps/plotter/src/text/CMakeFiles/plot_font_ttf.dir/depend.make
# Include any dependencies generated by the compiler for this target.
include deps/plotter/src/text/CMakeFiles/plot_font_ttf.dir/compiler_depend.make
# Include the progress variables for this target.
include deps/plotter/src/text/CMakeFiles/plot_font_ttf.dir/progress.make
# Include the compile flags for this target's objects.
include deps/plotter/src/text/CMakeFiles/plot_font_ttf.dir/flags.make
deps/plotter/src/text/CMakeFiles/plot_font_ttf.dir/codegen:
.PHONY : deps/plotter/src/text/CMakeFiles/plot_font_ttf.dir/codegen
deps/plotter/src/text/CMakeFiles/plot_font_ttf.dir/true_type_font.cpp.o: deps/plotter/src/text/CMakeFiles/plot_font_ttf.dir/flags.make
deps/plotter/src/text/CMakeFiles/plot_font_ttf.dir/true_type_font.cpp.o: /home/martino/Projects/tools/udpscope/deps/plotter/src/text/true_type_font.cpp
deps/plotter/src/text/CMakeFiles/plot_font_ttf.dir/true_type_font.cpp.o: deps/plotter/src/text/CMakeFiles/plot_font_ttf.dir/compiler_depend.ts
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green --progress-dir=/home/martino/Projects/tools/udpscope/tools/CMakeFiles --progress-num=$(CMAKE_PROGRESS_1) "Building CXX object deps/plotter/src/text/CMakeFiles/plot_font_ttf.dir/true_type_font.cpp.o"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter/src/text && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -MD -MT deps/plotter/src/text/CMakeFiles/plot_font_ttf.dir/true_type_font.cpp.o -MF CMakeFiles/plot_font_ttf.dir/true_type_font.cpp.o.d -o CMakeFiles/plot_font_ttf.dir/true_type_font.cpp.o -c /home/martino/Projects/tools/udpscope/deps/plotter/src/text/true_type_font.cpp
deps/plotter/src/text/CMakeFiles/plot_font_ttf.dir/true_type_font.cpp.i: cmake_force
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green "Preprocessing CXX source to CMakeFiles/plot_font_ttf.dir/true_type_font.cpp.i"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter/src/text && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -E /home/martino/Projects/tools/udpscope/deps/plotter/src/text/true_type_font.cpp > CMakeFiles/plot_font_ttf.dir/true_type_font.cpp.i
deps/plotter/src/text/CMakeFiles/plot_font_ttf.dir/true_type_font.cpp.s: cmake_force
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green "Compiling CXX source to assembly CMakeFiles/plot_font_ttf.dir/true_type_font.cpp.s"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter/src/text && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -S /home/martino/Projects/tools/udpscope/deps/plotter/src/text/true_type_font.cpp -o CMakeFiles/plot_font_ttf.dir/true_type_font.cpp.s
# Object files for target plot_font_ttf
plot_font_ttf_OBJECTS = \
"CMakeFiles/plot_font_ttf.dir/true_type_font.cpp.o"
# External object files for target plot_font_ttf
plot_font_ttf_EXTERNAL_OBJECTS = \
"/home/martino/Projects/tools/udpscope/tools/deps/plotter/src/text/CMakeFiles/plot_stb_truetype.dir/stb_truetype_impl.cpp.o"
deps/plotter/src/text/libplot_font_ttf.a: deps/plotter/src/text/CMakeFiles/plot_font_ttf.dir/true_type_font.cpp.o
deps/plotter/src/text/libplot_font_ttf.a: deps/plotter/src/text/CMakeFiles/plot_stb_truetype.dir/stb_truetype_impl.cpp.o
deps/plotter/src/text/libplot_font_ttf.a: deps/plotter/src/text/CMakeFiles/plot_font_ttf.dir/build.make
deps/plotter/src/text/libplot_font_ttf.a: deps/plotter/src/text/CMakeFiles/plot_font_ttf.dir/link.txt
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green --bold --progress-dir=/home/martino/Projects/tools/udpscope/tools/CMakeFiles --progress-num=$(CMAKE_PROGRESS_2) "Linking CXX static library libplot_font_ttf.a"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter/src/text && $(CMAKE_COMMAND) -P CMakeFiles/plot_font_ttf.dir/cmake_clean_target.cmake
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter/src/text && $(CMAKE_COMMAND) -E cmake_link_script CMakeFiles/plot_font_ttf.dir/link.txt --verbose=$(VERBOSE)
# Rule to build all files generated by this target.
deps/plotter/src/text/CMakeFiles/plot_font_ttf.dir/build: deps/plotter/src/text/libplot_font_ttf.a
.PHONY : deps/plotter/src/text/CMakeFiles/plot_font_ttf.dir/build
deps/plotter/src/text/CMakeFiles/plot_font_ttf.dir/clean:
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter/src/text && $(CMAKE_COMMAND) -P CMakeFiles/plot_font_ttf.dir/cmake_clean.cmake
.PHONY : deps/plotter/src/text/CMakeFiles/plot_font_ttf.dir/clean
deps/plotter/src/text/CMakeFiles/plot_font_ttf.dir/depend:
cd /home/martino/Projects/tools/udpscope/tools && $(CMAKE_COMMAND) -E cmake_depends "Unix Makefiles" /home/martino/Projects/tools/udpscope /home/martino/Projects/tools/udpscope/deps/plotter/src/text /home/martino/Projects/tools/udpscope/tools /home/martino/Projects/tools/udpscope/tools/deps/plotter/src/text /home/martino/Projects/tools/udpscope/tools/deps/plotter/src/text/CMakeFiles/plot_font_ttf.dir/DependInfo.cmake "--color=$(COLOR)" plot_font_ttf
.PHONY : deps/plotter/src/text/CMakeFiles/plot_font_ttf.dir/depend
@@ -0,0 +1,11 @@
file(REMOVE_RECURSE
"CMakeFiles/plot_font_ttf.dir/true_type_font.cpp.o"
"CMakeFiles/plot_font_ttf.dir/true_type_font.cpp.o.d"
"libplot_font_ttf.a"
"libplot_font_ttf.pdb"
)
# Per-language clean rules from dependency scanning.
foreach(lang CXX)
include(CMakeFiles/plot_font_ttf.dir/cmake_clean_${lang}.cmake OPTIONAL)
endforeach()
@@ -0,0 +1,3 @@
file(REMOVE_RECURSE
"libplot_font_ttf.a"
)
@@ -0,0 +1,2 @@
# Empty compiler generated dependencies file for plot_font_ttf.
# This may be replaced when dependencies are built.
@@ -0,0 +1,2 @@
# CMAKE generated file: DO NOT EDIT!
# Timestamp file for compiler generated dependencies management for plot_font_ttf.
@@ -0,0 +1,2 @@
# Empty dependencies file for plot_font_ttf.
# This may be replaced when dependencies are built.
@@ -0,0 +1,10 @@
# CMAKE generated file: DO NOT EDIT!
# Generated by "Unix Makefiles" Generator, CMake Version 4.4
# compile CXX with /usr/bin/c++
CXX_DEFINES =
CXX_INCLUDES = -I/home/martino/Projects/tools/udpscope/include -I/home/martino/Projects/tools/udpscope/third_party -I/home/martino/Projects/tools/udpscope/deps/plotter/include -I/home/martino/Projects/tools/udpscope/tools/deps/plotter/include
CXX_FLAGS = -O3 -DNDEBUG -std=c++20 -Wall -Wextra -Wpedantic -Wconversion -Wshadow
@@ -0,0 +1,2 @@
/usr/bin/ar qc libplot_font_ttf.a CMakeFiles/plot_font_ttf.dir/true_type_font.cpp.o CMakeFiles/plot_stb_truetype.dir/stb_truetype_impl.cpp.o
/usr/bin/ranlib libplot_font_ttf.a
@@ -0,0 +1,3 @@
CMAKE_PROGRESS_1 = 28
CMAKE_PROGRESS_2 = 29
@@ -0,0 +1,23 @@
# Consider dependencies only in project.
set(CMAKE_DEPENDS_IN_PROJECT_ONLY OFF)
# The set of languages for which implicit dependencies are needed:
set(CMAKE_DEPENDS_LANGUAGES
)
# The set of dependency files which are needed:
set(CMAKE_DEPENDS_DEPENDENCY_FILES
"/home/martino/Projects/tools/udpscope/deps/plotter/src/text/stb_truetype_impl.cpp" "deps/plotter/src/text/CMakeFiles/plot_stb_truetype.dir/stb_truetype_impl.cpp.o" "gcc" "deps/plotter/src/text/CMakeFiles/plot_stb_truetype.dir/stb_truetype_impl.cpp.o.d"
)
# Targets to which this target links which contain Fortran sources.
set(CMAKE_Fortran_TARGET_LINKED_INFO_FILES
)
# Targets to which this target links which contain Fortran sources.
set(CMAKE_Fortran_TARGET_FORWARD_LINKED_INFO_FILES
)
# Fortran module output directory.
set(CMAKE_Fortran_TARGET_MODULE_DIR "")
@@ -0,0 +1,104 @@
# CMAKE generated file: DO NOT EDIT!
# Generated by "Unix Makefiles" Generator, CMake Version 4.4
# Delete rule output on recipe failure.
.DELETE_ON_ERROR:
#=============================================================================
# Special targets provided by cmake.
# Disable implicit rules so canonical targets will work.
.SUFFIXES:
# Disable VCS-based implicit rules.
% : %,v
# Disable VCS-based implicit rules.
% : RCS/%
# Disable VCS-based implicit rules.
% : RCS/%,v
# Disable VCS-based implicit rules.
% : SCCS/s.%
# Disable VCS-based implicit rules.
% : s.%
.SUFFIXES: .hpux_make_needs_suffix_list
# Command-line flag to silence nested $(MAKE).
$(VERBOSE)MAKESILENT = -s
#Suppress display of executed commands.
$(VERBOSE).SILENT:
# A target that is always out of date.
cmake_force:
.PHONY : cmake_force
#=============================================================================
# Set environment variables for the build.
# The shell in which to execute make rules.
SHELL = /bin/sh
# The CMake executable.
CMAKE_COMMAND = /usr/bin/cmake
# The command to remove a file.
RM = /usr/bin/cmake -E rm -f
# Escaping for special characters.
EQUALS = =
# The top-level source directory on which CMake was run.
CMAKE_SOURCE_DIR = /home/martino/Projects/tools/udpscope
# The top-level build directory on which CMake was run.
CMAKE_BINARY_DIR = /home/martino/Projects/tools/udpscope/tools
# Include any dependencies generated for this target.
include deps/plotter/src/text/CMakeFiles/plot_stb_truetype.dir/depend.make
# Include any dependencies generated by the compiler for this target.
include deps/plotter/src/text/CMakeFiles/plot_stb_truetype.dir/compiler_depend.make
# Include the progress variables for this target.
include deps/plotter/src/text/CMakeFiles/plot_stb_truetype.dir/progress.make
# Include the compile flags for this target's objects.
include deps/plotter/src/text/CMakeFiles/plot_stb_truetype.dir/flags.make
deps/plotter/src/text/CMakeFiles/plot_stb_truetype.dir/codegen:
.PHONY : deps/plotter/src/text/CMakeFiles/plot_stb_truetype.dir/codegen
deps/plotter/src/text/CMakeFiles/plot_stb_truetype.dir/stb_truetype_impl.cpp.o: deps/plotter/src/text/CMakeFiles/plot_stb_truetype.dir/flags.make
deps/plotter/src/text/CMakeFiles/plot_stb_truetype.dir/stb_truetype_impl.cpp.o: /home/martino/Projects/tools/udpscope/deps/plotter/src/text/stb_truetype_impl.cpp
deps/plotter/src/text/CMakeFiles/plot_stb_truetype.dir/stb_truetype_impl.cpp.o: deps/plotter/src/text/CMakeFiles/plot_stb_truetype.dir/compiler_depend.ts
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green --progress-dir=/home/martino/Projects/tools/udpscope/tools/CMakeFiles --progress-num=$(CMAKE_PROGRESS_1) "Building CXX object deps/plotter/src/text/CMakeFiles/plot_stb_truetype.dir/stb_truetype_impl.cpp.o"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter/src/text && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -MD -MT deps/plotter/src/text/CMakeFiles/plot_stb_truetype.dir/stb_truetype_impl.cpp.o -MF CMakeFiles/plot_stb_truetype.dir/stb_truetype_impl.cpp.o.d -o CMakeFiles/plot_stb_truetype.dir/stb_truetype_impl.cpp.o -c /home/martino/Projects/tools/udpscope/deps/plotter/src/text/stb_truetype_impl.cpp
deps/plotter/src/text/CMakeFiles/plot_stb_truetype.dir/stb_truetype_impl.cpp.i: cmake_force
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green "Preprocessing CXX source to CMakeFiles/plot_stb_truetype.dir/stb_truetype_impl.cpp.i"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter/src/text && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -E /home/martino/Projects/tools/udpscope/deps/plotter/src/text/stb_truetype_impl.cpp > CMakeFiles/plot_stb_truetype.dir/stb_truetype_impl.cpp.i
deps/plotter/src/text/CMakeFiles/plot_stb_truetype.dir/stb_truetype_impl.cpp.s: cmake_force
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --green "Compiling CXX source to assembly CMakeFiles/plot_stb_truetype.dir/stb_truetype_impl.cpp.s"
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter/src/text && /usr/bin/c++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -S /home/martino/Projects/tools/udpscope/deps/plotter/src/text/stb_truetype_impl.cpp -o CMakeFiles/plot_stb_truetype.dir/stb_truetype_impl.cpp.s
plot_stb_truetype: deps/plotter/src/text/CMakeFiles/plot_stb_truetype.dir/stb_truetype_impl.cpp.o
plot_stb_truetype: deps/plotter/src/text/CMakeFiles/plot_stb_truetype.dir/build.make
.PHONY : plot_stb_truetype
# Rule to build all files generated by this target.
deps/plotter/src/text/CMakeFiles/plot_stb_truetype.dir/build: plot_stb_truetype
.PHONY : deps/plotter/src/text/CMakeFiles/plot_stb_truetype.dir/build
deps/plotter/src/text/CMakeFiles/plot_stb_truetype.dir/clean:
cd /home/martino/Projects/tools/udpscope/tools/deps/plotter/src/text && $(CMAKE_COMMAND) -P CMakeFiles/plot_stb_truetype.dir/cmake_clean.cmake
.PHONY : deps/plotter/src/text/CMakeFiles/plot_stb_truetype.dir/clean
deps/plotter/src/text/CMakeFiles/plot_stb_truetype.dir/depend:
cd /home/martino/Projects/tools/udpscope/tools && $(CMAKE_COMMAND) -E cmake_depends "Unix Makefiles" /home/martino/Projects/tools/udpscope /home/martino/Projects/tools/udpscope/deps/plotter/src/text /home/martino/Projects/tools/udpscope/tools /home/martino/Projects/tools/udpscope/tools/deps/plotter/src/text /home/martino/Projects/tools/udpscope/tools/deps/plotter/src/text/CMakeFiles/plot_stb_truetype.dir/DependInfo.cmake "--color=$(COLOR)" plot_stb_truetype
.PHONY : deps/plotter/src/text/CMakeFiles/plot_stb_truetype.dir/depend
Loaded 100 of 110 files, more files were not shown because too many files have changed in this diff. Show more