Термостат на балконе два месяца жил с порогом, зашитым в скетч: если датчик холоднее 22 градусов — реле щёлкает. Каждая правка порога означала перепрошивку, а перепрошивка — тащить коробку с балкона в комнату и откладывать то, что делается за минуту. Лечится это одним вечером: плата читает строку из монитора порта, первое слово в строке — команда, число после — новый порог. Печатаете SET 18 — и термостат уже живёт с другим порогом, без перепрошивки и без IDE. Весь этот урок про такой обмен: как устроен простейший текстовый протокол из глагола и параметра и почему для него хватает одной цепочки if-else.
Команды с числом вы уже пробовали: Serial.parseInt() выхватывает из порта первое попавшееся число — так работает урок про parseInt и числа из монитора. У подхода есть потолок, и он концептуальный: цифра без имени не говорит, что с ней делать. Рядом с помидорами на грядке приближается 40 — это новый порог? Номер канала? Минуты полива? Одинокое число превращает плату в гадалку. Протокол SET/GET добавляет к числу глагол: SET 40 — присвоить, GET — вернуть. Включать свет словом on вы уже делали в уроке про светодиод из монитора — здесь шаг дальше: команда тащит за собой параметр.
Словарь: глагол и его аргумент
Командная строка — обычный текст до перевода строки: глагол, пробел, число. Читается она целиком через readStringUntil — как принимать строку и почему это удобнее посимвольного чтения, показано в уроке про приём строки. Дальше два движения: проверить, с какого глагола начинается строка, и отрезать глагол, чтобы осталось число. Проверка — через startsWith(), разрез — через substring(): line.substring(3) после SET отбрасывает три символа и оставляет аргумент целиком. Сравнение через == точное по всей строке — оно для GET, у которого аргумента нет и быть не должно.
Словарём я называю цепочку else if, где каждая ветка — один глагол. Звучит примитивно, и это честная примитивность: в конфигураторе поливального стенда таким словарём у меня живут восемь команд, ветки не мешают друг другу и читаются сверху вниз. Ветка SET проверяет аргумент до того, как тронет переменную: пустой аргумент — отказ с примером правильного ввода, число вне диапазона 0-100 — отказ с границей. Плата, которая молча превратит SET 900 в SET 90, однажды зальёт соседям потолок: жёсткие границы в парсере стоят дешевле последствий, и это не фигура речи, а счёт за ремонт.
Неизвестный глагол получает bad с тем, что плата услышала. Это не украшение для красоты в логе: однажды вы будете печатать команды с телефона через Bluetooth, где по дороге теряются буквы, и bad с эхом покажет, что именно доехало до парсера. Про то, что буфер порта читают только когда он не пуст, напоминает урок про available и read — здесь та же дисциплина: сначала available(), потом чтение строки. Читатель, которого зовут в пустоту, будет ждать тайм-аут и подвешивать loop — подробности в следующем уроке.
Глагол я пишу словом, а не одной буквой, и это расчёт, а не вкусовщина. Слово из трёх-пяти букв стоит при 9600 бод несколько миллисекунд по проводу — ничто по сравнению с расшифровкой через месяц: что значит S, а что ST, вспоминать не придётся. Однобуквенные команды оправданы в единственном случае — когда по линии печатает машина и шлёт сотни команд в секунду, там каждый байт на счету. Для человека за клавиатурой читаемый лог дороже пары миллисекунд, и спорить здесь можно только о вкусах.
// Словарь команд: SET <0-100>, GET, STconst int POT = A0;const int LED = 12; int threshold = 50; // порог в процентах, меняет только SET void runCommand(String line) { line = line.trim(); if (line.length() == 0) return; line = line.toUpperCase(); // set 70 и SET 70 для платы одно и то же if (line.startsWith("SET")) { String arg = line.substring(3); // всё после слова SET arg = arg.trim(); if (arg.length() == 0) { Serial.println("bad: SET bez chisla, primer: SET 70"); return; } int v = arg.toInt(); if (v < 0 || v > 100) { Serial.println("bad: chislo tolko 0-100"); return; } threshold = v; Serial.print("ok: threshold = "); Serial.println(threshold); } else if (line == "GET") { Serial.print("ok: threshold = "); Serial.println(threshold); } else if (line == "ST") { Serial.print("st: level = "); Serial.println(map(analogRead(POT), 0, 1023, 0, 100)); } else { Serial.print("bad: ne znayu "); Serial.println(line); }} void setup() { Serial.begin(9600); pinMode(LED, OUTPUT); Serial.println("gotov: SET 0-100, GET, ST");} void loop() { if (Serial.available()) runCommand(Serial.readStringUntil(10)); // 10 = LF int level = map(analogRead(POT), 0, 1023, 0, 100); if (level > threshold) digitalWrite(LED, HIGH); else digitalWrite(LED, LOW); delay(50);}В loop() два занятия: раз в 50 миллисекунд забрать строку из порта, если она пришла, и сравнить уровень с порогом. Скорость здесь не критична — строка лежит в буфере и никуда не убежит, а 50 мс для человека за клавиатурой неотличимы от мгновения. Когда команд станет десяток, а в loop появятся датчики с собственными таймингами, гейт от millis() из урока про millis вместо delay аккуратно разнесёт опрос порта и остальную работу по независимым веткам.
Проверьте скетч на трёх вводах подряд. SET 70 — ok с числом: переменная записана, диод теперь переключается по новому порогу. GET — тот же формат ответа с фактическим значением. SET без числа — bad с примером: это самый частый ввод с телефона одной рукой, и отказ с подсказкой здесь дороже любой проверки на честность слова. Если третий ввод вдруг ответил ok, парсер теряет пустой аргумент — и это находка за пять минут до того, как пустая команда испортит вам настройку в бою.
Скорость порта в таком протоколе — 9600 бод, то есть примерно байт в миллисекунду: команда из двадцати символов доезжает за двадцать мс, незаметно для loop(). Гнаться за 115200 ради клавиатурного ввода смысла нет: человек печатает в тысячи раз медленнее провода, а на длинных линиях и дешёвых переходниках медленные скорости как раз устойчивее. Экономия одного байта на глагол здесь ничего не покупает.
GET и ST: плата отвечает тем же языком
GET — половина протокола, которую чаще всего выбрасывают как лишнюю. А зря: без запроса состояния настройка живёт только в голове платы, и после недели правок вы уже не помните, стоит там порог 40 или 60. Ответ я печатаю одним и тем же форматом для SET и GET: ok: threshold = 40. Однообразная строка, которую и человек прочтёт без легенды, и скрипт на компьютере распарсит по префиксу — это качество протокола, а не скука. Порядок в ответах — фундамент, на котором позже встанет вся отладка логики через порт.
Третья команда словаря — ST: живой уровень с датчика прямо сейчас. GET возвращает настройку, ST — показание, и разделение звучит педантично, пока не выручает первый раз: если ST отвечает, а SET молчит, виноват парсер команд, а не датчик и не провод. Два разных вопроса — два разных ответа, и каждый точно указывает на свою половину схемы. В таблице ниже весь словарь скетча — он умещается в четыре строки и масштабируется добавлением ветки, а не переписыванием.
Отдельная история — первый GET после подачи питания. Переменная threshold держит то, что записал скетч при объявлении: 50. Всё, что вы наслали командами до выключения, ушло вместе с питанием, и GET честно покажет 50 — не потому что плата сбоит, а потому что RAM живёт только под током. Если настройка обязана переживать перезапуск, ей нужно другое место, энергонезависимое, и это отдельная дисциплина со своими правилами. Для старта хватит другого: GET различает настройку по умолчанию и неработающую настройку, и разницу видно с первой секунды.
| Что шлём | Что отвечает плата | Что происходит внутри |
|---|---|---|
| SET 70 | ok: threshold = 70 | аргумент проверен на 0-100, переменная записана, подтверждение тем же форматом, что и GET |
| SET | bad: SET bez chisla, primer: SET 70 | аргумент пустой: переменная не тронута, вводу подсказывают правильную форму |
| GET | ok: threshold = 40 | возвращает фактическое состояние — то, что хранится, а не то, что вы думали |
| QQQ | bad: ne znayu QQQ | неизвестный глагол: в отказ уходит эхо строки, чтобы было видно, что доехало по проводу |
- 1. Что содержит первый токен командной строки SET 40?
- 2. Чем отрезать глагол SET от аргумента в строке?
- 3. Плата получила SET 900 при допустимом диапазоне 0-100. Какой ответ правильный?
- 4. Зачем GET, если SET и так печатает подтверждение с числом?
- 5. Монитор досылает CR, до плата доходит строка GET\r, словарь отвечает bad. Как чинить?
- Строка команды = глагол + аргумент: SET меняет настройку, GET возвращает, ST показывает живое значение с датчика
- Словарь — цепочка else if: каждая ветка проверяет аргумент до записи в переменную
- Границы диапазона держит парсер: SET 900 получает bad, состояние платы не трогается
- Неизвестный глагол получает bad с эхом строки — сразу видно, что доехало по проводу
- trim() и toUpperCase() в начале парсера снимают хвосты и капризы регистра один раз за всё время
Частые вопросы по теме урока
- Что содержит первый токен командной строки SET 40?
- Первое слово — глагол: он говорит, что делать. Число 40 — аргумент, а имя переменной плата узнаёт из своей ветки словаря, по проводу оно не ходит. Глагол команды
- Чем отрезать глагол SET от аргумента в строке?
- substring(3) отбрасывает S, E и T и оставляет аргумент. parseInt здесь не годится: он не отличит SET от GET, ведь оба слова начинаются не с цифры. line.substring(3) — всё после первых трёх символов
- Плата получила SET 900 при допустимом диапазоне 0-100. Какой ответ правильный?
- Границы проверяет парсер до записи: отказ с причиной и нетронутое состояние. Молчаливые исправления ввода хороши, пока не зальют соседям потолок. bad с указанием границы и переменная без изменений
- Зачем GET, если SET и так печатает подтверждение с числом?
- Подтверждение говорит о последней вашей команде, GET — о факте. Между ними порог мог поменять второй оператор, скрипт или сбой питания с перезапуском. Порог мог изменить кто-то другой — GET вернёт фактическое состояние
- Монитор досылает CR, до плата доходит строка GET\r, словарь отвечает bad. Как чинить?
- CR — символ в конце строки, и сравнение с GET не совпадает. trim() в начале runCommand убирает пробелы и невидимые символы по краям — и скорость тут ни при чём. trim() в начале парсера срезает невидимые хвосты