diff --git a/devices/button/button_esp8266/ButtonLogic.cpp b/devices/button/button_esp8266/ButtonLogic.cpp new file mode 100644 index 0000000..e13cae4 --- /dev/null +++ b/devices/button/button_esp8266/ButtonLogic.cpp @@ -0,0 +1,250 @@ +#include "ButtonLogic.h" + +#include +#include + +// Сенсорные модули обычно дают HIGH при касании. +// Если у тебя наоборот — поставь false. +static const bool TOUCH_ACTIVE_HIGH = true; + +// Антиспам: не слать press чаще, чем раз в 100мс на канал +static const uint32_t PRESS_ANTISPAM_MS = 100; + +// Debounce +static const uint32_t DEBOUNCE_MS = 30; + +// Тайминги индикаций +static const uint32_t WARNING_BLINK_MS = 500; +static const uint32_t ERROR_BLINK_MS = 1000; +static const uint32_t GLOBAL_BLINK_MS = 1000; + +// Таймаут ожидания ответа сервера (по доке 10 секунд) +static const uint32_t WAIT_TIMEOUT_MS = 10000; + +// endpoint события (на сервере) +static const char* EVENT_PATH = "/events/new"; + +// лента +static Adafruit_NeoPixel pixels(BUTTON_CHANNEL_NUM, SIGNAL_LED, NEO_GRB + NEO_KHZ800); + +// debounce/state +static uint8_t last_raw[BUTTON_CHANNEL_NUM]; +static uint8_t stable_state[BUTTON_CHANNEL_NUM]; +static uint32_t last_change_at[BUTTON_CHANNEL_NUM]; + +// антиспам +static uint32_t last_press_sent_at[BUTTON_CHANNEL_NUM]; + +// временные статусы +static bool temp_active[BUTTON_CHANNEL_NUM]; +static IndicatorState temp_state[BUTTON_CHANNEL_NUM]; +static uint32_t temp_until_ms[BUTTON_CHANNEL_NUM]; + +// базовые индикаторы (что задаёт сервер) +IndicatorState channel_indicator[BUTTON_CHANNEL_NUM] = { IND_DISABLED, IND_DISABLED, IND_DISABLED }; + +static uint32_t now_ms() { return millis(); } + +static uint32_t color_rgb(uint8_t r, uint8_t g, uint8_t b) { + return pixels.Color(r, g, b); +} + +static bool is_global_setup() { + return (deviceMode == DEVICE_MODE_SETUP); +} + +static bool is_global_nowifi() { + if (deviceMode == DEVICE_MODE_SETUP) return false; + return (WiFi.status() != WL_CONNECTED); +} + +void set_channel_indicator(uint8_t ch, IndicatorState st) { + if (ch >= BUTTON_CHANNEL_NUM) return; + channel_indicator[ch] = st; +} + +IndicatorState get_channel_indicator(uint8_t ch) { + if (ch >= BUTTON_CHANNEL_NUM) return IND_DISABLED; + return channel_indicator[ch]; +} + +void set_channel_waiting(uint8_t ch) { + if (ch >= BUTTON_CHANNEL_NUM) return; + temp_active[ch] = true; + temp_state[ch] = IND_WAITING; + temp_until_ms[ch] = now_ms() + WAIT_TIMEOUT_MS; +} + +void set_channel_warning_temp(uint8_t ch, uint32_t ms) { + if (ch >= BUTTON_CHANNEL_NUM) return; + temp_active[ch] = true; + temp_state[ch] = IND_WARNING; + temp_until_ms[ch] = now_ms() + ms; +} + +void clear_channel_temp(uint8_t ch) { + if (ch >= BUTTON_CHANNEL_NUM) return; + temp_active[ch] = false; +} + +static IndicatorState get_effective_indicator(uint8_t ch) { + if (ch >= BUTTON_CHANNEL_NUM) return IND_DISABLED; + + if (temp_active[ch]) { + if ((int32_t)(temp_until_ms[ch] - now_ms()) <= 0) { + temp_active[ch] = false; + } else { + return temp_state[ch]; + } + } + + return channel_indicator[ch]; +} + +static void render_pixels() { + uint32_t t = now_ms(); + + // глобальные режимы перекрывают канальные + if (is_global_setup()) { + bool on = ((t / GLOBAL_BLINK_MS) % 2) == 0; + uint32_t c = on ? color_rgb(255, 255, 255) : color_rgb(0, 0, 0); + for (uint8_t i = 0; i < BUTTON_CHANNEL_NUM; i++) pixels.setPixelColor(i, c); + pixels.show(); + return; + } + + if (is_global_nowifi()) { + bool on = ((t / GLOBAL_BLINK_MS) % 2) == 0; + uint32_t c = on ? color_rgb(0, 0, 255) : color_rgb(0, 0, 0); + for (uint8_t i = 0; i < BUTTON_CHANNEL_NUM; i++) pixels.setPixelColor(i, c); + pixels.show(); + return; + } + + // обычный режим: по каналам + for (uint8_t ch = 0; ch < BUTTON_CHANNEL_NUM; ch++) { + IndicatorState st = get_effective_indicator(ch); + + uint32_t c = 0; + switch (st) { + case IND_ENABLED: + c = color_rgb(0, 255, 0); + break; + case IND_DISABLED: + c = color_rgb(255, 255, 255); + break; + case IND_MUTE: + c = color_rgb(0, 0, 0); + break; + case IND_WAITING: + c = color_rgb(255, 255, 0); + break; + case IND_WARNING: { + bool on = ((t / WARNING_BLINK_MS) % 2) == 0; + c = on ? color_rgb(255, 120, 0) : color_rgb(0, 0, 0); + break; + } + case IND_ERROR: { + bool on = ((t / ERROR_BLINK_MS) % 2) == 0; + c = on ? color_rgb(255, 0, 0) : color_rgb(0, 0, 0); + break; + } + } + + pixels.setPixelColor(ch, c); + } + + pixels.show(); +} + +static bool antispam_allow_press(uint8_t ch) { + if (ch >= BUTTON_CHANNEL_NUM) return false; + + uint32_t t = now_ms(); + if ((uint32_t)(t - last_press_sent_at[ch]) < PRESS_ANTISPAM_MS) return false; + + last_press_sent_at[ch] = t; + return true; +} + +void send_press_event(uint8_t ch) { + if (ch >= BUTTON_CHANNEL_NUM) return; + + // если канал в mute — игнорируем + if (get_channel_indicator(ch) == IND_MUTE) return; + + // антиспам + if (!antispam_allow_press(ch)) return; + + set_channel_waiting(ch); + + int http_code = -1; + + String body = "{"; + body += "\"event_name\":\"press\","; + body += "\"data\": {"; + body += "\"channel\":" + String(ch) + "},"; + body += "\"device_id\":\"" + getUniqueID() + "\""; + body += "}"; + + bool ok = core_post_json_to_server(EVENT_PATH, body, WAIT_TIMEOUT_MS, http_code); + + if (!ok) { + set_channel_warning_temp(ch, 2000); + } else { + clear_channel_temp(ch); + } +} + +void button_logic_setup() { + // сенсорные модули: INPUT + for (uint8_t ch = 0; ch < BUTTON_CHANNEL_NUM; ch++) { + pinMode(BUTTON_PINS[ch], INPUT); + + uint8_t r = digitalRead(BUTTON_PINS[ch]) ? 1 : 0; + last_raw[ch] = r; + stable_state[ch] = r; + last_change_at[ch] = now_ms(); + + temp_active[ch] = false; + temp_state[ch] = IND_DISABLED; + temp_until_ms[ch] = 0; + + last_press_sent_at[ch] = 0; + } + + pixels.begin(); + pixels.setBrightness(64); + pixels.clear(); + pixels.show(); +} + +void button_logic_loop() { + uint32_t t = now_ms(); + + for (uint8_t ch = 0; ch < BUTTON_CHANNEL_NUM; ch++) { + uint8_t r = digitalRead(BUTTON_PINS[ch]) ? 1 : 0; + + if (r != last_raw[ch]) { + last_raw[ch] = r; + last_change_at[ch] = t; + continue; + } + + if ((t - last_change_at[ch]) < DEBOUNCE_MS) continue; + + if (stable_state[ch] != r) { + uint8_t prev = stable_state[ch]; + stable_state[ch] = r; + + if (TOUCH_ACTIVE_HIGH) { + if (prev == 0 && r == 1) send_press_event(ch); + } else { + if (prev == 1 && r == 0) send_press_event(ch); + } + } + } + + render_pixels(); + delay(0); +} diff --git a/devices/button/button_esp8266/ButtonLogic.h b/devices/button/button_esp8266/ButtonLogic.h index ce5efa4..b0c3b2a 100644 --- a/devices/button/button_esp8266/ButtonLogic.h +++ b/devices/button/button_esp8266/ButtonLogic.h @@ -1,23 +1,36 @@ +#pragma once #include +#include -// Массив пинов — определяем в .ino, а здесь только объявляем -extern const uint8_t BUTTON_PINS[CHANNEL_NUM]; +// compile-time число каналов для device-layer (.cpp файлы) +#ifndef BUTTON_CHANNEL_NUM + #define BUTTON_CHANNEL_NUM 3 +#endif -// Состояния каналов — тоже логика реле, а не ядра -extern bool channelState[CHANNEL_NUM]; +enum IndicatorState : uint8_t { + IND_ENABLED = 0, + IND_DISABLED, + IND_MUTE, + IND_WAITING, + IND_WARNING, + IND_ERROR +}; -inline bool getChannelState(uint8_t ch) { - if (ch >= CHANNEL_NUM) return false; - return channelState[ch]; -} +extern const uint8_t BUTTON_PINS[BUTTON_CHANNEL_NUM]; +extern const uint8_t SIGNAL_LED; // GPIO пин ленты -inline void applyChannelState(uint8_t ch, bool on) { - // if (ch >= CHANNEL_NUM) return; +extern IndicatorState channel_indicator[BUTTON_CHANNEL_NUM]; - // uint8_t pin = BUTTON_PINS[ch]; +void button_logic_setup(); +void button_logic_loop(); - // // Логика: on ^ invert → физический уровень - // bool physicalOn = on ^ RELAY_INVERT[ch]; // XOR +void set_channel_indicator(uint8_t ch, IndicatorState st); +IndicatorState get_channel_indicator(uint8_t ch); - // digitalWrite(pin, physicalOn ? HIGH : LOW); -} \ No newline at end of file +// локальная индикация ожидания/ошибки после нажатия +void set_channel_waiting(uint8_t ch); +void set_channel_warning_temp(uint8_t ch, uint32_t ms); +void clear_channel_temp(uint8_t ch); + +// отправка события +void send_press_event(uint8_t ch); diff --git a/devices/button/button_esp8266/RelayLogic.h b/devices/button/button_esp8266/RelayLogic.h deleted file mode 100644 index c8023ec..0000000 --- a/devices/button/button_esp8266/RelayLogic.h +++ /dev/null @@ -1,48 +0,0 @@ -#ifndef RELAY_LOGIC_H -#define RELAY_LOGIC_H - -#include -#include "Config.h" - -// Массив пинов — определяем в .ino, а здесь только объявляем -extern const uint8_t RELAY_PINS[CHANNEL_NUM]; - -// Состояния каналов — тоже логика реле, а не ядра -extern bool channelState[CHANNEL_NUM]; - -extern const bool RELAY_INVERT[CHANNEL_NUM]; - -// Включение/выключение конкретного канала -inline void applyChannelState(uint8_t ch, bool on) { - if (ch >= CHANNEL_NUM) return; - - uint8_t pin = RELAY_PINS[ch]; - - // Логика: on ^ invert → физический уровень - bool physicalOn = on ^ RELAY_INVERT[ch]; // XOR - - digitalWrite(pin, physicalOn ? HIGH : LOW); -} - -inline bool getChannelState(uint8_t ch) { - if (ch >= CHANNEL_NUM) return false; - return channelState[ch]; -} - -// Установить состояние канала + callback + сохранение -void setChannelState(uint8_t ch, bool on) { - if (ch >= CHANNEL_NUM) return; - channelState[ch] = on; - applyChannelState(ch, on); -} - -// Для старой совместимости -bool getPrimaryState() { - return getChannelState(0); -} - -inline void setOn(bool on) { - setChannelState(0, on); -} - -#endif diff --git a/devices/button/button_esp8266/button_esp8266.ino b/devices/button/button_esp8266/button_esp8266.ino index ae83cf1..cded489 100755 --- a/devices/button/button_esp8266/button_esp8266.ino +++ b/devices/button/button_esp8266/button_esp8266.ino @@ -1,86 +1,94 @@ #include +// Кол-во каналов (и для ядра, и для device-layer) +#define BUTTON_CHANNEL_NUM 3 + const char* DEVICE_TYPE = "button"; -const char* FW_VERSION = "1.0.0 alpha"; -const uint8_t CHANNEL_NUM = 3; +const char* FW_VERSION = "1.0.0 alpha"; +const uint8_t CHANNEL_NUM = BUTTON_CHANNEL_NUM; #include #include "ButtonLogic.h" -// --------------------- УСТРОЙСТВЕННЫЙ КОНФИГ --------------------- +// Используем GPIO номера напрямую: +// D1 mini: D2=GPIO4, D5=GPIO14, D6=GPIO12, D7=GPIO13 +const uint8_t SIGNAL_LED = 4; // GPIO4 +const uint8_t BUTTON_PINS[BUTTON_CHANNEL_NUM] = { 14, 12, 13 }; // GPIO14, GPIO12, GPIO13 -const uint16_t BUTTON_EEPROM_BASE = getDeviceEepromStart(); +static String extract_json_string_value_local(const String &body, const String &key) { + String pattern = "\"" + key + "\""; + int keyIndex = body.indexOf(pattern); + if (keyIndex < 0) return ""; -// если когда-нибудь понадобится хранить состояние каналов: -const uint16_t BUTTON_STATE_ADDR = BUTTON_EEPROM_BASE; // например -// const uint16_t RELAY_OTHER_ADDR = BUTTON_EEPROM_BASE + 16; + int colonIndex = body.indexOf(':', keyIndex); + if (colonIndex < 0) return ""; + int firstQuote = body.indexOf('"', colonIndex + 1); + if (firstQuote < 0) return ""; -// Количество каналов уже задано в Config.h через CHANNEL_NUM. -const uint8_t BUTTON_PINS[CHANNEL_NUM] = { 5, 6, 7 }; + int secondQuote = body.indexOf('"', firstQuote + 1); + if (secondQuote < 0) return ""; -const uint8_t SIGNAL_LED = 2; + return body.substring(firstQuote + 1, secondQuote); +} -// Состояние каналов (по умолчанию все выключены) -bool channelState[CHANNEL_NUM] = { false, false, false }; +static int extract_json_int_value_local(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] == ' ') i++; -// void relayLoadStates() { -// EEPROM.begin(EEPROM_SIZE); - -// for (uint8_t ch = 0; ch < CHANNEL_NUM; ch++) { -// uint8_t v = EEPROM.read(BUTTON_STATE_ADDR + ch); - -// // 0xFF считаем "ничего не записано" → по умолчанию выкл -// if (v == 0xFF) { -// channelState[ch] = false; -// } else { -// channelState[ch] = (v != 0); -// } -// } - -// EEPROM.end(); - -// // применяем к железу -// for (uint8_t ch = 0; ch < CHANNEL_NUM; ch++) { -// applyChannelState(ch, channelState[ch]); -// } -// } - -// void relaySaveStates() { -// EEPROM.begin(EEPROM_SIZE); - -// for (uint8_t ch = 0; ch < CHANNEL_NUM; ch++) { -// EEPROM.write(BUTTON_STATE_ADDR + ch, channelState[ch] ? 1 : 0); -// } - -// EEPROM.commit(); -// EEPROM.end(); -// } - + long v = 0; + bool any = false; + while (i < (int)body.length()) { + char c = body[i]; + if (c < '0' || c > '9') break; + any = true; + v = v * 10 + (c - '0'); + i++; + } + if (!any) return -1; + return (int)v; +} void appendStatusJsonFields(String &json) { - // Базовое поле "status":"ok" уже добавлено в REST_API.h, - // мы лишь дополняем JSON полем channels. + json += ",\"indicators\":\""; + if (deviceMode == DEVICE_MODE_SETUP) { + json += "setup"; + } else if (WiFi.status() != WL_CONNECTED) { + json += "nowifi"; + } else { + json += "none"; + } + json += "\""; json += ",\"channels\":["; - for (uint8_t ch = 0; ch < CHANNEL_NUM; ch++) { - if (ch > 0) json += ","; + for (uint8_t ch = 0; ch < BUTTON_CHANNEL_NUM; ch++) { + if (ch) json += ","; + json += "{\"id\":" + String(ch) + ",\"indicator\":\""; - json += "{"; - json += "\"id\":" + String(ch) + ","; - json += "\"state\":\""; - json += getChannelState(ch) ? "on" : "off"; - json += "\""; - json += "}"; + IndicatorState st = get_channel_indicator(ch); + switch (st) { + case IND_ENABLED: json += "enabled"; break; + case IND_DISABLED: json += "disabled"; break; + case IND_MUTE: json += "mute"; break; + case IND_WAITING: json += "waiting"; break; + case IND_WARNING: json += "warning"; break; + case IND_ERROR: json += "error"; break; + } + + json += "\"}"; } json += "]"; } - void appendAboutJsonFields(String &json) { json += ",\"channels\":" + String(CHANNEL_NUM); + json += ",\"has_indicators\":true"; } bool deviceHandleAction(const String &action, @@ -88,58 +96,30 @@ String &errorCode, String &errorMessage) { - // if (action == "set_state") { - // String state = extractJsonStringValue(paramsJson, "state"); + if (action == "set_channel_state") { + int ch = extract_json_int_value_local(paramsJson, "channel"); + if (ch < 0 || ch >= BUTTON_CHANNEL_NUM) { + errorCode = "IllegalActionOrParams"; + errorMessage = "Invalid channel"; + return false; + } - // if (state != "on" && state != "off") { - // errorCode = "IllegalActionOrParams"; - // errorMessage = "Invalid state"; - // return false; - // } + String st = extract_json_string_value_local(paramsJson, "state"); + st.toLowerCase(); - // setOn(state == "on"); - // relaySaveStates(); - // return true; - // } + if (st == "enabled") set_channel_indicator((uint8_t)ch, IND_ENABLED); + else if (st == "disabled") set_channel_indicator((uint8_t)ch, IND_DISABLED); + else if (st == "mute") set_channel_indicator((uint8_t)ch, IND_MUTE); + else if (st == "warning") set_channel_indicator((uint8_t)ch, IND_WARNING); + else if (st == "error") set_channel_indicator((uint8_t)ch, IND_ERROR); + else { + errorCode = "IllegalActionOrParams"; + errorMessage = "Invalid state"; + return false; + } - // if (action == "set_channel_state") { - // // Канал - // int ch = extractJsonIntValue(paramsJson, "channel"); - // if (ch < 0 || ch >= CHANNEL_NUM) { - // errorCode = "IllegalActionOrParams"; - // errorMessage = "Invalid channel index"; - // return false; - // } - - // // State - // String state = extractJsonStringValue(paramsJson, "state"); - // state.toLowerCase(); - // if (state != "on" && state != "off") { - // errorCode = "IllegalActionOrParams"; - // errorMessage = "Invalid state"; - // return false; - // } - - // bool newState = (state == "on"); - // setChannelState(ch, newState); - // relaySaveStates(); - // return true; - // } - - // if (action == "toggle_channel") { - // int ch = extractJsonIntValue(paramsJson, "channel"); - // if (ch < 0 || ch >= CHANNEL_NUM) { - // errorCode = "IllegalActionOrParams"; - // errorMessage = "Invalid channel index"; - // return false; - // } - - // bool newState = !getChannelState(ch); - // setChannelState(ch, newState); - // relaySaveStates(); - // return true; - // } - + return true; + } errorCode = "IllegalActionOrParams"; errorMessage = "Unknown action"; @@ -147,26 +127,19 @@ } void deviceHandleReset() { - for (uint8_t ch = 0; ch < CHANNEL_NUM; ch++) { - channelState[ch] = false; - applyChannelState(ch, false); + for (uint8_t ch = 0; ch < BUTTON_CHANNEL_NUM; ch++) { + set_channel_indicator(ch, IND_DISABLED); + clear_channel_temp(ch); } - - EEPROM.begin(EEPROM_SIZE); - for (uint16_t addr = DEVICE_EEPROM_START; addr < EEPROM_SIZE; addr++) { - EEPROM.write(addr, 0xFF); - } - EEPROM.commit(); - EEPROM.end(); } - void setup() { - // relayLoadStates(); - coreSetup(); + button_logic_setup(); } void loop() { coreLoop(); + delay(0); + button_logic_loop(); } diff --git a/devices/sh_core_esp8266/src/REST_API.cpp b/devices/sh_core_esp8266/src/REST_API.cpp index 9972db5..fcaec17 100755 --- a/devices/sh_core_esp8266/src/REST_API.cpp +++ b/devices/sh_core_esp8266/src/REST_API.cpp @@ -30,7 +30,10 @@ } static void sendJson(int code, const String &body) { + server.sendHeader(F("Connection"), F("close")); + server.sendHeader(F("Content-Length"), String(body.length())); server.send(code, F("application/json; charset=utf-8"), body); + server.client().stop(); } static String extractJsonStringValue(const String &body, const String &key) { diff --git a/devices/sh_core_esp8266/src/sh_core_esp8266.cpp b/devices/sh_core_esp8266/src/sh_core_esp8266.cpp index 7f1df8b..bc891d9 100644 --- a/devices/sh_core_esp8266/src/sh_core_esp8266.cpp +++ b/devices/sh_core_esp8266/src/sh_core_esp8266.cpp @@ -294,4 +294,31 @@ server.handleClient(); wifi_tick(); + delay(0); } + +// -------------------- core: post json helper (добавлено) -------------------- +bool core_post_json_to_server(const String &path, const String &json_body, uint32_t timeout_ms, int &http_code_out) { + http_code_out = -1; + + if (WiFi.status() != WL_CONNECTED) return false; + if (authToken.length() == 0) return false; + if (serverBaseUrl.length() == 0 || serverBaseUrl == "0.0.0.0") return false; + + String url = "http://" + serverBaseUrl + path; + + WiFiClient client; + HTTPClient http; + + if (!http.begin(client, url)) return false; + + http.setTimeout(timeout_ms); + http.addHeader("Content-Type", "application/json"); + http.addHeader("Authorization", "Bearer " + authToken); + + int code = http.POST(json_body); + http.end(); + + http_code_out = code; + return (code >= 200 && code < 300); +} \ No newline at end of file diff --git a/devices/sh_core_esp8266/src/sh_core_esp8266.h b/devices/sh_core_esp8266/src/sh_core_esp8266.h index 1363b58..a83a96e 100755 --- a/devices/sh_core_esp8266/src/sh_core_esp8266.h +++ b/devices/sh_core_esp8266/src/sh_core_esp8266.h @@ -6,6 +6,8 @@ #include #include #include +#include +#include #include // -------------------- compile-time defaults -------------------- @@ -94,6 +96,10 @@ void coreSetup(); void coreLoop(); +// Универсальный POST JSON на serverBaseUrl (добавлено) +bool core_post_json_to_server(const String &path, const String &json_body, uint32_t timeout_ms, int &http_code_out); + + // -------------------- routes registration (реализованы в .cpp) -------------------- void registerWebUiRoutes(); void registerRestApiRoutes();