diff --git a/devices/relay/relay_esp8266/Config.h b/devices/relay/relay_esp8266/Config.h new file mode 100755 index 0000000..67f8218 --- /dev/null +++ b/devices/relay/relay_esp8266/Config.h @@ -0,0 +1,16 @@ +#ifndef CONFIG_H +#define CONFIG_H + +#include + +// -------------------- FIRMWARE -------------------- +#define FW_VERSION "1.1.0dev" +#define DEVICE_TYPE "relay" +#define CHANNEL_NUM 1 + +// -------------------- LED / MATRIX -------------------- +constexpr uint8_t LED_PIN = 2; +constexpr uint8_t RELAY_PIN = 0; + + +#endif // CONFIG_H diff --git a/devices/relay/relay_esp8266/Global.h b/devices/relay/relay_esp8266/Global.h new file mode 100755 index 0000000..b0d3061 --- /dev/null +++ b/devices/relay/relay_esp8266/Global.h @@ -0,0 +1,286 @@ +#ifndef GLOBAL_H +#define GLOBAL_H + +#include +#include +#include +#include + +#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(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(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 diff --git a/devices/relay/relay_esp8266/REST_API.h b/devices/relay/relay_esp8266/REST_API.h new file mode 100755 index 0000000..718cd8e --- /dev/null +++ b/devices/relay/relay_esp8266/REST_API.h @@ -0,0 +1,344 @@ +#ifndef REST_API_H +#define REST_API_H + +#include +#include +#include + +#include "WebPages.h" + +// ---------------------- внешние сущности из .ino / других .h ---------------------- +extern ESP8266WebServer server; + +extern String savedSSID; +extern String savedPASS; +extern bool isOn; + +void saveWiFiConfig(const String &ssid, const String &pass); +void saveIsOn(bool on); +void setOn(bool on); + +String getUniqueID(); +String getMAC(); + + +// ---------------------- утилиты ---------------------- +inline String deviceModeToString(DeviceMode m) { + switch (m) { + case DEVICE_MODE_SETUP: return F("setup"); + case DEVICE_MODE_NORMAL: return F("normal"); + case DEVICE_MODE_ERROR: return F("error"); + case DEVICE_MODE_UPDATING: return F("updating"); + } + return F("error"); +} + +inline void sendJson(int code, const String &body) { + server.send(code, F("application/json; charset=utf-8"), body); +} + +// Простенький парсер "ключ":"значение" из JSON-строки (без вложенных кавычек и т.п.) +inline String extractJsonStringValue(const String &body, const String &key) { + String pattern = "\"" + key + "\""; + int keyIndex = body.indexOf(pattern); + if (keyIndex < 0) return ""; + + int colonIndex = body.indexOf(':', keyIndex); + if (colonIndex < 0) return ""; + + int firstQuote = body.indexOf('"', colonIndex + 1); + if (firstQuote < 0) return ""; + + int secondQuote = body.indexOf('"', firstQuote + 1); + if (secondQuote < 0) return ""; + + return body.substring(firstQuote + 1, secondQuote); +} + +// Проверка токена из заголовка Authorization: Bearer +inline bool hasValidToken() { + if (authToken.length() == 0) return false; + + String header = server.header(F("Authorization")); + if (!header.startsWith(F("Bearer "))) return false; + + String token = header.substring(7); // после "Bearer " + token.trim(); + return token == authToken; +} + +inline bool requireAuth() { + if (!hasValidToken()) { + String json = F("{\"status\":\"error\",\"error\":\"Unauthorized\",\"message\":\"Missing or invalid token\"}"); + sendJson(401, json); + return false; + } + return true; +} + +// Быстрый ответ "NotAvailable" для неподходящего режима +inline void sendNotAvailable() { + String json = F("{\"status\":\"error\",\"error\":\"NotAvailable\",\"message\":\"Setup mode is not active\"}"); + sendJson(403, json); +} + +// ---------------------- 1. GET /about ---------------------- +inline void handleAbout() { + IPAddress ip = WiFi.localIP(); + + String json = "{"; + + json += "\"device_name\":\"" + deviceName + "\","; + json += "\"device_type\":\"" + String(DEVICE_TYPE) + "\","; + json += "\"firmware_version\":\"" + String(FW_VERSION) + "\","; + json += "\"device_id\":\"" + getUniqueID() + "\","; + json += "\"server\":\"" + serverBaseUrl + "\","; + json += "\"status\":\"" + deviceModeToString(deviceMode) + "\","; + json += "\"ip_address\":\"" + ip.toString() + "\","; + json += "\"mac_address\":\"" + getMAC() + "\","; + json += "\"uptime\":" + String(millis() / 1000); + + json += "}"; + + sendJson(200, json); +} + +// ---------------------- 2. GET /status ---------------------- +// Для реле: {"state": "on" | "off" } +inline void handleStatus() { + // В режиме setup можем отдавать статус и без токена (по спецификации разрешено ограниченное поведение) + if (deviceMode == DEVICE_MODE_NORMAL) { + if (!requireAuth()) return; + } + + String json = "{"; + json += "\"state\":\""; + json += (isOn ? "on" : "off"); + json += "\"}"; + sendJson(200, json); +} + +// ---------------------- 3. POST /action ---------------------- +// { +// "action": "set_state", +// "params": { "state": "on" | "off" } +// } +inline void handleAction() { + if (deviceMode != DEVICE_MODE_NORMAL) { + // В режиме setup /action недоступен + String json = F("{\"status\":\"error\",\"error\":\"NotAvailable\",\"message\":\"Action not available in this mode\"}"); + sendJson(403, json); + return; + } + + if (!requireAuth()) return; + + String body = server.arg("plain"); + body.trim(); + + String action = extractJsonStringValue(body, F("action")); + + if (action != F("set_state")) { + String json = F("{\"status\":\"error\",\"error\":\"IllegalActionOrParams\",\"message\":\"Device does not support this action or params\"}"); + sendJson(400, json); + return; + } + + String state = extractJsonStringValue(body, F("state")); + state.toLowerCase(); + + bool newState; + if (state == F("on")) { + newState = true; + } else if (state == F("off")) { + newState = false; + } else { + String json = F("{\"status\":\"error\",\"error\":\"IllegalActionOrParams\",\"message\":\"Unknown state value\"}"); + sendJson(400, json); + return; + } + + setOn(newState); + saveIsOn(newState); + + String json = F("{\"status\":\"ok\",\"message\":\"State changed\"}"); + sendJson(200, json); +} + +// ---------------------- 4. POST /set_token ---------------------- +// setup: без токена, первый раз устанавливает токен и переводит в normal +// normal: требует токен, меняет его +inline void handleSetToken() { + String body = server.arg("plain"); + body.trim(); + + String newToken = extractJsonStringValue(body, F("token")); + if (newToken.length() == 0) { + String json = F("{\"status\":\"error\",\"error\":\"IllegalActionOrParams\",\"message\":\"Token is required\"}"); + sendJson(400, json); + return; + } + + if (deviceMode == DEVICE_MODE_SETUP) { + if (authToken.length() > 0) { + // Устройство уже было провиженено + String json = F("{\"status\":\"error\",\"error\":\"AlreadyProvisioned\",\"message\":\"Device already provisioned\"}"); + sendJson(409, json); + return; + } + + authToken = newToken; + deviceMode = DEVICE_MODE_NORMAL; + saveDeviceConfig(); + + String json = F("{\"status\":\"ok\",\"message\":\"Token set. Device mode: normal\"}"); + sendJson(200, json); + return; + } + + // DEVICE_MODE_NORMAL — смена токена только с действующим токеном + if (deviceMode == DEVICE_MODE_NORMAL) { + if (!requireAuth()) return; + + authToken = newToken; + saveDeviceConfig(); + + String json = F("{\"status\":\"ok\",\"message\":\"Token updated\"}"); + sendJson(200, json); + return; + } + + // Прочие режимы + String json = F("{\"status\":\"error\",\"error\":\"NotAvailable\",\"message\":\"Cannot set token in current mode\"}"); + sendJson(403, json); +} + +// ---------------------- 5. GET /setup ---------------------- +// В режиме setup отдаём HTML-страницу настройки Wi-Fi +inline void handleSetupGet() { + if (deviceMode != DEVICE_MODE_SETUP) { + sendNotAvailable(); + return; + } + + // Используем уже готовую HTML-страницу + server.send(200, F("text/html; charset=utf-8"), wifiSetupPage); +} + +// ---------------------- 5. POST /setup ---------------------- +// JSON: {"ssid":"...","password":"...","server":"http://..."} +// или form-data, как в текущей веб-форме +inline void handleSetupPost() { + if (deviceMode != DEVICE_MODE_SETUP) { + sendNotAvailable(); + return; + } + + String ssid; + String pass; + String serverUrl; + + // 1) Попытка прочитать как форму (из существующей веб-страницы) + if (server.hasArg(F("ssid"))) { + ssid = server.arg(F("ssid")); + pass = server.arg(F("pass")); + if (server.hasArg(F("server"))) { + serverUrl = server.arg(F("server")); + } + } else { + // 2) Попытка прочитать JSON + String body = server.arg("plain"); + body.trim(); + ssid = extractJsonStringValue(body, F("ssid")); + pass = extractJsonStringValue(body, F("password")); + serverUrl = extractJsonStringValue(body, F("server")); + } + + if (ssid.length() == 0) { + String json = F("{\"status\":\"error\",\"error\":\"IllegalActionOrParams\",\"message\":\"SSID is required\"}"); + sendJson(400, json); + return; + } + + savedSSID = ssid; + savedPASS = pass; + saveWiFiConfig(savedSSID, savedPASS); + + if (serverUrl.length() > 0) { + serverBaseUrl = serverUrl; + } + + String json = F("{\"status\":\"ok\",\"message\":\"Wi-Fi configured. Connecting...\"}"); + sendJson(200, json); + + // Дальше просто перезапустимся — при старте прошивка сама попробует подключиться к Wi-Fi + delay(800); + ESP.restart(); +} + +// ---------------------- 6. POST /reboot ---------------------- +inline void handleRebootApi() { + if (deviceMode != DEVICE_MODE_NORMAL) { + String json = F("{\"status\":\"error\",\"error\":\"NotAvailable\",\"message\":\"Reboot available only in normal mode\"}"); + sendJson(403, json); + return; + } + + if (!requireAuth()) return; + + String json = F("{\"status\":\"ok\",\"message\":\"Device will reboot now\"}"); + sendJson(200, json); + + Serial.println(F("Reboot requested via REST API")); + delay(500); + ESP.restart(); +} + +// ---------------------- 7. POST /reset ---------------------- +// Сброс всех настроек к заводским, переход в setup +inline void handleResetApi() { + if (deviceMode != DEVICE_MODE_NORMAL) { + String json = F("{\"status\":\"error\",\"error\":\"NotAvailable\",\"message\":\"Reset available only in normal mode\"}"); + sendJson(403, json); + return; + } + + if (!requireAuth()) return; + + // Сбрасываем Wi-Fi + savedSSID = ""; + savedPASS = ""; + saveWiFiConfig(savedSSID, savedPASS); + + // Выключаем реле + setOn(false); + saveIsOn(false); + + // Сбрасываем токен и режим + authToken = ""; + serverBaseUrl = ""; + deviceMode = DEVICE_MODE_SETUP; + saveDeviceConfig(); + + String json = F("{\"status\":\"ok\",\"message\":\"Device reset to factory settings. Entering setup mode.\"}"); + sendJson(200, json); + + delay(800); + ESP.restart(); +} + +// ---------------------- Регистрация роутов ---------------------- +inline void registerRestApiRoutes() { + server.on(F("/about"), HTTP_GET, handleAbout); + server.on(F("/status"), HTTP_GET, handleStatus); + server.on(F("/action"), HTTP_POST, handleAction); + server.on(F("/set_token"), HTTP_POST, handleSetToken); + + server.on(F("/setup"), HTTP_GET, handleSetupGet); + server.on(F("/setup"), HTTP_POST, handleSetupPost); + + server.on(F("/reboot"), HTTP_POST, handleRebootApi); + server.on(F("/reset"), HTTP_POST, handleResetApi); +} + +#endif // REST_API_H diff --git a/devices/relay/relay_esp8266/WebHandlers.h b/devices/relay/relay_esp8266/WebHandlers.h new file mode 100755 index 0000000..eebeee0 --- /dev/null +++ b/devices/relay/relay_esp8266/WebHandlers.h @@ -0,0 +1,123 @@ +#ifndef WEB_HANDLERS_H +#define WEB_HANDLERS_H + +#include "Config.h" + +#include +#include +#include +#include + +#include "Global.h" +#include "WebPages.h" + +// -------------------- ROOT -------------------- +inline void handleRoot() { + if (WiFi.getMode() == WIFI_AP) + return server.send(200, "text/html", wifiSetupPage); + + String msg = "ESP8266 (-01S) WiFi Relay\nID: " + getUniqueID() + "\nVersion: "; + msg += FW_VERSION; + + msg += "\nGo to /panel"; + server.send(200, "text/plain", msg); +} + +// -------------------- SAVE WIFI -------------------- +inline void handleSaveWiFi() { + String ssid = server.arg("ssid"); + String pass = server.arg("pass"); + + savedSSID = ssid; + savedPASS = pass; + saveWiFiConfig(savedSSID, savedPASS); + + server.send(200, "text/plain", "Saved! Rebooting..."); + delay(800); + ESP.restart(); +} + +// -------------------- OTA PAGES -------------------- +inline void handleUpdateGet() { + server.send(200, "text/html", updatePage); +} + +inline void handleUpdateUpload() { + HTTPUpload& upload = server.upload(); + + if (upload.status == UPLOAD_FILE_START) { + Serial.printf("OTA start: %s\n", upload.filename.c_str()); + + size_t sketchSpace = (ESP.getFreeSketchSpace() - 0x2000) & 0xFFFFF000; + + if (!Update.begin(sketchSpace)) { // более безопасный вариант + Update.printError(Serial); + } + } + else if (upload.status == UPLOAD_FILE_WRITE) { + if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) { + Update.printError(Serial); + } + } + else if (upload.status == UPLOAD_FILE_END) { + if (!Update.end(true)) { // true = сразу перезаписать раздел + Update.printError(Serial); + } + } +} + +inline void handleUpdatePost() { + if (Update.hasError()) server.send(200, "text/plain", "Update Failed!"); + else server.send(200, "text/plain", "Update OK! Rebooting..."); + + delay(800); + ESP.restart(); +} + +// -------------------- PANEL (GET) -------------------- +inline void handlePanelGet() { + if (WiFi.getMode() == WIFI_AP) + return server.send(200, "text/html", wifiSetupPage); + + String html = panelPageHeader; + + html += ""; + html += panelPageFooter; + + server.send(200, "text/html", html); +} + +// -------------------- PANEL (POST) -------------------- +inline void handlePanelPost() { + bool turnOn = isOn; // значение по умолчанию — текущее состояние + + if (server.hasArg("powerState")) { + String val = server.arg("powerState"); + turnOn = (val == "1" || val == "on" || val == "true" || val == "yes"); + + setOn(turnOn); + Serial.println(turnOn ? "ON via Web" : "OFF via Web"); + } + + // TODO: блок с device_name потом поправишь/уберёшь + + // сохраняем состояние в EEPROM + saveIsOn(turnOn); + + server.sendHeader("Location", "/panel"); + server.send(303); +} + +// -------------------- Регистрация роутов веб-панели -------------------- +inline void registerWebUiRoutes() { + server.on("/", HTTP_GET, handleRoot); + server.on("/save_wifi", HTTP_POST, handleSaveWiFi); + server.on("/update", HTTP_GET, handleUpdateGet); + server.on("/update", HTTP_POST, handleUpdatePost, handleUpdateUpload); + server.on("/panel", HTTP_GET, handlePanelGet); + server.on("/panel", HTTP_POST, handlePanelPost); +} + +#endif // WEB_HANDLERS_H diff --git a/devices/relay/relay_esp8266/WebPages.h b/devices/relay/relay_esp8266/WebPages.h new file mode 100755 index 0000000..409f3f3 --- /dev/null +++ b/devices/relay/relay_esp8266/WebPages.h @@ -0,0 +1,200 @@ +#ifndef WEB_PAGES_H +#define WEB_PAGES_H + +// -------------------- WiFi setup page -------------------- +const char* wifiSetupPage = R"HTML( + + + + + WiFi setup + + + + +
+
+
+
+
+

