8.0 KiB
8.0 KiB
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:
- Plugin framework + UDPS client ported onto it (this document).
- Flat UDP plugin (standard MARTe2 UDPWriter stream; the wire format must be supplied or
located before that spec is written -- none was found in
~/workspaceor~/Projects). - MCC E-1608 plugin. The protocol code in
MCC_E1608_DataSourceis bound to MARTe2 (BasicTCPSocket,StreamString,SingleThreadService); it is ported to plain C++, not linked against MARTe2. AI streaming, DIO and DAC/generator panels. - ABScope plugin. Needs a multi-timebase data path and a wire format: the ABScope design
only sketches the UDP header and
streamerdis 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-optform, and what a config widget reads and writes.Frame: source-neutral, mirrorsudps_frame_t:counter,lost,late,recv_time,publish_mode,num_samples,signals[](the existingSignalInfo) andvalues[](doubles).signalsandvaluesare pointers into the source's own buffers, valid only for the duration ofon_data;Acquisitioncopies what it keeps. The hole, backfill and time-base logic needscounter/lost/late; a source with no counter supplies a running counter andlost = 0.Frame::value(sig, k, col)replacesudps_frame_value.DataSource: the per-connection object.open(),poll(timeout_ms),close().stats()returns the counters now inStreamStats;connected()reports link state.post(Command): thread-safe queue, drained insidepoll().Commandisstd::function<void()>.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 insidepoll(), asudps_clientdoes today.
SourceKind: factory and UI description of one plugin type:id,display_name,identity(cfg),make(cfg),make_config_form()andmake_panels(source)(each{id, title, GtkWidget*}).make_config_form()takes no argument: the chooser callsform.write(cfg)to fill the form. AConfigFormis a list ofAdwPreferencesRowwidgets pluswrite/read/valid/on_changeclosures, so the chooser can place the rows in its own group and gate its Connect button onvalid().SourceRegistry: filled by an explicitregister_builtin_sources()call. No static-init self-registration (linkers can drop it). A futuredlopenloader would only callregister_kind().
2. Acquisition and the UDPS plugin
Acquisitiontakes astd::unique_ptr<DataSource>instead of building audps_client_tand implementsSourceSink; it creates the source lazily instart(), from the kind andSourceConfigit was given; the four trampolines go away.on_config/on_datachange only in the type they read. Time base, holes, backfill, decimation, trigger and math code are untouched.run()callssource_->poll(50)and copiessource_->stats()intoScopeModel::stats().PacketCapturestays inAcquisition, fed through the datagram tap. Sources without datagrams (E-1608) leave the capture UI disabled, not hidden.src/source/udps_source.cppwrapsdeps/udpsclientunchanged: it mapsudps_signal_ttoSignalInfoandudps_frame_ttoFrame, and carries the existing knobs (server, port, multicast, interface, recv buffer). Identity ishost:port.- The
udps_*types are used only inacquisition.cpptoday, so no other file changes for this step beyond theFrametype.
3. UI hosting, config and persistence
- Connect dialog (
source_dialog.cpp): gains a "Source type" dropdown; choosing a kind swaps in that kind'smake_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 the existingside_stack(the side area's page stack, owned by the tab switcher). 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 callsource->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 insrc/source/widgets/but is only added when a plugin uses it, so it arrives with sub-project 3. - Config / CLI:
Configgainssource_kind(defaultudps) and aSourceConfig.--server/--port/--multicast/--interfacepopulate the UDPS entries.--source NAMEand repeatable--source-opt k=vwork for any kind.--helplists the registered kinds. - Sessions: the source's config is stored as top-level TOML keys,
source_kindandsource_opt_<key>, written only for non-UDPS kinds (a UDPS session file is unchanged, so legacyhost_port.tomlfiles read asudpswith nothing migrated). It is not a[source]table. A non-UDPS session file is named by a pseudo-host<kind>-<identity>with port 1. - Mocks:
tools/mock_udpstreamer.pyremains the UDPS mock. Each later plugin ships its own mock undertools/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, noGtk-CRITICAL(seedocs/claude/verification.md). - New probe
UDPSCOPE_PROBE_SOURCE_KINDS(UDPSCOPE_PROBE_SOURCESis already the chooser discovery probe). Its value is a delay in ms, and its presence also registers the stubprobekind. It extends the probe/dump insrc/ui.cpprather than adding a harness. It checks that the registry lists its kinds, that a config form round-trips itsSourceConfig, 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 bypoll(). - Document the new probe lines in
docs/claude/probe-readings.md; updatearchitecture.md(aDataSourcereplaces the client in the thread description),build-and-run.md(new CLI options) andREADME.md.
Out of scope
- Runtime
.soloading, 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).