Встроенный Serial Plotter рисует линию красиво, но это чёрный ящик: ни подписать ось, ни нарисовать полоску-гейдж, ни сменить цвет при выходе за порог нельзя. Заказчик моего последнего проекта сформулировал задачу коротко: окно, в котором уровень из бака виден с трёх метров, — полоска во всю ширину и цифра крупно. Plotter такое не умеет в принципе, Excel — не про реальное время. Инструмент, которым это собирается за вечер, — Processing: бесплатная среда с processing.org, младший брат Arduino IDE, и приёмник там умещается в двадцать строк.
План такой: договориться о минимальном протоколе v:<число> с переводом строки, написать платную часть на потенциометре, собрать приёмник в Processing — полоску и цифру — и честно разметить границу: что видно в симуляторе на этой странице, а что живёт только на настоящей паре плата плюс компьютер. Приём чисел из порта мы уже делали в уроке про parseInt — здесь направление обратное: поток идёт с платы наружу.
Почему не Python? Можно и Python: скрипт из урока про pyserial рисует график тем же matplotlib. Processing я выбираю, когда нужен живой интерфейс с полоской и цифрами: среда заточена под рисование, draw() выдаёт 60 кадров в секунду из коробки, скетч запускается кнопкой Run без установки пакетов и зависимостей. Для разового графика разницы почти нет; для окна, которое висит на стенде месяцами, простота запуска решает всё.
Протокол v:NN — договор на шесть байт
Каждая посылка — строка вида v:87 плюс перевод строки. Префикс v: нужен не для красоты: если плата печатает ещё и служебные сообщения, приёмник по первым двум символам отличает показания от болтовни и молча пропускает чужие строки. Перевод строки — граница посылки: без него приёмник не отличит конец числа от середины, и поток слипнется в кашу. Скорости 9600 бод хватает с запасом: строка v:100 с переводом весит семь байт, при пяти посылках в секунду это 35 байт в секунду против 960 доступных — канал занят на три-четыре процента.
Про длину посылки и границы чисел. Проценты 0-100 занимают от одного до трёх знаков, и посылка дышит от четырёх до семи байт — приёмнику всё равно, он читает до терминатора, а не до фиксированной длины. Понадобится точность выше процента — map переедет на 0-1000, префикс останется прежним, и старый приёмник продолжит понимать поток, только цифры станут точнее. Обратно совместимые изменения протокола экономят часы: плата и приёмник обновляются вразнобой, без синхронного выката.
На стороне платы всё собирается в три строки: analogRead с A0, map в проценты 0-100, печать с префиксом. Светодиод на пине 12 я вешаю как живой индикатор порога: выше пятидесяти — горит, ниже — гаснет. Это одновременно проверка и задел: когда визуализатор на компьютере отвалится, порог продолжит работать на самой плате — у прибора остаётся собственная сигнализация, не зависящая от чужого окна.
// Платная часть протокола: потенциометр A0 -> v:<число>const int LED_PIN = 12; void setup() { Serial.begin(9600); pinMode(LED_PIN, OUTPUT); Serial.println("Stend: pot na A0, protokol v:<chislo>");} void loop() { int syroe = analogRead(A0); // 0..1023 int procenty = map(syroe, 0, 1023, 0, 100); Serial.print("v:"); Serial.println(procenty); // посылка: v:NN + перевод строки if (procenty > 50) { digitalWrite(LED_PIN, HIGH); // порог живёт на плате } else { digitalWrite(LED_PIN, LOW); } delay(200); // пять посылок в секунду}Приёмник в Processing: двадцать строк
Processing ставится с processing.org одним архивом и выглядит как Arduino IDE: тот же формат скетча, только вместо setup()/loop() здесь setup()/draw() — setup выполняется один раз, draw крутится 60 раз в секунду. Библиотека Serial встроена, и первым делом я запускаю printArray(Serial.list()): метод печатает все свободные порты с номерами и отвечает на вечный вопрос, под каким именем пришла плата. Угадывать индекс наугад — самая дешёвая дорога к пустому окну при живом скетче.
Дальше включается bufferUntil(10): библиотека копит байты до перевода строки и только потом будит обработчик serialEvent — без неё строку пришлось бы клеить из байтов вручную, как мы делали в уроке про буфер UART. В обработчике readUntil(10) отдаёт готовую строку (в старых версиях библиотеки метод звался readStringUntil — суть та же: прочитать всё до символа-терминатора), trim срезает хвосты, префикс проверяется startsWith, число берётся substring-ом с третьего символа. Всё остальное — рисование: фон, полоска шириной от значения, цифра сверху.
Среда тут — обычный редактор с кнопкой Run: скетч не шьётся никуда, он исполняется на компьютере, и перезапуск занимает секунду. Сообщения компилятора читаются внизу окна так же, как в Arduino IDE, и первые из них будут про типы: int в Processing — тридцатидвухбитное целое, дробь печатается функцией nf() с нужным числом знаков. Если окно запустилось, а полоска стоит на нуле, разрыв между платой и приёмником ищите в консоли: printArray печатал список портов при старте — сверяйте, тот ли номер уплыл в new Serial.
// Processing 4: полоска уровня из потока v:<число>import processing.serial.*; Serial serialPort;char TERMINATOR = 10; // 10 = перевод строкиint uroven = 0; // 0..100 с платы void setup() { size(420, 140); // окно 420 на 140 пикселей printArray(Serial.list()); // какой номер у платы? serialPort = new Serial(this, Serial.list()[0], 9600); serialPort.bufferUntil(TERMINATOR);} void draw() { background(30); fill(80, 200, 120); rect(20, 60, uroven * 3.8, 40); // 100% = 380 пикселей fill(255); text("uroven: " + uroven + " %", 20, 40);} void serialEvent(Serial port) { String s = serialPort.readUntil(TERMINATOR); if (s != null) { s = trim(s); if (s.startsWith("v:")) { uroven = int(s.substring(2)); } }}Читайте скетч слева направо: size задаёт окно, printArray честно печатает порты, new Serial открывает плату на выбранном номере при тех же 9600 бод, что в Serial.begin(). bufferUntil(10) — главный секрет спокойствия: без него serialEvent дёргался бы на каждом байте, с ним обработчик получает готовую строку раз в 200 миллисекунд, ровно в темпе платы. draw() при этом крутится без остановки все свои 60 кадров в секунду, и полоска ездит плавно между посылками — обработка и рисование живут независимо.
До графика один шаг: завести массив на сотню значений, на каждой посылке сдвигать его влево, а в draw() пробегать line() от точки к точке. Полоска и история живут в одном окне — это уже приборный интерфейс, а не учебная поделка. Второй канал добавляется очевидно: префиксы v: и p:, приёмник ветвит по substring(0,2). Разбор строк на стороне Arduino мы упражняли в уроке про strtok и atoi — дисциплина та же, только там плата читает, а здесь компьютер. Если же цель — не живой луч, а журнал с фильтрами, то это дорога в урок про Excel-лог: инструменты дополняют друг друга, а не заменяют.
Про производительность честно: line() в цикле по сотне точек — копейки для Processing, но каждая посылка сдвигает массив целиком, и на сотнях посылок в секунду такой приём упирается в потолок. Моя рабочая зона — десятки посылок в секунду: визуализация живого процесса, а не осциллограф. Настоящая скоростная телеметрия требует двоичной упаковки и буферизации — об этом говорит урок про двоичный протокол, здесь же цель — понятный текстовый поток, который читается глазами прямо в логе.
| Инструмент | Что рисует | Где живёт код |
|---|---|---|
| Serial Plotter из IDE | живая линия без единой настройки | нигде: часть среды |
| Processing-скетч из урока | полоска, график, свой интерфейс с подписями | 20 строк на компьютере |
| Excel-лог из соседнего урока | таблицу и графики по столбцам | формат строки на плате |
| Python + pyserial | график или файл по расписанию | скрипт на компьютере |
Честная граница урока: в симуляторе вы видели только платную половину пары. Скетч Processing исполняется на компьютере — в браузере его нет, как нет и окна с полоской. Зато поток v:NN в логе симулятора — в точности тот байтовый поток, который уходит в настоящий порт, и на живой связке Uno плюс ноутбук приёмник из урока съедает его без правок: скорость, префикс и терминатор уже согласованы.
Порядок внедрения, который я использую: сначала стенд в симуляторе и разбор лога, потом скетч на плату, потом Processing на ноутбук. Каждый шаг проверяется отдельно, и когда связка не заводится, виноватый локализован заранее: поток с платы смотрят монитором порта, приёмник — выводом в консоль. Так за один вечер собирается и полоска, и график, и ничего не переписывается дважды.
- 1. Из чего состоит одна посылка протокола из урока?
- 2. Зачем приёмнику bufferUntil(10)?
- 3. Как соотносятся setup() и draw() в Processing?
- 4. Светодиод на пине 12 в этом уроке нужен, чтобы...
- 5. Строка v:100 уходит пять раз в секунду на 9600 бод. Как загружен порт?
- Протокол v:NN с переводом строки: префикс отделяет данные от болтовни, перевод строки закрывает посылку.
- На плате три строки: analogRead, map в проценты, печать с префиксом; светодиод держит порог независимо от компьютера.
- Processing: printArray(Serial.list()) отвечает, где плата; bufferUntil(10) и readUntil(10) собирают готовые строки.
- draw() крутится 60 раз в секунду сам — полоска ездит плавно между посылками раз в 200 мс.
- До графика один шаг: массив на сотню значений и line() от точки к точке; второй канал — новый префикс.
Частые вопросы по теме урока
- Из чего состоит одна посылка протокола из урока?
- Префикс v: отличает данные от служебных строк, перевод строки закрывает посылку. Двоичные варианты — тема урока про двоичный протокол, там всё строже. строка v:87 и перевод строки
- Зачем приёмнику bufferUntil(10)?
- Библиотека копит байты до перевода строки и затем отдаёт готовую посылку в serialEvent — без этого строку пришлось бы собирать вручную. чтобы обработчик будили только на готовую строку, а не на каждый байт
- Как соотносятся setup() и draw() в Processing?
- Setup выполняется при старте, draw живёт в цикле с потолком 60 кадров в секунду — прямой аналог setup()/loop() на плате. setup один раз, draw — 60 раз в секунду
- Светодиод на пине 12 в этом уроке нужен, чтобы...
- Порог выше пятидесяти обслуживает сама плата: визуализатор отвалился — сигнализация осталась. Это разделение ответственности платы и компьютера. порог работал даже без компьютера
- Строка v:100 уходит пять раз в секунду на 9600 бод. Как загружен порт?
- Семь байт на посылку умножить на пять — 35 байт в секунду при ёмкости 960. Запас десятикратный, менять скорость незачем. около 35 байт в секунду — три-четыре процента от 960