WiFi setup

+

+ Connect the device to your home WiFi network. +

+
+
+ + +
+
+ + +
+
+ +
+
+
+
+

+ ESP-01 Relay +

+
+
+
+ + +)HTML"; + +// -------------------- Firmware update page -------------------- +const char* updatePage = R"HTML( + + + + + Firmware Update + + + + +
+
+
+
+
+

Firmware Update

+

+ Select a compiled .bin file and upload it to update the firmware. +

+
+
+ +
+
+ +
+
+
+
+

+ Back to control panel +

+
+
+
+ + +)HTML"; + +// -------------------- Panel page (header + footer) -------------------- +const char* panelPageHeader = R"HTML( + + + + + Bedside Light Panel + + + + +
+
+
+
+
+

Bedside Matrix Night Light

+

+ Tap the touch button on the device to turn the light on or off. +

+
+
+)HTML"; + +const char* panelPageFooter = R"HTML( +
+
+ + +
+
+ + + Firmware update + + +
+
+
+
+

+ ESP-01 Relay +

+
+
+
+ + + + +)HTML"; + +#endif diff --git a/devices/relay/relay_esp8266/relay_esp8266.ino b/devices/relay/relay_esp8266/relay_esp8266.ino new file mode 100755 index 0000000..f6fc7ea --- /dev/null +++ b/devices/relay/relay_esp8266/relay_esp8266.ino @@ -0,0 +1,67 @@ +#include "Config.h" +#include + +#include "Global.h" +#include "REST_API.h" + +// -------------------- ЛОГИКА УСТРОЙСТВА -------------------- + +// Простейшее 1-канальное реле +void applyChannelState(uint8_t ch, bool on) { + if (ch != 0) return; + digitalWrite(RELAY_PIN, on ? HIGH : LOW); +} + +// Добавляем поля в /status +void appendStatusJsonFields(String &json) { + // Пример: {"status":"ok","relay_state":"on"} + json += ",\"relay_state\":\""; + json += getPrimaryState() ? "on" : "off"; + json += "\""; +} + +// Добавляем поля в /about (если нужно) +void appendAboutJsonFields(String &json) { + // Например, количество каналов + json += ",\"channels\":" + String(CHANNEL_NUM); +} + +// Обработка действий +bool deviceHandleAction(const String &action, + const String ¶msJson, + String &errorCode, + String &errorMessage) +{ + if (action == "set_state") { + String state = extractJsonStringValue(paramsJson, "state"); + state.toLowerCase(); + + if (state != "on" && state != "off") { + errorCode = "IllegalActionOrParams"; + errorMessage = "Unknown state value"; + return false; + } + + bool on = (state == "on"); + // ВАЖНО: здесь мы сами решаем, сохранять ли это в EEPROM, + // вызывать ли setChannelState/ setOn и т.п. + setOn(on); // внутри setOn → setChannelState(0, on, true); + + return true; + } + + // Неизвестное действие + errorCode = "IllegalActionOrParams"; + errorMessage = "Unknown action"; + return false; +} + +// -------------------- SETUP / LOOP -------------------- +void setup() { + pinMode(RELAY_PIN, OUTPUT); + coreSetup(); +} + +void loop() { + coreLoop(); +} diff --git a/docs/MVP.md b/docs/MVP.md new file mode 100644 index 0000000..dfa79dc --- /dev/null +++ b/docs/MVP.md @@ -0,0 +1,22 @@ +### MVP +- Device Spec +- Server API + - Auth + - Devices + - General + - Users + - Scripts + +--- + +### Stage 1 +- Simple UI web panel + +### Stage 2 +- Server APISS + - Groups + - Logs + +## Stage 3 +- Server API + - Notifications \ No newline at end of file diff --git a/orders.json b/orders.json deleted file mode 100644 index cde2899..0000000 --- a/orders.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "timestamp": "2025-12-11 15:30:51", - "orders": [ - { - "product_id": "Внутренний id на портале", - "sku": "Артикул", - "code": "Код товара", - "price": "По какой цене было продано", - "quantity": "Количество", - "create_at": "Дата заказа", - "brand": "На случай, если заказы обрабатываются с разделением по брендам", - "counterparty": { - "portal_id": "Внутренний id юзера на портале", - "code": "Код", - "b2b_account_id": "", - "organization_binding": "Прив’язка до організації (ФОП / юрособа)" - }, - "delivery": { - // На сколько я помню, то доставка или самовывоз или новая почта и больше альтернатив нет." - "region": "", - "city": "", - "address": "номер отделения или адресс доставки", - "full_name": "Имя получателя", - "phone_number": "Телефон получателя", - "TTN": "Номер накладной, если создаём" - } - }, - // ... - ] -} \ No newline at end of file