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


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