working on todo.md features
This commit is contained in:
1 parent
210d124814
commit
8f54841c2c
110 files changed
+15932
-6576
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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.**
|
||||
|
||||
Vendored
+18
-1
@@ -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);
|
||||
}
|
||||
|
||||
Vendored
+20
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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 |
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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";
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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/<source>/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
@@ -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><host>_<port>.toml</tt>.
|
||||
unset. Each source has a directory <tt><host>_<port>/</tt> holding one
|
||||
<tt><name>.toml</tt> per configuration and an <tt>active</tt> file naming the one it is on.
|
||||
|
||||
<b>Configurations</b>
|
||||
|
||||
<b>Configurations…</b> in the menu lists the running source's configurations: click a row to open
|
||||
it, <tt>×</tt> to delete it, or type a name and a note and press <i>Save this session</i> to keep
|
||||
the current arrangement under a new one. Opening a configuration saves the current session first,
|
||||
so switching never loses what it switches from.
|
||||
|
||||
The startup chooser (start with no <tt>--server</tt>) lists one row per configuration, with its
|
||||
note and its settings, so two arrangements of one source are told apart before they open.
|
||||
|
||||
A configuration also carries the settings that can only be decided when the buffers are
|
||||
allocated: the <b>buffer size</b> (<tt>--buffer-mb</tt>), relative time, and the packet capture's
|
||||
ring size. Opening a configuration from the chooser applies all three; switching inside a running
|
||||
app applies the rest and says in the status line that these take effect on the next start.
|
||||
|
||||
The chooser's form sets the first two for a new host, and its pencil button edits them on an
|
||||
existing configuration — along with the name and the note. Renaming a configuration, or editing
|
||||
the address it points at, moves it: the old file is removed rather than left behind.
|
||||
|
||||
<b>Worth knowing</b>
|
||||
|
||||
• What a 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><host>_<port>.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 |
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
File diff suppressed because it is too large.
Load diff
+1302
File diff suppressed because it is too large.
Load diff
@@ -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
|
||||
@@ -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
|
||||
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Executable
+135
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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
Reference in new issue
Block a user