Skip to content

Releases: 240974a/FmtLog

1.3.0 — SNMP, Loki и выход в каталог Arduino

Choose a tag to compare

@240974a 240974a released this 08 Sep 21:57

Первый выпуск для каталога Arduino: библиотека подаётся в реестр и ставится
через Менеджер библиотек, а не выкачиванием с GitHub.

Журнал научился уходить туда, где его читают не глазами: в систему мониторинга
и в хранилище логов. Оба приёмника были обещаны в 1.2.0 — теперь они есть.

Telnet отдаёт то же, что порт, и с цветом

Раньше в telnet уходил только текст сообщения: ни времени, ни уровня, ни
источника — по такому журналу нельзя было понять, когда что случилось и откуда.
Теперь строка та же, что в порту, и раскрашена так же: терминал понимает те же
последовательности ANSI.

Включать ничего не нужно. Убрать цвет, если журнал разбирают скриптом, —
color::setEnabled(false); настройка общая для порта и telnet.

Для этого FmtColor научился писать в любой Print, а не только в Serial:
color::write(поток, запись). Из неё же собран и прежний serialSink.

Обошлось в двенадцать байт: таблицы цветов в прошивке уже были.

Уведомления по SNMP

FmtSnmp.h шлёт монитору то, что требует вмешательства, — err и выше:

snmp::begin(IPAddress(192, 168, 1, 10));
snmp::setEnterpriseOid("1.3.6.1.4.1.12345");
log::addSink(snmp::sink);

Уходит trap SNMPv2c на порт 162 — туда, где его ждёт Zabbix, PRTG или Nagios.
В trap идут текст сообщения, номер уровня и имя источника: по двум последним
фильтруют, не разбирая текст.

Пакет собирается своим кодом, без библиотеки SNMP: trap — единственное, что
нужно журналу, и его кодирование занимает меньше места, чем любая готовая
реализация вместе с разбором запросов, которые плате слушать незачем.

Строки для Loki

FmtLoki.h складывает строки в Loki — их ищут по меткам в той же Grafana, что
и метрики Prometheus:

loki::begin("192.168.1.10");
loki::addLabel("job", "boiler");
log::addSink(loki::sink);

level и source библиотека ставит метками сама, остальные задаёте вы. Строки
уходят пачками — по шестнадцать или раз в пять секунд, что случится раньше;
loki::flush() досылает их перед перезагрузкой.

Поднять Loki с Grafana можно одной командой: в extras/loki лежит готовый
контейнер, источник данных в нём уже прописан. Там же — что делать, если
строки не появляются.

Оба приёмника не задерживают loop()

sink() вызывается изнутри log::err(), поэтому он только запоминает строку,
а по сети её отправляет handle(). Что не поместилось, пока сеть недоступна,
теряется предсказуемо — самое старое, и это видно в lost().

Замеры пересняты

Прежние числа сравнивали прошивку с журналом и прошивку совсем без сети,
поэтому в цену приёмника попадал весь сетевой стек. Теперь мерится то, что
добавляет сам модуль:

было стало
журнал с выводом в порт +2024 Б +236 Б
цветной вывод +436 Б +12 Б
telnet с историей +10 696 Б +2296 Б
журнал на веб-странице +14 216 Б +4268 Б

Новые приёмники: SNMP +564 Б, Loki +2560 Б. SNMP дёшев, потому что шлёт
UDP-пакет; Loki открывает HTTP-соединение; telnet и веб-страница поднимают на
плате сервер — он и составляет их цену.

Веб-страница выглядит как панель управления

Раньше это была ровная полоса из полей и кнопок, где нужное искали
перечитыванием. Теперь элементы собраны в группы — плата, фильтр, время,
журнал, — с подписями и разделителями между ними; глаз находит нужное по
месту. Связь с платой показывает точка рядом со словом: зелёная — строки
идут, жёлтая — связь оборвалась и восстанавливается.

Подписи и подсказки переведены на русский.

Светлая тема

Кнопка со солнцем и луной в правом углу переключает тему; выбор запоминается,
а при первом открытии берётся системный — у кого светлая система, тот и
страницу увидит светлой.

