Lab043: establish reproducible HackRF BPSK working point
This commit is contained in:
401
docs/lab043_current_state.md
Normal file
401
docs/lab043_current_state.md
Normal file
@@ -0,0 +1,401 @@
|
||||
# Lab043 — фактический checkpoint
|
||||
|
||||
Технический снимок состояния для продолжения работы без истории чата.
|
||||
Только факты и результаты измерений. Дата снимка: 27 августа 2026 года.
|
||||
|
||||
Пометки источника: **[файл]** — подтверждено чтением raw/analysis/report;
|
||||
**[handoff]** — со слов оператора/handoff-промпта, в файлах не найдено;
|
||||
**[расчёт]** — вычислено из существующих artifacts при этом снимке.
|
||||
|
||||
Сокращения при первом употреблении: TX (Transmitter) — передатчик;
|
||||
RX (Receiver) — приёмник; LNA (Low-Noise Amplifier) — малошумящий входной
|
||||
усилитель; VGA (Variable-Gain Amplifier) — усилитель с регулируемым
|
||||
коэффициентом; ADC (Analog-to-Digital Converter) — аналого-цифровой
|
||||
преобразователь; BPSK (Binary Phase Shift Keying) — двоичная фазовая
|
||||
манипуляция, два состояния фазы, один бит на символ; QPSK (Quadrature
|
||||
Phase Shift Keying) — четырёхпозиционная фазовая манипуляция, два бита на
|
||||
символ; SNR (Signal-to-Noise Ratio) — отношение сигнал/шум; BER (Bit Error
|
||||
Rate) — доля ошибочных битов; SER (Symbol Error Rate) — доля ошибочных
|
||||
символов, для BPSK совпадает с BER; CFO (Carrier Frequency Offset) —
|
||||
остаточное смещение несущей; TCXO — термокомпенсированный кварцевый
|
||||
генератор; CRC (Cyclic Redundancy Check) — циклический контроль
|
||||
избыточности.
|
||||
|
||||
## 1. Цель Lab043
|
||||
|
||||
Довести цифровой канал HackRF → HackRF при общей опоре до устойчивого
|
||||
приёма BPSK, затем последовательно перейти: BPSK → QPSK → бинарные данные
|
||||
→ пакет → CRC → файл → сжатое изображение → редкие видеокадры. Не
|
||||
превращать диагностику BPSK в бесконечное исследование, но перейти дальше
|
||||
только после подтверждённого рабочего BPSK.
|
||||
|
||||
## 2. Оборудование
|
||||
|
||||
Две платы HackRF One r9. Host tools 2024.02.1, libhackrf 2024.02.1 (API
|
||||
0.9), прошивка обеих 2024.02.1 (API 1.08). Обе — клоны, hackrf_info
|
||||
сообщает, что железо не выглядит произведённым Great Scott Gadgets; это
|
||||
известно и само по себе не STOP. Инструменты: `C:\Users\user\Tools\HackRF\Library\bin\`.
|
||||
**Pluto+ в текущем тракте не используется.** **[файл: hackrf_info]**
|
||||
|
||||
## 3. Serial
|
||||
|
||||
- MASTER / TX: `000000000000000097c063dc323b97d3`
|
||||
- SLAVE / RX: `000000000000000097c063dc2e5327d3` **[файл: hackrf_info]**
|
||||
|
||||
## 4. Внешняя опора
|
||||
|
||||
JYEC TCXO 10.000000 MHz, 0.1 ppm, установлен на MASTER. **[handoff]**
|
||||
|
||||
## 5. Физическая схема опоры
|
||||
|
||||
TCXO 10 MHz → MASTER → MASTER CLKOUT → SLAVE CLKIN → SLAVE. SLAVE CLKOUT
|
||||
должен оставаться OFF, clock loop не создавать.
|
||||
|
||||
## 6. Доказательство выбора внешней опоры через reg29
|
||||
|
||||
Si5351 register 29: до запуска трансивера 0x0e; после RX при внешней
|
||||
10 MHz опоре 0x26. Это переключение PLLA с внутреннего 25 MHz кварца на
|
||||
внешний 10 MHz CLKIN. Цепь подтверждена: MASTER после RX reg29=0x26,
|
||||
MASTER CLKOUT ON, SLAVE до CLKOUT сигнала нет, после CLKOUT сигнал
|
||||
обнаружен, SLAVE после RX reg29=0x26. **Считать доказанным, заново не
|
||||
исследовать.** **[файл: оба report.txt подтверждают reg29=0x26 у MASTER и SLAVE]**
|
||||
|
||||
## 7. Тоновый опыт до правильной синхронизации
|
||||
|
||||
Наблюдался значительный уход несущей. **[handoff]**
|
||||
|
||||
## 8. Старый CFO
|
||||
|
||||
Примерно +1165.124 Hz до общей опоры. **[handoff; в файлах тонового опыта только сырые IQ, отчёт не сохранён]**
|
||||
|
||||
## 9. Результат после общей опоры
|
||||
|
||||
Residual CFO примерно +0.041 Hz. Улучшение примерно в 28417 раз.
|
||||
Килогерцовый CFO устранён общей 10 MHz опорой. **К программной
|
||||
компенсации большого CFO как к причине отказа BPSK не возвращаться.**
|
||||
**[handoff; тоновый raw существует, файла анализа с этим числом нет]**
|
||||
|
||||
## 10. Параметры RF-тракта
|
||||
|
||||
- RF center: 1080 MHz
|
||||
- Fs (частота дискретизации): 8 000 000 отсчётов/с
|
||||
- symbol rate: 20 000 символов/с
|
||||
- samples/symbol: 400
|
||||
- digital carrier offset: +50 000 Hz
|
||||
- MASTER TX VGA: 10 dB
|
||||
- TX RF amp: OFF; antenna bias/power: OFF
|
||||
- SLAVE RX RF amp: OFF; bias: OFF; RX gain — по текущей точке лестницы **[файл: metadata.json, rx_stdout.txt]**
|
||||
|
||||
## 11. Параметры BPSK-формы
|
||||
|
||||
17664 символов = 17664 бита. Преамбула 1280 символов (источник
|
||||
pilot_prbs7, initial_state 0x7f). Payload 16384 бита (NumPy PCG64, seed
|
||||
0x043). Rolloff 0.35, RRC span 10 символов, guard 16 символов с каждой
|
||||
стороны. bits SHA-256: `aeae114e831ac6b20a82d70fa5153072f574ef04ea2aec8d9f6d0103100107c2`.
|
||||
Программный roundtrip формы: корреляция преамбулы 0.99995, bit errors 0,
|
||||
BER 0. **[файл: source_waveform_metadata.json]**
|
||||
|
||||
## 12. SHA-256 эталонной TX-формы
|
||||
|
||||
`fed634fb2a4fbf41ea5a1fce547447b8e3e6dbe8110b8429c507637c6d2d1baa`
|
||||
|
||||
Проверено при этом снимке: SHA файла `tx_bpsk_cs8.iq` во внешнем каталоге
|
||||
лестницы совпал с эталоном. **До любого TX сверять SHA; при отличии STOP.**
|
||||
**[файл + расчёт]**
|
||||
|
||||
## 13. Результат первого BPSK при общей опоре
|
||||
|
||||
Каталог `20260827_125733_common_clock_bpsk`, RX LNA 8 / VGA 16. **[файл: analysis.json]**
|
||||
|
||||
- независимая преамбула: НЕ найдена (preamble_found=False)
|
||||
- symbol SNR: −28.31 dB
|
||||
- BER (forced diagnostic): 47.88 %
|
||||
- constellation separation/spread: 0.0767
|
||||
- clipping: 0; peak ADC: I=2, Q=3
|
||||
- классификация: C
|
||||
|
||||
Примечание: handoff-промпт для этого опыта приводил SNR −26.20 dB,
|
||||
BER 47.70 %, separation 0.0978. Расхождение с файлом небольшое; при
|
||||
конфликте источником считается файл analysis.json.
|
||||
|
||||
## 14. Первая RX gain ladder
|
||||
|
||||
Каталог `20260827_130958_common_clock_bpsk_gain_ladder`. TX постоянно
|
||||
VGA 10 dB, RF amp OFF. |h| — комплексный коэффициент канала rx ≈ h·tx,
|
||||
здесь абсолютный RMS полезного сигнала в кодах ADC (rms_centered_complex);
|
||||
шкала ADC HackRF: int8, −128…127. Δ|h| — изменение относительно
|
||||
предыдущей точки. **[файл: analysis.json по точкам; Δ|h| и dBFS — расчёт]**
|
||||
|
||||
| LNA | VGA | peak, кодов | \|h\| (RMS), кодов | \|h\|, dBFS | Δ\|h\| | symbol SNR, dB | separation | BER | preamble | clipping | status |
|
||||
|---:|---:|---:|---:|---:|---:|---:|---:|---:|:--:|:--:|---|
|
||||
| 8 | 8 | 3 | 0.176 | −57.2 | — | −51.70 (diag) | 0.0053 (diag) | 49.98 % (diag) | NO | 0 | нет синхр. |
|
||||
| 16 | 8 | 3 | 0.264 | −53.7 | +3.5 dB | −20.81 (diag) | 0.1823 | 44.87 % (diag) | NO | 0 | нет синхр. |
|
||||
| 16 | 16 | 4 | 0.579 | −46.9 | +6.8 dB | −10.62 | 0.5892 | 33.84 % | YES | 0 | порог не достигнут |
|
||||
| 24 | 16 | 3 | 0.346 | −51.4 | −4.5 dB | −2.62 | 1.4795 | 14.70 % | YES | 0 | порог не достигнут |
|
||||
|
||||
Классификация ladder: B.
|
||||
|
||||
## 15. Лучшая текущая точка
|
||||
|
||||
LNA 24 / VGA 16: preamble YES; symbol SNR −2.62 dB; BER 14.70 %;
|
||||
separation 1.4795; clipping 0. Это лучший проверенный результат, но не
|
||||
рабочая рекомендация. **[файл]**
|
||||
|
||||
## 16. Почему QPSK не запущен
|
||||
|
||||
Рабочий порог не достигнут: BER 14.70 % при цели ≤ 1 %; SNR −2.62 dB при
|
||||
цели ≥ 6 dB; separation 1.4795 при цели ≥ 2. QPSK, JPEG, пакеты, видео до
|
||||
подтверждённого BPSK не запускать.
|
||||
|
||||
## 17. Текущая гипотеза и открытый вопрос
|
||||
|
||||
Формально задача — продолжить RX gain ladder выше 24/16 и одновременно
|
||||
проверять физический рост полезного |h|. Но данные уже показывают
|
||||
проблему: полезный сигнал занимает лишь единицы кодов ADC (peak ≤ 4 из
|
||||
128, RMS ≤ 0.58), и рост номинального усиления на 24 dB (8/8 → 24/16) дал
|
||||
прирост |h| лишь около +6 dB, немонотонно, с падением на 4.5 dB при
|
||||
переходе 16/16 → 24/16. Это близко к STOP-условию: усиление не
|
||||
превращается в амплитуду на ADC. Прежде чем крутить усиление выше,
|
||||
проверить, растёт ли физически |h|, и почему полезный сигнал оцифровывается
|
||||
всего 2 битами из 8. **[расчёт из analysis.json]**
|
||||
|
||||
## 18. Рабочие критерии BPSK
|
||||
|
||||
Все одновременно: independent preamble YES; BER ≤ 1 %; symbol SNR ≥ 6 dB;
|
||||
constellation separation ≥ 2; без clipping и перегруза ADC. Цель — не
|
||||
максимальный gain, а минимальная безопасная рабочая точка.
|
||||
|
||||
## 19. Пути к raw / report / analysis
|
||||
|
||||
Внешний корень (в Git не хранить): `C:\Users\user\Documents\SDR_Rover_Lab043_raw\`
|
||||
|
||||
- `20260827_common_clock_tone\` — тоновый опыт, только сырые IQ.
|
||||
- `20260827_125733_common_clock_bpsk\` — первый BPSK: `report.txt`, `analysis.json`, `rx_cs8.iq`.
|
||||
- `20260827_130958_common_clock_bpsk_gain_ladder\` — лестница: `report.txt`, `summary.json`, `source_waveform_metadata.json`, `tx_bpsk_cs8.iq`, подкаталоги `lnaXX_vgaXX\` (`rx_cs8.iq`, `background_cs8.iq`, `analysis.json`, `metadata.json`, `constellation.png`, `correlation.png`, `rx_stdout.txt`, `rx_stderr.txt`).
|
||||
|
||||
## 20. Следующий конкретный эксперимент
|
||||
|
||||
Продолжить лестницу выше 24/16 (кандидаты 24/24, 32/24, 32/32, 40/32
|
||||
после проверки допустимых ступеней libhackrf 2024.02.1), TX не менять,
|
||||
RF amp OFF, TX VGA 10 dB. Каждая точка — отдельный опыт: preflight общей
|
||||
опоры, RX до TX, один короткий BPSK TX, сохранить raw, измерить |h| и
|
||||
Δ|h| dB, проверить перегруз, и только затем решать о следующей точке.
|
||||
STOP немедленно при достижении рабочей точки, при clipping/перегрузе, или
|
||||
если рост gain не даёт роста |h|.
|
||||
|
||||
**Критический разрыв передачи (обнаружен при этом снимке):** инструмент,
|
||||
которым выполнялись BPSK-опыты HackRF → HackRF (генерация формы, вызовы
|
||||
`hackrf_transfer` для TX/RX, анализ преамбулы/SNR/BER/созвездия), **в
|
||||
репозитории отсутствует** — ни отслеженного, ни неотслеженного `.py`. В
|
||||
репозитории есть только `experiments/lab043_hackrf_gain_ladder.py`, но это
|
||||
Pluto+ → HackRF и один непрерывный тон, не BPSK, потолок 24/24. Внешние
|
||||
artifacts (TX-форма, RX-захваты, analysis.json) существуют, но
|
||||
порождающий код — нет. Для воспроизводимого продолжения «той же формой и
|
||||
тем же анализом» этот инструмент нужно закоммитить в проект либо написать
|
||||
заново отдельной согласованной задачей. **[расчёт: полный поиск по репозиторию]**
|
||||
|
||||
## 21. Запреты
|
||||
|
||||
- Не возвращаться без причины к Pluto+ в текущем тракте.
|
||||
- Не пересматривать доказанный common clock и reg29.
|
||||
- Не запускать QPSK, JPEG, пакеты и видео до BPSK PASS.
|
||||
- Raw-файлы IQ в Git не добавлять; хранить во внешнем каталоге.
|
||||
- Форму BPSK не менять; до TX сверять SHA-256, при отличии STOP.
|
||||
- Усиление вслепую не крутить: каждая точка — отдельный контролируемый опыт.
|
||||
|
||||
## 22. Common-clock recheck 28 августа 2026 года
|
||||
|
||||
Повторная аппаратная проверка после восстановления кабеля MASTER CLKOUT →
|
||||
SLAVE CLKIN завершена с результатом PASS: MASTER после короткого RX имел
|
||||
reg29=0x26; SLAVE дословно вернул `CLKIN status: clock signal detected`;
|
||||
SLAVE после короткого RX имел reg29=0x26. После проверки CLKOUT обеих плат
|
||||
переведены в `Down`, процессов `hackrf_transfer` не осталось. RF TX не
|
||||
выполнялся.
|
||||
|
||||
Обнаруженный false-positive parser исправлен fail-closed: полные строки
|
||||
`clock signal detected` и `no clock signal detected` распознаются раздельно;
|
||||
пустой или неожиданный вывод даёт UNKNOWN. Продолжение разрешает только
|
||||
DETECTED.
|
||||
|
||||
## 23. Offline reconciliation старой BPSK gain ladder
|
||||
|
||||
Фактический анализатор первой лестницы восстановлен из журнала запуска Codex
|
||||
от 27 августа. Git, PowerShell history, временные `.py/.ps1` и каталог raw
|
||||
исходного кода не содержали. Старый код сначала находил oracle-start полной
|
||||
известной 17664-битной формой, а затем проверял близость глобального пика
|
||||
преамбулы. Поэтому историческая реализация содержала data leakage в одной из
|
||||
проверок, хотя сам preamble-only пик вычислялся отдельно.
|
||||
|
||||
Причина расхождения нового runner: старый detector исключал вокруг главного
|
||||
пика ±0.05 с, то есть ±1000 символов, а первая версия runner сравнивала с
|
||||
PRBS7-повтором через 127 символов за пределами лишь ±4 символов. Дополнительно
|
||||
runner применял `10*log10` к отношению амплитуд корреляции вместо исторического
|
||||
`20*log10`. До этого этапа signed int8, I/Q, DC removal, перенос `exp(-j...)`,
|
||||
50 кГц, 400 отсчётов/символ и timing совпадали.
|
||||
|
||||
Исправленный официальный путь глобально ищет start и timing только по
|
||||
преамбуле. Полная форма не влияет на preamble PASS и хранится только как явно
|
||||
помеченная oracle diagnostic. Остаточный CFO оценивается non-data-aided по
|
||||
фазе квадрата BPSK-потока; квадрат устраняет неизвестные знаки payload.
|
||||
Полярность и начальная фаза снимаются по преамбуле.
|
||||
|
||||
Regression четырёх неизменённых raw:
|
||||
|
||||
| LNA | VGA | sync | \|h\| | Δ\|h\| | SNR, dB | separation | BER |
|
||||
|---:|---:|---|---:|---:|---:|---:|---:|
|
||||
| 8 | 8 | oracle diagnostic | 3.6166e-6 | — | -51.70 diag | 0.0053 diag | NOT COMPUTED; 49.98% diag |
|
||||
| 16 | 8 | oracle diagnostic | 1.8902e-4 | +34.36 dB | -20.81 diag | 0.1823 diag | NOT COMPUTED; 44.87% diag |
|
||||
| 16 | 16 | independent | 1.3641e-3 | +17.17 dB | -10.62 | 0.5892 | 33.78% |
|
||||
| 24 | 16 | independent | 2.1859e-3 | +4.10 dB | -2.62 | 1.4795 | 14.78% |
|
||||
|
||||
Первые два `|h|` являются oracle diagnostic при почти случайном BER, поэтому
|
||||
их большие дельты нельзя считать точной физической калибровкой gain. Между
|
||||
двумя независимо синхронизированными точками 16/16 → 24/16 полезная BPSK-
|
||||
компонента выросла на 4.10 dB: RX gain физически помогал, но прирост оказался
|
||||
меньше номинальных 8 dB.
|
||||
|
||||
Для 24/16 научно корректный результат: preamble YES только по преамбуле;
|
||||
detected sample 6263020, refined start 6263013, timing phase -1; correlation
|
||||
29.43 dB над median и 12.41 dB над независимым вторым кандидатом; CFO
|
||||
-0.01224 Hz; carrier phase 122.17°; preamble errors 200/1280; официальный
|
||||
BER 14.78%; измерительный symbol SNR -2.62 dB; separation 1.4795. Старые
|
||||
14.70% воспроизводятся только при full-frame-aided оценке carrier и остаются
|
||||
сравнительной, а не официальной величиной.
|
||||
|
||||
Offline analyzer, deterministic waveform, direct invocation и fail-closed
|
||||
common-clock skeleton покрыты тестами. Реальный hardware RX/TX режим по-прежнему
|
||||
не реализован: `run_one_gain_point()` намеренно заканчивается
|
||||
`NotImplementedError`, RF TX calls=0. Следующую gain ladder не запускать до
|
||||
отдельной реализации и проверки аппаратного режима.
|
||||
|
||||
## 24. Hardware runner и остановка ladder 31 августа 2026 года
|
||||
|
||||
`run_one_gain_point()` завершён как fail-closed аппаратная процедура:
|
||||
детерминированная форма и SHA-gate выполняются до обращения к HackRF; затем
|
||||
common-clock preflight, старт SLAVE RX до TX, ровно один разрешённый TX,
|
||||
штатное завершение RX, двойная проверка SHA-256 и размера raw, проверка
|
||||
overrun/underrun, независимый preamble-only анализ и безусловное выключение
|
||||
обоих CLKOUT. Последовательный режим создаёт каталог только для фактически
|
||||
начатой точки и после каждого анализа проверяет PASS/STOP.
|
||||
|
||||
Mock-проверки воспроизвели preflight failure, SHA failure, RX startup failure,
|
||||
успешный единичный TX, TX failure без повтора, analysis failure без повтора,
|
||||
cleanup процесса и обоих CLKOUT, а также запрет второго TX. Перед аппаратной
|
||||
попыткой: `python tools/quick_gate.py` PASS; `python -m pytest tests/ -q`:
|
||||
343 passed; `git diff --check` PASS. Предупреждение pytest относилось только к
|
||||
невозможности создать `.pytest_cache` и не затрагивало тесты.
|
||||
|
||||
Разрешённая лестница 24/24 → 32/24 → 32/32 → 40/32 была запущена через
|
||||
репозиторный `experiments/lab043_hackrf_bpsk.py`, но common-clock preflight
|
||||
остановил её до первой точки: MASTER прошёл свою часть проверки, после
|
||||
включения MASTER CLKOUT SLAVE дословно вернул
|
||||
`CLKIN status: no clock signal detected`. Классификация запуска D, выполненных
|
||||
gain points 0, RF TX calls 0, RECOMMENDED RX GAIN отсутствует. Итоговый отчёт:
|
||||
`C:\Users\user\Documents\SDR_Rover_Lab043_raw\20260831_110014_common_clock_bpsk_gain_ladder_2\ladder_report.json`.
|
||||
|
||||
После STOP обе платы явно получили команду CLKOUT OFF; процессов
|
||||
`hackrf_transfer`, `hackrf_clock` и `hackrf_debug` не осталось. Следующий шаг:
|
||||
физически проверить соединение MASTER CLKOUT → SLAVE CLKIN и отдельным
|
||||
common-clock preflight восстановить тройной PASS до нового разрешения RF.
|
||||
|
||||
После физического восстановления соединения отдельная ручная проверка дала
|
||||
тройной PASS: MASTER reg29=0x26, точная строка SLAVE
|
||||
`CLKIN status: clock signal detected`, SLAVE reg29=0x26; оба коротких RX имели
|
||||
0 overruns. Однако немедленно запущенный репозиторный runner повторил свой
|
||||
обязательный preflight и получил от SLAVE
|
||||
`CLKIN status: no clock signal detected`. Поэтому лестница снова остановлена
|
||||
до первой точки: gain points 0, RF TX calls 0, классификация D. Отчёт второй
|
||||
попытки:
|
||||
`C:\Users\user\Documents\SDR_Rover_Lab043_raw\20260831_111352_common_clock_bpsk_gain_ladder_2\ladder_report.json`.
|
||||
Противоречие PASS → NOT_DETECTED означает нестабильное соединение или
|
||||
детектирование общей опоры; повторять RF-запуск автоматически нельзя.
|
||||
|
||||
После замены пользователем провода общей опоры выполнена ещё одна явно
|
||||
разрешённая попытка runner. Его обязательный preflight снова получил точную
|
||||
строку `CLKIN status: no clock signal detected`; gain points 0, RF TX calls 0,
|
||||
классификация D. Отчёт:
|
||||
`C:\Users\user\Documents\SDR_Rover_Lab043_raw\20260831_111801_common_clock_bpsk_gain_ladder_2\ladder_report.json`.
|
||||
Поскольку отдельная ручная последовательность ранее дала PASS, а runner
|
||||
проверяет CLKIN сразу после включения MASTER CLKOUT, следующая программная
|
||||
гипотеза — недостаточная выдержка перед чтением CLKIN. До её согласованной
|
||||
реализации и mock-проверки новые RF-попытки не выполнять.
|
||||
|
||||
## 25. Common-clock settle-time 31 августа 2026 года
|
||||
|
||||
Timing/race hypothesis подтверждена измерением без RF TX. В исходном
|
||||
измерительном цикле запросы после MASTER CLKOUT ON дали NOT_DETECTED на
|
||||
37.9 и 115.3 мс, затем DETECTED на 171.0 мс. MASTER и SLAVE после коротких
|
||||
RX имели reg29=0x26, cleanup обеих плат завершился без ошибок.
|
||||
|
||||
В runner реализован fail-closed bounded polling на монотонных часах:
|
||||
`initial_settle_s=0.050`, `poll_interval_s=0.050`, `timeout_s=0.500`.
|
||||
UNKNOWN, ненулевой exit code и subprocess exception немедленно закрывают
|
||||
gate; постоянный NOT_DETECTED заканчивается TIMEOUT. Предел 500 мс выбран
|
||||
почти втрое больше первого измеренного времени 171 мс и более чем вдвое
|
||||
больше максимума подтверждающей серии 196.55 мс, но не допускает
|
||||
неограниченного ожидания.
|
||||
|
||||
Пять полностью раздельных common-clock-only циклов дали 5/5 PASS:
|
||||
|
||||
| cycle | time to DETECTED, ms | queries | MASTER reg29 | SLAVE CLKIN | SLAVE reg29 |
|
||||
|---:|---:|---:|---:|---|---:|
|
||||
| 1 | 132.27 | 2 | 0x26 | DETECTED | 0x26 |
|
||||
| 2 | 134.45 | 2 | 0x26 | DETECTED | 0x26 |
|
||||
| 3 | 130.76 | 2 | 0x26 | DETECTED | 0x26 |
|
||||
| 4 | 130.17 | 2 | 0x26 | DETECTED | 0x26 |
|
||||
| 5 | 196.55 | 3 | 0x26 | DETECTED | 0x26 |
|
||||
|
||||
Во всех циклах первый запрос после initial settle ещё возвращал
|
||||
NOT_DETECTED. Поэтому одиночная мгновенная проверка была реальной гонкой, а
|
||||
не доказательством отсутствия общей опоры. Common-clock gate: STABLE. RF TX
|
||||
count 0; после серии оба CLKOUT выключены, процессов HackRF не осталось.
|
||||
|
||||
Артефакты:
|
||||
`C:\Users\user\Documents\SDR_Rover_Lab043_raw\20260831_112534_common_clock_settle_test\settle_report.json`
|
||||
и `settle_report.txt`. Mock tests покрывают первый DETECTED, delayed DETECTED,
|
||||
TIMEOUT, UNKNOWN, subprocess exception, PASS на границе timeout, конечность
|
||||
polling и cleanup. После изменения: quick_gate PASS, pytest 350 passed,
|
||||
`git diff --check` PASS. BPSK gain ladder на этом этапе не запускалась.
|
||||
|
||||
## 26. Первая рабочая BPSK-точка 31 августа 2026 года
|
||||
|
||||
После закрытия common-clock timing issue репозиторный runner выполнил первую
|
||||
разрешённую точку LNA 24 / VGA 24 и немедленно остановил ladder по BPSK PASS.
|
||||
Точки 32/24, 32/32 и 40/32 не запускались. Выполнен ровно один RF TX;
|
||||
waveform SHA-256 совпал с эталоном
|
||||
`fed634fb2a4fbf41ea5a1fce547447b8e3e6dbe8110b8429c507637c6d2d1baa`.
|
||||
|
||||
Common-clock preflight перед TX: MASTER reg29=0x26; SLAVE DETECTED через
|
||||
132.74 мс и два запроса; SLAVE reg29=0x26. Официальная синхронизация найдена
|
||||
глобально только по преамбуле: peak/median 38.02 dB, peak/second independent
|
||||
candidate 10.98 dB, detected start 6231460, refined start 6231458, timing +1
|
||||
sample. Payload не использовался для start/timing/carrier phase/polarity.
|
||||
|
||||
Результат LNA 24 / VGA 24: preamble YES, errors 9/1280; CFO +0.00475 Hz;
|
||||
carrier phase -132.45°; |h|=4.5363e-2; symbol SNR 7.94 dB; centers примерно
|
||||
(-0.9936-0.0056j) и (+0.9912+0.0089j); within-cluster RMS 0.3980;
|
||||
separation 4.9867; официальный BER=SER=0.4586% (81 ошибок из 17664 бит).
|
||||
Все рабочие критерии выполнены: BER≤1%, SNR≥6 dB, separation≥2,
|
||||
independent preamble, clipping 0 и отсутствие ADC overload.
|
||||
|
||||
Raw: mean I/Q 0.948/1.837 кода; RMS после DC removal 0.791; p95 3.162;
|
||||
p99 3.606; p99.9 3.606; peak component 5; ADC headroom 122 кода;
|
||||
clipping 0/32000000. Файл имеет 64000000 байт, 32000000 complex samples,
|
||||
длительность 4.0 с. Две проверки SHA-256 совпали:
|
||||
`a8d857a7fdd895475d8c58b56d8f1778ffce96966709ce98d691e078b4a832c8`.
|
||||
RX overruns 0, TX underruns 0.
|
||||
|
||||
Относительно старой точки 24/16 runner получил Δ|h|=+26.34 dB. Это намного
|
||||
больше номинального изменения VGA +8 dB; после замены кабеля и физических
|
||||
переподключений дельту нельзя интерпретировать как чистую калибровку RX gain.
|
||||
Она не отменяет достоверность текущего внутрикадрового результата и отсутствие
|
||||
перегруза, но для калибровки тракта требует отдельного опыта.
|
||||
|
||||
Классификация A. RECOMMENDED RX GAIN: LNA 24 / VGA 24. Практически BER
|
||||
0.4586% означает около 4.6 ошибочных битов на 1000 до коррекции ошибок.
|
||||
Артефакты:
|
||||
`C:\Users\user\Documents\SDR_Rover_Lab043_raw\20260831_114210_common_clock_bpsk_gain_ladder_2\`.
|
||||
После опыта MASTER и SLAVE CLKOUT явно выключены с exit code 0, процессов
|
||||
HackRF нет. Послеруновые проверки: quick_gate PASS, pytest 351 passed,
|
||||
`git diff --check` PASS. QPSK/JPEG/video не запускались. Следующий этап —
|
||||
переход к полезным данным, но только отдельной согласованной задачей.
|
||||
1489
experiments/lab043_hackrf_bpsk.py
Normal file
1489
experiments/lab043_hackrf_bpsk.py
Normal file
File diff suppressed because it is too large
Load Diff
511
tests/test_lab043_hackrf_bpsk.py
Normal file
511
tests/test_lab043_hackrf_bpsk.py
Normal file
@@ -0,0 +1,511 @@
|
||||
"""Проверки fail-closed разбора CLKIN для Lab043."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from experiments import lab043_hackrf_bpsk as bpsk
|
||||
|
||||
|
||||
class FakeCommandRunner:
|
||||
"""Предсказуемый HackRF host без обращения к оборудованию."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
clkin_output: str | list[str] = "CLKIN status: clock signal detected\n",
|
||||
tx_returncode: int = 0,
|
||||
clkin_exception_at: int | None = None,
|
||||
) -> None:
|
||||
self.clkin_outputs = [clkin_output] if isinstance(clkin_output, str) else list(clkin_output)
|
||||
self.tx_returncode = tx_returncode
|
||||
self.clkin_exception_at = clkin_exception_at
|
||||
self.clkin_queries = 0
|
||||
self.commands: list[list[str]] = []
|
||||
|
||||
def __call__(self, command: list[str]) -> subprocess.CompletedProcess:
|
||||
self.commands.append(command)
|
||||
executable = Path(command[0]).name
|
||||
if executable == "hackrf_debug.exe":
|
||||
return subprocess.CompletedProcess(command, 0, "[ 29] -> 0x26\n", "")
|
||||
if executable == "hackrf_clock.exe" and "-i" in command:
|
||||
self.clkin_queries += 1
|
||||
if self.clkin_exception_at == self.clkin_queries:
|
||||
raise subprocess.SubprocessError("mock clock query failure")
|
||||
index = min(self.clkin_queries - 1, len(self.clkin_outputs) - 1)
|
||||
return subprocess.CompletedProcess(command, 0, self.clkin_outputs[index], "")
|
||||
if executable == "hackrf_transfer.exe" and "-t" in command:
|
||||
return subprocess.CompletedProcess(command, self.tx_returncode, "", "")
|
||||
return subprocess.CompletedProcess(command, 0, "", "")
|
||||
|
||||
|
||||
class FakeClock:
|
||||
"""Монотонные часы, которые продвигаются только через sleep."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.seconds = 0.0
|
||||
self.sleeps: list[float] = []
|
||||
|
||||
def monotonic(self) -> float:
|
||||
return self.seconds
|
||||
|
||||
def sleep(self, seconds: float) -> None:
|
||||
self.sleeps.append(seconds)
|
||||
self.seconds += seconds
|
||||
|
||||
|
||||
class FakeRunningCommand:
|
||||
def __init__(self, startup_failed: bool = False, returncode: int = 0) -> None:
|
||||
self.startup_failed = startup_failed
|
||||
self.returncode = returncode
|
||||
self.terminated = False
|
||||
self.logs_closed = False
|
||||
|
||||
def poll(self) -> int | None:
|
||||
if self.startup_failed:
|
||||
return self.returncode or 1
|
||||
return self.returncode if self.terminated else None
|
||||
|
||||
def wait(self, timeout: float) -> int:
|
||||
del timeout
|
||||
self.logs_closed = True
|
||||
return self.returncode
|
||||
|
||||
def terminate(self) -> None:
|
||||
self.terminated = True
|
||||
self.logs_closed = True
|
||||
|
||||
def close_logs(self) -> None:
|
||||
self.logs_closed = True
|
||||
|
||||
|
||||
class FakeProcessStarter:
|
||||
def __init__(self, startup_failed: bool = False, returncode: int = 0) -> None:
|
||||
self.startup_failed = startup_failed
|
||||
self.returncode = returncode
|
||||
self.processes: list[FakeRunningCommand] = []
|
||||
|
||||
def __call__(self, command: list[str], stdout_path: Path, stderr_path: Path) -> FakeRunningCommand:
|
||||
stdout_path.write_text("0 overruns\n", encoding="utf-8")
|
||||
stderr_path.write_text("", encoding="utf-8")
|
||||
raw_path = Path(command[command.index("-r") + 1])
|
||||
raw_path.write_bytes(bytes(2 * bpsk.CAPTURE_SAMPLE_COUNT))
|
||||
process = FakeRunningCommand(self.startup_failed, self.returncode)
|
||||
self.processes.append(process)
|
||||
return process
|
||||
|
||||
|
||||
def fake_analysis() -> dict:
|
||||
return {
|
||||
"timing": {"refined_start_sample": 2},
|
||||
"detection": {"preamble_detected": True},
|
||||
"official_ber": 0.0,
|
||||
"symbol_snr_db": 10.0,
|
||||
"separation": 3.0,
|
||||
"channel": {"abs_h": 0.01},
|
||||
"raw": {
|
||||
"clipping_count": 0,
|
||||
"near_int8_count": 0,
|
||||
"adc_headroom_code": 100.0,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_parse_clkin_status_detected() -> None:
|
||||
assert (
|
||||
bpsk.parse_clkin_status("CLKIN status: clock signal detected\n")
|
||||
== bpsk.CLKIN_STATUS_DETECTED
|
||||
)
|
||||
|
||||
|
||||
def test_parse_clkin_status_not_detected() -> None:
|
||||
assert (
|
||||
bpsk.parse_clkin_status("CLKIN status: no clock signal detected\n")
|
||||
== bpsk.CLKIN_STATUS_NOT_DETECTED
|
||||
)
|
||||
|
||||
|
||||
def test_parse_clkin_status_empty_output_is_unknown() -> None:
|
||||
assert bpsk.parse_clkin_status("") == bpsk.CLKIN_STATUS_UNKNOWN
|
||||
|
||||
|
||||
def test_parse_clkin_status_unexpected_output_is_unknown() -> None:
|
||||
assert bpsk.parse_clkin_status("CLKIN status unavailable") == bpsk.CLKIN_STATUS_UNKNOWN
|
||||
|
||||
|
||||
def test_wait_for_slave_clock_detected_on_first_query() -> None:
|
||||
runner = FakeCommandRunner()
|
||||
clock = FakeClock()
|
||||
|
||||
result = bpsk.wait_for_slave_clock(
|
||||
runner, sleep_function=clock.sleep, monotonic_function=clock.monotonic
|
||||
)
|
||||
|
||||
assert result["state"] == bpsk.CLKIN_STATUS_DETECTED
|
||||
assert result["queries"] == 1
|
||||
assert result["time_to_detect_ms"] == pytest.approx(50.0)
|
||||
|
||||
|
||||
def test_wait_for_slave_clock_delayed_detected() -> None:
|
||||
runner = FakeCommandRunner([
|
||||
"CLKIN status: no clock signal detected\n",
|
||||
"CLKIN status: no clock signal detected\n",
|
||||
"CLKIN status: clock signal detected\n",
|
||||
])
|
||||
clock = FakeClock()
|
||||
|
||||
result = bpsk.wait_for_slave_clock(
|
||||
runner, sleep_function=clock.sleep, monotonic_function=clock.monotonic
|
||||
)
|
||||
|
||||
assert result["state"] == bpsk.CLKIN_STATUS_DETECTED
|
||||
assert [poll["state"] for poll in result["polls"]] == [
|
||||
bpsk.CLKIN_STATUS_NOT_DETECTED,
|
||||
bpsk.CLKIN_STATUS_NOT_DETECTED,
|
||||
bpsk.CLKIN_STATUS_DETECTED,
|
||||
]
|
||||
|
||||
|
||||
def test_wait_for_slave_clock_timeout_is_bounded_and_fail_closed() -> None:
|
||||
runner = FakeCommandRunner("CLKIN status: no clock signal detected\n")
|
||||
clock = FakeClock()
|
||||
guard = bpsk.TransmitGuard()
|
||||
|
||||
with pytest.raises(RuntimeError, match="TIMEOUT"):
|
||||
bpsk.common_clock_preflight(
|
||||
runner,
|
||||
guard,
|
||||
sleep_function=clock.sleep,
|
||||
monotonic_function=clock.monotonic,
|
||||
)
|
||||
|
||||
assert runner.clkin_queries == 10
|
||||
assert clock.seconds == pytest.approx(bpsk.CLOCK_TIMEOUT_SECONDS)
|
||||
assert guard.transmit_calls == 0
|
||||
assert guard.tx_allowed is False
|
||||
|
||||
|
||||
def test_wait_for_slave_clock_unknown_is_immediate_fail_closed() -> None:
|
||||
runner = FakeCommandRunner("CLKIN status unavailable\n")
|
||||
clock = FakeClock()
|
||||
guard = bpsk.TransmitGuard()
|
||||
|
||||
with pytest.raises(RuntimeError, match="UNKNOWN"):
|
||||
bpsk.common_clock_preflight(
|
||||
runner,
|
||||
guard,
|
||||
sleep_function=clock.sleep,
|
||||
monotonic_function=clock.monotonic,
|
||||
)
|
||||
|
||||
assert runner.clkin_queries == 1
|
||||
assert guard.transmit_calls == 0
|
||||
|
||||
|
||||
def test_wait_for_slave_clock_subprocess_exception_is_fail_closed() -> None:
|
||||
runner = FakeCommandRunner(clkin_exception_at=1)
|
||||
clock = FakeClock()
|
||||
guard = bpsk.TransmitGuard()
|
||||
|
||||
with pytest.raises(RuntimeError, match="ERROR"):
|
||||
bpsk.common_clock_preflight(
|
||||
runner,
|
||||
guard,
|
||||
sleep_function=clock.sleep,
|
||||
monotonic_function=clock.monotonic,
|
||||
)
|
||||
|
||||
assert guard.transmit_calls == 0
|
||||
assert guard.tx_allowed is False
|
||||
|
||||
|
||||
def test_wait_for_slave_clock_detected_at_timeout_boundary() -> None:
|
||||
runner = FakeCommandRunner([
|
||||
"CLKIN status: no clock signal detected\n",
|
||||
"CLKIN status: no clock signal detected\n",
|
||||
"CLKIN status: no clock signal detected\n",
|
||||
"CLKIN status: clock signal detected\n",
|
||||
])
|
||||
clock = FakeClock()
|
||||
|
||||
result = bpsk.wait_for_slave_clock(
|
||||
runner,
|
||||
initial_settle_s=0.05,
|
||||
poll_interval_s=0.05,
|
||||
timeout_s=0.20,
|
||||
sleep_function=clock.sleep,
|
||||
monotonic_function=clock.monotonic,
|
||||
)
|
||||
|
||||
assert result["state"] == bpsk.CLKIN_STATUS_DETECTED
|
||||
assert result["queries"] == 4
|
||||
assert result["time_to_detect_ms"] == pytest.approx(200.0)
|
||||
|
||||
|
||||
def test_clock_failure_cleanup_disables_both_outputs() -> None:
|
||||
runner = FakeCommandRunner("unexpected")
|
||||
clock = FakeClock()
|
||||
guard = bpsk.TransmitGuard()
|
||||
|
||||
with pytest.raises(RuntimeError):
|
||||
bpsk.common_clock_preflight(
|
||||
runner,
|
||||
guard,
|
||||
sleep_function=clock.sleep,
|
||||
monotonic_function=clock.monotonic,
|
||||
)
|
||||
|
||||
clock_commands = [
|
||||
command for command in runner.commands if Path(command[0]).name == "hackrf_clock.exe"
|
||||
]
|
||||
assert [command[command.index("-d") + 1] for command in clock_commands[-2:]] == [
|
||||
bpsk.MASTER_SERIAL,
|
||||
bpsk.SLAVE_SERIAL,
|
||||
]
|
||||
assert all(command[-2:] == ["-o", "0"] for command in clock_commands[-2:])
|
||||
|
||||
|
||||
def test_common_clock_preflight_is_fail_closed_and_skips_slave_rx() -> None:
|
||||
runner = FakeCommandRunner("CLKIN status: no clock signal detected\n")
|
||||
guard = bpsk.TransmitGuard()
|
||||
|
||||
with pytest.raises(RuntimeError, match="TIMEOUT"):
|
||||
bpsk.common_clock_preflight(runner, guard)
|
||||
|
||||
transfer_commands = [
|
||||
command for command in runner.commands if Path(command[0]).name == "hackrf_transfer.exe"
|
||||
]
|
||||
assert len(transfer_commands) == 1
|
||||
assert bpsk.MASTER_SERIAL in transfer_commands[0]
|
||||
assert guard.tx_allowed is False
|
||||
assert guard.transmit_calls == 0
|
||||
|
||||
|
||||
def test_common_clock_preflight_runs_both_short_rx_before_allowing_tx() -> None:
|
||||
runner = FakeCommandRunner()
|
||||
guard = bpsk.TransmitGuard()
|
||||
|
||||
result = bpsk.common_clock_preflight(runner, guard)
|
||||
|
||||
transfer_commands = [
|
||||
command for command in runner.commands if Path(command[0]).name == "hackrf_transfer.exe"
|
||||
]
|
||||
assert [command[command.index("-d") + 1] for command in transfer_commands] == [
|
||||
bpsk.MASTER_SERIAL,
|
||||
bpsk.SLAVE_SERIAL,
|
||||
]
|
||||
assert result["master_reg29"] == 0x26
|
||||
assert result["slave_reg29"] == 0x26
|
||||
assert result["slave_clkin_status"] == bpsk.CLKIN_STATUS_DETECTED
|
||||
assert guard.tx_allowed is True
|
||||
|
||||
|
||||
def test_failure_before_tx_produces_zero_tx_calls(tmp_path: Path) -> None:
|
||||
runner = FakeCommandRunner("CLKIN status: no clock signal detected\n")
|
||||
|
||||
with pytest.raises(RuntimeError, match="TIMEOUT"):
|
||||
bpsk.run_one_gain_point(24, 16, tmp_path, runner)
|
||||
|
||||
assert not any("-t" in command for command in runner.commands)
|
||||
|
||||
|
||||
def prepare_mock_point(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(bpsk, "CAPTURE_SAMPLE_COUNT", 16)
|
||||
monkeypatch.setattr(bpsk, "analyze_capture", lambda samples: fake_analysis())
|
||||
|
||||
|
||||
def test_waveform_sha_failure_produces_zero_tx_calls(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
runner = FakeCommandRunner()
|
||||
monkeypatch.setattr(bpsk, "build_tx_cs8", lambda: (_ for _ in ()).throw(ValueError("SHA")))
|
||||
|
||||
with pytest.raises(ValueError, match="SHA"):
|
||||
bpsk.run_one_gain_point(24, 24, tmp_path, runner)
|
||||
|
||||
assert runner.commands == []
|
||||
|
||||
|
||||
def test_rx_startup_failure_produces_zero_tx_calls(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
prepare_mock_point(monkeypatch)
|
||||
runner = FakeCommandRunner()
|
||||
starter = FakeProcessStarter(startup_failed=True, returncode=1)
|
||||
|
||||
with pytest.raises(RuntimeError, match="RX завершился до разрешения TX"):
|
||||
bpsk.run_one_gain_point(24, 24, tmp_path, runner, starter, lambda _: None)
|
||||
|
||||
assert not any("-t" in command for command in runner.commands)
|
||||
assert starter.processes[0].terminated is True
|
||||
|
||||
|
||||
def test_successful_experiment_has_one_tx_and_expected_order(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
prepare_mock_point(monkeypatch)
|
||||
events: list[str] = []
|
||||
runner = FakeCommandRunner()
|
||||
original_runner = runner
|
||||
|
||||
def recording_runner(command: list[str]) -> subprocess.CompletedProcess:
|
||||
if "-t" in command:
|
||||
events.append("tx_start_finish")
|
||||
return original_runner(command)
|
||||
|
||||
def preflight(command_runner: bpsk.CommandRunner, guard: bpsk.TransmitGuard) -> dict:
|
||||
del command_runner
|
||||
events.append("preflight")
|
||||
guard.allow()
|
||||
return {"passed": True}
|
||||
|
||||
class RecordingProcess(FakeRunningCommand):
|
||||
def wait(self, timeout: float) -> int:
|
||||
events.append("rx_finish")
|
||||
return super().wait(timeout)
|
||||
|
||||
def starter(command: list[str], stdout_path: Path, stderr_path: Path) -> RecordingProcess:
|
||||
events.append("rx_start")
|
||||
stdout_path.write_text("0 overruns\n", encoding="utf-8")
|
||||
stderr_path.write_text("", encoding="utf-8")
|
||||
Path(command[command.index("-r") + 1]).write_bytes(bytes(2 * bpsk.CAPTURE_SAMPLE_COUNT))
|
||||
return RecordingProcess()
|
||||
|
||||
monkeypatch.setattr(bpsk, "common_clock_preflight", preflight)
|
||||
monkeypatch.setattr(
|
||||
bpsk,
|
||||
"analyze_capture",
|
||||
lambda samples: events.append("analysis") or fake_analysis(),
|
||||
)
|
||||
|
||||
result = bpsk.run_one_gain_point(
|
||||
24, 24, tmp_path, recording_runner, starter, lambda _: None
|
||||
)
|
||||
|
||||
assert events == ["preflight", "rx_start", "tx_start_finish", "rx_finish", "analysis"]
|
||||
assert result["tx_calls"] == 1
|
||||
assert result["passed"] is True
|
||||
assert (tmp_path / "metadata.json").is_file()
|
||||
assert (tmp_path / "analysis.json").is_file()
|
||||
|
||||
|
||||
def test_tx_failure_has_no_retry_and_cleans_process_and_clocks(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
prepare_mock_point(monkeypatch)
|
||||
runner = FakeCommandRunner(tx_returncode=1)
|
||||
starter = FakeProcessStarter()
|
||||
|
||||
with pytest.raises(RuntimeError, match="Единственный TX"):
|
||||
bpsk.run_one_gain_point(24, 24, tmp_path, runner, starter, lambda _: None)
|
||||
|
||||
assert sum("-t" in command for command in runner.commands) == 1
|
||||
assert starter.processes[0].terminated is True
|
||||
clock_commands = [
|
||||
command for command in runner.commands if Path(command[0]).name == "hackrf_clock.exe"
|
||||
]
|
||||
assert [command[command.index("-d") + 1] for command in clock_commands[-2:]] == [
|
||||
bpsk.MASTER_SERIAL,
|
||||
bpsk.SLAVE_SERIAL,
|
||||
]
|
||||
assert all(command[-2:] == ["-o", "0"] for command in clock_commands[-2:])
|
||||
|
||||
|
||||
def test_analysis_failure_has_no_retry_and_cleans_process(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.setattr(bpsk, "CAPTURE_SAMPLE_COUNT", 16)
|
||||
monkeypatch.setattr(
|
||||
bpsk, "analyze_capture", lambda samples: (_ for _ in ()).throw(RuntimeError("analysis"))
|
||||
)
|
||||
runner = FakeCommandRunner()
|
||||
starter = FakeProcessStarter()
|
||||
|
||||
with pytest.raises(RuntimeError, match="analysis"):
|
||||
bpsk.run_one_gain_point(24, 24, tmp_path, runner, starter, lambda _: None)
|
||||
|
||||
assert sum("-t" in command for command in runner.commands) == 1
|
||||
assert starter.processes[0].terminated is True
|
||||
|
||||
|
||||
def test_transmit_guard_allows_exactly_one_tx() -> None:
|
||||
guard = bpsk.TransmitGuard()
|
||||
with pytest.raises(RuntimeError, match="общий clock-gate"):
|
||||
guard.register_transmit()
|
||||
guard.allow()
|
||||
guard.register_transmit()
|
||||
with pytest.raises(RuntimeError, match="Повторная передача"):
|
||||
guard.register_transmit()
|
||||
assert guard.transmit_calls == 1
|
||||
|
||||
|
||||
def test_gain_ladder_first_delta_uses_scientific_24_16_baseline(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
previous_values: list[float | None] = []
|
||||
|
||||
def fake_point(
|
||||
lna_gain_db: int,
|
||||
vga_gain_db: int,
|
||||
output_directory: Path,
|
||||
runner: bpsk.CommandRunner | None = None,
|
||||
process_starter: bpsk.ProcessStarter | None = None,
|
||||
sleep_function: bpsk.SleepFunction = bpsk.time.sleep,
|
||||
previous_abs_h: float | None = None,
|
||||
) -> dict:
|
||||
del output_directory, runner, process_starter, sleep_function
|
||||
previous_values.append(previous_abs_h)
|
||||
return {
|
||||
"lna_db": lna_gain_db,
|
||||
"vga_db": vga_gain_db,
|
||||
"tx_calls": 1,
|
||||
"passed": True,
|
||||
"stop_reason": "BPSK_PASS",
|
||||
"analysis": {"channel": {"abs_h": 0.004}},
|
||||
}
|
||||
|
||||
monkeypatch.setattr(bpsk, "run_one_gain_point", fake_point)
|
||||
|
||||
report = bpsk.run_gain_ladder(tmp_path)
|
||||
|
||||
assert previous_values == [bpsk.BASELINE_24_16_ABS_H]
|
||||
assert report["classification"] == "A"
|
||||
assert report["baseline"]["abs_h"] == bpsk.BASELINE_24_16_ABS_H
|
||||
|
||||
|
||||
def test_deterministic_waveform_sha256() -> None:
|
||||
assert hashlib.sha256(bpsk.build_tx_cs8()).hexdigest() == bpsk.EXPECTED_TX_SHA256
|
||||
|
||||
|
||||
def test_direct_invocation_smoke() -> None:
|
||||
project_root = Path(__file__).resolve().parents[1]
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(project_root / "experiments" / "lab043_hackrf_bpsk.py"), "--help"],
|
||||
cwd=project_root,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
)
|
||||
assert result.returncode == 0, result.stderr
|
||||
assert "--analyze-existing" in result.stdout
|
||||
|
||||
|
||||
def test_offline_synthetic_bpsk() -> None:
|
||||
tx = bpsk.signed_iq_bytes_to_complex64(bpsk.build_tx_cs8())
|
||||
capture = np.concatenate((
|
||||
np.zeros(80_000, dtype=np.complex64),
|
||||
tx,
|
||||
np.zeros(80_000, dtype=np.complex64),
|
||||
))
|
||||
|
||||
analysis = bpsk.analyze_capture(capture, include_oracle=False)
|
||||
|
||||
assert analysis["detection"]["preamble_detected"] is True
|
||||
assert analysis["detection"]["payload_used_for_start"] is False
|
||||
assert analysis["official_ber"] == 0.0
|
||||
assert analysis["synchronization"]["preamble_errors"] == 0
|
||||
Reference in New Issue
Block a user