RadioPhotonic radar (RPH) collector
Application-owned acquisition binary for the L-Card E-502 ADC. It captures the
dual-channel main + reference IQ stream and publishes complete RPH1 sweeps over
a PTY. RphRadarService in python_app/hardware_full/rph_radar reads and
processes that stream for python_app.scripts.rph_radar_raw_producer.
This is the data source for radar.model == "rph_radar" with
radar.rph_radar.source: "e502", paired with the Python rph_radar_raw_producer.
The rfg_udp source reads the RFG board's UDP stream directly and does not use this
binary. LibreVNA acquisition uses Python drivers.
Layout
| Path | Contents |
|---|---|
src/ |
Collector implementation (main.cpp) and the PTY writer. |
include/ |
PTY header and switch_sequencer.h. |
vendor/lcard/ |
L-Card X502/E502 SDK headers (x502api.h, e502api.h, lcard_pstdint.h), vendored verbatim so the collector builds with no external include path. |
In switch-aware mode, SwitchSequencer drives the H7992 switches at actual
DI_SYN2 sweep ends. Duration limits and Stop never advance the switches. Every
physical combination change discards the remaining decoded ADC/DIN block and
recreates the source session after max(1 ms, run.settling_ms) has elapsed.
Timestamps never decide whether a sweep belongs to the new combination. The
collector completes a cycle only after a full, clean sweep for each combination.
An unchanged combination keeps the existing session.
A restart stops cyclic DOUT with WAIT_DONE, stops and disables the streams,
restores DO2 high / DO1 and DO8 low, then closes and frees the SDK descriptor.
After settling, a new descriptor is created, opened and configured. The collector
reloads the DO1/DO8 pattern, waits for OutCycleSetup(WAIT_DONE), and starts
ADC/DIN/DOUT together. Pending samples and phase accumulators are cleared;
source sweep and cycle IDs remain continuous. The initial high DI_SYN2 portion
is dirty: acquisition waits for a complete rising-to-falling sweep. This can
skip a variable number of physical sweeps while reopening; no fixed skip count
or firmware-pause estimate is used. Hardware validation must measure reopen
latency, USB resource release and multi-combination throughput.
The application uses the internal ADC clock. ADC and DIN start together,
continuously; DI_SYN2 gates sweeps in software. Source times are the monotonic
clock immediately before StreamsStart plus the ADC sample index divided by the
configured sample rate. These are source-clock estimates, not hardware timestamps;
startup latency shifts the initial epoch earlier. USB and PTY queueing do not
shift timestamps to the time Python reads a sweep. An estimated source timestamp
ahead of receipt stops acquisition instead of publishing a future capture window.
ADC/host drift can trigger this guard during long single-combination runs; its
bound and the accuracy of capture windows need measurement on hardware. Drift
cannot make data from an old SDK session clean after a physical switch.
The collector uses the common H7992 driver, with its own include set and
build/rph/ object tree.
PTY format
Each packet is a 48-byte little-endian header (<4sIQQQQHHI) followed by the
payload: RPH1, payload bytes (u32), source sweep ID, cycle ID, start/end ns
(four u64), input/output positions (two u16), flags (u32; bit 0 means dirty).
Without collector switching both positions are 0xffff, and cycle equals sweep
ID. Payload points remain four u16 words: marker (0x000a main, 0x00a8
reference), step index, signed I, signed Q. There are no boundary or combo frames.
The queue drops complete sweeps on overflow; a packet already being written is
never truncated. Maximum payload is 1,048,544 bytes. Python matches complete
cycles. Python-controlled switching closes the previous collector and PTY reader,
then starts a new collector after settling; phase-processing state is preserved.
Build
make rph_radar_collector # -> build/bin/rph_radar_collector
It is not part of make all; start.sh builds it automatically when the
active config selects radar.model == "rph_radar", unless --skip-build is set.
The Linux build uses the repository's common sources and vendored headers and
links with -pthread -ldl -lutil; no installed L-Card SDK is needed to build it.
Runtime
The collector loads the proprietary libx502api.so / libe502api.so at run time
via dlopen, so those libraries must be discoverable by the dynamic loader.
The RPH radar service prepends ~/.local/lib (their default install location)
unless the config pins LD_LIBRARY_PATH in radar.rph_radar.env.
There is one acquisition path: USB, differential ADC channels 2/3, maximum
internal-clock rate, rising/falling DI_SYN2 gating, DO1 pair subtraction and DO8
reference overlay. radar.rph_radar.args sets ADC range, internal reference,
maximum sweep duration and DO8 period. The example's fixed-path options remain
accepted as guards; incompatible modes and unused diagnostic options are rejected.
The service appends tty:<path> from radar.rph_radar.tty_path.
When both RF switches use driver_mode: "native", Python passes
switching:<JSON> with the already decoded input/output switch settings, combos
and settling time. The collector reads no profile file. Otherwise, Python selects
the switches and starts a new collector after settling when the combination
changes. Calibration captures also use this Python-controlled path.
The launching service owns the collector process and forwards its output to the application log. On Linux, parent death sends SIGKILL to the collector; normal Stop drains and closes the service's own child. No global process-name kill is used.