Feature/source plugins #1
No files matched your search
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in new issue
Block a user