Вторая жизнь дисплея электронной сигареты
Тот, кто разбирал такую же электронную сигарету с экраном, знает, что внутри две платы: основная с контроллером и аккумулятором и отдельная плата дисплея со своим микроконтроллером. Значит, между ними есть свой протокол. Разбирали мы PLONQ. Другие модели не проверяли: если у вас другая сигарета, сначала сверьте подписи контактов и запишите кадры анализатором, протокол может отличаться. Мы его расшифровали, научились рисовать на дисплее что угодно из того, что в нём есть, и сделали из него метеостанцию на ESP32-C3 Super Mini.

Что внутри
Основная плата зелёная, на ней контроллер Puya PY32C642, он управляет нагревом и считает заряд аккумулятора. Плата дисплея чёрная, с маркировкой 03-LED-V5, на ней свой контроллер Puya. Платы соединены четырьмя проводами.
| Провод | Тип | Назначение |
|---|---|---|
| 5V | вход питания дисплея | питание платы дисплея |
| GND | общий | общий провод |
| TX (на плате C-D) | вход дисплея | данные от основной платы к дисплею |
| KEY | выход дисплея | кнопка на плате дисплея |
Данные идут только в одну сторону, от основной платы к дисплею. Обратно дисплей ничего не отвечает.



Как расшифровали протокол
Линию TX подключили к логическому анализатору и сделали затяжку, чтобы основная плата начала слать кадры. Видны короткие пачки импульсов, самый короткий импульс около 104 мкс. Это скорость 9600 бит/с. В каждом символе 11 бит: старт, 8 бит данных, бит чётности и стоп. Чётность нечётная, то есть формат 9600 8O1. Между пачками основная плата держит линию в низком уровне и поднимает её примерно за один бит до начала кадра.

Кадр состоит из четырёх байт, каждый кадр повторяется четыре раза с интервалом около 6 мс.
| Байт | Назначение |
|---|---|
| 0 | шевроны (5 полос) и бег кольца вокруг кнопки |
| 1 | значки: ракета, хвост ракеты, капля, батарея, кольцо |
| 2 | число от 0 до 199, значения 200–255 гасят цифры |
| 3 | контрольная сумма |
Контрольную сумму подобрали по записанным кадрам: это сумма первых трёх байт плюс 0xA5, младший байт.
cs = (b0 + b1 + b2 + 0xA5) & 0xFF
Пример: обычный экран с зарядом 95 % — это кадр 1F 3A 5F 5D.
Карта битов дисплея
Дальше подключили к линии TX USB-TTL переходник на CH340 и перебрали все биты по одному, каждый раз фотографируя экран.

