Загрузка данных
#include <Adafruit_GFX.h>
#include <Adafruit_ST7735.h>
#include <SPI.h>
#include <Preferences.h>
// Пины экрана
#define TFT_SCL 13
#define TFT_SDA 12
#define TFT_DC 14
#define TFT_RST 27
#define TFT_CS 26
// Пины энкодера
#define ENCODER_S1 25
#define ENCODER_S2 33
#define ENCODER_KEY 32
// Силовой светодиод на D18
#define LED_PIN 18
Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_SDA, TFT_SCL, TFT_RST);
Preferences prefs;
struct Theme {
uint16_t bg;
uint16_t text;
uint16_t accent;
uint16_t frame;
};
Theme themes[6];
int current_theme_idx = 0;
uint16_t C_BG, C_TEXT, C_ACCENT, C_FRAME;
int watts = 40;
int old_watts = -1; // Для отслеживания изменений
int puff_total = 0;
int puff_day = 0;
int puff_week = 0;
float coil_ohm = 0.18;
long puff_timer = 0;
bool is_vaping = false;
bool block_vape_after_menu = false;
int last_s1_state;
unsigned long last_key_press = 0;
int click_count = 0;
enum Mode { MAIN_SCREEN, MENU, MENU_THEME, MENU_STATS };
Mode current_mode = MAIN_SCREEN;
int menu_select = 0;
void loadTheme(int idx) {
C_BG = themes[idx].bg;
C_TEXT = themes[idx].text;
C_ACCENT = themes[idx].accent;
C_FRAME = themes[idx].frame;
}
void drawStaticUI();
void updateWattsUI(bool force = false);
void drawVapingStatus(bool active);
void drawMenu();
void drawThemeMenu();
void drawStatsMenu();
void handleMenuSelect();
void setup() {
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, LOW);
themes[0] = {0x0000, 0xFFFF, 0x07E0, 0x39E7}; // 1. Neon Green
themes[1] = {0x0000, 0xFFFF, 0xF6A0, 0x781F}; // 2. Cyber Punk
themes[2] = {0x0000, 0xFFFF, 0xF800, 0x7800}; // 3. Inferno Red
themes[3] = {0x0000, 0xFFFF, 0x001F, 0x041F}; // 4. Deep Blue
themes[4] = {0x0000, 0x0000, 0xF81F, 0xF81F}; // 5. Toxic Pink
themes[5] = {0xFFFF, 0x0000, 0x7BEF, 0x0000}; // 6. Light Mode
prefs.begin("vape_stats", false);
puff_total = prefs.getInt("total", 0);
puff_day = prefs.getInt("day", 0);
puff_week = prefs.getInt("week", 0);
current_theme_idx = prefs.getInt("theme", 0);
watts = prefs.getInt("watts", 40);
loadTheme(current_theme_idx);
tft.initR(INITR_BLACKTAB);
SPI.setFrequency(27000000);
tft.setRotation(1);
pinMode(ENCODER_S1, INPUT_PULLUP);
pinMode(ENCODER_S2, INPUT_PULLUP);
pinMode(ENCODER_KEY, INPUT_PULLUP);
last_s1_state = digitalRead(ENCODER_S1);
// Рисуем станичную разметку ОДИН раз при запуске
drawStaticUI();
updateWattsUI(true);
}
void loop() {
unsigned long now = millis();
bool key_now = (digitalRead(ENCODER_KEY) == LOW);
static bool key_last = false;
if (block_vape_after_menu && key_now) {
// Ждем отпускания кнопки
} else if (block_vape_after_menu && !key_now) {
block_vape_after_menu = false;
}
// 1. ОБРАБОТКА НАЖАТИЙ КНОПКИ
if (key_now && !key_last) {
if (current_mode == MAIN_SCREEN) {
if (now - last_key_press > 400) {
click_count = 0;
}
click_count++;
last_key_press = now;
if (click_count == 3) {
current_mode = MENU;
menu_select = 0;
analogWrite(LED_PIN, 0);
drawMenu(); // Меню стирает экран, это нормально
click_count = 0;
delay(300);
}
} else {
handleMenuSelect();
delay(300);
}
}
// Логика затяжки
if (current_mode == MAIN_SCREEN && !block_vape_after_menu) {
if (key_now && (now - last_key_press > 400)) {
if (!is_vaping) {
is_vaping = true;
puff_timer = millis();
drawVapingStatus(true);
int led_brightness = map(watts, 5, 80, 15, 255);
analogWrite(LED_PIN, led_brightness);
} else {
// Локальное обновление ТОЛЬКО таймера затяжки
float duration = (millis() - puff_timer) / 1000.0;
tft.fillRect(15, 100, 60, 12, C_BG); // Затираем кусочек под таймер
tft.setCursor(15, 100);
tft.setTextColor(C_TEXT);
tft.setTextSize(1);
tft.print(duration, 1);
tft.print(" sec");
}
delay(20);
} else if (!key_now && is_vaping) {
is_vaping = false;
analogWrite(LED_PIN, 0);
puff_total++;
puff_day++;
puff_week++;
prefs.putInt("total", puff_total);
prefs.putInt("day", puff_day);
prefs.putInt("week", puff_week);
// Возвращаем интерфейс БЕЗ fillScreen, просто стирая область затяжки
tft.fillRect(12, 22, 136, 38, C_BG);
tft.fillRect(15, 100, 80, 15, C_BG);
drawStaticUI();
updateWattsUI(true);
}
}
key_last = key_now;
// 2. ОБРАБОТКА ВРАЩЕНИЯ КРУТИЛКИ
int current_s1_state = digitalRead(ENCODER_S1);
if (current_s1_state != last_s1_state && current_s1_state == LOW) {
bool clock_wise = (digitalRead(ENCODER_S2) != current_s1_state);
if (current_mode == MAIN_SCREEN) {
if (clock_wise) watts = (watts >= 80) ? 80 : watts + 5;
else watts = (watts <= 5) ? 5 : watts - 5;
prefs.putInt("watts", watts);
updateWattsUI(); // Локально перерисует только цифру ватт
}
else if (current_mode == MENU) {
if (clock_wise) menu_select = (menu_select + 1) % 3;
else menu_select = (menu_select - 1 + 3) % 3;
drawMenu();
}
else if (current_mode == MENU_THEME) {
if (clock_wise) menu_select = (menu_select + 1) % 6;
else menu_select = (menu_select - 1 + 6) % 6;
drawThemeMenu();
}
else if (current_mode == MENU_STATS) {
current_mode = MENU;
menu_select = 1;
drawMenu();
}
delay(5);
}
last_s1_state = current_s1_state;
}
// Рисует только неизменяемые элементы интерфейса
void drawStaticUI() {
tft.drawRect(0, 0, tft.width(), tft.height(), C_FRAME);
// Батарейка
tft.drawRect(125, 8, 20, 10, C_TEXT);
tft.fillRect(145, 11, 2, 4, C_TEXT);
tft.fillRect(127, 10, 16, 6, C_ACCENT);
// Сопротивление
tft.setTextSize(1);
tft.setCursor(15, 65); tft.setTextColor(C_FRAME); tft.print("COIL: ");
tft.fillRect(45, 65, 50, 10, C_BG); tft.setTextColor(C_TEXT); tft.print(coil_ohm); tft.print(" R");
// Статистика затяжек (чистим только числовое поле)
tft.setCursor(100, 65); tft.setTextColor(C_FRAME); tft.print("PUFF");
tft.fillRect(100, 78, 50, 16, C_BG);
tft.setCursor(100, 78); tft.setTextColor(C_ACCENT); tft.setTextSize(2);
tft.print(puff_total);
}
// Моментальное локальное обновление Ватт и Вольт
void updateWattsUI(bool force) {
if (current_mode != MAIN_SCREEN) return;
if (watts == old_watts && !force) return; // Если ничего не поменялось — выходим
// Затираем старые цифры Ватт небольшим черным квадратом
tft.fillRect(12, 22, 110, 32, C_BG);
// Рисуем новые Ватты
tft.setCursor(15, 25); tft.setTextColor(C_TEXT); tft.setTextSize(4); tft.print(watts);
tft.setTextSize(2); tft.setTextColor(C_ACCENT); tft.print(" W");
// Обновляем полоску мощности
tft.drawRect(12, 55, 136, 5, C_FRAME);
tft.fillRect(13, 56, 134, 3, C_BG); // Очистка старой полосы внутри рамки
int bar_w = map(watts, 5, 80, 0, 132);
tft.fillRect(14, 57, bar_w, 2, C_ACCENT);
// Обновляем Вольты
tft.fillRect(45, 80, 45, 10, C_BG);
tft.setCursor(15, 80); tft.setTextColor(C_FRAME); tft.setTextSize(1); tft.print("VOLT: ");
tft.setTextColor(C_TEXT);
float fake_volt = sqrt(watts * coil_ohm);
tft.print(fake_volt, 2); tft.print(" V");
old_watts = watts; // Запоминаем текущее значение
}
void drawVapingStatus(bool active) {
if (active) {
tft.fillRect(12, 22, 136, 38, C_BG);
tft.setCursor(15, 30); tft.setTextColor(C_ACCENT); tft.setTextSize(3); tft.print("VAPING...");
}
}
// Отрисовка меню (тут fillScreen нужен, так как меняется весь экран)
void drawMenu() {
tft.fillScreen(C_BG);
tft.drawRect(0, 0, tft.width(), tft.height(), C_FRAME);
tft.setCursor(25, 12); tft.setTextColor(C_ACCENT); tft.setTextSize(1); tft.print("--- SETTINGS ---");
String items[3] = {" 1. THEME SELECTION", " 2. PUFF STATISTICS", " 3. EXIT TO HOME"};
for (int i = 0; i < 3; i++) {
tft.setCursor(15, 40 + (i * 22));
if (i == menu_select) tft.setTextColor(C_BG, C_ACCENT);
else tft.setTextColor(C_TEXT);
tft.print(items[i]);
}
}
void drawThemeMenu() {
tft.fillScreen(C_BG);
tft.drawRect(0, 0, tft.width(), tft.height(), C_FRAME);
tft.setCursor(25, 10); tft.setTextColor(C_ACCENT); tft.setTextSize(1); tft.print("--- SELECT THEME ---");
String t_names[6] = {" > NEON GREEN", " > CYBER PUNK", " > INFERNO RED", " > DEEP BLUE", " > TOXIC PINK", " > LIGHT MODE"};
for (int i = 0; i < 6; i++) {
tft.setCursor(15, 30 + (i * 15));
if (i == menu_select) tft.setTextColor(C_BG, C_ACCENT);
else tft.setTextColor(C_TEXT);
tft.print(t_names[i]);
}
}
void drawStatsMenu() {
tft.fillScreen(C_BG);
tft.drawRect(0, 0, tft.width(), tft.height(), C_FRAME);
tft.setCursor(20, 12); tft.setTextColor(C_ACCENT); tft.setTextSize(1); tft.print("--- PUFF HISTORY ---");
tft.setTextColor(C_TEXT);
tft.setCursor(15, 40); tft.print("TODAY PUFFS: "); tft.setTextColor(C_ACCENT); tft.print(puff_day);
tft.setTextColor(C_TEXT);
tft.setCursor(15, 60); tft.print("WEEK TOTAL: "); tft.setTextColor(C_ACCENT); tft.print(puff_week);
tft.setTextColor(C_TEXT);
tft.setCursor(15, 80); tft.print("YEAR TOTAL: "); tft.setTextColor(C_ACCENT); tft.print(puff_total);
tft.setTextColor(C_FRAME);
tft.setCursor(20, 108); tft.print("Turn knob to return");
}
void handleMenuSelect() {
if (current_mode == MENU) {
if (menu_select == 0) { current_mode = MENU_THEME; menu_select = current_theme_idx; drawThemeMenu(); }
else if (menu_select == 1) { current_mode = MENU_STATS; drawStatsMenu(); }
else if (menu_select == 2) {
block_vape_after_menu = true;
current_mode = MAIN_SCREEN;
tft.fillScreen(C_BG); // Очищаем экран один раз при возврате домой
drawStaticUI();
updateWattsUI(true);
}
}
else if (current_mode == MENU_THEME) {
current_theme_idx = menu_select;
prefs.putInt("theme", current_theme_idx);
loadTheme(current_theme_idx);
current_mode = MENU; menu_select = 0; drawMenu();
}
else if (current_mode == MENU_STATS) { current_mode = MENU; menu_select = 1; drawMenu(); }
}