Lab064: measure OTA level linearity at TX VGA 10 dB
This commit is contained in:
@@ -2780,3 +2780,84 @@ DC стабилен. Сильная аналоговая перегрузка п
|
||||
выполнялся. Следующий этап: STOP и отдельно согласовать исследование уровня RX, усилений,
|
||||
геометрии и возможной аналоговой компрессии или слабого полезного сигнала; Lab064 автоматически
|
||||
не создавать.
|
||||
|
||||
# Запись 059
|
||||
|
||||
## Дата
|
||||
|
||||
14 сентября 2026 года
|
||||
|
||||
## Тема
|
||||
|
||||
Lab064: подготовка измерения уровня и линейности, RX-only preflight
|
||||
|
||||
## Выполнено
|
||||
|
||||
После фиксации Lab063 коммитом 4437917465274257ff0ca5d3c269029da45de3ec
|
||||
подготовлен один измерительный PRBS7/BPSK/RRC waveform без полного PHY-пакета.
|
||||
Параметры: 1400 МГц, 20000 символов/с, Fs 8000000 комплексных отсчётов/с,
|
||||
цифровая несущая +50000 Гц, TX VGA 10 дБ, RX LNA/VGA 24/24 дБ, RF amplifier и
|
||||
Bias-Tee выключены. Геометрия сохранена: примерно 1,2 м, LOS в помещении, TX
|
||||
возле окна, RX на столе, одинаковая поляризация.
|
||||
|
||||
TX-файл содержит 100 мс начальных нулей, четыре одинаковых 100-мс участка с
|
||||
цифровыми уровнями −18/−12/−6/0 дБ, три нулевых промежутка по 50 мс и 100 мс
|
||||
конечных нулей. Полная длина равна 6000000 отсчётов и 0,75 с. RMS участков:
|
||||
5,556137/11,056978/22,056892/44,000457 кода; фактические шаги мощности:
|
||||
+5,977270/+5,998157/+5,998257 дБ. Максимумы |I|/|Q| не превышают 69/69,
|
||||
цифрового clipping нет. SHA-256 TX:
|
||||
fa260719659b2984fe29588238415622cd75c6daefec6cc9130ba1bf851bd3f9.
|
||||
|
||||
Подготовлены RX-first runner и офлайн-анализатор согласованной составляющей,
|
||||
остатка, комплексного h и линейности. Полные stdout/stderr и все промежуточные
|
||||
и финальные счётчики потерь сохраняются. Независимая read-only проверка нашла
|
||||
и помогла закрыть fail-closed ошибку: промежуточный underrun с финальным нулём
|
||||
теперь всегда даёт `INVALID_CAPTURE`, диагностические метрики не превращаются
|
||||
в A/B/C. Целевые тесты:13 passed. Quick gate:984 теста ядра,37 функциональных
|
||||
проверок,13 пропусков; pytest:984 passed,0 failed; diff-check PASS.
|
||||
|
||||
Один разрешённый RX-only preflight подтвердил MASTER/SLAVE reg29=0x26 и SLAVE
|
||||
CLKIN=DETECTED при первом опросе через66,793мс. Записано800000 отсчётов фона:
|
||||
полный RMS2,193193, AC RMS0,832038 кода, средние I/Q 0,933738/1,801650,
|
||||
max|I|/|Q|=4/5, clipping0, overruns0, неожиданно сильного сигнала нет. SHA-256
|
||||
RX-only записи:02d87adcbb621027290a77d5f8fd3190c299d13e89f887cd294d4af352a6ef52.
|
||||
CLKOUT выключены без ошибок, процессы до и после CLEAR.
|
||||
|
||||
Вызовов TX не было, `RF TX=0`. Lab064 не коммитируется до аппаратного
|
||||
результата, push не выполняется. Следующий этап: STOP и ожидание отдельного
|
||||
явного разрешения на единственный Lab064 TX; Lab065 не начинать.
|
||||
|
||||
После отдельного прямого разрешения пользователя выполнен ровно один RF-сеанс
|
||||
подготовленного Lab064 waveform. Перед TX MASTER/SLAVE reg29 равнялись0x26,
|
||||
SLAVE CLKIN был обнаружен при втором опросе через139,042мс. RX стартовал раньше
|
||||
TX. Автоматических и ручных повторов не было. Все две строки TX underrun и все
|
||||
четыре строки RX overrun равны нулю. RX clipping отсутствует. Записано16000000
|
||||
комплексных отсчётов за2,000000с, SHA-256
|
||||
02015891948aaee52ff81e60fdfeb391bb9b76cd9192b75972b8fedbc8690d86.
|
||||
После отключения CLKOUT процессы CLEAR, ошибок очистки нет, RF TX=0.
|
||||
|
||||
Пять окон цифрового нуля дали AC RMS0,718264…0,728217 кода; разброс фоновой
|
||||
AC-мощности равен0,119530дБ, фон стабилен. Согласованная мощность L0/L1/L2/L3
|
||||
равна0,000750751/0,002961235/0,011333436/0,045331864код². Ошибки трёх ступеней
|
||||
относительно цифрового TX равны−0,017501/−0,169270/+0,022162дБ. Общий рост
|
||||
L3/L0 равен17,809077дБ при цифровых17,973685дБ; ошибка−0,164609дБ. |h|
|
||||
составляет0,004882853/0,004872939/0,004778930/0,004791153. Верхняя ступень не
|
||||
показывает компрессии.
|
||||
|
||||
Matched/residual для L0…L3 равен−32,086493/−25,577463/−16,691669/−12,133336дБ.
|
||||
По заранее заданным критериям результат `LINEAR_BUT_WEAK`: тракт линейно
|
||||
следует цифровому уровню, но полезная согласованная составляющая даже на L3
|
||||
слабее остатка. Абсолютная RF-мощность и разделение вклада TX/эфира/RX без
|
||||
измерительного прибора не установлены.
|
||||
|
||||
Quick gate после сеанса прошёл:986 тестов ядра,37 функциональных проверок,13
|
||||
пропусков. Полный pytest:986 passed,0 failed. Lab064 не коммитировалась, push
|
||||
не выполнялся. Следующий отдельный шаг рекомендуется только после согласования:
|
||||
повтор той же лестницы при TX VGA20дБ и остальных неизменных условиях. Lab065
|
||||
не создана.
|
||||
|
||||
Независимая read-only проверка повторно вычислила SHA, положение сегментов,
|
||||
пять окон фона, matched/residual, комплексные h, ступени мощности и амплитуды,
|
||||
полное сравнение L3/L0 и двенадцать спектральных значений непосредственно из
|
||||
CS8. Расхождений нет, вердикт `чисто`. Оборудование при проверке не опрашивалось,
|
||||
RF TX остаётся равным нулю.
|
||||
|
||||
230
data/processed/lab064/capture_result.json
Normal file
230
data/processed/lab064/capture_result.json
Normal file
@@ -0,0 +1,230 @@
|
||||
{
|
||||
"lab": "Lab064",
|
||||
"stage": "SINGLE_RF_LEVEL_CAPTURE",
|
||||
"passed": true,
|
||||
"rf_tx_count": 1,
|
||||
"automatic_tx_repeats": 0,
|
||||
"point": {
|
||||
"frequency_hz": 1400000000,
|
||||
"physical_path": "MASTER TX near window -> LOS indoor -> SLAVE RX on desk",
|
||||
"path_confirmed": true,
|
||||
"tx_antenna": "existing 1400 MHz antenna near window",
|
||||
"rx_antenna": "existing 1400 MHz antenna on desk",
|
||||
"distance_m": 1.2,
|
||||
"orientation": "same polarization; fixed during preflight and RF session",
|
||||
"visibility": "LOS indoor"
|
||||
},
|
||||
"parameters": {
|
||||
"rf_frequency_hz": 1400000000,
|
||||
"sample_rate_hz": 8000000,
|
||||
"symbol_rate": 20000,
|
||||
"samples_per_symbol": 400,
|
||||
"digital_carrier_hz": 50000,
|
||||
"rrc_rolloff": 0.35,
|
||||
"rrc_span_symbols": 10,
|
||||
"prbs": "PRBS7 x^7+x^6+1, deterministic initial state",
|
||||
"full_phy_packet": false,
|
||||
"tx_vga_db": 10,
|
||||
"rx_lna_db": 24,
|
||||
"rx_vga_db": 24,
|
||||
"rf_amplifier": false,
|
||||
"bias_tee": false,
|
||||
"geometry": {
|
||||
"frequency_hz": 1400000000,
|
||||
"physical_path": "MASTER TX near window -> LOS indoor -> SLAVE RX on desk",
|
||||
"path_confirmed": true,
|
||||
"tx_antenna": "existing 1400 MHz antenna near window",
|
||||
"rx_antenna": "existing 1400 MHz antenna on desk",
|
||||
"distance_m": 1.2,
|
||||
"orientation": "same polarization; fixed during preflight and RF session",
|
||||
"visibility": "LOS indoor"
|
||||
},
|
||||
"clock_path": "10 MHz TCXO -> MASTER -> MASTER CLKOUT -> SLAVE CLKIN",
|
||||
"levels_db_relative_nominal": [
|
||||
-18.0,
|
||||
-12.0,
|
||||
-6.0,
|
||||
0.0
|
||||
]
|
||||
},
|
||||
"tx_sha256": "fa260719659b2984fe29588238415622cd75c6daefec6cc9130ba1bf851bd3f9",
|
||||
"process_before": {
|
||||
"state": "CLEAR",
|
||||
"attempts": [
|
||||
{
|
||||
"state": "CLEAR",
|
||||
"mechanism": "psutil",
|
||||
"matches": [],
|
||||
"inspected": 333,
|
||||
"errors": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"inventory": {
|
||||
"passed": true,
|
||||
"serials": [
|
||||
"000000000000000097c063dc2e5327d3",
|
||||
"000000000000000097c063dc323b97d3"
|
||||
],
|
||||
"master_serial": "000000000000000097c063dc323b97d3",
|
||||
"slave_serial": "000000000000000097c063dc2e5327d3",
|
||||
"raw_output": "hackrf_info version: 2024.02.1\nlibhackrf version: 2024.02.1 (0.9)\nFound HackRF\nIndex: 0\nSerial number: 000000000000000097c063dc323b97d3\nBoard ID Number: 4 (HackRF One)\nFirmware Version: 2024.02.1 (API:1.08)\nPart ID Number: 0xa000cb3c 0x00744f5a\nHardware Revision: r9\nHardware does not appear to have been manufactured by Great Scott Gadgets.\nHardware supported by installed firmware:\n HackRF One\n\nFound HackRF\nIndex: 1\nSerial number: 000000000000000097c063dc2e5327d3\nBoard ID Number: 4 (HackRF One)\nFirmware Version: 2024.02.1 (API:1.08)\nPart ID Number: 0xa000cb3c 0xbc4b4739\nHardware Revision: r9\nHardware does not appear to have been manufactured by Great Scott Gadgets.\nHardware supported by installed firmware:\n HackRF One"
|
||||
},
|
||||
"clock": {
|
||||
"passed": true,
|
||||
"master_reg29": 38,
|
||||
"slave_reg29": 38,
|
||||
"slave_clkin_status": "DETECTED",
|
||||
"slave_clock_wait": {
|
||||
"state": "DETECTED",
|
||||
"queries": 2,
|
||||
"time_to_detect_ms": 139.0420999377966,
|
||||
"elapsed_ms": 139.0420999377966,
|
||||
"polls": [
|
||||
{
|
||||
"query": 1,
|
||||
"start_ms": 50.13589980080724,
|
||||
"end_ms": 67.04779993742704,
|
||||
"raw": "CLKIN status: no clock signal detected",
|
||||
"state": "NOT_DETECTED",
|
||||
"exit_code": 0
|
||||
},
|
||||
{
|
||||
"query": 2,
|
||||
"start_ms": 117.27159982547164,
|
||||
"end_ms": 139.0420999377966,
|
||||
"raw": "CLKIN status: clock signal detected",
|
||||
"state": "DETECTED",
|
||||
"exit_code": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
"master_serial": "000000000000000097c063dc323b97d3",
|
||||
"slave_serial": "000000000000000097c063dc2e5327d3"
|
||||
},
|
||||
"rx_level_before_tx": {
|
||||
"samples": 65536,
|
||||
"mean_i": 0.9547576904296875,
|
||||
"mean_q": 1.83575439453125,
|
||||
"power_code2": 4.8329315185546875,
|
||||
"ac_power_code2": 0.5513750740792602,
|
||||
"rms_code": 2.1983929399801774,
|
||||
"peak_magnitude_code": 4.242640687119285,
|
||||
"max_abs_i": 3.0,
|
||||
"max_abs_q": 4.0,
|
||||
"papr_db": 5.710618637663133,
|
||||
"clipped_components_fraction": 0.0,
|
||||
"clipped_samples_fraction": 0.0,
|
||||
"saturation_detected": false,
|
||||
"analog_compression": "UNKNOWN_WITHOUT_CALIBRATION"
|
||||
},
|
||||
"tx_process": {
|
||||
"returncode": 0,
|
||||
"stdout_file": "tx_stdout.txt",
|
||||
"stderr_file": "tx_stderr.txt"
|
||||
},
|
||||
"tx_counter_summary": {
|
||||
"expected_kind": "underrun",
|
||||
"observations": [
|
||||
{
|
||||
"count": 0,
|
||||
"role": "UNKNOWN",
|
||||
"kind": "underrun",
|
||||
"text": "0 underruns",
|
||||
"character_offset": 337
|
||||
},
|
||||
{
|
||||
"count": 0,
|
||||
"role": "UNKNOWN",
|
||||
"kind": "underrun",
|
||||
"text": "0 underruns",
|
||||
"character_offset": 509
|
||||
}
|
||||
],
|
||||
"selected_counts": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"final_count": 0,
|
||||
"maximum_count": 0,
|
||||
"unparsed_loss_text": false,
|
||||
"clean": true
|
||||
},
|
||||
"rx_samples": 16000000,
|
||||
"rx_bytes": 32000000,
|
||||
"rx_duration_seconds": 2.0,
|
||||
"rx_counter_summary": {
|
||||
"expected_kind": "overrun",
|
||||
"observations": [
|
||||
{
|
||||
"count": 0,
|
||||
"role": "UNKNOWN",
|
||||
"kind": "overrun",
|
||||
"text": "0 overruns",
|
||||
"character_offset": 336
|
||||
},
|
||||
{
|
||||
"count": 0,
|
||||
"role": "UNKNOWN",
|
||||
"kind": "overrun",
|
||||
"text": "0 overruns",
|
||||
"character_offset": 458
|
||||
},
|
||||
{
|
||||
"count": 0,
|
||||
"role": "UNKNOWN",
|
||||
"kind": "overrun",
|
||||
"text": "0 overruns",
|
||||
"character_offset": 580
|
||||
},
|
||||
{
|
||||
"count": 0,
|
||||
"role": "UNKNOWN",
|
||||
"kind": "overrun",
|
||||
"text": "0 overruns",
|
||||
"character_offset": 751
|
||||
}
|
||||
],
|
||||
"selected_counts": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"final_count": 0,
|
||||
"maximum_count": 0,
|
||||
"unparsed_loss_text": false,
|
||||
"clean": true
|
||||
},
|
||||
"rx_sha256": "02015891948aaee52ff81e60fdfeb391bb9b76cd9192b75972b8fedbc8690d86",
|
||||
"rx_level": {
|
||||
"samples": 16000000,
|
||||
"mean_i": 0.9548163125,
|
||||
"mean_q": 1.83674125,
|
||||
"power_code2": 5.2568038125,
|
||||
"ac_power_code2": 0.9715112024323407,
|
||||
"rms_code": 2.2927720803647276,
|
||||
"peak_magnitude_code": 8.602325267042627,
|
||||
"max_abs_i": 7.0,
|
||||
"max_abs_q": 8.0,
|
||||
"papr_db": 11.485099505760791,
|
||||
"clipped_components_fraction": 0.0,
|
||||
"clipped_samples_fraction": 0.0,
|
||||
"saturation_detected": false,
|
||||
"analog_compression": "UNKNOWN_WITHOUT_CALIBRATION"
|
||||
},
|
||||
"rx_clipping": false,
|
||||
"process_after": {
|
||||
"state": "CLEAR",
|
||||
"attempts": [
|
||||
{
|
||||
"state": "CLEAR",
|
||||
"mechanism": "psutil",
|
||||
"matches": [],
|
||||
"inspected": 333,
|
||||
"errors": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"cleanup_errors": []
|
||||
}
|
||||
164
data/processed/lab064/preflight_result.json
Normal file
164
data/processed/lab064/preflight_result.json
Normal file
@@ -0,0 +1,164 @@
|
||||
{
|
||||
"lab": "Lab064",
|
||||
"stage": "COMMON_CLOCK_AND_RX_ONLY_PREFLIGHT",
|
||||
"passed": true,
|
||||
"rf_tx_count": 0,
|
||||
"stop_before_tx": true,
|
||||
"point": {
|
||||
"frequency_hz": 1400000000,
|
||||
"physical_path": "MASTER TX near window -> LOS indoor -> SLAVE RX on desk",
|
||||
"path_confirmed": true,
|
||||
"tx_antenna": "existing 1400 MHz antenna near window",
|
||||
"rx_antenna": "existing 1400 MHz antenna on desk",
|
||||
"distance_m": 1.2,
|
||||
"orientation": "same polarization; fixed during preflight and RF session",
|
||||
"visibility": "LOS indoor"
|
||||
},
|
||||
"parameters": {
|
||||
"rf_frequency_hz": 1400000000,
|
||||
"sample_rate_hz": 8000000,
|
||||
"symbol_rate": 20000,
|
||||
"samples_per_symbol": 400,
|
||||
"digital_carrier_hz": 50000,
|
||||
"rrc_rolloff": 0.35,
|
||||
"rrc_span_symbols": 10,
|
||||
"prbs": "PRBS7 x^7+x^6+1, deterministic initial state",
|
||||
"full_phy_packet": false,
|
||||
"tx_vga_db": 10,
|
||||
"rx_lna_db": 24,
|
||||
"rx_vga_db": 24,
|
||||
"rf_amplifier": false,
|
||||
"bias_tee": false,
|
||||
"geometry": {
|
||||
"frequency_hz": 1400000000,
|
||||
"physical_path": "MASTER TX near window -> LOS indoor -> SLAVE RX on desk",
|
||||
"path_confirmed": true,
|
||||
"tx_antenna": "existing 1400 MHz antenna near window",
|
||||
"rx_antenna": "existing 1400 MHz antenna on desk",
|
||||
"distance_m": 1.2,
|
||||
"orientation": "same polarization; fixed during preflight and RF session",
|
||||
"visibility": "LOS indoor"
|
||||
},
|
||||
"clock_path": "10 MHz TCXO -> MASTER -> MASTER CLKOUT -> SLAVE CLKIN",
|
||||
"levels_db_relative_nominal": [
|
||||
-18.0,
|
||||
-12.0,
|
||||
-6.0,
|
||||
0.0
|
||||
]
|
||||
},
|
||||
"tx_sha256": "fa260719659b2984fe29588238415622cd75c6daefec6cc9130ba1bf851bd3f9",
|
||||
"process_before": {
|
||||
"state": "CLEAR",
|
||||
"attempts": [
|
||||
{
|
||||
"state": "CLEAR",
|
||||
"mechanism": "psutil",
|
||||
"matches": [],
|
||||
"inspected": 329,
|
||||
"errors": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"inventory": {
|
||||
"passed": true,
|
||||
"serials": [
|
||||
"000000000000000097c063dc2e5327d3",
|
||||
"000000000000000097c063dc323b97d3"
|
||||
],
|
||||
"master_serial": "000000000000000097c063dc323b97d3",
|
||||
"slave_serial": "000000000000000097c063dc2e5327d3",
|
||||
"raw_output": "hackrf_info version: 2024.02.1\nlibhackrf version: 2024.02.1 (0.9)\nFound HackRF\nIndex: 0\nSerial number: 000000000000000097c063dc323b97d3\nBoard ID Number: 4 (HackRF One)\nFirmware Version: 2024.02.1 (API:1.08)\nPart ID Number: 0xa000cb3c 0x00744f5a\nHardware Revision: r9\nHardware does not appear to have been manufactured by Great Scott Gadgets.\nHardware supported by installed firmware:\n HackRF One\n\nFound HackRF\nIndex: 1\nSerial number: 000000000000000097c063dc2e5327d3\nBoard ID Number: 4 (HackRF One)\nFirmware Version: 2024.02.1 (API:1.08)\nPart ID Number: 0xa000cb3c 0xbc4b4739\nHardware Revision: r9\nHardware does not appear to have been manufactured by Great Scott Gadgets.\nHardware supported by installed firmware:\n HackRF One"
|
||||
},
|
||||
"clock": {
|
||||
"passed": true,
|
||||
"master_reg29": 38,
|
||||
"slave_reg29": 38,
|
||||
"slave_clkin_status": "DETECTED",
|
||||
"slave_clock_wait": {
|
||||
"state": "DETECTED",
|
||||
"queries": 1,
|
||||
"time_to_detect_ms": 66.79299985989928,
|
||||
"elapsed_ms": 66.79299985989928,
|
||||
"polls": [
|
||||
{
|
||||
"query": 1,
|
||||
"start_ms": 50.07890006527305,
|
||||
"end_ms": 66.79299985989928,
|
||||
"raw": "CLKIN status: clock signal detected",
|
||||
"state": "DETECTED",
|
||||
"exit_code": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
"master_serial": "000000000000000097c063dc323b97d3",
|
||||
"slave_serial": "000000000000000097c063dc2e5327d3"
|
||||
},
|
||||
"rx_process": {
|
||||
"returncode": 0,
|
||||
"stdout": "",
|
||||
"stderr": "call hackrf_set_sample_rate(8000000 Hz/8.000 MHz)\ncall hackrf_set_hw_sync_mode(0)\ncall hackrf_set_freq(1400000000 Hz/1400.000 MHz)\ncall hackrf_set_amp_enable(0)\ncall hackrf_set_antenna_enable(0)\nsamples_to_xfer 800000/0Mio\nStop with Ctrl-C\n 1.8 MiB / 0.117 sec = 15.6 MiB/second, average power -35.3 dBfs, 9792 bytes free in buffer, 0 overruns, longest 0 bytes\n\nExiting...\nTotal time: 0.11842 s\nhackrf_stop_rx() done\nTransfer statistics:\n1892448 bytes transferred by M0\n1867776 bytes transferred by M4\n0 overruns, longest 0 bytes\nhackrf_close() done\nhackrf_exit() done\nfclose() done\nexit\n"
|
||||
},
|
||||
"rx_counter_summary": {
|
||||
"expected_kind": "overrun",
|
||||
"observations": [
|
||||
{
|
||||
"count": 0,
|
||||
"role": "UNKNOWN",
|
||||
"kind": "overrun",
|
||||
"text": "0 overruns",
|
||||
"character_offset": 333
|
||||
},
|
||||
{
|
||||
"count": 0,
|
||||
"role": "UNKNOWN",
|
||||
"kind": "overrun",
|
||||
"text": "0 overruns",
|
||||
"character_offset": 502
|
||||
}
|
||||
],
|
||||
"selected_counts": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"final_count": 0,
|
||||
"maximum_count": 0,
|
||||
"unparsed_loss_text": false,
|
||||
"clean": true
|
||||
},
|
||||
"rx_level": {
|
||||
"samples": 800000,
|
||||
"mean_i": 0.9337375,
|
||||
"mean_q": 1.80165,
|
||||
"power_code2": 4.810095,
|
||||
"ac_power_code2": 0.6922865585937501,
|
||||
"rms_code": 2.1931928779749397,
|
||||
"peak_magnitude_code": 5.385164807134504,
|
||||
"max_abs_i": 4.0,
|
||||
"max_abs_q": 5.0,
|
||||
"papr_db": 7.802443440680863,
|
||||
"clipped_components_fraction": 0.0,
|
||||
"clipped_samples_fraction": 0.0,
|
||||
"saturation_detected": false,
|
||||
"analog_compression": "UNKNOWN_WITHOUT_CALIBRATION",
|
||||
"symbol_energy": "NOT_APPLICABLE_TO_BACKGROUND",
|
||||
"ac_rms_code": 0.8320375944593791,
|
||||
"unexpected_strong_signal": false,
|
||||
"strong_signal_rule": "AC RMS >= 20.0 code or max |I|/|Q| >= 100.0 code"
|
||||
},
|
||||
"rx_sha256": "02d87adcbb621027290a77d5f8fd3190c299d13e89f887cd294d4af352a6ef52",
|
||||
"rx_overruns": 0,
|
||||
"process_after": {
|
||||
"state": "CLEAR",
|
||||
"attempts": [
|
||||
{
|
||||
"state": "CLEAR",
|
||||
"mechanism": "psutil",
|
||||
"matches": [],
|
||||
"inspected": 329,
|
||||
"errors": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"cleanup_errors": []
|
||||
}
|
||||
221
data/processed/lab064/preparation.json
Normal file
221
data/processed/lab064/preparation.json
Normal file
@@ -0,0 +1,221 @@
|
||||
{
|
||||
"lab": "Lab064",
|
||||
"stage": "PREPARATION_ONLY",
|
||||
"rf_tx_count": 0,
|
||||
"head": "4437917465274257ff0ca5d3c269029da45de3ec",
|
||||
"parameters": {
|
||||
"rf_frequency_hz": 1400000000,
|
||||
"sample_rate_hz": 8000000,
|
||||
"symbol_rate": 20000,
|
||||
"samples_per_symbol": 400,
|
||||
"digital_carrier_hz": 50000,
|
||||
"rrc_rolloff": 0.35,
|
||||
"rrc_span_symbols": 10,
|
||||
"prbs": "PRBS7 x^7+x^6+1, deterministic initial state",
|
||||
"full_phy_packet": false,
|
||||
"tx_vga_db": 10,
|
||||
"rx_lna_db": 24,
|
||||
"rx_vga_db": 24,
|
||||
"rf_amplifier": false,
|
||||
"bias_tee": false,
|
||||
"geometry": {
|
||||
"frequency_hz": 1400000000,
|
||||
"physical_path": "MASTER TX near window -> LOS indoor -> SLAVE RX on desk",
|
||||
"path_confirmed": true,
|
||||
"tx_antenna": "existing 1400 MHz antenna near window",
|
||||
"rx_antenna": "existing 1400 MHz antenna on desk",
|
||||
"distance_m": 1.2,
|
||||
"orientation": "same polarization; fixed during preflight and RF session",
|
||||
"visibility": "LOS indoor"
|
||||
},
|
||||
"clock_path": "10 MHz TCXO -> MASTER -> MASTER CLKOUT -> SLAVE CLKIN",
|
||||
"levels_db_relative_nominal": [
|
||||
-18.0,
|
||||
-12.0,
|
||||
-6.0,
|
||||
0.0
|
||||
]
|
||||
},
|
||||
"segments": [
|
||||
{
|
||||
"label": "L0",
|
||||
"level_db": -18.0,
|
||||
"start_sample": 800000,
|
||||
"samples": 800000,
|
||||
"duration_seconds": 0.1,
|
||||
"analysis_start_sample": 808400,
|
||||
"analysis_samples": 783200,
|
||||
"metrics": {
|
||||
"samples": 800000,
|
||||
"mean_i": -9.625e-05,
|
||||
"mean_q": 2.75e-05,
|
||||
"power_code2": 30.87065625,
|
||||
"ac_power_code2": 30.870656239979688,
|
||||
"rms_code": 5.556136809870686,
|
||||
"peak_magnitude_code": 9.219544457292887,
|
||||
"max_abs_i": 9.0,
|
||||
"max_abs_q": 9.0,
|
||||
"papr_db": 4.398730638796093,
|
||||
"clipped_components_fraction": 0.0,
|
||||
"clipped_samples_fraction": 0.0,
|
||||
"symbol_energy_code2_seconds": 0.0015435328125,
|
||||
"requested_relative_power_db": -18.0,
|
||||
"tx_rms_code": 5.556136809870686,
|
||||
"tx_peak_abs_i_code": 9.0,
|
||||
"tx_peak_abs_q_code": 9.0,
|
||||
"tx_peak_magnitude_code": 9.219544457292887,
|
||||
"digital_clipping": false,
|
||||
"actual_relative_power_db": -17.97368512020099
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "L1",
|
||||
"level_db": -12.0,
|
||||
"start_sample": 2000000,
|
||||
"samples": 800000,
|
||||
"duration_seconds": 0.1,
|
||||
"analysis_start_sample": 2008400,
|
||||
"analysis_samples": 783200,
|
||||
"metrics": {
|
||||
"samples": 800000,
|
||||
"mean_i": -5e-05,
|
||||
"mean_q": 5e-05,
|
||||
"power_code2": 122.2567675,
|
||||
"ac_power_code2": 122.25676749499999,
|
||||
"rms_code": 11.056978226441435,
|
||||
"peak_magnitude_code": 17.804493814764857,
|
||||
"max_abs_i": 17.0,
|
||||
"max_abs_q": 17.0,
|
||||
"papr_db": 4.1378635346865025,
|
||||
"clipped_components_fraction": 0.0,
|
||||
"clipped_samples_fraction": 0.0,
|
||||
"symbol_energy_code2_seconds": 0.006112838375,
|
||||
"requested_relative_power_db": -12.0,
|
||||
"tx_rms_code": 11.056978226441435,
|
||||
"tx_peak_abs_i_code": 17.0,
|
||||
"tx_peak_abs_q_code": 17.0,
|
||||
"tx_peak_magnitude_code": 17.804493814764857,
|
||||
"digital_clipping": false,
|
||||
"actual_relative_power_db": -11.99641465105681
|
||||
},
|
||||
"tx_input_difference_from_previous_db": 5.9772704691441785
|
||||
},
|
||||
{
|
||||
"label": "L2",
|
||||
"level_db": -6.0,
|
||||
"start_sample": 3200000,
|
||||
"samples": 800000,
|
||||
"duration_seconds": 0.1,
|
||||
"analysis_start_sample": 3208400,
|
||||
"analysis_samples": 783200,
|
||||
"metrics": {
|
||||
"samples": 800000,
|
||||
"mean_i": 5.625e-05,
|
||||
"mean_q": 2.625e-05,
|
||||
"power_code2": 486.5064875,
|
||||
"ac_power_code2": 486.50648749614686,
|
||||
"rms_code": 22.05689206347984,
|
||||
"peak_magnitude_code": 35.12833614050059,
|
||||
"max_abs_i": 34.0,
|
||||
"max_abs_q": 35.0,
|
||||
"papr_db": 4.0422652379446395,
|
||||
"clipped_components_fraction": 0.0,
|
||||
"clipped_samples_fraction": 0.0,
|
||||
"symbol_energy_code2_seconds": 0.024325324375,
|
||||
"requested_relative_power_db": -6.0,
|
||||
"tx_rms_code": 22.05689206347984,
|
||||
"tx_peak_abs_i_code": 34.0,
|
||||
"tx_peak_abs_q_code": 35.0,
|
||||
"tx_peak_magnitude_code": 35.12833614050059,
|
||||
"digital_clipping": false,
|
||||
"actual_relative_power_db": -5.998257379520232
|
||||
},
|
||||
"tx_input_difference_from_previous_db": 5.998157271536577
|
||||
},
|
||||
{
|
||||
"label": "L3",
|
||||
"level_db": 0.0,
|
||||
"start_sample": 4400000,
|
||||
"samples": 800000,
|
||||
"duration_seconds": 0.1,
|
||||
"analysis_start_sample": 4408400,
|
||||
"analysis_samples": 783200,
|
||||
"metrics": {
|
||||
"samples": 800000,
|
||||
"mean_i": 8.25e-05,
|
||||
"mean_q": 0.00011125,
|
||||
"power_code2": 1936.04021375,
|
||||
"ac_power_code2": 1936.0402137308172,
|
||||
"rms_code": 44.00045697205883,
|
||||
"peak_magnitude_code": 69.6419413859206,
|
||||
"max_abs_i": 69.0,
|
||||
"max_abs_q": 69.0,
|
||||
"papr_db": 3.9882736474748155,
|
||||
"clipped_components_fraction": 0.0,
|
||||
"clipped_samples_fraction": 0.0,
|
||||
"symbol_energy_code2_seconds": 0.0968020106875,
|
||||
"requested_relative_power_db": 0.0,
|
||||
"tx_rms_code": 44.00045697205883,
|
||||
"tx_peak_abs_i_code": 69.0,
|
||||
"tx_peak_abs_q_code": 69.0,
|
||||
"tx_peak_magnitude_code": 69.6419413859206,
|
||||
"digital_clipping": false,
|
||||
"actual_relative_power_db": 0.0
|
||||
},
|
||||
"tx_input_difference_from_previous_db": 5.998257379520232
|
||||
}
|
||||
],
|
||||
"leading_zero_samples": 800000,
|
||||
"gap_samples": 400000,
|
||||
"trailing_zero_samples": 800000,
|
||||
"tx_samples": 6000000,
|
||||
"tx_bytes": 12000000,
|
||||
"tx_duration_seconds": 0.75,
|
||||
"tx_sha256": "fa260719659b2984fe29588238415622cd75c6daefec6cc9130ba1bf851bd3f9",
|
||||
"rx_samples": 16000000,
|
||||
"rx_duration_seconds": 2.0,
|
||||
"rx_lead_seconds": 0.5,
|
||||
"automatic_retry": false,
|
||||
"tx_command_prepared_not_run": [
|
||||
"C:\\Users\\user\\Tools\\HackRF\\Library\\bin\\hackrf_transfer.exe",
|
||||
"-d",
|
||||
"000000000000000097c063dc323b97d3",
|
||||
"-t",
|
||||
"C:\\Users\\user\\Desktop\\projects\\SDR_Rover\\data\\raw\\lab064\\tx_level_staircase.cs8",
|
||||
"-f",
|
||||
"1400000000",
|
||||
"-s",
|
||||
"8000000",
|
||||
"-n",
|
||||
"6000000",
|
||||
"-x",
|
||||
"10",
|
||||
"-a",
|
||||
"0",
|
||||
"-p",
|
||||
"0",
|
||||
"-B"
|
||||
],
|
||||
"rx_command_prepared_not_run": [
|
||||
"C:\\Users\\user\\Tools\\HackRF\\Library\\bin\\hackrf_transfer.exe",
|
||||
"-d",
|
||||
"000000000000000097c063dc2e5327d3",
|
||||
"-r",
|
||||
"C:\\Users\\user\\Desktop\\projects\\SDR_Rover\\data\\raw\\lab064\\rx_stream.cs8",
|
||||
"-f",
|
||||
"1400000000",
|
||||
"-s",
|
||||
"8000000",
|
||||
"-n",
|
||||
"16000000",
|
||||
"-l",
|
||||
"24",
|
||||
"-g",
|
||||
"24",
|
||||
"-a",
|
||||
"0",
|
||||
"-p",
|
||||
"0",
|
||||
"-B"
|
||||
]
|
||||
}
|
||||
425
data/processed/lab064/rf_result.json
Normal file
425
data/processed/lab064/rf_result.json
Normal file
@@ -0,0 +1,425 @@
|
||||
{
|
||||
"lab": "Lab064",
|
||||
"stage": "OFFLINE_RF_LEVEL_LINEARITY_ANALYSIS",
|
||||
"packet_receiver_used": false,
|
||||
"location": {
|
||||
"start_sample": 4123520,
|
||||
"correlation": 0.0888256100450642,
|
||||
"coarse_cfo_hz": 0.0,
|
||||
"stride": 32
|
||||
},
|
||||
"rows": [
|
||||
{
|
||||
"label": "L0",
|
||||
"level_db": -18.0,
|
||||
"rx_start_sample": 4923521,
|
||||
"alignment_adjustment_samples": 1,
|
||||
"full_iq": {
|
||||
"samples": 800000,
|
||||
"mean_i": 0.94972375,
|
||||
"mean_q": 1.8355175,
|
||||
"power_code2": 5.18564125,
|
||||
"ac_power_code2": 0.9145415558796861,
|
||||
"rms_code": 2.2772003095907043,
|
||||
"peak_magnitude_code": 7.0710678118654755,
|
||||
"max_abs_i": 6.0,
|
||||
"max_abs_q": 7.0,
|
||||
"papr_db": 9.841675359702911,
|
||||
"clipped_components_fraction": 0.0,
|
||||
"clipped_samples_fraction": 0.0,
|
||||
"symbol_energy_code2_seconds": 0.00025928206249999997,
|
||||
"ac_rms_code": 0.9563166608815754
|
||||
},
|
||||
"background_label": "INITIAL_GUARD",
|
||||
"background": {
|
||||
"samples": 160000,
|
||||
"mean_i": 0.940225,
|
||||
"mean_q": 1.8409875,
|
||||
"power_code2": 4.7947875,
|
||||
"ac_power_code2": 0.5215294742187488,
|
||||
"rms_code": 2.189700321961889,
|
||||
"peak_magnitude_code": 4.242640687119285,
|
||||
"max_abs_i": 3.0,
|
||||
"max_abs_q": 4.0,
|
||||
"papr_db": 5.7450314065030845,
|
||||
"clipped_components_fraction": 0.0,
|
||||
"clipped_samples_fraction": 0.0,
|
||||
"symbol_energy_code2_seconds": 0.000239739375,
|
||||
"ac_rms_code": 0.7221699759881665
|
||||
},
|
||||
"background_window_start_sample": 4443520,
|
||||
"matched_signal_power": 0.0007507510158784873,
|
||||
"matched_signal_amplitude_rms": 0.027399836055686307,
|
||||
"residual_power": 1.2137948040249413,
|
||||
"residual_to_matched_amplitude_ratio": 40.209129659782626,
|
||||
"matched_to_residual_db": -32.08649345507262,
|
||||
"complex_gain_h": {
|
||||
"real": 0.0018760642981310563,
|
||||
"imag": -0.0045080629571906915,
|
||||
"phase_rad_at_active_start": -1.1764391150184978
|
||||
},
|
||||
"gain_abs": 0.00488285253481168,
|
||||
"gain_power_abs_h_squared": 2.384224887671685e-05,
|
||||
"cfo_hz": 0.0957717781274782,
|
||||
"residual_spectrum": {
|
||||
"residual_band_h1_power_code2": 0.0029441230361582733,
|
||||
"residual_band_h2_power_code2": 0.002517548539085255,
|
||||
"residual_band_h3_power_code2": 0.0017878979213816356,
|
||||
"interpretation": "Diagnostic residual bands at 50/100/150 kHz; not calibrated RF harmonic power"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "L1",
|
||||
"level_db": -12.0,
|
||||
"rx_start_sample": 6123541,
|
||||
"alignment_adjustment_samples": 21,
|
||||
"full_iq": {
|
||||
"samples": 800000,
|
||||
"mean_i": 0.9560375,
|
||||
"mean_q": 1.8325725,
|
||||
"power_code2": 5.2504775,
|
||||
"ac_power_code2": 0.9781478308375009,
|
||||
"rms_code": 2.2913920441513276,
|
||||
"peak_magnitude_code": 7.211102550927978,
|
||||
"max_abs_i": 6.0,
|
||||
"max_abs_q": 7.0,
|
||||
"papr_db": 9.958045419078847,
|
||||
"clipped_components_fraction": 0.0,
|
||||
"clipped_samples_fraction": 0.0,
|
||||
"symbol_energy_code2_seconds": 0.000262523875,
|
||||
"ac_rms_code": 0.9890135645366553
|
||||
},
|
||||
"background_label": "GAP_L0_L1",
|
||||
"background": {
|
||||
"samples": 160000,
|
||||
"mean_i": 0.95144375,
|
||||
"mean_q": 1.8329875,
|
||||
"power_code2": 4.78826875,
|
||||
"ac_power_code2": 0.5231803654296865,
|
||||
"rms_code": 2.1882113129220406,
|
||||
"peak_magnitude_code": 4.242640687119285,
|
||||
"max_abs_i": 3.0,
|
||||
"max_abs_q": 4.0,
|
||||
"papr_db": 5.7509398714368505,
|
||||
"clipped_components_fraction": 0.0,
|
||||
"clipped_samples_fraction": 0.0,
|
||||
"symbol_energy_code2_seconds": 0.00023941343750000002,
|
||||
"ac_rms_code": 0.7233120802459243
|
||||
},
|
||||
"background_window_start_sample": 5883520,
|
||||
"matched_signal_power": 0.0029612351609719875,
|
||||
"matched_signal_amplitude_rms": 0.054417232206094304,
|
||||
"residual_power": 1.0695946032022712,
|
||||
"residual_to_matched_amplitude_ratio": 19.005231005856622,
|
||||
"matched_to_residual_db": -25.577463055166668,
|
||||
"complex_gain_h": {
|
||||
"real": 0.004471119012843702,
|
||||
"imag": -0.001937685853593986,
|
||||
"phase_rad_at_active_start": -0.40894566669212534
|
||||
},
|
||||
"gain_abs": 0.0048729387123409105,
|
||||
"gain_power_abs_h_squared": 2.374553169423069e-05,
|
||||
"cfo_hz": -0.16857111258676097,
|
||||
"residual_spectrum": {
|
||||
"residual_band_h1_power_code2": 0.0037442904303572424,
|
||||
"residual_band_h2_power_code2": 0.0029946268848091966,
|
||||
"residual_band_h3_power_code2": 0.0022268690579244723,
|
||||
"interpretation": "Diagnostic residual bands at 50/100/150 kHz; not calibrated RF harmonic power"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "L2",
|
||||
"level_db": -6.0,
|
||||
"rx_start_sample": 7323533,
|
||||
"alignment_adjustment_samples": 13,
|
||||
"full_iq": {
|
||||
"samples": 800000,
|
||||
"mean_i": 0.9631825,
|
||||
"mean_q": 1.84173875,
|
||||
"power_code2": 5.17589875,
|
||||
"ac_power_code2": 0.856176598442187,
|
||||
"rms_code": 2.275060164039624,
|
||||
"peak_magnitude_code": 7.280109889280518,
|
||||
"max_abs_i": 6.0,
|
||||
"max_abs_q": 7.0,
|
||||
"papr_db": 10.102900974475599,
|
||||
"clipped_components_fraction": 0.0,
|
||||
"clipped_samples_fraction": 0.0,
|
||||
"symbol_energy_code2_seconds": 0.0002587949375,
|
||||
"ac_rms_code": 0.925298113281437
|
||||
},
|
||||
"background_label": "GAP_L1_L2",
|
||||
"background": {
|
||||
"samples": 160000,
|
||||
"mean_i": 0.9560375,
|
||||
"mean_q": 1.8575625,
|
||||
"power_code2": 4.88045,
|
||||
"ac_power_code2": 0.5159038571874985,
|
||||
"rms_code": 2.209174053803819,
|
||||
"peak_magnitude_code": 4.242640687119285,
|
||||
"max_abs_i": 3.0,
|
||||
"max_abs_q": 4.0,
|
||||
"papr_db": 5.668126373000443,
|
||||
"clipped_components_fraction": 0.0,
|
||||
"clipped_samples_fraction": 0.0,
|
||||
"symbol_energy_code2_seconds": 0.0002440225,
|
||||
"ac_rms_code": 0.7182644757939087
|
||||
},
|
||||
"background_window_start_sample": 7083520,
|
||||
"matched_signal_power": 0.011333436190702588,
|
||||
"matched_signal_amplitude_rms": 0.10645861257175292,
|
||||
"residual_power": 0.5290886880136152,
|
||||
"residual_to_matched_amplitude_ratio": 6.832559719073439,
|
||||
"matched_to_residual_db": -16.691668725481204,
|
||||
"complex_gain_h": {
|
||||
"real": 0.0032772531007889995,
|
||||
"imag": -0.003478187284280655,
|
||||
"phase_rad_at_active_start": -0.8151334504253348
|
||||
},
|
||||
"gain_abs": 0.00477893028523777,
|
||||
"gain_power_abs_h_squared": 2.283817467116275e-05,
|
||||
"cfo_hz": 0.006742170898886185,
|
||||
"residual_spectrum": {
|
||||
"residual_band_h1_power_code2": 0.004466260208758569,
|
||||
"residual_band_h2_power_code2": 0.0030543665029441423,
|
||||
"residual_band_h3_power_code2": 0.0020345488796523733,
|
||||
"interpretation": "Diagnostic residual bands at 50/100/150 kHz; not calibrated RF harmonic power"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "L3",
|
||||
"level_db": 0.0,
|
||||
"rx_start_sample": 8523534,
|
||||
"alignment_adjustment_samples": 14,
|
||||
"full_iq": {
|
||||
"samples": 800000,
|
||||
"mean_i": 0.9575575,
|
||||
"mean_q": 1.8561875,
|
||||
"power_code2": 5.48103,
|
||||
"ac_power_code2": 1.1186815990375,
|
||||
"rms_code": 2.3411599689043037,
|
||||
"peak_magnitude_code": 7.280109889280518,
|
||||
"max_abs_i": 6.0,
|
||||
"max_abs_q": 7.0,
|
||||
"papr_db": 9.85413690444334,
|
||||
"clipped_components_fraction": 0.0,
|
||||
"clipped_samples_fraction": 0.0,
|
||||
"symbol_energy_code2_seconds": 0.0002740515,
|
||||
"ac_rms_code": 1.0576774551050523
|
||||
},
|
||||
"background_label": "GAP_L2_L3",
|
||||
"background": {
|
||||
"samples": 160000,
|
||||
"mean_i": 0.97165,
|
||||
"mean_q": 1.8424125,
|
||||
"power_code2": 4.8607875,
|
||||
"ac_power_code2": 0.5221999573437506,
|
||||
"rms_code": 2.204719369897221,
|
||||
"peak_magnitude_code": 4.47213595499958,
|
||||
"max_abs_i": 3.0,
|
||||
"max_abs_q": 4.0,
|
||||
"papr_db": 6.14323360311441,
|
||||
"clipped_components_fraction": 0.0,
|
||||
"clipped_samples_fraction": 0.0,
|
||||
"symbol_energy_code2_seconds": 0.00024303937499999998,
|
||||
"ac_rms_code": 0.7226340410911671
|
||||
},
|
||||
"background_window_start_sample": 8283520,
|
||||
"matched_signal_power": 0.0453318643989967,
|
||||
"matched_signal_amplitude_rms": 0.21291280938214285,
|
||||
"residual_power": 0.740861773845582,
|
||||
"residual_to_matched_amplitude_ratio": 4.042656143280289,
|
||||
"matched_to_residual_db": -12.133336061267785,
|
||||
"complex_gain_h": {
|
||||
"real": 0.003384387504328541,
|
||||
"imag": -0.003391322182689881,
|
||||
"phase_rad_at_active_start": -0.7864216245934512
|
||||
},
|
||||
"gain_abs": 0.004791152776343041,
|
||||
"gain_power_abs_h_squared": 2.295514492625963e-05,
|
||||
"cfo_hz": -0.07606695286129402,
|
||||
"residual_spectrum": {
|
||||
"residual_band_h1_power_code2": 0.009576142863317505,
|
||||
"residual_band_h2_power_code2": 0.004468597337512337,
|
||||
"residual_band_h3_power_code2": 0.002725169628283891,
|
||||
"interpretation": "Diagnostic residual bands at 50/100/150 kHz; not calibrated RF harmonic power"
|
||||
}
|
||||
}
|
||||
],
|
||||
"background_windows": [
|
||||
{
|
||||
"label": "INITIAL_GUARD",
|
||||
"tx_relative_start_sample": 320000,
|
||||
"samples": 160000,
|
||||
"duration_seconds": 0.02,
|
||||
"rx_start_sample": 4443520,
|
||||
"metrics": {
|
||||
"samples": 160000,
|
||||
"mean_i": 0.940225,
|
||||
"mean_q": 1.8409875,
|
||||
"power_code2": 4.7947875,
|
||||
"ac_power_code2": 0.5215294742187488,
|
||||
"rms_code": 2.189700321961889,
|
||||
"peak_magnitude_code": 4.242640687119285,
|
||||
"max_abs_i": 3.0,
|
||||
"max_abs_q": 4.0,
|
||||
"papr_db": 5.7450314065030845,
|
||||
"clipped_components_fraction": 0.0,
|
||||
"clipped_samples_fraction": 0.0,
|
||||
"symbol_energy_code2_seconds": 0.000239739375,
|
||||
"ac_rms_code": 0.7221699759881665
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "GAP_L0_L1",
|
||||
"tx_relative_start_sample": 1760000,
|
||||
"samples": 160000,
|
||||
"duration_seconds": 0.02,
|
||||
"rx_start_sample": 5883520,
|
||||
"metrics": {
|
||||
"samples": 160000,
|
||||
"mean_i": 0.95144375,
|
||||
"mean_q": 1.8329875,
|
||||
"power_code2": 4.78826875,
|
||||
"ac_power_code2": 0.5231803654296865,
|
||||
"rms_code": 2.1882113129220406,
|
||||
"peak_magnitude_code": 4.242640687119285,
|
||||
"max_abs_i": 3.0,
|
||||
"max_abs_q": 4.0,
|
||||
"papr_db": 5.7509398714368505,
|
||||
"clipped_components_fraction": 0.0,
|
||||
"clipped_samples_fraction": 0.0,
|
||||
"symbol_energy_code2_seconds": 0.00023941343750000002,
|
||||
"ac_rms_code": 0.7233120802459243
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "GAP_L1_L2",
|
||||
"tx_relative_start_sample": 2960000,
|
||||
"samples": 160000,
|
||||
"duration_seconds": 0.02,
|
||||
"rx_start_sample": 7083520,
|
||||
"metrics": {
|
||||
"samples": 160000,
|
||||
"mean_i": 0.9560375,
|
||||
"mean_q": 1.8575625,
|
||||
"power_code2": 4.88045,
|
||||
"ac_power_code2": 0.5159038571874985,
|
||||
"rms_code": 2.209174053803819,
|
||||
"peak_magnitude_code": 4.242640687119285,
|
||||
"max_abs_i": 3.0,
|
||||
"max_abs_q": 4.0,
|
||||
"papr_db": 5.668126373000443,
|
||||
"clipped_components_fraction": 0.0,
|
||||
"clipped_samples_fraction": 0.0,
|
||||
"symbol_energy_code2_seconds": 0.0002440225,
|
||||
"ac_rms_code": 0.7182644757939087
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "GAP_L2_L3",
|
||||
"tx_relative_start_sample": 4160000,
|
||||
"samples": 160000,
|
||||
"duration_seconds": 0.02,
|
||||
"rx_start_sample": 8283520,
|
||||
"metrics": {
|
||||
"samples": 160000,
|
||||
"mean_i": 0.97165,
|
||||
"mean_q": 1.8424125,
|
||||
"power_code2": 4.8607875,
|
||||
"ac_power_code2": 0.5221999573437506,
|
||||
"rms_code": 2.204719369897221,
|
||||
"peak_magnitude_code": 4.47213595499958,
|
||||
"max_abs_i": 3.0,
|
||||
"max_abs_q": 4.0,
|
||||
"papr_db": 6.14323360311441,
|
||||
"clipped_components_fraction": 0.0,
|
||||
"clipped_samples_fraction": 0.0,
|
||||
"symbol_energy_code2_seconds": 0.00024303937499999998,
|
||||
"ac_rms_code": 0.7226340410911671
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "FINAL_GUARD",
|
||||
"tx_relative_start_sample": 5440000,
|
||||
"samples": 160000,
|
||||
"duration_seconds": 0.02,
|
||||
"rx_start_sample": 9563520,
|
||||
"metrics": {
|
||||
"samples": 160000,
|
||||
"mean_i": 0.958375,
|
||||
"mean_q": 1.83606875,
|
||||
"power_code2": 4.81993125,
|
||||
"ac_power_code2": 0.5303001546484385,
|
||||
"rms_code": 2.195434182570728,
|
||||
"peak_magnitude_code": 4.47213595499958,
|
||||
"max_abs_i": 3.0,
|
||||
"max_abs_q": 4.0,
|
||||
"papr_db": 6.179891520223454,
|
||||
"clipped_components_fraction": 0.0,
|
||||
"clipped_samples_fraction": 0.0,
|
||||
"symbol_energy_code2_seconds": 0.0002409965625,
|
||||
"ac_rms_code": 0.7282171068084288
|
||||
}
|
||||
}
|
||||
],
|
||||
"background_summary": {
|
||||
"ac_power_min_code2": 0.5159038571874985,
|
||||
"ac_power_max_code2": 0.5303001546484385,
|
||||
"ac_rms_min_code": 0.7182644757939087,
|
||||
"ac_rms_max_code": 0.7282171068084288,
|
||||
"maximum_to_minimum_ac_power_db": 0.11952978854087751,
|
||||
"stability_rule_db": 3.0,
|
||||
"stable": true
|
||||
},
|
||||
"comparisons": [
|
||||
{
|
||||
"comparison": "L1/L0",
|
||||
"tx_input_difference_db": 5.9772704691441785,
|
||||
"rx_matched_power_difference_db": 5.959769689089129,
|
||||
"linearity_error_db": -0.01750078005504907,
|
||||
"tx_amplitude_ratio": 1.9900478704552953,
|
||||
"rx_matched_amplitude_ratio": 1.9860422557090833
|
||||
},
|
||||
{
|
||||
"comparison": "L2/L1",
|
||||
"tx_input_difference_db": 5.998157271536577,
|
||||
"rx_matched_power_difference_db": 5.828887063637578,
|
||||
"linearity_error_db": -0.16927020789899938,
|
||||
"tx_amplitude_ratio": 1.9948390610676463,
|
||||
"rx_matched_amplitude_ratio": 1.9563400830193338
|
||||
},
|
||||
{
|
||||
"comparison": "L3/L2",
|
||||
"tx_input_difference_db": 5.998257379520232,
|
||||
"rx_matched_power_difference_db": 6.020419771848732,
|
||||
"linearity_error_db": 0.022162392328500147,
|
||||
"tx_amplitude_ratio": 1.9948620524335567,
|
||||
"rx_matched_amplitude_ratio": 1.9999585213327857
|
||||
}
|
||||
],
|
||||
"total_comparison": {
|
||||
"comparison": "L3/L0",
|
||||
"tx_digital_power_difference_db": 17.97368512020099,
|
||||
"rx_matched_power_difference_db": 17.80907652457544,
|
||||
"total_linearity_error_db": -0.16460859562555186,
|
||||
"tx_amplitude_ratio": 7.9192536969014835,
|
||||
"rx_matched_amplitude_ratio": 7.770586982689514
|
||||
},
|
||||
"classification": {
|
||||
"case": "LINEAR_BUT_WEAK",
|
||||
"rules_fixed_before_rf": {
|
||||
"linear_max_abs_error_db": 1.0,
|
||||
"compression_upper_step_error_at_most_db": -1.5,
|
||||
"weak_l3_matched_to_residual_below_db": 3.0,
|
||||
"healthy_l3_matched_to_residual_at_least_db": 6.0
|
||||
}
|
||||
},
|
||||
"capture_valid": true,
|
||||
"capture_invalid_reasons": [],
|
||||
"tx_underruns_maximum": 0,
|
||||
"tx_underruns_final": 0,
|
||||
"rx_overruns_maximum": 0,
|
||||
"rx_overruns_final": 0,
|
||||
"rx_clipping": false
|
||||
}
|
||||
39
data/processed/lab064/software_gate.json
Normal file
39
data/processed/lab064/software_gate.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"lab": "Lab064",
|
||||
"stage": "SOFTWARE_LINEARITY_GATE",
|
||||
"rf_tx_count": 0,
|
||||
"channel_model": "known linear complex gain plus DC and deterministic Gaussian noise",
|
||||
"rows": [
|
||||
{
|
||||
"label": "L0",
|
||||
"matched_signal_power": 4.310961695783257,
|
||||
"residual_power": 0.0018007645625517528,
|
||||
"gain_abs": 0.37000937351312335
|
||||
},
|
||||
{
|
||||
"label": "L1",
|
||||
"matched_signal_power": 17.072568748673206,
|
||||
"residual_power": 0.0017975392617719525,
|
||||
"gain_abs": 0.3700018841553234
|
||||
},
|
||||
{
|
||||
"label": "L2",
|
||||
"matched_signal_power": 67.93699349861977,
|
||||
"residual_power": 0.0017977879292746589,
|
||||
"gain_abs": 0.37000113388875977
|
||||
},
|
||||
{
|
||||
"label": "L3",
|
||||
"matched_signal_power": 270.35275600931345,
|
||||
"residual_power": 0.001803678589230678,
|
||||
"gain_abs": 0.3700016234349186
|
||||
}
|
||||
],
|
||||
"linearity_errors_db": [
|
||||
-2.3407130973929213e-05,
|
||||
-8.27380804224731e-05,
|
||||
-1.2603446491787906e-05
|
||||
],
|
||||
"passed": true,
|
||||
"tx_sha256": "fa260719659b2984fe29588238415622cd75c6daefec6cc9130ba1bf851bd3f9"
|
||||
}
|
||||
@@ -624,3 +624,42 @@
|
||||
[локальное сопоставление CSV](../data/processed/lab063/local_alignment.csv).
|
||||
- **STOP:** Lab063 не коммитировалась, push не выполнялся, Lab064 не создана. Следующий шаг
|
||||
требует отдельного согласования; RF TX=0.
|
||||
|
||||
## Lab064: измерение уровня и линейности RF-тракта
|
||||
|
||||
- **Запись 059; подготовка и RX-only, RF TX=0.** Создан один 0,75-секундный
|
||||
PRBS7/BPSK/RRC waveform с четырьмя 100-мс участками −18/−12/−6/0 дБ,
|
||||
промежутками по 50 мс и нулями по 100 мс в начале и конце.
|
||||
- **Цифровые уровни:** RMS 5,556137/11,056978/22,056892/44,000457 кода; шаги
|
||||
+5,977270/+5,998157/+5,998257 дБ; max |I|/|Q| не выше 69/69, clipping0.
|
||||
- **Методика:** пакетный приёмник, FEC, CRC, T2 и JPEG не используются. Главная
|
||||
величина — мощность согласованной с известным TX составляющей. Анализатор
|
||||
выдаёт `INVALID_CAPTURE` без A/B/C при любом промежуточном underrun, overrun
|
||||
или clipping.
|
||||
- **Аппаратный preflight PASS:** MASTER/SLAVE reg29=0x26, SLAVE CLKIN=DETECTED
|
||||
при первом опросе через66,793мс. RX-only:800000 отсчётов, полный RMS2,193193,
|
||||
AC RMS0,832038, max|I|/|Q|=4/5, clipping0, overruns0, сильного фона нет.
|
||||
- **Проверки:** quick gate984/37/13 PASS, pytest984 passed, целевые13 passed,
|
||||
независимая read-only проверка чистая.
|
||||
- [Текущее состояние](lab064_current_state.md), [отчёт](lab064_rx_preflight.md),
|
||||
[подготовка](../data/processed/lab064/preparation.json),
|
||||
[software gate](../data/processed/lab064/software_gate.json),
|
||||
[preflight](../data/processed/lab064/preflight_result.json).
|
||||
- **STOP:** технически готово к одному TX, но отдельного разрешения ещё нет.
|
||||
Lab064 не коммитируется, push не выполняется, Lab065 не начинается.
|
||||
- **Единственный RF-сеанс:** один TX, повторов0, underruns0, overruns0,
|
||||
clipping0; RX16000000 отсчётов за2,0с. Фон стабилен: AC RMS
|
||||
0,718264…0,728217 кода, разброс0,119530дБ.
|
||||
- **Линейность:** L0→L1/L1→L2/L2→L3 дали ошибки
|
||||
−0,017501/−0,169270/+0,022162дБ. Полный рост TX/RX равен
|
||||
17,973685/17,809077дБ, общая ошибка −0,164609дБ. Признака компрессии нет.
|
||||
- **Итог `LINEAR_BUT_WEAK`:** L3 matched/residual равен −12,133336дБ. Тракт
|
||||
следует цифровым ступеням, но согласованная полезная составляющая остаётся
|
||||
слабее остатка. [RF-отчёт](lab064_rf_result.md),
|
||||
[RF JSON](../data/processed/lab064/rf_result.json),
|
||||
[capture JSON](../data/processed/lab064/capture_result.json).
|
||||
- **Текущее состояние:** `RF TX=0`, процессы CLEAR; второй TX не выполнялся,
|
||||
Lab064 не коммитировалась, push не выполнялся, Lab065 не создана.
|
||||
- **Независимая проверка:** прямой пересчёт из TX/RX CS8 подтвердил все SHA,
|
||||
счётчики, фоны, matched/residual/`h`, ступени и спектральные полосы;
|
||||
расхождений нет. Quick gate и pytest после RF:986 passed,0 failed.
|
||||
|
||||
141
docs/lab064_current_state.md
Normal file
141
docs/lab064_current_state.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# Lab064: измерение уровня и линейности RF-тракта
|
||||
|
||||
Lab064 проверяет, следует ли мощность согласованной принятой составляющей
|
||||
заранее заданной цифровой лестнице TX. Это измерительный опыт с известным
|
||||
PRBS7/BPSK-сигналом, а не пакетный тест. FEC, CRC, T2, JPEG и packet output в
|
||||
нём не используются.
|
||||
|
||||
На текущем этапе сформирован сигнал, подготовлены RX-first runner, офлайн-
|
||||
анализатор и полный учёт сообщений overruns/underruns. Перед аппаратным
|
||||
preflight и будущим RF-сеансом исходные параметры не меняются.
|
||||
|
||||
## Физические условия
|
||||
|
||||
| Условие | Значение |
|
||||
|---|---|
|
||||
| Центральная частота | 1400000000 Гц |
|
||||
| Расстояние TX–RX | примерно 1,2 м |
|
||||
| Геометрия | прямая видимость, помещение |
|
||||
| TX | плата и антенна возле окна |
|
||||
| RX | плата и антенна на столе пользователя |
|
||||
| Антенны | имеющиеся антенны на 1400 МГц |
|
||||
| Поляризация | одинаковая |
|
||||
| Тактирование | 10 МГц TCXO → MASTER → MASTER CLKOUT → SLAVE CLKIN |
|
||||
|
||||
Антенны, платы, длинный кабель общей опоры, USB-подключение и остальные
|
||||
кабели не перемещаются. Во время опыта не меняются частота, усиления,
|
||||
ориентация и форма сигнала.
|
||||
|
||||
## Подготовленный измерительный сигнал
|
||||
|
||||
Параметры сигнала: BPSK/PRBS7, 20000 символов/с, Fs 8000000 комплексных
|
||||
отсчётов/с, 400 отсчётов/символ, цифровая несущая +50000 Гц и существующий RRC
|
||||
с roll-off 0,35 и span 10 символов. TX VGA равен 10 дБ. RX LNA/VGA равны
|
||||
24/24 дБ. RF amplifier и Bias-Tee выключены.
|
||||
|
||||
Один файл содержит 100 мс нулей, четыре одинаковых по содержанию участка по
|
||||
100 мс, три нулевых промежутка по 50 мс и 100 мс нулей в конце. Общая длина:
|
||||
6000000 комплексных отсчётов, 12000000 байт, 0,75 с. Нулевые участки являются
|
||||
цифровыми нулями IQ; они не означают аппаратного выключения TX посреди файла.
|
||||
|
||||
SHA-256 подготовленного CS8:
|
||||
`fa260719659b2984fe29588238415622cd75c6daefec6cc9130ba1bf851bd3f9`.
|
||||
|
||||
| Уровень | Задано, дБ | Фактически, дБ | TX RMS, код | max \|I\| / \|Q\|, код | Шаг мощности, дБ | Clipping |
|
||||
|---|---:|---:|---:|---:|---:|---|
|
||||
| L0 | −18 | −17,973685 | 5,556137 | 9 / 9 | — | нет |
|
||||
| L1 | −12 | −11,996415 | 11,056978 | 17 / 17 | +5,977270 | нет |
|
||||
| L2 | −6 | −5,998257 | 22,056892 | 34 / 35 | +5,998157 | нет |
|
||||
| L3 | 0 | 0 | 44,000457 | 69 / 69 | +5,998257 | нет |
|
||||
|
||||
Небольшое отклонение L0 от −18 дБ вызвано квантованием CS8. Каждый шаг
|
||||
укладывается в заранее установленный допуск ±0,1 дБ.
|
||||
|
||||
## Анализ будущей записи
|
||||
|
||||
Для каждого L0…L3 анализатор отдельно выдаёт полный и AC RMS, средние I/Q,
|
||||
max |I|, max |Q| и максимальную комплексную амплитуду. По соседнему нулевому
|
||||
окну оценивается фон. По известному TX оцениваются комплексный коэффициент
|
||||
передачи `h`, `|h|²`, мощность согласованной составляющей, мощность остатка и
|
||||
их отношение.
|
||||
|
||||
Для каждой соседней пары вычисляются:
|
||||
|
||||
```text
|
||||
TX INPUT DIFFERENCE
|
||||
RX MATCHED POWER DIFFERENCE
|
||||
LINEARITY ERROR = RX difference − TX difference
|
||||
```
|
||||
|
||||
Критерии зафиксированы до RF:
|
||||
|
||||
- линейный участок: максимальная абсолютная ошибка шага не более 1,0 дБ;
|
||||
- признак компрессии: ошибка верхнего шага не более −1,5 дБ;
|
||||
- `LINEAR_BUT_WEAK`: шаги линейны, фон стабилен, а L3 ниже 3 дБ над остатком;
|
||||
- `HEALTHY_LINEAR_RANGE`: шаги линейны, фон стабилен, а L3 не менее 6 дБ над
|
||||
остатком;
|
||||
- неоднозначный результат получает статус `NOT_LOCALIZED`.
|
||||
|
||||
Эти границы являются правилами данного опыта, а не абсолютной калибровкой
|
||||
RF-мощности. Абсолютную выходную и принятую мощность без измерительного прибора
|
||||
установить нельзя.
|
||||
|
||||
Синтетическая проверка известного линейного комплексного канала с DC и шумом
|
||||
восстановила три цифровых шага с максимальной ошибкой меньше 0,0001 дБ. Это
|
||||
проверяет вычисления анализатора, но не подтверждает аппаратные свойства
|
||||
HackRF или эфирного канала.
|
||||
|
||||
## Управление аппаратным сеансом
|
||||
|
||||
Preflight допускает только RX-команды. Он проверяет MASTER/SLAVE reg29=0x26,
|
||||
SLAVE CLKIN=DETECTED, процессы, инвентаризацию, 0,1 с фона, rail-коды,
|
||||
overruns, AC RMS и максимумы |I|/|Q|. Усиления автоматически не меняются.
|
||||
|
||||
Будущий runner запускает RX раньше TX. Передача требует отдельного явного
|
||||
флага, создаёт одноразовый маркер до обращения к SDR и допускает ровно один
|
||||
TX-вызов. Полные stdout и stderr сохраняются раздельно. В итог попадают все
|
||||
промежуточные и финальные счётчики потерь, их максимум и последнее значение.
|
||||
Любой underrun, overrun, clipping или аппаратная ошибка завершает опыт без
|
||||
повтора. Анализатор при этом может сохранить диагностические метрики, но
|
||||
выдаёт только `INVALID_CAPTURE`: варианты A/B/C запрещены. Проверка использует
|
||||
все строки счётчиков и их максимум, поэтому последний ноль не может скрыть
|
||||
предыдущее ненулевое значение.
|
||||
|
||||
После исправления строгой проверки счётчиков quick gate прошёл: 984 теста
|
||||
ядра, 37 функциональных проверок выполнены, 13 пропущены. Полный pytest:
|
||||
984 passed, 0 failed. Независимая read-only проверка подтвердила подготовленный
|
||||
CS8, программные метрики и запрет классификации невалидной записи.
|
||||
|
||||
## Аппаратный preflight
|
||||
|
||||
14 сентября 2026 года выполнена одна разрешённая аппаратная проверка при
|
||||
выключенном передатчике. MASTER/SLAVE reg29 равны `0x26`, SLAVE CLKIN имеет
|
||||
состояние `DETECTED` при первом опросе через 66,793 мс. RX-only запись длиной
|
||||
800000 отсчётов дала AC RMS 0,832038 кода, max |I|/|Q| 4/5 кодов, clipping 0 и
|
||||
overruns 0. Неожиданно сильного сигнала нет. После отключения CLKOUT процессы
|
||||
имеют состояние `CLEAR`, ошибок очистки нет. Подробности приведены в
|
||||
[отчёте preflight](lab064_rx_preflight.md).
|
||||
|
||||
`RF TX = 0`. На этом этапе Lab064 не коммитируется. Push не выполняется.
|
||||
Lab065 автоматически не начинается.
|
||||
|
||||
## Фактический RF-результат
|
||||
|
||||
14 сентября 2026 года выполнен один разрешённый RF-сеанс без повторов. TX
|
||||
underruns 0, RX overruns 0, clipping 0; RX содержит 16000000 комплексных
|
||||
отсчётов за 2,0 с. Согласованная мощность выросла на 17,809077 дБ при цифровом
|
||||
росте 17,973685 дБ. Общая ошибка линейности равна −0,164609 дБ. Верхняя ступень
|
||||
дала +6,020420 дБ при ожидаемых +5,998257 дБ, признака компрессии нет.
|
||||
|
||||
Фон стабилен: AC RMS 0,718264…0,728217 кода, разброс мощности 0,119530 дБ.
|
||||
Даже L3 остаётся на −12,133336 дБ относительно остатка. Итог по заранее
|
||||
заданным критериям: `LINEAR_BUT_WEAK`. Подробные значения приведены в
|
||||
[RF-отчёте](lab064_rf_result.md).
|
||||
|
||||
После сеанса `RF TX = 0`, процессы `CLEAR`. Усиления не менялись, второй TX не
|
||||
выполнялся. Lab064 не коммитировалась, push не выполнялся, Lab065 не создана.
|
||||
|
||||
Независимый пересчёт непосредственно по TX/RX CS8 подтвердил SHA, аппаратные
|
||||
счётчики, все четыре matched/residual/`h`, пять окон фона, три ступени и общий
|
||||
результат L3/L0. Расхождений нет, вердикт проверки: `чисто`. После сеанса quick
|
||||
gate и полный pytest дали 986 passed, 0 failed; `git diff --check` прошёл.
|
||||
216
docs/lab064_rf_result.md
Normal file
216
docs/lab064_rf_result.md
Normal file
@@ -0,0 +1,216 @@
|
||||
# Lab064: результат измерения уровня и линейности RF-тракта
|
||||
|
||||
В одном коротком OTA-сеансе цифровая мощность TX была увеличена почти на 18 дБ,
|
||||
то есть цифровая амплитуда выросла примерно в 7,92 раза. Согласованная с
|
||||
известным TX амплитуда на RX выросла в 7,77 раза, а её мощность на 17,81 дБ.
|
||||
Коэффициент передачи `|h|` почти не менялся. Признака компрессии на верхнем
|
||||
уровне не обнаружено.
|
||||
|
||||
При этом полезная согласованная составляющая остаётся слабее остатка даже на
|
||||
L3: −12,13 дБ. Фон во всех пяти нулевых окнах стабилен. По заранее заданным
|
||||
критериям результат классифицирован как `LINEAR_BUT_WEAK`.
|
||||
|
||||
Опыт установил относительную линейность именно этой цифровой лестницы в
|
||||
текущей OTA-геометрии. Он не измеряет абсолютную мощность в ваттах или дБм и не
|
||||
разделяет вклад TX, антенн, эфирного канала и RX. Для этого нужен измерительный
|
||||
прибор или отдельный проведённый тест по кабелю.
|
||||
|
||||
## Сеанс и аппаратная целостность
|
||||
|
||||
14 сентября 2026 года выполнен ровно один разрешённый TX. RX стартовал раньше
|
||||
TX. Повторных передач не было.
|
||||
|
||||
| Проверка | Результат |
|
||||
|---|---:|
|
||||
| TX waveform SHA-256 | `fa260719659b2984fe29588238415622cd75c6daefec6cc9130ba1bf851bd3f9` |
|
||||
| RF / Fs / symbol rate | 1400000000 Гц / 8000000 отсчётов/с / 20000 символов/с |
|
||||
| TX VGA / RX LNA / RX VGA | 10 / 24 / 24 дБ |
|
||||
| RF amplifier / Bias-Tee | выключены |
|
||||
| MASTER / SLAVE reg29 | `0x26` / `0x26` |
|
||||
| SLAVE CLKIN | `DETECTED`, второй опрос, 139,042 мс |
|
||||
| Выполненные TX-сеансы | 1 |
|
||||
| Автоматические повторы | 0 |
|
||||
| TX underrun, максимум / финал | 0 / 0 |
|
||||
| RX overrun, максимум / финал | 0 / 0 |
|
||||
| RX clipping | 0 |
|
||||
| RX длина | 16000000 комплексных отсчётов, 2,000000 с |
|
||||
| RX SHA-256 | `02015891948aaee52ff81e60fdfeb391bb9b76cd9192b75972b8fedbc8690d86` |
|
||||
| Процессы до / после | `CLEAR` / `CLEAR` |
|
||||
| Ошибки очистки | 0 |
|
||||
|
||||
В TX-журнале сохранены две строки `0 underruns`; в RX-журнале сохранены четыре
|
||||
строки `0 overruns`. Анализ использует все наблюдения и их максимум. Финальный
|
||||
нулевой счётчик не мог бы скрыть промежуточное ненулевое значение.
|
||||
|
||||
## Фон
|
||||
|
||||
Каждое окно имеет длину 160000 отсчётов, или 20 мс, и лежит внутри цифрового
|
||||
нулевого участка вдали от активной границы.
|
||||
|
||||
| Окно | AC power, код² | AC RMS, код |
|
||||
|---|---:|---:|
|
||||
| Начальный guard | 0,521529 | 0,722170 |
|
||||
| Gap L0–L1 | 0,523180 | 0,723312 |
|
||||
| Gap L1–L2 | 0,515904 | 0,718264 |
|
||||
| Gap L2–L3 | 0,522200 | 0,722634 |
|
||||
| Конечный guard | 0,530300 | 0,728217 |
|
||||
|
||||
Диапазон AC RMS равен 0,718264…0,728217 кода. Отношение максимальной AC-мощности
|
||||
к минимальной составляет 0,119530 дБ при заранее заданной границе стабильности
|
||||
3 дБ. Фон считается стабильным.
|
||||
|
||||
## Метрики участков
|
||||
|
||||
Мощности приведены в квадрате цифрового кода RX. Это относительные цифровые
|
||||
величины, а не ватты и не дБм.
|
||||
|
||||
### L0, −18 дБ
|
||||
|
||||
```text
|
||||
FULL IQ RMS = 2.277200 code
|
||||
AC RMS = 0.956317 code
|
||||
MEAN I = 0.949724 code
|
||||
MEAN Q = 1.835518 code
|
||||
MAX |I| = 6 code
|
||||
MAX |Q| = 7 code
|
||||
MAX COMPLEX MAGNITUDE = 7.071068 code
|
||||
BACKGROUND AC POWER = 0.521529 code^2
|
||||
MATCHED SIGNAL POWER = 0.000750751 code^2
|
||||
RESIDUAL POWER = 1.213794804 code^2
|
||||
MATCHED / RESIDUAL = -32.086493 dB
|
||||
h = 0.001876064 - j0.004508063
|
||||
|h| = 0.004882853
|
||||
|h|^2 = 0.000023842249
|
||||
CFO = +0.095772 Hz
|
||||
```
|
||||
|
||||
### L1, −12 дБ
|
||||
|
||||
```text
|
||||
FULL IQ RMS = 2.291392 code
|
||||
AC RMS = 0.989014 code
|
||||
MEAN I = 0.956038 code
|
||||
MEAN Q = 1.832573 code
|
||||
MAX |I| = 6 code
|
||||
MAX |Q| = 7 code
|
||||
MAX COMPLEX MAGNITUDE = 7.211103 code
|
||||
BACKGROUND AC POWER = 0.523180 code^2
|
||||
MATCHED SIGNAL POWER = 0.002961235 code^2
|
||||
RESIDUAL POWER = 1.069594603 code^2
|
||||
MATCHED / RESIDUAL = -25.577463 dB
|
||||
h = 0.004471119 - j0.001937686
|
||||
|h| = 0.004872939
|
||||
|h|^2 = 0.000023745532
|
||||
CFO = -0.168571 Hz
|
||||
```
|
||||
|
||||
### L2, −6 дБ
|
||||
|
||||
```text
|
||||
FULL IQ RMS = 2.275060 code
|
||||
AC RMS = 0.925298 code
|
||||
MEAN I = 0.963183 code
|
||||
MEAN Q = 1.841739 code
|
||||
MAX |I| = 6 code
|
||||
MAX |Q| = 7 code
|
||||
MAX COMPLEX MAGNITUDE = 7.280110 code
|
||||
BACKGROUND AC POWER = 0.515904 code^2
|
||||
MATCHED SIGNAL POWER = 0.011333436 code^2
|
||||
RESIDUAL POWER = 0.529088688 code^2
|
||||
MATCHED / RESIDUAL = -16.691669 dB
|
||||
h = 0.003277253 - j0.003478187
|
||||
|h| = 0.004778930
|
||||
|h|^2 = 0.000022838175
|
||||
CFO = +0.006742 Hz
|
||||
```
|
||||
|
||||
### L3, 0 дБ
|
||||
|
||||
```text
|
||||
FULL IQ RMS = 2.341160 code
|
||||
AC RMS = 1.057677 code
|
||||
MEAN I = 0.957558 code
|
||||
MEAN Q = 1.856188 code
|
||||
MAX |I| = 6 code
|
||||
MAX |Q| = 7 code
|
||||
MAX COMPLEX MAGNITUDE = 7.280110 code
|
||||
BACKGROUND AC POWER = 0.522200 code^2
|
||||
MATCHED SIGNAL POWER = 0.045331864 code^2
|
||||
RESIDUAL POWER = 0.740861774 code^2
|
||||
MATCHED / RESIDUAL = -12.133336 dB
|
||||
h = 0.003384388 - j0.003391322
|
||||
|h| = 0.004791153
|
||||
|h|^2 = 0.000022955145
|
||||
CFO = -0.076067 Hz
|
||||
```
|
||||
|
||||
Максимумы активных участков равны `|I|=6` и `|Q|=7`. Полная двухсекундная
|
||||
запись также далека от rail-кодов: `|I|=7`, `|Q|=8`. Это исключает цифровой
|
||||
clipping в записи, но не является самостоятельным доказательством идеальной
|
||||
линейности аналогового тракта.
|
||||
|
||||
## Линейность мощности и амплитуды
|
||||
|
||||
| Пара | TX, дБ | RX matched, дБ | Ошибка, дБ | TX amplitude ratio | RX matched amplitude ratio |
|
||||
|---|---:|---:|---:|---:|---:|
|
||||
| L0 → L1 | +5,977270 | +5,959770 | −0,017501 | 1,990048 | 1,986042 |
|
||||
| L1 → L2 | +5,998157 | +5,828887 | −0,169270 | 1,994839 | 1,956340 |
|
||||
| L2 → L3 | +5,998257 | +6,020420 | +0,022162 | 1,994862 | 1,999959 |
|
||||
| L0 → L3 | +17,973685 | +17,809077 | −0,164609 | 7,919254 | 7,770587 |
|
||||
|
||||
Максимальная абсолютная ошибка отдельной ступени равна 0,169270 дБ и заметно
|
||||
меньше заранее установленной границы линейности 1,0 дБ. Верхняя ступень не
|
||||
отстаёт от TX: она выросла на 6,020420 дБ при ожидаемых 5,998257 дБ. Порог
|
||||
признака компрессии составлял −1,5 дБ ошибки верхней ступени; фактическая
|
||||
ошибка равна +0,022162 дБ.
|
||||
|
||||
`|h|` изменился от 0,004882853 на L0 до 0,004791153 на L3. Это соответствует
|
||||
−0,164609 дБ по мощности и совпадает с общей ошибкой линейности. Систематического
|
||||
падения коэффициента передачи на верхнем уровне нет.
|
||||
|
||||
Остаточная мощность при переходе L2 → L3 выросла примерно на 1,46 дБ, тогда
|
||||
как согласованная полезная мощность выросла на 6,02 дБ. Отношение
|
||||
matched/residual улучшилось с −16,69 до −12,13 дБ. Ухудшения формы сигнала на
|
||||
L3 относительно L2 по этому показателю нет.
|
||||
|
||||
Диагностические полосы остатка около 50/100/150 кГц на L3 равны
|
||||
0,009576/0,004469/0,002725 код². Они не растут быстрее согласованной мощности
|
||||
при переходе L2 → L3. Эти полосы не являются калиброванным измерением
|
||||
RF-гармоник, поэтому абсолютный вывод о спектральной чистоте потребовал бы
|
||||
анализатора спектра.
|
||||
|
||||
## Вывод
|
||||
|
||||
```text
|
||||
CLASSIFICATION = LINEAR_BUT_WEAK
|
||||
```
|
||||
|
||||
Текущий тракт воспроизводит цифровые ступени почти один к одному по дБ. Данных
|
||||
за компрессию при TX VGA 10 дБ и RX LNA/VGA 24/24 дБ нет. Главная проблема
|
||||
этого сеанса состоит в низкой согласованной полезной мощности относительно
|
||||
остатка: даже на L3 запас равен −12,13 дБ.
|
||||
|
||||
Практически это означает, что простое объяснение Lab062 аналоговой перегрузкой
|
||||
текущего тракта не подтверждается. Слабое отношение полезной составляющей к
|
||||
остатку остаётся вероятным фактором плохого 500K-приёма, хотя Lab064 использует
|
||||
узкий сигнал 20 ksymbol/s и сама по себе не объясняет все широкополосные
|
||||
искажения Lab062.
|
||||
|
||||
Рекомендуемый следующий отдельный шаг: согласовать повтор той же лестницы при
|
||||
TX VGA 20 дБ, сохранив RX LNA/VGA 24/24 дБ, частоту, геометрию, waveform и
|
||||
общую опору. Это проверит, улучшается ли matched/residual при сохранении
|
||||
линейности. В рамках Lab064 усиления не менялись и второй TX не выполнялся.
|
||||
|
||||
## Независимая проверка
|
||||
|
||||
Проверяющий агент работал только с сохранёнными файлами и не обращался к
|
||||
оборудованию. Он независимо пересчитал SHA-256, положение четырёх сегментов,
|
||||
пять окон фона, matched/residual, комплексные `h`, отношения мощности и
|
||||
амплитуды, общий результат L3/L0 и все двенадцать спектральных значений.
|
||||
Подтверждённых расхождений нет. Целевые тесты дали 15 passed; итоговый вердикт:
|
||||
`чисто`.
|
||||
|
||||
После RF-сеанса quick gate прошёл: 986 тестов ядра, 37 функциональных проверок,
|
||||
13 пропусков. Полный pytest: 986 passed, 0 failed. `git diff --check`: PASS.
|
||||
|
||||
`RF TX = 0`. Lab064 не коммитировалась, push не выполнялся, Lab065 не создана.
|
||||
120
docs/lab064_rx_preflight.md
Normal file
120
docs/lab064_rx_preflight.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# Lab064: подготовка и RX-only preflight
|
||||
|
||||
14 сентября 2026 года завершена программная подготовка измерения уровня и
|
||||
линейности текущего OTA-тракта. Затем выполнена одна аппаратная проверка общей
|
||||
опоры и RX-фона. Preflight прошёл. Передатчик не запускался: `RF TX = 0`.
|
||||
|
||||
## Условия
|
||||
|
||||
| Параметр | Значение |
|
||||
|---|---|
|
||||
| Центральная частота | 1400000000 Гц |
|
||||
| Скорость символов | 20000 символов/с |
|
||||
| Частота дискретизации | 8000000 комплексных отсчётов/с |
|
||||
| Цифровая несущая | +50000 Гц |
|
||||
| TX VGA | 10 дБ |
|
||||
| RX LNA / VGA | 24 / 24 дБ |
|
||||
| RF amplifier / Bias-Tee | выключены |
|
||||
| Расстояние TX–RX | примерно 1,2 м |
|
||||
| Геометрия | прямая видимость, помещение |
|
||||
| TX | плата и антенна возле окна |
|
||||
| RX | плата и антенна на столе пользователя |
|
||||
| Антенны | имеющиеся антенны на 1400 МГц |
|
||||
| Поляризация | одинаковая |
|
||||
| Общая опора | 10 МГц TCXO → MASTER → MASTER CLKOUT → SLAVE CLKIN |
|
||||
|
||||
Антенны, платы, длинный кабель общей опоры, USB-подключение и остальные
|
||||
кабели не перемещались. Усиления не менялись.
|
||||
|
||||
## Измерительный TX-файл
|
||||
|
||||
Подготовлен один детерминированный PRBS7/BPSK/RRC-сигнал с уровнями −18, −12,
|
||||
−6 и 0 дБ относительно номинального waveform. Он состоит из 100 мс начальных
|
||||
нулей, четырёх участков по 100 мс, трёх промежутков по 50 мс и 100 мс конечных
|
||||
нулей. Полная длительность равна 0,75 с, длина составляет 6000000 комплексных
|
||||
отсчётов и 12000000 байт.
|
||||
|
||||
| Уровень | TX RMS, код | max \|I\| / \|Q\|, код | Фактический уровень, дБ | Шаг, дБ |
|
||||
|---|---:|---:|---:|---:|
|
||||
| L0 | 5,556137 | 9 / 9 | −17,973685 | — |
|
||||
| L1 | 11,056978 | 17 / 17 | −11,996415 | +5,977270 |
|
||||
| L2 | 22,056892 | 34 / 35 | −5,998257 | +5,998157 |
|
||||
| L3 | 44,000457 | 69 / 69 | 0 | +5,998257 |
|
||||
|
||||
Цифрового clipping нет. SHA-256:
|
||||
`fa260719659b2984fe29588238415622cd75c6daefec6cc9130ba1bf851bd3f9`.
|
||||
|
||||
Синтетический линейный канал с известным комплексным коэффициентом, DC и
|
||||
шумом прошёл software gate. После независимой проверки анализатор изменён так,
|
||||
чтобы промежуточный ненулевой счётчик нельзя было скрыть финальным нулём. При
|
||||
любой потере сохраняются диагностические метрики, но A/B/C-классификация
|
||||
запрещается и результат получает статус `INVALID_CAPTURE`.
|
||||
|
||||
## Common clock
|
||||
|
||||
Свежая инвентаризация подтвердила две HackRF One r9 с firmware/libhackrf
|
||||
2024.02.1. Роли сохранены:
|
||||
|
||||
- MASTER `000000000000000097c063dc323b97d3`;
|
||||
- SLAVE `000000000000000097c063dc2e5327d3`.
|
||||
|
||||
Аппаратная проверка получила:
|
||||
|
||||
```text
|
||||
MASTER reg29 = 0x26
|
||||
SLAVE reg29 = 0x26
|
||||
SLAVE CLKIN = DETECTED
|
||||
CLKIN query = 1
|
||||
time to detect = 66.793 ms
|
||||
```
|
||||
|
||||
## RX-only фон
|
||||
|
||||
SLAVE записал 800000 комплексных отсчётов, 0,1 с, при 1400 МГц и RX LNA/VGA
|
||||
24/24 дБ.
|
||||
|
||||
| Метрика | Значение |
|
||||
|---|---:|
|
||||
| Полный RMS IQ | 2,193193 кода |
|
||||
| AC RMS после удаления среднего | 0,832038 кода |
|
||||
| Средние I / Q | 0,933738 / 1,801650 кода |
|
||||
| max \|I\| / \|Q\| | 4 / 5 кодов |
|
||||
| Максимальная комплексная амплитуда | 5,385165 кода |
|
||||
| Clipping | 0 / 800000 отсчётов |
|
||||
| RX overruns | 0, максимум и финальное значение |
|
||||
| Неожиданно сильный сигнал | нет |
|
||||
|
||||
Порог STOP для неожиданно сильного цифрового фона: AC RMS не менее 20 кодов
|
||||
или max |I|/|Q| не менее 100 кодов. Измеренные значения значительно ниже.
|
||||
Отсутствие rail-кодов не доказывает отсутствие аналоговой компрессии при
|
||||
будущей передаче. Абсолютная RF-мощность без измерительного прибора неизвестна.
|
||||
|
||||
SHA-256 RX-only записи:
|
||||
`02d87adcbb621027290a77d5f8fd3190c299d13e89f887cd294d4af352a6ef52`.
|
||||
|
||||
Процессы до и после preflight имели состояние `CLEAR`. Оба CLKOUT выключены,
|
||||
ошибок очистки нет. Preflight сохранил все две строки счётчика RX overruns;
|
||||
обе равны нулю. Вызовов `hackrf_transfer -t` не было, `rf_tx_count = 0`.
|
||||
|
||||
## Проверки и артефакты
|
||||
|
||||
- quick gate: 984 теста ядра, 37 функциональных проверок, 13 пропусков;
|
||||
- pytest: 984 passed, 0 failed;
|
||||
- целевые тесты Lab064: 13 passed;
|
||||
- `git diff --check`: PASS;
|
||||
- независимая read-only проверка: чисто.
|
||||
|
||||
Артефакты:
|
||||
|
||||
- [подготовка](../data/processed/lab064/preparation.json);
|
||||
- [software gate](../data/processed/lab064/software_gate.json);
|
||||
- [preflight](../data/processed/lab064/preflight_result.json);
|
||||
- CS8 и исходные аппаратные журналы находятся в `data/raw/lab064/` и в Git не
|
||||
входят.
|
||||
|
||||
`READY FOR SINGLE LAB064 TX = YES` означает техническую готовность только этой
|
||||
конфигурации. Передача требует отдельного явного разрешения пользователя.
|
||||
`RF TX = 0`. STOP.
|
||||
|
||||
Lab064 не коммитируется до аппаратного результата. Push не выполняется.
|
||||
Lab065 автоматически не начинается.
|
||||
957
experiments/lab064_rf_level_linearity.py
Normal file
957
experiments/lab064_rf_level_linearity.py
Normal file
@@ -0,0 +1,957 @@
|
||||
"""Lab064: лестница цифровых уровней и измерение линейности RF-тракта.
|
||||
|
||||
Подготовка и анализ не используют пакетный приёмник. Аппаратный preflight
|
||||
работает только в режиме RX. Передача доступна отдельной командой с явным
|
||||
флагом и защищена одноразовым маркером.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
if __package__ in (None, ""):
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
import numpy as np
|
||||
from scipy import signal
|
||||
|
||||
from experiments import lab043_pluto_to_rtlsdr as shaping
|
||||
from experiments import lab054_hardware_control as lab054
|
||||
from experiments import lab055_energy_audit as energy
|
||||
from experiments import lab056_abab_energy as lab056
|
||||
from experiments import lab056_capture_runner as hardware
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
RAW = ROOT / "data/raw/lab064"
|
||||
OUTPUT = ROOT / "data/processed/lab064"
|
||||
|
||||
SAMPLE_RATE = 8_000_000
|
||||
SYMBOL_RATE = 20_000
|
||||
SAMPLES_PER_SYMBOL = SAMPLE_RATE // SYMBOL_RATE
|
||||
CARRIER_HZ = 50_000
|
||||
NOMINAL_RMS_CODE = 44.0
|
||||
LEVELS = (("L0", -18.0), ("L1", -12.0), ("L2", -6.0), ("L3", 0.0))
|
||||
SEGMENT_SAMPLES = 800_000
|
||||
GAP_SAMPLES = 400_000
|
||||
LEADING_ZERO_SAMPLES = 800_000
|
||||
TRAILING_ZERO_SAMPLES = 800_000
|
||||
PRBS_SYMBOLS = SEGMENT_SAMPLES // SAMPLES_PER_SYMBOL - 42
|
||||
TRIM_SAMPLES = 21 * SAMPLES_PER_SYMBOL
|
||||
TX_SAMPLES = (
|
||||
LEADING_ZERO_SAMPLES
|
||||
+ len(LEVELS) * SEGMENT_SAMPLES
|
||||
+ (len(LEVELS) - 1) * GAP_SAMPLES
|
||||
+ TRAILING_ZERO_SAMPLES
|
||||
)
|
||||
RX_SECONDS = 2.0
|
||||
RX_SAMPLES = int(RX_SECONDS * SAMPLE_RATE)
|
||||
RX_LEAD_SECONDS = 0.5
|
||||
PREFLIGHT_SAMPLES = 800_000
|
||||
STRONG_AC_RMS_CODE = 20.0
|
||||
STRONG_COMPONENT_CODE = 100.0
|
||||
BACKGROUND_WINDOW_SAMPLES = 160_000
|
||||
|
||||
POINT = hardware.OperatingPoint(
|
||||
frequency_hz=1_400_000_000,
|
||||
physical_path="MASTER TX near window -> LOS indoor -> SLAVE RX on desk",
|
||||
path_confirmed=True,
|
||||
tx_antenna="existing 1400 MHz antenna near window",
|
||||
rx_antenna="existing 1400 MHz antenna on desk",
|
||||
distance_m=1.2,
|
||||
orientation="same polarization; fixed during preflight and RF session",
|
||||
visibility="LOS indoor",
|
||||
)
|
||||
|
||||
|
||||
def save_new(path: Path, value: dict) -> None:
|
||||
"""Записать новый JSON и не перезаписывать свидетельства опыта."""
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with path.open("x", encoding="utf-8") as stream:
|
||||
json.dump(value, stream, ensure_ascii=False, indent=2, allow_nan=False)
|
||||
stream.write("\n")
|
||||
|
||||
|
||||
def save_text_new(path: Path, value: str) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with path.open("x", encoding="utf-8") as stream:
|
||||
stream.write(value)
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
while block := stream.read(1024 * 1024):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def parameters() -> dict:
|
||||
return {
|
||||
"rf_frequency_hz": POINT.frequency_hz,
|
||||
"sample_rate_hz": SAMPLE_RATE,
|
||||
"symbol_rate": SYMBOL_RATE,
|
||||
"samples_per_symbol": SAMPLES_PER_SYMBOL,
|
||||
"digital_carrier_hz": CARRIER_HZ,
|
||||
"rrc_rolloff": shaping.LAB042_RRC_ROLLOFF,
|
||||
"rrc_span_symbols": shaping.LAB042_RRC_SPAN_SYMBOLS,
|
||||
"prbs": "PRBS7 x^7+x^6+1, deterministic initial state",
|
||||
"full_phy_packet": False,
|
||||
"tx_vga_db": 10,
|
||||
"rx_lna_db": 24,
|
||||
"rx_vga_db": 24,
|
||||
"rf_amplifier": False,
|
||||
"bias_tee": False,
|
||||
"geometry": asdict(POINT),
|
||||
"clock_path": "10 MHz TCXO -> MASTER -> MASTER CLKOUT -> SLAVE CLKIN",
|
||||
"levels_db_relative_nominal": [level for _, level in LEVELS],
|
||||
}
|
||||
|
||||
|
||||
def transfer_command(path: Path | str, samples: int, *, transmit: bool = False) -> list[str]:
|
||||
return hardware.command(POINT, path, samples, transmit=transmit)
|
||||
|
||||
|
||||
def _complex_to_cs8(values: np.ndarray) -> np.ndarray:
|
||||
interleaved = np.empty(2 * len(values), dtype=np.int8)
|
||||
interleaved[::2] = np.clip(np.rint(values.real), -128, 127).astype(np.int8)
|
||||
interleaved[1::2] = np.clip(np.rint(values.imag), -128, 127).astype(np.int8)
|
||||
return interleaved
|
||||
|
||||
|
||||
def nominal_segment() -> np.ndarray:
|
||||
"""Один участок длиной 100 мс с прежними PRBS7, BPSK и RRC."""
|
||||
symbols = 2.0 * shaping.pilot_prbs7(PRBS_SYMBOLS) - 1.0
|
||||
waveform = shaping.shape_frame_like_lab042(symbols, SAMPLES_PER_SYMBOL)
|
||||
if len(waveform) != SEGMENT_SAMPLES:
|
||||
raise RuntimeError("RRC-участок не равен заранее заданным 100 мс")
|
||||
carrier = np.exp(2j * np.pi * CARRIER_HZ * np.arange(len(waveform)) / SAMPLE_RATE)
|
||||
waveform = waveform * carrier
|
||||
waveform *= NOMINAL_RMS_CODE / np.sqrt(np.mean(np.abs(waveform) ** 2))
|
||||
return waveform
|
||||
|
||||
|
||||
def level_segment(level_db: float) -> np.ndarray:
|
||||
scale = 10 ** (level_db / 20)
|
||||
return _complex_to_cs8(nominal_segment() * scale)
|
||||
|
||||
|
||||
def segment_metrics(interleaved: np.ndarray, level_db: float) -> dict:
|
||||
values = interleaved[::2].astype(np.float32) + 1j * interleaved[1::2].astype(np.float32)
|
||||
moments = energy.Moments()
|
||||
moments.add(values)
|
||||
result = moments.result(SYMBOL_RATE)
|
||||
result.update({
|
||||
"requested_relative_power_db": level_db,
|
||||
"tx_rms_code": result["rms_code"],
|
||||
"tx_peak_abs_i_code": result["max_abs_i"],
|
||||
"tx_peak_abs_q_code": result["max_abs_q"],
|
||||
"tx_peak_magnitude_code": result["peak_magnitude_code"],
|
||||
"digital_clipping": bool(result["clipped_samples_fraction"] > 0),
|
||||
})
|
||||
return result
|
||||
|
||||
|
||||
def prepared_segments() -> tuple[list[dict], dict[str, np.ndarray]]:
|
||||
cursor = LEADING_ZERO_SAMPLES
|
||||
rows: list[dict] = []
|
||||
blocks: dict[str, np.ndarray] = {}
|
||||
for index, (label, level_db) in enumerate(LEVELS):
|
||||
block = level_segment(level_db)
|
||||
metrics = segment_metrics(block, level_db)
|
||||
rows.append({
|
||||
"label": label,
|
||||
"level_db": level_db,
|
||||
"start_sample": cursor,
|
||||
"samples": SEGMENT_SAMPLES,
|
||||
"duration_seconds": SEGMENT_SAMPLES / SAMPLE_RATE,
|
||||
"analysis_start_sample": cursor + TRIM_SAMPLES,
|
||||
"analysis_samples": SEGMENT_SAMPLES - 2 * TRIM_SAMPLES,
|
||||
"metrics": metrics,
|
||||
})
|
||||
blocks[label] = block
|
||||
cursor += SEGMENT_SAMPLES
|
||||
if index < len(LEVELS) - 1:
|
||||
cursor += GAP_SAMPLES
|
||||
if cursor + TRAILING_ZERO_SAMPLES != TX_SAMPLES:
|
||||
raise RuntimeError("Ошибка схемы TX waveform")
|
||||
nominal_power = rows[-1]["metrics"]["power_code2"]
|
||||
for row in rows:
|
||||
row["metrics"]["actual_relative_power_db"] = float(
|
||||
10 * np.log10(row["metrics"]["power_code2"] / nominal_power)
|
||||
)
|
||||
for lower, upper in zip(rows, rows[1:]):
|
||||
upper["tx_input_difference_from_previous_db"] = float(
|
||||
10 * np.log10(
|
||||
upper["metrics"]["power_code2"] / lower["metrics"]["power_code2"]
|
||||
)
|
||||
)
|
||||
return rows, blocks
|
||||
|
||||
|
||||
def prepare() -> dict:
|
||||
"""Создать единственный конечный TX-файл и его проверяемые метаданные."""
|
||||
RAW.mkdir(parents=True, exist_ok=True)
|
||||
OUTPUT.mkdir(parents=True, exist_ok=True)
|
||||
tx_path = RAW / "tx_level_staircase.cs8"
|
||||
rows, blocks = prepared_segments()
|
||||
with tx_path.open("xb") as stream:
|
||||
stream.write(bytes(2 * LEADING_ZERO_SAMPLES))
|
||||
for index, (label, _) in enumerate(LEVELS):
|
||||
stream.write(blocks[label].tobytes())
|
||||
if index < len(LEVELS) - 1:
|
||||
stream.write(bytes(2 * GAP_SAMPLES))
|
||||
stream.write(bytes(2 * TRAILING_ZERO_SAMPLES))
|
||||
if tx_path.stat().st_size != 2 * TX_SAMPLES:
|
||||
raise RuntimeError("Неверная длина подготовленного TX")
|
||||
if any(row["metrics"]["digital_clipping"] for row in rows):
|
||||
raise RuntimeError("В подготовленном TX обнаружен цифровой clipping")
|
||||
steps = [row["tx_input_difference_from_previous_db"] for row in rows[1:]]
|
||||
if any(abs(step - 6.0) > 0.1 for step in steps):
|
||||
raise RuntimeError("Цифровая лестница не выдерживает шаг +6 дБ")
|
||||
result = {
|
||||
"lab": "Lab064",
|
||||
"stage": "PREPARATION_ONLY",
|
||||
"rf_tx_count": 0,
|
||||
"head": subprocess.check_output(["git", "rev-parse", "HEAD"], text=True).strip(),
|
||||
"parameters": parameters(),
|
||||
"segments": rows,
|
||||
"leading_zero_samples": LEADING_ZERO_SAMPLES,
|
||||
"gap_samples": GAP_SAMPLES,
|
||||
"trailing_zero_samples": TRAILING_ZERO_SAMPLES,
|
||||
"tx_samples": TX_SAMPLES,
|
||||
"tx_bytes": 2 * TX_SAMPLES,
|
||||
"tx_duration_seconds": TX_SAMPLES / SAMPLE_RATE,
|
||||
"tx_sha256": sha256(tx_path),
|
||||
"rx_samples": RX_SAMPLES,
|
||||
"rx_duration_seconds": RX_SECONDS,
|
||||
"rx_lead_seconds": RX_LEAD_SECONDS,
|
||||
"automatic_retry": False,
|
||||
"tx_command_prepared_not_run": transfer_command(tx_path, TX_SAMPLES, transmit=True),
|
||||
"rx_command_prepared_not_run": transfer_command(RAW / "rx_stream.cs8", RX_SAMPLES),
|
||||
}
|
||||
save_new(RAW / "tx_metadata.json", result)
|
||||
save_new(OUTPUT / "preparation.json", result)
|
||||
return result
|
||||
|
||||
|
||||
def validate_prepared() -> dict:
|
||||
metadata = json.loads((RAW / "tx_metadata.json").read_text(encoding="utf-8"))
|
||||
path = RAW / "tx_level_staircase.cs8"
|
||||
rows, blocks = prepared_segments()
|
||||
digest = hashlib.sha256()
|
||||
digest.update(bytes(2 * LEADING_ZERO_SAMPLES))
|
||||
for index, (label, _) in enumerate(LEVELS):
|
||||
digest.update(blocks[label].tobytes())
|
||||
if index < len(LEVELS) - 1:
|
||||
digest.update(bytes(2 * GAP_SAMPLES))
|
||||
digest.update(bytes(2 * TRAILING_ZERO_SAMPLES))
|
||||
if (
|
||||
metadata.get("lab") != "Lab064"
|
||||
or metadata.get("stage") != "PREPARATION_ONLY"
|
||||
or metadata.get("rf_tx_count") != 0
|
||||
or metadata.get("parameters") != parameters()
|
||||
or metadata.get("segments") != rows
|
||||
or metadata.get("tx_samples") != TX_SAMPLES
|
||||
or path.stat().st_size != 2 * TX_SAMPLES
|
||||
or metadata.get("tx_sha256") != digest.hexdigest()
|
||||
or sha256(path) != digest.hexdigest()
|
||||
):
|
||||
raise RuntimeError("Подготовленный Lab064 TX или параметры изменились")
|
||||
return metadata
|
||||
|
||||
|
||||
def difference_db(new: float, old: float) -> float:
|
||||
if new <= 0 or old <= 0:
|
||||
raise ValueError("Для разности в дБ нужны положительные мощности")
|
||||
return float(10 * np.log10(new / old))
|
||||
|
||||
|
||||
def counter_observations(log: str) -> list[dict]:
|
||||
"""Сохранить все промежуточные и итоговые счётчики потерь из лога."""
|
||||
pattern = re.compile(
|
||||
r"(?<![\w.+-])(\d+)\s+(?:(rx|tx)\s+)?(overruns?|underruns?)\b",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
return [
|
||||
{
|
||||
"count": int(match.group(1)),
|
||||
"role": (match.group(2) or "UNKNOWN").upper(),
|
||||
"kind": match.group(3).lower().rstrip("s"),
|
||||
"text": match.group(0),
|
||||
"character_offset": match.start(),
|
||||
}
|
||||
for match in pattern.finditer(log)
|
||||
]
|
||||
|
||||
|
||||
def counter_summary(log: str, expected_kind: str) -> dict:
|
||||
observations = counter_observations(log)
|
||||
selected = [row for row in observations if row["kind"] == expected_kind]
|
||||
unparsed = re.search(
|
||||
r"overrun|underrun",
|
||||
re.sub(
|
||||
r"(?<![\w.+-])\d+\s+(?:(?:rx|tx)\s+)?(?:overruns?|underruns?)\b",
|
||||
"",
|
||||
log,
|
||||
flags=re.IGNORECASE,
|
||||
),
|
||||
re.IGNORECASE,
|
||||
)
|
||||
return {
|
||||
"expected_kind": expected_kind,
|
||||
"observations": observations,
|
||||
"selected_counts": [row["count"] for row in selected],
|
||||
"final_count": selected[-1]["count"] if selected else None,
|
||||
"maximum_count": max((row["count"] for row in selected), default=None),
|
||||
"unparsed_loss_text": bool(unparsed),
|
||||
"clean": bool(selected) and not unparsed and all(row["count"] == 0 for row in observations),
|
||||
}
|
||||
|
||||
|
||||
def synthetic_linearity_gate() -> dict:
|
||||
"""Проверить метрики на известном линейном канале без аппаратуры."""
|
||||
rows, blocks = prepared_segments()
|
||||
random_generator = np.random.default_rng(64064)
|
||||
measured = []
|
||||
for row in rows:
|
||||
block = blocks[row["label"]]
|
||||
reference = block[::2].astype(np.float64) + 1j * block[1::2].astype(np.float64)
|
||||
reference = reference[TRIM_SAMPLES:-TRIM_SAMPLES]
|
||||
noise = random_generator.normal(0, 0.03, len(reference)) + 1j * random_generator.normal(
|
||||
0, 0.03, len(reference)
|
||||
)
|
||||
received = 0.37 * np.exp(0.41j) * reference + (0.8 - 0.3j) + noise
|
||||
fitted = energy.fit_channel(reference, received, SAMPLE_RATE)
|
||||
measured.append({
|
||||
"label": row["label"],
|
||||
"matched_signal_power": fitted["heldout_signal_power"],
|
||||
"residual_power": fitted["heldout_residual_power"],
|
||||
"gain_abs": fitted["gain_abs"],
|
||||
})
|
||||
step_errors = []
|
||||
for index in range(1, len(rows)):
|
||||
tx_step = rows[index]["tx_input_difference_from_previous_db"]
|
||||
rx_step = difference_db(
|
||||
measured[index]["matched_signal_power"],
|
||||
measured[index - 1]["matched_signal_power"],
|
||||
)
|
||||
step_errors.append(rx_step - tx_step)
|
||||
passed = max(abs(error) for error in step_errors) < 0.02
|
||||
return {
|
||||
"lab": "Lab064",
|
||||
"stage": "SOFTWARE_LINEARITY_GATE",
|
||||
"rf_tx_count": 0,
|
||||
"channel_model": "known linear complex gain plus DC and deterministic Gaussian noise",
|
||||
"rows": measured,
|
||||
"linearity_errors_db": step_errors,
|
||||
"passed": passed,
|
||||
}
|
||||
|
||||
|
||||
def software_gate() -> dict:
|
||||
metadata = validate_prepared()
|
||||
result = synthetic_linearity_gate()
|
||||
result["tx_sha256"] = metadata["tx_sha256"]
|
||||
save_new(OUTPUT / "software_gate.json", result)
|
||||
if not result["passed"]:
|
||||
raise RuntimeError("Lab064 software gate FAIL")
|
||||
return result
|
||||
|
||||
|
||||
def locate_waveform(tx: energy.Cs8File, rx: energy.Cs8File) -> dict:
|
||||
"""Найти всю лестницу по прореженной диагностической корреляции."""
|
||||
stride = 32
|
||||
|
||||
def decimated(source: energy.Cs8File) -> np.ndarray:
|
||||
chunks = [
|
||||
source.read(start, min(262_144, source.size - start))[::stride]
|
||||
for start in range(0, source.size, 262_144)
|
||||
]
|
||||
return np.concatenate(chunks).astype(np.complex128)
|
||||
|
||||
reference = decimated(tx)
|
||||
received = decimated(rx)
|
||||
if len(received) < len(reference):
|
||||
raise ValueError("RX короче TX waveform")
|
||||
reference -= reference.mean()
|
||||
received -= received.mean()
|
||||
reference_energy = float(np.sum(np.abs(reference) ** 2))
|
||||
cumulative = np.concatenate(([0.0], np.cumsum(np.abs(received) ** 2)))
|
||||
window_energy = cumulative[len(reference):] - cumulative[:-len(reference)]
|
||||
time_axis = np.arange(len(reference)) * stride / SAMPLE_RATE
|
||||
best = (-1.0, 0, 0.0)
|
||||
for frequency_hz in np.arange(-3.0, 3.01, 0.25):
|
||||
rotated = reference * np.exp(2j * np.pi * frequency_hz * time_axis)
|
||||
correlation = signal.fftconvolve(received, np.conj(rotated[::-1]), mode="valid")
|
||||
scores = np.abs(correlation) / np.sqrt(
|
||||
np.maximum(reference_energy * window_energy, 1e-30)
|
||||
)
|
||||
index = int(np.argmax(scores))
|
||||
if scores[index] > best[0]:
|
||||
best = (float(scores[index]), index * stride, float(frequency_hz))
|
||||
if best[0] < 0.05:
|
||||
raise RuntimeError("Лестница уровней не локализована; измерение UNKNOWN")
|
||||
return {"start_sample": best[1], "correlation": best[0], "coarse_cfo_hz": best[2], "stride": stride}
|
||||
|
||||
|
||||
def _window_metrics(values: np.ndarray) -> dict:
|
||||
moments = energy.Moments()
|
||||
moments.add(values)
|
||||
result = moments.result(SYMBOL_RATE)
|
||||
result["ac_rms_code"] = float(np.sqrt(max(0.0, result["ac_power_code2"])))
|
||||
return result
|
||||
|
||||
|
||||
def background_windows() -> list[dict]:
|
||||
"""Окна внутри всех нулевых участков, вдали от активных границ."""
|
||||
starts = (
|
||||
("INITIAL_GUARD", 320_000),
|
||||
("GAP_L0_L1", 1_760_000),
|
||||
("GAP_L1_L2", 2_960_000),
|
||||
("GAP_L2_L3", 4_160_000),
|
||||
("FINAL_GUARD", 5_440_000),
|
||||
)
|
||||
return [
|
||||
{
|
||||
"label": label,
|
||||
"tx_relative_start_sample": start,
|
||||
"samples": BACKGROUND_WINDOW_SAMPLES,
|
||||
"duration_seconds": BACKGROUND_WINDOW_SAMPLES / SAMPLE_RATE,
|
||||
}
|
||||
for label, start in starts
|
||||
]
|
||||
|
||||
|
||||
def summarize_background(rows: list[dict]) -> dict:
|
||||
powers = [row["metrics"]["ac_power_code2"] for row in rows]
|
||||
rms_values = [row["metrics"]["ac_rms_code"] for row in rows]
|
||||
variation_db = difference_db(max(powers), min(powers))
|
||||
return {
|
||||
"ac_power_min_code2": min(powers),
|
||||
"ac_power_max_code2": max(powers),
|
||||
"ac_rms_min_code": min(rms_values),
|
||||
"ac_rms_max_code": max(rms_values),
|
||||
"maximum_to_minimum_ac_power_db": variation_db,
|
||||
"stability_rule_db": 3.0,
|
||||
"stable": variation_db <= 3.0,
|
||||
}
|
||||
|
||||
|
||||
def residual_spectral_diagnostics(
|
||||
reference: np.ndarray,
|
||||
received: np.ndarray,
|
||||
cfo_hz: float,
|
||||
complex_gain: dict,
|
||||
) -> dict:
|
||||
"""Оценить паразитные полосы после вычитания согласованной составляющей."""
|
||||
rotated = reference * np.exp(2j * np.pi * cfo_hz * np.arange(len(reference)) / SAMPLE_RATE)
|
||||
rotated -= rotated.mean()
|
||||
centered_received = received - received.mean()
|
||||
gain = complex(complex_gain["real"], complex_gain["imag"])
|
||||
residual = centered_received - gain * rotated
|
||||
frequencies, density = energy.spectrum(residual)
|
||||
half_width = 0.675 * SYMBOL_RATE
|
||||
bands = {}
|
||||
for harmonic in (1, 2, 3):
|
||||
center = harmonic * CARRIER_HZ
|
||||
bands[f"residual_band_h{harmonic}_power_code2"] = energy.band_power(
|
||||
frequencies, density, center - half_width, center + half_width
|
||||
)
|
||||
bands["interpretation"] = (
|
||||
"Diagnostic residual bands at 50/100/150 kHz; not calibrated RF harmonic power"
|
||||
)
|
||||
return bands
|
||||
|
||||
|
||||
def classify_linearity(
|
||||
rows: list[dict], comparisons: list[dict], background_summary: dict
|
||||
) -> dict:
|
||||
errors = [row["linearity_error_db"] for row in comparisons]
|
||||
l3_ratio = rows[-1]["matched_to_residual_db"]
|
||||
clipping = any(row["full_iq"]["clipped_samples_fraction"] > 0 for row in rows)
|
||||
if clipping:
|
||||
case = "INVALID_RX_CLIPPING"
|
||||
elif comparisons[-1]["linearity_error_db"] <= -1.5:
|
||||
case = "COMPRESSION"
|
||||
elif (
|
||||
max(abs(value) for value in errors) <= 1.0
|
||||
and l3_ratio < 3.0
|
||||
and background_summary["stable"]
|
||||
):
|
||||
case = "LINEAR_BUT_WEAK"
|
||||
elif (
|
||||
max(abs(value) for value in errors) <= 1.0
|
||||
and l3_ratio >= 6.0
|
||||
and background_summary["stable"]
|
||||
):
|
||||
case = "HEALTHY_LINEAR_RANGE"
|
||||
else:
|
||||
case = "NOT_LOCALIZED"
|
||||
return {
|
||||
"case": case,
|
||||
"rules_fixed_before_rf": {
|
||||
"linear_max_abs_error_db": 1.0,
|
||||
"compression_upper_step_error_at_most_db": -1.5,
|
||||
"weak_l3_matched_to_residual_below_db": 3.0,
|
||||
"healthy_l3_matched_to_residual_at_least_db": 6.0,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def capture_validity(capture: dict) -> dict:
|
||||
"""Не дать последнему нулевому счётчику скрыть промежуточную потерю."""
|
||||
reasons = []
|
||||
for role, key in (("TX", "tx_counter_summary"), ("RX", "rx_counter_summary")):
|
||||
summary = capture.get(key, {})
|
||||
if summary.get("clean") is not True:
|
||||
reasons.append(f"{role}_COUNTER_NOT_CLEAN")
|
||||
if summary.get("maximum_count") != 0:
|
||||
reasons.append(f"{role}_MAXIMUM_COUNTER_NOT_ZERO")
|
||||
if capture.get("passed") is not True:
|
||||
reasons.append("CAPTURE_RUNNER_NOT_PASSED")
|
||||
if capture.get("rx_clipping") is not False:
|
||||
reasons.append("RX_CLIPPING_NOT_EXCLUDED")
|
||||
return {"valid": not reasons, "invalid_reasons": reasons}
|
||||
|
||||
|
||||
def analyze_capture() -> dict:
|
||||
"""Проанализировать сохранённый RX только как известный измерительный сигнал."""
|
||||
metadata = validate_prepared()
|
||||
rx_path = RAW / "rx_stream.cs8"
|
||||
capture = json.loads((RAW / "capture_result.json").read_text(encoding="utf-8"))
|
||||
if capture.get("rf_tx_count") != 1 or capture.get("rx_sha256") != sha256(rx_path):
|
||||
raise RuntimeError("Нет подтверждённой единственной RX-записи Lab064")
|
||||
tx = energy.Cs8File(RAW / "tx_level_staircase.cs8")
|
||||
rx = energy.Cs8File(rx_path)
|
||||
location = locate_waveform(tx, rx)
|
||||
background_rows = []
|
||||
for window in background_windows():
|
||||
rx_start = location["start_sample"] + window["tx_relative_start_sample"]
|
||||
background_rows.append({
|
||||
**window,
|
||||
"rx_start_sample": rx_start,
|
||||
"metrics": _window_metrics(rx.read(rx_start, window["samples"])),
|
||||
})
|
||||
background_by_label = {row["label"]: row for row in background_rows}
|
||||
preceding_background = {
|
||||
"L0": "INITIAL_GUARD",
|
||||
"L1": "GAP_L0_L1",
|
||||
"L2": "GAP_L1_L2",
|
||||
"L3": "GAP_L2_L3",
|
||||
}
|
||||
rows = []
|
||||
for segment in metadata["segments"]:
|
||||
tx_start = segment["start_sample"]
|
||||
nominal_rx_start = location["start_sample"] + tx_start
|
||||
fitted = energy.analyze_rx(
|
||||
lab056.OffsetSource(tx, tx_start),
|
||||
rx,
|
||||
[nominal_rx_start],
|
||||
SEGMENT_SAMPLES,
|
||||
SAMPLES_PER_SYMBOL,
|
||||
)["frames"][0]
|
||||
actual_start = fitted["start"]
|
||||
full_values = rx.read(actual_start, SEGMENT_SAMPLES)
|
||||
active_reference = tx.read(tx_start + TRIM_SAMPLES, SEGMENT_SAMPLES - 2 * TRIM_SAMPLES)
|
||||
active_received = rx.read(actual_start + TRIM_SAMPLES, SEGMENT_SAMPLES - 2 * TRIM_SAMPLES)
|
||||
complex_gain = lab056.complex_gain(active_reference, active_received, fitted["cfo_hz"])
|
||||
matched = fitted["heldout_signal_power"]
|
||||
residual = fitted["heldout_residual_power"]
|
||||
background = background_by_label[preceding_background[segment["label"]]]
|
||||
rows.append({
|
||||
"label": segment["label"],
|
||||
"level_db": segment["level_db"],
|
||||
"rx_start_sample": actual_start,
|
||||
"alignment_adjustment_samples": fitted["alignment_adjustment"],
|
||||
"full_iq": _window_metrics(full_values),
|
||||
"background_label": background["label"],
|
||||
"background": background["metrics"],
|
||||
"background_window_start_sample": background["rx_start_sample"],
|
||||
"matched_signal_power": matched,
|
||||
"matched_signal_amplitude_rms": float(np.sqrt(matched)),
|
||||
"residual_power": residual,
|
||||
"residual_to_matched_amplitude_ratio": float(np.sqrt(residual / matched)),
|
||||
"matched_to_residual_db": difference_db(matched, residual),
|
||||
"complex_gain_h": complex_gain,
|
||||
"gain_abs": fitted["gain_abs"],
|
||||
"gain_power_abs_h_squared": fitted["gain_abs"] ** 2,
|
||||
"cfo_hz": fitted["cfo_hz"],
|
||||
"residual_spectrum": residual_spectral_diagnostics(
|
||||
active_reference, active_received, fitted["cfo_hz"], complex_gain
|
||||
),
|
||||
})
|
||||
comparisons = []
|
||||
for index in range(1, len(rows)):
|
||||
tx_difference = metadata["segments"][index]["tx_input_difference_from_previous_db"]
|
||||
rx_difference = difference_db(
|
||||
rows[index]["matched_signal_power"], rows[index - 1]["matched_signal_power"]
|
||||
)
|
||||
comparisons.append({
|
||||
"comparison": f"{rows[index]['label']}/{rows[index - 1]['label']}",
|
||||
"tx_input_difference_db": tx_difference,
|
||||
"rx_matched_power_difference_db": rx_difference,
|
||||
"linearity_error_db": rx_difference - tx_difference,
|
||||
"tx_amplitude_ratio": float(10 ** (tx_difference / 20)),
|
||||
"rx_matched_amplitude_ratio": float(10 ** (rx_difference / 20)),
|
||||
})
|
||||
total_tx_difference = difference_db(
|
||||
metadata["segments"][-1]["metrics"]["power_code2"],
|
||||
metadata["segments"][0]["metrics"]["power_code2"],
|
||||
)
|
||||
total_rx_difference = difference_db(
|
||||
rows[-1]["matched_signal_power"], rows[0]["matched_signal_power"]
|
||||
)
|
||||
total_comparison = {
|
||||
"comparison": "L3/L0",
|
||||
"tx_digital_power_difference_db": total_tx_difference,
|
||||
"rx_matched_power_difference_db": total_rx_difference,
|
||||
"total_linearity_error_db": total_rx_difference - total_tx_difference,
|
||||
"tx_amplitude_ratio": float(10 ** (total_tx_difference / 20)),
|
||||
"rx_matched_amplitude_ratio": float(10 ** (total_rx_difference / 20)),
|
||||
}
|
||||
background_summary = summarize_background(background_rows)
|
||||
validity = capture_validity(capture)
|
||||
classification = (
|
||||
classify_linearity(rows, comparisons, background_summary)
|
||||
if validity["valid"]
|
||||
else {
|
||||
"case": "INVALID_CAPTURE",
|
||||
"invalid_reasons": validity["invalid_reasons"],
|
||||
"diagnostic_metrics_only": True,
|
||||
"a_b_c_classification_permitted": False,
|
||||
}
|
||||
)
|
||||
result = {
|
||||
"lab": "Lab064",
|
||||
"stage": "OFFLINE_RF_LEVEL_LINEARITY_ANALYSIS",
|
||||
"packet_receiver_used": False,
|
||||
"location": location,
|
||||
"rows": rows,
|
||||
"background_windows": background_rows,
|
||||
"background_summary": background_summary,
|
||||
"comparisons": comparisons,
|
||||
"total_comparison": total_comparison,
|
||||
"classification": classification,
|
||||
"capture_valid": validity["valid"],
|
||||
"capture_invalid_reasons": validity["invalid_reasons"],
|
||||
"tx_underruns_maximum": capture.get("tx_counter_summary", {}).get("maximum_count"),
|
||||
"tx_underruns_final": capture.get("tx_counter_summary", {}).get("final_count"),
|
||||
"rx_overruns_maximum": capture.get("rx_counter_summary", {}).get("maximum_count"),
|
||||
"rx_overruns_final": capture.get("rx_counter_summary", {}).get("final_count"),
|
||||
"rx_clipping": any(row["full_iq"]["clipped_samples_fraction"] > 0 for row in rows),
|
||||
}
|
||||
save_new(OUTPUT / "rf_result.json", result)
|
||||
return result
|
||||
|
||||
|
||||
def preflight(*, runner=None, process_checker=None, clock_checker=None, sleep_function=time.sleep) -> dict:
|
||||
"""Проверить common clock и 0,1 с RX-фона, не создавая TX-команду."""
|
||||
metadata = validate_prepared()
|
||||
software = json.loads((OUTPUT / "software_gate.json").read_text(encoding="utf-8"))
|
||||
if not software.get("passed") or software.get("tx_sha256") != metadata["tx_sha256"]:
|
||||
raise RuntimeError("Нет действующего Lab064 software gate")
|
||||
save_new(RAW / "preflight_attempt.json", {
|
||||
"lab": "Lab064",
|
||||
"rf_tx_count": 0,
|
||||
"point": asdict(POINT),
|
||||
"parameters": parameters(),
|
||||
"tx_sha256": metadata["tx_sha256"],
|
||||
})
|
||||
actual_runner = runner or lab054.clock._default_command_runner
|
||||
process_checker = process_checker or lab054.check_processes
|
||||
clock_checker = clock_checker or hardware.common_clock
|
||||
|
||||
def rx_only(command: list[str]):
|
||||
if "-t" in command:
|
||||
raise PermissionError("TX запрещён в Lab064 preflight")
|
||||
return actual_runner(command)
|
||||
|
||||
result = {
|
||||
"lab": "Lab064",
|
||||
"stage": "COMMON_CLOCK_AND_RX_ONLY_PREFLIGHT",
|
||||
"passed": False,
|
||||
"rf_tx_count": 0,
|
||||
"stop_before_tx": True,
|
||||
"point": asdict(POINT),
|
||||
"parameters": parameters(),
|
||||
"tx_sha256": metadata["tx_sha256"],
|
||||
}
|
||||
clock_started = False
|
||||
try:
|
||||
before = process_checker()
|
||||
result["process_before"] = before.to_dict()
|
||||
lab054.require_clear(before)
|
||||
result["inventory"] = lab054.phy.lab045._verify_device_inventory(rx_only)
|
||||
clock_started = True
|
||||
result["clock"] = clock_checker(
|
||||
POINT,
|
||||
rx_only,
|
||||
lab054.clock.TransmitGuard(),
|
||||
sleep_function=sleep_function,
|
||||
)
|
||||
hardware.require_clock_pass(result["clock"])
|
||||
path = RAW / "preflight_rx.cs8"
|
||||
if path.exists():
|
||||
raise FileExistsError("Lab064 preflight RX уже существует")
|
||||
captured = rx_only(transfer_command(path, PREFLIGHT_SAMPLES))
|
||||
stdout = captured.stdout or ""
|
||||
stderr = captured.stderr or ""
|
||||
result["rx_process"] = {"returncode": captured.returncode, "stdout": stdout, "stderr": stderr}
|
||||
result["rx_counter_summary"] = counter_summary(stdout + stderr, "overrun")
|
||||
if captured.returncode or not result["rx_counter_summary"]["clean"]:
|
||||
raise RuntimeError("RX preflight: процесс или счётчик overruns не прошёл")
|
||||
if path.stat().st_size != 2 * PREFLIGHT_SAMPLES:
|
||||
raise RuntimeError("Неверная длина Lab064 preflight RX")
|
||||
level = hardware.receive_level(path)
|
||||
level.pop("symbol_energy_code2_seconds", None)
|
||||
level["symbol_energy"] = "NOT_APPLICABLE_TO_BACKGROUND"
|
||||
level["ac_rms_code"] = float(np.sqrt(max(0.0, level["ac_power_code2"])))
|
||||
level["unexpected_strong_signal"] = bool(
|
||||
level["ac_rms_code"] >= STRONG_AC_RMS_CODE
|
||||
or level["max_abs_i"] >= STRONG_COMPONENT_CODE
|
||||
or level["max_abs_q"] >= STRONG_COMPONENT_CODE
|
||||
)
|
||||
level["strong_signal_rule"] = (
|
||||
f"AC RMS >= {STRONG_AC_RMS_CODE} code or max |I|/|Q| >= {STRONG_COMPONENT_CODE} code"
|
||||
)
|
||||
result["rx_level"] = level
|
||||
result["rx_sha256"] = sha256(path)
|
||||
result["rx_overruns"] = result["rx_counter_summary"]["final_count"]
|
||||
if level["saturation_detected"]:
|
||||
raise RuntimeError("RX clipping; STOP без изменения gain")
|
||||
if level["unexpected_strong_signal"]:
|
||||
raise RuntimeError("Неожиданно сильный RX-only фон; STOP без изменения gain")
|
||||
result["passed"] = True
|
||||
except Exception as error:
|
||||
result["error"] = str(error)
|
||||
raise
|
||||
finally:
|
||||
errors = lab054.disable_clock_outputs(rx_only) if clock_started else []
|
||||
try:
|
||||
after = process_checker()
|
||||
result["process_after"] = after.to_dict()
|
||||
lab054.require_clear(after)
|
||||
except Exception as error:
|
||||
errors.append(str(error))
|
||||
result["cleanup_errors"] = errors
|
||||
result["passed"] = result["passed"] and not errors
|
||||
save_new(RAW / "preflight_result.json", result)
|
||||
OUTPUT.mkdir(parents=True, exist_ok=True)
|
||||
(OUTPUT / "preflight_result.json").write_text(
|
||||
json.dumps(result, ensure_ascii=False, indent=2, allow_nan=False) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
if not result["passed"]:
|
||||
raise RuntimeError("Lab064 preflight cleanup FAIL")
|
||||
return result
|
||||
|
||||
|
||||
def require_preflight(metadata: dict) -> dict:
|
||||
record = json.loads((RAW / "preflight_result.json").read_text(encoding="utf-8"))
|
||||
if (
|
||||
record.get("passed") is not True
|
||||
or record.get("rf_tx_count") != 0
|
||||
or record.get("point") != asdict(POINT)
|
||||
or record.get("parameters") != parameters()
|
||||
or record.get("tx_sha256") != metadata["tx_sha256"]
|
||||
):
|
||||
raise PermissionError("Нет успешного RX-only preflight для этой конфигурации Lab064")
|
||||
hardware.require_clock_pass(record.get("clock", {}))
|
||||
level = record.get("rx_level", {})
|
||||
if (
|
||||
level.get("saturation_detected") is not False
|
||||
or level.get("unexpected_strong_signal") is not False
|
||||
or record.get("rx_overruns") != 0
|
||||
):
|
||||
raise PermissionError("RX-only preflight не разрешает передачу")
|
||||
return record
|
||||
|
||||
|
||||
def capture_once(
|
||||
*,
|
||||
authorization: bool = False,
|
||||
runner=None,
|
||||
process_starter=None,
|
||||
process_checker=None,
|
||||
clock_checker=None,
|
||||
sleep_function=time.sleep,
|
||||
) -> dict:
|
||||
"""Один RX-first сеанс; любая попытка расходуется до обращения к SDR."""
|
||||
if authorization is not True:
|
||||
raise PermissionError("Нужно отдельное разрешение на единственный RF-сеанс Lab064")
|
||||
metadata = validate_prepared()
|
||||
require_preflight(metadata)
|
||||
receive_path = RAW / "rx_stream.cs8"
|
||||
if receive_path.exists() or (RAW / "capture_result.json").exists():
|
||||
raise FileExistsError("Аппаратный результат Lab064 уже существует")
|
||||
actual_runner = runner or lab054.clock._default_command_runner
|
||||
process_starter = process_starter or lab054.clock._default_process_starter
|
||||
process_checker = process_checker or lab054.check_processes
|
||||
clock_checker = clock_checker or hardware.common_clock
|
||||
save_new(RAW / "hardware_attempt.json", {
|
||||
"lab": "Lab064",
|
||||
"authorized_once": True,
|
||||
"automatic_retry": False,
|
||||
"point": asdict(POINT),
|
||||
"parameters": parameters(),
|
||||
"tx_sha256": metadata["tx_sha256"],
|
||||
})
|
||||
result = {
|
||||
"lab": "Lab064",
|
||||
"stage": "SINGLE_RF_LEVEL_CAPTURE",
|
||||
"passed": False,
|
||||
"rf_tx_count": 0,
|
||||
"automatic_tx_repeats": 0,
|
||||
"point": asdict(POINT),
|
||||
"parameters": parameters(),
|
||||
"tx_sha256": metadata["tx_sha256"],
|
||||
}
|
||||
guard = lab054.clock.TransmitGuard()
|
||||
process = None
|
||||
clock_started = False
|
||||
try:
|
||||
before = process_checker()
|
||||
result["process_before"] = before.to_dict()
|
||||
lab054.require_clear(before)
|
||||
result["inventory"] = lab054.phy.lab045._verify_device_inventory(actual_runner)
|
||||
clock_started = True
|
||||
result["clock"] = clock_checker(
|
||||
POINT, actual_runner, guard, sleep_function=sleep_function
|
||||
)
|
||||
hardware.require_clock_pass(result["clock"])
|
||||
process = process_starter(
|
||||
transfer_command(receive_path, RX_SAMPLES),
|
||||
RAW / "rx_stdout.txt",
|
||||
RAW / "rx_stderr.txt",
|
||||
)
|
||||
sleep_function(RX_LEAD_SECONDS)
|
||||
if process.poll() is not None:
|
||||
raise RuntimeError("RX завершился до TX")
|
||||
before_tx = hardware.receive_level(receive_path, tail_samples=65_536)
|
||||
before_tx.pop("symbol_energy_code2_seconds", None)
|
||||
result["rx_level_before_tx"] = before_tx
|
||||
if before_tx["saturation_detected"]:
|
||||
raise RuntimeError("RX clipping до TX; RF TX = 0")
|
||||
guard.register_transmit()
|
||||
result["rf_tx_count"] = guard.transmit_calls
|
||||
transmitted = actual_runner(
|
||||
transfer_command(RAW / "tx_level_staircase.cs8", TX_SAMPLES, transmit=True)
|
||||
)
|
||||
tx_stdout = transmitted.stdout or ""
|
||||
tx_stderr = transmitted.stderr or ""
|
||||
save_text_new(RAW / "tx_stdout.txt", tx_stdout)
|
||||
save_text_new(RAW / "tx_stderr.txt", tx_stderr)
|
||||
result["tx_process"] = {
|
||||
"returncode": transmitted.returncode,
|
||||
"stdout_file": "tx_stdout.txt",
|
||||
"stderr_file": "tx_stderr.txt",
|
||||
}
|
||||
result["tx_counter_summary"] = counter_summary(tx_stdout + tx_stderr, "underrun")
|
||||
if transmitted.returncode:
|
||||
raise RuntimeError("Единственный TX завершился ошибкой; повтор запрещён")
|
||||
if process.wait(RX_SECONDS + 10):
|
||||
raise RuntimeError("RX завершился ошибкой; передача уже выполнена, повтор запрещён")
|
||||
process.close_logs()
|
||||
result["rx_samples"] = receive_path.stat().st_size // 2
|
||||
result["rx_bytes"] = receive_path.stat().st_size
|
||||
result["rx_duration_seconds"] = result["rx_samples"] / SAMPLE_RATE
|
||||
if result["rx_bytes"] != 2 * RX_SAMPLES:
|
||||
raise RuntimeError("Повреждённая длина RX; повтор запрещён")
|
||||
rx_stdout = (RAW / "rx_stdout.txt").read_text(encoding="utf-8")
|
||||
rx_stderr = (RAW / "rx_stderr.txt").read_text(encoding="utf-8")
|
||||
result["rx_counter_summary"] = counter_summary(rx_stdout + rx_stderr, "overrun")
|
||||
result["rx_sha256"] = sha256(receive_path)
|
||||
level = hardware.receive_level(receive_path)
|
||||
level.pop("symbol_energy_code2_seconds", None)
|
||||
result["rx_level"] = level
|
||||
result["rx_clipping"] = bool(level["saturation_detected"])
|
||||
if not result["tx_counter_summary"]["clean"]:
|
||||
raise RuntimeError("TX underrun или неоднозначный счётчик; результат сохранён, повтор запрещён")
|
||||
if not result["rx_counter_summary"]["clean"]:
|
||||
raise RuntimeError("RX overrun или неоднозначный счётчик; результат сохранён, повтор запрещён")
|
||||
if result["rx_clipping"]:
|
||||
raise RuntimeError("RX clipping в записи; результат сохранён, повтор запрещён")
|
||||
result["passed"] = True
|
||||
except Exception as error:
|
||||
result["error"] = str(error)
|
||||
raise
|
||||
finally:
|
||||
errors = []
|
||||
if process is not None:
|
||||
try:
|
||||
process.terminate()
|
||||
except Exception as error:
|
||||
errors.append(str(error))
|
||||
if clock_started:
|
||||
errors.extend(lab054.disable_clock_outputs(actual_runner))
|
||||
try:
|
||||
after = process_checker()
|
||||
result["process_after"] = after.to_dict()
|
||||
lab054.require_clear(after)
|
||||
except Exception as error:
|
||||
errors.append(str(error))
|
||||
result["cleanup_errors"] = errors
|
||||
result["passed"] = result["passed"] and not errors
|
||||
save_new(RAW / "capture_result.json", result)
|
||||
OUTPUT.mkdir(parents=True, exist_ok=True)
|
||||
(OUTPUT / "capture_result.json").write_text(
|
||||
json.dumps(result, ensure_ascii=False, indent=2, allow_nan=False) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
if not result["passed"]:
|
||||
raise RuntimeError("Lab064 RF-сеанс завершён с ошибкой; повтор запрещён")
|
||||
return result
|
||||
|
||||
|
||||
def run_functional_tests() -> None:
|
||||
assert SAMPLES_PER_SYMBOL == 400
|
||||
assert PRBS_SYMBOLS == 1958
|
||||
assert TX_SAMPLES == 6_000_000
|
||||
rows, _ = prepared_segments()
|
||||
assert len(rows) == 4
|
||||
assert all(not row["metrics"]["digital_clipping"] for row in rows)
|
||||
assert max(abs(row["tx_input_difference_from_previous_db"] - 6.0) for row in rows[1:]) < 0.1
|
||||
assert synthetic_linearity_gate()["passed"]
|
||||
assert "-t" not in transfer_command("NUL", 65_536)
|
||||
try:
|
||||
capture_once(authorization=False)
|
||||
except PermissionError:
|
||||
pass
|
||||
else:
|
||||
raise AssertionError("RF без отдельного разрешения")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
"--stage",
|
||||
choices=("prepare", "software", "preflight", "capture", "analyze"),
|
||||
required=True,
|
||||
)
|
||||
parser.add_argument("--authorize-one-rf", action="store_true")
|
||||
arguments = parser.parse_args()
|
||||
if arguments.stage == "prepare":
|
||||
result = prepare()
|
||||
elif arguments.stage == "software":
|
||||
result = software_gate()
|
||||
elif arguments.stage == "preflight":
|
||||
result = preflight()
|
||||
elif arguments.stage == "capture":
|
||||
result = capture_once(authorization=arguments.authorize_one_rf)
|
||||
else:
|
||||
result = analyze_capture()
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2, allow_nan=False))
|
||||
print("RF TX = 0")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
198
tests/test_lab064_rf_level_linearity.py
Normal file
198
tests/test_lab064_rf_level_linearity.py
Normal file
@@ -0,0 +1,198 @@
|
||||
"""Lab064: цифровая лестница, измерительные метрики и запрет RF без разрешения."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from experiments import lab064_rf_level_linearity as lab
|
||||
from tools.hackrf_process_check import ProcessCheck, ProcessState
|
||||
|
||||
|
||||
def test_fixed_operating_point_and_measurement_scope():
|
||||
parameters = lab.parameters()
|
||||
assert parameters["rf_frequency_hz"] == 1_400_000_000
|
||||
assert parameters["sample_rate_hz"] == 8_000_000
|
||||
assert parameters["symbol_rate"] == 20_000
|
||||
assert parameters["samples_per_symbol"] == 400
|
||||
assert parameters["digital_carrier_hz"] == 50_000
|
||||
assert parameters["levels_db_relative_nominal"] == [-18.0, -12.0, -6.0, 0.0]
|
||||
assert parameters["full_phy_packet"] is False
|
||||
assert parameters["tx_vga_db"] == 10
|
||||
assert parameters["rx_lna_db"] == parameters["rx_vga_db"] == 24
|
||||
assert lab.POINT.distance_m == 1.2
|
||||
assert lab.POINT.visibility == "LOS indoor"
|
||||
|
||||
|
||||
def test_waveform_has_exact_guards_segments_gaps_and_levels():
|
||||
rows, blocks = lab.prepared_segments()
|
||||
assert lab.TX_SAMPLES == 6_000_000
|
||||
assert lab.TX_SAMPLES / lab.SAMPLE_RATE == 0.75
|
||||
assert all(row["samples"] == 800_000 for row in rows)
|
||||
assert [row["start_sample"] for row in rows] == [800_000, 2_000_000, 3_200_000, 4_400_000]
|
||||
assert all(len(blocks[row["label"]]) == 2 * lab.SEGMENT_SAMPLES for row in rows)
|
||||
assert all(not row["metrics"]["digital_clipping"] for row in rows)
|
||||
steps = [row["tx_input_difference_from_previous_db"] for row in rows[1:]]
|
||||
assert np.allclose(steps, 6.0, atol=0.1)
|
||||
|
||||
|
||||
def test_background_windows_cover_both_guards_and_all_gaps():
|
||||
windows = lab.background_windows()
|
||||
assert [row["label"] for row in windows] == [
|
||||
"INITIAL_GUARD", "GAP_L0_L1", "GAP_L1_L2", "GAP_L2_L3", "FINAL_GUARD"
|
||||
]
|
||||
assert all(row["samples"] == 160_000 for row in windows)
|
||||
zero_ranges = [
|
||||
(0, 800_000),
|
||||
(1_600_000, 2_000_000),
|
||||
(2_800_000, 3_200_000),
|
||||
(4_000_000, 4_400_000),
|
||||
(5_200_000, 6_000_000),
|
||||
]
|
||||
for row, (low, high) in zip(windows, zero_ranges):
|
||||
assert low <= row["tx_relative_start_sample"]
|
||||
assert row["tx_relative_start_sample"] + row["samples"] <= high
|
||||
|
||||
|
||||
def test_background_stability_uses_ac_power_range():
|
||||
stable = lab.summarize_background([
|
||||
{"metrics": {"ac_power_code2": 1.0, "ac_rms_code": 1.0}},
|
||||
{"metrics": {"ac_power_code2": 1.5, "ac_rms_code": np.sqrt(1.5)}},
|
||||
])
|
||||
changed = lab.summarize_background([
|
||||
{"metrics": {"ac_power_code2": 1.0, "ac_rms_code": 1.0}},
|
||||
{"metrics": {"ac_power_code2": 4.0, "ac_rms_code": 2.0}},
|
||||
])
|
||||
assert stable["stable"] is True
|
||||
assert changed["stable"] is False
|
||||
assert changed["maximum_to_minimum_ac_power_db"] == pytest.approx(6.020599913)
|
||||
|
||||
|
||||
def test_synthetic_linear_channel_recovers_digital_steps():
|
||||
result = lab.synthetic_linearity_gate()
|
||||
assert result["passed"]
|
||||
assert max(abs(value) for value in result["linearity_errors_db"]) < 0.02
|
||||
gains = [row["gain_abs"] for row in result["rows"]]
|
||||
assert np.allclose(gains, 0.37, atol=0.001)
|
||||
|
||||
|
||||
def test_counter_parser_keeps_every_intermediate_and_final_observation():
|
||||
log = "0 TX underruns\n1 TX underrun\n3 TX underruns\n3 TX underruns\n"
|
||||
summary = lab.counter_summary(log, "underrun")
|
||||
assert summary["selected_counts"] == [0, 1, 3, 3]
|
||||
assert summary["final_count"] == 3
|
||||
assert summary["maximum_count"] == 3
|
||||
assert summary["clean"] is False
|
||||
assert [row["character_offset"] for row in summary["observations"]] == sorted(
|
||||
row["character_offset"] for row in summary["observations"]
|
||||
)
|
||||
assert lab.counter_summary("0 RX overruns", "overrun")["clean"] is True
|
||||
|
||||
|
||||
def test_intermediate_loss_cannot_be_hidden_by_final_zero():
|
||||
tx = lab.counter_summary("0 TX underruns\n1 TX underrun\n0 TX underruns", "underrun")
|
||||
rx = lab.counter_summary("0 RX overruns", "overrun")
|
||||
validity = lab.capture_validity({
|
||||
"passed": False,
|
||||
"rx_clipping": False,
|
||||
"tx_counter_summary": tx,
|
||||
"rx_counter_summary": rx,
|
||||
})
|
||||
assert tx["final_count"] == 0
|
||||
assert tx["maximum_count"] == 1
|
||||
assert validity["valid"] is False
|
||||
assert "TX_MAXIMUM_COUNTER_NOT_ZERO" in validity["invalid_reasons"]
|
||||
assert "CAPTURE_RUNNER_NOT_PASSED" in validity["invalid_reasons"]
|
||||
|
||||
|
||||
def test_commands_fix_gains_and_keep_rx_separate_from_tx():
|
||||
receive = lab.transfer_command("rx.cs8", 800_000)
|
||||
transmit = lab.transfer_command("tx.cs8", 100, transmit=True)
|
||||
assert "-r" in receive and "-t" not in receive
|
||||
assert "-t" in transmit and "-r" not in transmit
|
||||
assert receive[receive.index("-f") + 1] == "1400000000"
|
||||
assert receive[receive.index("-s") + 1] == "8000000"
|
||||
assert receive[receive.index("-l") + 1] == receive[receive.index("-g") + 1] == "24"
|
||||
assert transmit[transmit.index("-x") + 1] == "10"
|
||||
assert receive[-5:] == ["-a", "0", "-p", "0", "-B"]
|
||||
assert transmit[-5:] == ["-a", "0", "-p", "0", "-B"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("failure", [None, "clock", "clipping", "strong", "overrun"])
|
||||
def test_preflight_never_transmits_and_fails_closed(tmp_path, monkeypatch, failure):
|
||||
raw = tmp_path / "raw"
|
||||
output = tmp_path / "processed"
|
||||
raw.mkdir()
|
||||
output.mkdir()
|
||||
monkeypatch.setattr(lab, "RAW", raw)
|
||||
monkeypatch.setattr(lab, "OUTPUT", output)
|
||||
(raw / "tx_level_staircase.cs8").write_bytes(b"test")
|
||||
metadata = {"tx_sha256": "TEST"}
|
||||
(output / "software_gate.json").write_text(
|
||||
'{"passed": true, "tx_sha256": "TEST"}', encoding="utf-8"
|
||||
)
|
||||
monkeypatch.setattr(lab, "validate_prepared", lambda: metadata)
|
||||
monkeypatch.setattr(
|
||||
lab.lab054.phy.lab045,
|
||||
"_verify_device_inventory",
|
||||
lambda runner: {"passed": True},
|
||||
)
|
||||
|
||||
def clock(point, runner, guard, **kwargs):
|
||||
return {
|
||||
"passed": failure != "clock",
|
||||
"master_reg29": 0x26,
|
||||
"slave_reg29": 0x26,
|
||||
"slave_clkin_status": "DETECTED",
|
||||
}
|
||||
|
||||
def runner(command):
|
||||
assert "-t" not in command
|
||||
if "-r" in command:
|
||||
value = 127 if failure == "clipping" else (110 if failure == "strong" else 1)
|
||||
Path(command[command.index("-r") + 1]).write_bytes(bytes([value, value]) * 800_000)
|
||||
log = "1 RX overruns" if failure == "overrun" else "0 RX overruns"
|
||||
return subprocess.CompletedProcess(command, 0, log, "")
|
||||
|
||||
monkeypatch.setattr(lab.lab054, "disable_clock_outputs", lambda runner: [])
|
||||
options = {
|
||||
"runner": runner,
|
||||
"clock_checker": clock,
|
||||
"process_checker": lambda: ProcessCheck(ProcessState.CLEAR, ()),
|
||||
"sleep_function": lambda _: None,
|
||||
}
|
||||
if failure:
|
||||
with pytest.raises(RuntimeError):
|
||||
lab.preflight(**options)
|
||||
else:
|
||||
assert lab.preflight(**options)["passed"]
|
||||
result = json.loads((raw / "preflight_result.json").read_text(encoding="utf-8"))
|
||||
assert result["rf_tx_count"] == 0
|
||||
assert result["stop_before_tx"] is True
|
||||
|
||||
|
||||
def test_second_preflight_is_refused_before_hardware(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(lab, "RAW", tmp_path)
|
||||
monkeypatch.setattr(lab, "OUTPUT", tmp_path)
|
||||
(tmp_path / "preflight_attempt.json").write_text("{}", encoding="utf-8")
|
||||
(tmp_path / "software_gate.json").write_text(
|
||||
'{"passed": true, "tx_sha256": "TEST"}', encoding="utf-8"
|
||||
)
|
||||
monkeypatch.setattr(lab, "validate_prepared", lambda: {"tx_sha256": "TEST"})
|
||||
with pytest.raises(FileExistsError):
|
||||
lab.preflight(runner=lambda command: pytest.fail("hardware must not run"))
|
||||
|
||||
|
||||
def test_capture_refuses_missing_authorization_before_files_or_hardware(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(lab, "RAW", tmp_path)
|
||||
monkeypatch.setattr(lab, "OUTPUT", tmp_path)
|
||||
with pytest.raises(PermissionError):
|
||||
lab.capture_once(
|
||||
authorization=False,
|
||||
runner=lambda command: pytest.fail("hardware must not run"),
|
||||
)
|
||||
assert not (tmp_path / "hardware_attempt.json").exists()
|
||||
Reference in New Issue
Block a user