// SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: 2026 Martino Ferrari #pragma once #include "model.hpp" #include "plot_host.hpp" #include #include namespace udpscope { /// What the plots are showing, as a table: one row per sample of the visible x window, /// one column per plotted signal. /// /// The samples are the *stored* ones, never the LOD pyramid's buckets: the pyramid is /// what makes a million samples drawable in a few hundred pixels, and exporting it would /// hand back the envelope the renderer drew rather than the acquisition. So an export is /// at the full rate the ring holds, however far the view is zoomed out. struct ExportTable { struct Column { std::string name; ///< the legend name, sanitised for a column label /// The signal's unit exactly as the CONFIG carried it, the empty string when it /// carried none. The `.npy`/`.csv` writers do not spell it out (their readers get the /// names and the numbers); MDF does, because the unit *is* part of what a channel is /// there, and it is the only thing ScryLab reads a signal's unit from. std::string unit; std::vector values; ///< `rows()` long, the ring's own `f32` }; /// The x the plot shows and the crosshair reads, which is **relative** in every mode: seconds /// before now while streaming (so the newest sample is 0 and the window is `[-window, 0]`), /// seconds from the edge in `[-pre, +post]` while an event is held, and seconds from the /// instant of the freeze on a paused view. std::vector time; /// The stream's own absolute clock -- the number the time signal carried, which is what the /// source stamped the samples with -- so two exports taken in different modes, or from /// different runs, line up on one timeline. /// /// `f64 seconds` rather than the wire's integer, because the integer does not exist in this /// process: `udps_frame_t::values` is a `const double*`, so the client has already widened /// every value by the time acquisition sees it. At Unix-epoch magnitudes an `f64` resolves /// about 240 ns, which is coarser than a 1 MS/s sample period -- so this column is the /// *provenance* of a row and `time` is the column to difference. Its own `np.diff` is /// legitimately not single-valued for that reason. std::vector abs_seconds; std::vector columns; std::size_t rows() const noexcept { return time.size(); } bool empty() const noexcept { return columns.empty() || time.empty(); } }; /// Collects every signal bound to a visible row, over the shared x window, at the stored /// rate. A signal on several rows is one column; the columns come out in channel order, /// which is the signal list's order. /// /// Takes `model.mutex()` for the live cut, so call it from the GTK thread like any other /// 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 /// ` `write_csv`, /// `.mf3`/`.mdf` -> `write_mdf`, anything else -> `write_npy` (the default, as the save dialog's /// default filter is). The export dialog and the self-check's export probe both come through /// here, so the two cannot offer different formats. bool write_export(const std::string& path, const ExportTable& table, std::string& error); } // namespace udpscope