Байт 0 — шевроны. Биты 0–4 включают полосы: 10 ближняя к ракете, 01 дальняя. Бит 7 (80) запускает бег огней по кольцу вокруг кнопки. Биты 5 и 6 ничего не делают.
Байт 1 — значки.
| Маска | Что загорается |
|---|---|
| 01 | ракета красная |
| 02 | ракета белая (01 и 02 вместе — розовая) |
| 04 | красное кольцо вокруг кнопки |
| 08 | белое кольцо, подсветка кнопки |
| 10 | капля |
| 20 | полоска батареи над цифрами |
| 40 | красный хвост ракеты |
| 80 | ничего |
Байт 2 — число. На дисплее «1» и две цифры, поэтому максимум 199. Две младшие цифры горят всегда вместе: 5 показывается как «05». Гаснуть отдельно умеет только старшая «1». Отдельными сегментами управлять нельзя.
Несколько особенностей, которые сэкономят время:
- По кольцу бегают только красный и розовый цвет (
04и04|08вместе с битом80). Белый отдельно с битом бега не загорается. - Кадр
80 24 00выключает экран. - После подачи питания дисплей спит и не реагирует на кадры, пока один раз не нажать кнопку.
- Провод KEY нельзя оставлять висящим в воздухе: дисплей перестаёт отвечать. Нужна подтяжка к питанию.
- Переключение цвета подсветки кнопкой плата дисплея обрабатывает сама, по линии TX это не передаётся.
Как дисплей работает в сигарете
Во время затяжки шевроны бегут снизу вверх (10 18 1C 1E 1F 00), а число показывает заряд. После затяжки около 7 секунд горит обычный экран, затем всё плавно гаснет: шевроны по одному, потом значки, потом цифры. При нажатии кнопки то же самое идёт в обратном порядке. Если снять испаритель, экран мигает: обычный кадр, через 0,7 с — кадр 1F FF BC со всеми сегментами.
Управление с компьютера
Для проверки хватит USB-TTL переходника. Провод TX от основной платы отключаем, вместо него подаём TX переходника через резистор 1 кОм. Земля общая, дисплей питаем от 5 В.
Нажмите, чтобы посмотреть код
import serial
def frame(b0, b1, b2):
b = [b0, b1, b2]
return bytes(b + [(sum(b) + 0xA5) & 0xFF])
port = serial.Serial('/dev/ttyUSB0', 9600, parity=serial.PARITY_ODD)
port.write(frame(0x1F, 0x3A, 42) * 4) # обычный экран, число 42
Видео на ВК
Видео на YouTube
Монитор компьютера без ESP32
Если Wi-Fi не нужен, дисплей можно подключить к компьютеру через тот же переходник CH340 и выводить на него параметры самого компьютера. Плата микроконтроллера в этом случае не нужна вообще.
| CH340 | Тип | Дисплей |
|---|---|---|
| 5V | выход питания | 5V |
| GND | общий | GND |
| TXD | цифровой выход | TX (C-D) через 1 кОм |
| — | — | KEY через 10 кОм на 5V (подтяжка) |
Скрипт pc_monitor.py на Python работает в Windows и Linux, нужны библиотеки pyserial и psutil. Что он показывает:
- цифры — температура процессора;
- шевроны — загрузка процессора, от 0 до 5 полос;
- ракета — белая до 60 °C, с красным хвостом от 60 до 80 °C, красная выше 80 °C;
- полоска батареи — занято больше 75 % оперативной памяти;
- капля — на 2 секунды вместо температуры показываются обороты вентилятора в сотнях: 12 означает 1200 об/мин.
В Windows библиотека psutil не видит датчики температуры и вентиляторов, поэтому вместо температуры на цифрах будет загрузка процессора в процентах. Вывести туда что-то своё можно, поменяв пару строк: например, число непрочитанных писем или курс валюты.
Нажмите, чтобы посмотреть код pc_monitor.py
#!/usr/bin/env python3
"""Монитор компьютера на дисплее от электронной сигареты через USB-TTL переходник CH340.
Подключение: TX переходника -> резистор 1 кОм -> TX дисплея (C-D), GND -> GND,
5V переходника -> 5V дисплея, KEY дисплея -> 5V через резистор 10 кОм (подтяжка).
После подачи питания один раз нажмите кнопку дисплея.
Что показывает:
цифры — температура процессора (если датчика нет, например в Windows, — загрузка процессора в %)
шевроны — загрузка процессора, 0–5 полос
ракета — белая до 60 °C, с красным хвостом 60–80 °C, красная выше 80 °C
батарея — занято больше 75 % оперативной памяти
капля — каждые 4 с вместо температуры показывает обороты вентилятора в сотнях (12 = 1200 об/мин), если датчик есть
Установка: pip install pyserial psutil
Запуск: python pc_monitor.py (порт найдётся сам)
python pc_monitor.py COM5 (Windows)
python pc_monitor.py /dev/ttyUSB0 (Linux)
"""
import sys, time
import psutil, serial
from serial.tools import list_ports
R, W, RING_W, DROP, BATT, TAIL = 0x01, 0x02, 0x08, 0x10, 0x20, 0x40
BARS = [0x00, 0x10, 0x18, 0x1C, 0x1E, 0x1F]
PERIOD = 0.25 # обновление 4 раза в секунду
def frame(port, b0, b1, b2):
b = [b0, b1, b2]
port.write(bytes(b + [(sum(b) + 0xA5) & 0xFF]) * 4) # кадр шлём 4 раза, как родная плата
def find_port():
for p in list_ports.comports():
if p.vid == 0x1A86: # CH340
return p.device
sys.exit('Переходник CH340 не найден, укажите порт: python pc_monitor.py COM5')
def cpu_temp():
temps = getattr(psutil, 'sensors_temperatures', lambda: {})()
for name in ('coretemp', 'k10temp', 'zenpower', 'cpu_thermal', 'acpitz'):
for s in temps.get(name, []):
if s.current:
return s.current
return None
def fan_rpm():
fans = getattr(psutil, 'sensors_fans', lambda: {})()
rpm = [f.current for lst in fans.values() for f in lst if f.current]
return max(rpm) if rpm else None
def main():
port = serial.Serial(sys.argv[1] if len(sys.argv) > 1 else find_port(),
9600, parity=serial.PARITY_ODD)
psutil.cpu_percent(None)
load, tick = 0.0, 0
while True:
time.sleep(PERIOD)
tick += 1
load = 0.6 * load + 0.4 * psutil.cpu_percent(None) # сглаживание
t = cpu_temp()
rpm = fan_rpm()
icons = RING_W
if t is None:
icons |= W
number = round(load)
else:
icons |= W if t < 60 else (W | TAIL if t < 80 else R)
number = round(t)
if psutil.virtual_memory().percent > 75:
icons |= BATT
if rpm and tick % 32 >= 24: # 2 с из 8 — обороты вентилятора
icons |= DROP
number = round(rpm / 100)
bars = BARS[min(5, int(load / 20 + 0.999))] if load > 2 else 0
frame(port, bars, icons, max(0, min(199, number)))
if __name__ == '__main__':
main()
Метеостанция и курс доллара на ESP32-C3
Основную плату сигареты заменили на ESP32-C3 Super Mini. Логика 3,3 В дисплей понимает нормально, даже когда сам он питается от 5 В. Протокол формируется программно на любом выводе с длительностью бита 104 мкс.
| ESP32-C3 | Тип | Дисплей |
|---|---|---|
| 5V | выход питания | 5V |
| GND | общий | GND |
| GPIO4 | цифровой выход | TX (C-D) через 1 кОм |
| GPIO6 | цифровой вход, подтяжка включена | KEY |

