Newer
Older
smart-home-server / devices / relay / relay_esp8266 / Global.h
#ifndef GLOBAL_H
#define GLOBAL_H

#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <EEPROM.h>

#include "Config.h"

// -------------------- EEPROM layout --------------------
//  0..31   - SSID (макс 32 байта, включая '\0')
// 32..95   - PASS (макс 64 байта, включая '\0')
//    96    - isOn (1 байт)
//    97    - deviceMode (1 байт)
// 98..129  - deviceName (макс 32 байта, включая '\0')
// 130..193 - authToken (макс 64 байта, включая '\0')

const uint16_t EEPROM_SIZE          = 256;   // было 128, теперь немного с запасом

const uint16_t SSID_ADDR            = 0;
const uint16_t SSID_MAX_LEN         = 32;

const uint16_t PASS_ADDR            = SSID_ADDR + SSID_MAX_LEN; // 32
const uint16_t PASS_MAX_LEN         = 64;                       // 32..95

const uint16_t IS_ON_ADDR           = PASS_ADDR + PASS_MAX_LEN; // 96

const uint16_t DEVICE_MODE_ADDR     = IS_ON_ADDR + 1;           // 97

const uint16_t DEVICE_NAME_ADDR     = DEVICE_MODE_ADDR + 1;     // 98
const uint16_t DEVICE_NAME_MAX_LEN  = 32;                       // 98..129

const uint16_t AUTH_TOKEN_ADDR      = DEVICE_NAME_ADDR + DEVICE_NAME_MAX_LEN; // 130
const uint16_t AUTH_TOKEN_MAX_LEN   = 64;                       // 130..193


// -------------------- Глобальные объекты ядра --------------------
ESP8266WebServer server(80);

// WiFi-конфиг, прочитанный из EEPROM
String savedSSID = "";
String savedPASS = "";

// -------------------- Device mode & config --------------------
enum DeviceMode : uint8_t {
  DEVICE_MODE_SETUP    = 0,
  DEVICE_MODE_NORMAL   = 1,
  DEVICE_MODE_ERROR    = 2,
  DEVICE_MODE_UPDATING = 3
};

// Глобальная конфигурация устройства (живёт в RAM, но сохраняется в EEPROM)
DeviceMode deviceMode   = DEVICE_MODE_SETUP;  // по умолчанию setup
String     deviceName   = "Relay 1";          // дефолтное имя
String     serverBaseUrl = "";                // пока держим только в RAM
String     authToken    = "";                 // токен авторизации


// Состояние устройства (канала 1)
bool isOn = false;

// -------------------- Внешние функции, которые реализует устройство --------------------
// Реализация задаётся в конкретном проекте (реле, кнопка и т.п.)
void setOn(bool on);

// Эти функции реализованы в других .h (WebHandlers / REST_API),
// но вызываются из ядра:
void registerWebUiRoutes();
void registerRestApiRoutes();

// -------------------- helpers для EEPROM --------------------
inline String readStringFromEEPROM(uint16_t addr, uint16_t maxLen) {
  char buf[100];
  if (maxLen > sizeof(buf)) maxLen = sizeof(buf);

  for (uint16_t i = 0; i < maxLen; i++) {
    buf[i] = EEPROM.read(addr + i);
    if (buf[i] == '\0') {
      return String(buf);
    }
  }
  buf[maxLen - 1] = '\0';
  return String(buf);
}

inline void writeStringToEEPROM(uint16_t addr, const String &str, uint16_t maxLen) {
  uint16_t len = str.length();
  if (len >= maxLen) len = maxLen - 1;

  for (uint16_t i = 0; i < len; i++) {
    EEPROM.write(addr + i, str[i]);
  }
  EEPROM.write(addr + len, 0);

  for (uint16_t i = len + 1; i < maxLen; i++) {
    EEPROM.write(addr + i, 0);
  }
}

inline void saveWiFiConfig(const String &ssid, const String &pass) {
  EEPROM.begin(EEPROM_SIZE);
  writeStringToEEPROM(SSID_ADDR, ssid, SSID_MAX_LEN);
  writeStringToEEPROM(PASS_ADDR, pass, PASS_MAX_LEN);
  EEPROM.commit();
  EEPROM.end();
}

inline void loadWiFiConfig() {
  EEPROM.begin(EEPROM_SIZE);
  savedSSID = readStringFromEEPROM(SSID_ADDR, SSID_MAX_LEN);
  savedPASS = readStringFromEEPROM(PASS_ADDR, PASS_MAX_LEN);
  EEPROM.end();
}

