Погодный виджет на даче у меня долго показывал одни цифры: плюс двенадцать, ветер четыре метра в секунду. Цифры честные, но их нужно читать — полсекунды внимания на каждый взгляд. Ярлык с иконкой читается иначе: периферийным зрением, с другого конца веранды, за долю секунды. Солнце, облако, дождь — три состояния, которые покрывают большую часть дня, и для каждого хочется по картинке на экране.
Здесь собираю погодный ярлык на OLED: три битмапа 16x16 — солнце, облако и дождь — и механизм переключения между ними. Для проверки — цифры 1, 2 и 3 из Serial-монитора и кнопка на корпусе, которая листает иконки по кругу. В готовом приборе номер иконки приходит из сети: из ответа OpenWeatherMap достаётся слово с описанием погоды — весь цикл от запроса до разобранного значения разобран в разделе про JSON, здесь берётся только маппинг слова в номер.
Почему именно 16x16: иконка шириной в два шрифтовых знака видна и выцветшим глазом, а весит ровно 32 байта — шестнадцать строк по два байта. Три иконки — 96 байт, и даже они не трогают ОЗУ: массивы переезжают во флеш с PROGMEM. Анатомия байта — восемь пикселей, старший бит слева — уже разобрана в уроке про drawBitmap; пересказывать её не буду, вместо этого кладу на стол три готовых массива и показываю, как я их рисовал.
Три массива: клетчатый листок и честные байты
Массивы ниже нарисованы по-честному, на листке в клетку: крестик — горящий пиксель. Солнце — круг из восьми строк с лучами вверх, вниз и в стороны плюс точки по диагоналям; облако — шапка-дуга и пухлое тело с плоским дном; дождь — та же туча, сжатая в верхние семь строк, и четыре штриха-капли под ней. Каждая строка картинки — два байта: левый накрывает столбцы с нулевого по седьмой, правый — с восьмого по пятнадцатый.
Проверка на глаз: байт 0xCF — это 11001111 — горят два крайних столбца слева и четыре справа; у солнца в седьмой строке это луч влево и край круга. Такие чтения байтов стоили мне получаса, но платят привычкой: в чужой простыне чисел из интернета я теперь вижу форму до заливки на плату. Свои картинки после листка удобно прогонять через конвертер image2cpp — он принимает PNG и отдаёт готовые байты в том же формате, что и массивы ниже.
| Иконка | Вес | Условие погоды | Как нарисована |
|---|---|---|---|
| Солнце 16x16 | 32 байта | Clear — ясно | диск и лучи: четыре по сторонам, четыре точки по диагоналям |
| Облако 16x16 | 32 байта | Clouds — облачно | шапка-дуга над телом с плоским дном |
| Дождь 16x16 | 32 байта | Rain, Drizzle | туча сверху и четыре штриха-капли снизу |
Переключение устроено как машина с одной переменной: mode хранит номер текущей иконки, и любая команда — цифра из порта или фронт кнопки — переписывает его и вызывает перерисовку. Никаких анимаций перехода: старый кадр стирается целиком, новый собирается в буфере и уезжает на стекло одним display(). Мерцания при такой смене нет, потому что кадр живёт в ОЗУ, а экран получает уже собранную картинку — механика двойной буферизации разбиралась в уроке про clearDisplay.
Кнопка здесь не украшение: пока прибор стоит на столе и сеть ещё не подцеплена, листать иконки рукой удобнее, чем держать ноутбук с монитором порта. Фронт ищется парой prevBtn и btn — приём из урока про антидребезг — с межиконной отсечкой 300 мс, чтобы одно долгое нажатие не прокручивало три иконки подряд.
#include <Wire.h>#include <Adafruit_GFX.h>#include <Adafruit_SSD1306.h> Adafruit_SSD1306 display(128, 64, &Wire, -1);const int BTN = 2; // солнце 16x16: диск и лучи, 16 строк по 2 байтаconst unsigned char ikonaSolnce [32] PROGMEM = { 0x01, 0x80, 0x01, 0x80, 0x20, 0x08, 0x00, 0x00, 0x03, 0xC0, 0x07, 0xE0, 0x0F, 0xF0, 0xCF, 0xF3, 0xCF, 0xF3, 0x0F, 0xF0, 0x07, 0xE0, 0x03, 0xC0, 0x00, 0x00, 0x20, 0x08, 0x01, 0x80, 0x01, 0x80}; // облако 16x16: шапка и тело с плоским дномconst unsigned char ikonaOblako [32] PROGMEM = { 0x00, 0x00, 0x00, 0x00, 0x03, 0xC0, 0x07, 0xE0, 0x1F, 0xF8, 0x3F, 0xFC, 0x7F, 0xFC, 0x7F, 0xFC, 0x7F, 0xFC, 0x3F, 0xF8, 0x1F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; // дождь 16x16: туча сверху, штрихи-капли снизуconst unsigned char ikonaDozhd [32] PROGMEM = { 0x00, 0x00, 0x07, 0xE0, 0x0F, 0xF0, 0x1F, 0xF8, 0x3F, 0xFC, 0x3F, 0xFC, 0x1F, 0xF8, 0x0F, 0xF0, 0x00, 0x00, 0x04, 0x10, 0x08, 0x20, 0x10, 0x40, 0x00, 0x00, 0x01, 0x04, 0x02, 0x08, 0x00, 0x00}; int mode = 1;int prevBtn = HIGH; void risuj() { display.clearDisplay(); display.drawRect(4, 4, 24, 24, SSD1306_WHITE); // рамка ярлыка if (mode == 1) { display.drawBitmap(8, 8, ikonaSolnce, 16, 16, SSD1306_WHITE); } else if (mode == 2) { display.drawBitmap(8, 8, ikonaOblako, 16, 16, SSD1306_WHITE); } else { display.drawBitmap(8, 8, ikonaDozhd, 16, 16, SSD1306_WHITE); } display.setTextSize(1); display.setCursor(36, 8); if (mode == 1) display.println("SOLNCE"); else if (mode == 2) display.println("OBLAKO"); else display.println("DOZHD"); display.setCursor(36, 20); if (mode == 1) display.print("yasno"); else if (mode == 2) display.print("oblachno"); else display.print("dozhd"); display.display();} void smeni(int m) { mode = m; risuj(); Serial.print("ikona="); Serial.println(mode);} void setup() { Serial.begin(9600); pinMode(BTN, INPUT_PULLUP); display.begin(SSD1306_SWITCHCAPVCC, 0x3C); display.setTextColor(SSD1306_WHITE); Serial.println("Komandy: 1 solnce, 2 oblako, 3 dozhd"); risuj();} void loop() { if (Serial.available() > 0) { String s = Serial.readStringUntil(''); s.trim(); if (s == "1") smeni(1); else if (s == "2") smeni(2); else if (s == "3") smeni(3); } int btn = digitalRead(BTN); if (btn == LOW && prevBtn == HIGH) { // фронт: перебор по кругу int m = mode + 1; if (m > 3) m = 1; smeni(m); } prevBtn = btn;}Каша и мерцание: две грабли смены иконки
Кашу вместо иконки дают размеры в вызове drawBitmap: картинка 16x16 требует честные 16 и 16, и если по привычке оставить 8 и 8, библиотека возьмёт первые восемь байтов массива — полторы строки солнца — и растянет их в полную высоту экрана. На форуме arduino.cc я встречал случай, когда Uno делала всё: текст, шкалы, круги, — а единственная иконка 16x16 отказывалась рисоваться; почти всегда это размер в вызове или массив, свёрстанный в другой раскладке. Проверка на один взгляд: байтов в массиве ровно (w + 7) / 8 × h.
Мерцание приходит из другой истории — из долгих операций между clearDisplay и display(). Худший из моих промахов: чтение порта в середине отрисовки. readStringUntil ждёт терминатор до секунды, и если Serial пуст, кадр стоит полустёртым весь этот таймаут — глаз видит вспышку. Правило простое: порт опрашиваем только когда available() сказал, что читать есть что, а путь от clearDisplay до display() не прерываем ничем.
В рабочем приборе вместо цифр из монитора приходит ответ погодного API: в поле weather[0].main лежит слово Clear, Clouds или Rain — его и превращаем в номер иконки. Разбор ответа OpenWeatherMap до готовых значений — отдельный урок, здесь берётся только маппинг: Clear — 1, Clouds — 2, всё про дождь — 3. Ритм согласован с физикой процесса: погода меняется раз в часы, поэтому опрос сети раз в пять минут — норма, между опросами ярлык просто стоит и не трогает ни сеть, ни экран.
В метео-панели из соседнего урока такой ярлык встраивается вторым этажом: иконка занимает квадрат в углу, цифры живут по зонам со своей фиксированной шириной. А если хочется оживить ярлык — моргающие капли, вращающиеся лучи — это уже спрайты и маски: там разобрано, как переключать кадры, не стирая фон.
Что проверить после запуска: смены происходят целиком, без вспышек, даже если дёргать команды подряд — кадр собирался в буфере, так и задумано. Осечка кнопки видна при долгом удержании: одна прокрутка, а не три. Если хочется спокойнее — ставьте 500 мс: глазу на смену картинки нужно время, быстрее он не различает, а каждой перерисовке по I2C стоит около 92 мс шины — уже посчитанные в этом разделе цифры.
Набор легко растёт: снег, гроза, туман — по 32 байта каждый, десять иконок займут во флеше 320 байт и не потревожат ОЗУ вовсе. Рисуются тем же листком: облачко с точками, треугольник-молния, три горизонтальные черты. Правило прежнее: сначала клетка и форма, потом байты и проверка (w + 7) / 8 × h — и каждый новый ярлык встаёт в скетч добавкой одного else if.
- Иконка 16x16 весит ровно 32 байта: 16 строк по два байта, старший бит — левый пиксель; три погодных иконки — 96 байт, а с PROGMEM они не трогают ОЗУ вовсе
- drawBitmap требует честные размеры: массив 16x16 с параметрами 8x8 рисует кашу из первых байтов; проверка — байтов ровно (w + 7) / 8 × h
- Мерцания нет, пока кадр собирается в буфере и уезжает одним display(); порт читайте только при available — readStringUntil ждёт терминатор до секунды
- Кнопка листает иконки по фронту с отсечкой 300 мс — отладка без ноутбука; в готовом приборе номер приходит из weather[0].main погодного API
- Набор расширяется по 32 байта за иконку: десять ярлыков — 320 байт флеша и один else if в коде
- 1. Сколько байтов занимает битмап 16x16?
- 2. Что нарисует drawBitmap, если передать массив 16x16 с размерами 8 и 8?
- 3. Откуда берётся мерцание при смене иконки?
- 4. Как кнопка листает иконки, не прокручивая три за одно нажатие?
- 5. Где в ответе OpenWeatherMap лежит условие погоды для маппинга на иконку?
Частые вопросы по теме урока
- Сколько байтов занимает битмап 16x16?
- Шестнадцать строк по (16 + 7) / 8 = 2 байта — итого 32. Восемь пикселей в байте, старший бит — самый левый пиксель строки. 32 — по два байта на строку
- Что нарисует drawBitmap, если передать массив 16x16 с размерами 8 и 8?
- Библиотека не знает истинного размера: возьмёт первые (8 + 7) / 8 × 8 = 8 байтов и растянет их на всю высоту. Отсюда полосы и мусор. кашу: первые 8 байтов массива растянутся в полную высоту
- Откуда берётся мерцание при смене иконки?
- Кадр собирается в буфере, и до вызова display() на экране стоит старая картинка. Вспышку даёт пауза в сборке: readStringUntil без available ждёт терминатор до секунды с полустёртым кадром. от долгой операции между clearDisplay и display() — например, чтения Serial с таймаутом
- Как кнопка листает иконки, не прокручивая три за одно нажатие?
- Фронт ловится парой prevBtn и btn, а отсечка 300 мс глушит дребезг и долгое удержание — та же схема, что в уроке про антидребезг. по фронту HIGH-LOW с межиконной отсечкой 300 мс
- Где в ответе OpenWeatherMap лежит условие погоды для маппинга на иконку?
- weather[0].main отдаёт Clear, Clouds, Rain — слово превращается в номер иконки. Полный разбор ответа — в уроке про OpenWeatherMap. в поле main внутри weather[0]