Загрузка данных


#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();
  }
}