Эксперимент с поливом длился две недели, и все эти дни в монитор лился поток: строки телеметрии, мои же служебные сообщения rele on и rele off, ответы на команды. Разбирать вечерами — боль: где кончается шум и начинается суть? Второй заход был честнее. Плата молчит, пока я не нажму кнопку; с нажатия печатается маркер, шапка и строки с метками; со второго нажатия — стоп-маркер с итогом. Блок между маркерами уезжает в таблицу целиком, и в нём нет ни одной лишней строки. Здесь — прошивка такого рекордера: флаг записи, метки millis, маркеры и дорога блока в таблицу.
Протокол рекордера — четыре строки-события. #LOG BEGIN открывает сессию, за ним шапка millis,uroven — будущие заголовки столбцов. Каждая строка лога — метка, запятая, значение — печатается сразу, по ходу дела, и это принципиально: у Uno в запасе два килобайта RAM, и String-копилка на сотнях строк кончается тихо, без всяких сообщений, просто со странными числами в конце блока. #LOG END закрывает сессию и печатает итог: сколько строк записано и сколько секунд длилась запись. Скрипт с компьютера из урока про Python и pyserial ищет маркеры и пишет в файл только то, что между ними, — разговорчивый поток вокруг лога ему не мешает.
Маркеры — не украшение, а ответ на вопрос «а что это за строка?». Без них приёмник вынужден верить, что поток состоит только из лога: одна служебная строка — и разрез по запятой поедет мимо. С ними сессия самодостаточна: даже через год файл с логом скажет, где начался, чем кончился и сколько честных строк внутри. Я пробовал версию без маркеров — короткую, временную, — и нашёл её через год в архиве без единой подсказки.
Метки: millis против настенных часов
Метка millis() монотонна и точна до миллисекунды, и для графика это ровно то, что нужно: ось X равномерная, потому что период держит планировщик на millis — урок про millis вместо delay объясняет, почему delay здесь ломал бы и темп записей, и кнопки. Настенного времени у метки нет: привязку делаете при разборе, время старта пишете в соседнюю ячейку. Честная граница: millis переполняется через 49,7 суток, после чего метки пойдут с меньших чисел, — для сессий на минуты и часы это неважно, а для многодневных логов метку берут у часов реального времени.
Привязка к часам делается в две ячейки: рядом с блоком в листе пишете время старта сессии, а соседний столбец пересчитывает формулой старт плюс секунды. Печатать настенное время самой платой — вариант для длинных логов, но для минутных сессий это лишняя зависимость от модуля часов и его батарейки: millis дешевле, точнее и уже есть на любой плате.
Миллисекундная точность стоит места: millis растёт до десяти цифр, и на часовой сессии метка занимает до десяти байтов из шестнадцати — дороже значения. Это нормально: ось времени — половина пользы лога, и сжимать её в байтовый кадр из прошлого урока стоит только когда канал впритык. Секунды вместо миллисекунд — компромисс, который режет часть графиков: всплеск между замерами на границе полусекунды превратится в ровный ряд, и вы его никогда не увидите. Период записи 500 мс и строка из пятнадцати-восемнадцати символов — около 30 байтов в секунду, три процента канала 9600; часовая сессия даст 7200 строк и около 115 КБ — копипастом из монитора уже тяжело, скриптом — автоматически.
// Рекордер: кнопка D2 или команды rec/stop включают сессию лога.// Строки печатаются по ходу дела, RAM не копится. Метки — millis().const int BTN = 2; // кнопка на GND, pinMode INPUT_PULLUPconst int POT = A0;const int LED = 12; // горит, пока идёт запись int zapis = 0;int btnPred = 1; // прошлое состояние кнопки: 1 = отпущенаunsigned long poslednyayaStroka = 0;unsigned long startSessii = 0;int strok = 0; void nachatSessiyu() { zapis = 1; strok = 0; startSessii = millis(); poslednyayaStroka = millis(); Serial.println("#LOG BEGIN"); Serial.println("millis,uroven"); // шапка — будущие заголовки столбцов digitalWrite(LED, HIGH);} void zakonchitSessiyu() { zapis = 0; Serial.println("#LOG END"); Serial.print("itog: strok="); Serial.print(strok); Serial.print(" dlit="); Serial.print((millis() - startSessii) / 1000); Serial.println(" sek"); digitalWrite(LED, LOW);} void primeni(String line) { line = line.trim(); if (line == "rec" && zapis == 0) nachatSessiyu(); if (line == "stop" && zapis == 1) zakonchitSessiyu();} void setup() { Serial.begin(9600); pinMode(LED, OUTPUT); pinMode(BTN, INPUT_PULLUP); Serial.println("gotov: knopka D2 ili komandy rec/stop");} void loop() { int knopka = (digitalRead(BTN) == LOW); // прижата к земле при нажатии if (knopka == 1 && btnPred == 0) { // фронт нажатия if (zapis == 0) nachatSessiyu(); else zakonchitSessiyu(); } btnPred = knopka; if (zapis == 1 && millis() - poslednyayaStroka >= 500) { poslednyayaStroka = millis(); Serial.print(millis()); Serial.print(","); Serial.println(map(analogRead(POT), 0, 1023, 0, 100)); strok += 1; } if (Serial.available() > 0) primeni(Serial.readStringUntil(10));}Кнопка на D2 и команды rec/stop меняют один флаг zapis — источник не важен, состояние одно. У рекордера не бывает двух дорог включения: однажды они разъедутся, и вы будете смотреть на горящий светодиод, при котором строки почему-то не идут. Фронт нажатия отслеживает btnPred: без него длинное нажатие превратится в десяток переключений. Шапка печатается один раз, на границе сессии: если слать её в каждой строке, таблица после разреза по запятой рассыплется. Строка лога — один println с двумя print перед ним: метка, запятая, значение; всё, что длиннее, уходит в служебные строки вне блока.
Принцип одна строка — одна запись мы проговаривали в уроке про таблицу величин: там он обслуживал монитор порта, здесь та же строка становится строкой таблицы. Разница в дисциплине: монитор прощает пропущенную строку, таблица — нет, поэтому у рекордера есть счётчик strok, и итог в стоп-маркере сразу показывает дыру — записали 350 строк за 300 секунд при периоде 500 мс, значит, треть потерялась, и графику верить нельзя. Дыры в логе — это всегда железо или занятый loop, и искать их надо в момент, когда итог не сошёлся, а не через неделю.
Флаг по умолчанию выключен: после подачи питания плата молчит и ждёт команды — так в лог не попадает ни прогрев датчика, ни пробная крутка потенциометра. Следующая ступень — автозапись по условию: уровень вышел за порог, флаг поднялся сам, вернулся в норму — опустился. Дороги включения при этом не множатся: условие вызывает тот же nachatSessiyu, что и кнопка, а маркеры в логе честно показывают, кто поднял флаг — человек или порог.
Блок в таблице: пять движений
Блок между маркерами копируется в лист целиком, шапка становится заголовками столбцов. Дальше пять движений: разрез по запятой — Текст по столбцам с разделителем, полный маршрут по меню разобран в уроке про Excel; соседний столбец с формулой millis/1000 — секунды от старта; точечная диаграмма с осью X по секундам — не линейчатая, иначе метки встанут по порядку строк, а не по времени; фильтр по значению, чтобы выкинуть прогрев датчика в первые полминуты; и сохранение листа как шаблона — следующий лог ложится в него без настройки. Всё вместе — минута работы, если шапка и формат строки честные.
Если лог нужен не в таблицу, а в живую картинку на компьютере, тот же блок едет в Processing — урок про визуализацию рисует полоски и графики прямо из порта. Разбор полей на стороне платы, когда таблице нужны не строки, а поля, — урок про strtok и atoi. Уровни служебных сообщений вокруг лога — урок про уровни логов: рекордер не отменяет болтовню, он просто отделяет её маркерами, чтобы у каждой строки был понятный адресат.
Период записи выбирается по инерции процесса, а не по вкусу: DHT между замерами молчит две секунды — писать чаще некуда, потенциометр живой — на 500 мс видны всплески. Слишком мелкий период толстит лог и превращает короткую сессию в кашу, слишком крупный глотает события, которые живут между строками. Мой компромисс: период втрое-впятеро короче самого быстрого процесса, за которым охотитесь, — и ни строкой мельче.
| Строка протокола | Кто читает | Зачем она |
|---|---|---|
| #LOG BEGIN | человек и скрипт | начало сессии: всё до маркера — болтовня, в файл её не пишем |
| millis,uroven | таблица | шапка: станет заголовками столбцов после разреза по запятой |
| 12345,41 | таблица и график | строка лога: метка и значение, одна запись — одна строка |
| #LOG END + itog | человек | конец сессии и статистика: по итогу сразу видно дыру в записи |
- 1. Почему строки лога печатаются сразу, а не копятся в String до конца сессии?
- 2. Нажатие кнопки длится 80 мс, пауза между строками — delay(500). Что будет?
- 3. Что делает скрипт на pyserial с потоком вокруг лога?
- 4. Метка millis обнулится через...
- 5. Первое движение с блоком CSV, попавшим в таблицу?
- Рекордер — это флаг и четыре строки-события: BEGIN, шапка, строки лога, END с итогом; кнопка и команды меняют один и тот же флаг.
- Строки печатаются по ходу дела: RAM Uno не резиновая, String-копилка молча кончается на сотнях строк.
- Метка millis даёт ровную ось времени и запас в 49,7 суток; настенное время пришивается при разборе в таблице.
- Период записи держит планировщик на millis, не на delay: кнопка и команды обязаны читаться во время записи.
- В таблице: копипаст блока между маркерами, разрез по запятой, millis/1000 — секунды, точечная диаграмма с X по времени.
Частые вопросы по теме урока
- Почему строки лога печатаются сразу, а не копятся в String до конца сессии?
- Два килобайта RAM кончаются тихо: блок обрежется, числа в хвосте испортятся, и вы этого не заметите в момент записи. Печать по ходу дела делает RAM не нужной вовсе. String на Uno упрётся в 2 КБ RAM на сотнях строк
- Нажатие кнопки длится 80 мс, пауза между строками — delay(500). Что будет?
- digitalRead вызывается раз в полсекунды, нажатие короче паузы проходит между опросами. Отсюда правило: всё живое — на millis, delay — только там, где замирание допустимо. Нажатие потеряется: loop стоит в delay
- Что делает скрипт на pyserial с потоком вокруг лога?
- Маркеры BEGIN и END существуют именно для машины: скрипт копит строки только между ними, и служебная болтовня вокруг не попадает в файл с логом. Ищет маркеры и пишет только блок между ними
- Метка millis обнулится через...
- unsigned long переполняется на 4294967295 мс — это 49,7 суток. Сессиям на часы хватит с запасом в тысячи раз; многодневный лог берёт метку у RTC. 49,7 суток
- Первое движение с блоком CSV, попавшим в таблицу?
- Пока ячейки не разрезаны по разделителю, вся строка лежит в одном столбце и диаграммы бессмысленны. Шапку удалять не надо — она станет заголовками столбцов. Разрезать по запятой: Текст по столбцам