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


// Вставляем фикс для старых функций MD5 в новых ядрах ESP32
#define mbedtls_md5_starts_ret mbedtls_md5_starts
#define mbedtls_md5_update_ret mbedtls_md5_update
#define mbedtls_md5_finish_ret mbedtls_md5_finish

#include <Adafruit_GFX.h>    
#include <Adafruit_ST7735.h> 
#include <SPI.h>
#include <Preferences.h> 
#include <WiFi.h>
#include <ESPAsyncWebServer.h> 
#include <AsyncTCP.h>
#include "time.h"

const char* ap_ssid     = "ESP32_Vape_Mod";
const char* ap_password = "vape12345678"; 

AsyncWebServer server(80);
AsyncWebSocket ws("/ws");

// НОВЫЕ СВЕРХСКОРОСТНЫЕ ПИНЫ (Аппаратный VSPI)
#define TFT_SCL        18  
#define TFT_SDA        23  
#define TFT_DC         14  
#define TFT_RST        27  
#define TFT_CS         26  

#define ENCODER_S1     25  
#define ENCODER_S2     33  
#define ENCODER_KEY    32  

// Светодиод переехал на свободный D21
#define LED_PIN        21  

// Инициализируем через супер-быстрый аппаратный SPI (&SPI)
Adafruit_ST7735 tft = Adafruit_ST7735(&SPI, TFT_CS, TFT_DC, TFT_RST);
Preferences prefs; 

