Lab066: validate 500 ksymbol/s full-packet OTA reception

This commit is contained in:
LittleSam129
2026-09-15 15:51:57 +03:00
parent dacf04f2d0
commit b27a5cf00e
14 changed files with 6597 additions and 0 deletions

View File

@@ -2924,3 +2924,80 @@ clipping0, cleanup без ошибок; процессы после CLEAR. RX:16
RF TX=0. Lab065 не коммитировалась, push не выполнялся, Lab066 не создана. RF TX=0. Lab065 не коммитировалась, push не выполнялся, Lab066 не создана.
Следующий этап: STOP, рассмотрение результата и отдельное разрешение на коммит Следующий этап: STOP, рассмотрение результата и отдельное разрешение на коммит
Lab065; повторная передача или изменение усилений запрещены. Lab065; повторная передача или изменение усилений запрещены.
# Запись 061
## Дата
15 сентября 2026 года
## Тема
Lab065 зафиксирована; подготовка исправленного Lab066 и RX-only preflight
## Выполнено
По прямому разрешению пользователя создан локальный коммит Lab065
`dacf04f2d0a15c0c1b56a7fb527db61fc2e38462` с сообщением
`Lab065: validate OTA gain improvement at TX VGA 20 dB`. Push не выполнялся.
Lab066 подготовлена как исправленный повтор цели Lab062. Исторический результат
Lab062 сохранён: `RF_SESSION_INVALID_TX_UNDERRUN`, legacy/T2 0/16. В Lab066
использованы16 штатных полных PHY-пакетов по256 байт при500ксимв/с, Fs8Мвыб/с,
цифровой несущей+50кГц, RF1400МГц, TX VGA20дБ и RX LNA/VGA24/24дБ. RF amplifier
и Bias-Tee выключены; геометрия около1,2м, LOS, антенны и поляризация прежние.
Каждый packet waveform содержит92256 отсчётов. Между пакетами записаны15
фактических нулевых пауз по40000 отсчётов, то есть по5мс. Добавлены начальный и
конечный guards по800000 отсчётов, или по100мс. Длина без guards равна2076096,
полная длина3676096 отсчётов,7352192 байта,0,459512с. SHA-256 TX:
621b0bb8ec72bba4f522c79a5c77669c850740ea481270c4dcc2a7317819f199.
Весь конечный TX-файл независимо обработан legacy и T2. Оба режима дали16/16
HEADER, FEC, CRC, packet output и точных payload в правильном порядке. Позиции
пакетов совпали с разметкой пауз. Между режимами fallback отсутствует; T2 для
каждого пакета заново оценивает состояние по его собственной преамбуле.
Программные шлюзы: quick gate1013 тестов ядра/39 функциональных/13 аппаратных
пропусков PASS; pytest1013 passed; diff check PASS. Затем выполнен один RX-only
preflight. Обе HackRF One r9 и роли подтверждены. MASTER/SLAVE reg29=0x26,
SLAVE CLKIN=DETECTED на втором опросе через132,113мс. Фон:800000 отсчётов,
полный RMS2,225641, AC RMS0,827637, max|I|/|Q|=4/5, clipping0, maximum
overruns0, сильного сигнала нет. Cleanup без ошибок, процессы после CLEAR.
Независимый verifier пересчитал SHA, длину,15 нулевых пауз, оба guards,
16 канонических packet waveform, payload и RX-only метрики; расхождений нет.
Read-only adversary проверил запрет TX, одноразовость, связь preflight/capture
с конфигурацией, максимум счётчиков, live-фон перед TX и границу диагностик;
после исправлений открытых замечаний нет.
RF TX=0. Lab066 не коммитировалась, push не выполнялся, Lab067 не создавалась.
Следующий этап: STOP и ожидание отдельного прямого разрешения на один TX Lab066.
После отдельного прямого разрешения выполнен ровно один OTA RF-сеанс Lab066.
Передача всего потока запущена одним вызовом, автоматических повторов не было.
Перед TX MASTER/SLAVE reg29=0x26, SLAVE CLKIN=DETECTED на втором опросе через
131,291мс. Live RX-фон: AC RMS1,173567, max|I|/|Q|=4/5, clipping0. TX underrun
observations0/0, RX overrun observations0/0/0/0; максимумы0/0. RX записал
16000000 отсчётов за2,0с, SHA-256
72ff7ddd8b30baa4a5c066361ca1f8dfa664f1da37f65c0c373b211ce6cf00f0.
Cleanup без ошибок, процессы после CLEAR; аппаратная запись валидна.
Одна RX-запись независимо обработана legacy и T2 без fallback. Оба режима дали
145 кандидатов,10 HEADER/FEC/CRC PASS,10 packet output и10 точных payload.
Оба приняли пакеты4–8 и10–14; пакеты1–3,9,15,16 остановились на PHY header.
Выигрыш T2 равен нулю.
Диагностика всех16 пакетов после строгого результата дала SER0,0110466,
RMS-отклонение0,528374, matched/residual+5,542117дБ и медианную неравномерность
полосы2,870070дБ. Это лучше Lab056 B на0,0239123 SER,0,316976 RMS,
4,077271дБ matched/residual и2,601312дБ неравномерности. Относительно Lab062
улучшения составляют0,0882400 SER,0,692027 RMS,7,272168дБ matched/residual и
3,790271дБ неравномерности; сравнение с Lab062 контекстное из-за её underrun.
Классификация `PARTIAL_PACKET_RECOVERY_NO_T2_GAIN`: аппаратно чистый сеанс
существенно улучшил физическое качество, но не достиг16/16, а T2 не изменил
строгий результат. RF TX=0, повтор не выполнялся. Lab066 не коммитировалась,
push не выполнялся, Lab067 не создавалась. Следующий возможный одиночный шаг —
отдельно разрешённый офлайн-анализ PHY header не прошедших пакетов по сохранённой
IQ-записи; новый TX не нужен.

View File

