Загрузка данных
#include <Adafruit_GFX.h>
#include <Adafruit_ST7735.h>
#include <SPI.h>
#include <Preferences.h> // Библиотека для работы с памятью ESP32
// Пины экрана
#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
Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_SDA, TFT_SCL, TFT_RST);
Preferences prefs; // Объект памяти
// Структура для управления цветами темы
struct Theme {
uint16_t bg;
tft.color565(0, 0, 0); // Инициализация ниже
uint16_t text;
uint16_t accent;
uint16_t frame;
};
Theme themes[3];
int current_theme_idx = 0;
// Цвета для удобства
uint16_t C_BG, C_TEXT, C_ACCENT, C_FRAME;
// Переменные вейпа
int watts = 40;
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;
// Состояние крутилки
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 setup() {
// Инициализация тем оформления
themes[0] = {0x0000, 0xFFFF, 0x07E0, 0x39E7}; // 0: Neon Green
themes[1] = {0x0000, 0xFFFF, 0xF6A0, 0x781F}; // 1: Cyber Punk (Желтый + Фиолетовый корпус)
themes[2] = {0x0000, 0xFFFF, 0xF800, 0x7800}; // 2: Inferno Red (Красный)
// Открываем хранилище памяти "vape_stats"
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);
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();
}
void loop() {
unsigned long now = millis();
// 1. ОБРАБОТКА ТРОЙНОГО НАЖАТИЯ ИЛИ КЛИКОВ КНОПКИ
bool key_now = (digitalRead(ENCODER_KEY) == LOW);
static bool key_last = false;
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;
drawMenu();
click_count = 0;
delay(300); // Антидребезг
}
} else {
// Если мы уже в меню, обычный клик работает как ОК (выбор)
handleMenuSelect();
delay(300);
}
}
// Логика удержания кнопки для парения (РАБОТАЕТ ТОЛЬКО НА ГЛАВНОМ ЭКРАНЕ)
if (current_mode == MAIN_SCREEN) {
if (key_now && (now - last_key_press > 400)) { // Держим кнопку дольше 400мс
if (!is_vaping) {
is_vaping = true;
puff_timer = millis();
drawVapingStatus(true);
} else {
float duration = (millis() - puff_timer) / 1000.0;
tft.fillRect(15, 100, 70, 15, 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;
puff_total++;
puff_day++;
puff_week++;
// Сохраняем новые пуфы в память ESP32 железно!
prefs.putInt("total", puff_total);
prefs.putInt("day", puff_day);
prefs.putInt("week", puff_week);
drawStaticUI();
updateWattsUI();
}
}
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) {
// В основном меню бегаем по 3 пунктам
if (clock_wise) menu_select = (menu_select + 1) % 3;
else menu_select = (menu_select - 1 + 3) % 3;
drawMenu();
}
else if (current_mode == MENU_THEME) {
// В меню выбора тем бегаем по 3 темам
if (clock_wise) menu_select = (menu_select + 1) % 3;
else menu_select = (menu_select - 1 + 3) % 3;
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.fillScreen(C_BG);
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.setTextColor(C_FRAME);
tft.setCursor(15, 65); tft.print("COIL: ");
tft.setTextColor(C_TEXT); tft.print(coil_ohm); tft.print(" ohm");
tft.setCursor(15, 80); tft.setTextColor(C_FRAME); tft.print("VOLT: ");
tft.setTextColor(C_TEXT);
float fake_volt = sqrt(watts * coil_ohm);
tft.print(fake_volt, 2); tft.print(" V");
tft.setCursor(100, 65); tft.setTextColor(C_FRAME); tft.print("PUFF");
tft.setCursor(100, 78); tft.setTextColor(C_ACCENT); tft.setTextSize(2);
tft.print(puff_total);
}
void updateWattsUI() {
if (current_mode != MAIN_SCREEN) return;
tft.fillRect(12, 22, 100, 35, C_BG);
tft.fillRect(12, 55, 136, 5, 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);
int bar_w = map(watts, 5, 80, 0, 132);
tft.fillRect(14, 57, bar_w, 2, C_ACCENT);
tft.fillRect(45, 78, 45, 12, C_BG);
tft.setCursor(45, 80); tft.setTextColor(C_TEXT); tft.setTextSize(1);
float fake_volt = sqrt(watts * coil_ohm);
tft.print(fake_volt, 2); tft.print(" V");
}
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...");
}
}
// --- СЕКЦИЯ МЕНЮ НАСТРОЕК ---
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.setTextSize(1);
tft.print(items[i]);
}
}
void drawThemeMenu() {
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("--- SELECT THEME ---");
String t_names[3] = {" > NEON GREEN", " > CYBER PUNK", " > INFERNO RED"};
for (int i = 0; i < 3; i++) {
tft.setCursor(20, 40 + (i * 22));
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.setTextSize(1);
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.setTextSize(1); 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) {
current_mode = MAIN_SCREEN;
drawStaticUI();
updateWattsUI();
}
}
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();
}
}