struct Theme { uint16_t bg; 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 old_watts = -1; 
int puff_total = 0;      
int puff_day = 0;
float coil_ohm = 0.18;     
long puff_timer = 0;       
bool is_vaping = false;
bool block_vape_after_menu = false; 

int last_s1_state;
String old_time_str = "";
String current_status_str = "IDLE"; 

unsigned long last_key_press = 0;
int click_count = 0;
enum Mode { MAIN_SCREEN, MENU };
Mode current_mode = MAIN_SCREEN;

unsigned long last_clock_update = 0;
int rtc_hour = 0;
int rtc_minute = 0;
int rtc_second = 0;
bool time_is_set = false;

// HTML страница для телефона
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
    <meta name=viewport content="width=device-width, initial-scale=1">
    <meta charset=utf-8>
    <title>ESP32 Vape Dashboard</title>
    <style>
        body { font-family: Arial; text-align: center; background: #121212; color: #fff; margin: 0; padding: 20px; }
        .card { background: #1e1e1e; padding: 20px; border-radius: 15px; box-shadow: 0 4px 10px rgba(0,0,0,0.5); max-width: 400px; margin: 20px auto; border: 2px solid #07E0; }
        h1 { color: #07E0; margin-bottom: 5px; }
        .val { font-size: 40px; font-weight: bold; color: #fff; margin: 10px 0; }
        .unit { font-size: 20px; color: #07E0; }
        .status { font-size: 24px; padding: 10px; border-radius: 8px; font-weight: bold; background: #2a2a2a; transition: 0.2s; margin-bottom: 15px; }
        .vaping { background: #ff3333; color: white; animation: blink 1s infinite; }
        .stats { display: flex; justify-content: space-around; margin-top: 20px; font-size: 14px; color: #aaa; margin-bottom: 20px; }
        .btn { background: #07E0; color: #000; border: none; padding: 12px 20px; font-size: 16px; font-weight: bold; border-radius: 8px; cursor: pointer; width: 100%; transition: 0.2s; }
        .btn:active { background: #059900; }
        @keyframes blink { 0% {opacity: 1;} 50% {opacity: 0.5;} 100% {opacity: 1;} }
    </style>
</head>
<body>
    <div class="card">
        <h1>VAPE MOD OS</h1>
        <div id="status" class="status">ПОДКЛЮЧЕНИЕ...</div>
        <div class="val"><span id="watts">--</span><span class="unit"> W</span></div>
        <div style="font-size:18px; color:#888; margin-bottom: 15px;">Вольтаж: <span id="volt" style="color:#fff;">0.00</span> V</div>
        <button class="btn" onclick="syncTime()">СИНХРОНИЗИРОВАТЬ ВРЕМЯ</button>
        <div class="stats">
            <div>Затяжек сегодня:<br><b id="p_day" style="color:#07E0; font-size:16px;">--</b></div>
            <div>Всего затяжек:<br><b id="p_total" style="color:#07E0; font-size:16px;">--</b></div>
        </div>
    </div>
    <script>
        var gateway = `ws://${window.location.hostname}/ws`;
        var websocket;
        window.addEventListener('load', initWebSocket);
        function initWebSocket() {
            websocket = new WebSocket(gateway);
            websocket.onopen = function(e) { document.getElementById('status').innerHTML = 'В СЕТИ'; };
            websocket.onclose = function(e) { document.getElementById('status').innerHTML = 'ОТКЛЮЧЕНО'; setTimeout(initWebSocket, 2000); };
            websocket.onmessage = function(e) {
                var data = JSON.parse(e.data);
                document.getElementById('watts').innerHTML = data.watts;
                document.getElementById('volt').innerHTML = data.volt;
                document.getElementById('p_day').innerHTML = data.p_day;
                document.getElementById('p_total').innerHTML = data.p_total;
                var st = document.getElementById('status');
                st.innerHTML = data.status;
                if(data.status === "VAPING...") { st.classList.add('vaping'); }
                else { st.classList.remove('vaping'); }
            };
        }
        function syncTime() {
            var now = new Date();
            var timeJson = JSON.stringify({
                type: "set_time",
                h: now.getHours(),
                m: now.getMinutes(),
                s: now.getSeconds()
            });
            websocket.send(timeJson);
            alert("Время отправлено на вейп!");
        }
    </script>
</body>
</html>
)rawliteral";

void sendDataToPhone() {
  float fake_volt = sqrt(watts * coil_ohm);
  String json = "{";
  json += "\"watts\":" + String(watts) + ",";
  json += "\"volt\":" + String(fake_volt, 2) + ",";
  json += "\"p_day\":" + String(puff_day) + ",";
  json += "\"p_total\":" + String(puff_total) + ",";
  json += "\"status\":\"" + current_status_str + "\"";
  json += "}";
  ws.textAll(json);
}

void onWsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) {
  if (type == WS_EVT_CONNECT) {
    sendDataToPhone();
  }
  else if (type == WS_EVT_DATA) {
    data[len] = '\0';
    String message = String((char*)data);
    if (message.indexOf("set_time") > 0) {
      int h_idx = message.indexOf("\"h\":") + 4;
      int m_idx = message.indexOf("\"m\":") + 4;
      int s_idx = message.indexOf("\"s\":") + 4;
      rtc_hour = message.substring(h_idx, message.indexOf(",", h_idx)).toInt();
      rtc_minute = message.substring(m_idx, message.indexOf(",", m_idx)).toInt();
      rtc_second = message.substring(s_idx, message.indexOf("}", s_idx)).toInt();
      time_is_set = true;
      last_clock_update = millis();
      tft.fillRect(15, 8, 40, 10, C_BG); 
      old_time_str = ""; 
    }
  }
}

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 updateClockUI(bool force = false);

void setup() {
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, LOW);

  themes[0] = {0x0000, 0xFFFF, 0x07E0, 0x39E7}; 
  themes[1] = {0x0000, 0xFFFF, 0xF6A0, 0x781F}; 
  themes[2] = {0x0000, 0xFFFF, 0xF800, 0x7800}; 

  prefs.begin("vape_stats", false);
  puff_total = prefs.getInt("total", 0);
  puff_day   = prefs.getInt("day", 0);
  current_theme_idx = prefs.getInt("theme", 0);
  watts = prefs.getInt("watts", 40);
  loadTheme(current_theme_idx);

  // 1. ЗАПУСКАЕМ WI-FI (Теперь он не мешает экрану)
  WiFi.softAP(ap_ssid, ap_password);

  ws.onEvent(onWsEvent);
  server.addHandler(&ws);
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send_P(200, "text/html", index_html);
  });
  server.begin(); 

  // 2. ЗАПУСКАЕМ АППАРАТНЫЙ SPI ЭКРАНА НА ПИНАХ 18 И 23
  SPI.begin(TFT_SCL, -1, TFT_SDA, TFT_CS); 

  tft.initR(INITR_BLACKTAB); 
  SPI.setFrequency(27000000); // Экран будет обновляться мгновенно!
  tft.setRotation(1); 
  tft.fillScreen(C_BG);

  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;

  ws.cleanupClients(); 

  if (time_is_set && (now - last_clock_update >= 1000)) {
    last_clock_update += 1000;
    rtc_second++;
    if (rtc_second >= 60) { rtc_second = 0; rtc_minute++; }
    if (rtc_minute >= 60) { rtc_minute = 0; rtc_hour++; }
    if (rtc_hour >= 24) { rtc_hour = 0; }
    updateClockUI();
  }

  if (block_vape_after_menu && key_now) {} 
  else if (block_vape_after_menu && !key_now) { block_vape_after_menu = 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;
        current_status_str = "IN MENU";
        sendDataToPhone();
        analogWrite(LED_PIN, 0); 
        tft.fillScreen(C_BG);
        tft.setCursor(20, 40); tft.print("MENU OPENED");
        click_count = 0;
        delay(300); 
      }
    } else {
      block_vape_after_menu = true; 
      current_mode = MAIN_SCREEN; 
      current_status_str = "IDLE";
      tft.fillScreen(C_BG); drawStaticUI(); updateWattsUI(true);
      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();
        current_status_str = "VAPING...";
        sendDataToPhone(); 
        drawVapingStatus(true);
        analogWrite(LED_PIN, map(watts, 5, 80, 15, 255)); 
      } 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.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++;
      prefs.putInt("total", puff_total); prefs.putInt("day", puff_day);
      current_status_str = "IDLE";
      sendDataToPhone(); 
      tft.fillRect(12, 22, 136, 38, C_BG); tft.fillRect(15, 100, 80, 15, C_BG);
      drawStaticUI(); updateWattsUI(true);
    }
  }
  key_last = key_now;

  int current_s1_state = digitalRead(ENCODER_S1);
  if (current_s1_state != last_s1_state && current_s1_state == LOW) {
    if (current_mode == MAIN_SCREEN) {
      if (digitalRead(ENCODER_S2) != current_s1_state) watts = (watts >= 80) ? 80 : watts + 5;
      else watts = (watts <= 5) ? 5 : watts - 5;
      prefs.putInt("watts", watts); 
      updateWattsUI(); 
      sendDataToPhone(); 
    }
    delay(5);
  }
  last_s1_state = current_s1_state;
}

void updateClockUI(bool force) {
  if (current_mode != MAIN_SCREEN || is_vaping) return;
  if (!time_is_set) {
    tft.setCursor(15, 8); tft.setTextColor(C_FRAME); tft.setTextSize(1);
    tft.print("--:--");
    return;
  }
  char timeStringBuff[6];
  sprintf(timeStringBuff, "%02d:%02d", rtc_hour, rtc_minute);
  String new_time_str = String(timeStringBuff);

  if (new_time_str != old_time_str || force) {
    tft.fillRect(15, 8, 35, 10, C_BG); 
    tft.setCursor(15, 8); tft.setTextColor(C_FRAME); tft.setTextSize(1);
    tft.print(new_time_str);
    old_time_str = new_time_str;
  }
}

void drawStaticUI() {
  tft.drawRect(0, 0, tft.width(), tft.height(), C_FRAME);
  tft.setCursor(15, 65); tft.setTextColor(C_FRAME); tft.print("COIL: ");
  tft.print(coil_ohm); tft.print(" R");
  tft.setCursor(100, 65); tft.print("PUFF");
  tft.setCursor(100, 78); tft.setTextColor(C_ACCENT); tft.setTextSize(2); tft.print(puff_total);
  updateClockUI(true); 
}

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");
  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...");
  }
}