Lab049: define ELRS control and SDR video architecture
This commit is contained in:
274
PROJECT_LOG.md
274
PROJECT_LOG.md
@@ -1367,3 +1367,277 @@ passed и 0 failed; `git diff --check` прошёл.
|
|||||||
|
|
||||||
STOP. Не переходить к Lab049, видео, QPSK, повышению символьной скорости или
|
STOP. Не переходить к Lab049, видео, QPSK, повышению символьной скорости или
|
||||||
новому RF-сеансу без отдельной постановки. `RF TX = 0`; HackRF не запускать.
|
новому RF-сеансу без отдельной постановки. `RF TX = 0`; HackRF не запускать.
|
||||||
|
|
||||||
|
# Запись 036
|
||||||
|
|
||||||
|
## Дата
|
||||||
|
|
||||||
|
3 сентября 2026 года
|
||||||
|
|
||||||
|
## Тема
|
||||||
|
|
||||||
|
Lab049: программное повышение символьной скорости BPSK с сохранением
|
||||||
|
приоритета управления.
|
||||||
|
|
||||||
|
## Выполнено
|
||||||
|
|
||||||
|
До изменения кода проверены Lab047--Lab048, зависимости PHY и решения
|
||||||
|
Lab033--Lab041. Подтверждено, что новый планировщик не нужен:
|
||||||
|
`protocol/priority_scheduler.py` уже задаёт непрерываемое обслуживание с
|
||||||
|
порядком `EMERGENCY > CONTROL > TELEMETRY > VIDEO`, а `LATEST_STATE` заменяет
|
||||||
|
устаревшие ожидающие состояния CONTROL и TELEMETRY. Политики Lab034--Lab036
|
||||||
|
сохраняют целостность видеокадров и допускают служебный трафик между пакетами.
|
||||||
|
|
||||||
|
Реализован отдельный программный параметрический адаптер Lab049, не меняющий
|
||||||
|
доказанный аппаратный тракт Lab047. При `Fs = 8 Мвыб/с` проверены 20, 50, 100
|
||||||
|
и дополнительный режим 200 ксимв/с, то есть ровно 400, 160, 80 и 40
|
||||||
|
выборок/символ. Режим 20 ксимв/с побайтно воспроизводит прежнюю CS8-форму
|
||||||
|
Lab045. BPSK, PRBS7, PHY-заголовок, свёрточный FEC `R=1/2, K=7, 171/133
|
||||||
|
octal`, перемежитель, CRC, packet format и fragment format не изменялись.
|
||||||
|
|
||||||
|
Во всех режимах использован сохранённый JPEG Lab047: 8724 байта, RGB
|
||||||
|
320 x 240, SHA-256
|
||||||
|
`e913dab9ac32795250880a4be67fbbbf89a4f96536842476ab591d1acaafc02a`,
|
||||||
|
36 физических кадров. Каждый режим прошёл полный frame-aligned IQ-тракт от
|
||||||
|
фрагментации и BPSK/RRC до независимого восстановления PHY-длины, FEC, CRC и
|
||||||
|
сборки объекта. Приёмник кадра не получает TX bits, payload, packet или
|
||||||
|
ожидаемое число кадров; диагностическое сравнение выполняется после CRC.
|
||||||
|
|
||||||
|
До полного прогона для всех скоростей одинаково зафиксирован шум
|
||||||
|
`coded-bit Eb/N0 = 7,0 dB`, семена `49000 + frame_index`. Sigma вычисляется
|
||||||
|
из фактической энергии кодированного BPSK-бита, поэтому корректно меняется с
|
||||||
|
samples/symbol и не подбирается по результату режима.
|
||||||
|
|
||||||
|
Идеальный и шумный каналы восстановили 36/36 кадров и точный JPEG во всех
|
||||||
|
четырёх режимах. В шумном канале получено 129/114/107/110 ошибок кодированных
|
||||||
|
бит до FEC для 20/50/100/200 ксимв/с; после FEC во всех режимах 0 ошибок,
|
||||||
|
CRC PASS/FAIL `36/0`, ошибочных байтов и битов JPEG `0/0`.
|
||||||
|
|
||||||
|
Длительность одного физического кадра равна 288,30/115,32/57,66/28,83 мс,
|
||||||
|
полного JPEG — 10,37880/4,15152/2,07576/1,03788 с. Полезная скорость объекта
|
||||||
|
равна 6,724/16,811/33,622/67,245 кбит/с: фактический выигрыш 1,0/2,5/5,0/10,0
|
||||||
|
раза, рассчитанный по неизменному кадру.
|
||||||
|
|
||||||
|
Шумный приём занял 7,14440/3,15468/1,92894/1,30565 с; отношение обработки к
|
||||||
|
длительности сигнала 0,68836/0,75989/0,92927/1,25799. Режимы 20, 50 и 100
|
||||||
|
ксимв/с успевают в реальном времени, 200 ксимв/с — нет. Поэтому 200 ксимв/с
|
||||||
|
является устойчиво декодируемым, но только частичным результатом.
|
||||||
|
|
||||||
|
Проверка существующего `LATEST_STATE` на постоянно заполненной VIDEO-очереди,
|
||||||
|
CONTROL каждые 50 мс, TELEMETRY каждые 100 мс и одной EMERGENCY подтвердила
|
||||||
|
порядок `VIDEO(active) -> EMERGENCY -> CONTROL -> TELEMETRY -> VIDEO`.
|
||||||
|
Начатый кадр не прерывается. Верхняя граница блокировки новой CONTROL или
|
||||||
|
EMERGENCY равна длительности кадра: 288,30/115,32/57,66/28,83 мс.
|
||||||
|
|
||||||
|
Для нагрузки планировщика каждое сообщение занимает целый неизменный
|
||||||
|
физический кадр Lab045. Прикладные CONTROL и EMERGENCY не дополняются внутри
|
||||||
|
`LinkPacket`: сохранены точные 36- и 34-байтные сообщения, проверено их
|
||||||
|
обратное декодирование. При CONTROL 20 сообщений/с и TELEMETRY 10 сообщений/с
|
||||||
|
требуется 30 физических кадров/с. Ёмкость режимов равна
|
||||||
|
3,469/8,672/17,343/34,686 кадра/с, поэтому остаток полезной скорости VIDEO
|
||||||
|
равен 0/0/0/9,085 кбит/с, или 0/0/0/0,1302 JPEG в секунду. Это программная
|
||||||
|
модель планирования, а не реализованная упаковка `LinkPacket` в физический
|
||||||
|
кадр.
|
||||||
|
|
||||||
|
Штатный `LATEST_STATE` заменяет ожидающее старое состояние, но сам по себе не
|
||||||
|
удаляет пакет по истечении `deadline_ms`. В нагрузочном сценарии при 50
|
||||||
|
ксимв/с один CONTROL начал передаваться через 100,760 мс при сроке 100 мс;
|
||||||
|
при 20 ксимв/с EMERGENCY ожидала 87,900 мс при сроке 50 мс. Конкретное
|
||||||
|
событие при 100 ксимв/с ожидало 30,240 мс, но худшая фаза 57,660 мс всё равно
|
||||||
|
нарушает 50-миллисекундный срок EMERGENCY.
|
||||||
|
|
||||||
|
Статусы всех режимов — `PARTIAL`. 20 и 50 ксимв/с нарушают требования по
|
||||||
|
блокировке и не оставляют VIDEO; 100 ксимв/с укладывает CONTROL в 100 мс, но
|
||||||
|
нарушает срок EMERGENCY и также не оставляет VIDEO; 200 ксимв/с исправляет
|
||||||
|
эти ограничения, но приёмник не успевает в реальном времени с отношением
|
||||||
|
1,25799. Для следующего отдельного аппаратного опыта только с PHY выбран
|
||||||
|
диагностический кандидат 100 ксимв/с и 80 выборок/символ. Для объединённой
|
||||||
|
работы он не рекомендован: `recommended_for_integrated_operation = false`,
|
||||||
|
полностью пригодного режима не найдено.
|
||||||
|
|
||||||
|
Созданы `experiments/lab049_bpsk_symbol_rate.py`,
|
||||||
|
`tests/test_lab049_bpsk_symbol_rate.py`, `docs/lab049_current_state.md` и
|
||||||
|
`data/processed/lab049/result.json`. Новый RF не выполнялся, `RF TX = 0`,
|
||||||
|
HackRF не запускался.
|
||||||
|
|
||||||
|
Целевые проверки Lab049 дали 17 passed. `python tools/quick_gate.py` прошёл:
|
||||||
|
564 теста ядра и 12 функциональных проверок. `python -m pytest tests/ -q` дал
|
||||||
|
564 passed и 0 failed; единственное предупреждение относится к запрету записи
|
||||||
|
в служебный `.pytest_cache`. `git diff --check` прошёл.
|
||||||
|
|
||||||
|
## Следующий этап
|
||||||
|
|
||||||
|
STOP. До объединённого режима отдельно решить просроченные команды, упаковку
|
||||||
|
служебных сообщений в физический кадр и перегрузку бюджета. Не выполнять
|
||||||
|
аппаратный Lab049, QPSK, реальное видео, новый RF или переход на 180 МГц без
|
||||||
|
отдельной постановки и явной авторизации.
|
||||||
|
|
||||||
|
# Запись 037
|
||||||
|
|
||||||
|
## Дата
|
||||||
|
|
||||||
|
8 сентября 2026 года.
|
||||||
|
|
||||||
|
## Тема
|
||||||
|
|
||||||
|
Lab049: независимый IQ-приём и исправление расчёта полной доставки команд.
|
||||||
|
|
||||||
|
## Выполнено
|
||||||
|
|
||||||
|
Проверен HEAD ac6cc792b896e34f7dd6d291955a0387fdafcf41 и обнаружен уже
|
||||||
|
существующий незакоммиченный вариант Lab049. Запись 036 сохранена.
|
||||||
|
Независимые verifier/adversary воспроизвели отказ исходного приёмника при
|
||||||
|
неизвестном начале +240 выборок и CFO +10 Гц на 100 ксимв/с, а также ошибочный
|
||||||
|
расчёт аварийного срока по началу вместо конца передачи.
|
||||||
|
|
||||||
|
Основной IQ-проход использует блочный поиск и строгий декодер Lab048,
|
||||||
|
lab046c_offline_recovery.stream_process_capture. Адаптер читает синтетический
|
||||||
|
complex64 без отсечения шума, временно задаёт Rs/SPS и возвращает константы
|
||||||
|
и функции через finally. TX offsets, объект и число кадров в RX не передаются.
|
||||||
|
Порог Lab047 0.030964473262429237, PRBS7, BPSK, PHY, FEC, CRC и форматы сохранены.
|
||||||
|
Дробные скорости и нецелое samples/symbol отвергаются без округления.
|
||||||
|
|
||||||
|
При Fs=8 Мвыб/с проверен тот же JPEG 8724 байта, RGB 320×240,
|
||||||
|
SHA e913dab9ac32795250880a4be67fbbbf89a4f96536842476ab591d1acaafc02a,
|
||||||
|
в режимах 20/50/100/200 ксимв/с. Идеальный канал восстановил 36/36 и точный JPEG
|
||||||
|
во всех режимах. Шум един: coded-bit Eb/N0=7 dB, фиксированные seeds,
|
||||||
|
остаточный CFO +10 Гц, фаза 0,73 рад и чередование полярности.
|
||||||
|
|
||||||
|
В шуме 20 ксимв/с восстановил только 6/36 кадров, JPEG не собран — FAIL.
|
||||||
|
50/100/200 восстановили 36/36, CRC PASS/FAIL 36/0, SHA match, ошибки JPEG 0/0.
|
||||||
|
До FEC на принятых кадрах 67/147/119/125 ошибок, после FEC — 0; первая пара
|
||||||
|
относится только к шести кадрам. Все восемь проходов дошли до конца IQ.
|
||||||
|
|
||||||
|
Полная обработка шумного IQ заняла 173,65057 / 5,72980 / 7,76244 / 8,17573 с,
|
||||||
|
при длительности IQ 10,458018 / 4,230738 / 2,154978 / 1,117098 с.
|
||||||
|
Отношения 16,60454 / 1,35433 / 3,60210 / 7,31872: ни один режим не успевает
|
||||||
|
в реальном времени. Меньшие времена записи 036 описывали заранее выровненные
|
||||||
|
кадры и не переносятся на полный независимый тракт. 50/100/200 — PARTIAL.
|
||||||
|
|
||||||
|
Переиспользован LATEST_STATE и сохранены форматы CONTROL/EMERGENCY.
|
||||||
|
Отдельно считаются ожидание начала, полная доставка, активное окно 2 с
|
||||||
|
и последующее опустошение очереди. На 100 ксимв/с максимум доставки CONTROL
|
||||||
|
105,64 мс и EMERGENCY 87,90 мс; превышений сроков соответственно 6 и 1.
|
||||||
|
На 20/50/100 внутри активного окна не доставлен ни один TELEMETRY.
|
||||||
|
На 200 худшая доставка EMERGENCY равна почти 57,66 мс, а не 28,83 мс:
|
||||||
|
к блокировке добавляется передача самой команды. Срок 50 мс нарушается.
|
||||||
|
|
||||||
|
В модели одного полного PHY-кадра на пакет CONTROL 20 Гц и TELEMETRY 10 Гц
|
||||||
|
перегружают 20/50/100 ксимв/с. На 200 остаются 9,085 кбит/с VIDEO, около
|
||||||
|
0,1302 JPEG/с; с одной EMERGENCY за 2 с — 8,116 кбит/с. Аппаратная упаковка
|
||||||
|
служебных LinkPacket в PHY не реализована и этой моделью не доказывается.
|
||||||
|
|
||||||
|
Для отдельного будущего диагностического PHY-опыта выбран 50 ксимв/с:
|
||||||
|
наименьшая вычислительная перегрузка среди точных шумных проходов. Это не
|
||||||
|
пригодный объединённый канал и не разрешение RF. Старый JSON сохранён как
|
||||||
|
data/processed/lab049/frame_aligned_result.json; актуальный result.json
|
||||||
|
и docs/lab049_current_state.md содержат отрицательные результаты.
|
||||||
|
Добавлены docs/lab049_audit.md, experiments/lab049_independent_iq.py и
|
||||||
|
семь регрессий tests/test_lab049_independent_iq.py. Старые тесты не изменялись.
|
||||||
|
Обновлён docs/LABS_INDEX.md. Модули protocol не изменялись.
|
||||||
|
|
||||||
|
Verifier: 24 целевых теста PASS, независимый пересчёт JSON совпал.
|
||||||
|
Adversary подтвердил корректный отрицательный аварийный вердикт.
|
||||||
|
quick_gate PASS: 571 тест ядра, 12 функциональных проверок.
|
||||||
|
pytest: 571 passed, 0 failed; одно предупреждение служебного .pytest_cache.
|
||||||
|
git diff --check PASS. RF TX=0. Git add/commit/push/pull/fetch не выполнялись.
|
||||||
|
|
||||||
|
Следующий этап: STOP; отдельная постановка для вычислительной перегрузки,
|
||||||
|
бюджета и задержек перед аппаратной проверкой. Не запускать RF, QPSK,
|
||||||
|
реальный видеопоток, новый FEC или 180 МГц в рамках этой работы.
|
||||||
|
|
||||||
|
# Запись 038
|
||||||
|
|
||||||
|
## Дата
|
||||||
|
|
||||||
|
8 сентября 2026 года.
|
||||||
|
|
||||||
|
## Тема
|
||||||
|
|
||||||
|
Lab049: архитектура CRSF/ExpressLRS и бюджет отдельного SDR-видеоканала.
|
||||||
|
|
||||||
|
## Выполнено
|
||||||
|
|
||||||
|
Повторно проверены main и HEAD ac6cc792b896e34f7dd6d291955a0387fdafcf41.
|
||||||
|
Существовавшие незакоммиченные Lab049, тесты, данные и записи 036–037 сохранены.
|
||||||
|
Аудит README, журнала, индекса, Lab033–041, Lab047–048 и protocol подтвердил:
|
||||||
|
общий EMERGENCY > CONTROL > TELEMETRY > VIDEO создавался для собственного
|
||||||
|
единого ресурса. Его CONTROL 20 Гц и TELEMETRY 10 Гц в полных видеокадрах
|
||||||
|
остаются историческим стресс-тестом, не целевой моделью CRSF/ELRS.
|
||||||
|
|
||||||
|
По официальному ExpressLRS 4.1.0, коммиту
|
||||||
|
a9d4a9cb5b5687c4c9d7e9e7fbdf44ad93651da6 и спецификации TBS проверены
|
||||||
|
CRSF framing, UART, OTA, интервалы, Full/повторные режимы и телеметрия.
|
||||||
|
CRSF RC: 26 байт по проводу; ELRS OTA4/OTA8: 8/13 байт, DATA: 5/10 байт.
|
||||||
|
Ссылки на источники и точные допущения размещены в
|
||||||
|
docs/lab049_crsf_elrs_architecture.md и crsf_elrs_budget.json.
|
||||||
|
|
||||||
|
Рекомендуемая архитектура: обычный RadioMaster, штатные ELRS TX/RX,
|
||||||
|
CRSF к контроллеру ровера для управления и критической телеметрии;
|
||||||
|
отдельный SDR для изображений и объёмных данных. Будущие примерно 180 МГц
|
||||||
|
относятся к SDR, не к стандартному ELRS. Лабораторная RF-база — 1400 МГц.
|
||||||
|
Модели аппаратуры и ELRS-диапазон не уточнены; 100 Full/8ch/1:8 — условный
|
||||||
|
кандидат сравнительной модели, не назначенная аппаратная конфигурация.
|
||||||
|
|
||||||
|
При 100 Full/1:8 расчёт даёт до 87,5 RC-возможностей/с, 12,5 TLM-слотов/с,
|
||||||
|
среднее ожидание RC 6,25 мс и верхнюю границу 20 мс только для идеального
|
||||||
|
расписания RC/TLM. Полная задержка стики→ровер не измерена. DATA после
|
||||||
|
LINK имеет потолок 116,667 байт/с до прочих расходов. Даже наиболее щедрый
|
||||||
|
предел 10000 байт/с для двух DATA-трактов ELRS ниже JPEG 3 кадра/с:
|
||||||
|
26172 байт/с. Обычная ELRS-телеметрия для видео недостаточна.
|
||||||
|
|
||||||
|
Свойства безопасности Lab038–041 сохранены. При потере ELRS живое видео
|
||||||
|
не разрешает движение. Требуются локальный таймер, аварийная защёлка,
|
||||||
|
безопасная загрузка и отдельное повторное разрешение движения.
|
||||||
|
Сопряжение не реализовано: RC CRSF не содержит session/boot/epoch/sequence
|
||||||
|
нашего протокола. Локальная нумерация UART не доказывает свежесть источника.
|
||||||
|
Порог ELRS TX для старого ввода пульта 1 с явно отмечен как незакрытое
|
||||||
|
ограничение относительно модельных таймеров 100/200 и 150/250 мс.
|
||||||
|
|
||||||
|
JPEG Lab047 сохранён: 8724 байта, RGB 320×240, SHA
|
||||||
|
e913dab9ac32795250880a4be67fbbbf89a4f96536842476ab591d1acaafc02a.
|
||||||
|
Фактические планы Lab046/Lab045 дают 36 кадров, 206064 BPSK-бита и
|
||||||
|
207576 символьных интервалов с защитными интервалами/фильтром.
|
||||||
|
Для 3/5/10 кадров/с JPEG payload требует 209,376/348,960/697,920 кбит/с,
|
||||||
|
нынешняя PHY — минимум 622,728/1037,880/2075,760 ксимв/с без запаса.
|
||||||
|
Управление ELRS из этого SDR-бюджета не вычитается.
|
||||||
|
|
||||||
|
20/50/100/200 ксимв/с дают в идеале 0,09635/0,24088/0,48175/0,96350 JPEG/с.
|
||||||
|
Существующие независимые шумные замеры переиспользованы по SHA, не повторены:
|
||||||
|
на 50/100/200 точный JPEG, но обработка занимает 5,72980/7,76244/8,17573 с,
|
||||||
|
отношение к IQ 1,35433/3,60210/7,31872. На 20 только 6/36 кадров.
|
||||||
|
Lab048 15 с → 13,966058 с остаётся фактом одной записи, не доказательством
|
||||||
|
реального времени при повышении скорости PHY.
|
||||||
|
|
||||||
|
Конечная цель — больше 3 кадров/с; 3 промежуточно, далее 5 и 10.
|
||||||
|
При 25 км/ч между изображениями соответственно 2,315/1,389/0,694 м.
|
||||||
|
Рассчитана полная таблица для 10/15/25 км/ч и 1/3/5/10 кадров/с.
|
||||||
|
Предел задержки изображения в один период (333,33/200/100 мс) обозначен
|
||||||
|
как инженерное предложение, не измеренный или утверждённый норматив.
|
||||||
|
Камера, живое кодирование, очередь и экран пока не измерены; RX включает
|
||||||
|
JPEG decode, поэтому оно не прибавляется повторно. Кодек не менялся.
|
||||||
|
|
||||||
|
Добавлены experiments/lab049_crsf_elrs_budget.py, 35 тестов в
|
||||||
|
tests/test_lab049_crsf_elrs_budget.py и отдельный машинный результат
|
||||||
|
data/processed/lab049/crsf_elrs_budget.json. Обновлены документы состояния,
|
||||||
|
индекс и краткий указатель актуальной архитектуры в README.
|
||||||
|
Старые protocol tests и модули protocol не изменены.
|
||||||
|
|
||||||
|
Verifier независимо пересчитал расходы PHY, RC-ожидание перечислением
|
||||||
|
интервалов, LINK/DATA и SHA исходных замеров; 35 целевых тестов PASS.
|
||||||
|
Adversary: 7 проверок сеансов PASS; при живом видео без команд watchdog
|
||||||
|
переходит в ступени 100/200 мс. Искусственная нумерация старого состояния
|
||||||
|
подтверждает описанное ограничение свежести, не готовность bridge.
|
||||||
|
Воспроизведённых расхождений в новой работе не обнаружено.
|
||||||
|
|
||||||
|
quick_gate PASS: 606 тестов и 13 функциональных проверок.
|
||||||
|
pytest: 606 passed за 64,32 с; одно предупреждение о доступе к .pytest_cache.
|
||||||
|
git diff --check PASS. Дополнительный ruff не выполнен: модуль не установлен.
|
||||||
|
RF TX = 0; оборудование не запускалось и не инвентаризировалось.
|
||||||
|
Git add/commit/push/pull/fetch не выполнялись.
|
||||||
|
|
||||||
|
Следующий этап: STOP. Рекомендуемый Lab050 — отдельная программная задача
|
||||||
|
профилирования/потоковой обработки SDR-видео с ограничением возраста очереди
|
||||||
|
и проверкой выполнимости 5 кадров/с. Сопряжение CRSF и его сценарии отказов
|
||||||
|
требуют отдельной постановки и уточнения аппаратуры. Нового RF, QPSK,
|
||||||
|
CRSF/ELRS-протокола, перекодирования JPEG или переноса на 180 МГц здесь нет.
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
# SDR Rover Link
|
# SDR Rover Link
|
||||||
|
|
||||||
|
Актуальное уточнение архитектуры, Lab049 от 8 сентября 2026 года:
|
||||||
|
управление и критическая телеметрия — стандартные RadioMaster/ExpressLRS и CRSF;
|
||||||
|
изображения и объёмные данные — отдельный SDR, в перспективе около 180 МГц.
|
||||||
|
Цель изображения — больше 3 кадров/с, следующий ориентир 5, дальнейший 10.
|
||||||
|
См. [аудит и бюджет Lab049](docs/lab049_crsf_elrs_architecture.md).
|
||||||
|
Основной текст ниже описывает более раннюю контрольную точку; текущие результаты
|
||||||
|
Lab047–049 перечислены в [индексе лабораторий](docs/LABS_INDEX.md).
|
||||||
|
|
||||||
## 1. Назначение проекта
|
## 1. Назначение проекта
|
||||||
|
|
||||||
**SDR Rover Link** — учебно-исследовательский проект по разработке цифрового радиоканала для наземного робототехнического комплекса с использованием программно-определяемого радио — SDR, Software Defined Radio.
|
**SDR Rover Link** — учебно-исследовательский проект по разработке цифрового радиоканала для наземного робототехнического комплекса с использованием программно-определяемого радио — SDR, Software Defined Radio.
|
||||||
|
|||||||
@@ -200,6 +200,46 @@
|
|||||||
- **RF:** новый радиосеанс не выполнялся, `RF TX = 0`.
|
- **RF:** новый радиосеанс не выполнялся, `RF TX = 0`.
|
||||||
- **Журнал:** запись 035 ([`PROJECT_LOG.md`](../PROJECT_LOG.md)).
|
- **Журнал:** запись 035 ([`PROJECT_LOG.md`](../PROJECT_LOG.md)).
|
||||||
|
|
||||||
|
## Lab049
|
||||||
|
|
||||||
|
- **Актуальная архитектура:** RadioMaster + штатные ELRS TX/RX, CRSF к контроллеру
|
||||||
|
ровера; независимый SDR для видео и объёмных данных. Будущий SDR около 180 МГц,
|
||||||
|
лабораторная база 1400 МГц. Это не перенос штатного ELRS на 180 МГц.
|
||||||
|
- **Аудит CRSF/ELRS:** официальный релиз 4.1.0, размеры OTA 8/13 байт,
|
||||||
|
реальные интервалы и доли телеметрии; ELRS недостаточно для JPEG >3 кадра/с.
|
||||||
|
- **Бюджет SDR:** RC/критическая телеметрия в отдельном ELRS не вычитаются.
|
||||||
|
20/50/100/200 ксимв/с дают в идеале 0,09635 / 0,24088 / 0,48175 / 0,96350 JPEG/с.
|
||||||
|
Для 3/5/10 требуются минимум 622,728 / 1037,880 / 2075,760 ксимв/с
|
||||||
|
по фактическому текущему PHY. 3 — промежуточный уровень; цель >3, ориентиры 5 и 10.
|
||||||
|
- **Задача предыдущего опыта:** 20/50/100/200 ксимв/с при Fs=8 Мвыб/с,
|
||||||
|
полный независимый IQ-приём и исторический стресс-тест общего канала.
|
||||||
|
- **Актуальный результат:** идеал — 36/36 во всех режимах. Шум Eb/N0=7 dB, CFO +10 Гц:
|
||||||
|
20 — 6/36, FAIL; 50/100/200 — точный JPEG 36/36, CRC 36/0, PARTIAL.
|
||||||
|
- **Полный приёмник Lab048:** processing/IQ 16,60454 / 1,35433 / 3,60210 / 7,31872;
|
||||||
|
ни один режим не успевает в реальном времени в шумном сценарии.
|
||||||
|
- **Исторические задержки общего канала:** блокировка 288,30 / 115,32 / 57,66 / 28,83 мс. Доставка EMERGENCY
|
||||||
|
в худшей фазе включает ещё один кадр; даже на 200 она требует почти 57,66 мс.
|
||||||
|
- **Исторический стресс-тест:** CONTROL 20 Гц и TELEMETRY 10 Гц в 30 полных
|
||||||
|
кадрах/с оставляли VIDEO только на 200. Это не целевая CRSF/ELRS-архитектура.
|
||||||
|
- **Диагностический PHY-кандидат:** 50 ксимв/с, 160 выборок/символ —
|
||||||
|
наименьшая вычислительная перегрузка среди точных шумных проходов. RF не разрешён.
|
||||||
|
- **Код:** [расчётчик ELRS/SDR](../experiments/lab049_crsf_elrs_budget.py),
|
||||||
|
[Lab049 IQ](../experiments/lab049_bpsk_symbol_rate.py),
|
||||||
|
[независимый IQ-адаптер](../experiments/lab049_independent_iq.py).
|
||||||
|
- **Тесты:** [35 расчётных](../tests/test_lab049_crsf_elrs_budget.py),
|
||||||
|
[исходные](../tests/test_lab049_bpsk_symbol_rate.py),
|
||||||
|
[регрессии IQ](../tests/test_lab049_independent_iq.py).
|
||||||
|
- **Отчёт:** [архитектура CRSF/ELRS](lab049_crsf_elrs_architecture.md),
|
||||||
|
[новый бюджет JSON](../data/processed/lab049/crsf_elrs_budget.json),
|
||||||
|
[состояние](lab049_current_state.md), [аудит](lab049_audit.md),
|
||||||
|
[JSON](../data/processed/lab049/result.json).
|
||||||
|
- **История:** [старый frame-aligned результат](../data/processed/lab049/frame_aligned_result.json)
|
||||||
|
не измерял полный независимый поиск. Журнал: записи 036–038.
|
||||||
|
- **Безопасность:** свойства Lab038–041 сохраняются; сопряжение CRSF ещё не проверено.
|
||||||
|
Видео не разрешает движение при потере ELRS. Модули protocol и прежние тесты сохранены.
|
||||||
|
- **Далее:** программный Lab050 — профиль RX, поток видео и ограничение возраста очереди.
|
||||||
|
- **RF:** RF TX = 0, STOP.
|
||||||
|
|
||||||
## Правило хранения данных
|
## Правило хранения данных
|
||||||
|
|
||||||
Большие записи IQ, потоки `cs8`, бинарные объекты и другие аппаратные исходные
|
Большие записи IQ, потоки `cs8`, бинарные объекты и другие аппаратные исходные
|
||||||
|
|||||||
86
docs/lab049_audit.md
Normal file
86
docs/lab049_audit.md
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
# Lab049: аудит исходного незакоммиченного варианта
|
||||||
|
|
||||||
|
8 сентября 2026 года. Корень `C:/Users/user/Desktop/projects/SDR_Rover`,
|
||||||
|
ветка `main`, HEAD `ac6cc792b896e34f7dd6d291955a0387fdafcf41`.
|
||||||
|
До работы уже существовали незакоммиченные реализация Lab049, тесты,
|
||||||
|
`data/processed/lab049/result.json`, документ состояния и запись 036 журнала.
|
||||||
|
Прежний JSON сохранён; он относится к выровненным кадрам и не является
|
||||||
|
результатом полного независимого приёмника.
|
||||||
|
|
||||||
|
## Карта архитектуры
|
||||||
|
|
||||||
|
1. Базовый Rs задан в `experiments/lab043_hackrf_bpsk.py`: 20 000 символов/с.
|
||||||
|
Lab044 и Lab045 наследуют его, Lab047 явно проверяет аппаратную точку.
|
||||||
|
2. В Lab043 `SAMPLES_PER_SYMBOL = SAMPLE_RATE_HZ // SYMBOL_RATE`; Lab047
|
||||||
|
отдельно фиксирует 400. Lab049 теперь отвергает дробные частоты и
|
||||||
|
нецелое отношение, не округляя вход.
|
||||||
|
3. Жёсткие зависимости Lab043: `_refine_frame_start`, `_matched_filter_taps`,
|
||||||
|
`_matched_waveform`, `_preamble_timing_offset`, `_estimate_cfo_gain`,
|
||||||
|
`_evaluate_symbols`. Lab044: `_scale_and_carrier`,
|
||||||
|
`_preamble_reference_samples`. Lab045: длительности TransmissionPlan;
|
||||||
|
Lab046B: `_extract_soft_window` и `_decode_iq_candidate`. Временные шкалы
|
||||||
|
используют Rs, фильтры и индексы — samples/symbol. В локальном поиске
|
||||||
|
также есть фиксированные пределы ±800 и ±2000 выборок; они сохранены.
|
||||||
|
4. `shape_frame_like_lab042`, RRC-фильтр ядра, преобразования бит/BPSK,
|
||||||
|
фрагментация, packet/PHY header, FEC, перемежитель, CRC и сборка объекта
|
||||||
|
уже параметрические либо вообще не зависят от Rs.
|
||||||
|
5. `protocol/priority_scheduler.py`: строгий приоритет четырёх классов,
|
||||||
|
непрерываемая передача пакета и `LATEST_STATE` для ожидающих состояний.
|
||||||
|
Lab034–Lab036 добавили удаление устаревших изображений и изоляцию
|
||||||
|
видеокадров в собственных блоках FEC; это внешнее стирающее кодирование
|
||||||
|
видео, не новый свёрточный FEC Lab045.
|
||||||
|
6. Минимальная доработка: адаптер скорости и чтения синтетического IQ к
|
||||||
|
`lab046c_offline_recovery.stream_process_capture`, который используется
|
||||||
|
Lab048; разделение ожидания/доставки и активного окна/опустошения очереди.
|
||||||
|
Новый планировщик и изменения `protocol/` не потребовались.
|
||||||
|
|
||||||
|
## Проверка Lab033–Lab041
|
||||||
|
|
||||||
|
Сверены сохранённые отчёты этих лабораторий и связанные модули.
|
||||||
|
Lab033 подтверждает строгий приоритет и отдельно считает ожидание начала
|
||||||
|
и завершение передачи. Lab034: frame-aligned FEC изолирует удаляемое
|
||||||
|
изображение, начатый пакет не прерывается. Lab035–Lab036 ограничивают возраст
|
||||||
|
изображения и число ожидающих кадров. Lab037 подтверждает вставку управления
|
||||||
|
между пакетами VIDEO. Lab038–Lab040 добавляют failsafe, аварийную защёлку,
|
||||||
|
повторы и ACK; Lab041 — сеансы/перезапуски. Эти механизмы безопасности
|
||||||
|
в Lab049 не заменены и не заявлены как аппаратно проверенные.
|
||||||
|
|
||||||
|
CONTROL взят из последнего Lab041: `ControlStateMessage`, 36 байт, 20 Гц;
|
||||||
|
с 32-байтным LinkPacket — 68 байт и 10 880 бит/с до физического кодирования.
|
||||||
|
TELEMETRY наследуется из Lab037 через Lab038–Lab041: синтетические 64 байта,
|
||||||
|
10 Гц, с LinkPacket — 96 байт и 7680 бит/с. Отдельного формата физических
|
||||||
|
измерений телеметрии эти лаборатории не задают.
|
||||||
|
|
||||||
|
## Воспроизведённые несоответствия
|
||||||
|
|
||||||
|
Проверки выполнены независимыми `verifier` и `adversary`, без правок с их
|
||||||
|
стороны и без оборудования.
|
||||||
|
|
||||||
|
- Исходный `receive_iq_frame`: идеальный кадр 100 ксимв/с проходит CRC,
|
||||||
|
добавление 240 нулевых выборок перед ним даёт `PHY=False, CRC=NOT_REACHED`.
|
||||||
|
Поиск ограничивался двумя символами от известной TX-границы.
|
||||||
|
- Добавление +10 Гц к тому же кадру даёт `PHY=True, CRC=NOT_REACHED`;
|
||||||
|
+100 Гц — `PHY=False`. Остаточная частота не оценивалась.
|
||||||
|
- При 100 ксимв/с старый отчёт CONTROL max 47,980 мс описывал начало,
|
||||||
|
а получение пакета занимает 105,640 мс; пропущены 6 сроков CONTROL.
|
||||||
|
EMERGENCY: 30,240 мс до начала, 87,900 мс до получения — пропущен срок 50 мс.
|
||||||
|
- При 200 ксимв/с EMERGENCY через 1 мкс после VIDEO доставляется через
|
||||||
|
57,659 мс, а не за 50 мс. Штатный failsafe до получения пакета остаётся
|
||||||
|
в `normal`. Блокировка 28,83 мс и доставка почти 57,66 мс — разные метрики.
|
||||||
|
- На 20/50/100 ксимв/с в двухсекундном окне не доставлено ни одного
|
||||||
|
TELEMETRY. Единственное сообщение старой таблицы доставляется при
|
||||||
|
последующем опустошении очереди.
|
||||||
|
|
||||||
|
Регрессии воспроизводятся командой:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m pytest tests/test_lab049_independent_iq.py -q
|
||||||
|
```
|
||||||
|
|
||||||
|
Исторический `receive_iq_frame` сохранён для локальных регрессий формы;
|
||||||
|
основной проход Lab049 его больше не использует. Старые тесты не изменены.
|
||||||
|
Новый RX получает полный поток, самостоятельно находит начало, оценивает CFO,
|
||||||
|
фазу и полярность. PRBS7, физические и пакетные форматы, FEC, CRC и порог
|
||||||
|
Lab047 `0.030964473262429237` сохранены.
|
||||||
|
|
||||||
|
RF TX = 0. Git add/commit/push/pull/fetch не выполнялись.
|
||||||
450
docs/lab049_crsf_elrs_architecture.md
Normal file
450
docs/lab049_crsf_elrs_architecture.md
Normal file
@@ -0,0 +1,450 @@
|
|||||||
|
# Lab049: CRSF / ExpressLRS и отдельный SDR-видеоканал
|
||||||
|
|
||||||
|
Состояние на 8 сентября 2026 года. SOFTWARE + ARCHITECTURE ONLY, RF TX = 0.
|
||||||
|
Решение по результатам аудита: **сохранить стандартный ELRS для управления,
|
||||||
|
а SDR оптимизировать под изображения и объёмные данные**. Смешанная модель
|
||||||
|
CONTROL 20 Гц + TELEMETRY 10 Гц, каждый в полном видеокадре, остаётся
|
||||||
|
историческим стресс-тестом; она не определяет бюджет этой архитектуры.
|
||||||
|
|
||||||
|
## Источники и границы доказанного
|
||||||
|
|
||||||
|
Проверены официальный [последний релиз ExpressLRS 4.1.0](https://github.com/ExpressLRS/ExpressLRS/releases/tag/4.1.0)
|
||||||
|
от 17 июля 2026 года и его
|
||||||
|
[коммит a9d4a9cb5b5687c4c9d7e9e7fbdf44ad93651da6](https://github.com/ExpressLRS/ExpressLRS/commit/a9d4a9cb5b5687c4c9d7e9e7fbdf44ad93651da6).
|
||||||
|
Версия исходников не означает, что такая прошивка уже стоит у пользователя.
|
||||||
|
Ссылки ниже закреплены на этом коммите; страницы документации проверены в дату отчёта.
|
||||||
|
|
||||||
|
Локальная база: main, HEAD `ac6cc792b896e34f7dd6d291955a0387fdafcf41`.
|
||||||
|
До работы уже были изменены журнал и индекс, присутствовали незакоммиченные
|
||||||
|
Lab049, её тесты и результаты, а также сторонние локальные файлы. Они сохранены.
|
||||||
|
README описывает старую точку Lab042 и диапазон 200–250 МГц; текущая постановка
|
||||||
|
пользователя уточняет будущий SDR-видеоканал как примерно 180 МГц.
|
||||||
|
|
||||||
|
Физические модели RadioMaster, ELRS TX/RX и их диапазон пока не указаны.
|
||||||
|
Поэтому приведено сравнение, а не назначение конкретных настроек оборудования.
|
||||||
|
Инвентаризация и аппаратная проверка не выполнялись. Совместимость системы
|
||||||
|
ниже — архитектурная, сопряжение с контроллером ровера ещё не реализовано.
|
||||||
|
|
||||||
|
## Что уже есть в проекте
|
||||||
|
|
||||||
|
Аудит README, PROJECT_LOG, LABS_INDEX, Lab033–Lab041, Lab047–048 и связанных
|
||||||
|
модулей дополняет [предыдущий аудит Lab049](lab049_audit.md).
|
||||||
|
|
||||||
|
| Код / лаборатории | Для чего создавался | Что сохраняется при отдельном ELRS |
|
||||||
|
|---|---|---|
|
||||||
|
| `protocol/link_packet.py`, `priority_scheduler.py`; Lab033 | Один абстрактный непрерываемый ресурс для двух направлений, EMERGENCY > CONTROL > TELEMETRY > VIDEO, последнее ожидающее состояние | Историческая модель и регрессии; не размещаем её CONTROL/TELEMETRY в бюджете SDR |
|
||||||
|
| `video_frame_scheduler.py`, Lab034–036 | Удаление устаревшего видео, допуск целого изображения, ограничение очереди, FEC-блоки в пределах изображения | Нужны принципы возраста и ограничения очереди; сопряжение с нынешним JPEG/PHY ещё предстоит |
|
||||||
|
| Lab037, `control_repetition.py` | Повторы управления, потери и видео в одном канале, удаление дубликатов | Тестовые сценарии потерь; собственные повторы не накладываются автоматически поверх ELRS |
|
||||||
|
| Lab038, `control_failsafe.py`, `emergency_ack.py` | Таймер свежей команды, остановка, подтверждение аварии | Локальный контроль потери управления и идемпотентная аварийная остановка остаются обязательными |
|
||||||
|
| Lab039, `two_stage_failsafe.py` | Две ступени замедления/торможения, кинематика | Автомат и сценарии; тормозные параметры реального ровера пока не измерены |
|
||||||
|
| Lab040, `persistent_emergency.py` | Намерение остановить сохраняется после ACK; отдельный подтверждённый сброс | Смысл аварийной защёлки и запрет самовозобновления; перенос наземного автомата на обычный пульт не выполнен |
|
||||||
|
| Lab041, `control_messages.py`, `session_state.py` | Сеансы, загрузки, эпохи, номера команд, защита от старых ACK/reset | Сохранить свойства; CRSF не предоставляет эти поля автоматически |
|
||||||
|
| Lab047–048, Lab049 IQ | JPEG как двоичный объект, независимый PHY/FEC/CRC, производительность | Доказательная база отдельного SDR-видеоканала |
|
||||||
|
|
||||||
|
Синтетические 64 байта TELEMETRY из Lab037 и собственные сообщения CONTROL
|
||||||
|
Lab041 не являются форматами CRSF. Модули `protocol/` и старые тесты не менялись.
|
||||||
|
|
||||||
|
## Одна схема системы
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
subgraph RC[Управление и критическая телеметрия]
|
||||||
|
RM[RadioMaster: стики и переключатели] <-->|CRSF| ETX[Штатный ExpressLRS TX]
|
||||||
|
ETX <-->|ELRS OTA: поддерживаемый аппаратурой диапазон| ERX[Штатный ExpressLRS RX]
|
||||||
|
ERX <-->|CRSF UART| R[Контроллер ровера]
|
||||||
|
R --> S[Локальный таймер, аварийная защёлка, торможение]
|
||||||
|
S --> M[Приводы]
|
||||||
|
end
|
||||||
|
subgraph VIDEO[Независимый SDR video/data]
|
||||||
|
C[Камера] --> J[JPEG; будущий видеокодек]
|
||||||
|
J --> STX[SDR передатчик]
|
||||||
|
B[Объёмные данные ровера] --> STX
|
||||||
|
STX -->|Будущий RF около 180 МГц; лабораторная база 1400 МГц| SRX[SDR приёмник]
|
||||||
|
SRX --> D[Декодер изображения] --> O[Экран оператора]
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
При пропаже ELRS продолжающееся видео **не продлевает разрешение движения**.
|
||||||
|
Таймер, тормозной автомат и аварийная защёлка должны работать независимо от
|
||||||
|
очередей видео, загруженности декодера и доступности экрана.
|
||||||
|
Частота 180 МГц относится к будущему SDR-тракту, не к стандартному ELRS.
|
||||||
|
Это целевая архитектура; готовый двухканальный ровер пока не испытан.
|
||||||
|
|
||||||
|
## CRSF на проводных границах
|
||||||
|
|
||||||
|
По [спецификации TBS](https://github.com/tbs-fpv/tbs-crsf-spec/blob/main/crsf.md):
|
||||||
|
кадр содержит sync/address, длину, тип, данные и CRC8 (полином 0xD5).
|
||||||
|
Длина считает тип, данные и CRC, исключая первые два байта; полный кадр ≤64 байт.
|
||||||
|
Расширенный заголовок добавляет destination и origin.
|
||||||
|
|
||||||
|
| Тип | Содержание | Данные / полный кадр, байт |
|
||||||
|
|---|---|---:|
|
||||||
|
| 0x16 | 16 RC-каналов по 11 бит | 22 / 26 |
|
||||||
|
| 0x14 | Статистика связи | 10 / 14 |
|
||||||
|
| 0x08 | Батарея | 8 / 12 |
|
||||||
|
| 0x02 | GPS | 15 / 19 |
|
||||||
|
| 0x1E | Ориентация | 6 / 10 |
|
||||||
|
| 0x21 | Режим, строка с завершающим нулём | переменный |
|
||||||
|
| 0x28–0x2D, 0x32 | Обнаружение, параметры, команды/ACK | переменный, расширенный |
|
||||||
|
|
||||||
|
Пульт↔модуль: обычно однопроводный полудуплекс, 400 кбод, 8N1, 3,3 В;
|
||||||
|
приёмник↔контроллер: двухпроводный полный дуплекс, без инверсии,
|
||||||
|
в спецификации 416666 бод, 8N1, 3,0–3,3 В.
|
||||||
|
У ELRS штатная настройка UART приёмника — **420000 бод**:
|
||||||
|
[официальная Web UI](https://www.expresslrs.org/quick-start/webui/#receiver-options).
|
||||||
|
Настройки обеих сторон должны совпадать. Расчёт сериализации 26 байт при
|
||||||
|
420000 бод: 26×10/420000 = 0,619 мс; это только провод, не радиозадержка.
|
||||||
|
|
||||||
|
[SerialCRSF::sendRCFrame](https://github.com/ExpressLRS/ExpressLRS/blob/a9d4a9cb5b5687c4c9d7e9e7fbdf44ad93651da6/src/src/rx-serial/SerialCRSF.cpp)
|
||||||
|
упаковывает текущие каналы в 0x16 только при `frameAvailable`.
|
||||||
|
Это не сквозная пересылка исходных CRSF-байтов через эфир: ELRS сначала
|
||||||
|
кодирует OTA, затем RX заново формирует CRSF. Статистика и сообщения
|
||||||
|
контроллера идут отдельными кадрами. Не все 16 выходных полей обновляются
|
||||||
|
по радио одновременно; часть может содержать состояние, полученное ранее.
|
||||||
|
|
||||||
|
Для обычных стиков специальная прошивка RadioMaster не требуется: нужен
|
||||||
|
вариант с ELRS либо совместимый внешний ELRS-модуль и штатная настройка CRSF.
|
||||||
|
Вариант RadioMaster с другим внутренним RF-модулем сам по себе ELRS не даёт.
|
||||||
|
Нужен RX с доступным CRSF UART, общий поддерживаемый диапазон и совместимая
|
||||||
|
версия ELRS; конкретную пару ещё надо выбрать. См.
|
||||||
|
[настройку передатчика](https://www.expresslrs.org/quick-start/transmitters/lua-howto/).
|
||||||
|
Программное сопряжение CRSF с ровером остаётся будущей задачей.
|
||||||
|
|
||||||
|
## ELRS в эфире
|
||||||
|
|
||||||
|
[OTA.h](https://github.com/ExpressLRS/ExpressLRS/blob/a9d4a9cb5b5687c4c9d7e9e7fbdf44ad93651da6/src/lib/OTA/OTA.h)
|
||||||
|
задаёт OTA4 = **8 байт**, OTA8 = **13 байт**, включая служебные поля и CRC.
|
||||||
|
Это длина содержимого радиопакета ELRS, не весь физический расход LoRa/FLRC/GFSK.
|
||||||
|
RC-пакеты, DATA, SYNC и LINK имеют разные назначения; 26 байт CRSF не следует
|
||||||
|
подставлять вместо этих размеров. Поле DATA в
|
||||||
|
[telemetry_protocol.h](https://github.com/ExpressLRS/ExpressLRS/blob/a9d4a9cb5b5687c4c9d7e9e7fbdf44ad93651da6/src/include/telemetry_protocol.h)
|
||||||
|
содержит **5 либо 10 байт**, а не 8/13 байт пользовательской нагрузки.
|
||||||
|
|
||||||
|
Точные периоды и Std взяты из
|
||||||
|
[ExpressLRS_AirRateConfig](https://github.com/ExpressLRS/ExpressLRS/blob/a9d4a9cb5b5687c4c9d7e9e7fbdf44ad93651da6/src/src/common.cpp).
|
||||||
|
Таблица ниже рассчитана для установившегося соединения, без потерь,
|
||||||
|
без повторных режимов D/DK; RC означает возможности отправки, не измеренные обновления стиков.
|
||||||
|
|
||||||
|
| Режим | Доступность | OTA, байт | Период, мс | Std | TLM-слотов/с | RC-возможностей/с ≤ | Среднее / верхнее ожидание RC, мс |
|
||||||
|
|---|---|---:|---:|---:|---:|---:|---:|
|
||||||
|
| 25 | 900 | 8 | 40 | 1:8 | 3,125 | 21,875 | 25 / 80 |
|
||||||
|
| 50 | 900 и 2,4 | 8 | 20 | 1:16 | 3,125 | 46,875 | 11,250 / 40 |
|
||||||
|
| 100 | 900 | 8 | 10 | 1:32 | 3,125 | 96,875 | 5,313 / 20 |
|
||||||
|
| 100 Full | 900 и 2,4 | 13 | 10 | 1:32 | 3,125 | 96,875 | 5,313 / 20 |
|
||||||
|
| 150 | 2,4 | 8 | 6,666 | 1:32 | 4,688 | 145,327 | 3,541 / 13,332 |
|
||||||
|
| 200 | 900 | 8 | 5 | 1:64 | 3,125 | 196,875 | 2,578 / 10 |
|
||||||
|
| 250 | 2,4; 900 на LR1121 | 8 | 4 | 1:64 | 3,906 | 246,094 | 2,063 / 8 |
|
||||||
|
| 333 Full | 2,4 | 13 | 3,003 | 1:128 | 2,602 | 330,399 | 1,525 / 6,006 |
|
||||||
|
| 500 LoRa | 2,4 | 8 | 2 | 1:128 | 3,906 | 496,094 | 1,016 / 4 |
|
||||||
|
| F1000 / K1000 | 2,4, SX128X / LR1121 | 8 | 1 | 1:128 | 7,813 | 992,188 | 0,508 / 2 |
|
||||||
|
| K1000 Full | 900, LR1121 | 13 | 1 | 1:128 | 7,813 | 992,188 | 0,508 / 2 |
|
||||||
|
|
||||||
|
Это представительная, не исчерпывающая таблица. Также существуют F500,
|
||||||
|
900 200 Full на LR1121, D50, D250/D500, DK250/DK500 и двухдиапазонные X-режимы;
|
||||||
|
аппаратные ограничения описаны в
|
||||||
|
[официальном руководстве Packet Rate](https://www.expresslrs.org/quick-start/transmitters/lua-howto/#packet-rate-and-telemetry-ratio).
|
||||||
|
Для внешнего модуля 333 Гц и выше требуется скорость связи с пультом выше 400 кбод.
|
||||||
|
Не все режимы доступны одному и тому же RX.
|
||||||
|
|
||||||
|
Full повышает число полноразмерных каналов в OTA. В режиме 8ch каналы 1–8
|
||||||
|
передаются вместе; 16ch Rate/2 делит частоту пополам; 12ch Mixed оставляет
|
||||||
|
первые четыре быстрыми, остальные передаёт с половинной частотой.
|
||||||
|
Hybrid/Wide передают дополнительные переключатели по очереди.
|
||||||
|
Поэтому аварийный орган нельзя без проверки помещать в медленно обновляемую группу.
|
||||||
|
Источник: [Switch Configs, варианты 4.x](https://www.expresslrs.org/software/switch-config/).
|
||||||
|
D50 повторяет команду четыре раза в слотах по 5 мс; D250/DK250 — четыре раза
|
||||||
|
по 1 мс, D500/DK500 — два. Называть их 200/1000 новыми командами в секунду нельзя.
|
||||||
|
Расчётчик отвергает повторные режимы вместо ложного пересчёта.
|
||||||
|
|
||||||
|
## Задержка управления и телеметрия
|
||||||
|
|
||||||
|
Обозначения: T — период RF-слота, D — знаменатель 1:D, f=1/T.
|
||||||
|
При фиксированном D телеметрии выделяется 1/D слотов, RC остаётся не более
|
||||||
|
f×(D−1)/D возможностей. До произвольного ближайшего RF-слота среднее ожидание
|
||||||
|
T/2, верхняя граница T. Если исключить каждый D-й слот под TLM, промежутки
|
||||||
|
между RC равны T или 2T; при равномерной временной фазе среднее ожидание
|
||||||
|
T×(1/2+1/D), верхняя граница 2T. Это наши аналитические расчёты, проверенные
|
||||||
|
независимым перечислением интервалов в тестах.
|
||||||
|
|
||||||
|
[TX-планировщик](https://github.com/ExpressLRS/ExpressLRS/blob/a9d4a9cb5b5687c4c9d7e9e7fbdf44ad93651da6/src/src/tx_main.cpp)
|
||||||
|
заменяет часть передач RC на SYNC и активный DATA uplink; телеметрийные
|
||||||
|
слоты освобождаются для обратного направления. Стандартный Std зависит от режима,
|
||||||
|
Race выключает телеметрию при вооружении, служебный обмен может временно
|
||||||
|
изменить отношение до 1:2. Следовательно, 2T — граница только упрощённого
|
||||||
|
расписания RC/TLM, **не гарантия реального ELRS**. Потери или разрыв связи
|
||||||
|
вообще не имеют такой конечной верхней границы доставки.
|
||||||
|
|
||||||
|
Пример 100 Full (T=10 мс), все величины ниже вычислены тем же расчётчиком:
|
||||||
|
|
||||||
|
| TLM | Доля слотов | TLM/с | RC/с ≤ | Ожидание RC среднее / верхнее, мс | DATA с учётом LINK ≤ байт/с |
|
||||||
|
|---|---:|---:|---:|---:|---:|
|
||||||
|
| Off | 0% | 0 | 100 | 5 / 10 | 0 |
|
||||||
|
| 1:128 | 0,78125% | 0,781 | 99,219 | 5,078 / 20 | 6,250 |
|
||||||
|
| 1:64 | 1,5625% | 1,563 | 98,438 | 5,156 / 20 | 12,500 |
|
||||||
|
| 1:32 Std | 3,125% | 3,125 | 96,875 | 5,313 / 20 | 25,000 |
|
||||||
|
| 1:16 | 6,25% | 6,250 | 93,750 | 5,625 / 20 | 54,167 |
|
||||||
|
| 1:8 | 12,5% | 12,500 | 87,500 | 6,250 / 20 | 116,667 |
|
||||||
|
| 1:4 | 25% | 25 | 75 | 7,500 / 20 | 241,667 |
|
||||||
|
| 1:2 | 50% | 50 | 50 | 10 / 20 | 492,000 |
|
||||||
|
|
||||||
|
Предварительный кандидат для программной модели ровера — **100 Full, 8ch,
|
||||||
|
TLM 1:8**, если выбранная пара его поддерживает: достаточно быстрые каналы
|
||||||
|
стиков/остановки и больше места служебным сообщениям, чем при Std.
|
||||||
|
Это не назначенная конфигурация и не доказательство сроков аварии/ACK.
|
||||||
|
Для 900 МГц при приоритете чувствительности следует сравнить 50 и обычный 100;
|
||||||
|
для 2,4 ГГц — 50/100 Full/150. Окончательный выбор требует моделей TX/RX,
|
||||||
|
диапазона, требований к дальности и отдельного измерения задержек/потерь.
|
||||||
|
|
||||||
|
Чувствительность действительно меняется: официальные ориентиры для 2,4 ГГц
|
||||||
|
50/100 Full/250/500 — −115/−112/−108/−105 dBm, для 900 МГц 50/100/100 Full —
|
||||||
|
−120/−117/−112 dBm. Это параметры режимов из
|
||||||
|
[официальной таблицы](https://www.expresslrs.org/software/mavlink/#throughput-per-rf-mode),
|
||||||
|
а не обещание дальности. ELRS-дальность и будущая дальность SDR на 180 МГц
|
||||||
|
проверяются отдельно; дальнее видео не компенсирует короткий радиус управления.
|
||||||
|
|
||||||
|
Полная цепь задержки управления: считывание стиков → цикл RadioMaster →
|
||||||
|
CRSF к TX → ожидание слота и OTA → RX → CRSF к роверу → цикл управления →
|
||||||
|
привод. Она **не измерена**. К рассчитанному ожиданию нельзя приписывать
|
||||||
|
название stick-to-rover latency.
|
||||||
|
|
||||||
|
## Сколько данных в штатной телеметрии и почему это не видео
|
||||||
|
|
||||||
|
Верхний предел одного тракта DATA: P×f/D байт/с, P=5 либо 10.
|
||||||
|
[RX-планировщик 4.1.0](https://github.com/ExpressLRS/ExpressLRS/blob/a9d4a9cb5b5687c4c9d7e9e7fbdf44ad93651da6/src/src/rx_main.cpp)
|
||||||
|
чередует LINK и серию DATA. LINK занимает четыре байта статистикой,
|
||||||
|
оставляя 1/6 байт DATA. При постоянной насыщенной очереди:
|
||||||
|
B=max(1, floor(512×floor(f)/D/1000)−1), средний предел
|
||||||
|
C=(f/D)×(P−4/(B+1)). Это учёт слотов, а не гарантированный полезный поток:
|
||||||
|
CRC/служебные поля OTA уже вычтены размером P; добивка, подтверждения,
|
||||||
|
повторы DATA, потери и заголовки переносимых CRSF/MAVLink снижают результат.
|
||||||
|
При низкой частоте TLM цель LINK раз в 512 мс сама по себе не гарантируется.
|
||||||
|
|
||||||
|
Даже предельно щедрая оценка K1000 Full, 1:2 даёт 5000 байт/с на один тракт
|
||||||
|
до LINK; условное удвоение для двух DATA-трактов даёт 10000 байт/с. Уже
|
||||||
|
**JPEG 3 кадра/с требует 26172 байт/с**, то есть больше даже такого потолка.
|
||||||
|
|
||||||
|
[Официальные оценки MAVLink](https://www.expresslrs.org/software/mavlink/#throughput-per-rf-mode)
|
||||||
|
при 1:2: 100 Full около 435 байт/с, 333 Full около 1470,
|
||||||
|
F1000 около 2375; K1000 Full около 4420, Gemini около 8845 байт/с.
|
||||||
|
Последнее — 70,76 кбит/с, против 209,376 кбит/с одного JPEG payload при 3 кадрах/с.
|
||||||
|
Это документированные оценки специального режима MAVLink, не наши замеры
|
||||||
|
и не обещание доступного JPEG-сокета у обычного CRSF RX.
|
||||||
|
**STANDARD ELRS VIDEO CAPABILITY = INSUFFICIENT.**
|
||||||
|
|
||||||
|
Телеметрию разделяем так:
|
||||||
|
|
||||||
|
| Класс | Путь | Реализация и бюджет |
|
||||||
|
|---|---|---|
|
||||||
|
| Качество RC-связи: LQ, RSSI, SNR | Штатный ELRS LINK → RadioMaster; CRSF к роверу | Поддерживается ELRS, но не подтверждает выполнение команды |
|
||||||
|
| Напряжение/батарея | Контроллер → CRSF → ELRS DATA → RadioMaster | Формат 0x08 предусмотрен; датчик и выдачу значений должен реализовать контроллер |
|
||||||
|
| Аварийная защёлка, режим ровера, подтверждение критического действия | Тот же ELRS/CRSF обратный путь | Семантика, события, идентификаторы и отображение на пульте ещё не согласованы/реализованы; 0x21/0x32 не дают их автоматически |
|
||||||
|
| Большие диагностические структуры и сенсорные массивы | SDR data/video | Отдельный будущий ограниченный бюджет и политика возраста, нагрузка пока не задана |
|
||||||
|
|
||||||
|
Для масштаба: батарея 12 байт CRSF дважды в секунду требует 24 байт/с.
|
||||||
|
При 100 Full Std расчётный потолок после LINK всего 25 байт/с до прочих потерь:
|
||||||
|
запаса почти нет. При 1:8 потолок 116,667 байт/с. Предлагаемый начальный лимит
|
||||||
|
периодической служебной нагрузки — 50 байт/с **включая CRSF-заголовки**;
|
||||||
|
остаток не объявляется гарантированным: нужны измерения и резерв событий/повторов.
|
||||||
|
Критические события должны опережать периодические датчики в будущей очереди
|
||||||
|
контроллера. TLM 1:8 даёт слот раз в 80 мс: ACK за 50 мс здесь не обещается.
|
||||||
|
Остановка происходит локально по управляющему тракту и не ждёт обратного ACK.
|
||||||
|
Off/Race не подходят для требуемой постоянной критической телеметрии.
|
||||||
|
|
||||||
|
## Связь с безопасностью Lab038–Lab041
|
||||||
|
|
||||||
|
Здесь сформулирован контракт будущего сопряжения, код CRSF/bridge не написан.
|
||||||
|
|
||||||
|
1. Ровер стартует с запрещённым движением; после загрузки, пропажи RC или
|
||||||
|
аварии одно появление старого положения стиков не разрешает движение.
|
||||||
|
Нужны безопасное положение органов и явное новое разрешение оператора.
|
||||||
|
2. Команда остановки поступает через быстрый RC-орган ELRS/CRSF. Локальная
|
||||||
|
защёлка сохраняет остановку независимо от последующих стиков; ACK только
|
||||||
|
сообщает о ней. Снятие защёлки и разрешение движения — отдельные действия.
|
||||||
|
Обычный тумблер пульта не заменяет весь `PersistentEmergencyController`.
|
||||||
|
3. Таймер свежести управления не обновляется от JPEG, LINK statistics,
|
||||||
|
напряжения, DATA или любого произвольного CRSF-кадра. На потерю допустимого
|
||||||
|
управления реагируют существующие ступени торможения. Пороги Lab039
|
||||||
|
100/200 и 150/250 мс являются исследованными модельными вариантами,
|
||||||
|
не измеренными тормозными нормативами ровера; здесь они не изменены.
|
||||||
|
4. `ground_session_id`, `rover_boot_id`, `control_epoch`, `sequence_number`,
|
||||||
|
идентификаторы аварии/сброса Lab041 отсутствуют в RC 0x16. Локально
|
||||||
|
присвоенный номер приёма доказывает порядок UART-событий, но не свежесть
|
||||||
|
источника, защиту от повтора или подлинность команды. CRC этого не заменяет.
|
||||||
|
Binding/model match также нельзя объявлять эквивалентом автомата Lab041.
|
||||||
|
5. В [SendRCdataToRF](https://github.com/ExpressLRS/ExpressLRS/blob/a9d4a9cb5b5687c4c9d7e9e7fbdf44ad93651da6/src/src/tx_main.cpp)
|
||||||
|
порог давности последнего RC от пульта — 1000000 мкс. При остановке
|
||||||
|
обновлений RadioMaster до этого порога TX может посылать прежнее состояние.
|
||||||
|
Приём регулярных UART RC-кадров не обеспечивает исходную свежесть за
|
||||||
|
100/200 мс. Сценарий требует отдельного решения и проверки всей цепи;
|
||||||
|
нормальное неизменное положение стика нельзя считать отказом только
|
||||||
|
потому, что значения равны предыдущим.
|
||||||
|
6. На возобновление соединения, reboot RX/контроллера, старый reset/ACK,
|
||||||
|
удерживаемый разрешающий тумблер и отсутствие обратной телеметрии нужны
|
||||||
|
воспроизводимые проверки. До них `SAFETY INTEGRATION = NOT VALIDATED`.
|
||||||
|
Автомат Lab041 нельзя просто обойти синтезом «нового сеанса» на каждый пакет.
|
||||||
|
|
||||||
|
Таким образом, отдельный ELRS устраняет конкуренцию с видеокадрами, но сам
|
||||||
|
по себе не доказывает безопасность готового ровера. Сохранённые модули и
|
||||||
|
их тесты остаются эталоном свойств для последующей реализации.
|
||||||
|
|
||||||
|
## Видео: фактическая упаковка контрольного JPEG
|
||||||
|
|
||||||
|
Объект [Lab047](../data/processed/lab047/tx_image.jpg): 8724 байта, RGB 320×240,
|
||||||
|
SHA-256 `e913dab9ac32795250880a4be67fbbbf89a4f96536842476ab591d1acaafc02a`.
|
||||||
|
JPEG не перекодирован. [Расчётчик](../experiments/lab049_crsf_elrs_budget.py)
|
||||||
|
вызывает настоящие `build_object_frames` и `build_transmission_plan` из
|
||||||
|
[Lab046](../experiments/lab046_bpsk_object_transport.py) /
|
||||||
|
[Lab045](../experiments/lab045_hackrf_bpsk_fec.py), не генерируя IQ.
|
||||||
|
|
||||||
|
| Ступень на одно изображение | Объём |
|
||||||
|
|---|---:|
|
||||||
|
| JPEG | 8724 байта |
|
||||||
|
| 36 фрагментов: заголовки по 12 байт + 60 байт добивки последнего | 9216 байт нагрузки пакетов |
|
||||||
|
| 36 пакетов по 268 байт: ещё 12 байт заголовка/CRC на пакет | 9648 байт |
|
||||||
|
| Свёрточный FEC R=1/2, шесть хвостовых бит на пакет | 154800 кодированных бит |
|
||||||
|
| PHY-заголовки 144×36 | 5184 бит |
|
||||||
|
| Преамбулы 1280×36 | 46080 бит |
|
||||||
|
| Передаваемые биты BPSK, сумма | 206064 бит |
|
||||||
|
| Защитные интервалы + хвост фильтра (32+10)×36 | 1512 интервалов без новых бит |
|
||||||
|
| Полное занятие радио | **207576 символьных интервалов** |
|
||||||
|
|
||||||
|
Перемежитель длину не меняет. Внешний стирающий FEC старого видеопланировщика
|
||||||
|
Lab034 сюда дополнительно не добавлен: в доказанном потоке Lab047 его нет.
|
||||||
|
Радиосимвол BPSK несёт один передаваемый бит, но защитный интервал не является
|
||||||
|
битом. Поэтому ниже отдельно показаны поток передаваемых бит и минимальная
|
||||||
|
символьная скорость с учётом всего занятого времени. Это расход текущего
|
||||||
|
формата; он не переносится на ELRS и не включает новую модуляцию.
|
||||||
|
|
||||||
|
| Цель | JPEG, байт | Только JPEG, байт/с | Только JPEG, кбит/с | Передаваемые BPSK-биты, кбит/с | Требуемая PHY с защитными интервалами, ксимв/с | RX: изображений/с; RF-кадров/с | Максимум обслуживания RX на изображение | Предложенный предел полной задержки* |
|
||||||
|
|---|---:|---:|---:|---:|---:|---:|---:|---:|
|
||||||
|
| 3 кадра/с, промежуточно | 8724 | 26172 | 209,376 | 618,192 | **622,728** | 3; 108 | 333,33 мс | 333,33 мс |
|
||||||
|
| 5 кадров/с, следующий ориентир | 8724 | 43620 | 348,960 | 1030,320 | **1037,880** | 5; 180 | 200 мс | 200 мс |
|
||||||
|
| 10 кадров/с, дальнейший ориентир | 8724 | 87240 | 697,920 | 2060,640 | **2075,760** | 10; 360 | 100 мс | 100 мс |
|
||||||
|
|
||||||
|
*Предел полной задержки предложен как один период изображения. Это открытое
|
||||||
|
инженерное допущение, **не утверждённый пользователем предел безопасности и
|
||||||
|
не измеренная характеристика**. RX service budget — необходимая средняя
|
||||||
|
производительность последовательного обработчика, не разрешение занять этим
|
||||||
|
весь сквозной бюджет. Измерение хвостов распределения задержки также необходимо.
|
||||||
|
|
||||||
|
Числа PHY — нижние границы при отсутствии потерь, повторов и объёмной нагрузки,
|
||||||
|
весь ресурс отдан видео. На границе ёмкости нет запаса от роста очереди.
|
||||||
|
Произвольные 20 CONTROL + 10 TELEMETRY из SDR **не вычитаются**.
|
||||||
|
Когда появятся объёмные данные, расчётчик принимает их явно как расход
|
||||||
|
символьных интервалов в секунду. Если занято 20% ресурса, требуемая PHY
|
||||||
|
для того же видео возрастает в 1/0,8=1,25 раза — это пример, не выбранный резерв.
|
||||||
|
Точные требуемые скорости выше проверенных 200 ксимв/с; их работоспособность
|
||||||
|
и допустимое число выборок/символ при Fs=8 Мвыб/с не доказаны этой арифметикой.
|
||||||
|
|
||||||
|
## Текущая ёмкость и фактическая обработка
|
||||||
|
|
||||||
|
Замеры переиспользованы из
|
||||||
|
[independent/result.json](../data/processed/lab049/independent/result.json),
|
||||||
|
SHA-256 `8a2c799a9754cf439159ebd68f3d77212200a391ecfdbd3520c312a71a4e49b3`.
|
||||||
|
Полный тяжёлый IQ-опыт на этом архитектурном этапе повторно не запускался.
|
||||||
|
Расчётчик проверяет SHA JPEG, число кадров и согласие длительности с текущим PHY.
|
||||||
|
|
||||||
|
| Rs, ксимв/с | Полезно в идеале, кбит/с | JPEG в эфире, с | Идеальные кадры/с без RC-резерва | Измеренная обработка шумного IQ, с | Длительность IQ, с | Обработка/IQ | JPEG в этом шумном сценарии |
|
||||||
|
|---|---:|---:|---:|---:|---:|---:|---|
|
||||||
|
| 20 | 6,724 | 10,37880 | 0,09635 | 173,65057 | 10,45802 | 16,60454 | Нет, 6/36 |
|
||||||
|
| 50 | 16,811 | 4,15152 | 0,24088 | 5,72980 | 4,23074 | 1,35433 | Точный, 36/36 |
|
||||||
|
| 100 | 33,622 | 2,07576 | 0,48175 | 7,76244 | 2,15498 | 3,60210 | Точный, 36/36 |
|
||||||
|
| 200 | 67,245 | 1,03788 | 0,96350 | 8,17573 | 1,11710 | 7,31872 | Точный, 36/36 |
|
||||||
|
|
||||||
|
Шумной модели заданы одинаковые coded-bit Eb/N0=7 dB, CFO +10 Гц, фаза 0,73 рад,
|
||||||
|
чередование полярности и фиксированные seeds. Порог независимого приёмника
|
||||||
|
Lab047 сохранён: 0.030964473262429237. Это один программный сценарий,
|
||||||
|
не оценка дальности и не аппаратное подтверждение ускоренных режимов.
|
||||||
|
|
||||||
|
Исторический [Lab048](lab048_current_state.md): 15 с записи → 13,966058 с
|
||||||
|
обработки при 20 ксимв/с, отношение 0,931071. Это реальное время только на
|
||||||
|
той сохранённой записи. В нынешней шумной модели ни один режим не успевает;
|
||||||
|
для 5 кадров/с нужен RX как минимум 180 RF-кадров/с, а не нынешние 36 за
|
||||||
|
5,73–8,18 с. Производительность нельзя линейно экстраполировать по Rs.
|
||||||
|
При неизменном Fs вход остаётся 8 Мвыб/с, но число кандидатов и полезных
|
||||||
|
кадров возрастает. 50 ксимв/с — полезная точка для профилирования, а не цель видео.
|
||||||
|
|
||||||
|
## Движение и задержка изображения
|
||||||
|
|
||||||
|
Расчёт расстояния: d=(v/3,6)/fps. Все числа — метры между моментами съёмки
|
||||||
|
при постоянной скорости, не тормозной путь и не полный возраст изображения.
|
||||||
|
|
||||||
|
| Скорость | 1 кадр/с | 3 кадра/с | 5 кадров/с | 10 кадров/с |
|
||||||
|
|---|---:|---:|---:|---:|
|
||||||
|
| 10 км/ч | 2,778 | 0,926 | 0,556 | 0,278 |
|
||||||
|
| 15 км/ч | 4,167 | 1,389 | 0,833 | 0,417 |
|
||||||
|
| 25 км/ч | 6,944 | 2,315 | 1,389 | 0,694 |
|
||||||
|
|
||||||
|
При 25 км/ч ещё 200 мс возраста показанной сцены соответствуют 1,389 м
|
||||||
|
пройденного пути. Потому 3 кадра/с — только промежуточный минимум;
|
||||||
|
конечная цель **больше 3**, следующий практический ориентир 5, дальнейший 10.
|
||||||
|
|
||||||
|
| Участок сквозной задержки | Что известно сейчас |
|
||||||
|
|---|---|
|
||||||
|
| Съёмка/экспозиция/считывание камеры | Не измерено; сейчас готовый JPEG |
|
||||||
|
| Кодирование JPEG | Не измерено в живом тракте; перекодирование здесь не выполнялось |
|
||||||
|
| Радиоочередь | В расчёте ёмкости нулевая; живая очередь не реализована/не измерена |
|
||||||
|
| Передача RF | Рассчитанное время из фактической формы 10,37880 / 4,15152 / 2,07576 / 1,03788 с |
|
||||||
|
| Поиск и обработка RX | Измерены вместе со сборкой и JPEG decode в таблице выше |
|
||||||
|
| Декодирование JPEG | Входит в общее время RX; отдельно не выделено, второй раз не прибавляется |
|
||||||
|
| Очередь экрана/отрисовка | Не измерено |
|
||||||
|
|
||||||
|
Полная задержка L = capture + encode + queue + RF + RX + JPEG decode + display.
|
||||||
|
Сложение RF и имеющегося общего RX описывает последовательную обработку
|
||||||
|
после накопления записи, не измеренный потоковый конвейер. Для такого
|
||||||
|
варианта на 50 ксимв/с известная часть уже 4,15152+5,72980=9,88132 с,
|
||||||
|
плюс неизвестные стадии; реального измерения камера→экран пока нет.
|
||||||
|
|
||||||
|
Предложение для следующего проектирования: при 5 кадрах/с L≤200 мс,
|
||||||
|
например не более 100 мс на радио и оставшиеся 100 мс суммарно на прочие
|
||||||
|
стадии. Тогда по нынешнему формату требуется уже ≥2075,760 ксимв/с,
|
||||||
|
а не 1037,880: минимальный потоковый битрейт сам по себе задержку не обеспечивает.
|
||||||
|
Это иллюстративное распределение бюджета, которое предстоит проверить.
|
||||||
|
Удаление устаревших изображений ограничит возраст очереди, но не ускорит PHY.
|
||||||
|
|
||||||
|
## Если отказаться от отдельного ELRS
|
||||||
|
|
||||||
|
Перенос CRSF-семантики на SDR означал бы собственные радиомодем,
|
||||||
|
синхронизацию, дуплекс/планировщик, повторения, контроль потерь и весь путь
|
||||||
|
безопасности. Стандартный ELRS RX такой SDR-поток не декодирует; для
|
||||||
|
RadioMaster понадобился бы совместимый проводной адаптер/модуль.
|
||||||
|
Теряются готовая OTA-совместимость и доказанная работа штатной пары ELRS.
|
||||||
|
Это уже не стандартный ExpressLRS. Пользователь такой переход не запросил;
|
||||||
|
в данной Lab049 он не реализуется.
|
||||||
|
|
||||||
|
## Следующий этап и воспроизведение
|
||||||
|
|
||||||
|
Рекомендуемый Lab050 — **программный потоковый SDR-видеотракт и профиль RX**:
|
||||||
|
тот же JPEG, независимый непрерывный поиск, ограничение возраста/очереди,
|
||||||
|
измерение стадий обработки и проверка выполнимости 5 кадров/с по вычислениям
|
||||||
|
и PHY. Начать с профиля существующих 50/100/200 ксимв/с; отделить стоимость
|
||||||
|
поиска ложных кандидатов от FEC и сборки. Не объявлять ускорение успешным
|
||||||
|
до полного CRC/SHA и устойчивой очереди. Для CRSF отдельно подготовить
|
||||||
|
контракт безопасности и сценарии отказов; реализация сопряжения требует
|
||||||
|
следующей согласованной постановки и уточнения аппаратуры.
|
||||||
|
|
||||||
|
Позднее сравнить разрешение, grayscale, JPEG quality, ROI, более лёгкий
|
||||||
|
кодек и межкадровые/дельта-данные. Контрольная картинка не окончательна,
|
||||||
|
но в этой работе ни один из этих параметров не менялся.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python experiments/lab049_crsf_elrs_budget.py
|
||||||
|
python -m pytest tests/test_lab049_crsf_elrs_budget.py -q -p no:cacheprovider
|
||||||
|
python tools/quick_gate.py
|
||||||
|
python -m pytest tests/ -q
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
Машинный результат: [crsf_elrs_budget.json](../data/processed/lab049/crsf_elrs_budget.json).
|
||||||
|
Он хранит источники, версию, допущения, SHA исходных замеров, расходы PHY,
|
||||||
|
таблицы ELRS, видео и движения. Старые JSON не перезаписаны.
|
||||||
|
|
||||||
|
Итог: 35 расчётных тестов PASS; quick_gate PASS — 606 тестов и 13 функциональных
|
||||||
|
проверок; полный pytest — 606 passed за 64,32 с, одно предупреждение о доступе
|
||||||
|
к `.pytest_cache`; git diff --check PASS. Дополнительный ruff недоступен:
|
||||||
|
модуль не установлен. Verifier независимо подтвердил числа и источники;
|
||||||
|
adversary подтвердил семь проверок сеансов и локальный watchdog при живом видео.
|
||||||
|
Сквозная безопасность CRSF/ELRS остаётся непроверенной, поскольку сопряжения нет.
|
||||||
|
|
||||||
|
RF TX = 0. HackRF и другое оборудование не запускались.
|
||||||
|
Git add/commit/push/pull/fetch не выполнялись. STOP.
|
||||||
219
docs/lab049_current_state.md
Normal file
219
docs/lab049_current_state.md
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
# Lab049: отдельный ELRS для управления и SDR для видео
|
||||||
|
|
||||||
|
Актуальная архитектурная постановка от 8 сентября 2026 года:
|
||||||
|
**RadioMaster → штатный ELRS → CRSF → контроллер ровера** для управления и
|
||||||
|
критической телеметрии; отдельный SDR — для изображений и объёмных данных.
|
||||||
|
Будущие примерно 180 МГц относятся только к SDR; аппаратная лабораторная
|
||||||
|
база остаётся 1400 МГц. Стандартный ELRS не объявляется работающим на 180 МГц.
|
||||||
|
|
||||||
|
Подробный [аудит CRSF/ELRS, схема и контракт безопасности](lab049_crsf_elrs_architecture.md)
|
||||||
|
сверен с официальным ExpressLRS 4.1.0. Модели аппаратуры пока не указаны,
|
||||||
|
сопряжение контроллера с CRSF и сквозная безопасность не реализованы/не проверены.
|
||||||
|
Сохраняются локальное торможение при потере управления, аварийная защёлка,
|
||||||
|
защита от самовозобновления. Видео не обновляет таймер управления.
|
||||||
|
|
||||||
|
Прежний общий CONTROL > TELEMETRY > VIDEO — исторический стресс-тест ниже.
|
||||||
|
Его 20 CONTROL и 10 TELEMETRY в секунду **не резервируют ёмкость SDR**.
|
||||||
|
Старые программные результаты и отрицательные вердикты сохранены.
|
||||||
|
|
||||||
|
| SDR, ксимв/с | JPEG 8724 байта в идеале, кадры/с | Полезно, кбит/с |
|
||||||
|
|---|---:|---:|
|
||||||
|
| 20 | 0,09635 | 6,724 |
|
||||||
|
| 50 | 0,24088 | 16,811 |
|
||||||
|
| 100 | 0,48175 | 33,622 |
|
||||||
|
| 200 | 0,96350 | 67,245 |
|
||||||
|
|
||||||
|
Требуется **больше 3 кадров/с**: 3 — промежуточный минимум, 5 — следующий
|
||||||
|
ориентир, 10 — дальнейший. Тот же JPEG в нынешнем PHY требует соответственно
|
||||||
|
622,728 / 1037,880 / 2075,760 ксимв/с с защитными интервалами, без запаса на
|
||||||
|
потери и объёмные данные. Только JPEG: 209,376 / 348,960 / 697,920 кбит/с.
|
||||||
|
Штатная ELRS-телеметрия для такого видео недостаточна; официальные источники,
|
||||||
|
расходы по ступеням, движение и бюджет полной задержки приведены в новом отчёте.
|
||||||
|
|
||||||
|
Новый [расчётчик](../experiments/lab049_crsf_elrs_budget.py) создаёт отдельный
|
||||||
|
[crsf_elrs_budget.json](../data/processed/lab049/crsf_elrs_budget.json),
|
||||||
|
переиспользует существующие независимые замеры IQ по SHA и не запускает RF.
|
||||||
|
Он считает ELRS-возможности слотов, не измеренную задержку стики→ровер.
|
||||||
|
35 новых тестов бюджета проходят; quick_gate: 606 тестов и 13 функциональных
|
||||||
|
проверок, полный pytest: 606 passed, git diff --check: PASS. Одно предупреждение
|
||||||
|
pytest относится к доступу к `.pytest_cache`; ruff не установлен.
|
||||||
|
Независимые verifier/adversary не обнаружили воспроизведённых расхождений.
|
||||||
|
Подробности общих проверок — в записи 038.
|
||||||
|
Следующий рекомендуемый Lab050: программное профилирование и потоковая
|
||||||
|
обработка SDR-видео с ограничением возраста очереди; затем отдельное сопряжение CRSF.
|
||||||
|
|
||||||
|
## Сохранённый независимый IQ-опыт и общий стресс-тест
|
||||||
|
|
||||||
|
Актуальный программный результат от 8 сентября 2026 года: **полностью пригодного режима нет**.
|
||||||
|
Максимальная проверенная скорость с точным JPEG в идеальном и фиксированном шумном сценариях —
|
||||||
|
200 ксимв/с. Полный приёмник в шумном сценарии не успевает в реальном времени ни в одном режиме.
|
||||||
|
Это результат одного воспроизводимого сценария, не универсальная оценка предела устойчивости BPSK.
|
||||||
|
|
||||||
|
Для отдельного будущего диагностического PHY-опыта выбран **50 ксимв/с, 160 выборок/символ**:
|
||||||
|
точный JPEG, полезная скорость в 2,5 раза выше базовой и наименьшая вычислительная перегрузка
|
||||||
|
среди точных шумных проходов (1,3543). Он не готов к непрерывному объединённому каналу:
|
||||||
|
блокировка 115,32 мс, перегрузка служебным трафиком и нарушенный аварийный срок.
|
||||||
|
Это кандидат для диагностики, не разрешение RF. 100 ксимв/с лучше по блокировке, но требует
|
||||||
|
3,60 длительности IQ для обработки; 200 ксимв/с — 7,32.
|
||||||
|
|
||||||
|
## Аудит и исправления
|
||||||
|
|
||||||
|
При входе в задачу Lab049 уже существовала без коммита. Старый результат сохранён как
|
||||||
|
`data/processed/lab049/frame_aligned_result.json`; запись 036 журнала не переписана.
|
||||||
|
Они измеряли заранее выровненные кадры без остаточного CFO. Новые данные —
|
||||||
|
`data/processed/lab049/result.json` и идентичный `independent/result.json`.
|
||||||
|
[Аудит](lab049_audit.md) содержит карту функций, проверку Lab033–Lab041 и воспроизведённые отказы.
|
||||||
|
|
||||||
|
Основной RX получает целый IQ-поток без TX-границ, payload, объекта или ожидаемого числа кадров.
|
||||||
|
Используются блочный поиск и строгий декодер Lab048: `lab046c_offline_recovery.stream_process_capture`.
|
||||||
|
Сохранён порог Lab047 `0.030964473262429237`. Адаптер последовательно задаёт Rs/SPS и предоставляет
|
||||||
|
чтение синтетического complex64; исходные константы и функции возвращаются через finally.
|
||||||
|
Старый локальный `receive_iq_frame` остаётся для регрессий формы, основной проход его не использует.
|
||||||
|
|
||||||
|
Fs = 8 000 000 выборок/с. Отношения Fs/Rs точные, дробные входы и нецелое samples/symbol
|
||||||
|
отвергаются. BPSK, PRBS7, PHY header, packet/fragment, CRC, свёрточный FEC R=1/2 K=7
|
||||||
|
(171/133 octal), tail bits и перемежитель сохранены. Новый планировщик не создан, protocol не изменён.
|
||||||
|
|
||||||
|
## Канал и объект
|
||||||
|
|
||||||
|
Один JPEG Lab047: 8724 байта, RGB 320×240, SHA-256
|
||||||
|
`e913dab9ac32795250880a4be67fbbbf89a4f96536842476ab591d1acaafc02a`; без перекодирования.
|
||||||
|
Каждый проход: JPEG → фрагменты → protocol packets → FEC → interleaver → BPSK/RRC →
|
||||||
|
CS8-квантование → IQ → независимый поиск → CFO/фаза/полярность → PHY/FEC/CRC → сборка → JPEG decode.
|
||||||
|
|
||||||
|
Идеальный канал не добавляет шум/CFO. Шумный: единый coded-bit Eb/N0=7 dB,
|
||||||
|
seed=49000+номер кадра, остаточный CFO +10 Гц, фаза 0,73 рад и чередование полярности.
|
||||||
|
Eb = sum(abs(IQ)**2)/число символов; sigma² каждой компоненты шума = Eb/(2×10**(7/10)).
|
||||||
|
Нормировка одинакова во всех режимах, параметры не подбирались до PASS.
|
||||||
|
Ведущий, межкадровые и конечный интервалы не кратны символу; фон имеет отдельные фиксированные seeds.
|
||||||
|
После добавления шума IQ не переквантован в CS8, чтобы не отсекать шум.
|
||||||
|
|
||||||
|
Кадр занимает 5766 символьных интервалов с фильтром и защитными интервалами; JPEG — 36 кадров.
|
||||||
|
Длительность IQ включает ещё 0,079217625 с фона. Полезная скорость считается по чистому времени
|
||||||
|
JPEG, отношение обработки — по всей длительности IQ. Измеряются поиск, декодирование, сборка и
|
||||||
|
JPEG decode; генерация канала и TX-сравнение после вердикта исключены. Все восемь проходов
|
||||||
|
завершились END_OF_STREAM. Первый пробный прогон с лимитами Lab046B 32/256 был неполным;
|
||||||
|
итоговые результаты получены после перехода на полный блочный путь с независимыми лимитами 4096/100000.
|
||||||
|
|
||||||
|
## Результаты IQ
|
||||||
|
|
||||||
|
| Метрика | 20 ксимв/с | 50 ксимв/с | 100 ксимв/с | 200 ксимв/с |
|
||||||
|
|---|---:|---:|---:|---:|
|
||||||
|
| Выборок/символ | 400 | 160 | 80 | 40 |
|
||||||
|
| Кадр, мс | 288.300 | 115.320 | 57.660 | 28.830 |
|
||||||
|
| Кадров JPEG | 36 | 36 | 36 | 36 |
|
||||||
|
| JPEG airtime, с | 10.37880 | 4.15152 | 2.07576 | 1.03788 |
|
||||||
|
| Полезно, байт/с, идеал | 840.560 | 2101.399 | 4202.798 | 8405.596 |
|
||||||
|
| Полезно, кбит/с, идеал | 6.724 | 16.811 | 33.622 | 67.245 |
|
||||||
|
| Идеал: кадры / SHA | 36/36 / да | 36/36 / да | 36/36 / да | 36/36 / да |
|
||||||
|
| Шум: кадры | 6/36 | 36/36 | 36/36 | 36/36 |
|
||||||
|
| До FEC, принятые кадры | 67 | 147 | 119 | 125 |
|
||||||
|
| После FEC, принятые кадры | 0 | 0 | 0 | 0 |
|
||||||
|
| CRC PASS / FAIL | 6 / 0 | 36 / 0 | 36 / 0 | 36 / 0 |
|
||||||
|
| Шум: JPEG SHA / decode | нет / нет | да / да | да / да | да / да |
|
||||||
|
| Ошибки JPEG байт / бит | не восстановлен | 0 / 0 | 0 / 0 | 0 / 0 |
|
||||||
|
| Обработка идеала, с | 8.73746 | 5.69848 | 7.36877 | 6.45915 |
|
||||||
|
| Обработка шума, с | 173.65057 | 5.72980 | 7.76244 | 8.17573 |
|
||||||
|
| Длительность IQ, с | 10.458018 | 4.230738 | 2.154978 | 1.117098 |
|
||||||
|
| Обработка / IQ, шум | 16.604540 | 1.354327 | 3.602096 | 7.318724 |
|
||||||
|
| Реальное время, шум | нет | нет | нет | нет |
|
||||||
|
| Статус | FAIL | PARTIAL | PARTIAL | PARTIAL |
|
||||||
|
|
||||||
|
На 20 ксимв/с 67 ошибок до FEC и 0 после относятся **только к шести принятым кадрам**.
|
||||||
|
JPEG не восстановлен; полезная скорость целых JPEG в шумном проходе равна нулю.
|
||||||
|
Остальные 30 кадров нельзя объявлять безошибочными. Проверено 2378 кандидатов, 2372
|
||||||
|
отклонены на PHY header. На 50/100/200 проверено 59/237/474 кандидата.
|
||||||
|
Кандидат, не достигший CRC, не записан как CRC FAIL. Причины сохранены в JSON.
|
||||||
|
Порог и шум после отказов не менялись. Все идеальные проходы имеют ноль ошибок до/после FEC.
|
||||||
|
|
||||||
|
Базовые 6,724 кбит/с согласуются с примерно 6,7 кбит/с Lab047. Выигрыш 2,5/5/10
|
||||||
|
рассчитан по фактической форме и числу кадров, а не обещан заранее.
|
||||||
|
Исторические аппаратные результаты Lab047/Lab048 не опровергнуты: здесь другой канал.
|
||||||
|
|
||||||
|
## Планирование и задержки
|
||||||
|
|
||||||
|
Переиспользован LATEST_STATE: EMERGENCY > CONTROL > TELEMETRY > VIDEO.
|
||||||
|
Проверка даёт VIDEO(active) → EMERGENCY → CONTROL → TELEMETRY → VIDEO.
|
||||||
|
Физический кадр не прерывается, но весь JPEG не удерживает канал.
|
||||||
|
CONTROL — штатные 36 байт, 20 Гц (Lab041); TELEMETRY — синтетические 64 байта,
|
||||||
|
10 Гц (Lab037–Lab041); EMERGENCY — штатный EmergencyStop, 34 байта, t=0,777 с.
|
||||||
|
Служебная нагрузка генерируется 2 с. Запас VIDEO 256 пакетов не исчерпывается в этом окне.
|
||||||
|
|
||||||
|
Каждый пакет модели занимает один полный PHY-кадр. Это модель занятости;
|
||||||
|
упаковка LinkPacket в аппаратный 256-байтный PHY payload не реализована.
|
||||||
|
Прикладные CONTROL/EMERGENCY сохраняют точные сериализованные байты.
|
||||||
|
Задержки не включают программное время RX, потери, повторы и привод; это не гарантия
|
||||||
|
времени физического торможения. Блокировка — остаток начатого VIDEO;
|
||||||
|
доставка EMERGENCY в худшей фазе — почти два кадра, включая саму команду.
|
||||||
|
Старое поле deadline в priority_probe сохранено как совместимое поле блокировки
|
||||||
|
с явным scope; классификация использует новый delivery flag.
|
||||||
|
|
||||||
|
| Метрика | 20 ксимв/с | 50 ксимв/с | 100 ксимв/с | 200 ксимв/с |
|
||||||
|
|---|---:|---:|---:|---:|
|
||||||
|
| CONTROL max VIDEO blocking, мс | 288.30 | 115.32 | 57.66 | 28.83 |
|
||||||
|
| EMERGENCY max VIDEO blocking, мс | 288.30 | 115.32 | 57.66 | 28.83 |
|
||||||
|
| EMERGENCY worst delivery, мс | 576.60 | 230.64 | 115.32 | 57.66 |
|
||||||
|
| CONTROL blocking <100 / <50 мс | нет / нет | нет / нет | да / нет | да / да |
|
||||||
|
| EMERGENCY delivery ≤50 мс | нет | нет | нет | нет |
|
||||||
|
|
||||||
|
Доставка **в активном окне 0–2 с**, min / median / max, мс. Заменённые и ещё не доставленные состояния сюда не входят:
|
||||||
|
|
||||||
|
| Метрика | 20 ксимв/с | 50 ксимв/с | 100 ксимв/с | 200 ксимв/с |
|
||||||
|
|---|---:|---:|---:|---:|
|
||||||
|
| CONTROL | 289.90 / 303.20 / 316.50 | 116.92 / 136.28 / 162.88 | 58.84 / 82.03 / 105.64 | 30.01 / 43.94 / 61.07 |
|
||||||
|
| EMERGENCY | 376.20 / 376.20 / 376.20 | 145.56 / 145.56 / 145.56 | 87.90 / 87.90 / 87.90 | 30.24 / 30.24 / 30.24 |
|
||||||
|
| TELEMETRY | нет доставленных | нет доставленных | нет доставленных | 33.84 / 45.54 / 89.69 |
|
||||||
|
| VIDEO | 288.30 / 288.30 / 288.30 | 115.32 / 115.32 / 115.32 | 57.66 / 57.66 / 57.66 | 28.83 / 850.49 / 1845.12 |
|
||||||
|
|
||||||
|
Доставка **до полного опустошения очереди**, отдельно от активной нагрузки:
|
||||||
|
|
||||||
|
| Метрика | 20 ксимв/с | 50 ксимв/с | 100 ксимв/с | 200 ксимв/с |
|
||||||
|
|---|---:|---:|---:|---:|
|
||||||
|
| CONTROL, мс | 289.90 / 303.20 / 331.40 | 116.92 / 139.48 / 216.08 | 58.84 / 83.84 / 105.64 | 30.01 / 43.73 / 61.07 |
|
||||||
|
| EMERGENCY, мс | 376.20 / 376.20 / 376.20 | 145.56 / 145.56 / 145.56 | 87.90 / 87.90 / 87.90 | 30.24 / 30.24 / 30.24 |
|
||||||
|
| TELEMETRY, мс | 644.70 / 644.70 / 644.70 | 356.40 / 356.40 / 356.40 | 183.42 / 183.42 / 183.42 | 33.84 / 45.54 / 89.69 |
|
||||||
|
| VIDEO, мс | 288.30 / 39352.95 / 76111.20 | 115.32 / 17009.70 / 31713.00 | 57.66 / 9485.07 / 16836.72 | 28.83 / 5434.46 / 9110.28 |
|
||||||
|
| Конец опустошения, с | 76.11120 | 31.71300 | 16.83672 | 9.11028 |
|
||||||
|
| Превышения CONTROL / EMERGENCY | 6 / 1 | 17 / 1 | 6 / 1 | 0 / 0 |
|
||||||
|
|
||||||
|
Очередь заменяет старое ожидающее состояние, но не удаляет пакет по deadline.
|
||||||
|
На 20/50/100 за активные 2 с доставлено 0 TELEMETRY. У 200 конкретная EMERGENCY
|
||||||
|
доставлена за 30,24 мс, но худшая фаза 57,66 мс всё равно нарушает срок 50 мс.
|
||||||
|
|
||||||
|
## Исторический бюджет единого канала — не бюджет целевой архитектуры
|
||||||
|
|
||||||
|
Прикладные CONTROL/TELEMETRY: 5760/5120 бит/с; вместе с LinkPacket до PHY —
|
||||||
|
10880/7680 бит/с. Физическая стоимость: 20/10 полных кадров/с, или
|
||||||
|
**115320/57660 символьных интервалов в секунду** BPSK, включая всю форму.
|
||||||
|
Эту стоимость нельзя прямо вычитать из JPEG-бит/с: остаток считается через время эфира.
|
||||||
|
JSON-поля reserved_link_bps 46080/23040 относятся к внутренним 288-байтным
|
||||||
|
представителям планировщика, а не к физическому кодированному потоку.
|
||||||
|
|
||||||
|
| Метрика | 20 ксимв/с | 50 ксимв/с | 100 ксимв/с | 200 ксимв/с |
|
||||||
|
|---|---:|---:|---:|---:|
|
||||||
|
| CONTROL, запрошено % эфира | 576.60 | 230.64 | 115.32 | 57.66 |
|
||||||
|
| TELEMETRY, запрошено % эфира | 288.30 | 115.32 | 57.66 | 28.83 |
|
||||||
|
| EMERGENCY, событие/2с, % эфира | 14.415 | 5.766 | 2.883 | 1.442 |
|
||||||
|
| VIDEO после C+T, кбит/с | 0.000 | 0.000 | 0.000 | 9.085 |
|
||||||
|
| Теоретические JPEG/с после C+T | 0.00000 | 0.00000 | 0.00000 | 0.13017 |
|
||||||
|
| VIDEO после C+T+E, кбит/с | 0.000 | 0.000 | 0.000 | 8.115 |
|
||||||
|
|
||||||
|
Процент больше 100 означает перегрузку, а не фактически переданные данные.
|
||||||
|
Только 200 оставляет VIDEO: 9,085 кбит/с и около 0,1302 JPEG/с;
|
||||||
|
с одной EMERGENCY за 2 с — 8,116 кбит/с. Это потенциал модели, не видеопоток:
|
||||||
|
вычислительная перегрузка и аварийный срок остаются.
|
||||||
|
|
||||||
|
## Проверки предыдущего IQ-этапа
|
||||||
|
|
||||||
|
Независимые verifier/adversary воспроизвели прежние отказы и подтвердили исправления.
|
||||||
|
Verifier: 24 целевых теста PASS, независимый пересчёт JSON совпал.
|
||||||
|
Adversary: правильный отрицательный аварийный вердикт и отдельное активное окно.
|
||||||
|
|
||||||
|
- python tools/quick_gate.py: PASS, 571 тест ядра, 12 функциональных проверок.
|
||||||
|
- python -m pytest tests/ -q: 571 passed, 0 failed; одно предупреждение о запрете записи .pytest_cache.
|
||||||
|
- git diff --check: PASS.
|
||||||
|
|
||||||
|
RF TX = 0. HackRF не запускался. Git add/commit/push/pull/fetch не выполнялись.
|
||||||
|
STOP. Следующий этап требует отдельной постановки: вычислительная перегрузка,
|
||||||
|
бюджет и задержки перед аппаратным опытом. QPSK, новый FEC, реальное видео и
|
||||||
|
переход на 180 МГц в этой работе не выполнялись.
|
||||||
1172
experiments/lab049_bpsk_symbol_rate.py
Normal file
1172
experiments/lab049_bpsk_symbol_rate.py
Normal file
File diff suppressed because it is too large
Load Diff
304
experiments/lab049_crsf_elrs_budget.py
Normal file
304
experiments/lab049_crsf_elrs_budget.py
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
"""Lab049: раздельные бюджеты штатного ELRS и SDR-видеоканала, без RF.
|
||||||
|
|
||||||
|
Формулы описывают возможности слотов без потерь, а не полную задержку
|
||||||
|
управления. Исходные интервалы зафиксированы по ExpressLRS 4.1.0.
|
||||||
|
Никакие интерфейсы оборудования не открываются.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import asdict, dataclass
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
|
||||||
|
if __package__ in (None, ""):
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
ELRS_VERSION = "4.1.0"
|
||||||
|
ELRS_COMMIT = "a9d4a9cb5b5687c4c9d7e9e7fbdf44ad93651da6"
|
||||||
|
SOURCE_ROOT = f"https://github.com/ExpressLRS/ExpressLRS/blob/{ELRS_COMMIT}/src/"
|
||||||
|
SOURCES = {
|
||||||
|
"rates_and_link_burst": SOURCE_ROOT + "src/common.cpp",
|
||||||
|
"ota_sizes": SOURCE_ROOT + "lib/OTA/OTA.h",
|
||||||
|
"data_sizes": SOURCE_ROOT + "include/telemetry_protocol.h",
|
||||||
|
"tx_scheduler": SOURCE_ROOT + "src/tx_main.cpp",
|
||||||
|
"rx_scheduler": SOURCE_ROOT + "src/rx_main.cpp",
|
||||||
|
"crsf": "https://github.com/tbs-fpv/tbs-crsf-spec/blob/main/crsf.md",
|
||||||
|
"mavlink_estimates": "https://www.expresslrs.org/software/mavlink/#throughput-per-rf-mode",
|
||||||
|
}
|
||||||
|
TELEMETRY_DENOMINATORS = (128, 64, 32, 16, 8, 4, 2)
|
||||||
|
JPEG_SHA256 = "e913dab9ac32795250880a4be67fbbbf89a4f96536842476ab591d1acaafc02a"
|
||||||
|
|
||||||
|
|
||||||
|
def _positive(value: float, name: str) -> None:
|
||||||
|
if isinstance(value, bool) or not math.isfinite(value) or value <= 0:
|
||||||
|
raise ValueError(f"{name}: требуется конечное положительное число")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ElrsMode:
|
||||||
|
name: str
|
||||||
|
interval_us: int
|
||||||
|
ota_packet_bytes: int
|
||||||
|
standard_telemetry_denominator: int
|
||||||
|
repetitions: int = 1
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
if type(self.interval_us) is not int or self.interval_us <= 0:
|
||||||
|
raise ValueError("Интервал должен быть положительным целым числом микросекунд")
|
||||||
|
if type(self.ota_packet_bytes) is not int or self.ota_packet_bytes not in (8, 13):
|
||||||
|
raise ValueError("Поддерживаются OTA4=8 и OTA8=13 байт")
|
||||||
|
if (type(self.standard_telemetry_denominator) is not int
|
||||||
|
or self.standard_telemetry_denominator not in TELEMETRY_DENOMINATORS):
|
||||||
|
raise ValueError("Неизвестное штатное отношение телеметрии")
|
||||||
|
if type(self.repetitions) is not int or self.repetitions < 1:
|
||||||
|
raise ValueError("Число повторов должно быть положительным целым")
|
||||||
|
|
||||||
|
|
||||||
|
# Отдельные представительные режимы; наличие зависит от микросхемы и диапазона.
|
||||||
|
ELRS_MODES = (
|
||||||
|
ElrsMode("900: 25", 40_000, 8, 8),
|
||||||
|
ElrsMode("900/2.4: 50", 20_000, 8, 16),
|
||||||
|
ElrsMode("900: 100", 10_000, 8, 32),
|
||||||
|
ElrsMode("900/2.4: 100 Full", 10_000, 13, 32),
|
||||||
|
ElrsMode("2.4: 150", 6_666, 8, 32),
|
||||||
|
ElrsMode("900: 200", 5_000, 8, 64),
|
||||||
|
ElrsMode("2.4 / LR1121 900: 250", 4_000, 8, 64),
|
||||||
|
ElrsMode("2.4: 333 Full", 3_003, 13, 128),
|
||||||
|
ElrsMode("2.4: 500", 2_000, 8, 128),
|
||||||
|
ElrsMode("2.4: F1000 / K1000", 1_000, 8, 128),
|
||||||
|
ElrsMode("LR1121 900: K1000 Full", 1_000, 13, 128),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ElrsBudget:
|
||||||
|
packet_interval_ms: float
|
||||||
|
rf_slot_rate_hz: float
|
||||||
|
telemetry_slot_fraction: float
|
||||||
|
telemetry_slots_per_second: float
|
||||||
|
rc_opportunities_per_second_upper_bound: float
|
||||||
|
next_rf_slot_wait_mean_ms: float
|
||||||
|
next_rf_slot_wait_supremum_ms: float
|
||||||
|
next_rc_opportunity_wait_mean_ms: float
|
||||||
|
next_rc_opportunity_wait_supremum_ms: float
|
||||||
|
data_bytes_per_full_telemetry_packet: int
|
||||||
|
data_ceiling_bytes_per_second: float
|
||||||
|
data_ceiling_with_link_stats_bytes_per_second: float
|
||||||
|
data_burst_packets: int
|
||||||
|
|
||||||
|
|
||||||
|
def elrs_budget(mode: ElrsMode, telemetry_denominator: int | None) -> ElrsBudget:
|
||||||
|
"""Один TLM-слот из D; None означает Off, а не отношение 1:1.
|
||||||
|
|
||||||
|
Начальная фаза равномерна по времени. RC-слоты имеют интервалы T
|
||||||
|
(D-2 раза) и 2T (один раз) в цикле DT. Не учитываются SYNC, DATA
|
||||||
|
uplink, потери, LBT, мультиплексирование каналов и очередь UART.
|
||||||
|
Повторные D/DK-режимы требуют иной модели и здесь отвергаются.
|
||||||
|
"""
|
||||||
|
if mode.repetitions != 1:
|
||||||
|
raise ValueError("D/DK: частота RF-слотов не равна частоте новых команд")
|
||||||
|
if telemetry_denominator is not None and (
|
||||||
|
type(telemetry_denominator) is not int
|
||||||
|
or telemetry_denominator not in TELEMETRY_DENOMINATORS
|
||||||
|
):
|
||||||
|
raise ValueError("Отношение должно быть Off или 1:128 ... 1:2")
|
||||||
|
interval_ms = mode.interval_us / 1000
|
||||||
|
slot_rate = 1_000_000 / mode.interval_us
|
||||||
|
fraction = 0.0 if telemetry_denominator is None else 1 / telemetry_denominator
|
||||||
|
telemetry_rate = slot_rate * fraction
|
||||||
|
data_bytes = 5 if mode.ota_packet_bytes == 8 else 10
|
||||||
|
burst = 0
|
||||||
|
with_stats = 0.0
|
||||||
|
if telemetry_denominator is not None:
|
||||||
|
# Точное целочисленное вычисление updateTelemetryBurst/common.cpp.
|
||||||
|
integer_rate = 1_000_000 // mode.interval_us
|
||||||
|
burst = max(1, 512 * integer_rate // telemetry_denominator // 1000 - 1)
|
||||||
|
# LINK содержит 4 байта статистики, оставляя DATA 1 либо 6 байт.
|
||||||
|
with_stats = telemetry_rate * (data_bytes - 4 / (burst + 1))
|
||||||
|
return ElrsBudget(
|
||||||
|
interval_ms, slot_rate, fraction, telemetry_rate,
|
||||||
|
slot_rate * (1 - fraction), interval_ms / 2, interval_ms,
|
||||||
|
interval_ms * (0.5 + fraction),
|
||||||
|
interval_ms * (1 if telemetry_denominator is None else 2),
|
||||||
|
data_bytes, telemetry_rate * data_bytes, with_stats, burst,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ImageAccounting:
|
||||||
|
jpeg_bytes: int
|
||||||
|
rf_frame_count: int
|
||||||
|
fragment_header_bytes_total: int
|
||||||
|
padding_bytes_total: int
|
||||||
|
packet_bytes_total: int
|
||||||
|
packet_overhead_bytes_total: int
|
||||||
|
fec_coded_bits_total: int
|
||||||
|
phy_header_bits_total: int
|
||||||
|
preamble_bits_total: int
|
||||||
|
transmitted_bpsk_bits_total: int
|
||||||
|
guard_and_filter_intervals_total: int
|
||||||
|
symbol_intervals_total: int
|
||||||
|
|
||||||
|
|
||||||
|
def image_accounting(data: bytes) -> ImageAccounting:
|
||||||
|
"""Получить расходы из настоящих планов Lab046/Lab045, без формы IQ."""
|
||||||
|
if not data:
|
||||||
|
raise ValueError("Объект не должен быть пустым")
|
||||||
|
from experiments import lab046_bpsk_object_transport as lab046
|
||||||
|
|
||||||
|
frames = lab046.build_object_frames(data)
|
||||||
|
plans = [frame.plan for frame in frames]
|
||||||
|
payload_bytes = sum(len(plan.payload) for plan in plans)
|
||||||
|
packet_bytes = sum(len(plan.packet) for plan in plans)
|
||||||
|
transmitted_bits = sum(plan.frame_symbol_count for plan in plans)
|
||||||
|
waveform_samples = sum(plan.waveform_sample_count for plan in plans)
|
||||||
|
symbol_intervals, remainder = divmod(waveform_samples, lab046.lab045.SAMPLES_PER_SYMBOL)
|
||||||
|
if remainder:
|
||||||
|
raise ValueError("Форма не занимает целое число символьных интервалов")
|
||||||
|
return ImageAccounting(
|
||||||
|
len(data), len(frames), len(frames) * lab046.fragments.FRAGMENT_HEADER_SIZE,
|
||||||
|
payload_bytes - sum(frame.encoded_payload_length for frame in frames),
|
||||||
|
packet_bytes, packet_bytes - payload_bytes,
|
||||||
|
sum(len(plan.coded_bits) for plan in plans),
|
||||||
|
sum(len(plan.phy_header_bits) for plan in plans),
|
||||||
|
sum(plan.frame_symbol_count - len(plan.post_preamble_bits) for plan in plans),
|
||||||
|
transmitted_bits, symbol_intervals - transmitted_bits, symbol_intervals,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def jpeg_payload_bitrate(jpeg_bytes: int, frames_per_second: float) -> float:
|
||||||
|
if type(jpeg_bytes) is not int or jpeg_bytes <= 0:
|
||||||
|
raise ValueError("Размер JPEG должен быть положительным целым")
|
||||||
|
_positive(frames_per_second, "Частота изображений")
|
||||||
|
return jpeg_bytes * 8 * frames_per_second
|
||||||
|
|
||||||
|
|
||||||
|
def distance_per_frame(speed_kmh: float, frames_per_second: float) -> float:
|
||||||
|
_positive(frames_per_second, "Частота изображений")
|
||||||
|
if isinstance(speed_kmh, bool) or not math.isfinite(speed_kmh) or speed_kmh < 0:
|
||||||
|
raise ValueError("Скорость должна быть конечной и неотрицательной")
|
||||||
|
return speed_kmh / 3.6 / frames_per_second
|
||||||
|
|
||||||
|
|
||||||
|
def video_requirement(accounting: ImageAccounting, frames_per_second: float) -> dict:
|
||||||
|
"""Нижние границы без потерь и объёмных данных; задержка — предложение."""
|
||||||
|
payload_bits = jpeg_payload_bitrate(accounting.jpeg_bytes, frames_per_second)
|
||||||
|
return {
|
||||||
|
"target_fps": frames_per_second,
|
||||||
|
"jpeg_bytes": accounting.jpeg_bytes,
|
||||||
|
"payload_bytes_per_second": payload_bits / 8,
|
||||||
|
"payload_kbit_per_second": payload_bits / 1000,
|
||||||
|
"transmitted_bpsk_kbit_per_second": accounting.transmitted_bpsk_bits_total * frames_per_second / 1000,
|
||||||
|
"minimum_phy_symbol_rate_including_guards": accounting.symbol_intervals_total * frames_per_second,
|
||||||
|
"rx_images_per_second_minimum": frames_per_second,
|
||||||
|
"rx_rf_frames_per_second_minimum": accounting.rf_frame_count * frames_per_second,
|
||||||
|
"rx_service_budget_ms_per_image": 1000 / frames_per_second,
|
||||||
|
"proposed_e2e_image_latency_limit_ms": 1000 / frames_per_second,
|
||||||
|
"e2e_limit_is_approved_or_measured": False,
|
||||||
|
"distance_per_image_m": {str(speed): distance_per_frame(speed, frames_per_second) for speed in (10, 15, 25)},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def sdr_capacity(accounting: ImageAccounting, symbol_rate: float,
|
||||||
|
*, bulk_symbol_intervals_per_second: float = 0.0) -> dict:
|
||||||
|
"""Идеальная ёмкость видео; резерв только под явно заданные SDR-данные."""
|
||||||
|
_positive(symbol_rate, "Символьная скорость")
|
||||||
|
if (isinstance(bulk_symbol_intervals_per_second, bool)
|
||||||
|
or not math.isfinite(bulk_symbol_intervals_per_second)
|
||||||
|
or not 0 <= bulk_symbol_intervals_per_second <= symbol_rate):
|
||||||
|
raise ValueError("Резерв SDR-данных должен лежать между нулём и ёмкостью")
|
||||||
|
available = symbol_rate - bulk_symbol_intervals_per_second
|
||||||
|
fps = available / accounting.symbol_intervals_total
|
||||||
|
return {
|
||||||
|
"symbol_rate": symbol_rate,
|
||||||
|
"jpeg_airtime_seconds": accounting.symbol_intervals_total / symbol_rate,
|
||||||
|
"ideal_video_fps": fps,
|
||||||
|
"ideal_payload_kbit_per_second": accounting.jpeg_bytes * 8 * fps / 1000,
|
||||||
|
"sdr_rc_reserved_symbol_intervals_per_second": 0,
|
||||||
|
"bulk_reserved_symbol_intervals_per_second": bulk_symbol_intervals_per_second,
|
||||||
|
"is_measured_streaming_fps": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def run_functional_tests() -> None:
|
||||||
|
assert elrs_budget(ELRS_MODES[1], 16).telemetry_slots_per_second == 3.125
|
||||||
|
assert jpeg_payload_bitrate(8724, 5) == 348_960
|
||||||
|
assert math.isclose(distance_per_frame(25, 5), 25 / 18)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
run_functional_tests()
|
||||||
|
jpeg_path = ROOT / "data/processed/lab047/tx_image.jpg"
|
||||||
|
measurement_path = ROOT / "data/processed/lab049/independent/result.json"
|
||||||
|
jpeg = jpeg_path.read_bytes()
|
||||||
|
if len(jpeg) != 8724 or hashlib.sha256(jpeg).hexdigest() != JPEG_SHA256:
|
||||||
|
raise ValueError("Контрольный JPEG отличается от Lab047")
|
||||||
|
accounting = image_accounting(jpeg)
|
||||||
|
measurement_bytes = measurement_path.read_bytes()
|
||||||
|
measurements = json.loads(measurement_bytes)
|
||||||
|
if measurements["jpeg"]["sha256"] != JPEG_SHA256:
|
||||||
|
raise ValueError("Замеры относятся к другому JPEG")
|
||||||
|
modes = []
|
||||||
|
for measured in measurements["modes"]:
|
||||||
|
if measured["jpeg_rf_frames"] != accounting.rf_frame_count:
|
||||||
|
raise ValueError("Число физических кадров изменилось")
|
||||||
|
if not math.isclose(measured["jpeg_airtime_seconds"] * measured["symbol_rate"],
|
||||||
|
accounting.symbol_intervals_total, rel_tol=1e-12):
|
||||||
|
raise ValueError("Физический формат отличается от сохранённых замеров")
|
||||||
|
row = sdr_capacity(accounting, measured["symbol_rate"])
|
||||||
|
row["reused_noisy_measurement"] = {
|
||||||
|
key: measured["noisy"][key] for key in (
|
||||||
|
"receiver_processing_seconds", "capture_equivalent_seconds",
|
||||||
|
"processing_to_capture_ratio", "valid_frames", "sha_match", "object_complete",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
modes.append(row)
|
||||||
|
result = {
|
||||||
|
"stage": "Lab049 CRSF/ELRS architecture and separate SDR video budget",
|
||||||
|
"source_checked_date": "2026-09-08",
|
||||||
|
"elrs_version": ELRS_VERSION, "elrs_commit": ELRS_COMMIT, "sources": SOURCES,
|
||||||
|
"jpeg_sha256": JPEG_SHA256, "accounting": asdict(accounting),
|
||||||
|
"measurement_provenance": {
|
||||||
|
"path": measurement_path.relative_to(ROOT).as_posix(),
|
||||||
|
"sha256": hashlib.sha256(measurement_bytes).hexdigest(),
|
||||||
|
"new_iq_measurement_performed": False,
|
||||||
|
},
|
||||||
|
"assumptions": {
|
||||||
|
"elrs_hardware_model_and_band_confirmed": False,
|
||||||
|
"elrs_budgets_are_lossless_slot_opportunities_not_e2e_latency": True,
|
||||||
|
"elrs_data_capacities_are_upper_bounds_not_application_goodput": True,
|
||||||
|
"elrs_repetitions_in_calculator": 1,
|
||||||
|
"sdr_control_telemetry_reservation": 0,
|
||||||
|
"video_loss_retries_and_bulk_load": 0,
|
||||||
|
"proposed_image_latency": "one image period; engineering proposal, not a measured or approved safety limit",
|
||||||
|
},
|
||||||
|
"elrs_standard_ratio_modes": [
|
||||||
|
{"mode": asdict(mode), "budget": asdict(elrs_budget(mode, mode.standard_telemetry_denominator))}
|
||||||
|
for mode in ELRS_MODES
|
||||||
|
],
|
||||||
|
"elrs_100full_ratio_comparison": [
|
||||||
|
{"denominator": denominator, "budget": asdict(elrs_budget(ELRS_MODES[3], denominator))}
|
||||||
|
for denominator in (*TELEMETRY_DENOMINATORS, None)
|
||||||
|
],
|
||||||
|
"elrs_maximum_single_data_ceiling": asdict(elrs_budget(ELRS_MODES[-1], 2)),
|
||||||
|
"video_requirements": [video_requirement(accounting, fps) for fps in (3, 5, 10)],
|
||||||
|
"distance_per_image_m": {str(speed): {str(fps): distance_per_frame(speed, fps)
|
||||||
|
for fps in (1, 3, 5, 10)} for speed in (10, 15, 25)},
|
||||||
|
"sdr_modes": modes,
|
||||||
|
"rf_tx": 0, "hardware_access_performed": False,
|
||||||
|
}
|
||||||
|
output = ROOT / "data/processed/lab049/crsf_elrs_budget.json"
|
||||||
|
output.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||||
|
print(output.relative_to(ROOT).as_posix())
|
||||||
|
print(json.dumps({"accounting": asdict(accounting), "targets": result["video_requirements"],
|
||||||
|
"sdr_modes": modes}, ensure_ascii=False, indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
190
experiments/lab049_independent_iq.py
Normal file
190
experiments/lab049_independent_iq.py
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
"""Полный независимый IQ-проход Lab049 штатным приёмником Lab046B.
|
||||||
|
|
||||||
|
Адаптация старых лабораторий выполняется только в последовательном программном
|
||||||
|
процессе. Контекст возвращает константы и очищает зависящие от них кэши даже
|
||||||
|
при исключении. Аппаратные функции здесь не вызываются.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from io import BytesIO
|
||||||
|
from pathlib import Path
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
from experiments import lab046b_continuous_iq as receiver
|
||||||
|
from experiments import lab046c_offline_recovery as offline
|
||||||
|
from experiments import lab049_bpsk_symbol_rate as lab
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def receiver_mode(mode: lab.PhyMode):
|
||||||
|
"""Временно задать только Rs и Fs/Rs для последовательного офлайн RX."""
|
||||||
|
modules = (lab.lab045, lab.lab045.lab044, lab.lab045.lab044.lab043)
|
||||||
|
originals = [(module, module.SYMBOL_RATE, module.SAMPLES_PER_SYMBOL)
|
||||||
|
for module in modules]
|
||||||
|
caches = (lab.lab045.lab044._preamble_reference_samples,
|
||||||
|
lab.lab045.lab044.lab043._matched_filter_taps,
|
||||||
|
lab.lab045.lab044.lab043._carrier_downconversion_vector)
|
||||||
|
try:
|
||||||
|
for module in modules:
|
||||||
|
module.SYMBOL_RATE = mode.symbol_rate
|
||||||
|
module.SAMPLES_PER_SYMBOL = mode.samples_per_symbol
|
||||||
|
for cache in caches:
|
||||||
|
cache.cache_clear()
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
for module, rate, samples_per_symbol in originals:
|
||||||
|
module.SYMBOL_RATE = rate
|
||||||
|
module.SAMPLES_PER_SYMBOL = samples_per_symbol
|
||||||
|
for cache in caches:
|
||||||
|
cache.cache_clear()
|
||||||
|
|
||||||
|
|
||||||
|
def receive_stream(iq_samples: np.ndarray, mode: lab.PhyMode):
|
||||||
|
"""Поиск, CFO, PHY, FEC и CRC без границ TX или числа кадров."""
|
||||||
|
with receiver_mode(mode):
|
||||||
|
# У синтетического канала после добавления AWGN формат complex64.
|
||||||
|
# Подменяется только чтение IQ; блочный поиск и строгий декодер Lab048
|
||||||
|
# выполняются без изменений. Повторное CS8-квантование отсекло бы шум.
|
||||||
|
original_validate = offline._validate_capture
|
||||||
|
original_window = offline.lab046c._cs8_window
|
||||||
|
samples = np.asarray(iq_samples, dtype=np.complex64)
|
||||||
|
if samples.ndim != 1 or not samples.size or not np.all(np.isfinite(samples)):
|
||||||
|
raise ValueError("Нужен конечный одномерный IQ-поток")
|
||||||
|
try:
|
||||||
|
offline._validate_capture = lambda unused: (samples.view(np.float32), samples.size)
|
||||||
|
offline.lab046c._cs8_window = (
|
||||||
|
lambda raw, start, end: raw[2 * start:2 * end].view(np.complex64)
|
||||||
|
)
|
||||||
|
coarse, strict = offline.stream_process_capture(
|
||||||
|
Path("synthetic_iq_in_memory"),
|
||||||
|
offline.CandidateRule("FIXED_LAB047", 0.999,
|
||||||
|
lab.lab047.FIXED_HARDWARE_CANDIDATE_THRESHOLD),
|
||||||
|
max_candidates=100_000, max_valid_frames=4096,
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
offline._validate_capture = original_validate
|
||||||
|
offline.lab046c._cs8_window = original_window
|
||||||
|
counters = strict.counters
|
||||||
|
return SimpleNamespace(
|
||||||
|
termination_reason=("END_OF_STREAM" if strict.termination_reason == "END_OF_CAPTURE"
|
||||||
|
else strict.termination_reason),
|
||||||
|
accepted_frames=strict.accepted_frames,
|
||||||
|
crc_pass_count=counters.crc_pass, crc_fail_count=counters.crc_fail,
|
||||||
|
phy_header_pass_count=counters.phy_header_pass,
|
||||||
|
fec_pass_count=counters.fec_pass,
|
||||||
|
preamble_candidates=coarse.candidate_count,
|
||||||
|
processed_candidates=counters.candidates_processed,
|
||||||
|
rejected_candidates=counters.candidates_processed - counters.valid_frames,
|
||||||
|
rejected_reasons=tuple(outcome.reason for outcome in strict.outcomes if not outcome.accepted),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_roundtrips(jpeg: bytes, mode: lab.PhyMode) -> tuple[dict, dict]:
|
||||||
|
"""Сформировать целый поток, завершить RX, затем открыть TX-диагностику."""
|
||||||
|
frames = tuple(lab.lab046.build_object_frames(jpeg, lab.lab047.OBJECT_ID))
|
||||||
|
frame_sizes = [lab.frame_waveform_sample_count(frame.plan, mode) for frame in frames]
|
||||||
|
# Не кратные символу интервалы известны только генератору канала.
|
||||||
|
gaps = [12007 + (index * 7919) % 10009 for index in range(len(frames) + 1)]
|
||||||
|
sample_count = sum(frame_sizes) + sum(gaps)
|
||||||
|
results = []
|
||||||
|
with tempfile.TemporaryDirectory(prefix="lab049_iq_") as temporary:
|
||||||
|
for noisy in (False, True):
|
||||||
|
path = Path(temporary) / "stream.c64"
|
||||||
|
iq_stream = np.memmap(path, dtype=np.complex64, mode="w+", shape=(sample_count,))
|
||||||
|
sigmas = []
|
||||||
|
cursor = gaps[0]
|
||||||
|
for index, frame in enumerate(frames):
|
||||||
|
iq = lab.lab045.lab044.signed_iq_bytes_to_complex64(
|
||||||
|
lab.build_parameterized_tx_cs8(frame.plan, mode)
|
||||||
|
)
|
||||||
|
if noisy:
|
||||||
|
# Одинаковые остаточный CFO, фаза и чередование полярности.
|
||||||
|
indexes = np.arange(iq.size, dtype=np.float64)
|
||||||
|
iq *= np.exp(1j * (0.73 + (index % 2) * np.pi
|
||||||
|
+ 2 * np.pi * 10.0 * indexes / mode.sample_rate_hz)).astype(np.complex64)
|
||||||
|
iq, sigma = lab.add_coded_bit_ebn0_noise(
|
||||||
|
iq, frame.plan.frame_symbol_count,
|
||||||
|
lab.NOISY_CODED_BIT_EBN0_DB, lab.NOISE_BASE_SEED + index,
|
||||||
|
)
|
||||||
|
sigmas.append(sigma)
|
||||||
|
random_generator = np.random.default_rng(149000 + index)
|
||||||
|
gap_start = cursor - gaps[index]
|
||||||
|
gap = sigma * (random_generator.standard_normal(gaps[index])
|
||||||
|
+ 1j * random_generator.standard_normal(gaps[index]))
|
||||||
|
iq_stream[gap_start:cursor] = gap.astype(np.complex64)
|
||||||
|
iq_stream[cursor:cursor + iq.size] = iq
|
||||||
|
cursor += iq.size + gaps[index + 1]
|
||||||
|
if noisy:
|
||||||
|
random_generator = np.random.default_rng(249000)
|
||||||
|
iq_stream[-gaps[-1]:] = (sigmas[-1] * (
|
||||||
|
random_generator.standard_normal(gaps[-1])
|
||||||
|
+ 1j * random_generator.standard_normal(gaps[-1])
|
||||||
|
)).astype(np.complex64)
|
||||||
|
iq_stream.flush()
|
||||||
|
started = time.perf_counter()
|
||||||
|
received = receive_stream(iq_stream, mode)
|
||||||
|
assembler = lab.lab046.ObjectAssembler()
|
||||||
|
for item in received.accepted_frames:
|
||||||
|
payload = item.reception.payload_bytes
|
||||||
|
if payload is not None:
|
||||||
|
assembler.offer(lab.lab046.recover_fragment_from_payload(payload))
|
||||||
|
recovered = assembler.recovered_object()
|
||||||
|
jpeg_decode = False
|
||||||
|
if recovered is not None:
|
||||||
|
with Image.open(BytesIO(recovered)) as image:
|
||||||
|
image.load()
|
||||||
|
jpeg_decode = (image.format, image.size, image.mode) == (
|
||||||
|
"JPEG", (320, 240), "RGB")
|
||||||
|
elapsed = time.perf_counter() - started
|
||||||
|
# Всё ниже находится после независимого решения и сборки.
|
||||||
|
accumulator = lab._RoundtripAccumulator(assembler, processing_seconds=elapsed,
|
||||||
|
phy_header_pass=received.phy_header_pass_count,
|
||||||
|
phy_header_fail=received.processed_candidates - received.phy_header_pass_count,
|
||||||
|
fec_pass=received.fec_pass_count, crc_pass=received.crc_pass_count,
|
||||||
|
fec_fail=received.phy_header_pass_count - received.fec_pass_count,
|
||||||
|
crc_not_reached=(received.processed_candidates
|
||||||
|
- received.crc_pass_count - received.crc_fail_count),
|
||||||
|
crc_fail=received.crc_fail_count, noise_sigmas=sigmas)
|
||||||
|
diagnosed = 0
|
||||||
|
by_index = {frame.fragment_index: frame for frame in frames}
|
||||||
|
for item in received.accepted_frames:
|
||||||
|
fragment = lab.lab046.recover_fragment_from_payload(item.reception.payload_bytes)
|
||||||
|
if fragment is None or fragment.fragment_index not in by_index:
|
||||||
|
continue
|
||||||
|
diagnostic = lab.lab045.diagnostic_pre_and_post_fec(
|
||||||
|
item.reception, item.post_preamble_soft, by_index[fragment.fragment_index].plan)
|
||||||
|
accumulator.pre_fec_errors += int(diagnostic["pre_fec_coded"]["bit_errors"])
|
||||||
|
accumulator.post_fec_errors += int(diagnostic["post_fec_packet"]["bit_errors"])
|
||||||
|
diagnosed += 1
|
||||||
|
summary = lab._roundtrip_summary(
|
||||||
|
accumulator, jpeg, len(frames), sample_count / mode.sample_rate_hz)
|
||||||
|
summary.update({
|
||||||
|
"receiver": "lab046c_offline_recovery.stream_process_capture (Lab048)",
|
||||||
|
"candidate_threshold": lab.lab047.FIXED_HARDWARE_CANDIDATE_THRESHOLD,
|
||||||
|
"frame_aligned_iq": False, "tx_offsets_provided": False,
|
||||||
|
"residual_cfo_hz": 10.0 if noisy else 0.0,
|
||||||
|
"phase_rad": 0.73 if noisy else 0.0,
|
||||||
|
"polarity_alternates": noisy,
|
||||||
|
"jpeg_decode": jpeg_decode,
|
||||||
|
"preamble_candidates": received.preamble_candidates,
|
||||||
|
"processed_candidates": received.processed_candidates,
|
||||||
|
"rejected_candidates": received.rejected_candidates,
|
||||||
|
"rejected_reasons": list(received.rejected_reasons),
|
||||||
|
"termination_reason": received.termination_reason,
|
||||||
|
"diagnosed_frames": diagnosed,
|
||||||
|
"error_counts_cover_complete_object": diagnosed == len(frames),
|
||||||
|
"estimated_cfo_hz": [item.estimated_cfo_hz for item in received.accepted_frames],
|
||||||
|
"payload_airtime_seconds": sum(frame_sizes) / mode.sample_rate_hz,
|
||||||
|
"rf_tx": 0,
|
||||||
|
})
|
||||||
|
results.append(summary)
|
||||||
|
del iq_stream
|
||||||
|
path.unlink()
|
||||||
|
return results[0], results[1]
|
||||||
211
tests/test_lab049_bpsk_symbol_rate.py
Normal file
211
tests/test_lab049_bpsk_symbol_rate.py
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from functools import lru_cache
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from experiments import lab045_hackrf_bpsk_fec as lab045
|
||||||
|
from experiments import lab049_bpsk_symbol_rate as lab049
|
||||||
|
from protocol.control_messages import ControlStateMessage, EmergencyStop, decode_message
|
||||||
|
from protocol.link_packet import TrafficClass, decode_link_packet
|
||||||
|
from protocol.priority_scheduler import QueuedPacket
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=1)
|
||||||
|
def _plan() -> lab045.TransmissionPlan:
|
||||||
|
payload = bytes((index * 49 + 7) & 0xFF for index in range(256))
|
||||||
|
return lab045.build_transmission_plan(payload)
|
||||||
|
|
||||||
|
|
||||||
|
def _frame_duration(symbol_rate: int) -> float:
|
||||||
|
mode = lab049.phy_mode(symbol_rate)
|
||||||
|
return lab049.frame_waveform_sample_count(_plan(), mode) / mode.sample_rate_hz
|
||||||
|
|
||||||
|
|
||||||
|
def test_parameterized_symbol_rates_have_exact_samples_per_symbol() -> None:
|
||||||
|
assert {
|
||||||
|
rate: lab049.phy_mode(rate).samples_per_symbol
|
||||||
|
for rate in lab049.TESTED_SYMBOL_RATES
|
||||||
|
} == {20_000: 400, 50_000: 160, 100_000: 80, 200_000: 40}
|
||||||
|
|
||||||
|
|
||||||
|
def test_non_integer_samples_per_symbol_is_rejected() -> None:
|
||||||
|
with pytest.raises(ValueError, match="целым"):
|
||||||
|
lab049.phy_mode(30_000)
|
||||||
|
|
||||||
|
|
||||||
|
def test_20ksymbol_waveform_is_byte_exact_lab045_result() -> None:
|
||||||
|
assert lab049.build_parameterized_tx_cs8(
|
||||||
|
_plan(),
|
||||||
|
lab049.phy_mode(20_000),
|
||||||
|
) == lab045.build_tx_cs8(_plan())
|
||||||
|
|
||||||
|
|
||||||
|
def test_20ksymbol_frame_duration_preserves_lab047_baseline() -> None:
|
||||||
|
mode = lab049.phy_mode(20_000)
|
||||||
|
assert lab049.frame_waveform_sample_count(_plan(), mode) == _plan().waveform_sample_count
|
||||||
|
assert _frame_duration(20_000) == _plan().airtime_seconds
|
||||||
|
|
||||||
|
|
||||||
|
def test_50ksymbol_iq_roundtrip() -> None:
|
||||||
|
result = lab049.single_frame_roundtrip(50_000)
|
||||||
|
assert result == {
|
||||||
|
"phy_header_pass": True,
|
||||||
|
"fec_pass": True,
|
||||||
|
"crc": lab045.CRC_PASS,
|
||||||
|
"payload_match": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_100ksymbol_iq_roundtrip() -> None:
|
||||||
|
result = lab049.single_frame_roundtrip(100_000)
|
||||||
|
assert result == {
|
||||||
|
"phy_header_pass": True,
|
||||||
|
"fec_pass": True,
|
||||||
|
"crc": lab045.CRC_PASS,
|
||||||
|
"payload_match": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_scheduler_priority_order_is_reused() -> None:
|
||||||
|
result = lab049.priority_probe(_frame_duration(100_000))
|
||||||
|
assert result["priority_pass"]
|
||||||
|
assert result["transmission_order"] == [
|
||||||
|
"VIDEO",
|
||||||
|
"EMERGENCY",
|
||||||
|
"CONTROL",
|
||||||
|
"TELEMETRY",
|
||||||
|
"VIDEO",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_video_is_not_interrupted_but_control_enters_at_next_boundary() -> None:
|
||||||
|
duration = _frame_duration(100_000)
|
||||||
|
result = lab049.priority_probe(duration)
|
||||||
|
assert result["first_video_not_preempted"]
|
||||||
|
assert result["control_max_video_blocking_ms"] == duration * 1000.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_emergency_has_highest_priority_after_active_frame() -> None:
|
||||||
|
result = lab049.priority_probe(_frame_duration(100_000))
|
||||||
|
assert result["transmission_order"][1] == "EMERGENCY"
|
||||||
|
assert not result["emergency_deadline_met_in_worst_phase"]
|
||||||
|
assert lab049.priority_probe(_frame_duration(200_000))[
|
||||||
|
"emergency_deadline_met_in_worst_phase"
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_loaded_scheduler_reports_all_latency_classes() -> None:
|
||||||
|
result = lab049.loaded_scheduler_scenario(_frame_duration(100_000))
|
||||||
|
assert result["video_queue_saturated"]
|
||||||
|
assert set(result["latency"]) == {
|
||||||
|
"EMERGENCY",
|
||||||
|
"CONTROL",
|
||||||
|
"TELEMETRY",
|
||||||
|
"VIDEO",
|
||||||
|
}
|
||||||
|
for statistics in result["latency"].values():
|
||||||
|
assert statistics["count"] > 0
|
||||||
|
assert 0.0 <= statistics["min_ms"] <= statistics["median_ms"] <= statistics["max_ms"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_scheduler_preserves_valid_control_and_emergency_payloads() -> None:
|
||||||
|
control_payload = lab049._control_payload(0)
|
||||||
|
emergency_payload = lab049._emergency_payload(0)
|
||||||
|
control = decode_message(control_payload)
|
||||||
|
emergency = decode_message(emergency_payload)
|
||||||
|
assert isinstance(control, ControlStateMessage)
|
||||||
|
assert isinstance(emergency, EmergencyStop)
|
||||||
|
assert len(control_payload) < lab049.lab046.PACKET_PAYLOAD_SIZE
|
||||||
|
assert len(emergency_payload) < lab049.lab046.PACKET_PAYLOAD_SIZE
|
||||||
|
queued = (
|
||||||
|
QueuedPacket(
|
||||||
|
lab049._link_packet(TrafficClass.CONTROL, 0, 0.0, control_payload),
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
QueuedPacket(
|
||||||
|
lab049._link_packet(TrafficClass.EMERGENCY, 0, 0.0, emergency_payload),
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
schedule = lab049._schedule_fixed_physical_frames(
|
||||||
|
queued,
|
||||||
|
_frame_duration(100_000),
|
||||||
|
)
|
||||||
|
restored_payloads = {
|
||||||
|
item.queued.packet.traffic_class: decode_link_packet(item.wire_packet).payload
|
||||||
|
for item in schedule.transmitted
|
||||||
|
}
|
||||||
|
assert restored_payloads[TrafficClass.CONTROL] == control_payload
|
||||||
|
assert restored_payloads[TrafficClass.EMERGENCY] == emergency_payload
|
||||||
|
result = lab049.loaded_scheduler_scenario(_frame_duration(100_000))
|
||||||
|
assert not result["application_payloads_padded"]
|
||||||
|
assert result["fixed_physical_frame_duration_model"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_scheduler_reports_expired_control_without_claiming_it_was_dropped() -> None:
|
||||||
|
result = lab049.loaded_scheduler_scenario(_frame_duration(50_000))
|
||||||
|
assert result["deadline_misses"]["CONTROL"] >= 1
|
||||||
|
assert not result["scheduler_drops_expired_packets"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_channel_budget_uses_control_telemetry_before_video() -> None:
|
||||||
|
slow = lab049.channel_budget(
|
||||||
|
_frame_duration(20_000),
|
||||||
|
lab049.EXPECTED_JPEG_BYTES,
|
||||||
|
36,
|
||||||
|
)
|
||||||
|
fast = lab049.channel_budget(
|
||||||
|
_frame_duration(200_000),
|
||||||
|
lab049.EXPECTED_JPEG_BYTES,
|
||||||
|
36,
|
||||||
|
)
|
||||||
|
middle = lab049.channel_budget(
|
||||||
|
_frame_duration(100_000),
|
||||||
|
lab049.EXPECTED_JPEG_BYTES,
|
||||||
|
36,
|
||||||
|
)
|
||||||
|
assert slow["video_available_bitrate_bps"] == 0.0
|
||||||
|
assert middle["video_available_bitrate_bps"] == 0.0
|
||||||
|
assert fast["video_available_bitrate_bps"] > 0.0
|
||||||
|
assert middle["control_reserved_link_bps"] == pytest.approx(46_080.0)
|
||||||
|
assert middle["telemetry_load_link_bps"] == pytest.approx(23_040.0)
|
||||||
|
assert middle["fixed_physical_frame_per_packet"]
|
||||||
|
assert middle["service_requested_airtime_fraction"] > 1.0
|
||||||
|
assert fast["service_requested_airtime_fraction"] < 1.0
|
||||||
|
assert fast["video_available_frames_per_second"] == pytest.approx(
|
||||||
|
fast["physical_frame_capacity_per_second"] - 30.0
|
||||||
|
)
|
||||||
|
assert fast["remaining_link_bps"] == pytest.approx(
|
||||||
|
fast["scheduler_link_bitrate_bps"]
|
||||||
|
- fast["control_reserved_link_bps"]
|
||||||
|
- fast["telemetry_load_link_bps"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_receiver_real_time_ratio_calculation() -> None:
|
||||||
|
assert lab049.processing_to_capture_ratio(0.5, 1.0) == 0.5
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
lab049.processing_to_capture_ratio(1.0, 0.0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_noise_is_normalized_by_transmitted_bit_energy() -> None:
|
||||||
|
long = np.ones(400, dtype=np.complex64)
|
||||||
|
short = np.ones(160, dtype=np.complex64)
|
||||||
|
_, long_sigma = lab049.add_coded_bit_ebn0_noise(long, 1, 7.0, 49)
|
||||||
|
_, short_sigma = lab049.add_coded_bit_ebn0_noise(short, 1, 7.0, 49)
|
||||||
|
assert long_sigma / short_sigma == pytest.approx(np.sqrt(400 / 160))
|
||||||
|
|
||||||
|
|
||||||
|
def test_receiver_api_has_no_tx_plan_or_payload_oracle() -> None:
|
||||||
|
parameters = inspect.signature(lab049.receive_iq_frame).parameters
|
||||||
|
assert tuple(parameters) == ("iq_samples", "mode")
|
||||||
|
|
||||||
|
|
||||||
|
def test_mode_classification_requires_all_latency_capacity_and_speed_gates() -> None:
|
||||||
|
assert lab049.classify_mode(True, True, True, True, False, False) == "PARTIAL"
|
||||||
|
assert lab049.classify_mode(True, True, False, True, True, True) == "PARTIAL"
|
||||||
|
assert lab049.classify_mode(True, True, True, True, True, True) == "PASS"
|
||||||
|
assert lab049.classify_mode(False, True, True, True, True, True) == "FAIL"
|
||||||
119
tests/test_lab049_crsf_elrs_budget.py
Normal file
119
tests/test_lab049_crsf_elrs_budget.py
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
"""Проверки расчётов Lab049 без файлов эксперимента и оборудования."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import replace
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from experiments import lab049_crsf_elrs_budget as budget
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def accounting() -> budget.ImageAccounting:
|
||||||
|
return budget.image_accounting(bytes(8724))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("interval_us, expected_ms", [(20_000, 20), (10_000, 10), (6666, 6.666), (3003, 3.003)])
|
||||||
|
def test_source_interval_preserves_integer_microseconds(interval_us, expected_ms):
|
||||||
|
mode = budget.ElrsMode("test", interval_us, 8, 16)
|
||||||
|
result = budget.elrs_budget(mode, None)
|
||||||
|
assert result.packet_interval_ms == expected_ms
|
||||||
|
assert result.rf_slot_rate_hz == pytest.approx(1_000_000 / interval_us)
|
||||||
|
assert result.next_rc_opportunity_wait_mean_ms == pytest.approx(expected_ms / 2)
|
||||||
|
assert result.next_rc_opportunity_wait_supremum_ms == expected_ms
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("denominator", budget.TELEMETRY_DENOMINATORS)
|
||||||
|
def test_telemetry_and_waits_against_enumerated_cycle(denominator):
|
||||||
|
result = budget.elrs_budget(budget.ELRS_MODES[3], denominator)
|
||||||
|
# Вторая независимая форма: перечислить моменты RC и интегрировать
|
||||||
|
# треугольники ожидания между ними за полный период.
|
||||||
|
rc_times = [slot * 10 for slot in range(denominator) if slot != denominator - 1]
|
||||||
|
gaps = [right - left for left, right in zip(rc_times, rc_times[1:] + [denominator * 10])]
|
||||||
|
assert result.next_rc_opportunity_wait_mean_ms == pytest.approx(sum(gap**2 / 2 for gap in gaps) / (denominator * 10))
|
||||||
|
assert result.next_rc_opportunity_wait_supremum_ms == max(gaps)
|
||||||
|
assert result.telemetry_slot_fraction == 1 / denominator
|
||||||
|
assert result.telemetry_slots_per_second == 100 / denominator
|
||||||
|
assert result.rc_opportunities_per_second_upper_bound == len(rc_times) / (denominator / 100)
|
||||||
|
|
||||||
|
|
||||||
|
def test_off_is_zero_telemetry_and_not_ratio_one():
|
||||||
|
result = budget.elrs_budget(budget.ELRS_MODES[3], None)
|
||||||
|
assert result.telemetry_slots_per_second == result.data_ceiling_bytes_per_second == 0
|
||||||
|
assert result.data_ceiling_with_link_stats_bytes_per_second == 0
|
||||||
|
assert result.rc_opportunities_per_second_upper_bound == 100
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
budget.elrs_budget(budget.ELRS_MODES[3], 1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_data_capacity_uses_data_not_whole_ota_packet():
|
||||||
|
result = budget.elrs_budget(budget.ELRS_MODES[3], 2)
|
||||||
|
assert result.data_ceiling_bytes_per_second == 500
|
||||||
|
assert result.data_burst_packets == 24
|
||||||
|
assert result.data_ceiling_with_link_stats_bytes_per_second == 492
|
||||||
|
maximum = budget.elrs_budget(budget.ELRS_MODES[-1], 2)
|
||||||
|
assert maximum.data_ceiling_bytes_per_second == 5000
|
||||||
|
assert 2 * maximum.data_ceiling_bytes_per_second < 8724 * 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_repeated_packets_are_not_counted_as_new_commands():
|
||||||
|
with pytest.raises(ValueError, match="D/DK"):
|
||||||
|
budget.elrs_budget(replace(budget.ELRS_MODES[-1], repetitions=4), 128)
|
||||||
|
|
||||||
|
|
||||||
|
def test_actual_transport_overhead(accounting):
|
||||||
|
assert accounting.rf_frame_count == 36
|
||||||
|
assert accounting.fragment_header_bytes_total == 432
|
||||||
|
assert accounting.padding_bytes_total == 60
|
||||||
|
assert accounting.packet_bytes_total == 9648
|
||||||
|
assert accounting.packet_overhead_bytes_total == 432
|
||||||
|
assert accounting.fec_coded_bits_total == 154800
|
||||||
|
assert accounting.phy_header_bits_total == 5184
|
||||||
|
assert accounting.preamble_bits_total == 46080
|
||||||
|
assert accounting.transmitted_bpsk_bits_total == 206064
|
||||||
|
assert accounting.guard_and_filter_intervals_total == 1512
|
||||||
|
assert accounting.symbol_intervals_total == 207576
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("fps,payload_bits,symbols", [(3, 209376, 622728), (5, 348960, 1037880), (10, 697920, 2075760)])
|
||||||
|
def test_targets(accounting, fps, payload_bits, symbols):
|
||||||
|
result = budget.video_requirement(accounting, fps)
|
||||||
|
assert budget.jpeg_payload_bitrate(8724, fps) == payload_bits
|
||||||
|
assert result["payload_kbit_per_second"] == payload_bits / 1000
|
||||||
|
assert result["minimum_phy_symbol_rate_including_guards"] == symbols
|
||||||
|
assert result["rx_rf_frames_per_second_minimum"] == 36 * fps
|
||||||
|
assert result["rx_service_budget_ms_per_image"] == 1000 / fps
|
||||||
|
assert not result["e2e_limit_is_approved_or_measured"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("speed,fps,distance", [(10, 3, 25/27), (15, 5, 5/6), (25, 10, 25/36), (0, 1, 0)])
|
||||||
|
def test_distance(speed, fps, distance):
|
||||||
|
assert budget.distance_per_frame(speed, fps) == pytest.approx(distance)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("symbol_rate,airtime", [(20000, 10.3788), (50000, 4.15152), (100000, 2.07576), (200000, 1.03788)])
|
||||||
|
def test_sdr_capacity_reserves_no_elrs_traffic(accounting, symbol_rate, airtime):
|
||||||
|
result = budget.sdr_capacity(accounting, symbol_rate)
|
||||||
|
assert result["jpeg_airtime_seconds"] == pytest.approx(airtime)
|
||||||
|
assert result["ideal_video_fps"] == pytest.approx(1 / airtime)
|
||||||
|
assert result["ideal_payload_kbit_per_second"] == pytest.approx(8724 * 8 / airtime / 1000)
|
||||||
|
assert result["sdr_rc_reserved_symbol_intervals_per_second"] == 0
|
||||||
|
half = budget.sdr_capacity(accounting, symbol_rate, bulk_symbol_intervals_per_second=symbol_rate/2)
|
||||||
|
assert half["ideal_video_fps"] == pytest.approx(result["ideal_video_fps"] / 2)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("bad", [0, -1, float("inf"), float("nan"), True])
|
||||||
|
def test_invalid_rates_rejected(accounting, bad):
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
budget.jpeg_payload_bitrate(8724, bad)
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
budget.distance_per_frame(25, bad)
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
budget.sdr_capacity(accounting, bad)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("bad", [True, 16.0, 3, 0])
|
||||||
|
def test_invalid_telemetry_ratios_rejected(bad):
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
budget.elrs_budget(budget.ELRS_MODES[1], bad)
|
||||||
66
tests/test_lab049_independent_iq.py
Normal file
66
tests/test_lab049_independent_iq.py
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
"""Регрессии неизвестной границы, CFO и полной задержки доставки Lab049."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from experiments import lab049_bpsk_symbol_rate as lab
|
||||||
|
from experiments import lab049_independent_iq as independent
|
||||||
|
|
||||||
|
|
||||||
|
def test_fractional_symbol_rate_is_not_silently_rounded() -> None:
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
lab.phy_mode(20000.5)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("rate", (50_000, 100_000))
|
||||||
|
def test_unknown_frame_start_and_residual_cfo(rate: int) -> None:
|
||||||
|
mode = lab.phy_mode(rate)
|
||||||
|
payload = bytes(range(256))
|
||||||
|
plan = lab.lab045.build_transmission_plan(payload)
|
||||||
|
iq = lab.lab045.lab044.signed_iq_bytes_to_complex64(
|
||||||
|
lab.build_parameterized_tx_cs8(plan, mode))
|
||||||
|
iq *= np.exp(1j * (2.7 + 2 * np.pi * 10 * np.arange(iq.size)
|
||||||
|
/ mode.sample_rate_hz)).astype(np.complex64)
|
||||||
|
stream = np.pad(iq, (12007, 12003))
|
||||||
|
result = independent.receive_stream(stream, mode)
|
||||||
|
assert result.termination_reason == "END_OF_STREAM"
|
||||||
|
assert result.crc_pass_count == 1
|
||||||
|
assert result.accepted_frames[0].reception.payload_bytes == payload
|
||||||
|
assert result.accepted_frames[0].estimated_cfo_hz == pytest.approx(10, abs=1)
|
||||||
|
assert lab.lab045.SYMBOL_RATE == 20_000
|
||||||
|
assert lab.lab045.lab044.lab043.SAMPLES_PER_SYMBOL == 400
|
||||||
|
|
||||||
|
|
||||||
|
def test_receiver_mode_restores_constants_on_exception() -> None:
|
||||||
|
with pytest.raises(RuntimeError):
|
||||||
|
with independent.receiver_mode(lab.phy_mode(100_000)):
|
||||||
|
raise RuntimeError("контроль выхода")
|
||||||
|
assert lab.lab045.SAMPLES_PER_SYMBOL == 400
|
||||||
|
assert lab.lab045.lab044.SAMPLES_PER_SYMBOL == 400
|
||||||
|
|
||||||
|
|
||||||
|
def test_noise_only_does_not_produce_a_packet() -> None:
|
||||||
|
generator = np.random.default_rng(49)
|
||||||
|
noise = (generator.standard_normal(180000)
|
||||||
|
+ 1j * generator.standard_normal(180000)).astype(np.complex64)
|
||||||
|
result = independent.receive_stream(noise, lab.phy_mode(100_000))
|
||||||
|
assert result.crc_pass_count == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_emergency_delivery_includes_its_own_frame() -> None:
|
||||||
|
probe = lab.priority_probe(0.02883)
|
||||||
|
assert probe["emergency_max_video_blocking_ms"] == pytest.approx(28.83)
|
||||||
|
assert probe["emergency_worst_delivery_ms"] == pytest.approx(57.66)
|
||||||
|
assert not probe["emergency_delivery_deadline_met_in_worst_phase"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_deadlines_use_complete_delivery_and_drain_is_separate() -> None:
|
||||||
|
result = lab.loaded_scheduler_scenario(0.05766)
|
||||||
|
assert result["deadline_misses"]["CONTROL"] == 6
|
||||||
|
assert result["deadline_misses"]["EMERGENCY"] == 1
|
||||||
|
assert result["active_window_delivered_packets"]["TELEMETRY"] == 0
|
||||||
|
assert result["active_window_delivery_latency"]["TELEMETRY"]["count"] == 0
|
||||||
|
assert result["delivery_latency_until_drain_end"]["CONTROL"]["max_ms"] == pytest.approx(105.64)
|
||||||
|
assert result["drain_end_seconds"] > result["source_duration_seconds"]
|
||||||
Reference in New Issue
Block a user