docs: design for the data-source plugin framework (sub-project 1 of 4)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
f3f8f67a62
commit
1a3393229a
1 file changed
+116
@@ -0,0 +1,116 @@
|
|||||||
|
# Source plugins: framework and UDPS port (sub-project 1 of 4)
|
||||||
|
|
||||||
|
Date: 2026-10-31
|
||||||
|
Branch: `feature/source-plugins`
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
udpscope reads exactly one kind of source, a UDPStreamer stream through `deps/udpsclient`.
|
||||||
|
The goal is to accept several kinds of data source as plugins, each with its own config
|
||||||
|
dialog and, where the hardware offers it, its own UI (signal generator, digital I/O,
|
||||||
|
multiple timebases).
|
||||||
|
|
||||||
|
The work is split into four sub-projects, each with its own spec, plan and implementation:
|
||||||
|
|
||||||
|
1. **Plugin framework + UDPS client ported onto it** (this document).
|
||||||
|
2. Flat UDP plugin (standard MARTe2 UDPWriter stream; the wire format must be supplied or
|
||||||
|
located before that spec is written -- none was found in `~/workspace` or `~/Projects`).
|
||||||
|
3. MCC E-1608 plugin. The protocol code in `MCC_E1608_DataSource` is bound to MARTe2
|
||||||
|
(`BasicTCPSocket`, `StreamString`, `SingleThreadService`); it is ported to plain C++, not
|
||||||
|
linked against MARTe2. AI streaming, DIO and DAC/generator panels.
|
||||||
|
4. ABScope plugin. Needs a multi-timebase data path and a wire format: the ABScope design
|
||||||
|
only sketches the UDP header and `streamerd` is unwritten.
|
||||||
|
|
||||||
|
Sub-project 1 changes no UDPS behaviour. It is a refactor plus the hosting machinery the
|
||||||
|
later plugins need.
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
| Question | Decision |
|
||||||
|
| --- | --- |
|
||||||
|
| What "plugin" means | Compiled-in registry; the interface is kept clean enough for a `dlopen` loader to be added later (not built now). |
|
||||||
|
| Threading | `Acquisition` keeps the single producer thread and calls `source->poll()`. UI actions reach the source through a thread-safe command queue drained inside `poll()`. CLAUDE.md's two-thread rule holds. (A per-source I/O thread was considered and deferred.) |
|
||||||
|
| Source UI | Widget factory: a source returns `GtkWidget*` for its config form and each capability panel. |
|
||||||
|
| Timebases in the frame | The neutral frame is single-timebase. Sub-project 4 extends interface and data path together; this may break the interface once, accepted. |
|
||||||
|
| CLI / sessions | `--server/--port/...` keep meaning UDPS; `--source NAME` and `--source-opt k=v` added. Sessions keyed by kind + identity; legacy `host_port.toml` read as `udps`. |
|
||||||
|
|
||||||
|
## Design
|
||||||
|
|
||||||
|
### 1. The source interface (new `src/source/`)
|
||||||
|
|
||||||
|
- **`SourceConfig`**: ordered string key/value map with typed getters. It is the stored form
|
||||||
|
(a TOML table), the `--source-opt` form, and what a config widget reads and writes.
|
||||||
|
- **`Frame`**: source-neutral, mirrors `udps_frame_t`: `counter`, `lost`, `late`,
|
||||||
|
`recv_time`, `publish_mode`, `num_samples`, `signals[]` (the existing `SignalInfo`) and
|
||||||
|
`values[]` (doubles). The hole, backfill and time-base logic needs `counter/lost/late`; a
|
||||||
|
source with no counter supplies a running counter and `lost = 0`. `Frame::value(sig, k,
|
||||||
|
col)` replaces `udps_frame_value`.
|
||||||
|
- **`DataSource`**: the per-connection object.
|
||||||
|
- `open()`, `poll(timeout_ms)`, `close()`.
|
||||||
|
- `stats()` returns the counters now in `StreamStats`; `connected()` reports link state.
|
||||||
|
- `post(Command)`: thread-safe queue, drained inside `poll()`.
|
||||||
|
- `set_datagram_tap(fn)`: optional; lets UDP-based sources feed the raw-packet capture.
|
||||||
|
- Output goes to a `SourceSink` (`on_config`, `on_data`, `on_event`) called inside
|
||||||
|
`poll()`, as `udps_client` does today.
|
||||||
|
- **`SourceKind`**: factory and UI description of one plugin type: `id`, `display_name`,
|
||||||
|
`identity(cfg)`, `make(cfg)`, `make_config_form(cfg)` (a `GtkWidget*` plus a `read()`
|
||||||
|
closure) and `make_panels(source)` (each `{id, title, GtkWidget*}`).
|
||||||
|
- **`SourceRegistry`**: filled by an explicit `register_builtin_sources()` call. No
|
||||||
|
static-init self-registration (linkers can drop it). A future `dlopen` loader would only
|
||||||
|
call `register_kind()`.
|
||||||
|
|
||||||
|
### 2. Acquisition and the UDPS plugin
|
||||||
|
|
||||||
|
- `Acquisition` takes a `std::unique_ptr<DataSource>` instead of building a `udps_client_t`
|
||||||
|
and implements `SourceSink`; the four trampolines go away.
|
||||||
|
- `on_config` / `on_data` change only in the type they read. Time base, holes, backfill,
|
||||||
|
decimation, trigger and math code are untouched.
|
||||||
|
- `run()` calls `source_->poll(50)` and copies `source_->stats()` into
|
||||||
|
`ScopeModel::stats()`. `PacketCapture` stays in `Acquisition`, fed through the datagram
|
||||||
|
tap. Sources without datagrams (E-1608) leave the capture UI disabled, not hidden.
|
||||||
|
- `src/source/udps_source.cpp` wraps `deps/udpsclient` unchanged: it maps `udps_signal_t`
|
||||||
|
to `SignalInfo` and `udps_frame_t` to `Frame`, and carries the existing knobs (server,
|
||||||
|
port, multicast, interface, recv buffer). Identity is `host:port`.
|
||||||
|
- The `udps_*` types are used only in `acquisition.cpp` today, so no other file changes for
|
||||||
|
this step beyond the `Frame` type.
|
||||||
|
|
||||||
|
### 3. UI hosting, config and persistence
|
||||||
|
|
||||||
|
- **Connect dialog** (`source_dialog.cpp`): gains a "Source type" dropdown; choosing a kind
|
||||||
|
swaps in that kind's `make_config_form()`. The UDPS form (server, port, multicast,
|
||||||
|
interface) moves into the UDPS plugin. The dialog keeps the known-sources list, buffer
|
||||||
|
size, relative time and capture ring size.
|
||||||
|
- **Capability panels**: after the source is created the app calls `make_panels()` and adds
|
||||||
|
each panel as a page of a "Source" stack in the side area. A panel is insensitive when
|
||||||
|
disconnected (disabled, not hidden; the tab switcher owns visibility). A source with no
|
||||||
|
panels adds no tab; UDPS has none. Panels call `source->post(Command)`, never the source
|
||||||
|
directly, so cross-thread safety lives in one place. A helper kit for common panels (DIO
|
||||||
|
bank, generator form) lives in `src/source/widgets/` but is only added when a plugin uses
|
||||||
|
it, so it arrives with sub-project 3.
|
||||||
|
- **Config / CLI**: `Config` gains `source_kind` (default `udps`) and a `SourceConfig`.
|
||||||
|
`--server/--port/--multicast/--interface` populate the UDPS entries. `--source NAME` and
|
||||||
|
repeatable `--source-opt k=v` work for any kind. `--help` lists the registered kinds.
|
||||||
|
- **Sessions**: file `<kind>_<identity>.toml` with the source's config in a `[source]`
|
||||||
|
table. Legacy `host_port.toml` is read as `udps`; nothing is migrated destructively.
|
||||||
|
- **Mocks**: `tools/mock_udpstreamer.py` remains the UDPS mock. Each later plugin ships its
|
||||||
|
own mock under `tools/` so the headless self-check keeps working.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- This is a pure refactor for UDPS: the existing headless self-check must read exactly as on
|
||||||
|
`main` -- exactly three build warnings, no `Gtk-CRITICAL` (see `docs/claude/verification.md`).
|
||||||
|
- New probe `UDPSCOPE_PROBE_SOURCES`, extending the probe/dump in `src/ui.cpp` rather than
|
||||||
|
adding a harness. It checks that the registry lists its kinds, that a config form
|
||||||
|
round-trips its `SourceConfig`, and -- with a stub test-only kind carrying one panel --
|
||||||
|
that the panel's tab is built, insensitive while disconnected, and that a posted command is
|
||||||
|
drained by `poll()`.
|
||||||
|
- Document the new probe lines in `docs/claude/probe-readings.md`; update `architecture.md`
|
||||||
|
(a `DataSource` replaces the client in the thread description), `build-and-run.md`
|
||||||
|
(new CLI options) and `README.md`.
|
||||||
|
|
||||||
|
## Out of scope
|
||||||
|
|
||||||
|
- Runtime `.so` loading, and a per-source I/O thread.
|
||||||
|
- Multi-timebase data path (sub-project 4).
|
||||||
|
- The flat UDP, E-1608 and ABScope plugins (sub-projects 2-4).
|
||||||
|
- The DIO / generator helper widgets (arrive with sub-project 3).
|
||||||
Reference in new issue
Block a user