Lab065: validate OTA gain improvement at TX VGA 20 dB

This commit is contained in:
LittleSam129
2026-09-15 13:42:08 +03:00
parent cff226e0bf
commit dacf04f2d0
12 changed files with 2305 additions and 0 deletions

View File

@@ -2861,3 +2861,66 @@ 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; повторная передача или изменение усилений запрещены.

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

@@ -663,3 +663,40 @@
- **Независимая проверка:** прямой пересчёт из 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.

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