--buffer-samples (k/M/G) replaces --buffer-mb: the request is rounded down to a power of two for every signal and the shared time ring. A separate memory limit (--memory-limit-mb, a Preferences row, default 8 GiB) halves the plan until the real ring bytes fit and says so on stderr and in the status line. The per-source key buffer_mb becomes buffer_samples; an old buffer_mb is ignored. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
242 lines
13 KiB
C++
242 lines
13 KiB
C++
// SPDX-License-Identifier: MIT
|
|
// SPDX-FileCopyrightText: 2026 Martino Ferrari
|
|
#pragma once
|
|
|
|
#include <cstdint>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "math_channel.hpp"
|
|
#include "measure.hpp"
|
|
#include "source/source_config.hpp"
|
|
|
|
namespace udpscope {
|
|
|
|
/// The user-arranged part of a session, persisted per data source in
|
|
/// `~/.config/udpscope/<host>_<port>.toml`: the display window, the plot layout
|
|
/// (rows and which signal each of a row's slots holds) and the per-signal
|
|
/// calibration. Signal names are the stable identity (channel indexes are
|
|
/// renumbered by every enable/disable, see the model's own invariant).
|
|
struct LayoutState {
|
|
struct Row {
|
|
/// One entry per slot; an empty string is an empty slot.
|
|
std::vector<std::string> signals;
|
|
/// The row's share of the figure's height, 1 for equal rows. Part of the layout because a sizing a
|
|
/// user dragged is an arrangement, and an arrangement that is not saved is one that is lost.
|
|
double weight = 1.0;
|
|
};
|
|
|
|
double window_s = 0.0; ///< 0 keeps the default window
|
|
std::vector<Row> rows;
|
|
|
|
struct Calibration {
|
|
std::string name;
|
|
double gain = 1.0;
|
|
double offset = 0.0;
|
|
};
|
|
std::vector<Calibration> calibration;
|
|
|
|
/// The trace colours the user chose, `0xRRGGBB`. Only the *overrides*: a signal with no entry
|
|
/// takes the positional palette entry, which is what keeps a stream's new signals coloured
|
|
/// without a session having to name them. Saved by name like every identity here.
|
|
struct Color {
|
|
std::string name;
|
|
std::uint32_t rgb = 0;
|
|
};
|
|
std::vector<Color> colors;
|
|
|
|
/// Signals the session had enabled. Enabling is what starts acquisition, so this is what makes a
|
|
/// reopened scope look like the one that was closed -- and it is saved by *name*, like every other
|
|
/// identity here, because a CONFIG renumbers the channels.
|
|
std::vector<std::string> enabled;
|
|
|
|
/// How this source was reached, so the startup chooser can reconnect to it exactly. The file is
|
|
/// named `<host>_<port>.toml` and that name can say neither of these. Empty `multicast` is
|
|
/// unicast; empty `interface_addr` is the default route.
|
|
std::string multicast;
|
|
std::string interface_addr;
|
|
/// Which source kind this session belongs to. Empty means `udps`: every file written before the
|
|
/// plugins existed. Only a kind other than `udps` is written, so a UDPS session file is
|
|
/// byte-identical to what earlier versions wrote.
|
|
std::string source_kind;
|
|
/// That kind's settings (`SourceConfig`), stored as top-level `source_opt_<key>` entries.
|
|
SourceConfig source_opts;
|
|
|
|
/// The derived channels, graphs and all: the editor's work is part of the session, not of the
|
|
/// process. A graph is stored as one string per node so the file stays in this store's TOML
|
|
/// subset (tables, string arrays, scalars) rather than growing a nested-table parser for it; see
|
|
/// `math_node_text()` for the field order.
|
|
std::vector<MathChannel> math;
|
|
|
|
/// Whether the measurements pane shows its statistics line. The only piece of the
|
|
/// statistics that is persisted: the accumulated figures themselves describe a session's
|
|
/// stream and mean nothing restored against another one.
|
|
bool measure_stats = false;
|
|
|
|
/// The dock's shown state (the user's last word on the toolbar's Dock button). The one thing the dock
|
|
/// persists -- its 320 px width is the `.ui`'s. An older file's `side_tab` key is not read: the parser
|
|
/// ignores keys it does not know.
|
|
bool dock_shown = true;
|
|
|
|
/// The measurements pane, by signal *name* and with the type and options a row was built with.
|
|
std::vector<Measure> measures;
|
|
|
|
/// The trigger: its settings *and* whether trigger mode was on. Both, and the settings even while
|
|
/// it is off, because a trigger set up once and switched off is still the trigger you come back to
|
|
/// -- and the pane's locked view is built from exactly this.
|
|
///
|
|
/// The signal and the two enumerations are stored as *text*: the signal by name, because every
|
|
/// rebuild renumbers the channels (the model's own rule), and the shapes by the words the panel's
|
|
/// dropdowns show, because an index would tie this file to a declaration order -- which the math
|
|
/// channel's op index already taught this store once.
|
|
struct Trigger {
|
|
bool enabled = false;
|
|
std::string channel; ///< the acquired channel's name; empty is "none chosen"
|
|
std::string type = "edge"; ///< edge | pulse width | window | dropout
|
|
std::string edge = "rising"; ///< rising | falling | both
|
|
std::string cmp = "greater"; ///< pulse width: `<` | `>` | within | outside
|
|
std::string mode = "normal"; ///< normal | single
|
|
double level = 0.0;
|
|
double level2 = 0.0; ///< window: the band's other edge
|
|
double width_s = 1.0e-3;
|
|
double width2_s = 2.0e-3;
|
|
double timeout_s = 1.0e-3;
|
|
double pretrigger_pct = 50.0;
|
|
double holdoff_s = 0.010;
|
|
double noise_band = 0.0;
|
|
double hf_fc = 0.0;
|
|
/// How many of the most recent acquisitions the trigger mode keeps on screen (1 = the event
|
|
/// alone). Part of the trigger's own setup, like the window it captures and the shape it waits
|
|
/// for: it is what the display does with the acquisitions, and there is no second switch.
|
|
///
|
|
/// This is the **multiple event** display -- N retained acquisitions, drawn as N traces. The
|
|
/// phosphor density beside it is a different display with a different cost (one bounded image a
|
|
/// trace against N window copies), which is why it has its own four fields below and why this
|
|
/// one keeps the key it has always had: an older file's `trigger_persistence` is still the
|
|
/// multiple-event depth.
|
|
int persistence = 1;
|
|
/// The **phosphor density** (`TriggerConfig::density` and the three beside it): whether the
|
|
/// trigger-mode display accumulates every acquisition into one image, what it fades by, and the
|
|
/// constant or factor that fade uses. Persisted because it is a display the user set up and
|
|
/// comes back to -- the trigger settings around it already are.
|
|
bool density = false;
|
|
/// "acquisitions" | "time": the same two words the panel's dropdown shows, for the reason the
|
|
/// shapes above are stored as text -- an index would tie the file to a declaration order.
|
|
std::string density_decay = "acquisitions";
|
|
double density_tau_s = 1.0;
|
|
double density_factor = 0.5;
|
|
/// Whether the file said anything about the level. A session that did keeps it; one that did not
|
|
/// leaves the panel's own "follow the channel's range" default alone.
|
|
bool level_given = false;
|
|
};
|
|
Trigger trigger;
|
|
|
|
/// 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_samples` and an empty `capture_packets` mean "not stored":
|
|
/// the command line's value stands, so an old file cannot silently shrink a buffer. The budget is
|
|
/// samples per acquired signal (the memory limit is the application's, not a source's). A file from
|
|
/// before that unit carries `buffer_mb`, which is not read: it reads as "not stored" and the
|
|
/// default stands, rather than a megabyte count being taken for a sample count.
|
|
std::uint64_t buffer_samples = 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.
|
|
struct KnownSource {
|
|
std::string host; ///< the CONFIG server, even for a multicast session
|
|
std::uint16_t port = 0;
|
|
std::string multicast; ///< empty = unicast
|
|
std::string interface_addr; ///< empty = the default route
|
|
std::string kind = "udps"; ///< the session's source kind
|
|
SourceConfig opts; ///< that kind's settings (empty for UDPS, whose fields are above)
|
|
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.
|
|
std::uint64_t buffer_samples = 0;
|
|
bool relative_time = false;
|
|
std::uint32_t capture_packets = 0;
|
|
};
|
|
|
|
/// 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 — 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
|
|
/// could not be removed, which costs the caller nothing -- there is no state to roll back.
|
|
bool forget_source(const KnownSource& src);
|
|
|
|
/// One `MathNode` as `kind|source|in_a|in_b|gain|offset|fc|q|lo|hi|x|y`, and back. The kind is the
|
|
/// op's *index* into `math_op_at()`, which is why the enum's order is a stored format and not just
|
|
/// an implementation detail: reordering it renames every saved graph's operations.
|
|
std::string math_node_text(const MathNode& node);
|
|
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();
|
|
|
|
/// Creates `path` and every missing component, like `mkdir -p`. Public because the preferences are
|
|
/// written under the config directory too.
|
|
void mkdirs(const std::string& path);
|
|
|
|
/// 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).
|
|
bool load_layout(const std::string& path, LayoutState& out);
|
|
|
|
/// Writes `state` as TOML. Atomic in effect: writes a sibling temp file and
|
|
/// renames it over the target.
|
|
bool save_layout(const std::string& path, const LayoutState& state);
|
|
|
|
} // namespace udpscope
|