Отладка на объекте всегда упиралась в одно и то же: устройство спрятано в короб на высоте двух метров, Serial-монитор живёт в ноутбуке, а ноутбук — в машине, через двор и лужу. Приезжать ради трёх строк лога — расточительство, но без этих строк непонятно, жив ли прибор вообще. Решение — терминал на самом корпусе: OLED показывает последние пять строк из Serial-потока, и диагноз ставится на месте, открыв люк и не распаковывая рюкзак.
Собираем такой терминал: строки принимаются через Serial.readStringUntil(' '), ложатся в кольцевой буфер на пять записей, длинные режутся на куски по 21 символу — столько влезает в строку мелкого шрифта. Кнопка чистит экран, светодиод вспыхивает на каждую принятую строку — с другого конца гаража видно, что терминал жив и поток идёт.
По механике порта я уже писал в уроке про первый Serial-монитор — там монитор разобран с ноутбуком; здесь ноутбук отрезан, и его роль берёт на себя экран. Если терминал захочет не только показывать, но и слушать команды, разбор чисел из строк — в уроке про parseInt.
Кольцо на пять строк и перенос длинных
Кольцо — массив из пяти String и указатель: stroki[head] = s; head = (head + 1) % 5. Перезаписывается всегда самая старая строка, поэтому экран показывает хвост потока — то, что происходит сейчас. Пять строк — не жадность, а расчёт: мелкий шрифт даёт восемь строк на экран, пять уходит под лог, строка внизу — счётчик принятых, остальное — воздух. Хотите семь — отнимите сервисные строки, но не уплотняйте вплотную: экран без полей читается хуже.
Строка длиннее 21 символа не влезает: режем substring'ом на куски, и каждый кусок кладётся в кольцо отдельной записью. Сообщение из сорока символов займёт две ячейки — честная цена экрана шириной 128 точек; кольцо поэтому берут на пять-семь записей, а не на пять сообщений. Раскладку сетки и поведение курсора print я разбирал в уроке про текст на OLED — здесь та же сетка, только строки идут снизу вверх по мере поступления.
| Что выводить | Ритм | Зачем на экране |
|---|---|---|
| Ответы на команды | по событию | видно, что команда дошла и что ответила плата |
| Ключевые значения датчика | раз в 10-60 с | тренд последних пяти записей без графика |
| События кнопок и режимов | по событию | восстанавливает хронологию: что нажимали и когда |
| Сбои и откаты | по событию | диагноз на месте, без ноутбука |
Приём строки — самое узкое место. readStringUntil(' ') читает до терминатора, а если его нет — ждёт таймаут, по умолчанию тысячу миллисекунд. Дёргать его на каждом проходе loop нельзя: пустой порт будет съедать секунду на проход, и кнопка с курсором оживут раз в секунду. Ставьте чтение за if (Serial.available() > 0) — или меняйте Serial.setTimeout осознанно, если источник шлёт строку рыхло, по символу.
Второй потолок — RX-буфер Serial на 64 байта: пачка длинных строк, которая приходит быстрее, чем loop успевает разбирать, теряет хвост. Лечится ритмом источника: строки лога длиной до полусотни символов идут не чаще пары в секунду — 64 байта держат три-четыре строки запаса, и ничего не теряется. Плотнее — значит, часть строк должна молчать: терминал показывает решения, а не поток телеметрии.
#include <Wire.h>#include <Adafruit_GFX.h>#include <Adafruit_SSD1306.h> Adafruit_SSD1306 display(128, 64, &Wire, -1);const int LED = 12;const int BTN = 2; String stroki[5];int head = 0;int n = 0; // сколько записей в кольцеlong vsego = 0;long ledUntil = 0;int prevBtn = HIGH; void zapisi(String s) { stroki[head] = s; head = (head + 1) % 5; if (n < 5) n++; vsego++; digitalWrite(LED, HIGH); // LED-курсор: вспышка на строку ledUntil = millis() + 50;} void dobavit(String s) { while (s.length() > 21) { // перенос длинных по 21 символу zapisi(s.substring(0, 21)); s = s.substring(21); } if (s.length() > 0) zapisi(s);} void risuj() { display.clearDisplay(); display.setTextSize(1); int start = head; // при неполном кольце - с нулевой ячейки if (n < 5) start = 0; for (int i = 0; i < 5; i++) { display.setCursor(0, i * 8); if (i < n) display.print(stroki[(start + i) % 5]); } display.setCursor(0, 56); display.print("vsego: " + String(vsego)); display.display();} void setup() { Serial.begin(9600); pinMode(LED, OUTPUT); pinMode(BTN, INPUT_PULLUP); display.begin(SSD1306_SWITCHCAPVCC, 0x3C); display.setTextColor(SSD1306_WHITE); dobavit("TERMINAL GOTOV"); risuj();} void loop() { if (Serial.available() > 0) { String s = Serial.readStringUntil(''); s.trim(); if (s.length() > 0) { dobavit(s); risuj(); } } if (ledUntil != 0 && millis() > ledUntil) { digitalWrite(LED, LOW); ledUntil = 0; } int btn = digitalRead(BTN); if (btn == LOW && prevBtn == HIGH) { // кнопка: чистим кольцо head = 0; n = 0; vsego = 0; for (int i = 0; i < 5; i++) stroki[i] = ""; risuj(); Serial.println("--- ochishcheno ---"); } prevBtn = btn;}Кнопка-очистка и LED-курсор
Кнопка на D2 чистит кольцо по фронту: массив заполняется пустыми строками, счётчик обнуляется, экран перерисовывается — прибор готов принимать новый сценарий. Антидребезг здесь минимальный: перерисовка занимает десятки миллисекунд, и дребезг за это время затихает сам; если у вас рыхлая кнопка — та же схема с отсечкой из урока про антидребезг.
LED-курсор — светодиод на D12, вспыхивающий на 50 мс при каждой принятой строке. Это ответ на вопрос, который я слышу с другого конца стола: «оно вообще живое?» Экран не всегда виден под углом, а огонёк — виден. Вспышка гасится по разности millis, без delay: блокирующая пауза внутри приёма строки притормозила бы разбор. Общий фон несинхронного loop — схема на millis.
Что печатать в такой терминал, чтобы он помогал: не сырые цифры каждые сто миллисекунд, а решения и события. POMPA ON, NET SVYAZI, NIZHNYAYA DVER — пять таких строк отвечают на вопрос «что он там делает» точнее графика. Значения датчиков — раз в десятки секунд; плотнее — кольцо переполнится, и нужная строка уедет за горизонт раньше, чем вы поднимете глаза от люка.
Границы приёма честные: пять строк по 21 символу — витрина хвоста, а не журнал. Историю на часы пишет SD-карта — буфер строк в RAM перед пакетной записью закрывает паузы между сессиями; терминал же закрывает другое: взгляд на работающий прибор без единого провода. Если захочется перерисовывать только изменившиеся строки — частичное обновление снизит и без того скромный трафик текстового экрана.
- readStringUntil('\n') читает до терминатора или до таймаута 1000 мс: чтение всегда за if (Serial.available() > 0), иначе loop ходит с секундным тактом
- Кольцо на пять строк: stroki[head] = s; head = (head + 1) % 5 — экран показывает хвост потока, старое уходит само
- Строку длиннее 21 символа режем substring'ом на куски: сообщение из 40 знаков займёт две ячейки кольца
- LED-курсор: вспышка 50 мс на строку по разности millis — живость терминала видна с другого конца стола
- Кнопка по фронту чистит кольцо; пять строк по 21 символу — витрина хвоста, а не журнал: историю пишет SD-логгер
- 1. Сколько времени readStringUntil ждёт терминатор по умолчанию?
- 2. Кольцо на пять строк: что лежит на экране?
- 3. Строка из 40 символов пришла в терминал со строкой экрана в 21 символ. Что станет?
- 4. Зачем LED вспыхивает на 50 мс при каждой строке?
- 5. Что печатать в OLED-терминал, чтобы он помогал на объекте?
Частые вопросы по теме урока
- Сколько времени readStringUntil ждёт терминатор по умолчанию?
- Таймаут Serial.setTimeout по умолчанию — 1000 мс. Пустой порт на каждом проходе loop превратит скетч в секундный такт — чтение ставят за available(). 1000 мс
- Кольцо на пять строк: что лежит на экране?
- Перезаписывается всегда самая старая запись, поэтому экран показывает хвост потока — последние пять физических строк, включая куски переноса. последние пять строк потока
- Строка из 40 символов пришла в терминал со строкой экрана в 21 символ. Что станет?
- Длинная строка режется substring'ом по 21 символу, каждый кусок — отдельная ячейка кольца. Сообщение из 40 знаков занимает две записи. ляжет в кольцо двумя записями по 21 и 19 символов
- Зачем LED вспыхивает на 50 мс при каждой строке?
- Экран не всегда виден под углом, а огонёк — виден с другого конца стола: вспышка на строку отвечает на вопрос «оно вообще живое?». показывает с расстояния, что поток идёт
- Что печатать в OLED-терминал, чтобы он помогал на объекте?
- Кольцо мелкое — плотный поток выталкивает нужные строки за горизонт. Решения и события с редкими значениями датчиков отвечают на вопрос «что он там делает». решения и события: POMPA ON, NET SVYAZI