Плата молчит после команды. Это может значить четыре вещи: строка не доехала, доехала кривой, парсер её не понял, или понял, исполнил — а эффект не там, где ждали. Без второй платы, осциллографа и логического анализатора эти четыре случая не различить на глаз — и именно здесь их различает один приём, который не стоит ни копейки: заставить плату отвечать на каждую входящую строку вслух. Две строки на команду: что услышала и что решила. Называю это эхо-протоколом и ставлю в скетч раньше любой бизнес-логики.
Механику приёма и базовую разборку строки вы уже видели в двух прошлых уроках; здесь фокус на второй половине разговора — на ответах. Отладка через порт вообще начинается с дисциплины вывода, это разобрано в уроке про отладку логики; эхо-протокол — следующая ступень той же мысли: не просто печатать значения, а вести диалог, где у каждой реплики есть класс и причина. Уже после первой недели с таким протоколом исчезают вопросы в духе «почему не работает»: ответ лежит в мониторе с указанием ступени, на которой всё встало.
ok и bad как контракт
Префиксы ok, bad и st — контракт: первый токен ответа всегда называет класс события. ok — команда принята и исполнена, bad — отвергнута с причиной, st — снимок состояния без команды. Такой лог читается глазами даже через неделю после записи, а сортировкой по префиксам превращается в таблицу отказов за минуту. Полную иерархию сообщений — когда что печатать, сколько слов тратить и куда писать подробности — я разворачиваю в уроке про уровни логов; для командного протокола хватает трёх классов, и три держатся годами.
Почему не слова вроде «принято» и «отвергнуто»? Длина и различимость: префиксы в два-три символа видны в логе с одного взгляда, не сливаются с аргументами и фильтруются поиском без конфигурации. Короткий префикс экономит и буфер печати: ответ с эхом и причиной укладывается в 40-50 байтов, очередь не растягивается на проходы loop. Протокол вообще любит такие мелкие числа: они не старятся и не требуют комментариев.
Эхо строки я беру в квадратные скобки: >> eho: [GET]. Скобки рисуют границу строки, и невидимые символы перестают быть невидимыми: [GET ] с хвостом после скобки честно показывает, что монитор досылает что-то лишнее, и trim в парсере только что оправдал своё место в коде. Без скобок тот же хвост выглядел бы как загадочная разница между поведением двух плат — ищи её потом глазом по экрану.
Ответ зеркалит команду: SET возвращает присвоенное число, ON — фактический пин и его состояние. Мелочь, которая экономит вечера: увидели ok: relay on, а реле молчит — виновата электрика, а не протокол, и проверять надо пин и питание, а не парсер. Границу между программой и железом протокол должен показывать, а не смазывать: ровно для этого ok печатает то, что сделал, а не то, что собирался.
// Эхо-протокол: вердикт на каждую строкуconst int RELAY = 7;const int POT = A0; int threshold = 50;bool relayOn = false; void replyBad(String why) { Serial.print("bad: "); Serial.println(why);} void runCommand(String line) { line = line.trim(); Serial.print(">> eho: ["); Serial.print(line); Serial.println("]"); if (line.length() == 0) { replyBad("pustaya stroka"); return; } if (line.startsWith("SET")) { String arg = line.substring(3); arg = arg.trim(); if (arg.length() == 0) { replyBad("SET bez chisla"); return; } int v = arg.toInt(); if (v < 1 || v > 100) { replyBad("chislo tolko 1-100"); return; } threshold = v; Serial.print("ok: threshold = "); Serial.println(threshold); } else if (line == "GET") { Serial.print("ok: threshold = "); Serial.print(threshold); Serial.print(", level = "); Serial.println(map(analogRead(POT), 0, 1023, 0, 100)); } else if (line == "ON") { relayOn = true; digitalWrite(RELAY, HIGH); Serial.println("ok: relay on"); } else if (line == "OFF") { relayOn = false; digitalWrite(RELAY, LOW); Serial.println("ok: relay off"); } else { replyBad("ne znayu komandu"); }} void setup() { Serial.begin(9600); pinMode(RELAY, OUTPUT); Serial.println("protokol: SET 1-100, GET, ON, OFF");} void loop() { if (Serial.available()) runCommand(Serial.readStringUntil(10)); // 10 = LF if (relayOn) digitalWrite(RELAY, HIGH); else digitalWrite(RELAY, LOW); delay(20);}Четыре симптома и их расшифровка
Разберём сценарий «команда не сработала» по протоколу. Ни эха, ни ответа — строка не доехала: скорость, кабель, не тот порт, монитор не открыт. Эхо есть, ответа нет — парсер принял строку и замолчал: где-то ранний return без печати, смотрите ветки if. bad с причиной — парсер здоров, виновато значение: причина называет поле и границу. ok есть, а эффекта нет — протокол отработал, ищите в железе: пин, питание, проводка. Четыре случая, четыре разных лекарства, и все различаются по двум строкам в мониторе — без пайки и без второго комплекта платы.
Когда подозрение всё же падает на сам провод — дрожи линии, помехи, пропадающие байты — дальше порта не уйдёшь без приборов, и тут есть отдельная методика: урок про диагностику глюков линии. Эхо-протокол не заменяет её, а отрезает: если эхо доходит байт в байт, линия чистая, и искать надо в коде, а не в кабеле. А когда к плате подключится второй собеседник — скрипт или другая плата — формат уже готов: машина читает префикс, человек читает остальное. Провод для пары плат показан в уроке про связь двух Arduino, а формат разговора у вас уже есть.
Живой пример. Насос в теплице включался командой ON, ok приходил, насос молчал. Эхо-протокол отрезал половину поисков за минуту: в логе красовалось ok: relay on — значит, пин D7 получил высокий уровень, код дошёл до конца ветки. Осталась физика, и физика не заставила себя ждать: реле-модуль оказался с активным низом, где включение — это LOW. Три строки в мониторе сэкономили вечер, который иначе ушёл бы на перепроверку исправного скетча.
Ещё два качества скетча стоит проговорить вслух. Состояние relayOn живёт в переменной, а не в чтении выходного пина: digitalRead на выходе и правда работает, но переменная показывает замысел, и GET опирается именно на неё. Ветка else с replyBad — единственная точка, куда падают неизвестные глаголы: новая команда означает новую ветку до неё, и точка отказа остаётся одна. Протокол растёт, не теряя гарантий, — это главное отличие архитектуры от кучи if.
Обратная сторона — ответы, которые хуже молчания. bad без причины: вы узнаёте про отказ ровно столько же, сколько без него, только экран замусорен. Ответ в две-три строки с рамками из равно: в живом логе рамки слипаются, и искать начало события — работа для археолога. Самое коварное — печать телеметрии внутри быстрого цикла: показание раз в 100 мс выдаёт шестьсот строк в минуту, и ваша команда тонет в потоке раньше, чем вы допечатаете. Телеметрию — реже или на запрос GET, команды — всегда и сразу. Большой словарь с вложенными подкомандами и правила оформления ответов для него собраны отдельно в уроке про командный парсер.
Отдельная прививка — молчание при пустой строке. Соблазн отбросить пустой ввод без ответа велик, но тогда на линии появится случай без реплики, неотличимый от обрыва связи. Мой скетч отвечает и на пустоту: bad: pustaya stroka. Пустых строк в живом вводе мало, лог не страдает, а полнота восстанавливается: каждое событие на входе получает реплику на выходе, и диагностическая лестница остаётся без дыр.
| Симптом в мониторе | Что это значит | Куда смотреть |
|---|---|---|
| Ни эха, ни ответа | строка не доехала до парсера | скорость порта, кабель, открытый монитор, выбранный порт |
| Эхо есть, ответа нет | парсер принял строку и промолчал: ранний return без печати | ветки if и return в парсере, порядок проверок |
| bad: chislo tolko 1-100 | строка доехала, значение вне границы — протокол здоров | что напечатано в аргументе, кто и откуда его послал |
| ok: relay on, реле молчит | протокол отработал, железо нет | пин, питание, проводка, номинал транзистора |
Таблица выше — рабочий чек-лист, который я первое время держал напечатанным над столом. Порядок обхода всегда один: транспорт, парсер, значения, железо. Навигация в обратную сторону сжирает вечера, потому что на каждом этаже версия выглядит правдоподобно, и перебирать их без направления — удовольствие так себе.
Заметьте, как мало стоит сам механизм: одна печать эха в начале runCommand и одна функция replyBad на четыре строки. Всё остальное — привычка печатать вердикт в каждой ветке, включая отказные. Привычка ставится за один вечер с симулятором ниже: десять вводов, десять вердиктов, и рука сама тянется печатать причину, а не только вердикт.
- 1. Строка не доехала до платы. Что вы увидите в мониторе?
- 2. Зачем эхо строки берут в квадратные скобки?
- 3. Печать ok: relay on прошла, реле молчит. Где искать?
- 4. Сколько вердиктов ok/bad на одну команду печатает эхо-протокол?
- 5. Почему телеметрию в командном протоколе печатают реже, чем ответы на команды?
- Эхо-протокол: на каждую строку плата печатает, что услышала, и что решила — ok или bad с причиной
- Префиксы ok / bad / st — контракт: лог читается глазами и парсится скриптом без легенды
- Четыре симптома: нет эха — провод, эхо без ответа — парсер, bad — значение, ok без эффекта — железо
- Эхо в квадратных скобках вскрывает невидимые хвосты строки — trim получает доказательство, а не подозрение
- Одна команда — один вердикт; телеметрия реже команд, иначе лог тонет и протокол теряет смысл
Частые вопросы по теме урока
- Строка не доехала до платы. Что вы увидите в мониторе?
- Если эха нет, парсер строку не получал — виновата доставка: скорость, кабель, порт. Ветка bad тут не при чём: плата ещё не знает о команде. Ни эха, ни ответа
- Зачем эхо строки берут в квадратные скобки?
- [GET ] с хвостом после скобки сразу показывает лишний символ от монитора. Без скобок та же разница ищется глазом по экрану и не находится. Границы строки видны: невидимый хвост перестаёт быть невидимым
- Печать ok: relay on прошла, реле молчит. Где искать?
- Протокол подтвердил исполнение — значит код дошёл до digitalWrite. Дальше физика: пин, питание, транзистор. Скорость и буфер тут уже проверены эхом. В железе: пин, питание, проводка
- Сколько вердиктов ok/bad на одну команду печатает эхо-протокол?
- Эхо и вердикт — две строки, но вердикт один. Скрипт на компьютере рассчитывает на точное число ok/bad и рассинхронизируется на лишнем. Ровно один — иначе собеседники собьются со счёта
- Почему телеметрию в командном протоколе печатают реже, чем ответы на команды?
- Шестьсот строк показаний в минуту утопят ответ на команду раньше, чем вы его дочитаете. Телеметрию выносят на запрос GET или редкий период. Поток телеметрии топит команды в логе