Цвета уровней подобраны отдельно для каждой темы и проверены на контраст: все
проходят порог читаемости WCAG. Заодно поднят контраст отметки времени на
тёмной теме — она была чуть ниже порога.

Журнал не теряется при перезагрузке страницы

До двух тысяч строк хранятся в браузере, привязанные к адресу платы: F5
посреди разбора больше ничего не стирает. Плата отдаёт лишь свою историю —
последние несколько десятков строк, — и раньше всё, что набралось за час
наблюдения, пропадало от одного нажатия.

Строки, которые плата отдаёт заново при подключении, не удваивают журнал:
повторы отсеиваются. Кнопка «очистить» теперь спрашивает подтверждение —
она убирает и сохранённое, а вернуть его будет неоткуда.

Веб-страницу стало можно читать

Раньше каждая новая строка утаскивала страницу вниз, и прочитать что-то в
середине журнала не получалось. Теперь стоит перемотать выше — и страница
замирает на прочитанном месте: новые строки копятся, не двигая текст, даже
когда старые вытесняются из буфера. Вернуться к свежему можно кнопкой «Читать
последнее», на ней же видно, сколько строк пришло, пока вы читали.

Пауза останавливает поток, чтобы прочитать текущий вид не спеша, без
перемоток.

Вместо выбора даты — скольжение по времени: и + с выбором шага —
минута, десять минут, час, полсуток, сутки. Календарь показывал начало дня, а
нужное место всё равно искали прокруткой; шаг доводит до него сразу. Шаг запоминается между
открытиями страницы, а шагают кнопки от собственной цели, а не от того, что
видно на экране, — иначе промах копился бы с каждым нажатием.

Плату выбирают из списка

Адреса, к которым подключались, остаются в списке у поля: к соседней плате
переключаются выбором, а не набором заново. Ненужный убирает кнопка рядом —
вместе с адресом забывается и сохранённый от него журнал, чтобы не занимал
место.

Свежий адрес встаёт первым: к последней плате возвращаются чаще всего.

Адрес платы можно передать ссылкой

extras/log.html?host=192.168.1.50 открывает журнал сразу нужной платы.
Удобно, когда их несколько: на каждую заводится своя закладка. Указанный в
ссылке адрес главнее запомненного.

Что проверено

  • 107 тестов на машине разработчика, плата не нужна: pio test -e native
  • пакет SNMP разобран сторонней библиотекой pyasn1 — метки, длины и OID
    оказались верны не только по нашим меркам
  • тело запроса отправлено в настоящий Loki: строки вернулись обратно с целыми
    метками, кавычками и кириллицей
  • сборка на шести платформах, семь примеров, arduino-lint в строгом режиме

1.2.0 - журнал по сети и вынос форматирования в FmtTiny

Choose a tag to compare

@240974a 240974a released this 08 Sep 19:36

Предварительный выпуск.

Форматирование уехало в отдельную библиотеку

Разбор образца, правила вывода и formatter<T> теперь живут в
FmtTiny. Здесь остался журнал: уровни,
источники, приёмники, цвет и сеть.

Для вас ничего не меняется — FmtTiny подтянется сама, а её имена (Fmt,
formatter, opt, Duration) доступны и через fmtlog, так что писать два
пространства имён не придётся. Зато проекту, которому нужны только образцы
{}, больше не достаётся журнал, которым он не пользуется.

Настройки разделились: FMTTINY_* — про форматирование, FMTLOG_* — про
журнал.

Журнал на веб-странице

Новый модуль FmtWeb.h: откройте extras/log.html в
браузере, впишите адрес платы — и увидите живой журнал. Есть цвет, поиск,
выбор уровня, переход к дате, пауза и выгрузка в файл.

Страница лежит у вас на диске, а не в прошивке: править её можно, не трогая
плату. Строки идут по SSE — это обычный HTTP-ответ, который не закрывают. Для
одностороннего потока он проще WebSocket: ни рукопожатия с кадрированием, ни
отдельной библиотеки, а при обрыве связи браузер переподключается сам.

Открывший страницу позже увидит и то, что накопилось до него.

Журнал по сети

FmtTelnet.h отдаёт журнал по telnet, а FmtHistory.h хранит для этого
последние строки.