Погоду плата берёт с бесплатного сервиса met.no раз в 10 минут. На дисплее по кругу идут три фазы, и в каждой цифры показывают свою величину.
Температура и облачность
- цифры — температура, полоска батареи означает минус;
- ракета — облачность: красная — ясно, розовая — переменная облачность, белая — облачно;
- красный хвост ракеты — в ближайший час ожидаются осадки.
Ветер
- ракета гаснет, горят только полоски хвоста;
- цифры — скорость ветра в м/с;
- шевроны — шкала ветра, одна полоса на каждые 3 м/с.
Влажность
- горит капля, ракеты нет;
- цифры — относительная влажность в процентах;
- шевроны — шкала влажности, одна полоса на каждые 20 %.
Между фазами цифры на мгновение гаснут, чтобы было видно, что величина сменилась. Белое кольцо вокруг кнопки означает, что включён режим погоды, а бегущее красное кольцо — сильный ветер, от 10 м/с.

Курс доллара на экране
Для перехода во второй режим нужно нажать кнопку. Второй режим показывает курс доллара к рублю. Плата берёт его с Coinbase раз в 30 минут и сравнивает с курсом прошлого дня. Сервис бесплатный и работает без ключа.
- красное кольцо вокруг кнопки — включён режим курса;
- цифры — рубли, затем копейки;
- полоска батареи — это запятая: когда она горит, на цифрах копейки. На фото ниже сначала 81, потом 96 с полоской — значит, в день съёмки курс был 81,96 рубля.
- красная ракета, шевроны бегут вниз — рубль дешевеет, курс доллара вырос;
- белая ракета с мигающим красным хвостом, шевроны бегут вверх — рубль дорожает, курс доллара снизился;
- белая ракета, шевроны стоят — курс за день изменился меньше чем на 10 копеек.
Обратите внимание: полоска батареи в разных режимах значит разное. В режиме погоды это минус перед температурой, в режиме курса — запятая.
Кнопкой на дисплее режимы переключаются по кругу: погода, курс, выключено. Выбранный режим сохраняется в памяти платы. В нашей домашней версии есть ещё режим солнечной станции: цифры показывают заряд аккумулятора, шевроны бегут вверх при зарядке и вниз при разряде.

