# Протокол старого пульта тележки (реверс-инжиниринг) Снято 2026-08-20 осциллографом Hantek DPO7204C с сигнального провода пульта (канал CH3). Инструменты: `tools/capture.py` (захват), `tools/decode.py` (декодер), сырые данные и картинки — в `captures/`. ## Физический уровень - Один сигнальный провод, логика **3.3 В**. - **Инвертированный UART** (стандартная полярность SBUS): в покое линия **низкая** (~0 В), импульсы вверх до ~3.3 В. - Скорость **100 000 бод**, формат **8E2** (8 бит данных, чётность even, 2 стоп-бита), биты LSB-first. Длительность бита 10 мкс. ## Кадровый уровень — SBUS Стандартный кадр Futaba SBUS, 25 байт (3 мс на линии): | Смещение | Размер | Содержимое | |---|---|---| | 0 | 1 | Заголовок `0x0F` | | 1 | 22 | 16 каналов × 11 бит, упакованы подряд LSB-first | | 23 | 1 | Флаги: bit0=CH17, bit1=CH18, bit2=frame_lost, bit3=failsafe | | 24 | 1 | Футер `0x00` | Распаковка каналов: 22 байта складываются в 176-битное число LSB-first, канал N (N=0..15) = биты [11·N .. 11·N+10], диапазон значений 0–2047. - Кадры отправляются каждые **50 мс** (20 Гц). Это медленнее стандартного SBUS (7/14 мс) — ответная часть с этим темпом работает. - Флаги во всех наблюдениях = `0x00`. ## Карта каналов (нумерация с 1) | Управление | Канал | Мин | Нейтраль | Макс | |---|---|---|---|---| | Стик вперёд/назад | **2** | 433 (назад) | 1024 | 1643 (вперёд) | | Стик влево/вправо | **4** | 446 (влево) | 1024 | 1654 (вправо) | | Остальные 14 | — | всегда 1024 | | | Диапазон осей ~±600 от нейтрали (не полная шкала SBUS). Других органов управления на пульте нет. Эталонный кадр нейтрали (hex): ``` 0F 00 04 20 00 01 08 40 00 02 10 80 00 04 20 00 01 08 40 00 02 10 80 00 00 ``` ## Воспроизведение на STM32G431 - USART: 100000 бод, 8 бит + чётность even (в терминах STM32: M=1, 9-bit с PCE=1), 2 стоп-бита, **TXINV=1** (аппаратная инверсия TX) — бит-бэнг не нужен. - Отправлять 25-байтовый кадр по таймеру каждые 50 мс. - Нейтраль: все каналы 1024; управление — каналы 2 и 4 в измеренных диапазонах. # Сетевой мост «пульт → тележка» Та же команда управления может идти не по проводу рядом, а через локальную сеть/интернет, не ломая прежний проводной режим. Цепочка: ``` [пульт] ADC→ STM(CART-TX) ─USB─► bridge --role tx ─UDP─► bridge --role rx ─USB─► STM(CART-RX) ─SBUS─► драйвер ``` STM-TX продолжает гнать SBUS на свой локальный провод как раньше и дополнительно отдаёт те же значения каналов по USB. STM-RX принимает их по USB и выдаёт на свой провод так, будто сигнал пришёл с пульта. ## Роли и идентификация устройства Роль выбирается на сборке (`platformio.ini`): `pio run -e cart_tx` или `-e cart_rx`. Общий код (`sbus`, `usb_link`) собирается в оба env, специфичный — через `build_src_filter`, мёртвого кода неактивной роли нет. Каждая прошивка публикует USB **product string**: `CART-TX` / `CART-RX`. По ней `tools/bridge.py` находит нужный порт (`serial.tools.list_ports`). После открытия порта ПК шлёт `HELLO` и ждёт `IDENT` — подтверждение роли, версии протокола и 96-битного UID чипа (на случай неверно прошитого устройства, рассинхрона версий или чужого CDC с тем же VID/PID). Serial-дескриптор (UID) различает экземпляры. ## Кадр поверх USB CDC Единственный источник истины — `src/link_protocol.h`, зеркало на Python — `tools/link_proto.py` (сверка: `python3 tools/link_proto.py --selftest`). ``` [0xA5][0x5A][VER=1][TYPE][LEN][PAYLOAD..LEN][CRC16_LO][CRC16_HI] ``` - CRC16-CCITT (poly `0x1021`, init `0xFFFF`) по байтам `[VER..конец PAYLOAD]`, LE. Контрольный вектор: `crc16("123456789") = 0x29B1`. - Приёмник (STM и Python) — стейт-машина: ищет синхрослово, читает по `LEN`, проверяет CRC; при сбое сдвиг на байт и ресинхронизация (устойчивость к мусору и потере байта). - Типы: `0x01 CONTROL`, `0x02 IDENT`, `0x03 HELLO`, `0x04 LOG`. Отладочный текст идёт только кадром `LOG` — «голых» `Serial.print`, ломающих бинарный поток, нет. Payload `CONTROL` (LE): `seq:u32`, `t_ms:u32`, `flags:u8`, `ch[16]:u16` = 41 байт. `ch[]` — значения SBUS-каналов 0..2047, ровно те, что TX кладёт на свой провод. Это общая «валюта» всей цепочки: ПК перекладывает payload в UDP и обратно, RX распаковывает и пакует в SBUS. `flags` bit0 = failsafe. ## Сетевой слой (между экземплярами bridge) - Транспорт — **UDP**: для управления 20 Гц свежий кадр ценнее доставленного; TCP при потере встал бы в head-of-line blocking и копил лаг. - Полезная нагрузка UDP — тот же `CONTROL` payload. `seq` даёт отбрасывание устаревших/повторных пакетов (защита от переупорядочивания и дубликатов UDP) с корректной арифметикой переполнения uint32. Перезапуск источника (seq снова с 0) распознаётся по пачке подряд «несвежих» пакетов и трактуется как новая сессия — иначе поток завис бы в failsafe навсегда. Это best-effort защита от повтора старых команд, не криптографическая: аутентичность даёт HMAC, против целевого replay на недоверенной сети используй WireGuard. - Приём из сети и выдача SBUS на провод — **разные таймеры**: джиттер сети не попадает на провод, STM-RX гонит SBUS строго каждые 50 мс из последнего состояния. - **Безопасность.** Для интернета обязателен общий ключ (`--key`): каждый UDP- пакет аутентифицируется усечённым HMAC-SHA256 (8 байт), RX отбрасывает неаутентичные и принимает только с ожидаемого адреса (`--peer`). Рекомендуемый способ наружу — WireGuard-туннель, внутри которого идёт простой UDP. «Голый» неаутентифицированный UDP допустим только в доверенной локальной сети. ## FAILSAFE Потеря связи никогда не оставляет тележку на последней команде — выход всегда нейтраль (все каналы 1024), а не удержание. - **Главный предохранитель — на самом STM-RX.** Если валидный `CONTROL` по USB не приходил дольше `RX_USB_TIMEOUT_MS = 150` мс (3 кадра), STM-RX сам выдаёт на провод нейтраль с флагом failsafe (SBUS bit3). Срабатывает при обрыве USB, зависании или падении ПК-процесса. SBUS на провод идёт непрерывно 20 Гц в любом случае — драйвер не видит пропажи сигнала. - **Сетевой failsafe на ПК-RX.** При тишине из сети дольше 100 мс bridge активно шлёт на STM-RX нейтральные `CONTROL` с флагом `CTRL_FLAG_FAILSAFE` в темпе 20 Гц, чтобы причина была явной. STM-RX транслирует этот флаг в бит failsafe SBUS-кадра (byte 23 bit3). Восстановление — по первому свежему валидному пакету. ## Запуск ``` # сторона пульта .venv/bin/python tools/bridge.py --role tx --peer 10.0.0.2:5005 --key SECRET # сторона тележки .venv/bin/python tools/bridge.py --role rx --bind 0.0.0.0:5005 --peer 10.0.0.1 --key SECRET ``` Локальная проверка без сети: оба экземпляра на одной машине с `--peer 127.0.0.1` и двумя STM по USB; SBUS с RX снять `tools/decode.py` и сверить с осями стика TX.