@@ -0,0 +1,224 @@
{
"lab": "Lab066",
"stage": "SINGLE_RF_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": 500000,
"samples_per_symbol": 16,
"digital_carrier_hz": 50000.0,
"packets": 16,
"payload_bytes": 256,
"tx_vga_db": 20,
"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"
},
"geometry_unchanged": true,
"clock_path": "10 MHz TCXO -> MASTER -> MASTER CLKOUT -> SLAVE CLKIN"
},
"tx_sha256": "621b0bb8ec72bba4f522c79a5c77669c850740ea481270c4dcc2a7317819f199",
"process_before": {
"state": "CLEAR",
"attempts": [
{
"state": "CLEAR",
"mechanism": "psutil",
"matches": [],
"inspected": 300,
"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": 131.2907999381423,
"elapsed_ms": 131.2907999381423,
"polls": [
{
"query": 1,
"start_ms": 50.125200068578124,
"end_ms": 66.83470006100833,
"raw": "CLKIN status: no clock signal detected",
"state": "NOT_DETECTED",
"exit_code": 0
},
{
"query": 2,
"start_ms": 116.86519999057055,
"end_ms": 131.2907999381423,
"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.952423095703125,
"mean_q": 1.83642578125,
"power_code2": 5.656829833984375,
"ac_power_code2": 1.377260430715979,
"rms_code": 2.3784090972716143,
"peak_magnitude_code": 5.385164807134504,
"max_abs_i": 4.0,
"max_abs_q": 5.0,
"papr_db": 7.0982488319834305,
"clipped_components_fraction": 0.0,
"clipped_samples_fraction": 0.0,
"saturation_detected": false,
"analog_compression": "UNKNOWN_WITHOUT_CALIBRATION",
"ac_rms_code": 1.173567395046394,
"unexpected_strong_signal": false
},
"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": 336
},
{
"count": 0,
"role": "UNKNOWN",
"kind": "underrun",
"text": "0 underruns",
"character_offset": 506
}
],
"selected_counts": [
0,
0
],
"final_count": 0,
"maximum_count": 0,
"unparsed_loss_text": false,
"clean": true
},
"rx_counter_summary": {
"expected_kind": "overrun",
"observations": [
{
"count": 0,
"role": "UNKNOWN",
"kind": "overrun",
"text": "0 overruns",
"character_offset": 337
},
{
"count": 0,
"role": "UNKNOWN",
"kind": "overrun",
"text": "0 overruns",
"character_offset": 459
},
{
"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_samples": 16000000,
"rx_sha256": "72ff7ddd8b30baa4a5c066361ca1f8dfa664f1da37f65c0c373b211ce6cf00f0",
"rx_level": {
"samples": 16000000,
"mean_i": 0.9510684375,
"mean_q": 1.844211625,
"power_code2": 5.0153848125,
"ac_power_code2": 0.7097371219061666,
"rms_code": 2.239505483918269,
"peak_magnitude_code": 5.830951894845301,
"max_abs_i": 4.0,
"max_abs_q": 5.0,
"papr_db": 8.311746565484663,
"clipped_components_fraction": 0.0,
"clipped_samples_fraction": 0.0,
"symbol_energy_code2_seconds": 0.000250769240625,
"saturation_detected": false,
"analog_compression": "UNKNOWN_WITHOUT_CALIBRATION"
},
"rx_clipping": false,
"process_after": {
"state": "CLEAR",
"attempts": [
{
"state": "CLEAR",
"mechanism": "psutil",
"matches": [],
"inspected": 300,
"errors": []
}
]
},
"cleanup_errors": []
}

View File

@@ -0,0 +1,17 @@
packet_index,legacy_header,legacy_fec,legacy_crc,legacy_packet_output,t2_header,t2_fec,t2_crc,t2_packet_output
1,False,False,False,False,False,False,False,False
2,False,False,False,False,False,False,False,False
3,False,False,False,False,False,False,False,False
4,True,True,True,True,True,True,True,True
5,True,True,True,True,True,True,True,True
6,True,True,True,True,True,True,True,True
7,True,True,True,True,True,True,True,True
8,True,True,True,True,True,True,True,True
9,False,False,False,False,False,False,False,False
10,True,True,True,True,True,True,True,True
11,True,True,True,True,True,True,True,True
12,True,True,True,True,True,True,True,True
13,True,True,True,True,True,True,True,True
14,True,True,True,True,True,True,True,True
15,False,False,False,False,False,False,False,False
16,False,False,False,False,False,False,False,False
1 packet_index legacy_header legacy_fec legacy_crc legacy_packet_output t2_header t2_fec t2_crc t2_packet_output
2 1 False False False False False False False False
3 2 False False False False False False False False
4 3 False False False False False False False False
5 4 True True True True True True True True
6 5 True True True True True True True True
7 6 True True True True True True True True
8 7 True True True True True True True True
9 8 True True True True True True True True
10 9 False False False False False False False False
11 10 True True True True True True True True
12 11 True True True True True True True True
13 12 True True True True True True True True
14 13 True True True True True True True True
15 14 True True True True True True True True
16 15 False False False False False False False False
17 16 False False False False False False False False

View File

@@ -0,0 +1,164 @@
{
"lab": "Lab066",
"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": 20,
"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"
},
"geometry_unchanged": true,
"clock_path": "10 MHz TCXO -> MASTER -> MASTER CLKOUT -> SLAVE CLKIN"
},
"tx_sha256": "621b0bb8ec72bba4f522c79a5c77669c850740ea481270c4dcc2a7317819f199",
"stop_before_tx": true,
"process_before": {
"state": "CLEAR",
"attempts": [
{
"state": "CLEAR",
"mechanism": "psutil",
"matches": [],
"inspected": 294,
"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": 132.11270002648234,
"elapsed_ms": 132.11270002648234,
"polls": [
{
"query": 1,
"start_ms": 50.09360006079078,
"end_ms": 66.02549995295703,
"raw": "CLKIN status: no clock signal detected",
"state": "NOT_DETECTED",
"exit_code": 0
},
{
"query": 2,
"start_ms": 116.46570009179413,
"end_ms": 132.11270002648234,
"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 -35.2 dBfs, 11712 bytes free in buffer, 0 overruns, longest 0 bytes\n\nExiting...\nTotal time: 0.11546 s\nhackrf_stop_rx() done\nTransfer statistics:\n1850016 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_counter_summary": {
"expected_kind": "overrun",
"observations": [
{
"count": 0,
"role": "UNKNOWN",
"kind": "overrun",
"text": "0 overruns",
"character_offset": 334
},
{
"count": 0,
"role": "UNKNOWN",
"kind": "overrun",
"text": "0 overruns",
"character_offset": 503
}
],
"selected_counts": [
0,
0
],
"final_count": 0,
"maximum_count": 0,
"unparsed_loss_text": false,
"clean": true
},
"rx_level": {
"samples": 800000,
"mean_i": 0.94753,
"mean_q": 1.83594125,
"power_code2": 4.95347625,
"ac_power_code2": 0.6849828756484371,
"rms_code": 2.2256406381084974,
"peak_magnitude_code": 5.385164807134504,
"max_abs_i": 4.0,
"max_abs_q": 5.0,
"papr_db": 7.674879128392212,
"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.8276369225985735,
"unexpected_strong_signal": false
},
"rx_sha256": "ff817582e6a8fc03b489bf89f46322a9db8a9493335a4360a1b0f9bedad7c4d5",
"rx_overruns": 0,
"process_after": {
"state": "CLEAR",
"attempts": [
{
"state": "CLEAR",
"mechanism": "psutil",
"matches": [],
"inspected": 294,
"errors": []
}
]
},
"cleanup_errors": []
}

View File

@@ -0,0 +1,217 @@
{
"lab": "Lab066",
"stage": "PREPARATION_ONLY",
"rf_tx_count": 0,
"head": "dacf04f2d0a15c0c1b56a7fb527db61fc2e38462",
"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": 20,
"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"
},
"geometry_unchanged": true,
"clock_path": "10 MHz TCXO -> MASTER -> MASTER CLKOUT -> SLAVE CLKIN"
},
"packet_samples": 92256,
"inter_packet_gap_seconds": 0.005,
"inter_packet_gap_samples": 40000,
"number_of_gaps": 15,
"zero_guard_before_samples": 800000,
"zero_guard_after_samples": 800000,
"base_tx_samples": 2076096,
"expected_tx_samples": 3676096,
"tx_samples": 3676096,
"tx_bytes": 7352192,
"tx_duration_seconds": 0.459512,
"tx_sha256": "621b0bb8ec72bba4f522c79a5c77669c850740ea481270c4dcc2a7317819f199",
"layout": [
{
"packet_index": 1,
"start_sample": 800000,
"samples": 92256,
"payload_sha256": "414dee46d0555da2caabe0d4e446eb52d22be2f380ed4529fd7f192420d06465",
"following_gap_start_sample": 892256,
"following_gap_samples": 40000
},
{
"packet_index": 2,
"start_sample": 932256,
"samples": 92256,
"payload_sha256": "f5304773cc626858c4ad1cfe509bfe40418cfee6d9c65718c7c271c664465f2d",
"following_gap_start_sample": 1024512,
"following_gap_samples": 40000
},
{
"packet_index": 3,
"start_sample": 1064512,
"samples": 92256,
"payload_sha256": "e5604108e4a0a0441b962cf65822838d817a228d5df899dfd64dab958f32a59c",
"following_gap_start_sample": 1156768,
"following_gap_samples": 40000
},
{
"packet_index": 4,
"start_sample": 1196768,
"samples": 92256,
"payload_sha256": "43a7b5866bcf826920aae650830abfd2fa75b6e5460078c1bbde3ec7b461703f",
"following_gap_start_sample": 1289024,
"following_gap_samples": 40000
},
{
"packet_index": 5,
"start_sample": 1329024,
"samples": 92256,
"payload_sha256": "46836a613f080ddeba9c1b23bc8d8dd981db24975115074fa01b31699099efb8",
"following_gap_start_sample": 1421280,
"following_gap_samples": 40000
},
{
"packet_index": 6,
"start_sample": 1461280,
"samples": 92256,
"payload_sha256": "6ff8c9b941d37c39834f8cc5be3fdf563b3e55c51fc0c7d7344d4e046a15d13c",
"following_gap_start_sample": 1553536,
"following_gap_samples": 40000
},
{
"packet_index": 7,
"start_sample": 1593536,
"samples": 92256,
"payload_sha256": "273543a396d7499a1e1b325415b38555c72bb2665a227cd88aff5b797e18875d",
"following_gap_start_sample": 1685792,
"following_gap_samples": 40000
},
{
"packet_index": 8,
"start_sample": 1725792,
"samples": 92256,
"payload_sha256": "20051b24e17274d3b0cf3c170ea09559409c18ff429a3020ff28cb83d9b7bb3b",
"following_gap_start_sample": 1818048,
"following_gap_samples": 40000
},
{
"packet_index": 9,
"start_sample": 1858048,
"samples": 92256,
"payload_sha256": "9239a9fdb3fc94354cc8372dfa06867a18500ad13f867ea3a4cd4468e76bb9f0",
"following_gap_start_sample": 1950304,
"following_gap_samples": 40000
},
{
"packet_index": 10,
"start_sample": 1990304,
"samples": 92256,
"payload_sha256": "997be4e3b7a26708a48afd8ac0932460dbb34d67a359f1770dd76214f810e658",
"following_gap_start_sample": 2082560,
"following_gap_samples": 40000
},
{
"packet_index": 11,
"start_sample": 2122560,
"samples": 92256,
"payload_sha256": "fb1d718a4c2fa358f24ba34197b3100384b0df4229efae846944949894526903",
"following_gap_start_sample": 2214816,
"following_gap_samples": 40000
},
{
"packet_index": 12,
"start_sample": 2254816,
"samples": 92256,
"payload_sha256": "778de948620ddcf8c70f639f72ed042f97915234a57055421af9cbeb16c47e1f",
"following_gap_start_sample": 2347072,
"following_gap_samples": 40000
},
{
"packet_index": 13,
"start_sample": 2387072,
"samples": 92256,
"payload_sha256": "cabcd273073d70d686900e180be3c59e970cb3633bb370ba2198e64540727190",
"following_gap_start_sample": 2479328,
"following_gap_samples": 40000
},
{
"packet_index": 14,
"start_sample": 2519328,
"samples": 92256,
"payload_sha256": "3d6fda80bdd274d781399b1d1831cb4cb4a7c17bcd69930330564bf0ed6eb125",
"following_gap_start_sample": 2611584,
"following_gap_samples": 40000
},
{
"packet_index": 15,
"start_sample": 2651584,
"samples": 92256,
"payload_sha256": "ece74586bedf93fc3f5e07f41f64eea4383425d43424fe209ca30d911279c6ff",
"following_gap_start_sample": 2743840,
"following_gap_samples": 40000
},
{
"packet_index": 16,
"start_sample": 2783840,
"samples": 92256,
"payload_sha256": "bba4133aea193e327938abc18d2a0215f1df0e821d378d530dc0b47498af9a5c"
}
],
"rx_samples": 16000000,
"rx_duration_seconds": 2,
"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\\lab066\\tx_stream.cs8",
"-f",
"1400000000",
"-s",
"8000000",
"-n",
"3676096",
"-x",
"20",
"-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\\lab066\\rx_stream.cs8",
"-f",
"1400000000",
"-s",
"8000000",
"-n",
"16000000",
"-l",
"24",
"-g",
"24",
"-a",
"0",
"-p",
"0",
"-B"
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,382 @@
{
"lab": "Lab066",
"capture_valid": true,
"legacy": {
"candidates": 145,
"header_pass": 10,
"fec_started": 10,
"fec_pass": 10,
"crc_started": 10,
"crc_pass": 10,
"packet_output": 10,
"exact_payload_match": 10,
"packet_indexes": [
4,
5,
6,
7,
8,
10,
11,
12,
13,
14
]
},
"t2": {
"candidates": 145,
"header_pass": 10,
"fec_started": 10,
"fec_pass": 10,
"crc_started": 10,
"crc_pass": 10,
"packet_output": 10,
"exact_payload_match": 10,
"packet_indexes": [
4,
5,
6,
7,
8,
10,
11,
12,
13,
14
]
},
"passed_by_both": [
4,
5,
6,
7,
8,
10,
11,
12,
13,
14
],
"passed_only_by_legacy": [],
"passed_only_by_t2": [],
"failed_by_both": [
1,
2,
3,
9,
15,
16
],
"packet_rows": [
{
"packet_index": 1,
"legacy": {
"candidate_at_expected_packet": true,
"header": false,
"fec": false,
"crc": false,
"packet_output": false
},
"t2": {
"candidate_at_expected_packet": true,
"header": false,
"fec": false,
"crc": false,
"packet_output": false
}
},
{
"packet_index": 2,
"legacy": {
"candidate_at_expected_packet": true,
"header": false,
"fec": false,
"crc": false,
"packet_output": false
},
"t2": {
"candidate_at_expected_packet": true,
"header": false,
"fec": false,
"crc": false,
"packet_output": false
}
},
{
"packet_index": 3,
"legacy": {
"candidate_at_expected_packet": true,
"header": false,
"fec": false,
"crc": false,
"packet_output": false
},
"t2": {
"candidate_at_expected_packet": true,
"header": false,
"fec": false,
"crc": false,
"packet_output": false
}
},
{
"packet_index": 4,
"legacy": {
"candidate_at_expected_packet": true,
"header": true,
"fec": true,
"crc": true,
"packet_output": true
},
"t2": {
"candidate_at_expected_packet": true,
"header": true,
"fec": true,
"crc": true,
"packet_output": true
}
},
{
"packet_index": 5,
"legacy": {
"candidate_at_expected_packet": true,
"header": true,
"fec": true,
"crc": true,
"packet_output": true
},
"t2": {
"candidate_at_expected_packet": true,
"header": true,
"fec": true,
"crc": true,
"packet_output": true
}
},
{
"packet_index": 6,
"legacy": {
"candidate_at_expected_packet": true,
"header": true,
"fec": true,
"crc": true,
"packet_output": true
},
"t2": {
"candidate_at_expected_packet": true,
"header": true,
"fec": true,
"crc": true,
"packet_output": true
}
},
{
"packet_index": 7,
"legacy": {
"candidate_at_expected_packet": true,
"header": true,
"fec": true,
"crc": true,
"packet_output": true
},
"t2": {
"candidate_at_expected_packet": true,
"header": true,
"fec": true,
"crc": true,
"packet_output": true
}
},
{
"packet_index": 8,
"legacy": {
"candidate_at_expected_packet": true,
"header": true,
"fec": true,
"crc": true,
"packet_output": true
},
"t2": {
"candidate_at_expected_packet": true,
"header": true,
"fec": true,
"crc": true,
"packet_output": true
}
},
{
"packet_index": 9,
"legacy": {
"candidate_at_expected_packet": true,
"header": false,
"fec": false,
"crc": false,
"packet_output": false
},
"t2": {
"candidate_at_expected_packet": true,
"header": false,
"fec": false,
"crc": false,
"packet_output": false
}
},
{
"packet_index": 10,
"legacy": {
"candidate_at_expected_packet": true,
"header": true,
"fec": true,
"crc": true,
"packet_output": true
},
"t2": {
"candidate_at_expected_packet": true,
"header": true,
"fec": true,
"crc": true,
"packet_output": true
}
},
{
"packet_index": 11,
"legacy": {
"candidate_at_expected_packet": true,
"header": true,
"fec": true,
"crc": true,
"packet_output": true
},
"t2": {
"candidate_at_expected_packet": true,
"header": true,
"fec": true,
"crc": true,
"packet_output": true
}
},
{
"packet_index": 12,
"legacy": {
"candidate_at_expected_packet": true,
"header": true,
"fec": true,
"crc": true,
"packet_output": true
},
"t2": {
"candidate_at_expected_packet": true,
"header": true,
"fec": true,
"crc": true,
"packet_output": true
}
},
{
"packet_index": 13,
"legacy": {
"candidate_at_expected_packet": true,
"header": true,
"fec": true,
"crc": true,
"packet_output": true
},
"t2": {
"candidate_at_expected_packet": true,
"header": true,
"fec": true,
"crc": true,
"packet_output": true
}
},
{
"packet_index": 14,
"legacy": {
"candidate_at_expected_packet": true,
"header": true,
"fec": true,
"crc": true,
"packet_output": true
},
"t2": {
"candidate_at_expected_packet": true,
"header": true,
"fec": true,
"crc": true,
"packet_output": true
}
},
{
"packet_index": 15,
"legacy": {
"candidate_at_expected_packet": true,
"header": false,
"fec": false,
"crc": false,
"packet_output": false
},
"t2": {
"candidate_at_expected_packet": true,
"header": false,
"fec": false,
"crc": false,
"packet_output": false
}
},
{
"packet_index": 16,
"legacy": {
"candidate_at_expected_packet": true,
"header": false,
"fec": false,
"crc": false,
"packet_output": false
},
"t2": {
"candidate_at_expected_packet": true,
"header": false,
"fec": false,
"crc": false,
"packet_output": false
}
}
],
"physical": {
"symbol_error_rate": 0.011046635434412266,
"rms_symbol_deviation": 0.5283737542111951,
"matched_component_power": 8.359397909690596,
"residual_power": 2.333261015636848,
"matched_residual_db": 5.542116733950265,
"band_gain_nonuniformity_p95_p05_db_median": 2.8700698316435833,
"band_gain_nonuniformity_p95_p05_db_min": 2.7249057267348604,
"band_gain_nonuniformity_p95_p05_db_max": 3.1116789732634516
},
"lab056_B": {
"symbol_error_rate": 0.03495896464646465,
"rms_symbol_deviation": 0.8453497751219741,
"matched_residual_db": 1.4648455366051558,
"band_gain_nonuniformity_db": 5.471381501203027
},
"difference_from_lab056_B": {
"symbol_error_rate_absolute": -0.023912329212052382,
"rms_symbol_deviation_absolute": -0.3169760209107789,
"matched_residual_db": 4.077271197345109,
"band_gain_nonuniformity_db": -2.6013116695594434
},
"lab062": {
"symbol_error_rate": 0.0992866269165247,
"rms_symbol_deviation": 1.2204009809742542,
"matched_residual_db": -1.7300509612417754,
"band_gain_nonuniformity_db": 6.660340655081576,
"capture_status": "RF_SESSION_INVALID_TX_UNDERRUN"
},
"difference_from_lab062": {
"symbol_error_rate_absolute": -0.08823999148211244,
"rms_symbol_deviation_absolute": -0.6920272267630591,
"matched_residual_db": 7.27216769519204,
"band_gain_nonuniformity_db": -3.790270823437993
},
"classification": "PARTIAL_PACKET_RECOVERY_NO_T2_GAIN",
"primary_result": "Аппаратно валидный Lab066 дал одинаковые 10/16 точных пакетов в legacy и T2; физическое качество лучше Lab056 B и невалидной Lab062.",
"recommended_next_single_step": "Отдельно разрешить офлайн-сравнение header-ошибок пакетов 1-3, 9, 15-16 с принятыми пакетами по сохранённому RX; новый TX не нужен."
}

View File

@@ -0,0 +1,507 @@
{
"lab": "Lab066",
"stage": "SOFTWARE_GATE",
"rf_tx_count": 0,
"tx_sha256": "621b0bb8ec72bba4f522c79a5c77669c850740ea481270c4dcc2a7317819f199",
"legacy": {
"mode": "legacy",
"candidates": 16,
"header_pass": 16,
"fec_started": 16,
"fec_pass": 16,
"crc_started": 16,
"crc_pass": 16,
"packet_output": 16,
"exact_payload_match": 16,
"packet_indexes": [
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16
],
"exact_ordered_payloads": true,
"t2_state_scope": "NOT_APPLICABLE",
"fallback_between_modes": false,
"passed": true,
"rows": [
{
"absolute_refined": 800000,
"reason": "CRC_PASS",
"header": true,
"fec_started": true,
"fec": true,
"crc_started": true,
"crc": true,
"packet_output": true,
"packet_index_after_verdict": 1,
"t2_tracking_runs_for_candidate": 0
},
{
"absolute_refined": 932256,
"reason": "CRC_PASS",
"header": true,
"fec_started": true,
"fec": true,
"crc_started": true,
"crc": true,
"packet_output": true,
"packet_index_after_verdict": 2,
"t2_tracking_runs_for_candidate": 0
},
{
"absolute_refined": 1064512,
"reason": "CRC_PASS",
"header": true,
"fec_started": true,
"fec": true,
"crc_started": true,
"crc": true,
"packet_output": true,
"packet_index_after_verdict": 3,
"t2_tracking_runs_for_candidate": 0
},
{
"absolute_refined": 1196768,
"reason": "CRC_PASS",
"header": true,
"fec_started": true,
"fec": true,
"crc_started": true,
"crc": true,
"packet_output": true,
"packet_index_after_verdict": 4,
"t2_tracking_runs_for_candidate": 0
},
{
"absolute_refined": 1329024,
"reason": "CRC_PASS",
"header": true,
"fec_started": true,
"fec": true,
"crc_started": true,
"crc": true,
"packet_output": true,
"packet_index_after_verdict": 5,
"t2_tracking_runs_for_candidate": 0
},
{
"absolute_refined": 1461280,
"reason": "CRC_PASS",
"header": true,
"fec_started": true,
"fec": true,
"crc_started": true,
"crc": true,
"packet_output": true,
"packet_index_after_verdict": 6,
"t2_tracking_runs_for_candidate": 0
},
{
"absolute_refined": 1593536,
"reason": "CRC_PASS",
"header": true,
"fec_started": true,
"fec": true,
"crc_started": true,
"crc": true,
"packet_output": true,
"packet_index_after_verdict": 7,
"t2_tracking_runs_for_candidate": 0
},
{
"absolute_refined": 1725792,
"reason": "CRC_PASS",
"header": true,
"fec_started": true,
"fec": true,
"crc_started": true,
"crc": true,
"packet_output": true,
"packet_index_after_verdict": 8,
"t2_tracking_runs_for_candidate": 0
},
{
"absolute_refined": 1858048,
"reason": "CRC_PASS",
"header": true,
"fec_started": true,
"fec": true,
"crc_started": true,
"crc": true,
"packet_output": true,
"packet_index_after_verdict": 9,
"t2_tracking_runs_for_candidate": 0
},
{
"absolute_refined": 1990304,
"reason": "CRC_PASS",
"header": true,
"fec_started": true,
"fec": true,
"crc_started": true,
"crc": true,
"packet_output": true,
"packet_index_after_verdict": 10,
"t2_tracking_runs_for_candidate": 0
},
{
"absolute_refined": 2122560,
"reason": "CRC_PASS",
"header": true,
"fec_started": true,
"fec": true,
"crc_started": true,
"crc": true,
"packet_output": true,
"packet_index_after_verdict": 11,
"t2_tracking_runs_for_candidate": 0
},
{
"absolute_refined": 2254816,
"reason": "CRC_PASS",
"header": true,
"fec_started": true,
"fec": true,
"crc_started": true,
"crc": true,
"packet_output": true,
"packet_index_after_verdict": 12,
"t2_tracking_runs_for_candidate": 0
},
{
"absolute_refined": 2387072,
"reason": "CRC_PASS",
"header": true,
"fec_started": true,
"fec": true,
"crc_started": true,
"crc": true,
"packet_output": true,
"packet_index_after_verdict": 13,
"t2_tracking_runs_for_candidate": 0
},
{
"absolute_refined": 2519328,
"reason": "CRC_PASS",
"header": true,
"fec_started": true,
"fec": true,
"crc_started": true,
"crc": true,
"packet_output": true,
"packet_index_after_verdict": 14,
"t2_tracking_runs_for_candidate": 0
},
{
"absolute_refined": 2651584,
"reason": "CRC_PASS",
"header": true,
"fec_started": true,
"fec": true,
"crc_started": true,
"crc": true,
"packet_output": true,
"packet_index_after_verdict": 15,
"t2_tracking_runs_for_candidate": 0
},
{
"absolute_refined": 2783840,
"reason": "CRC_PASS",
"header": true,
"fec_started": true,
"fec": true,
"crc_started": true,
"crc": true,
"packet_output": true,
"packet_index_after_verdict": 16,
"t2_tracking_runs_for_candidate": 0
}
],
"detector_statistics": {
"total_local_peaks": 80004,
"first_stage_candidates": 21193,
"cheap_stage_checked": 21193,
"cheap_stage_rejected": 20800,
"clusters": 37,
"representative_checks": 16,
"inside_accepted_frame": 377
},
"candidate_overflow": 0
},
"t2": {
"mode": "t2",
"candidates": 16,
"header_pass": 16,
"fec_started": 16,
"fec_pass": 16,
"crc_started": 16,
"crc_pass": 16,
"packet_output": 16,
"exact_payload_match": 16,
"packet_indexes": [
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16
],
"exact_ordered_payloads": true,
"t2_state_scope": "NEW_PREAMBLE_ESTIMATE_PER_PACKET",
"fallback_between_modes": false,
"passed": true,
"rows": [
{
"absolute_refined": 800000,
"reason": "CRC_PASS",
"header": true,
"fec_started": true,
"fec": true,
"crc_started": true,
"crc": true,
"packet_output": true,
"packet_index_after_verdict": 1,
"t2_tracking_runs_for_candidate": 2
},
{
"absolute_refined": 932256,
"reason": "CRC_PASS",
"header": true,
"fec_started": true,
"fec": true,
"crc_started": true,
"crc": true,
"packet_output": true,
"packet_index_after_verdict": 2,
"t2_tracking_runs_for_candidate": 2
},
{
"absolute_refined": 1064512,
"reason": "CRC_PASS",
"header": true,
"fec_started": true,
"fec": true,
"crc_started": true,
"crc": true,
"packet_output": true,
"packet_index_after_verdict": 3,
"t2_tracking_runs_for_candidate": 2
},
{
"absolute_refined": 1196768,
"reason": "CRC_PASS",
"header": true,
"fec_started": true,
"fec": true,
"crc_started": true,
"crc": true,
"packet_output": true,
"packet_index_after_verdict": 4,
"t2_tracking_runs_for_candidate": 2
},
{
"absolute_refined": 1329024,
"reason": "CRC_PASS",
"header": true,
"fec_started": true,
"fec": true,
"crc_started": true,
"crc": true,
"packet_output": true,
"packet_index_after_verdict": 5,
"t2_tracking_runs_for_candidate": 2
},
{
"absolute_refined": 1461280,
"reason": "CRC_PASS",
"header": true,
"fec_started": true,
"fec": true,
"crc_started": true,
"crc": true,
"packet_output": true,
"packet_index_after_verdict": 6,
"t2_tracking_runs_for_candidate": 2
},
{
"absolute_refined": 1593536,
"reason": "CRC_PASS",
"header": true,
"fec_started": true,
"fec": true,
"crc_started": true,
"crc": true,
"packet_output": true,
"packet_index_after_verdict": 7,
"t2_tracking_runs_for_candidate": 2
},
{
"absolute_refined": 1725792,
"reason": "CRC_PASS",
"header": true,
"fec_started": true,
"fec": true,
"crc_started": true,
"crc": true,
"packet_output": true,
"packet_index_after_verdict": 8,
"t2_tracking_runs_for_candidate": 2
},
{
"absolute_refined": 1858048,
"reason": "CRC_PASS",
"header": true,
"fec_started": true,
"fec": true,
"crc_started": true,
"crc": true,
"packet_output": true,
"packet_index_after_verdict": 9,
"t2_tracking_runs_for_candidate": 2
},
{
"absolute_refined": 1990304,
"reason": "CRC_PASS",
"header": true,
"fec_started": true,
"fec": true,
"crc_started": true,
"crc": true,
"packet_output": true,
"packet_index_after_verdict": 10,
"t2_tracking_runs_for_candidate": 2
},
{
"absolute_refined": 2122560,
"reason": "CRC_PASS",
"header": true,
"fec_started": true,
"fec": true,
"crc_started": true,
"crc": true,
"packet_output": true,
"packet_index_after_verdict": 11,
"t2_tracking_runs_for_candidate": 2
},
{
"absolute_refined": 2254816,
"reason": "CRC_PASS",
"header": true,
"fec_started": true,
"fec": true,
"crc_started": true,
"crc": true,
"packet_output": true,
"packet_index_after_verdict": 12,
"t2_tracking_runs_for_candidate": 2
},
{
"absolute_refined": 2387072,
"reason": "CRC_PASS",
"header": true,
"fec_started": true,
"fec": true,
"crc_started": true,
"crc": true,
"packet_output": true,
"packet_index_after_verdict": 13,
"t2_tracking_runs_for_candidate": 2
},
{
"absolute_refined": 2519328,
"reason": "CRC_PASS",
"header": true,
"fec_started": true,
"fec": true,
"crc_started": true,
"crc": true,
"packet_output": true,
"packet_index_after_verdict": 14,
"t2_tracking_runs_for_candidate": 2
},
{
"absolute_refined": 2651584,
"reason": "CRC_PASS",
"header": true,
"fec_started": true,
"fec": true,
"crc_started": true,
"crc": true,
"packet_output": true,
"packet_index_after_verdict": 15,
"t2_tracking_runs_for_candidate": 2
},
{
"absolute_refined": 2783840,
"reason": "CRC_PASS",
"header": true,
"fec_started": true,
"fec": true,
"crc_started": true,
"crc": true,
"packet_output": true,
"packet_index_after_verdict": 16,
"t2_tracking_runs_for_candidate": 2
}
],
"detector_statistics": {
"total_local_peaks": 80004,
"first_stage_candidates": 21193,
"cheap_stage_checked": 21193,
"cheap_stage_rejected": 20800,
"clusters": 37,
"representative_checks": 16,
"inside_accepted_frame": 377
},
"candidate_overflow": 0,
"t2_state_reset_evidence": {
"estimate_source": "each candidate's own preamble",
"context_scope": "new PacketMeasure context for every candidate",
"tracking_runs_per_accepted_packet": [
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2
],
"all_accepted_packets_have_fresh_tracking_runs": true,
"persistent_tracking_state_in_packet_processor": false
}
},
"gaps_verified_zero": true,
"t2_state_reset_verified_by_fresh_preamble_per_packet": true,
"passed": true
}

View File

@@ -700,3 +700,44 @@
- **Независимая проверка:** прямой пересчёт CS8 подтвердил SHA, счётчики, - **Независимая проверка:** прямой пересчёт CS8 подтвердил SHA, счётчики,
локализацию, фон, matched/residual, `h`, CFO и спектральные полосы. Расхождений локализацию, фон, matched/residual, `h`, CFO и спектральные полосы. Расхождений
нет. Quick gate1000/38/13 PASS, pytest1000 passed, diff check PASS. нет. Quick gate1000/38/13 PASS, pytest1000 passed, diff check PASS.
- **Фиксация:** Lab065 зафиксирована локальным коммитом
`dacf04f2d0a15c0c1b56a7fb527db61fc2e38462`; push не выполнялся.
## Lab066 — исправленный повтор полного 500K PHY-сеанса
- **Основание:** Lab062 остаётся `RF_SESSION_INVALID_TX_UNDERRUN`, legacy/T2
0/16. Lab066 исправляет отсутствовавшие межпакетные паузы и использует
максимальный наблюдавшийся счётчик потерь.
- **TX:** 16 полных PHY-пакетов по256 байт, packet waveform92256 отсчётов;
15 нулевых пауз по40000 отсчётов/5мс и guards по800000 отсчётов/100мс.
Всего3676096 отсчётов,0,459512с; SHA-256
`621b0bb8ec72bba4f522c79a5c77669c850740ea481270c4dcc2a7317819f199`.
- **Параметры:** 1400МГц, Fs8Мвыб/с,500ксимв/с, несущая+50кГц,
TX VGA20дБ, RX LNA/VGA24/24дБ, amplifier/Bias-Tee OFF; прежняя геометрия.
- **Software gate:** legacy и T2 независимо дали16/16 HEADER/FEC/CRC,
packet output и точных упорядоченных payload. Fallback отсутствует; T2
инициализируется по собственной преамбуле каждого пакета.
- **RX-only preflight:** MASTER/SLAVE reg29=0x26, CLKIN DETECTED через132,113мс;
AC RMS0,827637, max|I|/|Q|=4/5, clipping0, maximum overruns0. Cleanup PASS,
процессы CLEAR.
- **Проверки:** quick gate1013/39/13 PASS, pytest1013 passed, diff check PASS;
verifier и adversary не оставили открытых замечаний.
- [Состояние](lab066_current_state.md), [RX-only отчёт](lab066_rx_preflight.md),
[подготовка](../data/processed/lab066/preparation.json),
[software gate](../data/processed/lab066/software_gate.json),
[preflight](../data/processed/lab066/preflight_result.json).
- **STOP:** RF TX=0; требуется отдельное прямое разрешение на один TX. Lab066
не коммитировалась, push не выполнялся, Lab067 не создавалась.
- **Единственный RF-сеанс:** один TX, повторов0; maximum underrun/overrun0/0,
clipping0, cleanup PASS, процессы CLEAR. RX16000000 отсчётов, SHA-256
`72ff7ddd8b30baa4a5c066361ca1f8dfa664f1da37f65c0c373b211ce6cf00f0`.
- **Строгий результат:** legacy и T2 совпали — по10/16 точных пакетов;
приняты4–8 и10–14, отказали на header пакеты1–3,9,15,16. Выигрыша T2 нет.
- **Физическое качество:** SER0,011047, RMS0,528374, matched/residual
+5,542117дБ, медианная неравномерность2,870070дБ. Все четыре показателя лучше
Lab056 B и невалидной Lab062.
- **Итог:** `PARTIAL_PACKET_RECOVERY_NO_T2_GAIN`. [RF-отчёт](lab066_rf_result.md),
[RF summary](../data/processed/lab066/rf_summary.json),
[packet CSV](../data/processed/lab066/packet_comparison.csv).
- **После сеанса:** RF TX=0, второй TX не выполнялся. Lab066 не
коммитировалась, push не выполнялся, Lab067 не создавалась.

View File

@@ -0,0 +1,92 @@
# Lab066 — исправленный повтор полного 500K PHY-сеанса
## Цель и границы
Lab066 повторяет исходную цель Lab062 после двух установленных дефектов того
сеанса: пакеты Lab062 шли вплотную, без заявленных пауз 5 мс, а в TX-журнале
был промежуточный underrun. Исторический результат Lab062 сохраняется без
изменений: `RF_SESSION_INVALID_TX_UNDERRUN`, legacy 0/16, T2 0/16.
На этапе подготовки Lab066 была ограничена программным декодированием,
common-clock и RX-only preflight; передача тогда не выполнялась. После этого
пользователь отдельно разрешил один TX. Автоматический повтор оставался
запрещён и не выполнялся.
## Радиопараметры
- RF: 1400000000 Гц;
- Fs: 8000000 комплексных отсчётов/с;
- символьная скорость: 500000 символов/с, 16 отсчётов/символ;
- цифровая несущая: +50000 Гц;
- TX VGA: 20 дБ;
- RX LNA/VGA: 24/24 дБ;
- RF amplifier и Bias-Tee: OFF;
- 16 штатных PHY-пакетов с PRBS7-преамбулой, заголовком, FEC и CRC;
- payload: 256 байт, детерминированный и различимый для каждого пакета;
- геометрия: около 1,2 м, LOS в помещении, прежние антенны и поляризация;
MASTER TX возле окна, SLAVE RX на столе.
## Итоговый TX-файл
Каждый packet waveform занимает 92256 отсчётов. Между соседними пакетами
записаны 15 нулевых пауз по 40000 отсчётов, то есть ровно по 5 мс. Перед первым
и после последнего пакета добавлены нулевые guards по 800000 отсчётов, или по
100 мс.
Расчёт длины:
```text
16 × 92256 + 15 × 40000 = 2076096 отсчётов без внешних guards
2076096 + 2 × 800000 = 3676096 отсчётов всего
3676096 / 8000000 = 0,459512 с
```
Фактическая длина совпала с расчётом: 3676096 комплексных отсчётов,
7352192 байта. SHA-256:
`621b0bb8ec72bba4f522c79a5c77669c850740ea481270c4dcc2a7317819f199`.
Повторная сборка дала те же байты. Нулевые guards и каждая из 15 пауз проверены
по точным границам из сохранённой разметки.
## Программное декодирование
Весь конечный TX-файл дважды прошёл один и тот же строгий приёмный тракт. Режимы
legacy и T2 запускались отдельно, автоматического fallback между ними нет.
| Режим | Кандидаты | Header | FEC | CRC | Выход | Payload и порядок |
|---|---:|---:|---:|---:|---:|---:|
| legacy | 16 | 16/16 | 16/16 | 16/16 | 16/16 | 16/16 |
| T2 | 16 | 16/16 | 16/16 | 16/16 | 16/16 | 16/16 |
Позиции принятых пакетов равны ожидаемым позициям в разметке. Для каждого
кандидата T2 заново оценивает начальные параметры по собственной преамбуле.
Наблюдение дало по два локальных запуска слежения на пакет: для заголовка и
полного кадра. Контекст обработки создаётся заново, состояния между пакетами
не переносятся.
Аппаратный анализ применил legacy и T2 к одному сохранённому RX-файлу. До
строгого вердикта декодеру не передавались payload или ожидаемые биты. Известный
TX использован после вердикта только для физических диагностик и сравнения с
Lab056 B, Lab062 и Lab066.
## Проверки перед preflight
- `python tools/quick_gate.py`: PASS, 1013 проверок ядра, 39 функциональных,
13 аппаратных пропусков;
- `python -m pytest tests/ -q`: 1013 passed;
- `git diff --check`: PASS.
Независимый verifier пересчитал длину, SHA, все нулевые диапазоны, пакетные
срезы, программное декодирование и RX-only метрики; расхождений нет. Read-only
adversary проверил запрет TX, одноразовость, привязку capture к конфигурации,
счётчики, live-проверку фона и границу oracle-диагностики; открытых замечаний
нет.
## Текущее состояние
Common-clock и RX-only preflight прошли; измерения приведены в
[отдельном отчёте](lab066_rx_preflight.md). `RF TX=0`. Lab066 не
коммитировалась, push не выполнялся, Lab067 не создавалась.
После отдельного разрешения выполнен один аппаратно валидный RF-сеанс без
повторов. Legacy и T2 дали одинаковые 10/16 точных пакетов. Полные результаты
и физическое сравнение приведены в [RF-отчёте](lab066_rf_result.md).

118
docs/lab066_rf_result.md Normal file
View File

@@ -0,0 +1,118 @@
# Lab066 — один OTA RF-сеанс полного 500K PHY-потока
## Аппаратная валидность
15 сентября 2026 года выполнен один разрешённый TX всего подготовленного
потока. Передача запускалась один раз; автоматических повторов не было. RX
стартовал раньше TX и сохранил 16000000 комплексных отсчётов за 2,000000 с.
- TX SHA-256:
`621b0bb8ec72bba4f522c79a5c77669c850740ea481270c4dcc2a7317819f199`;
- RX SHA-256:
`72ff7ddd8b30baa4a5c066361ca1f8dfa664f1da37f65c0c373b211ce6cf00f0`;
- TX underrun observations: 0, 0; maximum 0;
- RX overrun observations: 0, 0, 0, 0; maximum 0;
- clipping: нет;
- cleanup errors: нет;
- SDR-процессы до и после: `CLEAR`.
Непосредственно перед TX обе платы имели reg29=`0x26`; SLAVE CLKIN обнаружен
на втором опросе через 131,291 мс. Live RX-проверка дала AC RMS 1,173567 кода,
max |I|/|Q|=4/5 и не обнаружила clipping или сильный фон. После сеанса оба
CLKOUT отключены.
## Строгое декодирование одной RX-записи
Legacy и T2 получили одну и ту же непрерывную IQ-запись. Поиск кандидатов,
временная синхронизация, RRC, пороги, PHY header, FEC, CRC и критерии принятия
не менялись. Автоматического fallback между режимами нет.
| Метрика | legacy | T2 |
|---|---:|---:|
| Кандидаты | 145 | 145 |
| Header PASS | 10 | 10 |
| FEC started | 10 | 10 |
| FEC PASS | 10 | 10 |
| CRC started | 10 | 10 |
| CRC PASS | 10 | 10 |
| Packet output | 10 | 10 |
| Exact payload match | 10 | 10 |
Оба режима приняли пакеты 4, 5, 6, 7, 8, 10, 11, 12, 13 и 14. Пакеты 1, 2,
3, 9, 15 и 16 не прошли PHY header у обоих режимов. Выигрыша одного режима
над другим нет.
| Пакет | Legacy H/F/C/O | T2 H/F/C/O |
|---:|:---:|:---:|
| 1 | 0/0/0/0 | 0/0/0/0 |
| 2 | 0/0/0/0 | 0/0/0/0 |
| 3 | 0/0/0/0 | 0/0/0/0 |
| 4 | 1/1/1/1 | 1/1/1/1 |
| 5 | 1/1/1/1 | 1/1/1/1 |
| 6 | 1/1/1/1 | 1/1/1/1 |
| 7 | 1/1/1/1 | 1/1/1/1 |
| 8 | 1/1/1/1 | 1/1/1/1 |
| 9 | 0/0/0/0 | 0/0/0/0 |
| 10 | 1/1/1/1 | 1/1/1/1 |
| 11 | 1/1/1/1 | 1/1/1/1 |
| 12 | 1/1/1/1 | 1/1/1/1 |
| 13 | 1/1/1/1 | 1/1/1/1 |
| 14 | 1/1/1/1 | 1/1/1/1 |
| 15 | 0/0/0/0 | 0/0/0/0 |
| 16 | 0/0/0/0 | 0/0/0/0 |
H/F/C/O означает header/FEC/CRC/output. Для каждого из 16 ожидаемых пакетов
детектор имел кандидат точно на диагностической границе пакета. У шести
непринятых пакетов FEC и CRC не запускались из-за отказа PHY header.
## Физическая диагностика после строгого результата
Сопоставимые показатели рассчитаны отдельно по всем 16 пакетам методикой
Lab058. Известный TX использован после завершения строгих прогонов и не влиял
на принятие пакетов.
| Показатель | Lab066 |
|---|---:|
| Доля ошибочных символов | 0,0110466, или 1,104664% |
| Нормированное RMS-отклонение символов | 0,528374 |
| Согласованная мощность | 8,359398 код² |
| Остаточная мощность | 2,333261 код² |
| Matched/residual | +5,542117 дБ |
| Неравномерность усиления p95–p05 | медиана 2,870070 дБ |
| Диапазон неравномерности по пакетам | 2,724906…3,111679 дБ |
Остаток включает шум и немоделированные искажения; это не калиброванная
мощность шума. Физические показатели являются диагностическими и не превращают
непринятый пакет в успешный.
## Сравнение
| Показатель | Lab056 B | Lab062 | Lab066 |
|---|---:|---:|---:|
| Доля ошибочных символов | 0,034959 | 0,099287 | 0,011047 |
| RMS-отклонение | 0,845350 | 1,220401 | 0,528374 |
| Matched/residual | +1,464846 дБ | −1,730051 дБ | +5,542117 дБ |
| Неравномерность полосы | 5,471382 дБ | 6,660341 дБ | 2,870070 дБ |
Относительно Lab056 B доля ошибочных символов ниже на 0,0239123, или на
68,40%; RMS-отклонение ниже на 0,316976, matched/residual выше на 4,077271 дБ,
а неравномерность меньше на 2,601312 дБ.
Относительно Lab062 доля ошибочных символов ниже на 0,0882400, или на 88,87%;
RMS-отклонение ниже на 0,692027, matched/residual выше на 7,272168 дБ, а
неравномерность меньше на 3,790271 дБ. Сравнение с Lab062 контекстное: тот
сеанс остаётся аппаратно невалидным из-за TX underrun.
## Итог
Классификация: `PARTIAL_PACKET_RECOVERY_NO_T2_GAIN`. Аппаратно чистая запись
дала одинаковые 10/16 точных пакетов в legacy и T2. Физическое качество заметно
лучше Lab056 B и невалидной Lab062, однако шесть пакетов остановились на PHY
header, поэтому результат не является полным 16/16 успехом.
Следующий отдельный шаг, если он будет разрешён: только офлайн сравнить ошибки
PHY header, timing и CFO пакетов 1–3, 9, 15–16 с принятыми пакетами по уже
сохранённому RX. Новый TX для этого не нужен.
Сейчас `RF TX=0`, процессы `CLEAR`. Lab066 не коммитировалась, push не
выполнялся, Lab067 не создавалась.

View File

@@ -0,0 +1,35 @@
# Lab066 — common-clock и RX-only preflight
## Результат
15 сентября 2026 года выполнен один preflight без передачи. До и после него
SDR-процессы имели состояние `CLEAR`; обе HackRF One r9 и назначение ролей
подтверждены.
Общая опора прошла проверку:
- MASTER reg29 = `0x26`;
- SLAVE reg29 = `0x26`;
- SLAVE CLKIN = `DETECTED`;
- обнаружение произошло на втором опросе через 132,113 мс.
RX-only команда записала 800000 комплексных отсчётов на частоте 1400 МГц с
RX LNA/VGA 24/24 дБ. Максимум всех наблюдавшихся счётчиков overrun равен нулю.
После проверки оба CLKOUT отключены; ошибок очистки нет.
## Уровень фона
- полный RMS: 2,225641 кода;
- AC RMS: 0,827637 кода;
- средние I/Q: 0,947530 / 1,835941 кода;
- max |I| / |Q|: 4 / 5;
- доля clipped components и samples: 0 / 0;
- неожиданный сильный сигнал: нет;
- SHA-256 RX-only записи:
`ff817582e6a8fc03b489bf89f46322a9db8a9493335a4360a1b0f9bedad7c4d5`.
## Граница разрешения
Preflight не запускал TX и не расходует право на будущий единственный сеанс.
Сейчас `RF TX=0`. Для одного Lab066 TX требуется отдельное прямое разрешение.
Автоматические повторы, изменение усилений и переход к Lab067 запрещены.

View File

@@ -0,0 +1,786 @@
"""Lab066: исправленный повтор полного 500K PHY-сеанса.
Подготовка, программная проверка и preflight не передают RF. Единственный
аппаратный сеанс доступен только отдельной командой с явным разрешением.
"""
from __future__ import annotations
from dataclasses import asdict
import argparse
import csv
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 scipy import signal
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_capture_runner as hardware
from experiments import lab058_abab_symbol_quality as lab058
from experiments import lab064_rf_level_linearity as lab064
ROOT = Path(__file__).resolve().parents[1]
RAW = ROOT / "data/raw/lab066"
OUTPUT = ROOT / "data/processed/lab066"
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
PACKET_SAMPLES = 92_256
INTER_PACKET_GAP_SECONDS = 0.005
INTER_PACKET_GAP_SAMPLES = 40_000
NUMBER_OF_GAPS = PACKETS - 1
GUARD_SECONDS = 0.1
GUARD_SAMPLES = 800_000
BASE_TX_SAMPLES = PACKETS * PACKET_SAMPLES + NUMBER_OF_GAPS * INTER_PACKET_GAP_SAMPLES
EXPECTED_TX_SAMPLES = BASE_TX_SAMPLES + 2 * GUARD_SAMPLES
PREFLIGHT_SAMPLES = 800_000
RX_SECONDS = 2
RX_SAMPLES = RX_SECONDS * MODE.sample_rate_hz
RX_LEAD_SECONDS = 0.5
TX_VGA_DB = 20
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, allow_nan=False)
stream.write("\n")
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()
payloads = [frame.plan.payload for frame in result]
if len(result) != PACKETS or any(len(payload) != PAYLOAD_BYTES for payload in payloads):
raise RuntimeError("Штатный генератор не дал 16 payload по 256 байт")
if len(set(payloads)) != PACKETS:
raise RuntimeError("Payload пакетов должны однозначно различаться")
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": TX_VGA_DB,
"rx_lna_db": 24,
"rx_vga_db": 24,
"rf_amplifier": False,
"bias_tee": False,
"geometry": asdict(POINT),
"geometry_unchanged": True,
"clock_path": "10 MHz TCXO -> MASTER -> MASTER CLKOUT -> SLAVE CLKIN",
}
def transfer_command(path: Path | str, samples: int, *, transmit: bool = False) -> list[str]:
command = hardware.command(POINT, path, samples, transmit=transmit)
if transmit:
command[command.index("-x") + 1] = str(TX_VGA_DB)
return command
def build_waveform() -> tuple[bytes, list[dict]]:
"""Собрать финальный поток с нулевыми guards и паузами."""
blocks: list[bytes] = [bytes(2 * GUARD_SAMPLES)]
layout: list[dict] = []
cursor = GUARD_SAMPLES
for index, frame in enumerate(plans(), start=1):
packet = phy.build_parameterized_tx_cs8(frame.plan, MODE)
if len(packet) != 2 * PACKET_SAMPLES:
raise RuntimeError(f"Пакет {index}: ожидалось {PACKET_SAMPLES} отсчётов")
layout.append({
"packet_index": index,
"start_sample": cursor,
"samples": PACKET_SAMPLES,
"payload_sha256": hashlib.sha256(frame.plan.payload).hexdigest(),
})
blocks.append(packet)
cursor += PACKET_SAMPLES
if index != PACKETS:
blocks.append(bytes(2 * INTER_PACKET_GAP_SAMPLES))
layout[-1]["following_gap_start_sample"] = cursor
layout[-1]["following_gap_samples"] = INTER_PACKET_GAP_SAMPLES
cursor += INTER_PACKET_GAP_SAMPLES
blocks.append(bytes(2 * GUARD_SAMPLES))
waveform = b"".join(blocks)
if len(waveform) != 2 * EXPECTED_TX_SAMPLES or cursor + GUARD_SAMPLES != EXPECTED_TX_SAMPLES:
raise RuntimeError("Неверная длина итогового TX-потока Lab066")
return waveform, layout
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("Lab066 TX уже существует; перезапись запрещена")
waveform, layout = build_waveform()
waveform_path.write_bytes(waveform)
result = {
"lab": "Lab066", "stage": "PREPARATION_ONLY", "rf_tx_count": 0,
"head": subprocess.check_output(["git", "rev-parse", "HEAD"], text=True).strip(),
"parameters": parameters(), "packet_samples": PACKET_SAMPLES,
"inter_packet_gap_seconds": INTER_PACKET_GAP_SECONDS,
"inter_packet_gap_samples": INTER_PACKET_GAP_SAMPLES,
"number_of_gaps": NUMBER_OF_GAPS,
"zero_guard_before_samples": GUARD_SAMPLES,
"zero_guard_after_samples": GUARD_SAMPLES,
"base_tx_samples": BASE_TX_SAMPLES,
"expected_tx_samples": EXPECTED_TX_SAMPLES,
"tx_samples": EXPECTED_TX_SAMPLES, "tx_bytes": len(waveform),
"tx_duration_seconds": EXPECTED_TX_SAMPLES / MODE.sample_rate_hz,
"tx_sha256": sha256(waveform_path), "layout": layout,
"rx_samples": RX_SAMPLES, "rx_duration_seconds": RX_SECONDS,
"automatic_retry": False,
"tx_command_prepared_not_run": transfer_command(waveform_path, EXPECTED_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_stream.cs8"
expected, layout = build_waveform()
expected_digest = hashlib.sha256(expected).hexdigest()
actual = path.read_bytes()
if (
metadata.get("lab") != "Lab066"
or metadata.get("stage") != "PREPARATION_ONLY"
or metadata.get("rf_tx_count") != 0
or metadata.get("parameters") != parameters()
or metadata.get("layout") != layout
or metadata.get("expected_tx_samples") != EXPECTED_TX_SAMPLES
or metadata.get("tx_samples") != EXPECTED_TX_SAMPLES
or len(actual) != 2 * EXPECTED_TX_SAMPLES
or metadata.get("tx_sha256") != expected_digest
or hashlib.sha256(actual).hexdigest() != expected_digest
or actual != expected
):
raise RuntimeError("Подготовленный Lab066 TX или его разметка изменились")
return metadata
def decode(mode: str, source_path: Path | None = None, *, diagnostics: bool = False) -> dict:
"""Строго декодировать весь поток; legacy и T2 запускаются отдельно."""
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]
payload_to_index = {payload: index for index, payload in enumerate(expected, start=1)}
result = {
"mode": mode, "candidates": len(observations),
"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 payload_to_index for payload in actual),
"packet_indexes": [payload_to_index.get(payload) for payload in actual],
"exact_ordered_payloads": actual == expected,
"t2_state_scope": "NEW_PREAMBLE_ESTIMATE_PER_PACKET" if mode == "t2" else "NOT_APPLICABLE",
"fallback_between_modes": False,
"passed": len(actual) == PACKETS and actual == expected,
}
if diagnostics:
result["rows"] = [{
"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(
bytes(outcome.frame.reception.payload_bytes)
if outcome.frame is not None and outcome.frame.reception.payload_bytes is not None else b""
),
"t2_tracking_runs_for_candidate": len(row.get("carrier_tracking_traces", ())),
} for row, outcome in observations]
result["detector_statistics"] = dict(receiver.detection_statistics)
result["candidate_overflow"] = receiver.candidate_overflow
if mode == "t2":
result["t2_state_reset_evidence"] = {
"estimate_source": "each candidate's own preamble",
"context_scope": "new PacketMeasure context for every candidate",
"tracking_runs_per_accepted_packet": [
row["t2_tracking_runs_for_candidate"]
for row in result["rows"] if row["packet_output"]
],
"all_accepted_packets_have_fresh_tracking_runs": all(
row["t2_tracking_runs_for_candidate"] > 0
for row in result["rows"] if row["packet_output"]
),
"persistent_tracking_state_in_packet_processor": False,
}
return result
def software_gate() -> dict:
metadata = validate_prepared()
legacy = decode("legacy", diagnostics=True)
t2 = decode("t2", diagnostics=True)
result = {
"lab": "Lab066", "stage": "SOFTWARE_GATE", "rf_tx_count": 0,
"tx_sha256": metadata["tx_sha256"], "legacy": legacy, "t2": t2,
"gaps_verified_zero": True,
"t2_state_reset_verified_by_fresh_preamble_per_packet": bool(
t2.get("t2_state_reset_evidence", {}).get(
"all_accepted_packets_have_fresh_tracking_runs"
)
),
"passed": (
legacy["passed"] and t2["passed"]
and t2.get("t2_state_reset_evidence", {}).get(
"all_accepted_packets_have_fresh_tracking_runs"
) is True
),
}
save_new(OUTPUT / "software_gate.json", result)
if not result["passed"]:
raise RuntimeError("Lab066 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 Lab066")
save_new(RAW / "preflight_attempt.json", {
"lab": "Lab066", "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 запрещён в Lab066 preflight")
return actual_runner(command)
result = {
"lab": "Lab066", "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("Lab066 preflight RX уже существует")
captured = rx_only(transfer_command(path, PREFLIGHT_SAMPLES))
stdout, stderr = captured.stdout or "", captured.stderr or ""
result["rx_process"] = {"returncode": captured.returncode, "stdout": stdout, "stderr": stderr}
result["rx_counter_summary"] = lab064.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("Неверная длина Lab066 preflight RX")
level = hardware.receive_level(path)
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})
result["rx_level"] = level
result["rx_sha256"] = sha256(path)
result["rx_overruns"] = result["rx_counter_summary"]["maximum_count"]
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, allow_nan=False) + "\n", encoding="utf-8"
)
if not result["passed"]:
raise RuntimeError("Lab066 preflight 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 для конфигурации Lab066")
hardware.require_clock_pass(record.get("clock", {}))
if (record.get("rx_overruns") != 0
or record.get("rx_level", {}).get("saturation_detected") is not False
or record.get("rx_level", {}).get("unexpected_strong_signal") is not False):
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 сеанс после отдельного разрешения."""
if authorization is not True:
raise PermissionError("Нужно отдельное разрешение на единственный RF-сеанс Lab066")
metadata = validate_prepared()
require_preflight(metadata)
receive_path = RAW / "rx_stream.cs8"
if receive_path.exists() or (RAW / "capture_result.json").exists():
raise FileExistsError("Аппаратный результат Lab066 уже существует")
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": "Lab066", "authorized_once": True, "automatic_retry": False,
"point": asdict(POINT), "parameters": parameters(), "tx_sha256": metadata["tx_sha256"],
})
result = {
"lab": "Lab066", "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, process, clock_started = lab054.clock.TransmitGuard(), None, 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)
before_tx_ac_rms = float(np.sqrt(max(0.0, before_tx["ac_power_code2"])))
before_tx_strong = bool(
before_tx_ac_rms >= STRONG_AC_RMS_CODE
or before_tx["max_abs_i"] >= STRONG_COMPONENT_CODE
or before_tx["max_abs_q"] >= STRONG_COMPONENT_CODE
)
before_tx.update({
"ac_rms_code": before_tx_ac_rms,
"unexpected_strong_signal": before_tx_strong,
})
result["rx_level_before_tx"] = before_tx
if before_tx["saturation_detected"]:
raise RuntimeError("RX clipping до TX; RF TX = 0")
if before_tx_strong:
raise RuntimeError("Неожиданно сильный фон непосредственно перед TX; RF TX = 0")
guard.register_transmit(); result["rf_tx_count"] = guard.transmit_calls
transmitted = actual_runner(transfer_command(RAW / "tx_stream.cs8", EXPECTED_TX_SAMPLES, transmit=True))
tx_stdout, tx_stderr = transmitted.stdout or "", transmitted.stderr or ""
(RAW / "tx_stdout.txt").write_text(tx_stdout, encoding="utf-8")
(RAW / "tx_stderr.txt").write_text(tx_stderr, encoding="utf-8")
result["tx_process"] = {"returncode": transmitted.returncode,
"stdout_file": "tx_stdout.txt", "stderr_file": "tx_stderr.txt"}
result["tx_counter_summary"] = lab064.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()
rx_log = (RAW / "rx_stdout.txt").read_text(encoding="utf-8") + (RAW / "rx_stderr.txt").read_text(encoding="utf-8")
result["rx_counter_summary"] = lab064.counter_summary(rx_log, "overrun")
result["rx_samples"] = receive_path.stat().st_size // 2
result["rx_sha256"] = sha256(receive_path)
result["rx_level"] = hardware.receive_level(receive_path)
result["rx_clipping"] = bool(result["rx_level"]["saturation_detected"])
result["passed"] = result["rx_samples"] == RX_SAMPLES
validity = lab064.capture_validity(result)
if not validity["valid"] or not result["passed"]:
result["passed"] = False
raise RuntimeError("INVALID_CAPTURE; запись сохранена, повтор запрещён")
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("Lab066 RF-сеанс невалиден; повтор запрещён")
return result
def locate_first_packet(tx: lab058.Cs8File, rx: lab058.Cs8File) -> dict:
"""Найти первый пакет после строгого verdict, не раскрывая границы декодеру."""
stride = MODE.samples_per_symbol
reference = tx.read(GUARD_SAMPLES, PACKET_SAMPLES).astype(np.complex64)
received = rx.read(0, rx.size).astype(np.complex64)
reference = reference[::stride] - np.mean(reference[::stride])
received = received[::stride] - np.mean(received[::stride])
correlation = signal.correlate(received, reference, mode="valid", method="fft")
coarse = int(np.argmax(abs(correlation))) * stride
full_reference = tx.read(GUARD_SAMPLES, PACKET_SAMPLES).astype(np.complex128)
full_reference -= np.mean(full_reference)
starts = range(max(0, coarse - 2 * stride), min(rx.size - PACKET_SAMPLES, coarse + 2 * stride) + 1)
scores = []
for start in starts:
values = rx.read(start, PACKET_SAMPLES).astype(np.complex128)
values -= np.mean(values)
scores.append(abs(np.vdot(full_reference, values)))
first_start = starts.start + int(np.argmax(scores))
return {
"first_packet_rx_start_sample": first_start,
"stream_rx_start_sample": first_start - GUARD_SAMPLES,
"coarse_first_packet_start_sample": coarse,
"method": "first known packet correlation after strict receiver verdict",
}
def physical_packet_diagnostics(tx: lab058.Cs8File, rx: lab058.Cs8File,
location: dict) -> dict:
"""Оценить 16 пакетов известным TX только после строгого решения."""
rows = []
stream_start = location["stream_rx_start_sample"]
for layout, frame in zip(validate_prepared()["layout"], plans()):
packet_index = layout["packet_index"]
tx_start = layout["start_sample"]
rx_start = stream_start + tx_start
transmitted = tx.read(tx_start, PACKET_SAMPLES)
received = rx.read(rx_start, PACKET_SAMPLES)
expected = 2.0 * frame.plan.frame_bits.astype(np.float64) - 1.0
metrics, _working = lab058.analyze_values(received, expected, MODE.symbol_rate)
channel, _arrays = lab058.channel_metrics(
transmitted, received, MODE.symbol_rate, metrics["oracle"]["cfo_hz"]
)
oracle = metrics["oracle"]["metrics"]
rows.append({
"packet_index": packet_index, "tx_start_sample": tx_start,
"rx_start_sample": rx_start,
"symbol_error_rate": oracle["errors"] / oracle["symbols"],
"symbol_rms_deviation": float(np.sqrt(
oracle["residual_power"] / oracle["matched_component_power"]
)),
"matched_component_power": oracle["matched_component_power"],
"residual_power": oracle["residual_power"],
"matched_residual_db": float(10 * np.log10(
oracle["matched_component_power"] / oracle["residual_power"]
)),
"band_gain_nonuniformity_p95_p05_db": channel["amplitude_ripple_p95_p05_db"],
})
return {
"method": "Lab058 oracle diagnostics per packet after strict verdict",
"rows": rows,
"comparison": {
"lab056_B_oracle_symbol_error_rate": 0.03495896,
"lab056_B_oracle_evm_rms": 0.845350,
"lab062_source": "data/processed/lab062/rf_analysis.json",
"scope": "Lab056 B and Lab062 are contextual comparisons, not identical estimands",
},
}
def summarize_analysis(result: dict) -> dict:
"""Свести строгие и физические результаты без повторного декодирования."""
legacy, t2 = result["run_a_legacy"], result["run_b_t2"]
legacy_set, t2_set = set(legacy["packet_indexes"]), set(t2["packet_indexes"])
layout = validate_prepared()["layout"]
stream_start = result["location_after_strict_runs"]["stream_rx_start_sample"]
packet_rows = []
for layout_row in layout:
index = layout_row["packet_index"]
nominal = stream_start + layout_row["start_sample"]
modes = {}
for label, strict, accepted in (
("legacy", legacy, index in legacy_set), ("t2", t2, index in t2_set)
):
candidates = [
row for row in strict["rows"]
if abs(row["absolute_refined"] - nominal) <= 64
]
best = max(candidates, key=lambda row: (
row["packet_output"], row["crc"], row["fec"], row["header"],
-abs(row["absolute_refined"] - nominal),
)) if candidates else None
modes[label] = {
"candidate_at_expected_packet": best is not None,
"header": accepted or bool(best and best["header"]),
"fec": accepted or bool(best and best["fec"]),
"crc": accepted or bool(best and best["crc"]),
"packet_output": accepted,
}
packet_rows.append({"packet_index": index, **modes})
physical_rows = result["physical_diagnostics"]["rows"]
matched = float(np.mean([row["matched_component_power"] for row in physical_rows]))
residual = float(np.mean([row["residual_power"] for row in physical_rows]))
physical = {
"symbol_error_rate": float(np.mean([row["symbol_error_rate"] for row in physical_rows])),
"rms_symbol_deviation": float(np.sqrt(np.mean([
row["symbol_rms_deviation"] ** 2 for row in physical_rows
]))),
"matched_component_power": matched,
"residual_power": residual,
"matched_residual_db": float(10 * np.log10(matched / residual)),
"band_gain_nonuniformity_p95_p05_db_median": float(np.median([
row["band_gain_nonuniformity_p95_p05_db"] for row in physical_rows
])),
"band_gain_nonuniformity_p95_p05_db_min": float(np.min([
row["band_gain_nonuniformity_p95_p05_db"] for row in physical_rows
])),
"band_gain_nonuniformity_p95_p05_db_max": float(np.max([
row["band_gain_nonuniformity_p95_p05_db"] for row in physical_rows
])),
}
lab056 = {
"symbol_error_rate": 0.03495896464646465,
"rms_symbol_deviation": 0.8453497751219741,
"matched_residual_db": 1.4648455366051558,
"band_gain_nonuniformity_db": 5.471381501203027,
}
lab062_result = json.loads(
(ROOT / "data/processed/lab062/rf_analysis.json").read_text(encoding="utf-8")
)["lab058_quality"]
lab062 = {
"symbol_error_rate": lab062_result["symbol_error_rate"],
"rms_symbol_deviation": lab062_result["evm_rms"],
"matched_residual_db": lab062_result["matched_residual_db"],
"band_gain_nonuniformity_db": lab062_result[
"band_gain_nonuniformity_p95_p05_db_median"
],
"capture_status": "RF_SESSION_INVALID_TX_UNDERRUN",
}
def differences(reference: dict) -> dict:
return {
"symbol_error_rate_absolute": (
physical["symbol_error_rate"] - reference["symbol_error_rate"]
),
"rms_symbol_deviation_absolute": (
physical["rms_symbol_deviation"] - reference["rms_symbol_deviation"]
),
"matched_residual_db": (
physical["matched_residual_db"] - reference["matched_residual_db"]
),
"band_gain_nonuniformity_db": (
physical["band_gain_nonuniformity_p95_p05_db_median"]
- reference["band_gain_nonuniformity_db"]
),
}
return {
"lab": "Lab066", "capture_valid": result["capture_valid"],
"legacy": {key: legacy[key] for key in (
"candidates", "header_pass", "fec_started", "fec_pass", "crc_started",
"crc_pass", "packet_output", "exact_payload_match", "packet_indexes",
)},
"t2": {key: t2[key] for key in (
"candidates", "header_pass", "fec_started", "fec_pass", "crc_started",
"crc_pass", "packet_output", "exact_payload_match", "packet_indexes",
)},
"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, PACKETS + 1)) - legacy_set - t2_set),
"packet_rows": packet_rows, "physical": physical,
"lab056_B": lab056, "difference_from_lab056_B": differences(lab056),
"lab062": lab062, "difference_from_lab062": differences(lab062),
"classification": "PARTIAL_PACKET_RECOVERY_NO_T2_GAIN",
"primary_result": (
"Аппаратно валидный Lab066 дал одинаковые 10/16 точных пакетов "
"в legacy и T2; физическое качество лучше Lab056 B и невалидной Lab062."
),
"recommended_next_single_step": (
"Отдельно разрешить офлайн-сравнение header-ошибок пакетов "
"1-3, 9, 15-16 с принятыми пакетами по сохранённому RX; новый TX не нужен."
),
}
def write_summary_outputs(result: dict) -> dict:
summary = summarize_analysis(result)
save_new(OUTPUT / "rf_summary.json", summary)
with (OUTPUT / "packet_comparison.csv").open("x", newline="", encoding="utf-8-sig") as stream:
writer = csv.writer(stream)
writer.writerow([
"packet_index", "legacy_header", "legacy_fec", "legacy_crc",
"legacy_packet_output", "t2_header", "t2_fec", "t2_crc", "t2_packet_output",
])
for row in summary["packet_rows"]:
writer.writerow([
row["packet_index"], row["legacy"]["header"], row["legacy"]["fec"],
row["legacy"]["crc"], row["legacy"]["packet_output"],
row["t2"]["header"], row["t2"]["fec"], row["t2"]["crc"],
row["t2"]["packet_output"],
])
return summary
def analyze_saved_capture() -> dict:
"""Строго обработать одну будущую RX-запись обоими режимами."""
metadata = validate_prepared()
capture = json.loads((RAW / "capture_result.json").read_text(encoding="utf-8"))
rx_path = RAW / "rx_stream.cs8"
if (
capture.get("lab") != "Lab066"
or capture.get("rf_tx_count") != 1
or capture.get("automatic_tx_repeats") != 0
or capture.get("point") != asdict(POINT)
or capture.get("parameters") != parameters()
or capture.get("tx_sha256") != metadata["tx_sha256"]
or capture.get("rx_sha256") != sha256(rx_path)
):
raise RuntimeError("Нет подтверждённой единственной RX-записи Lab066")
validity = lab064.capture_validity(capture)
if not validity["valid"]:
result = {
"lab": "Lab066", "stage": "STRICT_DUAL_OFFLINE_ANALYSIS",
"classification": "INVALID_CAPTURE", "capture_valid": False,
"capture_invalid_reasons": validity["invalid_reasons"],
"rf_tx_count": capture.get("rf_tx_count"), "automatic_tx_repeats": 0,
}
save_new(OUTPUT / "rf_result.json", result)
return result
legacy = decode("legacy", rx_path, diagnostics=True)
t2 = decode("t2", rx_path, diagnostics=True)
strict_runs_complete = (
legacy.get("candidate_overflow") == 0
and t2.get("candidate_overflow") == 0
and isinstance(legacy.get("rows"), list)
and isinstance(t2.get("rows"), list)
)
if not strict_runs_complete:
raise RuntimeError("Строгие legacy/T2 прогоны не завершены; oracle-диагностика запрещена")
tx = lab058.Cs8File(RAW / "tx_stream.cs8")
rx = lab058.Cs8File(rx_path)
location = locate_first_packet(tx, rx)
result = {
"lab": "Lab066", "stage": "STRICT_DUAL_OFFLINE_ANALYSIS",
"tx_sha256": metadata["tx_sha256"], "rx_sha256": capture["rx_sha256"],
"capture_valid": validity["valid"], "capture_invalid_reasons": validity["invalid_reasons"],
"run_a_legacy": legacy, "run_b_t2": t2,
"same_rx_capture": True, "fallback_between_modes": False,
"strict_runs_complete": True,
"location_after_strict_runs": location,
"physical_diagnostics_scope": "KNOWN_TX_ONLY_AFTER_COMPLETE_STRICT_RUNS",
"physical_diagnostics": physical_packet_diagnostics(tx, rx, location),
"comparison_targets": ["Lab056 B", "Lab062", "Lab066"],
}
save_new(OUTPUT / "rf_result.json", result)
return result
def summarize_saved_analysis() -> dict:
return write_summary_outputs(json.loads(
(OUTPUT / "rf_result.json").read_text(encoding="utf-8")
))
def run_functional_tests() -> None:
assert EXPECTED_TX_SAMPLES == 3_676_096
assert transfer_command("NUL", 100, transmit=True)[-7:-5] == ["-x", "20"]
assert "-t" not in transfer_command("NUL", 100)
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", "summarize"),
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)
elif arguments.stage == "analyze": result = analyze_saved_capture()
else: result = summarize_saved_analysis()
print(json.dumps(result, ensure_ascii=False, indent=2, allow_nan=False))
print("RF TX NOW = 0")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,149 @@
"""Lab066: исправленные паузы, строгий software gate и запрет TX."""
from __future__ import annotations
import hashlib
import json
from pathlib import Path
import subprocess
import pytest
from experiments import lab066_full_packet_rf_retest as lab
from tools.hackrf_process_check import ProcessCheck, ProcessState
def test_fixed_layout_and_operating_point():
waveform, layout = lab.build_waveform()
assert len(lab.plans()) == 16
assert all(len(frame.plan.payload) == 256 for frame in lab.plans())
assert len({frame.plan.payload for frame in lab.plans()}) == 16
assert lab.PACKET_SAMPLES == 92_256
assert lab.INTER_PACKET_GAP_SAMPLES == 40_000
assert lab.NUMBER_OF_GAPS == 15
assert lab.GUARD_SAMPLES == 800_000
assert lab.BASE_TX_SAMPLES == 2_076_096
assert lab.EXPECTED_TX_SAMPLES == 3_676_096
assert len(waveform) == 2 * lab.EXPECTED_TX_SAMPLES
assert [row["packet_index"] for row in layout] == list(range(1, 17))
assert lab.parameters()["tx_vga_db"] == 20
assert lab.parameters()["rx_lna_db"] == lab.parameters()["rx_vga_db"] == 24
def test_guards_and_all_gaps_are_exact_zero():
waveform, layout = lab.build_waveform()
samples = memoryview(waveform)
assert not any(samples[:2 * lab.GUARD_SAMPLES])
assert not any(samples[-2 * lab.GUARD_SAMPLES:])
for row in layout[:-1]:
low = 2 * row["following_gap_start_sample"]
high = low + 2 * row["following_gap_samples"]
assert high - low == 2 * 40_000
assert not any(samples[low:high])
def test_transfer_commands_are_rx_only_or_fixed_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("-l") + 1] == receive[receive.index("-g") + 1] == "24"
assert transmit[transmit.index("-x") + 1] == "20"
def test_prepare_metadata_and_two_strict_roundtrips(tmp_path, monkeypatch):
raw, output = tmp_path / "raw", tmp_path / "processed"
monkeypatch.setattr(lab, "RAW", raw); monkeypatch.setattr(lab, "OUTPUT", output)
metadata = lab.prepare()
assert metadata["tx_samples"] == lab.EXPECTED_TX_SAMPLES
assert metadata["tx_sha256"] == hashlib.sha256((raw / "tx_stream.cs8").read_bytes()).hexdigest()
result = lab.software_gate()
for mode in ("legacy", "t2"):
row = result[mode]
assert row["header_pass"] == row["fec_pass"] == row["crc_pass"] == 16
assert row["packet_output"] == row["exact_payload_match"] == 16
assert row["packet_indexes"] == list(range(1, 17))
assert row["exact_ordered_payloads"] and row["passed"]
assert row["fallback_between_modes"] is False
assert result["t2"]["t2_state_scope"] == "NEW_PREAMBLE_ESTIMATE_PER_PACKET"
assert result["t2"]["t2_state_reset_evidence"][
"tracking_runs_per_accepted_packet"
] == [2] * 16
assert result["t2_state_reset_verified_by_fresh_preamble_per_packet"] is True
@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")
monkeypatch.setattr(lab, "validate_prepared", lambda: {"tx_sha256": "TEST"})
(output / "software_gate.json").write_text('{"passed": true, "tx_sha256": "TEST"}')
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]) * lab.PREFLIGHT_SAMPLES)
log = "1 RX overruns\n0 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"]
record = json.loads((raw / "preflight_result.json").read_text())
assert record["rf_tx_count"] == 0
if failure == "overrun":
assert record["rx_counter_summary"]["maximum_count"] == 1
def test_capture_refuses_without_authorization_before_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_future_analysis_declares_strict_modes_and_physical_comparison():
source = Path(lab.__file__).read_text(encoding="utf-8")
assert 'decode("legacy", rx_path, diagnostics=True)' in source
assert 'decode("t2", rx_path, diagnostics=True)' in source
assert "physical_packet_diagnostics(tx, rx, location)" in source
assert "if not strict_runs_complete" in source
assert '["Lab056 B", "Lab062", "Lab066"]' in source
def test_future_analysis_rejects_capture_from_another_configuration(tmp_path, monkeypatch):
monkeypatch.setattr(lab, "RAW", tmp_path); monkeypatch.setattr(lab, "OUTPUT", tmp_path)
rx = tmp_path / "rx_stream.cs8"; rx.write_bytes(b"\x00\x00")
capture = {
"lab": "Lab066", "rf_tx_count": 1, "automatic_tx_repeats": 0,
"point": {}, "parameters": {}, "tx_sha256": "WRONG",
"rx_sha256": lab.sha256(rx),
}
(tmp_path / "capture_result.json").write_text(json.dumps(capture))
monkeypatch.setattr(lab, "validate_prepared", lambda: {"tx_sha256": "EXPECTED"})
monkeypatch.setattr(lab, "decode", lambda *args, **kwargs: pytest.fail("decode"))
with pytest.raises(RuntimeError, match="подтверждённой"):
lab.analyze_saved_capture()
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"))