// SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: 2026 Martino Ferrari #pragma once #include "model.hpp" #include #include #include 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& 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 names_; /// Bytes per *field*, in file order: the two time columns then the signals. Kept so a column an /// external tool widened to ` 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 /// `/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 recent_waveforms(); void remember_waveform(const std::string& path); } // namespace udpscope