Главное отличие от обычного кольцевого буфера: чтение ничего не стирает.
Место в истории помнит не буфер, а каждое соединение — поэтому второй и третий
клиент видят ту же глубину, что и первый.

Если кольцо догнало отставшего, он продолжит с самого старого и получит
отметку [N bytes lost]: потеря видна, а не случается молча.

Настоящее время в журнале

Скажите библиотеке, который час, — и вместо счётчика с запуска появится дата:

log::setTime(epochSeconds);        // секунды эпохи Unix
log::setTime(epochSeconds, 250);   // и доля секунды, если известна
steady : 00000012.340 I app: before time is set
26-09-03 12:30:45.123 I app: time is known now

Между вызовами библиотека досчитывает время по millis(), а кварц уходит на
секунды в сутки — повторяйте вызов, каждый следующий сбрасывает накопленную
погрешность. Счёт не собьётся и при переполнении millis() каждые ~49.7 суток.

Обе отметки занимают ровно 21 знак, поэтому столбцы не поедут в тот момент,
когда время появится.

Семь уровней, два неотключаемых

К trace, debug, info, warn и err добавились critical и system.
Порог их не глушит: critical — о том, после чего работать нельзя, system
о пуске, остановке и прочих вехах, которые нужны всегда.

warning и error стали warn и err — короче и в один ряд с остальными.

Уровни можно хранить у себя

log::setLevelSource передаёт библиотеке функцию, и она спрашивает уровень
перед каждой строкой. Пригодится, когда уровни лежат в EEPROM и меняются с
веб-страницы: правка подействует сразу.

Имена уровней словом

log::levelName() возвращает "trace", "info", "error" — рядом с прежним
levelMark(), дающим букву. Нужно приёмникам, чей вывод читают не только
глазами: в метке Prometheus буква E ничего не скажет, а error поймут и
человек, и программа.

Работает почти везде

Раньше в описании стояли только AVR и ESP. Оказалось, ядро журнала переносимо:
проверено сборкой на ATtiny85, Uno, ESP8266, ESP32, RP2040 и STM32.

Заодно нашлась причина, по которой сборка на RP2040 падала: анализатор
зависимостей PlatformIO читает текст, а не компилирует, и находил #include <WiFi.h> внутри FmtTelnet.cpp даже под #if. Теперь библиотека просит
режим chain+, который разбирает условия.

Что дальше

Задуманы alarms по SNMP и строки для Loki. Интерфейс для них готов — в
Record есть и время в наносекундах, и имя уровня, и текст.

Что проверено

  • 64 теста на машине разработчика, плата не нужна: pio test -e native
  • сборка на шести платформах
  • прирост к прошивке замерен сборкой: журнал +2024 Б, цвет +436 Б,
    telnet +10 696 Б, веб +14 216 Б — сетевые модули дороги не сами по себе,
    с ними в прошивку приходит серверная часть стека

1.1.0 - печатает дробные числа своим кодом вместо dtostrf

Choose a tag to compare

@240974a 240974a released this 02 Sep 19:35

1.1.0 — 2 сентября 2026

Предварительный выпуск: библиотека выложена для оценки сообществом, замечания
приветствуются.

Устанавливается из репозитория — в каталоге Arduino и реестре PlatformIO её
пока нет; как это сделать, сказано в README.

Главное в этом выпуске

Дробные печатаются собственным кодом, целочисленной арифметикой, без
dtostrf. Замеры:

было стало
скорость (300 000 вызовов) 127.8 мс 12.3 мс в 10 раз быстрее
флеш, AVR 3432 2052 −1380 байт
флеш, ESP8266 266747 266535 −212 байт
флеш, ESP32 267961 268241 +280 байт

Против snprintf дробные теперь быстрее в 6.2 раза — раньше отставали в 1.25.

На ESP32 прошивка немного выросла: ветка для чисел от 2³², где дробной части у
double уже нет, печатает целую часть через uint64, а 64-битное деление там
отдельная подпрограмма.

Округление совпадает с printf и dtostrf до последнего знака, включая
правило «ровная половина — к чётному»: сверено на 316 004 значениях.

Исправлено: у -0.0 терялся знак — сравнение с нулём для отрицательного
нуля ложно.

