Плата с часами живёт в корпусе на стене и кнопок не имеет. Батарейку в DS3231 я менял в феврале — часы честно стартовали с 1 января 2000 года, и весь журнал событий пересортился мимо реальных дат. Перешивать скетч с rtc.adjust ради каждого перевода — откручивать корпус и тащить ноутбук к стене; ставить кнопки подвода — городить схему ради двух нажатий в год. Выход, который я выбрал тогда и таскаю с тех пор: протокол из двух команд в Serial-мониторе. SET 2025-01-15 10:30:00 ставит дату и время целиком, NOW показывает, что в часах сейчас — появилась она второй, но по частоте использования обогнала SET: правки редки, а посмотреть хочется. Пятьдесят строк скетча — и время устанавливается из монитора IDE без единой перезаливки. Два года спустя протокол разросся до четырёх команд, но принцип не изменился: пришло — ответь. И да, это скучнее кнопочек с энкодером — но оно работает в два часа ночи, когда кнопочки уже некому нажимать.
Про механику adjust и ловушки __DATE__ уже был урок про установку времени — здесь остаётся половина задачи, которой там не было: как разобрать строку от человека и не пропустить мусор. Человеческий ввод — самая лживая часть любого протокола: он приходит без ведущих нулей, с лишними пробелами и буквами там, где ждёшь цифры. Против этого стоит не вера в аккуратность, а три рубежа проверки — о них ниже. Сама база — подключение модуля и первый вывод времени — уже есть в уроке про DS3231.
Анатомия команды SET
Строка приходит из readStringUntil(10) — терминатор с кодом 10, это клавиша Enter. По умолчанию timeout — секунда: если строка пришла кусками, функция дождётся терминала или таймаута и отдаст то, что собрала. Дальше три слоя разбора. Первый — префикс: s.indexOf("SET") == 0 отделяет нашу команду от всего остального, trim() срезает случайные пробелы по краям. Второй — позиции разделителей: у правильной строки после четырёх символов SET и пробела идут десять символов даты (четыре цифры, дефис, две цифры, дефис, две цифры), пробел и восемь символов времени. Третий — substring и toInt по этим позициям. Почему числа из Serial коварны, отдельная история — parseInt и числа из Serial. Спойлер: toInt() читает префикс строки и молчит о том, что после цифр стоял мусор.
Валидация идёт до применения, а не после: год 2000-2099, месяц 1-12, день 1-31, часы 0-23, минуты и секунды 0-59. Диапазон — последний рубеж, а не первый: toInt() у мусора возвращает ноль, и «abc» в поле месяца отсечётся проверкой m >= 1. Но ноль легален для часов и секунд, поэтому одной проверкой диапазонов не обойтись — позиции разделителей и длина отсеют буквы раньше, чем дело дойдёт до чисел. Отдельно про длину: полная команда SET занимает ровно 23 символа, и проверка s.length() >= 23 отсеивает обрезанные строки до всякого парсинга — через Bluetooth-модуль ввод идёт кусками, и обрыв не редкость.
И про поведение adjust: rtc.adjust(DateTime(g, m, d, ch, mi, se)) переписывает всё поле целиком — год, месяц, день, часы, минуты, секунды. Для SET это ровно то, что нужно. Но если расширите протокол командой TIME HH:MM, чтобы перевести только время, — читайте now() и подставляйте его год, месяц и день в конструктор: частичных записей в DS3231 нет, любая запись — полный кадр. Это не каприз библиотеки, а устройство регистров чипа: дата и время пишутся семью байтами разом.
#include <Wire.h>#include <RTClib.h> RTC_DS3231 rtc;const int LED = 12; void setup() { Serial.begin(9600); pinMode(LED, OUTPUT); if (rtc.begin() == false) { Serial.println("DS3231 ne najden: proverite SDA(A4) i SCL(A5)"); while (1) {} } if (rtc.lostPower()) { Serial.println("Vremya ne ustanovleno. Prishlite:"); Serial.println("SET 2025-01-15 10:30:00"); } Serial.println("Komandy: SET 2025-01-15 10:30:00 ili NOW");} void loop() { if (Serial.available() > 0) { String s = Serial.readStringUntil(10); s.trim(); obsluzhit(s); } delay(50);} void obsluzhit(String s) { if (s.indexOf("NOW") == 0) { DateTime t = rtc.now(); Serial.print("sejchas: "); Serial.println(t.text()); return; } if (s.indexOf("SET") != 0 || s.length() < 23) { Serial.println("otkaz: nuzhen SET 2025-01-15 10:30:00"); return; } String data = s.substring(4, 14); // 2025-01-15 String vremya = s.substring(15); // 10:30:00 int d1 = data.indexOf("-"); int d2 = data.lastIndexOf("-"); int t1 = vremya.indexOf(":"); int t2 = vremya.lastIndexOf(":"); if (d1 != 4 || d2 != 7 || t1 != 2 || t2 != 5) { Serial.println("otkaz: razdeliteli ne na mestah"); return; } int g = data.substring(0, 4).toInt(); int m = data.substring(5, 7).toInt(); int d = data.substring(8, 10).toInt(); int ch = vremya.substring(0, 2).toInt(); int mi = vremya.substring(3, 5).toInt(); int se = vremya.substring(6, 8).toInt(); bool okData = (g >= 2000 && g <= 2099 && m >= 1 && m <= 12 && d >= 1 && d <= 31); bool okVremya = (ch >= 0 && ch <= 23 && mi >= 0 && mi <= 59 && se >= 0 && se <= 59); if (!okData || !okVremya) { Serial.println("otkaz: znacheniya vne diapazona"); return; } rtc.adjust(DateTime(g, m, d, ch, mi, se)); digitalWrite(LED, HIGH); delay(150); digitalWrite(LED, LOW); DateTime t = rtc.now(); // читаю обратно то, что записалось Serial.print("ok: v chasah teper "); Serial.println(t.text());}Контракт команды: ответ на каждый ввод
Договорённость у монитора простая: на любую строку — ответ. ok с тем, что записалось, либо отказ с ожидаемым форматом. Молчащий скетч не отличить от зависшего, а человек за монитором обычно не помнит, сколько дефисов в формате. Строка «otkaz: nuzhen SET 2025-01-15 10:30:00» экономит минуту листания документации — я считаю такие подсказки частью скетча, а не украшением. Формат из подсказки удобно копировать прямо в поле ввода: человек правит дату и получает ok с первой попытки.
Второй полезный контракт — предупредить первым. RTClib умеет lostPower(): флаг в микросхеме поднимается, когда питание пропадало и часы останавливались. В setup я печатаю по нему напоминание «время не установлено, пришлите SET» — плата сама подсказывает первый шаг после замены батарейки. А для устройств с сетью команда SET — ровно до первого ESP32 рядом: дальше NTP-синхронизация делает то же без человека. Но и там SET остаётся запасным парашютом: сеть падает, а кабель — нет.
Расширения: TIME, ALARM и границы формата
Протокол растёт в двух направлениях. Первое — короткие команды без полного кадра: TIME 10:30 переводит только время. Здесь всплывает свойство adjust, о котором шла речь выше: частичной записи нет, поэтому читайте now() и подставляйте его год, месяц и день в конструктор, меняя только часы и минуты. Второе — команды с параметрами поверх времени: ALARM 07:30 складывает время будильника в переменные скетча, TEMP показывает температуру чипа — у DS3231 есть свой термометр, читается одним вызовом; он разобран в уроке про термометр DS3231.
И про границы формата. Полночь — это 00:00:00, а не 24:00:00: второй вариант отсечётся диапазоном, и это правильно, хоть и выглядит обидно. День 1-31 отсеивает явный мусор, а тонкости високосных лет остаются на совести вводящего: NOW покажет, что записалось на самом деле, и спорить тут не с чем. И последнее — порядок применения: сначала полный разбор, потом одна запись, потом одно подтверждение. Скетчи, которые пишут adjust по кускам в трёх местах, рано или поздно ловят состояние «половина применена»: дата от новой команды, время от старой. Один кодовый путь записи — это не педантизм, а способ спать спокойно.
Что видно в симуляторе. Первый SET проходит: виджет часов перерисовывается, LED коротко вспыхивает, в мониторе встанет ok с фактическим временем из rtc.now() — печатаю не то, что ввели, а то, что записалось, так ловятся промахи конверсии. Ввод с тринадцатым месяцем упирается в диапазон, ввод без ведущих нулей — в позиции разделителей: оба отказа названы словами, а не кодами. Заметьте: ok печатает результат rtc.now(), а не эхо ввода — совпало, значит, запись дошла до чипа.
На плате всё то же самое: Serial-монитор IDE и тот же скетч. Дальше два естественных шага: прикрутить команды к журналу с метками — урок про журнал в Serial — и наращивать протокол по образцу: каждая новая команда, ALARM 07:30 или TEMP, это ещё одна ветка в том же if с тем же контрактом ответа. Тот же разбор по позициям, тот же порядок — протокол не разрастается в сторону сюрпризов. И печатайте время после каждой правки — привычка, которая ловит перепутанные месяц и день за секунду.
| Способ выставить время | Что нужно | Когда беру |
|---|---|---|
| adjust с __DATE__ и __TIME__ | перезаливка скетча | первый запуск на столе |
| Кнопки подвода | две кнопки и цикл опроса | устройство без кабеля и без сети |
| SET из Serial | USB-кабель и монитор | плата в корпусе, правка без разборки |
| NTP и adjust | ESP32 или модуль с сетью | устройство в сети: человек не нужен |
- 1. Что вернёт toInt() для строки, где вместо числа буквы?
- 2. Почему проверки диапазонов недостаточно для валидации SET?
- 3. Что перезапишет rtc.adjust(DateTime(g, m, d, ch, mi, se))?
- 4. Что означает readStringUntil(10)?
- 5. Зачем после adjust читать rtc.now() обратно?
- Формат SET 2025-01-15 10:30:00: префикс, позиции разделителей, диапазоны — три рубежа до adjust
- toInt() у мусора даёт ноль и читает префикс: длину и разделители проверяйте раньше чисел
- adjust пишет полный кадр — для команды TIME HH:MM подставляйте дату из now()
- На каждую строку из монитора — ответ: ok с фактическим временем или отказ с форматом
- lostPower() в setup подсказывает, что время не установлено, — плата говорит первой
Частые вопросы по теме урока
- Что вернёт toInt() для строки, где вместо числа буквы?
- toInt() возвращает ноль для всего, что не начинается с цифры, и молчит об этом. Отсюда правило: диапазоны проверять только после проверки формата и разделителей. Ноль — и без предупреждения
- Почему проверки диапазонов недостаточно для валидации SET?
- Мусорное поле даёт ноль, а ноль в поле часов или секунд — законное значение. Диапазоны остаются последним рубежом, а буквы отсеивают длина строки и позиции разделителей. Ноль легален для часов и секунд, а мусор toInt превращает ровно в ноль
- Что перезапишет rtc.adjust(DateTime(g, m, d, ch, mi, se))?
- adjust пишет полный кадр: частичной записи у DS3231 нет. Команда TIME 10:30 поэтому обязана смешивать now() и ввод — иначе дата молча обнулится. Всё поле целиком: год, месяц, день, часы, минуты, секунды
- Что означает readStringUntil(10)?
- Аргумент — код символа-терминатора. Код 10 это перевод строки, то есть Enter в мониторе. Ограничение по времени настраивается отдельно через Serial.setTimeout. Читать до символа с кодом 10 — до Enter
- Зачем после adjust читать rtc.now() обратно?
- Обратное чтение показывает фактическое состояние чипа, а не то, что вы отправили. Плохой контакт шины или зависание на записи вскрываются за секунду. Убедиться, что запись дошла: модуль с плохим контактом молча не тикает