Термометр в теплице я повесил на дверцу, и с прохода цифры размера 1 не читались вообще: 6 пикселей высоты на расстоянии двух метров — точка. Увеличил setTextSize до трёх, обрадовался и упёрся в следующее: десятичная точка шрифта в ячейке 18×24 пикселя выглядит как опечатка — два пикселя в углу гигантской пустой клетки, а минус превращается в тире посреди неба. Крупные цифры — это не один вызов setTextSize, а раскладка строки значения: целая часть, точка, десятые, знак и размер каждой части в отдельности.
Систему размеров мы раскладывали в уроке про setTextSize: знак масштабируется кратно 6×8, и каждый размер отъедает свою долю строки из 128 пикселей. Часы с крупным временем уже собирались в уроке про RTC и крупный шрифт — там значение без дробей, а температура живёт с десятыми, и именно дробь ломает наивный print. Зоны кадра и отступы считаются по той же сетке из урока про раскладку кадра.
План: сколько цифр какого размера влезает, своя точка через fillRect, обработка минуса и обновление раз в полсекунды вместо потока кадров.
Почему не FreeFonts и не U8g2: крупные цифры можно получить и чужими шрифтами, но каждый тянет свою библиотеку и таблицу глифов, а базового setTextSize хватает в девяти случаях из десяти. Тяжёлые шрифты оставьте для проектов, где нужен типографский вид, — для термометра честнее восемнадцать пикселей и своя точка.
Размер против читаемости
Арифметика размеров простая. Размер 1 — знак 6×8, в строке 21 символ. Размер 2 — знак 12×16, в строке 10 символов: читается с двух-трёх метров. Размер 3 — знак 18×24, в строке 7 символов, высота занимает больше трети экрана. Для термометра я остановился на связке: целая часть размера 3, дробь и единицы — размера 2, шапка и подсказки — размера 1. Строка −12.3 занимает при этом около 72 пикселя ширины, остаётся поле под значок заморозка.
Десятые доли через print(temp, 1) печатаются одной строкой, но точка в ней — глиф шрифта: на размере 3 это клетка 18 пикселей с крошечным пятнышком. Я разделяю значение вручную: целая часть и дробь печатаются отдельно, а точка рисуется fillRect(x, y + 20, 4, 4, SSD1306_WHITE) — четыре пикселя у базовой линии, которых не видно издалека, зато видно с прохода. Цену приёма считаем честно: один fillRect против красивого, но нечитаемого глифа.
Минус — вторая ловушка. Отрицательные значения делают строку переменной длины: −7.3 на цифру короче, чем 12.3, и если просто печатать поверх старого кадра, от прошлой семёрки останется палка. Лечение — стирать зону строки перед печатью или всегда печатать знак: я стираю fillRect по всей строке значения, а минус рисую отдельным fillRect шириной 10 пикселей на середине высоты знака. Такой минус виден издалека лучше, чем глиф шрифта.
| Элемент | Размер | Знак (px) | Ширина в кадре |
|---|---|---|---|
| Целая часть | setTextSize(3) | 18×24 | До 36 px на две цифры |
| Дробь | setTextSize(2) | 12×16 | 12 px на одну цифру |
| Точка | fillRect 4×4 | у базовой линии | 4 px + зазор 2 px |
| Минус | fillRect 10×4 | на середине высоты | 10 px перед числом |
Adafruit_SSD1306 display(128, 64, &Wire, -1); long tKadr = 0;int ledZamoroz = 12; void risujTemperaturu(long desyatye) { // desyatye: -100..400, to est -10.0..40.0 int cel = desyatye / 10; // celaya chast int dol = desyatye % 10; // desyatye doli if (dol < 0) dol = -dol; bool minus = desyatye < 0; display.clearDisplay(); display.setTextColor(SSD1306_WHITE); // Shapka display.setTextSize(1); display.setCursor(0, 0); display.print("TEPLO A0"); // Znacheniye: celaya chast razmera 3 display.setTextSize(3); int x = 6; if (minus) { display.fillRect(x, 26, 10, 4, SSD1306_WHITE); // minus 10x4 x += 16; } display.setCursor(x, 20); display.print(cel); x += 18 * (cel >= 10 || cel <= -10 ? 2 : 1); // Svoya tochka u bazovoy linii display.fillRect(x, 40, 4, 4, SSD1306_WHITE); x += 8; // Desyatye razmera 2 i edinitsy display.setTextSize(2); display.setCursor(x, 28); display.print(dol); display.print("C"); // Futer display.setTextSize(1); display.setCursor(0, 57); display.print("Obnovleniye 500 ms"); display.display();} void setup() { pinMode(ledZamoroz, OUTPUT); display.begin(SSD1306_SWITCHCAPVCC, 0x3C); display.setTextColor(SSD1306_WHITE);} void loop() { if (millis() - tKadr >= 500) { tKadr = millis(); long desyatye = map(analogRead(A0), 0, 1023, -100, 400); risujTemperaturu(desyatye); digitalWrite(ledZamoroz, desyatye < 0 ? HIGH : LOW); }}Разбор по строкам. Значение живёт в десятых долях — целое число от −100 до 400: целочисленный map без float, деление и остаток дают целую часть и десятые без библиотечных округлений. Смена знака не оставляет следов, потому что кадр каждый раз строится с чистого clearDisplay: за 25-30 миллисекунд на I2C перерисовка незаметна глазу, а обновление раз в 500 мс грузит шину на пять процентов. Светодиод на D12 горит при минусе — заморозок для теплицы виден даже спиной к прибору.
Смещение x после целой части считается по ширине знаков: 18 пикселей на цифру размера 3, плюс 16 на минус, плюс 12 на точку с зазором. Эта арифметика — та же сетка знакомест из урока про раскладку, только посчитанная для смешанных размеров в одной строке. Если датчик настоящий, а не ручка: опрос DHT чаще раза в две секунды бессмыслен, он и так отвечает раз в две — интервалы опроса разобраны в уроке про интервал опроса DHT, а таймер кадра на millis оставляю как есть.
Кадр по таймеру: сглаживание и пороги
Десятая доля — самая дёрганая часть показаний. Датчик живой: медленный рост с 21.4 на 21.7 превращается в прыжки последней цифры каждые полсекунды, и глаз читает это как шум прибора. Лечение — скользящее среднее: последние пять значений в массиве, показание — усреднение, и строка оживает плавно. Массив на пять ячеек с кольцевой записью — тот же паттерн журнала, что разобран в уроке про массивы и журнал; шаг усреднения 500 миллисекунд даёт полный цикл за две с половиной секунды — для теплицы честно, для лаборатории медленно, и это надо признать вслух.
Пороговый светодиод на границе нуля мигает, если температура стоит на 0.1: включилось-выключилось-включилось. Гистерезис лечит: включаем при −0.5, выключаем при +0.5, и между ними прибор держит прошлое решение. Зона в один градус стоит дешевле, чем мигающая лампочка, и ни одного лишнего дёргания ноги D12. Тот же приём годится для верхнего порога вентилятора: разница только в числах.
Компактный экран 128×32 сжимает раскладку: четыре строки вместо восьми, и размер 3 в кадр рядом с чем-либо не помещается. Рабочая связка для 128×32 — значение размера 2 в теле и шапка: 16 пикселей на строку, две строки на экран, и это ровно та схема двухполосной раскладки, что разобрана в уроке про перенос на 128×32. Точка и минус при этом рисуются теми же fillRect, только координаты считаются от высоты 16, а не 24.
Единицы измерения — отдельная строка бюджета кадра. Знак градуса в классическом шрифте GFX прячется под кодом 248: включите display.cp437(true) в setup и напечатайте display.write(248) — на экране появится настоящая точка-градус, а не буква o. Без cp437 тот же код может вытянуть не тот глиф из старой половины таблицы, поэтому пара вызовов стоит целиком. Деталь копеечная, но именно она отличает приборный экран от лабораторного макета.
Ширину строки считайте от худшего случая, а не от текущего показания: диапазон −10.0..40.0 — это максимум минус и две цифры целой части, и точка не должна переезжать по строке между кадрами. Если диапазон шире — скажем до −99.9 — отведите место под три цифры и минус заранее: точка, прыгающая вслед за значением, читается хуже, чем мелкий шрифт.
Проведите ручку через ноль и следите за колонками: минус занимает свою клетку, целая часть сдвигается вправо, точка стоит после цифр, и при переходе от −7.3 к 5.0 строка не оставляет мусора — кадр строится с чистого листа каждый раз. В симуляторе точка нарисована звёздочкой, потому что движок печатает текст; на плате тот же fillRect в том же месте даёт аккуратный четырёхпиксельный квадрат у базовой линии. Светодиод переключается ровно на границе нуля — тем же самым значением desyatye, что идёт на экран, без второй пары вычислений.
Половину кадра можно не трогать вовсе: шапка и футер не меняются месяцами, и печатать их каждый кадр — работа для процессора. Вариант экономнее: печать статики один раз в setup, в loop — стирание fillRect только зоны значения и печать цифр, а display() выталкивает весь буфер как обычно. Где режут кадр без полной перерисовки и что это экономит — разобрано в уроке про частичное обновление RAM; для термометра выигрыш скромный, но привычка пригодится на больших интерфейсах.
- 1. Какой размер знака даёт setTextSize(3) на базовом шрифте 6×8?
- 2. Чем плоха точка из шрифта на размере 3?
- 3. Почему строка температуры оставляет мусор при смене знака?
- 4. Зачем держать температуру в целых десятых долях?
- 5. Сколько шины занимает перерисовка кадра 128×64 по I2C?
- Размеры знака: 6×8, 12×16 и 18×24 — в строке 128 px помещается 21, 10 и 7 символов
- Своя точка fillRect 4×4 у базовой линии читается с прохода, глиф шрифта на размере 3 — нет
- Минус рисуют отдельно 10×4 на середине высоты: строка меняет длину, зону значения стирают
- Температуру хранят в целых десятых: map без float, целая часть и дробь через / и %
- Кадр по I2C — 1024 байта и 25-30 мс: обновление раз в 500 мс грузит шину на пять процентов
Частые вопросы по теме урока
- Какой размер знака даёт setTextSize(3) на базовом шрифте 6×8?
- Масштаб кратен базовой ячейке 6×8: размер 3 растягивает знак до 18×24 пикселя, и в строке 128 пикселей помещается семь таких знаков. 18×24
- Чем плоха точка из шрифта на размере 3?
- Глиф точки остаётся крошечным внутри гигантской ячейки. Свою точку рисуют fillRect 4×4 у базовой линии — видно и с расстояния. Пара пикселей в пустой клетке 18×24 — не читается с прохода
- Почему строка температуры оставляет мусор при смене знака?
- −7.3 на знак и цифру короче 12.3: прежние цифры остаются в буфере. Кадр строят с clearDisplay или стирают зону значения fillRect. Строка меняет длину, а печать поверх не стирает старые пиксели
- Зачем держать температуру в целых десятых долях?
- Значение −100..400 в десятых даёт целую часть и дробь через / и %, а граница заморозка — просто desyatye < 0 без плавающей точки. Целочисленный map и деление без float, порог — сравнение с нулём
- Сколько шины занимает перерисовка кадра 128×64 по I2C?
- 1024 байта буфера на 400 кГц — около 25-30 мс. Обновление раз в 500 мс занимает около пяти процентов шины и глазу не видно. 25-30 мс