Feature/source plugins #1
@@ -0,0 +1,153 @@
|
||||
# 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 `AdwExpanderRow`s 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.
|
||||
Reference in new issue
Block a user