Поливалка на балконе три месяца писала мне журнал одним цветом — каким выдаст монитор. Пока строк было десять, этого хватало; когда я дописал уровни из урока про уровни логов, бегать глазами стало тяжело: ERROR лежал в той же серой каше, что и INFO, и беду я находил постфактум — по мокрому полу. Решение взял из веба: серию байтов вида ESC[31m перед цветной строкой. На плате я обернул метку в Serial.print и с интересом открыл монитор: в строке честно напечаталось [31mERROR — буквами, с квадратной скобкой. Час разбирательств дал два вывода: коды у меня были правильные, а монитор — не терминал, красить его бесполезно.
В этом уроке разберём язык терминалов: из чего складывается ANSI-последовательность, почему байт 27 никто не печатает, а скобка с цифрами и буквой доезжает до экрана, и что делают коды цветов, очистки и курсора. Соберём цветные уровни лога — тот случай, когда краска окупается, — и честно поговорим о грабле: монитор Arduino IDE последовательности не рендерит, для цветов нужен настоящий терминал вроде PuTTY. В симуляторе мы увидим то же, что в IDE: сырые коды, — и это честная часть демонстрации, потому что движок тоже рисует текст, а не экранные эффекты.
Байт, который терминал съедает сам
Любая последовательность начинается с байта 27 — в коде его пишут как \x1B или 0x1B, в документациях зовут ESC. Это управляющий символ: печатной буквы он не изображает, а объявляет, что следующие байты — команда экрану, а не текст. Дальше идёт открывающая скобка, потом параметры и одна финальная буква: m меняет цвет и стиль текста, J чистит экран, H переносит курсор. Связка ESC плюс скобка в терминале не печатается никогда: программа-терминал перехватывает байты и делает то, что им велено. Плата при этом ничего не знает про экраны — Serial.print шлёт байты как есть, вся магия происходит на приёмнике.
Цвета текста — коды от 31 до 36: красный, зелёный, жёлтый, синий, пурпурный, циан. Сразу после цветной метки ставят сброс \x1B0m, иначе краска потечёт по всем следующим строкам — я однажды покрасил ERROR, а сброс забыл, и неделю смотрел красный журнал целиком. Ещё пара на каждый день: \x1B[1m делает текст ярче, \x1B[2J чистит весь экран, \x1B[H ставит курсор в левый верхний угол, а запись вида \x1B[3;1H ставит курсор на строку 3, столбец 1 — почти экранная форма для меню из [урока про текстовое меню.
Краска не бесплатная: метка цвета — четыре байта сверху (ESC, скобка, цифра, m), сброс — три. Строка в двадцать символов с двумя красками растёт до тридцати четырёх байтов, и на 9600 бод, где байт стоит около миллисекунды, печатается примерно на четырнадцать миллисекунд дольше. Для лога раз в секунду это пустяк, для быстрого потока — заметно: та же арифметика, что с постфиксами BIN и HEX из урока про системы счисления. Поэтому я красю метку уровня, а не всю строку.
| Код | Что делает | Замечание из практики |
|---|---|---|
| \x1B[31m | красный текст | 32 зелёный, 33 жёлтый, 34 синий, 35 пурпурный, 36 циан |
| \x1B[0m | сброс к обычному цвету | ставить после каждого цветного куска, иначе краска потечёт |
| \x1B[1m | яркий или жирный текст | хорошо держать для меток уровней |
| \x1B[2J | очистить весь экран | съедает прокрутку — для логов не годится |
| \x1B[H | курсор в левый верхний угол | обычно идёт в паре с 2J |
| \x1B[3;1H | курсор на строку 3, столбец 1 | экранная форма: значения печатаются на своих местах |
// Cvetnoj log dlya nastoyashchego terminala (PuTTY, screen, terminal OS).// Monitor Arduino IDE kody NE risuet — pokazhet [31m kak tekst.// ESC = 27 = 0x1B; v stroke ego zapisyvayut kak \x1B.void logi(String uroven, String cvet, String soobshenie) { Serial.print("\x1B["); // ESC i skobka — nachalo komandy Serial.print(cvet); // naprimer 31 — krasnyj Serial.print("m"); Serial.print(uroven); Serial.print("\x1B[0m"); // sbros: bez nego cvet potechet dalshe Serial.print(" "); Serial.println(soobshenie);} void setup() { Serial.begin(9600); Serial.print("\x1B[2J"); // ochistit ekran terminala Serial.print("\x1B[H"); // kursor v levij verhnij ugol Serial.println("Polivalka zapushchena");} void loop() { logi("INFO", "32", "nasos rabotaet: davlenie 2.1 bar"); delay(3000); logi("WARN", "33", "bak 38 procentov: gotovlyu doliv"); delay(3000); logi("ERROR", "31", "klapan 2 ne zakrylsya za 5 s"); delay(3000);}Цветные уровни лога: где краска окупается
Мой паттерн: INFO печатается без всякой краски, WARN — жёлтым, ERROR — красным, и только метка уровня, не текст. Два аргумента. Первый — совместимость: журнал, который я пересылаю себе в чат или читаю в сыром мониторе, остаётся читаемым — коды видны, но слова не потерялись. Второй — внимание: когда красным светится одна метка из тысячи, глаз находит её мгновенно; красный столбец текста никуда не ведёт. Поиск по архиву у меня всегда по коду: grep находит [31m быстрее, чем слово ERROR, — код не переводится и не склоняется.
Очистка экрана — отдельная история с границей применимости. Пара \x1B2J и \x1B[H в начале setup() даёт чистое окно на каждый старт платы — приятно. Но очистка съедает прокрутку: длинный журнал стирать на ходу — вредительство, история потеряна. Я применяю очистку только в экранных интерфейсах, где плата рисует форму и обновляет значения на местах — так устроен экран конфигуратора, который мы собирали в [уроке про настройки через терминал; для журнала — никогда. Позиция курсора тут работает как setCursor на символьном LCD — та же механика без библиотеки, что в уроке про мини-меню и курсор.
Современные терминалы понимают и яркие коды 90–97 — серым 90m у меня помечен DEBUG, и он не рябит в глазах. Но я держусь классических 30–37: их знают даже древние терминалы и телефонные эмуляторы. Запасной план для чужих машин простой: если не знаю, чем человек будет читать мой лог, печатаю без краски и без кириллицы — про то, как чужой приёмник портит даже буквы, следующий урок [про кодировки и кракозябры.
В симуляторе — та же честная оговорка, что и с монитором IDE: движок рисует текстовый журнал, ANSI-рендеринга у него нет. Скетч отправляет настоящий байт ESC кодом 27 (Serial.write пропускает его в журнал невидимым символом) и читаемую часть последовательности, поэтому вы увидите ровно то, что показала бы IDE: [33m перед WARN и чистый текст после сброса. На реальной плате с PuTTY этот же вывод будет раскрашен — байты те же, меняется только сторона приёмника.
Попробуйте: команда demo выпускает три строки уровней, clear чистит журнал и возвращает курсор домой, home просто ставит курсор. Кнопка на макетке — генератор живых событий: каждое нажатие пишет WARN со счётчиком, и сырые коды в журнале видны прямо рядом с текстом. Затем поиграйте в перенос: уберите сброс [0m из функции info и отправьте demo подряд — «краска» прилипнет ко всем следующим строкам, как в настоящем терминале без сброса.
Перенос на железо — короткий, и в нём три гвоздя. У терминала проверьте кодировку вместе с цветами: PuTTY по умолчанию порой стоит в Latin-1, и русские буквы рядом с цветами уедут в кракозябры — про словарь приёмника подробно говорит следующий урок. Скорость тоже влияет на цену краски: на 115200 служебный байт стоит около девяти микросекунд, и те же четырнадцать байтов растягивают строку на сто с небольшим микросекунд — для журнала пустяк, для плотного потока уже заметно. И проверьте, что понимает приёмник: часть телефонных эмуляторов умеет только цвета и молча игнорирует позиционирование курсора, поэтому экранная форма на них разваливается.
Дисциплину журнала краска менять не должна. Порядок у меня всегда такой: сначала уровни — что INFO, что ERROR, — потом цвет на метки, и никогда наоборот. Если после добавления ANSI читать сырой монитор стало хуже, вы покрасили слишком много: оставьте цвет только ERROR и посмотрите на журнал ещё раз. Мой ориентир простой: в терминале должно стать понятнее, а в сыром тексте — не заметно хуже; иначе следующий цвет — первый кандидат на удаление.
- ANSI — обычные байты в порту: ESC с кодом 27, скобка, параметры, буква; плата шлёт их как есть, понимает их терминал
- Базовый набор: [31m…[36m красят текст, [0m обязателен после краски, [2J чистит экран, [H возвращает курсор, [3;1H ставит курсор на место
- Монитор Arduino IDE сырой: коды останутся текстом; PuTTY, screen, minicom и терминалы Linux и macOS рендерят
- Красю метку уровня, а не строку: две краски — четырнадцать лишних байтов, на 9600 бод около четырнадцати миллисекунд на строку
- 2J и H — для экранных форм, не для логов: очистка съедает прокрутку, а историю журнала не вернуть
- 1. Монитор Arduino IDE показывает в строке литеральный текст [31mERROR. Что это значит?
- 2. С чего начинается любая ANSI-последовательность?
- 3. Что делает пара \x1B[2J и \x1B[H?
- 4. Почему после цветной метки обязателен сброс [0m?
- 5. Строка в двадцать символов с двумя красками длиннее на четырнадцать байтов. Насколько дольше она печатается на 9600 бод?
Частые вопросы по теме урока
- Монитор Arduino IDE показывает в строке литеральный текст [31mERROR. Что это значит?
- Коды правильные, но монитор IDE — не терминал: он печатает всё как есть. Цвета покажут PuTTY, screen, minicom и терминалы ОС. Терминал сырой: ANSI не рендерится, коды видны как текст
- С чего начинается любая ANSI-последовательность?
- Байт 27 (0x1B, в коде \x1B) объявляет команду экрану, скобка начинает её, буква в конце — сама команда: m, J или H. С байта 27 (ESC) и открывающей скобки
- Что делает пара \x1B[2J и \x1B[H?
- 2J стирает весь экран, H ставит курсор домой. Для журнала пара вредна: вместе с экраном стирается прокрутка. Чистит экран и возвращает курсор в левый верхний угол
- Почему после цветной метки обязателен сброс [0m?
- Цвет держится до следующей команды цвета. Я забыл сброс один раз — и неделю читал красный журнал целиком. Сброс возвращает обычный цвет: без него краска потечёт на следующие строки
- Строка в двадцать символов с двумя красками длиннее на четырнадцать байтов. Насколько дольше она печатается на 9600 бод?
- На 9600 бод байт стоит около 1,04 миллисекунды, поэтому четырнадцать лишних байтов — примерно четырнадцать миллисекунд. Красите метку, а не строку. Примерно на четырнадцать миллисекунд