Прошивка для Arduino IDE
Скетч ниже — полная прошивка метеостанции и курса доллара. Сторонние библиотеки не нужны, всё входит в пакет esp32 от Espressif. В Arduino IDE выберите плату ESP32C3 Dev Module и включите USB CDC On Boot. В начале скетча впишите имя и пароль своей сети Wi-Fi, а также координаты города для прогноза погоды. Остальные настройки можно не трогать.
Нажмите, чтобы посмотреть код vape_weather.ino
// Метеостанция и курс доллара на дисплее от электронной сигареты (вейпа) и ESP32-C3 Super Mini
// Статья: https://duino.ru/blog/vtoraya-zhizn-displeya-elektronnoy-sigarety/
//
// Arduino IDE: плата "ESP32C3 Dev Module" (пакет esp32 от Espressif), USB CDC On Boot: Enabled.
// Библиотеки ставить не нужно, всё входит в пакет esp32.
//
// Подключение:
// ESP32-C3 5V -> дисплей 5V
// ESP32-C3 GND -> дисплей GND
// ESP32-C3 GPIO4 -> резистор 1 кОм -> дисплей TX (на плате C-D)
// ESP32-C3 GPIO6 -> дисплей KEY (кнопка, подтяжка включается в скетче)
//
// Режим ПОГОДА (белое кольцо вокруг кнопки):
// цифры — температура, полоска батареи над цифрами = минус
// ракета — облачность: красная ясно, розовая переменно, белая пасмурно
// хвост ракеты — в ближайший час осадки
// затем ВЕТЕР — только полоски хвоста без ракеты, цифры = ветер в м/с, шевроны 1 полоса на 3 м/с
// затем ВЛАЖНОСТЬ — капля без ракеты, цифры = влажность в %, шевроны 1 полоса на 20 %
// кольцо — белое = работает, бегущее красное = ветер от 10 м/с
// нет данных — мигает красная ракета
// Режим КУРС ДОЛЛАРА (красное кольцо), Coinbase раз в 30 минут, сравнение с курсом прошлого дня:
// цифры — рубли, затем на 1,5 с копейки с полоской батареи
// рубль дешевеет (курс вырос) — красная ракета, шевроны бегут вниз
// рубль дорожает (курс упал) — белая ракета, мигает красный хвост, шевроны бегут вверх
// изменение меньше 10 копеек — белая ракета, шевроны стоят
// Кнопка на дисплее: погода -> курс -> экран выключен -> погода (режим запоминается).
#include <WiFi.h>
#include <HTTPClient.h>
#include <WiFiClientSecure.h>
#include <Preferences.h>
// ======== НАСТРОЙКИ ========
const char *WIFI_SSID = "имя_сети";
const char *WIFI_PASS = "пароль";
// Координаты города (сейчас Москва). Свои можно взять в Яндекс или Google Картах.
const char *LAT = "55.7558";
const char *LON = "37.6173";
// ===========================
#define TX_PIN 4
#define KEY_PIN 6
#define BIT_US 104 // 9600 бит/с
// met.no требует User-Agent с названием программы
const char *UA = "vape-weather-display/1.0 (ESP32-C3; duino.ru)";
const uint32_t WX_POLL_MS = 10UL * 60 * 1000; // погода раз в 10 минут
const uint32_t BAR_MS = 280, HOLD_MS = 4000, GAP_MS = 600, FRAME_MS = 200;
const float WIND_PER_BAR = 3.0, HUM_PER_BAR = 20.0, WIND_ALERT = 10.0;
const char *FX_URL = "https://api.coinbase.com/v2/prices/USD-RUB/spot";
const uint32_t FX_POLL_MS = 30UL * 60 * 1000; // курс раз в 30 минут
const uint32_t RUN_MS = 220, RUN_TOTAL_MS = 6000, KOP_MS = 1500;
const float FX_FLAT = 0.10; // меньше 10 копеек — без изменений
// Байт 1 — значки
enum { RED = 0x01, WHITE = 0x02, RING_R = 0x04, RING_W = 0x08, DROP = 0x10, BATT = 0x20, TAIL = 0x40 };
// Байт 0 — шевроны (бит 4 ближний к ракете ... бит 0 дальний), бит 7 запускает бег по кольцу
const uint8_t RUN_RING = 0x80;
const uint8_t BAR[6] = { 0x00, 0x10, 0x18, 0x1C, 0x1E, 0x1F };
const uint8_t BLANK = 0xFF; // число 200..255 гасит цифры
Preferences prefs;
enum Mode { WEATHER = 0, FX = 1, OFF = 2, NMODES = 3 };
int mode = WEATHER;
// ---------- Передача на дисплей: программный UART 9600 8O1 ----------
// Между кадрами линия лежит в низком уровне, как у родной платы.
static inline void bitOut(uint8_t v) { digitalWrite(TX_PIN, v); delayMicroseconds(BIT_US); }
void sendByte(uint8_t b) {
uint8_t ones = 0;
bitOut(0); // старт
for (uint8_t i = 0; i < 8; i++) { uint8_t v = (b >> i) & 1; ones += v; bitOut(v); }
bitOut(!(ones & 1)); // бит нечётности
bitOut(1); // стоп
}
// Кадр: шевроны, значки, число, контрольная сумма. Шлём 4 раза, как родная плата.
void frame(uint8_t bars, uint8_t icons, uint8_t num) {
uint8_t p[4] = { bars, icons, num, (uint8_t)(bars + icons + num + 0xA5) };
for (int rep = 0; rep < 4; rep++) {
portDISABLE_INTERRUPTS(); // чтобы Wi-Fi не сбивал длительность битов
digitalWrite(TX_PIN, HIGH); delayMicroseconds(BIT_US);
for (int i = 0; i < 4; i++) sendByte(p[i]);
digitalWrite(TX_PIN, LOW);
portENABLE_INTERRUPTS();
delay(6);
}
}
void screenOff() { frame(0x80, 0x24, 0x00); }
// ---------- Погода с met.no (отдельная задача, чтобы не тормозить анимацию) ----------
volatile float gTemp = 0, gHum = 0, gCloud = 0, gWind = 0, gPrecip = 0;
volatile uint32_t gWxAt = 0;
volatile float gFx = 0, gFxPrev = 0; // курс сейчас и закрытие прошлого дня
volatile uint32_t gFxAt = 0;
bool showKop = false; // на цифрах копейки вместо рублей
bool num(const String &s, const char *key, int from, float &out) {
String k = String("\"") + key + "\":";
int i = s.indexOf(k, from);
if (i < 0) return false;
out = s.substring(i + k.length()).toFloat();
return true;
}
bool parseWeather(const String &body) {
int cur = body.indexOf("\"instant\":{\"details\":{"); // первая запись прогноза = сейчас
if (cur < 0) return false;
float t, h, c, w, p = 0;
if (!num(body, "air_temperature", cur, t) || !num(body, "relative_humidity", cur, h) ||
!num(body, "cloud_area_fraction", cur, c) || !num(body, "wind_speed", cur, w)) return false;
int n1 = body.indexOf("\"next_1_hours\"", cur);
if (n1 > 0) num(body, "precipitation_amount", n1, p);
gTemp = t; gHum = h; gCloud = c; gWind = w; gPrecip = p;
return true;
}
void weatherTask(void *) {
String url = String("https://api.met.no/weatherapi/locationforecast/2.0/compact?lat=") + LAT + "&lon=" + LON;
for (;;) {
if (WiFi.status() != WL_CONNECTED) { WiFi.reconnect(); vTaskDelay(pdMS_TO_TICKS(5000)); continue; }
WiFiClientSecure tls; tls.setInsecure();
HTTPClient http; http.setTimeout(8000); http.useHTTP10(true); http.setUserAgent(UA);
int code = http.begin(tls, url) ? http.GET() : -1;
String head;
if (code == 200) { // прогноз около 40 КБ, текущая погода в самом начале
WiFiClient *st = http.getStreamPtr();
uint32_t t0 = millis();
while (head.length() < 3500 && millis() - t0 < 5000) {
while (st->available() && head.length() < 3500) head += (char)st->read();
int p = head.indexOf("precipitation_amount");
if (p > 0 && head.indexOf('}', p) > 0) break;
delay(5);
}
}
http.end();
bool ok = code == 200 && parseWeather(head);
if (ok) {
gWxAt = millis();
Serial.printf("Погода: %.1f C, влажность %.0f %%, облачность %.0f %%, ветер %.1f м/с, осадки %.1f мм\n",
gTemp, gHum, gCloud, gWind, gPrecip);
} else Serial.printf("Не удалось получить погоду, код %d\n", code);
vTaskDelay(pdMS_TO_TICKS(ok ? WX_POLL_MS : 30000));
}
}
// ---------- Курс доллара с Coinbase ----------
// Курс с Coinbase: сейчас (date = nullptr) или на дату ГГГГ-ММ-ДД. GET https://api.coinbase.com/v2/prices/USD-RUB/spot[?date=YYYY-MM-DD] -> "amount":"81.95..."
float cbSpot(const char *date) {
WiFiClientSecure tls; tls.setInsecure();
HTTPClient http; http.setTimeout(8000); http.useHTTP10(true);
String url = String(FX_URL) + (date ? String("?date=") + date : String(""));
int code = http.begin(tls, url) ? http.GET() : -999;
String body = code == 200 ? http.getString() : "";
http.end();
int i = body.indexOf("\"amount\":\"");
if (code != 200 || i < 0) { Serial.printf("fx: coinbase %s code %d\n", date ? date : "now", code); return 0; }
return body.substring(i + 10).toFloat();
}
void fxTask(void *) {
for (;;) {
if (WiFi.status() != WL_CONNECTED) { vTaskDelay(pdMS_TO_TICKS(5000)); continue; }
for (int i = 0; i < 40 && time(nullptr) < 1700000000; i++) vTaskDelay(pdMS_TO_TICKS(500)); // ждём время по NTP
float v = cbSpot(nullptr), prev = 0;
time_t now = time(nullptr);
if (now > 1700000000) { // вчерашняя дата (UTC)
time_t y = now - 86400; struct tm t; gmtime_r(&y, &t);
char d[11]; strftime(d, sizeof d, "%Y-%m-%d", &t);
prev = cbSpot(d);
}
bool ok = v > 1;
if (ok) {
gFx = v; if (prev > 1) gFxPrev = prev; gFxAt = millis();
Serial.printf("Курс доллара: %.2f, вчера %.2f\n", gFx, gFxPrev);
}
vTaskDelay(pdMS_TO_TICKS(ok ? FX_POLL_MS : 120000));
}
}
int fxTrend() { // +1 рубль дорожает, -1 дешевеет, 0 без изменений
if (gFxPrev <= 0) return 0;
float d = gFx - gFxPrev;
return d > FX_FLAT ? -1 : (d < -FX_FLAT ? 1 : 0);
}
// ---------- Кнопка на дисплее ----------
bool pressed = false;
void pollKey() {
static uint8_t last = HIGH; static uint32_t t = 0;
uint8_t k = digitalRead(KEY_PIN);
if (k != last && millis() - t > 40) {
last = k; t = millis();
if (k == LOW) {
static bool woke = false; // первое нажатие после включения только будит дисплей
if (woke) mode = (mode + 1) % NMODES;
woke = true;
prefs.putInt("mode", mode);
pressed = true;
}
}
}
// ---------- Что показать ----------
bool fresh() {
if (mode == FX) return gFxAt && millis() - gFxAt < 2 * FX_POLL_MS + 60000;
return gWxAt && millis() - gWxAt < 2 * WX_POLL_MS + 60000;
}
int wxPhase = 0; // погода: 0 — температура и облачность, 1 — ветер, 2 — влажность
bool windy() { return mode == WEATHER && fresh() && gWind >= WIND_ALERT; }
uint8_t rocket() {
if (!fresh()) return ((millis() / 400) & 1) ? RED : 0; // нет данных: мигает красная ракета
if (mode == FX) {
int tr = fxTrend();
if (tr < 0) return RED;
if (tr > 0) return WHITE | (((millis() / 250) & 1) ? TAIL : 0);
return WHITE;
}
if (wxPhase == 1) return TAIL; // ветер: полоски без ракеты
if (wxPhase == 2) return 0; // влажность: только капля
uint8_t ic = gCloud < 30 ? RED : (gCloud <= 70 ? (RED | WHITE) : WHITE);
if (gPrecip > 0.05f) ic |= TAIL;
return ic;
}
// Держим один кадр ms миллисекунд. false — нажали кнопку.
bool hold(uint32_t ms, bool showNum, bool showRocket, uint8_t bars, uint8_t extra) {
uint32_t t0 = millis();
while (millis() - t0 < ms) {
pollKey();
if (pressed) return false;
uint8_t ic = extra;
if (fresh()) ic |= (mode == FX || windy()) ? RING_R : RING_W;
if (showRocket) ic |= rocket();
uint8_t n = BLANK;
if (showNum && fresh() && mode == FX) {
int k = (int)lroundf(gFx * 100);
if (showKop) { ic |= BATT; n = k % 100; } // копейки
else n = k / 100 > 199 ? 199 : k / 100; // рубли
} else if (showNum && fresh()) {
float x = wxPhase == 1 ? gWind : wxPhase == 2 ? gHum : fabsf(gTemp);
if (wxPhase == 0 && gTemp < -0.5f) ic |= BATT; // минус
int v = (int)lroundf(x);
n = v > 199 ? 199 : v;
}
frame(bars | (windy() ? RUN_RING : 0), ic, n);
delay(FRAME_MS);
}
return true;
}
int nBars(float v, float per) { int n = (int)(v / per + 0.5f); return n < 0 ? 0 : (n > 5 ? 5 : n); }
// Шевроны растут по одному, держатся, убираются по одному
bool bars(int n, uint8_t extra) {
for (int i = 1; i <= n; i++) if (!hold(BAR_MS, true, true, BAR[i], extra)) return false;
if (!hold(HOLD_MS, true, true, BAR[n], extra)) return false;
for (int i = n - 1; i >= 0; i--) if (!hold(BAR_MS, true, true, BAR[i], extra)) return false;
return true;
}
// Бегущие шевроны: up = вверх (рубль дорожает), иначе вниз
bool running(bool up) {
for (uint32_t step = 0, t0 = millis(); millis() - t0 < RUN_TOTAL_MS; step++) {
int phase = up ? (int)step : -(int)step;
uint8_t b = 0;
for (int pos = 0; pos < 5; pos++) // pos 0 — нижний шеврон (бит 4)
if ((((pos - phase) % 3) + 3) % 3 == 0) b |= 1 << (4 - pos);
if (!hold(RUN_MS, true, true, b, 0)) return false;
}
return true;
}
void cycleFx() {
if (!hold(GAP_MS, true, true, 0, 0)) return;
int tr = fxTrend();
if (tr != 0) { if (!running(tr > 0)) return; }
else if (!hold(HOLD_MS, true, true, 0, 0)) return;
showKop = true;
bool ok = hold(KOP_MS, true, true, 0, 0);
showKop = false;
(void)ok;
}
void cycle() {
if (!hold(GAP_MS, true, false, 0, 0)) return; // цифры
if (!hold(GAP_MS, true, true, 0, 0)) return; // ракета
if (!hold(HOLD_MS, true, true, 0, 0)) return; // температура и облачность
if (!hold(GAP_MS, false, false, 0, 0)) return;
wxPhase = 1; // ветер: полоски, м/с, шевроны
if (!hold(GAP_MS, true, true, 0, 0)) return;
if (!bars(nBars(gWind, WIND_PER_BAR), 0)) return;
if (!hold(GAP_MS, false, false, 0, 0)) return;
wxPhase = 2; // влажность: капля, %, шевроны
if (!hold(GAP_MS, true, true, 0, DROP)) return;
if (!bars(nBars(gHum, HUM_PER_BAR), DROP)) return;
if (!hold(GAP_MS, false, false, 0, 0)) return;
wxPhase = 0;
hold(GAP_MS, true, false, 0, 0);
}
void setup() {
pinMode(TX_PIN, OUTPUT); digitalWrite(TX_PIN, LOW);
pinMode(KEY_PIN, INPUT_PULLUP); // без подтяжки дисплей перестаёт отвечать
Serial.begin(115200);
prefs.begin("vape", false);
mode = prefs.getInt("mode", WEATHER) % NMODES;
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS);
configTime(3 * 3600, 0, "pool.ntp.org", "time.google.com"); // дата нужна для вчерашнего курса
xTaskCreate(weatherTask, "weather", 10240, nullptr, 1, nullptr);
xTaskCreate(fxTask, "fx", 10240, nullptr, 1, nullptr);
// После включения питания дисплей спит, пока один раз не нажать его кнопку.
}
void loop() {
if (pressed) { pressed = false; delay(300); }
if (mode == OFF) { screenOff(); for (int i = 0; i < 50 && !pressed; i++) { pollKey(); delay(20); } return; }
wxPhase = 0;
if (!fresh()) { hold(1000, true, true, 0, 0); return; }
if (mode == FX) cycleFx(); else cycle();
}
Итог
Дисплей, который обычно уходит в мусор вместе с сигаретой, оказался готовым индикатором с простым протоколом: три байта данных, контрольная сумма, обычный UART. На нём можно выводить погоду, заряд аккумулятора, загрузку процессора компьютера или показания любого датчика.
0 комментариев