Lab062: record invalidated 500 ksymbol/s RF packet trial
This commit is contained in:
@@ -2661,3 +2661,72 @@ READY_BEHIND_EXPLICIT_MODE до отдельного контролируемо
|
||||
500K-пакетами. Оборудование не опрашивалось, RF TX=0. Lab061 фиксируется этим
|
||||
отдельным коммитом; push выполняется пользователем отдельно. Следующий этап:
|
||||
STOP; Lab062 автоматически не начинать.
|
||||
|
||||
# Запись 056
|
||||
|
||||
## Дата
|
||||
|
||||
14 сентября 2026 года
|
||||
|
||||
## Тема
|
||||
|
||||
Lab062 — подготовка полного пакета 500K и аппаратный RX-only preflight
|
||||
|
||||
## Выполнено
|
||||
|
||||
От HEAD 25cc89f0c734f77141632d19dbcdb6b2f066d60b подготовлен один waveform
|
||||
из16 полных PHY-пакетов по256 байт payload:1476096 отсчётов,0,184512с,
|
||||
SHA-256 e84bbce227426c0aac13a48ded1286ca4a44fc6e3efce01f6e5a513217c87ff5.
|
||||
Строгий программный roundtrip legacy/T2 дал16/16 HEADER/FEC/CRC и точных
|
||||
payload в исходном порядке. Quick gate:962 теста ядра,35 функциональных
|
||||
проверок,13 пропусков; pytest:962 passed,0 failed.
|
||||
|
||||
Фактическая схема:1400МГц,500ксимв/с, Fs8Мвыб/с, расстояние примерно1,2м,
|
||||
LOS в помещении; TX возле окна, RX на столе, одинаковая поляризация. Антенны,
|
||||
кабели и USB-схема неподвижны. Усиления сохранены: TX VGA10дБ, RX LNA/VGA
|
||||
24/24дБ, RF amplifier и Bias-Tee OFF. Общая опора:10МГц TCXO → MASTER →
|
||||
MASTER CLKOUT → SLAVE CLKIN, длинный кабель сохранён.
|
||||
|
||||
Один аппаратный RX-only preflight подтвердил MASTER/SLAVE reg29=0x26 и SLAVE
|
||||
CLKIN=DETECTED на втором опросе через133,5795мс. Записано800000 отсчётов фона:
|
||||
полный RMS2,314058, AC RMS0,985483 кода, max|I|/|Q|=6/8, clipping0/800000,
|
||||
RX overruns0, неожиданно сильного сигнала нет. Отсутствие аналоговой компрессии
|
||||
этим не доказано. CLKOUT выключены без ошибок, процессы до/после CLEAR.
|
||||
|
||||
RF TX=0, TX-команд не было, усиления не менялись, preflight не повторялся.
|
||||
Lab062 не коммитировалась, push не выполнялся. Следующий этап: STOP и ожидание
|
||||
отдельного явного разрешения на единственный Lab062 TX; Lab063 не начинать.
|
||||
|
||||
# Запись 057
|
||||
|
||||
## Дата
|
||||
|
||||
14 сентября 2026 года
|
||||
|
||||
## Тема
|
||||
|
||||
Lab062 — единственный RF-сеанс полного пакета 500K и сравнение legacy/T2
|
||||
|
||||
## Выполнено
|
||||
|
||||
После отдельного разрешения пользователя выполнен один TX-сеанс подготовленного потока из 16
|
||||
полных PHY-пакетов. Перед TX повторная проверка подтвердила MASTER/SLAVE reg29=0x26 и SLAVE
|
||||
CLKIN=DETECTED. Автоматических и ручных повторов не было. RX сохранил 24 000 000 комплексных
|
||||
отсчётов за 3,000 с, 48 000 000 байт, SHA-256
|
||||
ce0bd603e4ca356af67d504ee11c9e9f38171f31c7427fbe61216a28126b2b32; overruns0 и clipping0.
|
||||
|
||||
В промежуточной строке TX-журнала зарегистрирован один underrun длиной до16672 байт, хотя
|
||||
итоговая строка утилиты сообщила0. Ненулевое наблюдение сохранено, сеанс признан
|
||||
`INVALID_TX_UNDERRUN`, повтор запрещён и не выполнялся. Cleanup прошёл без ошибок, SDR-процессы
|
||||
после сеанса CLEAR, RF TX=0.
|
||||
|
||||
Одна и та же RX-запись независимо обработана строгими режимами legacy и T2 без fallback. Оба
|
||||
получили144 строгих кандидата,3 прохода PHY header,3 запуска и прохода FEC,3 запуска CRC,0 CRC
|
||||
PASS и0/16 пакетов. T2 gain равен0. Диагностические границы всех16 пакетов найдены только после
|
||||
строгого вердикта и не передавались приёмнику. По сопоставимой методике Lab058 средняя доля
|
||||
ошибочных символов равна0,099287, EVM1,220401, matched/residual−1,730051дБ, медианная
|
||||
неравномерность полосы p95–p05 равна6,660341дБ. Эти значения хуже Lab056 B, но их нельзя
|
||||
отделить от TX underrun.
|
||||
|
||||
Lab062 не коммитировалась, push не выполнялся, Lab063 не создана. Следующий этап: RF TX=0 и
|
||||
отдельно согласовать только офлайн-локализацию underrun относительно границ пакетов.
|
||||
|
||||
130
data/processed/lab062/capture_result.json
Normal file
130
data/processed/lab062/capture_result.json
Normal file
@@ -0,0 +1,130 @@
|
||||
{
|
||||
"lab": "Lab062",
|
||||
"stage": "SINGLE_RF_CAPTURE",
|
||||
"passed": false,
|
||||
"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": 500000,
|
||||
"samples_per_symbol": 16,
|
||||
"digital_carrier_hz": 50000.0,
|
||||
"packets": 16,
|
||||
"payload_bytes": 256,
|
||||
"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"
|
||||
},
|
||||
"tx_sha256": "e84bbce227426c0aac13a48ded1286ca4a44fc6e3efce01f6e5a513217c87ff5",
|
||||
"process_before": {
|
||||
"state": "CLEAR",
|
||||
"attempts": [
|
||||
{
|
||||
"state": "CLEAR",
|
||||
"mechanism": "psutil",
|
||||
"matches": [],
|
||||
"inspected": 304,
|
||||
"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": 140.9547000657767,
|
||||
"elapsed_ms": 140.9547000657767,
|
||||
"polls": [
|
||||
{
|
||||
"query": 1,
|
||||
"start_ms": 50.31830002553761,
|
||||
"end_ms": 67.1174000017345,
|
||||
"raw": "CLKIN status: no clock signal detected",
|
||||
"state": "NOT_DETECTED",
|
||||
"exit_code": 0
|
||||
},
|
||||
{
|
||||
"query": 2,
|
||||
"start_ms": 117.15810000896454,
|
||||
"end_ms": 140.9547000657767,
|
||||
"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.957977294921875,
|
||||
"mean_q": 1.857269287109375,
|
||||
"power_code2": 4.886199951171875,
|
||||
"ac_power_code2": 0.5190302487462768,
|
||||
"rms_code": 2.2104750510177387,
|
||||
"peak_magnitude_code": 4.123105625617661,
|
||||
"max_abs_i": 3.0,
|
||||
"max_abs_q": 4.0,
|
||||
"papr_db": 5.414776863475277,
|
||||
"clipped_components_fraction": 0.0,
|
||||
"clipped_samples_fraction": 0.0,
|
||||
"saturation_detected": false,
|
||||
"analog_compression": "UNKNOWN_WITHOUT_CALIBRATION"
|
||||
},
|
||||
"rx_samples": 24000000,
|
||||
"rx_bytes": 48000000,
|
||||
"rx_duration_seconds": 3.0,
|
||||
"error": "TX underrun или неоднозначный счётчик; повтор запрещён",
|
||||
"process_after": {
|
||||
"state": "CLEAR",
|
||||
"attempts": [
|
||||
{
|
||||
"state": "CLEAR",
|
||||
"mechanism": "psutil",
|
||||
"matches": [],
|
||||
"inspected": 304,
|
||||
"errors": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"cleanup_errors": []
|
||||
}
|
||||
2043
data/processed/lab062/legacy_rf_decode.json
Normal file
2043
data/processed/lab062/legacy_rf_decode.json
Normal file
File diff suppressed because it is too large
Load Diff
17
data/processed/lab062/packet_comparison.csv
Normal file
17
data/processed/lab062/packet_comparison.csv
Normal file
@@ -0,0 +1,17 @@
|
||||
packet_index,legacy_header,legacy_fec,legacy_crc,legacy_output,t2_header,t2_fec,t2_crc,t2_output,preamble_errors_t2,header_errors_t2,payload_errors_before_fec_t2,initial_frequency_hz_t2,t2_final_frequency_hz,t2_lock_status
|
||||
1,False,False,False,False,False,False,False,False,108,20,367,-2.511306789128502,1.938828000443245,NOT_DEFINED_BY_IMPLEMENTATION
|
||||
2,False,False,False,False,False,False,False,False,135,11,377,16.850274814266026,-2.223133252684196,NOT_DEFINED_BY_IMPLEMENTATION
|
||||
3,False,False,False,False,False,False,False,False,466,115,2229,-266.23811405950204,-265.79617151697045,NOT_DEFINED_BY_IMPLEMENTATION
|
||||
4,False,False,False,False,False,False,False,False,515,104,2266,297.2495839963796,293.7996556301242,NOT_DEFINED_BY_IMPLEMENTATION
|
||||
5,False,False,False,False,False,False,False,False,108,27,369,-1.7121731230326125,-0.8347122576256625,NOT_DEFINED_BY_IMPLEMENTATION
|
||||
6,True,True,False,False,True,True,False,False,112,10,345,4.67554618043703,-3.16390424095876,NOT_DEFINED_BY_IMPLEMENTATION
|
||||
7,False,False,False,False,False,False,False,False,113,7,344,2.1125086049646824,1.0944814837028547,NOT_DEFINED_BY_IMPLEMENTATION
|
||||
8,False,False,False,False,False,False,False,False,91,19,368,0.9638879273500407,0.35355297825700155,NOT_DEFINED_BY_IMPLEMENTATION
|
||||
9,False,False,False,False,False,False,False,False,105,21,367,-2.6793109690685877,0.19088110622128074,NOT_DEFINED_BY_IMPLEMENTATION
|
||||
10,False,False,False,False,False,False,False,False,116,7,345,-0.29035656707501434,0.9472030503555074,NOT_DEFINED_BY_IMPLEMENTATION
|
||||
11,False,False,False,False,True,True,False,False,109,9,392,0.7573187277726318,-0.14196556023732618,NOT_DEFINED_BY_IMPLEMENTATION
|
||||
12,False,False,False,False,False,False,False,False,297,110,2181,-223.04966205995262,-222.38930722105994,NOT_DEFINED_BY_IMPLEMENTATION
|
||||
13,False,False,False,False,False,False,False,False,304,36,524,-0.16099769754669116,-2.081420319755734,NOT_DEFINED_BY_IMPLEMENTATION
|
||||
14,True,True,False,False,False,False,False,False,314,127,2091,228.20916012396785,224.92813768839025,NOT_DEFINED_BY_IMPLEMENTATION
|
||||
15,True,True,False,False,True,True,False,False,131,7,395,-9.95965988857566,2.2844456653638283,NOT_DEFINED_BY_IMPLEMENTATION
|
||||
16,False,False,False,False,False,False,False,False,100,23,355,13.156526986310709,-0.8684121381823278,NOT_DEFINED_BY_IMPLEMENTATION
|
||||
|
144
data/processed/lab062/preflight_result.json
Normal file
144
data/processed/lab062/preflight_result.json
Normal file
@@ -0,0 +1,144 @@
|
||||
{
|
||||
"lab": "Lab062",
|
||||
"stage": "RX_ONLY_PREFLIGHT",
|
||||
"passed": true,
|
||||
"rf_tx_count": 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": 500000,
|
||||
"samples_per_symbol": 16,
|
||||
"digital_carrier_hz": 50000.0,
|
||||
"packets": 16,
|
||||
"payload_bytes": 256,
|
||||
"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"
|
||||
},
|
||||
"tx_sha256": "e84bbce227426c0aac13a48ded1286ca4a44fc6e3efce01f6e5a513217c87ff5",
|
||||
"stop_before_tx": true,
|
||||
"process_before": {
|
||||
"state": "CLEAR",
|
||||
"attempts": [
|
||||
{
|
||||
"state": "CLEAR",
|
||||
"mechanism": "psutil",
|
||||
"matches": [],
|
||||
"inspected": 308,
|
||||
"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": 133.57950001955032,
|
||||
"elapsed_ms": 133.57950001955032,
|
||||
"polls": [
|
||||
{
|
||||
"query": 1,
|
||||
"start_ms": 50.528000108897686,
|
||||
"end_ms": 67.34060007147491,
|
||||
"raw": "CLKIN status: no clock signal detected",
|
||||
"state": "NOT_DETECTED",
|
||||
"exit_code": 0
|
||||
},
|
||||
{
|
||||
"query": 2,
|
||||
"start_ms": 117.69610014744103,
|
||||
"end_ms": 133.57950001955032,
|
||||
"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.115 sec = 15.9 MiB/second, average power -34.5 dBfs, 9120 bytes free in buffer, 0 overruns, longest 0 bytes\n\nExiting...\nTotal time: 0.11549 s\nhackrf_stop_rx() done\nTransfer statistics:\n1847680 bytes transferred by M0\n1835008 bytes transferred by M4\n0 overruns, longest 0 bytes\nhackrf_close() done\nhackrf_exit() done\nfclose() done\nexit\n"
|
||||
},
|
||||
"rx_level": {
|
||||
"samples": 800000,
|
||||
"mean_i": 0.9660625,
|
||||
"mean_q": 1.85752875,
|
||||
"power_code2": 5.35486625,
|
||||
"ac_power_code2": 0.9711764390171878,
|
||||
"rms_code": 2.3140583938180987,
|
||||
"peak_magnitude_code": 8.246211251235321,
|
||||
"max_abs_i": 6.0,
|
||||
"max_abs_q": 8.0,
|
||||
"papr_db": 11.037602848982825,
|
||||
"clipped_components_fraction": 0.0,
|
||||
"clipped_samples_fraction": 0.0,
|
||||
"saturation_detected": false,
|
||||
"analog_compression": "UNKNOWN_WITHOUT_CALIBRATION",
|
||||
"ac_rms_code": 0.9854828456229909,
|
||||
"unexpected_strong_signal": false,
|
||||
"strong_signal_rule": "AC RMS >= 20.0 code or max |I|/|Q| >= 100.0 code",
|
||||
"symbol_energy": "NOT_APPLICABLE_TO_BACKGROUND"
|
||||
},
|
||||
"rx_sha256": "4cc0ceef5a95f84e5aa0c232d14ff1123efd307494b5c8a5646854be912fdbca",
|
||||
"rx_overruns": 0,
|
||||
"process_after": {
|
||||
"state": "CLEAR",
|
||||
"attempts": [
|
||||
{
|
||||
"state": "CLEAR",
|
||||
"mechanism": "psutil",
|
||||
"matches": [],
|
||||
"inspected": 308,
|
||||
"errors": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"cleanup_errors": [],
|
||||
"processed_correction": {
|
||||
"source": "data/raw/lab062/preflight_result.json",
|
||||
"removed_field": "rx_level.symbol_energy_code2_seconds",
|
||||
"removed_value": 0.00026774331249999997,
|
||||
"reason": "inherited Lab056 20 ksymbol/s normalization is not applicable to RX-only background",
|
||||
"preflight_pass_affected": false
|
||||
}
|
||||
}
|
||||
80
data/processed/lab062/preparation.json
Normal file
80
data/processed/lab062/preparation.json
Normal file
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"lab": "Lab062",
|
||||
"stage": "PREPARATION_ONLY",
|
||||
"rf_tx_count": 0,
|
||||
"head": "25cc89f0c734f77141632d19dbcdb6b2f066d60b",
|
||||
"parameters": {
|
||||
"rf_frequency_hz": 1400000000,
|
||||
"sample_rate_hz": 8000000,
|
||||
"symbol_rate": 500000,
|
||||
"samples_per_symbol": 16,
|
||||
"digital_carrier_hz": 50000.0,
|
||||
"packets": 16,
|
||||
"payload_bytes": 256,
|
||||
"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"
|
||||
},
|
||||
"tx_samples": 1476096,
|
||||
"tx_bytes": 2952192,
|
||||
"tx_duration_seconds": 0.184512,
|
||||
"tx_sha256": "e84bbce227426c0aac13a48ded1286ca4a44fc6e3efce01f6e5a513217c87ff5",
|
||||
"rx_samples": 24000000,
|
||||
"rx_duration_seconds": 3,
|
||||
"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\\lab062\\tx_stream.cs8",
|
||||
"-f",
|
||||
"1400000000",
|
||||
"-s",
|
||||
"8000000",
|
||||
"-n",
|
||||
"1476096",
|
||||
"-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\\lab062\\rx_stream.cs8",
|
||||
"-f",
|
||||
"1400000000",
|
||||
"-s",
|
||||
"8000000",
|
||||
"-n",
|
||||
"24000000",
|
||||
"-l",
|
||||
"24",
|
||||
"-g",
|
||||
"24",
|
||||
"-a",
|
||||
"0",
|
||||
"-p",
|
||||
"0",
|
||||
"-B"
|
||||
]
|
||||
}
|
||||
2986
data/processed/lab062/rf_analysis.json
Normal file
2986
data/processed/lab062/rf_analysis.json
Normal file
File diff suppressed because it is too large
Load Diff
25
data/processed/lab062/software_gate.json
Normal file
25
data/processed/lab062/software_gate.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"lab": "Lab062",
|
||||
"stage": "SOFTWARE_GATE",
|
||||
"rf_tx_count": 0,
|
||||
"tx_sha256": "e84bbce227426c0aac13a48ded1286ca4a44fc6e3efce01f6e5a513217c87ff5",
|
||||
"legacy": {
|
||||
"mode": "legacy",
|
||||
"packets": 16,
|
||||
"header_pass": 16,
|
||||
"fec_pass": 16,
|
||||
"crc_pass": 16,
|
||||
"exact_ordered_payloads": true,
|
||||
"passed": true
|
||||
},
|
||||
"t2": {
|
||||
"mode": "t2",
|
||||
"packets": 16,
|
||||
"header_pass": 16,
|
||||
"fec_pass": 16,
|
||||
"crc_pass": 16,
|
||||
"exact_ordered_payloads": true,
|
||||
"passed": true
|
||||
},
|
||||
"passed": true
|
||||
}
|
||||
2043
data/processed/lab062/t2_rf_decode.json
Normal file
2043
data/processed/lab062/t2_rf_decode.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -570,3 +570,29 @@
|
||||
[synthetic CSV](../data/processed/lab061/synthetic_packet_results.csv).
|
||||
- **STOP:** Lab061 фиксируется этим отдельным коммитом; push выполняется
|
||||
пользователем отдельно, Lab062 не создана.
|
||||
|
||||
## Lab062 — полный 500K PHY-пакет, подготовка и RX-only preflight
|
||||
|
||||
- **Запись 056; RF TX=0.** Подготовлены16 пакетов по256 байт payload,
|
||||
0,184512с. Legacy/T2 дали16/16 строгих HEADER/FEC/CRC и точных payload.
|
||||
- **Геометрия:**1400МГц, примерно1,2м, LOS в помещении; TX возле окна, RX на
|
||||
столе, одинаковая поляризация, антенны и кабели неподвижны.
|
||||
- **Аппаратный preflight PASS:** MASTER/SLAVE reg29=0x26, SLAVE CLKIN=DETECTED
|
||||
через133,5795мс. RX-only:800000 отсчётов, RMS2,314058, AC RMS0,985483,
|
||||
max|I|/|Q|=6/8, clipping0, overruns0, сильного фона нет.
|
||||
- **Проверки:** quick gate962/35/13 PASS, pytest962 passed. Усиления10/24/24дБ
|
||||
сохранены; аналоговая компрессия без прибора остаётся неизвестной.
|
||||
- [План](lab062_current_state.md), [отчёт](lab062_rx_preflight.md),
|
||||
[software](../data/processed/lab062/software_gate.json),
|
||||
[preflight](../data/processed/lab062/preflight_result.json).
|
||||
- **STOP:** технически готово к одному TX, но отдельного разрешения ещё нет.
|
||||
Lab062 не коммитировалась, push не выполнялся, Lab063 не создана.
|
||||
- **Единственный RF-сеанс:** выполнен один TX без повторов. RX сохранён полностью: 24 000 000
|
||||
комплексных отсчётов, 3,000 с, overruns 0, clipping 0. В TX-журнале зарегистрирован
|
||||
промежуточный `1 underruns, longest 16672 bytes`, поэтому сеанс признан невалидным для чистого
|
||||
A/B-сравнения. Legacy/T2 дали одинаковый пакетный итог 0/16, T2 gain 0.
|
||||
- [RF-отчёт](lab062_rf_result.md),
|
||||
[JSON](../data/processed/lab062/rf_analysis.json),
|
||||
[покадровый CSV](../data/processed/lab062/packet_comparison.csv).
|
||||
- **Текущее состояние:** `RF TX=0`; повтор не выполнялся, Lab062 не закоммичена, push не
|
||||
выполнялся, Lab063 не создана. Следующий шаг требует отдельного согласования.
|
||||
|
||||
48
docs/lab062_current_state.md
Normal file
48
docs/lab062_current_state.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# Lab062 — полный пакет 500K, подготовка к одному RF-сеансу
|
||||
|
||||
Lab062 проверяет T2 на полном штатном PHY-пакете в одном будущем коротком
|
||||
эфирном сеансе. На текущем этапе разрешены подготовка, программный шлюз,
|
||||
аппаратная проверка общей опоры и RX-only запись фона. Передача запрещена до
|
||||
отдельного разрешения пользователя.
|
||||
|
||||
## Неподвижная физическая схема
|
||||
|
||||
| Условие | Фактическое значение |
|
||||
|---|---|
|
||||
| Частота | 1400000000 Гц, только для Lab062 |
|
||||
| Расстояние TX–RX | примерно 1,2 м |
|
||||
| Геометрия | прямая видимость, помещение |
|
||||
| TX | плата и антенна возле окна |
|
||||
| RX | плата и антенна на столе пользователя |
|
||||
| Антенны | имеющиеся антенны на 1400 МГц, неподвижны |
|
||||
| Поляризация | одинаковая |
|
||||
| Тактирование | 10 МГц TCXO → MASTER → MASTER CLKOUT → SLAVE CLKIN |
|
||||
|
||||
Длинный кабель общей опоры, USB-подключение, антенны и остальные кабели не
|
||||
перемещаются. Пользователь по возможности не проходит между антеннами. Во время
|
||||
будущего сеанса не меняются частота, усиления, ориентация и PHY.
|
||||
|
||||
## Параметры
|
||||
|
||||
- 16 полных пакетов, payload каждого — 256 байт;
|
||||
- BPSK, 500000 символов/с, Fs 8000000 выборок/с, 16 выборок/символ;
|
||||
- центральная частота 1400 МГц, цифровая несущая +50 кГц;
|
||||
- TX VGA 10 дБ, RX LNA/VGA 24/24 дБ;
|
||||
- RF amplifier и Bias-Tee выключены;
|
||||
- один конечный TX-waveform и одна будущая RX-запись;
|
||||
- режим приёмника задаётся заранее: `legacy` или `t2`, без CRC-fallback.
|
||||
|
||||
RX-only preflight не меняет усиления. Он проверяет rail-коды, overruns,
|
||||
максимальные |I|/|Q|, полный и AC RMS, сильный фон и ошибки аппаратных команд.
|
||||
Эвристический STOP для неожиданно сильного цифрового фона: AC RMS не менее 20
|
||||
кодов или максимум |I|/|Q| не менее 100 кодов. Отсутствие цифрового клиппинга
|
||||
не доказывает отсутствие аналоговой компрессии.
|
||||
|
||||
Обязательная аппаратная точка: MASTER reg29=0x26, SLAVE reg29=0x26 и SLAVE
|
||||
CLKIN=DETECTED. Любой отказ означает `RF TX = 0` и STOP.
|
||||
|
||||
Lab062 не коммитируется. Push не выполняется. Lab063 не создаётся автоматически.
|
||||
|
||||
## Фактический RF-результат
|
||||
|
||||
14 сентября 2026 года выполнен ровно один разрешённый TX-сеанс. Полная RX-запись сохранена, но в промежуточной строке `hackrf_transfer` зарегистрирован `1 underruns, longest 16672 bytes`. Повтор не выполнялся. Строгие `legacy` и `t2` на одной записи дали 0/16 CRC и пакетов; T2 gain равен 0. Подробности и покадровая таблица приведены в [отчёте](lab062_rf_result.md). Текущий статус: `RF TX = 0`, Lab062 не закоммичена, push не выполнялся, Lab063 не создана.
|
||||
108
docs/lab062_rf_result.md
Normal file
108
docs/lab062_rf_result.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# Lab062 — результат единственного RF-сеанса
|
||||
|
||||
Передача была запущена один раз. Приёмник сохранил полную трёхсекундную IQ-запись без переполнений и цифрового клиппинга. Однако `hackrf_transfer` во время передачи сообщил об одном кратком underrun: буфер передатчика один раз опустел на интервал до 16 672 байт. В итоговой строке утилита напечатала ноль underrun, но промежуточную ненулевую строку игнорировать нельзя. Поэтому этот сеанс нельзя считать чистым сравнением `legacy` и `t2`.
|
||||
|
||||
Оба строгих варианта приёмника обработали одну и ту же запись и выдали 0 из 16 пакетов. T2 не дал выигрыша в пакетах. Установлено, что передача состоялась, общий тактовый сигнал перед ней был исправен, RX-файл полный, а строгий результат равен 0/16 в обоих режимах. Нельзя установить, каким был бы результат без underrun, и нельзя приписывать все ошибки самому T2 или эфирному каналу. Аналоговая компрессия также остаётся неизвестной без измерительного прибора.
|
||||
|
||||
## Условия
|
||||
|
||||
| Параметр | Значение |
|
||||
|---|---:|
|
||||
| RF | 1 400 000 000 Гц |
|
||||
| Символьная скорость | 500 000 символов/с |
|
||||
| Частота дискретизации | 8 000 000 комплексных отсчётов/с |
|
||||
| Цифровая несущая | +50 000 Гц |
|
||||
| Пакеты | 16 полных PHY-пакетов по 256 байт payload |
|
||||
| TX | 1 476 096 отсчётов, 0,184512 с, VGA 10 дБ |
|
||||
| RX | LNA/VGA 24/24 дБ, 3 с |
|
||||
| RF amplifier / Bias-Tee | OFF / OFF |
|
||||
| Геометрия | около 1,2 м, LOS, помещение; TX возле окна, RX на столе |
|
||||
| Антенны | имеющиеся антенны на 1400 МГц, одинаковая поляризация, неподвижны |
|
||||
| Опора | 10 МГц TCXO → MASTER → MASTER CLKOUT → SLAVE CLKIN |
|
||||
|
||||
SHA-256 передаваемого файла: `e84bbce227426c0aac13a48ded1286ca4a44fc6e3efce01f6e5a513217c87ff5`.
|
||||
|
||||
## Аппаратный результат
|
||||
|
||||
- перед TX повторно получены `MASTER reg29 = 0x26`, `SLAVE reg29 = 0x26`, `SLAVE CLKIN = DETECTED`;
|
||||
- выполнен один TX-сеанс, автоматических повторов нет;
|
||||
- RX: 24 000 000 комплексных отсчётов, 48 000 000 байт, 3,000 с;
|
||||
- SHA-256 RX: `ce0bd603e4ca356af67d504ee11c9e9f38171f31c7427fbe61216a28126b2b32`;
|
||||
- RX overruns: 0 во всех пяти строках журнала;
|
||||
- RX clipping: 0;
|
||||
- TX underruns: промежуточный максимум 1, `longest 16672 bytes`; финальная строка — 0;
|
||||
- cleanup: оба CLKOUT выключены без зарегистрированных ошибок, SDR-процессы после сеанса — `CLEAR`;
|
||||
- текущее состояние: `RF TX = 0`.
|
||||
|
||||
Из-за ненулевой промежуточной строки underrun статус сеанса: `INVALID_TX_UNDERRUN`. Полученные файлы сохранены, повтор не выполнялся.
|
||||
|
||||
## Строгое декодирование одной записи
|
||||
|
||||
| Стадия | legacy | T2 |
|
||||
|---|---:|---:|
|
||||
| CANDIDATES | 144 | 144 |
|
||||
| TRUE PACKETS FOUND | 0 | 0 |
|
||||
| HEADER ACCEPTED | 3 | 3 |
|
||||
| FEC STARTED | 3 | 3 |
|
||||
| FEC PASS | 3 | 3 |
|
||||
| CRC STARTED | 3 | 3 |
|
||||
| CRC PASS | 0 | 0 |
|
||||
| PACKET OUTPUT | 0 | 0 |
|
||||
| EXACT PAYLOAD MATCH | 0 | 0 |
|
||||
|
||||
Детектор в обоих запусках получил 209 956 первичных кандидатов, дешёвая ступень отклонила 209 812, после группировки строгий декодер проверил 144 кандидата. Переполнения очереди кандидатов не было. Никакого fallback между `legacy` и `t2` не выполнялось.
|
||||
|
||||
- `PASSED BY BOTH = []`
|
||||
- `PASSED ONLY BY LEGACY = []`
|
||||
- `PASSED ONLY BY T2 = []`
|
||||
- `FAILED BY BOTH = [1, 2, …, 16]`
|
||||
|
||||
## Таблица 16 фактических границ
|
||||
|
||||
Начало потока найдено только после строгого вердикта корреляцией с первым TX-пакетом: отсчёт 4 256 091, отношение пика к медиане 21,739. Знание TX не передавалось детектору и не использовалось для повторной настройки приёмника. Во всех строках `H/F/C/O` означают header/FEC/CRC/output; `1` — проход, `0` — непроход. Ошибки символов и оценки частоты — диагностические, по известной границе и известным переданным символам.
|
||||
|
||||
| № | legacy H/F/C/O | T2 H/F/C/O | Preamble errors | Header errors | Payload errors до FEC | CFO нач., Гц | T2 фин., Гц | T2 lock |
|
||||
|---:|:---:|:---:|---:|---:|---:|---:|---:|:---:|
|
||||
| 1 | 0/0/0/0 | 0/0/0/0 | 108 | 20 | 367 | -2.511 | +1.939 | не определён |
|
||||
| 2 | 0/0/0/0 | 0/0/0/0 | 135 | 11 | 377 | +16.850 | -2.223 | не определён |
|
||||
| 3 | 0/0/0/0 | 0/0/0/0 | 466 | 115 | 2229 | -266.238 | -265.796 | не определён |
|
||||
| 4 | 0/0/0/0 | 0/0/0/0 | 515 | 104 | 2266 | +297.250 | +293.800 | не определён |
|
||||
| 5 | 0/0/0/0 | 0/0/0/0 | 108 | 27 | 369 | -1.712 | -0.835 | не определён |
|
||||
| 6 | 1/1/0/0 | 1/1/0/0 | 112 | 10 | 345 | +4.676 | -3.164 | не определён |
|
||||
| 7 | 0/0/0/0 | 0/0/0/0 | 113 | 7 | 344 | +2.113 | +1.094 | не определён |
|
||||
| 8 | 0/0/0/0 | 0/0/0/0 | 91 | 19 | 368 | +0.964 | +0.354 | не определён |
|
||||
| 9 | 0/0/0/0 | 0/0/0/0 | 105 | 21 | 367 | -2.679 | +0.191 | не определён |
|
||||
| 10 | 0/0/0/0 | 0/0/0/0 | 116 | 7 | 345 | -0.290 | +0.947 | не определён |
|
||||
| 11 | 0/0/0/0 | 1/1/0/0 | 109 | 9 | 392 | +0.757 | -0.142 | не определён |
|
||||
| 12 | 0/0/0/0 | 0/0/0/0 | 297 | 110 | 2181 | -223.050 | -222.389 | не определён |
|
||||
| 13 | 0/0/0/0 | 0/0/0/0 | 304 | 36 | 524 | -0.161 | -2.081 | не определён |
|
||||
| 14 | 1/1/0/0 | 0/0/0/0 | 314 | 127 | 2091 | +228.209 | +224.928 | не определён |
|
||||
| 15 | 1/1/0/0 | 1/1/0/0 | 131 | 7 | 395 | -9.960 | +2.284 | не определён |
|
||||
| 16 | 0/0/0/0 | 0/0/0/0 | 100 | 23 | 355 | +13.157 | -0.868 | не определён |
|
||||
|
||||
В реализации T2 нет отдельного детектора lock, поэтому поле нельзя честно заменить на `PASS` или `FAIL`. Сохранены начальное состояние частоты и конечное состояние петли.
|
||||
|
||||
## Физическое качество по методике Lab058
|
||||
|
||||
Функции `analyze_values` и `channel_metrics` из Lab058 применены отдельно к каждому полному пакету. Для оценки использованы диагностические границы и чередующиеся обучающие/проверочные участки с защитными краями. Это измерение сделано после строгого результата и не меняет 0/16.
|
||||
|
||||
| Метрика | Lab062 | Lab056, 500K | Разница по смыслу |
|
||||
|---|---:|---:|---|
|
||||
| Диагностическая доля ошибочных символов | 0,099287 | 0,034959 | Lab062 хуже |
|
||||
| Нормированное RMS-отклонение, EVM | 1,220401 | 0,845350 | Lab062 хуже |
|
||||
| Matched/residual | -1,730051 дБ | +1,464846 дБ | Lab062 хуже на 3,194897 дБ |
|
||||
| Неравномерность усиления, p95–p05 | медиана 6,660341 дБ | среднее 5,471382 дБ | Lab062 больше |
|
||||
|
||||
Для Lab062 разброс неравномерности между пакетами составил 6,236828…10,617549 дБ. Остаток включает шум, искажения канала и возможное влияние underrun; называть его чистым шумом нельзя. Отдельная диагностика именно выходных символов T2 дала SER 0,186626 и RMS 1,699461; это другой срез данных, поэтому он не подменяет приведённые выше сопоставимые метрики Lab058.
|
||||
|
||||
## Инженерный вывод
|
||||
|
||||
Чистое сравнение `legacy` и T2 не состоялось из-за зафиксированного TX underrun. Фактический строгий результат существующей записи — 0/16 для обоих режимов, выигрыш T2 равен нулю. В записи видны неоднородные повреждения пакетов и заметно худшее качество символов, чем у 500K-сегментов Lab056, но эти наблюдения нельзя отделить от аппаратной ошибки передачи.
|
||||
|
||||
Следующий одиночный шаг: без RF локализовать момент underrun относительно границ пакетов и проверить, совместимы ли провалы качества с его положением. Новый RF-сеанс, Lab063, изменение PHY или усилений этим отчётом не разрешаются.
|
||||
|
||||
`LAB062 COMMIT = NOT PERFORMED`
|
||||
|
||||
`PUSH = NOT PERFORMED`
|
||||
|
||||
`RF TX = 0`
|
||||
108
docs/lab062_rx_preflight.md
Normal file
108
docs/lab062_rx_preflight.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# Lab062 — подготовка и RX-only preflight
|
||||
|
||||
14 сентября 2026 года выполнена подготовка полного 500K PHY-сеанса и одна
|
||||
аппаратная проверка при выключенном передатчике. Результат preflight — PASS.
|
||||
Передача не запускалась: `RF TX = 0`.
|
||||
|
||||
## Условия
|
||||
|
||||
| Параметр | Значение |
|
||||
|---|---|
|
||||
| Центральная частота | 1400000000 Гц |
|
||||
| Скорость символов | 500000 символов/с |
|
||||
| Частота дискретизации | 8000000 выборок/с |
|
||||
| Пакеты | 16 полных PHY-пакетов |
|
||||
| Payload | 256 байт в каждом пакете |
|
||||
| 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 |
|
||||
|
||||
Длинный clock-кабель и USB-схема не менялись. Антенны, кабели, ориентация,
|
||||
частота, усиления и PHY должны остаться неизменными до будущего разрешённого
|
||||
сеанса.
|
||||
|
||||
## Программная проверка
|
||||
|
||||
Создан один waveform из 16 пакетов. Его длина — 1476096 комплексных отсчётов,
|
||||
2952192 байта и 0,184512 с. SHA-256:
|
||||
`e84bbce227426c0aac13a48ded1286ca4a44fc6e3efce01f6e5a513217c87ff5`.
|
||||
|
||||
Строгий приёмник в заранее выбранных режимах дал:
|
||||
|
||||
| Режим | Header | FEC | CRC | Точный payload и порядок |
|
||||
|---|---:|---:|---:|---:|
|
||||
| legacy | 16/16 | 16/16 | 16/16 | 16/16 |
|
||||
| T2 | 16/16 | 16/16 | 16/16 | 16/16 |
|
||||
|
||||
Автоматического повторного декодирования по результату CRC нет.
|
||||
|
||||
## Общая опора и RX-only фон
|
||||
|
||||
Свежая инвентаризация подтвердила обе HackRF One r9 с firmware/libhackrf
|
||||
2024.02.1. Роли сохранены:
|
||||
|
||||
- MASTER `000000000000000097c063dc323b97d3`;
|
||||
- SLAVE `000000000000000097c063dc2e5327d3`.
|
||||
|
||||
Аппаратная проверка получила:
|
||||
|
||||
```text
|
||||
MASTER reg29 = 0x26
|
||||
SLAVE reg29 = 0x26
|
||||
SLAVE CLKIN = DETECTED
|
||||
CLKIN query = 2
|
||||
time to detect = 133.5795 ms
|
||||
```
|
||||
|
||||
Первый опрос SLAVE ещё показывал отсутствие опоры, второй штатный опрос её
|
||||
обнаружил. После этого SLAVE записал 800000 комплексных отсчётов, 0,1 с, при
|
||||
1400 МГц и RX LNA/VGA 24/24 дБ.
|
||||
|
||||
| Метрика RX-only записи | Значение |
|
||||
|---|---:|
|
||||
| Полный RMS IQ | 2,314058 кода |
|
||||
| AC RMS после удаления среднего | 0,985483 кода |
|
||||
| Средние I / Q | 0,966063 / 1,857529 кода |
|
||||
| Максимум \|I\| / \|Q\| | 6 / 8 кодов |
|
||||
| Максимальная комплексная амплитуда | 8,246211 кода |
|
||||
| Цифровой clipping | 0 / 800000 отсчётов |
|
||||
| RX overruns | 0 |
|
||||
| Неожиданно сильный сигнал | нет |
|
||||
|
||||
Сильным цифровым фоном считался бы AC RMS не менее 20 кодов или максимум
|
||||
`|I|/|Q|` не менее 100 кодов. Оба показателя значительно ниже порога.
|
||||
|
||||
Отсутствие rail-кодов не доказывает отсутствия аналоговой компрессии при
|
||||
будущем TX. Абсолютная RF-мощность без измерительного прибора также неизвестна.
|
||||
Усиления по результату preflight не менялись.
|
||||
|
||||
Унаследованное от Lab056 поле энергии на символ исключено из обработанной
|
||||
сводки: у фоновой записи нет символов, поэтому такая величина к ней неприменима.
|
||||
Сырой аппаратный JSON сохранён без переписывания; это поле не участвовало в
|
||||
решении PASS.
|
||||
|
||||
SHA-256 RX-only записи:
|
||||
`4cc0ceef5a95f84e5aa0c232d14ff1123efd307494b5c8a5646854be912fdbca`.
|
||||
|
||||
После записи оба CLKOUT выключены без ошибок. Проверка процессов до и после
|
||||
дала `CLEAR`. Вызовов `hackrf_transfer -t` не было. Эксклюзивный маркер
|
||||
preflight запрещает его автоматический повтор.
|
||||
|
||||
Артефакты:
|
||||
|
||||
- [подготовка](../data/processed/lab062/preparation.json);
|
||||
- [программный шлюз](../data/processed/lab062/software_gate.json);
|
||||
- [результат preflight](../data/processed/lab062/preflight_result.json);
|
||||
- исходные TX и RX-only CS8 находятся в `data/raw/lab062/` и в Git не входят.
|
||||
|
||||
`READY FOR SINGLE LAB062 TX = YES` означает только техническую готовность.
|
||||
Отдельного разрешения на передачу ещё нет. `RF TX = 0`. STOP.
|
||||
|
||||
Lab062 не коммитируется. Push не выполняется. Lab063 не создаётся автоматически.
|
||||
475
experiments/lab062_full_packet_rf_preflight.py
Normal file
475
experiments/lab062_full_packet_rf_preflight.py
Normal file
@@ -0,0 +1,475 @@
|
||||
"""Lab062: подготовка полного 500K PHY-сеанса и RX-only preflight."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
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 experiments import lab049_bpsk_symbol_rate as phy
|
||||
from experiments import lab053_packet_processing as packet_processing
|
||||
from experiments import lab053_streaming_receiver as streaming
|
||||
from experiments import lab054_hardware_control as lab054
|
||||
from experiments import lab056_abab_energy as measurements
|
||||
from experiments import lab056_capture_runner as hardware
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
RAW = ROOT / "data/raw/lab062"
|
||||
OUTPUT = ROOT / "data/processed/lab062"
|
||||
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",
|
||||
)
|
||||
MODE = phy.PhyMode(500_000)
|
||||
PACKETS = 16
|
||||
PAYLOAD_BYTES = 256
|
||||
PREFLIGHT_SAMPLES = 800_000
|
||||
RX_SECONDS = 3
|
||||
RX_SAMPLES = RX_SECONDS * MODE.sample_rate_hz
|
||||
RX_LEAD_SECONDS = 0.5
|
||||
STRONG_AC_RMS_CODE = 20.0
|
||||
STRONG_COMPONENT_CODE = 100.0
|
||||
|
||||
|
||||
def save_new(path: Path, value: dict) -> None:
|
||||
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)
|
||||
|
||||
|
||||
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 plans():
|
||||
result = lab054.plans()
|
||||
if len(result) != PACKETS or any(len(frame.plan.payload) != PAYLOAD_BYTES for frame in result):
|
||||
raise RuntimeError("Штатный генератор не дал 16 payload по 256 байт")
|
||||
return result
|
||||
|
||||
|
||||
def parameters() -> dict:
|
||||
return {
|
||||
"rf_frequency_hz": POINT.frequency_hz,
|
||||
"sample_rate_hz": MODE.sample_rate_hz,
|
||||
"symbol_rate": MODE.symbol_rate,
|
||||
"samples_per_symbol": MODE.samples_per_symbol,
|
||||
"digital_carrier_hz": lab054.phy.lab045.CARRIER_OFFSET_HZ,
|
||||
"packets": PACKETS,
|
||||
"payload_bytes": PAYLOAD_BYTES,
|
||||
"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",
|
||||
}
|
||||
|
||||
|
||||
def transfer_command(path: Path | str, samples: int, *, transmit: bool = False) -> list[str]:
|
||||
return hardware.command(POINT, path, samples, transmit=transmit)
|
||||
|
||||
|
||||
def prepare() -> dict:
|
||||
RAW.mkdir(parents=True, exist_ok=True)
|
||||
OUTPUT.mkdir(parents=True, exist_ok=True)
|
||||
waveform_path = RAW / "tx_stream.cs8"
|
||||
if waveform_path.exists():
|
||||
raise FileExistsError("Lab062 TX уже существует; перезапись запрещена")
|
||||
with waveform_path.open("xb") as stream:
|
||||
for frame in plans():
|
||||
stream.write(phy.build_parameterized_tx_cs8(frame.plan, MODE))
|
||||
samples = waveform_path.stat().st_size // 2
|
||||
result = {
|
||||
"lab": "Lab062", "stage": "PREPARATION_ONLY", "rf_tx_count": 0,
|
||||
"head": subprocess.check_output(["git", "rev-parse", "HEAD"], text=True).strip(),
|
||||
"parameters": parameters(), "tx_samples": samples,
|
||||
"tx_bytes": waveform_path.stat().st_size,
|
||||
"tx_duration_seconds": samples / MODE.sample_rate_hz,
|
||||
"tx_sha256": sha256(waveform_path), "rx_samples": RX_SAMPLES,
|
||||
"rx_duration_seconds": RX_SECONDS, "automatic_retry": False,
|
||||
"tx_command_prepared_not_run": transfer_command(waveform_path, 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_stream.cs8"
|
||||
expected = hashlib.sha256()
|
||||
expected_bytes = 0
|
||||
for frame in plans():
|
||||
block = phy.build_parameterized_tx_cs8(frame.plan, MODE)
|
||||
expected.update(block)
|
||||
expected_bytes += len(block)
|
||||
if (metadata.get("stage") != "PREPARATION_ONLY" or metadata.get("rf_tx_count") != 0
|
||||
or metadata.get("parameters") != parameters() or path.stat().st_size != expected_bytes
|
||||
or metadata.get("tx_sha256") != expected.hexdigest() or sha256(path) != expected.hexdigest()):
|
||||
raise RuntimeError("Подготовленный Lab062 TX или параметры изменились")
|
||||
return metadata
|
||||
|
||||
|
||||
def decode(mode: str, source_path: Path | None = None, *, diagnostics: bool = False) -> dict:
|
||||
observations = []
|
||||
receiver = streaming.StreamingReceiver(
|
||||
processor=packet_processing.PacketProcessor(carrier_tracking_mode=mode),
|
||||
packet_observer=lambda row, outcome: observations.append((row, outcome)),
|
||||
)
|
||||
received = []
|
||||
with (source_path or (RAW / "tx_stream.cs8")).open("rb") as stream:
|
||||
while block := stream.read(2 * receiver.block_samples):
|
||||
values = np.frombuffer(block, np.int8)
|
||||
samples = values[::2].astype(np.float32) + 1j * values[1::2].astype(np.float32)
|
||||
received.extend(receiver.push_iq_block(samples.astype(np.complex64)))
|
||||
received.extend(receiver.finish())
|
||||
expected = [frame.plan.payload for frame in plans()]
|
||||
actual = [item.payload for item in received]
|
||||
result = {
|
||||
"mode": mode, "candidates": len(observations), "true_packets_found": len(actual),
|
||||
"header_pass": sum(outcome.phy_header_pass for _, outcome in observations),
|
||||
"fec_started": sum(outcome.fec_attempted for _, outcome in observations),
|
||||
"fec_pass": sum(outcome.fec_pass for _, outcome in observations),
|
||||
"crc_started": sum(outcome.crc != "NOT_REACHED" for _, outcome in observations),
|
||||
"crc_pass": sum(outcome.crc == "PASS" for _, outcome in observations),
|
||||
"packet_output": len(actual),
|
||||
"exact_payload_match": sum(payload in expected for payload in actual),
|
||||
"exact_ordered_payloads": actual == expected,
|
||||
"passed": len(actual) == PACKETS and actual == expected,
|
||||
}
|
||||
if diagnostics:
|
||||
payload_to_index = {payload: index + 1 for index, payload in enumerate(expected)}
|
||||
result["passed_packet_indexes"] = sorted(
|
||||
payload_to_index[payload] for payload in actual if payload in payload_to_index
|
||||
)
|
||||
rows = []
|
||||
for row, outcome in observations:
|
||||
payload = None
|
||||
if outcome.frame is not None and outcome.frame.reception.payload_bytes is not None:
|
||||
payload = bytes(outcome.frame.reception.payload_bytes)
|
||||
traces = row.get("carrier_tracking_traces", ())
|
||||
final_frequency = None
|
||||
if traces and len(traces[-1].frequency_hz):
|
||||
final_frequency = float(traces[-1].frequency_hz[-1])
|
||||
rows.append({
|
||||
"absolute_refined": int(row["absolute_refined"]),
|
||||
"reason": outcome.reason,
|
||||
"header": bool(outcome.phy_header_pass),
|
||||
"fec_started": bool(outcome.fec_attempted),
|
||||
"fec": bool(outcome.fec_pass),
|
||||
"crc_started": outcome.crc != "NOT_REACHED",
|
||||
"crc": outcome.crc == "PASS",
|
||||
"packet_output": bool(outcome.accepted and outcome.crc == "PASS"),
|
||||
"packet_index_after_verdict": payload_to_index.get(payload),
|
||||
"initial_frequency_hz": row.get("cfo_hz"),
|
||||
"t2_final_frequency_hz": final_frequency,
|
||||
"t2_lock_status": "NOT_DEFINED_BY_IMPLEMENTATION" if mode == "t2" else "NOT_APPLICABLE",
|
||||
})
|
||||
result["rows"] = rows
|
||||
result["detector_statistics"] = dict(receiver.detection_statistics)
|
||||
result["candidate_overflow"] = receiver.candidate_overflow
|
||||
return result
|
||||
|
||||
|
||||
def software_gate() -> dict:
|
||||
metadata = validate_prepared()
|
||||
legacy = decode("legacy")
|
||||
t2 = decode("t2")
|
||||
result = {
|
||||
"lab": "Lab062", "stage": "SOFTWARE_GATE", "rf_tx_count": 0,
|
||||
"tx_sha256": metadata["tx_sha256"], "legacy": legacy, "t2": t2,
|
||||
"passed": legacy["passed"] and t2["passed"],
|
||||
}
|
||||
save_new(OUTPUT / "software_gate.json", result)
|
||||
if not result["passed"]:
|
||||
raise RuntimeError("Lab062 software gate FAIL")
|
||||
return result
|
||||
|
||||
|
||||
def preflight(*, runner=None, process_checker=None, clock_checker=None,
|
||||
sleep_function=time.sleep) -> dict:
|
||||
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("Нет действующего software gate")
|
||||
save_new(RAW / "preflight_attempt.json", {
|
||||
"lab": "Lab062", "rf_tx_count": 0, "point": asdict(POINT),
|
||||
"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):
|
||||
if "-t" in command:
|
||||
raise PermissionError("TX запрещён в Lab062 preflight")
|
||||
return actual_runner(command)
|
||||
|
||||
result = {
|
||||
"lab": "Lab062", "stage": "RX_ONLY_PREFLIGHT", "passed": False,
|
||||
"rf_tx_count": 0, "point": asdict(POINT), "parameters": parameters(),
|
||||
"tx_sha256": metadata["tx_sha256"], "stop_before_tx": True,
|
||||
}
|
||||
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("Lab062 preflight RX уже существует")
|
||||
captured = rx_only(transfer_command(path, PREFLIGHT_SAMPLES))
|
||||
result["rx_process"] = {
|
||||
"returncode": captured.returncode,
|
||||
"stdout": captured.stdout or "", "stderr": captured.stderr or "",
|
||||
}
|
||||
log = (captured.stdout or "") + (captured.stderr or "")
|
||||
if captured.returncode or not hardware.clean_counter_log(log, "overrun"):
|
||||
raise RuntimeError("RX preflight: процесс или счётчик overruns не прошёл")
|
||||
if path.stat().st_size != 2 * PREFLIGHT_SAMPLES:
|
||||
raise RuntimeError("Неверная длина Lab062 preflight RX")
|
||||
level = hardware.receive_level(path)
|
||||
# У RX-only фона нет символов. Унаследованная метрика Lab056 была бы
|
||||
# привязана к её 20 ksymbol/s и здесь физически неприменима.
|
||||
level.pop("symbol_energy_code2_seconds", None)
|
||||
level["symbol_energy"] = "NOT_APPLICABLE_TO_BACKGROUND"
|
||||
ac_rms = float(np.sqrt(max(0.0, level["ac_power_code2"])))
|
||||
strong = bool(ac_rms >= STRONG_AC_RMS_CODE or level["max_abs_i"] >= STRONG_COMPONENT_CODE
|
||||
or level["max_abs_q"] >= STRONG_COMPONENT_CODE)
|
||||
level.update({
|
||||
"ac_rms_code": ac_rms,
|
||||
"unexpected_strong_signal": strong,
|
||||
"strong_signal_rule": (
|
||||
f"AC RMS >= {STRONG_AC_RMS_CODE} code or max |I|/|Q| >= "
|
||||
f"{STRONG_COMPONENT_CODE} code"
|
||||
),
|
||||
})
|
||||
result["rx_level"] = level
|
||||
result["rx_sha256"] = sha256(path)
|
||||
result["rx_overruns"] = 0
|
||||
if level["saturation_detected"]:
|
||||
raise RuntimeError("RX clipping; STOP без изменения gain")
|
||||
if strong:
|
||||
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), encoding="utf-8"
|
||||
)
|
||||
if not result["passed"]:
|
||||
raise RuntimeError("Lab062 preflight cleanup FAIL")
|
||||
return result
|
||||
|
||||
|
||||
def require_preflight(metadata: dict) -> dict:
|
||||
"""Проверить сохранённый preflight именно для подготовленного потока Lab062."""
|
||||
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 для этой конфигурации Lab062")
|
||||
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-сеанс Lab062")
|
||||
metadata = validate_prepared()
|
||||
require_preflight(metadata)
|
||||
receive_path = RAW / "rx_stream.cs8"
|
||||
if receive_path.exists() or (RAW / "capture_result.json").exists():
|
||||
raise FileExistsError("Аппаратный результат Lab062 уже существует")
|
||||
|
||||
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": "Lab062", "authorized_once": True, "automatic_retry": False,
|
||||
"point": asdict(POINT), "parameters": parameters(),
|
||||
"tx_sha256": metadata["tx_sha256"],
|
||||
})
|
||||
|
||||
result = {
|
||||
"lab": "Lab062", "stage": "SINGLE_RF_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=65536)
|
||||
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_stream.cs8", metadata["tx_samples"], transmit=True
|
||||
))
|
||||
save_new(RAW / "tx_process_result.json", {
|
||||
"returncode": transmitted.returncode,
|
||||
"stdout": transmitted.stdout or "", "stderr": transmitted.stderr or "",
|
||||
})
|
||||
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"] / MODE.sample_rate_hz
|
||||
if result["rx_bytes"] != RX_SAMPLES * 2:
|
||||
raise RuntimeError("Повреждённая длина RX; передача уже выполнена, повтор запрещён")
|
||||
rx_log = "".join(
|
||||
(RAW / f"rx_{kind}.txt").read_text(encoding="utf-8")
|
||||
for kind in ("stdout", "stderr")
|
||||
)
|
||||
tx_log = (transmitted.stdout or "") + (transmitted.stderr or "")
|
||||
if not hardware.clean_counter_log(rx_log, "overrun"):
|
||||
raise RuntimeError("RX overrun или неоднозначный счётчик; повтор запрещён")
|
||||
if not hardware.clean_counter_log(tx_log, "underrun"):
|
||||
raise RuntimeError("TX underrun или неоднозначный счётчик; повтор запрещён")
|
||||
result["rx_overruns"] = 0
|
||||
result["tx_underruns"] = 0
|
||||
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 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), encoding="utf-8"
|
||||
)
|
||||
if not result["passed"]:
|
||||
raise RuntimeError("Lab062 RF-сеанс завершён с ошибкой; повтор запрещён")
|
||||
return result
|
||||
|
||||
|
||||
def run_functional_tests() -> None:
|
||||
assert MODE.samples_per_symbol == 16
|
||||
assert len(plans()) == 16
|
||||
assert all(len(frame.plan.payload) == 256 for frame in plans())
|
||||
assert "-t" not in transfer_command("NUL", 65536)
|
||||
assert "-t" in transfer_command("NUL", 65536, transmit=True)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--stage", choices=("prepare", "software", "preflight", "capture"), 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()
|
||||
else:
|
||||
result = capture_once(authorization=arguments.authorize_one_rf)
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
print("RF TX = 0")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
330
experiments/lab062_rf_analysis.py
Normal file
330
experiments/lab062_rf_analysis.py
Normal file
@@ -0,0 +1,330 @@
|
||||
"""Lab062: офлайн-анализ единственной сохранённой RX IQ-записи."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import Counter
|
||||
import csv
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
|
||||
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 lab058_abab_symbol_quality as lab058
|
||||
from experiments import lab062_full_packet_rf_preflight as lab062
|
||||
|
||||
|
||||
def counter_values(text: str, name: str) -> list[int]:
|
||||
pattern = re.compile(rf"(?<![\w.+-])(\d+)\s+(?:(?:rx|tx)\s+)?{name}s?\b", re.I)
|
||||
return [int(match.group(1)) for match in pattern.finditer(text)]
|
||||
|
||||
|
||||
def locate_burst(tx: lab058.Cs8File, rx: lab058.Cs8File, frame_samples: int) -> dict:
|
||||
"""Найти начало по первому полному пакету, не передавая границы декодеру."""
|
||||
stride = lab062.MODE.samples_per_symbol
|
||||
tx_raw = np.memmap(tx.path, np.int8, mode="r")
|
||||
rx_raw = np.memmap(rx.path, np.int8, mode="r")
|
||||
tx_decimated = (
|
||||
tx_raw[0:2 * frame_samples:2 * stride].astype(np.float32)
|
||||
+ 1j * tx_raw[1:2 * frame_samples:2 * stride].astype(np.float32)
|
||||
)
|
||||
rx_decimated = (
|
||||
rx_raw[0::2 * stride].astype(np.float32)
|
||||
+ 1j * rx_raw[1::2 * stride].astype(np.float32)
|
||||
)
|
||||
tx_decimated -= np.mean(tx_decimated)
|
||||
rx_decimated -= np.mean(rx_decimated)
|
||||
correlation = signal.correlate(rx_decimated, tx_decimated, mode="valid", method="fft")
|
||||
coarse = int(np.argmax(abs(correlation))) * stride
|
||||
first = tx.read(0, frame_samples).astype(np.complex128)
|
||||
first -= np.mean(first)
|
||||
candidates = range(max(0, coarse - 2 * stride), min(rx.size - frame_samples, coarse + 2 * stride) + 1)
|
||||
values = []
|
||||
for start in candidates:
|
||||
received = rx.read(start, frame_samples).astype(np.complex128)
|
||||
received -= np.mean(received)
|
||||
values.append(abs(np.vdot(first, received)))
|
||||
offset = int(np.argmax(values))
|
||||
start = candidates.start + offset
|
||||
median = float(np.median(abs(correlation)))
|
||||
return {
|
||||
"rx_start_sample": start,
|
||||
"coarse_start_sample": coarse,
|
||||
"correlation_peak_to_median": float(abs(correlation[coarse // stride]) / max(median, 1e-30)),
|
||||
"method": "TX first-frame correlation; used only after strict receiver verdict",
|
||||
}
|
||||
|
||||
|
||||
def strict_row(rows: list[dict], nominal: int) -> dict | None:
|
||||
if not rows:
|
||||
return None
|
||||
selected = min(rows, key=lambda row: abs(row["absolute_refined"] - nominal))
|
||||
return selected if abs(selected["absolute_refined"] - nominal) <= 64 else None
|
||||
|
||||
|
||||
def diagnostic_symbol_rows(rx: lab058.Cs8File, start: int, frame_samples: int,
|
||||
mode: str) -> list[dict]:
|
||||
processor = lab062.packet_processing.PacketProcessor(carrier_tracking_mode=mode)
|
||||
rows = []
|
||||
for index, frame in enumerate(lab062.plans(), 1):
|
||||
values = rx.read(start + (index - 1) * frame_samples, frame_samples).astype(np.complex64)
|
||||
context = lab062.packet_processing.PacketMeasure()
|
||||
token = lab062.packet_processing.CURRENT.set(context)
|
||||
try:
|
||||
probe = processor.extract(values, 0, frame.plan.frame_symbol_count)
|
||||
finally:
|
||||
lab062.packet_processing.CURRENT.reset(token)
|
||||
expected = 2.0 * frame.plan.frame_bits.astype(np.float64) - 1.0
|
||||
if not probe.passed or probe.normalized_real.size != expected.size:
|
||||
rows.append({"packet_index": index, "measurable": False, "reason": probe.reason})
|
||||
continue
|
||||
soft = np.asarray(probe.normalized_real, dtype=np.float64)
|
||||
errors = np.signbit(soft) != np.signbit(expected)
|
||||
p = lab062.phy.lab045.PREAMBLE_BIT_COUNT
|
||||
h = p + lab062.phy.lab045.PHY_HEADER_BIT_COUNT
|
||||
traces = context.cache.get("carrier_tracking_traces", ())
|
||||
final_frequency = None
|
||||
if traces and len(traces[-1].frequency_hz):
|
||||
final_frequency = float(traces[-1].frequency_hz[-1])
|
||||
rows.append({
|
||||
"packet_index": index,
|
||||
"measurable": True,
|
||||
"preamble_symbol_errors": int(np.sum(errors[:p])),
|
||||
"header_symbol_errors": int(np.sum(errors[p:h])),
|
||||
"payload_symbols_before_fec_errors": int(np.sum(errors[h:])),
|
||||
"symbols": int(len(expected)),
|
||||
"symbol_errors": int(np.sum(errors)),
|
||||
"symbol_error_rate": float(np.mean(errors)),
|
||||
"symbol_rms_deviation": float(np.sqrt(np.mean((soft - expected) ** 2))),
|
||||
"initial_frequency_hz": float(probe.carrier["cfo_hz"]),
|
||||
"t2_final_frequency_hz": final_frequency,
|
||||
"t2_lock_status": "NOT_DEFINED_BY_IMPLEMENTATION" if mode == "t2" else "NOT_APPLICABLE",
|
||||
})
|
||||
return rows
|
||||
|
||||
|
||||
def waveform_quality(tx: lab058.Cs8File, rx: lab058.Cs8File, start: int) -> dict:
|
||||
transmitted = tx.read(0, tx.size).astype(np.complex128)
|
||||
received = rx.read(start, tx.size).astype(np.complex128)
|
||||
transmitted -= np.mean(transmitted)
|
||||
received -= np.mean(received)
|
||||
product = received * np.conj(transmitted)
|
||||
block = 4096
|
||||
count = len(product) // block
|
||||
block_values = np.mean(product[:count * block].reshape(count, block), axis=1)
|
||||
block_times = (np.arange(count) * block + 0.5 * block) / lab062.MODE.sample_rate_hz
|
||||
phase = np.unwrap(np.angle(block_values))
|
||||
weights = np.maximum(abs(block_values), 1e-12)
|
||||
design = np.column_stack((np.ones(count), block_times))
|
||||
root = np.sqrt(weights / np.max(weights))
|
||||
coefficients, *_ = np.linalg.lstsq(design * root[:, None], phase * root, rcond=None)
|
||||
frequency_hz = float(coefficients[1] / (2 * np.pi))
|
||||
times = np.arange(len(received)) / lab062.MODE.sample_rate_hz
|
||||
corrected = received * np.exp(-2j * np.pi * frequency_hz * times)
|
||||
gain = np.vdot(transmitted, corrected) / np.vdot(transmitted, transmitted)
|
||||
matched = gain * transmitted
|
||||
residual = corrected - matched
|
||||
matched_power = float(np.mean(abs(matched) ** 2))
|
||||
residual_power = float(np.mean(abs(residual) ** 2))
|
||||
|
||||
frequencies, tx_psd = signal.welch(
|
||||
transmitted, fs=lab062.MODE.sample_rate_hz, nperseg=8192,
|
||||
noverlap=4096, return_onesided=False,
|
||||
)
|
||||
_, rx_psd = signal.welch(
|
||||
corrected, fs=lab062.MODE.sample_rate_hz, nperseg=8192,
|
||||
noverlap=4096, return_onesided=False,
|
||||
)
|
||||
_, cross = signal.csd(
|
||||
corrected, transmitted, fs=lab062.MODE.sample_rate_hz, nperseg=8192,
|
||||
noverlap=4096, return_onesided=False,
|
||||
)
|
||||
transfer = np.conj(cross) / np.maximum(tx_psd, 1e-30)
|
||||
coherence = abs(cross) ** 2 / np.maximum(tx_psd * rx_psd, 1e-30)
|
||||
occupied = tx_psd >= np.max(tx_psd) * 0.01
|
||||
stable = occupied & (coherence >= 0.5)
|
||||
gain_db = 20 * np.log10(np.maximum(abs(transfer[stable]), 1e-30))
|
||||
return {
|
||||
"frequency_offset_hz": frequency_hz,
|
||||
"complex_gain": {"real": float(gain.real), "imag": float(gain.imag)},
|
||||
"matched_component_power_code2": matched_power,
|
||||
"residual_power_code2": residual_power,
|
||||
"matched_residual_db": float(10 * math.log10(matched_power / residual_power)),
|
||||
"residual_interpretation": "Contains noise, channel distortion and any TX-underrun effect; not pure noise",
|
||||
"band_bins": int(np.sum(stable)),
|
||||
"band_gain_nonuniformity_p95_p05_db": (
|
||||
float(np.percentile(gain_db, 95) - np.percentile(gain_db, 5)) if gain_db.size else None
|
||||
),
|
||||
"band_gain_max_min_db": float(np.ptp(gain_db)) if gain_db.size else None,
|
||||
"median_coherence": float(np.median(coherence[stable])) if np.any(stable) else None,
|
||||
"tx_psd_threshold_db": -20.0,
|
||||
"coherence_threshold": 0.5,
|
||||
}
|
||||
|
||||
|
||||
def lab058_quality(tx: lab058.Cs8File, rx: lab058.Cs8File, start: int,
|
||||
frame_samples: int) -> dict:
|
||||
"""Применить к каждому полному PHY-кадру неизменённую методику Lab058."""
|
||||
rows = []
|
||||
for index, frame in enumerate(lab062.plans(), 1):
|
||||
offset = (index - 1) * frame_samples
|
||||
transmitted = tx.read(offset, frame_samples)
|
||||
received = rx.read(start + offset, frame_samples)
|
||||
expected = 2.0 * frame.plan.frame_bits.astype(np.float64) - 1.0
|
||||
metrics, _working = lab058.analyze_values(received, expected, lab062.MODE.symbol_rate)
|
||||
channel, _arrays = lab058.channel_metrics(
|
||||
transmitted, received, lab062.MODE.symbol_rate, metrics["oracle"]["cfo_hz"]
|
||||
)
|
||||
rows.append({"packet_index": index, "oracle": metrics["oracle"], "channel": channel})
|
||||
selected = [row["oracle"]["metrics"] for row in rows]
|
||||
errors = sum(row["errors"] for row in selected)
|
||||
symbols = sum(row["symbols"] for row in selected)
|
||||
matched = float(np.mean([row["matched_component_power"] for row in selected]))
|
||||
residual = float(np.mean([row["residual_power"] for row in selected]))
|
||||
ripple = [row["channel"]["amplitude_ripple_p95_p05_db"] for row in rows]
|
||||
return {
|
||||
"method": "Lab058 analyze_values/oracle validation tiles, independently per full PHY packet",
|
||||
"packets": rows,
|
||||
"symbol_errors": errors,
|
||||
"symbols": symbols,
|
||||
"symbol_error_rate": errors / symbols,
|
||||
"evm_rms": float(math.sqrt(residual / matched)),
|
||||
"matched_component_power": matched,
|
||||
"residual_power": residual,
|
||||
"matched_residual_db": float(10 * math.log10(matched / residual)),
|
||||
"residual_interpretation": "Contains noise and unmodelled distortion; not pure noise",
|
||||
"band_gain_nonuniformity_p95_p05_db_median": float(np.median(ripple)),
|
||||
"band_gain_nonuniformity_p95_p05_db_min": float(np.min(ripple)),
|
||||
"band_gain_nonuniformity_p95_p05_db_max": float(np.max(ripple)),
|
||||
}
|
||||
|
||||
|
||||
def analyze() -> dict:
|
||||
capture = json.loads((lab062.RAW / "capture_result.json").read_text(encoding="utf-8"))
|
||||
legacy = json.loads((lab062.OUTPUT / "legacy_rf_decode.json").read_text(encoding="utf-8"))
|
||||
t2 = json.loads((lab062.OUTPUT / "t2_rf_decode.json").read_text(encoding="utf-8"))
|
||||
tx = lab058.Cs8File(lab062.RAW / "tx_stream.cs8")
|
||||
rx = lab058.Cs8File(lab062.RAW / "rx_stream.cs8")
|
||||
frame_samples = lab062.phy.frame_waveform_sample_count(lab062.plans()[0].plan, lab062.MODE)
|
||||
location = locate_burst(tx, rx, frame_samples)
|
||||
start = location["rx_start_sample"]
|
||||
legacy_symbols = diagnostic_symbol_rows(rx, start, frame_samples, "legacy")
|
||||
t2_symbols = diagnostic_symbol_rows(rx, start, frame_samples, "t2")
|
||||
|
||||
packet_rows = []
|
||||
for index in range(1, lab062.PACKETS + 1):
|
||||
nominal = start + (index - 1) * frame_samples
|
||||
legacy_strict = strict_row(legacy["rows"], nominal)
|
||||
t2_strict = strict_row(t2["rows"], nominal)
|
||||
packet_rows.append({
|
||||
"packet_index": index,
|
||||
"nominal_rx_start": nominal,
|
||||
"legacy": legacy_strict,
|
||||
"t2": t2_strict,
|
||||
"diagnostic_legacy": legacy_symbols[index - 1],
|
||||
"diagnostic_t2": t2_symbols[index - 1],
|
||||
})
|
||||
|
||||
rx_log = (lab062.RAW / "rx_stderr.txt").read_text(encoding="utf-8")
|
||||
tx_process = json.loads((lab062.RAW / "tx_process_result.json").read_text(encoding="utf-8"))
|
||||
tx_log = (tx_process.get("stdout") or "") + (tx_process.get("stderr") or "")
|
||||
level = lab062.hardware.receive_level(lab062.RAW / "rx_stream.cs8")
|
||||
level.pop("symbol_energy_code2_seconds", None)
|
||||
quality = waveform_quality(tx, rx, start)
|
||||
comparable_quality = lab058_quality(tx, rx, start, frame_samples)
|
||||
all_diag = [row for row in t2_symbols if row.get("measurable")]
|
||||
total_errors = sum(row["symbol_errors"] for row in all_diag)
|
||||
total_symbols = sum(row["symbols"] for row in all_diag)
|
||||
rms = math.sqrt(sum(row["symbol_rms_deviation"] ** 2 * row["symbols"] for row in all_diag) / total_symbols)
|
||||
legacy_set = set(legacy["passed_packet_indexes"])
|
||||
t2_set = set(t2["passed_packet_indexes"])
|
||||
result = {
|
||||
"lab": "Lab062",
|
||||
"status": "RF_SESSION_INVALID_TX_UNDERRUN",
|
||||
"rf_tx_count": capture["rf_tx_count"],
|
||||
"automatic_tx_repeats": capture["automatic_tx_repeats"],
|
||||
"integrity": {
|
||||
"tx_sha256": lab062.sha256(lab062.RAW / "tx_stream.cs8"),
|
||||
"rx_sha256": lab062.sha256(lab062.RAW / "rx_stream.cs8"),
|
||||
"rx_samples": rx.size,
|
||||
"rx_bytes": (lab062.RAW / "rx_stream.cs8").stat().st_size,
|
||||
"rx_duration_seconds": rx.size / lab062.MODE.sample_rate_hz,
|
||||
"rx_overrun_values": counter_values(rx_log, "overrun"),
|
||||
"tx_underrun_values": counter_values(tx_log, "underrun"),
|
||||
"rx_overruns_max": max(counter_values(rx_log, "overrun"), default=None),
|
||||
"tx_underruns_max": max(counter_values(tx_log, "underrun"), default=None),
|
||||
"rx_clipping": bool(level["saturation_detected"]),
|
||||
"rx_clipped_samples_fraction": level["clipped_samples_fraction"],
|
||||
},
|
||||
"clock": capture["clock"],
|
||||
"cleanup_errors": capture["cleanup_errors"],
|
||||
"location": location,
|
||||
"legacy": {key: value for key, value in legacy.items() if key != "rows"},
|
||||
"t2": {key: value for key, value in t2.items() if key != "rows"},
|
||||
"passed_by_both": sorted(legacy_set & t2_set),
|
||||
"passed_only_by_legacy": sorted(legacy_set - t2_set),
|
||||
"passed_only_by_t2": sorted(t2_set - legacy_set),
|
||||
"failed_by_both": sorted(set(range(1, 17)) - legacy_set - t2_set),
|
||||
"packet_rows": packet_rows,
|
||||
"diagnostic_symbol_error_rate_t2": total_errors / total_symbols,
|
||||
"symbol_rms_deviation_t2": rms,
|
||||
"lab058_quality": comparable_quality,
|
||||
"waveform_quality": quality,
|
||||
"lab056_comparison": {
|
||||
"lab056_B_oracle_symbol_error_rate": 0.03495896,
|
||||
"lab056_B_oracle_evm_rms": 0.845350,
|
||||
"scope": "Lab056 used a different PRBS ABAB waveform and method; values are contextual, not identical estimands",
|
||||
},
|
||||
"primary_result": (
|
||||
"The sole RF session is invalid as a clean legacy-versus-T2 acceptance test because "
|
||||
"hackrf_transfer reported one transient TX underrun; both strict modes produced 0/16."
|
||||
),
|
||||
"recommended_next_single_step": (
|
||||
"Offline localize the underrun position against packet boundaries before deciding whether a new RF lab is justified."
|
||||
),
|
||||
"rf_tx_now": 0,
|
||||
"commit": "NOT_PERFORMED", "push": "NOT_PERFORMED", "lab063": "NOT_CREATED",
|
||||
}
|
||||
lab062.OUTPUT.mkdir(parents=True, exist_ok=True)
|
||||
(lab062.OUTPUT / "rf_analysis.json").write_text(
|
||||
json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8"
|
||||
)
|
||||
with (lab062.OUTPUT / "packet_comparison.csv").open("w", newline="", encoding="utf-8-sig") as stream:
|
||||
writer = csv.writer(stream)
|
||||
writer.writerow([
|
||||
"packet_index", "legacy_header", "legacy_fec", "legacy_crc", "legacy_output",
|
||||
"t2_header", "t2_fec", "t2_crc", "t2_output", "preamble_errors_t2",
|
||||
"header_errors_t2", "payload_errors_before_fec_t2", "initial_frequency_hz_t2",
|
||||
"t2_final_frequency_hz", "t2_lock_status",
|
||||
])
|
||||
for row in packet_rows:
|
||||
legacy_row, t2_row, diagnostic = row["legacy"], row["t2"], row["diagnostic_t2"]
|
||||
writer.writerow([
|
||||
row["packet_index"], *(legacy_row.get(key) if legacy_row else False for key in ("header", "fec", "crc", "packet_output")),
|
||||
*(t2_row.get(key) if t2_row else False for key in ("header", "fec", "crc", "packet_output")),
|
||||
diagnostic.get("preamble_symbol_errors"), diagnostic.get("header_symbol_errors"),
|
||||
diagnostic.get("payload_symbols_before_fec_errors"), diagnostic.get("initial_frequency_hz"),
|
||||
diagnostic.get("t2_final_frequency_hz"), diagnostic.get("t2_lock_status"),
|
||||
])
|
||||
return result
|
||||
|
||||
|
||||
def main() -> None:
|
||||
result = analyze()
|
||||
concise = {key: result[key] for key in (
|
||||
"status", "rf_tx_count", "automatic_tx_repeats", "integrity", "legacy", "t2",
|
||||
"passed_by_both", "passed_only_by_legacy", "passed_only_by_t2", "failed_by_both",
|
||||
"diagnostic_symbol_error_rate_t2", "symbol_rms_deviation_t2", "waveform_quality",
|
||||
"primary_result", "recommended_next_single_step", "rf_tx_now",
|
||||
)}
|
||||
print(json.dumps(concise, ensure_ascii=False, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
142
tests/test_lab062_full_packet_rf_preflight.py
Normal file
142
tests/test_lab062_full_packet_rf_preflight.py
Normal file
@@ -0,0 +1,142 @@
|
||||
"""Lab062: программные условия и запрет TX до отдельного разрешения."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from experiments import lab062_full_packet_rf_preflight as lab
|
||||
from tools.hackrf_process_check import ProcessCheck, ProcessState
|
||||
|
||||
|
||||
def test_fixed_packet_and_operating_point():
|
||||
assert len(lab.plans()) == 16
|
||||
assert all(len(frame.plan.payload) == 256 for frame in lab.plans())
|
||||
assert lab.parameters()["rf_frequency_hz"] == 1_400_000_000
|
||||
assert lab.parameters()["symbol_rate"] == 500_000
|
||||
assert lab.parameters()["tx_vga_db"] == 10
|
||||
assert lab.parameters()["rx_lna_db"] == lab.parameters()["rx_vga_db"] == 24
|
||||
assert lab.POINT.distance_m == 1.2 and lab.POINT.visibility == "LOS indoor"
|
||||
|
||||
|
||||
def test_rx_and_tx_commands_are_fixed_and_distinct():
|
||||
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("-l") + 1] == receive[receive.index("-g") + 1] == "24"
|
||||
assert transmit[transmit.index("-x") + 1] == "10"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("failure", [None, "clock", "clipping", "strong", "overrun"])
|
||||
def test_preflight_never_transmits_and_fails_closed(tmp_path, monkeypatch, failure):
|
||||
raw, output = tmp_path / "raw", tmp_path / "processed"
|
||||
raw.mkdir(); output.mkdir()
|
||||
monkeypatch.setattr(lab, "RAW", raw); monkeypatch.setattr(lab, "OUTPUT", output)
|
||||
(raw / "tx_stream.cs8").write_bytes(b"test")
|
||||
metadata = {"tx_sha256": "TEST"}
|
||||
(output / "software_gate.json").write_text('{"passed": true, "tx_sha256": "TEST"}')
|
||||
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: [])
|
||||
kwargs = dict(runner=runner, clock_checker=clock,
|
||||
process_checker=lambda: ProcessCheck(ProcessState.CLEAR, ()),
|
||||
sleep_function=lambda _: None)
|
||||
if failure:
|
||||
with pytest.raises(RuntimeError):
|
||||
lab.preflight(**kwargs)
|
||||
else:
|
||||
assert lab.preflight(**kwargs)["passed"]
|
||||
result = __import__("json").loads((raw / "preflight_result.json").read_text())
|
||||
assert result["rf_tx_count"] == 0
|
||||
if failure is None:
|
||||
assert "symbol_energy_code2_seconds" not in result["rx_level"]
|
||||
assert result["rx_level"]["symbol_energy"] == "NOT_APPLICABLE_TO_BACKGROUND"
|
||||
|
||||
|
||||
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("{}")
|
||||
monkeypatch.setattr(lab, "validate_prepared", lambda: {"tx_sha256": "TEST"})
|
||||
(tmp_path / "software_gate.json").write_text('{"passed": true, "tx_sha256": "TEST"}')
|
||||
with pytest.raises(FileExistsError):
|
||||
lab.preflight(runner=lambda command: pytest.fail("hardware"))
|
||||
|
||||
|
||||
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"),
|
||||
)
|
||||
assert not (tmp_path / "hardware_attempt.json").exists()
|
||||
|
||||
|
||||
def test_capture_is_rx_first_single_tx_and_refuses_second_attempt(tmp_path, monkeypatch):
|
||||
raw, output = tmp_path / "raw", tmp_path / "processed"
|
||||
raw.mkdir(); output.mkdir()
|
||||
monkeypatch.setattr(lab, "RAW", raw); monkeypatch.setattr(lab, "OUTPUT", output)
|
||||
tx = raw / "tx_stream.cs8"; tx.write_bytes(b"\x01\x01" * 100)
|
||||
metadata = {"tx_sha256": lab.sha256(tx), "tx_samples": 100}
|
||||
monkeypatch.setattr(lab, "validate_prepared", lambda: metadata)
|
||||
monkeypatch.setattr(lab, "require_preflight", lambda value: {})
|
||||
monkeypatch.setattr(lab, "RX_SAMPLES", 800_000)
|
||||
monkeypatch.setattr(lab.lab054.phy.lab045, "_verify_device_inventory", lambda runner: {"passed": True})
|
||||
monkeypatch.setattr(lab.lab054, "disable_clock_outputs", lambda runner: [])
|
||||
events = []
|
||||
|
||||
class Process:
|
||||
def poll(self): return None
|
||||
def wait(self, timeout): return 0
|
||||
def close_logs(self): pass
|
||||
def terminate(self): pass
|
||||
|
||||
def starter(command, stdout, stderr):
|
||||
events.append("RX")
|
||||
Path(command[command.index("-r") + 1]).write_bytes(b"\x01\x01" * 800_000)
|
||||
Path(stdout).write_text("")
|
||||
Path(stderr).write_text("0 RX overruns")
|
||||
return Process()
|
||||
|
||||
def runner(command):
|
||||
if "-t" in command:
|
||||
events.append("TX")
|
||||
return subprocess.CompletedProcess(command, 0, "0 TX underruns", "")
|
||||
return subprocess.CompletedProcess(command, 0, "", "")
|
||||
|
||||
def clock(point, runner, guard, **kwargs):
|
||||
guard.tx_allowed = True
|
||||
return {
|
||||
"passed": True, "master_reg29": 0x26, "slave_reg29": 0x26,
|
||||
"slave_clkin_status": "DETECTED",
|
||||
}
|
||||
|
||||
result = lab.capture_once(
|
||||
authorization=True, runner=runner, process_starter=starter,
|
||||
process_checker=lambda: ProcessCheck(ProcessState.CLEAR, ()),
|
||||
clock_checker=clock, sleep_function=lambda _: None,
|
||||
)
|
||||
assert events == ["RX", "TX"]
|
||||
assert result["rf_tx_count"] == 1 and result["automatic_tx_repeats"] == 0
|
||||
assert result["rx_overruns"] == result["tx_underruns"] == 0
|
||||
assert json.loads((raw / "capture_result.json").read_text())["passed"]
|
||||
with pytest.raises(FileExistsError):
|
||||
lab.capture_once(authorization=True, runner=lambda command: pytest.fail("hardware"))
|
||||
Reference in New Issue
Block a user