// SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: 2026 Martino Ferrari #pragma once #include #include #include #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/_.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 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 rows; struct Calibration { std::string name; double gain = 1.0; double offset = 0.0; }; std::vector 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 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 enabled; /// How this source was reached, so the startup chooser can reconnect to it exactly. The file is /// named `_.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_` 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 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; /// Which sidebar tab was showing: "measure" or "signals". The one thing the panel persists -- /// no revealer states remain, and the 320 px width is the `.ui`'s. std::string side_tab = "signals"; /// The measurements pane, by signal *name* and with the type and options a row was built with. std::vector 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_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. 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. double buffer_mb = 0.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 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/_.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/_/`. Not created by this call. std::string config_dir_for(const std::string& host, std::uint16_t port); /// One configuration's file, `/.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 configuration_names(const std::string& host, std::uint16_t port); /// The configuration the source was last left in: `/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