diff --git a/PROJECT_LOG.md b/PROJECT_LOG.md index e4df771..9994d04 100644 --- a/PROJECT_LOG.md +++ b/PROJECT_LOG.md @@ -2218,3 +2218,229 @@ HEAD прежний0368c6d3e930f4d245e056223fd4e5c4f70e6b49, main; предыд изменения Lab054 сохранены. Git add/commit/push/pull/fetch не выполнялись. Следующий этап: STOP и согласование измерительной схемы, приборов и допустимой частоты; никакого автоматического аппаратного продолжения. + +# Запись 047 + +## Дата + +9 сентября 2026 года + +## Тема + +Lab055 — фиксация результата; Lab056 — программная подготовка эфирного ABAB + +## Выполнено + +По новому явному разрешению пользователя Lab055 зафиксирована коммитом +5add95ecb8865f59075bbbced08d97b183fb3b48 с сообщением +Lab055: audit received signal energy across symbol rates. +В коммите семь файлов только Lab055: анализатор, тесты, отчёт, два JSON, +запись 046 и раздел индекса. Предшествующие изменения Lab054 сохранены +незакоммиченными. Перед коммитом quick_gate 823/28/13 PASS, pytest 823 passed. +Push не выполнялся. Исторический Lab054 NOT_A, 0/16, не изменён; +повторная проверка SHA-256 всех 34 входов Lab055 не выявила изменений. + +Прямой выбор пользователя — эфирный опыт вместо предпочтительного кабеля +из приложения. Подготовлен один TX-файл ABAB: 20/500/20/500 ксимв/с, +прежние PRBS7/BPSK/RRC, Fs 8 Мвыб/с и цифровая несущая +50 кГц. +Созданы experiments/lab056_abab_energy.py, lab056_capture_runner.py, +tests/test_lab056_preparation.py, docs/lab056_current_state.md и малые +результаты data/processed/lab056. Большие CS8 находятся в data/raw/lab056. + +TX RMS всего сегмента A/B: 44,004413/44,001925 код; разница мощности +−0,000491 дБ. Активные участки отдельно: −0,042606 дБ; клиппинг 0. +TX 10 588 544 отсчёта, 1,323568 с; RX планируется 3 с с упреждением 0,5 с. +A1=A2 и B1=B2 побайтно. Пять gaps по 0,1 с содержат нулевой цифровой IQ +при работающем TX-процессе: это не доказательство выключенного RF. + +Анализатор сохраняет полную и AC-мощность RX, локальный фон, устойчивую +разность мощностей, комплексный коэффициент передачи, согласованную +компоненту, остаток и интегральный спектр. Мощность и энергия символа +выводятся отдельно. RX не выравнивается по амплитуде после приёма. +Искусственные записи с одинаковым усилением и падением B на 6,020600 дБ +обработаны успешно: оценки разницы коэффициента передачи соответственно ++0,001786/−0,002894 дБ и −6,014144/−6,022175 дБ для двух пар. +Это программные проверки, не аппаратный результат. + +Подготовлен RX-first runner с единственной конечной TX-попыткой, без повтора, +проверкой процессов, SHA, размеров, счётчиков потерь и cleanup. Common clock +проверен только mock-тестами; реальные reg29/CLKIN сейчас NOT_PERFORMED. +Частота не назначена: 1400 МГц пересекает запрещённую для излучения полосу +1400–1427 МГц (ITU RR2024, п. 5.340). Для будущего опыта нужны допустимая +частота/полоса, страна и подтверждение антенн/неизменной геометрии. +Эфирный ABAB измеряет весь путь, включая антенны и распространение; +он не изолирует внутренние аппаратные потери HackRF и не измеряет dBm. + +Verifier независимо пересчитал коэффициенты, проверил TX и mock-отказы. +Воспроизведённая ошибка разбора счётчиков с префиксом RX/TX исправлена; +повторная проверка закрыла находку. Остаточных воспроизведённых находок нет. +34 целевых теста PASS; итоговые quick_gate 857/30/13 PASS, +pytest 857 passed, 0 failed, 96,50 с; git diff --check PASS. +HackRF не запускался, RF TX=0. Lab056 не коммитилась; push не выполнялся. +Следующий этап: STOP, согласование допустимой эфирной конфигурации и отдельное +разрешение на конкретный однократный RF-сеанс перед аппаратным preflight. + +# Запись 048 + +## Дата + +10 сентября 2026 года + +## Тема + +Lab056 — принятое пользователем изменение методики на контролируемый OTA + +## Выполнено + +Пользователь сообщил об отсутствии необходимого SMA-переходника и осознанно +выбрал эфирный ABAB. Кабельный TX → 30 дБ → RX отложен до получения +переходника как отдельная проверка. Центральную частоту определяет пользователь: +RF_FREQ = USER_AUTHORIZED_RF_HZ. Конкретное значение пока не сообщено; +обязательные поля страны/регуляторной полосы убраны из параметров runner, +национальная регуляторика повторно не исследовалась. В отчёте подготовлены +поля антенн, расстояния, ориентации/поляризации, LOS/NLOS и частоты. + +Готовый ABAB waveform, метаданные, прежние результаты и исходная сводка +857 passed сохранены. RMS A/B 44,004413/44,001925 код, разница мощности +−0,000491 дБ, TX 1,323568 с, RX 3 с. Усиления TX10/RX24/24, +Fs 8 Мвыб/с и несущая +50 кГц не менялись; внешних PA/LNA нет. + +В runner оформлен отдельный RX-only preflight: common clock с обоими +reg29=0x26 и SLAVE CLKIN=DETECTED, 0,1 с приёма фона для проверки уровня, +сохранение результата и cleanup, затем STOP перед отдельным разрешением TX. +При частоте UNKNOWN аппаратных действий нет. capture_once требует PASS +этой конфигурации, отдельной авторизации и свежей проверки опоры. +При клиппинге до TX — RF TX=0; при обнаружении только в записи ABAB — +RF TX=1 и отказ без повтора. Счётчик уже выполненной передачи не обнуляется. +Ни частота, ни gains автоматически не подбираются. + +Добавлен явный комплексный h прежней модели, сводки X/Y/Z и повторяемость +A2/A1, B2/B1. Те же искусственные IQ прочитаны повторно с новыми именами +выходных JSON. Для равного усиления X/Y/Z −0,046404/−0,000553/−14,025804 дБ; +для ослабления B на 6,020600 дБ: −6,039124/−6,018156/−20,018524 дБ. +Это программные значения, не аппаратный результат Lab056. Эфирный опыт +будет характеризовать весь путь, а не отдельно внутренние потери HackRF. + +44 целевых теста PASS. Итоговые quick_gate 867/30/13 PASS, +pytest 867 passed, 0 failed, 89,32 с; git diff --check PASS. +Verifier независимо исполнил 44 случая с данными в памяти, без записи +и оборудования; воспроизведённых находок нет. Исторический Lab054 NOT_A +сохранён. HEAD 5add95ecb8865f59075bbbced08d97b183fb3b48, Lab056 не коммитилась, +push не выполнялся. Аппаратных команд нет, RF TX=0. +Следующий этап: получить RF_FREQ, выполнить разрешённый RX-only аппаратный +preflight, остановиться с кратким отчётом и ждать отдельного разрешения +на один ABAB-сеанс. Кабельный контроль и PA автоматически не добавляются. + +# Запись 049 + +## Дата + +10 сентября 2026 года + +## Тема + +Lab056 — аппаратный common-clock и RX-only preflight на частоте пользователя + +## Выполнено + +Пользователь задал RF_FREQ=1400000000 Гц только для текущей лабораторной +проверки. Это не рабочая частота SDR_Rover; ориентир полевых работ около +180 МГц и варианты 50/144 МГц здесь не исследуются. Задана геометрия OTA: +имеющиеся антенны 1400 МГц, LOS, неподвижное оборудование, одинаковые +поляризация/ориентация, расстояние примерно 3–5 м. Точные модели антенн +и измеренное расстояние не сообщены; описание сохранено без выдуманных данных. + +Выполнен только разрешённый аппаратный preflight. Инвентаризация подтвердила +MASTER ...323b97d3 и SLAVE ...2e5327d3. После коротких RX-start обоих устройств +MASTER reg29=0x26, SLAVE reg29=0x26, SLAVE CLKIN=DETECTED; опора обнаружена +первым запросом через 67,787 мс ожидания. Затем SLAVE записал 800 000 +комплексных отсчётов, 0,1 с, на той же частоте, Fs 8 Мвыб/с, RX24/24 дБ, +RF amplifier/Bias-Tee OFF. Внешних PA/LNA нет. TX не запускался. + +CS8 1 600 000 байт, SHA-256 +c4257f26e4cc8562eb32228f9a50fdeb055879c928af0744cdf4a81cf7cd085b. +RMS 2,203152571 код, мощность 4,85388125 код², AC-мощность 0,6658039582 код², +максимумы абсолютных I/Q 4/6 код. Клиппинг 0 из 800 000 отсчётов, overruns 0. +Отсутствие клиппинга фона без TX не гарантирует отсутствие перегрузки +при будущем включении TX. После проверки CLKOUT обеих плат выключены, +процессы CLEAR до/после, cleanup без ошибок. + +Сохранены исходный RX, preflight_result/attempt, план и журнал фактических +команд. Три hackrf_transfer-команды были только RX: два коротких запуска +для опоры и запись фона; TX-команд 0. Отчёт docs/lab056_rx_preflight.md. +Verifier только по сохранённым файлам подтвердил 12 команд, регистры, +CLKIN, параметры и cleanup; пересчитал SHA и мощность/клиппинг вторым +способом. Воспроизведённых расхождений нет, PASS, аппаратных команд не было. +Код и готовый TX не менялись; SHA совпадают с проверенной версией. +Последние программные проверки этой версии: 44 целевых теста, +quick_gate 867/30/13 PASS, pytest 867 passed. Git diff --check PASS. +Lab054 NOT_A сохранён. HEAD 5add95ecb8865f59075bbbced08d97b183fb3b48; +Lab056 не коммитилась, push не выполнялся. RF TX=0. +Следующий этап: STOP с техническим READY FOR SINGLE ABAB TX=YES; +дождаться отдельного разрешения пользователя на единственный ABAB TX. + +# Запись 050 + +## Дата + +10 сентября 2026 года + +## Тема + +Lab056 — единственный OTA ABAB 20/500 ксимв/с; большой провал не воспроизведён + +## Выполнено + +После отдельного явного разрешения пользователя выполнен один конечный +ABAB TX с неизменным ранее подготовленным файлом 10 588 544 отсчёта, +1,323568 с. RX запущен первым и сохранил 24 000 000 комплексных отсчётов, +48 000 000 байт, 3 с. Частота 1400000000 Гц только для Lab056, +Fs 8 Мвыб/с, TX10/RX24/24 дБ, несущая +50 кГц, прежние PRBS7/BPSK/RRC; +RF amplifier/Bias-Tee OFF, внешних PA/LNA нет. Геометрия по заданию +пользователя: имеющиеся антенны 1400 МГц, LOS, примерно 3–5 м, +неподвижность и одинаковые поляризация/ориентация. Параметры между A/B +не менялись. Код, пороги, timing, FEC/CRC и цифровой TX не менялись. + +Свежая опора перед сеансом PASS: оба reg29=0x26, SLAVE CLKIN=DETECTED +через 137,359 мс штатного ожидания. По журналу RX стартовал на 0,517342 с +раньше TX. RX overruns=0, TX underruns=0, клиппинг 0 до и после ABAB. +Полный RX RMS 2,235380477 код, максимумы абсолютных I/Q 5/6 код. +Все 14 аппаратных команд сохранены; среди них один TX без повтора. +CLKOUT после опыта выключены, процессы CLEAR, cleanup без ошибок. + +Подготовленный анализатор нашёл четыре сегмента с корреляцией 0,384366. +Начала A1/B1/A2/B2 в RX: 5 254 791 / 7 709 991 / 10 149 062 / +12 604 263; общий сдвиг относительно TX расходится лишь на один отсчёт. +Все активные окна 0,2048 с, порядок ABAB и gaps подтверждены. +Полезные мощности после фона A1/B1/A2/B2: +0,462680958 / 0,476010015 / 0,414075173 / 0,423007891 код². +Согласованные компоненты: 0,312320987 / 0,297815525 / +0,299566893 / 0,285847043 код². Raw/AC/фон/комплексный h/остаток/спектры +по каждому сегменту приведены в docs/lab056_rf_result.md и rf_analysis.json. + +Итоги по средним линейным мощностям: +X, принятая мощность B/A после вычитания фона: +0,108895 дБ; +Y, коэффициент передачи abs(h)² B/A: −0,163042 дБ; +Z, энергия символа этой же принятой мощности: −13,870505 дБ. +Повтор A2/A1 по мощности −0,482024 дБ, B2/B1 −0,512676 дБ; +по abs(h)² −0,181074/−0,178136 дБ. Парные оценки B/A близки. + +Прежний провал порядка 15,41 дБ в одной короткой OTA-сессии не воспроизвёлся. +Это не выделяет собственные потери HackRF из антенн и распространения +и не объясняет причину старого различия раздельных опытов. Кабельный контроль +после SMA-переходника остаётся отдельным необходимым этапом для такого +разделения. PA не назначен. Packet/PHY PASS, видео и FEC не исследовались. +Исторический Lab054 NOT_A, 0/16 сохранён; 34 исходных SHA Lab055 не изменились. + +Verifier только по сохранённым файлам независимо подтвердил команды, +SHA/размеры/клиппинг, четыре начала, мощности/фон и комплексный МНК; +расчёты согласованы до 1e−12, воспроизведённых находок нет. +RX SHA e336dd653fa554058052fcf851e9f2462f66b8cf7c710815ca2b9a09ffc23ad4. +TX SHA 5b34bbe1a8e6d037d2d6d367a74e9eb84e8940db7da61600ffaa7f7497d4f600. +Сохранены raw IQ, журналы, машинная сводка rf_result.json и график +data/processed/lab056/rf_overview.png. Код прежний: последние проверки +44 целевых теста, quick_gate 867/30/13, pytest 867 passed; diff check PASS. +HEAD 5add95ecb8865f59075bbbced08d97b183fb3b48. Lab056 не коммитилась, +push не выполнялся. Выполненных TX=1, сейчас RF TX=0, повторов нет. +Следующий этап: STOP и рассмотрение отчёта пользователем; никакого +автоматического второго RF-сеанса, кабельного контроля или добавления PA. diff --git a/data/processed/lab056/acceptance.json b/data/processed/lab056/acceptance.json new file mode 100644 index 0000000..945081c --- /dev/null +++ b/data/processed/lab056/acceptance.json @@ -0,0 +1,76 @@ +{ + "date": "2026-09-09", + "stage": "SOFTWARE_READY_RF_CONFIGURATION_PENDING", + "lab055_commit": "5add95ecb8865f59075bbbced08d97b183fb3b48", + "new_head": "5add95ecb8865f59075bbbced08d97b183fb3b48", + "lab055_commit_files": 7, + "lab055_inputs_sha256_rechecked": 34, + "lab055_inputs_changed": [], + "lab054_official_result": "NOT_A", + "lab054_packets": "0/16", + "rf_tx": 0, + "hardware_commands_executed": 0, + "hardware_common_clock_preflight": "NOT_PERFORMED", + "mock_common_clock": "PASS", + "rf_frequency_hz": null, + "physical_path": "OTA_REQUESTED_FREQUENCY_AND_GEOMETRY_PENDING", + "waiting_for_explicit_rf_authorization": true, + "lab056_committed": false, + "push_performed": false, + "targeted_tests": { + "passed": 34, + "failed": 0 + }, + "quick_gate": { + "command": "python tools/quick_gate.py", + "passed": true, + "core_tests": 857, + "functional_labs": 30, + "skipped_labs": 13 + }, + "pytest": { + "command": "python -m pytest tests/ -q -p no:cacheprovider", + "passed": 857, + "failed": 0, + "elapsed_seconds": 96.5 + }, + "diff_check": "PASS", + "verifier": { + "mode": "READ_ONLY_SOFTWARE", + "remaining_reproduced_findings": 0, + "counter_parser_finding": "FIXED_AND_REPRODUCIBLY_CLOSED" + }, + "synthetic_equal": { + "passed": true, + "expected_gain_difference_db": 0.0, + "measured_pairs_db": [ + 0.0017861283874782213, + -0.0028938012026523408 + ] + }, + "synthetic_minus6db": { + "passed": true, + "expected_gain_difference_db": -6.020599913279624, + "measured_pairs_db": [ + -6.014143555521402, + -6.022174975174189 + ] + }, + "power_units": "RX_CODE_SQUARED_NOT_CALIBRATED_RF_WATTS", + "gap_is_zero_digital_iq_not_rf_off": true, + "ota_scope": "ENTIRE_TX_ANTENNA_PROPAGATION_RX_PATH_NOT_ISOLATED_HACKRF", + "artifact_sha256": { + "experiments/lab056_abab_energy.py": "a36d277998b9b07c97eed0cfedec98ffed1ee2ea44e150f870b191b9a2d957c7", + "experiments/lab056_capture_runner.py": "4ccfec4bfd123602d61dba5107e5ac7763326aaaf6cdb2b1e7dbcfdfcb059488", + "tests/test_lab056_preparation.py": "74b428096a28c91ce603d3e13ad03ef9526a1a5a4be2d5001ec61441a4d1da1e", + "experiments/lab054_hardware_control.py": "199046f939d39eaa7b2a5769149dd43052b353bba79b4cccba60b7ab72071101", + "experiments/lab055_energy_audit.py": "ac4da1e31bc20b1740aebfab31f1368adb4ed6afe845ba8c4e45f579ea0f3753", + "data/processed/lab056/preparation.json": "535375d08a0699ac4be31992ddbb6c5d175b3156a0a3b508b45d7cb567c24071", + "data/processed/lab056/software_equal.json": "2c8bd05a0a1d488961a9a353c3ba9bf624e929792b66371bdb2b190b2cf22ca8", + "data/processed/lab056/software_minus6db.json": "c015de2d5238d5d3c1ea9d56b0e4a5da200f2e0f14e3640fa27a396ffd489f67", + "data/raw/lab056/tx_abab.cs8": "5b34bbe1a8e6d037d2d6d367a74e9eb84e8940db7da61600ffaa7f7497d4f600", + "data/raw/lab056/tx_metadata.json": "535375d08a0699ac4be31992ddbb6c5d175b3156a0a3b508b45d7cb567c24071", + "data/raw/lab056/synthetic_equal.cs8": "a88afcc1bbeb50092f9f5baca6eb607d25daf508e0fafcdcada318818d9ffd62", + "data/raw/lab056/synthetic_minus6db.cs8": "a189c1ffaf029983d41f7d87e262cbbc28ba740fc8915a75e41ee0adec9c1fe6" + } +} diff --git a/data/processed/lab056/ota_correction.json b/data/processed/lab056/ota_correction.json new file mode 100644 index 0000000..db64c5e --- /dev/null +++ b/data/processed/lab056/ota_correction.json @@ -0,0 +1,135 @@ +{ + "date": "2026-09-10", + "stage": "OTA_CORRECTION_READY_WAITING_USER_RF_FREQ", + "head": "5add95ecb8865f59075bbbced08d97b183fb3b48", + "method": "CONTROLLED_OTA_ABAB", + "reason": "USER_REPORTED_MISSING_SMA_ADAPTER", + "conducted_test": "DEFERRED_SEPARATE_CONTROL_AFTER_ADAPTER", + "rf_frequency_hz": null, + "rf_frequency_source": "USER_AUTHORIZED_RF_HZ", + "country_required": false, + "national_regulatory_research_performed": false, + "geometry": { + "tx_antenna": null, + "rx_antenna": null, + "distance_m": null, + "orientation": null, + "los_nlos": null + }, + "rf_tx": 0, + "hardware_commands_executed": 0, + "hardware_common_clock": "NOT_PERFORMED", + "next_step": "USER_RF_FREQ_THEN_RX_ONLY_PREFLIGHT_THEN_STOP_BEFORE_TX_AUTHORIZATION", + "preflight_rx_seconds": 0.1, + "rx_capture_seconds": 3.0, + "tx_seconds": 1.323568, + "preflight_background_does_not_prove_no_clipping_during_tx": true, + "saturation_policy": { + "before_tx": "STOP_WITH_TX_COUNT_0", + "during_tx": "PRESERVE_ACTUAL_TX_COUNT_1_AND_STOP_NO_RETRY" + }, + "baseline_acceptance_preserved": { + "path": "data/processed/lab056/acceptance.json", + "sha256": "ba48c2bb22947333aee99d5e72b0c6b661e617b1047b2672cb2d4c21ef721320", + "pytest_passed": 857 + }, + "targeted_tests": { + "passed": 44, + "failed": 0 + }, + "quick_gate": { + "passed": true, + "core_tests": 867, + "functional_labs": 30, + "skipped_labs": 13 + }, + "pytest": { + "command": "python -m pytest tests/ -q -p no:cacheprovider", + "passed": 867, + "failed": 0, + "elapsed_seconds": 89.32 + }, + "verifier": { + "mode": "READ_ONLY_IN_MEMORY", + "cases_passed": 44, + "remaining_reproduced_findings": 0 + }, + "original_segment_measurements_and_pairs_unchanged": true, + "synthetic_comparison_summaries": { + "equal": { + "repeatability": [ + { + "comparison": "A2/A1", + "raw_rx_power_difference_db": -0.0012085532551280598, + "background_difference_db": -0.0017255178918689754, + "received_power_difference_db": -0.005652782408264756, + "channel_gain_difference_db": -0.0023081300526506692, + "matched_component_difference_db": -0.00230813005264874 + }, + { + "comparison": "B2/B1", + "raw_rx_power_difference_db": 0.00036367664985406537, + "background_difference_db": -0.011492673362299905, + "received_power_difference_db": 0.004534904727870617, + "channel_gain_difference_db": -0.006988059642781434, + "matched_component_difference_db": -0.006988059642780951 + } + ], + "averaging": "RATIO_OF_MEAN_LINEAR_POWERS_B1_B2_OVER_A1_A2", + "received_power_difference_db": -0.04640374115997541, + "channel_gain_difference_db": -0.000552584218408658, + "energy_per_symbol_difference_db": -14.025803827880353, + "energy_basis": "BACKGROUND_SUBTRACTED_RECEIVED_POWER_DIVIDED_BY_SYMBOL_RATE" + }, + "minus6db": { + "repeatability": [ + { + "comparison": "A2/A1", + "raw_rx_power_difference_db": -0.0012085532551280598, + "background_difference_db": -0.0017255178918689754, + "received_power_difference_db": -0.005652782408264756, + "channel_gain_difference_db": -0.0023081300526506692, + "matched_component_difference_db": -0.00230813005264874 + }, + { + "comparison": "B2/B1", + "raw_rx_power_difference_db": 0.0016044036249902335, + "background_difference_db": -0.011492673362299905, + "received_power_difference_db": 0.016513577295241087, + "channel_gain_difference_db": -0.0103395497054386, + "matched_component_difference_db": -0.010339549705438118 + } + ], + "averaging": "RATIO_OF_MEAN_LINEAR_POWERS_B1_B2_OVER_A1_A2", + "received_power_difference_db": -6.039124271880495, + "channel_gain_difference_db": -6.018156341674954, + "energy_per_symbol_difference_db": -20.01852435860087, + "energy_basis": "BACKGROUND_SUBTRACTED_RECEIVED_POWER_DIVIDED_BY_SYMBOL_RATE" + } + }, + "lab055_inputs_rechecked": 34, + "lab055_inputs_changed": [], + "lab054_official": "NOT_A, 0/16", + "preserved_artifact_sha256": { + "experiments/lab054_hardware_control.py": "199046f939d39eaa7b2a5769149dd43052b353bba79b4cccba60b7ab72071101", + "experiments/lab055_energy_audit.py": "ac4da1e31bc20b1740aebfab31f1368adb4ed6afe845ba8c4e45f579ea0f3753", + "data/processed/lab056/preparation.json": "535375d08a0699ac4be31992ddbb6c5d175b3156a0a3b508b45d7cb567c24071", + "data/processed/lab056/software_equal.json": "2c8bd05a0a1d488961a9a353c3ba9bf624e929792b66371bdb2b190b2cf22ca8", + "data/processed/lab056/software_minus6db.json": "c015de2d5238d5d3c1ea9d56b0e4a5da200f2e0f14e3640fa27a396ffd489f67", + "data/raw/lab056/tx_abab.cs8": "5b34bbe1a8e6d037d2d6d367a74e9eb84e8940db7da61600ffaa7f7497d4f600", + "data/raw/lab056/tx_metadata.json": "535375d08a0699ac4be31992ddbb6c5d175b3156a0a3b508b45d7cb567c24071", + "data/raw/lab056/synthetic_equal.cs8": "a88afcc1bbeb50092f9f5baca6eb607d25daf508e0fafcdcada318818d9ffd62", + "data/raw/lab056/synthetic_minus6db.cs8": "a189c1ffaf029983d41f7d87e262cbbc28ba740fc8915a75e41ee0adec9c1fe6" + }, + "new_artifact_sha256": { + "experiments/lab056_abab_energy.py": "323408b3bc1aa02c8b2f2855cd6295ac2efd99f13afec2ca0c872aa013a78f96", + "experiments/lab056_capture_runner.py": "24c17060b8b3e3f6899f9e922ae818e7ae2efba456c5f5cada7c69b5e5429334", + "tests/test_lab056_preparation.py": "d48aef79ae2455ca397862082a89f4c5f9a7e4d678e9b464c8489dd0e319a5ac", + "docs/lab056_current_state.md": "1accf50e4b67ec60fd6e8f041048d4b4b4a04551ebb51e43dafb0d52e40c7e35", + "data/processed/lab056/software_equal_ota_summary.json": "2a04b074eb9f59e726fd9cb70744498384ce4a5aa3c3bd7e92ae21f4e55d05a1", + "data/processed/lab056/software_minus6db_ota_summary.json": "01eb38bdc7f3bbd5bf468959cf7395a2b865318d6340cc9a70683576606cd3e0" + }, + "lab056_commit": false, + "push": false, + "diff_check": "PASS" +} diff --git a/data/processed/lab056/preparation.json b/data/processed/lab056/preparation.json new file mode 100644 index 0000000..2585063 --- /dev/null +++ b/data/processed/lab056/preparation.json @@ -0,0 +1,173 @@ +{ + "stage": "PREPARATION_ONLY", + "rf_tx": 0, + "rf_frequency_hz": null, + "physical_path": "OTA_REQUESTED_FREQUENCY_AND_GEOMETRY_PENDING", + "sample_rate_hz": 8000000, + "carrier_hz": 50000, + "tx_samples": 10588544, + "tx_bytes": 21177088, + "tx_seconds": 1.323568, + "rx_samples": 24000000, + "rx_seconds": 3.0, + "rx_lead_seconds": 0.5, + "gap_seconds": 0.1, + "gap_is_zero_digital_iq_not_rf_off": true, + "tx_sha256": "5b34bbe1a8e6d037d2d6d367a74e9eb84e8940db7da61600ffaa7f7497d4f600", + "segments": [ + { + "label": "A1", + "symbol_rate": 20000, + "start": 800000, + "samples": 1655200, + "active_start": 808400, + "active_samples": 1638400, + "whole": { + "samples": 1655200, + "mean_i": -3.624939584340261e-06, + "mean_q": -8.458192363460609e-06, + "power_code2": 1936.3883820686322, + "ac_power_code2": 1936.3883820685476, + "rms_code": 44.00441321127498, + "peak_magnitude_code": 69.29646455628166, + "max_abs_i": 68.0, + "max_abs_q": 69.0, + "papr_db": 3.9442968742176006, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.09681941910343161 + }, + "active": { + "samples": 1638400, + "mean_i": 2.99072265625e-05, + "mean_q": -0.0008270263671875, + "power_code2": 1956.0524084472656, + "ac_power_code2": 1956.0524077623986, + "rms_code": 44.22728126900031, + "peak_magnitude_code": 69.29646455628166, + "max_abs_i": 68.0, + "max_abs_q": 69.0, + "papr_db": 3.9004166907590005, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.09780262042236328 + } + }, + { + "label": "B1", + "symbol_rate": 500000, + "start": 3255200, + "samples": 1639072, + "active_start": 3255536, + "active_samples": 1638400, + "whole": { + "samples": 1639072, + "mean_i": 0.00011042833993869702, + "mean_q": -0.002146336463559868, + "power_code2": 1936.1693793805275, + "ac_power_code2": 1936.169374761573, + "rms_code": 44.00192472359053, + "peak_magnitude_code": 68.6804193347711, + "max_abs_i": 66.0, + "max_abs_q": 68.0, + "papr_db": 3.8672252879366917, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.003872338758761055 + }, + "active": { + "samples": 1638400, + "mean_i": -8.544921875e-06, + "mean_q": -0.002210693359375, + "power_code2": 1936.9563952636718, + "ac_power_code2": 1936.9563903764335, + "rms_code": 44.010866786098084, + "peak_magnitude_code": 68.6804193347711, + "max_abs_i": 66.0, + "max_abs_q": 68.0, + "papr_db": 3.865460322480824, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.0038739127905273434 + } + }, + { + "label": "A2", + "symbol_rate": 20000, + "start": 5694272, + "samples": 1655200, + "active_start": 5702672, + "active_samples": 1638400, + "whole": { + "samples": 1655200, + "mean_i": -3.624939584340261e-06, + "mean_q": -8.458192363460609e-06, + "power_code2": 1936.3883820686322, + "ac_power_code2": 1936.3883820685476, + "rms_code": 44.00441321127498, + "peak_magnitude_code": 69.29646455628166, + "max_abs_i": 68.0, + "max_abs_q": 69.0, + "papr_db": 3.9442968742176006, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.09681941910343161 + }, + "active": { + "samples": 1638400, + "mean_i": 2.99072265625e-05, + "mean_q": -0.0008270263671875, + "power_code2": 1956.0524084472656, + "ac_power_code2": 1956.0524077623986, + "rms_code": 44.22728126900031, + "peak_magnitude_code": 69.29646455628166, + "max_abs_i": 68.0, + "max_abs_q": 69.0, + "papr_db": 3.9004166907590005, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.09780262042236328 + } + }, + { + "label": "B2", + "symbol_rate": 500000, + "start": 8149472, + "samples": 1639072, + "active_start": 8149808, + "active_samples": 1638400, + "whole": { + "samples": 1639072, + "mean_i": 0.00011042833993869702, + "mean_q": -0.002146336463559868, + "power_code2": 1936.1693793805275, + "ac_power_code2": 1936.169374761573, + "rms_code": 44.00192472359053, + "peak_magnitude_code": 68.6804193347711, + "max_abs_i": 66.0, + "max_abs_q": 68.0, + "papr_db": 3.8672252879366917, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.003872338758761055 + }, + "active": { + "samples": 1638400, + "mean_i": -8.544921875e-06, + "mean_q": -0.002210693359375, + "power_code2": 1936.9563952636718, + "ac_power_code2": 1936.9563903764335, + "rms_code": 44.010866786098084, + "peak_magnitude_code": 68.6804193347711, + "max_abs_i": 66.0, + "max_abs_q": 68.0, + "papr_db": 3.865460322480824, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.0038739127905273434 + } + } + ], + "whole_power_difference_db": -0.000491208472157679, + "active_power_difference_db": -0.0426064264748898 +} diff --git a/data/processed/lab056/rf_analysis.json b/data/processed/lab056/rf_analysis.json new file mode 100644 index 0000000..995f1ce --- /dev/null +++ b/data/processed/lab056/rf_analysis.json @@ -0,0 +1,530 @@ +{ + "scope": "TX_REFERENCE_DIAGNOSTICS_NOT_INDEPENDENT_PACKET_RX", + "rf_tx": 0, + "tx_sha256": "5b34bbe1a8e6d037d2d6d367a74e9eb84e8940db7da61600ffaa7f7497d4f600", + "rx_sha256": "e336dd653fa554058052fcf851e9f2462f66b8cf7c710815ca2b9a09ffc23ad4", + "alignment": { + "start": 4454784, + "correlation": 0.3843661479500724, + "coarse_cfo_hz": 0.0, + "stride": 32 + }, + "segments": [ + { + "label": "A1", + "symbol_rate": 20000, + "raw_rx": { + "samples": 1638400, + "mean_i": 0.9375872802734375, + "mean_q": 1.827298583984375, + "power_code2": 5.303053588867187, + "ac_power_code2": 1.084963565705345, + "rms_code": 2.3028359882690705, + "peak_magnitude_code": 6.4031242374328485, + "max_abs_i": 5.0, + "max_abs_q": 6.0, + "papr_db": 8.88257840909542, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.0002651526794433594, + "band_27000_power": 0.3472643231791639, + "band_675000_power": 0.5082992616247881, + "power_standard_error": 0.020810438717554197 + }, + "local_zero_iq_background_windows": [ + { + "samples": 480000, + "mean_i": 0.9454104166666667, + "mean_q": 1.8225354166666667, + "power_code2": 4.857945833333333, + "ac_power_code2": 0.642509632387152, + "rms_code": 2.204074824803671, + "peak_magnitude_code": 5.385164807134504, + "max_abs_i": 5.0, + "max_abs_q": 5.0, + "papr_db": 7.759453298373341, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.00024289729166666665, + "band_27000_power": 0.011441788935577513, + "band_675000_power": 0.1346566488652154, + "power_standard_error": 0.011146582753825698 + }, + { + "samples": 480000, + "mean_i": 0.9274020833333333, + "mean_q": 1.8395416666666666, + "power_code2": 4.84604375, + "ac_power_code2": 0.6020555824262157, + "rms_code": 2.2013731510127945, + "peak_magnitude_code": 5.0, + "max_abs_i": 3.0, + "max_abs_q": 4.0, + "papr_db": 7.125526780497051, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.0002423021875, + "band_27000_power": 0.009082996916230657, + "band_675000_power": 0.10440265872939565, + "power_standard_error": 0.005986244399660487 + } + ], + "background_power": 0.6222826074066838, + "background_subtracted_raw": 0.46268095829866107, + "background_subtracted_uncertainty": 0.029702310624809113, + "background_subtracted_stable": true, + "background_subtracted_signal_power": 0.46268095829866107, + "channel_gain_complex": { + "real": -0.008543898749645305, + "imag": 0.009310849226837265, + "phase_rad_at_active_start": 2.3132658926165255 + }, + "channel_gain_power": 0.0001596901191690867, + "measurement": { + "frame": 1, + "start": 5254791, + "alignment_adjustment": 7, + "rx_active": { + "samples": 1638400, + "mean_i": 0.9375872802734375, + "mean_q": 1.827298583984375, + "power_code2": 5.303053588867187, + "ac_power_code2": 1.084963565705345, + "rms_code": 2.3028359882690705, + "peak_magnitude_code": 6.4031242374328485, + "max_abs_i": 5.0, + "max_abs_q": 6.0, + "papr_db": 8.88257840909542, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.0002651526794433594 + }, + "cfo_hz": -0.028056675950692833, + "gain_abs": 0.012636855588677378, + "dc_i": 0.9388651889450522, + "dc_q": 1.8271201059862494, + "train_signal_power": 0.312403496916226, + "train_residual_power": 0.722356125700177, + "train_actual_ac_power": 1.0347596226164035, + "train_cross_power": 7.854827899222983e-17, + "heldout_signal_power": 0.31232098749561993, + "heldout_residual_power": 0.8215137283109276, + "heldout_actual_ac_power": 1.135170838604222, + "heldout_cross_power": 0.0013361227976749755, + "heldout_residual_band_27000_power": 0.03428707776367196, + "heldout_residual_band_675000_power": 0.18938129777540513, + "heldout_residual_band_1000000_power": 0.21334080727941662, + "heldout_residual_matched_power": 11.421082264513538, + "effective_signal_to_residual_db": -4.200136561843322 + } + }, + { + "label": "B1", + "symbol_rate": 500000, + "raw_rx": { + "samples": 1638400, + "mean_i": 0.9203680419921875, + "mean_q": 1.8407562255859375, + "power_code2": 5.328170166015625, + "ac_power_code2": 1.0927093512617043, + "rms_code": 2.30828294756419, + "peak_magnitude_code": 6.708203932499369, + "max_abs_i": 5.0, + "max_abs_q": 6.0, + "papr_db": 9.266344273087686, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 1.0656340332031251e-05, + "band_27000_power": 0.029697349664473127, + "band_675000_power": 0.5128322432606733, + "power_standard_error": 0.031085529715571047 + }, + "local_zero_iq_background_windows": [ + { + "samples": 480000, + "mean_i": 0.9274020833333333, + "mean_q": 1.8395416666666666, + "power_code2": 4.84604375, + "ac_power_code2": 0.6020555824262157, + "rms_code": 2.2013731510127945, + "peak_magnitude_code": 5.0, + "max_abs_i": 3.0, + "max_abs_q": 4.0, + "papr_db": 7.125526780497051, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 9.692087499999999e-06, + "band_27000_power": 0.009082996916230657, + "band_675000_power": 0.10440265872939565, + "power_standard_error": 0.005986244399660487 + }, + { + "samples": 480000, + "mean_i": 0.9213041666666667, + "mean_q": 1.8316708333333334, + "power_code2": 4.8351625, + "ac_power_code2": 0.6313430907986115, + "rms_code": 2.198900293328463, + "peak_magnitude_code": 5.0990195135927845, + "max_abs_i": 3.0, + "max_abs_q": 5.0, + "papr_db": 7.3056227355026895, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 9.670325e-06, + "band_27000_power": 0.011430099601181243, + "band_675000_power": 0.13125486016140642, + "power_standard_error": 0.0037407319242408464 + } + ], + "background_power": 0.6166993366124136, + "background_subtracted_raw": 0.4760100146492907, + "background_subtracted_uncertainty": 0.03454282477879906, + "background_subtracted_stable": true, + "background_subtracted_signal_power": 0.4760100146492907, + "channel_gain_complex": { + "real": -0.008089659584664989, + "imag": 0.009397483555426113, + "phase_rad_at_active_start": 2.2815457153366783 + }, + "channel_gain_power": 0.00015375528937026635, + "measurement": { + "frame": 1, + "start": 7709991, + "alignment_adjustment": 7, + "rx_active": { + "samples": 1638400, + "mean_i": 0.9203680419921875, + "mean_q": 1.8407562255859375, + "power_code2": 5.328170166015625, + "ac_power_code2": 1.0927093512617043, + "rms_code": 2.30828294756419, + "peak_magnitude_code": 6.708203932499369, + "max_abs_i": 5.0, + "max_abs_q": 6.0, + "papr_db": 9.266344273087686, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 1.0656340332031251e-05 + }, + "cfo_hz": -0.0731458112990823, + "gain_abs": 0.012399810053797854, + "dc_i": 0.920656229639791, + "dc_q": 1.8411261312835556, + "train_signal_power": 0.2978190572101343, + "train_residual_power": 0.7068527315140937, + "train_actual_ac_power": 1.0046717887242282, + "train_cross_power": -6.682154829462661e-17, + "heldout_signal_power": 0.29781552489257346, + "heldout_residual_power": 0.884789710339823, + "heldout_actual_ac_power": 1.1807473535638735, + "heldout_cross_power": -0.001857881668523386, + "heldout_residual_band_27000_power": 0.015187641333986188, + "heldout_residual_band_675000_power": 0.217181000785572, + "heldout_residual_band_1000000_power": 0.24523520739571264, + "heldout_residual_matched_power": 3.0703433988460103, + "effective_signal_to_residual_db": -4.72892729922049 + } + }, + { + "label": "A2", + "symbol_rate": 20000, + "raw_rx": { + "samples": 1638400, + "mean_i": 0.9284002685546875, + "mean_q": 1.84388427734375, + "power_code2": 5.311703491210937, + "ac_power_code2": 1.0498672043230375, + "rms_code": 2.304713320829933, + "peak_magnitude_code": 5.830951894845301, + "max_abs_i": 4.0, + "max_abs_q": 5.0, + "papr_db": 8.062450931004047, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.0002655851745605469, + "band_27000_power": 0.33592297470769195, + "band_675000_power": 0.500120184644421, + "power_standard_error": 0.01600143643844262 + }, + "local_zero_iq_background_windows": [ + { + "samples": 480000, + "mean_i": 0.92130625, + "mean_q": 1.8316708333333334, + "power_code2": 4.83516875, + "ac_power_code2": 0.6313455020269103, + "rms_code": 2.198901714492942, + "peak_magnitude_code": 5.0990195135927845, + "max_abs_i": 3.0, + "max_abs_q": 5.0, + "papr_db": 7.30561712175391, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.0002417584375, + "band_27000_power": 0.011430090514699676, + "band_675000_power": 0.13125475431725353, + "power_standard_error": 0.0037385404799615833 + }, + { + "samples": 480000, + "mean_i": 0.9283375, + "mean_q": 1.8603020833333332, + "power_code2": 4.962772916666666, + "ac_power_code2": 0.6402385615060755, + "rms_code": 2.2277281963171958, + "peak_magnitude_code": 5.830951894845301, + "max_abs_i": 4.0, + "max_abs_q": 5.0, + "papr_db": 8.357545135545596, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.00024813864583333333, + "band_27000_power": 0.011660806052520012, + "band_675000_power": 0.13208548597324482, + "power_standard_error": 0.009340294961573032 + } + ], + "background_power": 0.6357920317664929, + "background_subtracted_raw": 0.4140751725565446, + "background_subtracted_uncertainty": 0.017352868452820306, + "background_subtracted_stable": true, + "background_subtracted_signal_power": 0.4140751725565446, + "channel_gain_complex": { + "real": -0.007000883443887156, + "imag": 0.010205712402071436, + "phase_rad_at_active_start": 2.172048716307076 + }, + "channel_gain_power": 0.000153168934628688, + "measurement": { + "frame": 1, + "start": 10149062, + "alignment_adjustment": 6, + "rx_active": { + "samples": 1638400, + "mean_i": 0.9284002685546875, + "mean_q": 1.84388427734375, + "power_code2": 5.311703491210937, + "ac_power_code2": 1.0498672043230375, + "rms_code": 2.304713320829933, + "peak_magnitude_code": 5.830951894845301, + "max_abs_i": 4.0, + "max_abs_q": 5.0, + "papr_db": 8.062450931004047, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.0002655851745605469 + }, + "cfo_hz": 0.005915202430533777, + "gain_abs": 0.012376143770524322, + "dc_i": 0.9271611521067152, + "dc_q": 1.8443065713552416, + "train_signal_power": 0.29964603349233376, + "train_residual_power": 0.7155398694209132, + "train_actual_ac_power": 1.0151859029132475, + "train_cross_power": 2.4270516152391507e-16, + "heldout_signal_power": 0.29956689346716014, + "heldout_residual_power": 0.7826844440402193, + "heldout_actual_ac_power": 1.084551933216437, + "heldout_cross_power": 0.002300595709057756, + "heldout_residual_band_27000_power": 0.03443073439045887, + "heldout_residual_band_675000_power": 0.19776523377923858, + "heldout_residual_band_1000000_power": 0.22176016276354857, + "heldout_residual_matched_power": 11.341896374078306, + "effective_signal_to_residual_db": -4.170928864706292 + } + }, + { + "label": "B2", + "symbol_rate": 500000, + "raw_rx": { + "samples": 1638400, + "mean_i": 0.9302490234375, + "mean_q": 1.854871826171875, + "power_code2": 5.396551513671875, + "ac_power_code2": 1.0906387765392669, + "rms_code": 2.3230478931076464, + "peak_magnitude_code": 6.4031242374328485, + "max_abs_i": 5.0, + "max_abs_q": 6.0, + "papr_db": 8.806675296737527, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 1.0793103027343752e-05, + "band_27000_power": 0.030437416621842406, + "band_675000_power": 0.5333148504916424, + "power_standard_error": 0.03318734421277559 + }, + "local_zero_iq_background_windows": [ + { + "samples": 480000, + "mean_i": 0.9283375, + "mean_q": 1.8603020833333332, + "power_code2": 4.962772916666666, + "ac_power_code2": 0.6402385615060755, + "rms_code": 2.2277281963171958, + "peak_magnitude_code": 5.830951894845301, + "max_abs_i": 4.0, + "max_abs_q": 5.0, + "papr_db": 8.357545135545596, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 9.925545833333333e-06, + "band_27000_power": 0.011660770674569443, + "band_675000_power": 0.13208533141133466, + "power_standard_error": 0.009339092140283283 + }, + { + "samples": 480000, + "mean_i": 0.9385041666666667, + "mean_q": 1.8505104166666666, + "power_code2": 5.000202083333333, + "ac_power_code2": 0.6950232102907972, + "rms_code": 2.236113164250265, + "peak_magnitude_code": 5.385164807134504, + "max_abs_i": 4.0, + "max_abs_q": 5.0, + "papr_db": 7.634104411823291, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 1.0000404166666666e-05, + "band_27000_power": 0.011507358850526561, + "band_675000_power": 0.12385207349410385, + "power_standard_error": 0.037784295029485354 + } + ], + "background_power": 0.6676308858984363, + "background_subtracted_raw": 0.42300789064083055, + "background_subtracted_uncertainty": 0.04722771590637799, + "background_subtracted_stable": true, + "background_subtracted_signal_power": 0.42300789064083055, + "channel_gain_complex": { + "real": -0.006710536973189343, + "imag": 0.010126447128417442, + "phase_rad_at_active_start": 2.1560299377113283 + }, + "channel_gain_power": 0.000147576237913175, + "measurement": { + "frame": 1, + "start": 12604263, + "alignment_adjustment": 7, + "rx_active": { + "samples": 1638400, + "mean_i": 0.9302490234375, + "mean_q": 1.854871826171875, + "power_code2": 5.396551513671875, + "ac_power_code2": 1.0906387765392669, + "rms_code": 2.3230478931076464, + "peak_magnitude_code": 6.4031242374328485, + "max_abs_i": 5.0, + "max_abs_q": 6.0, + "papr_db": 8.806675296737527, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 1.0793103027343752e-05 + }, + "cfo_hz": 0.0026425717900911143, + "gain_abs": 0.012148096061242479, + "dc_i": 0.9315836612485033, + "dc_q": 1.8553663362050554, + "train_signal_power": 0.28585043299602836, + "train_residual_power": 0.7245263851886631, + "train_actual_ac_power": 1.010376818184691, + "train_cross_power": -8.954642582992278e-17, + "heldout_signal_power": 0.2858470426337267, + "heldout_residual_power": 0.8807229722868642, + "heldout_actual_ac_power": 1.1709047864903603, + "heldout_cross_power": 0.004334771569769229, + "heldout_residual_band_27000_power": 0.018795072832326204, + "heldout_residual_band_675000_power": 0.3125053797217844, + "heldout_residual_band_1000000_power": 0.3566678823693195, + "heldout_residual_matched_power": 4.361909383453252, + "effective_signal_to_residual_db": -4.887056210122219 + } + } + ], + "pairs": [ + { + "pair": 1, + "received_power_difference_db": 0.1233446340794126, + "channel_gain_difference_db": -0.1644798001918496, + "matched_component_difference_db": -0.20653835698711864, + "matched_component_symbol_energy_difference_db": -14.185938443707496 + }, + { + "pair": 2, + "received_power_difference_db": 0.09269277113182464, + "channel_gain_difference_db": -0.16154256719796828, + "matched_component_difference_db": -0.2036011239932389, + "matched_component_symbol_energy_difference_db": -14.183001210713615 + } + ], + "comparison_summary": { + "repeatability": [ + { + "comparison": "A2/A1", + "raw_rx_power_difference_db": 0.007078081316567549, + "background_difference_db": 0.0932741793444109, + "received_power_difference_db": -0.4820243490149181, + "channel_gain_difference_db": -0.18107353366707263, + "matched_component_difference_db": -0.1810735336670716 + }, + { + "comparison": "B2/B1", + "raw_rx_power_difference_db": 0.055382405794076286, + "background_difference_db": 0.34462938523123676, + "received_power_difference_db": -0.5126762119625052, + "channel_gain_difference_db": -0.17813630067319114, + "matched_component_difference_db": -0.17813630067319214 + } + ], + "averaging": "RATIO_OF_MEAN_LINEAR_POWERS_B1_B2_OVER_A1_A2", + "received_power_difference_db": 0.10889530008758355, + "channel_gain_difference_db": -0.16304154709643187, + "energy_per_symbol_difference_db": -13.870504786632791, + "energy_basis": "BACKGROUND_SUBTRACTED_RECEIVED_POWER_DIVIDED_BY_SYMBOL_RATE" + }, + "outside_waveform_background": [ + { + "start": 3654784, + "samples": 400000, + "mean_i": 0.9425475, + "mean_q": 1.823755, + "power_code2": 4.8400925, + "ac_power_code2": 0.6256144102187502, + "rms_code": 2.2000210226268293, + "peak_magnitude_code": 5.0, + "max_abs_i": 3.0, + "max_abs_q": 4.0, + "papr_db": 7.1308634705743845, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.000242004625, + "band_27000_power": 0.011134411836977165, + "band_675000_power": 0.12535191126457693, + "power_standard_error": 0.002472279873801531 + }, + { + "start": 15443328, + "samples": 400000, + "mean_i": 0.938865, + "mean_q": 1.832065, + "power_code2": 4.891855, + "ac_power_code2": 0.6539253475500004, + "rms_code": 2.2117538289782614, + "peak_magnitude_code": 5.0, + "max_abs_i": 3.0, + "max_abs_q": 4.0, + "papr_db": 7.0846643308644, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.00024459275, + "band_27000_power": 0.012745444722198074, + "band_675000_power": 0.13232609430446385, + "power_standard_error": 0.008612381752597367 + } + ], + "background_note": "Внутренние gaps: нулевой цифровой IQ при работающем TX; это не RF OFF. Внешние окна также требуют сверки времён запуска/остановки по журналам.", + "power_units": "RX code squared, not calibrated watts/dBm", + "residual_note": "Шум и неописанные моделью искажения; RX не нормируется для выравнивания A/B" +} diff --git a/data/processed/lab056/rf_overview.png b/data/processed/lab056/rf_overview.png new file mode 100644 index 0000000..be4d75c Binary files /dev/null and b/data/processed/lab056/rf_overview.png differ diff --git a/data/processed/lab056/rf_result.json b/data/processed/lab056/rf_result.json new file mode 100644 index 0000000..3a332ca --- /dev/null +++ b/data/processed/lab056/rf_result.json @@ -0,0 +1,336 @@ +{ + "date": "2026-09-10", + "stage": "OTA_ABAB_VALID_LARGE_DROP_NOT_REPRODUCED_STOP", + "rf_tx_current": 0, + "completed_tx_sessions": 1, + "automatic_tx_repeats": 0, + "authorization_consumed": true, + "hardware": { + "passed": true, + "rf_tx_count": 1, + "point": { + "frequency_hz": 1400000000, + "physical_path": "MASTER TX -> TX antenna -> free-space LOS -> RX antenna -> SLAVE RX; user-specified approximate distance 3-5 m; fixed equipment and cables", + "path_confirmed": true, + "tx_antenna": "Existing 1400 MHz antenna, as described by user; model/marking not supplied", + "rx_antenna": "Existing 1400 MHz antenna, as described by user; model/marking not supplied", + "distance_m": null, + "orientation": "Both stationary; same antenna orientation and polarization, per user", + "visibility": "LOS" + }, + "process_before": { + "state": "CLEAR", + "attempts": [ + { + "state": "CLEAR", + "mechanism": "psutil", + "matches": [], + "inspected": 302, + "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": 137.35900004394352, + "elapsed_ms": 137.35900004394352, + "polls": [ + { + "query": 1, + "start_ms": 50.7242000894621, + "end_ms": 69.19760000891984, + "raw": "CLKIN status: no clock signal detected", + "state": "NOT_DETECTED", + "exit_code": 0 + }, + { + "query": 2, + "start_ms": 119.4211000110954, + "end_ms": 137.35900004394352, + "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.938690185546875, + "mean_q": 1.825347900390625, + "power_code2": 4.84039306640625, + "ac_power_code2": 0.6273588445037603, + "rms_code": 2.2000893314604864, + "peak_magnitude_code": 4.47213595499958, + "max_abs_i": 3.0, + "max_abs_q": 4.0, + "papr_db": 6.161493654989252, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.0002420196533203125, + "saturation_detected": false, + "analog_compression": "UNKNOWN_WITHOUT_CALIBRATION" + }, + "rx_sha256": "e336dd653fa554058052fcf851e9f2462f66b8cf7c710815ca2b9a09ffc23ad4", + "rx_level": { + "samples": 24000000, + "mean_i": 0.9305354583333333, + "mean_q": 1.8321060833333334, + "power_code2": 4.996925875, + "ac_power_code2": 0.7744169351973671, + "rms_code": 2.235380476563218, + "peak_magnitude_code": 6.708203932499369, + "max_abs_i": 5.0, + "max_abs_q": 6.0, + "papr_db": 9.545096066616024, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.00024984629375, + "saturation_detected": false, + "analog_compression": "UNKNOWN_WITHOUT_CALIBRATION" + }, + "process_after": { + "state": "CLEAR", + "attempts": [ + { + "state": "CLEAR", + "mechanism": "psutil", + "matches": [], + "inspected": 302, + "errors": [] + } + ] + }, + "cleanup_errors": [] + }, + "rx_bytes": 48000000, + "rx_samples": 24000000, + "rx_seconds": 3.0, + "tx_bytes": 21177088, + "tx_samples": 10588544, + "tx_waveform_seconds": 1.323568, + "rx_overruns": 0, + "tx_underruns": 0, + "rx_clipped_samples": 0, + "rx_clipped_components": 0, + "all_four_segments_present": true, + "segmentation_start_spread_samples": 1, + "hardware_parameters_constant_between_segments": true, + "physical_geometry_source": "USER_DESCRIPTION_NOT_SOFTWARE_MEASUREMENT", + "audited_hardware_commands": 14, + "audited_tx_commands": 1, + "segments": [ + { + "label": "A1", + "start_sample": 5254791, + "end_sample_exclusive": 6909991, + "start_seconds": 0.656848875, + "end_seconds": 0.863748875, + "inferred_sequence_start_sample": 4454791, + "active_samples": 1638400, + "raw_rx_power_code2": 5.303053588867187, + "rx_ac_power_code2": 1.084963565705345, + "background_power_code2": 0.6222826074066838, + "background_subtracted_signal_power_code2": 0.46268095829866107, + "background_subtracted_uncertainty_code2": 0.029702310624809113, + "background_subtracted_stable": true, + "complex_gain_h": { + "real": -0.008543898749645305, + "imag": 0.009310849226837265, + "phase_rad_at_active_start": 2.3132658926165255 + }, + "channel_gain_power": 0.0001596901191690867, + "matched_signal_component_power_code2": 0.31232098749561993, + "residual_power_code2": 0.8215137283109276, + "spectral_band_27000_power_code2": 0.3472643231791639, + "spectral_band_675000_power_code2": 0.5082992616247881 + }, + { + "label": "B1", + "start_sample": 7709991, + "end_sample_exclusive": 9349063, + "start_seconds": 0.963748875, + "end_seconds": 1.168632875, + "inferred_sequence_start_sample": 4454791, + "active_samples": 1638400, + "raw_rx_power_code2": 5.328170166015625, + "rx_ac_power_code2": 1.0927093512617043, + "background_power_code2": 0.6166993366124136, + "background_subtracted_signal_power_code2": 0.4760100146492907, + "background_subtracted_uncertainty_code2": 0.03454282477879906, + "background_subtracted_stable": true, + "complex_gain_h": { + "real": -0.008089659584664989, + "imag": 0.009397483555426113, + "phase_rad_at_active_start": 2.2815457153366783 + }, + "channel_gain_power": 0.00015375528937026635, + "matched_signal_component_power_code2": 0.29781552489257346, + "residual_power_code2": 0.884789710339823, + "spectral_band_27000_power_code2": 0.029697349664473127, + "spectral_band_675000_power_code2": 0.5128322432606733 + }, + { + "label": "A2", + "start_sample": 10149062, + "end_sample_exclusive": 11804262, + "start_seconds": 1.26863275, + "end_seconds": 1.47553275, + "inferred_sequence_start_sample": 4454790, + "active_samples": 1638400, + "raw_rx_power_code2": 5.311703491210937, + "rx_ac_power_code2": 1.0498672043230375, + "background_power_code2": 0.6357920317664929, + "background_subtracted_signal_power_code2": 0.4140751725565446, + "background_subtracted_uncertainty_code2": 0.017352868452820306, + "background_subtracted_stable": true, + "complex_gain_h": { + "real": -0.007000883443887156, + "imag": 0.010205712402071436, + "phase_rad_at_active_start": 2.172048716307076 + }, + "channel_gain_power": 0.000153168934628688, + "matched_signal_component_power_code2": 0.29956689346716014, + "residual_power_code2": 0.7826844440402193, + "spectral_band_27000_power_code2": 0.33592297470769195, + "spectral_band_675000_power_code2": 0.500120184644421 + }, + { + "label": "B2", + "start_sample": 12604263, + "end_sample_exclusive": 14243335, + "start_seconds": 1.575532875, + "end_seconds": 1.780416875, + "inferred_sequence_start_sample": 4454791, + "active_samples": 1638400, + "raw_rx_power_code2": 5.396551513671875, + "rx_ac_power_code2": 1.0906387765392669, + "background_power_code2": 0.6676308858984363, + "background_subtracted_signal_power_code2": 0.42300789064083055, + "background_subtracted_uncertainty_code2": 0.04722771590637799, + "background_subtracted_stable": true, + "complex_gain_h": { + "real": -0.006710536973189343, + "imag": 0.010126447128417442, + "phase_rad_at_active_start": 2.1560299377113283 + }, + "channel_gain_power": 0.000147576237913175, + "matched_signal_component_power_code2": 0.2858470426337267, + "residual_power_code2": 0.8807229722868642, + "spectral_band_27000_power_code2": 0.030437416621842406, + "spectral_band_675000_power_code2": 0.5333148504916424 + } + ], + "comparison_summary": { + "repeatability": [ + { + "comparison": "A2/A1", + "raw_rx_power_difference_db": 0.007078081316567549, + "background_difference_db": 0.0932741793444109, + "received_power_difference_db": -0.4820243490149181, + "channel_gain_difference_db": -0.18107353366707263, + "matched_component_difference_db": -0.1810735336670716 + }, + { + "comparison": "B2/B1", + "raw_rx_power_difference_db": 0.055382405794076286, + "background_difference_db": 0.34462938523123676, + "received_power_difference_db": -0.5126762119625052, + "channel_gain_difference_db": -0.17813630067319114, + "matched_component_difference_db": -0.17813630067319214 + } + ], + "averaging": "RATIO_OF_MEAN_LINEAR_POWERS_B1_B2_OVER_A1_A2", + "received_power_difference_db": 0.10889530008758355, + "channel_gain_difference_db": -0.16304154709643187, + "energy_per_symbol_difference_db": -13.870504786632791, + "energy_basis": "BACKGROUND_SUBTRACTED_RECEIVED_POWER_DIVIDED_BY_SYMBOL_RATE" + }, + "pairs": [ + { + "pair": 1, + "received_power_difference_db": 0.1233446340794126, + "channel_gain_difference_db": -0.1644798001918496, + "matched_component_difference_db": -0.20653835698711864, + "matched_component_symbol_energy_difference_db": -14.185938443707496 + }, + { + "pair": 2, + "received_power_difference_db": 0.09269277113182464, + "channel_gain_difference_db": -0.16154256719796828, + "matched_component_difference_db": -0.2036011239932389, + "matched_component_symbol_energy_difference_db": -14.183001210713615 + } + ], + "energy_basis": "BACKGROUND_SUBTRACTED_RECEIVED_POWER_DIVIDED_BY_SYMBOL_RATE", + "units": "RX_CODE_SQUARED_AND_CODE_SQUARED_SECONDS_NOT_CALIBRATED_WATTS_OR_JOULES", + "interpretation": { + "historical_15_41_db_drop_reproduced": false, + "isolated_hackrf_loss_established": false, + "scope": "ENTIRE_OTA_PATH_IN_THIS_SESSION", + "conducted_control_required_to_separate_hardware_and_channel": true, + "external_amplifier_necessity_established": false, + "packet_acceptance_tested": false + }, + "verifier": { + "passed": true, + "mode": "SAVED_FILES_READ_ONLY_NO_HARDWARE", + "independent_methods": [ + "INTEGER_RAW_MOMENTS", + "LOCAL_VARIANCE", + "WEIGHTED_PHASE_CFO", + "COMPLEX_LSTSQ" + ], + "remaining_reproduced_findings": 0 + }, + "lab054_official": "NOT_A, 0/16", + "lab055_inputs_unchanged": 34, + "last_software_tests_same_code": { + "targeted": 44, + "pytest": 867, + "quick_gate": [ + 867, + 30, + 13 + ] + }, + "code_or_tx_changed": false, + "head": "5add95ecb8865f59075bbbced08d97b183fb3b48", + "lab056_committed": false, + "push": false, + "stop": true, + "artifact_sha256": { + "data/raw/lab056/tx_abab.cs8": "5b34bbe1a8e6d037d2d6d367a74e9eb84e8940db7da61600ffaa7f7497d4f600", + "data/raw/lab056/tx_metadata.json": "535375d08a0699ac4be31992ddbb6c5d175b3156a0a3b508b45d7cb567c24071", + "data/raw/lab056/rx_stream.cs8": "e336dd653fa554058052fcf851e9f2462f66b8cf7c710815ca2b9a09ffc23ad4", + "data/raw/lab056/hardware_attempt.json": "24dfed564a260c6286233a21823cf13f5d19731adaa5bf6664debeddde634ffd", + "data/raw/lab056/capture_result.json": "107ff5b43cd98acbb6ea77631894a1ff2fcb95868702f2dafa73675b69a16bdd", + "data/raw/lab056/tx_process_result.json": "88e13edd733430b2a42dd176ff275f518be39e1733da6b904a03493b99354475", + "data/raw/lab056/rx_stdout.txt": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "data/raw/lab056/rx_stderr.txt": "3b0c3dc749f353bad62f36b198745d9dd2850372ad2a0450ae645afad300361a", + "data/processed/lab056/rf_analysis.json": "21576f57910458a3fbe0090894a6ba61540b8a1a08094fca16cd06c46058d141", + "data/processed/lab056/rf_session_authorization.json": "52bf979934c2c27bee055b07e09685248d411a48b79f49b6f000b5e2c631e0c1", + "data/processed/lab056/rf_session_commands.json": "c816829a57f8613c29fa228ce1a03b93df79997e619d8dfaa827186c7820171b", + "data/processed/lab056/rf_overview.png": "c08a8dab99677afb46b0cc43dbf2360da47da35a03dc00fdec48618c7c794f4f", + "experiments/lab056_capture_runner.py": "24c17060b8b3e3f6899f9e922ae818e7ae2efba456c5f5cada7c69b5e5429334", + "experiments/lab056_abab_energy.py": "323408b3bc1aa02c8b2f2855cd6295ac2efd99f13afec2ca0c872aa013a78f96" + } +} diff --git a/data/processed/lab056/rf_session_authorization.json b/data/processed/lab056/rf_session_authorization.json new file mode 100644 index 0000000..4cf58cb --- /dev/null +++ b/data/processed/lab056/rf_session_authorization.json @@ -0,0 +1,30 @@ +{ + "authorized_tx_sessions": 1, + "automatic_retry": false, + "point": { + "frequency_hz": 1400000000, + "physical_path": "MASTER TX -> TX antenna -> free-space LOS -> RX antenna -> SLAVE RX; user-specified approximate distance 3-5 m; fixed equipment and cables", + "path_confirmed": true, + "tx_antenna": "Existing 1400 MHz antenna, as described by user; model/marking not supplied", + "rx_antenna": "Existing 1400 MHz antenna, as described by user; model/marking not supplied", + "distance_m": null, + "orientation": "Both stationary; same antenna orientation and polarization, per user", + "visibility": "LOS" + }, + "tx_sha256": "5b34bbe1a8e6d037d2d6d367a74e9eb84e8940db7da61600ffaa7f7497d4f600", + "tx_samples": 10588544, + "tx_seconds": 1.323568, + "rx_samples": 24000000, + "rx_seconds": 3.0, + "tx_gain_db": 10, + "rx_gain_db": [ + 24, + 24 + ], + "amp": false, + "bias_tee": false, + "sample_rate_hz": 8000000, + "digital_carrier_hz": 50000, + "source": "Explicit user authorization in current turn", + "recorded_at_utc": "2026-09-10T09:32:24.185185+00:00" +} diff --git a/data/processed/lab056/rf_session_commands.json b/data/processed/lab056/rf_session_commands.json new file mode 100644 index 0000000..07ac902 --- /dev/null +++ b/data/processed/lab056/rf_session_commands.json @@ -0,0 +1,262 @@ +{ + "tx_process_invocations": 1, + "automatic_retry": false, + "commands": [ + { + "command": [ + "C:\\Users\\user\\Tools\\HackRF\\Library\\bin\\hackrf_info.exe" + ], + "started_at_utc": "2026-09-10T09:32:24.697133+00:00", + "returncode": 0, + "stdout": "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\n", + "stderr": "", + "finished_at_utc": "2026-09-10T09:32:24.719605+00:00" + }, + { + "command": [ + "C:\\Users\\user\\Tools\\HackRF\\Library\\bin\\hackrf_clock.exe", + "-d", + "000000000000000097c063dc323b97d3", + "-o", + "0" + ], + "started_at_utc": "2026-09-10T09:32:24.719707+00:00", + "returncode": 0, + "stdout": "", + "stderr": "", + "finished_at_utc": "2026-09-10T09:32:24.736985+00:00" + }, + { + "command": [ + "C:\\Users\\user\\Tools\\HackRF\\Library\\bin\\hackrf_clock.exe", + "-d", + "000000000000000097c063dc2e5327d3", + "-o", + "0" + ], + "started_at_utc": "2026-09-10T09:32:24.737066+00:00", + "returncode": 0, + "stdout": "", + "stderr": "", + "finished_at_utc": "2026-09-10T09:32:24.753370+00:00" + }, + { + "command": [ + "C:\\Users\\user\\Tools\\HackRF\\Library\\bin\\hackrf_transfer.exe", + "-d", + "000000000000000097c063dc323b97d3", + "-r", + "NUL", + "-f", + "1400000000", + "-s", + "8000000", + "-n", + "65536", + "-l", + "24", + "-g", + "24", + "-a", + "0", + "-p", + "0", + "-B" + ], + "started_at_utc": "2026-09-10T09:32:24.753426+00:00", + "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 65536/0Mio\nStop with Ctrl-C\n 0.3 MiB / 0.018 sec = 14.8 MiB/second, average power -29.9 dBfs, 9376 bytes free in buffer, 0 overruns, longest 0 bytes\n\nExiting...\nTotal time: 0.01795 s\nhackrf_stop_rx() done\nTransfer statistics:\n274144 bytes transferred by M0\n262144 bytes transferred by M4\n0 overruns, longest 0 bytes\nhackrf_close() done\nhackrf_exit() done\nfclose() done\nexit\n", + "finished_at_utc": "2026-09-10T09:32:24.790201+00:00" + }, + { + "command": [ + "C:\\Users\\user\\Tools\\HackRF\\Library\\bin\\hackrf_debug.exe", + "-d", + "000000000000000097c063dc323b97d3", + "--si5351c", + "-n", + "29", + "-r" + ], + "started_at_utc": "2026-09-10T09:32:24.790253+00:00", + "returncode": 0, + "stdout": "[ 29] -> 0x26\n", + "stderr": "", + "finished_at_utc": "2026-09-10T09:32:24.807254+00:00" + }, + { + "command": [ + "C:\\Users\\user\\Tools\\HackRF\\Library\\bin\\hackrf_clock.exe", + "-d", + "000000000000000097c063dc323b97d3", + "-o", + "1" + ], + "started_at_utc": "2026-09-10T09:32:24.807310+00:00", + "returncode": 0, + "stdout": "", + "stderr": "", + "finished_at_utc": "2026-09-10T09:32:24.823113+00:00" + }, + { + "command": [ + "C:\\Users\\user\\Tools\\HackRF\\Library\\bin\\hackrf_clock.exe", + "-d", + "000000000000000097c063dc2e5327d3", + "-i" + ], + "started_at_utc": "2026-09-10T09:32:24.858007+00:00", + "returncode": 0, + "stdout": "CLKIN status: no clock signal detected\n", + "stderr": "", + "finished_at_utc": "2026-09-10T09:32:24.876412+00:00" + }, + { + "command": [ + "C:\\Users\\user\\Tools\\HackRF\\Library\\bin\\hackrf_clock.exe", + "-d", + "000000000000000097c063dc2e5327d3", + "-i" + ], + "started_at_utc": "2026-09-10T09:32:24.926726+00:00", + "returncode": 0, + "stdout": "CLKIN status: clock signal detected\n", + "stderr": "", + "finished_at_utc": "2026-09-10T09:32:24.944618+00:00" + }, + { + "command": [ + "C:\\Users\\user\\Tools\\HackRF\\Library\\bin\\hackrf_transfer.exe", + "-d", + "000000000000000097c063dc2e5327d3", + "-r", + "NUL", + "-f", + "1400000000", + "-s", + "8000000", + "-n", + "65536", + "-l", + "24", + "-g", + "24", + "-a", + "0", + "-p", + "0", + "-B" + ], + "started_at_utc": "2026-09-10T09:32:24.944681+00:00", + "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 65536/0Mio\nStop with Ctrl-C\n 0.3 MiB / 0.017 sec = 15.4 MiB/second, average power -35.4 dBfs, 8832 bytes free in buffer, 0 overruns, longest 0 bytes\n\nExiting...\nTotal time: 0.01704 s\nhackrf_stop_rx() done\nTransfer statistics:\n274976 bytes transferred by M0\n262144 bytes transferred by M4\n0 overruns, longest 0 bytes\nhackrf_close() done\nhackrf_exit() done\nfclose() done\nexit\n", + "finished_at_utc": "2026-09-10T09:32:24.979112+00:00" + }, + { + "command": [ + "C:\\Users\\user\\Tools\\HackRF\\Library\\bin\\hackrf_debug.exe", + "-d", + "000000000000000097c063dc2e5327d3", + "--si5351c", + "-n", + "29", + "-r" + ], + "started_at_utc": "2026-09-10T09:32:24.979161+00:00", + "returncode": 0, + "stdout": "[ 29] -> 0x26\n", + "stderr": "", + "finished_at_utc": "2026-09-10T09:32:24.995463+00:00" + }, + { + "command": [ + "C:\\Users\\user\\Tools\\HackRF\\Library\\bin\\hackrf_transfer.exe", + "-d", + "000000000000000097c063dc2e5327d3", + "-r", + "C:\\Users\\user\\Desktop\\projects\\SDR_Rover\\data\\raw\\lab056\\rx_stream.cs8", + "-f", + "1400000000", + "-s", + "8000000", + "-n", + "24000000", + "-l", + "24", + "-g", + "24", + "-a", + "0", + "-p", + "0", + "-B" + ], + "started_at_utc": "2026-09-10T09:32:24.995551+00:00", + "background_process": true, + "stdout_path": "C:\\Users\\user\\Desktop\\projects\\SDR_Rover\\data\\raw\\lab056\\rx_stdout.txt", + "stderr_path": "C:\\Users\\user\\Desktop\\projects\\SDR_Rover\\data\\raw\\lab056\\rx_stderr.txt", + "returncode": 0, + "finished_at_utc": "2026-09-10T09:32:28.029923+00:00", + "returncode_after_cleanup": 0, + "cleanup_at_utc": "2026-09-10T09:32:28.683438+00:00" + }, + { + "command": [ + "C:\\Users\\user\\Tools\\HackRF\\Library\\bin\\hackrf_transfer.exe", + "-d", + "000000000000000097c063dc323b97d3", + "-t", + "C:\\Users\\user\\Desktop\\projects\\SDR_Rover\\data\\raw\\lab056\\tx_abab.cs8", + "-f", + "1400000000", + "-s", + "8000000", + "-n", + "10588544", + "-x", + "10", + "-a", + "0", + "-p", + "0", + "-B" + ], + "started_at_utc": "2026-09-10T09:32:25.512893+00:00", + "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 10588544/10Mio\nStop with Ctrl-C\n16.0 MiB / 1.004 sec = 15.9 MiB/second, average power -11.3 dBfs, 13920 bytes filled in buffer, 0 underruns, longest 0 bytes\n 5.2 MiB / 0.322 sec = 16.1 MiB/second, average power -11.2 dBfs, 5120 bytes filled in buffer, 0 underruns, longest 0 bytes\n\nExiting...\nTotal time: 1.32538 s\nhackrf_stop_tx() done\nTransfer statistics:\n21200896 bytes transferred by M0\n21200896 bytes transferred by M4\n0 underruns, longest 0 bytes\nhackrf_close() done\nhackrf_exit() done\nfclose() done\nexit\n", + "finished_at_utc": "2026-09-10T09:32:26.901844+00:00" + }, + { + "command": [ + "C:\\Users\\user\\Tools\\HackRF\\Library\\bin\\hackrf_clock.exe", + "-d", + "000000000000000097c063dc323b97d3", + "-o", + "0" + ], + "started_at_utc": "2026-09-10T09:32:28.683494+00:00", + "returncode": 0, + "stdout": "", + "stderr": "", + "finished_at_utc": "2026-09-10T09:32:28.698845+00:00" + }, + { + "command": [ + "C:\\Users\\user\\Tools\\HackRF\\Library\\bin\\hackrf_clock.exe", + "-d", + "000000000000000097c063dc2e5327d3", + "-o", + "0" + ], + "started_at_utc": "2026-09-10T09:32:28.698895+00:00", + "returncode": 0, + "stdout": "", + "stderr": "", + "finished_at_utc": "2026-09-10T09:32:28.714042+00:00" + } + ], + "orchestration_finished_at_utc": "2026-09-10T09:32:29.203758+00:00" +} diff --git a/data/processed/lab056/rx_preflight_commands.json b/data/processed/lab056/rx_preflight_commands.json new file mode 100644 index 0000000..782e0f8 --- /dev/null +++ b/data/processed/lab056/rx_preflight_commands.json @@ -0,0 +1,218 @@ +{ + "rf_tx_count": 0, + "tx_commands": 0, + "commands": [ + { + "command": [ + "C:\\Users\\user\\Tools\\HackRF\\Library\\bin\\hackrf_info.exe" + ], + "started_at_utc": "2026-09-10T09:16:28.777578+00:00", + "returncode": 0, + "stdout": "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\n", + "stderr": "", + "finished_at_utc": "2026-09-10T09:16:28.849704+00:00" + }, + { + "command": [ + "C:\\Users\\user\\Tools\\HackRF\\Library\\bin\\hackrf_clock.exe", + "-d", + "000000000000000097c063dc323b97d3", + "-o", + "0" + ], + "started_at_utc": "2026-09-10T09:16:28.849843+00:00", + "returncode": 0, + "stdout": "", + "stderr": "", + "finished_at_utc": "2026-09-10T09:16:28.881216+00:00" + }, + { + "command": [ + "C:\\Users\\user\\Tools\\HackRF\\Library\\bin\\hackrf_clock.exe", + "-d", + "000000000000000097c063dc2e5327d3", + "-o", + "0" + ], + "started_at_utc": "2026-09-10T09:16:28.881271+00:00", + "returncode": 0, + "stdout": "", + "stderr": "", + "finished_at_utc": "2026-09-10T09:16:28.899025+00:00" + }, + { + "command": [ + "C:\\Users\\user\\Tools\\HackRF\\Library\\bin\\hackrf_transfer.exe", + "-d", + "000000000000000097c063dc323b97d3", + "-r", + "NUL", + "-f", + "1400000000", + "-s", + "8000000", + "-n", + "65536", + "-l", + "24", + "-g", + "24", + "-a", + "0", + "-p", + "0", + "-B" + ], + "started_at_utc": "2026-09-10T09:16:28.899092+00:00", + "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 65536/0Mio\nStop with Ctrl-C\n 0.3 MiB / 0.019 sec = 13.5 MiB/second, average power -27.9 dBfs, 9280 bytes free in buffer, 0 overruns, longest 0 bytes\n\nExiting...\nTotal time: 0.01979 s\nhackrf_stop_rx() done\nTransfer statistics:\n276256 bytes transferred by M0\n262144 bytes transferred by M4\n0 overruns, longest 0 bytes\nhackrf_close() done\nhackrf_exit() done\nfclose() done\nexit\n", + "finished_at_utc": "2026-09-10T09:16:28.950743+00:00" + }, + { + "command": [ + "C:\\Users\\user\\Tools\\HackRF\\Library\\bin\\hackrf_debug.exe", + "-d", + "000000000000000097c063dc323b97d3", + "--si5351c", + "-n", + "29", + "-r" + ], + "started_at_utc": "2026-09-10T09:16:28.950806+00:00", + "returncode": 0, + "stdout": "[ 29] -> 0x26\n", + "stderr": "", + "finished_at_utc": "2026-09-10T09:16:28.980731+00:00" + }, + { + "command": [ + "C:\\Users\\user\\Tools\\HackRF\\Library\\bin\\hackrf_clock.exe", + "-d", + "000000000000000097c063dc323b97d3", + "-o", + "1" + ], + "started_at_utc": "2026-09-10T09:16:28.980796+00:00", + "returncode": 0, + "stdout": "", + "stderr": "", + "finished_at_utc": "2026-09-10T09:16:28.996692+00:00" + }, + { + "command": [ + "C:\\Users\\user\\Tools\\HackRF\\Library\\bin\\hackrf_clock.exe", + "-d", + "000000000000000097c063dc2e5327d3", + "-i" + ], + "started_at_utc": "2026-09-10T09:16:29.030946+00:00", + "returncode": 0, + "stdout": "CLKIN status: clock signal detected\n", + "stderr": "", + "finished_at_utc": "2026-09-10T09:16:29.048522+00:00" + }, + { + "command": [ + "C:\\Users\\user\\Tools\\HackRF\\Library\\bin\\hackrf_transfer.exe", + "-d", + "000000000000000097c063dc2e5327d3", + "-r", + "NUL", + "-f", + "1400000000", + "-s", + "8000000", + "-n", + "65536", + "-l", + "24", + "-g", + "24", + "-a", + "0", + "-p", + "0", + "-B" + ], + "started_at_utc": "2026-09-10T09:16:29.048596+00:00", + "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 65536/0Mio\nStop with Ctrl-C\n 0.3 MiB / 0.019 sec = 14.0 MiB/second, average power -32.0 dBfs, 9120 bytes free in buffer, 0 overruns, longest 0 bytes\n\nExiting...\nTotal time: 0.01894 s\nhackrf_stop_rx() done\nTransfer statistics:\n273920 bytes transferred by M0\n262144 bytes transferred by M4\n0 overruns, longest 0 bytes\nhackrf_close() done\nhackrf_exit() done\nfclose() done\nexit\n", + "finished_at_utc": "2026-09-10T09:16:29.084743+00:00" + }, + { + "command": [ + "C:\\Users\\user\\Tools\\HackRF\\Library\\bin\\hackrf_debug.exe", + "-d", + "000000000000000097c063dc2e5327d3", + "--si5351c", + "-n", + "29", + "-r" + ], + "started_at_utc": "2026-09-10T09:16:29.084857+00:00", + "returncode": 0, + "stdout": "[ 29] -> 0x26\n", + "stderr": "", + "finished_at_utc": "2026-09-10T09:16:29.100716+00:00" + }, + { + "command": [ + "C:\\Users\\user\\Tools\\HackRF\\Library\\bin\\hackrf_transfer.exe", + "-d", + "000000000000000097c063dc2e5327d3", + "-r", + "C:\\Users\\user\\Desktop\\projects\\SDR_Rover\\data\\raw\\lab056\\preflight_rx.cs8", + "-f", + "1400000000", + "-s", + "8000000", + "-n", + "800000", + "-l", + "24", + "-g", + "24", + "-a", + "0", + "-p", + "0", + "-B" + ], + "started_at_utc": "2026-09-10T09:16:29.100863+00:00", + "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.119 sec = 15.4 MiB/second, average power -35.2 dBfs, 15552 bytes free in buffer, 0 overruns, longest 0 bytes\n\nExiting...\nTotal time: 0.11922 s\nhackrf_stop_rx() done\nTransfer statistics:\n1913376 bytes transferred by M0\n1900544 bytes transferred by M4\n0 overruns, longest 0 bytes\nhackrf_close() done\nhackrf_exit() done\nfclose() done\nexit\n", + "finished_at_utc": "2026-09-10T09:16:29.239488+00:00" + }, + { + "command": [ + "C:\\Users\\user\\Tools\\HackRF\\Library\\bin\\hackrf_clock.exe", + "-d", + "000000000000000097c063dc323b97d3", + "-o", + "0" + ], + "started_at_utc": "2026-09-10T09:16:29.271470+00:00", + "returncode": 0, + "stdout": "", + "stderr": "", + "finished_at_utc": "2026-09-10T09:16:29.288606+00:00" + }, + { + "command": [ + "C:\\Users\\user\\Tools\\HackRF\\Library\\bin\\hackrf_clock.exe", + "-d", + "000000000000000097c063dc2e5327d3", + "-o", + "0" + ], + "started_at_utc": "2026-09-10T09:16:29.288689+00:00", + "returncode": 0, + "stdout": "", + "stderr": "", + "finished_at_utc": "2026-09-10T09:16:29.309700+00:00" + } + ] +} diff --git a/data/processed/lab056/rx_preflight_plan.json b/data/processed/lab056/rx_preflight_plan.json new file mode 100644 index 0000000..8254419 --- /dev/null +++ b/data/processed/lab056/rx_preflight_plan.json @@ -0,0 +1,26 @@ +{ + "point": { + "frequency_hz": 1400000000, + "physical_path": "MASTER TX -> TX antenna -> free-space LOS -> RX antenna -> SLAVE RX; user-specified approximate distance 3-5 m; fixed equipment and cables", + "path_confirmed": true, + "tx_antenna": "Existing 1400 MHz antenna, as described by user; model/marking not supplied", + "rx_antenna": "Existing 1400 MHz antenna, as described by user; model/marking not supplied", + "distance_m": null, + "orientation": "Both stationary; same antenna orientation and polarization, per user", + "visibility": "LOS" + }, + "distance_description": "Approximately 3-5 m per user; exact measured value not supplied", + "frequency_role": "CURRENT_LAB056_TEST_ONLY_NOT_ROVER_OPERATING_BAND", + "field_band_context": "Approximately 180 MHz considered separately; 50/144 MHz outside Lab056", + "scope": "COMMON_CLOCK_AND_RX_ONLY_PREFLIGHT_THEN_STOP", + "tx_authorized": false, + "rf_tx_count": 0, + "rx_preflight_samples": 800000, + "sample_rate_hz": 8000000, + "rx_lna_db": 24, + "rx_vga_db": 24, + "rf_amplifier": false, + "bias_tee": false, + "external_pa_lna": false, + "recorded_at_utc": "2026-09-10T09:16:28.289788+00:00" +} diff --git a/data/processed/lab056/rx_preflight_result.json b/data/processed/lab056/rx_preflight_result.json new file mode 100644 index 0000000..bc95ef8 --- /dev/null +++ b/data/processed/lab056/rx_preflight_result.json @@ -0,0 +1,114 @@ +{ + "passed": true, + "rf_tx_count": 0, + "point": { + "frequency_hz": 1400000000, + "physical_path": "MASTER TX -> TX antenna -> free-space LOS -> RX antenna -> SLAVE RX; user-specified approximate distance 3-5 m; fixed equipment and cables", + "path_confirmed": true, + "tx_antenna": "Existing 1400 MHz antenna, as described by user; model/marking not supplied", + "rx_antenna": "Existing 1400 MHz antenna, as described by user; model/marking not supplied", + "distance_m": null, + "orientation": "Both stationary; same antenna orientation and polarization, per user", + "visibility": "LOS" + }, + "tx_sha256": "5b34bbe1a8e6d037d2d6d367a74e9eb84e8940db7da61600ffaa7f7497d4f600", + "stop_before_tx": true, + "process_before": { + "state": "CLEAR", + "attempts": [ + { + "state": "CLEAR", + "mechanism": "psutil", + "matches": [], + "inspected": 297, + "errors": [] + } + ] + }, + "inventory": { + "passed": true, + "serials": [ + "000000000000000097c063dc2e5327d3", + "000000000000000097c063dc323b97d3" + ], + "master_serial": "000000000000000097c063dc323b97d3", + "slave_serial": "000000000000000097c063dc2e5327d3", + "raw_output": "hackrf_info version: 2024.02.1\nlibhackrf version: 2024.02.1 (0.9)\nFound HackRF\nIndex: 0\nSerial number: 000000000000000097c063dc323b97d3\nBoard ID Number: 4 (HackRF One)\nFirmware Version: 2024.02.1 (API:1.08)\nPart ID Number: 0xa000cb3c 0x00744f5a\nHardware Revision: r9\nHardware does not appear to have been manufactured by Great Scott Gadgets.\nHardware supported by installed firmware:\n HackRF One\n\nFound HackRF\nIndex: 1\nSerial number: 000000000000000097c063dc2e5327d3\nBoard ID Number: 4 (HackRF One)\nFirmware Version: 2024.02.1 (API:1.08)\nPart ID Number: 0xa000cb3c 0xbc4b4739\nHardware Revision: r9\nHardware does not appear to have been manufactured by Great Scott Gadgets.\nHardware supported by installed firmware:\n HackRF One" + }, + "clock": { + "passed": true, + "master_reg29": 38, + "slave_reg29": 38, + "slave_clkin_status": "DETECTED", + "slave_clock_wait": { + "state": "DETECTED", + "queries": 1, + "time_to_detect_ms": 67.78709997888654, + "elapsed_ms": 67.78709997888654, + "polls": [ + { + "query": 1, + "start_ms": 50.16230000182986, + "end_ms": 67.78709997888654, + "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.119 sec = 15.4 MiB/second, average power -35.2 dBfs, 15552 bytes free in buffer, 0 overruns, longest 0 bytes\n\nExiting...\nTotal time: 0.11922 s\nhackrf_stop_rx() done\nTransfer statistics:\n1913376 bytes transferred by M0\n1900544 bytes transferred by M4\n0 overruns, longest 0 bytes\nhackrf_close() done\nhackrf_exit() done\nfclose() done\nexit\n" + }, + "rx_level": { + "samples": 800000, + "mean_i": 0.92535625, + "mean_q": 1.82532, + "power_code2": 4.85388125, + "ac_power_code2": 0.6658039581859372, + "rms_code": 2.20315257074947, + "peak_magnitude_code": 6.082762530298219, + "max_abs_i": 4.0, + "max_abs_q": 6.0, + "papr_db": 8.821125769372884, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.0002426940625, + "saturation_detected": false, + "analog_compression": "UNKNOWN_WITHOUT_CALIBRATION" + }, + "rx_sha256": "c4257f26e4cc8562eb32228f9a50fdeb055879c928af0744cdf4a81cf7cd085b", + "process_after": { + "state": "CLEAR", + "attempts": [ + { + "state": "CLEAR", + "mechanism": "psutil", + "matches": [], + "inspected": 297, + "errors": [] + } + ] + }, + "cleanup_errors": [], + "source_path": "data/raw/lab056/preflight_result.json", + "source_sha256": "b691e51653f9e814627fd100cf99de0f22fef837b177030227ecddc5af573cf3", + "ready_for_single_abab_tx": true, + "tx_authorization_received": false, + "verifier": { + "mode": "SAVED_FILES_READ_ONLY", + "passed": true, + "reproduced_discrepancies": 0, + "independent_recalculation": "INTEGER_MOMENTS_AND_RAIL_COUNTS", + "hardware_commands": 0 + }, + "audited_hardware_commands": 12, + "audited_rx_transfers": 3, + "audited_tx_commands": 0, + "frequency_role": "LAB056_TEST_ONLY_NOT_ROVER_OPERATING_BAND", + "stop": true +} diff --git a/data/processed/lab056/software_equal.json b/data/processed/lab056/software_equal.json new file mode 100644 index 0000000..8efa3a2 --- /dev/null +++ b/data/processed/lab056/software_equal.json @@ -0,0 +1,489 @@ +{ + "scope": "TX_REFERENCE_DIAGNOSTICS_NOT_INDEPENDENT_PACKET_RX", + "rf_tx": 0, + "tx_sha256": "5b34bbe1a8e6d037d2d6d367a74e9eb84e8940db7da61600ffaa7f7497d4f600", + "rx_sha256": "a88afcc1bbeb50092f9f5baca6eb607d25daf508e0fafcdcada318818d9ffd62", + "alignment": { + "start": 4109216, + "correlation": 0.712109578315035, + "coarse_cfo_hz": 0.25, + "stride": 32 + }, + "segments": [ + { + "label": "A1", + "symbol_rate": 20000, + "raw_rx": { + "samples": 1638400, + "mean_i": 0.9995880126953125, + "mean_q": 2.00025390625, + "power_code2": 7.596464233398438, + "ac_power_code2": 2.596272348805888, + "rms_code": 2.7561683971409363, + "peak_magnitude_code": 6.708203932499369, + "max_abs_i": 5.0, + "max_abs_q": 6.0, + "papr_db": 7.726010163935343, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.0003798232116699219, + "band_27000_power": 1.729349736724617, + "band_675000_power": 1.8014764601725908, + "power_standard_error": 0.039755811858960266 + }, + "local_zero_iq_background_windows": [ + { + "samples": 480000, + "mean_i": 1.0012270833333334, + "mean_q": 2.0008, + "power_code2": 5.88834375, + "ac_power_code2": 0.8826874375998264, + "rms_code": 2.4265909729494997, + "peak_magnitude_code": 5.385164807134504, + "max_abs_i": 4.0, + "max_abs_q": 5.0, + "papr_db": 6.924048425663756, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.0002944171875, + "band_27000_power": 0.002998337896541192, + "band_675000_power": 0.0744039583655369, + "power_standard_error": 0.0012691882704167598 + }, + { + "samples": 480000, + "mean_i": 0.9989395833333333, + "mean_q": 2.000110416666667, + "power_code2": 5.883275, + "ac_power_code2": 0.8849530299913191, + "rms_code": 2.42554633021099, + "peak_magnitude_code": 5.385164807134504, + "max_abs_i": 4.0, + "max_abs_q": 5.0, + "papr_db": 6.9277884895206405, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.00029416375, + "band_27000_power": 0.003012451963227414, + "band_675000_power": 0.07417080321149919, + "power_standard_error": 0.0013506571909630695 + } + ], + "background_power": 0.8838202337955727, + "background_subtracted_raw": 1.7124521150103154, + "background_subtracted_uncertainty": 0.039782742266144235, + "background_subtracted_stable": true, + "background_subtracted_signal_power": 1.7124521150103154, + "channel_gain_power": 0.0007924265932905088, + "measurement": { + "frame": 1, + "start": 4909213, + "alignment_adjustment": -3, + "rx_active": { + "samples": 1638400, + "mean_i": 0.9995880126953125, + "mean_q": 2.00025390625, + "power_code2": 7.596464233398438, + "ac_power_code2": 2.596272348805888, + "rms_code": 2.7561683971409363, + "peak_magnitude_code": 6.708203932499369, + "max_abs_i": 5.0, + "max_abs_q": 6.0, + "papr_db": 7.726010163935343, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.0003798232116699219 + }, + "cfo_hz": 0.1968177799965115, + "gain_abs": 0.028150072704888503, + "dc_i": 0.9997948620728929, + "dc_q": 2.0010324962850192, + "train_signal_power": 1.55023266362049, + "train_residual_power": 0.9894122701085495, + "train_actual_ac_power": 2.539644933729041, + "train_cross_power": 2.1058155219577658e-15, + "heldout_signal_power": 1.549823229026632, + "heldout_residual_power": 0.8945490859865625, + "heldout_actual_ac_power": 2.6529010618609536, + "heldout_cross_power": 0.20852874684775813, + "heldout_residual_band_27000_power": 0.010080618023636462, + "heldout_residual_band_675000_power": 0.08179753911385561, + "heldout_residual_band_1000000_power": 0.11760791625473926, + "heldout_residual_matched_power": 3.4888049404460095, + "effective_signal_to_residual_db": 2.3867798958438566 + } + }, + { + "label": "B1", + "symbol_rate": 500000, + "raw_rx": { + "samples": 1638400, + "mean_i": 0.998636474609375, + "mean_q": 1.9995599365234376, + "power_code2": 7.572622680664063, + "ac_power_code2": 2.5771079324942088, + "rms_code": 2.751839871915527, + "peak_magnitude_code": 7.211102550927978, + "max_abs_i": 5.0, + "max_abs_q": 7.0, + "papr_db": 8.367570257627738, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 1.5145245361328126e-05, + "band_27000_power": 0.08434245734375428, + "band_675000_power": 1.7818539229368797, + "power_standard_error": 0.03929664272862704 + }, + "local_zero_iq_background_windows": [ + { + "samples": 480000, + "mean_i": 0.9989395833333333, + "mean_q": 2.000110416666667, + "power_code2": 5.883275, + "ac_power_code2": 0.8849530299913191, + "rms_code": 2.42554633021099, + "peak_magnitude_code": 5.385164807134504, + "max_abs_i": 4.0, + "max_abs_q": 5.0, + "papr_db": 6.9277884895206405, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 1.176655e-05, + "band_27000_power": 0.003012451963227414, + "band_675000_power": 0.07417080321149919, + "power_standard_error": 0.0013506571909630695 + }, + { + "samples": 480000, + "mean_i": 0.9989791666666666, + "mean_q": 2.00009375, + "power_code2": 5.883064583333334, + "ac_power_code2": 0.8847301991102432, + "rms_code": 2.4255029547154408, + "peak_magnitude_code": 5.0990195135927845, + "max_abs_i": 4.0, + "max_abs_q": 5.0, + "papr_db": 6.45369731942485, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 1.1766129166666667e-05, + "band_27000_power": 0.0030190450807723868, + "band_675000_power": 0.07463668207172841, + "power_standard_error": 0.0009021263608471298 + } + ], + "background_power": 0.8848416145507811, + "background_subtracted_raw": 1.6922663179434276, + "background_subtracted_uncertainty": 0.03930519138544367, + "background_subtracted_stable": true, + "background_subtracted_signal_power": 1.6922663179434276, + "channel_gain_power": 0.0007927525626002337, + "measurement": { + "frame": 1, + "start": 7364413, + "alignment_adjustment": -3, + "rx_active": { + "samples": 1638400, + "mean_i": 0.998636474609375, + "mean_q": 1.9995599365234376, + "power_code2": 7.572622680664063, + "ac_power_code2": 2.5771079324942088, + "rms_code": 2.751839871915527, + "peak_magnitude_code": 7.211102550927978, + "max_abs_i": 5.0, + "max_abs_q": 7.0, + "papr_db": 8.367570257627738, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 1.5145245361328126e-05 + }, + "cfo_hz": 0.20030217648162243, + "gain_abs": 0.028155861958040526, + "dc_i": 0.9986008794024919, + "dc_q": 2.00024705167195, + "train_signal_power": 1.5355362521933293, + "train_residual_power": 0.9879108077929747, + "train_actual_ac_power": 2.523447059986303, + "train_cross_power": -1.306454944227653e-15, + "heldout_signal_power": 1.5355180397870447, + "heldout_residual_power": 0.8925132940453717, + "heldout_actual_ac_power": 2.630769751790605, + "heldout_cross_power": 0.2027384179581883, + "heldout_residual_band_27000_power": 0.0033142687288771526, + "heldout_residual_band_675000_power": 0.0813443897071584, + "heldout_residual_band_1000000_power": 0.11762789507725498, + "heldout_residual_matched_power": 0.9819762872749349, + "effective_signal_to_residual_db": 2.3564022937017817 + } + }, + { + "label": "A2", + "symbol_rate": 20000, + "raw_rx": { + "samples": 1638400, + "mean_i": 1.000250244140625, + "mean_q": 2.0000390625, + "power_code2": 7.5943505859375, + "ac_power_code2": 2.5936937835082423, + "rms_code": 2.755784931001964, + "peak_magnitude_code": 7.211102550927978, + "max_abs_i": 5.0, + "max_abs_q": 6.0, + "papr_db": 8.355127015785026, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.000379717529296875, + "band_27000_power": 1.7263462989934586, + "band_675000_power": 1.7983862355807485, + "power_standard_error": 0.03961683202831068 + }, + "local_zero_iq_background_windows": [ + { + "samples": 480000, + "mean_i": 0.9989791666666666, + "mean_q": 2.00009375, + "power_code2": 5.883064583333334, + "ac_power_code2": 0.8847301991102432, + "rms_code": 2.4255029547154408, + "peak_magnitude_code": 5.0990195135927845, + "max_abs_i": 4.0, + "max_abs_q": 5.0, + "papr_db": 6.45369731942485, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.00029415322916666667, + "band_27000_power": 0.0030190450807723868, + "band_675000_power": 0.07463668207172841, + "power_standard_error": 0.0009021263608471298 + }, + { + "samples": 480000, + "mean_i": 1.0004854166666666, + "mean_q": 2.0000104166666666, + "power_code2": 5.883220833333334, + "ac_power_code2": 0.8822080975954867, + "rms_code": 2.425535164315977, + "peak_magnitude_code": 5.830951894845301, + "max_abs_i": 4.0, + "max_abs_q": 5.0, + "papr_db": 7.6186376661535915, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.0002941610416666667, + "band_27000_power": 0.003053650977045963, + "band_675000_power": 0.073961606327725, + "power_standard_error": 0.0012067695631605175 + } + ], + "background_power": 0.8834691483528649, + "background_subtracted_raw": 1.7102246351553774, + "background_subtracted_uncertainty": 0.03964405579856132, + "background_subtracted_stable": true, + "background_subtracted_signal_power": 1.7102246351553774, + "channel_gain_power": 0.0007920055569282462, + "measurement": { + "frame": 1, + "start": 9803485, + "alignment_adjustment": -3, + "rx_active": { + "samples": 1638400, + "mean_i": 1.000250244140625, + "mean_q": 2.0000390625, + "power_code2": 7.5943505859375, + "ac_power_code2": 2.5936937835082423, + "rms_code": 2.755784931001964, + "peak_magnitude_code": 7.211102550927978, + "max_abs_i": 5.0, + "max_abs_q": 6.0, + "papr_db": 8.355127015785026, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.000379717529296875 + }, + "cfo_hz": 0.19617739757807567, + "gain_abs": 0.02814259328719097, + "dc_i": 1.0004532662364058, + "dc_q": 2.0003859700871685, + "train_signal_power": 1.5494089856585476, + "train_residual_power": 0.9895407718712785, + "train_actual_ac_power": 2.5389497575298265, + "train_cross_power": 4.0495384823202584e-16, + "heldout_signal_power": 1.548999768607681, + "heldout_residual_power": 0.8941535479569137, + "heldout_actual_ac_power": 2.6484381326123456, + "heldout_cross_power": 0.20528481604775145, + "heldout_residual_band_27000_power": 0.009765782070021769, + "heldout_residual_band_675000_power": 0.08171891866368976, + "heldout_residual_band_1000000_power": 0.1177276686242853, + "heldout_residual_matched_power": 3.3724974710374163, + "effective_signal_to_residual_db": 2.3863924873600086 + } + }, + { + "label": "B2", + "symbol_rate": 500000, + "raw_rx": { + "samples": 1638400, + "mean_i": 0.99942626953125, + "mean_q": 1.999466552734375, + "power_code2": 7.5732568359375, + "ac_power_code2": 2.5765374722048655, + "rms_code": 2.751955093372256, + "peak_magnitude_code": 6.708203932499369, + "max_abs_i": 5.0, + "max_abs_q": 6.0, + "papr_db": 7.739298282383329, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 1.5146513671875001e-05, + "band_27000_power": 0.08437478193987907, + "band_675000_power": 1.7815424066137258, + "power_standard_error": 0.03932113008198285 + }, + "local_zero_iq_background_windows": [ + { + "samples": 480000, + "mean_i": 1.0004854166666666, + "mean_q": 2.0000104166666666, + "power_code2": 5.883220833333334, + "ac_power_code2": 0.8822080975954867, + "rms_code": 2.425535164315977, + "peak_magnitude_code": 5.830951894845301, + "max_abs_i": 4.0, + "max_abs_q": 5.0, + "papr_db": 7.6186376661535915, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 1.1766441666666666e-05, + "band_27000_power": 0.003053650977045963, + "band_675000_power": 0.073961606327725, + "power_standard_error": 0.0012067695631605175 + }, + { + "samples": 480000, + "mean_i": 0.9988083333333333, + "mean_q": 1.9994125, + "power_code2": 5.878066666666666, + "ac_power_code2": 0.8827982347743042, + "rms_code": 2.4244724512080285, + "peak_magnitude_code": 5.0990195135927845, + "max_abs_i": 4.0, + "max_abs_q": 5.0, + "papr_db": 6.457388406130481, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 1.1756133333333333e-05, + "band_27000_power": 0.0029587129864172557, + "band_675000_power": 0.07444268200354834, + "power_standard_error": 0.0016031313210695718 + } + ], + "background_power": 0.8825031661848954, + "background_subtracted_raw": 1.69403430601997, + "background_subtracted_uncertainty": 0.039335034220138454, + "background_subtracted_stable": true, + "background_subtracted_signal_power": 1.69403430601997, + "channel_gain_power": 0.0007914780017038821, + "measurement": { + "frame": 1, + "start": 12258685, + "alignment_adjustment": -3, + "rx_active": { + "samples": 1638400, + "mean_i": 0.99942626953125, + "mean_q": 1.999466552734375, + "power_code2": 7.5732568359375, + "ac_power_code2": 2.5765374722048655, + "rms_code": 2.751955093372256, + "peak_magnitude_code": 6.708203932499369, + "max_abs_i": 5.0, + "max_abs_q": 6.0, + "papr_db": 7.739298282383329, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 1.5146513671875001e-05 + }, + "cfo_hz": 0.197924755610056, + "gain_abs": 0.028133218829417335, + "dc_i": 1.0007017709706703, + "dc_q": 1.999435627739676, + "train_signal_power": 1.5330674686733408, + "train_residual_power": 0.9881175175558363, + "train_actual_ac_power": 2.5211849862291755, + "train_cross_power": -1.4430123762565473e-15, + "heldout_signal_power": 1.5330492855483506, + "heldout_residual_power": 0.8931876810530448, + "heldout_actual_ac_power": 2.6318932139011078, + "heldout_cross_power": 0.20565624729971124, + "heldout_residual_band_27000_power": 0.0033209337668698995, + "heldout_residual_band_675000_power": 0.08160471700839024, + "heldout_residual_band_1000000_power": 0.11754379856225462, + "heldout_residual_matched_power": 0.9863150049618183, + "effective_signal_to_residual_db": 2.346133924823488 + } + } + ], + "pairs": [ + { + "pair": 1, + "received_power_difference_db": -0.051497256938167285, + "channel_gain_difference_db": 0.0017861283874782213, + "matched_component_difference_db": -0.04027242840779062, + "matched_component_symbol_energy_difference_db": -14.019672515128168 + }, + { + "pair": 2, + "received_power_difference_db": -0.04130956980203142, + "channel_gain_difference_db": -0.0028938012026523408, + "matched_component_difference_db": -0.044952357997923256, + "matched_component_symbol_energy_difference_db": -14.0243524447183 + } + ], + "outside_waveform_background": [ + { + "start": 3309216, + "samples": 400000, + "mean_i": 0.9997525, + "mean_q": 2.0, + "power_code2": 5.8853875, + "ac_power_code2": 0.8858824387437494, + "rms_code": 2.4259817600303593, + "peak_magnitude_code": 5.385164807134504, + "max_abs_i": 4.0, + "max_abs_q": 5.0, + "papr_db": 6.926229353802586, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.000294269375, + "band_27000_power": 0.00300239645688663, + "band_675000_power": 0.07464470494185707, + "power_standard_error": 0.001189001018090443 + }, + { + "start": 15097760, + "samples": 400000, + "mean_i": 1.00201, + "mean_q": 1.9998775, + "power_code2": 5.8860125, + "ac_power_code2": 0.8824784448937502, + "rms_code": 2.4261105704398553, + "peak_magnitude_code": 5.0990195135927845, + "max_abs_i": 4.0, + "max_abs_q": 5.0, + "papr_db": 6.451521679042596, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.000294300625, + "band_27000_power": 0.002877436011683443, + "band_675000_power": 0.07431026562360402, + "power_standard_error": 0.0010078630955450837 + } + ], + "background_note": "Внутренние gaps: нулевой цифровой IQ при работающем TX; это не RF OFF. Внешние окна также требуют сверки времён запуска/остановки по журналам.", + "power_units": "RX code squared, not calibrated watts/dBm", + "residual_note": "Шум и неописанные моделью искажения; RX не нормируется для выравнивания A/B", + "synthetic": true, + "expected_gain_difference_db": 0.0, + "known_sequence_start": 4160013, + "passed": true +} diff --git a/data/processed/lab056/software_equal_ota_summary.json b/data/processed/lab056/software_equal_ota_summary.json new file mode 100644 index 0000000..9d73089 --- /dev/null +++ b/data/processed/lab056/software_equal_ota_summary.json @@ -0,0 +1,532 @@ +{ + "scope": "TX_REFERENCE_DIAGNOSTICS_NOT_INDEPENDENT_PACKET_RX", + "rf_tx": 0, + "tx_sha256": "5b34bbe1a8e6d037d2d6d367a74e9eb84e8940db7da61600ffaa7f7497d4f600", + "rx_sha256": "a88afcc1bbeb50092f9f5baca6eb607d25daf508e0fafcdcada318818d9ffd62", + "alignment": { + "start": 4109216, + "correlation": 0.712109578315035, + "coarse_cfo_hz": 0.25, + "stride": 32 + }, + "segments": [ + { + "label": "A1", + "symbol_rate": 20000, + "raw_rx": { + "samples": 1638400, + "mean_i": 0.9995880126953125, + "mean_q": 2.00025390625, + "power_code2": 7.596464233398438, + "ac_power_code2": 2.596272348805888, + "rms_code": 2.7561683971409363, + "peak_magnitude_code": 6.708203932499369, + "max_abs_i": 5.0, + "max_abs_q": 6.0, + "papr_db": 7.726010163935343, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.0003798232116699219, + "band_27000_power": 1.729349736724617, + "band_675000_power": 1.8014764601725908, + "power_standard_error": 0.039755811858960266 + }, + "local_zero_iq_background_windows": [ + { + "samples": 480000, + "mean_i": 1.0012270833333334, + "mean_q": 2.0008, + "power_code2": 5.88834375, + "ac_power_code2": 0.8826874375998264, + "rms_code": 2.4265909729494997, + "peak_magnitude_code": 5.385164807134504, + "max_abs_i": 4.0, + "max_abs_q": 5.0, + "papr_db": 6.924048425663756, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.0002944171875, + "band_27000_power": 0.002998337896541192, + "band_675000_power": 0.0744039583655369, + "power_standard_error": 0.0012691882704167598 + }, + { + "samples": 480000, + "mean_i": 0.9989395833333333, + "mean_q": 2.000110416666667, + "power_code2": 5.883275, + "ac_power_code2": 0.8849530299913191, + "rms_code": 2.42554633021099, + "peak_magnitude_code": 5.385164807134504, + "max_abs_i": 4.0, + "max_abs_q": 5.0, + "papr_db": 6.9277884895206405, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.00029416375, + "band_27000_power": 0.003012451963227414, + "band_675000_power": 0.07417080321149919, + "power_standard_error": 0.0013506571909630695 + } + ], + "background_power": 0.8838202337955727, + "background_subtracted_raw": 1.7124521150103154, + "background_subtracted_uncertainty": 0.039782742266144235, + "background_subtracted_stable": true, + "background_subtracted_signal_power": 1.7124521150103154, + "channel_gain_complex": { + "real": -0.0027287468398317707, + "imag": -0.028017504063970737, + "phase_rad_at_active_start": -1.6678844763817968 + }, + "channel_gain_power": 0.0007924265932905088, + "measurement": { + "frame": 1, + "start": 4909213, + "alignment_adjustment": -3, + "rx_active": { + "samples": 1638400, + "mean_i": 0.9995880126953125, + "mean_q": 2.00025390625, + "power_code2": 7.596464233398438, + "ac_power_code2": 2.596272348805888, + "rms_code": 2.7561683971409363, + "peak_magnitude_code": 6.708203932499369, + "max_abs_i": 5.0, + "max_abs_q": 6.0, + "papr_db": 7.726010163935343, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.0003798232116699219 + }, + "cfo_hz": 0.1968177799965115, + "gain_abs": 0.028150072704888503, + "dc_i": 0.9997948620728929, + "dc_q": 2.0010324962850192, + "train_signal_power": 1.55023266362049, + "train_residual_power": 0.9894122701085495, + "train_actual_ac_power": 2.539644933729041, + "train_cross_power": 2.1058155219577658e-15, + "heldout_signal_power": 1.549823229026632, + "heldout_residual_power": 0.8945490859865625, + "heldout_actual_ac_power": 2.6529010618609536, + "heldout_cross_power": 0.20852874684775813, + "heldout_residual_band_27000_power": 0.010080618023636462, + "heldout_residual_band_675000_power": 0.08179753911385561, + "heldout_residual_band_1000000_power": 0.11760791625473926, + "heldout_residual_matched_power": 3.4888049404460095, + "effective_signal_to_residual_db": 2.3867798958438566 + } + }, + { + "label": "B1", + "symbol_rate": 500000, + "raw_rx": { + "samples": 1638400, + "mean_i": 0.998636474609375, + "mean_q": 1.9995599365234376, + "power_code2": 7.572622680664063, + "ac_power_code2": 2.5771079324942088, + "rms_code": 2.751839871915527, + "peak_magnitude_code": 7.211102550927978, + "max_abs_i": 5.0, + "max_abs_q": 7.0, + "papr_db": 8.367570257627738, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 1.5145245361328126e-05, + "band_27000_power": 0.08434245734375428, + "band_675000_power": 1.7818539229368797, + "power_standard_error": 0.03929664272862704 + }, + "local_zero_iq_background_windows": [ + { + "samples": 480000, + "mean_i": 0.9989395833333333, + "mean_q": 2.000110416666667, + "power_code2": 5.883275, + "ac_power_code2": 0.8849530299913191, + "rms_code": 2.42554633021099, + "peak_magnitude_code": 5.385164807134504, + "max_abs_i": 4.0, + "max_abs_q": 5.0, + "papr_db": 6.9277884895206405, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 1.176655e-05, + "band_27000_power": 0.003012451963227414, + "band_675000_power": 0.07417080321149919, + "power_standard_error": 0.0013506571909630695 + }, + { + "samples": 480000, + "mean_i": 0.9989791666666666, + "mean_q": 2.00009375, + "power_code2": 5.883064583333334, + "ac_power_code2": 0.8847301991102432, + "rms_code": 2.4255029547154408, + "peak_magnitude_code": 5.0990195135927845, + "max_abs_i": 4.0, + "max_abs_q": 5.0, + "papr_db": 6.45369731942485, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 1.1766129166666667e-05, + "band_27000_power": 0.0030190450807723868, + "band_675000_power": 0.07463668207172841, + "power_standard_error": 0.0009021263608471298 + } + ], + "background_power": 0.8848416145507811, + "background_subtracted_raw": 1.6922663179434276, + "background_subtracted_uncertainty": 0.03930519138544367, + "background_subtracted_stable": true, + "background_subtracted_signal_power": 1.6922663179434276, + "channel_gain_complex": { + "real": 0.007947194796737946, + "imag": -0.027011009930451232, + "phase_rad_at_active_start": -1.2846501572766547 + }, + "channel_gain_power": 0.0007927525626002337, + "measurement": { + "frame": 1, + "start": 7364413, + "alignment_adjustment": -3, + "rx_active": { + "samples": 1638400, + "mean_i": 0.998636474609375, + "mean_q": 1.9995599365234376, + "power_code2": 7.572622680664063, + "ac_power_code2": 2.5771079324942088, + "rms_code": 2.751839871915527, + "peak_magnitude_code": 7.211102550927978, + "max_abs_i": 5.0, + "max_abs_q": 7.0, + "papr_db": 8.367570257627738, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 1.5145245361328126e-05 + }, + "cfo_hz": 0.20030217648162243, + "gain_abs": 0.028155861958040526, + "dc_i": 0.9986008794024919, + "dc_q": 2.00024705167195, + "train_signal_power": 1.5355362521933293, + "train_residual_power": 0.9879108077929747, + "train_actual_ac_power": 2.523447059986303, + "train_cross_power": -1.306454944227653e-15, + "heldout_signal_power": 1.5355180397870447, + "heldout_residual_power": 0.8925132940453717, + "heldout_actual_ac_power": 2.630769751790605, + "heldout_cross_power": 0.2027384179581883, + "heldout_residual_band_27000_power": 0.0033142687288771526, + "heldout_residual_band_675000_power": 0.0813443897071584, + "heldout_residual_band_1000000_power": 0.11762789507725498, + "heldout_residual_matched_power": 0.9819762872749349, + "effective_signal_to_residual_db": 2.3564022937017817 + } + }, + { + "label": "A2", + "symbol_rate": 20000, + "raw_rx": { + "samples": 1638400, + "mean_i": 1.000250244140625, + "mean_q": 2.0000390625, + "power_code2": 7.5943505859375, + "ac_power_code2": 2.5936937835082423, + "rms_code": 2.755784931001964, + "peak_magnitude_code": 7.211102550927978, + "max_abs_i": 5.0, + "max_abs_q": 6.0, + "papr_db": 8.355127015785026, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.000379717529296875, + "band_27000_power": 1.7263462989934586, + "band_675000_power": 1.7983862355807485, + "power_standard_error": 0.03961683202831068 + }, + "local_zero_iq_background_windows": [ + { + "samples": 480000, + "mean_i": 0.9989791666666666, + "mean_q": 2.00009375, + "power_code2": 5.883064583333334, + "ac_power_code2": 0.8847301991102432, + "rms_code": 2.4255029547154408, + "peak_magnitude_code": 5.0990195135927845, + "max_abs_i": 4.0, + "max_abs_q": 5.0, + "papr_db": 6.45369731942485, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.00029415322916666667, + "band_27000_power": 0.0030190450807723868, + "band_675000_power": 0.07463668207172841, + "power_standard_error": 0.0009021263608471298 + }, + { + "samples": 480000, + "mean_i": 1.0004854166666666, + "mean_q": 2.0000104166666666, + "power_code2": 5.883220833333334, + "ac_power_code2": 0.8822080975954867, + "rms_code": 2.425535164315977, + "peak_magnitude_code": 5.830951894845301, + "max_abs_i": 4.0, + "max_abs_q": 5.0, + "papr_db": 7.6186376661535915, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.0002941610416666667, + "band_27000_power": 0.003053650977045963, + "band_675000_power": 0.073961606327725, + "power_standard_error": 0.0012067695631605175 + } + ], + "background_power": 0.8834691483528649, + "background_subtracted_raw": 1.7102246351553774, + "background_subtracted_uncertainty": 0.03964405579856132, + "background_subtracted_stable": true, + "background_subtracted_signal_power": 1.7102246351553774, + "channel_gain_complex": { + "real": 0.01755314566236957, + "imag": -0.02199755973474973, + "phase_rad_at_active_start": -0.8973012782222721 + }, + "channel_gain_power": 0.0007920055569282462, + "measurement": { + "frame": 1, + "start": 9803485, + "alignment_adjustment": -3, + "rx_active": { + "samples": 1638400, + "mean_i": 1.000250244140625, + "mean_q": 2.0000390625, + "power_code2": 7.5943505859375, + "ac_power_code2": 2.5936937835082423, + "rms_code": 2.755784931001964, + "peak_magnitude_code": 7.211102550927978, + "max_abs_i": 5.0, + "max_abs_q": 6.0, + "papr_db": 8.355127015785026, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.000379717529296875 + }, + "cfo_hz": 0.19617739757807567, + "gain_abs": 0.02814259328719097, + "dc_i": 1.0004532662364058, + "dc_q": 2.0003859700871685, + "train_signal_power": 1.5494089856585476, + "train_residual_power": 0.9895407718712785, + "train_actual_ac_power": 2.5389497575298265, + "train_cross_power": 4.0495384823202584e-16, + "heldout_signal_power": 1.548999768607681, + "heldout_residual_power": 0.8941535479569137, + "heldout_actual_ac_power": 2.6484381326123456, + "heldout_cross_power": 0.20528481604775145, + "heldout_residual_band_27000_power": 0.009765782070021769, + "heldout_residual_band_675000_power": 0.08171891866368976, + "heldout_residual_band_1000000_power": 0.1177276686242853, + "heldout_residual_matched_power": 3.3724974710374163, + "effective_signal_to_residual_db": 2.3863924873600086 + } + }, + { + "label": "B2", + "symbol_rate": 500000, + "raw_rx": { + "samples": 1638400, + "mean_i": 0.99942626953125, + "mean_q": 1.999466552734375, + "power_code2": 7.5732568359375, + "ac_power_code2": 2.5765374722048655, + "rms_code": 2.751955093372256, + "peak_magnitude_code": 6.708203932499369, + "max_abs_i": 5.0, + "max_abs_q": 6.0, + "papr_db": 7.739298282383329, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 1.5146513671875001e-05, + "band_27000_power": 0.08437478193987907, + "band_675000_power": 1.7815424066137258, + "power_standard_error": 0.03932113008198285 + }, + "local_zero_iq_background_windows": [ + { + "samples": 480000, + "mean_i": 1.0004854166666666, + "mean_q": 2.0000104166666666, + "power_code2": 5.883220833333334, + "ac_power_code2": 0.8822080975954867, + "rms_code": 2.425535164315977, + "peak_magnitude_code": 5.830951894845301, + "max_abs_i": 4.0, + "max_abs_q": 5.0, + "papr_db": 7.6186376661535915, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 1.1766441666666666e-05, + "band_27000_power": 0.003053650977045963, + "band_675000_power": 0.073961606327725, + "power_standard_error": 0.0012067695631605175 + }, + { + "samples": 480000, + "mean_i": 0.9988083333333333, + "mean_q": 1.9994125, + "power_code2": 5.878066666666666, + "ac_power_code2": 0.8827982347743042, + "rms_code": 2.4244724512080285, + "peak_magnitude_code": 5.0990195135927845, + "max_abs_i": 4.0, + "max_abs_q": 5.0, + "papr_db": 6.457388406130481, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 1.1756133333333333e-05, + "band_27000_power": 0.0029587129864172557, + "band_675000_power": 0.07444268200354834, + "power_standard_error": 0.0016031313210695718 + } + ], + "background_power": 0.8825031661848954, + "background_subtracted_raw": 1.69403430601997, + "background_subtracted_uncertainty": 0.039335034220138454, + "background_subtracted_stable": true, + "background_subtracted_signal_power": 1.69403430601997, + "channel_gain_complex": { + "real": 0.02447249803166995, + "imag": -0.013877133774443209, + "phase_rad_at_active_start": -0.5158392359795115 + }, + "channel_gain_power": 0.0007914780017038821, + "measurement": { + "frame": 1, + "start": 12258685, + "alignment_adjustment": -3, + "rx_active": { + "samples": 1638400, + "mean_i": 0.99942626953125, + "mean_q": 1.999466552734375, + "power_code2": 7.5732568359375, + "ac_power_code2": 2.5765374722048655, + "rms_code": 2.751955093372256, + "peak_magnitude_code": 6.708203932499369, + "max_abs_i": 5.0, + "max_abs_q": 6.0, + "papr_db": 7.739298282383329, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 1.5146513671875001e-05 + }, + "cfo_hz": 0.197924755610056, + "gain_abs": 0.028133218829417335, + "dc_i": 1.0007017709706703, + "dc_q": 1.999435627739676, + "train_signal_power": 1.5330674686733408, + "train_residual_power": 0.9881175175558363, + "train_actual_ac_power": 2.5211849862291755, + "train_cross_power": -1.4430123762565473e-15, + "heldout_signal_power": 1.5330492855483506, + "heldout_residual_power": 0.8931876810530448, + "heldout_actual_ac_power": 2.6318932139011078, + "heldout_cross_power": 0.20565624729971124, + "heldout_residual_band_27000_power": 0.0033209337668698995, + "heldout_residual_band_675000_power": 0.08160471700839024, + "heldout_residual_band_1000000_power": 0.11754379856225462, + "heldout_residual_matched_power": 0.9863150049618183, + "effective_signal_to_residual_db": 2.346133924823488 + } + } + ], + "pairs": [ + { + "pair": 1, + "received_power_difference_db": -0.051497256938167285, + "channel_gain_difference_db": 0.0017861283874782213, + "matched_component_difference_db": -0.04027242840779062, + "matched_component_symbol_energy_difference_db": -14.019672515128168 + }, + { + "pair": 2, + "received_power_difference_db": -0.04130956980203142, + "channel_gain_difference_db": -0.0028938012026523408, + "matched_component_difference_db": -0.044952357997923256, + "matched_component_symbol_energy_difference_db": -14.0243524447183 + } + ], + "comparison_summary": { + "repeatability": [ + { + "comparison": "A2/A1", + "raw_rx_power_difference_db": -0.0012085532551280598, + "background_difference_db": -0.0017255178918689754, + "received_power_difference_db": -0.005652782408264756, + "channel_gain_difference_db": -0.0023081300526506692, + "matched_component_difference_db": -0.00230813005264874 + }, + { + "comparison": "B2/B1", + "raw_rx_power_difference_db": 0.00036367664985406537, + "background_difference_db": -0.011492673362299905, + "received_power_difference_db": 0.004534904727870617, + "channel_gain_difference_db": -0.006988059642781434, + "matched_component_difference_db": -0.006988059642780951 + } + ], + "averaging": "RATIO_OF_MEAN_LINEAR_POWERS_B1_B2_OVER_A1_A2", + "received_power_difference_db": -0.04640374115997541, + "channel_gain_difference_db": -0.000552584218408658, + "energy_per_symbol_difference_db": -14.025803827880353, + "energy_basis": "BACKGROUND_SUBTRACTED_RECEIVED_POWER_DIVIDED_BY_SYMBOL_RATE" + }, + "outside_waveform_background": [ + { + "start": 3309216, + "samples": 400000, + "mean_i": 0.9997525, + "mean_q": 2.0, + "power_code2": 5.8853875, + "ac_power_code2": 0.8858824387437494, + "rms_code": 2.4259817600303593, + "peak_magnitude_code": 5.385164807134504, + "max_abs_i": 4.0, + "max_abs_q": 5.0, + "papr_db": 6.926229353802586, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.000294269375, + "band_27000_power": 0.00300239645688663, + "band_675000_power": 0.07464470494185707, + "power_standard_error": 0.001189001018090443 + }, + { + "start": 15097760, + "samples": 400000, + "mean_i": 1.00201, + "mean_q": 1.9998775, + "power_code2": 5.8860125, + "ac_power_code2": 0.8824784448937502, + "rms_code": 2.4261105704398553, + "peak_magnitude_code": 5.0990195135927845, + "max_abs_i": 4.0, + "max_abs_q": 5.0, + "papr_db": 6.451521679042596, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.000294300625, + "band_27000_power": 0.002877436011683443, + "band_675000_power": 0.07431026562360402, + "power_standard_error": 0.0010078630955450837 + } + ], + "background_note": "Внутренние gaps: нулевой цифровой IQ при работающем TX; это не RF OFF. Внешние окна также требуют сверки времён запуска/остановки по журналам.", + "power_units": "RX code squared, not calibrated watts/dBm", + "residual_note": "Шум и неописанные моделью искажения; RX не нормируется для выравнивания A/B", + "synthetic": true, + "source_result_preserved": "software_equal.json" +} diff --git a/data/processed/lab056/software_minus6db.json b/data/processed/lab056/software_minus6db.json new file mode 100644 index 0000000..7d59bd0 --- /dev/null +++ b/data/processed/lab056/software_minus6db.json @@ -0,0 +1,489 @@ +{ + "scope": "TX_REFERENCE_DIAGNOSTICS_NOT_INDEPENDENT_PACKET_RX", + "rf_tx": 0, + "tx_sha256": "5b34bbe1a8e6d037d2d6d367a74e9eb84e8940db7da61600ffaa7f7497d4f600", + "rx_sha256": "a189c1ffaf029983d41f7d87e262cbbc28ba740fc8915a75e41ee0adec9c1fe6", + "alignment": { + "start": 4109216, + "correlation": 0.593709534474723, + "coarse_cfo_hz": 0.25, + "stride": 32 + }, + "segments": [ + { + "label": "A1", + "symbol_rate": 20000, + "raw_rx": { + "samples": 1638400, + "mean_i": 0.9995880126953125, + "mean_q": 2.00025390625, + "power_code2": 7.596464233398438, + "ac_power_code2": 2.596272348805888, + "rms_code": 2.7561683971409363, + "peak_magnitude_code": 6.708203932499369, + "max_abs_i": 5.0, + "max_abs_q": 6.0, + "papr_db": 7.726010163935343, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.0003798232116699219, + "band_27000_power": 1.729349736724617, + "band_675000_power": 1.8014764601725908, + "power_standard_error": 0.039755811858960266 + }, + "local_zero_iq_background_windows": [ + { + "samples": 480000, + "mean_i": 1.0012270833333334, + "mean_q": 2.0008, + "power_code2": 5.88834375, + "ac_power_code2": 0.8826874375998264, + "rms_code": 2.4265909729494997, + "peak_magnitude_code": 5.385164807134504, + "max_abs_i": 4.0, + "max_abs_q": 5.0, + "papr_db": 6.924048425663756, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.0002944171875, + "band_27000_power": 0.002998337896541192, + "band_675000_power": 0.0744039583655369, + "power_standard_error": 0.0012691882704167598 + }, + { + "samples": 480000, + "mean_i": 0.9989395833333333, + "mean_q": 2.000110416666667, + "power_code2": 5.883275, + "ac_power_code2": 0.8849530299913191, + "rms_code": 2.42554633021099, + "peak_magnitude_code": 5.385164807134504, + "max_abs_i": 4.0, + "max_abs_q": 5.0, + "papr_db": 6.9277884895206405, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.00029416375, + "band_27000_power": 0.003012451963227414, + "band_675000_power": 0.07417080321149919, + "power_standard_error": 0.0013506571909630695 + } + ], + "background_power": 0.8838202337955727, + "background_subtracted_raw": 1.7124521150103154, + "background_subtracted_uncertainty": 0.039782742266144235, + "background_subtracted_stable": true, + "background_subtracted_signal_power": 1.7124521150103154, + "channel_gain_power": 0.0007924265932905088, + "measurement": { + "frame": 1, + "start": 4909213, + "alignment_adjustment": -3, + "rx_active": { + "samples": 1638400, + "mean_i": 0.9995880126953125, + "mean_q": 2.00025390625, + "power_code2": 7.596464233398438, + "ac_power_code2": 2.596272348805888, + "rms_code": 2.7561683971409363, + "peak_magnitude_code": 6.708203932499369, + "max_abs_i": 5.0, + "max_abs_q": 6.0, + "papr_db": 7.726010163935343, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.0003798232116699219 + }, + "cfo_hz": 0.1968177799965115, + "gain_abs": 0.028150072704888503, + "dc_i": 0.9997948620728929, + "dc_q": 2.0010324962850192, + "train_signal_power": 1.55023266362049, + "train_residual_power": 0.9894122701085495, + "train_actual_ac_power": 2.539644933729041, + "train_cross_power": 2.1058155219577658e-15, + "heldout_signal_power": 1.549823229026632, + "heldout_residual_power": 0.8945490859865625, + "heldout_actual_ac_power": 2.6529010618609536, + "heldout_cross_power": 0.20852874684775813, + "heldout_residual_band_27000_power": 0.010080618023636462, + "heldout_residual_band_675000_power": 0.08179753911385561, + "heldout_residual_band_1000000_power": 0.11760791625473926, + "heldout_residual_matched_power": 3.4888049404460095, + "effective_signal_to_residual_db": 2.3867798958438566 + } + }, + { + "label": "B1", + "symbol_rate": 500000, + "raw_rx": { + "samples": 1638400, + "mean_i": 0.998985595703125, + "mean_q": 1.99885986328125, + "power_code2": 6.303458251953125, + "ac_power_code2": 1.3100452784940613, + "rms_code": 2.5106688853676276, + "peak_magnitude_code": 6.4031242374328485, + "max_abs_i": 4.0, + "max_abs_q": 6.0, + "papr_db": 8.132049758894885, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 1.260691650390625e-05, + "band_27000_power": 0.023313641821752495, + "band_675000_power": 0.5022288931564798, + "power_standard_error": 0.009890376052273048 + }, + "local_zero_iq_background_windows": [ + { + "samples": 480000, + "mean_i": 0.9989395833333333, + "mean_q": 2.000110416666667, + "power_code2": 5.883275, + "ac_power_code2": 0.8849530299913191, + "rms_code": 2.42554633021099, + "peak_magnitude_code": 5.385164807134504, + "max_abs_i": 4.0, + "max_abs_q": 5.0, + "papr_db": 6.9277884895206405, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 1.176655e-05, + "band_27000_power": 0.003012451963227414, + "band_675000_power": 0.07417080321149919, + "power_standard_error": 0.0013506571909630695 + }, + { + "samples": 480000, + "mean_i": 0.9989791666666666, + "mean_q": 2.00009375, + "power_code2": 5.883064583333334, + "ac_power_code2": 0.8847301991102432, + "rms_code": 2.4255029547154408, + "peak_magnitude_code": 5.0990195135927845, + "max_abs_i": 4.0, + "max_abs_q": 5.0, + "papr_db": 6.45369731942485, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 1.1766129166666667e-05, + "band_27000_power": 0.0030190450807723868, + "band_675000_power": 0.07463668207172841, + "power_standard_error": 0.0009021263608471298 + } + ], + "background_power": 0.8848416145507811, + "background_subtracted_raw": 0.42520366394328013, + "background_subtracted_uncertainty": 0.009924287307427941, + "background_subtracted_stable": true, + "background_subtracted_signal_power": 0.42520366394328013, + "channel_gain_power": 0.0001984013788928856, + "measurement": { + "frame": 1, + "start": 7364413, + "alignment_adjustment": -3, + "rx_active": { + "samples": 1638400, + "mean_i": 0.998985595703125, + "mean_q": 1.99885986328125, + "power_code2": 6.303458251953125, + "ac_power_code2": 1.3100452784940613, + "rms_code": 2.5106688853676276, + "peak_magnitude_code": 6.4031242374328485, + "max_abs_i": 4.0, + "max_abs_q": 6.0, + "papr_db": 8.132049758894885, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 1.260691650390625e-05 + }, + "cfo_hz": 0.2030817936052521, + "gain_abs": 0.014085502436650445, + "dc_i": 0.9994693255003283, + "dc_q": 1.9991360434375782, + "train_signal_power": 0.38429709867591966, + "train_residual_power": 0.9122921896535339, + "train_actual_ac_power": 1.2965892883294532, + "train_cross_power": 1.1296519275560969e-16, + "heldout_signal_power": 0.38429254067549146, + "heldout_residual_power": 0.8884947950547913, + "heldout_actual_ac_power": 1.3235018891986572, + "heldout_cross_power": 0.050714553468375057, + "heldout_residual_band_27000_power": 0.00304194937150677, + "heldout_residual_band_675000_power": 0.07681375917338087, + "heldout_residual_band_1000000_power": 0.11305538222420118, + "heldout_residual_matched_power": 0.9157792365512673, + "effective_signal_to_residual_db": -3.639929333248138 + } + }, + { + "label": "A2", + "symbol_rate": 20000, + "raw_rx": { + "samples": 1638400, + "mean_i": 1.000250244140625, + "mean_q": 2.0000390625, + "power_code2": 7.5943505859375, + "ac_power_code2": 2.5936937835082423, + "rms_code": 2.755784931001964, + "peak_magnitude_code": 7.211102550927978, + "max_abs_i": 5.0, + "max_abs_q": 6.0, + "papr_db": 8.355127015785026, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.000379717529296875, + "band_27000_power": 1.7263462989934586, + "band_675000_power": 1.7983862355807485, + "power_standard_error": 0.03961683202831068 + }, + "local_zero_iq_background_windows": [ + { + "samples": 480000, + "mean_i": 0.9989791666666666, + "mean_q": 2.00009375, + "power_code2": 5.883064583333334, + "ac_power_code2": 0.8847301991102432, + "rms_code": 2.4255029547154408, + "peak_magnitude_code": 5.0990195135927845, + "max_abs_i": 4.0, + "max_abs_q": 5.0, + "papr_db": 6.45369731942485, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.00029415322916666667, + "band_27000_power": 0.0030190450807723868, + "band_675000_power": 0.07463668207172841, + "power_standard_error": 0.0009021263608471298 + }, + { + "samples": 480000, + "mean_i": 1.0004854166666666, + "mean_q": 2.0000104166666666, + "power_code2": 5.883220833333334, + "ac_power_code2": 0.8822080975954867, + "rms_code": 2.425535164315977, + "peak_magnitude_code": 5.830951894845301, + "max_abs_i": 4.0, + "max_abs_q": 5.0, + "papr_db": 7.6186376661535915, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.0002941610416666667, + "band_27000_power": 0.003053650977045963, + "band_675000_power": 0.073961606327725, + "power_standard_error": 0.0012067695631605175 + } + ], + "background_power": 0.8834691483528649, + "background_subtracted_raw": 1.7102246351553774, + "background_subtracted_uncertainty": 0.03964405579856132, + "background_subtracted_stable": true, + "background_subtracted_signal_power": 1.7102246351553774, + "channel_gain_power": 0.0007920055569282462, + "measurement": { + "frame": 1, + "start": 9803485, + "alignment_adjustment": -3, + "rx_active": { + "samples": 1638400, + "mean_i": 1.000250244140625, + "mean_q": 2.0000390625, + "power_code2": 7.5943505859375, + "ac_power_code2": 2.5936937835082423, + "rms_code": 2.755784931001964, + "peak_magnitude_code": 7.211102550927978, + "max_abs_i": 5.0, + "max_abs_q": 6.0, + "papr_db": 8.355127015785026, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.000379717529296875 + }, + "cfo_hz": 0.19617739757807567, + "gain_abs": 0.02814259328719097, + "dc_i": 1.0004532662364058, + "dc_q": 2.0003859700871685, + "train_signal_power": 1.5494089856585476, + "train_residual_power": 0.9895407718712785, + "train_actual_ac_power": 2.5389497575298265, + "train_cross_power": 4.0495384823202584e-16, + "heldout_signal_power": 1.548999768607681, + "heldout_residual_power": 0.8941535479569137, + "heldout_actual_ac_power": 2.6484381326123456, + "heldout_cross_power": 0.20528481604775145, + "heldout_residual_band_27000_power": 0.009765782070021769, + "heldout_residual_band_675000_power": 0.08171891866368976, + "heldout_residual_band_1000000_power": 0.1177276686242853, + "heldout_residual_matched_power": 3.3724974710374163, + "effective_signal_to_residual_db": 2.3863924873600086 + } + }, + { + "label": "B2", + "symbol_rate": 500000, + "raw_rx": { + "samples": 1638400, + "mean_i": 0.9994140625, + "mean_q": 1.999407958984375, + "power_code2": 6.305787353515625, + "ac_power_code2": 1.3093266987428072, + "rms_code": 2.511132683375298, + "peak_magnitude_code": 5.830951894845301, + "max_abs_i": 4.0, + "max_abs_q": 5.0, + "papr_db": 7.317395958495091, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 1.261157470703125e-05, + "band_27000_power": 0.023440988618055048, + "band_675000_power": 0.5015327722551061, + "power_standard_error": 0.009843366386499481 + }, + "local_zero_iq_background_windows": [ + { + "samples": 480000, + "mean_i": 1.0004854166666666, + "mean_q": 2.0000104166666666, + "power_code2": 5.883220833333334, + "ac_power_code2": 0.8822080975954867, + "rms_code": 2.425535164315977, + "peak_magnitude_code": 5.830951894845301, + "max_abs_i": 4.0, + "max_abs_q": 5.0, + "papr_db": 7.6186376661535915, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 1.1766441666666666e-05, + "band_27000_power": 0.003053650977045963, + "band_675000_power": 0.073961606327725, + "power_standard_error": 0.0012067695631605175 + }, + { + "samples": 480000, + "mean_i": 0.9988083333333333, + "mean_q": 1.9994125, + "power_code2": 5.878066666666666, + "ac_power_code2": 0.8827982347743042, + "rms_code": 2.4244724512080285, + "peak_magnitude_code": 5.0990195135927845, + "max_abs_i": 4.0, + "max_abs_q": 5.0, + "papr_db": 6.457388406130481, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 1.1756133333333333e-05, + "band_27000_power": 0.0029587129864172557, + "band_675000_power": 0.07444268200354834, + "power_standard_error": 0.0016031313210695718 + } + ], + "background_power": 0.8825031661848954, + "background_subtracted_raw": 0.4268235325579117, + "background_subtracted_uncertainty": 0.009898762952718625, + "background_subtracted_stable": true, + "background_subtracted_signal_power": 0.4268235325579117, + "channel_gain_power": 0.00019792959281029668, + "measurement": { + "frame": 1, + "start": 12258685, + "alignment_adjustment": -3, + "rx_active": { + "samples": 1638400, + "mean_i": 0.9994140625, + "mean_q": 1.999407958984375, + "power_code2": 6.305787353515625, + "ac_power_code2": 1.3093266987428072, + "rms_code": 2.511132683375298, + "peak_magnitude_code": 5.830951894845301, + "max_abs_i": 4.0, + "max_abs_q": 5.0, + "papr_db": 7.317395958495091, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 1.261157470703125e-05 + }, + "cfo_hz": 0.20036016971332649, + "gain_abs": 0.014068745246477977, + "dc_i": 1.0007268354608458, + "dc_q": 1.9995225464921158, + "train_signal_power": 0.3833832641867325, + "train_residual_power": 0.9125567848168452, + "train_actual_ac_power": 1.2959400490035784, + "train_cross_power": 3.459732500488144e-16, + "heldout_signal_power": 0.3833787170249446, + "heldout_residual_power": 0.8881519217870418, + "heldout_actual_ac_power": 1.3227168214883227, + "heldout_cross_power": 0.051186182676336164, + "heldout_residual_band_27000_power": 0.003087916076869909, + "heldout_residual_band_675000_power": 0.0766452188649128, + "heldout_residual_band_1000000_power": 0.11237262287689712, + "heldout_residual_matched_power": 0.9135235192972193, + "effective_signal_to_residual_db": -3.648592601804231 + } + } + ], + "pairs": [ + { + "pair": 1, + "received_power_difference_db": -6.05021438090514, + "channel_gain_difference_db": -6.014143555521402, + "matched_component_difference_db": -6.056202112316672, + "matched_component_symbol_energy_difference_db": -20.035602199037047 + }, + { + "pair": 2, + "received_power_difference_db": -6.028048021201634, + "channel_gain_difference_db": -6.022174975174189, + "matched_component_difference_db": -6.06423353196946, + "matched_component_symbol_energy_difference_db": -20.04363361868984 + } + ], + "outside_waveform_background": [ + { + "start": 3309216, + "samples": 400000, + "mean_i": 0.9997525, + "mean_q": 2.0, + "power_code2": 5.8853875, + "ac_power_code2": 0.8858824387437494, + "rms_code": 2.4259817600303593, + "peak_magnitude_code": 5.385164807134504, + "max_abs_i": 4.0, + "max_abs_q": 5.0, + "papr_db": 6.926229353802586, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.000294269375, + "band_27000_power": 0.00300239645688663, + "band_675000_power": 0.07464470494185707, + "power_standard_error": 0.001189001018090443 + }, + { + "start": 15097760, + "samples": 400000, + "mean_i": 1.00201, + "mean_q": 1.9998775, + "power_code2": 5.8860125, + "ac_power_code2": 0.8824784448937502, + "rms_code": 2.4261105704398553, + "peak_magnitude_code": 5.0990195135927845, + "max_abs_i": 4.0, + "max_abs_q": 5.0, + "papr_db": 6.451521679042596, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.000294300625, + "band_27000_power": 0.002877436011683443, + "band_675000_power": 0.07431026562360402, + "power_standard_error": 0.0010078630955450837 + } + ], + "background_note": "Внутренние gaps: нулевой цифровой IQ при работающем TX; это не RF OFF. Внешние окна также требуют сверки времён запуска/остановки по журналам.", + "power_units": "RX code squared, not calibrated watts/dBm", + "residual_note": "Шум и неописанные моделью искажения; RX не нормируется для выравнивания A/B", + "synthetic": true, + "expected_gain_difference_db": -6.020599913279624, + "known_sequence_start": 4160013, + "passed": true +} diff --git a/data/processed/lab056/software_minus6db_ota_summary.json b/data/processed/lab056/software_minus6db_ota_summary.json new file mode 100644 index 0000000..bbe8feb --- /dev/null +++ b/data/processed/lab056/software_minus6db_ota_summary.json @@ -0,0 +1,532 @@ +{ + "scope": "TX_REFERENCE_DIAGNOSTICS_NOT_INDEPENDENT_PACKET_RX", + "rf_tx": 0, + "tx_sha256": "5b34bbe1a8e6d037d2d6d367a74e9eb84e8940db7da61600ffaa7f7497d4f600", + "rx_sha256": "a189c1ffaf029983d41f7d87e262cbbc28ba740fc8915a75e41ee0adec9c1fe6", + "alignment": { + "start": 4109216, + "correlation": 0.593709534474723, + "coarse_cfo_hz": 0.25, + "stride": 32 + }, + "segments": [ + { + "label": "A1", + "symbol_rate": 20000, + "raw_rx": { + "samples": 1638400, + "mean_i": 0.9995880126953125, + "mean_q": 2.00025390625, + "power_code2": 7.596464233398438, + "ac_power_code2": 2.596272348805888, + "rms_code": 2.7561683971409363, + "peak_magnitude_code": 6.708203932499369, + "max_abs_i": 5.0, + "max_abs_q": 6.0, + "papr_db": 7.726010163935343, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.0003798232116699219, + "band_27000_power": 1.729349736724617, + "band_675000_power": 1.8014764601725908, + "power_standard_error": 0.039755811858960266 + }, + "local_zero_iq_background_windows": [ + { + "samples": 480000, + "mean_i": 1.0012270833333334, + "mean_q": 2.0008, + "power_code2": 5.88834375, + "ac_power_code2": 0.8826874375998264, + "rms_code": 2.4265909729494997, + "peak_magnitude_code": 5.385164807134504, + "max_abs_i": 4.0, + "max_abs_q": 5.0, + "papr_db": 6.924048425663756, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.0002944171875, + "band_27000_power": 0.002998337896541192, + "band_675000_power": 0.0744039583655369, + "power_standard_error": 0.0012691882704167598 + }, + { + "samples": 480000, + "mean_i": 0.9989395833333333, + "mean_q": 2.000110416666667, + "power_code2": 5.883275, + "ac_power_code2": 0.8849530299913191, + "rms_code": 2.42554633021099, + "peak_magnitude_code": 5.385164807134504, + "max_abs_i": 4.0, + "max_abs_q": 5.0, + "papr_db": 6.9277884895206405, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.00029416375, + "band_27000_power": 0.003012451963227414, + "band_675000_power": 0.07417080321149919, + "power_standard_error": 0.0013506571909630695 + } + ], + "background_power": 0.8838202337955727, + "background_subtracted_raw": 1.7124521150103154, + "background_subtracted_uncertainty": 0.039782742266144235, + "background_subtracted_stable": true, + "background_subtracted_signal_power": 1.7124521150103154, + "channel_gain_complex": { + "real": -0.0027287468398317707, + "imag": -0.028017504063970737, + "phase_rad_at_active_start": -1.6678844763817968 + }, + "channel_gain_power": 0.0007924265932905088, + "measurement": { + "frame": 1, + "start": 4909213, + "alignment_adjustment": -3, + "rx_active": { + "samples": 1638400, + "mean_i": 0.9995880126953125, + "mean_q": 2.00025390625, + "power_code2": 7.596464233398438, + "ac_power_code2": 2.596272348805888, + "rms_code": 2.7561683971409363, + "peak_magnitude_code": 6.708203932499369, + "max_abs_i": 5.0, + "max_abs_q": 6.0, + "papr_db": 7.726010163935343, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.0003798232116699219 + }, + "cfo_hz": 0.1968177799965115, + "gain_abs": 0.028150072704888503, + "dc_i": 0.9997948620728929, + "dc_q": 2.0010324962850192, + "train_signal_power": 1.55023266362049, + "train_residual_power": 0.9894122701085495, + "train_actual_ac_power": 2.539644933729041, + "train_cross_power": 2.1058155219577658e-15, + "heldout_signal_power": 1.549823229026632, + "heldout_residual_power": 0.8945490859865625, + "heldout_actual_ac_power": 2.6529010618609536, + "heldout_cross_power": 0.20852874684775813, + "heldout_residual_band_27000_power": 0.010080618023636462, + "heldout_residual_band_675000_power": 0.08179753911385561, + "heldout_residual_band_1000000_power": 0.11760791625473926, + "heldout_residual_matched_power": 3.4888049404460095, + "effective_signal_to_residual_db": 2.3867798958438566 + } + }, + { + "label": "B1", + "symbol_rate": 500000, + "raw_rx": { + "samples": 1638400, + "mean_i": 0.998985595703125, + "mean_q": 1.99885986328125, + "power_code2": 6.303458251953125, + "ac_power_code2": 1.3100452784940613, + "rms_code": 2.5106688853676276, + "peak_magnitude_code": 6.4031242374328485, + "max_abs_i": 4.0, + "max_abs_q": 6.0, + "papr_db": 8.132049758894885, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 1.260691650390625e-05, + "band_27000_power": 0.023313641821752495, + "band_675000_power": 0.5022288931564798, + "power_standard_error": 0.009890376052273048 + }, + "local_zero_iq_background_windows": [ + { + "samples": 480000, + "mean_i": 0.9989395833333333, + "mean_q": 2.000110416666667, + "power_code2": 5.883275, + "ac_power_code2": 0.8849530299913191, + "rms_code": 2.42554633021099, + "peak_magnitude_code": 5.385164807134504, + "max_abs_i": 4.0, + "max_abs_q": 5.0, + "papr_db": 6.9277884895206405, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 1.176655e-05, + "band_27000_power": 0.003012451963227414, + "band_675000_power": 0.07417080321149919, + "power_standard_error": 0.0013506571909630695 + }, + { + "samples": 480000, + "mean_i": 0.9989791666666666, + "mean_q": 2.00009375, + "power_code2": 5.883064583333334, + "ac_power_code2": 0.8847301991102432, + "rms_code": 2.4255029547154408, + "peak_magnitude_code": 5.0990195135927845, + "max_abs_i": 4.0, + "max_abs_q": 5.0, + "papr_db": 6.45369731942485, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 1.1766129166666667e-05, + "band_27000_power": 0.0030190450807723868, + "band_675000_power": 0.07463668207172841, + "power_standard_error": 0.0009021263608471298 + } + ], + "background_power": 0.8848416145507811, + "background_subtracted_raw": 0.42520366394328013, + "background_subtracted_uncertainty": 0.009924287307427941, + "background_subtracted_stable": true, + "background_subtracted_signal_power": 0.42520366394328013, + "channel_gain_complex": { + "real": 0.003963777071987038, + "imag": -0.013516280931398084, + "phase_rad_at_active_start": -1.2855349404464855 + }, + "channel_gain_power": 0.0001984013788928856, + "measurement": { + "frame": 1, + "start": 7364413, + "alignment_adjustment": -3, + "rx_active": { + "samples": 1638400, + "mean_i": 0.998985595703125, + "mean_q": 1.99885986328125, + "power_code2": 6.303458251953125, + "ac_power_code2": 1.3100452784940613, + "rms_code": 2.5106688853676276, + "peak_magnitude_code": 6.4031242374328485, + "max_abs_i": 4.0, + "max_abs_q": 6.0, + "papr_db": 8.132049758894885, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 1.260691650390625e-05 + }, + "cfo_hz": 0.2030817936052521, + "gain_abs": 0.014085502436650445, + "dc_i": 0.9994693255003283, + "dc_q": 1.9991360434375782, + "train_signal_power": 0.38429709867591966, + "train_residual_power": 0.9122921896535339, + "train_actual_ac_power": 1.2965892883294532, + "train_cross_power": 1.1296519275560969e-16, + "heldout_signal_power": 0.38429254067549146, + "heldout_residual_power": 0.8884947950547913, + "heldout_actual_ac_power": 1.3235018891986572, + "heldout_cross_power": 0.050714553468375057, + "heldout_residual_band_27000_power": 0.00304194937150677, + "heldout_residual_band_675000_power": 0.07681375917338087, + "heldout_residual_band_1000000_power": 0.11305538222420118, + "heldout_residual_matched_power": 0.9157792365512673, + "effective_signal_to_residual_db": -3.639929333248138 + } + }, + { + "label": "A2", + "symbol_rate": 20000, + "raw_rx": { + "samples": 1638400, + "mean_i": 1.000250244140625, + "mean_q": 2.0000390625, + "power_code2": 7.5943505859375, + "ac_power_code2": 2.5936937835082423, + "rms_code": 2.755784931001964, + "peak_magnitude_code": 7.211102550927978, + "max_abs_i": 5.0, + "max_abs_q": 6.0, + "papr_db": 8.355127015785026, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.000379717529296875, + "band_27000_power": 1.7263462989934586, + "band_675000_power": 1.7983862355807485, + "power_standard_error": 0.03961683202831068 + }, + "local_zero_iq_background_windows": [ + { + "samples": 480000, + "mean_i": 0.9989791666666666, + "mean_q": 2.00009375, + "power_code2": 5.883064583333334, + "ac_power_code2": 0.8847301991102432, + "rms_code": 2.4255029547154408, + "peak_magnitude_code": 5.0990195135927845, + "max_abs_i": 4.0, + "max_abs_q": 5.0, + "papr_db": 6.45369731942485, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.00029415322916666667, + "band_27000_power": 0.0030190450807723868, + "band_675000_power": 0.07463668207172841, + "power_standard_error": 0.0009021263608471298 + }, + { + "samples": 480000, + "mean_i": 1.0004854166666666, + "mean_q": 2.0000104166666666, + "power_code2": 5.883220833333334, + "ac_power_code2": 0.8822080975954867, + "rms_code": 2.425535164315977, + "peak_magnitude_code": 5.830951894845301, + "max_abs_i": 4.0, + "max_abs_q": 5.0, + "papr_db": 7.6186376661535915, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.0002941610416666667, + "band_27000_power": 0.003053650977045963, + "band_675000_power": 0.073961606327725, + "power_standard_error": 0.0012067695631605175 + } + ], + "background_power": 0.8834691483528649, + "background_subtracted_raw": 1.7102246351553774, + "background_subtracted_uncertainty": 0.03964405579856132, + "background_subtracted_stable": true, + "background_subtracted_signal_power": 1.7102246351553774, + "channel_gain_complex": { + "real": 0.01755314566236957, + "imag": -0.02199755973474973, + "phase_rad_at_active_start": -0.8973012782222721 + }, + "channel_gain_power": 0.0007920055569282462, + "measurement": { + "frame": 1, + "start": 9803485, + "alignment_adjustment": -3, + "rx_active": { + "samples": 1638400, + "mean_i": 1.000250244140625, + "mean_q": 2.0000390625, + "power_code2": 7.5943505859375, + "ac_power_code2": 2.5936937835082423, + "rms_code": 2.755784931001964, + "peak_magnitude_code": 7.211102550927978, + "max_abs_i": 5.0, + "max_abs_q": 6.0, + "papr_db": 8.355127015785026, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.000379717529296875 + }, + "cfo_hz": 0.19617739757807567, + "gain_abs": 0.02814259328719097, + "dc_i": 1.0004532662364058, + "dc_q": 2.0003859700871685, + "train_signal_power": 1.5494089856585476, + "train_residual_power": 0.9895407718712785, + "train_actual_ac_power": 2.5389497575298265, + "train_cross_power": 4.0495384823202584e-16, + "heldout_signal_power": 1.548999768607681, + "heldout_residual_power": 0.8941535479569137, + "heldout_actual_ac_power": 2.6484381326123456, + "heldout_cross_power": 0.20528481604775145, + "heldout_residual_band_27000_power": 0.009765782070021769, + "heldout_residual_band_675000_power": 0.08171891866368976, + "heldout_residual_band_1000000_power": 0.1177276686242853, + "heldout_residual_matched_power": 3.3724974710374163, + "effective_signal_to_residual_db": 2.3863924873600086 + } + }, + { + "label": "B2", + "symbol_rate": 500000, + "raw_rx": { + "samples": 1638400, + "mean_i": 0.9994140625, + "mean_q": 1.999407958984375, + "power_code2": 6.305787353515625, + "ac_power_code2": 1.3093266987428072, + "rms_code": 2.511132683375298, + "peak_magnitude_code": 5.830951894845301, + "max_abs_i": 4.0, + "max_abs_q": 5.0, + "papr_db": 7.317395958495091, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 1.261157470703125e-05, + "band_27000_power": 0.023440988618055048, + "band_675000_power": 0.5015327722551061, + "power_standard_error": 0.009843366386499481 + }, + "local_zero_iq_background_windows": [ + { + "samples": 480000, + "mean_i": 1.0004854166666666, + "mean_q": 2.0000104166666666, + "power_code2": 5.883220833333334, + "ac_power_code2": 0.8822080975954867, + "rms_code": 2.425535164315977, + "peak_magnitude_code": 5.830951894845301, + "max_abs_i": 4.0, + "max_abs_q": 5.0, + "papr_db": 7.6186376661535915, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 1.1766441666666666e-05, + "band_27000_power": 0.003053650977045963, + "band_675000_power": 0.073961606327725, + "power_standard_error": 0.0012067695631605175 + }, + { + "samples": 480000, + "mean_i": 0.9988083333333333, + "mean_q": 1.9994125, + "power_code2": 5.878066666666666, + "ac_power_code2": 0.8827982347743042, + "rms_code": 2.4244724512080285, + "peak_magnitude_code": 5.0990195135927845, + "max_abs_i": 4.0, + "max_abs_q": 5.0, + "papr_db": 6.457388406130481, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 1.1756133333333333e-05, + "band_27000_power": 0.0029587129864172557, + "band_675000_power": 0.07444268200354834, + "power_standard_error": 0.0016031313210695718 + } + ], + "background_power": 0.8825031661848954, + "background_subtracted_raw": 0.4268235325579117, + "background_subtracted_uncertainty": 0.009898762952718625, + "background_subtracted_stable": true, + "background_subtracted_signal_power": 0.4268235325579117, + "channel_gain_complex": { + "real": 0.012227408500259825, + "imag": -0.006958453432916718, + "phase_rad_at_active_start": -0.517378791591959 + }, + "channel_gain_power": 0.00019792959281029668, + "measurement": { + "frame": 1, + "start": 12258685, + "alignment_adjustment": -3, + "rx_active": { + "samples": 1638400, + "mean_i": 0.9994140625, + "mean_q": 1.999407958984375, + "power_code2": 6.305787353515625, + "ac_power_code2": 1.3093266987428072, + "rms_code": 2.511132683375298, + "peak_magnitude_code": 5.830951894845301, + "max_abs_i": 4.0, + "max_abs_q": 5.0, + "papr_db": 7.317395958495091, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 1.261157470703125e-05 + }, + "cfo_hz": 0.20036016971332649, + "gain_abs": 0.014068745246477977, + "dc_i": 1.0007268354608458, + "dc_q": 1.9995225464921158, + "train_signal_power": 0.3833832641867325, + "train_residual_power": 0.9125567848168452, + "train_actual_ac_power": 1.2959400490035784, + "train_cross_power": 3.459732500488144e-16, + "heldout_signal_power": 0.3833787170249446, + "heldout_residual_power": 0.8881519217870418, + "heldout_actual_ac_power": 1.3227168214883227, + "heldout_cross_power": 0.051186182676336164, + "heldout_residual_band_27000_power": 0.003087916076869909, + "heldout_residual_band_675000_power": 0.0766452188649128, + "heldout_residual_band_1000000_power": 0.11237262287689712, + "heldout_residual_matched_power": 0.9135235192972193, + "effective_signal_to_residual_db": -3.648592601804231 + } + } + ], + "pairs": [ + { + "pair": 1, + "received_power_difference_db": -6.05021438090514, + "channel_gain_difference_db": -6.014143555521402, + "matched_component_difference_db": -6.056202112316672, + "matched_component_symbol_energy_difference_db": -20.035602199037047 + }, + { + "pair": 2, + "received_power_difference_db": -6.028048021201634, + "channel_gain_difference_db": -6.022174975174189, + "matched_component_difference_db": -6.06423353196946, + "matched_component_symbol_energy_difference_db": -20.04363361868984 + } + ], + "comparison_summary": { + "repeatability": [ + { + "comparison": "A2/A1", + "raw_rx_power_difference_db": -0.0012085532551280598, + "background_difference_db": -0.0017255178918689754, + "received_power_difference_db": -0.005652782408264756, + "channel_gain_difference_db": -0.0023081300526506692, + "matched_component_difference_db": -0.00230813005264874 + }, + { + "comparison": "B2/B1", + "raw_rx_power_difference_db": 0.0016044036249902335, + "background_difference_db": -0.011492673362299905, + "received_power_difference_db": 0.016513577295241087, + "channel_gain_difference_db": -0.0103395497054386, + "matched_component_difference_db": -0.010339549705438118 + } + ], + "averaging": "RATIO_OF_MEAN_LINEAR_POWERS_B1_B2_OVER_A1_A2", + "received_power_difference_db": -6.039124271880495, + "channel_gain_difference_db": -6.018156341674954, + "energy_per_symbol_difference_db": -20.01852435860087, + "energy_basis": "BACKGROUND_SUBTRACTED_RECEIVED_POWER_DIVIDED_BY_SYMBOL_RATE" + }, + "outside_waveform_background": [ + { + "start": 3309216, + "samples": 400000, + "mean_i": 0.9997525, + "mean_q": 2.0, + "power_code2": 5.8853875, + "ac_power_code2": 0.8858824387437494, + "rms_code": 2.4259817600303593, + "peak_magnitude_code": 5.385164807134504, + "max_abs_i": 4.0, + "max_abs_q": 5.0, + "papr_db": 6.926229353802586, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.000294269375, + "band_27000_power": 0.00300239645688663, + "band_675000_power": 0.07464470494185707, + "power_standard_error": 0.001189001018090443 + }, + { + "start": 15097760, + "samples": 400000, + "mean_i": 1.00201, + "mean_q": 1.9998775, + "power_code2": 5.8860125, + "ac_power_code2": 0.8824784448937502, + "rms_code": 2.4261105704398553, + "peak_magnitude_code": 5.0990195135927845, + "max_abs_i": 4.0, + "max_abs_q": 5.0, + "papr_db": 6.451521679042596, + "clipped_components_fraction": 0.0, + "clipped_samples_fraction": 0.0, + "symbol_energy_code2_seconds": 0.000294300625, + "band_27000_power": 0.002877436011683443, + "band_675000_power": 0.07431026562360402, + "power_standard_error": 0.0010078630955450837 + } + ], + "background_note": "Внутренние gaps: нулевой цифровой IQ при работающем TX; это не RF OFF. Внешние окна также требуют сверки времён запуска/остановки по журналам.", + "power_units": "RX code squared, not calibrated watts/dBm", + "residual_note": "Шум и неописанные моделью искажения; RX не нормируется для выравнивания A/B", + "synthetic": true, + "source_result_preserved": "software_minus6db.json" +} diff --git a/docs/LABS_INDEX.md b/docs/LABS_INDEX.md index 0dfe99a..3bc14d1 100644 --- a/docs/LABS_INDEX.md +++ b/docs/LABS_INDEX.md @@ -406,6 +406,51 @@ - **Далее:** STOP. Отдельное согласование измерительной схемы и допустимой частоты; усилитель не назначен, Git add/commit/push/pull/fetch не выполнялись. +## Lab056 — подготовка одного эфирного ABAB, 20 и 500 ксимв/с + +- **База:** Lab055 зафиксирована коммитом + `5add95ecb8865f59075bbbced08d97b183fb3b48`, только её семь файлов; без push. +- **Подготовлено:** один PRBS7/BPSK/RRC TX-файл ABAB, 1,323568 с; + Fs 8 Мвыб/с, цифровая несущая +50 кГц. RMS A/B 44,004413/44,001925 код, + разница мощности −0,000491 дБ; клиппинг 0. RX планируется на 3 с. +- **Проверено программно:** известные разницы усиления 0 и −6,020600 дБ + восстановлены с ошибкой менее 0,3 дБ в обеих парах. Анализируются отдельно + принятая мощность, фон, коэффициент передачи, остаток, спектр и энергия символа. +- **Проверки:** 34 целевых теста; quick_gate 857/30/13 PASS, + pytest 857 passed, 0 failed; verifier без остаточных находок; diff check PASS. +- **Граница:** пользователь выбрал эфир; частота/полоса и геометрия пока + не согласованы. 1400 МГц для излучения не используется. Общий такт проверен + только mock-вызовами; аппаратный preflight NOT_PERFORMED. RF TX=0. + Эфирный результат будет характеризовать весь путь с антеннами и распространением. +- **Отчёт:** [состояние](lab056_current_state.md), + [подготовка TX](../data/processed/lab056/preparation.json), + [сводка проверок](../data/processed/lab056/acceptance.json), журнал 047. +- **Далее:** STOP и отдельное разрешение на согласованный RF-сеанс. + Lab056 не коммитилась, push не выполнялся; Lab054 NOT_A сохранён. +- **Корректировка, запись 048:** кабельный контроль отложен из-за отсутствия + SMA-переходника; принят контролируемый OTA. RF_FREQ задаёт пользователь, + страна и границы диапазона не запрашиваются. Конкретная частота пока UNKNOWN. + Добавлен отдельный RX-only preflight с STOP перед разрешением TX, проверки + клиппинга до/после передачи, вывод комплексного h и повторов A2/A1, B2/B1. + TX и прежние результаты сохранены. 44 целевых теста; quick_gate 867/30/13 + PASS, pytest 867 passed; verifier без находок. Аппаратных команд нет, RF TX=0. + [Сводка корректировки](../data/processed/lab056/ota_correction.json). +- **Аппаратный preflight, запись 049:** пользователь задал 1400000000 Гц + только для Lab056, OTA LOS примерно 3–5 м. Common clock PASS: оба reg29 + 0x26, SLAVE CLKIN DETECTED. RX-only 800 000 отсчётов/0,1 с, overruns 0, + клиппинг 0, RMS 2,203153 код. CLKOUT выключены, процессы CLEAR, RF TX=0. + [Отчёт preflight](lab056_rx_preflight.md). STOP до отдельного разрешения TX. +- **Единственный ABAB, запись 050:** после отдельного разрешения один TX + 1,323568 с и RX 3 с; 4/4 сегмента, ошибка согласования начал ≤1 отсчёта, + клиппинг/overruns/underruns 0. X=+0,108895 дБ, Y=−0,163042 дБ, + Z=−13,870505 дБ. Повтор мощности A2/A1 −0,482024 дБ, B2/B1 −0,512676 дБ; + повтор коэффициента передачи примерно −0,18 дБ у обоих режимов. + Прежний провал 15,41 дБ в этой OTA-сессии не воспроизвёлся; собственные + потери HackRF не изолированы. Verifier подтвердил сырые данные и расчёты. + [RF-отчёт](lab056_rf_result.md), [сводка](../data/processed/lab056/rf_result.json), + [график](../data/processed/lab056/rf_overview.png). Выполненных TX 1, сейчас + RF TX=0, повторов нет. STOP, Lab056 не коммитилась, push не выполнялся. + ## Правило хранения данных Большие записи IQ, потоки `cs8`, бинарные объекты и другие аппаратные исходные diff --git a/docs/lab056_current_state.md b/docs/lab056_current_state.md new file mode 100644 index 0000000..1616cef --- /dev/null +++ b/docs/lab056_current_state.md @@ -0,0 +1,326 @@ +# Lab056 — подготовка сравнения 20 и 500 ксимв/с в одной сессии + +Подготовка 9 сентября 2026 года; корректировка OTA 10 сентября 2026 года. +Lab056 завершила единственный разрешённый OTA ABAB-сеанс 10 сентября. +Принятая мощность B/A +0,108895 дБ, коэффициент передачи по мощности +−0,163042 дБ, энергия символа −13,870505 дБ. Прежний провал 15,41 дБ +не воспроизвёлся. [Полный RF-отчёт](lab056_rf_result.md). +Выполненных передач 1, повторов 0; **RF TX сейчас = 0, STOP**. +Ниже сохранено описание этапов подготовки и preflight; актуальный итог +приведён в RF-отчёте и в заключительном блоке состояния. + +Простыми словами: для будущего опыта создан файл, который дважды чередует +медленную и быструю передачу с практически одинаковой цифровой амплитудой. +Анализатор на искусственных записях различает равное усиление и падение на 6 дБ. +Как изменятся полезный принятый сигнал и фон между A/B в реальном эфире, +ещё не измерено; пока записан только RX-фон без TX. +Одинаковый TX VGA и одинаковая цифровая мощность не доказывают одинаковую +мощность на выходе HackRF. Для абсолютных ватт/dBm нужен калиброванный прибор. + +## Зафиксированная база и границы задания + +По новому явному разрешению пользователя создан коммит Lab055: + +`5add95ecb8865f59075bbbced08d97b183fb3b48` + +`Lab055: audit received signal energy across symbol rates` + +В коммите ровно семь файлов: анализатор и тесты Lab055, её отчёт, два JSON +результата, только запись 046 журнала и только раздел Lab055 индекса. +Предшествующие незакоммиченные изменения Lab054 сохранены в рабочем каталоге. +Перед коммитом quick_gate: 823 теста, 28 функциональных проверок, 13 штатных +пропусков; pytest: 823 passed, 0 failed. Push не выполнялся. + +Lab055 установила почти равную цифровую мощность активного TX +(1950,308/1950,598 код²), снижение согласованной RX-компоненты на 15,413 дБ +и сопоставимый фон в одинаковой полосе. Разные аппаратные сессии не позволяют +приписать это снижение одной скорости. Lab054 остаётся **NOT_A, 0/16**. +Повторно проверены все 34 SHA-256 входов Lab055: изменений нет. + +Пользователь осознанно изменил методику: необходимого SMA-переходника нет, +поэтому сейчас выполняется подготовка контролируемого эфирного ABAB. +Кабельный контроль TX → 30 дБ → RX отложен до получения переходника +и будет отдельной проверкой, без автоматического запуска после Lab056. + +Схема: MASTER TX → TX-антенна → свободное пространство → RX-антенна → SLAVE RX. +На всю ABAB сохраняются антенны, кабели, одинаковая поляризация, расстояние, +ориентация, положение оборудования, частота, усиления и common clock. +Антенны не трогают; по возможности исключают движение людей возле них. +Внешние PA и LNA, в том числе TQP3M9037, не используются. + +`RF_FREQ = USER_AUTHORIZED_RF_HZ = 1400000000`: значение сообщил пользователь. +Частоту, разрешённую для лаборатории, определяет пользователь. Страна и +границы национальных диапазонов больше не являются обязательными полями +runner; повторное исследование национальной регуляторики не проводилось. +1400 МГц не подставляется по умолчанию. Mock-значения 915/916 МГц также +не являются выбором частоты для аппаратного опыта. + +| Параметр эфирной схемы | Текущее значение | +|---|---| +| Центральная RF-частота | 1400000000 Гц, только лабораторная Lab056 | +| TX-антенна / RX-антенна | Имеющиеся антенны 1400 МГц по описанию пользователя; модели/маркировка не сообщены | +| Расстояние TX–RX | Примерно 3–5 м по заданию; точное измерение не сообщено | +| Взаимная ориентация и поляризация | Одинаковые, антенны неподвижны, по заданию пользователя | +| Прямая видимость LOS / отсутствие прямой видимости NLOS | LOS | + +После конкретного RF_FREQ выполнен аппаратный RX-only preflight, PASS. +Сейчас STOP. TX — только после отдельного разрешения на один ABAB-сеанс. +Само сообщение частоты не является разрешением на TX. Частота 1400 МГц +не назначена рабочей частотой SDR_Rover: ориентир около 180 МГц и варианты +50/144 МГц рассматриваются отдельно, вне Lab056. + +## Подготовленный TX + +Файл: `data/raw/lab056/tx_abab.cs8`, signed int8 IQ, 21 177 088 байт. +SHA-256: `5b34bbe1a8e6d037d2d6d367a74e9eb84e8940db7da61600ffaa7f7497d4f600`. +Параметры сохранены в `data/raw/lab056/tx_metadata.json` и +[preparation.json](../data/processed/lab056/preparation.json). + +Используются прежние PRBS7, BPSK и RRC из тракта Lab043/Lab042: +roll-off 0,35, span 10 символов, защитные участки по 16 символов. +Fs = 8 000 000 комплексных отсчётов/с, цифровая несущая +50 кГц. +Алгоритмы PHY/FEC/CRC, пороги и штатный приёмник не изменялись. + +| Параметр | A: 20 ксимв/с | B: 500 ксимв/с | +|---|---:|---:| +| Отсчётов на символ | 400 | 16 | +| Полезных PRBS7-символов | 4096 | 102400 | +| Длительность активных символов | 0,204800 с | 0,204800 с | +| Длительность сегмента с защитными участками | 0,206900 с | 0,204884 с | +| RMS всего сегмента после int8 | 44,004413 код | 44,001925 код | +| Средняя мощность всего сегмента | 1936,388382 код² | 1936,169379 код² | +| RMS активного участка | 44,227281 код | 44,010867 код | +| Средняя мощность активного участка | 1956,052408 код² | 1936,956395 код² | +| Клиппинг | 0 | 0 | + +Разница средней мощности B/A всего сегмента: **−0,000491 дБ**, +критерий подготовки |разница| ≤ 0,01 дБ выполнен. Активные участки отличаются +на −0,042606 дБ из-за разной доли защитных участков; это явно сохранено +в метаданных и учитывается при сравнении коэффициента передачи. +RMS указан в кодах, средняя мощность — RMS² в код². + +Последовательность: gap → A1 → gap → B1 → gap → A2 → gap → B2 → gap. +Каждый gap длится 0,1 с. A1 и A2 совпадают побайтно; B1 и B2 тоже. +Один файл содержит 10 588 544 комплексных отсчёта, **1,323568 с**. +План RX: 24 000 000 отсчётов, **3 с**, запуск на 0,5 с раньше TX. + +Важно: gap — нулевые цифровые IQ при работающем TX-процессе. +Это не выключение передатчика; утечка несущей и собственный фон возможны. +Полная длительность файла включает эти участки. + +## Измерения сохранённого RX + +Анализатор `experiments/lab056_abab_energy.py` сначала ищет всю ABAB +корреляцией с известным TX: шаг 32 отсчёта, сетка остаточного частотного +смещения (CFO) от −2 до +2 Гц с шагом 0,25 Гц. Затем существующий метод +Lab055 уточняет задержку, CFO, фазу, постоянное смещение и комплексный +коэффициент на обучающих плитках; мощность компоненты и остатка проверяется +на отдельных плитках. При недостаточном совпадении результат UNKNOWN. +Порог 0,05 относится только к этой диагностической локализации; +он не меняет пороги PHY/CRC и не даёт независимого приёма пакетов. + +Прореживание без фильтра используется только при грубом поиске. Мощности +и спектры вычисляются по исходным полным отсчётам. RX не масштабируется +для искусственного выравнивания A/B. + +Для каждого A1/B1/A2/B2 сохраняются: + +- Полная средняя IQ-мощность `mean(|r|²)` и мощность после удаления среднего + (AC), RMS, постоянное смещение, пик и клиппинг. +- Два ближайших окна фона по 60 мс в соседних нулевых цифровых участках. + Дополнительно — доступные окна по 50 мс вне всей последовательности. + Внешние окна нельзя автоматически назвать RF OFF без проверки времён + запуска/остановки: в текущей подготовке аппаратного фона ещё нет. +- Разность AC-мощности и среднего локального фона. Разброс оценивается + по неперекрывающимся блокам 4 мс, включая отличие фона до и после сегмента. + Если превышение не больше трёх оценок неопределённости, итоговая мощность + полезного сигнала `null`, а исходная разность сохраняется. Это критерий + численной устойчивости, не калиброванный доверительный интервал. +- Комплексный коэффициент `h = sum(conj(s)*r) / sum(|s|²)` после согласования + и удаления постоянного смещения; сравнивается `|h|²`. +- Мощность согласованной компоненты и остатка. Остаток включает шум и + искажения, которые не описывает модель; это не чистый тепловой шум. +- Интегральная мощность спектра Welch в полосах 27 и 675 кГц вокруг + +50 кГц. Для A штатная полоса 27 кГц, для B — 675 кГц; + обе полосы записываются также для сравнения фона в одинаковых условиях. + +Отдельно для двух пар сохраняются B/A в дБ по принятой мощности после +вычитания фона, по `|h|²`, по согласованной компоненте и по её энергии +символа `P/Rs`. Единицы мощности — коды RX², энергии — код²·с; +это относительные цифровые оценки, не калиброванные ватты и джоули. +При одинаковой принятой мощности рост скорости в 25 раз даст уменьшение +энергии символа на 13,9794 дБ. Равенство принятой мощности сначала проверяется. + +Корректировка добавляет явный вывод `h.real`, `h.imag` и фазы относительно +начала активного участка. Он вычисляется на тех же обучающих плитках +при уже найденном CFO; его модуль проверяется на совпадение с моделью +Lab055. Алгоритм согласования и разделение обучения/проверки сохранены. + +Отдельно выводятся A2/A1 и B2/B1: полная raw-мощность, фон, полезная +мощность после вычитания фона, `|h|²` и согласованная компонента. +Итоговые X/Y/Z в `comparison_summary` используют отношение средних +линейных мощностей двух B к двум A. X — полезная принятая мощность +после вычитания фона, Y — `|h|²`, Z — энергия символа этой же полезной +мощности. Если вычитание фона неустойчиво хотя бы в одном сегменте, +X/Z остаются `null`; компонентные метрики и повторяемость сохраняются. + +## Проверка искусственными записями + +Созданы две явно синтетические записи CS8 по 3 с: одинаковое усиление +обоих режимов и уменьшение амплитуды B вдвое (мощность −6,020600 дБ). +В модель добавлены шум, постоянное смещение, CFO +0,2 Гц, фаза и неизвестное +для поиска начало 4 160 013 отсчётов. Известное начало записано в результат +после анализа и не передавалось алгоритму поиска. + +| Вход модели | Пара | Разница принятой мощности B/A | Разница `|h|²` B/A | Разница энергии согласованной компоненты | +|---|---:|---:|---:|---:| +| Одинаковое усиление | 1 | −0,051497 дБ | +0,001786 дБ | −14,019673 дБ | +| Одинаковое усиление | 2 | −0,041310 дБ | −0,002894 дБ | −14,024352 дБ | +| B: −6,020600 дБ | 1 | −6,050214 дБ | −6,014144 дБ | −20,035602 дБ | +| B: −6,020600 дБ | 2 | −6,028048 дБ | −6,022175 дБ | −20,043634 дБ | + +Обе проверки PASS: ошибка коэффициента передачи в каждой паре меньше +заданных 0,3 дБ. Разница принятой мощности включает небольшое различие +цифровой мощности активных участков. Это результаты модели, не RF Lab056. +Данные: [равное усиление](../data/processed/lab056/software_equal.json), +[ослабление B](../data/processed/lab056/software_minus6db.json). + +При корректировке те же сохранённые IQ проанализированы без перегенерации. +Прежние файлы результатов сохранены; новые сводки: +[равное усиление, OTA summary](../data/processed/lab056/software_equal_ota_summary.json), +[ослабление B, OTA summary](../data/processed/lab056/software_minus6db_ota_summary.json). + +| Искусственная запись | X: мощность B/A | Y: коэффициент передачи B/A | Z: энергия символа B/A | Повтор `|h|²`, A2/A1 | Повтор `|h|²`, B2/B1 | +|---|---:|---:|---:|---:|---:| +| Равное усиление | −0,046404 дБ | −0,000553 дБ | −14,025804 дБ | −0,002308 дБ | −0,006988 дБ | +| B: −6,020600 дБ | −6,039124 дБ | −6,018156 дБ | −20,018524 дБ | −0,002308 дБ | −0,010340 дБ | + +Это по-прежнему результаты программной модели. Они не устанавливают +величину эфирного различия Lab056. + +Повторный анализ существующего файла с новым именем результата: + +```powershell +python experiments/lab056_abab_energy.py analyze --rx data/raw/lab056/synthetic_equal.cs8 --output data/processed/lab056/recheck_equal.json +``` + +Существующие выходные файлы не перезаписываются. Команды `prepare` и +`software` создают данные только при отсутствии соответствующих файлов. + +## Подготовка однократного аппаратного запуска + +`experiments/lab056_capture_runner.py` не имеет аппаратного CLI. Без конкретного +положительного целого RF_FREQ из задания пользователя аппаратные действия +не начинаются. Частота одинакова для всех RX-команд и единственной TX-команды; +цифровая несущая остаётся +50 кГц. Страна/регуляторная полоса не запрашиваются +программой. Геометрия сохраняется вместе с частотой в отдельном OperatingPoint. + +Роли плат сохранены: MASTER/TX `000000000000000097c063dc323b97d3`, +SLAVE/RX `000000000000000097c063dc2e5327d3`. Планируется прежнее тактирование +10 МГц TCXO → MASTER → CLKOUT → SLAVE CLKIN. Требуются оба reg29 = 0x26 +и SLAVE CLKIN = DETECTED после короткого RX-start. Сначала логика проверена +mock-вызовами; теперь эти значения подтверждены аппаратным preflight. + +Вызов `preflight_only` после получения RF_FREQ проверит инвентаризацию, +common clock и сохранит 0,1 с RX без TX на заданной частоте при RX 24/24 дБ. +Он проверит размер, явный нулевой счётчик потерь, уровень и клиппинг, +запишет `preflight_result.json`, выключит CLKOUT и завершится с STOP. +Любая TX-команда внутри этого этапа запрещена обёрткой runner. +Файл попытки создаётся эксклюзивно; автоматического повтора нет. + +После отдельного разрешения `capture_once` потребует PASS preflight той же +частоты, геометрии и SHA TX. Поскольку после preflight CLKOUT выключены, +перед фактическим сеансом опора проверяется заново при запуске. Это проверка +готовности перед единственным TX, а не повтор передачи. + +TX VGA 10 дБ, RX LNA/VGA 24/24 дБ, RF amplifier и Bias-Tee OFF, +Fs 8 Мвыб/с фиксированы на всю ABAB. RX запускается первым; затем допускается +один TX-процесс с конечным числом отсчётов, без `-R`. Маркер попытки создаётся +эксклюзивно; ошибка расходует попытку, автоматического повтора нет. +Требуются процессы CLEAR до/после, правильный размер RX, явные нулевые +RX overruns/TX underruns и успешное выключение CLKOUT обеих плат. +После упреждения RX проверяются последние 65 536 записанных отсчётов: +при rail-кодах −128/127 TX не запускается, RF TX=0 и STOP. Полная запись +проверяется также после ABAB. Если клиппинг появился только при передаче, +фиксируется RF TX=1 и невалидное измерение без повтора. Уже выполненная +передача не переобозначается нулём. В отчёте сохраняются RMS, пики I/Q, +доли клиппинга; отсутствие rail-кодов само по себе не доказывает отсутствие +аналоговой компрессии. Gain не меняется автоматически ни в одном сценарии. +Аппаратные помощники переиспользованы из существующей Lab054; её файлы +`experiments/lab054_hardware_control.py` и `tools/hackrf_process_check.py` +включаются в фиксацию Lab056 как необходимые зависимости вместе с их +двумя тестовыми файлами. Без них runner Lab056 не импортируется в чистой +копии репозитория, что независимо воспроизведено verifier. Старые отчёты, +данные и записи журнала Lab054 в эту фиксацию не входят. Реализации +зависимостей не переписывались; аппаратная логика осталась проверенной. + +Verifier независимо подтвердил SHA/размеры TX, нулевые gaps, побайтную +повторяемость A/B и пересчитал коэффициенты передачи вторым методом. +Он воспроизвёл ошибку разбора журнала `1 RX overruns\n0 overruns`; +исправленный разбор отклоняет ненулевые, неизвестные и неразобранные +счётчики. Повторные mock-вызовы `capture_once` закрыли находку. +Остаточных воспроизведённых находок нет. Оборудование verifier не использовал. + +Исходная подготовка: 34 целевых теста passed; quick_gate: 857 тестов, +30 функциональных проверок, 13 штатных пропусков, PASS. Полный pytest: +857 passed, 0 failed, 96,50 с (кеш отключён из-за прежнего отказа доступа). +`git diff --check`: PASS. Машинная сводка: +[acceptance.json](../data/processed/lab056/acceptance.json). + +Корректировка OTA: 44 целевых теста PASS; quick_gate 867 тестов / +30 функциональных проверок / 13 штатных пропусков, PASS; pytest +867 passed, 0 failed, 89,32 с. Verifier исполнил все 44 сценария +в памяти без файлов и оборудования; остаточных находок нет. +Новая сводка [ota_correction.json](../data/processed/lab056/ota_correction.json) +дополняет исходную acceptance, не заменяя исторические 857 passed. + +## Как читать будущий эфирный результат + +Эфирный ABAB сравнит весь путь: TX, антенны, распространение, RX. Малое +различие означает, что большой провал не воспроизведён в этом пути и этой +сессии. Оно не доказывает равенство мощности непосредственно на выходе TX. +Большое повторяемое различие тоже нельзя автоматически отнести внутрь +HackRF: частотная избирательность антенн и многолучевого распространения +остаётся частью измерения. Для отделения аппаратного эффекта пригодится +отдельно согласованный кабельный или калиброванный измерительный опыт. + +Ни необходимость PA, ни изменение результатов Lab047/Lab054 из этой +подготовки не следуют. Архитектура проекта сохраняется: управление через +RadioMaster/ExpressLRS/CRSF, SDR — видеотракт с целью минимум 5 свежих кадров/с. + +## Состояние перед STOP + +```text +LAB055 COMMIT = 5add95ecb8865f59075bbbced08d97b183fb3b48 +NEW HEAD = 5add95ecb8865f59075bbbced08d97b183fb3b48 +LAB056 RESULT = OTA_ABAB_VALID_LARGE_DROP_NOT_REPRODUCED_STOP +PHYSICAL CONNECTION = OTA, LOS, fixed antennas, same polarization/orientation, approximately 3-5 m per user +RF FREQUENCY = 1400000000 Hz (Lab056 test only) +A SYMBOL RATE = 20 ksymbol/s +B SYMBOL RATE = 500 ksymbol/s +TX RMS A = 44.004413 code +TX RMS B = 44.001925 code +TX POWER DIFFERENCE B/A = -0.000491 dB (whole segment) +ABAB TX DURATION = 1.323568 s +RX PLANNED DURATION = 3 s, lead 0.5 s +COMMON CLOCK PREFLIGHT = HARDWARE PASS, both reg29=0x26, SLAVE CLKIN=DETECTED +RX PREFLIGHT = PASS, 800000 complex samples, 0.1 s, overruns=0 +CLIPPING = 0 +READY FOR ANOTHER TX = NO (authorized single attempt consumed) +COMPLETED TX SESSIONS = 1 +AUTOMATIC TX REPEATS = 0 +RX ABAB RECORD = 24000000 complex samples, 3 s, clipping=0, overruns=0 +TX UNDERRUNS = 0 +RECEIVED POWER DIFFERENCE B/A = +0.108895 dB +TX-MATCHED CHANNEL GAIN DIFFERENCE B/A = -0.163042 dB +ENERGY PER SYMBOL DIFFERENCE = -13.870505 dB +BASELINE pytest = 857 passed (сохранённый результат подготовки) +quick_gate = PASS, 867 tests / 30 functional / 13 skipped +pytest = PASS, 867 passed / 0 failed (после корректировки) +git diff --check = PASS +RF TX = 0 +LAB056 COMMIT = NOT_PERFORMED +PUSH = NOT_PERFORMED +CURRENT SINGLE-TX AUTHORIZATION = CONSUMED +STOP +``` diff --git a/docs/lab056_rf_result.md b/docs/lab056_rf_result.md new file mode 100644 index 0000000..5e6ee37 --- /dev/null +++ b/docs/lab056_rf_result.md @@ -0,0 +1,222 @@ +# Lab056 — единственный OTA ABAB: результат + +10 сентября 2026 года, сеанс 12:32:24–12:32:28 по Москве. + +**Внутри одного короткого эфирного сеанса большой провал при переходе +20 → 500 ксимв/с не воспроизвёлся.** Принятая мощность после вычитания фона +у B выше A на 0,109 дБ; согласованный коэффициент передачи по мощности +ниже на 0,163 дБ. Обе пары A/B дают близкие результаты. Энергия символа +уменьшилась на 13,871 дБ: при почти одинаковой принятой мощности она +приходится на в 25 раз большее число символов в секунду. + +Это результат всего OTA-пути с антеннами и распространением. Он не +измеряет выходные dBm HackRF и не объясняет задним числом причину прежнего +различия Lab047/Lab054 на 15,41 дБ. Lab054 остаётся **NOT_A, 0/16**. + +Выполнена ровно **одна** передача. Сейчас **RF TX = 0**, передатчик +остановлен; повторов нет. Разрешённая попытка израсходована. **STOP.** + +## Фактические условия + +Пользователь отдельно разрешил этот сеанс после аппаратного RX-only +preflight. Частота 1 400 000 000 Гц относится только к Lab056, не является +выбором рабочего диапазона SDR_Rover. Ориентир около 180 МГц и варианты +50/144 МГц рассматриваются отдельно и здесь не исследовались. + +OTA: MASTER TX → TX-антенна → LOS → RX-антенна → SLAVE RX. По заданию +пользователя антенны неподвижны, ориентация и поляризация одинаковы, +расстояние примерно 3–5 м, кабели не меняются. Использованы имеющиеся +антенны на 1400 МГц; модели/маркировка и точное измеренное расстояние +не сообщены. Программа не измеряет геометрию; это описание пользователя. + +Один сохранённый TX-файл содержит A1 → gap → B1 → gap → A2 → gap → B2 +с дополнительными начальным/конечным gaps. A = 20 ксимв/с, B = 500 ксимв/с. +Fs 8 Мвыб/с; цифровая несущая +50 кГц; прежние PRBS7/BPSK/RRC. +TX VGA 10 дБ, RX LNA/VGA 24/24 дБ, RF amplifier/Bias-Tee OFF. +Внешние PA/LNA отсутствуют. Частота, усиления, Fs и параметры формы +не менялись между сегментами. PHY/FEC/CRC, пороги и timing не изменялись. + +Цифровой RMS всего сегмента A/B: 44,004413/44,001925 код; +разница мощности B/A −0,000491 дБ. Разница активных частей −0,042606 дБ +из-за разной доли защитных участков сохранена в метаданных и не скрывается. +RX не нормировался для искусственного выравнивания A/B. + +## Полнота записи и аппаратные проверки + +| Проверка | Фактический результат | +|---|---| +| Число TX-процессов | 1, без `-R`, конечный `-n 10588544` | +| TX-файл | 21 177 088 байт, 10 588 544 комплексных отсчёта, 1,323568 с | +| Основной RX | 48 000 000 байт, 24 000 000 комплексных отсчётов, ровно 3 с | +| Порядок запуска | RX раньше TX на 0,517342 с по журналу запуска процессов | +| MASTER / SLAVE reg29 | 0x26 / 0x26 | +| SLAVE CLKIN | DETECTED в пределах штатного ожидания запуска опоры | +| RX overruns / TX underruns | 0 / 0 по всем явным счётчикам журналов | +| Клиппинг RX до TX | 0 / 65 536 отсчётов проверенного хвоста | +| Клиппинг полного RX после ABAB | 0 / 24 000 000 отсчётов; 0 / 48 000 000 компонент | +| Полный RX: RMS / максимумы абсолютных I,Q | 2,235380477 код / 5, 6 код | +| Процессы до / после | CLEAR / CLEAR | +| Завершение | CLKOUT обеих плат выключены, ошибок cleanup нет | + +Общая опора проверена при запуске заново, поскольку предыдущий preflight +завершился выключением CLKOUT. Первый запрос CLKIN ещё не обнаружил опору; +второй обнаружил её через 137,359 мс штатного ожидания. После PASS +состоялся единственный TX; опрос CLKIN не является повтором передачи. + +В журнале 14 команд: инвентаризация, опора, два коротких RX-start по +65 536 отсчётов для выбора опоры, один основной RX, один TX и cleanup. +После запуска TX до его окончания других аппаратных команд нет. +Параметры команд и их исходный вывод сохранены; аппаратной перестройки +между A/B не было. Физическая неподвижность антенн следует из заданных +пользователем условий, а не из программного измерения. + +Утилита указала TX transfer time 1,32538 с и RX transfer time 3,01535 с. +Это служебные интервалы обмена, отличные от длительности отсчётов файла; +они не используются вместо расчёта длительности по числу отсчётов и Fs. +Отсутствие rail-кодов не является калиброванной проверкой аналоговой +компрессии; обнаруженных цифровых признаков насыщения нет. + +## Все четыре сегмента + +Подготовленный анализатор нашёл ABAB с нормированной корреляцией 0,384366 +и грубым остаточным CFO 0 Гц. Оценённые начала всей последовательности +по отдельным сегментам: 4 454 791, 4 454 791, 4 454 790, 4 454 791 отсчёт. +Разброс составляет один отсчёт, 0,125 мкс. Все сегменты расположены +внутри RX, идут в правильном порядке и не перекрываются. + +| Сегмент | Начало сегмента, отсчёт RX | Начало, с | Конец, с | Активных отсчётов | +|---|---:|---:|---:|---:| +| A1 | 5 254 791 | 0,656848875 | 0,863748875 | 1 638 400 | +| B1 | 7 709 991 | 0,963748875 | 1,168632875 | 1 638 400 | +| A2 | 10 149 062 | 1,268632750 | 1,475532750 | 1 638 400 | +| B2 | 12 604 263 | 1,575532875 | 1,780416875 | 1 638 400 | + +Длительность активного участка каждого сегмента 0,2048 с. С защитными +участками A длится 0,2069 с, B — 0,204884 с. Межсегментные интервалы +составляют 0,1 с с расхождением не более одного отсчёта из-за согласования. +В спектрах A и B видны соответственно узкая и широкая полосы PRBS/BPSK. +Границы, ширина спектров и согласованные компоненты подтверждают +наличие четырёх сегментов; это диагностическое измерение с TX-эталоном, +не независимый приём пакетов. + +![Запись, мощности и спектры единственного ABAB](../data/processed/lab056/rf_overview.png) + +## Измерения A1/B1/A2/B2 + +Все мощности ниже — **код² RX**, не ватты/dBm. `h` выражает отношение +цифровых кодов RX/TX после согласования и удаления постоянного смещения. +Для вычитания фона используется AC-мощность — после удаления среднего, +поэтому она показана отдельно от RAW, включающего постоянную составляющую. + +| Метрика | A1 | B1 | A2 | B2 | +|---|---:|---:|---:|---:| +| RAW RX POWER, включая среднее | 5,303053589 | 5,328170166 | 5,311703491 | 5,396551514 | +| RX AC POWER, без среднего | 1,084963566 | 1,092709351 | 1,049867204 | 1,090638777 | +| BACKGROUND POWER, AC | 0,622282607 | 0,616699337 | 0,635792032 | 0,667630886 | +| BACKGROUND-SUBTRACTED SIGNAL POWER | 0,462680958 | 0,476010015 | 0,414075173 | 0,423007891 | +| Оценка неопределённости вычитания фона | 0,029702311 | 0,034542825 | 0,017352868 | 0,047227716 | +| TX-MATCHED COMPLEX GAIN h | −0,008543899 + j0,009310849 | −0,008089660 + j0,009397484 | −0,007000883 + j0,010205712 | −0,006710537 + j0,010126447 | +| Коэффициент передачи по мощности, abs(h)² | 0,000159690119 | 0,000153755289 | 0,000153168935 | 0,000147576238 | +| MATCHED SIGNAL COMPONENT POWER | 0,312320987 | 0,297815525 | 0,299566893 | 0,285847043 | +| RESIDUAL POWER | 0,821513728 | 0,884789710 | 0,782684444 | 0,880722972 | +| INTEGRATED SPECTRAL POWER, полоса 27 кГц | 0,347264323 | 0,029697350 | 0,335922975 | 0,030437417 | +| INTEGRATED SPECTRAL POWER, полоса 675 кГц | 0,508299262 | 0,512832243 | 0,500120185 | 0,533314850 | + +Полосы спектрального интегрирования центрированы на +50 кГц. Фактическая +расчётная полоса A — 27 кГц, B — 675 кГц; строка 675 кГц также даёт +сравнение в общей полосе. Спектральные интегралы включают фон. Различие +значений в полосе 27 кГц между A/B не является потерей полной мощности B: +большая часть широкого сигнала B лежит вне этой узкой полосы. + +Вычитание фона устойчиво во всех четырёх сегментах по заранее подготовленному +критерию: превышение больше трёх оценок неопределённости. Эта оценка учитывает +разброс блоков и отличие фона до/после, но не является калиброванным +доверительным интервалом. Фон берётся из ближайших нулевых цифровых gaps; +TX-процесс в них работает, поэтому gaps нельзя называть выключенным RF. +Отдельные внешние окна фона и их спектры сохранены в полном JSON. + +Комплексный h оценён на обучающих плитках; его фаза относится к началу +активного участка после коррекции CFO. Мощность согласованной компоненты +и остатка измерена на проверочных плитках. Остаток содержит шум и +неописанные моделью искажения. Поэтому он не равен отдельно измеренному +фону, а согласованная компонента не равна всей мощности после вычитания фона. +Полная RAW/AC-статистика относится ко всему активному участку; плиточные +метрики не следует складывать с ней как одну точную энергетическую сумму. + +## Повторяемость и итоговые X/Y/Z + +В таблице повторяемости знак относится ко **второму повтору относительно +первого**: A2/A1 и B2/B1, 10 log10 отношения мощностей. + +| Сравнение повторов | RAW RX | Фон AC | Полезная мощность после фона | abs(h)² | Согласованная компонента | +|---|---:|---:|---:|---:|---:| +| A1 / A2 REPEATABILITY: A2/A1 | +0,007078 дБ | +0,093274 дБ | −0,482024 дБ | −0,181074 дБ | −0,181074 дБ | +| B1 / B2 REPEATABILITY: B2/B1 | +0,055382 дБ | +0,344629 дБ | −0,512676 дБ | −0,178136 дБ | −0,178136 дБ | + +| Пара A/B | Принятая мощность B/A после фона | abs(h)² B/A | Согласованная компонента B/A | +|---|---:|---:|---:| +| B1/A1 | +0,123345 дБ | −0,164480 дБ | −0,206538 дБ | +| B2/A2 | +0,092693 дБ | −0,161543 дБ | −0,203601 дБ | + +Итоговые X/Y/Z — отношение средних **линейных** мощностей B1/B2 к A1/A2, +а не среднее значений в дБ. Z рассчитано из той же мощности после фона, +что X, с делением на скорость символов. Это энергия в код²·с, не джоули. + +```text +500K / 20K RECEIVED POWER DIFFERENCE = +0.108895 dB +500K / 20K TX-MATCHED CHANNEL GAIN DIFFERENCE = -0.163042 dB +ENERGY PER SYMBOL DIFFERENCE = -13.870505 dB +``` + +Расчёт Z: +0,108895 − 10 log10(500000/20000) = −13,870505 дБ. +Отдельная энергия согласованной компоненты в парных результатах изменилась +на −14,185938 и −14,183001 дБ; это другой числитель, её не подменяют Z. + +## Инженерная интерпретация + +1. Внутри этой сессии 20 и 500 ксимв/с дали почти равные мощности. Небольшое + различие повторилось в обеих парах; утверждать точную частотную + характеристику по двум повторам нельзя. +2. Вторые повторы слабее первых на 0,48/0,51 дБ по мощности после фона + и примерно на 0,18 дБ по коэффициенту передачи. Фон также немного + меняется и содержит кратковременные всплески; при этом относительное + сравнение B/A сохраняется: расхождение двух оценок X около 0,031 дБ, + двух оценок Y около 0,003 дБ. +3. Прежний провал порядка 15,41 дБ **не воспроизвёлся** в текущей короткой + OTA-сессии. Причина различия старых раздельных опытов остаётся + неустановленной; их результаты не пересматриваются. +4. По OTA нельзя объявить эффект собственной аппаратной потерей HackRF + Plus: антенны и распространение входят в измеренный путь. В этой + сессии большого эффекта вообще не обнаружено. Абсолютная мощность + на выходе передатчика не измерена. +5. Кабельный контроль после получения SMA-переходника остаётся необходимым + отдельным этапом для отделения свойств аппаратуры от антенн и канала. + OTA его не заменяет. Автоматического повторения опыта или добавления + PA/внешнего LNA нет; необходимость усилителя не установлена. + +## Проверка и артефакты + +Verifier независимо прочитал журналы и raw IQ без оборудования и записи +файлов: проверил число команд, постоянство параметров, опору, размеры, +SHA, отсутствие клиппинга/потерь и согласованность четырёх начал. Raw AC, +два окна фона, CFO и комплексный МНК пересчитаны вторым способом; +результаты совпали с анализатором до 1e−12. Воспроизведённых находок нет. + +RX SHA-256: `e336dd653fa554058052fcf851e9f2462f66b8cf7c710815ca2b9a09ffc23ad4`. +TX SHA-256: `5b34bbe1a8e6d037d2d6d367a74e9eb84e8940db7da61600ffaa7f7497d4f600`. + +- [Полный подготовленный анализ](../data/processed/lab056/rf_analysis.json). +- [Итоговая машинная сводка](../data/processed/lab056/rf_result.json). +- [Авторизация и фиксированные параметры](../data/processed/lab056/rf_session_authorization.json). +- [Фактические команды и время](../data/processed/lab056/rf_session_commands.json). +- Raw: `data/raw/lab056/rx_stream.cs8`, `capture_result.json`, + `hardware_attempt.json`, `tx_process_result.json`, `rx_stdout.txt`, `rx_stderr.txt`. + +Исходные результаты подготовки, preflight и искусственные записи сохранены. +Код анализатора/runner и TX не изменялись; использована проверенная версия +44 целевых теста / quick_gate 867/30/13 / pytest 867 passed. Для анализа +аппаратной записи полные тесты повторно не запускались. Diff check PASS. +HEAD `5add95ecb8865f59075bbbced08d97b183fb3b48`, Lab056 не коммитилась, +push не выполнялся. Приём пакетов, видео и FEC не проверялись. + +**Выполненных TX-сеансов: 1. RF TX сейчас = 0. STOP.** diff --git a/docs/lab056_rx_preflight.md b/docs/lab056_rx_preflight.md new file mode 100644 index 0000000..922ff8a --- /dev/null +++ b/docs/lab056_rx_preflight.md @@ -0,0 +1,98 @@ +# Lab056 — аппаратный common-clock и RX-only preflight + +10 сентября 2026 года, 12:16:28–12:16:29 по Москве. + +**Аппаратный preflight PASS. RF TX = 0. STOP перед отдельным разрешением TX.** +Проверена общая опора и записан фон при выключенном передатчике. +Сравнение A/B по эфиру ещё не выполнялось. + +```text +RF_FREQ = 1400000000 Hz +COMMON CLOCK = PASS; MASTER reg29=0x26; SLAVE reg29=0x26; SLAVE CLKIN=DETECTED +RX PREFLIGHT = PASS; 800000 complex samples; 0.1 s; RX overruns=0 +CLIPPING = 0 / 800000 samples (0%) +RF TX = 0 +READY FOR SINGLE ABAB TX = YES (technical preflight; separate authorization pending) +STOP +``` + +## Условия + +По заданию пользователя 1400 МГц — только частота текущего лабораторного +Lab056. Она не назначает рабочий диапазон SDR_Rover. Ориентир дальнейших +полевых работ около 180 МГц рассматривается отдельно; антенна на этот +диапазон уже имеется. Варианты 50/144 МГц здесь не исследуются. + +Схема задана пользователем: MASTER TX → TX-антенна → LOS → RX-антенна → SLAVE RX. +Используются имеющиеся антенны на 1400 МГц. Точные модели/маркировка не +сообщены и программно не установлены; они не являлись препятствием для +этого RX-only этапа. Антенны неподвижны, ориентация и поляризация одинаковы. +Расстояние по заданию примерно 3–5 м; точное измеренное значение отсутствует. +Кабели и геометрия сохраняются на будущую ABAB. + +Аппаратный RX: Fs 8 Мвыб/с, LNA/VGA 24/24 дБ, RF amplifier и Bias-Tee OFF. +Внешние PA/LNA не используются. TX VGA 10 дБ остаётся запланированным +параметром будущего сеанса; TX в этом этапе не включался. + +MASTER `000000000000000097c063dc323b97d3` и SLAVE +`000000000000000097c063dc2e5327d3` подтверждены свежей инвентаризацией. +Обе платы определены как HackRF One r9 с firmware/libhackrf 2024.02.1. + +## Что выполнено + +Перед оборудованием подтверждены HEAD, пустой Git index, SHA проверенного +кода и неизменность готового TX. Проверка процессов дала CLEAR. + +Выполнены короткие RX-start по 65 536 отсчётов на MASTER и SLAVE для выбора +источника тактирования, чтения reg29 обеих плат и SLAVE CLKIN. Оба reg29 +равны 0x26; CLKIN обнаружен первым запросом, через 67,787 мс после начала +ожидания опоры. Затем SLAVE записал 800 000 комплексных отсчётов RX, 0,1 с. +Все три команды hackrf_transfer работали только с `-r`, на одной частоте +и с одинаковыми RX-параметрами. Журнал команд не содержит `-t`. + +| Метрика сохранённого RX | Значение | +|---|---:| +| Размер CS8 | 1 600 000 байт | +| Комплексных отсчётов | 800 000 | +| Полная средняя мощность | 4,85388125 код² | +| Мощность после удаления среднего | 0,6658039582 код² | +| RMS полного IQ | 2,203152571 код | +| Среднее I / Q | 0,92535625 / 1,82532 код | +| Максимум абсолютного I / Q | 4 / 6 код | +| Максимальная комплексная амплитуда | 6,08276253 код | +| Отсчётов с rail-кодами −128/127 | 0 | +| RX overruns | 0 | + +Цифровых признаков насыщения в этом RX-only фоне нет. Это не доказывает +отсутствие перегрузки при включённом TX: его сигнал пока отсутствовал. +Из этих отсчётов также нельзя установить абсолютную RF-мощность или +доказать отсутствие аналоговой компрессии. При будущем разрешённом запуске +сохраняются подготовленные проверки клиппинга без автоподбора gain. + +SHA-256 RX: `c4257f26e4cc8562eb32228f9a50fdeb055879c928af0744cdf4a81cf7cd085b`. +SHA-256 TX: `5b34bbe1a8e6d037d2d6d367a74e9eb84e8940db7da61600ffaa7f7497d4f600`. +После проверки CLKOUT обеих плат выключены успешно, процессы CLEAR, +ошибок cleanup нет. Больше аппаратных действий не выполнялось. + +## Доказательства и STOP + +- [План и описание геометрии](../data/processed/lab056/rx_preflight_plan.json). +- [Журнал фактических команд с временем и выводом](../data/processed/lab056/rx_preflight_commands.json). +- [Результат RX-only preflight](../data/processed/lab056/rx_preflight_result.json). +- Исходный RX и исходный результат: `data/raw/lab056/preflight_rx.cs8`, + `preflight_result.json`; маркер попытки: `preflight_attempt.json`. + +Код и TX waveform не менялись. Последние программные проверки относятся +к этой же версии: 44 целевых теста, quick_gate 867/30/13 PASS и pytest +867 passed. Полные тесты повторно не запускались для записи аппаратного +результата. Git diff --check PASS. Lab054 NOT_A, 0/16 сохранён. + +Verifier проверил только сохранённые файлы: 12 команд, три RX-start, +ни одной TX-команды; регистры, CLKIN, cleanup и параметры подтверждены +по исходному выводу. Целочисленный пересчёт мощности/клиппинга и SHA +полностью согласован с JSON. Воспроизведённых расхождений нет, PASS. +Дополнительных аппаратных команд verifier не выполнял. + +HEAD `5add95ecb8865f59075bbbced08d97b183fb3b48`. Lab056 не коммитилась, +push не выполнялся. Технические условия preflight выполнены; отдельного +разрешения на единственный ABAB TX ещё нет. **STOP.** diff --git a/experiments/lab054_hardware_control.py b/experiments/lab054_hardware_control.py new file mode 100644 index 0000000..2bf88c0 --- /dev/null +++ b/experiments/lab054_hardware_control.py @@ -0,0 +1,359 @@ +"""Lab054: подготовка короткого BPSK-контроля; RF только отдельной командой.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +from pathlib import Path +import subprocess +import sys +import time + +if __package__ in (None, ""): + sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +import numpy as np + +from experiments import lab047_jpeg_object_transport as hardware +from experiments import lab049_bpsk_symbol_rate as phy +from experiments.lab053_packet_processing import bind +from experiments.lab053_streaming_receiver import StreamingReceiver +from tools.hackrf_process_check import check_processes, require_clear + +clock = hardware.lab044.lab043 +MODE = phy.PhyMode(500_000) +RF_HZ = 1_400_000_000 +PACKETS = 16 +RX_SECONDS = 3 +RX_LEAD_SECONDS = 0.5 +RX_SAMPLES = RX_SECONDS * MODE.sample_rate_hz +DIRECTORY = Path("data/raw/lab054") +OUTPUT = Path("data/processed/lab054") + + +def save(path: Path, value: dict) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(value, ensure_ascii=False, indent=2), encoding="utf-8") + + +def plans(): + """Обычный бинарный объект, 16 фрагментов прежнего формата; не видеопоток.""" + random_generator = np.random.default_rng(54054) + data = random_generator.integers(0, 256, PACKETS * phy.lab046.FRAGMENT_DATA_SIZE, dtype=np.uint8) + return phy.lab046.build_object_frames(data.tobytes(), object_id=54054) + + +def parameters() -> dict: + return {"rf_hz": RF_HZ, "sample_rate_hz": MODE.sample_rate_hz, + "symbol_rate": MODE.symbol_rate, "samples_per_symbol": MODE.samples_per_symbol, + "carrier_hz": phy.lab045.CARRIER_OFFSET_HZ, + "master_tx": clock.MASTER_SERIAL, "slave_rx": clock.SLAVE_SERIAL, + "tx_vga_db": 10, "rx_lna_db": 24, "rx_vga_db": 24, + "amplifier": False, "bias_tee": False, + "threshold": hardware.FIXED_HARDWARE_CANDIDATE_THRESHOLD} + + +def transfer_command(serial: str, path: Path | str, samples: int, *, transmit=False) -> list[str]: + command = [str(clock.HACKRF_TOOLS_DIR / "hackrf_transfer.exe"), "-d", serial, + "-t" if transmit else "-r", str(path), "-f", str(RF_HZ), + "-s", str(MODE.sample_rate_hz), "-n", str(samples)] + command += ["-x", "10"] if transmit else ["-l", "24", "-g", "24"] + return command + ["-a", "0", "-p", "0", "-B"] + + +def short_receive(serial, runner): + """Штатный запуск приёмника для выбора опоры, без передающего режима.""" + result = runner(transfer_command(serial, "NUL", clock.SHORT_RX_SAMPLE_COUNT)) + if result.returncode: + raise RuntimeError(f"Короткий RX не выполнен: {result.stderr}") + + +def disable_clock_outputs(runner) -> list[str]: + """Попытаться отключить обе платы даже при timeout первой команды.""" + errors = [] + for serial in (clock.MASTER_SERIAL, clock.SLAVE_SERIAL): + try: + clock._set_clkout(serial, False, runner) + except Exception as error: + errors.append(f"{serial}: {error}") + return errors + + +clock_preflight = bind(clock.common_clock_preflight, _short_rx=short_receive, + _disable_clock_outputs=disable_clock_outputs) + + +def preflight_only(directory: Path = OUTPUT, runner=None, process_checker=None) -> dict: + """Реальный RX-only preflight; выходы тактирования выключаются в finally.""" + runner = runner or clock._default_command_runner + process_checker = process_checker or check_processes + record = {"rf_tx_count": 0, "passed": False} + guard = clock.TransmitGuard() + clock_started = False + try: + process_state = process_checker() + record["process_check"] = process_state.to_dict() + require_clear(process_state) + record["inventory"] = phy.lab045._verify_device_inventory(runner) + clock_started = True + record["clock"] = clock_preflight(runner, guard) + record["passed"] = True + except Exception as error: + record["error"] = str(error) + raise + finally: + record["cleanup_errors"] = disable_clock_outputs(runner) if clock_started else [] + record["cleanup_performed"] = clock_started + if clock_started: + try: + process_state = process_checker() + record["process_check_after_cleanup"] = process_state.to_dict() + require_clear(process_state) + except Exception as error: + record["cleanup_errors"].append(str(error)) + record["passed"] = record["passed"] and not record["cleanup_errors"] + save(directory / "common_clock_preflight.json", record) + if not record["passed"]: + raise RuntimeError("Не удалось выключить CLKOUT") + return record + + +def prepare(directory: Path = DIRECTORY) -> dict: + directory.mkdir(parents=True, exist_ok=True) + path = directory / "tx_stream.cs8" + if path.exists(): + raise FileExistsError("TX-файл уже существует; подготовка его не перезаписывает") + with path.open("xb") as stream: + for frame in plans(): + stream.write(phy.build_parameterized_tx_cs8(frame.plan, MODE)) + count = path.stat().st_size // 2 + result = {"stage": "PREPARATION_ONLY", "rf_tx_count": 0, + "head": subprocess.check_output(["git", "rev-parse", "HEAD"], text=True).strip(), + "parameters": parameters(), "packets": PACKETS, "tx_samples": count, + "tx_bytes": path.stat().st_size, "tx_seconds": count / MODE.sample_rate_hz, + "tx_sha256": hardware._sha256_file(path), "rx_seconds": RX_SECONDS, + "rx_samples": RX_SAMPLES, "rx_lead_seconds": RX_LEAD_SECONDS, + "rx_command": transfer_command(clock.SLAVE_SERIAL, directory / "rx_stream.cs8", RX_SAMPLES), + "tx_command": transfer_command(clock.MASTER_SERIAL, path, count, transmit=True)} + save(directory / "tx_metadata.json", result) + save(OUTPUT / "preparation.json", result) + return result + + +def validate_prepared(directory: Path) -> dict: + metadata = json.loads((directory / "tx_metadata.json").read_text(encoding="utf-8")) + path = directory / "tx_stream.cs8" + expected = hashlib.sha256() + count = 0 + for frame in plans(): + waveform = phy.build_parameterized_tx_cs8(frame.plan, MODE) + expected.update(waveform) + count += len(waveform) + if (metadata["parameters"] != parameters() or metadata["stage"] != "PREPARATION_ONLY" + or metadata["rf_tx_count"] != 0 or metadata["packets"] != PACKETS + or metadata["tx_samples"] * 2 != count or path.stat().st_size != count + or metadata["tx_sha256"] != expected.hexdigest() + or hardware._sha256_file(path) != expected.hexdigest()): + raise RuntimeError("TX-артефакты или параметры изменены") + return metadata + + +def decode_file(path: Path) -> tuple[dict, list]: + """RX получает только CS8-файл; TX truth отсутствует в интерфейсе.""" + observations = [] + receiver = StreamingReceiver(packet_observer=lambda row, outcome: observations.append((row.copy(), outcome))) + received = [] + clipping = 0 + try: + with path.open("rb") as stream: + while block := stream.read(2 * 65536): + values = np.frombuffer(block, np.int8) + if len(values) % 2: + raise ValueError("Неполный IQ-отсчёт") + clipping += int(np.count_nonzero((values == -128) | (values == 127))) + samples = (values[::2].astype(np.float32) + 1j * values[1::2]).astype(np.complex64) + received.extend(receiver.push_iq_block(samples)) + received.extend(receiver.finish()) + finally: + if not receiver.closed: + receiver.finish() + rows = [] + for row, outcome in observations: + frame = outcome.frame + rows.append({"accepted": outcome.accepted, "reason": outcome.reason, + "sync_pass": outcome.refinement_pass and outcome.timing_pass and outcome.carrier_pass, + "phy_header_pass": outcome.phy_header_pass, "fec_pass": outcome.fec_pass, + "crc": outcome.crc, "start": row["absolute_refined"], + "timing": frame.timing_offset_samples if frame else None, + "cfo_hz": frame.estimated_cfo_hz if frame else None, + "phase_rad": frame.estimated_phase_rad if frame else None, + "snr_db": frame.snr_db if frame else None, + "preamble_errors": frame.preamble_errors if frame else None}) + return {"independent": True, "packets": len(received), + "frames_detected": len({r["start"] for r in rows if r["sync_pass"]}), + "frames_detected_definition": "Unique refined starts with refinement/timing/carrier PASS; may include false candidates", + "strict_candidates": len(rows), "phy_header_pass": sum(r["phy_header_pass"] for r in rows), + "fec_pass": sum(r["fec_pass"] for r in rows), + "crc_pass": sum(r["crc"] == phy.lab045.CRC_PASS for r in rows), + "crc_fail": sum(r["crc"] == phy.lab045.CRC_FAIL for r in rows), + "clipping_components": clipping, "rows": rows}, observations + + +def roundtrip(directory: Path = DIRECTORY) -> dict: + validate_prepared(directory) + result, observations = decode_file(directory / "tx_stream.cs8") + result.update(compare_after_verdict(observations)) + result["passed"] = (result["packets"] == result["exact_packets"] == result["unique_exact_packets"] == PACKETS + and result["post_fec_errors"] == 0) + save(OUTPUT / "software_roundtrip.json", result) + if not result["passed"]: + raise RuntimeError("Программный roundtrip FAIL") + return result + + +def compare_after_verdict(observations) -> dict: + """Сверка разрешена только после завершения независимого приёма.""" + by_payload = {frame.plan.payload: frame.plan for frame in plans()} + comparisons = [] + matched_payloads = set() + exact_count = 0 + for _, outcome in observations: + if outcome.accepted and outcome.frame: + frame = outcome.frame + plan = by_payload.get(frame.reception.payload_bytes) + if plan is not None: + if frame.reception.packet_bytes == plan.packet: + matched_payloads.add(frame.reception.payload_bytes) + exact_count += 1 + comparisons.append(phy.lab045.diagnostic_pre_and_post_fec( + frame.reception, frame.post_preamble_soft, plan)) + return {"exact_packets": exact_count, "unique_exact_packets": len(matched_payloads), + "payload_matched_packets": len(comparisons), + "pre_fec_errors": sum(item["pre_fec_coded"]["bit_errors"] for item in comparisons) if comparisons else None, + "post_fec_errors": sum(item["post_fec_packet"]["bit_errors"] for item in comparisons) if comparisons else None, + "error_scope": "CRC accepted packets matched by payload AFTER independent verdict; unmatched packets UNKNOWN"} + + +def analyze_capture(directory: Path = DIRECTORY) -> dict: + """Офлайн-анализ сохранённой записи, без повторной передачи.""" + evidence = json.loads((directory / "capture_result.json").read_text(encoding="utf-8")) + path = directory / "rx_stream.cs8" + if evidence.get("rx_sha256") != hardware._sha256_file(path): + raise RuntimeError("Нет подтверждённого SHA записи") + result, observations = decode_file(path) + save(OUTPUT / "independent_result.json", result) + result.update(compare_after_verdict(observations)) + result.update(frames_tx=PACKETS, rx_overruns="UNKNOWN_UNLESS_EXPLICIT_IN_LOG", + tx_underruns="UNKNOWN_UNLESS_EXPLICIT_IN_LOG") + result["result"] = "A" if (result["unique_exact_packets"] >= 15 and evidence["passed"] + and result["post_fec_errors"] == 0 + and result["clipping_components"] == 0) else "NOT_A" + result["scope"] = "One short control only; no long-term stability or video RF claim" + save(OUTPUT / "hardware_result.json", result) + return result + + +def capture_once(directory: Path = DIRECTORY, *, authorization=False, runner=None, + process_starter=None, process_checker=None, sleep_function=time.sleep) -> dict: + """Один конечный TX; эксклюзивный маркер запрещает повтор после любой ошибки.""" + if authorization is not True: + raise PermissionError("Нужно отдельное разрешение пользователя на Lab054 RF") + metadata = validate_prepared(directory) + runner = runner or clock._default_command_runner + process_starter = process_starter or clock._default_process_starter + process_checker = process_checker or check_processes + with (directory / "hardware_attempt.json").open("x", encoding="utf-8") as stream: + json.dump({"authorized_once": True, "automatic_retry": False}, stream) + guard = clock.TransmitGuard() + process = None + clock_started = False + result = {"passed": False, "rf_tx_count": 0} + try: + process_state = process_checker() + result["process_check"] = process_state.to_dict() + require_clear(process_state) + result["inventory"] = phy.lab045._verify_device_inventory(runner) + clock_started = True + result["clock"] = clock_preflight(runner, guard) + receive_path = directory / "rx_stream.cs8" + process = process_starter(transfer_command(clock.SLAVE_SERIAL, receive_path, RX_SAMPLES), + directory / "rx_stdout.txt", directory / "rx_stderr.txt") + sleep_function(RX_LEAD_SECONDS) + if process.poll() is not None: + raise RuntimeError("RX завершился до TX") + guard.register_transmit() + result["rf_tx_count"] = guard.transmit_calls + transmitted = runner(transfer_command(clock.MASTER_SERIAL, directory / "tx_stream.cs8", + metadata["tx_samples"], transmit=True)) + (directory / "tx_stdout.txt").write_text(transmitted.stdout or "", encoding="utf-8") + (directory / "tx_stderr.txt").write_text(transmitted.stderr or "", encoding="utf-8") + if transmitted.returncode or process.wait(RX_SECONDS + 10): + raise RuntimeError("Ошибка единственного TX/RX; повтор запрещён") + process.close_logs() + if receive_path.stat().st_size != RX_SAMPLES * 2: + raise RuntimeError("Неверный размер RX") + for role in ("rx", "tx"): + log = "".join((directory / f"{role}_{kind}.txt").read_text(encoding="utf-8") for kind in ("stdout", "stderr")) + if not clock._transfer_log_is_clean(log): + raise RuntimeError(f"{role}: overrun/underrun; STOP") + result.update(passed=True, rx_sha256=hardware._sha256_file(receive_path)) + except Exception as error: + result["error"] = str(error) + raise + finally: + result["cleanup_errors"] = [] + if process is not None: + try: + process.terminate() + except Exception as error: + result["cleanup_errors"].append(str(error)) + if clock_started: + result["cleanup_errors"].extend(disable_clock_outputs(runner)) + try: + process_state = process_checker() + result["process_check_after_cleanup"] = process_state.to_dict() + require_clear(process_state) + result["lingering_processes"] = [] + except Exception as error: + result["cleanup_errors"].append(str(error)) + result["lingering_processes"] = None + result["passed"] = result["passed"] and not result["cleanup_errors"] and not result["lingering_processes"] + save(directory / "capture_result.json", result) + if not result["passed"]: + raise RuntimeError("Очистка аппаратного сеанса не прошла; STOP") + return result + + +def run_functional_tests() -> None: + assert MODE.samples_per_symbol == 16 + assert len(plans()) == PACKETS + assert "-t" not in transfer_command(clock.MASTER_SERIAL, "NUL", 65536) + 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", "roundtrip", "preflight", "capture", "analyze"), required=True) + parser.add_argument("--authorize-one-rf", action="store_true") + args = parser.parse_args() + if args.stage == "prepare": + result = prepare() + elif args.stage == "roundtrip": + result = roundtrip() + elif args.stage == "preflight": + result = preflight_only() + elif args.stage == "analyze": + result = analyze_capture() + else: + result = capture_once(authorization=args.authorize_one_rf) + print(json.dumps(result, ensure_ascii=False, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/experiments/lab056_abab_energy.py b/experiments/lab056_abab_energy.py new file mode 100644 index 0000000..9f959ca --- /dev/null +++ b/experiments/lab056_abab_energy.py @@ -0,0 +1,312 @@ +"""Lab056: программная подготовка ABAB и диагностика сохранённого RX.""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path +import sys + +if __package__ in (None, ""): + sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +import numpy as np +from scipy import signal + +from experiments import lab043_pluto_to_rtlsdr as shaping +from experiments import lab055_energy_audit as energy + +SAMPLE_RATE = 8_000_000 +CARRIER = 50_000 +GAP_SAMPLES = 800_000 +RX_SAMPLES = 24_000_000 +RX_LEAD_SECONDS = .5 +RAW_DIRECTORY = energy.PROJECT_ROOT / "data/raw/lab056" +OUTPUT = energy.PROJECT_ROOT / "data/processed/lab056" + + +def save_new(path: Path, value: dict) -> None: + text = json.dumps(value, ensure_ascii=False, indent=2, allow_nan=False) + "\n" + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("x", encoding="utf-8") as stream: + stream.write(text) + + +def build_burst(symbol_rate: int) -> np.ndarray: + """Прежние PRBS7/BPSK/RRC, 204,8 мс символов, RMS всего burst=44.""" + if symbol_rate not in (20000, 500000): + raise ValueError("Разрешены только два исследуемых режима") + symbols = 2. * shaping.pilot_prbs7(4096 * (symbol_rate // 20000)) - 1. + samples_per_symbol = SAMPLE_RATE // symbol_rate + waveform = shaping.shape_frame_like_lab042(symbols, samples_per_symbol) + waveform *= np.exp(2j * np.pi * CARRIER * np.arange(len(waveform)) / SAMPLE_RATE) + waveform *= 44 / np.sqrt(np.mean(abs(waveform)**2)) + values = np.empty(2 * len(waveform), dtype=np.int8) + values[::2] = np.clip(np.rint(waveform.real), -128, 127).astype(np.int8) + values[1::2] = np.clip(np.rint(waveform.imag), -128, 127).astype(np.int8) + return values + + +def prepare(directory: Path = RAW_DIRECTORY) -> dict: + """Создать единственный конечный TX-файл; аппаратная частота не назначена.""" + directory.mkdir(parents=True, exist_ok=True) + tx_path, metadata_path = directory / "tx_abab.cs8", directory / "tx_metadata.json" + if tx_path.exists() or metadata_path.exists(): + raise FileExistsError("Артефакты Lab056 уже существуют") + bursts = {rate: build_burst(rate) for rate in (20000, 500000)} + segments = [] + cursor = GAP_SAMPLES + with tx_path.open("xb") as stream: + stream.write(bytes(2 * GAP_SAMPLES)) + for label, rate in (("A1", 20000), ("B1", 500000), ("A2", 20000), ("B2", 500000)): + values = bursts[rate] + count = len(values) // 2 + complex_values = values[::2].astype(np.float32) + 1j * values[1::2].astype(np.float32) + whole, active = energy.Moments(), energy.Moments() + whole.add(complex_values) + trim = 21 * (SAMPLE_RATE // rate) + active.add(complex_values[trim:-trim]) + segments.append({"label": label, "symbol_rate": rate, "start": cursor, + "samples": count, "active_start": cursor + trim, "active_samples": count - 2 * trim, + "whole": whole.result(rate), "active": active.result(rate)}) + stream.write(values.tobytes()) + stream.write(bytes(2 * GAP_SAMPLES)) + cursor += count + GAP_SAMPLES + result = {"stage": "PREPARATION_ONLY", "rf_tx": 0, "rf_frequency_hz": None, + "physical_path": "OTA_REQUESTED_FREQUENCY_AND_GEOMETRY_PENDING", "sample_rate_hz": SAMPLE_RATE, + "carrier_hz": CARRIER, "tx_samples": cursor, "tx_bytes": 2 * cursor, + "tx_seconds": cursor / SAMPLE_RATE, "rx_samples": RX_SAMPLES, + "rx_seconds": RX_SAMPLES / SAMPLE_RATE, "rx_lead_seconds": RX_LEAD_SECONDS, + "gap_seconds": GAP_SAMPLES / SAMPLE_RATE, "gap_is_zero_digital_iq_not_rf_off": True, + "tx_sha256": energy.sha256(tx_path), "segments": segments, + "whole_power_difference_db": float(10 * np.log10(segments[1]["whole"]["power_code2"] / segments[0]["whole"]["power_code2"])), + "active_power_difference_db": float(10 * np.log10(segments[1]["active"]["power_code2"] / segments[0]["active"]["power_code2"]))} + if abs(result["whole_power_difference_db"]) > .01 or any(s["whole"]["clipped_samples_fraction"] for s in segments): + raise RuntimeError("Цифровая подготовка не прошла проверку мощности/клиппинга") + save_new(metadata_path, result) + return result + + +def locate_sequence(tx: energy.Cs8File, rx: energy.Cs8File) -> dict: + """Диагностическое согласование полной ABAB; TX reference разрешён. + + Грубая сетка 32 отсчёта использует всю последовательность, затем начало + каждого burst уточняется прежним методом Lab055 на обучающей плитке. + Прореживание без антиалиасинга здесь только для корреляционного поиска, + а не для измерений мощности или спектра. + """ + stride = 32 + def decimated(source): + chunks = [source.read(start, min(262144, source.size - start))[::stride] + for start in range(0, source.size, 262144)] + return np.concatenate(chunks).astype(np.complex128) + reference, received = decimated(tx), decimated(rx) + if len(received) < len(reference): + raise ValueError("RX короче TX") + reference -= reference.mean() + received -= received.mean() + cumulative = np.concatenate(([0.], np.cumsum(abs(received)**2))) + window_energy = cumulative[len(reference):] - cumulative[:-len(reference)] + reference_energy = np.sum(abs(reference)**2) + time = np.arange(len(reference)) * stride / SAMPLE_RATE + best = (-1., 0, 0.) + for frequency in np.arange(-2., 2.01, .25): + rotated = reference * np.exp(2j * np.pi * frequency * time) + correlation = signal.fftconvolve(received, np.conj(rotated[::-1]), mode="valid") + scores = abs(correlation) / np.sqrt(np.maximum(reference_energy * window_energy, 1e-30)) + index = int(np.argmax(scores)) + if scores[index] > best[0]: + best = (float(scores[index]), index * stride, float(frequency)) + # Это качество диагностического совпадения, не критерий PHY/CRC. + if best[0] < .05: + raise ValueError("ABAB не локализована достаточно уверенно; измерение UNKNOWN") + return {"start": best[1], "correlation": best[0], "coarse_cfo_hz": best[2], "stride": stride} + + +class OffsetSource: + def __init__(self, source: energy.Cs8File, offset: int): + self.source, self.offset = source, offset + + def read(self, start: int, count: int) -> np.ndarray: + return self.source.read(self.offset + start, count) + + +def window_power(values: np.ndarray, symbol_rate: int) -> dict: + moments = energy.Moments() + moments.add(values) + result = moments.result(symbol_rate) + centered = values - values.mean() + frequencies, density = energy.spectrum(centered) + for width in (27000, 675000): + result[f"band_{width}_power"] = energy.band_power(frequencies, density, CARRIER - width / 2, CARRIER + width / 2) + size = 32000 # Неперекрывающиеся блоки по 4 мс для оценки разброса мощности. + powers = [float(np.mean(abs(centered[start:start + size])**2)) for start in range(0, len(centered) - size + 1, size)] + result["power_standard_error"] = float(np.std(powers, ddof=1) / np.sqrt(len(powers))) if len(powers) > 1 else None + return result + + +def difference_db(new: float | None, old: float | None) -> float | None: + if new is None or old is None or new <= 0 or old <= 0: + return None + return float(10 * np.log10(new / old)) + + +def complex_gain(reference: np.ndarray, received: np.ndarray, cfo_hz: float) -> dict: + """Вывести комплексный h прежней модели; те же чётные обучающие плитки.""" + boundaries = np.linspace(0, len(reference), 9, dtype=int) + train = np.zeros(len(reference), dtype=bool) + for tile in range(0, 8, 2): + train[boundaries[tile]:boundaries[tile + 1]] = True + rotated = reference * np.exp(2j * np.pi * cfo_hz * np.arange(len(reference)) / SAMPLE_RATE) + x, y = rotated[train], received[train].astype(np.complex128) + x, y = x - x.mean(), y - y.mean() + gain = np.vdot(x, y) / max(float(np.sum(abs(x)**2)), 1e-30) + return {"real": float(gain.real), "imag": float(gain.imag), + "phase_rad_at_active_start": float(np.angle(gain))} + + +def comparison_summary(rows: list[dict]) -> dict: + """Сравнить повторы отдельно; X/Z используют средние линейные мощности.""" + by_label = {row["label"]: row for row in rows} + repeatability = [] + for mode in ("A", "B"): + first, second = by_label[mode + "1"], by_label[mode + "2"] + repeatability.append({"comparison": mode + "2/" + mode + "1", + "raw_rx_power_difference_db": difference_db(second["raw_rx"]["power_code2"], first["raw_rx"]["power_code2"]), + "background_difference_db": difference_db(second["background_power"], first["background_power"]), + "received_power_difference_db": difference_db(second["background_subtracted_signal_power"], first["background_subtracted_signal_power"]), + "channel_gain_difference_db": difference_db(second["channel_gain_power"], first["channel_gain_power"]), + "matched_component_difference_db": difference_db(second["measurement"]["heldout_signal_power"], first["measurement"]["heldout_signal_power"])}) + def mean(mode, key): + values = [by_label[mode + str(number)][key] for number in (1, 2)] + return None if any(value is None or value <= 0 for value in values) else float(np.mean(values)) + a_power, b_power = mean("A", "background_subtracted_signal_power"), mean("B", "background_subtracted_signal_power") + return {"repeatability": repeatability, + "averaging": "RATIO_OF_MEAN_LINEAR_POWERS_B1_B2_OVER_A1_A2", + "received_power_difference_db": difference_db(b_power, a_power), + "channel_gain_difference_db": difference_db(mean("B", "channel_gain_power"), mean("A", "channel_gain_power")), + "energy_per_symbol_difference_db": difference_db(None if b_power is None else b_power / 500000, + None if a_power is None else a_power / 20000), + "energy_basis": "BACKGROUND_SUBTRACTED_RECEIVED_POWER_DIVIDED_BY_SYMBOL_RATE"} + + +def analyze(directory: Path, rx_path: Path) -> dict: + metadata = energy.read_json(directory / "tx_metadata.json") + tx_path = directory / "tx_abab.cs8" + if energy.sha256(tx_path) != metadata["tx_sha256"]: + raise ValueError("TX SHA не совпал") + tx, rx = energy.Cs8File(tx_path), energy.Cs8File(rx_path) + alignment = locate_sequence(tx, rx) + rows = [] + for segment in metadata["segments"]: + rate, count = segment["symbol_rate"], segment["samples"] + start = alignment["start"] + segment["start"] + measurement = energy.analyze_rx(OffsetSource(tx, segment["start"]), rx, [start], count, + SAMPLE_RATE // rate)["frames"][0] + start = measurement["start"] + trim = 21 * (SAMPLE_RATE // rate) + received_active = rx.read(start + trim, count - 2 * trim) + raw = window_power(received_active, rate) + gain = complex_gain(tx.read(segment["start"] + trim, count - 2 * trim), received_active, measurement["cfo_hz"]) + if not np.isclose(gain["real"]**2 + gain["imag"]**2, measurement["gain_abs"]**2, rtol=1e-10, atol=1e-15): + raise ValueError("Вывод комплексного h расходится с прежней моделью Lab055") + windows = [window_power(rx.read(begin, 480000), rate) for begin in + (start - 640000, start + count + 160000)] + background = float(np.mean([w["ac_power_code2"] for w in windows])) + excess = raw["ac_power_code2"] - background + uncertainty = float(np.sqrt(raw["power_standard_error"]**2 + + sum(w["power_standard_error"]**2 for w in windows) / 4 + + ((windows[0]["ac_power_code2"] - windows[1]["ac_power_code2"]) / 2)**2)) + stable = excess > 3 * uncertainty + rows.append({"label": segment["label"], "symbol_rate": rate, "raw_rx": raw, + "local_zero_iq_background_windows": windows, "background_power": background, + "background_subtracted_raw": excess, "background_subtracted_uncertainty": uncertainty, + "background_subtracted_stable": stable, "background_subtracted_signal_power": excess if stable else None, + "channel_gain_complex": gain, "channel_gain_power": measurement["gain_abs"]**2, "measurement": measurement}) + by_label = {r["label"]: r for r in rows} + pairs = [] + for number in (1, 2): + a, b = by_label[f"A{number}"], by_label[f"B{number}"] + pairs.append({"pair": number, + "received_power_difference_db": difference_db(b["background_subtracted_signal_power"], a["background_subtracted_signal_power"]), + "channel_gain_difference_db": difference_db(b["channel_gain_power"], a["channel_gain_power"]), + "matched_component_difference_db": difference_db(b["measurement"]["heldout_signal_power"], a["measurement"]["heldout_signal_power"]), + "matched_component_symbol_energy_difference_db": difference_db(b["measurement"]["heldout_signal_power"] / b["symbol_rate"], a["measurement"]["heldout_signal_power"] / a["symbol_rate"])}) + # До начала и после конца всего цифрового файла: отдельные внешние окна. + outside = [] + for begin in (alignment["start"] - 800000, alignment["start"] + tx.size + 400000): + if 0 <= begin and begin + 400000 <= rx.size: + outside.append({"start": begin, **window_power(rx.read(begin, 400000), 20000)}) + return {"scope": "TX_REFERENCE_DIAGNOSTICS_NOT_INDEPENDENT_PACKET_RX", "rf_tx": 0, + "tx_sha256": metadata["tx_sha256"], "rx_sha256": energy.sha256(rx_path), + "alignment": alignment, "segments": rows, "pairs": pairs, "comparison_summary": comparison_summary(rows), + "outside_waveform_background": outside, + "background_note": "Внутренние gaps: нулевой цифровой IQ при работающем TX; это не RF OFF. Внешние окна также требуют сверки времён запуска/остановки по журналам.", + "power_units": "RX code squared, not calibrated watts/dBm", + "residual_note": "Шум и неописанные моделью искажения; RX не нормируется для выравнивания A/B"} + + +def software_capture(directory: Path, b_gain: float, name: str) -> dict: + """Синтетический CS8 с известным усилением, DC и CFO; не аппаратная запись.""" + metadata = energy.read_json(directory / "tx_metadata.json") + tx = energy.Cs8File(directory / "tx_abab.cs8") + path = directory / f"synthetic_{name}.cs8" + random_generator = np.random.default_rng(56056) + lead = 4_160_013 + with path.open("xb") as stream: + for low in range(0, RX_SAMPLES, 262144): + count = min(262144, RX_SAMPLES - low) + values = (1 + 2j + .6 * (random_generator.normal(size=count) + 1j * random_generator.normal(size=count))) + first, last = max(low, lead), min(low + count, lead + tx.size) + if first < last: + source = tx.read(first - lead, last - first) + gains = np.full(len(source), .03) + indexes = np.arange(first - lead, last - lead) + for segment in metadata["segments"]: + if segment["label"].startswith("B"): + gains[(indexes >= segment["start"]) & (indexes < segment["start"] + segment["samples"])] = b_gain + values[first - low:last - low] += source * gains * np.exp(1j * (.7 + 2 * np.pi * .2 * np.arange(first, last) / SAMPLE_RATE)) + packed = np.empty(2 * count, dtype=np.int8) + packed[::2] = np.clip(np.rint(values.real), -128, 127) + packed[1::2] = np.clip(np.rint(values.imag), -128, 127) + stream.write(packed.tobytes()) + result = analyze(directory, path) + expected = 20 * np.log10(b_gain / .03) + errors = [abs(p["channel_gain_difference_db"] - expected) for p in result["pairs"]] + result.update(synthetic=True, expected_gain_difference_db=float(expected), + known_sequence_start=lead, passed=bool(max(errors) < .3)) + if not result["passed"]: + raise RuntimeError(f"Синтетическая проверка не прошла: {errors}") + return result + + +def run_functional_tests() -> None: + assert difference_db(.25, 1) == 10 * np.log10(.25) + assert difference_db(None, 1) is None + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("stage", choices=("prepare", "software", "analyze")) + parser.add_argument("--rx", type=Path) + parser.add_argument("--output", type=Path) + args = parser.parse_args() + if args.stage == "prepare": + result = prepare() + save_new(OUTPUT / "preparation.json", result) + elif args.stage == "software": + for name, gain in (("equal", .03), ("minus6db", .015)): + result = software_capture(RAW_DIRECTORY, gain, name) + save_new(OUTPUT / f"software_{name}.json", result) + print(name, result["pairs"], flush=True) + return + else: + if args.rx is None or args.output is None: + parser.error("Для сохранённого RX нужны --rx и новый --output") + result = analyze(RAW_DIRECTORY, args.rx) + save_new(args.output, result) + print(json.dumps(result, ensure_ascii=False, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/experiments/lab056_capture_runner.py b/experiments/lab056_capture_runner.py new file mode 100644 index 0000000..56eca46 --- /dev/null +++ b/experiments/lab056_capture_runner.py @@ -0,0 +1,263 @@ +"""Lab056: конечный RX-first runner; этот модуль не имеет аппаратного CLI.""" + +from __future__ import annotations + +from dataclasses import asdict, dataclass +import json +from pathlib import Path +import re +import time + +from experiments import lab054_hardware_control as previous +from experiments import lab056_abab_energy as waveform + + +@dataclass(frozen=True) +class OperatingPoint: + """Частота пользователя и описание неподвижного эфирного тракта.""" + + frequency_hz: int | None = None + physical_path: str = "MASTER TX -> TX antenna -> free space -> RX antenna -> SLAVE RX" + path_confirmed: bool = False + tx_antenna: str = "UNKNOWN" + rx_antenna: str = "UNKNOWN" + distance_m: float | None = None + orientation: str = "UNKNOWN" + visibility: str = "UNKNOWN" + + def validate(self) -> None: + # Допустимую лабораторную частоту определяет пользователь, не этот модуль. + if type(self.frequency_hz) is not int or self.frequency_hz <= 0: + raise PermissionError("Нужно конкретное целое RF_FREQ в Гц от пользователя") + + +def command(point: OperatingPoint, path: Path | str, samples: int, *, transmit: bool = False) -> list[str]: + point.validate() + if not isinstance(samples, int) or samples <= 0: + raise ValueError("Нужен положительный конечный счётчик отсчётов") + clock = previous.clock + result = [str(clock.HACKRF_TOOLS_DIR / "hackrf_transfer.exe"), "-d", + clock.MASTER_SERIAL if transmit else clock.SLAVE_SERIAL, "-t" if transmit else "-r", + str(path), "-f", str(point.frequency_hz), "-s", str(waveform.SAMPLE_RATE), "-n", str(samples)] + return result + (["-x", "10"] if transmit else ["-l", "24", "-g", "24"]) + ["-a", "0", "-p", "0", "-B"] + + +def common_clock(point: OperatingPoint, runner, guard, *, sleep_function=time.sleep) -> dict: + """Прежняя логика регистров/CLKIN; короткий RX использует новую согласованную RF.""" + point.validate() + def short_receive(serial, command_runner): + parameters = command(point, "NUL", previous.clock.SHORT_RX_SAMPLE_COUNT) + parameters[parameters.index("-d") + 1] = serial + result = command_runner(parameters) + if result.returncode: + raise RuntimeError("Короткий RX-start не выполнен") + preflight = previous.bind(previous.clock.common_clock_preflight, _short_rx=short_receive, + _disable_clock_outputs=previous.disable_clock_outputs) + return preflight(runner, guard, sleep_function=sleep_function) + + +def validated_metadata(directory: Path) -> dict: + metadata = waveform.energy.read_json(directory / "tx_metadata.json") + path = directory / "tx_abab.cs8" + if (metadata["stage"] != "PREPARATION_ONLY" or metadata["rf_tx"] != 0 or + metadata["sample_rate_hz"] != waveform.SAMPLE_RATE or metadata["carrier_hz"] != waveform.CARRIER or + metadata["tx_samples"] * 2 != path.stat().st_size or + waveform.energy.sha256(path) != metadata["tx_sha256"] or + metadata["rx_samples"] != waveform.RX_SAMPLES or + [(s["label"], s["symbol_rate"]) for s in metadata["segments"]] != + [("A1", 20000), ("B1", 500000), ("A2", 20000), ("B2", 500000)] or + abs(metadata["whole_power_difference_db"]) > .01): + raise ValueError("Подготовленный TX/параметры изменились") + return metadata + + +def clean_counter_log(log: str, expected: str) -> bool: + """Все упоминания потерь должны быть разобраны и иметь явный нулевой счётчик.""" + pattern = re.compile(r"(? None: + if (not result.get("passed") or result.get("master_reg29") != 0x26 or + result.get("slave_reg29") != 0x26 or result.get("slave_clkin_status") != "DETECTED"): + raise RuntimeError("Common clock FAIL") + + +def receive_level(path: Path, *, tail_samples: int | None = None) -> dict: + """Уровень CS8 без изменения gain; rail-коды являются признаком клиппинга.""" + source = waveform.energy.Cs8File(path) + if source.size < 65536: + raise RuntimeError("Недостаточно сохранённых RX-отсчётов для проверки уровня") + start = max(0, source.size - tail_samples) if tail_samples else 0 + moments = waveform.energy.Moments() + for begin in range(start, source.size, 262144): + moments.add(source.read(begin, min(262144, source.size - begin))) + result = moments.result(20000) + result["saturation_detected"] = result["clipped_samples_fraction"] > 0 + result["analog_compression"] = "UNKNOWN_WITHOUT_CALIBRATION" + return result + + +def preflight_only(directory: Path, point: OperatingPoint, *, runner=None, + process_checker=None, clock_checker=None, sleep_function=time.sleep) -> dict: + """Только RX: опора и 0,1 с фона; сохранить результат и STOP до разрешения TX.""" + point.validate() + metadata = validated_metadata(directory) + waveform.save_new(directory / "preflight_attempt.json", {"point": asdict(point), "rf_tx_count": 0}) + actual_runner = runner or previous.clock._default_command_runner + process_checker = process_checker or previous.check_processes + clock_checker = clock_checker or common_clock + def receive_only_runner(parameters): + if "-t" in parameters: + raise PermissionError("TX запрещён внутри preflight_only") + return actual_runner(parameters) + record = {"passed": False, "rf_tx_count": 0, "point": asdict(point), + "tx_sha256": metadata["tx_sha256"], "stop_before_tx": True} + clock_started = False + try: + before = process_checker() + record["process_before"] = before.to_dict() + previous.require_clear(before) + record["inventory"] = previous.phy.lab045._verify_device_inventory(receive_only_runner) + clock_started = True + record["clock"] = clock_checker(point, receive_only_runner, previous.clock.TransmitGuard(), + sleep_function=sleep_function) + require_clock_pass(record["clock"]) + path = directory / "preflight_rx.cs8" + if path.exists(): + raise FileExistsError("RX preflight уже существует") + captured = receive_only_runner(command(point, path, 800000)) + record["rx_process"] = {"returncode": captured.returncode, + "stdout": captured.stdout, "stderr": captured.stderr} + if captured.returncode or not clean_counter_log(captured.stdout + captured.stderr, "overrun"): + raise RuntimeError("RX preflight: ошибка процесса или счётчиков") + if path.stat().st_size != 1600000: + raise RuntimeError("Неверный размер RX preflight") + record["rx_level"] = receive_level(path) + record["rx_sha256"] = waveform.energy.sha256(path) + if record["rx_level"]["saturation_detected"]: + raise RuntimeError("RX saturation до TX; STOP без изменения gains") + record["passed"] = True + except Exception as error: + record["error"] = str(error) + raise + finally: + errors = previous.disable_clock_outputs(receive_only_runner) if clock_started else [] + try: + after = process_checker() + record["process_after"] = after.to_dict() + previous.require_clear(after) + except Exception as error: + errors.append(str(error)) + record["cleanup_errors"] = errors + record["passed"] = record["passed"] and not errors + waveform.save_new(directory / "preflight_result.json", record) + if not record["passed"]: + raise RuntimeError("Preflight cleanup FAIL") + return record + + +def require_preflight(directory: Path, point: OperatingPoint, metadata: dict) -> None: + record = waveform.energy.read_json(directory / "preflight_result.json") + if (record.get("passed") is not True or record.get("rf_tx_count") != 0 or + record.get("point") != asdict(point) or record.get("tx_sha256") != metadata["tx_sha256"]): + raise PermissionError("Нужен успешный RX-only preflight именно этой конфигурации") + require_clock_pass(record.get("clock", {})) + if record.get("rx_level", {}).get("saturation_detected") is not False: + raise PermissionError("Нет подтверждённой проверки уровня RX до TX") + + +def capture_once(directory: Path, point: OperatingPoint, *, authorization: bool = False, + runner=None, process_starter=None, process_checker=None, + clock_checker=None, sleep_function=time.sleep) -> dict: + """Один RX и один TX; ошибка расходует попытку, автоматического повтора нет.""" + if authorization is not True: + raise PermissionError("Нужно отдельное разрешение на конкретный RF-сеанс Lab056") + point.validate() + metadata = validated_metadata(directory) + require_preflight(directory, point, metadata) + receive_path = directory / "rx_stream.cs8" + if receive_path.exists() or (directory / "capture_result.json").exists(): + raise FileExistsError("Аппаратные результаты уже существуют") + runner = runner or previous.clock._default_command_runner + process_starter = process_starter or previous.clock._default_process_starter + process_checker = process_checker or previous.check_processes + clock_checker = clock_checker or common_clock + waveform.save_new(directory / "hardware_attempt.json", {"authorized_once": True, + "point": asdict(point), "tx_sha256": metadata["tx_sha256"], "automatic_retry": False}) + result = {"passed": False, "rf_tx_count": 0, "point": asdict(point)} + guard = previous.clock.TransmitGuard() + process, clock_started = None, False + try: + before = process_checker() + result["process_before"] = before.to_dict() + previous.require_clear(before) + result["inventory"] = previous.phy.lab045._verify_device_inventory(runner) + clock_started = True + result["clock"] = clock_checker(point, runner, guard, sleep_function=sleep_function) + require_clock_pass(result["clock"]) + process = process_starter(command(point, receive_path, waveform.RX_SAMPLES), + directory / "rx_stdout.txt", directory / "rx_stderr.txt") + sleep_function(waveform.RX_LEAD_SECONDS) + if process.poll() is not None: + raise RuntimeError("RX завершился до TX") + result["rx_level_before_tx"] = receive_level(receive_path, tail_samples=65536) + if result["rx_level_before_tx"]["saturation_detected"]: + raise RuntimeError("RX saturation до TX; STOP без изменения gains") + guard.register_transmit() + result["rf_tx_count"] = guard.transmit_calls + transmitted = runner(command(point, directory / "tx_abab.cs8", metadata["tx_samples"], transmit=True)) + waveform.save_new(directory / "tx_process_result.json", {"returncode": transmitted.returncode, + "stdout": transmitted.stdout, "stderr": transmitted.stderr}) + if transmitted.returncode or process.wait(13): + raise RuntimeError("Единственный TX/RX завершился ошибкой") + process.close_logs() + if receive_path.stat().st_size != waveform.RX_SAMPLES * 2: + raise RuntimeError("Неверный размер RX") + rx_log = "".join((directory / f"rx_{kind}.txt").read_text(encoding="utf-8") for kind in ("stdout", "stderr")) + for role, log, counter in (("RX", rx_log, "overrun"), + ("TX", transmitted.stdout + transmitted.stderr, "underrun")): + if not clean_counter_log(log, counter): + raise RuntimeError(f"{role}: нет явного нулевого счётчика потерь") + result.update(rx_sha256=waveform.energy.sha256(receive_path), rx_level=receive_level(receive_path)) + if result["rx_level"]["saturation_detected"]: + raise RuntimeError("RX saturation в сохранённом сеансе; TX уже был, повтор запрещён") + result["passed"] = True + except Exception as error: + result["error"] = str(error) + raise + finally: + errors = [] + if process is not None: + try: + process.terminate() + except Exception as error: + errors.append(str(error)) + if clock_started: + errors.extend(previous.disable_clock_outputs(runner)) + try: + after = process_checker() + result["process_after"] = after.to_dict() + previous.require_clear(after) + except Exception as error: + errors.append(str(error)) + result["cleanup_errors"] = errors + result["passed"] = result["passed"] and not errors + waveform.save_new(directory / "capture_result.json", result) + if not result["passed"]: + raise RuntimeError("Cleanup FAIL; повтор запрещён") + return result + + +def run_functional_tests() -> None: + try: + capture_once(Path("UNUSED"), OperatingPoint()) + except PermissionError: + pass + else: + raise AssertionError("Разрешение проверяется раньше любых действий") diff --git a/tests/test_hackrf_process_check.py b/tests/test_hackrf_process_check.py new file mode 100644 index 0000000..79976b1 --- /dev/null +++ b/tests/test_hackrf_process_check.py @@ -0,0 +1,159 @@ +"""Достоверность перечисления процессов и запрет аппаратных действий при FAIL.""" + +from __future__ import annotations + +import json +import subprocess +from types import SimpleNamespace + +import pytest + +from experiments import lab054_hardware_control as lab +from tools import hackrf_process_check as check + + +def snapshot(names=("python.exe",), errors=()): + return check.classify_snapshot("fixture", [check.ProcessIdentity(index + 1, name) + for index, name in enumerate(names)], errors) + + +def completed(stdout="", stderr="", code=0): + return lambda command: subprocess.CompletedProcess(command, code, stdout, stderr) + + +def test_clear_full_snapshot(): + result = check.check_processes([lambda: snapshot()]) + assert result.state is check.ProcessState.CLEAR + check.require_clear(result) + + +@pytest.mark.parametrize("name", ["hackrf_transfer.exe", "hackrf_clock.exe", "HACKRF_DEBUG.EXE", + "hackrf_info.exe", "sdrangel.exe", "SDRSharp", "gqrx"]) +def test_busy_known_sdr_clients(name): + result = check.check_processes([lambda: snapshot((name,), errors=("partial snapshot",)), + lambda: pytest.fail("BUSY запрещает fallback")]) + assert result.state is check.ProcessState.BUSY + with pytest.raises(RuntimeError): + check.require_clear(result) + + +@pytest.mark.parametrize("names,errors", [((), ()), (("python",), ("Access denied",)), (("",), ())]) +def test_unknown_never_becomes_clear(names, errors): + result = check.check_processes([lambda: snapshot(names, errors)]) + assert result.state is check.ProcessState.UNKNOWN + with pytest.raises(RuntimeError): + check.require_clear(result) + + +def test_tasklist_access_denied_is_unknown(): + result = check.scan_tasklist(completed(stderr="ERROR: Access denied", code=1)) + assert result.state is check.ProcessState.UNKNOWN + assert "Access denied" in str(result.errors) + + +def test_fallback_to_powershell_after_psutil_unavailable(monkeypatch): + powershell = check.scan_powershell + monkeypatch.setattr(check, "scan_psutil", lambda: snapshot((), ("ImportError: psutil",))) + monkeypatch.setattr(check, "scan_powershell", lambda: powershell( + completed('[{"Id":12,"ProcessName":"python"}]'))) + monkeypatch.setattr(check, "scan_tasklist", lambda: pytest.fail("Полный CLEAR уже получен")) + result = check.check_processes() + assert result.state is check.ProcessState.CLEAR + assert [item.state for item in result.attempts] == [check.ProcessState.UNKNOWN, check.ProcessState.CLEAR] + + +def test_tasklist_is_last_fallback(monkeypatch): + tasklist = check.scan_tasklist + monkeypatch.setattr(check, "scan_psutil", lambda: snapshot((), ("unavailable",))) + monkeypatch.setattr(check, "scan_powershell", lambda: snapshot((), ("denied",))) + monkeypatch.setattr(check, "scan_tasklist", lambda: tasklist( + completed('"python.exe","123","Console","1","123 K"\n'))) + result = check.check_processes() + assert result.state is check.ProcessState.CLEAR + assert len(result.attempts) == 3 + + +@pytest.mark.parametrize("output", ["", "[]", "null", "not json", '[{"Id":4,"ProcessName":null}]']) +def test_powershell_malformed_not_clear(output): + assert check.scan_powershell(completed(output)).state is check.ProcessState.UNKNOWN + + +@pytest.mark.parametrize("output", ["", "INFO: No tasks are running", '"python.exe","bad pid","a","b","c"']) +def test_tasklist_malformed_not_clear(output): + assert check.scan_tasklist(completed(output)).state is check.ProcessState.UNKNOWN + + +def test_provider_exception_and_empty_provider_list_unknown(): + def failed(): + raise PermissionError("denied") + assert check.check_processes([failed]).state is check.ProcessState.UNKNOWN + assert check.check_processes([]).state is check.ProcessState.UNKNOWN + + +@pytest.mark.parametrize("mechanism", ["powershell", "tasklist"]) +@pytest.mark.parametrize("malformed_first", [False, True]) +def test_busy_survives_malformed_neighbor(mechanism, malformed_first): + if mechanism == "powershell": + rows = [{"Id":42,"ProcessName":"hackrf_transfer"}, {}] + if malformed_first: + rows.reverse() + scan = check.scan_powershell(completed(json.dumps(rows))) + else: + rows = ['"hackrf_transfer.exe","42","Console","1","100 K"', "BROKEN"] + if malformed_first: + rows.reverse() + scan = check.scan_tasklist(completed("\n".join(rows))) + assert scan.errors + result = check.check_processes([lambda: scan, lambda: pytest.fail("Найденный BUSY нельзя потерять")]) + assert result.state is check.ProcessState.BUSY + + +@pytest.mark.parametrize("disappeared", [False, True]) +def test_psutil_access_denied_or_exited_process(monkeypatch, disappeared): + class NoSuchProcess(Exception): + pass + + def unreadable(): + raise NoSuchProcess() if disappeared else PermissionError("Access denied") + + module = SimpleNamespace(NoSuchProcess=NoSuchProcess, process_iter=lambda: iter([ + SimpleNamespace(pid=1, name=lambda: "python.exe"), + SimpleNamespace(pid=2, name=unreadable)])) + monkeypatch.setattr(check.importlib, "import_module", lambda name: module) + result = check.scan_psutil() + assert result.state is (check.ProcessState.CLEAR if disappeared else check.ProcessState.UNKNOWN) + + +@pytest.mark.parametrize("state", list(check.ProcessState)) +def test_preflight_only_clear_reaches_hardware(tmp_path, monkeypatch, state): + actions = [] + scan = snapshot(("hackrf_transfer.exe",)) if state is check.ProcessState.BUSY else snapshot() + if state is check.ProcessState.UNKNOWN: + scan = snapshot((), ("tasklist Access denied",)) + result = check.ProcessCheck(state, (scan,)) + monkeypatch.setattr(lab.phy.lab045, "_verify_device_inventory", lambda runner: actions.append("inventory") or {}) + monkeypatch.setattr(lab, "clock_preflight", lambda runner, guard: actions.append("clock") or {"passed": True}) + monkeypatch.setattr(lab, "disable_clock_outputs", lambda runner: actions.append("cleanup") or []) + if state is check.ProcessState.CLEAR: + assert lab.preflight_only(tmp_path, runner=lambda _: None, process_checker=lambda: result)["passed"] + assert actions == ["inventory", "clock", "cleanup"] + else: + with pytest.raises(RuntimeError): + lab.preflight_only(tmp_path, runner=lambda _: None, process_checker=lambda: result) + assert actions == [] + report = json.loads((tmp_path / "common_clock_preflight.json").read_text(encoding="utf-8")) + assert report["process_check"]["state"] == state.value + assert report["rf_tx_count"] == 0 + + +def test_unknown_after_cleanup_rejects_preflight(tmp_path, monkeypatch): + states = iter([check.ProcessCheck(check.ProcessState.CLEAR, (snapshot(),)), + check.ProcessCheck(check.ProcessState.UNKNOWN, (snapshot((), ("denied",)),))]) + monkeypatch.setattr(lab.phy.lab045, "_verify_device_inventory", lambda runner: {}) + monkeypatch.setattr(lab, "clock_preflight", lambda runner, guard: {"passed": True}) + monkeypatch.setattr(lab, "disable_clock_outputs", lambda runner: []) + with pytest.raises(RuntimeError): + lab.preflight_only(tmp_path, runner=lambda _: None, process_checker=lambda: next(states)) + report = json.loads((tmp_path / "common_clock_preflight.json").read_text(encoding="utf-8")) + assert not report["passed"] + assert report["process_check_after_cleanup"]["state"] == "UNKNOWN" diff --git a/tests/test_lab054_hardware_control.py b/tests/test_lab054_hardware_control.py new file mode 100644 index 0000000..0aab574 --- /dev/null +++ b/tests/test_lab054_hardware_control.py @@ -0,0 +1,140 @@ +"""Lab054: фиксированные RF-команды и отказы runner без оборудования.""" + +from __future__ import annotations + +import json +from pathlib import Path +import subprocess + +import pytest + +from experiments import lab054_hardware_control as lab +from tools.hackrf_process_check import ProcessCheck, ProcessState + + +@pytest.mark.parametrize("failure", [None, "master", "slave", "clkin", "receive"]) +def test_clock_gates_never_transmit(failure): + commands = [] + + def runner(command): + commands.append(command) + output, code = "", 0 + if "--si5351c" in command: + serial = command[command.index("-d") + 1] + bad = ((failure == "master" and serial == lab.clock.MASTER_SERIAL) + or (failure == "slave" and serial == lab.clock.SLAVE_SERIAL)) + output = "0x0e" if bad else "0x26" + elif "-i" in command: + output = "UNKNOWN" if failure == "clkin" else "CLKIN status: clock signal detected" + elif "-r" in command: + code = int(failure == "receive") + return subprocess.CompletedProcess(command, code, output, "") + + guard = lab.clock.TransmitGuard() + if failure: + with pytest.raises(RuntimeError): + lab.clock_preflight(runner, guard, sleep_function=lambda _: None) + assert not guard.tx_allowed + else: + assert lab.clock_preflight(runner, guard, sleep_function=lambda _: None)["passed"] + assert guard.tx_allowed + assert not any("-t" in command for command in commands) + for command in commands: + if Path(command[0]).name == "hackrf_transfer.exe": + assert command[command.index("-f") + 1] == "1400000000" + assert command[command.index("-l") + 1] == command[command.index("-g") + 1] == "24" + + +def test_authorization_precedes_all_side_effects(tmp_path): + with pytest.raises(PermissionError): + lab.capture_once(tmp_path, runner=lambda _: pytest.fail("hardware")) + assert list(tmp_path.iterdir()) == [] + + +@pytest.mark.parametrize("failure", [None, "clock", "early_rx", "tx", "cleanup"]) +def test_one_shot_order_and_no_retry(tmp_path, monkeypatch, failure): + events = [] + monkeypatch.setattr(lab, "validate_prepared", lambda directory: {"tx_samples": 123}) + monkeypatch.setattr(lab.phy.lab045, "_verify_device_inventory", lambda runner: {}) + + def preflight(runner, guard): + if failure == "clock": + raise RuntimeError("clock") + guard.allow() + return {"passed": True} + + monkeypatch.setattr(lab, "clock_preflight", preflight) + monkeypatch.setattr(lab, "disable_clock_outputs", lambda runner: events.append("clocks_off") or []) + + class Process: + def poll(self): + return 1 if failure == "early_rx" else None + + def wait(self, timeout): + with (tmp_path / "rx_stream.cs8").open("wb") as stream: + stream.truncate(lab.RX_SAMPLES * 2) + return 0 + + def close_logs(self): + pass + + def terminate(self): + events.append("terminate") + if failure == "cleanup": + raise RuntimeError("cleanup") + + def starter(command, stdout, stderr): + events.append("rx") + stdout.write_text("") + stderr.write_text("") + return Process() + + def runner(command): + assert "-t" in command and "-R" not in command + events.append("tx") + return subprocess.CompletedProcess(command, int(failure == "tx"), "", "") + + kwargs = dict(authorization=True, runner=runner, process_starter=starter, + process_checker=lambda: ProcessCheck(ProcessState.CLEAR, ()), + sleep_function=lambda _: events.append("lead")) + if failure: + with pytest.raises(RuntimeError): + lab.capture_once(tmp_path, **kwargs) + else: + assert lab.capture_once(tmp_path, **kwargs)["passed"] + assert events.count("tx") == (0 if failure in ("clock", "early_rx") else 1) + if "tx" in events: + assert events.index("rx") < events.index("lead") < events.index("tx") + assert "clocks_off" in events + assert json.loads((tmp_path / "capture_result.json").read_text())["passed"] == (failure is None) + before = events.copy() + with pytest.raises(FileExistsError): + lab.capture_once(tmp_path, **kwargs) + assert events == before + + +def test_clock_cleanup_continues_after_timeout(): + commands = [] + + def runner(command): + commands.append(command) + raise subprocess.TimeoutExpired(command, 1) + + errors = lab.disable_clock_outputs(runner) + assert len(commands) == len(errors) == 2 + assert [command[2] for command in commands] == [lab.clock.MASTER_SERIAL, lab.clock.SLAVE_SERIAL] + + +def test_payload_match_does_not_hide_changed_packet(): + from types import SimpleNamespace + + plan = lab.plans()[0].plan + packet = bytes([plan.packet[0] ^ 1]) + plan.packet[1:] + reception = SimpleNamespace(payload_bytes=plan.payload, packet_bytes=packet) + frame = SimpleNamespace(reception=reception, + post_preamble_soft=lab.phy.lab045.modulate_bits_to_ideal_soft( + __import__("numpy").concatenate((plan.phy_header_bits, plan.interleaved_bits)))) + result = lab.compare_after_verdict([(None, SimpleNamespace(accepted=True, frame=frame))]) + assert result["payload_matched_packets"] == 1 + assert result["exact_packets"] == result["unique_exact_packets"] == 0 + assert result["post_fec_errors"] == 1 diff --git a/tests/test_lab056_preparation.py b/tests/test_lab056_preparation.py new file mode 100644 index 0000000..f5fed09 --- /dev/null +++ b/tests/test_lab056_preparation.py @@ -0,0 +1,248 @@ +"""Lab056: цифровые условия и отказы runner без оборудования.""" + +from __future__ import annotations + +import json +from dataclasses import asdict +from pathlib import Path +import subprocess + +import numpy as np +import pytest + +from experiments import lab056_abab_energy as lab +from experiments import lab056_capture_runner as capture +from tools.hackrf_process_check import ProcessCheck, ProcessState + + +def point(): + # Только синтетическая конфигурация теста; это не разрешение на эфир. + return capture.OperatingPoint(915000000, physical_path="SIMULATED", path_confirmed=True) + + +def test_no_authorization_has_no_side_effects(tmp_path): + with pytest.raises(PermissionError): + capture.capture_once(tmp_path, point(), runner=lambda _: pytest.fail("hardware")) + assert list(tmp_path.iterdir()) == [] + + +@pytest.mark.parametrize("configuration", [capture.OperatingPoint(), + capture.OperatingPoint(0), capture.OperatingPoint(True), capture.OperatingPoint(915e6)]) +def test_missing_or_invalid_frequency_precedes_io(tmp_path, configuration): + with pytest.raises(PermissionError): + capture.capture_once(tmp_path, configuration, authorization=True) + with pytest.raises(PermissionError): + capture.preflight_only(tmp_path, configuration) + assert list(tmp_path.iterdir()) == [] + + +def test_fixed_gain_and_finite_nonrepeating_commands(): + rx = capture.command(point(), "rx.cs8", 24000000) + tx = capture.command(point(), "tx.cs8", 1000000, transmit=True) + assert rx[rx.index("-l") + 1] == rx[rx.index("-g") + 1] == "24" + assert tx[tx.index("-x") + 1] == "10" + for command in (rx, tx): + assert "-R" not in command + assert command[command.index("-a") + 1] == command[command.index("-p") + 1] == "0" + assert command[command.index("-f") + 1] == "915000000" + assert command[command.index("-s") + 1] == "8000000" + + +@pytest.mark.parametrize("failure", [None, "master", "slave", "clkin", "receive"]) +def test_common_clock_logic_uses_agreed_frequency(failure): + commands = [] + def runner(command): + commands.append(command) + output, code = "", 0 + if "--si5351c" in command: + serial = command[command.index("-d") + 1] + bad = ((failure == "master" and serial == capture.previous.clock.MASTER_SERIAL) or + (failure == "slave" and serial == capture.previous.clock.SLAVE_SERIAL)) + output = "0x0e" if bad else "0x26" + elif "-i" in command: + output = "UNKNOWN" if failure == "clkin" else "CLKIN status: clock signal detected" + elif "-r" in command: + code = int(failure == "receive") + assert command[command.index("-f") + 1] == "915000000" + return subprocess.CompletedProcess(command, code, output, "") + guard = capture.previous.clock.TransmitGuard() + if failure: + with pytest.raises(RuntimeError): + capture.common_clock(point(), runner, guard, sleep_function=lambda _: None) + assert not guard.tx_allowed + else: + assert capture.common_clock(point(), runner, guard, sleep_function=lambda _: None)["passed"] + assert not any("-t" in command for command in commands) + + +@pytest.mark.parametrize("failure", [None, "busy", "clock", "early_rx", "tx", "counters", "cleanup", "rx_role_loss", "tx_role_loss", "clipping_before", "clipping_during"]) +def test_one_shot_rx_first_and_failures(tmp_path, monkeypatch, failure): + events = [] + monkeypatch.setattr(capture, "validated_metadata", lambda _: {"tx_sha256": "TEST", "tx_samples": 200}) + monkeypatch.setattr(capture.previous.phy.lab045, "_verify_device_inventory", lambda _: {"passed": True}) + monkeypatch.setattr(lab, "RX_SAMPLES", 65536) + approved = {"passed": True, "rf_tx_count": 0, "point": asdict(point()), "tx_sha256": "TEST", + "clock": {"passed": True, "master_reg29": 38, "slave_reg29": 38, "slave_clkin_status": "DETECTED"}, + "rx_level": {"saturation_detected": False}} + lab.save_new(tmp_path / "preflight_result.json", approved) + def clock_checker(point, runner, guard, **kwargs): + events.append("clock") + if failure == "clock": + raise RuntimeError("clock") + guard.allow() + return {"passed": True, "master_reg29": 38, "slave_reg29": 38, "slave_clkin_status": "DETECTED"} + class Process: + def poll(self): + return 1 if failure == "early_rx" else None + + def wait(self, timeout): + (tmp_path / "rx_stream.cs8").write_bytes(bytes([127 if failure == "clipping_during" else 1]) * 131072) + return 0 + + def close_logs(self): + pass + + def terminate(self): + events.append("terminate") + if failure == "cleanup": + raise RuntimeError("cleanup") + def starter(command, stdout, stderr): + events.append("rx") + (tmp_path / "rx_stream.cs8").write_bytes(bytes([127 if failure == "clipping_before" else 1]) * 131072) + stdout.write_text("") + stderr.write_text("1 RX overruns\n0 overruns" if failure == "rx_role_loss" else "0 overruns") + return Process() + def runner(command): + if "-t" in command: + events.append("tx") + log = "" if failure == "counters" else ("1 TX underruns\n0 underruns" if failure == "tx_role_loss" else "0 underruns") + return subprocess.CompletedProcess(command, int(failure == "tx"), "", log) + events.append("cleanup") + return subprocess.CompletedProcess(command, 0, "", "") + kwargs = dict(authorization=True, runner=runner, process_starter=starter, + process_checker=lambda: ProcessCheck(ProcessState.BUSY if failure == "busy" else ProcessState.CLEAR, ()), + clock_checker=clock_checker, sleep_function=lambda _: events.append("lead")) + if failure: + with pytest.raises(RuntimeError): + capture.capture_once(tmp_path, point(), **kwargs) + else: + assert capture.capture_once(tmp_path, point(), **kwargs)["passed"] + assert events.count("tx") == (0 if failure in ("busy", "clock", "early_rx", "clipping_before") else 1) + if "tx" in events: + assert events.index("rx") < events.index("lead") < events.index("tx") + before = events.copy() + with pytest.raises(FileExistsError): + capture.capture_once(tmp_path, point(), **kwargs) + assert events == before + result = json.loads((tmp_path / "capture_result.json").read_text()) + assert result["passed"] == (failure is None) + assert result["rf_tx_count"] == events.count("tx") + + +@pytest.mark.parametrize("failure", [None, "clock", "clipping", "tx_attempt", "cleanup"]) +def test_rx_only_preflight_stops_before_tx(tmp_path, monkeypatch, failure): + calls = [] + monkeypatch.setattr(capture, "validated_metadata", lambda _: {"tx_sha256": "TEST"}) + monkeypatch.setattr(capture.previous.phy.lab045, "_verify_device_inventory", lambda _: {"passed": True}) + def runner(command): + calls.append(command) + assert "-t" not in command + if "-r" in command: + Path(command[command.index("-r") + 1]).write_bytes(bytes([127 if failure == "clipping" else 1]) * 1600000) + if failure == "cleanup": + raise RuntimeError("cleanup") + return subprocess.CompletedProcess(command, 0, "0 RX overruns", "") + def clock_checker(point, runner, guard, **kwargs): + if failure == "tx_attempt": + runner(["hackrf_transfer", "-t", "forbidden"]) + return {"passed": True, "master_reg29": 14 if failure == "clock" else 38, + "slave_reg29": 38, "slave_clkin_status": "DETECTED"} + kwargs = dict(runner=runner, clock_checker=clock_checker, + process_checker=lambda: ProcessCheck(ProcessState.CLEAR, ())) + if failure: + with pytest.raises((RuntimeError, PermissionError)): + capture.preflight_only(tmp_path, point(), **kwargs) + else: + assert capture.preflight_only(tmp_path, point(), **kwargs)["passed"] + result = json.loads((tmp_path / "preflight_result.json").read_text()) + assert result["rf_tx_count"] == 0 + assert result["passed"] == (failure is None) + assert not (tmp_path / "hardware_attempt.json").exists() + with pytest.raises(FileExistsError): + capture.preflight_only(tmp_path, point(), **kwargs) + + +def test_preflight_cannot_authorize_another_frequency(tmp_path): + approved = {"passed": True, "rf_tx_count": 0, "point": asdict(point()), "tx_sha256": "TEST", + "clock": {"passed": True, "master_reg29": 38, "slave_reg29": 38, "slave_clkin_status": "DETECTED"}, + "rx_level": {"saturation_detected": False}} + lab.save_new(tmp_path / "preflight_result.json", approved) + capture.require_preflight(tmp_path, point(), {"tx_sha256": "TEST"}) + with pytest.raises(PermissionError): + capture.require_preflight(tmp_path, capture.OperatingPoint(916000000), {"tx_sha256": "TEST"}) + + +def test_external_frequency_needs_no_country_or_band_fields(): + configuration = capture.OperatingPoint(916000000) + command = capture.command(configuration, "NUL", 65536) + assert command[command.index("-f") + 1] == "916000000" + + +def test_complex_gain_and_repeatability_are_not_rx_normalization(): + random_generator = np.random.default_rng(56) + reference = random_generator.normal(size=8000).astype(complex) + expected = .3 + .4j + received = 1 + 2j + expected * reference + gain = lab.complex_gain(reference, received, 0) + assert complex(gain["real"], gain["imag"]) == pytest.approx(expected) + rows = [] + for label, power in (("A1", 4), ("B1", 1), ("A2", 8), ("B2", 2)): + rows.append({"label": label, "background_power": 1, "raw_rx": {"power_code2": power + 1}, + "background_subtracted_signal_power": power, "channel_gain_power": power, + "measurement": {"heldout_signal_power": power}}) + result = lab.comparison_summary(rows) + assert result["received_power_difference_db"] == pytest.approx(-6.020599913) + assert result["energy_per_symbol_difference_db"] == pytest.approx(-20) + assert [r["channel_gain_difference_db"] for r in result["repeatability"]] == pytest.approx([3.010299957, 3.010299957]) + rows[-1]["background_subtracted_signal_power"] = None + assert lab.comparison_summary(rows)["received_power_difference_db"] is None + + +def test_nonpositive_excess_is_not_a_power_ratio(): + assert lab.difference_db(-1, 1) is None + assert lab.difference_db(None, 1) is None + assert lab.difference_db(.25, 1) == pytest.approx(-6.020599913) + + +def test_invalid_result_does_not_leave_partial_json(tmp_path): + path = tmp_path / "result.json" + with pytest.raises(ValueError): + lab.save_new(path, {"invalid": float("nan")}) + assert not path.exists() + lab.save_new(path, {"passed": True}) + with pytest.raises(FileExistsError): + lab.save_new(path, {"passed": False}) + assert json.loads(path.read_text())["passed"] is True + + +@pytest.mark.parametrize("log,expected,passed", [ + ("0 overruns", "overrun", True), ("0 RX overrun", "overrun", True), + ("0 TX underruns", "underrun", True), ("1 RX overruns\n0 overruns", "overrun", False), + ("1 TX underrun\n0 underruns", "underrun", False), + ("unknown overruns\n0 overruns", "overrun", False), + ("0 overruns\nunderrun detected", "overrun", False), + ("0.0 overruns", "overrun", False), ("-1 overruns", "overrun", False), + ("0 underruns", "overrun", False), ("", "overrun", False)]) +def test_all_loss_mentions_must_be_explicit_zero(log, expected, passed): + assert capture.clean_counter_log(log, expected) is passed + + +def test_deterministic_ab_rms_and_no_clipping(): + powers = [] + for rate in (20000, 500000): + raw = lab.build_burst(rate) + values = raw[::2].astype(float) + 1j * raw[1::2].astype(float) + assert not np.any((raw == -128) | (raw == 127)) + powers.append(float(np.mean(abs(values)**2))) + np.testing.assert_array_equal(raw, lab.build_burst(rate)) + assert abs(lab.difference_db(powers[1], powers[0])) < .01 diff --git a/tools/hackrf_process_check.py b/tools/hackrf_process_check.py new file mode 100644 index 0000000..5498e51 --- /dev/null +++ b/tools/hackrf_process_check.py @@ -0,0 +1,178 @@ +"""Проверка конфликтующих SDR-процессов без обращения к оборудованию.""" + +from __future__ import annotations + +import csv +from dataclasses import asdict, dataclass +from enum import Enum +import importlib +import io +import json +from pathlib import PureWindowsPath +import subprocess +from typing import Callable, Iterable + + +class ProcessState(str, Enum): + CLEAR = "CLEAR" + BUSY = "BUSY" + UNKNOWN = "UNKNOWN" + + +@dataclass(frozen=True) +class ProcessIdentity: + pid: int + name: str + + +@dataclass(frozen=True) +class ProcessScan: + state: ProcessState + mechanism: str + matches: tuple[ProcessIdentity, ...] = () + inspected: int = 0 + errors: tuple[str, ...] = () + + +@dataclass(frozen=True) +class ProcessCheck: + state: ProcessState + attempts: tuple[ProcessScan, ...] + + def to_dict(self) -> dict: + return asdict(self) + + +# Эти программы способны открыть SDR. Их наличие консервативно даёт BUSY, +# даже если конкретный экземпляр пока не открыл устройство. Это проверка имён, +# а не доказательство отсутствия произвольного переименованного USB-клиента. +SDR_PROCESS_NAMES = frozenset({ + "gqrx", "sdrangel", "sdrsharp", "sdrpp", "sdr++", "cubicsdr", "hdsdr", + "sdrconnect", "gnuradio-companion", "osmocom_fft", "osmocom_siggen", + "soapysdrserver", "soapyremote", "soapy_sdr_server", "urh", +}) + + +def is_conflicting(name: str) -> bool: + normalized = PureWindowsPath(name).name.casefold() + if normalized.endswith(".exe"): + normalized = normalized[:-4] + return normalized.startswith("hackrf_") or normalized in SDR_PROCESS_NAMES + + +def classify_snapshot(mechanism: str, rows: Iterable[ProcessIdentity], + errors: Iterable[str] = ()) -> ProcessScan: + """Пустое, неполное или неразобранное перечисление не означает CLEAR.""" + failures = list(errors) + matches = [] + count = 0 + try: + for row in rows: + if (not isinstance(row, ProcessIdentity) or type(row.pid) is not int or row.pid < 0 + or not isinstance(row.name, str) or not row.name.strip()): + failures.append("Некорректная запись процесса") + continue + count += 1 + if is_conflicting(row.name): + matches.append(row) + except Exception as error: + failures.append(f"{type(error).__name__}: {error}") + if count == 0: + failures.append("Не получено ни одного процесса") + state = ProcessState.BUSY if matches else (ProcessState.UNKNOWN if failures else ProcessState.CLEAR) + return ProcessScan(state, mechanism, tuple(matches), count, tuple(failures)) + + +def scan_psutil() -> ProcessScan: + """NoSuchProcess — обычное завершение; отказ доступа сохраняет UNKNOWN.""" + rows, errors = [], [] + try: + psutil = importlib.import_module("psutil") + clear_cache = getattr(psutil.process_iter, "cache_clear", None) + if clear_cache is not None: + clear_cache() + for process in psutil.process_iter(): + try: + rows.append(ProcessIdentity(process.pid, process.name())) + except psutil.NoSuchProcess: + continue + except Exception as error: + errors.append(f"pid={process.pid}: {type(error).__name__}: {error}") + except Exception as error: + errors.append(f"{type(error).__name__}: {error}") + return classify_snapshot("psutil", rows, errors) + + +def run_command(command: list[str]) -> subprocess.CompletedProcess: + return subprocess.run(command, capture_output=True, text=True, encoding="utf-8", + errors="strict", timeout=10, + creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0)) + + +def scan_powershell(runner: Callable = run_command) -> ProcessScan: + """Get-Process завершает перечисление целиком или возвращает ошибку.""" + script = ("$ErrorActionPreference='Stop'; " + "[Console]::OutputEncoding=[System.Text.UTF8Encoding]::new(); " + "try { $items=@(Get-Process -ErrorAction Stop | " + "Select-Object Id,ProcessName); " + "ConvertTo-Json -InputObject $items -Compress } " + "catch { [Console]::Error.WriteLine($_.Exception.Message); exit 1 }") + rows, errors = [], [] + try: + result = runner(["powershell.exe", "-NoLogo", "-NoProfile", "-NonInteractive", "-Command", script]) + if result.returncode != 0 or result.stderr.strip(): + raise RuntimeError(f"exit={result.returncode}: {result.stderr.strip()}") + data = json.loads(result.stdout) + if not isinstance(data, list): + raise ValueError("Ожидался полный JSON-массив процессов") + for row in data: + try: + rows.append(ProcessIdentity(row["Id"], row["ProcessName"])) + except Exception as error: + errors.append(f"{type(error).__name__}: {error}") + except Exception as error: + errors.append(f"{type(error).__name__}: {error}") + return classify_snapshot("powershell_get_process", rows, errors) + + +def scan_tasklist(runner: Callable = run_command) -> ProcessScan: + """Резервный полный CSV; текст ошибки или пустой вывод не равен CLEAR.""" + rows, errors = [], [] + try: + result = runner(["tasklist.exe", "/FO", "CSV", "/NH"]) + if result.returncode != 0 or result.stderr.strip(): + raise RuntimeError(f"exit={result.returncode}: {result.stderr.strip()}") + for row in csv.reader(io.StringIO(result.stdout), strict=True): + try: + if len(row) != 5: + raise ValueError("Некорректная строка tasklist CSV") + rows.append(ProcessIdentity(int(row[1]), row[0])) + except Exception as error: + errors.append(f"{type(error).__name__}: {error}") + except Exception as error: + errors.append(f"{type(error).__name__}: {error}") + return classify_snapshot("tasklist", rows, errors) + + +def check_processes(providers: Iterable[Callable[[], ProcessScan]] | None = None) -> ProcessCheck: + """UNKNOWN разрешает другой источник; только полноценный CLEAR открывает gate.""" + attempts = [] + sources = (scan_psutil, scan_powershell, scan_tasklist) if providers is None else providers + for provider in sources: + try: + scan = provider() + if not isinstance(scan, ProcessScan) or not isinstance(scan.state, ProcessState): + raise TypeError("Некорректный ответ проверки процессов") + except Exception as error: + scan = ProcessScan(ProcessState.UNKNOWN, getattr(provider, "__name__", "provider"), + errors=(f"{type(error).__name__}: {error}",)) + attempts.append(scan) + if scan.state in (ProcessState.CLEAR, ProcessState.BUSY): + return ProcessCheck(scan.state, tuple(attempts)) + return ProcessCheck(ProcessState.UNKNOWN, tuple(attempts)) + + +def require_clear(result: ProcessCheck) -> None: + if not isinstance(result, ProcessCheck) or result.state is not ProcessState.CLEAR: + detail = result.to_dict() if isinstance(result, ProcessCheck) else repr(result) + raise RuntimeError(f"Проверка процессов не дала CLEAR: {detail}")