Гирлянда в окне к декабрю обросла пожеланиями всех домашних: жена просит выключать из-за кровати, я — менять цвет под настроение, кот просто ходит мимо кнопки. Первая версия умела ровно одну команду on из монитора, вторая — уже пять команд, но отвечала кому попало и молчала на опечатки. Когда переключение цвета сработало трижды подряд от случайного нажатия, я сел и собрал протокол целиком: со словарём, тремя видами ответов и справкой. Это итоговый проект сериального раздела — здесь встречаются парсер, буфер, приём строк и дисциплина ответов из всех предыдущих уроков. Гирлянда взята для конкретики: подставьте реле насоса, сервопривод шторы или вентилятор — словарь останется тем же, поменяются только ветки исполнения.
Требования соберу списком, по ним удобно проверять себя. Команда — слово и, через пробел, параметр. Три вида ответов: OK — принято и исполнено, с фактом; ERR — команда знакома, но параметр плохой; UNKNOWN — слова в словаре нет. На каждую команду — ровно один ответ, молчания не бывает: неделя без ответов в журнале, и вы уже не помните, дошла ли команда. help печатает словарь сам, status отдаёт состояние, ничего не меняя. По кусочкам всё это мы собирали в уроке про командный парсер и приёме строк через readStringUntil — теперь сложим в один работающий организм. Список выглядит скромно, но это весь каркас, на котором позже встанут и Bluetooth-клиент, и сохранение настроек, и пинг из урока про связь двух плат.
Словарь протокола: OK, ERR и UNKNOWN
Грамматика держится на трёх правилах. Первое: ответ всегда начинается с вердикта — OK, ERR или UNKNOWN, дальше через двоеточие имя команды и пары имя=значение. Пары — не украшение: их читает и человек в мониторе, и плоттер, а ряд color= можно смотреть как график настроений. Второе правило разделяет ERR и UNKNOWN по смыслу. ERR значит: команда есть, но color 9 вне диапазона — клиент ошибся параметром и может поправить сам. UNKNOWN значит: слова нет вообще, и в журнал честно падает то, что плата получила. Половина моих опечаток была в кириллической раскладке — без эха я бы их не нашёл. Разница между ними — разница между «поправь параметр» и «такой команды нет»: первый ответ экономит второй заход, второй — возвращает вас к справке.
Третье правило: help — часть протокола, а не вежливость. Плата, которая сама рассказывает словарь, экономит вам документацию в блокноте: подключил Bluetooth-модуль, набрал help — и работай. Я держу help даже в прошивках для себя: через год забываешь собственные команды так же надёжно, как чужие. status — второй служебный: он ничего не меняет, но говорит, где плата сейчас, парами power= и color=. Та же дисциплина пар, что в таблице данных через порт из предыдущих уроков: статус должен читаться глазами без знаний о коде.
Про внутренности исполнителя скажу пару слов, потому что они же в коде ниже. В C++ switch не работает со String, поэтому слово из команды я превращаю в номер через цепочку if или функцию-словарь — оба приёма разобраны в уроке про командный парсер. Параметр идёт через toInt() и проверяется на диапазон до исполнения: гирлянда обязана отвечать ERR на color 9, а не делать «что-нибудь». Пары имя=значение печатаются цепочкой print и одним println в конце — так строка ответа собирается за один проход и не рвётся между клиентами.
| Команда | Пример | Ответ платы |
|---|---|---|
| help | help | OK help: on off color 0-4 status |
| on | on | OK on: power=1 |
| off | off | OK off: power=0 |
| color N | color 2 | OK color: color=2 imya=SIN |
| color вне диапазона | color 9 | ERR color: index 0-4 |
| нет в словаре | vkuchi svet | UNKNOWN vkuchi svet |
// Girlyanda: help, on, off, color 0-4, status.// LED na pine 12 — girlyanda, knopka na 2 — vtoroi klient.// Vetku color na realnoy girlyande zamenyaet lenta WS2812 (fill_solid).int power = 0;int cvet = 0;int knopkaByla = 0; String imyaCveta(int n) { if (n == 0) return "KRAS"; if (n == 1) return "ZEL"; if (n == 2) return "SIN"; if (n == 3) return "ZHOL"; return "BEL";} void ispolni(String kto, String cmd) { // odna tochka pravdy int probel = cmd.indexOf(" "); String slovo = cmd; String param = ""; if (probel > 0) { slovo = cmd.substring(0, probel); param = cmd.substring(probel + 1); } if (slovo == "help") { Serial.print(kto); Serial.println("> OK help: on off color 0-4 status"); } else if (slovo == "on") { power = 1; digitalWrite(12, HIGH); Serial.print(kto); Serial.println("> OK on: power=1"); } else if (slovo == "off") { power = 0; digitalWrite(12, LOW); Serial.print(kto); Serial.println("> OK off: power=0"); } else if (slovo == "color") { int n = param.toInt(); if (n >= 0 && n <= 4) { cvet = n; Serial.print(kto); Serial.print("> OK color: color="); Serial.print(cvet); Serial.print(" imya="); Serial.println(imyaCveta(cvet)); } else { Serial.print(kto); Serial.println("> ERR color: index 0-4"); } } else if (slovo == "status") { Serial.print(kto); Serial.print("> OK status: power="); Serial.print(power); Serial.print(" color="); Serial.println(cvet); } else { Serial.print(kto); Serial.print("> UNKNOWN "); Serial.println(cmd); }} void setup() { Serial.begin(9600); pinMode(12, OUTPUT); pinMode(2, INPUT); Serial.println("Girlyanda. Komandy: help, on, off, color 0-4, status");} void loop() { if (Serial.available() > 0) { String line = Serial.readStringUntil(10); line = line.trim(); if (line.length() > 0) { Serial.print("USB> "); Serial.println(line); ispolni("USB", line); } } int knopka = digitalRead(2); if (knopka == HIGH && knopkaByla == LOW) { // front: nazhali String c = "off"; if (power == 0) c = "on"; Serial.print("BTN> "); Serial.println(c); ispolni("BTN", c); } knopkaByla = knopka; delay(30); // zashchita ot drebezga}Кнопка как второй клиент протокола
Кнопка на пине 2 — не дубликат кода, а второй клиент. Когда-то я вешал на неё digitalWrite напрямую — и получал два источника правды: монитор считает, что гирлянда выключена, кнопка только что её включила, а status об этом не знает. Теперь кнопка не трогает светодиод сама: на фронте нажатия она отправляет в тот же исполнитель ispolni() строку on или off с меткой BTN, и журнал показывает, кто именно менял состояние. Одна точка правды: любое изменение проходит через исполнитель, поэтому status не расходится ни с журналом, ни со светодиодом.
Дребезг обрабатываем по-простому: после фронта — delay(30) и запомненное прошлое состояние; механику разобрали в уроке про дребезг кнопки. Дальше протокол растёт без переписывания: добавили HC-05 на SoftwareSerial — клиент BT вызывает ispolni со своей меткой, и гирляндой командуют из телефона; захотели помнить цвет между включениями — команда save с байтом в EEPROM из соседнего раздела. За это я и люблю протоколы: третий клиент — три строки кода, а не вторая копия логики.
Метка источника в ответе — не декорация, а журнал на вырост. Через месяц эксплуатации кто-нибудь спросит, кто включал гирлянду в три ночи, — и строки BTN> on с ответом OK on: power=1 ответят сами, без алиби и догадок. Я после этого случая печатаю метку и в OK, и в ERR, и в UNKNOWN: по журналу видно и кто командовал, и на чём спотыкался. Цена вопроса — один лишний print в ветке исполнителя, зато история событий собирается сама, без часов реального времени и модулей логирования.
В симуляторе протокол живёт целиком: LED на пине 12 играет роль гирлянды, кнопка на пине 2 — второй клиент. Оговорка про цвет: моя настоящая гирлянда стоит на WS2812, и color N зажигает пресет на ленте; в модели цвет — поле протокола, по нему отвечают OK и status, а питание показывает светодиод. Логику разговора это не трогает: команда, вердикт, пары имя=значение — на ленте или на одиночном LED разница только в ветке исполнения.
Прогоните по списку ниже: сначала команды с клавиатуры, потом кнопкой, потом status — и по журналу проверьте, что каждый ответ подписан, кто его вызвал: USB или BTN. Буфер на 64 байта и правило вычерпывания никуда не делись — урок про буфер UART объясняет, что будет, если слать команды пачками: очередь выравнивается, а ответы идут по одному, и это правильно.
И один эксперимент на перенос: попробуйте в ответе исполнителя заменить метку USB на CONSOLE — журнал изменится, а поведение протокола останется прежним. Это и есть проверка того, что разбор, словарь и ответы не срослись с источником: на плате вы сможете назвать клиента BT, GSM или TIMER, и ни одна строка логики не поедет следом. Итоговый проект тем и хорош, что после него командный разбор вы собираете по памяти, как таблицу умножения: словарь на бумаге, три вердикта, один исполнитель — и любое устройство отвечает так, что по журналу видно всё.
- Отправьте help и сравните ответ со словарём из таблицы: справка платы и словарь обязаны совпадать слово в слово.
- on, status, color 2, status: после каждой команды status отвечает новым состоянием, а ряд color= в плоттере можно смотреть как график.
- color 9 и любое выдуманное слово: ERR означает плохой параметр, UNKNOWN — слово вне словаря; молчания быть не должно.
- Нажмите кнопку при включённой и при выключенной гирлянде: в журнале видна метка BTN> и ответ OK — кто менял состояние, всегда подписан.
- Перенесите скетч на плату: LED на 12, кнопка на 2, а вместо ветки color — fill_solid на ленте WS2812; разбор строк и ответы остаются без правок.
- Протокол — это словарь плюс три вердикта: OK с парами имя=значение, ERR для плохого параметра, UNKNOWN с эхом неизвестного слова
- help и status — часть протокола: плата сама рассказывает словарь и отдаёт состояние, ничего не меняя
- Один исполнитель ispolni() на всех клиентов: Serial, кнопка и будущий Bluetooth вызывают его с разными метками
- Кнопка шлёт on и off через протокол: журнал показывает, кто менял состояние, status никогда не расходится с LED
- Расширение без переписывания: BT-клиент — три строки, save в EEPROM — одна команда в словаре
- 1. Чем ERR отличается от UNKNOWN в этом протоколе?
- 2. Кнопка меняет состояние гирлянды. Как правильно?
- 3. Что ответит плата на команду color 12?
- 4. Зачем help входит в словарь протокола?
- 5. Почему после color N стоит отправлять status?
Частые вопросы по теме урока
- Чем ERR отличается от UNKNOWN в этом протоколе?
- ERR говорит клиенту: поправь параметр, команда знакома. UNKNOWN честно возвращает полученный мусор — по эху видно опечатку и раскладку. ERR — команда есть, но параметр плохой; UNKNOWN — слова нет в словаре
- Кнопка меняет состояние гирлянды. Как правильно?
- Прямой digitalWrite создаёт второй источник правды: status разойдётся с реальностью. Кнопка отправляет команду через общий исполнитель — как Serial, только с меткой BTN. Через тот же исполнитель ispolni с меткой BTN: одна точка правды
- Что ответит плата на команду color 12?
- Слово color есть в словаре, параметр вне диапазона — это ERR с подсказкой. UNKNOWN достался бы выдуманному слову вроде vkuchi. ERR color: index 0-4
- Зачем help входит в словарь протокола?
- Подключили Bluetooth, набрали help — и работаете. Справка на плате заменяет документацию в блокноте и живёт вместе с прошивкой. Плата сама рассказывает словарь: новый клиент подключается без документации
- Почему после color N стоит отправлять status?
- status ничего не меняет, но отдаёт пары имя=значение: так подтверждают, что команда дошла и была исполнена так, а не иначе. Пары power= и color= показывают фактическое состояние после команды