// -------------------- Device config save/load --------------------
inline void loadDeviceConfig() {
  EEPROM.begin(EEPROM_SIZE);

  // читаем режим
  uint8_t rawMode = EEPROM.read(DEVICE_MODE_ADDR);

  // читаем имя и токен
  String name  = readStringFromEEPROM(DEVICE_NAME_ADDR, DEVICE_NAME_MAX_LEN);
  String token = readStringFromEEPROM(AUTH_TOKEN_ADDR, AUTH_TOKEN_MAX_LEN);

  EEPROM.end();

  // режим
  if (rawMode <= DEVICE_MODE_UPDATING) {
    deviceMode = static_cast<DeviceMode>(rawMode);
  } else {
    deviceMode = DEVICE_MODE_SETUP; // 0xFF / мусор -> считаем как первый запуск
  }

  // имя устройства
  if (name.length() > 0) {
    deviceName = name;
  } // иначе оставляем "Relay 1"

  // токен
  if (token.length() > 0) {
    authToken = token;
  }
}

inline void saveDeviceConfig() {
  EEPROM.begin(EEPROM_SIZE);

  EEPROM.write(DEVICE_MODE_ADDR, static_cast<uint8_t>(deviceMode));
  writeStringToEEPROM(DEVICE_NAME_ADDR, deviceName, DEVICE_NAME_MAX_LEN);
  writeStringToEEPROM(AUTH_TOKEN_ADDR, authToken, AUTH_TOKEN_MAX_LEN);

  EEPROM.commit();
  EEPROM.end();
}


inline void saveIsOn(bool on) {
  EEPROM.begin(EEPROM_SIZE);
  EEPROM.write(IS_ON_ADDR, on ? 1 : 0);
  EEPROM.commit();
  EEPROM.end();
}

inline void loadIsOn() {
  EEPROM.begin(EEPROM_SIZE);
  uint8_t v = EEPROM.read(IS_ON_ADDR);
  EEPROM.end();
  isOn = (v != 0);
}

// -------------------- WiFi helpers --------------------
inline bool tryConnectWiFi() {
  if (savedSSID.length() == 0) {
    Serial.println(F("No saved SSID, skipping STA connect"));
    return false;
  }

  Serial.print(F("Connecting to WiFi SSID: "));
  Serial.println(savedSSID);

  WiFi.mode(WIFI_STA);
  WiFi.begin(savedSSID.c_str(), savedPASS.c_str());

  unsigned long start = millis();
  const unsigned long timeout = 15000; // 15 сек

  while (WiFi.status() != WL_CONNECTED && millis() - start < timeout) {
    delay(500);
    Serial.print(".");
  }
  Serial.println();

  if (WiFi.status() == WL_CONNECTED) {
    Serial.print(F("WiFi connected, IP: "));
    Serial.println(WiFi.localIP());
    return true;
  } else {
    Serial.println(F("WiFi connect failed"));
    return false;
  }
}

inline void startAPMode() {
  WiFi.mode(WIFI_AP);

  String apSSID = "ESP-RELAY-";
  apSSID += String(ESP.getChipId(), HEX);

  const char* apPass = "noAccess";

  bool ok = WiFi.softAP(apSSID.c_str(), apPass);
  if (ok) {
    Serial.print(F("AP started, SSID: "));
    Serial.println(apSSID);
    Serial.print(F("AP IP: "));
    Serial.println(WiFi.softAPIP());
  } else {
    Serial.println(F("AP start FAILED"));
  }
}

// -------------------- Utils --------------------
inline String getUniqueID() {
  return String(ESP.getChipId(), HEX);
}

inline String getMAC() {
  uint8_t mac[6];
  WiFi.macAddress(mac);
  char buf[13];
  sprintf(buf, "%02X%02X%02X%02X%02X%02X",
          mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
  return String(buf);
}

// -------------------- Core setup / loop --------------------
inline void coreSetup() {
  Serial.begin(115200);
  delay(1200);

  Serial.println();
  Serial.println(F("Booting..."));
  Serial.print(F("Firmware: "));
  Serial.println(FW_VERSION);

  // читаем WiFi-конфиг и состояние реле из EEPROM
  loadWiFiConfig();
  loadIsOn();
  loadDeviceConfig();
  setOn(isOn); // применяем состояние к пину (реализует устройство)

  Serial.print(F("Saved SSID: "));
  Serial.println(savedSSID);

  // пытаемся подключиться, иначе AP
  if (!(savedSSID.length() > 0 && tryConnectWiFi())) {
    startAPMode();
  }

  // регистрируем роуты веб-панели и REST API
  registerWebUiRoutes();
  registerRestApiRoutes();

  server.begin();
  Serial.println(F("HTTP server started"));
}

inline void coreLoop() {
  server.handleClient();

  // авто-реконнект WiFi
  if (savedSSID.length() > 0 &&
      WiFi.getMode() == WIFI_STA &&
      WiFi.status() != WL_CONNECTED)
  {
    static uint32_t lastReconnectAttempt = 0;
    if (millis() - lastReconnectAttempt > 10000UL) {
      lastReconnectAttempt = millis();
      tryConnectWiFi();
    }
  }
}

#endif // GLOBAL_H