#ifndef REST_API_H
#define REST_API_H
#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include "WebPages.h"
#include "WebHandlers.h"
// ---------------------- внешние сущности из .ino / других .h ----------------------
extern ESP8266WebServer server;
extern String savedSSID;
extern String savedPASS;
void saveWiFiConfig(const String &ssid, const String &pass);
String getUniqueID();
String getMAC();
// ---------------------- Колбэки устройства ----------------------
// Устройство МОЖЕТ реализовать эти функции (если не нужно — можно оставить пустыми)
// Добавить свои поля в /status:
// json уже содержит открывающую "{" и какие-то базовые поля.
// Пример: json += ",\"relay_state\":\"on\"";
void appendStatusJsonFields(String &json);
// Добавить свои поля в /about:
void appendAboutJsonFields(String &json);
// Обработка действий устройства:
//
// action — строка, например "set_state"
// paramsJson — сырой JSON params или вообще весь body (как тебе удобнее)
// Если вернуло true → считаем, что действие выполнено успешно
// Если false → errorCode/errorMessage должны быть заполнены
bool deviceHandleAction(const String &action,
const String ¶msJson,
String &errorCode,
String &errorMessage);
// Вызывается при /reset — устройство может выполнить свой специфичный сброс
void deviceHandleReset();
// ---------------------- Вспомогательные функции ----------------------
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);
}
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);
}
// Попробовать вытащить под-JSON "params": { ... } как сырой текст
inline String extractJsonObject(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 braceIndex = body.indexOf('{', colonIndex);
if (braceIndex < 0) return "";
int depth = 0;
for (int i = braceIndex; i < (int)body.length(); i++) {
char c = body[i];
if (c == '{') depth++;
else if (c == '}') {
depth--;
if (depth == 0) {
return body.substring(braceIndex, i + 1);
}
}
}
return "";
}
inline int extractJsonIntValue(const String &body, const String &key) {
String pattern = "\"" + key + "\"";
int keyIndex = body.indexOf(pattern);
if (keyIndex < 0) return -1;
int colonIndex = body.indexOf(':', keyIndex);
if (colonIndex < 0) return -1;
int i = colonIndex + 1;
// пропускаем пробелы
while (i < (int)body.length() &&
(body[i] == ' ' || body[i] == '\t' || body[i] == '\n' || body[i] == '\r')) {
i++;
}
// на случай, если числа вдруг передадутся в кавычках
bool quoted = (i < (int)body.length() && body[i] == '"');
if (quoted) i++;
int sign = 1;
if (i < (int)body.length() && body[i] == '-') {
sign = -1;
i++;
}
long value = 0;
bool anyDigit = false;
while (i < (int)body.length() && body[i] >= '0' && body[i] <= '9') {
anyDigit = true;
value = value * 10 + (body[i] - '0');
i++;
}
if (!anyDigit) return -1;
return (int)(value * sign);
}
// ---------------------- Авторизация ----------------------
inline bool hasValidToken() {
if (authToken.length() == 0) return false;
// 1) Проверяем токен
String header = server.header(F("Authorization"));
if (!header.startsWith(F("Bearer "))) return false;
String token = header.substring(7);
token.trim();
if (token != authToken) return false;
// 2) Проверяем IP, если он был сохранён
if (serverBaseUrl.length() > 0) {
IPAddress remote = server.client().remoteIP();
String ipStr = remote.toString();
if (ipStr != serverBaseUrl) {
// Можно залогировать для отладки
Serial.print(F("Auth IP mismatch: got "));
Serial.print(ipStr);
Serial.print(F(", expected "));
Serial.println(serverBaseUrl);
return false;
}
}
return true;
}
inline bool requireAuth() {
if (!hasValidToken()) {
String json = F("{\"status\":\"error\",\"error\":\"Unauthorized\",\"message\":\"Missing or invalid token\"}");
sendJson(401, json);
return false;
}
return true;
}
inline void sendNotAvailable() {
String json = F("{\"status\":\"error\",\"error\":\"NotAvailable\",\"message\":\"Setup mode is not active\"}");
sendJson(403, json);
}
// ---------------------- /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);
// Хук для добавления специфичных полей (например, количество каналов, тип реле и т.п.)
appendAboutJsonFields(json);
json += "}";
sendJson(200, json);
}
// ---------------------- /status ----------------------
inline void handleStatus() {
if (deviceMode == DEVICE_MODE_NORMAL) {
if (!requireAuth()) return;
}
String json = "{";
// Базовый статус устройства
json += "\"status\":\"ok\"";
// Хук: устройство добавляет сюда свои поля (каналы, яркость, что угодно)
appendStatusJsonFields(json);
json += "}";
sendJson(200, json);
}
inline void handleSetDeviceName() {
// Только в нормальном режиме и только с авторизацией
if (deviceMode != DEVICE_MODE_NORMAL) {
String json = F("{\"status\":\"error\",\"error\":\"IllegalActionOrParams\",\"message\":\"Not in normal mode\"}");
sendJson(400, json);
return;
}
if (!requireAuth()) return;
String body = server.arg("plain");
body.trim();
String newName = extractJsonStringValue(body, F("device_name"));
newName.trim();
if (newName.length() == 0) {
String json = F("{\"status\":\"error\",\"error\":\"IllegalActionOrParams\",\"message\":\"device_name is required\"}");
sendJson(400, json);
return;
}
// Небольшая санитация + ограничение длины
String sanitized;
for (uint16_t i = 0; i < newName.length() && sanitized.length() < DEVICE_NAME_MAX_LEN - 1; i++) {
char c = newName[i];
// отбрасываем управляющие символы
if (c >= 32 && c != '\"') {
sanitized += c;
}
}
if (sanitized.length() == 0) {
String json = F("{\"status\":\"error\",\"error\":\"IllegalActionOrParams\",\"message\":\"Invalid device_name\"}");
sendJson(400, json);
return;
}
deviceName = sanitized;
saveDeviceConfig();
String json = F("{\"status\":\"ok\",\"message\":\"Device name updated\"}");
sendJson(200, json);
}
// ---------------------- /action ----------------------
// Обработка действий полностью делегируется устройству
inline void handleAction() {
if (deviceMode != DEVICE_MODE_NORMAL) {
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.length() == 0) {
String json = F("{\"status\":\"error\",\"error\":\"IllegalActionOrParams\",\"message\":\"Action is required\"}");
sendJson(400, json);
return;
}
// Пытаемся вытащить params как объект, если он есть
String paramsJson = extractJsonObject(body, F("params"));
if (paramsJson.length() == 0) {
// Можно по договорённости считать, что устройство само достанет что нужно из body
paramsJson = body;
}
String errorCode;
String errorMessage;
bool ok = deviceHandleAction(action, paramsJson, errorCode, errorMessage);
if (!ok) {
if (errorCode.length() == 0) errorCode = "IllegalActionOrParams";
if (errorMessage.length() == 0) errorMessage = "Device action failed";
String json = "{\"status\":\"error\",\"error\":\"" + errorCode +
"\",\"message\":\"" + errorMessage + "\"}";
sendJson(400, json);
return;
}
String json = F("{\"status\":\"ok\",\"message\":\"Action executed\"}");
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) {
// IP сервера, с которого пришёл запрос
IPAddress remote = server.client().remoteIP();
serverBaseUrl = remote.toString(); // например "192.168.2.10"
// В режиме setup всегда разрешаем установить токен,
// даже если в EEPROM что-то лежит.
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;
}
deviceHandleReset();
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);
// Сбрасываем токен и режим
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("/set_device_name"), HTTP_POST, handleSetDeviceName);
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