2 Commits

Author SHA1 Message Date
LittleSam129
b27a5cf00e Lab066: validate 500 ksymbol/s full-packet OTA reception 2026-09-15 15:51:57 +03:00
LittleSam129
dacf04f2d0 Lab065: validate OTA gain improvement at TX VGA 20 dB 2026-09-15 13:42:08 +03:00
24 changed files with 8902 additions and 0 deletions

View File

@@ -2861,3 +2861,143 @@ Quick gate после сеанса прошёл:986 тестов ядра,37 ф
полное сравнение L3/L0 и двенадцать спектральных значений непосредственно из
CS8. Расхождений нет, вердикт `чисто`. Оборудование при проверке не опрашивалось,
RF TX остаётся равным нулю.
# Запись 060
## Дата
15 сентября 2026 года
## Тема
Lab064 зафиксирована; подготовка Lab065 при TX VGA 20 дБ и RX-only preflight
## Выполнено
После проверки уже подготовленного индекса по прямому разрешению пользователя
создан коммит `cff226e0bf9e36a627829ea11a4ca3292ae20953` с сообщением
`Lab064: measure OTA level linearity at TX VGA 10 dB`. В коммит вошли только
12 файлов и фрагментов Lab064; изменения Lab054 и Lab058 и сырые IQ не вошли.
Push не выполнялся.
Пользователь подтвердил неизменность геометрии, антенн, поляризации, USB,
clock-кабеля и RF-кабелей. Для Lab065 TX VGA изменён с 10 до 20 дБ. Частота
1400000000 Гц, RX LNA/VGA 24/24 дБ, RF amplifier и Bias-Tee OFF, PRBS7/BPSK/RRC,
20000 символов/с, Fs 8000000 отсчётов/с, несущая +50000 Гц и лестница
−18/−12/−6/0 дБ сохранены. TX-файл побайтово совпадает с Lab064: 6000000
отсчётов, 12000000 байт, 0,75 с, SHA-256
fa260719659b2984fe29588238415622cd75c6daefec6cc9130ba1bf851bd3f9.
Подготовлены RX-first runner без повторов, fail-closed проверка максимальных
счётчиков и анализ двух видов линейности: внутри Lab065 и для одинаковых
уровней Lab064/Lab065. До RF численно заданы границы пяти классификаций.
Software gate и 12 целевых тестов прошли.
RX-only preflight прошёл. Обе HackRF One r9 и роли подтверждены,
MASTER/SLAVE reg29=0x26, SLAVE CLKIN=DETECTED на третьем опросе через201,986мс.
RX записал800000 отсчётов фона: полный RMS2,333068, AC RMS1,056565 кода,
max|I|/|Q|=6/8, clipping0, overruns0, неожиданно сильного сигнала нет.
Процессы до и после CLEAR, cleanup без ошибок.
После отдельного прямого разрешения выполнен ровно один OTA-сеанс Lab065.
Перед TX MASTER/SLAVE reg29=0x26, SLAVE CLKIN=DETECTED на втором опросе через
136,897мс. RX стартовал раньше TX. TX underrun maximum0, RX overrun maximum0,
clipping0, cleanup без ошибок; процессы после CLEAR. RX:16000000 отсчётов,
2,0с, SHA-256 845d7b7dcaef70e179ab69e6ba0041601859630786d2f4d51c16a8c1c7869d7e.
Автоматических и ручных повторов не было.
Внутренние ошибки ступеней Lab065 равны−0,077363/+0,034934/−0,013858дБ;
компрессии L2/L3 нет. Относительно Lab064 matched power вырос на
11,179328/11,119466/11,323670/11,287650дБ. Медианный прирост+11,233489дБ,
медианное изменение остатка+0,517004дБ, улучшение matched/residual+10,734495дБ.
Фон стабилен: разброс1,002840дБ при границе3дБ. Остаточные полосы50/100/150кГц
на L3 выросли на7,304593/6,432281/4,996232дБ, то есть слабее matched power.
Классификация `LINEAR_GAIN_IMPROVEMENT`.
Независимая read-only проверка пересчитала RX SHA и размер, все счётчики,
локализацию, фон, matched/residual, комплексный h, CFO, внутренние ступени,
сравнение с Lab064 и полосы50/100/150кГц. Расхождений нет. Adversary подтвердил
одноразовость runner и нашёл неоднозначную подпись текущего состояния RF после
сеанса; она исправлена на `RF TX NOW=0` и закрыта отдельным тестом. Финальные
шлюзы: quick gate1000/38/13 PASS, pytest1000 passed, diff check PASS.
RF TX=0. Lab065 не коммитировалась, push не выполнялся, Lab066 не создана.
Следующий этап: STOP, рассмотрение результата и отдельное разрешение на коммит
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,232 @@
{
"lab": "Lab065",
"stage": "SINGLE_RF_LEVEL_CAPTURE",
"passed": true,
"rf_tx_count": 1,
"automatic_tx_repeats": 0,
"point": {
"frequency_hz": 1400000000,
"physical_path": "MASTER TX near window -> LOS indoor -> SLAVE RX on desk",
"path_confirmed": true,
"tx_antenna": "existing 1400 MHz antenna near window",
"rx_antenna": "existing 1400 MHz antenna on desk",
"distance_m": 1.2,
"orientation": "same polarization; fixed during preflight and RF session",
"visibility": "LOS indoor"
},
"parameters": {
"rf_frequency_hz": 1400000000,
"sample_rate_hz": 8000000,
"symbol_rate": 20000,
"samples_per_symbol": 400,
"digital_carrier_hz": 50000,
"rrc_rolloff": 0.35,
"rrc_span_symbols": 10,
"prbs": "PRBS7 x^7+x^6+1, deterministic initial state",
"full_phy_packet": false,
"tx_vga_db": 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"
},
"clock_path": "10 MHz TCXO -> MASTER -> MASTER CLKOUT -> SLAVE CLKIN",
"levels_db_relative_nominal": [
-18.0,
-12.0,
-6.0,
0.0
],
"geometry_unchanged_from_lab064": true,
"waveform_source": "Lab064 exact CS8 copy"
},
"tx_sha256": "fa260719659b2984fe29588238415622cd75c6daefec6cc9130ba1bf851bd3f9",
"process_before": {
"state": "CLEAR",
"attempts": [
{
"state": "CLEAR",
"mechanism": "psutil",
"matches": [],
"inspected": 292,
"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": 136.89709990285337,
"elapsed_ms": 136.89709990285337,
"polls": [
{
"query": 1,
"start_ms": 50.08170008659363,
"end_ms": 67.4550998955965,
"raw": "CLKIN status: no clock signal detected",
"state": "NOT_DETECTED",
"exit_code": 0
},
{
"query": 2,
"start_ms": 117.92290001176298,
"end_ms": 136.89709990285337,
"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.9449310302734375,
"mean_q": 1.8423919677734375,
"power_code2": 4.87310791015625,
"ac_power_code2": 0.5858050952665499,
"rms_code": 2.2075117010236323,
"peak_magnitude_code": 4.47213595499958,
"max_abs_i": 3.0,
"max_abs_q": 4.0,
"papr_db": 6.132239671543243,
"clipped_components_fraction": 0.0,
"clipped_samples_fraction": 0.0,
"saturation_detected": false,
"analog_compression": "UNKNOWN_WITHOUT_CALIBRATION"
},
"tx_process": {
"returncode": 0,
"stdout_file": "tx_stdout.txt",
"stderr_file": "tx_stderr.txt"
},
"tx_counter_summary": {
"expected_kind": "underrun",
"observations": [
{
"count": 0,
"role": "UNKNOWN",
"kind": "underrun",
"text": "0 underruns",
"character_offset": 337
},
{
"count": 0,
"role": "UNKNOWN",
"kind": "underrun",
"text": "0 underruns",
"character_offset": 509
}
],
"selected_counts": [
0,
0
],
"final_count": 0,
"maximum_count": 0,
"unparsed_loss_text": false,
"clean": true
},
"rx_samples": 16000000,
"rx_bytes": 32000000,
"rx_duration_seconds": 2.0,
"rx_counter_summary": {
"expected_kind": "overrun",
"observations": [
{
"count": 0,
"role": "UNKNOWN",
"kind": "overrun",
"text": "0 overruns",
"character_offset": 337
},
{
"count": 0,
"role": "UNKNOWN",
"kind": "overrun",
"text": "0 overruns",
"character_offset": 459
},
{
"count": 0,
"role": "UNKNOWN",
"kind": "overrun",
"text": "0 overruns",
"character_offset": 581
},
{
"count": 0,
"role": "UNKNOWN",
"kind": "overrun",
"text": "0 overruns",
"character_offset": 752
}
],
"selected_counts": [
0,
0,
0,
0
],
"final_count": 0,
"maximum_count": 0,
"unparsed_loss_text": false,
"clean": true
},
"rx_sha256": "845d7b7dcaef70e179ab69e6ba0041601859630786d2f4d51c16a8c1c7869d7e",
"rx_level": {
"samples": 16000000,
"mean_i": 0.942780625,
"mean_q": 1.8349653125,
"power_code2": 5.6163274375,
"ac_power_code2": 1.3603944325463857,
"rms_code": 2.3698792031451728,
"peak_magnitude_code": 10.816653826391969,
"max_abs_i": 10.0,
"max_abs_q": 10.0,
"papr_db": 13.187334420841685,
"clipped_components_fraction": 0.0,
"clipped_samples_fraction": 0.0,
"saturation_detected": false,
"analog_compression": "UNKNOWN_WITHOUT_CALIBRATION"
},
"rx_clipping": false,
"process_after": {
"state": "CLEAR",
"attempts": [
{
"state": "CLEAR",
"mechanism": "psutil",
"matches": [],
"inspected": 292,
"errors": []
}
]
},
"cleanup_errors": []
}

View File

@@ -0,0 +1,181 @@
{
"lab": "Lab065",
"stage": "COMMON_CLOCK_AND_RX_ONLY_PREFLIGHT",
"passed": true,
"rf_tx_count": 0,
"stop_before_tx": true,
"point": {
"frequency_hz": 1400000000,
"physical_path": "MASTER TX near window -> LOS indoor -> SLAVE RX on desk",
"path_confirmed": true,
"tx_antenna": "existing 1400 MHz antenna near window",
"rx_antenna": "existing 1400 MHz antenna on desk",
"distance_m": 1.2,
"orientation": "same polarization; fixed during preflight and RF session",
"visibility": "LOS indoor"
},
"parameters": {
"rf_frequency_hz": 1400000000,
"sample_rate_hz": 8000000,
"symbol_rate": 20000,
"samples_per_symbol": 400,
"digital_carrier_hz": 50000,
"rrc_rolloff": 0.35,
"rrc_span_symbols": 10,
"prbs": "PRBS7 x^7+x^6+1, deterministic initial state",
"full_phy_packet": false,
"tx_vga_db": 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"
},
"clock_path": "10 MHz TCXO -> MASTER -> MASTER CLKOUT -> SLAVE CLKIN",
"levels_db_relative_nominal": [
-18.0,
-12.0,
-6.0,
0.0
],
"geometry_unchanged_from_lab064": true,
"waveform_source": "Lab064 exact CS8 copy"
},
"tx_sha256": "fa260719659b2984fe29588238415622cd75c6daefec6cc9130ba1bf851bd3f9",
"process_before": {
"state": "CLEAR",
"attempts": [
{
"state": "CLEAR",
"mechanism": "psutil",
"matches": [],
"inspected": 289,
"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": 3,
"time_to_detect_ms": 201.98559993878007,
"elapsed_ms": 201.98559993878007,
"polls": [
{
"query": 1,
"start_ms": 50.617999862879515,
"end_ms": 66.89479993656278,
"raw": "CLKIN status: no clock signal detected",
"state": "NOT_DETECTED",
"exit_code": 0
},
{
"query": 2,
"start_ms": 117.23309988155961,
"end_ms": 136.1208998132497,
"raw": "CLKIN status: no clock signal detected",
"state": "NOT_DETECTED",
"exit_code": 0
},
{
"query": 3,
"start_ms": 187.1324998792261,
"end_ms": 201.98559993878007,
"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.116 sec = 15.9 MiB/second, average power -34.8 dBfs, 9024 bytes free in buffer, 0 overruns, longest 0 bytes\n\nExiting...\nTotal time: 0.11556 s\nhackrf_stop_rx() done\nTransfer statistics:\n1847520 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": 333
},
{
"count": 0,
"role": "UNKNOWN",
"kind": "overrun",
"text": "0 overruns",
"character_offset": 502
}
],
"selected_counts": [
0,
0
],
"final_count": 0,
"maximum_count": 0,
"unparsed_loss_text": false,
"clean": true
},
"rx_level": {
"samples": 800000,
"mean_i": 0.94889625,
"mean_q": 1.85107375,
"power_code2": 5.4432075,
"ac_power_code2": 1.1163293787968742,
"rms_code": 2.333068258752838,
"peak_magnitude_code": 8.06225774829855,
"max_abs_i": 6.0,
"max_abs_q": 8.0,
"papr_db": 10.770584663112091,
"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": 1.0565648956864289,
"unexpected_strong_signal": false
},
"rx_sha256": "be3c61dfa1bc40d1a9f6777689026804a1ccfc340b82680d3ec5304ddd68f78c",
"rx_overruns": 0,
"process_after": {
"state": "CLEAR",
"attempts": [
{
"state": "CLEAR",
"mechanism": "psutil",
"matches": [],
"inspected": 289,
"errors": []
}
]
},
"cleanup_errors": []
}

View File

@@ -0,0 +1,219 @@
{
"lab": "Lab065",
"stage": "PREPARATION_ONLY",
"rf_tx_count": 0,
"head": "cff226e0bf9e36a627829ea11a4ca3292ae20953",
"parameters": {
"rf_frequency_hz": 1400000000,
"sample_rate_hz": 8000000,
"symbol_rate": 20000,
"samples_per_symbol": 400,
"digital_carrier_hz": 50000,
"rrc_rolloff": 0.35,
"rrc_span_symbols": 10,
"prbs": "PRBS7 x^7+x^6+1, deterministic initial state",
"full_phy_packet": false,
"tx_vga_db": 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"
},
"clock_path": "10 MHz TCXO -> MASTER -> MASTER CLKOUT -> SLAVE CLKIN",
"levels_db_relative_nominal": [
-18.0,
-12.0,
-6.0,
0.0
],
"geometry_unchanged_from_lab064": true,
"waveform_source": "Lab064 exact CS8 copy"
},
"segments": [
{
"label": "L0",
"level_db": -18.0,
"start_sample": 800000,
"samples": 800000,
"duration_seconds": 0.1,
"analysis_start_sample": 808400,
"analysis_samples": 783200,
"metrics": {
"samples": 800000,
"mean_i": -9.625e-05,
"mean_q": 2.75e-05,
"power_code2": 30.87065625,
"ac_power_code2": 30.870656239979688,
"rms_code": 5.556136809870686,
"peak_magnitude_code": 9.219544457292887,
"max_abs_i": 9.0,
"max_abs_q": 9.0,
"papr_db": 4.398730638796093,
"clipped_components_fraction": 0.0,
"clipped_samples_fraction": 0.0,
"symbol_energy_code2_seconds": 0.0015435328125,
"requested_relative_power_db": -18.0,
"tx_rms_code": 5.556136809870686,
"tx_peak_abs_i_code": 9.0,
"tx_peak_abs_q_code": 9.0,
"tx_peak_magnitude_code": 9.219544457292887,
"digital_clipping": false,
"actual_relative_power_db": -17.97368512020099
}
},
{
"label": "L1",
"level_db": -12.0,
"start_sample": 2000000,
"samples": 800000,
"duration_seconds": 0.1,
"analysis_start_sample": 2008400,
"analysis_samples": 783200,
"metrics": {
"samples": 800000,
"mean_i": -5e-05,
"mean_q": 5e-05,
"power_code2": 122.2567675,
"ac_power_code2": 122.25676749499999,
"rms_code": 11.056978226441435,
"peak_magnitude_code": 17.804493814764857,
"max_abs_i": 17.0,
"max_abs_q": 17.0,
"papr_db": 4.1378635346865025,
"clipped_components_fraction": 0.0,
"clipped_samples_fraction": 0.0,
"symbol_energy_code2_seconds": 0.006112838375,
"requested_relative_power_db": -12.0,
"tx_rms_code": 11.056978226441435,
"tx_peak_abs_i_code": 17.0,
"tx_peak_abs_q_code": 17.0,
"tx_peak_magnitude_code": 17.804493814764857,
"digital_clipping": false,
"actual_relative_power_db": -11.99641465105681
},
"tx_input_difference_from_previous_db": 5.9772704691441785
},
{
"label": "L2",
"level_db": -6.0,
"start_sample": 3200000,
"samples": 800000,
"duration_seconds": 0.1,
"analysis_start_sample": 3208400,
"analysis_samples": 783200,
"metrics": {
"samples": 800000,
"mean_i": 5.625e-05,
"mean_q": 2.625e-05,
"power_code2": 486.5064875,
"ac_power_code2": 486.50648749614686,
"rms_code": 22.05689206347984,
"peak_magnitude_code": 35.12833614050059,
"max_abs_i": 34.0,
"max_abs_q": 35.0,
"papr_db": 4.0422652379446395,
"clipped_components_fraction": 0.0,
"clipped_samples_fraction": 0.0,
"symbol_energy_code2_seconds": 0.024325324375,
"requested_relative_power_db": -6.0,
"tx_rms_code": 22.05689206347984,
"tx_peak_abs_i_code": 34.0,
"tx_peak_abs_q_code": 35.0,
"tx_peak_magnitude_code": 35.12833614050059,
"digital_clipping": false,
"actual_relative_power_db": -5.998257379520232
},
"tx_input_difference_from_previous_db": 5.998157271536577
},
{
"label": "L3",
"level_db": 0.0,
"start_sample": 4400000,
"samples": 800000,
"duration_seconds": 0.1,
"analysis_start_sample": 4408400,
"analysis_samples": 783200,
"metrics": {
"samples": 800000,
"mean_i": 8.25e-05,
"mean_q": 0.00011125,
"power_code2": 1936.04021375,
"ac_power_code2": 1936.0402137308172,
"rms_code": 44.00045697205883,
"peak_magnitude_code": 69.6419413859206,
"max_abs_i": 69.0,
"max_abs_q": 69.0,
"papr_db": 3.9882736474748155,
"clipped_components_fraction": 0.0,
"clipped_samples_fraction": 0.0,
"symbol_energy_code2_seconds": 0.0968020106875,
"requested_relative_power_db": 0.0,
"tx_rms_code": 44.00045697205883,
"tx_peak_abs_i_code": 69.0,
"tx_peak_abs_q_code": 69.0,
"tx_peak_magnitude_code": 69.6419413859206,
"digital_clipping": false,
"actual_relative_power_db": 0.0
},
"tx_input_difference_from_previous_db": 5.998257379520232
}
],
"tx_samples": 6000000,
"tx_bytes": 12000000,
"tx_duration_seconds": 0.75,
"tx_sha256": "fa260719659b2984fe29588238415622cd75c6daefec6cc9130ba1bf851bd3f9",
"source_tx_sha256": "fa260719659b2984fe29588238415622cd75c6daefec6cc9130ba1bf851bd3f9",
"waveform_unchanged_from_lab064": true,
"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\\lab065\\tx_level_staircase.cs8",
"-f",
"1400000000",
"-s",
"8000000",
"-n",
"6000000",
"-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\\lab065\\rx_stream.cs8",
"-f",
"1400000000",
"-s",
"8000000",
"-n",
"16000000",
"-l",
"24",
"-g",
"24",
"-a",
"0",
"-p",
"0",
"-B"
]
}

View File

@@ -0,0 +1,515 @@
{
"lab": "Lab065",
"stage": "OFFLINE_RF_LEVEL_LINEARITY_ANALYSIS",
"packet_receiver_used": false,
"location": {
"start_sample": 4217216,
"correlation": 0.28566040369708917,
"coarse_cfo_hz": 0.0,
"stride": 32
},
"rows": [
{
"label": "L0",
"level_db": -18.0,
"rx_start_sample": 5017215,
"alignment_adjustment_samples": -1,
"full_iq": {
"samples": 800000,
"mean_i": 0.9456825,
"mean_q": 1.849345,
"power_code2": 5.91858,
"ac_power_code2": 1.6041876801687494,
"rms_code": 2.4328131864160882,
"peak_magnitude_code": 7.280109889280518,
"max_abs_i": 6.0,
"max_abs_q": 7.0,
"papr_db": 9.520583473606806,
"clipped_components_fraction": 0.0,
"clipped_samples_fraction": 0.0,
"symbol_energy_code2_seconds": 0.000295929,
"ac_rms_code": 1.2665653082919766
},
"background_label": "INITIAL_GUARD",
"background": {
"samples": 160000,
"mean_i": 0.9375625,
"mean_q": 1.83864375,
"power_code2": 4.84290625,
"ac_power_code2": 0.5832719691796884,
"rms_code": 2.200660412239926,
"peak_magnitude_code": 4.47213595499958,
"max_abs_i": 3.0,
"max_abs_q": 4.0,
"papr_db": 6.1592393370019245,
"clipped_components_fraction": 0.0,
"clipped_samples_fraction": 0.0,
"symbol_energy_code2_seconds": 0.00024214531250000003,
"ac_rms_code": 0.7637224425009969
},
"matched_signal_power": 0.009849829325472823,
"residual_power": 1.147671646182205,
"matched_to_residual_db": -20.663889470509574,
"complex_gain_h": {
"real": -0.0069147093491316635,
"imag": 0.016278708105995708,
"phase_rad_at_active_start": 1.9724722764994853
},
"gain_abs": 0.01768642256034779,
"gain_power_abs_h_squared": 0.00031280954298317925,
"cfo_hz": 0.051385857393477286,
"residual_spectrum": {
"residual_band_h1_power_code2": 0.005700729949335216,
"residual_band_h2_power_code2": 0.004639110794845057,
"residual_band_h3_power_code2": 0.003007515042889266,
"interpretation": "Diagnostic residual bands at 50/100/150 kHz; not calibrated RF harmonic power"
}
},
{
"label": "L1",
"level_db": -12.0,
"rx_start_sample": 6217220,
"alignment_adjustment_samples": 4,
"full_iq": {
"samples": 800000,
"mean_i": 0.943035,
"mean_q": 1.84948125,
"power_code2": 5.45620125,
"ac_power_code2": 1.1463053446734381,
"rms_code": 2.335851290215197,
"peak_magnitude_code": 7.810249675906654,
"max_abs_i": 7.0,
"max_abs_q": 7.0,
"papr_db": 10.48439454272797,
"clipped_components_fraction": 0.0,
"clipped_samples_fraction": 0.0,
"symbol_energy_code2_seconds": 0.00027281006250000003,
"ac_rms_code": 1.0706565017191265
},
"background_label": "GAP_L0_L1",
"background": {
"samples": 160000,
"mean_i": 0.94049375,
"mean_q": 1.8457,
"power_code2": 4.89708125,
"ac_power_code2": 0.6059442662109387,
"rms_code": 2.2129349854887286,
"peak_magnitude_code": 5.0990195135927845,
"max_abs_i": 4.0,
"max_abs_q": 5.0,
"papr_db": 7.250360382891476,
"clipped_components_fraction": 0.0,
"clipped_samples_fraction": 0.0,
"symbol_energy_code2_seconds": 0.0002448540625,
"ac_rms_code": 0.7784242199539648
},
"matched_signal_power": 0.038319468321711674,
"residual_power": 0.6202840436902609,
"matched_to_residual_db": -12.091711347697867,
"complex_gain_h": {
"real": -0.010179637303215595,
"imag": 0.014270629544800722,
"phase_rad_at_active_start": 2.1904112551609387
},
"gain_abs": 0.017529286443833295,
"gain_power_abs_h_squared": 0.00030727588322995773,
"cfo_hz": -0.015456037277818255,
"residual_spectrum": {
"residual_band_h1_power_code2": 0.0075272705585461884,
"residual_band_h2_power_code2": 0.005178438128918921,
"residual_band_h3_power_code2": 0.0030044589971250603,
"interpretation": "Diagnostic residual bands at 50/100/150 kHz; not calibrated RF harmonic power"
}
},
{
"label": "L2",
"level_db": -6.0,
"rx_start_sample": 7417219,
"alignment_adjustment_samples": 3,
"full_iq": {
"samples": 800000,
"mean_i": 0.94256,
"mean_q": 1.85174125,
"power_code2": 5.55749625,
"ac_power_code2": 1.2401312394484378,
"rms_code": 2.3574342514691686,
"peak_magnitude_code": 7.280109889280518,
"max_abs_i": 6.0,
"max_abs_q": 7.0,
"papr_db": 9.793966912760688,
"clipped_components_fraction": 0.0,
"clipped_samples_fraction": 0.0,
"symbol_energy_code2_seconds": 0.0002778748125,
"ac_rms_code": 1.1136117992588073
},
"background_label": "GAP_L1_L2",
"background": {
"samples": 160000,
"mean_i": 0.94511875,
"mean_q": 1.8435375,
"power_code2": 4.89561875,
"ac_power_code2": 0.6037387844921858,
"rms_code": 2.2126045173053406,
"peak_magnitude_code": 8.06225774829855,
"max_abs_i": 7.0,
"max_abs_q": 7.0,
"papr_db": 11.231057671986083,
"clipped_components_fraction": 0.0,
"clipped_samples_fraction": 0.0,
"symbol_energy_code2_seconds": 0.0002447809375,
"ac_rms_code": 0.7770062963015073
},
"matched_signal_power": 0.15371936701352645,
"residual_power": 0.7099975700355287,
"matched_to_residual_db": -6.645282748943733,
"complex_gain_h": {
"real": -0.00913356439809709,
"imag": 0.015044605957089683,
"phase_rad_at_active_start": 2.1164192954656
},
"gain_abs": 0.01760006156291179,
"gain_power_abs_h_squared": 0.000309762167018285,
"cfo_hz": -0.02238057704898943,
"residual_spectrum": {
"residual_band_h1_power_code2": 0.016197144376941752,
"residual_band_h2_power_code2": 0.007835153285271243,
"residual_band_h3_power_code2": 0.003956469302302745,
"interpretation": "Diagnostic residual bands at 50/100/150 kHz; not calibrated RF harmonic power"
}
},
{
"label": "L3",
"level_db": 0.0,
"rx_start_sample": 8617220,
"alignment_adjustment_samples": 4,
"full_iq": {
"samples": 800000,
"mean_i": 0.941795,
"mean_q": 1.85647375,
"power_code2": 6.38914875,
"ac_power_code2": 2.055676143535938,
"rms_code": 2.52767655169723,
"peak_magnitude_code": 8.602325267042627,
"max_abs_i": 7.0,
"max_abs_q": 8.0,
"papr_db": 10.637887203839098,
"clipped_components_fraction": 0.0,
"clipped_samples_fraction": 0.0,
"symbol_energy_code2_seconds": 0.00031945743750000003,
"ac_rms_code": 1.4337629314276255
},
"background_label": "GAP_L2_L3",
"background": {
"samples": 160000,
"mean_i": 0.9481,
"mean_q": 1.8589625,
"power_code2": 4.9655625,
"ac_power_code2": 0.6109273135937494,
"rms_code": 2.228354213315289,
"peak_magnitude_code": 6.324555320336759,
"max_abs_i": 5.0,
"max_abs_q": 6.0,
"papr_db": 9.060915387364618,
"clipped_components_fraction": 0.0,
"clipped_samples_fraction": 0.0,
"symbol_energy_code2_seconds": 0.000248278125,
"ac_rms_code": 0.7816183938430246
},
"matched_signal_power": 0.609773488771332,
"residual_power": 1.8217771923731447,
"matched_to_residual_db": -4.753267221075902,
"complex_gain_h": {
"real": -0.009107358713761582,
"imag": 0.015027742846344474,
"phase_rad_at_active_start": 2.1156425161969685
},
"gain_abs": 0.017572052748526183,
"gain_power_abs_h_squared": 0.0003087770377969866,
"cfo_hz": -0.006443832004222744,
"residual_spectrum": {
"residual_band_h1_power_code2": 0.05148134451389538,
"residual_band_h2_power_code2": 0.019651663054535452,
"residual_band_h3_power_code2": 0.008610269373925542,
"interpretation": "Diagnostic residual bands at 50/100/150 kHz; not calibrated RF harmonic power"
}
}
],
"background_windows": [
{
"label": "INITIAL_GUARD",
"tx_relative_start_sample": 320000,
"samples": 160000,
"duration_seconds": 0.02,
"rx_start_sample": 4537216,
"metrics": {
"samples": 160000,
"mean_i": 0.9375625,
"mean_q": 1.83864375,
"power_code2": 4.84290625,
"ac_power_code2": 0.5832719691796884,
"rms_code": 2.200660412239926,
"peak_magnitude_code": 4.47213595499958,
"max_abs_i": 3.0,
"max_abs_q": 4.0,
"papr_db": 6.1592393370019245,
"clipped_components_fraction": 0.0,
"clipped_samples_fraction": 0.0,
"symbol_energy_code2_seconds": 0.00024214531250000003,
"ac_rms_code": 0.7637224425009969
}
},
{
"label": "GAP_L0_L1",
"tx_relative_start_sample": 1760000,
"samples": 160000,
"duration_seconds": 0.02,
"rx_start_sample": 5977216,
"metrics": {
"samples": 160000,
"mean_i": 0.94049375,
"mean_q": 1.8457,
"power_code2": 4.89708125,
"ac_power_code2": 0.6059442662109387,
"rms_code": 2.2129349854887286,
"peak_magnitude_code": 5.0990195135927845,
"max_abs_i": 4.0,
"max_abs_q": 5.0,
"papr_db": 7.250360382891476,
"clipped_components_fraction": 0.0,
"clipped_samples_fraction": 0.0,
"symbol_energy_code2_seconds": 0.0002448540625,
"ac_rms_code": 0.7784242199539648
}
},
{
"label": "GAP_L1_L2",
"tx_relative_start_sample": 2960000,
"samples": 160000,
"duration_seconds": 0.02,
"rx_start_sample": 7177216,
"metrics": {
"samples": 160000,
"mean_i": 0.94511875,
"mean_q": 1.8435375,
"power_code2": 4.89561875,
"ac_power_code2": 0.6037387844921858,
"rms_code": 2.2126045173053406,
"peak_magnitude_code": 8.06225774829855,
"max_abs_i": 7.0,
"max_abs_q": 7.0,
"papr_db": 11.231057671986083,
"clipped_components_fraction": 0.0,
"clipped_samples_fraction": 0.0,
"symbol_energy_code2_seconds": 0.0002447809375,
"ac_rms_code": 0.7770062963015073
}
},
{
"label": "GAP_L2_L3",
"tx_relative_start_sample": 4160000,
"samples": 160000,
"duration_seconds": 0.02,
"rx_start_sample": 8377216,
"metrics": {
"samples": 160000,
"mean_i": 0.9481,
"mean_q": 1.8589625,
"power_code2": 4.9655625,
"ac_power_code2": 0.6109273135937494,
"rms_code": 2.228354213315289,
"peak_magnitude_code": 6.324555320336759,
"max_abs_i": 5.0,
"max_abs_q": 6.0,
"papr_db": 9.060915387364618,
"clipped_components_fraction": 0.0,
"clipped_samples_fraction": 0.0,
"symbol_energy_code2_seconds": 0.000248278125,
"ac_rms_code": 0.7816183938430246
}
},
{
"label": "FINAL_GUARD",
"tx_relative_start_sample": 5440000,
"samples": 160000,
"duration_seconds": 0.02,
"rx_start_sample": 9657216,
"metrics": {
"samples": 160000,
"mean_i": 0.94553125,
"mean_q": 1.8361,
"power_code2": 5.00006875,
"ac_power_code2": 0.7347761952734375,
"rms_code": 2.2360833504142907,
"peak_magnitude_code": 9.486832980505138,
"max_abs_i": 9.0,
"max_abs_q": 9.0,
"papr_db": 12.55266533595234,
"clipped_components_fraction": 0.0,
"clipped_samples_fraction": 0.0,
"symbol_energy_code2_seconds": 0.0002500034375,
"ac_rms_code": 0.8571908744692966
}
}
],
"background_summary": {
"ac_power_min_code2": 0.5832719691796884,
"ac_power_max_code2": 0.7347761952734375,
"ac_rms_min_code": 0.7637224425009969,
"ac_rms_max_code": 0.8571908744692966,
"maximum_to_minimum_ac_power_db": 1.0028397226073626,
"stability_rule_db": 3.0,
"stable": true
},
"comparisons": [
{
"comparison": "L1/L0",
"tx_input_difference_db": 5.9772704691441785,
"rx_matched_power_difference_db": 5.899907694072936,
"linearity_error_db": -0.07736277507124267
},
{
"comparison": "L2/L1",
"tx_input_difference_db": 5.998157271536577,
"rx_matched_power_difference_db": 6.033091127942342,
"linearity_error_db": 0.03493385640576463
},
{
"comparison": "L3/L2",
"tx_input_difference_db": 5.998257379520232,
"rx_matched_power_difference_db": 5.9843995108689985,
"linearity_error_db": -0.013857868651233396
}
],
"capture_valid": true,
"capture_invalid_reasons": [],
"tx_underruns_maximum": 0,
"rx_overruns_maximum": 0,
"rx_clipping": false,
"lab064_to_lab065": {
"rows": [
{
"label": "L0",
"tx_vga_nominal_difference_db": 10.0,
"rx_matched_power_difference_db": 11.179327767173566,
"gain_power_abs_h_squared_difference_db": 11.179327767173568,
"residual_power_difference_db": -0.24327621738947727,
"matched_to_residual_improvement_db": 11.422603984563047,
"complex_gain_h_old": {
"real": 0.0018760642981310563,
"imag": -0.0045080629571906915,
"phase_rad_at_active_start": -1.1764391150184978
},
"complex_gain_h_new": {
"real": -0.0069147093491316635,
"imag": 0.016278708105995708,
"phase_rad_at_active_start": 1.9724722764994853
},
"complex_gain_phase_change_degrees": -179.5806672053524,
"residual_spectral_band_difference_db": {
"h1_50000_hz": 2.869745130254167,
"h2_100000_hz": 2.6545689216728405,
"h3_150000_hz": 2.25865088789477
}
},
{
"label": "L1",
"tx_vga_nominal_difference_db": 10.0,
"rx_matched_power_difference_db": 11.119465772157373,
"gain_power_abs_h_squared_difference_db": 11.119465772157373,
"residual_power_difference_db": -2.3662859353114283,
"matched_to_residual_improvement_db": 13.4857517074688,
"complex_gain_h_old": {
"real": 0.004471119012843702,
"imag": -0.001937685853593986,
"phase_rad_at_active_start": -0.40894566669212534
},
"complex_gain_h_new": {
"real": -0.010179637303215595,
"imag": 0.014270629544800722,
"phase_rad_at_active_start": 2.1904112551609387
},
"complex_gain_phase_change_degrees": 148.9321810702975,
"residual_spectral_band_difference_db": {
"h1_50000_hz": 3.0326799855695237,
"h2_100000_hz": 2.378560725133286,
"h3_150000_hz": 1.3007160057992286
}
},
{
"label": "L2",
"tx_vga_nominal_difference_db": 10.0,
"rx_matched_power_difference_db": 11.323669836462136,
"gain_power_abs_h_squared_difference_db": 11.323669836462136,
"residual_power_difference_db": 1.2772838599246652,
"matched_to_residual_improvement_db": 10.046385976537472,
"complex_gain_h_old": {
"real": 0.0032772531007889995,
"imag": -0.003478187284280655,
"phase_rad_at_active_start": -0.8151334504253348
},
"complex_gain_h_new": {
"real": -0.00913356439809709,
"imag": 0.015044605957089683,
"phase_rad_at_active_start": 2.1164192954656
},
"complex_gain_phase_change_degrees": 167.96559975953804,
"residual_spectral_band_difference_db": {
"h1_50000_hz": 5.594944314904455,
"h2_100000_hz": 4.091263491385863,
"h3_150000_hz": 2.888396722883724
}
},
{
"label": "L3",
"tx_vga_nominal_difference_db": 10.0,
"rx_matched_power_difference_db": 11.287649575482401,
"gain_power_abs_h_squared_difference_db": 11.287649575482403,
"residual_power_difference_db": 3.907580735290519,
"matched_to_residual_improvement_db": 7.380068840191884,
"complex_gain_h_old": {
"real": 0.003384387504328541,
"imag": -0.003391322182689881,
"phase_rad_at_active_start": -0.7864216245934512
},
"complex_gain_h_new": {
"real": -0.009107358713761582,
"imag": 0.015027742846344474,
"phase_rad_at_active_start": 2.1156425161969685
},
"complex_gain_phase_change_degrees": 166.2760271435506,
"residual_spectral_band_difference_db": {
"h1_50000_hz": 7.304592641009381,
"h2_100000_hz": 6.432280867349566,
"h3_150000_hz": 4.996231985221501
}
}
],
"rules_fixed_before_rf": {
"internal_linear_max_abs_error_db": 1.0,
"compression_upper_step_error_at_most_db": -1.5,
"meaningful_matched_gain_median_at_least_db": 3.0,
"meaningful_ratio_improvement_median_at_least_db": 3.0,
"maximum_level_to_level_gain_spread_db": 3.0,
"residual_tracks_if_within_matched_gain_db": 3.0
}
},
"classification": {
"case": "LINEAR_GAIN_IMPROVEMENT",
"summary": {
"median_matched_gain_db": 11.233488671327983,
"median_residual_gain_db": 0.5170038212675939,
"median_matched_to_residual_improvement_db": 10.73449498055026,
"matched_gain_level_spread_db": 0.20420406430476312,
"maximum_internal_linearity_error_db": 0.07736277507124267,
"upper_step_linearity_error_db": -0.013857868651233396
},
"rules_fixed_before_rf": {
"internal_linear_max_abs_error_db": 1.0,
"compression_upper_step_error_at_most_db": -1.5,
"meaningful_matched_gain_median_at_least_db": 3.0,
"meaningful_ratio_improvement_median_at_least_db": 3.0,
"maximum_level_to_level_gain_spread_db": 3.0,
"residual_tracks_if_within_matched_gain_db": 3.0
}
}
}

View File

@@ -0,0 +1,17 @@
{
"lab": "Lab065",
"stage": "SOFTWARE_GATE",
"rf_tx_count": 0,
"passed": true,
"tx_sha256": "fa260719659b2984fe29588238415622cd75c6daefec6cc9130ba1bf851bd3f9",
"waveform_unchanged_from_lab064": true,
"lab064_baseline_classification": "LINEAR_BUT_WEAK",
"classification_rules": {
"internal_linear_max_abs_error_db": 1.0,
"compression_upper_step_error_at_most_db": -1.5,
"meaningful_matched_gain_median_at_least_db": 3.0,
"meaningful_ratio_improvement_median_at_least_db": 3.0,
"maximum_level_to_level_gain_spread_db": 3.0,
"residual_tracks_if_within_matched_gain_db": 3.0
}
}

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

@@ -663,3 +663,81 @@
- **Независимая проверка:** прямой пересчёт из TX/RX CS8 подтвердил все SHA,
счётчики, фоны, matched/residual/`h`, ступени и спектральные полосы;
расхождений нет. Quick gate и pytest после RF:986 passed,0 failed.
## Lab065: измерение уровня при TX VGA 20 дБ
- **База:** Lab064 зафиксирована коммитом
`cff226e0bf9e36a627829ea11a4ca3292ae20953`; push не выполнялся.
- **Единственное изменение параметра:** TX VGA 10 → 20 дБ. Частота 1400 МГц,
RX LNA/VGA 24/24 дБ, усилители, геометрия, антенны и кабели сохранены.
- **Waveform:** точная копия Lab064, 6000000 отсчётов, 0,75 с; SHA-256
`fa260719659b2984fe29588238415622cd75c6daefec6cc9130ba1bf851bd3f9`.
- **Подготовлено:** RX-first runner, максимальные счётчики потерь, сравнение
линейности внутри Lab065 и по одинаковым уровням Lab064/Lab065, численные
правила классификации. Software gate PASS.
- **RX-only preflight:** обе HackRF и роли подтверждены, reg29=0x26 у MASTER и
SLAVE, CLKIN обнаружен на третьем опросе через201,986мс. Фон:800000 отсчётов,
AC RMS1,056565 кода, max|I|/|Q|=6/8, clipping0, overruns0. Cleanup PASS.
- **Граница:** `RF TX=0`. Lab065 не коммитировалась. Нужны рассмотрение
подготовки и отдельное прямое разрешение на один TX; Lab066 не начинать.
- [Текущее состояние](lab065_current_state.md),
[RX-only отчёт](lab065_rx_preflight.md),
[подготовка](../data/processed/lab065/preparation.json),
[software gate](../data/processed/lab065/software_gate.json),
[preflight](../data/processed/lab065/preflight_result.json).
- **Единственный RF-сеанс:** TX1, автоматических повторов0; underrun/overrun/
clipping0, cleanup PASS, процессы CLEAR. RX16000000 отсчётов, SHA-256
`845d7b7dcaef70e179ab69e6ba0041601859630786d2f4d51c16a8c1c7869d7e`.
- **Линейность:** ошибки ступеней −0,077363/+0,034934/−0,013858дБ, компрессии
L2/L3 нет. Медианный прирост matched power+11,233489дБ, изменение остатка
+0,517004дБ, улучшение matched/residual+10,734495дБ.
- **Результат:** `LINEAR_GAIN_IMPROVEMENT`. Фон стабилен; диагностические полосы
остатка растут слабее matched power. [RF-отчёт](lab065_rf_result.md),
[RF JSON](../data/processed/lab065/rf_result.json),
[capture JSON](../data/processed/lab065/capture_result.json).
- **STOP:** `RF TX=0`; повторов и изменения усилений не было. Lab065 не
коммитировалась, push отсутствует, Lab066 не создана.
- **Независимая проверка:** прямой пересчёт CS8 подтвердил SHA, счётчики,
локализацию, фон, matched/residual, `h`, CFO и спектральные полосы. Расхождений
нет. 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,70 @@
# Lab065: подготовка измерения при TX VGA 20 дБ
Lab065 повторяет измерение Lab064 с одним изменением аппаратного параметра:
TX VGA повышен с 10 до 20 дБ. Частота 1400000000 Гц, RX LNA/VGA 24/24 дБ,
геометрия, антенны, кабели, общая опора и цифровой waveform сохранены.
TX-файл Lab065 является побайтовой копией Lab064. Его SHA-256 должен быть равен
`fa260719659b2984fe29588238415622cd75c6daefec6cc9130ba1bf851bd3f9`.
Длина остаётся равной 6000000 комплексных отсчётов и 0,75 с. Ступени
−18/−12/−6/0 дБ, PRBS7, BPSK, RRC, 20000 символов/с, Fs 8000000 отсчётов/с и
цифровая несущая +50000 Гц не меняются.
## Сравнение с Lab064
Для L0, L1, L2 и L3 анализатор выдаёт изменение согласованной мощности,
`|h|²`, остаточной мощности и matched/residual. Номинальное изменение TX VGA
равно +10 дБ и приводится как справочная величина, а не как условие PASS.
До RF зафиксированы численные правила:
- линейность внутри Lab065: максимальная абсолютная ошибка шага не более 1 дБ;
- компрессия: ошибка верхнего шага не более −1,5 дБ;
- заметный прирост: медиана прироста matched power не менее 3 дБ;
- заметное улучшение matched/residual: медиана не менее 3 дБ;
- согласованность прироста четырёх уровней: разброс не более 3 дБ;
- остаток считается растущим почти как сигнал, если его медианный прирост
отстаёт от медианного прироста matched power не более чем на 3 дБ.
Эти пороги нужны для воспроизводимой классификации конкретного опыта. Они не
заменяют измерение абсолютной RF-мощности.
## Аппаратная граница
Подготовка допускает программные проверки, common-clock preflight и RX-only
запись фона. Любая команда с `-t` внутри preflight запрещена. Передача Lab065
требует нового прямого разрешения пользователя и в текущий этап не входит.
Автоматических повторов и изменения усилений после ошибки нет.
`RF TX = 0`.
## Результат подготовки
Подготовка выполнена от HEAD
`cff226e0bf9e36a627829ea11a4ca3292ae20953`. Software gate прошёл. Побайтовая
копия waveform имеет требуемый SHA-256, подготовленная TX-команда содержит
TX VGA 20 дБ, а RX-команда сохраняет LNA/VGA 24/24 дБ.
Common-clock и RX-only preflight прошёл. Обе HackRF One r9 обнаружены с
прежними ролями. MASTER и SLAVE имеют reg29 `0x26`; SLAVE CLKIN обнаружен на
третьем опросе через 201,986 мс. В RX-фоне из 800000 отсчётов AC RMS равен
1,056565 кода, max |I|/|Q| равны 6/8, clipping и overruns отсутствуют.
Процессы до и после проверки имеют состояние CLEAR, ошибок очистки нет.
Lab065 готова к рассмотрению перед одним RF-сеансом. Текущая подготовка такого
разрешения не содержит. `RF TX = 0`.
## Единственный RF-сеанс
После отдельного прямого разрешения выполнен один TX. Общая опора прошла:
MASTER/SLAVE reg29 `0x26`, SLAVE CLKIN обнаружен на втором опросе через
136,897 мс. TX underrun, RX overrun, clipping и ошибки очистки отсутствуют.
Повторов не было. RX содержит 16000000 отсчётов, SHA-256
`845d7b7dcaef70e179ab69e6ba0041601859630786d2f4d51c16a8c1c7869d7e`.
Медианный прирост matched power относительно Lab064 равен +11,233489 дБ,
медианное изменение остатка +0,517004 дБ, улучшение matched/residual
+10,734495 дБ. Ошибки ступеней равны −0,077363/+0,034934/−0,013858 дБ.
Признаков компрессии нет. Классификация: `LINEAR_GAIN_IMPROVEMENT`.
[Полный RF-отчёт](lab065_rf_result.md). `RF TX = 0`.

114
docs/lab065_rf_result.md Normal file
View File

@@ -0,0 +1,114 @@
# Lab065: один OTA-сеанс при TX VGA 20 дБ
15 сентября 2026 года выполнен один разрешённый OTA-сеанс Lab065 на частоте
1400000000 Гц. Передача использовала точную копию waveform Lab064 с SHA-256
`fa260719659b2984fe29588238415622cd75c6daefec6cc9130ba1bf851bd3f9`.
Автоматических и ручных повторов не было.
## Аппаратная валидность
Перед TX обе HackRF One r9 и их роли подтверждены. MASTER и SLAVE имели reg29
`0x26`; SLAVE CLKIN обнаружен на втором опросе через 136,897 мс. RX стартовал
раньше TX. Результат сеанса:
```text
COMPLETED TX SESSIONS = 1
AUTOMATIC TX REPEATS = 0
TX UNDERRUN MAXIMUM = 0
RX OVERRUN MAXIMUM = 0
RX CLIPPING = NO
CLEANUP ERRORS = []
SDR PROCESSES = CLEAR
```
RX содержит 16000000 комплексных отсчётов, 32000000 байт и 2,0 с. SHA-256:
`845d7b7dcaef70e179ab69e6ba0041601859630786d2f4d51c16a8c1c7869d7e`.
Максимумы всей записи равны `|I|=10` и `|Q|=10`; rail-кодов нет.
## Линейность внутри Lab065
| Переход | TX, дБ | RX matched, дБ | Ошибка, дБ |
|---|---:|---:|---:|
| L0 → L1 | +5,977270 | +5,899908 | −0,077363 |
| L1 → L2 | +5,998157 | +6,033091 | +0,034934 |
| L2 → L3 | +5,998257 | +5,984400 | −0,013858 |
Максимальная абсолютная ошибка равна 0,077363 дБ при границе 1 дБ. Ошибка
верхней ступени равна −0,013858 дБ при пороге компрессии −1,5 дБ. Согласованная
мощность L3 выросла относительно L2 на 5,984400 дБ. Признаков компрессии на
L2/L3 нет.
## Сравнение Lab064 и Lab065
| Уровень | Matched, дБ | Остаток, дБ | Улучшение matched/residual, дБ | Изменение \|h\|², дБ |
|---|---:|---:|---:|---:|
| L0 | +11,179328 | −0,243276 | +11,422604 | +11,179328 |
| L1 | +11,119466 | −2,366286 | +13,485752 | +11,119466 |
| L2 | +11,323670 | +1,277284 | +10,046386 | +11,323670 |
| L3 | +11,287650 | +3,907581 | +7,380069 | +11,287650 |
Медианный прирост согласованной мощности равен +11,233489 дБ. Его разброс по
четырём уровням составляет 0,204204 дБ. Медианное изменение остатка равно
+0,517004 дБ, а медианное улучшение matched/residual равно +10,734495 дБ.
При TX VGA 20 дБ matched/residual для L0/L1/L2/L3 равен
−20,663889/−12,091711/−6,645283/−4,753267 дБ. Отношение улучшилось на каждом
уровне, хотя даже на L3 согласованная составляющая остаётся слабее остатка.
## Комплексный коэффициент передачи
| Уровень | h Lab064 | h Lab065 | \|h\| Lab064 | \|h\| Lab065 | Изменение фазы, градусы |
|---|---|---|---:|---:|---:|
| L0 | 0,001876 − j0,004508 | −0,006915 + j0,016279 | 0,004883 | 0,017686 | −179,581 |
| L1 | 0,004471 − j0,001938 | −0,010180 + j0,014271 | 0,004873 | 0,017529 | +148,932 |
| L2 | 0,003277 − j0,003478 | −0,009134 + j0,015045 | 0,004779 | 0,017600 | +167,966 |
| L3 | 0,003384 − j0,003391 | −0,009107 + j0,015028 | 0,004791 | 0,017572 | +166,276 |
Модуль `h` внутри Lab065 почти постоянен: 0,017529…0,017686. Его мощность
относительно Lab064 выросла на 11,119…11,324 дБ, что согласуется с matched
power. Фаза между отдельными сеансами не имеет непрерывной временной привязки,
поэтому приведённый поворот является диагностикой конкретных записей, а не
калиброванной фазовой характеристикой тракта.
## Фон и остаточные полосы
Пять нулевых окон Lab065 имеют AC RMS 0,763722…0,857191 кода. Разброс фоновой
AC-мощности равен 1,002840 дБ при границе 3 дБ, поэтому фон классифицирован как
стабильный. В Lab064 разброс составлял 0,119530 дБ; фон Lab065 стабильный, но
межоконный разброс стал больше.
Изменение диагностических полос остатка относительно Lab064:
| Уровень | 50 кГц, дБ | 100 кГц, дБ | 150 кГц, дБ |
|---|---:|---:|---:|
| L0 | +2,869745 | +2,654569 | +2,258651 |
| L1 | +3,032680 | +2,378561 | +1,300716 |
| L2 | +5,594944 | +4,091263 | +2,888397 |
| L3 | +7,304593 | +6,432281 | +4,996232 |
На верхних уровнях остаточные полосы выросли, особенно около 50 кГц. На L3 их
рост остаётся ниже прироста matched power +11,287650 дБ, а matched/residual
улучшился на 7,380069 дБ. Эти цифровые полосы не являются калиброванным
измерением RF-гармоник.
## Итог
```text
CLASSIFICATION = LINEAR_GAIN_IMPROVEMENT
```
Повышение TX VGA с 10 до 20 дБ дало устойчивый прирост согласованной принятой
мощности примерно на 11,2 дБ без потери линейности. Остаток вырос существенно
меньше, поэтому отношение полезной составляющей к остатку улучшилось на всех
четырёх уровнях. Верхняя ступень не показывает компрессии.
Результат остаётся относительным измерением всего OTA-тракта. Он не определяет
абсолютную RF-мощность и не доказывает прохождение полного PHY-пакета, FEC, CRC,
T2 или JPEG.
Независимая проверка пересчитала сохранённый CS8, локализацию, фон, matched,
residual, `h`, CFO, внутреннюю линейность и сравнение с Lab064. Расхождений нет.
Рекомендуемый следующий одиночный шаг: рассмотреть scope Lab065 и отдельно
разрешить локальный коммит. Новую RF-передачу для этого выполнять не требуется.
`RF TX = 0`. Lab065 не коммитировалась, push не выполнялся, Lab066 не создана.

View File

@@ -0,0 +1,83 @@
# Lab065: подготовка и RX-only preflight
15 сентября 2026 года подготовлена Lab065 с TX VGA 20 дБ. Пользователь
подтвердил неизменность расстояния около 1,2 м, прямой видимости, положения TX
возле окна и RX на столе, антенн, поляризации, USB, общей опоры и RF-кабелей.
## Неизменность сигнала
TX-файл Lab065 является побайтовой копией файла Lab064:
```text
TX samples = 6000000
TX bytes = 12000000
TX duration = 0.750000 s
TX SHA-256 = fa260719659b2984fe29588238415622cd75c6daefec6cc9130ba1bf851bd3f9
WAVEFORM UNCHANGED FROM LAB064 = YES
```
Сохранены PRBS7, BPSK, RRC, ступени −18/−12/−6/0 дБ, интервалы и нулевые
защитные участки, 20000 символов/с, Fs 8000000 комплексных отсчётов/с и
цифровая несущая +50000 Гц. Из аппаратных параметров изменён только TX VGA:
10 → 20 дБ. RX LNA/VGA остаются равными 24/24 дБ; RF amplifier и Bias-Tee
выключены.
Software gate прошёл. Он подтвердил SHA, пригодность результата Lab064 как
базы сравнения, синтетическую проверку линейности и наличие численных правил
классификации, зафиксированных до RF.
## Common clock
Перед аппаратными командами проверка процессов через psutil осмотрела 289
процессов и получила CLEAR. Обнаружены обе HackRF One r9 с firmware и
libhackrf 2024.02.1. Роли совпали с Lab064:
- MASTER `000000000000000097c063dc323b97d3`;
- SLAVE `000000000000000097c063dc2e5327d3`.
Результат общей опоры:
```text
MASTER reg29 = 0x26
SLAVE reg29 = 0x26
SLAVE CLKIN = DETECTED
CLKIN query = 3
time to detect = 201.986 ms
```
Первые два опроса не обнаружили CLKIN, третий опрос прошёл в пределах штатного
окна ожидания. Итоговый common-clock gate имеет статус PASS.
## RX-only фон
SLAVE записал 800000 комплексных отсчётов при 1400000000 Гц и RX LNA/VGA
24/24 дБ.
| Метрика | Значение |
|---|---:|
| Полный RMS IQ | 2,333068 кода |
| AC RMS после удаления среднего | 1,056565 кода |
| Средние I / Q | 0,948896 / 1,851074 кода |
| max \|I\| / \|Q\| | 6 / 8 кодов |
| Максимальная комплексная амплитуда | 8,062258 кода |
| Clipping | 0 / 800000 отсчётов |
| RX overruns, максимум / финал | 0 / 0 |
| Неожиданно сильный сигнал | нет |
SHA-256 RX-фона:
`be3c61dfa1bc40d1a9f6777689026804a1ccfc340b82680d3ec5304ddd68f78c`.
Оба CLKOUT выключены без ошибок. Повторная проверка процессов получила CLEAR.
Команд передачи не было.
```text
LAB065 PREPARATION = PASS
COMMON CLOCK = PASS
RX PREFLIGHT = PASS
RF TX = 0
READY FOR SINGLE LAB065 TX = YES
WAITING FOR EXPLICIT RF AUTHORIZATION = YES
```
Готовность означает, что программные и аппаратные входные проверки пройдены.
Она не разрешает передачу и не предсказывает результат измерения.

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,623 @@
"""Lab065: та же лестница уровней при TX VGA 20 дБ.
Подготовка, программная проверка и preflight не передают RF. Единственный
аппаратный сеанс доступен только отдельной командой с явным разрешением.
"""
from __future__ import annotations
from dataclasses import asdict
import argparse
import hashlib
import json
from pathlib import Path
import shutil
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 lab054_hardware_control as lab054
from experiments import lab056_capture_runner as hardware
from experiments import lab064_rf_level_linearity as lab064
ROOT = Path(__file__).resolve().parents[1]
RAW = ROOT / "data/raw/lab065"
OUTPUT = ROOT / "data/processed/lab065"
LAB064_RAW = ROOT / "data/raw/lab064"
LAB064_RESULT = ROOT / "data/processed/lab064/rf_result.json"
EXPECTED_TX_SHA256 = "fa260719659b2984fe29588238415622cd75c6daefec6cc9130ba1bf851bd3f9"
TX_VGA_DB = 20
TX_VGA_DIFFERENCE_DB = 10.0
MEANINGFUL_GAIN_DB = 3.0
MEANINGFUL_RATIO_IMPROVEMENT_DB = 3.0
MAX_LEVEL_TO_LEVEL_GAIN_SPREAD_DB = 3.0
RESIDUAL_TRACKING_MARGIN_DB = 3.0
POINT = hardware.OperatingPoint(**asdict(lab064.POINT))
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 save_text_new(path: Path, value: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("x", encoding="utf-8") as stream:
stream.write(value)
def sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
while block := stream.read(1024 * 1024):
digest.update(block)
return digest.hexdigest()
def parameters() -> dict:
result = lab064.parameters()
result["tx_vga_db"] = TX_VGA_DB
result["geometry_unchanged_from_lab064"] = True
result["waveform_source"] = "Lab064 exact CS8 copy"
return result
def transfer_command(path: Path | str, samples: int, *, transmit: bool = False) -> list[str]:
command = lab064.transfer_command(path, samples, transmit=transmit)
if transmit:
command[command.index("-x") + 1] = str(TX_VGA_DB)
return command
def validate_lab064_waveform() -> dict:
source = LAB064_RAW / "tx_level_staircase.cs8"
metadata = json.loads((LAB064_RAW / "tx_metadata.json").read_text(encoding="utf-8"))
digest = sha256(source)
if (
digest != EXPECTED_TX_SHA256
or metadata.get("tx_sha256") != EXPECTED_TX_SHA256
or source.stat().st_size != 2 * lab064.TX_SAMPLES
or metadata.get("segments") != lab064.prepared_segments()[0]
):
raise RuntimeError("Waveform Lab064 не совпадает с зафиксированным образцом")
return metadata
def validate_prepared() -> dict:
metadata = json.loads((RAW / "tx_metadata.json").read_text(encoding="utf-8"))
path = RAW / "tx_level_staircase.cs8"
if (
metadata.get("lab") != "Lab065"
or metadata.get("stage") != "PREPARATION_ONLY"
or metadata.get("rf_tx_count") != 0
or metadata.get("parameters") != parameters()
or metadata.get("tx_sha256") != EXPECTED_TX_SHA256
or metadata.get("source_tx_sha256") != EXPECTED_TX_SHA256
or path.stat().st_size != 2 * lab064.TX_SAMPLES
or sha256(path) != EXPECTED_TX_SHA256
):
raise RuntimeError("Подготовленный Lab065 TX или параметры изменились")
return metadata
def prepare() -> dict:
"""Скопировать точные байты Lab064 в отдельный каталог Lab065."""
source_metadata = validate_lab064_waveform()
RAW.mkdir(parents=True, exist_ok=True)
OUTPUT.mkdir(parents=True, exist_ok=True)
target = RAW / "tx_level_staircase.cs8"
if target.exists():
raise FileExistsError("TX-файл Lab065 уже существует")
shutil.copyfile(LAB064_RAW / "tx_level_staircase.cs8", target)
if sha256(target) != EXPECTED_TX_SHA256:
target.unlink()
raise RuntimeError("Копия waveform Lab065 не прошла SHA-256")
result = {
"lab": "Lab065",
"stage": "PREPARATION_ONLY",
"rf_tx_count": 0,
"head": subprocess.check_output(["git", "rev-parse", "HEAD"], text=True).strip(),
"parameters": parameters(),
"segments": source_metadata["segments"],
"tx_samples": lab064.TX_SAMPLES,
"tx_bytes": 2 * lab064.TX_SAMPLES,
"tx_duration_seconds": lab064.TX_SAMPLES / lab064.SAMPLE_RATE,
"tx_sha256": sha256(target),
"source_tx_sha256": source_metadata["tx_sha256"],
"waveform_unchanged_from_lab064": True,
"automatic_retry": False,
"tx_command_prepared_not_run": transfer_command(target, lab064.TX_SAMPLES, transmit=True),
"rx_command_prepared_not_run": transfer_command(RAW / "rx_stream.cs8", lab064.RX_SAMPLES),
}
save_new(RAW / "tx_metadata.json", result)
save_new(OUTPUT / "preparation.json", result)
return result
def compare_sessions(old_result: dict, new_rows: list[dict]) -> dict:
"""Сопоставить одинаковые цифровые уровни Lab064 и Lab065."""
old_by_label = {row["label"]: row for row in old_result["rows"]}
rows = []
for new in new_rows:
old = old_by_label[new["label"]]
comparison = {
"label": new["label"],
"tx_vga_nominal_difference_db": TX_VGA_DIFFERENCE_DB,
"rx_matched_power_difference_db": lab064.difference_db(
new["matched_signal_power"], old["matched_signal_power"]
),
"gain_power_abs_h_squared_difference_db": lab064.difference_db(
new["gain_power_abs_h_squared"], old["gain_power_abs_h_squared"]
),
"residual_power_difference_db": lab064.difference_db(
new["residual_power"], old["residual_power"]
),
"matched_to_residual_improvement_db": (
new["matched_to_residual_db"] - old["matched_to_residual_db"]
),
}
if "complex_gain_h" in new and "residual_spectrum" in new:
old_gain = complex(
old["complex_gain_h"]["real"], old["complex_gain_h"]["imag"]
)
new_gain = complex(
new["complex_gain_h"]["real"], new["complex_gain_h"]["imag"]
)
comparison["complex_gain_h_old"] = old["complex_gain_h"]
comparison["complex_gain_h_new"] = new["complex_gain_h"]
comparison["complex_gain_phase_change_degrees"] = float(
np.degrees(np.angle(new_gain / old_gain))
)
comparison["residual_spectral_band_difference_db"] = {
f"h{harmonic}_{harmonic * lab064.CARRIER_HZ}_hz": lab064.difference_db(
new["residual_spectrum"][f"residual_band_h{harmonic}_power_code2"],
old["residual_spectrum"][f"residual_band_h{harmonic}_power_code2"],
)
for harmonic in (1, 2, 3)
}
rows.append(comparison)
return {"rows": rows, "rules_fixed_before_rf": classification_rules()}
def classification_rules() -> dict:
return {
"internal_linear_max_abs_error_db": 1.0,
"compression_upper_step_error_at_most_db": -1.5,
"meaningful_matched_gain_median_at_least_db": MEANINGFUL_GAIN_DB,
"meaningful_ratio_improvement_median_at_least_db": MEANINGFUL_RATIO_IMPROVEMENT_DB,
"maximum_level_to_level_gain_spread_db": MAX_LEVEL_TO_LEVEL_GAIN_SPREAD_DB,
"residual_tracks_if_within_matched_gain_db": RESIDUAL_TRACKING_MARGIN_DB,
}
def classify_result(new_result: dict, cross_comparison: dict) -> dict:
"""Выбрать один из пяти заранее заданных результатов Lab065."""
if not new_result.get("capture_valid"):
return {"case": "INVALID_CAPTURE", "rules_fixed_before_rf": classification_rules()}
internal_errors = [row["linearity_error_db"] for row in new_result["comparisons"]]
upper_error = internal_errors[-1]
cross_rows = cross_comparison["rows"]
matched = np.asarray([row["rx_matched_power_difference_db"] for row in cross_rows])
residual = np.asarray([row["residual_power_difference_db"] for row in cross_rows])
improvements = np.asarray([row["matched_to_residual_improvement_db"] for row in cross_rows])
median_matched = float(np.median(matched))
median_residual = float(np.median(residual))
median_improvement = float(np.median(improvements))
spread = float(np.ptp(matched))
linear = max(abs(value) for value in internal_errors) <= 1.0
if upper_error <= -1.5:
case = "COMPRESSION_AT_HIGHER_TX_GAIN"
elif median_matched < MEANINGFUL_GAIN_DB:
case = "NO_MEANINGFUL_GAIN"
elif (
linear
and spread <= MAX_LEVEL_TO_LEVEL_GAIN_SPREAD_DB
and median_improvement >= MEANINGFUL_RATIO_IMPROVEMENT_DB
):
case = "LINEAR_GAIN_IMPROVEMENT"
elif (
median_residual >= median_matched - RESIDUAL_TRACKING_MARGIN_DB
and median_improvement < MEANINGFUL_RATIO_IMPROVEMENT_DB
):
case = "RESIDUAL_RISES_WITH_SIGNAL"
else:
case = "NOT_LOCALIZED"
return {
"case": case,
"summary": {
"median_matched_gain_db": median_matched,
"median_residual_gain_db": median_residual,
"median_matched_to_residual_improvement_db": median_improvement,
"matched_gain_level_spread_db": spread,
"maximum_internal_linearity_error_db": max(abs(value) for value in internal_errors),
"upper_step_linearity_error_db": upper_error,
},
"rules_fixed_before_rf": classification_rules(),
}
def software_gate() -> dict:
metadata = validate_prepared()
old_result = json.loads(LAB064_RESULT.read_text(encoding="utf-8"))
synthetic = lab064.synthetic_linearity_gate()
result = {
"lab": "Lab065",
"stage": "SOFTWARE_GATE",
"rf_tx_count": 0,
"passed": bool(synthetic["passed"] and old_result["capture_valid"]),
"tx_sha256": metadata["tx_sha256"],
"waveform_unchanged_from_lab064": metadata["waveform_unchanged_from_lab064"],
"lab064_baseline_classification": old_result["classification"]["case"],
"classification_rules": classification_rules(),
}
save_new(OUTPUT / "software_gate.json", result)
if not result["passed"]:
raise RuntimeError("Lab065 software gate FAIL")
return result
def preflight(*, runner=None, process_checker=None, clock_checker=None, sleep_function=time.sleep) -> dict:
"""Проверить общую опору и 0,1 с RX-фона при полном запрете TX."""
metadata = validate_prepared()
software = json.loads((OUTPUT / "software_gate.json").read_text(encoding="utf-8"))
if not software.get("passed") or software.get("tx_sha256") != metadata["tx_sha256"]:
raise RuntimeError("Нет действующего Lab065 software gate")
save_new(RAW / "preflight_attempt.json", {
"lab": "Lab065", "rf_tx_count": 0, "point": asdict(POINT),
"parameters": parameters(), "tx_sha256": metadata["tx_sha256"],
})
actual_runner = runner or lab054.clock._default_command_runner
process_checker = process_checker or lab054.check_processes
clock_checker = clock_checker or hardware.common_clock
def rx_only(command: list[str]):
if "-t" in command:
raise PermissionError("TX запрещён в Lab065 preflight")
return actual_runner(command)
result = {
"lab": "Lab065", "stage": "COMMON_CLOCK_AND_RX_ONLY_PREFLIGHT",
"passed": False, "rf_tx_count": 0, "stop_before_tx": True,
"point": asdict(POINT), "parameters": parameters(),
"tx_sha256": metadata["tx_sha256"],
}
clock_started = False
try:
before = process_checker()
result["process_before"] = before.to_dict()
lab054.require_clear(before)
result["inventory"] = lab054.phy.lab045._verify_device_inventory(rx_only)
clock_started = True
result["clock"] = clock_checker(
POINT, rx_only, lab054.clock.TransmitGuard(), sleep_function=sleep_function
)
hardware.require_clock_pass(result["clock"])
path = RAW / "preflight_rx.cs8"
captured = rx_only(transfer_command(path, lab064.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 * lab064.PREFLIGHT_SAMPLES:
raise RuntimeError("Неверная длина Lab065 preflight RX")
level = hardware.receive_level(path)
level.pop("symbol_energy_code2_seconds", None)
level["symbol_energy"] = "NOT_APPLICABLE_TO_BACKGROUND"
level["ac_rms_code"] = float(np.sqrt(max(0.0, level["ac_power_code2"])))
level["unexpected_strong_signal"] = bool(
level["ac_rms_code"] >= lab064.STRONG_AC_RMS_CODE
or level["max_abs_i"] >= lab064.STRONG_COMPONENT_CODE
or level["max_abs_q"] >= lab064.STRONG_COMPONENT_CODE
)
result["rx_level"] = level
result["rx_sha256"] = sha256(path)
result["rx_overruns"] = result["rx_counter_summary"]["final_count"]
if level["saturation_detected"] or level["unexpected_strong_signal"]:
raise RuntimeError("RX-only уровень не прошёл gate; 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("Lab065 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 для конфигурации Lab065")
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 сеанс после отдельного прямого разрешения."""
if authorization is not True:
raise PermissionError("Нужно отдельное разрешение на единственный RF-сеанс Lab065")
metadata = validate_prepared()
require_preflight(metadata)
receive_path = RAW / "rx_stream.cs8"
if receive_path.exists() or (RAW / "capture_result.json").exists():
raise FileExistsError("Аппаратный результат Lab065 уже существует")
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": "Lab065", "authorized_once": True, "automatic_retry": False,
"point": asdict(POINT), "parameters": parameters(),
"tx_sha256": metadata["tx_sha256"],
})
result = {
"lab": "Lab065", "stage": "SINGLE_RF_LEVEL_CAPTURE", "passed": False,
"rf_tx_count": 0, "automatic_tx_repeats": 0, "point": asdict(POINT),
"parameters": parameters(), "tx_sha256": metadata["tx_sha256"],
}
guard = lab054.clock.TransmitGuard()
process = None
clock_started = False
try:
before = process_checker()
result["process_before"] = before.to_dict()
lab054.require_clear(before)
result["inventory"] = lab054.phy.lab045._verify_device_inventory(actual_runner)
clock_started = True
result["clock"] = clock_checker(
POINT, actual_runner, guard, sleep_function=sleep_function
)
hardware.require_clock_pass(result["clock"])
process = process_starter(
transfer_command(receive_path, lab064.RX_SAMPLES),
RAW / "rx_stdout.txt", RAW / "rx_stderr.txt",
)
sleep_function(lab064.RX_LEAD_SECONDS)
if process.poll() is not None:
raise RuntimeError("RX завершился до TX")
before_tx = hardware.receive_level(receive_path, tail_samples=65_536)
before_tx.pop("symbol_energy_code2_seconds", None)
result["rx_level_before_tx"] = before_tx
if before_tx["saturation_detected"]:
raise RuntimeError("RX clipping до TX; RF TX = 0")
guard.register_transmit()
result["rf_tx_count"] = guard.transmit_calls
transmitted = actual_runner(
transfer_command(RAW / "tx_level_staircase.cs8", lab064.TX_SAMPLES, transmit=True)
)
tx_stdout, tx_stderr = transmitted.stdout or "", transmitted.stderr or ""
save_text_new(RAW / "tx_stdout.txt", tx_stdout)
save_text_new(RAW / "tx_stderr.txt", tx_stderr)
result["tx_process"] = {
"returncode": transmitted.returncode,
"stdout_file": "tx_stdout.txt", "stderr_file": "tx_stderr.txt",
}
result["tx_counter_summary"] = lab064.counter_summary(
tx_stdout + tx_stderr, "underrun"
)
if transmitted.returncode:
raise RuntimeError("Единственный TX завершился ошибкой; повтор запрещён")
if process.wait(lab064.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"] / lab064.SAMPLE_RATE
if result["rx_bytes"] != 2 * lab064.RX_SAMPLES:
raise RuntimeError("Повреждённая длина RX; повтор запрещён")
rx_stdout = (RAW / "rx_stdout.txt").read_text(encoding="utf-8")
rx_stderr = (RAW / "rx_stderr.txt").read_text(encoding="utf-8")
result["rx_counter_summary"] = lab064.counter_summary(
rx_stdout + rx_stderr, "overrun"
)
result["rx_sha256"] = sha256(receive_path)
level = hardware.receive_level(receive_path)
level.pop("symbol_energy_code2_seconds", None)
result["rx_level"] = level
result["rx_clipping"] = bool(level["saturation_detected"])
result["passed"] = True
validity = lab064.capture_validity(result)
if not validity["valid"]:
result["passed"] = False
raise RuntimeError("Запись невалидна; результат сохранён, повтор запрещён")
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("Lab065 RF-сеанс завершён с ошибкой; повтор запрещён")
return result
def analyze_capture() -> dict:
"""Рассчитать метрики Lab065 и прямое сравнение с Lab064."""
metadata = validate_prepared()
rx_path = RAW / "rx_stream.cs8"
capture = json.loads((RAW / "capture_result.json").read_text(encoding="utf-8"))
if capture.get("rf_tx_count") != 1 or capture.get("rx_sha256") != sha256(rx_path):
raise RuntimeError("Нет подтверждённой единственной RX-записи Lab065")
tx = lab064.energy.Cs8File(RAW / "tx_level_staircase.cs8")
rx = lab064.energy.Cs8File(rx_path)
location = lab064.locate_waveform(tx, rx)
background_rows = []
for window in lab064.background_windows():
rx_start = location["start_sample"] + window["tx_relative_start_sample"]
background_rows.append({
**window, "rx_start_sample": rx_start,
"metrics": lab064._window_metrics(rx.read(rx_start, window["samples"])),
})
background_by_label = {row["label"]: row for row in background_rows}
preceding = {"L0": "INITIAL_GUARD", "L1": "GAP_L0_L1",
"L2": "GAP_L1_L2", "L3": "GAP_L2_L3"}
rows = []
for segment in metadata["segments"]:
tx_start = segment["start_sample"]
nominal_rx_start = location["start_sample"] + tx_start
fitted = lab064.energy.analyze_rx(
lab064.lab056.OffsetSource(tx, tx_start), rx, [nominal_rx_start],
lab064.SEGMENT_SAMPLES, lab064.SAMPLES_PER_SYMBOL,
)["frames"][0]
actual_start = fitted["start"]
active_reference = tx.read(
tx_start + lab064.TRIM_SAMPLES,
lab064.SEGMENT_SAMPLES - 2 * lab064.TRIM_SAMPLES,
)
active_received = rx.read(
actual_start + lab064.TRIM_SAMPLES,
lab064.SEGMENT_SAMPLES - 2 * lab064.TRIM_SAMPLES,
)
complex_gain = lab064.lab056.complex_gain(
active_reference, active_received, fitted["cfo_hz"]
)
matched = fitted["heldout_signal_power"]
residual = fitted["heldout_residual_power"]
background = background_by_label[preceding[segment["label"]]]
rows.append({
"label": segment["label"], "level_db": segment["level_db"],
"rx_start_sample": actual_start,
"alignment_adjustment_samples": fitted["alignment_adjustment"],
"full_iq": lab064._window_metrics(rx.read(actual_start, lab064.SEGMENT_SAMPLES)),
"background_label": background["label"], "background": background["metrics"],
"matched_signal_power": matched, "residual_power": residual,
"matched_to_residual_db": lab064.difference_db(matched, residual),
"complex_gain_h": complex_gain, "gain_abs": fitted["gain_abs"],
"gain_power_abs_h_squared": fitted["gain_abs"] ** 2,
"cfo_hz": fitted["cfo_hz"],
"residual_spectrum": lab064.residual_spectral_diagnostics(
active_reference, active_received, fitted["cfo_hz"], complex_gain
),
})
comparisons = []
for index in range(1, len(rows)):
tx_difference = metadata["segments"][index]["tx_input_difference_from_previous_db"]
rx_difference = lab064.difference_db(
rows[index]["matched_signal_power"], rows[index - 1]["matched_signal_power"]
)
comparisons.append({
"comparison": f"{rows[index]['label']}/{rows[index - 1]['label']}",
"tx_input_difference_db": tx_difference,
"rx_matched_power_difference_db": rx_difference,
"linearity_error_db": rx_difference - tx_difference,
})
validity = lab064.capture_validity(capture)
result = {
"lab": "Lab065", "stage": "OFFLINE_RF_LEVEL_LINEARITY_ANALYSIS",
"packet_receiver_used": False, "location": location, "rows": rows,
"background_windows": background_rows,
"background_summary": lab064.summarize_background(background_rows),
"comparisons": comparisons, "capture_valid": validity["valid"],
"capture_invalid_reasons": validity["invalid_reasons"],
"tx_underruns_maximum": capture.get("tx_counter_summary", {}).get("maximum_count"),
"rx_overruns_maximum": capture.get("rx_counter_summary", {}).get("maximum_count"),
"rx_clipping": any(row["full_iq"]["clipped_samples_fraction"] > 0 for row in rows),
}
old_result = json.loads(LAB064_RESULT.read_text(encoding="utf-8"))
result["lab064_to_lab065"] = compare_sessions(old_result, rows)
result["classification"] = classify_result(result, result["lab064_to_lab065"])
save_new(OUTPUT / "rf_result.json", result)
return result
def run_functional_tests() -> None:
assert transfer_command("NUL", 100, transmit=True)[-7:-5] == ["-x", "20"]
assert "-t" not in transfer_command("NUL", 100)
assert validate_lab064_waveform()["tx_sha256"] == EXPECTED_TX_SHA256
assert lab064.synthetic_linearity_gate()["passed"]
try:
capture_once(authorization=False)
except PermissionError:
pass
else:
raise AssertionError("RF без отдельного разрешения")
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--stage", choices=("prepare", "software", "preflight", "capture", "analyze"),
required=True,
)
parser.add_argument("--authorize-one-rf", action="store_true")
arguments = parser.parse_args()
if arguments.stage == "prepare":
result = prepare()
elif arguments.stage == "software":
result = software_gate()
elif arguments.stage == "preflight":
result = preflight()
elif arguments.stage == "capture":
result = capture_once(authorization=arguments.authorize_one_rf)
else:
result = analyze_capture()
print(json.dumps(result, ensure_ascii=False, indent=2, allow_nan=False))
print("RF TX NOW = 0")
if __name__ == "__main__":
main()

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,151 @@
"""Lab065: неизменный waveform, TX VGA 20 дБ и границы классификации."""
from __future__ import annotations
import json
from pathlib import Path
import subprocess
import sys
import pytest
from experiments import lab065_rf_level_linearity as lab
from tools.hackrf_process_check import ProcessCheck, ProcessState
def test_only_operating_parameter_change_is_tx_vga():
old = lab.lab064.parameters()
new = lab.parameters()
assert new.pop("geometry_unchanged_from_lab064") is True
assert new.pop("waveform_source") == "Lab064 exact CS8 copy"
assert new["tx_vga_db"] == 20
new["tx_vga_db"] = 10
assert new == old
def test_tx_command_uses_20_db_and_rx_remains_24_24():
transmit = lab.transfer_command("tx.cs8", 100, transmit=True)
receive = lab.transfer_command("rx.cs8", 100)
assert transmit[transmit.index("-x") + 1] == "20"
assert receive[receive.index("-l") + 1] == "24"
assert receive[receive.index("-g") + 1] == "24"
assert transmit[-5:] == receive[-5:] == ["-a", "0", "-p", "0", "-B"]
def test_exact_lab064_waveform_is_required():
metadata = lab.validate_lab064_waveform()
assert metadata["tx_sha256"] == lab.EXPECTED_TX_SHA256
assert metadata["tx_samples"] == 6_000_000
def _result(matched_gain, residual_gain, improvements, errors=(0.0, 0.0, 0.0)):
old = {row["label"]: row for row in json.loads(
lab.LAB064_RESULT.read_text(encoding="utf-8")
)["rows"]}
rows = []
for index, label in enumerate(("L0", "L1", "L2", "L3")):
baseline = old[label]
rows.append({
"label": label,
"matched_signal_power": baseline["matched_signal_power"] * 10 ** (matched_gain[index] / 10),
"residual_power": baseline["residual_power"] * 10 ** (residual_gain[index] / 10),
"matched_to_residual_db": baseline["matched_to_residual_db"] + improvements[index],
"gain_power_abs_h_squared": baseline["gain_power_abs_h_squared"] * 10 ** (matched_gain[index] / 10),
})
return {
"capture_valid": True,
"rows": rows,
"comparisons": [{"linearity_error_db": error} for error in errors],
}
@pytest.mark.parametrize(
("result", "expected"),
[
(_result([10] * 4, [1] * 4, [9] * 4), "LINEAR_GAIN_IMPROVEMENT"),
(_result([10] * 4, [10] * 4, [0] * 4), "RESIDUAL_RISES_WITH_SIGNAL"),
(_result([1] * 4, [0] * 4, [1] * 4), "NO_MEANINGFUL_GAIN"),
(_result([10] * 4, [1] * 4, [9] * 4, errors=(0, 0, -1.6)), "COMPRESSION_AT_HIGHER_TX_GAIN"),
],
)
def test_classification_cases(result, expected):
comparison = lab.compare_sessions(
json.loads(lab.LAB064_RESULT.read_text(encoding="utf-8")), result["rows"]
)
assert lab.classify_result(result, comparison)["case"] == expected
@pytest.mark.parametrize("failure", [None, "clock", "overrun", "clipping"])
def test_preflight_never_transmits_and_fails_closed(tmp_path, monkeypatch, failure):
raw, output = tmp_path / "raw", tmp_path / "output"
raw.mkdir()
output.mkdir()
monkeypatch.setattr(lab, "RAW", raw)
monkeypatch.setattr(lab, "OUTPUT", output)
(raw / "tx_level_staircase.cs8").write_bytes(b"test")
metadata = {"tx_sha256": "TEST"}
(output / "software_gate.json").write_text('{"passed": true, "tx_sha256": "TEST"}')
monkeypatch.setattr(lab, "validate_prepared", lambda: metadata)
monkeypatch.setattr(lab.lab054.phy.lab045, "_verify_device_inventory", lambda runner: {"passed": True})
monkeypatch.setattr(lab.lab054, "disable_clock_outputs", lambda runner: [])
monkeypatch.setattr(lab, "parameters", lambda: {})
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 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, "")
options = {
"runner": runner,
"clock_checker": clock,
"process_checker": lambda: ProcessCheck(ProcessState.CLEAR, ()),
"sleep_function": lambda _: None,
}
if failure:
with pytest.raises(RuntimeError):
lab.preflight(**options)
else:
assert lab.preflight(**options)["passed"]
assert json.loads((raw / "preflight_result.json").read_text())["rf_tx_count"] == 0
def test_capture_is_unavailable_without_new_authorization():
with pytest.raises(PermissionError):
lab.capture_once(authorization=False)
def test_existing_attempt_blocks_capture_before_hardware(tmp_path, monkeypatch):
monkeypatch.setattr(lab, "RAW", tmp_path)
monkeypatch.setattr(lab, "validate_prepared", lambda: {"tx_sha256": "TEST"})
monkeypatch.setattr(lab, "require_preflight", lambda metadata: {})
(tmp_path / "hardware_attempt.json").write_text("{}", encoding="utf-8")
with pytest.raises(FileExistsError):
lab.capture_once(
authorization=True,
runner=lambda command: pytest.fail("hardware must not run"),
process_starter=lambda *args: pytest.fail("hardware must not run"),
)
def test_cli_labels_current_rf_state_after_completed_capture(monkeypatch, capsys):
monkeypatch.setattr(
lab,
"capture_once",
lambda authorization=False: {
"stage": "SINGLE_RF_LEVEL_CAPTURE", "rf_tx_count": 1, "passed": True,
},
)
monkeypatch.setattr(
sys, "argv", ["lab065", "--stage", "capture", "--authorize-one-rf"]
)
lab.main()
output = capsys.readouterr().out
assert '"rf_tx_count": 1' in output
assert output.endswith("RF TX NOW = 0\n")

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"))