# Ganglion Serial Protocol v2 Протокол связи между Arduino-контроллером робота FRANCY и управляющим компьютером через UART-радиомодуль. ## Физический уровень | Параметр | Значение | |---|---| | Интерфейс | UART (Serial1 на Arduino, /dev/ttyUSB0 на хосте) | | Скорость | 57600 бод (~5760 байт/с) | | Формат | 8N1 (8 бит данных, без паритета, 1 стоп-бит) | | Кодировка | ASCII (текстовый протокол) | ## Анализ текущего протокола (v1) ### Сильные стороны 1. **Читаемость** -- текстовый протокол, можно отлаживать обычным терминалом (minicom, screen, picocom). 2. **Простота парсинга** -- фиксированный формат `KEY:VALUE\r\n`, regex-совместимый. 3. **Низкий overhead по пропускной способности** -- ~84 байта на пакет телеметрии, это ~840 Б/с при 100ms интервале, всего 15% от канала 5760 Б/с. 4. **Stateless** -- каждый пакет самодостаточен, потеря пакета не нарушает работу. 5. **Строчный разделитель** -- `\r\n` (println) естественно работает с readline-парсерами. ### Слабые стороны 1. **Нет фреймирования** -- невозможно определить начало/конец пакета телеметрии. Если потерять часть строки, парсер может склеить хвост одного поля с началом другого. 2. **Нет контрольной суммы** -- радиоканал может вносить ошибки, парсер не может их обнаружить. 3. **Суффикс `E` избыточен** -- роль терминатора поля уже выполняет `\r\n` от println; `E` не несёт дополнительной информации. 4. **Неочевидная кодировка моторов** -- диапазон 0-255 с нейтралью 128 неинтуитивен, требует постоянного пересчёта. 5. **Нет sequence number** -- невозможно определить потерю пакета или измерить latency. 6. **Нет handshake** -- при подключении хост не знает, работает ли робот и какой версии протокол. 7. **Debug-вывод в канал** -- `process_command()` делает `serial->println(command)` и `serial->println(atoi(digits))`, смешивая debug-данные с телеметрией. 8. **Непоследовательная семантика значений**: - `LS`/`RS` -- int, знаковый (-128..127 внутри, но передаётся как int) - `GX`/`GY` -- float * 1000 (milliTesla на деле, хост делит на 1000) - `V` -- float * 1000 (хост делит на 1000) - `LC`/`RC` -- сырой ADC (0-1023) 9. **Команда мотора фиксирована на 3 цифры** -- `L000`..`L255`, нельзя послать отрицательное число или расширить диапазон. ### Пропускная способность (v1) Типичный пакет телеметрии (7 полей): ``` LS:50E\r\n = 9 байт RS:-30E\r\n = 10 байт LC:512E\r\n = 10 байт RC:480E\r\n = 10 байт GX:-123.45E\r\n = 14 байт GY:456.78E\r\n = 13 байт V:4200.00E\r\n = 13 байт -------- ~79-90 байт ``` При интервале 100ms: ~850 Б/с = 15% канала. Запас достаточен для добавления 3-5 новых полей. ## Протокол v2 (рекомендуемый) Принцип: **минимальные изменения**, сохраняющие читаемость и отлаживаемость, но устраняющие критические слабости. ### Изменение 1: Убрать суффикс `E`, использовать только `\r\n` **Было:** `LS:50E\r\n` **Стало:** `LS:50\r\n` Суффикс `E` не несёт смысловой нагрузки -- `\r\n` уже является надёжным терминатором строки. Убираем `E`, экономим 1 байт на поле (7 байт на пакет). ### Изменение 2: Фреймирование пакета маркерами BEGIN/END Каждый пакет телеметрии обёрнут: ``` ---\r\n LS:50\r\n RS:-30\r\n LC:512\r\n RC:480\r\n GX:-123.45\r\n GY:456.78\r\n V:4.20\r\n ***\r\n ``` - `---` -- маркер начала пакета (start-of-frame) - `***` -- маркер конца пакета (end-of-frame) - Парсер накапливает строки между `---` и `***`, затем обрабатывает их атомарно - Если `***` не получен за разумное время (200ms), пакет отбрасывается Overhead: +10 байт на пакет (~12% рост), но это даёт надёжное фреймирование. ### Изменение 3: Кодировка моторов -- оставить 0-255 **Решение: оставить как есть.** Обоснование: - Диапазон 0-255 с нейтралью 128 -- это формат, который уже реализован в прошивке и proven работающим - Внутри Arduino значение конвертируется: `constrain(atoi(digits), 0, 255) - 128`, получая -128..127 - Перекодировка на signed формат потребовала бы менять state machine приёма (добавить обработку знака `-`) - В Python-слое (pilot/) конвертация `speed_normalized = raw - 128` тривиальна - Формат 3 фиксированных цифры (`L000`..`L255`) удобен для FSM-парсера на Arduino Однако рекомендуется, чтобы **pilot/ предоставлял API в человеко-понятных единицах** (-100..+100 или -1.0..+1.0), а конвертацию в 0-255 делал serial_link. ### Изменение 4: Убрать debug-println из rx_event В `process_command()` Arduino отправляет эхо команды: ```cpp serial->println(command); // <-- убрать serial->println(atoi(digits)); // <-- убрать ``` Эти строки засоряют телеметрический канал и ломают парсер (парсер видит `L\r\n` и `128\r\n` как неизвестные поля). ### Изменение 5: Нормализация единиц измерения | Поле | v1 (текущее) | v2 (новое) | Единица | |---|---|---|---| | `LS`, `RS` | int (знаковый) | int (знаковый) | Без изменений, внутренние единицы (-128..127) | | `LC`, `RC` | raw ADC (0-1023) | raw ADC (0-1023) | Без изменений, калибровка на хосте | | `GX`, `GY` | float*1000 (milligauss) | float (milligauss) | **Передавать как есть**, хост НЕ делит на 1000 | | `V` | float*1000 (millivolts) | float (volts) | **Передавать вольты** (делить на 1000 на Arduino) | Важное уточнение: в текущей прошивке `voltage = sensorValue * (5.0 / 1024.0) * 1000` -- это уже millivolts. А `gauss_x = compass->readGaussX() * 1000.0` -- milligauss. Хост потом делит на 1000 обратно. Проще передавать в натуральных единицах. ### Изменение 6: Добавление поля дальномера ``` RF:1234\r\n # дистанция в миллиметрах, int ``` - Появляется в пакете только когда датчик установлен и читается - Значение `0` или отсутствие поля = нет данных - Диапазон: 0-8000 (типичный для VL53L0X/VL53L1X) ### Отложенные улучшения (v3+) Следующие улучшения **не включаются в v2**, но запланированы: 1. **Контрольная сумма** -- XOR всех байт между `---` и `***`, формат `CK:XX\r\n` (hex). Добавить когда появятся реальные проблемы с помехами. 2. **Handshake** -- при подключении хост отправляет `PING\n`, робот отвечает `PONG:v2\r\n`. Добавить при реализации auto-reconnect в pilot/. 3. **Heartbeat/watchdog** -- если Arduino не получает команд 2 секунды, остановить моторы. Критично для безопасности, рекомендую добавить в v2.1. ## Реализованный протокол v2 ### Arduino → Хост (каждые ~100 мс) Пакет обёрнут маркерами `---` / `***`. Хост накапливает строки между ними и разбирает атомарно. ``` --- SQ: # sequence number 0-65535, переполнение → 0 LS: # скорость левого мотора (-128..127, 0 = стоп) RS: # скорость правого мотора LC: # ток левого мотора, сырой АЦП 0-1023 RC: # ток правого мотора GX: # магнетометр X, milligauss GY: # магнетометр Y, milligauss V: # напряжение аккумулятора, вольты RF: # дальномер вперёд, мм (0 = нет данных) GZ: # угловая скорость по Z, °/с (положительная = CCW) LR:<0|1> # состояние красного LED (пин 10): 0 = выкл, 1 = вкл LB:<0|1> # состояние синего LED (пин 11): 0 = выкл, 1 = вкл *** ``` - `SQ` отслеживается хостом для подсчёта потерянных пакетов: `expected = (last_sq + 1) % 65536` - Вне телеметрических фреймов Arduino шлёт ответы на motion-команды: `D::\n` Пример пакета: ``` --- SQ:1042 LS:42 RS:-38 LC:512 RC:480 GX:-123.45 GY:456.78 V:11.85 RF:1523 GZ:0.34 LR:0 LB:1 *** ``` ### Хост → Arduino | Команда | Формат | Описание | |---|---|---| | Левый мотор | `L\n` | DDD = 000..255, 128 = стоп | | Правый мотор | `R\n` | DDD = 000..255, 128 = стоп | | Биппер | `P\n` | DDD = длительность 000..999 мс; активный пьезо на пине 8 | | Выстрел | `F\n` | Серво-цикл (пин 9): 45°→150°→45°; ответ `D:OK:0.0\n` по завершению | | Серво (калибровка) | `S\n` | Прямая установка позиции 000..180°, без ответа | | Поворот | `T±DDDD\n` | Поворот на ±DDDD градусов; ответ `D::\n` | | Движение | `M±DDDD\n` | Движение ±DDDD мм; ответ `D::\n` | | Коррекция гироскопа | `G±DDDD\n` | Добавить ±DDDD/10 °/с к firmware bias гироскопа | | Управление LED | `E\n` | R = состояние красного (`0`/`1`), B = состояние синего (`0`/`1`) — оба сразу | Примеры: ``` L200\n # левый мотор вперёд R128\n # правый мотор стоп P250\n # бип 250 мс E10\n # красный ON, синий OFF E01\n # красный OFF, синий ON E11\n # оба ON E00\n # оба OFF T+0090\n # повернуть на 90° вправо M+0500\n # проехать 500 мм вперёд F\n # выстрел ``` ### Motion done response (Arduino → Хост, вне фреймов) ``` D::\n ``` | Код | Значение | |---|---| | `OK` | Выполнено успешно | | `TO` | Таймаут | | `ST` | Застрял (нет движения дальномера) | | `OB` | Препятствие < 200 мм, движение вперёд заблокировано | | `JM` | Луч дальномера соскользнул с препятствия | `angle` — накопленный угол гироскопа в градусах (для поворотов); для движений = 0.0. ### Тайминги | Параметр | Значение | |---|---| | Главный цикл Arduino | 10 мс (100 Гц) | | Дальномер | каждые 5 циклов = 50 мс (20 Гц) | | Телеметрия | каждые 10 циклов = 100 мс (10 Гц) | | Мягкий старт моторов | ~250 мс до MAX_SPEED=50 | ### Пропускная способность Типичный пакет (12 полей + фрейм): ``` ---\r\n = 5 байт SQ:1042\r\n = 9 байт LS:42\r\n = 7 байт RS:-38\r\n = 8 байт LC:512\r\n = 8 байт RC:480\r\n = 8 байт GX:-123.45\r\n = 12 байт GY:456.78\r\n = 11 байт V:11.85\r\n = 9 байт RF:1523\r\n = 9 байт GZ:0.34\r\n = 9 байт LR:0\r\n = 6 байт LB:1\r\n = 6 байт ***\r\n = 5 байт --------- ~112 байт ``` При 100 мс интервале: ~1120 Б/с = **19% канала** (57600 бод ≈ 5760 Б/с). Запас ~4640 Б/с.