added docs

This commit is contained in:
Ayzen
2026-10-16 18:35:02 +03:00
parent b88d704126
commit ad2b3e555a
19 changed files with 3719 additions and 663 deletions
@@ -1,101 +1,110 @@
# RadioPhotonic radar (RPH) collector
# Коллектор радиофотонного радара (RPH)
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`.
Программа сбора для АЦП L-Card E-502, которой владеет приложение. Она принимает
двухканальный поток IQ (основной и опорный сигналы) и передаёт целые развёртки в
пакетах `RPH1` через псевдотерминал. Поток читает и обрабатывает `RphRadarService` из
`python_app/hardware_full/rph_radar` для процесса сбора
`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.
Это источник данных для `radar.model: "rph_radar"` с `radar.rph_radar.source: "e502"`.
Источник `rfg_udp` читает UDP-поток платы RFG напрямую и эту программу не использует;
LibreVNA работает через драйверы на Python. Как радар устроен целиком — в главе
[Оборудование](../../docs/hardware.md).
## 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. |
| Путь | Что |
| --- | --- |
| `src/` | коллектор (`main.cpp`) и запись в псевдотерминал |
| `include/` | заголовок псевдотерминала и `switch_sequencer.h` |
| `vendor/lcard/` | заголовки SDK L-Card X502/E502 (`x502api.h`, `e502api.h`, `lcard_pstdint.h`) без изменений, чтобы коллектор собирался без внешних путей |
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.
Коллектор использует общий драйвер ВЧ-ключей H7992 со своим набором заголовков и
каталогом объектов `build/rph/`.
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.
Если ключами управляет коллектор, `SwitchSequencer` переключает ключи H7992 по
настоящим концам развёрток (спаду DI_SYN2). Ограничение длительности и остановка
ключи не переключают. При каждой смене пары коллектор отбрасывает оставшийся
декодированный блок АЦП и цифрового входа и после `max(1 ms, run.settling_ms)`
создаёт сеанс источника заново. Принадлежность развёртки новой паре по времени не
определяется. Цикл считается законченным, только когда для каждой пары есть целая
чистая развёртка. Если пара не меняется, сеанс сохраняется.
The collector uses the common H7992 driver, with its own include set and
`build/rph/` object tree.
Перезапуск сеанса: остановка циклического вывода DOUT с `WAIT_DONE`, остановка и
отключение потоков, возврат DO2 в высокий уровень, DO1 и DO8 — в низкий, закрытие и
освобождение
дескриптора SDK. После паузы установления создаётся, открывается и настраивается новый
дескриптор, загружается шаблон DO1/DO8, коллектор ждёт `OutCycleSetup(WAIT_DONE)` и
запускает АЦП, цифровой вход и вывод вместе. Накопленные отсчёты и фазовые
накопители сбрасываются; номера развёрток и циклов идут без разрыва. Начальный
высокий участок DI_SYN2 считается испорченным: сбор ждёт целую развёртку от фронта до
спада. Поэтому при переоткрытии пропускается переменное число развёрток; постоянного
числа пропусков или оценки паузы прошивки нет. Время переоткрытия, освобождение
ресурсов USB и скорость при многих парах нужно измерить на железе.
## 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.
Коллектор работает от внутренних часов АЦП. АЦП и цифровой вход запускаются вместе и
работают непрерывно; развёртки выделяет программа по линии DI_SYN2. Время отсчёта —
монотонные часы компьютера непосредственно перед `StreamsStart` плюс номер отсчёта,
делённый на частоту выборки. Это оценка по часам источника, а не аппаратная метка:
задержка запуска сдвигает начало отсчёта раньше. Очереди USB и псевдотерминала время
не сдвигают. Если оценка времени отсчёта оказалась позже момента его приёма, сбор
останавливается, чтобы не публиковать окно съёма из будущего. Расхождение часов АЦП и
компьютера может сработать на этой проверке при долгой работе на одной паре; её
предел и точность окон съёма нужно измерить на железе. Данные старого сеанса SDK
после переключения ключа чистыми не становятся ни при каком расхождении часов.
## Build
## Формат псевдотерминала
Пакет — 48-байтный заголовок в порядке байтов little-endian (`<4sIQQQQHHI`) и данные:
`RPH1`, длина данных (u32), номер развёртки источника, номер цикла, начало и конец
съёма в нс (четыре u64), положения входа и выхода (два u16), флаги (u32; бит 0 —
развёртка испорчена). Без переключения коллектором оба положения равны `0xffff`, а
номер цикла совпадает с номером развёртки. Точка данных — четыре слова u16: метка
(`0x000a` основной сигнал, `0x00a8` опорный), номер ступени, I и Q со знаком. Кадров
границ и пар нет. При переполнении очередь выбрасывает целые развёртки; пакет, который
уже пишется, не обрезается. Наибольший размер данных — 1 048 544 байта. Целые циклы
собирает Python. При переключении ключей из Python прежний коллектор и чтение
псевдотерминала закрываются, а после паузы установления запускается новый коллектор;
состояние фазовой обработки сохраняется.
## Сборка
```bash
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.
В `make all` коллектор не входит; `start.sh` собирает его сам, если в профиле
`radar.model: "rph_radar"`, кроме запуска с `--skip-build`. Сборка в Linux берёт общие
исходники и заголовки из репозитория и линкуется с `-pthread -ldl -lutil`;
установленный SDK L-Card для сборки не нужен.
## 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`.
Закрытые библиотеки `libx502api.so` и `libe502api.so` коллектор загружает при запуске
через `dlopen`, поэтому их должен находить загрузчик. Сервис RPH ставит
`~/.local/lib` (их обычное место) в начало пути поиска, если профиль не задаёт
`LD_LIBRARY_PATH` в `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`.
Путь съёма один: USB, дифференциальные каналы АЦП 2 и 3, наибольшая частота от
внутренних часов, выделение развёрток по фронту и спаду DI_SYN2, вычитание соседних
отсчётов с разными уровнями DO1 с усреднением и наложение опорного сигнала по DO8.
`radar.rph_radar.args` задаёт диапазон АЦП, внутреннюю опорную
частоту, наибольшую длительность развёртки и период DO8. Остальные параметры из
примера принимаются только как подтверждение этого пути; несовместимые режимы и
неиспользуемые отладочные параметры отвергаются. Параметр `tty:<путь>` из
`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.
Если оба ВЧ-ключа в `driver_mode: "native"`, Python передаёт коллектору
`switching:<JSON>` с уже прочитанными настройками ключей, парами и паузой
установления; профиль коллектор не читает. Иначе ключи переключает Python и при смене
пары после паузы запускает новый коллектор. Съём наборов калибровки тоже идёт этим
путём.
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.
Процессом коллектора владеет запустивший его сервис; вывод коллектора уходит в журнал
программы. В Linux смерть родителя завершает коллектор сигналом SIGKILL; обычный
**Stop** дочитывает и закрывает собственный дочерний процесс сервиса. Завершения
процессов по имени нет.