diff --git a/devices/button/button.ino b/devices/button/button.ino index 0727187..53d21f4 100755 --- a/devices/button/button.ino +++ b/devices/button/button.ino @@ -4,7 +4,7 @@ const char* FW_VERSION = "1.3 dev"; // число каналов устройства (1..8) -extern const uint8_t CHANNEL_NUM = 4; +extern const uint8_t CHANNEL_NUM = 2; #include #include "ButtonLogic.h" diff --git a/devices/hatch/HatchLogic.h b/devices/hatch/HatchLogic.h index 1f66ca6..f3fcd93 100644 --- a/devices/hatch/HatchLogic.h +++ b/devices/hatch/HatchLogic.h @@ -10,14 +10,51 @@ #define HATCH_CH_OPEN 0 #define HATCH_CH_CLOSE 1 +// -------------------- Силовое реле (хардкод) -------------------- +// Реле безопасности: коммутирует +питание к приводу. +// Включается ПОСЛЕ выбора сигнального канала, +// выключается ДО снятия сигнального канала. +// GPIO 26 — безопасный пин на ESP32 WROOM/DevKit, не влияет на boot. +extern uint8_t HATCH_POWER_RELAY_PIN; + +static constexpr uint8_t HATCH_POWER_RELAY_ACTIVE = HIGH; +static constexpr uint8_t HATCH_POWER_RELAY_IDLE = LOW; + +inline void hatch_power_relay(bool on) { + if (HATCH_POWER_RELAY_PIN == SH_PIN_UNUSED) return; + digitalWrite(HATCH_POWER_RELAY_PIN, on ? HATCH_POWER_RELAY_ACTIVE : HATCH_POWER_RELAY_IDLE); +} + // -------------------- Ограничения -------------------- -static constexpr float HATCH_LIMIT_MAX = 35.0f; // макс. открытие (сек) -static constexpr float HATCH_LIMIT_MIN = 0.0f; // полностью закрыто -static constexpr float HATCH_CALIB_OVERDRIVE = 0.1f; // доп. время закрытия при калибровке - // totalValue не опускается ниже -HATCH_CALIB_OVERDRIVE +static constexpr float HATCH_LIMIT_MAX = 35.0f; +static constexpr float HATCH_LIMIT_MIN = 0.0f; +static constexpr float HATCH_CALIB_OVERDRIVE = 0.1f; // -------------------- Состояние люка -------------------- -extern float hatchPosition; // текущая позиция в секундах [0 .. HATCH_LIMIT_MAX] +extern float hatchPosition; + +// -------------------- State machine -------------------- +enum HatchState : uint8_t { + HATCH_STATE_IDLE = 0, + HATCH_STATE_CALIBRATING, + HATCH_STATE_OPENING, + HATCH_STATE_CLOSING, +}; + +enum HatchActionResult : uint8_t { + HATCH_OK = 0, + HATCH_ERR_ALREADY_AT_LIMIT, + HATCH_ERR_ALREADY_CLOSED, + HATCH_ERR_CALIBRATION_FAILED, + HATCH_ERR_INVALID_TIME, + HATCH_ERR_BUSY, +}; + +static HatchState _hatchState = HATCH_STATE_IDLE; +static uint32_t _hatchMoveStart = 0; +static uint32_t _hatchMoveDuration = 0; +static float _hatchMoveTarget = 0.0f; +static float _hatchOpenAfterCalib = 0.0f; // -------------------- Низкоуровневое управление пинами -------------------- @@ -35,12 +72,10 @@ digitalWrite(pin, physical ? HIGH : LOW); } -// Считать концевик "закрыто" (feedback канала 1) -// Возвращает true если люк в положении "закрыто" inline bool hatch_read_limit_switch() { uint8_t pin = sh_channel_feedback_pin(HATCH_CH_CLOSE); if (pin == SH_PIN_UNUSED) return false; - return (digitalRead(pin) == HIGH); + return (digitalRead(pin) == LOW); } // -------------------- Отправка событий -------------------- @@ -62,7 +97,7 @@ // -------------------- Сохранение/загрузка позиции -------------------- -extern uint16_t HATCH_EEPROM_ADDR; // задаётся в .ino +extern uint16_t HATCH_EEPROM_ADDR; inline void hatchSavePosition() { EEPROM.begin(EEPROM_SIZE); @@ -82,140 +117,169 @@ } } -// -------------------- Основная логика движения -------------------- +// -------------------- Остановка -------------------- -// Закрываем люк до срабатывания концевика. -// Максимальное время закрытия: (hatchPosition + HATCH_CALIB_OVERDRIVE) сек. -// Возвращает true если концевик сработал (калибровка успешна). -inline bool hatchCalibrate() { +static void hatch_stop_all() { + hatch_power_relay(false); + hatch_pin_open(false); + hatch_pin_close(false); + _hatchState = HATCH_STATE_IDLE; +} + +// -------------------- Запуск движения -------------------- + +static void hatch_start_open(float targetPos) { + float actualSec = targetPos - hatchPosition; + if (actualSec <= 0.0f) return; + + Serial.print(F("[hatch_start_open] pos=")); + Serial.print(hatchPosition, 2); + Serial.print(F(" target=")); + Serial.print(targetPos, 2); + Serial.print(F(" actualSec=")); + Serial.println(actualSec, 2); + + _hatchMoveTarget = targetPos; + _hatchMoveDuration = (uint32_t)(actualSec * 1000.0f); + _hatchMoveStart = millis(); + _hatchState = HATCH_STATE_OPENING; + + hatch_pin_open(true); + hatch_power_relay(true); +} + +static void hatch_start_close(float targetPos) { + float actualSec = hatchPosition - targetPos; + if (actualSec <= 0.0f) return; + + _hatchMoveTarget = targetPos; + _hatchMoveDuration = (uint32_t)(actualSec * 1000.0f); + _hatchMoveStart = millis(); + _hatchState = HATCH_STATE_CLOSING; + + hatch_pin_close(true); + hatch_power_relay(true); +} + +static void hatch_start_calibrate(float openAfter) { float maxCloseTime = hatchPosition + HATCH_CALIB_OVERDRIVE; if (maxCloseTime < HATCH_CALIB_OVERDRIVE) maxCloseTime = HATCH_CALIB_OVERDRIVE; - uint32_t duration_ms = (uint32_t)(maxCloseTime * 1000.0f); - uint32_t start = millis(); + _hatchMoveDuration = (uint32_t)(maxCloseTime * 1000.0f); + _hatchMoveStart = millis(); + _hatchOpenAfterCalib = openAfter; + _hatchState = HATCH_STATE_CALIBRATING; hatch_pin_close(true); - - bool switched = false; - while (millis() - start < duration_ms) { - if (hatch_read_limit_switch()) { - switched = true; - break; - } - delay(10); - } - - hatch_pin_close(false); - - if (switched) { - hatchPosition = 0.0f; - hatchSavePosition(); - hatch_send_event("limit_switch_activated", "{}"); - } - - return switched; + hatch_power_relay(true); } -// Открыть люк на заданное время (с учётом текущей позиции и лимита). -// Возвращает реально затраченное время. -inline float hatchDoOpen(float requestedSec) { - float newPos = hatchPosition + requestedSec; - if (newPos > HATCH_LIMIT_MAX) newPos = HATCH_LIMIT_MAX; +// -------------------- Tick (вызывать из loop) -------------------- - float actualSec = newPos - hatchPosition; - if (actualSec <= 0.0f) return 0.0f; +inline void hatchTick() { + if (_hatchState == HATCH_STATE_IDLE) return; - hatch_pin_open(true); - delay((uint32_t)(actualSec * 1000.0f)); - hatch_pin_open(false); + uint32_t elapsed = millis() - _hatchMoveStart; + bool timeout = (elapsed >= _hatchMoveDuration); - hatchPosition = newPos; - hatchSavePosition(); - - return actualSec; -} - -// Закрыть люк на заданное время (с учётом текущей позиции и лимита 0). -// При срабатывании концевика — останавливаем досрочно. -inline float hatchDoClose(float requestedSec) { - float newPos = hatchPosition - requestedSec; - if (newPos < HATCH_LIMIT_MIN) newPos = HATCH_LIMIT_MIN; - - float actualSec = hatchPosition - newPos; - if (actualSec <= 0.0f) return 0.0f; - - uint32_t duration_ms = (uint32_t)(actualSec * 1000.0f); - uint32_t start = millis(); - - hatch_pin_close(true); - - while (millis() - start < duration_ms) { + // --- CALIBRATING --- + if (_hatchState == HATCH_STATE_CALIBRATING) { if (hatch_read_limit_switch()) { - hatch_pin_close(false); + hatch_stop_all(); hatchPosition = 0.0f; hatchSavePosition(); hatch_send_event("limit_switch_activated", "{}"); - return (float)(millis() - start) / 1000.0f; + + if (_hatchOpenAfterCalib > 0.0f) { + float target = _hatchOpenAfterCalib; + if (target > HATCH_LIMIT_MAX) target = HATCH_LIMIT_MAX; + hatch_start_open(target); + } + return; } - delay(10); + + if (timeout) { + hatch_stop_all(); + hatchPosition = 0.0f; + hatchSavePosition(); + hatch_send_event("calibration_failed", "{}"); + } + return; } - hatch_pin_close(false); + // --- OPENING --- + if (_hatchState == HATCH_STATE_OPENING) { + if (timeout) { + hatch_stop_all(); + hatchPosition = _hatchMoveTarget; + hatchSavePosition(); + } + return; + } - hatchPosition = newPos; - hatchSavePosition(); + // --- CLOSING --- + if (_hatchState == HATCH_STATE_CLOSING) { + if (hatch_read_limit_switch()) { + hatch_stop_all(); + hatchPosition = 0.0f; + hatchSavePosition(); + hatch_send_event("limit_switch_activated", "{}"); + return; + } - return actualSec; + if (timeout) { + hatch_stop_all(); + hatchPosition = _hatchMoveTarget; + hatchSavePosition(); + } + return; + } } -// -------------------- Высокоуровневые команды -------------------- +// -------------------- Публичные функции -------------------- -// Результат выполнения команды -enum HatchActionResult : uint8_t { - HATCH_OK = 0, - HATCH_ERR_ALREADY_AT_LIMIT, // уже на максимуме - HATCH_ERR_ALREADY_CLOSED, // уже закрыт - HATCH_ERR_CALIBRATION_FAILED, // концевик не сработал при калибровке - HATCH_ERR_INVALID_TIME, // некорректное время -}; +inline bool hatchIsBusy() { + return _hatchState != HATCH_STATE_IDLE; +} + +inline HatchState hatchGetState() { + return _hatchState; +} inline HatchActionResult hatchOpen(float timeSec) { - if (timeSec <= 0.0f) return HATCH_ERR_INVALID_TIME; - - // Уже на максимуме + if (timeSec <= 0.0f) return HATCH_ERR_INVALID_TIME; + if (hatchIsBusy()) return HATCH_ERR_BUSY; if (hatchPosition >= HATCH_LIMIT_MAX) return HATCH_ERR_ALREADY_AT_LIMIT; - // Если концевик активен — мы точно в 0 + float targetPos = hatchPosition + timeSec; + if (targetPos > HATCH_LIMIT_MAX) targetPos = HATCH_LIMIT_MAX; + if (hatch_read_limit_switch()) { hatchPosition = 0.0f; hatchSavePosition(); hatch_send_event("limit_switch_activated", "{}"); - hatchDoOpen(timeSec); + hatch_start_open(targetPos); return HATCH_OK; } - // Люк закрыт (по учёту), но концевик не активен — нужна калибровка if (hatchPosition == 0.0f) { - bool ok = hatchCalibrate(); - if (!ok) { - hatch_send_event("calibration_failed", "{}"); - return HATCH_ERR_CALIBRATION_FAILED; - } - hatchDoOpen(timeSec); + hatch_start_calibrate(targetPos); return HATCH_OK; } - // Люк уже открыт — просто открываем дальше (min(pos + time, 35)) - hatchDoOpen(timeSec); + hatch_start_open(targetPos); return HATCH_OK; } inline HatchActionResult hatchClose(float timeSec) { - if (timeSec <= 0.0f) return HATCH_ERR_INVALID_TIME; - + if (timeSec <= 0.0f) return HATCH_ERR_INVALID_TIME; + if (hatchIsBusy()) return HATCH_ERR_BUSY; if (hatchPosition <= HATCH_LIMIT_MIN) return HATCH_ERR_ALREADY_CLOSED; - hatchDoClose(timeSec); + float targetPos = hatchPosition - timeSec; + if (targetPos < HATCH_LIMIT_MIN) targetPos = HATCH_LIMIT_MIN; + + hatch_start_close(targetPos); return HATCH_OK; } diff --git a/devices/hatch/hatch.ino b/devices/hatch/hatch.ino index 264755d..f71517d 100644 --- a/devices/hatch/hatch.ino +++ b/devices/hatch/hatch.ino @@ -11,6 +11,11 @@ const uint16_t HATCH_EEPROM_BASE = getDeviceEepromStart(); uint16_t HATCH_EEPROM_ADDR = HATCH_EEPROM_BASE; // float, 4 байта +// -------------------- Силовое реле -------------------- +// GPIO 26 — безопасный пин на ESP32 WROOM/DevKit. +// Поменяй если используешь другой модуль. +uint8_t HATCH_POWER_RELAY_PIN = 18; + // -------------------- Состояние -------------------- float hatchPosition = 0.0f; @@ -58,9 +63,15 @@ uint8_t pct = (uint8_t)((hatchPosition / HATCH_LIMIT_MAX) * 100.0f + 0.5f); if (pct > 100) pct = 100; + const char* stateStr = "closed"; + if (hatchGetState() == HATCH_STATE_OPENING) stateStr = "opening"; + else if (hatchGetState() == HATCH_STATE_CLOSING) stateStr = "closing"; + else if (hatchGetState() == HATCH_STATE_CALIBRATING) stateStr = "calibrating"; + else if (hatchPosition > 0.0f) stateStr = "open"; + json += ",\"hatch\":{"; json += "\"state\":\""; - json += (hatchPosition > 0.0f) ? "open" : "closed"; + json += stateStr; json += "\","; json += "\"position_sec\":"; json += String(hatchPosition, 2); @@ -84,12 +95,19 @@ { if (action == "open") { float timeSec = extractJsonFloatValue(paramsJson, "time"); - if (timeSec <= 0.0f) { + float pct = extractJsonFloatValue(paramsJson, "percent"); + + if (timeSec <= 0.0f && pct <= 0.0f) { errorCode = "IllegalActionOrParams"; - errorMessage = "Parameter 'time' must be > 0"; + errorMessage = "Parameter 'time' or 'percent' must be > 0"; return false; } + if (pct > 0.0f) { + if (pct > 100.0f) pct = 100.0f; + timeSec = (pct / 100.0f) * HATCH_LIMIT_MAX; + } + HatchActionResult res = hatchOpen(timeSec); switch (res) { @@ -101,6 +119,11 @@ errorMessage = "Hatch is already fully open"; return false; + case HATCH_ERR_BUSY: + errorCode = "DeviceBusy"; + errorMessage = "Hatch is already moving"; + return false; + case HATCH_ERR_CALIBRATION_FAILED: errorCode = "CalibrationFailed"; errorMessage = "Limit switch did not trigger during calibration"; @@ -115,12 +138,19 @@ if (action == "close") { float timeSec = extractJsonFloatValue(paramsJson, "time"); - if (timeSec <= 0.0f) { + float pct = extractJsonFloatValue(paramsJson, "percent"); + + if (timeSec <= 0.0f && pct <= 0.0f) { errorCode = "IllegalActionOrParams"; - errorMessage = "Parameter 'time' must be > 0"; + errorMessage = "Parameter 'time' or 'percent' must be > 0"; return false; } + if (pct > 0.0f) { + if (pct > 100.0f) pct = 100.0f; + timeSec = (pct / 100.0f) * HATCH_LIMIT_MAX; + } + HatchActionResult res = hatchClose(timeSec); switch (res) { @@ -132,6 +162,11 @@ errorMessage = "Hatch is already closed"; return false; + case HATCH_ERR_BUSY: + errorCode = "DeviceBusy"; + errorMessage = "Hatch is already moving"; + return false; + default: errorCode = "IllegalActionOrParams"; errorMessage = "Close failed"; @@ -162,14 +197,48 @@ // -------------------- setup / loop -------------------- void setup() { + // Принудительно выключаем все реле ДО coreSetup — + // чтобы избежать случайного срабатывания при boot. + + // Силовое реле — хардкод, выключаем первым + if (HATCH_POWER_RELAY_PIN != SH_PIN_UNUSED) { + pinMode(HATCH_POWER_RELAY_PIN, OUTPUT); + digitalWrite(HATCH_POWER_RELAY_PIN, HATCH_POWER_RELAY_IDLE); + } + + // Загружаем карту каналов из EEPROM чтобы знать пины сигнальных реле + // (coreSetup ещё не вызван, но EEPROM читать можно) + if (is_channels_schema_valid()) { + load_channels_schema(); + } else { + init_default_channels_schema(); + } + + // Сигнальные реле — выключаем по карте каналов + // HIGH = реле выключено для активного LOW модуля + const uint8_t earlyPins[] = { sh_channel_pin(HATCH_CH_OPEN), sh_channel_pin(HATCH_CH_CLOSE) }; + for (uint8_t i = 0; i < 2; i++) { + if (earlyPins[i] != SH_PIN_UNUSED) { + pinMode(earlyPins[i], OUTPUT); + digitalWrite(earlyPins[i], HIGH); // HIGH = реле выключено (активный LOW) + } + } + coreSetup(); - // Инициализация пинов управления + // Инициализация силового реле (безопасное состояние — выключено) + if (HATCH_POWER_RELAY_PIN != SH_PIN_UNUSED) { + pinMode(HATCH_POWER_RELAY_PIN, OUTPUT); + digitalWrite(HATCH_POWER_RELAY_PIN, HATCH_POWER_RELAY_IDLE); + } + + // Инициализация пинов управления (сначала уровень, потом режим — избегаем глитча) for (uint8_t ch = 0; ch < CHANNEL_NUM; ch++) { uint8_t pin = sh_channel_pin(ch); if (pin == SH_PIN_UNUSED) continue; + bool idleLevel = sh_channel_is_inverted(ch) ? HIGH : LOW; + digitalWrite(pin, idleLevel); pinMode(pin, OUTPUT); - digitalWrite(pin, LOW); } // Инициализация пина концевика @@ -188,4 +257,5 @@ void loop() { coreLoop(); + hatchTick(); } diff --git a/devices/relay/relay.ino b/devices/relay/relay.ino index e347e1b..bd75bea 100755 --- a/devices/relay/relay.ino +++ b/devices/relay/relay.ino @@ -2,7 +2,7 @@ const char* DEVICE_TYPE = "relay"; const char* FW_VERSION = "1.22 dev"; -const uint8_t CHANNEL_NUM = 1; +const uint8_t CHANNEL_NUM = 8; #include #include "RelayLogic.h" // вот тут находятся каналы и функции управления diff --git a/devices/sensor/SensorLogic.cpp b/devices/sensor/SensorLogic.cpp new file mode 100644 index 0000000..be5850e --- /dev/null +++ b/devices/sensor/SensorLogic.cpp @@ -0,0 +1,232 @@ +#include "SensorLogic.h" +#include "sh_core.h" + +#include "ld2420_radar.h" +#include "bh1750_sensor.h" +#include "bme280_sensor.h" +#include "max4466_mic.h" + +/* + ========================================================= + Внешние объекты — объявлены и инициализированы в sensor.ino + ========================================================= +*/ +extern Ld2420Radar radar; +extern Bh1750Sensor light_sensor; +extern Bme280Sensor climate_sensor; +extern Max4466Mic mic; + +/* + ========================================================= + NeoPixel + ========================================================= +*/ +static Adafruit_NeoPixel led(SENSOR_LED_COUNT, SENSOR_LED_PIN, NEO_GRB + NEO_KHZ800); + +static uint32_t color(uint8_t r, uint8_t g, uint8_t b) { + return led.Color(r, g, b); +} + +/* + ========================================================= + Состояние presence — для отслеживания смены + ========================================================= +*/ +static bool _prev_presence = false; +static bool _presence_init_done = false; // первый цикл — не шлём событие + +/* + ========================================================= + Вспомогательные функции + ========================================================= +*/ + +static bool _is_setup() { + return (deviceMode == DEVICE_MODE_SETUP); +} + +static bool _is_nowifi() { + if (deviceMode == DEVICE_MODE_SETUP) return false; + return (WiFi.status() != WL_CONNECTED); +} + +/* + Хотя бы один датчик online — считаем устройство рабочим. +*/ +static bool _any_sensor_online() { + return radar.is_online() + || light_sensor.is_online() + || climate_sensor.is_online() + || mic.is_online(); +} + +/* + ========================================================= + Индикатор + ========================================================= +*/ +static void _render_led() { + uint32_t t = millis(); + + /* --- Глобальные режимы --- */ + if (_is_setup()) { + bool on = ((t / SENSOR_BLINK_SLOW_MS) % 2) == 0; + led.setPixelColor(0, on ? color(255, 255, 255) : color(0, 0, 0)); + led.show(); + return; + } + + if (_is_nowifi()) { + bool on = ((t / SENSOR_BLINK_SLOW_MS) % 2) == 0; + led.setPixelColor(0, on ? color(0, 0, 255) : color(0, 0, 0)); + led.show(); + return; + } + + if (!_any_sensor_online()) { + bool on = ((t / SENSOR_BLINK_SLOW_MS) % 2) == 0; + led.setPixelColor(0, on ? color(255, 0, 0) : color(0, 0, 0)); + led.show(); + return; + } + + /* --- Рабочий режим --- */ + if (radar.is_presence()) { + /* Присутствие — постоянный мягкий белый */ + led.setPixelColor(0, color(60, 60, 60)); + } else { + /* Тишина — тихий зелёный */ + led.setPixelColor(0, color(0, 20, 0)); + } + + led.show(); +} + +/* + ========================================================= + Событие presence_changed + ========================================================= +*/ +static void _send_presence_event(bool presence) { + if (deviceMode != DEVICE_MODE_NORMAL) return; + if (WiFi.status() != WL_CONNECTED) return; + + String body = "{"; + body += "\"event_name\":\"presence_changed\","; + body += "\"data\":{"; + body += "\"presence\":" + String(presence ? "true" : "false") + ","; + body += "\"activity_score\":" + String(radar.get_activity_score()) + ","; + body += "\"activity_score_current\":" + String(radar.get_activity_score_current()) + ","; + body += "\"distance_m\":" + String(radar.get_distance_m(), 2); + body += "},"; + body += "\"device_id\":\"" + getUniqueID() + "\""; + body += "}"; + + int http_code = -1; + core_post_json_to_server(SENSOR_EVENT_PATH, body, SENSOR_EVENT_TIMEOUT_MS, http_code); +} + +/* + ========================================================= + Отслеживание смены presence + ========================================================= +*/ +static void _tick_presence() { + bool current = radar.is_presence(); + + if (!_presence_init_done) { + _prev_presence = current; + _presence_init_done = true; + return; + } + + if (current != _prev_presence) { + _prev_presence = current; + _send_presence_event(current); + } +} + +/* + ========================================================= + weak-хуки ядра + ========================================================= +*/ + +/* + /status — добавляем поля sensors и indicators. + Ядро уже открыло '{' и написало "status":"ok", + мы дописываем в json начиная с ','. +*/ +void appendStatusJsonFields(String &json) { + /* indicators */ + String ind; + if (_is_setup()) ind = "setup"; + else if (_is_nowifi()) ind = "nowifi"; + else ind = "ok"; + + json += ",\"indicators\":\"" + ind + "\""; + + /* sensors */ + json += ",\"sensors\":{"; + + /* light */ + json += "\"light\":" + light_sensor.get_state_json(); + + /* temperature */ + json += ",\"temperature\":" + climate_sensor.get_temperature_json(); + + /* pressure */ + json += ",\"pressure\":" + climate_sensor.get_pressure_json(); + + /* humidity — только если BME280 */ + if (climate_sensor.has_humidity()) { + json += ",\"humidity\":" + climate_sensor.get_humidity_json(); + } + + /* radar */ + json += ",\"radar\":" + String(radar.get_state_json()); + + /* microphone */ + json += ",\"microphone\":" + mic.get_state_json(); + + json += "}"; +} + +/* + /action — датчик пассивный. + Поддерживается одно действие: reset_peak_noise. +*/ +bool deviceHandleAction(const String &action, + const String ¶msJson, + String &errorCode, + String &errorMessage) +{ + (void)paramsJson; + + if (action == "reset_peak_noise") { + mic.reset_peak(); + return true; + } + + errorCode = "IllegalActionOrParams"; + errorMessage = "Device does not support this action"; + return false; +} + +/* + ========================================================= + Setup / Loop + ========================================================= +*/ + +void sensor_logic_setup() { + led.begin(); + led.setBrightness(40); + led.clear(); + led.show(); +} + +void sensor_logic_loop() { + _tick_presence(); + _render_led(); +} diff --git a/devices/sensor/SensorLogic.h b/devices/sensor/SensorLogic.h new file mode 100644 index 0000000..ee56fd6 --- /dev/null +++ b/devices/sensor/SensorLogic.h @@ -0,0 +1,47 @@ +#pragma once + +#include +#include + +/* + ========================================================= + SensorLogic — слой интеграции сенсорного устройства + с ядром sh_core. + + Отвечает за: + - Автономную индикацию на одном WS2812 (GPIO 13) + - Отслеживание смены presence и отправку события + presence_changed на сервер + - Сборку JSON для /status (appendStatusJsonFields) + - Обработку /action (deviceHandleAction) + ========================================================= +*/ + +/* ---- Пин NeoPixel ---- */ +static constexpr uint8_t SENSOR_LED_PIN = 13; +static constexpr uint8_t SENSOR_LED_COUNT = 1; + +/* ---- Путь для событий на сервере ---- */ +static const char* SENSOR_EVENT_PATH = "/events/new"; + +/* ---- Таймаут ожидания ответа сервера (мс) ---- */ +static constexpr uint32_t SENSOR_EVENT_TIMEOUT_MS = 1500; + +/* ---- Тайминги мигания ---- */ +static constexpr uint32_t SENSOR_BLINK_SLOW_MS = 1000; +static constexpr uint32_t SENSOR_BLINK_FAST_MS = 300; + +/* + Состояния индикатора (автономные, сервер не управляет). +*/ +enum SensorIndicatorState : uint8_t { + SI_SETUP = 0, // белое мигание 1/сек — не подключён к серверу + SI_NOWIFI, // синее мигание 1/сек — нет WiFi + SI_ERROR, // красное мигание 1/сек — ни один датчик не online + SI_IDLE, // тихий зелёный — всё OK, присутствия нет + SI_PRESENCE, // постоянный белый — присутствие обнаружено +}; + +/* ---- Публичный API ---- */ +void sensor_logic_setup(); +void sensor_logic_loop(); diff --git a/devices/sensor/_sensor.1ino b/devices/sensor/_sensor.1ino new file mode 100644 index 0000000..5b23cbc --- /dev/null +++ b/devices/sensor/_sensor.1ino @@ -0,0 +1,291 @@ +#include + +#include "ld2420_radar.h" +#include "bh1750_sensor.h" +#include "bme280_sensor.h" +#include "max4466_mic.h" + +/* + ========================= + GLOBAL OBJECTS + ========================= +*/ + +Ld2420Radar radar; +RadarConfig radar_config; + +Bh1750Sensor light_sensor; +Bh1750Config light_config; + +Bme280Sensor climate_sensor; +Bme280Config climate_config; + +Max4466Mic mic; +Max4466Config mic_config; + +/* + Отдельные I2C-шины. + 0-я шина для BH1750. + 1-я шина для BME/BMP280. +*/ +TwoWire i2c_climate = TwoWire(1); + +/* + Интервал печати состояния. +*/ +static const uint32_t print_interval_ms = 500; +uint32_t last_print_ms = 0; + + +/* + ========================= + RADAR CONFIGURATION + ========================= +*/ + +void setup_radar_config() { + /* ---- UART ---- */ + radar_config.uart_rx_pin = 4; + radar_config.uart_tx_pin = 15; + radar_config.baud_rate = 115200; + + /* ---- Присутствие и онлайн ---- */ + radar_config.presence_hold_ms = 1500; + radar_config.stale_after_ms = 2000; + radar_config.distance_ema_alpha = 0.6f; + + /* ---- Параметры, записываемые в радар при старте ---- */ + radar_config.min_gate = 1; // игнорировать ворота < 0.7 м + radar_config.max_gate = 8; // детекция до ~5.6 м + radar_config.radar_timeout_s = 5; + + /* ---- Пороги для записи в модуль (внутренняя логика радара) ---- */ + radar_config.move_threshold = 10000; + radar_config.still_threshold = 8000; + + /* + Пороги присутствия по воротам — вычисляется в прошивке, + поле pres из фрейма радара игнорируется. + + Настройка: + 1. Включи enable_debug_frames=true + 2. Посмотри [GATES] в пустой комнате + 3. Порог ворота = максимум_шума_этого_ворота * 2 + 4. g0 и g1 всегда 0 + + Текущие пороги подобраны под эту комнату: + g0-g1: шум антенны и фон — игнорируем + g2: шум до ~30 → порог 800 + g3: шум до ~220 → порог 500 (основной сигнал на 2 м) + g4-g6: высокий шум от помех в комнате → высокие пороги + g7+: малый шум → порог 2000 + */ + static const uint32_t thresholds[LD2420_TOTAL_GATES] = { + 0, // g0 ~0.0 м шум антенны + 0, // g1 ~0.7 м фоновый шум + 800, // g2 ~1.4 м + 500, // g3 ~2.1 м + 15000, // g4 ~2.8 м (помехи в комнате) + 25000, // g5 ~3.5 м (помехи в комнате) + 15000, // g6 ~4.2 м (помехи в комнате) + 2000, // g7 ~4.9 м + 2000, // g8 ~5.6 м + 500, // g9 + 500, // g10 + 500, // g11 + 500, // g12 + 500, // g13 + 500, // g14 + 500, // g15 + }; + memcpy(radar_config.presence_thresholds, thresholds, + sizeof(radar_config.presence_thresholds)); + + radar_config.presence_min_active_gates = 1; + + /* ---- activity_score ---- */ + radar_config.total_energy_max = 150000; + radar_config.activity_avg_window_s = 60; + radar_config.activity_trend_window_min = 10; + + /* ---- Отладка ---- */ + radar_config.enable_debug_frames = false; +} + + +/* + ========================= + BH1750 CONFIGURATION + ========================= +*/ + +void setup_light_config() { + /* + Отдельная шина освещённости: + SDA -> GPIO16 + SCL -> GPIO17 + ADDR -> VCC -> 0x5C + */ + light_config.sda_pin = 16; + light_config.scl_pin = 17; + light_config.i2c_address = 0x5C; + light_config.measurement_mode = 0x10; + light_config.stale_after_ms = 2000; + light_config.read_interval_ms = 200; + light_config.lux_ema_alpha = 0.25f; + light_config.lux_max = 500.0f; +} + + +/* + ========================= + BME/BMP280 CONFIGURATION + ========================= +*/ + +void setup_climate_config() { + /* + Отдельная климатическая шина: + SDA -> GPIO18 + SCL -> GPIO19 + SDO -> GND => 0x76 + SDO -> VCC => 0x77 + */ + climate_config.sda_pin = 18; + climate_config.scl_pin = 19; + climate_config.i2c_address = 0x76; + + climate_config.stale_after_ms = 3000; + climate_config.read_interval_ms = 1000; + + climate_config.temperature_ema_alpha = 0.25f; + climate_config.pressure_ema_alpha = 0.20f; + climate_config.humidity_ema_alpha = 0.20f; +} + + +/* + ========================= + MAX4466 CONFIGURATION + ========================= +*/ + +void setup_mic_config() { + /* + Аналоговый выход MAX4466 -> GPIO34 + GPIO34 — input only, ADC1_CH6, подходит для analogRead. + + Калибровка: + db_noise_floor — уровень тишины в вашем помещении (дБ). + db_scale_range_db — диапазон от тишины до максимума (дБ). + db_ref_mv — опорное напряжение для расчёта дБ. + + Стартовые значения подходят для большинства помещений. + Для точной калибровки: + 1. В тишине посмотрите db_raw в Serial — это ваш noise floor. + 2. При максимальном ожидаемом шуме посмотрите db_raw — + разница с noise floor и есть db_scale_range_db. + */ + mic_config.adc_pin = 34; + mic_config.adc_max_value = 4095; + mic_config.adc_vref_mv = 3300.0f; + + mic_config.sample_count = 256; + mic_config.sample_interval_us = 100; + + mic_config.read_interval_ms = 100; + mic_config.stale_after_ms = 2000; + + mic_config.db_ema_alpha = 0.15f; + mic_config.db_ref_mv = 1.0f; + mic_config.db_noise_floor = 30.0f; + mic_config.db_scale_range_db = 30.0f; + + mic_config.db_avg_window_s = 30; + mic_config.db_trend_window_min = 10; + + mic_config.peak_reset_ms = 60000; +} + + +/* + ========================= + DEVICE INITIALIZATION + ========================= +*/ + +void init_radar() { + setup_radar_config(); + bool ok = radar.begin(Serial2, radar_config); + if (ok) Serial.println("Radar configured and initialized"); + else Serial.println("Radar init failed — running with defaults"); +} + +void init_light_sensor() { + setup_light_config(); + bool ok = light_sensor.begin(light_config); + if (ok) Serial.println("BH1750 initialized"); + else Serial.println("BH1750 init failed"); +} + +void init_climate_sensor() { + setup_climate_config(); + bool ok = climate_sensor.begin(i2c_climate, climate_config); + if (ok) { + Serial.print("Climate sensor initialized: "); + Serial.println(climate_sensor.get_sensor_type_string()); + } else { + Serial.println("Climate sensor init failed"); + } +} + +void init_mic() { + setup_mic_config(); + mic.begin(mic_config); + Serial.println("MAX4466 initialized"); +} + + +/* + ========================= + SETUP + ========================= +*/ + +void setup() { + Serial.begin(115200); + delay(1000); + + Serial.println(); + Serial.println("Radar + BH1750 + BME/BMP280 + MAX4466 project start"); + + init_radar(); + init_light_sensor(); + init_climate_sensor(); + init_mic(); +} + + +/* + ========================= + LOOP + ========================= +*/ + +void loop() { + radar.update(); + light_sensor.update(); + climate_sensor.update(); + mic.update(); + + uint32_t now_ms = millis(); + + if (now_ms - last_print_ms >= print_interval_ms) { + last_print_ms = now_ms; + + Serial.println(radar.get_state_json()); + Serial.println(light_sensor.get_state_json()); + Serial.println(climate_sensor.get_state_json()); + Serial.println(mic.get_state_json()); + } +} diff --git a/devices/sensor/sensor.ino b/devices/sensor/sensor.ino index 5b23cbc..0aa6084 100644 --- a/devices/sensor/sensor.ino +++ b/devices/sensor/sensor.ino @@ -1,85 +1,73 @@ #include +#include #include "ld2420_radar.h" #include "bh1750_sensor.h" #include "bme280_sensor.h" #include "max4466_mic.h" +#include "SensorLogic.h" /* ========================= - GLOBAL OBJECTS + Тип и версия устройства + (требуются ядром) ========================= */ +const char* DEVICE_TYPE = "sensor"; +const char* FW_VERSION = "1.0.0"; -Ld2420Radar radar; -RadarConfig radar_config; +/* + Устройство не использует канальную схему. + Ядро требует CHANNEL_NUM — ставим 0. +*/ +const uint8_t CHANNEL_NUM = 0; -Bh1750Sensor light_sensor; -Bh1750Config light_config; +/* + ========================= + Глобальные объекты + ========================= +*/ +Ld2420Radar radar; +RadarConfig radar_config; -Bme280Sensor climate_sensor; -Bme280Config climate_config; +Bh1750Sensor light_sensor; +Bh1750Config light_config; -Max4466Mic mic; +Bme280Sensor climate_sensor; +Bme280Config climate_config; + +Max4466Mic mic; Max4466Config mic_config; /* - Отдельные I2C-шины. - 0-я шина для BH1750. - 1-я шина для BME/BMP280. + Отдельные I2C-шины: + Wire (0) — BH1750 SDA=16, SCL=17 + Wire1 (1) — BME/BMP280 SDA=18, SCL=19 */ TwoWire i2c_climate = TwoWire(1); -/* - Интервал печати состояния. -*/ -static const uint32_t print_interval_ms = 500; -uint32_t last_print_ms = 0; - /* ========================= RADAR CONFIGURATION ========================= */ - -void setup_radar_config() { - /* ---- UART ---- */ +static void setup_radar_config() { radar_config.uart_rx_pin = 4; radar_config.uart_tx_pin = 15; radar_config.baud_rate = 115200; - /* ---- Присутствие и онлайн ---- */ radar_config.presence_hold_ms = 1500; radar_config.stale_after_ms = 2000; radar_config.distance_ema_alpha = 0.6f; - /* ---- Параметры, записываемые в радар при старте ---- */ - radar_config.min_gate = 1; // игнорировать ворота < 0.7 м - radar_config.max_gate = 8; // детекция до ~5.6 м + radar_config.min_gate = 1; + radar_config.max_gate = 8; radar_config.radar_timeout_s = 5; - /* ---- Пороги для записи в модуль (внутренняя логика радара) ---- */ radar_config.move_threshold = 10000; radar_config.still_threshold = 8000; - /* - Пороги присутствия по воротам — вычисляется в прошивке, - поле pres из фрейма радара игнорируется. - - Настройка: - 1. Включи enable_debug_frames=true - 2. Посмотри [GATES] в пустой комнате - 3. Порог ворота = максимум_шума_этого_ворота * 2 - 4. g0 и g1 всегда 0 - - Текущие пороги подобраны под эту комнату: - g0-g1: шум антенны и фон — игнорируем - g2: шум до ~30 → порог 800 - g3: шум до ~220 → порог 500 (основной сигнал на 2 м) - g4-g6: высокий шум от помех в комнате → высокие пороги - g7+: малый шум → порог 2000 - */ static const uint32_t thresholds[LD2420_TOTAL_GATES] = { 0, // g0 ~0.0 м шум антенны 0, // g1 ~0.7 м фоновый шум @@ -103,12 +91,10 @@ radar_config.presence_min_active_gates = 1; - /* ---- activity_score ---- */ radar_config.total_energy_max = 150000; radar_config.activity_avg_window_s = 60; radar_config.activity_trend_window_min = 10; - /* ---- Отладка ---- */ radar_config.enable_debug_frames = false; } @@ -118,22 +104,15 @@ BH1750 CONFIGURATION ========================= */ - -void setup_light_config() { - /* - Отдельная шина освещённости: - SDA -> GPIO16 - SCL -> GPIO17 - ADDR -> VCC -> 0x5C - */ - light_config.sda_pin = 16; - light_config.scl_pin = 17; - light_config.i2c_address = 0x5C; +static void setup_light_config() { + light_config.sda_pin = 16; + light_config.scl_pin = 17; + light_config.i2c_address = 0x5C; light_config.measurement_mode = 0x10; - light_config.stale_after_ms = 2000; + light_config.stale_after_ms = 2000; light_config.read_interval_ms = 200; - light_config.lux_ema_alpha = 0.25f; - light_config.lux_max = 500.0f; + light_config.lux_ema_alpha = 0.25f; + light_config.lux_max = 500.0f; } @@ -142,20 +121,12 @@ BME/BMP280 CONFIGURATION ========================= */ +static void setup_climate_config() { + climate_config.sda_pin = 18; + climate_config.scl_pin = 19; + climate_config.i2c_address = 0x76; -void setup_climate_config() { - /* - Отдельная климатическая шина: - SDA -> GPIO18 - SCL -> GPIO19 - SDO -> GND => 0x76 - SDO -> VCC => 0x77 - */ - climate_config.sda_pin = 18; - climate_config.scl_pin = 19; - climate_config.i2c_address = 0x76; - - climate_config.stale_after_ms = 3000; + climate_config.stale_after_ms = 3000; climate_config.read_interval_ms = 1000; climate_config.temperature_ema_alpha = 0.25f; @@ -169,42 +140,26 @@ MAX4466 CONFIGURATION ========================= */ +static void setup_mic_config() { + mic_config.adc_pin = 34; + mic_config.adc_max_value = 4095; + mic_config.adc_vref_mv = 3300.0f; -void setup_mic_config() { - /* - Аналоговый выход MAX4466 -> GPIO34 - GPIO34 — input only, ADC1_CH6, подходит для analogRead. - - Калибровка: - db_noise_floor — уровень тишины в вашем помещении (дБ). - db_scale_range_db — диапазон от тишины до максимума (дБ). - db_ref_mv — опорное напряжение для расчёта дБ. - - Стартовые значения подходят для большинства помещений. - Для точной калибровки: - 1. В тишине посмотрите db_raw в Serial — это ваш noise floor. - 2. При максимальном ожидаемом шуме посмотрите db_raw — - разница с noise floor и есть db_scale_range_db. - */ - mic_config.adc_pin = 34; - mic_config.adc_max_value = 4095; - mic_config.adc_vref_mv = 3300.0f; - - mic_config.sample_count = 256; + mic_config.sample_count = 256; mic_config.sample_interval_us = 100; - mic_config.read_interval_ms = 100; - mic_config.stale_after_ms = 2000; + mic_config.read_interval_ms = 100; + mic_config.stale_after_ms = 2000; - mic_config.db_ema_alpha = 0.15f; - mic_config.db_ref_mv = 1.0f; - mic_config.db_noise_floor = 30.0f; - mic_config.db_scale_range_db = 30.0f; + mic_config.db_ema_alpha = 0.15f; + mic_config.db_ref_mv = 1.0f; + mic_config.db_noise_floor = 30.0f; + mic_config.db_scale_range_db = 30.0f; - mic_config.db_avg_window_s = 30; - mic_config.db_trend_window_min = 10; + mic_config.db_avg_window_s = 30; + mic_config.db_trend_window_min = 10; - mic_config.peak_reset_ms = 60000; + mic_config.peak_reset_ms = 60000; } @@ -213,36 +168,33 @@ DEVICE INITIALIZATION ========================= */ - -void init_radar() { +static void init_radar() { setup_radar_config(); bool ok = radar.begin(Serial2, radar_config); - if (ok) Serial.println("Radar configured and initialized"); - else Serial.println("Radar init failed — running with defaults"); + Serial.println(ok ? "Radar OK" : "Radar FAIL"); } -void init_light_sensor() { +static void init_light_sensor() { setup_light_config(); bool ok = light_sensor.begin(light_config); - if (ok) Serial.println("BH1750 initialized"); - else Serial.println("BH1750 init failed"); + Serial.println(ok ? "BH1750 OK" : "BH1750 FAIL"); } -void init_climate_sensor() { +static void init_climate_sensor() { setup_climate_config(); bool ok = climate_sensor.begin(i2c_climate, climate_config); if (ok) { - Serial.print("Climate sensor initialized: "); + Serial.print("Climate OK: "); Serial.println(climate_sensor.get_sensor_type_string()); } else { - Serial.println("Climate sensor init failed"); + Serial.println("Climate FAIL"); } } -void init_mic() { +static void init_mic() { setup_mic_config(); mic.begin(mic_config); - Serial.println("MAX4466 initialized"); + Serial.println("MAX4466 OK"); } @@ -251,18 +203,28 @@ SETUP ========================= */ - void setup() { Serial.begin(115200); - delay(1000); + delay(500); - Serial.println(); - Serial.println("Radar + BH1750 + BME/BMP280 + MAX4466 project start"); + Serial.println("\n[sensor] starting..."); + /* + Ядро инициализирует WiFi, EEPROM, HTTP-сервер, + регистрирует базовые роуты (/about, /status, /action, ...). + */ + coreSetup(); + + /* Датчики */ init_radar(); init_light_sensor(); init_climate_sensor(); init_mic(); + + /* Индикатор и логика устройства */ + sensor_logic_setup(); + + Serial.println("[sensor] ready"); } @@ -271,21 +233,16 @@ LOOP ========================= */ - void loop() { + /* Ядро: обработка HTTP-запросов, watchdog и пр. */ + coreLoop(); + + /* Датчики */ radar.update(); light_sensor.update(); climate_sensor.update(); mic.update(); - uint32_t now_ms = millis(); - - if (now_ms - last_print_ms >= print_interval_ms) { - last_print_ms = now_ms; - - Serial.println(radar.get_state_json()); - Serial.println(light_sensor.get_state_json()); - Serial.println(climate_sensor.get_state_json()); - Serial.println(mic.get_state_json()); - } + /* Индикатор + отслеживание presence */ + sensor_logic_loop(); } diff --git a/server/SHServ/Entities/Device.php b/server/SHServ/Entities/Device.php index 83be383..5930bfd 100644 --- a/server/SHServ/Entities/Device.php +++ b/server/SHServ/Entities/Device.php @@ -8,6 +8,7 @@ use \SHServ\Tools\DeviceAPI\Relay; use \SHServ\Tools\DeviceAPI\Button; use \SHServ\Tools\DeviceAPI\Sensor; +use \SHServ\Tools\DeviceAPI\Hatch; class Device extends \SHServ\Middleware\Entity { @@ -76,6 +77,9 @@ case "sensor": $this -> device_api_instance = new Sensor($this -> device_ip); break; + case "hatch": + $this -> device_api_instance = new Hatch($this -> device_ip); + break; default: $this -> device_api_instance = new Base($this -> device_ip); } diff --git a/server/SHServ/Tools/DeviceAPI/Hatch.php b/server/SHServ/Tools/DeviceAPI/Hatch.php new file mode 100644 index 0000000..ac199ff --- /dev/null +++ b/server/SHServ/Tools/DeviceAPI/Hatch.php @@ -0,0 +1,59 @@ + get_status(); + + if($status_response["status"] != "ok") { + return null; + } + + return $status_response["hatch"]["state"]; + } + + public function is_opened(): Bool | null { + $state = $this -> get_state(); + + return is_null($state) + ? null + : $status_response["hatch"]["state"] == "open"; + } + + public function is_closed(): Bool | null { + $state = $this -> get_state(); + + return is_null($state) + ? null + : $status_response["hatch"]["state"] == "closed"; + } + + public function is_opening(): Bool | null { + $state = $this -> get_state(); + + return is_null($state) + ? null + : $status_response["hatch"]["state"] == "opening"; + } + + public function is_closing(): Bool { + $state = $this -> get_state(); + + return is_null($state) + ? null + : $status_response["hatch"]["state"] == "closing"; + } + + public function open(int $percent = 100) { + return $this -> post_action("open", [ + "percent" => max(0, $percent) + ]); + } + + public function close(int $percent = 100) { + return $this -> post_action("close", [ + "percent" => max(0, $percent) + ]); + } +} \ No newline at end of file