Files
udpscope/docs/superpowers/specs/2026-10-31-e1608-source-design.md
T

8.2 KiB

MCC E-1608 source plugin (sub-project 3 of 4)

Date: 2026-10-31 Branch: feature/source-e1608 (from feature/source-plugins, sub-project 1)

Context

Sub-project 1 added the DataSource / SourceKind / SourceRegistry framework and ported UDPS onto it. This sub-project adds a second kind, e1608: the Measurement Computing E-1608 Ethernet DAQ (8 SE / 4 DIFF 16-bit analog inputs, 2 analog outputs, 8 DIO bits, one 32-bit counter), with its own connect form and two side-stack panels.

References for the wire protocol, both written by the user:

  • Primary: ~/Projects/marte2_projects/MCC_E1608_DataSource, a MARTe2 DataSource (E1608DataSource.{h,cpp}, E1608ScanFifo). Its class comment in the header is the authoritative list of parameters and behaviour. It is bound to MARTe2 (BasicTCPSocket, StreamString, SingleThreadService), so it is ported to plain C++, not linked.
  • Cross-check: ~/Projects/uopi/internal/datasource/mcc/{protocol,e1608}.go, an independent Go implementation of the same protocol.

No board is available. The mock is the only verification until one is attached.

Decisions

Question Decision
DAC / generator Static level plus a software waveform (sine, square, triangle, ramp), 10 Hz at most. Runs as a timer inside poll(); no extra thread.
Hardware None available. A protocol mock (tools/mock_e1608.py) is the contract. Commands whose layout rests on a single reference (or where the two disagree) are listed as unverified.
Analog input config Full: per-channel enable and range, global SE/DIFF, MAC discovery, external clock, trigger, DI/counter poll rate.
Digital lines and counter Plot channels the user chooses, off by default, enabled per line in the connect form.
Acquisition modes Hardware-paced only (scan rate or external clock). Software-paced mode (ScanRate = 0) is not ported.
Structure Three layers: link, device, source. The bottom two have no GTK or DataSource types.

Design

1. Files (new, src/source/e1608/, plus the mock)

  • e1608_link.{hpp,cpp}: UDP discovery (broadcast, or a unicast sweep of a given subnet matched by MAC and the E-1608 product id) and connection-code handshake; TCP command framing with a timeout; the scan socket on port + 1. Plain POSIX sockets.
  • e1608_device.{hpp,cpp}: typed commands (AI, AO, DIO read/write/direction, counter read/reset, scan queue, scan start/stop, status), the factory calibration read from the board's calibration memory, and decode(), which turns raw interleaved uint16 scans into calibrated volts (double).
  • e1608_source.{hpp,cpp}: the DataSource: the poll() state machine, Frame building, the DI/counter and generator timers, the atomic readouts for the panels.
  • e1608_kind.cpp: the SourceKind: the connect form and the two panels.
  • tools/mock_e1608.py: the protocol mock.
  • register_builtin_sources() registers e1608 after udps.

2. Data path and threading

Everything runs inside poll() on the producer thread. The GTK thread reaches the board only through post(Command).

poll(timeout):

  1. Drains the posted commands (DAC writes, DIO toggles, counter reset, generator edits).
  2. Runs the connection state machine: optional MAC lookup, handshake, calibration read, DIO direction and initial outputs, scan queue and scan start take Disconnected to Scanning. A link error fires on_event(Disconnected) and retries every 2 s. A connect attempt can block poll() for up to the command timeout; that delays only the command queue.
  3. Waits on the scan socket for at most the time to the next timer, reads what arrived, keeps a partial scan for the next call, decodes the complete scans with the calibration and delivers a Frame.
  4. Runs the due timers: the DI/counter poll (DiPollRate) is a command round trip; the generator tick (at most 10 Hz) is a DAC write.

