per-point switching: hardware bring-up, profiles, protocol doc
Verified on the bench that the modified master bitstream excites in every stage and the RF switch routes: 16 traces per sweep, 62 dB between the two switch positions of one master port, 9.9k measurements/s. Adds the built master image, two run profiles, a verification script and librevna_protocol.md, and drops the stale prototype driver copy that the firmware tree still carried.
This commit is contained in:
+24
@@ -166,6 +166,25 @@ ENV/
|
|||||||
env.bak/
|
env.bak/
|
||||||
venv.bak/
|
venv.bak/
|
||||||
|
|
||||||
|
# Локальные инструменты, распакованные не из пакетного менеджера (GHDL для
|
||||||
|
# анализа и симуляции VHDL, clangd). Ставятся скриптом, в репозиторий не идут.
|
||||||
|
.tools/
|
||||||
|
|
||||||
|
# База компиляции для clangd: абсолютные пути этой машины.
|
||||||
|
# Генерируется .claude/tools/gen_compile_commands.sh из Makefile.
|
||||||
|
compile_commands.json
|
||||||
|
|
||||||
|
# Граф знаний graphify: 22 МБ, пересобирается из кода за минуту.
|
||||||
|
# Список исключений для него — в .graphifyignore, он тоже не версионируется
|
||||||
|
# (см. ниже, вместе с остальной обвязкой Claude Code).
|
||||||
|
graphify-out/
|
||||||
|
|
||||||
|
# Payload комплекта установки на Windows: копия файлов из дерева LibreVNA.
|
||||||
|
# Генерируется install_windows/make_files.sh; в git не идёт, чтобы не разойтись
|
||||||
|
# с деревом (уже расходился).
|
||||||
|
device_firmware/multi_libreVNA_firmware/reproduce/install_windows/files/
|
||||||
|
device_firmware/multi_libreVNA_firmware/reproduce/install_windows/backup_original/
|
||||||
|
|
||||||
# Spyder project settings
|
# Spyder project settings
|
||||||
.spyderproject
|
.spyderproject
|
||||||
.spyproject
|
.spyproject
|
||||||
@@ -226,7 +245,12 @@ __marimo__/
|
|||||||
python_app/runtime
|
python_app/runtime
|
||||||
SHARE_INTERNET_TO_PI.md
|
SHARE_INTERNET_TO_PI.md
|
||||||
|
|
||||||
|
# Локальная обвязка Claude Code: инструкции, хуки, скилы, конфиги инструментов.
|
||||||
|
# В репозиторий не идёт.
|
||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
|
.claude/
|
||||||
|
.graphifyignore
|
||||||
|
pyrightconfig.json
|
||||||
/docs/
|
/docs/
|
||||||
test_end_2/
|
test_end_2/
|
||||||
|
|
||||||
|
|||||||
@@ -69,9 +69,11 @@ controls plus a pixel-identical live plot through an embedded web UI.
|
|||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- Linux (POSIX shared memory; GPIO on Raspberry Pi).
|
- Linux (POSIX shared memory; GPIO on Raspberry Pi). The deployment target is a
|
||||||
|
**Raspberry Pi 5**; hardware PWM gating for the generator sweep is Pi 5
|
||||||
|
specific (GPIO12/13/18/19, `dtoverlay=pwm-2chan`).
|
||||||
- A C++20 compiler and `make` — Eigen and nlohmann/json are vendored, so no extra C++ dependencies are needed.
|
- A C++20 compiler and `make` — Eigen and nlohmann/json are vendored, so no extra C++ dependencies are needed.
|
||||||
- Python 3.12 with a virtual environment.
|
- **Python 3.10 or newer**, in a virtual environment.
|
||||||
|
|
||||||
## Build and run
|
## Build and run
|
||||||
|
|
||||||
|
|||||||
@@ -13,12 +13,18 @@ counterpart to the C++ `sweep_orchestrator` used by LibreVNA devices.
|
|||||||
| Path | Contents |
|
| Path | Contents |
|
||||||
|------|----------|
|
|------|----------|
|
||||||
| `src/` | Collector implementation (`main.cpp`) and the PTY / capture-file writers. |
|
| `src/` | Collector implementation (`main.cpp`) and the PTY / capture-file writers. |
|
||||||
| `include/` | Headers for the writers. |
|
| `include/` | Headers for the writers plus `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. |
|
| `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. |
|
||||||
|
|
||||||
The sources under `src/` and `include/` are imported verbatim from the original
|
The sources started as a verbatim import of the original standalone tool, but are
|
||||||
standalone tool and are kept byte-for-byte identical — the wire format and
|
**no longer identical to it**: the collector now drives the RF switches itself
|
||||||
acquisition behaviour are unchanged; only their home moved into this repo.
|
(switch-aware mode, `SwitchSequencer`), stepping to the next combination inside
|
||||||
|
the hardware idle gap between sweeps and flagging a sweep *dirty* when a readout
|
||||||
|
backlog means the toggle could not have landed in that gap. It therefore reuses
|
||||||
|
the orchestrator's switch drivers and the shared `run_config` loader, which is why
|
||||||
|
its build has its own include set and its own `build/kamil/` object tree.
|
||||||
|
|
||||||
|
The wire format towards the Python producer is unchanged.
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,267 @@
|
|||||||
|
# Бинарный протокол LibreVNA: что шлёт и парсит наш драйвер
|
||||||
|
|
||||||
|
Разбор USB-протокола LibreVNA и того, какое его подмножество использует
|
||||||
|
`python_app/hardware_full/librevna_multi_device_driver`. Про переключение
|
||||||
|
стимула внутри частотной точки — отдельный документ,
|
||||||
|
[per_point_switching.md](per_point_switching.md).
|
||||||
|
|
||||||
|
Прошивка в репозитории лежит под
|
||||||
|
`device_firmware/multi_libreVNA_firmware/reproduce/LibreVNA/Software/VNA_embedded/Application/`,
|
||||||
|
дальше он сокращён до `<FW>`. Версия протокола, заявленная прошивкой —
|
||||||
|
`Protocol::Version = 14` (`<FW>/Communication/Protocol.hpp:13`).
|
||||||
|
|
||||||
|
## 1. Транспорт
|
||||||
|
|
||||||
|
Поверх USB у LibreVNA свой бинарный протокол, а не tty/serial. Endpoints:
|
||||||
|
|
||||||
|
| Endpoint | Направление |
|
||||||
|
| --- | --- |
|
||||||
|
| `0x01` | хост -> устройство |
|
||||||
|
| `0x81` | устройство -> хост |
|
||||||
|
| `0x82` | отладочные ASCII-логи |
|
||||||
|
|
||||||
|
Наш драйвер шлёт в `0x01`, читает из `0x81`, `0x82` не использует. Приёмный
|
||||||
|
поток и очередь пакетов — в `librevna_multi_device_driver/transport.py`.
|
||||||
|
|
||||||
|
Очередь принятых пакетов ограничена и работает по принципу drop-oldest:
|
||||||
|
блокировать USB-поток чтения нельзя, а устаревшие пакеты всё равно
|
||||||
|
выбрасываются предсборочным дренажом (`transport.py:109`).
|
||||||
|
|
||||||
|
## 2. Структура кадра
|
||||||
|
|
||||||
|
Одна и та же почти для всех типов пакетов
|
||||||
|
(`<FW>/Communication/PacketConstants.h`):
|
||||||
|
|
||||||
|
| Поле | Длина | Значение |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Header | 1 | всегда `0x5A` |
|
||||||
|
| Length | 2 | длина **всего** кадра, little-endian |
|
||||||
|
| Type | 1 | номер типа пакета |
|
||||||
|
| Payload | переменная | |
|
||||||
|
| CRC32 | 4 | по всему кадру, включая header |
|
||||||
|
|
||||||
|
Итого накладных расходов 8 байт: `длина кадра = 8 + len(payload)`.
|
||||||
|
|
||||||
|
Кодирование и разбор на хосте — в общем модуле одиночного драйвера, а не в
|
||||||
|
multi-device: `python_app/hardware_full/librevna_driver/protocol/frame.py`,
|
||||||
|
функции `encode_frame` (строка 25) и `decode_frame` (строка 49). Поток байт
|
||||||
|
разбирает `FrameScanner`, который умеет ресинхронизироваться после битого кадра,
|
||||||
|
не роняя устройство.
|
||||||
|
|
||||||
|
### CRC: у `VNADatapoint` особый случай
|
||||||
|
|
||||||
|
Прошивка **не считает** CRC для `VNADatapoint` и пишет туда ноль, потому что
|
||||||
|
расчёт занимает около 18 мкс — это основная часть времени кодирования и
|
||||||
|
передачи точки (`<FW>/Communication/Protocol.cpp:149`). На приёме она,
|
||||||
|
наоборот, **требует** ровно нуля: непустой CRC у datapoint считается ошибкой
|
||||||
|
(`Protocol.cpp:69` и `Protocol.cpp:80`).
|
||||||
|
|
||||||
|
Наш хост повторяет это поведение точно: `decode_frame` проверяет CRC32 для всех
|
||||||
|
типов, а для `VNADatapoint` требует ноль (`frame.py:20` — список исключений).
|
||||||
|
|
||||||
|
Дополнительно multi-device парсер отбраковывает пакет, у которого тело не
|
||||||
|
кратно 9 байтам на значение: раз CRC у datapoint нет, это единственная защита
|
||||||
|
от ресинхронизации на `0x5A`, случайно оказавшийся внутри payload
|
||||||
|
(`librevna_multi_device_driver/protocol.py:137`).
|
||||||
|
|
||||||
|
## 3. Типы пакетов
|
||||||
|
|
||||||
|
Полный enum — `<FW>/Communication/Protocol.hpp:573`, там 30+ типов. Наш драйвер
|
||||||
|
использует шесть (`librevna_multi_device_driver/protocol.py:17`):
|
||||||
|
|
||||||
|
| Тип | Номер | Зачем |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `SweepSettings` | 2 | настройка развёртки, она же её запуск |
|
||||||
|
| `Ack` | 7 | подтверждение команды |
|
||||||
|
| `Nack` | 10 | отказ |
|
||||||
|
| `Reference` | 11 | выбор источника опорной частоты |
|
||||||
|
| `SetIdle` | 20 | остановить развёртку, перевести в idle |
|
||||||
|
| `VNADatapoint` | 27 | поток измерений |
|
||||||
|
|
||||||
|
То есть это узкий VNA-sweep-драйвер, а не полная реализация всех режимов
|
||||||
|
прибора. Не используются `RequestDeviceInfo`, проверка версии протокола,
|
||||||
|
`SetTrigger`/`ClearTrigger`, генератор, анализатор спектра, калибровочные
|
||||||
|
пакеты.
|
||||||
|
|
||||||
|
## 4. Пакеты, которые шлём
|
||||||
|
|
||||||
|
### `Reference` (11)
|
||||||
|
|
||||||
|
Структура — `Protocol.hpp:186`: `uint32 ExtRefOuputFreq` плюс битовое поле
|
||||||
|
`AutomaticSwitch` и `UseExternalRef`.
|
||||||
|
|
||||||
|
Шлём `output_frequency_hz = 0` и флаг внешней опоры: выход `Ref Out` не
|
||||||
|
включаем, а устройству принудительно указываем использовать внешние 10 МГц.
|
||||||
|
Строит `build_reference_settings_payload` (`protocol.py:48`), отправляет
|
||||||
|
`controller.py`.
|
||||||
|
|
||||||
|
### `SweepSettings` (2)
|
||||||
|
|
||||||
|
Структура — `Protocol.hpp:155`:
|
||||||
|
|
||||||
|
```c
|
||||||
|
uint64 f_start, f_stop;
|
||||||
|
uint16 points;
|
||||||
|
uint32 if_bandwidth;
|
||||||
|
int16 cdbm_excitation_start;
|
||||||
|
uint8 standby:1, syncMaster:1, suppressPeaks:1, fixedPowerSetting:1,
|
||||||
|
logSweep:1, syncMode:2, unused1:1;
|
||||||
|
uint16 stages:3, port1Stage:3, port2Stage:3, port3Stage:3, port4Stage:3, unused2:1;
|
||||||
|
int16 cdbm_excitation_stop;
|
||||||
|
uint16 dwell_time; // в микросекундах
|
||||||
|
```
|
||||||
|
|
||||||
|
Что кладём (`build_sweep_settings_payload`, `protocol.py:58`):
|
||||||
|
|
||||||
|
- `f_start`/`f_stop`/`points`/`if_bandwidth` — из `SweepConfiguration`;
|
||||||
|
- мощность старта и стопа одинаковая, `fixedPowerSetting = 1` — аттенюатор и
|
||||||
|
мощность PLL не меняются по развёртке;
|
||||||
|
- `suppressPeaks = 1`, `logSweep = 0`, `standby = 0`;
|
||||||
|
- `syncMode = 3` (триггерная синхронизация), если синхронизация включена;
|
||||||
|
- `syncMaster = 1` только у мастера;
|
||||||
|
- `stages` — номер последней стадии, не их количество;
|
||||||
|
- порт-стадии — см. ниже.
|
||||||
|
|
||||||
|
Про `syncMode = 3`: в стоковой прошивке рядом с полем стоит комментарий
|
||||||
|
«Trigger synchronization (not supported yet by hardware)». В нашем форке этот
|
||||||
|
режим и используется — устройство переводит обе триггерные линии FPGA в Hi-Z.
|
||||||
|
|
||||||
|
**Порт-стадии — не то, что было в первой версии драйвера.** Раньше мастер
|
||||||
|
получал `port1Stage = 0`, а слейв `1`. Сейчас:
|
||||||
|
|
||||||
|
- мастеру отправляется **первая стадия блока** порта (`index * substage_count`);
|
||||||
|
без под-стадий это просто индекс порта, то есть стоковая раскладка;
|
||||||
|
- приёмным устройствам обе порт-стадии выставляются в
|
||||||
|
`DISABLED_PORT_STAGE = 7` (`protocol.py:36`) — «порт выключен»;
|
||||||
|
- ширина блока под-стадий передаётся как `log2` в неиспользуемое поле
|
||||||
|
`port3Stage`.
|
||||||
|
|
||||||
|
Почему именно так и почему семёрка безопасна — в
|
||||||
|
[per_point_switching.md](per_point_switching.md).
|
||||||
|
|
||||||
|
### `SetIdle` (20)
|
||||||
|
|
||||||
|
Без payload. Выключает активный режим и останавливает развёртку. Используется
|
||||||
|
как служебная команда: остановить непрерывную развёртку, убраться при ошибке,
|
||||||
|
`close()`.
|
||||||
|
|
||||||
|
### `Ack` (7) / `Nack` (10)
|
||||||
|
|
||||||
|
Ответы устройства на команды хоста. `wait_for_acknowledgement`
|
||||||
|
(`transport.py:96`) читает входящие пакеты, на `Ack` считает команду
|
||||||
|
применённой, на `Nack` бросает исключение, прочие асинхронные пакеты
|
||||||
|
пропускает.
|
||||||
|
|
||||||
|
## 5. `VNADatapoint` (27) — поток измерений
|
||||||
|
|
||||||
|
Шаблон `VNADatapoint<s>` — `Protocol.hpp:25`; у прошивки `s = 32`, то есть до 32
|
||||||
|
значений в точке (три на стадию при восьми стадиях).
|
||||||
|
|
||||||
|
Payload:
|
||||||
|
|
||||||
|
| Поле | Тип | Длина |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `frequency` | uint64 | 8 |
|
||||||
|
| `cdBm` | int16 | 2 |
|
||||||
|
| `pointNum` | uint16 | 2 |
|
||||||
|
| `real_values` | n × float32 | 4n |
|
||||||
|
| `imag_values` | n × float32 | 4n |
|
||||||
|
| `descr_values` | n × uint8 | n |
|
||||||
|
|
||||||
|
Фиксированная часть — 12 байт, дальше по 9 байт на значение, причём **не
|
||||||
|
чередуясь**: сначала все реальные части, потом все мнимые, потом все
|
||||||
|
дескрипторы. Отсюда `n = (len(payload) - 12) / 9`.
|
||||||
|
|
||||||
|
Разбирает `parse_vna_datapoint_payload` (`protocol.py:137`).
|
||||||
|
|
||||||
|
### Байт дескриптора
|
||||||
|
|
||||||
|
Смещения битов — `<FW>/Communication/PacketConstants.h:42`:
|
||||||
|
|
||||||
|
| Биты | Значение |
|
||||||
|
| --- | --- |
|
||||||
|
| 0 | порт 1 |
|
||||||
|
| 1 | порт 2 |
|
||||||
|
| 2 | порт 3 |
|
||||||
|
| 3 | порт 4 |
|
||||||
|
| 4 | опорный приёмник (Reference) |
|
||||||
|
| 5..7 | номер стадии |
|
||||||
|
|
||||||
|
То же отражено в `enum class Source` (`Protocol.hpp:17`): `Port1 = 0x01`,
|
||||||
|
`Port2 = 0x02`, `Port3 = 0x04`, `Port4 = 0x08`, `Reference = 0x10`.
|
||||||
|
Прошивка собирает байт как `stage << 5 | sourceMask`.
|
||||||
|
|
||||||
|
На каждую стадию приходит ровно три значения (`<FW>/VNA.cpp:407`): порт 1, порт
|
||||||
|
2 и опорный приёмник. **Ловушка:** у опорного значения маска не `0x10`, а
|
||||||
|
`Port1 | Port2 | Reference = 0x13` — то есть биты портов у него тоже стоят.
|
||||||
|
Поэтому отбирать сигнальное значение только по биту порта нельзя, надо ещё
|
||||||
|
требовать сброшенный бит 4; именно так и сделано в `find_receiver_value`.
|
||||||
|
|
||||||
|
Ищет значение по этому байту `find_receiver_value` (`protocol.py:174`) —
|
||||||
|
фильтрует по стадии, по признаку опорного приёмника и, если задан, по порту.
|
||||||
|
Например: «стадия 0 + ref» — опорный приёмник мастера; «стадия 0 + порт 1, не
|
||||||
|
ref» — сигнал на первом приёмнике.
|
||||||
|
|
||||||
|
## 6. Как из этого получаются S-параметры
|
||||||
|
|
||||||
|
Устройство S-параметры **не считает** — оно отдаёт только сырые комплексные
|
||||||
|
отсчёты приёмников. Считает хост, в
|
||||||
|
`librevna_multi_device_driver/cycle_collection.py`.
|
||||||
|
|
||||||
|
Нормировка (`calculate_last_cycle_s_parameters`, строка 783): каждый канал
|
||||||
|
делится на опорный приёмник **мастера**, а не на локальный опорный приёмник
|
||||||
|
слейва — потому что излучает именно мастер. Опорные измерения хранятся по
|
||||||
|
номеру выходного канала (`master_reference_measurements_by_output`), так как с
|
||||||
|
переключением стимула выходов больше, чем портов мастера.
|
||||||
|
|
||||||
|
Имена S-параметров: отражение мастера — `master_reflection_s_parameter_name`
|
||||||
|
(строка 760), прямые каналы — `build_forward_s_parameter_names` (строка 765).
|
||||||
|
Нумерация приёмных портов слейвов начинается **после** всех выходов, чтобы
|
||||||
|
`S33`/`S44` отражения не столкнулись с прямыми каналами.
|
||||||
|
|
||||||
|
## 7. Почему поле мощности читается, но не используется
|
||||||
|
|
||||||
|
В парсере стоит:
|
||||||
|
|
||||||
|
```python
|
||||||
|
frequency_hz, _power_level_centidecibels_milliwatt, point_index = struct.unpack_from(
|
||||||
|
"<QhH", payload, 0
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Поле мощности в payload реально существует и стоит между частотой и номером
|
||||||
|
точки, поэтому прочитать его необходимо, чтобы правильно достать `point_index`.
|
||||||
|
Подчёркивание в имени — обычный питоновский признак «прочитали намеренно, но не
|
||||||
|
используем».
|
||||||
|
|
||||||
|
По смыслу оно не нужно: для сбора циклов достаточно частоты, индекса точки и
|
||||||
|
массива отсчётов, а S-параметры считаются как отношение сигнала к опоре.
|
||||||
|
К тому же развёртка идёт с фиксированной мощностью (`cdbm_excitation_start ==
|
||||||
|
cdbm_excitation_stop`, `fixedPowerSetting = 1`), так что поле лишь дублирует
|
||||||
|
уже известное из настройки.
|
||||||
|
|
||||||
|
Понадобилось бы оно при переменной по частоте мощности, при логировании
|
||||||
|
фактической мощности каждой точки или при проверке, что устройство
|
||||||
|
действительно выдаёт ожидаемый профиль.
|
||||||
|
|
||||||
|
## 8. Чего в драйвере сознательно нет
|
||||||
|
|
||||||
|
- запроса `DeviceInfo` и проверки версии протокола устройства;
|
||||||
|
- отладочного endpoint `0x82`;
|
||||||
|
- пакетов калибровки, генератора, анализатора спектра;
|
||||||
|
- `SetTrigger`/`ClearTrigger` — синхронизация настраивается полем `syncMode` в
|
||||||
|
`SweepSettings`, а сама петля триггера аппаратная.
|
||||||
|
|
||||||
|
CRC входящих пакетов, в отличие от первой версии драйвера, **проверяется** —
|
||||||
|
см. раздел 2.
|
||||||
|
|
||||||
|
## 9. Карта модулей хоста
|
||||||
|
|
||||||
|
| Файл | Роль |
|
||||||
|
| --- | --- |
|
||||||
|
| `librevna_driver/protocol/frame.py` | кадр: кодирование, разбор, CRC, ресинхронизация |
|
||||||
|
| `librevna_multi_device_driver/protocol.py` | номера пакетов, сборка payload, парсер datapoint, поиск по дескриптору |
|
||||||
|
| `librevna_multi_device_driver/transport.py` | USB bulk, приёмный поток, очередь, ожидание `Ack` |
|
||||||
|
| `librevna_multi_device_driver/controller.py` | высокоуровневые команды: настроить развёртку, собрать циклы, остановить |
|
||||||
|
| `librevna_multi_device_driver/cycle_collection.py` | сбор точек в полные циклы, нормировка в S-параметры, имена трасс |
|
||||||
|
| `librevna_multi_device_driver/models.py` | `SweepConfiguration`, выходные каналы, ограничения стадий |
|
||||||
Binary file not shown.
-10
@@ -1,10 +0,0 @@
|
|||||||
from .librevna_usb_transport import list_connected_device_serial_numbers
|
|
||||||
from .multi_device_vna_controller import MultiDeviceVnaController
|
|
||||||
from .sweep_measurement_models import SweepConfiguration, SweepMeasurementResult
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"MultiDeviceVnaController",
|
|
||||||
"SweepConfiguration",
|
|
||||||
"SweepMeasurementResult",
|
|
||||||
"list_connected_device_serial_numbers",
|
|
||||||
]
|
|
||||||
-383
@@ -1,383 +0,0 @@
|
|||||||
LibreVNA вообще, что именно делает наш текущий Python-код, какие пакеты мы используем, как они кодируются и парсятся, и почему `_power_level_centidecibels_milliwatt` сейчас не нужен.
|
|
||||||
|
|
||||||
**1. Где вообще начинается протокол**
|
|
||||||
У LibreVNA поверх USB идёт свой бинарный протокол, а не `tty/serial`. Это видно и в документации, и в firmware:
|
|
||||||
- USB endpoints: [Device_protocol_v13.tex](/home/europa/Documents/LibreVNA/Documentation/DeveloperInfo/Device_protocol_v13.tex:176)
|
|
||||||
- актуальный код firmware: [Protocol.hpp](/home/europa/Documents/LibreVNA/Software/VNA_embedded/Application/Communication/Protocol.hpp:573)
|
|
||||||
|
|
||||||
На USB уровне:
|
|
||||||
- `0x01` — host -> device
|
|
||||||
- `0x81` — device -> host
|
|
||||||
- `0x82` — debug ASCII logs
|
|
||||||
|
|
||||||
В нашем коде data transport работает только через:
|
|
||||||
- [librevna_usb_transport.py](/home/europa/Documents/LibreVNA/Python/librevna_multi_device_driver/librevna_usb_transport.py)
|
|
||||||
|
|
||||||
То есть:
|
|
||||||
- мы шлём команды в `0x01`
|
|
||||||
- читаем протокольные пакеты из `0x81`
|
|
||||||
- `0x82` вообще не используем
|
|
||||||
|
|
||||||
**2. Общая структура любого LibreVNA packet**
|
|
||||||
Общий framing один и тот же для почти всех packet types. Это описано в docs и реализовано в firmware:
|
|
||||||
- docs: [Device_protocol_v13.tex](/home/europa/Documents/LibreVNA/Documentation/DeveloperInfo/Device_protocol_v13.tex:208)
|
|
||||||
- firmware decode/encode: [Protocol.cpp](/home/europa/Documents/LibreVNA/Software/VNA_embedded/Application/Communication/Protocol.cpp:5)
|
|
||||||
- field offsets/constants: [PacketConstants.h](/home/europa/Documents/LibreVNA/Software/VNA_embedded/Application/Communication/PacketConstants.h:10)
|
|
||||||
|
|
||||||
Структура такая:
|
|
||||||
1. `Header` — 1 байт, всегда `0x5A`
|
|
||||||
2. `Length` — 2 байта, общая длина всего packet целиком
|
|
||||||
3. `Type` — 1 байт, номер packet type
|
|
||||||
4. `Payload` — переменная длина
|
|
||||||
5. `CRC32` — 4 байта
|
|
||||||
|
|
||||||
В Python это делает:
|
|
||||||
- `encode_protocol_frame(...)` в [librevna_device_protocol.py](/home/europa/Documents/LibreVNA/Python/librevna_multi_device_driver/librevna_device_protocol.py:31)
|
|
||||||
- `extract_complete_protocol_frames(...)` в [librevna_device_protocol.py](/home/europa/Documents/LibreVNA/Python/librevna_multi_device_driver/librevna_device_protocol.py:45)
|
|
||||||
|
|
||||||
`encode_protocol_frame(...)`:
|
|
||||||
- считает `total_frame_length = 8 + len(payload)`
|
|
||||||
- пакует header/length/type
|
|
||||||
- считает CRC32
|
|
||||||
- дописывает CRC в конец
|
|
||||||
|
|
||||||
`extract_complete_protocol_frames(...)`:
|
|
||||||
- получает поток байт, а не “готовый packet”
|
|
||||||
- ищет `0x5A`
|
|
||||||
- читает `length`
|
|
||||||
- если полного frame ещё нет, оставляет хвост в буфере
|
|
||||||
- если frame полный, возвращает `(packet_type, payload)`
|
|
||||||
|
|
||||||
Очень важно:
|
|
||||||
- наш Python сейчас **не проверяет CRC**
|
|
||||||
- firmware проверяет CRC для почти всех пакетов, но для `VNADatapoint` у них особый случай: CRC всегда `0`, чтобы не тратить время на вычисление CRC для каждого datapoint, см. [Protocol.cpp](/home/europa/Documents/LibreVNA/Software/VNA_embedded/Application/Communication/Protocol.cpp:69) и [Protocol.cpp](/home/europa/Documents/LibreVNA/Software/VNA_embedded/Application/Communication/Protocol.cpp:147)
|
|
||||||
|
|
||||||
То есть сейчас Python делает “framing by magic + length”, но не “integrity check”.
|
|
||||||
|
|
||||||
**3. Какие packet types вообще бывают**
|
|
||||||
Полный актуальный enum лежит тут:
|
|
||||||
- [Protocol.hpp](/home/europa/Documents/LibreVNA/Software/VNA_embedded/Application/Communication/Protocol.hpp:573)
|
|
||||||
|
|
||||||
Там есть много типов:
|
|
||||||
- `SweepSettings = 2`
|
|
||||||
- `ManualStatus = 3`
|
|
||||||
- `ManualControl = 4`
|
|
||||||
- `DeviceInfo = 5`
|
|
||||||
- `FirmwarePacket = 6`
|
|
||||||
- `Ack = 7`
|
|
||||||
- `ClearFlash = 8`
|
|
||||||
- `PerformFirmwareUpdate = 9`
|
|
||||||
- `Nack = 10`
|
|
||||||
- `Reference = 11`
|
|
||||||
- `Generator = 12`
|
|
||||||
- `SpectrumAnalyzerSettings = 13`
|
|
||||||
- `SpectrumAnalyzerResult = 14`
|
|
||||||
- `RequestDeviceInfo = 15`
|
|
||||||
- ...
|
|
||||||
- `SetIdle = 20`
|
|
||||||
- `DeviceStatus = 25`
|
|
||||||
- `RequestDeviceStatus = 26`
|
|
||||||
- `VNADatapoint = 27`
|
|
||||||
- `SetTrigger = 28`
|
|
||||||
- `ClearTrigger = 29`
|
|
||||||
- `InitiateSweep = 32`
|
|
||||||
- `PerformAction = 33`
|
|
||||||
|
|
||||||
Но наш текущий драйвер использует только маленький поднабор. Это зашито в:
|
|
||||||
- [librevna_device_protocol.py](/home/europa/Documents/LibreVNA/Python/librevna_multi_device_driver/librevna_device_protocol.py:9)
|
|
||||||
|
|
||||||
Мы используем:
|
|
||||||
- `SweepSettings = 2`
|
|
||||||
- `Ack = 7`
|
|
||||||
- `Nack = 10`
|
|
||||||
- `Reference = 11`
|
|
||||||
- `SetIdle = 20`
|
|
||||||
- `VNADatapoint = 27`
|
|
||||||
|
|
||||||
То есть текущий драйвер — это именно VNA sweep driver, а не “полная реализация всех режимов прибора”.
|
|
||||||
|
|
||||||
**4. Какие именно пакеты использует наш драйвер**
|
|
||||||
Разберу их по смыслу.
|
|
||||||
|
|
||||||
**`ReferenceSettings` / packet type `11`**
|
|
||||||
Структура в firmware:
|
|
||||||
- [Protocol.hpp](/home/europa/Documents/LibreVNA/Software/VNA_embedded/Application/Communication/Protocol.hpp:186)
|
|
||||||
|
|
||||||
Поля:
|
|
||||||
- `uint32 ExtRefOutputFreq`
|
|
||||||
- битовое поле:
|
|
||||||
- `AutomaticSwitch`
|
|
||||||
- `UseExternalRef`
|
|
||||||
|
|
||||||
В Python это строит:
|
|
||||||
- `build_reference_settings_payload(...)` в [librevna_device_protocol.py](/home/europa/Documents/LibreVNA/Python/librevna_multi_device_driver/librevna_device_protocol.py:74)
|
|
||||||
|
|
||||||
Сейчас мы шлём:
|
|
||||||
- `output_frequency_hz = 0`
|
|
||||||
- `force_external_reference = True`
|
|
||||||
|
|
||||||
То есть:
|
|
||||||
- никакой `Ref Out` не включаем
|
|
||||||
- просто принудительно говорим устройству использовать внешний `10 MHz`
|
|
||||||
|
|
||||||
Это применяется в controller:
|
|
||||||
- [multi_device_vna_controller.py](/home/europa/Documents/LibreVNA/Python/librevna_multi_device_driver/multi_device_vna_controller.py:183)
|
|
||||||
|
|
||||||
**`SweepSettings` / packet type `2`**
|
|
||||||
Структура в firmware:
|
|
||||||
- [Protocol.hpp](/home/europa/Documents/LibreVNA/Software/VNA_embedded/Application/Communication/Protocol.hpp:155)
|
|
||||||
|
|
||||||
Поля:
|
|
||||||
- `f_start`
|
|
||||||
- `f_stop`
|
|
||||||
- `points`
|
|
||||||
- `if_bandwidth`
|
|
||||||
- `cdbm_excitation_start`
|
|
||||||
- bitfield config:
|
|
||||||
- `standby`
|
|
||||||
- `syncMaster`
|
|
||||||
- `suppressPeaks`
|
|
||||||
- `fixedPowerSetting`
|
|
||||||
- `logSweep`
|
|
||||||
- `syncMode`
|
|
||||||
- stage mapping bitfield
|
|
||||||
- `cdbm_excitation_stop`
|
|
||||||
- `dwell_time`
|
|
||||||
|
|
||||||
В Python это строит:
|
|
||||||
- `build_sweep_settings_payload(...)` в [librevna_device_protocol.py](/home/europa/Documents/LibreVNA/Python/librevna_multi_device_driver/librevna_device_protocol.py:92)
|
|
||||||
|
|
||||||
Что мы туда кладём:
|
|
||||||
- start/stop/points/ifbw — из `SweepConfiguration`
|
|
||||||
- power start/stop — одинаковые
|
|
||||||
- `fixedPowerSetting = 1`
|
|
||||||
- `suppressPeaks = 1`
|
|
||||||
- `logSweep = 0`
|
|
||||||
- `syncMode = 3`, если есть slaves
|
|
||||||
- `syncMaster = 1` только у master
|
|
||||||
- `port_1_stimulus_stage = 0` для master, `1` для slave
|
|
||||||
|
|
||||||
Смысл:
|
|
||||||
- master возбуждает `Port 1`
|
|
||||||
- slaves только принимают
|
|
||||||
- sync идёт через hardware trigger path
|
|
||||||
|
|
||||||
Этот packet реально запускает VNA setup в firmware:
|
|
||||||
- [App.cpp](/home/europa/Documents/LibreVNA/Software/VNA_embedded/Application/App.cpp:126)
|
|
||||||
|
|
||||||
**`SetIdle` / packet type `20`**
|
|
||||||
Очень простой packet, без payload.
|
|
||||||
В firmware:
|
|
||||||
- [App.cpp](/home/europa/Documents/LibreVNA/Software/VNA_embedded/Application/App.cpp:196)
|
|
||||||
|
|
||||||
Он переводит устройство в idle:
|
|
||||||
- выключает активный режим
|
|
||||||
- останавливает sweep path
|
|
||||||
|
|
||||||
У нас он используется только как service-command:
|
|
||||||
- остановить continuous sweep
|
|
||||||
- cleanup при ошибке
|
|
||||||
- `close()`
|
|
||||||
|
|
||||||
Это делает controller:
|
|
||||||
- [multi_device_vna_controller.py](/home/europa/Documents/LibreVNA/Python/librevna_multi_device_driver/multi_device_vna_controller.py:173)
|
|
||||||
|
|
||||||
**`Ack` / `Nack` / packet types `7` и `10`**
|
|
||||||
Это ответы устройства на команды host.
|
|
||||||
В firmware:
|
|
||||||
- enum: [Protocol.hpp](/home/europa/Documents/LibreVNA/Software/VNA_embedded/Application/Communication/Protocol.hpp:581)
|
|
||||||
- отправка `Ack`/`Nack`: [App.cpp](/home/europa/Documents/LibreVNA/Software/VNA_embedded/Application/App.cpp:129)
|
|
||||||
|
|
||||||
В transport:
|
|
||||||
- `wait_for_acknowledgement(...)` в [librevna_usb_transport.py](/home/europa/Documents/LibreVNA/Python/librevna_multi_device_driver/librevna_usb_transport.py:62)
|
|
||||||
|
|
||||||
Логика:
|
|
||||||
- читаем входящие пакеты
|
|
||||||
- если пришёл `Ack`, команда считается применённой
|
|
||||||
- если пришёл `Nack`, бросаем exception
|
|
||||||
- прочие asynchronous packets просто пропускаем
|
|
||||||
|
|
||||||
**`VNADatapoint` / packet type `27`**
|
|
||||||
Это главный поток измерения.
|
|
||||||
Структура в firmware:
|
|
||||||
- template `VNADatapoint<s>`: [Protocol.hpp](/home/europa/Documents/LibreVNA/Software/VNA_embedded/Application/Communication/Protocol.hpp:25)
|
|
||||||
- декодирование: [Protocol.cpp](/home/europa/Documents/LibreVNA/Software/VNA_embedded/Application/Communication/Protocol.cpp:79)
|
|
||||||
|
|
||||||
Payload такой:
|
|
||||||
1. `frequency` — `uint64`, 8 байт
|
|
||||||
2. `cdBm` — `int16`, 2 байта
|
|
||||||
3. `pointNum` — `uint16`, 2 байта
|
|
||||||
4. `real_values` — `n * float32`
|
|
||||||
5. `imag_values` — `n * float32`
|
|
||||||
6. `descr_values` — `n * uint8`
|
|
||||||
|
|
||||||
То есть фиксированный header payload имеет длину 12 байт:
|
|
||||||
- `8 + 2 + 2`
|
|
||||||
|
|
||||||
А дальше идёт `n` значений, каждое занимает:
|
|
||||||
- `4` байта real
|
|
||||||
- `4` байта imag
|
|
||||||
- `1` байт descriptor
|
|
||||||
- итого `9` байт на одно значение
|
|
||||||
|
|
||||||
В Python это парсится тут:
|
|
||||||
- `parse_vna_datapoint_payload(...)` в [librevna_device_protocol.py](/home/europa/Documents/LibreVNA/Python/librevna_multi_device_driver/librevna_device_protocol.py:134)
|
|
||||||
|
|
||||||
Алгоритм:
|
|
||||||
- читаем `frequency_hz`, `power`, `point_index` через `struct.unpack_from("<QhH", ...)`
|
|
||||||
- считаем `value_count = (len(payload) - 12) // 9`
|
|
||||||
- читаем массив `real_components`
|
|
||||||
- читаем массив `imaginary_components`
|
|
||||||
- читаем массив `description_masks`
|
|
||||||
- собираем `dict[mask] = complex(real, imag)`
|
|
||||||
|
|
||||||
**5. Что такое `description_mask` внутри datapoint**
|
|
||||||
Это очень важный байт. Он говорит:
|
|
||||||
- какой это порт
|
|
||||||
- reference это или нет
|
|
||||||
- к какому stage относится значение
|
|
||||||
|
|
||||||
Точные offsets:
|
|
||||||
- [PacketConstants.h](/home/europa/Documents/LibreVNA/Software/VNA_embedded/Application/Communication/PacketConstants.h:41)
|
|
||||||
|
|
||||||
Биты:
|
|
||||||
- bit 0 = `P1`
|
|
||||||
- bit 1 = `P2`
|
|
||||||
- bit 2 = `P3`
|
|
||||||
- bit 3 = `P4`
|
|
||||||
- bit 4 = `Ref`
|
|
||||||
- bits 5..7 = `stage`
|
|
||||||
|
|
||||||
В firmware это же отражено и через enum `Source`:
|
|
||||||
- [Protocol.hpp](/home/europa/Documents/LibreVNA/Software/VNA_embedded/Application/Communication/Protocol.hpp:17)
|
|
||||||
|
|
||||||
В Python поиск по этому байту делает:
|
|
||||||
- `find_receiver_value(...)` в [librevna_device_protocol.py](/home/europa/Documents/LibreVNA/Python/librevna_multi_device_driver/librevna_device_protocol.py:174)
|
|
||||||
|
|
||||||
Она:
|
|
||||||
- фильтрует по `stage`
|
|
||||||
- фильтрует по `reference/non-reference`
|
|
||||||
- если задан `required_port_number`, фильтрует по порту
|
|
||||||
|
|
||||||
Например:
|
|
||||||
- `stage 0 + ref=True` — это master reference receiver
|
|
||||||
- `stage 0 + port=1 + ref=False` — это сигнал на receiver port 1
|
|
||||||
- `stage 0 + port=2 + ref=False` — это сигнал на receiver port 2
|
|
||||||
|
|
||||||
**6. Как `VNADatapoint` превращается в S-параметры**
|
|
||||||
Устройство само `S11/S21/S31` не считает.
|
|
||||||
Оно шлёт только сырые complex receiver values.
|
|
||||||
|
|
||||||
В нашей новой разбивке это делает модуль:
|
|
||||||
- [running_sweep_cycle_collection.py](/home/europa/Documents/LibreVNA/Python/librevna_multi_device_driver/running_sweep_cycle_collection.py)
|
|
||||||
|
|
||||||
Там:
|
|
||||||
- master datapoint даёт:
|
|
||||||
- master ref
|
|
||||||
- `S11` raw
|
|
||||||
- `S21` raw
|
|
||||||
- каждый slave datapoint даёт:
|
|
||||||
- два своих приёмных канала, которые потом становятся `S31/S41`, `S51/S61`, и так далее
|
|
||||||
|
|
||||||
Нормализация:
|
|
||||||
- `calculate_last_cycle_s_parameters(...)` в [running_sweep_cycle_collection.py](/home/europa/Documents/LibreVNA/Python/librevna_multi_device_driver/running_sweep_cycle_collection.py:226)
|
|
||||||
|
|
||||||
Формула:
|
|
||||||
- все каналы делятся на `master_reference_measurements[-1]`
|
|
||||||
|
|
||||||
Это принципиально важно:
|
|
||||||
- даже `slave` каналы делятся не на локальный slave `Ref`, а на master reference, потому что именно master возбуждает сигнал
|
|
||||||
|
|
||||||
**7. Почему `_power_level_centidecibels_milliwatt` не используется**
|
|
||||||
Это переменная вот здесь:
|
|
||||||
- [librevna_device_protocol.py](/home/europa/Documents/LibreVNA/Python/librevna_multi_device_driver/librevna_device_protocol.py:143)
|
|
||||||
|
|
||||||
Строка:
|
|
||||||
```python
|
|
||||||
frequency_hz, _power_level_centidecibels_milliwatt, point_index = struct.unpack_from(
|
|
||||||
"<QhH",
|
|
||||||
payload,
|
|
||||||
0,
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
Почему она есть:
|
|
||||||
- потому что в payload `VNADatapoint` поле мощности реально существует
|
|
||||||
- порядок полей такой: `frequency`, `cdBm`, `pointNum`
|
|
||||||
- чтобы корректно распаковать `point_index`, нам надо прочитать и среднее поле тоже
|
|
||||||
|
|
||||||
Почему она с `_`:
|
|
||||||
- это стандартный Python-сигнал “поле прочитали, но намеренно не используем”
|
|
||||||
|
|
||||||
Почему мы её не используем по смыслу:
|
|
||||||
1. Для сбора sweep-циклов она не нужна.
|
|
||||||
Нам нужны только:
|
|
||||||
- частота
|
|
||||||
- индекс точки
|
|
||||||
- массив complex receiver values
|
|
||||||
2. Для вычисления `S` она тоже не нужна.
|
|
||||||
`S` считаются как отношение signal/ref.
|
|
||||||
3. В нашем текущем sweep builder мы вообще задаём фиксированную мощность:
|
|
||||||
- `cdbm_excitation_start == cdbm_excitation_stop`
|
|
||||||
- `fixedPowerSetting = 1`
|
|
||||||
см. [librevna_device_protocol.py](/home/europa/Documents/LibreVNA/Python/librevna_multi_device_driver/librevna_device_protocol.py:108)
|
|
||||||
|
|
||||||
То есть в вашем текущем драйвере это поле практически просто дублирует уже известный нам факт о sweep configuration.
|
|
||||||
|
|
||||||
Когда оно могло бы понадобиться:
|
|
||||||
- если бы вы хотели логировать реальную мощность каждой точки
|
|
||||||
- если бы sweep был с изменяющейся мощностью по частоте
|
|
||||||
- если бы нужно было sanity-check’ить, что device реально шлёт ожидаемый power profile
|
|
||||||
- если бы вы строили экспорт “frequency + power + S-parameters”
|
|
||||||
|
|
||||||
Итого:
|
|
||||||
- поле реально есть в протоколе
|
|
||||||
- парсер его читает
|
|
||||||
- но текущему драйверу для работы оно не нужно
|
|
||||||
- поэтому имя с `_` здесь абсолютно осознанное и корректное
|
|
||||||
|
|
||||||
**8. Что именно у нас сейчас реализовано не полностью**
|
|
||||||
Это тоже важно понимать.
|
|
||||||
|
|
||||||
Наш текущий Python:
|
|
||||||
- реализует только subset packet types
|
|
||||||
- не делает `RequestDeviceInfo`
|
|
||||||
- не проверяет `ProtocolVersion`
|
|
||||||
- не проверяет CRC входящих пакетов
|
|
||||||
- не использует debug endpoint `0x82`
|
|
||||||
|
|
||||||
То есть это не “полный client protocol stack”, а достаточно чистый и узкий VNA multi-device driver.
|
|
||||||
|
|
||||||
**9. Краткая карта текущих protocol-функций**
|
|
||||||
В новом пакете роли такие:
|
|
||||||
|
|
||||||
- [librevna_device_protocol.py](/home/europa/Documents/LibreVNA/Python/librevna_multi_device_driver/librevna_device_protocol.py)
|
|
||||||
- описание packet ids
|
|
||||||
- frame encode
|
|
||||||
- frame split
|
|
||||||
- payload builders
|
|
||||||
- datapoint parser
|
|
||||||
- descriptor-mask lookup
|
|
||||||
|
|
||||||
- [librevna_usb_transport.py](/home/europa/Documents/LibreVNA/Python/librevna_multi_device_driver/librevna_usb_transport.py)
|
|
||||||
- raw USB bulk I/O
|
|
||||||
- receive thread
|
|
||||||
- packet queue
|
|
||||||
- waiting for `Ack`
|
|
||||||
|
|
||||||
- [multi_device_vna_controller.py](/home/europa/Documents/LibreVNA/Python/librevna_multi_device_driver/multi_device_vna_controller.py)
|
|
||||||
- high-level commands:
|
|
||||||
- configure continuous sweep
|
|
||||||
- collect cycles
|
|
||||||
- stop/close
|
|
||||||
|
|
||||||
- [running_sweep_cycle_collection.py](/home/europa/Documents/LibreVNA/Python/librevna_multi_device_driver/running_sweep_cycle_collection.py)
|
|
||||||
- datapoint collection threads
|
|
||||||
- alignment to full cycle
|
|
||||||
- raw receiver accumulation
|
|
||||||
- normalization to `S`-parameters
|
|
||||||
|
|
||||||
Если хотите, следующим сообщением я могу сделать ещё более низкоуровневый разбор:
|
|
||||||
- прям показать байтовую раскладку одного `SweepSettings` packet
|
|
||||||
или
|
|
||||||
- показать, как из конкретного `VNADatapoint` вручную достать `S21` и `S41`.
|
|
||||||
-191
@@ -1,191 +0,0 @@
|
|||||||
import binascii
|
|
||||||
import struct
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from .sweep_measurement_models import SweepConfiguration
|
|
||||||
|
|
||||||
|
|
||||||
class PacketType:
|
|
||||||
SWEEP_SETTINGS = 2
|
|
||||||
ACKNOWLEDGE = 7
|
|
||||||
NEGATIVE_ACKNOWLEDGE = 10
|
|
||||||
REFERENCE_SETTINGS = 11
|
|
||||||
SET_IDLE = 20
|
|
||||||
VNA_DATAPOINT = 27
|
|
||||||
|
|
||||||
|
|
||||||
SYNC_MODE_HARDWARE_TRIGGER = 3
|
|
||||||
FRAME_HEADER_MAGIC = 0x5A
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ParsedVnaDatapoint:
|
|
||||||
"""Decoded contents of one VNADatapoint payload."""
|
|
||||||
|
|
||||||
frequency_hz: int
|
|
||||||
point_index: int
|
|
||||||
receiver_values_by_description_mask: dict[int, complex]
|
|
||||||
|
|
||||||
|
|
||||||
def encode_protocol_frame(packet_type: int, payload: bytes = b"") -> bytes:
|
|
||||||
"""Wrap a packet payload in the LibreVNA transport frame."""
|
|
||||||
|
|
||||||
total_frame_length = 8 + len(payload)
|
|
||||||
frame_without_crc = struct.pack(
|
|
||||||
"<BHB",
|
|
||||||
FRAME_HEADER_MAGIC,
|
|
||||||
total_frame_length,
|
|
||||||
packet_type,
|
|
||||||
) + payload
|
|
||||||
crc32 = binascii.crc32(frame_without_crc) & 0xFFFF_FFFF
|
|
||||||
return frame_without_crc + struct.pack("<I", crc32)
|
|
||||||
|
|
||||||
|
|
||||||
def extract_complete_protocol_frames(
|
|
||||||
buffered_bytes: bytes,
|
|
||||||
) -> tuple[list[tuple[int, bytes]], bytes]:
|
|
||||||
"""
|
|
||||||
Extract every complete protocol frame from a byte stream buffer.
|
|
||||||
|
|
||||||
Returns a tuple of:
|
|
||||||
1. a list of `(packet_type, payload)` tuples
|
|
||||||
2. the unconsumed trailing bytes that do not yet form a full frame
|
|
||||||
"""
|
|
||||||
|
|
||||||
extracted_packets: list[tuple[int, bytes]] = []
|
|
||||||
while len(buffered_bytes) >= 4:
|
|
||||||
if buffered_bytes[0] != FRAME_HEADER_MAGIC:
|
|
||||||
buffered_bytes = buffered_bytes[1:]
|
|
||||||
continue
|
|
||||||
|
|
||||||
total_frame_length = struct.unpack_from("<H", buffered_bytes, 1)[0]
|
|
||||||
if len(buffered_bytes) < total_frame_length:
|
|
||||||
break
|
|
||||||
|
|
||||||
packet_type = buffered_bytes[3]
|
|
||||||
payload = buffered_bytes[4 : total_frame_length - 4]
|
|
||||||
extracted_packets.append((packet_type, payload))
|
|
||||||
buffered_bytes = buffered_bytes[total_frame_length:]
|
|
||||||
|
|
||||||
return extracted_packets, buffered_bytes
|
|
||||||
|
|
||||||
|
|
||||||
def build_reference_settings_payload(
|
|
||||||
output_frequency_hz: int,
|
|
||||||
force_external_reference: bool,
|
|
||||||
) -> bytes:
|
|
||||||
"""Build the ReferenceSettings payload."""
|
|
||||||
|
|
||||||
automatic_reference_switch = 0
|
|
||||||
external_input_configuration_bits = (
|
|
||||||
automatic_reference_switch << 0
|
|
||||||
| force_external_reference << 1
|
|
||||||
)
|
|
||||||
return struct.pack(
|
|
||||||
"<IB",
|
|
||||||
output_frequency_hz,
|
|
||||||
external_input_configuration_bits,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def build_sweep_settings_payload(
|
|
||||||
sweep_configuration: SweepConfiguration,
|
|
||||||
is_synchronization_master: bool,
|
|
||||||
synchronization_enabled: bool,
|
|
||||||
) -> bytes:
|
|
||||||
"""Build the current protocol-v14 SweepSettings payload."""
|
|
||||||
|
|
||||||
excitation_power_centidecibels_milliwatt = int(sweep_configuration.power_dbm * 100)
|
|
||||||
dwell_time_microseconds = max(
|
|
||||||
0,
|
|
||||||
min(int(sweep_configuration.dwell_us), 0xFFFF),
|
|
||||||
)
|
|
||||||
synchronization_mode = (
|
|
||||||
SYNC_MODE_HARDWARE_TRIGGER if synchronization_enabled else 0
|
|
||||||
)
|
|
||||||
|
|
||||||
configuration_bits = (
|
|
||||||
0 << 0
|
|
||||||
| is_synchronization_master << 1
|
|
||||||
| 1 << 2
|
|
||||||
| 1 << 3
|
|
||||||
| 0 << 4
|
|
||||||
| synchronization_mode << 5
|
|
||||||
)
|
|
||||||
|
|
||||||
port_1_stimulus_stage = 0 if is_synchronization_master else 1
|
|
||||||
stage_configuration_bits = 0 | port_1_stimulus_stage << 3 | 1 << 6
|
|
||||||
|
|
||||||
return struct.pack(
|
|
||||||
"<QQHIhBHhH",
|
|
||||||
sweep_configuration.start_hz,
|
|
||||||
sweep_configuration.stop_hz,
|
|
||||||
sweep_configuration.points,
|
|
||||||
sweep_configuration.if_bandwidth,
|
|
||||||
excitation_power_centidecibels_milliwatt,
|
|
||||||
configuration_bits,
|
|
||||||
stage_configuration_bits,
|
|
||||||
excitation_power_centidecibels_milliwatt,
|
|
||||||
dwell_time_microseconds,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_vna_datapoint_payload(payload: bytes) -> Optional[ParsedVnaDatapoint]:
|
|
||||||
"""Decode one VNADatapoint payload into a structured Python object."""
|
|
||||||
|
|
||||||
fixed_header_length_bytes = 12
|
|
||||||
per_value_storage_length_bytes = 9
|
|
||||||
|
|
||||||
if len(payload) < fixed_header_length_bytes:
|
|
||||||
return None
|
|
||||||
|
|
||||||
frequency_hz, _power_level_centidecibels_milliwatt, point_index = struct.unpack_from(
|
|
||||||
"<QhH",
|
|
||||||
payload,
|
|
||||||
0,
|
|
||||||
)
|
|
||||||
value_count = (len(payload) - fixed_header_length_bytes) // per_value_storage_length_bytes
|
|
||||||
if value_count == 0:
|
|
||||||
return None
|
|
||||||
|
|
||||||
real_components = struct.unpack_from(f"<{value_count}f", payload, 12)
|
|
||||||
imaginary_components = struct.unpack_from(
|
|
||||||
f"<{value_count}f",
|
|
||||||
payload,
|
|
||||||
12 + 4 * value_count,
|
|
||||||
)
|
|
||||||
description_masks = payload[12 + 8 * value_count : 12 + 9 * value_count]
|
|
||||||
|
|
||||||
receiver_values_by_description_mask = {
|
|
||||||
description_masks[value_index]: complex(
|
|
||||||
real_components[value_index],
|
|
||||||
imaginary_components[value_index],
|
|
||||||
)
|
|
||||||
for value_index in range(value_count)
|
|
||||||
}
|
|
||||||
return ParsedVnaDatapoint(
|
|
||||||
frequency_hz=frequency_hz,
|
|
||||||
point_index=point_index,
|
|
||||||
receiver_values_by_description_mask=receiver_values_by_description_mask,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def find_receiver_value(
|
|
||||||
receiver_values_by_description_mask: dict[int, complex],
|
|
||||||
stage_index: int,
|
|
||||||
is_reference_receiver: bool,
|
|
||||||
required_port_number: int = 0,
|
|
||||||
) -> Optional[complex]:
|
|
||||||
"""Look up one complex receiver value by stage, receiver type, and port number."""
|
|
||||||
|
|
||||||
required_port_bit = (1 << (required_port_number - 1)) if required_port_number else 0
|
|
||||||
for description_mask, complex_value in receiver_values_by_description_mask.items():
|
|
||||||
if (description_mask >> 5) != stage_index:
|
|
||||||
continue
|
|
||||||
if bool(description_mask & 0x10) != is_reference_receiver:
|
|
||||||
continue
|
|
||||||
if required_port_bit and not (description_mask & required_port_bit):
|
|
||||||
continue
|
|
||||||
return complex_value
|
|
||||||
return None
|
|
||||||
-169
@@ -1,169 +0,0 @@
|
|||||||
import logging
|
|
||||||
import queue
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
|
|
||||||
import usb.core
|
|
||||||
import usb.util
|
|
||||||
|
|
||||||
try:
|
|
||||||
import libusb_package
|
|
||||||
except ImportError:
|
|
||||||
libusb_package = None
|
|
||||||
|
|
||||||
from .librevna_device_protocol import (
|
|
||||||
PacketType,
|
|
||||||
encode_protocol_frame,
|
|
||||||
extract_complete_protocol_frames,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
USB_VENDOR_ID = 0x1209
|
|
||||||
USB_PRODUCT_ID = 0x4121
|
|
||||||
USB_DATA_ENDPOINT_OUT = 0x01
|
|
||||||
USB_DATA_ENDPOINT_IN = 0x81
|
|
||||||
|
|
||||||
transport_logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class LibreVnaUsbBackendError(RuntimeError):
|
|
||||||
"""Raised when PyUSB cannot access a libusb backend."""
|
|
||||||
|
|
||||||
|
|
||||||
def _build_missing_backend_message() -> str:
|
|
||||||
return (
|
|
||||||
"PyUSB could not find a libusb backend. "
|
|
||||||
"On Windows, install the Python USB backend with "
|
|
||||||
"`python -m pip install -r Python/requirements.txt` from the repository root "
|
|
||||||
"or `python -m pip install libusb-package` inside the active virtual environment. "
|
|
||||||
"If the LibreVNA GUI already sees the device, the USB driver is likely fine and "
|
|
||||||
"only the Python libusb DLL is missing."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _usb_find(**kwargs):
|
|
||||||
if libusb_package is not None:
|
|
||||||
return libusb_package.find(**kwargs)
|
|
||||||
|
|
||||||
try:
|
|
||||||
return usb.core.find(**kwargs)
|
|
||||||
except usb.core.NoBackendError as exc:
|
|
||||||
raise LibreVnaUsbBackendError(_build_missing_backend_message()) from exc
|
|
||||||
|
|
||||||
|
|
||||||
class LibreVnaUsbBulkConnection:
|
|
||||||
"""USB bulk transport wrapper for one LibreVNA device."""
|
|
||||||
|
|
||||||
_USB_TRANSFER_TIMEOUT_MS = 1_000
|
|
||||||
|
|
||||||
def __init__(self, serial_number: str) -> None:
|
|
||||||
usb_device = _usb_find(
|
|
||||||
idVendor=USB_VENDOR_ID,
|
|
||||||
idProduct=USB_PRODUCT_ID,
|
|
||||||
serial_number=serial_number,
|
|
||||||
)
|
|
||||||
if usb_device is None:
|
|
||||||
raise RuntimeError(f"LibreVNA with serial {serial_number!r} not found on USB")
|
|
||||||
|
|
||||||
try:
|
|
||||||
if usb_device.is_kernel_driver_active(0):
|
|
||||||
usb_device.detach_kernel_driver(0)
|
|
||||||
except (usb.core.USBError, NotImplementedError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
usb_device.set_configuration()
|
|
||||||
|
|
||||||
self.serial_number = serial_number
|
|
||||||
self._usb_device = usb_device
|
|
||||||
self._received_packets: queue.Queue[tuple[int, bytes]] = queue.Queue()
|
|
||||||
self._stop_requested = threading.Event()
|
|
||||||
self._transmit_lock = threading.Lock()
|
|
||||||
self._receive_thread = threading.Thread(
|
|
||||||
target=self._receive_packets_forever,
|
|
||||||
daemon=True,
|
|
||||||
name=f"vna-rx-{serial_number[:8]}",
|
|
||||||
)
|
|
||||||
self._receive_thread.start()
|
|
||||||
|
|
||||||
def close(self) -> None:
|
|
||||||
self._stop_requested.set()
|
|
||||||
self._receive_thread.join(timeout=0.5)
|
|
||||||
usb.util.dispose_resources(self._usb_device)
|
|
||||||
|
|
||||||
def drain_received_packets(self) -> list[tuple[int, bytes]]:
|
|
||||||
"""Remove all already-buffered packets without blocking."""
|
|
||||||
|
|
||||||
drained_packets: list[tuple[int, bytes]] = []
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
drained_packets.append(self._received_packets.get_nowait())
|
|
||||||
except queue.Empty:
|
|
||||||
return drained_packets
|
|
||||||
|
|
||||||
def send_packet(self, packet_type: int, payload: bytes = b"") -> None:
|
|
||||||
with self._transmit_lock:
|
|
||||||
self._usb_device.write(
|
|
||||||
USB_DATA_ENDPOINT_OUT,
|
|
||||||
encode_protocol_frame(packet_type, payload),
|
|
||||||
timeout=self._USB_TRANSFER_TIMEOUT_MS,
|
|
||||||
)
|
|
||||||
|
|
||||||
def receive_packet(self, timeout_seconds: float = 2.0) -> tuple[int, bytes]:
|
|
||||||
"""Wait for the next received packet."""
|
|
||||||
|
|
||||||
return self._received_packets.get(timeout=timeout_seconds)
|
|
||||||
|
|
||||||
def wait_for_acknowledgement(self, timeout_seconds: float = 2.0) -> None:
|
|
||||||
"""Wait for an ACK packet while ignoring unrelated asynchronous packets."""
|
|
||||||
|
|
||||||
end_time_monotonic = time.monotonic() + timeout_seconds
|
|
||||||
while True:
|
|
||||||
remaining_seconds = end_time_monotonic - time.monotonic()
|
|
||||||
if remaining_seconds <= 0:
|
|
||||||
raise TimeoutError(f"ACK not received within {timeout_seconds} s")
|
|
||||||
|
|
||||||
packet_type, _payload = self.receive_packet(timeout_seconds=remaining_seconds)
|
|
||||||
if packet_type == PacketType.ACKNOWLEDGE:
|
|
||||||
return
|
|
||||||
if packet_type == PacketType.NEGATIVE_ACKNOWLEDGE:
|
|
||||||
raise RuntimeError("Device returned NACK")
|
|
||||||
|
|
||||||
def _receive_packets_forever(self) -> None:
|
|
||||||
buffered_bytes = b""
|
|
||||||
while not self._stop_requested.is_set():
|
|
||||||
try:
|
|
||||||
usb_chunk = bytes(
|
|
||||||
self._usb_device.read(
|
|
||||||
USB_DATA_ENDPOINT_IN,
|
|
||||||
4096,
|
|
||||||
timeout=100,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
except usb.core.USBTimeoutError:
|
|
||||||
continue
|
|
||||||
except Exception as exc:
|
|
||||||
if not self._stop_requested.is_set():
|
|
||||||
transport_logger.error(
|
|
||||||
"USB read error on %s: %s",
|
|
||||||
self.serial_number,
|
|
||||||
exc,
|
|
||||||
)
|
|
||||||
break
|
|
||||||
|
|
||||||
buffered_bytes += usb_chunk
|
|
||||||
extracted_packets, buffered_bytes = extract_complete_protocol_frames(
|
|
||||||
buffered_bytes
|
|
||||||
)
|
|
||||||
for packet in extracted_packets:
|
|
||||||
self._received_packets.put(packet)
|
|
||||||
|
|
||||||
|
|
||||||
def list_connected_device_serial_numbers() -> list[str]:
|
|
||||||
"""Return the serial numbers of all connected LibreVNA devices."""
|
|
||||||
|
|
||||||
connected_devices = _usb_find(
|
|
||||||
idVendor=USB_VENDOR_ID,
|
|
||||||
idProduct=USB_PRODUCT_ID,
|
|
||||||
find_all=True,
|
|
||||||
)
|
|
||||||
return [device.serial_number for device in (connected_devices or [])]
|
|
||||||
-223
@@ -1,223 +0,0 @@
|
|||||||
import time
|
|
||||||
from collections.abc import Iterator, Sequence
|
|
||||||
from dataclasses import replace
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from .librevna_device_protocol import (
|
|
||||||
PacketType,
|
|
||||||
build_reference_settings_payload,
|
|
||||||
build_sweep_settings_payload,
|
|
||||||
)
|
|
||||||
from .librevna_usb_transport import (
|
|
||||||
LibreVnaUsbBulkConnection,
|
|
||||||
)
|
|
||||||
from .running_sweep_cycle_collection import collect_complete_running_sweep_cycles
|
|
||||||
from .sweep_measurement_models import SweepConfiguration, SweepMeasurementResult
|
|
||||||
|
|
||||||
|
|
||||||
class MultiDeviceVnaController:
|
|
||||||
"""
|
|
||||||
Multi-device LibreVNA controller for synchronized forward S-parameter sweeps.
|
|
||||||
|
|
||||||
Port mapping:
|
|
||||||
master Port 1 / Port 2 → S11 / S21
|
|
||||||
slave 0 Port 1 / Port 2 → S31 / S41
|
|
||||||
slave 1 Port 1 / Port 2 → S51 / S61
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
master_serial_number: str,
|
|
||||||
slave_serial_numbers: Sequence[str] = (),
|
|
||||||
force_external_reference: bool = True,
|
|
||||||
) -> None:
|
|
||||||
self._master_device = LibreVnaUsbBulkConnection(master_serial_number)
|
|
||||||
self._slave_devices = [
|
|
||||||
LibreVnaUsbBulkConnection(slave_serial_number)
|
|
||||||
for slave_serial_number in slave_serial_numbers
|
|
||||||
]
|
|
||||||
self._all_devices = [self._master_device, *self._slave_devices]
|
|
||||||
self._synchronization_enabled = len(self._slave_devices) > 0
|
|
||||||
self._force_external_reference = force_external_reference
|
|
||||||
self._reference_configuration_applied = False
|
|
||||||
self._last_applied_sweep_configuration: Optional[SweepConfiguration] = None
|
|
||||||
self._sweep_is_running = False
|
|
||||||
self._is_closed = False
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __exit__(self, *_):
|
|
||||||
self.close()
|
|
||||||
|
|
||||||
def close(self) -> None:
|
|
||||||
if self._is_closed:
|
|
||||||
return
|
|
||||||
|
|
||||||
self._is_closed = True
|
|
||||||
self._send_idle_to_all_devices()
|
|
||||||
for device_connection in self._all_devices:
|
|
||||||
device_connection.close()
|
|
||||||
|
|
||||||
def stop_continuous_sweep(self) -> None:
|
|
||||||
"""Stop the currently running sweep without closing the controller."""
|
|
||||||
|
|
||||||
if self._is_closed:
|
|
||||||
return
|
|
||||||
self._send_idle_to_all_devices()
|
|
||||||
|
|
||||||
def configure_continuous_sweep(
|
|
||||||
self,
|
|
||||||
sweep_configuration: SweepConfiguration,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
Apply reference and sweep settings and leave the devices sweeping.
|
|
||||||
|
|
||||||
Reapplying the same sweep configuration is a no-op.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if self._is_closed:
|
|
||||||
raise RuntimeError("Controller is already closed")
|
|
||||||
|
|
||||||
if not self._reference_configuration_applied:
|
|
||||||
self._configure_reference_clocks()
|
|
||||||
|
|
||||||
if (
|
|
||||||
self._sweep_is_running
|
|
||||||
and self._last_applied_sweep_configuration == sweep_configuration
|
|
||||||
):
|
|
||||||
return
|
|
||||||
|
|
||||||
if self._sweep_is_running:
|
|
||||||
self._send_idle_to_all_devices()
|
|
||||||
time.sleep(0.05)
|
|
||||||
|
|
||||||
self._drain_all_received_packets()
|
|
||||||
self._configure_sweep_on_all_devices(sweep_configuration)
|
|
||||||
|
|
||||||
def collect_running_sweep_cycles(
|
|
||||||
self,
|
|
||||||
cycle_count: int = 1,
|
|
||||||
) -> SweepMeasurementResult:
|
|
||||||
"""
|
|
||||||
Collect one or more complete cycles from the already-running sweep.
|
|
||||||
|
|
||||||
Call configure_continuous_sweep() first. The driver aligns itself to the
|
|
||||||
next full cycle in the incoming datapoint stream and then captures
|
|
||||||
exactly `cycle_count` complete cycles.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if self._is_closed:
|
|
||||||
raise RuntimeError("Controller is already closed")
|
|
||||||
if not self._sweep_is_running or self._last_applied_sweep_configuration is None:
|
|
||||||
raise RuntimeError(
|
|
||||||
"No running sweep is configured. Call configure_continuous_sweep() first."
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
return collect_complete_running_sweep_cycles(
|
|
||||||
master_device_connection=self._master_device,
|
|
||||||
slave_device_connections=self._slave_devices,
|
|
||||||
active_sweep_configuration=self._last_applied_sweep_configuration,
|
|
||||||
cycle_count=cycle_count,
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
self._send_idle_to_all_devices()
|
|
||||||
raise
|
|
||||||
|
|
||||||
def iter_running_sweep_cycles(self) -> Iterator[SweepMeasurementResult]:
|
|
||||||
"""Yield complete sweep cycles forever from the already-running sweep."""
|
|
||||||
|
|
||||||
while True:
|
|
||||||
yield self.collect_running_sweep_cycles(1)
|
|
||||||
|
|
||||||
def _send_command_and_wait_for_acknowledgement(
|
|
||||||
self,
|
|
||||||
device_connection: LibreVnaUsbBulkConnection,
|
|
||||||
packet_type: int,
|
|
||||||
payload: bytes = b"",
|
|
||||||
timeout_seconds: float = 3.0,
|
|
||||||
retry_count: int = 1,
|
|
||||||
) -> None:
|
|
||||||
last_error: Optional[Exception] = None
|
|
||||||
for _attempt_index in range(retry_count + 1):
|
|
||||||
device_connection.send_packet(packet_type, payload)
|
|
||||||
try:
|
|
||||||
device_connection.wait_for_acknowledgement(
|
|
||||||
timeout_seconds=timeout_seconds
|
|
||||||
)
|
|
||||||
return
|
|
||||||
except Exception as exc:
|
|
||||||
last_error = exc
|
|
||||||
|
|
||||||
assert last_error is not None
|
|
||||||
raise last_error
|
|
||||||
|
|
||||||
def _try_send_command_without_failing(
|
|
||||||
self,
|
|
||||||
device_connection: LibreVnaUsbBulkConnection,
|
|
||||||
packet_type: int,
|
|
||||||
payload: bytes = b"",
|
|
||||||
timeout_seconds: float = 3.0,
|
|
||||||
retry_count: int = 1,
|
|
||||||
) -> None:
|
|
||||||
try:
|
|
||||||
self._send_command_and_wait_for_acknowledgement(
|
|
||||||
device_connection,
|
|
||||||
packet_type,
|
|
||||||
payload,
|
|
||||||
timeout_seconds=timeout_seconds,
|
|
||||||
retry_count=retry_count,
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _send_idle_to_all_devices(self) -> None:
|
|
||||||
for device_connection in self._all_devices:
|
|
||||||
self._try_send_command_without_failing(
|
|
||||||
device_connection,
|
|
||||||
PacketType.SET_IDLE,
|
|
||||||
timeout_seconds=3.0,
|
|
||||||
retry_count=1,
|
|
||||||
)
|
|
||||||
self._sweep_is_running = False
|
|
||||||
|
|
||||||
def _configure_reference_clocks(self) -> None:
|
|
||||||
for device_connection in self._all_devices:
|
|
||||||
self._send_command_and_wait_for_acknowledgement(
|
|
||||||
device_connection,
|
|
||||||
PacketType.REFERENCE_SETTINGS,
|
|
||||||
build_reference_settings_payload(0, self._force_external_reference),
|
|
||||||
timeout_seconds=3.0,
|
|
||||||
retry_count=1,
|
|
||||||
)
|
|
||||||
|
|
||||||
time.sleep(0.05)
|
|
||||||
self._reference_configuration_applied = True
|
|
||||||
|
|
||||||
def _configure_sweep_on_all_devices(
|
|
||||||
self,
|
|
||||||
sweep_configuration: SweepConfiguration,
|
|
||||||
) -> None:
|
|
||||||
sweep_configuration_commands = [
|
|
||||||
*[(slave_device, False) for slave_device in self._slave_devices],
|
|
||||||
(self._master_device, True),
|
|
||||||
]
|
|
||||||
for device_connection, is_synchronization_master in sweep_configuration_commands:
|
|
||||||
self._send_command_and_wait_for_acknowledgement(
|
|
||||||
device_connection,
|
|
||||||
PacketType.SWEEP_SETTINGS,
|
|
||||||
build_sweep_settings_payload(
|
|
||||||
sweep_configuration,
|
|
||||||
is_synchronization_master=is_synchronization_master,
|
|
||||||
synchronization_enabled=self._synchronization_enabled,
|
|
||||||
),
|
|
||||||
timeout_seconds=3.0,
|
|
||||||
retry_count=1,
|
|
||||||
)
|
|
||||||
self._last_applied_sweep_configuration = replace(sweep_configuration)
|
|
||||||
self._sweep_is_running = True
|
|
||||||
|
|
||||||
def _drain_all_received_packets(self) -> None:
|
|
||||||
for device_connection in self._all_devices:
|
|
||||||
device_connection.drain_received_packets()
|
|
||||||
-284
@@ -1,284 +0,0 @@
|
|||||||
import queue
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
from collections.abc import Callable, Sequence
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
from .librevna_device_protocol import (
|
|
||||||
PacketType,
|
|
||||||
ParsedVnaDatapoint,
|
|
||||||
find_receiver_value,
|
|
||||||
parse_vna_datapoint_payload,
|
|
||||||
)
|
|
||||||
from .librevna_usb_transport import LibreVnaUsbBulkConnection
|
|
||||||
from .sweep_measurement_models import SweepConfiguration, SweepMeasurementResult
|
|
||||||
|
|
||||||
|
|
||||||
def collect_complete_running_sweep_cycles(
|
|
||||||
master_device_connection: LibreVnaUsbBulkConnection,
|
|
||||||
slave_device_connections: Sequence[LibreVnaUsbBulkConnection],
|
|
||||||
active_sweep_configuration: SweepConfiguration,
|
|
||||||
cycle_count: int,
|
|
||||||
) -> SweepMeasurementResult:
|
|
||||||
"""Collect complete cycles from an already-running synchronized sweep."""
|
|
||||||
|
|
||||||
cycle_count = int(cycle_count)
|
|
||||||
if cycle_count < 1:
|
|
||||||
raise ValueError("cycle_count must be >= 1")
|
|
||||||
|
|
||||||
all_device_connections = [master_device_connection, *slave_device_connections]
|
|
||||||
point_count = active_sweep_configuration.points
|
|
||||||
frequencies_hz = np.zeros(point_count, dtype=np.float64)
|
|
||||||
raw_receiver_measurements_by_s_parameter = {
|
|
||||||
s_parameter_name: np.full(
|
|
||||||
(cycle_count, point_count),
|
|
||||||
np.nan + 1j * np.nan,
|
|
||||||
dtype=complex,
|
|
||||||
)
|
|
||||||
for s_parameter_name in build_forward_s_parameter_names(len(slave_device_connections))
|
|
||||||
}
|
|
||||||
master_reference_measurements = np.full(
|
|
||||||
(cycle_count, point_count),
|
|
||||||
np.nan + 1j * np.nan,
|
|
||||||
dtype=complex,
|
|
||||||
)
|
|
||||||
collection_errors: list[Exception] = []
|
|
||||||
datapoint_counts_by_device_serial = {
|
|
||||||
device_connection.serial_number: 0
|
|
||||||
for device_connection in all_device_connections
|
|
||||||
}
|
|
||||||
stop_collection_requested = threading.Event()
|
|
||||||
datapoint_timeout_seconds = max(
|
|
||||||
10.0,
|
|
||||||
active_sweep_configuration.dwell_us / 1_000_000 + 2.0,
|
|
||||||
)
|
|
||||||
|
|
||||||
def collect_datapoints_from_device(
|
|
||||||
device_connection: LibreVnaUsbBulkConnection,
|
|
||||||
handle_datapoint: Callable[[ParsedVnaDatapoint], bool],
|
|
||||||
) -> None:
|
|
||||||
datapoints_received = 0
|
|
||||||
expected_datapoint_count = cycle_count * point_count
|
|
||||||
last_datapoint_timestamp = time.monotonic()
|
|
||||||
|
|
||||||
while datapoints_received < expected_datapoint_count:
|
|
||||||
if stop_collection_requested.is_set():
|
|
||||||
return
|
|
||||||
|
|
||||||
remaining_timeout_seconds = (
|
|
||||||
last_datapoint_timestamp + datapoint_timeout_seconds
|
|
||||||
) - time.monotonic()
|
|
||||||
if remaining_timeout_seconds <= 0:
|
|
||||||
collection_errors.append(
|
|
||||||
TimeoutError(
|
|
||||||
f"No datapoints from {device_connection.serial_number} for "
|
|
||||||
f"{datapoint_timeout_seconds:.1f} s "
|
|
||||||
f"(received {datapoints_received}/{point_count})"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
stop_collection_requested.set()
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
packet_type, payload = device_connection.receive_packet(
|
|
||||||
timeout_seconds=min(1.0, remaining_timeout_seconds)
|
|
||||||
)
|
|
||||||
except (queue.Empty, TimeoutError) as exc:
|
|
||||||
if stop_collection_requested.is_set():
|
|
||||||
return
|
|
||||||
if isinstance(exc, queue.Empty):
|
|
||||||
continue
|
|
||||||
collection_errors.append(exc)
|
|
||||||
stop_collection_requested.set()
|
|
||||||
return
|
|
||||||
|
|
||||||
if packet_type != PacketType.VNA_DATAPOINT:
|
|
||||||
continue
|
|
||||||
|
|
||||||
parsed_datapoint = parse_vna_datapoint_payload(payload)
|
|
||||||
if parsed_datapoint and 0 <= parsed_datapoint.point_index < point_count:
|
|
||||||
last_datapoint_timestamp = time.monotonic()
|
|
||||||
datapoint_was_consumed = handle_datapoint(parsed_datapoint)
|
|
||||||
if datapoint_was_consumed:
|
|
||||||
datapoint_counts_by_device_serial[device_connection.serial_number] += 1
|
|
||||||
datapoints_received += 1
|
|
||||||
|
|
||||||
def build_cycle_tracking_handler(
|
|
||||||
cycle_aware_handler: Callable[[ParsedVnaDatapoint, int], None],
|
|
||||||
) -> Callable[[ParsedVnaDatapoint], bool]:
|
|
||||||
cycle_tracking_state = {
|
|
||||||
"current_cycle_index": 0,
|
|
||||||
"previous_point_index": -1,
|
|
||||||
"has_seen_cycle_start": False,
|
|
||||||
}
|
|
||||||
|
|
||||||
def handle_datapoint(parsed_datapoint: ParsedVnaDatapoint) -> bool:
|
|
||||||
current_point_index = parsed_datapoint.point_index
|
|
||||||
if not cycle_tracking_state["has_seen_cycle_start"]:
|
|
||||||
if current_point_index != 0:
|
|
||||||
cycle_tracking_state["previous_point_index"] = current_point_index
|
|
||||||
return False
|
|
||||||
cycle_tracking_state["has_seen_cycle_start"] = True
|
|
||||||
|
|
||||||
if (
|
|
||||||
cycle_tracking_state["previous_point_index"] >= 0
|
|
||||||
and current_point_index < cycle_tracking_state["previous_point_index"]
|
|
||||||
):
|
|
||||||
cycle_tracking_state["current_cycle_index"] += 1
|
|
||||||
|
|
||||||
cycle_tracking_state["previous_point_index"] = current_point_index
|
|
||||||
current_cycle_index = cycle_tracking_state["current_cycle_index"]
|
|
||||||
if current_cycle_index >= cycle_count:
|
|
||||||
return False
|
|
||||||
|
|
||||||
cycle_aware_handler(parsed_datapoint, current_cycle_index)
|
|
||||||
return True
|
|
||||||
|
|
||||||
return handle_datapoint
|
|
||||||
|
|
||||||
def handle_master_datapoint(
|
|
||||||
parsed_datapoint: ParsedVnaDatapoint,
|
|
||||||
cycle_index: int,
|
|
||||||
) -> None:
|
|
||||||
point_index = parsed_datapoint.point_index
|
|
||||||
reference_receiver_value = find_receiver_value(
|
|
||||||
parsed_datapoint.receiver_values_by_description_mask,
|
|
||||||
stage_index=0,
|
|
||||||
is_reference_receiver=True,
|
|
||||||
)
|
|
||||||
if reference_receiver_value is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
frequencies_hz[point_index] = parsed_datapoint.frequency_hz
|
|
||||||
master_reference_measurements[cycle_index, point_index] = reference_receiver_value
|
|
||||||
|
|
||||||
for receiver_port_number, s_parameter_name in ((1, "S11"), (2, "S21")):
|
|
||||||
port_receiver_value = find_receiver_value(
|
|
||||||
parsed_datapoint.receiver_values_by_description_mask,
|
|
||||||
stage_index=0,
|
|
||||||
is_reference_receiver=False,
|
|
||||||
required_port_number=receiver_port_number,
|
|
||||||
)
|
|
||||||
if port_receiver_value is not None:
|
|
||||||
raw_receiver_measurements_by_s_parameter[s_parameter_name][
|
|
||||||
cycle_index,
|
|
||||||
point_index,
|
|
||||||
] = port_receiver_value
|
|
||||||
|
|
||||||
def build_slave_datapoint_handler(
|
|
||||||
slave_index: int,
|
|
||||||
) -> Callable[[ParsedVnaDatapoint], bool]:
|
|
||||||
first_s_parameter_name = f"S{2 * slave_index + 3}1"
|
|
||||||
second_s_parameter_name = f"S{2 * slave_index + 4}1"
|
|
||||||
|
|
||||||
def handle_slave_datapoint(
|
|
||||||
parsed_datapoint: ParsedVnaDatapoint,
|
|
||||||
cycle_index: int,
|
|
||||||
) -> None:
|
|
||||||
point_index = parsed_datapoint.point_index
|
|
||||||
for receiver_port_number, s_parameter_name in (
|
|
||||||
(1, first_s_parameter_name),
|
|
||||||
(2, second_s_parameter_name),
|
|
||||||
):
|
|
||||||
port_receiver_value = find_receiver_value(
|
|
||||||
parsed_datapoint.receiver_values_by_description_mask,
|
|
||||||
stage_index=0,
|
|
||||||
is_reference_receiver=False,
|
|
||||||
required_port_number=receiver_port_number,
|
|
||||||
)
|
|
||||||
if port_receiver_value is not None:
|
|
||||||
raw_receiver_measurements_by_s_parameter[s_parameter_name][
|
|
||||||
cycle_index,
|
|
||||||
point_index,
|
|
||||||
] = port_receiver_value
|
|
||||||
|
|
||||||
return build_cycle_tracking_handler(handle_slave_datapoint)
|
|
||||||
|
|
||||||
collection_threads = [
|
|
||||||
threading.Thread(
|
|
||||||
target=collect_datapoints_from_device,
|
|
||||||
args=(
|
|
||||||
master_device_connection,
|
|
||||||
build_cycle_tracking_handler(handle_master_datapoint),
|
|
||||||
),
|
|
||||||
daemon=True,
|
|
||||||
name="collect-master",
|
|
||||||
),
|
|
||||||
]
|
|
||||||
for slave_index, slave_device_connection in enumerate(slave_device_connections):
|
|
||||||
collection_threads.append(
|
|
||||||
threading.Thread(
|
|
||||||
target=collect_datapoints_from_device,
|
|
||||||
args=(slave_device_connection, build_slave_datapoint_handler(slave_index)),
|
|
||||||
daemon=True,
|
|
||||||
name=f"collect-slave{slave_index}",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
for collection_thread in collection_threads:
|
|
||||||
collection_thread.start()
|
|
||||||
for collection_thread in collection_threads:
|
|
||||||
collection_thread.join()
|
|
||||||
|
|
||||||
if collection_errors:
|
|
||||||
raise RuntimeError(
|
|
||||||
f"Sweep collection failed: {collection_errors[0]}"
|
|
||||||
) from collection_errors[0]
|
|
||||||
|
|
||||||
if slave_device_connections and min(datapoint_counts_by_device_serial.values(), default=0) == 0:
|
|
||||||
raise RuntimeError(
|
|
||||||
"No datapoints received from at least one device; "
|
|
||||||
"hardware trigger sync did not start. Check Trigger Out/In loop "
|
|
||||||
"and 10 MHz reference wiring."
|
|
||||||
)
|
|
||||||
|
|
||||||
return SweepMeasurementResult(
|
|
||||||
frequencies_hz=frequencies_hz,
|
|
||||||
s_parameters=calculate_last_cycle_s_parameters(
|
|
||||||
raw_receiver_measurements_by_s_parameter,
|
|
||||||
master_reference_measurements,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def calculate_last_cycle_s_parameters(
|
|
||||||
raw_receiver_measurements_by_s_parameter: dict[str, np.ndarray],
|
|
||||||
master_reference_measurements: np.ndarray,
|
|
||||||
) -> dict[str, np.ndarray]:
|
|
||||||
"""Convert raw receiver captures from the final sweep cycle into S-parameters."""
|
|
||||||
|
|
||||||
if np.isnan(master_reference_measurements).any():
|
|
||||||
missing_reference_count = int(np.isnan(master_reference_measurements).sum())
|
|
||||||
raise RuntimeError(
|
|
||||||
f"Master reference missing for {missing_reference_count} datapoints"
|
|
||||||
)
|
|
||||||
|
|
||||||
s_parameters: dict[str, np.ndarray] = {}
|
|
||||||
for s_parameter_name, raw_receiver_measurements in (
|
|
||||||
raw_receiver_measurements_by_s_parameter.items()
|
|
||||||
):
|
|
||||||
if np.isnan(raw_receiver_measurements).any():
|
|
||||||
missing_measurement_count = int(np.isnan(raw_receiver_measurements).sum())
|
|
||||||
raise RuntimeError(
|
|
||||||
f"Measurement {s_parameter_name} missing for "
|
|
||||||
f"{missing_measurement_count} datapoints"
|
|
||||||
)
|
|
||||||
s_parameters[s_parameter_name] = (
|
|
||||||
raw_receiver_measurements[-1] / master_reference_measurements[-1]
|
|
||||||
)
|
|
||||||
|
|
||||||
return s_parameters
|
|
||||||
|
|
||||||
|
|
||||||
def build_forward_s_parameter_names(slave_device_count: int) -> list[str]:
|
|
||||||
"""Return S-parameter names for the configured forward multi-device topology."""
|
|
||||||
|
|
||||||
s_parameter_names = ["S11", "S21"]
|
|
||||||
for slave_index in range(slave_device_count):
|
|
||||||
s_parameter_names += [
|
|
||||||
f"S{2 * slave_index + 3}1",
|
|
||||||
f"S{2 * slave_index + 4}1",
|
|
||||||
]
|
|
||||||
return s_parameter_names
|
|
||||||
-23
@@ -1,23 +0,0 @@
|
|||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class SweepConfiguration:
|
|
||||||
"""User-facing configuration for one VNA sweep."""
|
|
||||||
|
|
||||||
start_hz: int = 100_000
|
|
||||||
stop_hz: int = 3_000_000_000
|
|
||||||
points: int = 201
|
|
||||||
if_bandwidth: int = 1_000
|
|
||||||
power_dbm: float = -10.0
|
|
||||||
dwell_us: int = 0
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class SweepMeasurementResult:
|
|
||||||
"""Measured sweep data from one or more complete sweep cycles."""
|
|
||||||
|
|
||||||
frequencies_hz: np.ndarray
|
|
||||||
s_parameters: dict[str, np.ndarray]
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
pyusb
|
|
||||||
libusb-package; platform_system == "Windows"
|
|
||||||
numpy
|
|
||||||
matplotlib
|
|
||||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 299 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 295 KiB |
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 151 KiB |
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 142 KiB |
@@ -1,336 +0,0 @@
|
|||||||
"""
|
|
||||||
test_sweep.py - run synchronized sweeps continuously and draw the latest cycle live.
|
|
||||||
|
|
||||||
Usage
|
|
||||||
-----
|
|
||||||
List connected devices:
|
|
||||||
python test_sweep.py
|
|
||||||
|
|
||||||
Single device (S11, S21):
|
|
||||||
python test_sweep.py <master_serial>
|
|
||||||
|
|
||||||
Two devices - master + one slave (S11, S21, S31, S41):
|
|
||||||
python test_sweep.py <master_serial> <slave_serial>
|
|
||||||
|
|
||||||
Multi-device runs always use hardware trigger synchronization.
|
|
||||||
Close the plot window or press Ctrl+C to stop the live sweep.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import signal
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
import numpy as np
|
|
||||||
from matplotlib.axes import Axes
|
|
||||||
from matplotlib.figure import Figure
|
|
||||||
from matplotlib.lines import Line2D
|
|
||||||
from matplotlib.text import Text
|
|
||||||
|
|
||||||
from librevna_multi_device_driver import (
|
|
||||||
MultiDeviceVnaController,
|
|
||||||
SweepConfiguration,
|
|
||||||
SweepMeasurementResult,
|
|
||||||
list_connected_device_serial_numbers,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# Set to "internal" to keep each device on its own reference clock.
|
|
||||||
DEFAULT_REFERENCE_SOURCE = "external"
|
|
||||||
DEFAULT_SWEEP_CONFIGURATION = SweepConfiguration(
|
|
||||||
start_hz=100_000_000,
|
|
||||||
stop_hz=6_000_000_000,
|
|
||||||
points=501,
|
|
||||||
if_bandwidth=10000,
|
|
||||||
power_dbm=-10.0,
|
|
||||||
)
|
|
||||||
|
|
||||||
_ACTIVE_MEASUREMENT_CONTROLLER: MultiDeviceVnaController | None = None
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class LiveSweepPlot:
|
|
||||||
figure: Figure
|
|
||||||
status_text: Text
|
|
||||||
magnitude_axis: Axes
|
|
||||||
phase_axes: list[Axes]
|
|
||||||
magnitude_lines: dict[str, Line2D]
|
|
||||||
phase_lines: dict[str, Line2D]
|
|
||||||
|
|
||||||
|
|
||||||
def _force_external_reference_from_default_source() -> bool:
|
|
||||||
if DEFAULT_REFERENCE_SOURCE == "external":
|
|
||||||
return True
|
|
||||||
if DEFAULT_REFERENCE_SOURCE == "internal":
|
|
||||||
return False
|
|
||||||
raise ValueError(
|
|
||||||
"DEFAULT_REFERENCE_SOURCE must be either 'external' or 'internal'"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_device_serial_numbers_from_command_line(
|
|
||||||
command_line_arguments: list[str],
|
|
||||||
) -> list[str]:
|
|
||||||
for command_line_argument in command_line_arguments:
|
|
||||||
if command_line_argument.startswith("--"):
|
|
||||||
raise SystemExit(f"Unknown option: {command_line_argument}")
|
|
||||||
return command_line_arguments
|
|
||||||
|
|
||||||
|
|
||||||
def _print_usage() -> None:
|
|
||||||
try:
|
|
||||||
connected_device_serial_numbers = list_connected_device_serial_numbers()
|
|
||||||
except RuntimeError as exc:
|
|
||||||
print(exc)
|
|
||||||
return
|
|
||||||
|
|
||||||
if not connected_device_serial_numbers:
|
|
||||||
print("No LibreVNA devices found. Check the USB connection and OS USB access.")
|
|
||||||
return
|
|
||||||
|
|
||||||
print("Connected LibreVNA devices:")
|
|
||||||
for connected_device_serial_number in connected_device_serial_numbers:
|
|
||||||
print(f" {connected_device_serial_number}")
|
|
||||||
print(f"\nUsage: {sys.argv[0]} <master_serial> [slave_serial ...]")
|
|
||||||
|
|
||||||
|
|
||||||
def _install_termination_signal_handlers():
|
|
||||||
def handle_stop_signal(received_signal_number, _current_stack_frame) -> None:
|
|
||||||
global _ACTIVE_MEASUREMENT_CONTROLLER
|
|
||||||
if _ACTIVE_MEASUREMENT_CONTROLLER is not None:
|
|
||||||
try:
|
|
||||||
print(
|
|
||||||
f"\nSignal {received_signal_number} received, stopping devices...",
|
|
||||||
flush=True,
|
|
||||||
)
|
|
||||||
_ACTIVE_MEASUREMENT_CONTROLLER.close()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
raise SystemExit(128 + received_signal_number)
|
|
||||||
|
|
||||||
previous_sigint_handler = signal.getsignal(signal.SIGINT)
|
|
||||||
previous_sigterm_handler = signal.getsignal(signal.SIGTERM)
|
|
||||||
signal.signal(signal.SIGINT, handle_stop_signal)
|
|
||||||
signal.signal(signal.SIGTERM, handle_stop_signal)
|
|
||||||
return previous_sigint_handler, previous_sigterm_handler
|
|
||||||
|
|
||||||
|
|
||||||
def _restore_termination_signal_handlers(
|
|
||||||
previous_sigint_handler,
|
|
||||||
previous_sigterm_handler,
|
|
||||||
) -> None:
|
|
||||||
signal.signal(signal.SIGINT, previous_sigint_handler)
|
|
||||||
signal.signal(signal.SIGTERM, previous_sigterm_handler)
|
|
||||||
|
|
||||||
|
|
||||||
def _print_run_header(
|
|
||||||
master_serial_number: str,
|
|
||||||
slave_serial_numbers: list[str],
|
|
||||||
sweep_configuration: SweepConfiguration,
|
|
||||||
) -> None:
|
|
||||||
print(f"Master : {master_serial_number}")
|
|
||||||
for slave_index, slave_serial_number in enumerate(slave_serial_numbers):
|
|
||||||
print(f"Slave {slave_index} : {slave_serial_number}")
|
|
||||||
print(
|
|
||||||
"Sync : hardware trigger"
|
|
||||||
if slave_serial_numbers
|
|
||||||
else "Sync : disabled (single device)"
|
|
||||||
)
|
|
||||||
print(f"Ref : {DEFAULT_REFERENCE_SOURCE} on all devices")
|
|
||||||
print("Sweeps : continuous until Ctrl+C or the plot window is closed")
|
|
||||||
print(
|
|
||||||
f"Sweep : {sweep_configuration.start_hz / 1e6:.3f} MHz - "
|
|
||||||
f"{sweep_configuration.stop_hz / 1e6:.3f} MHz, "
|
|
||||||
f"{sweep_configuration.points} points, "
|
|
||||||
f"IF BW {sweep_configuration.if_bandwidth} Hz"
|
|
||||||
)
|
|
||||||
print("Running live sweep...")
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
global _ACTIVE_MEASUREMENT_CONTROLLER
|
|
||||||
|
|
||||||
device_serial_numbers = _parse_device_serial_numbers_from_command_line(
|
|
||||||
sys.argv[1:]
|
|
||||||
)
|
|
||||||
if not device_serial_numbers:
|
|
||||||
_print_usage()
|
|
||||||
return
|
|
||||||
|
|
||||||
master_serial_number = device_serial_numbers[0]
|
|
||||||
slave_serial_numbers = device_serial_numbers[1:]
|
|
||||||
sweep_configuration = DEFAULT_SWEEP_CONFIGURATION
|
|
||||||
force_external_reference = _force_external_reference_from_default_source()
|
|
||||||
sweep_cycle_durations_seconds: list[float] = []
|
|
||||||
|
|
||||||
_print_run_header(
|
|
||||||
master_serial_number,
|
|
||||||
slave_serial_numbers,
|
|
||||||
sweep_configuration,
|
|
||||||
)
|
|
||||||
previous_sigint_handler, previous_sigterm_handler = (
|
|
||||||
_install_termination_signal_handlers()
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
with MultiDeviceVnaController(
|
|
||||||
master_serial_number,
|
|
||||||
slave_serial_numbers,
|
|
||||||
force_external_reference=force_external_reference,
|
|
||||||
) as measurement_controller:
|
|
||||||
_ACTIVE_MEASUREMENT_CONTROLLER = measurement_controller
|
|
||||||
measurement_controller.configure_continuous_sweep(sweep_configuration)
|
|
||||||
plt.ion()
|
|
||||||
|
|
||||||
live_plot: LiveSweepPlot | None = None
|
|
||||||
sweep_cycle_count = 0
|
|
||||||
print("Waiting for the first complete sweep cycle...")
|
|
||||||
|
|
||||||
while True:
|
|
||||||
if live_plot is not None and not plt.fignum_exists(live_plot.figure.number):
|
|
||||||
print("\nPlot window closed, stopping sweep.", flush=True)
|
|
||||||
break
|
|
||||||
|
|
||||||
cycle_collection_start_time = time.monotonic()
|
|
||||||
sweep_measurement_result = measurement_controller.collect_running_sweep_cycles(
|
|
||||||
1,
|
|
||||||
)
|
|
||||||
cycle_duration_seconds = time.monotonic() - cycle_collection_start_time
|
|
||||||
sweep_cycle_durations_seconds.append(cycle_duration_seconds)
|
|
||||||
sweep_cycle_count += 1
|
|
||||||
|
|
||||||
if (
|
|
||||||
live_plot is not None
|
|
||||||
and not plt.fignum_exists(live_plot.figure.number)
|
|
||||||
):
|
|
||||||
print("\nPlot window closed, stopping sweep.", flush=True)
|
|
||||||
break
|
|
||||||
|
|
||||||
if live_plot is None:
|
|
||||||
live_plot = _create_live_sweep_plot(sweep_measurement_result)
|
|
||||||
|
|
||||||
_update_live_sweep_plot(
|
|
||||||
live_plot,
|
|
||||||
sweep_measurement_result,
|
|
||||||
sweep_cycle_count=sweep_cycle_count,
|
|
||||||
last_cycle_duration_seconds=cycle_duration_seconds,
|
|
||||||
average_cycle_duration_seconds=float(
|
|
||||||
np.mean(sweep_cycle_durations_seconds)
|
|
||||||
),
|
|
||||||
)
|
|
||||||
finally:
|
|
||||||
_ACTIVE_MEASUREMENT_CONTROLLER = None
|
|
||||||
_restore_termination_signal_handlers(
|
|
||||||
previous_sigint_handler,
|
|
||||||
previous_sigterm_handler,
|
|
||||||
)
|
|
||||||
|
|
||||||
if sweep_cycle_durations_seconds:
|
|
||||||
average_sweep_duration_seconds = float(np.mean(sweep_cycle_durations_seconds))
|
|
||||||
minimum_sweep_duration_seconds = float(np.min(sweep_cycle_durations_seconds))
|
|
||||||
maximum_sweep_duration_seconds = float(np.max(sweep_cycle_durations_seconds))
|
|
||||||
print(
|
|
||||||
"Sweep time (steady-state): "
|
|
||||||
f"{average_sweep_duration_seconds:.6f} s avg, "
|
|
||||||
f"{minimum_sweep_duration_seconds:.6f}-"
|
|
||||||
f"{maximum_sweep_duration_seconds:.6f} s "
|
|
||||||
f"over {len(sweep_cycle_durations_seconds)} cycles"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
print("Sweep time (steady-state): not enough repeated cycles to estimate")
|
|
||||||
|
|
||||||
print("Sweep stopped.")
|
|
||||||
|
|
||||||
|
|
||||||
def _create_live_sweep_plot(
|
|
||||||
sweep_measurement_result: SweepMeasurementResult,
|
|
||||||
) -> LiveSweepPlot:
|
|
||||||
frequencies_mhz = sweep_measurement_result.frequencies_hz / 1e6
|
|
||||||
s_parameter_names = sorted(sweep_measurement_result.s_parameters)
|
|
||||||
figure_height_inches = 3.5 + 2.5 * len(s_parameter_names)
|
|
||||||
figure, axes = plt.subplots(
|
|
||||||
1 + len(s_parameter_names),
|
|
||||||
1,
|
|
||||||
figsize=(10, figure_height_inches),
|
|
||||||
sharex=True,
|
|
||||||
)
|
|
||||||
axes = np.atleast_1d(axes)
|
|
||||||
magnitude_axis = axes[0]
|
|
||||||
phase_axes = list(axes[1:])
|
|
||||||
magnitude_lines: dict[str, Line2D] = {}
|
|
||||||
phase_lines: dict[str, Line2D] = {}
|
|
||||||
|
|
||||||
for s_parameter_name in s_parameter_names:
|
|
||||||
magnitude_line, = magnitude_axis.plot(
|
|
||||||
frequencies_mhz,
|
|
||||||
np.zeros_like(frequencies_mhz),
|
|
||||||
label=s_parameter_name,
|
|
||||||
linewidth=0.9,
|
|
||||||
)
|
|
||||||
magnitude_lines[s_parameter_name] = magnitude_line
|
|
||||||
|
|
||||||
magnitude_axis.set_title("Multi-port S-parameters (forward, Port 1 stimulus)")
|
|
||||||
magnitude_axis.set_ylabel("Magnitude (dB)")
|
|
||||||
magnitude_axis.set_ylim(-25, 0)
|
|
||||||
magnitude_axis.grid(True, alpha=0.4)
|
|
||||||
magnitude_axis.legend(loc="lower left")
|
|
||||||
|
|
||||||
for phase_axis, s_parameter_name in zip(phase_axes, s_parameter_names):
|
|
||||||
phase_line, = phase_axis.plot(
|
|
||||||
frequencies_mhz,
|
|
||||||
np.zeros_like(frequencies_mhz),
|
|
||||||
label=s_parameter_name,
|
|
||||||
linewidth=0.9,
|
|
||||||
)
|
|
||||||
phase_lines[s_parameter_name] = phase_line
|
|
||||||
phase_axis.set_ylabel("Phase (deg)")
|
|
||||||
phase_axis.set_title(f"{s_parameter_name} phase")
|
|
||||||
phase_axis.set_ylim(-180, 180)
|
|
||||||
phase_axis.grid(True, alpha=0.4)
|
|
||||||
phase_axis.legend(loc="lower left")
|
|
||||||
|
|
||||||
phase_axes[-1].set_xlabel("Frequency (MHz)")
|
|
||||||
|
|
||||||
status_text = figure.suptitle("")
|
|
||||||
figure.tight_layout(rect=(0, 0, 1, 0.97))
|
|
||||||
if figure.canvas.manager is not None:
|
|
||||||
figure.canvas.manager.set_window_title("LibreVNA Live Sweep")
|
|
||||||
plt.show(block=False)
|
|
||||||
|
|
||||||
return LiveSweepPlot(
|
|
||||||
figure=figure,
|
|
||||||
status_text=status_text,
|
|
||||||
magnitude_axis=magnitude_axis,
|
|
||||||
phase_axes=phase_axes,
|
|
||||||
magnitude_lines=magnitude_lines,
|
|
||||||
phase_lines=phase_lines,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _update_live_sweep_plot(
|
|
||||||
live_plot: LiveSweepPlot,
|
|
||||||
sweep_measurement_result: SweepMeasurementResult,
|
|
||||||
sweep_cycle_count: int,
|
|
||||||
last_cycle_duration_seconds: float,
|
|
||||||
average_cycle_duration_seconds: float,
|
|
||||||
) -> None:
|
|
||||||
for s_parameter_name, complex_trace in sweep_measurement_result.s_parameters.items():
|
|
||||||
magnitude_db = 20 * np.log10(np.abs(complex_trace) + 1e-15)
|
|
||||||
phase_degrees = np.rad2deg(np.angle(complex_trace))
|
|
||||||
live_plot.magnitude_lines[s_parameter_name].set_ydata(magnitude_db)
|
|
||||||
live_plot.phase_lines[s_parameter_name].set_ydata(phase_degrees)
|
|
||||||
|
|
||||||
live_plot.status_text.set_text(
|
|
||||||
"Cycle "
|
|
||||||
f"{sweep_cycle_count} | "
|
|
||||||
f"Last {last_cycle_duration_seconds:.3f} s | "
|
|
||||||
f"Avg {average_cycle_duration_seconds:.3f} s"
|
|
||||||
)
|
|
||||||
live_plot.figure.canvas.draw_idle()
|
|
||||||
live_plot.figure.canvas.flush_events()
|
|
||||||
plt.pause(0.001)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -4,9 +4,13 @@
|
|||||||
Проект лежит целиком — не патч, а полное дерево с уже внесёнными доработками.
|
Проект лежит целиком — не патч, а полное дерево с уже внесёнными доработками.
|
||||||
|
|
||||||
> **Исходники опережают бинарники.** В дерево внесено переключение стимула
|
> **Исходники опережают бинарники.** В дерево внесено переключение стимула
|
||||||
> внутри частотной точки (см. [`../../per_point_switching.md`](../../per_point_switching.md)),
|
> внутри частотной точки (см. [`../../per_point_switching.md`](../../per_point_switching.md)).
|
||||||
> а лежащие рядом `.vnafw`, `extracted/` и `build*/` — это ПРЕДЫДУЩАЯ прошивка,
|
> Битстрим и готовый образ с доработкой уже собраны:
|
||||||
> собранная до этой доработки и сейчас залитая в приборы. Поэтому:
|
> `FPGA/VNA/top_master_per_point_switching.bin` и
|
||||||
|
> `../combined_per_point_switching.vnafw`. **На железе они ещё не проверены.**
|
||||||
|
> А `combined.vnafw`, `extracted/` и `build/` — это ПРЕДЫДУЩАЯ прошивка,
|
||||||
|
> собранная до доработки и сейчас залитая в приборы, то есть путь отката.
|
||||||
|
> Поэтому:
|
||||||
>
|
>
|
||||||
> * `./build.sh` без аргументов по-прежнему даёт побайтовое совпадение — он
|
> * `./build.sh` без аргументов по-прежнему даёт побайтовое совпадение — он
|
||||||
> только переупаковывает готовые `.bin`, не трогая исходники;
|
> только переупаковывает готовые `.bin`, не трогая исходники;
|
||||||
@@ -87,9 +91,18 @@ tail -c +341737 combined.vnafw | head -c 85004 > VNA_embedded.bin # STM32
|
|||||||
|
|
||||||
Аппаратный триггер и external-LO slave — 13 файлов (не 12: в списке не значился
|
Аппаратный триггер и external-LO slave — 13 файлов (не 12: в списке не значился
|
||||||
`PC_Application/LibreVNA-GUI/Traces/trace.cpp`, правка в GUI, к прошивкам
|
`PC_Application/LibreVNA-GUI/Traces/trace.cpp`, правка в GUI, к прошивкам
|
||||||
отношения не имеющая). Переключение стимула внутри точки добавило ещё 5
|
отношения не имеющая). Переключение стимула внутри точки добавило ещё **7
|
||||||
изменённых файлов и один новый. Всё остальное дерево совпадает с апстримом байт
|
изменённых файлов и 2 новых** (`git diff --stat d2338ff HEAD -- reproduce/LibreVNA/`):
|
||||||
в байт.
|
`FPGA/VNA/{Sweep,SPIConfig,top}.vhd`, `FPGA/VNA/VNA.xise`,
|
||||||
|
`Application/Drivers/FPGA/FPGA.{hpp,cpp}`, `Application/VNA.cpp`; новые —
|
||||||
|
`FPGA/VNA/Test_Sweep.vhd` и `FPGA/VNA/top_master_per_point_switching.bin`.
|
||||||
|
|
||||||
|
Ещё из дерева удалён каталог `Python/` — прототип драйвера multi-device, который
|
||||||
|
переехал в `python_app/hardware_full/librevna_multi_device_driver/`. К сборке
|
||||||
|
прошивок он отношения не имел; разбор протокола из него сохранён как
|
||||||
|
[`../../librevna_protocol.md`](../../librevna_protocol.md).
|
||||||
|
|
||||||
|
Всё остальное дерево совпадает с апстримом байт в байт.
|
||||||
|
|
||||||
### STM32 — поддержка аппаратного триггера (это и есть съём без USB)
|
### STM32 — поддержка аппаратного триггера (это и есть съём без USB)
|
||||||
|
|
||||||
@@ -138,8 +151,28 @@ tail -c +341737 combined.vnafw | head -c 85004 > VNA_embedded.bin # STM32
|
|||||||
|
|
||||||
### Переключение стимула внутри частотной точки
|
### Переключение стимула внутри частотной точки
|
||||||
|
|
||||||
Внесено позже остального, бинарников рядом ещё нет (см. блок в начале файла).
|
Внесено позже остального. В репозитории лежит и битстрим
|
||||||
Полное описание — [`../../per_point_switching.md`](../../per_point_switching.md).
|
(`FPGA/VNA/top_master_per_point_switching.bin`), и готовый образ
|
||||||
|
`../combined_per_point_switching.vnafw` — оба с хешами в `SHA256SUMS`.
|
||||||
|
|
||||||
|
Образ собран **локально на Linux тулчейном `arm-none-eabi-gcc 14.2.1`**, а не
|
||||||
|
CubeIDE 1.17.0 / gcc 12.3.1, которым собран текущий рабочий `combined.vnafw`.
|
||||||
|
Логика та же (исходники те же), но байты MCU-секции отличаются: 88 660 против
|
||||||
|
85 044. Пересобрать этим же способом:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd LibreVNA/Software/VNA_embedded && make BUILD_DIR=build_per_point_switching
|
||||||
|
cd .. && python3 AssembleFirmware.py \
|
||||||
|
--bitstream FPGA/VNA/top_master_per_point_switching.bin \
|
||||||
|
--mcu Software/VNA_embedded/build_per_point_switching/VNA_embedded.bin \
|
||||||
|
--output ../combined_per_point_switching.vnafw
|
||||||
|
```
|
||||||
|
|
||||||
|
`BUILD_DIR` обязательно отдельный: в `build/` лежит эталонный
|
||||||
|
`VNA_embedded.bin`, извлечённый из рабочей прошивки, его перезаписывать нельзя.
|
||||||
|
|
||||||
|
Полное описание — [`../../per_point_switching.md`](../../per_point_switching.md),
|
||||||
|
базовый протокол — [`../../librevna_protocol.md`](../../librevna_protocol.md).
|
||||||
|
|
||||||
**`FPGA/VNA/Sweep.vhd`** — порт излучает не в одной стадии, а в БЛОКЕ подряд
|
**`FPGA/VNA/Sweep.vhd`** — порт излучает не в одной стадии, а в БЛОКЕ подряд
|
||||||
идущих: младшие `SUBSTAGE_BITS` бит счётчика стадий стали положением выходного
|
идущих: младшие `SUBSTAGE_BITS` бит счётчика стадий стали положением выходного
|
||||||
@@ -158,6 +191,13 @@ tail -c +341737 combined.vnafw | head -c 85004 > VNA_embedded.bin # STM32
|
|||||||
железа нет порта 3, поле свободно, и его переиспользование не меняет формат
|
железа нет порта 3, поле свободно, и его переиспользование не меняет формат
|
||||||
пакета.
|
пакета.
|
||||||
|
|
||||||
|
**`FPGA/VNA/VNA.xise`** — одна строка: `Starting Placer Cost Table (1-100) Map
|
||||||
|
spartan6` переведена с `1` на `2`. **Не сбрасывать.** Ограничение
|
||||||
|
`MCU_MOSI OFFSET hold` маргинально уже в стоке (запас +0.458 нс), поэтому любое
|
||||||
|
изменение нетлиста может его опрокинуть; с cost table 2 получается Timing Score 0
|
||||||
|
и запас +0.642 нс, то есть больше стокового. Подробности и цифры ресурсов — в
|
||||||
|
[`../../per_point_switching.md`](../../per_point_switching.md).
|
||||||
|
|
||||||
**При `SUBSTAGE_BITS = 0` логика бит в бит совпадает с апстримом** — это
|
**При `SUBSTAGE_BITS = 0` логика бит в бит совпадает с апстримом** — это
|
||||||
проверено симуляцией.
|
проверено симуляцией.
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
# готовые прошивки
|
# готовые прошивки
|
||||||
00431d534c2b2ccb05069399158b7a7f58f70d6955f3885ae322191683fc86d4 *../combined.vnafw
|
00431d534c2b2ccb05069399158b7a7f58f70d6955f3885ae322191683fc86d4 *../combined.vnafw
|
||||||
adf7b7c7bbc0f85af94876eef4ef6c774e434862977f716099df9c71599f14e6 *../combined_external_lo_slave.vnafw
|
adf7b7c7bbc0f85af94876eef4ef6c774e434862977f716099df9c71599f14e6 *../combined_external_lo_slave.vnafw
|
||||||
|
# переключение стимула внутри частотной точки (собрано локально, gcc 14.2.1)
|
||||||
|
e8c2b2c743ad3aed6f89ecb59f5d13c585203c88378b2d5f770c3e0a88463f17 *../combined_per_point_switching.vnafw
|
||||||
# секции, извлечённые из них
|
# секции, извлечённые из них
|
||||||
c0e69bbcadd9fac20de10acbe1875ceba39cc7ce35a7c64ed54f44150560ef75 *extracted/master_fpga.bin
|
c0e69bbcadd9fac20de10acbe1875ceba39cc7ce35a7c64ed54f44150560ef75 *extracted/master_fpga.bin
|
||||||
ba729999be7849187704cf4ccbc4ae4a8877b0860eee1b44225c0585537bb091 *extracted/master_stm32.bin
|
ba729999be7849187704cf4ccbc4ae4a8877b0860eee1b44225c0585537bb091 *extracted/master_stm32.bin
|
||||||
@@ -16,5 +18,5 @@ e184eb92bef1c698849511845fa8d7543287ed1e696c2b461520533112913113 *LibreVNA/FPGA/
|
|||||||
ba729999be7849187704cf4ccbc4ae4a8877b0860eee1b44225c0585537bb091 *LibreVNA/Software/VNA_embedded/build/VNA_embedded.bin
|
ba729999be7849187704cf4ccbc4ae4a8877b0860eee1b44225c0585537bb091 *LibreVNA/Software/VNA_embedded/build/VNA_embedded.bin
|
||||||
56654a0344556d24ecca10f119e0226b8297a2bb89d1aa4217280854e9ffa7bd *LibreVNA/Software/VNA_embedded/build_external_lo_slave/VNA_embedded.bin
|
56654a0344556d24ecca10f119e0226b8297a2bb89d1aa4217280854e9ffa7bd *LibreVNA/Software/VNA_embedded/build_external_lo_slave/VNA_embedded.bin
|
||||||
# комплект
|
# комплект
|
||||||
a3c88be8abbff988650d7fe20f4a93745d46c347c9d0936aa5b43a1ff29dc70b *README.md
|
9e0b675e8c5b31c5ee46e15380c498540ef06f0267f8595821e0d0c470c4ac92 *README.md
|
||||||
be686c2000094acb7703727a3eb9249f7ae8a3517dc7d8630fe97f394aadb849 *build.sh
|
be686c2000094acb7703727a3eb9249f7ae8a3517dc7d8630fe97f394aadb849 *build.sh
|
||||||
|
|||||||
@@ -7,17 +7,26 @@ device_firmware/per_point_switching.md в репозитории.
|
|||||||
|
|
||||||
ЧТО ЗДЕСЬ
|
ЧТО ЗДЕСЬ
|
||||||
---------
|
---------
|
||||||
files\ 7 изменённых файлов дерева LibreVNA
|
make_files.sh собирает files\ и SHA256_new.txt из дерева репозитория
|
||||||
|
files\ 8 файлов дерева LibreVNA (НЕ в git, генерируется)
|
||||||
install.cmd бэкап оригиналов + установка + сборка MCU
|
install.cmd бэкап оригиналов + установка + сборка MCU
|
||||||
build_mcu.cmd только сборка MCU (если правки уже стоят)
|
build_mcu.cmd только сборка MCU (если правки уже стоят)
|
||||||
backup_original\ появится после install.cmd — оригиналы для отката
|
backup_original\ появится после install.cmd — оригиналы для отката
|
||||||
SHA256_new.txt хеши установленных файлов для проверки
|
SHA256_new.txt хеши устанавливаемых файлов для проверки
|
||||||
|
|
||||||
Изменены: FPGA/VNA/{Sweep,SPIConfig,top}.vhd,
|
Изменены: FPGA/VNA/{Sweep,SPIConfig,top}.vhd,
|
||||||
|
FPGA/VNA/VNA.xise <- закреплён Starting Placer Cost Table = 2,
|
||||||
|
без него сборка даёт нарушение тайминга
|
||||||
Software/VNA_embedded/Application/VNA.cpp,
|
Software/VNA_embedded/Application/VNA.cpp,
|
||||||
Software/VNA_embedded/Application/Drivers/FPGA/FPGA.{cpp,hpp}
|
Software/VNA_embedded/Application/Drivers/FPGA/FPGA.{cpp,hpp}
|
||||||
Добавлен: FPGA/VNA/Test_Sweep.vhd (тестбенч, в проект ISE не входит)
|
Добавлен: FPGA/VNA/Test_Sweep.vhd (тестбенч, в проект ISE не входит)
|
||||||
|
|
||||||
|
ПЕРЕД ОТПРАВКОЙ НА WINDOWS запустить на машине с репозиторием:
|
||||||
|
bash install_windows/make_files.sh
|
||||||
|
Каталог files\ не версионируется намеренно: когда он лежал в git, после
|
||||||
|
переделки Sweep.vhd и закрепления cost table комплект молча остался со старыми
|
||||||
|
версиями. Генерация из дерева исключает это.
|
||||||
|
|
||||||
ВАЖНО: ПЕРЕПРОШИВАТЬ НАДО ТОЛЬКО МАСТЕР
|
ВАЖНО: ПЕРЕПРОШИВАТЬ НАДО ТОЛЬКО МАСТЕР
|
||||||
---------------------------------------
|
---------------------------------------
|
||||||
Слейвы остаются как есть. Хост выставляет им обе порт-стадии в 7, а стоковое
|
Слейвы остаются как есть. Хост выставляет им обе порт-стадии в 7, а стоковое
|
||||||
@@ -40,10 +49,14 @@ device_firmware/per_point_switching.md в репозитории.
|
|||||||
/home/ise/shared/LibreVNA/FPGA).
|
/home/ise/shared/LibreVNA/FPGA).
|
||||||
2. В ISE открыть VNA.xise — проект МАСТЕРА. Не VNA_external_lo_slave.xise.
|
2. В ISE открыть VNA.xise — проект МАСТЕРА. Не VNA_external_lo_slave.xise.
|
||||||
3. Process -> Generate Programming File.
|
3. Process -> Generate Programming File.
|
||||||
4. Забрать top.bin СРАЗУ и переименовать в top_master.bin: оба проекта пишут в
|
4. Забрать top.bin СРАЗУ и переименовать в top_master_per_point_switching.bin:
|
||||||
один и тот же файл, и следующая сборка его затрёт. Именно так был потерян
|
оба проекта пишут в один и тот же top.bin, и следующая сборка его затрёт.
|
||||||
исходный мастер-битстрим.
|
Именно так был потерян исходный мастер-битстрим.
|
||||||
5. Положить top_master.bin в ...\reproduce\LibreVNA\FPGA\VNA\
|
5. Положить его в ...\reproduce\LibreVNA\FPGA\VNA\
|
||||||
|
|
||||||
|
ВНИМАНИЕ: имя обязательно новое. Лежащий там top_master.bin — копия
|
||||||
|
НЕВОСПРОИЗВОДИМОГО битстрима текущей рабочей прошивки, перезаписывать его
|
||||||
|
нельзя ни при каких обстоятельствах.
|
||||||
|
|
||||||
Битстрим невоспроизводим побайтово (place & route недетерминирован плюс
|
Битстрим невоспроизводим побайтово (place & route недетерминирован плюс
|
||||||
timestamp внутри), так что новый файл будет отличаться от старого при любом
|
timestamp внутри), так что новый файл будет отличаться от старого при любом
|
||||||
@@ -52,7 +65,7 @@ timestamp внутри), так что новый файл будет отлич
|
|||||||
ШАГ 3. Упаковка .vnafw
|
ШАГ 3. Упаковка .vnafw
|
||||||
----------------------
|
----------------------
|
||||||
cd %USERPROFILE%\Documents\radar_system\device_firmware\reproduce\LibreVNA
|
cd %USERPROFILE%\Documents\radar_system\device_firmware\reproduce\LibreVNA
|
||||||
python AssembleFirmware.py --bitstream FPGA\VNA\top_master.bin ^
|
python AssembleFirmware.py --bitstream FPGA\VNA\top_master_per_point_switching.bin ^
|
||||||
--mcu Software\VNA_embedded\build\VNA_embedded.bin ^
|
--mcu Software\VNA_embedded\build\VNA_embedded.bin ^
|
||||||
--output ..\combined_per_point_switching.vnafw
|
--output ..\combined_per_point_switching.vnafw
|
||||||
|
|
||||||
@@ -67,7 +80,8 @@ LibreVNA-GUI: Device -> Firmware Update, скормить .vnafw целиком.
|
|||||||
---------------
|
---------------
|
||||||
1. Сначала СТАРЫЙ режим (stimulus_switch_positions: 1) — должен работать ровно
|
1. Сначала СТАРЫЙ режим (stimulus_switch_positions: 1) — должен работать ровно
|
||||||
как раньше. Только потом включать переключение.
|
как раньше. Только потом включать переключение.
|
||||||
2. Осциллографом на контроллере ключа: PB2 — положение ключа, PB5 — импульс на
|
2. Осциллографом на контроллере ключа: PB2 и PB6 — положение ключей (порт 1 и
|
||||||
|
порт 2, меняются одновременно), PB5 — импульс на
|
||||||
каждое исправление чётности (в норме молчит). На одной частоте должно быть
|
каждое исправление чётности (в норме молчит). На одной частоте должно быть
|
||||||
четыре стадии и два периода A B A B.
|
четыре стадии и два периода A B A B.
|
||||||
3. По USB CDC контроллера, остановив развёртку: parity corrections : 0 и
|
3. По USB CDC контроллера, остановив развёртку: parity corrections : 0 и
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
3cf838aeda126e38397eb97ab10519cc50c725f8c05090890f05837d759c63f2 ./FPGA/VNA/Sweep.vhd
|
||||||
7b59127f46dc16d3a22de4cff02a8499b51465ba29bd5f0be491149022828a8a ./FPGA/VNA/SPIConfig.vhd
|
7b59127f46dc16d3a22de4cff02a8499b51465ba29bd5f0be491149022828a8a ./FPGA/VNA/SPIConfig.vhd
|
||||||
624a5324ae13fd87285e59edea301ffb5e7eab8f2fed99418759fca52401b3b0 ./FPGA/VNA/Sweep.vhd
|
|
||||||
02cc0cf7fe2e253a253843578ab6195f1300702afb00c4ca43b3dbc8df118183 ./FPGA/VNA/Test_Sweep.vhd
|
|
||||||
e70da2b4216862a44b12e4cbde951442ccf1a4b919103fcdd4010613b43cfe46 ./FPGA/VNA/top.vhd
|
e70da2b4216862a44b12e4cbde951442ccf1a4b919103fcdd4010613b43cfe46 ./FPGA/VNA/top.vhd
|
||||||
|
a7cc99e473ab2ec704c55405e3f1cbcd75315fe14f65ee1dfc7b8dce11eb4ca4 ./FPGA/VNA/VNA.xise
|
||||||
|
5f8b7eeeb495b6c74e9b5137a6e8bd7dc0200fd084a7f0eaf67f2ee0a7c6b5a3 ./FPGA/VNA/Test_Sweep.vhd
|
||||||
|
fb116ff5aaf4161052e77837ac78a6824833e87372a6ce5dd142e0240fc17379 ./Software/VNA_embedded/Application/VNA.cpp
|
||||||
e12a9be4eee75d8b39b4b21fa7788f310d87ec22ae068ae7e82de9df91555db4 ./Software/VNA_embedded/Application/Drivers/FPGA/FPGA.cpp
|
e12a9be4eee75d8b39b4b21fa7788f310d87ec22ae068ae7e82de9df91555db4 ./Software/VNA_embedded/Application/Drivers/FPGA/FPGA.cpp
|
||||||
db3ad784c1b1e912c7986f98616585b4fe5d2e0c995d9c66268f756ec4d176ad ./Software/VNA_embedded/Application/Drivers/FPGA/FPGA.hpp
|
db3ad784c1b1e912c7986f98616585b4fe5d2e0c995d9c66268f756ec4d176ad ./Software/VNA_embedded/Application/Drivers/FPGA/FPGA.hpp
|
||||||
fb116ff5aaf4161052e77837ac78a6824833e87372a6ce5dd142e0240fc17379 ./Software/VNA_embedded/Application/VNA.cpp
|
|
||||||
|
|||||||
@@ -23,14 +23,22 @@ if not exist "%APPDIR%" mkdir "%APPDIR%"
|
|||||||
copy /y "%ROOT%\FPGA\VNA\Sweep.vhd" "%FPGADIR%\" >nul || exit /b 1
|
copy /y "%ROOT%\FPGA\VNA\Sweep.vhd" "%FPGADIR%\" >nul || exit /b 1
|
||||||
copy /y "%ROOT%\FPGA\VNA\SPIConfig.vhd" "%FPGADIR%\" >nul || exit /b 1
|
copy /y "%ROOT%\FPGA\VNA\SPIConfig.vhd" "%FPGADIR%\" >nul || exit /b 1
|
||||||
copy /y "%ROOT%\FPGA\VNA\top.vhd" "%FPGADIR%\" >nul || exit /b 1
|
copy /y "%ROOT%\FPGA\VNA\top.vhd" "%FPGADIR%\" >nul || exit /b 1
|
||||||
|
rem VNA.xise тоже правится: в нём закреплён Starting Placer Cost Table = 2,
|
||||||
|
rem без которого сборка даёт нарушение тайминга по MCU_MOSI hold.
|
||||||
|
copy /y "%ROOT%\FPGA\VNA\VNA.xise" "%FPGADIR%\" >nul || exit /b 1
|
||||||
copy /y "%ROOT%\Software\VNA_embedded\Application\Drivers\FPGA\FPGA.cpp" "%DRVDIR%\" >nul || exit /b 1
|
copy /y "%ROOT%\Software\VNA_embedded\Application\Drivers\FPGA\FPGA.cpp" "%DRVDIR%\" >nul || exit /b 1
|
||||||
copy /y "%ROOT%\Software\VNA_embedded\Application\Drivers\FPGA\FPGA.hpp" "%DRVDIR%\" >nul || exit /b 1
|
copy /y "%ROOT%\Software\VNA_embedded\Application\Drivers\FPGA\FPGA.hpp" "%DRVDIR%\" >nul || exit /b 1
|
||||||
copy /y "%ROOT%\Software\VNA_embedded\Application\VNA.cpp" "%APPDIR%\" >nul || exit /b 1
|
copy /y "%ROOT%\Software\VNA_embedded\Application\VNA.cpp" "%APPDIR%\" >nul || exit /b 1
|
||||||
echo 6 файлов сохранено в backup_original\
|
echo 7 файлов сохранено в backup_original\
|
||||||
|
|
||||||
echo === Установка новых файлов ===
|
echo === Установка новых файлов ===
|
||||||
|
if not exist "%STAGE%" (
|
||||||
|
echo ОШИБКА: нет каталога files\ — соберите его из дерева репозитория:
|
||||||
|
echo bash install_windows/make_files.sh
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
xcopy /s /y "%STAGE%\*" "%ROOT%\" >nul || exit /b 1
|
xcopy /s /y "%STAGE%\*" "%ROOT%\" >nul || exit /b 1
|
||||||
echo 7 файлов установлено (6 изменённых + Test_Sweep.vhd)
|
echo 8 файлов установлено (7 изменённых + Test_Sweep.vhd)
|
||||||
|
|
||||||
echo === Сборка прошивки MCU мастера ===
|
echo === Сборка прошивки MCU мастера ===
|
||||||
call "%~dp0build_mcu.cmd"
|
call "%~dp0build_mcu.cmd"
|
||||||
|
|||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Собирает payload install_windows/files/ из дерева LibreVNA этого репозитория
|
||||||
|
# и переписывает SHA256_new.txt.
|
||||||
|
#
|
||||||
|
# Зачем скрипт, а не файлы в git: раньше files/ и SHA256_new.txt заполнялись
|
||||||
|
# руками, и после переделки Sweep.vhd (маска вместо сдвига) и закрепления cost
|
||||||
|
# table в VNA.xise комплект молча остался со СТАРЫМИ версиями. Установка таким
|
||||||
|
# комплектом вернула бы битстрим с нарушением тайминга. Генерация из дерева
|
||||||
|
# делает этот класс расхождений невозможным.
|
||||||
|
#
|
||||||
|
# files/ в .gitignore: это копия того, что и так лежит в дереве.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
KIT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
TREE="$(cd "$KIT_DIR/../LibreVNA" && pwd)"
|
||||||
|
STAGE="$KIT_DIR/files"
|
||||||
|
|
||||||
|
# Пути относительно корня дерева LibreVNA. Порядок — как в README.txt.
|
||||||
|
FILES=(
|
||||||
|
FPGA/VNA/Sweep.vhd
|
||||||
|
FPGA/VNA/SPIConfig.vhd
|
||||||
|
FPGA/VNA/top.vhd
|
||||||
|
FPGA/VNA/VNA.xise
|
||||||
|
FPGA/VNA/Test_Sweep.vhd
|
||||||
|
Software/VNA_embedded/Application/VNA.cpp
|
||||||
|
Software/VNA_embedded/Application/Drivers/FPGA/FPGA.cpp
|
||||||
|
Software/VNA_embedded/Application/Drivers/FPGA/FPGA.hpp
|
||||||
|
)
|
||||||
|
|
||||||
|
rm -rf "$STAGE"
|
||||||
|
for relative_path in "${FILES[@]}"; do
|
||||||
|
source_file="$TREE/$relative_path"
|
||||||
|
[[ -f "$source_file" ]] || { echo "нет файла: $source_file" >&2; exit 1; }
|
||||||
|
mkdir -p "$STAGE/$(dirname "$relative_path")"
|
||||||
|
cp "$source_file" "$STAGE/$relative_path"
|
||||||
|
done
|
||||||
|
|
||||||
|
( cd "$STAGE" && sha256sum $(printf './%s\n' "${FILES[@]}") ) > "$KIT_DIR/SHA256_new.txt"
|
||||||
|
|
||||||
|
echo "files/: ${#FILES[@]} файл(ов), SHA256_new.txt обновлён"
|
||||||
|
echo "Проверка на Windows: certutil -hashfile <файл> SHA256"
|
||||||
@@ -12,6 +12,10 @@
|
|||||||
когерентности: все 16 комбинаций снимаются в пределах ~0.4 мс на одной частоте
|
когерентности: все 16 комбинаций снимаются в пределах ~0.4 мс на одной частоте
|
||||||
вместо разброса в сотни миллисекунд между развёртками.
|
вместо разброса в сотни миллисекунд между развёртками.
|
||||||
|
|
||||||
|
Базовое устройство протокола, поверх которого всё это работает (кадр, типы
|
||||||
|
пакетов, `SweepSettings`, разбор `VNADatapoint`) — в
|
||||||
|
[librevna_protocol.md](librevna_protocol.md).
|
||||||
|
|
||||||
## Раскладка стадий
|
## Раскладка стадий
|
||||||
|
|
||||||
| Стадия | Порт мастера | Ключ | Выход |
|
| Стадия | Порт мастера | Ключ | Выход |
|
||||||
@@ -33,8 +37,10 @@
|
|||||||
|
|
||||||
## Контракт по проводу
|
## Контракт по проводу
|
||||||
|
|
||||||
Хост уже отправляет это (см. `build_sweep_settings_payload`), прошивка должна
|
Хост отправляет это в `build_sweep_settings_payload`, прошивка читает — обе
|
||||||
считать. Слово стадий в `SweepSettings` (16 бит, смещение 25 в payload):
|
стороны реализованы. Слово стадий в `SweepSettings` (16 бит, смещение 25 в
|
||||||
|
payload из 31 байта; раскладку payload целиком см.
|
||||||
|
[librevna_protocol.md](librevna_protocol.md)):
|
||||||
|
|
||||||
| Биты | Поле | Значение в этом режиме |
|
| Биты | Поле | Значение в этом режиме |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
@@ -155,6 +161,70 @@ ghdl -a --std=93 Sweep.vhd Test_Sweep.vhd && ghdl -r --std=93 Test_Sweep
|
|||||||
|
|
||||||
VHDL держите в чистом ASCII: GHDL отвергает не-ASCII символы даже в комментариях.
|
VHDL держите в чистом ASCII: GHDL отвергает не-ASCII символы даже в комментариях.
|
||||||
|
|
||||||
|
### Проверено на железе 2026-09-11
|
||||||
|
|
||||||
|
Мастер прошит `combined_per_point_switching.vnafw`, контроллер врезан в петлю,
|
||||||
|
ВЧ-ключ **не** подключён. Скрипт
|
||||||
|
[`per_point_switching_check.py`](../python_app/scripts/per_point_switching_check.py)
|
||||||
|
снял 8 коллекций по 16 трасс:
|
||||||
|
|
||||||
|
```text
|
||||||
|
выход 0: -71.5 дБ выходы 0 и 1: расхождение 0.5 дБ
|
||||||
|
выход 1: -72.0 дБ выходы 2 и 3: расхождение 1.3 дБ
|
||||||
|
выход 2: -70.6 дБ
|
||||||
|
выход 3: -71.9 дБ
|
||||||
|
```
|
||||||
|
|
||||||
|
Что именно этим доказано:
|
||||||
|
|
||||||
|
- **Битстрим в приборе доработанный.** Со стоковым сравнением «стадия равна
|
||||||
|
порт-стадии» излучали бы только стадии 0 и 2, а выходы 1 и 3 провалились бы в
|
||||||
|
шум. Здесь все четыре на одном уровне.
|
||||||
|
- **Петля триггера с контроллером замкнута.** При `syncMode = 3` мастер семплирует
|
||||||
|
по вернувшемуся триггеру, так что без исправного зеркалирования в STM32 данных
|
||||||
|
не было бы вовсе.
|
||||||
|
- **Выходы попарно совпадают**, как и должно быть при отключённом ключе: обе его
|
||||||
|
позиции — физически один путь.
|
||||||
|
|
||||||
|
Считать фронты для этой проверки **бессмысленно**: поле `stages` стоковое, сток
|
||||||
|
тоже отработает четыре стадии на точку, просто без стимула в 1 и 3.
|
||||||
|
|
||||||
|
Межстадийная пауза, измеренная контроллером, — **62 мкс**, ровно `dwell_us = 2`
|
||||||
|
плюс 60 мкс собственного успокоения PLL прибора. Но распознавание границ точек по
|
||||||
|
длине паузы работает плохо, коэффициент 1.5 оказался мал — подробности и цифры в
|
||||||
|
[README контроллера](switch_controller/README.md). На переключение это не влияет.
|
||||||
|
|
||||||
|
### Проверено с подключённым ВЧ-ключом, 30–200 МГц
|
||||||
|
|
||||||
|
Профиль `run_config_examples/run_config_librevna_multi_per_point_lowband.example.json`:
|
||||||
|
30–200 МГц, IFBW 50 кГц, 201 точка. Диапазон выбран выше
|
||||||
|
`HW::BandSwitchFrequency = 25 МГц` — ниже неё прибор уходит на путь Si5351 через
|
||||||
|
I2C, останавливает развёртку и даёт нерегулярные длинные паузы. Верхняя граница
|
||||||
|
200 МГц лежит в полосе осциллографа, поэтому стимул видно напрямую.
|
||||||
|
|
||||||
|
Медиана `|S21|` по трём повторам, дБ:
|
||||||
|
|
||||||
|
| вход | выход 0 (A) | выход 1 (B) | выход 2 (A) | выход 3 (B) |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 0 | **−5.4** | −77.1 | −69.8 | −78.0 |
|
||||||
|
| 1 | −69.2 | −68.9 | −69.0 | −69.1 |
|
||||||
|
| 2 | −77.1 | −77.7 | −76.4 | −77.9 |
|
||||||
|
| 3 | −73.6 | −73.8 | −72.8 | −73.5 |
|
||||||
|
|
||||||
|
Подключён был один тракт — вход 0 в выход 0. Разница между двумя положениями
|
||||||
|
одного и того же порта мастера (выходы 0 и 1) составила **72 дБ**: в положении A
|
||||||
|
сигнал идёт в подключённый кабель, в положении B — в открытый порт. Это и есть
|
||||||
|
прямое доказательство, что ключ переключается по стадиям. Повторяемость
|
||||||
|
0.0–1.8 дБ, на сигнальном тракте 0.0 дБ.
|
||||||
|
|
||||||
|
**Ограничение по IFBW.** При `if_bandwidth_hz = 1000` прибор воспроизводимо
|
||||||
|
падает: `LIBUSB_ERROR_IO`, затем устройства отваливаются с шины
|
||||||
|
(`LIBUSB_ERROR_NO_DEVICE`). Ограничения по числу отсчётов тут нет — 800 отсчётов
|
||||||
|
против предела 8191, — так что причина, вероятно, в возросшей скважности
|
||||||
|
источника (при 1 кГц он включён 94% времени против 24% при 50 кГц) и, значит, в
|
||||||
|
среднем токе. IFBW 5, 10, 20 и 50 кГц собираются нормально. **Штатным считать
|
||||||
|
50 кГц**, ниже 5 кГц не опускаться без проверки питания приборов.
|
||||||
|
|
||||||
## Прошивка STM32 LibreVNA
|
## Прошивка STM32 LibreVNA
|
||||||
|
|
||||||
Сделано, собирается. Для самих стадий менять было нечего: `stageCnt` уже считается
|
Сделано, собирается. Для самих стадий менять было нечего: `stageCnt` уже считается
|
||||||
@@ -198,9 +268,10 @@ STM32 прибора в Hi-Z на обеих линиях (`GPIO_MODE_INPUT`, `N
|
|||||||
границе точки. Спад означает «оцифровка закончилась», дальше идёт окно
|
границе точки. Спад означает «оцифровка закончилась», дальше идёт окно
|
||||||
успокоения следующей стадии (~62 мкс при `dwell_us = 2`), которого ключу
|
успокоения следующей стадии (~62 мкс при `dwell_us = 2`), которого ключу
|
||||||
хватает с огромным запасом.
|
хватает с огромным запасом.
|
||||||
3. **Одна нога на оба ключа.** Нужное положение зависит только от номера стадии,
|
3. **По ноге на каждый ключ, но ходят они синхронно.** Нужное положение зависит
|
||||||
а не от того, какой порт излучает, поэтому ключи на порту 1 и порту 2 ходят
|
только от номера стадии, а не от того, какой порт излучает. PB2 управляет
|
||||||
синхронно.
|
ключом выходного порта 1, PB6 — порта 2; оба вывода на порту B, поэтому
|
||||||
|
переключаются одной записью в `BSRR`, то есть строго одновременно.
|
||||||
|
|
||||||
Страховка от сбоя чётности: один потерянный фронт инвертирует чередование
|
Страховка от сбоя чётности: один потерянный фронт инвертирует чередование
|
||||||
навсегда и молча меняет антенны местами. Контроллер распознаёт границу точки по
|
навсегда и молча меняет антенны местами. Контроллер распознаёт границу точки по
|
||||||
@@ -273,11 +344,17 @@ make BINPATH="<путь к arm-none-eabi из CubeIDE>/tools/bin" -j4
|
|||||||
|
|
||||||
### 3. Упаковка .vnafw
|
### 3. Упаковка .vnafw
|
||||||
|
|
||||||
|
Готовый образ уже лежит в репозитории:
|
||||||
|
`multi_libreVNA_firmware/combined_per_point_switching.vnafw`, хеш в
|
||||||
|
`reproduce/SHA256SUMS`. Пересобрать:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd multi_libreVNA_firmware/reproduce/LibreVNA
|
cd multi_libreVNA_firmware/reproduce/LibreVNA/Software/VNA_embedded
|
||||||
|
make BUILD_DIR=build_per_point_switching # НЕ в build/: там эталон
|
||||||
|
cd ../..
|
||||||
python3 AssembleFirmware.py \
|
python3 AssembleFirmware.py \
|
||||||
--bitstream FPGA/VNA/top_master_per_point_switching.bin \
|
--bitstream FPGA/VNA/top_master_per_point_switching.bin \
|
||||||
--mcu Software/VNA_embedded/build/VNA_embedded.bin \
|
--mcu Software/VNA_embedded/build_per_point_switching/VNA_embedded.bin \
|
||||||
--output ../combined_per_point_switching.vnafw
|
--output ../combined_per_point_switching.vnafw
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -300,8 +377,9 @@ GUI сам разложит битстрим и прошивку MCU.
|
|||||||
(`stimulus_switch_positions: 1`) работает как раньше — включать переключение.
|
(`stimulus_switch_positions: 1`) работает как раньше — включать переключение.
|
||||||
Порядок проверки на стенде:
|
Порядок проверки на стенде:
|
||||||
|
|
||||||
1. Осциллографом на PB2 и PB5 контроллера: на одной частоте четыре стадии и два
|
1. Осциллографом на PB2, PB6 и PB5 контроллера: на одной частоте четыре стадии и
|
||||||
периода `A B A B`, PB5 молчит.
|
два периода `A B A B`, PB2 и PB6 меняются одновременно и в одну сторону, PB5
|
||||||
|
молчит.
|
||||||
2. По USB CDC контроллера (развёртку остановить): `parity corrections : 0` и
|
2. По USB CDC контроллера (развёртку остановить): `parity corrections : 0` и
|
||||||
`stages per point : 4`.
|
`stages per point : 4`.
|
||||||
3. Сверить трейсы: выходы 0 и 1 должны совпасть с тем, что даёт старый режим —
|
3. Сверить трейсы: выходы 0 и 1 должны совпасть с тем, что даёт старый режим —
|
||||||
|
|||||||
@@ -0,0 +1,194 @@
|
|||||||
|
"""Проверка на железе, что переключение стимула внутри частотной точки работает.
|
||||||
|
|
||||||
|
Зачем отдельная проверка. Число триггерных фронтов на частотную точку НЕ
|
||||||
|
различает стоковый битстрим от доработанного: поле ``stages`` в ``SweepSettings``
|
||||||
|
стоковое, и сток тоже отрабатывает четыре стадии на точку. Отличие в том, КАКИЕ
|
||||||
|
стадии излучают. Сток сравнивает счётчик стадий с порт-стадией точно, поэтому при
|
||||||
|
``port1Stage = 0`` и ``port2Stage = 2`` излучают только стадии 0 и 2, а 1 и 3
|
||||||
|
остаются без стимула. Доработанный битстрим сравнивает по маске, и порт излучает
|
||||||
|
во всём блоке: 0 и 1 — порт 1, 2 и 3 — порт 2.
|
||||||
|
|
||||||
|
Отсюда две проверки по данным:
|
||||||
|
|
||||||
|
1. **Во всех четырёх выходах есть сигнал.** Если выходы 1 и 3 на уровне шума —
|
||||||
|
в приборе стоковый битстрим, заливка FPGA не удалась.
|
||||||
|
2. **Выходы попарно совпадают, когда ВЧ-ключ не подключён.** Тогда обе позиции
|
||||||
|
ключа — это физически один и тот же путь, поэтому 0 ≈ 1 и 2 ≈ 3. Расхождение
|
||||||
|
при отключённом ключе означает, что по стадиям разъезжается что-то ещё.
|
||||||
|
|
||||||
|
Заодно проверяется и контроллер в петле триггера: при ``syncMode = 3`` мастер
|
||||||
|
семплирует по вернувшемуся триггеру, так что без исправного зеркалирования в STM32
|
||||||
|
развёртка просто не пойдёт и данных не будет вовсе.
|
||||||
|
|
||||||
|
Запуск (на машине, к которой подключены приборы):
|
||||||
|
|
||||||
|
.venv/bin/python -m python_app.scripts.per_point_switching_check \\
|
||||||
|
--config run_config.json --collections 3
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
import statistics
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from python_app.hardware_full.librevna_multi_device_driver.models import (
|
||||||
|
build_output_channels,
|
||||||
|
normalize_master_stimulus_ports,
|
||||||
|
)
|
||||||
|
from python_app.hardware_full.matrix_radar_service import create_matrix_radar_service
|
||||||
|
from python_app.models.dataset_model import SweepCollection
|
||||||
|
from python_app.models.run_config_model import RunConfigModel
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Порты мастера, которые подставляет контроллер по умолчанию в
|
||||||
|
# `configure_continuous_sweep`. Держится здесь, чтобы раскладка выходов в этой
|
||||||
|
# проверке считалась ровно так же, как в драйвере.
|
||||||
|
MASTER_STIMULUS_PORTS = (1, 2)
|
||||||
|
|
||||||
|
# Ниже этого уровня считаем, что стимула в стадии не было. Порог грубый нарочно:
|
||||||
|
# разница между «излучает» и «не излучает» — это десятки дБ, а не единицы.
|
||||||
|
NOISE_FLOOR_DBFS = -80.0
|
||||||
|
# Насколько выходы одной пары могут расходиться при ОТКЛЮЧЁННОМ ключе.
|
||||||
|
PAIR_TOLERANCE_DB = 6.0
|
||||||
|
|
||||||
|
|
||||||
|
def magnitude_db(values: np.ndarray) -> float:
|
||||||
|
"""Медианный модуль трассы в дБ; медиана устойчива к отдельным выбросам."""
|
||||||
|
finite = values[np.isfinite(values)]
|
||||||
|
if finite.size == 0:
|
||||||
|
return float("-inf")
|
||||||
|
magnitude = float(np.median(np.abs(finite)))
|
||||||
|
if magnitude <= 0.0:
|
||||||
|
return float("-inf")
|
||||||
|
return 20.0 * float(np.log10(magnitude))
|
||||||
|
|
||||||
|
|
||||||
|
def build_switch_position_pairs(config: RunConfigModel) -> list[tuple[int, int]]:
|
||||||
|
"""Пары выходов «один порт мастера в двух положениях ключа».
|
||||||
|
|
||||||
|
Берётся из той же модели, что и раскладка стадий, поэтому не разъедется с
|
||||||
|
драйвером. Соседние индексы сравнивать НЕЛЬЗЯ: при двух портах и двух
|
||||||
|
положениях выход 0 — порт 1 в A, выход 1 — порт 2 в A, выход 2 — порт 1 в B,
|
||||||
|
выход 3 — порт 2 в B. Сравнивать надо (0, 2) и (1, 3).
|
||||||
|
"""
|
||||||
|
# Те же порты мастера, что подставляет контроллер по умолчанию
|
||||||
|
# (`configure_continuous_sweep(master_stimulus_ports=(1, 2))`).
|
||||||
|
channels = build_output_channels(
|
||||||
|
normalize_master_stimulus_ports(MASTER_STIMULUS_PORTS),
|
||||||
|
config.matrix_stimulus_switch_positions,
|
||||||
|
)
|
||||||
|
by_port: dict[int, list[int]] = {}
|
||||||
|
for channel in channels:
|
||||||
|
by_port.setdefault(channel.stimulus_port, []).append(channel.index)
|
||||||
|
pairs: list[tuple[int, int]] = []
|
||||||
|
for indices in by_port.values():
|
||||||
|
ordered = sorted(indices)
|
||||||
|
pairs.extend(zip(ordered, ordered[1:]))
|
||||||
|
return pairs
|
||||||
|
|
||||||
|
|
||||||
|
def summarize(collection: SweepCollection) -> dict[int, list[float]]:
|
||||||
|
"""Сгруппировать медианные уровни s21 по номеру выхода."""
|
||||||
|
by_output: dict[int, list[float]] = {}
|
||||||
|
for trace in collection.traces:
|
||||||
|
by_output.setdefault(trace.combo.output, []).append(magnitude_db(trace.s21))
|
||||||
|
return by_output
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||||
|
parser.add_argument("--config", required=True, type=Path, help="Путь к run_config.json")
|
||||||
|
parser.add_argument("--collections", type=int, default=3, help="Сколько коллекций снять (по умолчанию 3)")
|
||||||
|
parser.add_argument(
|
||||||
|
"--switch-connected",
|
||||||
|
action="store_true",
|
||||||
|
help="ВЧ-ключ подключён: выходы одной пары теперь ДОЛЖНЫ различаться "
|
||||||
|
"(это разные антенны), поэтому расхождение — признак работы, а не ошибки",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s")
|
||||||
|
|
||||||
|
config = RunConfigModel.load_from_path(args.config)
|
||||||
|
config.apply_device_model_constraints()
|
||||||
|
|
||||||
|
if not config.is_matrix_radar:
|
||||||
|
raise SystemExit("нужен матричный radar.model (librevna_multi)")
|
||||||
|
substages = config.matrix_stimulus_switch_positions
|
||||||
|
outputs = config.output_switch.positions
|
||||||
|
print(f"конфиг: stimulus_switch_positions={substages}, выходов={outputs}, "
|
||||||
|
f"входов={config.input_switch.positions}, combos={len(config.combos)}, "
|
||||||
|
f"driver_mode={config.radar.driver_mode}")
|
||||||
|
if substages <= 1:
|
||||||
|
raise SystemExit("stimulus_switch_positions = 1: проверять нечего, режим выключен")
|
||||||
|
|
||||||
|
radar = create_matrix_radar_service(config)
|
||||||
|
radar.open()
|
||||||
|
try:
|
||||||
|
radar.configure(config.radar.sweep)
|
||||||
|
per_output: dict[int, list[float]] = {}
|
||||||
|
for index in range(max(1, args.collections)):
|
||||||
|
collection = radar.acquire_collection(collection_id=index + 1)
|
||||||
|
print(f" коллекция {index + 1}: трасс {len(collection.traces)}")
|
||||||
|
for output, levels in summarize(collection).items():
|
||||||
|
per_output.setdefault(output, []).extend(levels)
|
||||||
|
finally:
|
||||||
|
radar.close()
|
||||||
|
|
||||||
|
if not per_output:
|
||||||
|
raise SystemExit("данных нет — развёртка не прошла (проверьте петлю триггера и контроллер)")
|
||||||
|
|
||||||
|
print("\nмедианный уровень s21 по выходам:")
|
||||||
|
medians: dict[int, float] = {}
|
||||||
|
for output in sorted(per_output):
|
||||||
|
medians[output] = statistics.median(per_output[output])
|
||||||
|
print(f" выход {output}: {medians[output]:+7.1f} дБ (трасс {len(per_output[output])})")
|
||||||
|
|
||||||
|
verdict_ok = True
|
||||||
|
|
||||||
|
silent = [o for o, db in medians.items() if db < NOISE_FLOOR_DBFS]
|
||||||
|
print("\n1) стимул во всех стадиях:")
|
||||||
|
if silent:
|
||||||
|
verdict_ok = False
|
||||||
|
print(f" ПРОВАЛ: без сигнала выходы {silent}. Похоже, в мастере СТОКОВЫЙ битстрим:")
|
||||||
|
print(" он излучает только в стадиях, точно равных порт-стадии (0 и 2).")
|
||||||
|
else:
|
||||||
|
print(" ок: сигнал есть во всех выходах, значит порт излучает во всём блоке стадий")
|
||||||
|
|
||||||
|
if args.switch_connected:
|
||||||
|
print("2) различие выходов внутри пары (ключ ПОДКЛЮЧЁН, ждём разные антенны):")
|
||||||
|
else:
|
||||||
|
print("2) попарное совпадение выходов (ключ не подключён, ждём один и тот же путь):")
|
||||||
|
# Пары берутся из самой модели, а не из арифметики по индексам: сравнивать
|
||||||
|
# надо ОДИН порт мастера в двух положениях ключа, а соседние индексы — это
|
||||||
|
# РАЗНЫЕ порты в одном положении (выход 0 = порт 1 A, выход 1 = порт 2 A,
|
||||||
|
# выход 2 = порт 1 B, выход 3 = порт 2 B).
|
||||||
|
pairs = build_switch_position_pairs(config)
|
||||||
|
for low, high in pairs:
|
||||||
|
if low not in medians or high not in medians:
|
||||||
|
continue
|
||||||
|
delta = abs(medians[low] - medians[high])
|
||||||
|
differs = delta > PAIR_TOLERANCE_DB
|
||||||
|
# Ключ подключён -> позиции ведут на РАЗНЫЕ антенны, совпадение значило бы,
|
||||||
|
# что ключ не движется. Ключ не подключён -> путь один, ждём совпадение.
|
||||||
|
good = differs if args.switch_connected else not differs
|
||||||
|
if not good:
|
||||||
|
verdict_ok = False
|
||||||
|
if args.switch_connected:
|
||||||
|
status = "ок, ключ переключается" if good else "СОВПАДАЮТ — ключ, похоже, не движется"
|
||||||
|
else:
|
||||||
|
status = "ок" if good else "РАСХОЖДЕНИЕ"
|
||||||
|
print(f" выходы {low} и {high}: расхождение {delta:4.1f} дБ — {status}")
|
||||||
|
|
||||||
|
print("\nИТОГ:", "переключение стимула внутри точки работает" if verdict_ok
|
||||||
|
else "проверка НЕ пройдена, см. выше")
|
||||||
|
return 0 if verdict_ok else 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
+40
-200
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"radar": {
|
"radar": {
|
||||||
"model": "librevna_multi",
|
"model": "librevna_multi",
|
||||||
"serial": "",
|
"serial": "206830985532",
|
||||||
"remote_host": "127.0.0.1",
|
"remote_host": "127.0.0.1",
|
||||||
"remote_port": 50209,
|
"remote_port": 50209,
|
||||||
"driver_mode": "mock",
|
"driver_mode": "mock",
|
||||||
@@ -9,13 +9,14 @@
|
|||||||
"visa_library": "",
|
"visa_library": "",
|
||||||
"multi_device": {
|
"multi_device": {
|
||||||
"slave_serials": [
|
"slave_serials": [
|
||||||
"20A1307D5532",
|
"206930965532",
|
||||||
"2072306C5532"
|
"206930A15532"
|
||||||
],
|
],
|
||||||
"force_external_reference": false,
|
"force_external_reference": true,
|
||||||
"recovery_attempts": 3,
|
"recovery_attempts": 3,
|
||||||
"output_switch_positions": 1,
|
"output_switch_positions": 1,
|
||||||
"input_switch_positions": 3
|
"input_switch_positions": 1,
|
||||||
|
"stimulus_switch_positions": 2
|
||||||
},
|
},
|
||||||
"kamil_adc": {
|
"kamil_adc": {
|
||||||
"project_dir": "",
|
"project_dir": "",
|
||||||
@@ -62,7 +63,8 @@
|
|||||||
"max_value": 35.0,
|
"max_value": 35.0,
|
||||||
"step": 0.1,
|
"step": 0.1,
|
||||||
"time_step": 20,
|
"time_step": 20,
|
||||||
"delay_time": 3
|
"delay_time": 3,
|
||||||
|
"temp_tolerance_c": 0.03
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sweep": {
|
"sweep": {
|
||||||
@@ -70,6 +72,7 @@
|
|||||||
"stop_hz": 6000000000.0,
|
"stop_hz": 6000000000.0,
|
||||||
"if_bandwidth_hz": 50000.0,
|
"if_bandwidth_hz": 50000.0,
|
||||||
"stimulus_power_dbm": -10.0,
|
"stimulus_power_dbm": -10.0,
|
||||||
|
"dwell_us": 2,
|
||||||
"points": 201
|
"points": 201
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -79,7 +82,7 @@
|
|||||||
"driver_mode": "mock",
|
"driver_mode": "mock",
|
||||||
"driver": "h7992",
|
"driver": "h7992",
|
||||||
"radar_port": 1,
|
"radar_port": 1,
|
||||||
"positions": 2,
|
"positions": 4,
|
||||||
"default_position": 0,
|
"default_position": 0,
|
||||||
"gpio_chip": "/dev/gpiochip0",
|
"gpio_chip": "/dev/gpiochip0",
|
||||||
"pin_a": 17,
|
"pin_a": 17,
|
||||||
@@ -91,7 +94,7 @@
|
|||||||
"driver_mode": "mock",
|
"driver_mode": "mock",
|
||||||
"driver": "h7992",
|
"driver": "h7992",
|
||||||
"radar_port": 2,
|
"radar_port": 2,
|
||||||
"positions": 12,
|
"positions": 4,
|
||||||
"default_position": 0,
|
"default_position": 0,
|
||||||
"gpio_chip": "/dev/gpiochip0",
|
"gpio_chip": "/dev/gpiochip0",
|
||||||
"pin_a": 22,
|
"pin_a": 22,
|
||||||
@@ -115,7 +118,7 @@
|
|||||||
"settling_ms": 0,
|
"settling_ms": 0,
|
||||||
"idle_sleep_ms": 2,
|
"idle_sleep_ms": 2,
|
||||||
"continuous": true,
|
"continuous": true,
|
||||||
"processing_live_config_path": "/home/guriy/Documents/radar_system/python_app/runtime/processing_live.json",
|
"processing_live_config_path": "python_app/runtime/processing_live.json",
|
||||||
"locator_server": {
|
"locator_server": {
|
||||||
"device_id": 3,
|
"device_id": 3,
|
||||||
"protocol_version": 1,
|
"protocol_version": 1,
|
||||||
@@ -142,38 +145,6 @@
|
|||||||
"input": 3,
|
"input": 3,
|
||||||
"output": 0
|
"output": 0
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"input": 4,
|
|
||||||
"output": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"input": 5,
|
|
||||||
"output": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"input": 6,
|
|
||||||
"output": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"input": 7,
|
|
||||||
"output": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"input": 8,
|
|
||||||
"output": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"input": 9,
|
|
||||||
"output": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"input": 10,
|
|
||||||
"output": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"input": 11,
|
|
||||||
"output": 0
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"input": 0,
|
"input": 0,
|
||||||
"output": 1
|
"output": 1
|
||||||
@@ -191,36 +162,36 @@
|
|||||||
"output": 1
|
"output": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"input": 4,
|
"input": 0,
|
||||||
"output": 1
|
"output": 2
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"input": 5,
|
"input": 1,
|
||||||
"output": 1
|
"output": 2
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"input": 6,
|
"input": 2,
|
||||||
"output": 1
|
"output": 2
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"input": 7,
|
"input": 3,
|
||||||
"output": 1
|
"output": 2
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"input": 8,
|
"input": 0,
|
||||||
"output": 1
|
"output": 3
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"input": 9,
|
"input": 1,
|
||||||
"output": 1
|
"output": 3
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"input": 10,
|
"input": 2,
|
||||||
"output": 1
|
"output": 3
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"input": 11,
|
"input": 3,
|
||||||
"output": 1
|
"output": 3
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -276,6 +247,18 @@
|
|||||||
"x_m": -0.905,
|
"x_m": -0.905,
|
||||||
"y_m": 0.0,
|
"y_m": 0.0,
|
||||||
"z_m": 0.0
|
"z_m": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"output_pos": 2,
|
||||||
|
"x_m": 0.905,
|
||||||
|
"y_m": 0.0,
|
||||||
|
"z_m": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"output_pos": 3,
|
||||||
|
"x_m": -0.905,
|
||||||
|
"y_m": 0.0,
|
||||||
|
"z_m": 0.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"rx_geometry": [
|
"rx_geometry": [
|
||||||
@@ -302,54 +285,6 @@
|
|||||||
"x_m": 0.185,
|
"x_m": 0.185,
|
||||||
"y_m": 0.0,
|
"y_m": 0.0,
|
||||||
"z_m": 0.0
|
"z_m": 0.0
|
||||||
},
|
|
||||||
{
|
|
||||||
"input_pos": 4,
|
|
||||||
"x_m": 0.0,
|
|
||||||
"y_m": 0.0,
|
|
||||||
"z_m": 0.0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"input_pos": 5,
|
|
||||||
"x_m": 0.0,
|
|
||||||
"y_m": 0.0,
|
|
||||||
"z_m": 0.0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"input_pos": 6,
|
|
||||||
"x_m": 0.0,
|
|
||||||
"y_m": 0.0,
|
|
||||||
"z_m": 0.0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"input_pos": 7,
|
|
||||||
"x_m": 0.0,
|
|
||||||
"y_m": 0.0,
|
|
||||||
"z_m": 0.0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"input_pos": 8,
|
|
||||||
"x_m": 0.0,
|
|
||||||
"y_m": 0.0,
|
|
||||||
"z_m": 0.0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"input_pos": 9,
|
|
||||||
"x_m": 0.0,
|
|
||||||
"y_m": 0.0,
|
|
||||||
"z_m": 0.0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"input_pos": 10,
|
|
||||||
"x_m": 0.0,
|
|
||||||
"y_m": 0.0,
|
|
||||||
"z_m": 0.0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"input_pos": 11,
|
|
||||||
"x_m": 0.0,
|
|
||||||
"y_m": 0.0,
|
|
||||||
"z_m": 0.0
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -379,100 +314,5 @@
|
|||||||
"capacity": 50,
|
"capacity": 50,
|
||||||
"slot_size_bytes": 2097152
|
"slot_size_bytes": 2097152
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"gui": {
|
|
||||||
"version": 1,
|
|
||||||
"switches": {
|
|
||||||
"combo_mode": "text",
|
|
||||||
"combos_text": "0:0,1:0,2:0,3:0,4:0,5:0,6:0,7:0,8:0,9:0,10:0,11:0,0:1,1:1,2:1,3:1,4:1,5:1,6:1,7:1,8:1,9:1,10:1,11:1",
|
|
||||||
"single_input": "0",
|
|
||||||
"single_output": "0"
|
|
||||||
},
|
|
||||||
"processing": {
|
|
||||||
"selected_mode": "gpr",
|
|
||||||
"pass_through": {
|
|
||||||
"show_magnitude": true,
|
|
||||||
"show_phase": false,
|
|
||||||
"unwrap_phase": false,
|
|
||||||
"combo_filter": "",
|
|
||||||
"fixed_y_enabled": false,
|
|
||||||
"y_min_db": -100.0,
|
|
||||||
"y_max_db": 0.0
|
|
||||||
},
|
|
||||||
"bscan": {
|
|
||||||
"axis": "abs",
|
|
||||||
"cut_m": 0.0,
|
|
||||||
"max_depth_m": 3.0,
|
|
||||||
"gain": 1.0,
|
|
||||||
"start_freq_mhz": 100.0,
|
|
||||||
"stop_freq_mhz": 6000.0,
|
|
||||||
"subtract_mean_ascan": false
|
|
||||||
},
|
|
||||||
"gpr": {
|
|
||||||
"input_positions": "0,1,2,3",
|
|
||||||
"output_positions": "0,1",
|
|
||||||
"min_depth_m": 2.0,
|
|
||||||
"max_depth_m": 14.0,
|
|
||||||
"range_comp_power": 0.1,
|
|
||||||
"object_min_frac": 0.7,
|
|
||||||
"score_mode": "combined",
|
|
||||||
"motion_mode": "int_minus",
|
|
||||||
"look_angle_deg": 0.0,
|
|
||||||
"direction_sign": 1.0,
|
|
||||||
"speed_m_s": 0.0,
|
|
||||||
"ignore_socket_speed_enabled": false,
|
|
||||||
"max_detected_objects_to_draw": 5,
|
|
||||||
"draw_top_m_objects": 2,
|
|
||||||
"object_approach_min_frames": 3,
|
|
||||||
"start_freq_mhz": 3000.0,
|
|
||||||
"stop_freq_mhz": 6000.0,
|
|
||||||
"background_subtract_enabled": true,
|
|
||||||
"background_mean_count": 10,
|
|
||||||
"remove_sidelobe_objects_enabled": false,
|
|
||||||
"imaging_plane_y_m": 0.0,
|
|
||||||
"render_mode": "heatmap",
|
|
||||||
"visible_x_min_m": -2.0,
|
|
||||||
"visible_x_max_m": 2.0,
|
|
||||||
"visible_z_min_m": 0.0,
|
|
||||||
"visible_z_max_m": 14.0
|
|
||||||
},
|
|
||||||
"legacy_gpr": {
|
|
||||||
"mode": "point",
|
|
||||||
"input_positions": "0,1,2,3",
|
|
||||||
"output_positions": "0,1",
|
|
||||||
"min_depth_m": 2.0,
|
|
||||||
"max_depth_m": 14.0,
|
|
||||||
"comp_power": 0.2,
|
|
||||||
"start_freq_mhz": 3000.0,
|
|
||||||
"stop_freq_mhz": 6000.0,
|
|
||||||
"speed_m_s": 0.0,
|
|
||||||
"ignore_socket_speed_enabled": false,
|
|
||||||
"look_angle_deg": 0.0,
|
|
||||||
"apply_freq_phase_correction": true,
|
|
||||||
"reference_mode": "frame_center",
|
|
||||||
"snr_thresh": 4.5,
|
|
||||||
"snr_comp_max": 25.0,
|
|
||||||
"background_subtract_enabled": true,
|
|
||||||
"background_mean_count": 10,
|
|
||||||
"render_mode": "heatmap",
|
|
||||||
"min_visible_pair_count": 1,
|
|
||||||
"visible_x_min_m": -2.0,
|
|
||||||
"visible_x_max_m": 2.0,
|
|
||||||
"visible_z_min_m": 0.0,
|
|
||||||
"visible_z_max_m": 14.0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"data_actions": {
|
|
||||||
"save_count": 10,
|
|
||||||
"save_path": "python_app/data/snapshots",
|
|
||||||
"save_name": "snapshot_simulator",
|
|
||||||
"record_count": 100
|
|
||||||
},
|
|
||||||
"preprocess_dialog": {
|
|
||||||
"set_name": "smoke_cal",
|
|
||||||
"radar_config_dir": "",
|
|
||||||
"use_all_radar_configs": false,
|
|
||||||
"median_sweep_count": 5
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,318 @@
|
|||||||
|
{
|
||||||
|
"radar": {
|
||||||
|
"model": "librevna_multi",
|
||||||
|
"serial": "206830985532",
|
||||||
|
"remote_host": "127.0.0.1",
|
||||||
|
"remote_port": 50209,
|
||||||
|
"driver_mode": "native",
|
||||||
|
"mock_signal_hz": 5000000.0,
|
||||||
|
"visa_library": "",
|
||||||
|
"multi_device": {
|
||||||
|
"slave_serials": [
|
||||||
|
"206930965532",
|
||||||
|
"206930A15532"
|
||||||
|
],
|
||||||
|
"force_external_reference": true,
|
||||||
|
"recovery_attempts": 3,
|
||||||
|
"output_switch_positions": 1,
|
||||||
|
"input_switch_positions": 1,
|
||||||
|
"stimulus_switch_positions": 2
|
||||||
|
},
|
||||||
|
"kamil_adc": {
|
||||||
|
"project_dir": "",
|
||||||
|
"executable_path": "",
|
||||||
|
"tty_path": "",
|
||||||
|
"args": [],
|
||||||
|
"env": {},
|
||||||
|
"startup_timeout_s": 5.0,
|
||||||
|
"sweep_timeout_s": 5.0,
|
||||||
|
"stop_timeout_s": 2.0,
|
||||||
|
"phase_calibration": {
|
||||||
|
"phase0_rad": 0.0,
|
||||||
|
"freq0_hz": 2046000000.0,
|
||||||
|
"phase1_rad": 300.0,
|
||||||
|
"freq1_hz": 5612000000.0
|
||||||
|
},
|
||||||
|
"band": {
|
||||||
|
"start_hz": 2100000000.0,
|
||||||
|
"stop_hz": 5500000000.0,
|
||||||
|
"points": 2048
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"laser_control": {
|
||||||
|
"enabled": false,
|
||||||
|
"port": "",
|
||||||
|
"mode": "manual",
|
||||||
|
"pi_coeff1_p": 2560,
|
||||||
|
"pi_coeff1_i": 128,
|
||||||
|
"pi_coeff2_p": 2560,
|
||||||
|
"pi_coeff2_i": 128,
|
||||||
|
"manual": {
|
||||||
|
"temp1": 25.0,
|
||||||
|
"temp2": 25.0,
|
||||||
|
"current1": 30.0,
|
||||||
|
"current2": 30.0
|
||||||
|
},
|
||||||
|
"variation": {
|
||||||
|
"variation_type": "CHANGE_CURRENT_LD1",
|
||||||
|
"static_temp1": 25.0,
|
||||||
|
"static_temp2": 25.0,
|
||||||
|
"static_current1": 30.0,
|
||||||
|
"static_current2": 30.0,
|
||||||
|
"min_value": 30.0,
|
||||||
|
"max_value": 35.0,
|
||||||
|
"step": 0.1,
|
||||||
|
"time_step": 20,
|
||||||
|
"delay_time": 3,
|
||||||
|
"temp_tolerance_c": 0.03
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sweep": {
|
||||||
|
"start_hz": 1000000.0,
|
||||||
|
"stop_hz": 6000000000.0,
|
||||||
|
"if_bandwidth_hz": 50000.0,
|
||||||
|
"stimulus_power_dbm": -10.0,
|
||||||
|
"dwell_us": 2,
|
||||||
|
"points": 201
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"switches": {
|
||||||
|
"port1": {
|
||||||
|
"name": "port1",
|
||||||
|
"driver_mode": "mock",
|
||||||
|
"driver": "h7992",
|
||||||
|
"radar_port": 1,
|
||||||
|
"positions": 4,
|
||||||
|
"default_position": 0,
|
||||||
|
"gpio_chip": "/dev/gpiochip0",
|
||||||
|
"pin_a": 17,
|
||||||
|
"pin_b": 27,
|
||||||
|
"invert_logic": false
|
||||||
|
},
|
||||||
|
"port2": {
|
||||||
|
"name": "port2",
|
||||||
|
"driver_mode": "mock",
|
||||||
|
"driver": "h7992",
|
||||||
|
"radar_port": 2,
|
||||||
|
"positions": 4,
|
||||||
|
"default_position": 0,
|
||||||
|
"gpio_chip": "/dev/gpiochip0",
|
||||||
|
"pin_a": 22,
|
||||||
|
"pin_b": 23,
|
||||||
|
"invert_logic": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"control_button": {
|
||||||
|
"enabled": false,
|
||||||
|
"gpio_chip": "/dev/gpiochip0",
|
||||||
|
"pin": -1,
|
||||||
|
"active_low": true,
|
||||||
|
"bias": "",
|
||||||
|
"debounce_ms": 50,
|
||||||
|
"action": "capture_tmp_reference"
|
||||||
|
},
|
||||||
|
"logging": {
|
||||||
|
"level": "debug"
|
||||||
|
},
|
||||||
|
"run": {
|
||||||
|
"settling_ms": 0,
|
||||||
|
"idle_sleep_ms": 2,
|
||||||
|
"continuous": true,
|
||||||
|
"processing_live_config_path": "python_app/runtime/processing_live.json",
|
||||||
|
"locator_server": {
|
||||||
|
"device_id": 3,
|
||||||
|
"protocol_version": 1,
|
||||||
|
"host": "0.0.0.0",
|
||||||
|
"port": 8888,
|
||||||
|
"max_payload_bytes": 65536,
|
||||||
|
"client_queue_size": 32,
|
||||||
|
"logger_name": "locator_runtime"
|
||||||
|
},
|
||||||
|
"combos": [
|
||||||
|
{
|
||||||
|
"input": 0,
|
||||||
|
"output": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 1,
|
||||||
|
"output": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 2,
|
||||||
|
"output": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 3,
|
||||||
|
"output": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 0,
|
||||||
|
"output": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 1,
|
||||||
|
"output": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 2,
|
||||||
|
"output": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 3,
|
||||||
|
"output": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 0,
|
||||||
|
"output": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 1,
|
||||||
|
"output": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 2,
|
||||||
|
"output": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 3,
|
||||||
|
"output": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 0,
|
||||||
|
"output": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 1,
|
||||||
|
"output": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 2,
|
||||||
|
"output": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 3,
|
||||||
|
"output": 3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"preprocess": {
|
||||||
|
"s21": {
|
||||||
|
"calibration": {
|
||||||
|
"set_name": "smoke_cal3",
|
||||||
|
"bundle_path": ""
|
||||||
|
},
|
||||||
|
"reference": {
|
||||||
|
"set_name": "smoke_cal3",
|
||||||
|
"bundle_path": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"s11": {
|
||||||
|
"calibration": {
|
||||||
|
"open": {
|
||||||
|
"set_name": "",
|
||||||
|
"bundle_path": ""
|
||||||
|
},
|
||||||
|
"short": {
|
||||||
|
"set_name": "",
|
||||||
|
"bundle_path": ""
|
||||||
|
},
|
||||||
|
"load": {
|
||||||
|
"set_name": "",
|
||||||
|
"bundle_path": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"reference": {
|
||||||
|
"set_name": "",
|
||||||
|
"bundle_path": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notch": {
|
||||||
|
"enabled": true,
|
||||||
|
"bands_hz": [],
|
||||||
|
"taper_width_hz": 40000000.0,
|
||||||
|
"taper_type": "cosine"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gpr": {
|
||||||
|
"relative_permittivity": 1.0,
|
||||||
|
"tx_geometry": [
|
||||||
|
{
|
||||||
|
"output_pos": 0,
|
||||||
|
"x_m": 0.905,
|
||||||
|
"y_m": 0.0,
|
||||||
|
"z_m": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"output_pos": 1,
|
||||||
|
"x_m": -0.905,
|
||||||
|
"y_m": 0.0,
|
||||||
|
"z_m": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"output_pos": 2,
|
||||||
|
"x_m": 0.905,
|
||||||
|
"y_m": 0.0,
|
||||||
|
"z_m": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"output_pos": 3,
|
||||||
|
"x_m": -0.905,
|
||||||
|
"y_m": 0.0,
|
||||||
|
"z_m": 0.0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rx_geometry": [
|
||||||
|
{
|
||||||
|
"input_pos": 0,
|
||||||
|
"x_m": -0.18,
|
||||||
|
"y_m": 0.0,
|
||||||
|
"z_m": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input_pos": 1,
|
||||||
|
"x_m": 0.485,
|
||||||
|
"y_m": 0.0,
|
||||||
|
"z_m": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input_pos": 2,
|
||||||
|
"x_m": -0.49,
|
||||||
|
"y_m": 0.0,
|
||||||
|
"z_m": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input_pos": 3,
|
||||||
|
"x_m": 0.185,
|
||||||
|
"y_m": 0.0,
|
||||||
|
"z_m": 0.0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"rings": {
|
||||||
|
"raw": {
|
||||||
|
"name": "/radar_raw_simulator",
|
||||||
|
"capacity": 50,
|
||||||
|
"slot_size_bytes": 2097152
|
||||||
|
},
|
||||||
|
"raw_tap": {
|
||||||
|
"name": "/radar_raw_tap_simulator",
|
||||||
|
"capacity": 50,
|
||||||
|
"slot_size_bytes": 2097152
|
||||||
|
},
|
||||||
|
"preprocessed": {
|
||||||
|
"name": "/radar_preprocessed_simulator",
|
||||||
|
"capacity": 50,
|
||||||
|
"slot_size_bytes": 2097152
|
||||||
|
},
|
||||||
|
"preprocessed_tap": {
|
||||||
|
"name": "/radar_preprocessed_tap_simulator",
|
||||||
|
"capacity": 50,
|
||||||
|
"slot_size_bytes": 2097152
|
||||||
|
},
|
||||||
|
"results": {
|
||||||
|
"name": "/radar_results_simulator",
|
||||||
|
"capacity": 50,
|
||||||
|
"slot_size_bytes": 2097152
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,318 @@
|
|||||||
|
{
|
||||||
|
"radar": {
|
||||||
|
"model": "librevna_multi",
|
||||||
|
"serial": "206830985532",
|
||||||
|
"remote_host": "127.0.0.1",
|
||||||
|
"remote_port": 50209,
|
||||||
|
"driver_mode": "native",
|
||||||
|
"mock_signal_hz": 5000000.0,
|
||||||
|
"visa_library": "",
|
||||||
|
"multi_device": {
|
||||||
|
"slave_serials": [
|
||||||
|
"206930965532",
|
||||||
|
"206930A15532"
|
||||||
|
],
|
||||||
|
"force_external_reference": true,
|
||||||
|
"recovery_attempts": 3,
|
||||||
|
"output_switch_positions": 1,
|
||||||
|
"input_switch_positions": 1,
|
||||||
|
"stimulus_switch_positions": 2
|
||||||
|
},
|
||||||
|
"kamil_adc": {
|
||||||
|
"project_dir": "",
|
||||||
|
"executable_path": "",
|
||||||
|
"tty_path": "",
|
||||||
|
"args": [],
|
||||||
|
"env": {},
|
||||||
|
"startup_timeout_s": 5.0,
|
||||||
|
"sweep_timeout_s": 5.0,
|
||||||
|
"stop_timeout_s": 2.0,
|
||||||
|
"phase_calibration": {
|
||||||
|
"phase0_rad": 0.0,
|
||||||
|
"freq0_hz": 2046000000.0,
|
||||||
|
"phase1_rad": 300.0,
|
||||||
|
"freq1_hz": 5612000000.0
|
||||||
|
},
|
||||||
|
"band": {
|
||||||
|
"start_hz": 2100000000.0,
|
||||||
|
"stop_hz": 5500000000.0,
|
||||||
|
"points": 2048
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"laser_control": {
|
||||||
|
"enabled": false,
|
||||||
|
"port": "",
|
||||||
|
"mode": "manual",
|
||||||
|
"pi_coeff1_p": 2560,
|
||||||
|
"pi_coeff1_i": 128,
|
||||||
|
"pi_coeff2_p": 2560,
|
||||||
|
"pi_coeff2_i": 128,
|
||||||
|
"manual": {
|
||||||
|
"temp1": 25.0,
|
||||||
|
"temp2": 25.0,
|
||||||
|
"current1": 30.0,
|
||||||
|
"current2": 30.0
|
||||||
|
},
|
||||||
|
"variation": {
|
||||||
|
"variation_type": "CHANGE_CURRENT_LD1",
|
||||||
|
"static_temp1": 25.0,
|
||||||
|
"static_temp2": 25.0,
|
||||||
|
"static_current1": 30.0,
|
||||||
|
"static_current2": 30.0,
|
||||||
|
"min_value": 30.0,
|
||||||
|
"max_value": 35.0,
|
||||||
|
"step": 0.1,
|
||||||
|
"time_step": 20,
|
||||||
|
"delay_time": 3,
|
||||||
|
"temp_tolerance_c": 0.03
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sweep": {
|
||||||
|
"start_hz": 30000000.0,
|
||||||
|
"stop_hz": 200000000.0,
|
||||||
|
"if_bandwidth_hz": 50000.0,
|
||||||
|
"stimulus_power_dbm": -10.0,
|
||||||
|
"dwell_us": 2,
|
||||||
|
"points": 201
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"switches": {
|
||||||
|
"port1": {
|
||||||
|
"name": "port1",
|
||||||
|
"driver_mode": "mock",
|
||||||
|
"driver": "h7992",
|
||||||
|
"radar_port": 1,
|
||||||
|
"positions": 4,
|
||||||
|
"default_position": 0,
|
||||||
|
"gpio_chip": "/dev/gpiochip0",
|
||||||
|
"pin_a": 17,
|
||||||
|
"pin_b": 27,
|
||||||
|
"invert_logic": false
|
||||||
|
},
|
||||||
|
"port2": {
|
||||||
|
"name": "port2",
|
||||||
|
"driver_mode": "mock",
|
||||||
|
"driver": "h7992",
|
||||||
|
"radar_port": 2,
|
||||||
|
"positions": 4,
|
||||||
|
"default_position": 0,
|
||||||
|
"gpio_chip": "/dev/gpiochip0",
|
||||||
|
"pin_a": 22,
|
||||||
|
"pin_b": 23,
|
||||||
|
"invert_logic": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"control_button": {
|
||||||
|
"enabled": false,
|
||||||
|
"gpio_chip": "/dev/gpiochip0",
|
||||||
|
"pin": -1,
|
||||||
|
"active_low": true,
|
||||||
|
"bias": "",
|
||||||
|
"debounce_ms": 50,
|
||||||
|
"action": "capture_tmp_reference"
|
||||||
|
},
|
||||||
|
"logging": {
|
||||||
|
"level": "debug"
|
||||||
|
},
|
||||||
|
"run": {
|
||||||
|
"settling_ms": 0,
|
||||||
|
"idle_sleep_ms": 2,
|
||||||
|
"continuous": true,
|
||||||
|
"processing_live_config_path": "python_app/runtime/processing_live.json",
|
||||||
|
"locator_server": {
|
||||||
|
"device_id": 3,
|
||||||
|
"protocol_version": 1,
|
||||||
|
"host": "0.0.0.0",
|
||||||
|
"port": 8888,
|
||||||
|
"max_payload_bytes": 65536,
|
||||||
|
"client_queue_size": 32,
|
||||||
|
"logger_name": "locator_runtime"
|
||||||
|
},
|
||||||
|
"combos": [
|
||||||
|
{
|
||||||
|
"input": 0,
|
||||||
|
"output": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 1,
|
||||||
|
"output": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 2,
|
||||||
|
"output": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 3,
|
||||||
|
"output": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 0,
|
||||||
|
"output": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 1,
|
||||||
|
"output": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 2,
|
||||||
|
"output": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 3,
|
||||||
|
"output": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 0,
|
||||||
|
"output": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 1,
|
||||||
|
"output": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 2,
|
||||||
|
"output": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 3,
|
||||||
|
"output": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 0,
|
||||||
|
"output": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 1,
|
||||||
|
"output": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 2,
|
||||||
|
"output": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 3,
|
||||||
|
"output": 3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"preprocess": {
|
||||||
|
"s21": {
|
||||||
|
"calibration": {
|
||||||
|
"set_name": "smoke_cal3",
|
||||||
|
"bundle_path": ""
|
||||||
|
},
|
||||||
|
"reference": {
|
||||||
|
"set_name": "smoke_cal3",
|
||||||
|
"bundle_path": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"s11": {
|
||||||
|
"calibration": {
|
||||||
|
"open": {
|
||||||
|
"set_name": "",
|
||||||
|
"bundle_path": ""
|
||||||
|
},
|
||||||
|
"short": {
|
||||||
|
"set_name": "",
|
||||||
|
"bundle_path": ""
|
||||||
|
},
|
||||||
|
"load": {
|
||||||
|
"set_name": "",
|
||||||
|
"bundle_path": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"reference": {
|
||||||
|
"set_name": "",
|
||||||
|
"bundle_path": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notch": {
|
||||||
|
"enabled": true,
|
||||||
|
"bands_hz": [],
|
||||||
|
"taper_width_hz": 40000000.0,
|
||||||
|
"taper_type": "cosine"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gpr": {
|
||||||
|
"relative_permittivity": 1.0,
|
||||||
|
"tx_geometry": [
|
||||||
|
{
|
||||||
|
"output_pos": 0,
|
||||||
|
"x_m": 0.905,
|
||||||
|
"y_m": 0.0,
|
||||||
|
"z_m": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"output_pos": 1,
|
||||||
|
"x_m": -0.905,
|
||||||
|
"y_m": 0.0,
|
||||||
|
"z_m": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"output_pos": 2,
|
||||||
|
"x_m": 0.905,
|
||||||
|
"y_m": 0.0,
|
||||||
|
"z_m": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"output_pos": 3,
|
||||||
|
"x_m": -0.905,
|
||||||
|
"y_m": 0.0,
|
||||||
|
"z_m": 0.0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rx_geometry": [
|
||||||
|
{
|
||||||
|
"input_pos": 0,
|
||||||
|
"x_m": -0.18,
|
||||||
|
"y_m": 0.0,
|
||||||
|
"z_m": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input_pos": 1,
|
||||||
|
"x_m": 0.485,
|
||||||
|
"y_m": 0.0,
|
||||||
|
"z_m": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input_pos": 2,
|
||||||
|
"x_m": -0.49,
|
||||||
|
"y_m": 0.0,
|
||||||
|
"z_m": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input_pos": 3,
|
||||||
|
"x_m": 0.185,
|
||||||
|
"y_m": 0.0,
|
||||||
|
"z_m": 0.0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"rings": {
|
||||||
|
"raw": {
|
||||||
|
"name": "/radar_raw_simulator",
|
||||||
|
"capacity": 50,
|
||||||
|
"slot_size_bytes": 2097152
|
||||||
|
},
|
||||||
|
"raw_tap": {
|
||||||
|
"name": "/radar_raw_tap_simulator",
|
||||||
|
"capacity": 50,
|
||||||
|
"slot_size_bytes": 2097152
|
||||||
|
},
|
||||||
|
"preprocessed": {
|
||||||
|
"name": "/radar_preprocessed_simulator",
|
||||||
|
"capacity": 50,
|
||||||
|
"slot_size_bytes": 2097152
|
||||||
|
},
|
||||||
|
"preprocessed_tap": {
|
||||||
|
"name": "/radar_preprocessed_tap_simulator",
|
||||||
|
"capacity": 50,
|
||||||
|
"slot_size_bytes": 2097152
|
||||||
|
},
|
||||||
|
"results": {
|
||||||
|
"name": "/radar_results_simulator",
|
||||||
|
"capacity": 50,
|
||||||
|
"slot_size_bytes": 2097152
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user