Возможности библиотеки

Полный перечень, включая то, что было в 1.0.0.

Форматирование по образцу. Одно место вставки {} для любого типа вместо
%d, %lu и %.3f. Тип выводит компилятор, поэтому несовпадение типа или
забытый аргумент перестают быть порчей стека.

Готовые правила вывода — числа, строки в RAM и во флеше, String,
длительности, дата и время, IP-адрес, дампы памяти. Свой тип подключается одной
специализацией formatter<T>.

Необязательное значение opt(условие, префикс, значение) — условие пишется
один раз вместо двух тернарных выражений с пустыми литералами.

Журнал с уровнями, источниками и сменными приёмниками вывода. Библиотека
никуда не пишет сама: порт, сеть, карта памяти — решает приложение.

Уровни задаются раздельно по источникам, так что разговорчивую часть
приложения можно приглушить, не трогая остальные. Проверка идёт до сборки
сообщения, поэтому отброшенный вызов почти ничего не стоит.

Порог FMTLOG_COMPILE_LEVEL убирает вызовы из прошивки целиком — вместе с
образцами и значениями.

Необязательные модули. Цветной вывод (FmtColor.h) и чтение строк из
EEPROM (FmtEeprom.h) подключаются отдельно и не занимают места, пока не нужны.


Сколько это стоит

Все числа получены сборкой, не оценкой. Сравнение с snprintf на сообщениях
вида "msg a={} b={}" с двумя целыми.

Флеш

постоянная надбавка каждый вызов
AVR (Uno) 676 Б против 1574 Б у snprintf 108 Б против 97 Б
ESP8266 512 Б против 336 Б 77 Б против 59 Б

На AVR библиотека экономит флеш примерно до 86 вызовов — дальше немного
дороже. На ESP8266 экономии по флешу нет: printf там слинкован ядром
независимо от кода приложения, и FmtLog добавляется поверх. Разница — около
0.1 % флеша на полсотни вызовов.

Дробные выводятся собственным кодом, без dtostrf: на AVR это экономит
1380 байт флеша.

Цветной вывод стоит 456 байт и только когда подключён.

Оперативная память

Динамическая память не используется вовсе. Статически — буфер сообщения
(128 байт, на AVR 64) и состояние буфера: 16 байт на ESP, 7 на AVR. Размер
буфера задаётся при сборке через FMTLOG_MESSAGE_SIZE.

Скорость

против snprintf
целые в 2.2 раза быстрее
дробные в 6.2 раза быстрее

Библиотека печатает и те, и другие сама, целочисленной арифметикой, без
разбора образца во время выполнения.

Округление совпадает с printf и dtostrf до последнего знака, включая
правило «ровная половина — к чётному»: сверено на 316 004 значениях.


Платформы

платформа состояние
ESP8266 проверено сборкой
ESP32 проверено сборкой
AVR (Uno, Nano, Mega) проверено сборкой

На AVR нет IPAddress — соответствующее правило вывода там не собирается.
<type_traits> ядро AVR не поставляет, нужные признаки типов библиотека
определяет сама.

Что проверено

  • 60 тестов на машине разработчика, плата не нужна: pio test -e native
  • 14 сочетаний примеров и плат собираются; предупреждений от библиотеки нет
  • замеры размера и скорости — сборкой, а не оценкой
  • вывод дробных сверен с printf на 316 004 значениях

Известные ограничения

  • Спецификаторов ширины и точности у {} нет: число знаков после запятой
    задаётся при сборке через FMTLOG_FLOAT_DECIMALS, общий для всех значений.
  • Буфер сообщения один на приложение, поэтому журнал не переживёт вызова из
    прерывания во время сборки другого сообщения.
  • Дата и время принимают время эпохи Unix и печатаются как есть: часовой пояс
    библиотека не знает, сдвиг задаёт приложение.

1.0.0: Первый выпуск.

Pre-release

Choose a tag to compare

@240974a 240974a released this 01 Sep 17:14

Первый выпуск.

Форматирование строк по образцу {} и лёгкий журнал для микроконтроллеров —
без динамической памяти и без std::string.

log::info(F("пин {} = {}, температура {} °C"), pin, level, 54.25);
[12s:104ms] I app: пин 13 = true, температура 54.250 °C

