source: DataSource, Frame, SourceConfig and the registry (unused yet)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino FerrariandClaude Opus 4.6 committed 2026-10-31 19:56:00 +01:00
1 parent 71b904a93b
commit 3896f404a9
11 files changed
+445 -3

No files matched your search

+3
View File
@@ -236,6 +236,9 @@ add_executable(udpscope
src/waveform.cpp
src/event_store.cpp
src/acquisition.cpp
src/source/source_config.cpp
src/source/data_source.cpp
src/source/registry.cpp
src/plot_host.cpp
src/export.cpp
src/search.cpp
+1 -3
View File
@@ -3,8 +3,6 @@
#include "model.hpp"
#include <udps_client.h>
#include <algorithm>
#include <chrono>
#include <cmath>
@@ -646,7 +644,7 @@ bool ScopeModel::is_time_signal(const std::vector<SignalInfo>& signals,
// signal: it has stamps but no cadence of its own, because it is not data.
// Data channels can also be declared full-array (per-element stamps), and they
// keep a declared rate -- treating them as time signals hid every real channel.
if (s.time_mode == UDPS_TIME_FULL_ARRAY && s.elements() > 1 && s.sampling_rate == 0.0) {
if (s.time_mode == kTimeFullArray && s.elements() > 1 && s.sampling_rate == 0.0) {
return true;
}
return false;
+7
View File
@@ -24,6 +24,13 @@
namespace udpscope {
/// `SignalInfo::time_mode` values. They equal the wire protocol's `UDPS_TIME_*`
/// (`udps_source.cpp` static_asserts it), but the model must not name a source's own header.
constexpr std::uint8_t kTimePacket = 0;
constexpr std::uint8_t kTimeFullArray = 1;
constexpr std::uint8_t kTimeFirstSample = 2;
constexpr std::uint8_t kTimeLastSample = 3;
/// One streamed signal, as the CONFIG packet described it.
struct SignalInfo {
std::string name;
+24
View File
@@ -0,0 +1,24 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 Martino Ferrari
#include "source/data_source.hpp"
#include <utility>
namespace udpscope {
void DataSource::post(Command cmd) {
std::lock_guard<std::mutex> lk(cmd_mu_);
cmds_.push_back(std::move(cmd));
}
void DataSource::drain_commands() {
std::deque<Command> local;
{
std::lock_guard<std::mutex> lk(cmd_mu_);
local.swap(cmds_);
}
for (auto& c : local) c();
}
} // namespace udpscope
+78
View File
@@ -0,0 +1,78 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 Martino Ferrari
#pragma once
#include "source/frame.hpp"
#include <cstddef>
#include <cstdint>
#include <deque>
#include <functional>
#include <mutex>
#include <vector>
namespace udpscope {
/// Work for the producer thread. UI code posts these; `poll()` drains and runs them, so a
/// source's state is only ever touched by the thread that owns it.
using Command = std::function<void()>;
/// The counters `StreamStats` shows, in source-neutral form. Producer thread only.
struct SourceStats {
std::uint64_t packets = 0;
std::uint64_t frames = 0;
std::uint64_t gaps = 0;
std::uint64_t late = 0;
std::uint64_t resequenced = 0;
std::uint64_t stale = 0;
std::uint64_t overflows = 0;
std::uint64_t reconnects = 0;
bool connected = false;
};
enum class SourceEvent { Connected, Disconnected, Error };
/// Where a source delivers, from inside `poll()`.
class SourceSink {
public:
virtual ~SourceSink() = default;
/// The signal set (re)defined. `publish_mode` is the stream's own publish mode.
virtual void on_config(const std::vector<SignalInfo>& signals, std::uint8_t publish_mode) = 0;
virtual void on_data(const Frame& frame) = 0;
virtual void on_event(SourceEvent event, const char* message) = 0;
};
/// Receives each raw datagram of a UDP-based source: bytes, length, receive time.
using DatagramTap = std::function<void(const std::uint8_t*, std::size_t, double)>;
/// The per-connection object. `Acquisition` owns one and drives it from the producer thread.
class DataSource {
public:
virtual ~DataSource() = default;
/// Allocates and connects. Throws `std::runtime_error` with a message fit for the user.
/// Does not deliver: CONFIG and `Connected` arrive from `poll()`.
virtual void open(SourceSink& sink) = 0;
/// Runs queued commands, then waits up to `timeout_ms` for work and delivers it.
virtual void poll(int timeout_ms) = 0;
virtual void close() = 0;
/// Producer thread only.
virtual SourceStats stats() const = 0;
virtual bool connected() const = 0;
/// Optional: UDP-based sources feed the raw-packet capture through this.
virtual void set_datagram_tap(DatagramTap) {}
/// Thread-safe. The command runs inside a later `poll()`.
void post(Command cmd);
protected:
/// For `poll()` implementations: runs and clears the queue.
void drain_commands();
private:
std::mutex cmd_mu_;
std::deque<Command> cmds_;
};
} // namespace udpscope
+45
View File
@@ -0,0 +1,45 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 Martino Ferrari
#pragma once
#include "model.hpp"
#include <cstdint>
namespace udpscope {
/// One signal's values in a frame. `values` points into the source's own buffer and is valid
/// only while the `on_data` call that received the frame is running.
struct SignalValues {
const double* values = nullptr;
std::uint32_t count = 0;
};
/// A data frame in source-neutral form; it mirrors `udps_frame_t`. A source with no packet
/// counter supplies a running one and `lost = 0`: the hole, backfill and time-base logic only
/// needs `counter`, `lost` and `late`.
struct Frame {
std::uint32_t counter = 0;
std::uint32_t lost = 0;
bool late = false;
double recv_time = 0.0;
std::uint8_t publish_mode = 0;
std::uint32_t num_samples = 0;
std::uint32_t num_signals = 0;
const SignalInfo* signals = nullptr;
const SignalValues* values = nullptr;
/// Element `elem` of signal `sig` at sample `sample`. Mirrors `udps_frame_value`: a scalar
/// signal carried once per sample is indexed by `sample`, a vector/matrix signal by `elem`.
/// Out of range reads 0.0.
double value(std::uint32_t sig, std::uint32_t sample, std::uint32_t elem) const noexcept {
if (sig >= num_signals) return 0.0;
const SignalValues& v = values[sig];
const std::uint32_t idx =
(v.count == num_samples && signals[sig].elements() == 1u) ? sample : elem;
return idx < v.count ? v.values[idx] : 0.0;
}
};
} // namespace udpscope
+70
View File
@@ -0,0 +1,70 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 Martino Ferrari
#include "source/registry.hpp"
#include <algorithm>
#include <cstdlib>
namespace udpscope {
namespace {
std::vector<std::unique_ptr<SourceKind>>& owned() {
static std::vector<std::unique_ptr<SourceKind>> v;
return v;
}
std::vector<SourceKind*>& raw() {
static std::vector<SourceKind*> v;
return v;
}
std::string sanitize(const std::string& s) {
std::string out = s;
for (char& c : out) {
const bool ok = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') ||
c == '.' || c == '_' || c == '-';
if (!ok) c = '_';
}
return out;
}
} // namespace
void register_kind(std::unique_ptr<SourceKind> kind) {
if (!kind || find_kind(kind->id())) return;
raw().push_back(kind.get());
owned().push_back(std::move(kind));
}
SourceKind* find_kind(const std::string& id) {
for (SourceKind* k : raw()) {
if (k->id() == id) return k;
}
return nullptr;
}
const std::vector<SourceKind*>& kinds() { return raw(); }
void register_builtin_sources() {
static bool done = false;
if (done) return;
done = true;
// Task 2: register_kind(make_udps_kind());
// Task 6: register make_probe_kind() when UDPSCOPE_PROBE_SOURCE_KINDS is set
}
std::string session_host(const std::string& kind_id, const SourceConfig& cfg) {
if (kind_id == "udps") return cfg.get("server", "127.0.0.1");
SourceKind* k = find_kind(kind_id);
return sanitize(kind_id + "-" + (k ? k->identity(cfg) : std::string("unknown")));
}
std::uint16_t session_port(const std::string& kind_id, const SourceConfig& cfg) {
if (kind_id == "udps") {
const long p = cfg.get_int("port", 8000);
return static_cast<std::uint16_t>(p > 0 && p <= 65535 ? p : 8000);
}
return 1;
}
} // namespace udpscope
+34
View File
@@ -0,0 +1,34 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 Martino Ferrari
#pragma once
#include "source/source_kind.hpp"
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
namespace udpscope {
/// Takes ownership. A second kind with the same id is ignored.
void register_kind(std::unique_ptr<SourceKind> kind);
SourceKind* find_kind(const std::string& id);
/// In registration order (the chooser's dropdown order).
const std::vector<SourceKind*>& kinds();
/// Registers the built-in kinds; idempotent. No static-init self-registration, which a linker
/// may drop. A later `dlopen` loader would only call `register_kind()`.
void register_builtin_sources();
/// The built-in kinds, each defined in its own file (`udps_kind.cpp`, `probe_source.cpp`).
std::unique_ptr<SourceKind> make_udps_kind();
std::unique_ptr<SourceKind> make_probe_kind();
/// The (host, port) pair that names a source's session file. UDPS keeps its legacy
/// `server`/`port`; any other kind is `"<kind>-<identity>"` (characters outside `[A-Za-z0-9._-]`
/// become `_`) on port 1 -- not 0, which the session-file parser rejects and waveform mode uses.
std::string session_host(const std::string& kind_id, const SourceConfig& cfg);
std::uint16_t session_port(const std::string& kind_id, const SourceConfig& cfg);
} // namespace udpscope
+75
View File
@@ -0,0 +1,75 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 Martino Ferrari
#include "source/source_config.hpp"
#include <algorithm>
#include <cctype>
#include <cstdlib>
namespace udpscope {
void SourceConfig::set(const std::string& key, const std::string& value) {
for (auto& kv : items_) {
if (kv.first == key) {
kv.second = value;
return;
}
}
items_.emplace_back(key, value);
}
bool SourceConfig::has(const std::string& key) const {
return std::any_of(items_.begin(), items_.end(),
[&](const auto& kv) { return kv.first == key; });
}
std::string SourceConfig::get(const std::string& key, const std::string& def) const {
for (const auto& kv : items_) {
if (kv.first == key) return kv.second;
}
return def;
}
long SourceConfig::get_int(const std::string& key, long def) const {
if (!has(key)) return def;
const std::string v = get(key);
char* end = nullptr;
const long n = std::strtol(v.c_str(), &end, 10);
return (end == v.c_str() || *end != '\0') ? def : n;
}
double SourceConfig::get_double(const std::string& key, double def) const {
if (!has(key)) return def;
const std::string v = get(key);
char* end = nullptr;
const double d = std::strtod(v.c_str(), &end);
return (end == v.c_str() || *end != '\0') ? def : d;
}
bool SourceConfig::get_bool(const std::string& key, bool def) const {
if (!has(key)) return def;
std::string v = get(key);
std::transform(v.begin(), v.end(), v.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
if (v == "1" || v == "true" || v == "yes" || v == "on") return true;
if (v == "0" || v == "false" || v == "no" || v == "off") return false;
return def;
}
bool SourceConfig::valid_key(const std::string& key) {
return !key.empty() && std::all_of(key.begin(), key.end(), [](unsigned char c) {
return std::isalnum(c) || c == '_';
});
}
bool SourceConfig::parse_assignment(const std::string& kv) {
const auto eq = kv.find('=');
if (eq == std::string::npos) return false;
const std::string key = kv.substr(0, eq);
if (!valid_key(key)) return false;
set(key, kv.substr(eq + 1));
return true;
}
} // namespace udpscope
+44
View File
@@ -0,0 +1,44 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 Martino Ferrari
#pragma once
#include <string>
#include <utility>
#include <vector>
namespace udpscope {
/// A source's settings: an ordered string key/value map. It is the stored form (top-level
/// `source_opt_<key>` entries of a session), the `--source-opt k=v` form, and what a config form
/// reads and writes. Order is insertion order, so a stored session stays diff-friendly.
class SourceConfig {
public:
using Items = std::vector<std::pair<std::string, std::string>>;
SourceConfig() = default;
explicit SourceConfig(Items items) : items_(std::move(items)) {}
/// Replaces the value in place if the key exists, else appends.
void set(const std::string& key, const std::string& value);
bool has(const std::string& key) const;
std::string get(const std::string& key, const std::string& def = {}) const;
long get_int(const std::string& key, long def) const;
double get_double(const std::string& key, double def) const;
/// 1/true/yes/on and 0/false/no/off (case-insensitive); anything else gives `def`.
bool get_bool(const std::string& key, bool def) const;
/// Parses `k=v`; false (and no change) if there is no `=` or the key is not `valid_key`.
bool parse_assignment(const std::string& kv);
const Items& items() const noexcept { return items_; }
bool empty() const noexcept { return items_.empty(); }
/// `[A-Za-z0-9_]+` -- the characters that survive as a TOML bare key suffix.
static bool valid_key(const std::string& key);
private:
Items items_;
};
} // namespace udpscope
+64
View File
@@ -0,0 +1,64 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 Martino Ferrari
#pragma once
#include "source/data_source.hpp"
#include "source/source_config.hpp"
#include <gtk/gtk.h>
#include <functional>
#include <memory>
#include <string>
#include <vector>
namespace udpscope {
/// A kind's configuration UI, as pieces the chooser places in its own group. The widgets are
/// floating `AdwPreferencesRow`s; the closures capture them, so a form must not outlive its rows
/// (the chooser drops the form when it removes the rows).
struct ConfigForm {
std::vector<GtkWidget*> rows;
/// Shown under the group's title; empty for none.
std::string description;
/// The widget that takes focus when the form is shown; may be null.
GtkWidget* focus = nullptr;
/// Puts `cfg` into the widgets (absent keys leave the kind's defaults).
std::function<void(const SourceConfig&)> write;
/// The widgets as a `SourceConfig`.
std::function<SourceConfig()> read;
/// Whether the form is complete enough to connect.
std::function<bool()> valid;
/// Calls `fn` whenever any widget changes.
std::function<void(std::function<void()>)> on_change;
};
/// A capability panel (signal generator, digital I/O, ...) for the side area.
struct SourcePanel {
std::string id; ///< stack page name
std::string title;
std::string icon; ///< icon name for the tab, may be empty
GtkWidget* widget = nullptr;
};
/// One plugin type: its identity, factory and UI.
class SourceKind {
public:
virtual ~SourceKind() = default;
virtual std::string id() const = 0; ///< short, `[a-z0-9]+`; the CLI and session name
virtual std::string display_name() const = 0;
/// A stable, human-readable name for one configured source (`host:port`, a serial number).
virtual std::string identity(const SourceConfig& cfg) const = 0;
virtual std::unique_ptr<DataSource> make(const SourceConfig& cfg) const = 0;
virtual ConfigForm make_config_form() const = 0;
/// Panels for a live source. The widgets must reach the source only through `post()`.
virtual std::vector<SourcePanel> make_panels(DataSource& source) const {
(void)source;
return {};
}
/// Whether the source reports raw datagrams (enables the packet-capture UI).
virtual bool has_datagrams() const { return false; }
};
} // namespace udpscope