Files
udpscope/src/waveform.hpp
T

85 lines
3.5 KiB
C++

// 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