Что умеет

Форматирование по образцу. Одно место вставки {} для любого типа вместо
%d, %lu и %.3f. Тип выводит компилятор, поэтому несовпадение типа или
забытый аргумент перестают быть порчей стека.

Готовые правила вывода — числа, строки в RAM и во флеше, String,
длительности, дата и время, IP-адрес, дампы памяти. Свой тип подключается одной
специализацией formatter<T>.

Необязательное значение opt(условие, префикс, значение) — условие пишется
один раз вместо двух тернарных выражений с пустыми литералами.

Журнал с уровнями, источниками и сменными приёмниками вывода. Библиотека
никуда не пишет сама: порт, сеть, карта памяти — решает приложение.

Уровни задаются раздельно по источникам, так что разговорчивую часть
приложения можно приглушить, не трогая остальные. Проверка идёт до сборки
сообщения, поэтому отброшенный вызов почти ничего не стоит.

Порог FMTLOG_COMPILE_LEVEL убирает вызовы из прошивки целиком — вместе с
образцами и значениями.

Необязательные модули. Цветной вывод (FmtColor.h) и чтение строк из
EEPROM (FmtEeprom.h) подключаются отдельно и не занимают места, пока не нужны.


Сколько это стоит

Все числа получены сборкой, не оценкой. Сравнение с snprintf на сообщениях
вида "msg a={} b={}" с двумя целыми.

Флеш

постоянная надбавка каждый вызов
AVR (Uno) 676 Б против 1574 Б у snprintf 108 Б против 97 Б
ESP8266 512 Б против 336 Б 77 Б против 59 Б

На AVR библиотека экономит флеш примерно до 86 вызовов — дальше немного
дороже. На ESP8266 экономии по флешу нет: printf там слинкован ядром
независимо от кода приложения, и FmtLog добавляется поверх. Разница — около
0.1 % флеша на полсотни вызовов.

Сообщения с дробными на AVR обходятся дороже snprintf примерно на 700 байт:
вывод дробных отдан dtostrf, и он линкуется вдобавок к уже имеющемуся
vfprintf.

Цветной вывод стоит 456 байт и только когда подключён.

Оперативная память

Динамическая память не используется вовсе. Статически — буфер сообщения
(128 байт, на AVR 64) и состояние буфера: 16 байт на ESP, 7 на AVR. Размер
буфера задаётся при сборке через FMTLOG_MESSAGE_SIZE.

Скорость

против snprintf
целые в 2.2 раза быстрее
с дробным в 1.25 раза медленнее

Целые библиотека печатает сама, без разбора образца во время выполнения.
Дробные отдаёт dtostrf — тому же коду, что зовёт printf.


Платформы

платформа состояние
ESP8266 проверено сборкой
ESP32 проверено сборкой
AVR (Uno, Nano, Mega) проверено сборкой

На AVR нет IPAddress — соответствующее правило вывода там не собирается.
<type_traits> ядро AVR не поставляет, нужные признаки типов библиотека
определяет сама.


Установка

Arduino IDE — Library Manager, найти «FmtLog».

PlatformIO — в platformio.ini:

lib_deps = FmtLog

Что проверено

  • 51 тест на машине разработчика, плата не нужна: pio test -e native
  • 14 сочетаний примеров и плат собираются; предупреждений от библиотеки нет
  • замеры размера и скорости — сборкой, а не оценкой

Примеры

пример о чём
Basic образец, свой буфер, необязательное значение
Logging уровни, источники, свой приёмник вывода
Color цветной вывод, свои цвета уровней и источников
CustomType подключение своего типа
Levels смена уровня на ходу и отсечение при сборке

Известные ограничения

  • Спецификаторов ширины и точности у {} нет: число знаков после запятой
    задаётся при сборке через FMTLOG_FLOAT_DECIMALS, общий для всех значений.
  • Буфер сообщения один на приложение, поэтому журнал не переживёт вызова из
    прерывания во время сборки другого сообщения.
  • Дата и время принимают время эпохи Unix и печатаются как есть: часовой пояс
    библиотека не знает, сдвиг задаёт приложение.

Лицензия

MIT