The frame:

  • Signals: the enabled AI channels (unit V, range_min/range_max from the channel's range), the DI lines the user chose (0/1), and the counter if chosen.
  • All signals are scalar; num_samples is the number of scans decoded in that read.
  • DI and counter values are held across the block (a step trace).
  • sampling_rate is the scan rate on every signal. counter is a running frame count.
  • The aggregate rate (enabled channels times scan rate) must stay at or below 250 kS/s. The form refuses a configuration above it.

Overrun: if the board reports a scan overrun the source fires on_event(Error) and restarts the scan when AutoRestart is set; the next frame carries lost = 1. This is approximate, because the board does not report how many scans it dropped.

Panel readouts: the source keeps the live DIO inputs, the counter, the DAC levels and the generator state in atomics. The panels read them from a GTK timer, so no widget touches the source's internals.

3. Connect form and panels

Connect form: grouped AdwExpanderRows in the chooser's group. SourceConfig stays flat; a per-channel setting is a key such as ai0=BIP10V (absent means off).

  • Connection: address, MAC, discovery subnet, port, connection code, timeout. valid() needs an address, or a MAC plus a subnet.
  • Acquisition: scan rate, external clock, trigger mode, auto-restart, calibrate (default on), DI/counter poll rate.
  • Analog inputs: global SE or DIFF mode and 8 channel rows (enable, range). In DIFF mode rows 4 to 7 are insensitive, not hidden. valid() also enforces the 250 kS/s aggregate.
  • Digital I/O: direction per bit (I or O), a "plot" switch per input bit, a "plot counter" switch, the initial DO value and the initial DAC levels.

identity() is the address, or the MAC when there is no address. A session file is named e1608-<identity>, port 1, as sub-project 1 defines for non-UDPS kinds. has_datagrams() is false, so the capture button is disabled.

Two side-stack panels, both insensitive while disconnected (disabled, not hidden):

  • Analog out: per DAC, a mode (Level, Sine, Square, Triangle, Ramp), a level or amplitude plus offset, a frequency of 0.1 to 10 Hz, and an on/off switch. It shows the last voltage written and clamps to ±10 V. The generator stops on disconnect and the DACs return to their initial levels on reconnect.
  • Digital I/O: eight bit rows; an input bit shows a live LED, an output bit has a toggle that posts a write. The counter shows its value and a Reset button. Directions come from the form and are not changed at runtime.

Tab icons are chosen in the plan under the project's bundled-icon rules.

4. Verification

Mock (tools/mock_e1608.py): UDP discovery with a MAC and the connection-code handshake; TCP commands for AI, AO, DIO, the counter, scan queue, scan start/stop, calibration memory and status; a scan stream on port+1 at the requested rate with a sine per channel; switches to drop the link after N seconds and to report an overrun. Its header lists every command whose layout is unverified (single reference, or the references disagree).

Probe UDPSCOPE_PROBE_E1608=<delay ms>, run against the mock with --source e1608, extends the probe/dump in src/ui.cpp. It checks:

  • Frames arrive with the expected channel count and sample rate.
  • The panels are present and insensitive while disconnected.
  • A DAC write, a DIO toggle and a counter reset each reach the mock.
  • A forced link drop leads to a reconnect and a resumed stream.
  • The calibration is applied (the mock serves non-trivial coefficients).

A decode check needs no mock: a fixed raw buffer decoded against hand-computed volts.

Baselines are unchanged: the UDPS self-check reads as on main, exactly three build warnings, no Gtk-CRITICAL.

Errors: a rejected handshake (bad connection code), no device found, timeouts and scan-socket loss surface through on_event(Error) with a readable message and follow the reconnect cycle. A rejected connection code stops the retries: the source stays in Error with that message until the user reconnects.

Docs: update architecture.md, build-and-run.md, verification.md, probe-readings.md and README.md.

Out of scope

  • Software-paced acquisition (ScanRate = 0).
  • A generator faster than 10 Hz, and a dedicated I/O thread.
  • Changing DIO directions at runtime.
  • Per-channel SE/DIFF mixing (one global mode).
  • Verification against a real board.