Порог включения вентилятора был зашит у меня в скетче — и каждая правка превращалась в вылазку с ноутбуком: перепрошить плату ради сдвига порога на полградуса. EEPROM не спасал: значения там живут, но глазами их не увидишь, нужен скетч-читалка и по-прежнему ноутбук. А править настройки надо бывает на объекте, в теплице, руками человека, который про прошивки слышит впервые. Тогда я переехал на settings.txt в корне карты: вставил карту в любой компьютер, открыл блокнотом, поправил строчку, вставил обратно.
Формат беру предельно плоский: одна строка — одна настройка, ключ равно значение. interval=60, fan_on=28.0, hum_max=80. Строки, начинающиеся с решётки, — комментарии для человека, пустые строки пропускаются, всё остальное скетч разбирает. Никакого JSON и никаких библиотек: текстовый формат с парой ключ-значение читается циклом из дюжины строк и не боится правок кривыми руками — лишний пробел или комментарий в конце ничего не ломают.
Механика чтения уже частично знакома: построчный разбор файла сделан в уроке про readStringUntil, а первый текстовый конфиг с карты — в уроке про файл настроек. Здесь добираю инженерную часть: разбор пары на ключ и значение, дефолты для отсутствующих строк и граблю, на которой спотыкается каждый второй конфиг, — невидимый символ возврата каретки из Windows-блокнота.
Размер такого файла — копейки: дюжина настроек по двадцать-тридцать байт на строку, итого пара сотен байт против журнала в мегабайты. Читается он целиком быстрее, чем моргает светодиод: одна сессия с картой на старте прибора, единицы миллисекунд на открытие, и вся работа с файловой системой закончена до первого замера. На карте этот файл — единственный, который человек трогает руками, и он того заслуживает: рядом с ним живут только журналы и архивы, которым человеческие руки не нужны.
Построчное чтение: цикл по available
Скелет чтения один и тот же: открыли файл в FILE_READ, пока available() говорит, что есть непрочитанное, читаем строку readStringUntil('\n') — она собирает символы до перевода строки и его съедает. Цикл проходит по файлу сверху вниз ровно один раз, и порядок обхода гарантирован: настройки применяются в том порядке, в каком записаны. Методы File и режимы открытия расписаны в уроке про File и режимы, здесь беру их как готовые кирпичи.
Каждую строку сначала приводят в приличный вид. trim() срезает пробелы по краям — и, что важнее, хвостовой '\r', о котором отдельный разговор ниже. Пустая строка после обрезки означает «пропустить», строка, начинающаяся с решётки, — комментарий, тоже мимо. Дальше ищем знак равенства: indexOf('=') возвращает позицию или минус единицу, если знака нет; строка без равенства — мусор, и её судьба зависит от строгости проекта: молча пропустить или напечатать в Serial и пропустить. Я печатаю: одна такая строка в Serial стоит ничего, а найденная через неделю объясняет, почему настройка не применилась.
Саму пару режет substring: от нуля до позиции равенства — ключ, от позиции плюс один до конца — значение. Обе половины ещё раз через trim, и можно применять. Значение приходит строкой, а живёт в скетче числом: toInt() для целых и toFloat() для дробных — те же привычки, что при разборе чисел из монитора, только источник другой. Про таймауты и терпимость parseInt к хвостам писалось в уроке про Serial.parseInt — readStringUntil плюс toInt ведут себя родственно.
Строки держите короткими и не копите в памяти: readStringUntil возвращает String, и на Uno каждый символ стоит байта ОЗУ, которого всего два килобайта. Строка конфига в тридцать байт — ничто, а вот попытка собрать весь файл в одну огромную строку, чтобы потом разобрать подешевле, — путь к молчаливой порче кучи на третьем десятке настроек. Прочитали строку, разобрали, применили, забыли — память под следующую освобождается сама. Это тот же принцип построчного чтения, что и для CSV-журналов, и там он работает годами.
Дефолты и грабля CRLF
Конфиг без дефолтов — это конфиг, который рассыпается от отсутствия одной строчки. Порядок жёсткий: сначала скетч заполняет все настройки рабочими значениями по умолчанию, и только потом файл их перекрывает. Ключа нет в файле — остаётся дефолт; файла нет вовсе — живём на дефолтах целиком и печатаем об этом в Serial. Второй слой защиты — границы разумного: interval меньше единицы или больше суток, влажность за сотню — значение бракуется, и снова стоит дефолт. Одна кривая правка в блокноте не должна уводить теплицу в отказ.
Теперь та самая грабля. Windows-блокнот заканчивает строки двумя символами: '\r' и '\n'. readStringUntil('\n') съедает перевод строки, а возврат каретки оставляет прилипшим к концу значения — и значение превращается из 60 в «60\r». Для toInt() это часто не катастрофа: разбор числа остановится на нецифровом хвосте. А вот сравнение строк ломается насмерть: ключ «mode» со значением «auto\r» никогда не совпадёт с «auto», настройка молча не применится, и дефолт замаскирует поломку. Лечение в одну строку: trim() каждой прочитанной строки до любых проверок — '\r' уходит вместе с пробелами.
Отличить карту от EEPROM по праву рождения несложно. Текстовый файл на карте правится любым блокнотом на любом компьютере, виден глазами и хранится вместе с журналом — но исчезает вместе с картой: вынули карту, и настройкам неоткуда взяться, кроме дефолтов. EEPROM живёт на плате и не зависит от наличия карты, но правится только через прошивку или отдельный скетч. Полная иерархия — файл на карте поверх EEPROM поверх дефолтов — разобрана в уроке про конфиг в три слоя; мой практический минимум — дефолты в скетче и файл на карте, а EEPROM подключаю, когда карта может физически отсутствовать неделями.
#include <SPI.h>#include <SD.h> const int CS_PIN = 10; struct Config { int intervalMin; // минуты между замерами float fanOn; // градус включения вентилятора int humMax; // % влажности для тревоги};Config cfg; void defaults() { cfg.intervalMin = 60; // то, с чем прибор живёт без файла cfg.fanOn = 28.0; cfg.humMax = 80;} void applyPair(String key, String value) { if (key == "interval") { int v = value.toInt(); if (v >= 1 && v <= 1440) cfg.intervalMin = v; // границы разумного } else if (key == "fan_on") { float v = value.toFloat(); if (v > 0.0 && v < 60.0) cfg.fanOn = v; } else if (key == "hum_max") { int v = value.toInt(); if (v > 0 && v <= 100) cfg.humMax = v; } else { Serial.print("ключ не знаю: "); // чужое - мимо Serial.println(key); }} void readConfig() { defaults(); // дефолты до чтения File f = SD.open("settings.txt", FILE_READ); if (!f) { Serial.println("settings.txt нет - живу на дефолтах"); return; } while (f.available()) { String line = f.readStringUntil('\n'); line = line.trim(); // \r от Windows уходит здесь if (line.length() == 0) continue; // пустая строка if (line.startsWith("#")) continue; // комментарий int eq = line.indexOf('='); if (eq < 0) { Serial.print("без знака равно: "); Serial.println(line); continue; } String key = line.substring(0, eq); String value = line.substring(eq + 1); key = key.trim(); value = value.trim(); applyPair(key, value); } f.close();} void setup() { Serial.begin(9600); if (!SD.begin(CS_PIN)) { Serial.println("карта не поднялась"); while (true) delay(500); } readConfig(); Serial.print("interval="); Serial.print(cfg.intervalMin); Serial.println(" мин"); Serial.print("fan_on="); Serial.println(cfg.fanOn, 1); Serial.print("hum_max="); Serial.println(cfg.humMax);} void loop() { delay(1000);}В модели сначала пройдите файл кнопкой: комментарий уходит мимо, interval уезжает в тридцать минут, fan_on — в 27,5, строка с хвостовым «\r» предупреждает, что без trim её значение не сравнилось бы, а ключ alarm честно объявлен незнакомым — и дефолт режима не пострадал. Потом наберите в мониторе interval=5000: границы разумного отвергнут значение, и в конфиге останутся прежние минуты. Это и есть два слоя защиты — дефолт до чтения и проверка границ после.
Своя строка из монитора проходит тем же парсером, что и «файл», — это не удобство демонстрации, а тестовый режим: прежде чем везти settings.txt на объект, я прогоняю его содержимое через Serial и смотрю глазами на каждый разбор. На плате тот же трюк стоит цикла на пять строк: читаем не карту, а монитор. Ловится всё, что не видно в блокноте: невидимый «\r», кириллица в ключах, лишний пробел после равенства.
| Место настроек | Правка на объекте | Что с ним при пропаже | Когда брать |
|---|---|---|---|
| settings.txt на SD | блокнотом, без ноутбука с IDE | карту вынули — остаются дефолты | настройки, которые правит человек |
| EEPROM | только скетчем или прошивкой | не зависит от карты | калибровки и счётчики, карта съёмная |
| константы в скетче | перепрошивкой | ничего не теряется | то, что не меняется никогда |
| Serial-команды | с подключённым компьютером | забывается при перезапуске | отладка и подгонка на столе |
Граница метода проходит по людям и по картам. Если настройки меняет тот, кто умеет подключать плату к компьютеру и не боится IDE, — EEPROM и константы живут спокойно годами. Как только править должен человек с блокнотом, а карта всё равно ездит в картридере с журналом, — файл на карте выигрывает по определению. Смешанный вариант тоже рабочий: частоту замеров и пороги держу в settings.txt, калибровочные смещения, которые правятся раз в год и только мной, — в EEPROM, чтобы вынутая карта не уносила их с собой.
Отдельно про объём и скорость: парсить файл целиком на каждый проход loop не нужно никогда. Читается он один раз в setup, конфиг живёт в переменных, и файловая система больше не участвует в работе. Единственный сценарий перечитывания — кнопка или команда «перечитать», и то как осознанное действие с печатью в Serial. Конфиг — это настройка до запуска, а не протокол обмена во время работы; для живых команд есть Serial и другие инструменты.
Про рабочий процесс добавлю одно наблюдение. Файл на карте — документ с авторством: через месяц на объекте в settings.txt появляются строки, которых никто не заказывал, — кто-то попробовал свой ключ, кто-то оставил комментарий на кириллице. Именно поэтому незнакомые ключи печатаются в Serial, а не съедаются молча: лист применённого конфига с чужими строками наверху экономит вечер диагностики. И держите эталонный settings.txt рядом со скетчем в репозитории: карта возвращается с объекта стёртой чаще, чем кажется, и восстановить содержимое за минуту — дорого стоит.
- 1. Что делает trim() с прочитанной строкой конфига?
- 2. Строка конфига не содержит знака равенства. Что делать?
- 3. Ключа interval в файле нет. Какое значение получит настройка?
- 4. Почему «auto\r» не совпадает с «auto» при сравнении строк?
- 5. Когда перечитывать settings.txt из loop?
- Одна строка файла — одна настройка: ключ, равенство, значение; решётка — комментарий, пустые строки мимо.
- Скелет чтения: FILE_READ, цикл while (f.available()), readStringUntil('\n') — файл проходится ровно один раз.
- Разбор пары: indexOf('='), substring до и после, trim обеим половинам, toInt или toFloat по типу значения.
- Дефолты заполняются до чтения, границы проверяются после: отсутствие файла и кривые значения не ломают прибор.
- Windows-блокнот оставляет \r в хвосте строки — trim каждой строки срезает его до того, как сравнение сорвётся.
Частые вопросы по теме урока
- Что делает trim() с прочитанной строкой конфига?
- trim() чистит края строки: пробелы и возврат каретки от Windows-блокнота уходят. Пробелы внутри строки он не трогает, разбивкой занимается substring по позиции равенства. Срезает пробелы и \r по краям строки
- Строка конфига не содержит знака равенства. Что делать?
- Строка без равенства не разбирается на пару — это мусор от правки. Печать в Serial стоит ничего и показывает, что именно криво в файле, а пропуск сохраняет работу прибора. Напечатать в Serial и пропустить
- Ключа interval в файле нет. Какое значение получит настройка?
- Дефолты заполняются до чтения файла, а файл их только перекрывает. Отсутствие ключа — штатная ситуация: конфиг может быть неполным, а файл — вообще отсутствовать. Дефолт, записанный до чтения файла
- Почему «auto\r» не совпадает с «auto» при сравнении строк?
- Возврат каретки — настоящий символ в конце строки, пришедший от Windows-блокнота. Строки «auto» и «auto\r» различаются длиной и содержимым, поэтому совпадения нет. \r — отдельный символ в хвосте, и строки разной длины не равны
- Когда перечитывать settings.txt из loop?
- Перечитывание — осознанное действие с печатью в Serial, а не фоновый процесс. Постоянное чтение карты в loop изнашивает ритм работы и ничего не даёт: конфиг меняется руками, а не сам. По осознанному действию: кнопке или команде