diff --git a/devices/hatch/HatchLogic.h b/devices/hatch/HatchLogic.h new file mode 100644 index 0000000..1f66ca6 --- /dev/null +++ b/devices/hatch/HatchLogic.h @@ -0,0 +1,222 @@ +#ifndef HATCH_LOGIC_H +#define HATCH_LOGIC_H + +#include + +// -------------------- Каналы -------------------- +// Канал 0: пин "открыть" +// Канал 1: пин "закрыть", SH_CH_FEEDBACK — концевик "закрыто" + +#define HATCH_CH_OPEN 0 +#define HATCH_CH_CLOSE 1 + +// -------------------- Ограничения -------------------- +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 + +// -------------------- Состояние люка -------------------- +extern float hatchPosition; // текущая позиция в секундах [0 .. HATCH_LIMIT_MAX] + +// -------------------- Низкоуровневое управление пинами -------------------- + +inline void hatch_pin_open(bool on) { + uint8_t pin = sh_channel_pin(HATCH_CH_OPEN); + if (pin == SH_PIN_UNUSED) return; + bool physical = on ^ sh_channel_is_inverted(HATCH_CH_OPEN); + digitalWrite(pin, physical ? HIGH : LOW); +} + +inline void hatch_pin_close(bool on) { + uint8_t pin = sh_channel_pin(HATCH_CH_CLOSE); + if (pin == SH_PIN_UNUSED) return; + bool physical = on ^ sh_channel_is_inverted(HATCH_CH_CLOSE); + 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); +} + +// -------------------- Отправка событий -------------------- + +static void hatch_send_event(const String &event_name, const String &data_json) { + if (serverBaseUrl.length() == 0 || serverBaseUrl == "0.0.0.0") return; + if (authToken.length() == 0) return; + + String body = "{"; + body += "\"device_type\":\"hatch\","; + body += "\"device_id\":\"" + getUniqueID() + "\","; + body += "\"event_name\":\"" + event_name + "\","; + body += "\"data\":" + data_json; + body += "}"; + + int http_code = -1; + core_post_json_to_server("/events/new", body, 1500, http_code); +} + +// -------------------- Сохранение/загрузка позиции -------------------- + +extern uint16_t HATCH_EEPROM_ADDR; // задаётся в .ino + +inline void hatchSavePosition() { + EEPROM.begin(EEPROM_SIZE); + EEPROM.put(HATCH_EEPROM_ADDR, hatchPosition); + EEPROM.commit(); + EEPROM.end(); +} + +inline void hatchLoadPosition() { + EEPROM.begin(EEPROM_SIZE); + EEPROM.get(HATCH_EEPROM_ADDR, hatchPosition); + EEPROM.end(); + + if (isnan(hatchPosition) || hatchPosition < HATCH_LIMIT_MIN || hatchPosition > HATCH_LIMIT_MAX) { + hatchPosition = 0.0f; + hatchSavePosition(); + } +} + +// -------------------- Основная логика движения -------------------- + +// Закрываем люк до срабатывания концевика. +// Максимальное время закрытия: (hatchPosition + HATCH_CALIB_OVERDRIVE) сек. +// Возвращает true если концевик сработал (калибровка успешна). +inline bool hatchCalibrate() { + 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(); + + 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; +} + +// Открыть люк на заданное время (с учётом текущей позиции и лимита). +// Возвращает реально затраченное время. +inline float hatchDoOpen(float requestedSec) { + float newPos = hatchPosition + requestedSec; + if (newPos > HATCH_LIMIT_MAX) newPos = HATCH_LIMIT_MAX; + + float actualSec = newPos - hatchPosition; + if (actualSec <= 0.0f) return 0.0f; + + hatch_pin_open(true); + delay((uint32_t)(actualSec * 1000.0f)); + hatch_pin_open(false); + + 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) { + if (hatch_read_limit_switch()) { + hatch_pin_close(false); + hatchPosition = 0.0f; + hatchSavePosition(); + hatch_send_event("limit_switch_activated", "{}"); + return (float)(millis() - start) / 1000.0f; + } + delay(10); + } + + hatch_pin_close(false); + + hatchPosition = newPos; + hatchSavePosition(); + + return actualSec; +} + +// -------------------- Высокоуровневые команды -------------------- + +// Результат выполнения команды +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 HatchActionResult hatchOpen(float timeSec) { + if (timeSec <= 0.0f) return HATCH_ERR_INVALID_TIME; + + // Уже на максимуме + if (hatchPosition >= HATCH_LIMIT_MAX) return HATCH_ERR_ALREADY_AT_LIMIT; + + // Если концевик активен — мы точно в 0 + if (hatch_read_limit_switch()) { + hatchPosition = 0.0f; + hatchSavePosition(); + hatch_send_event("limit_switch_activated", "{}"); + hatchDoOpen(timeSec); + return HATCH_OK; + } + + // Люк закрыт (по учёту), но концевик не активен — нужна калибровка + if (hatchPosition == 0.0f) { + bool ok = hatchCalibrate(); + if (!ok) { + hatch_send_event("calibration_failed", "{}"); + return HATCH_ERR_CALIBRATION_FAILED; + } + hatchDoOpen(timeSec); + return HATCH_OK; + } + + // Люк уже открыт — просто открываем дальше (min(pos + time, 35)) + hatchDoOpen(timeSec); + return HATCH_OK; +} + +inline HatchActionResult hatchClose(float timeSec) { + if (timeSec <= 0.0f) return HATCH_ERR_INVALID_TIME; + + if (hatchPosition <= HATCH_LIMIT_MIN) return HATCH_ERR_ALREADY_CLOSED; + + hatchDoClose(timeSec); + return HATCH_OK; +} + +#endif // HATCH_LOGIC_H \ No newline at end of file diff --git a/devices/hatch/hatch.ino b/devices/hatch/hatch.ino new file mode 100644 index 0000000..264755d --- /dev/null +++ b/devices/hatch/hatch.ino @@ -0,0 +1,191 @@ +#include + +const char* DEVICE_TYPE = "hatch"; +const char* FW_VERSION = "1.0 dev"; +const uint8_t CHANNEL_NUM = 2; // 0: открыть, 1: закрыть + +#include +#include "HatchLogic.h" + +// -------------------- EEPROM -------------------- +const uint16_t HATCH_EEPROM_BASE = getDeviceEepromStart(); +uint16_t HATCH_EEPROM_ADDR = HATCH_EEPROM_BASE; // float, 4 байта + +// -------------------- Состояние -------------------- +float hatchPosition = 0.0f; + +// -------------------- JSON helpers -------------------- + +static 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); +} + +static float extractJsonFloatValue(const String &body, const String &key) { + String pattern = "\"" + key + "\""; + int keyIndex = body.indexOf(pattern); + if (keyIndex < 0) return -1.0f; + int colonIndex = body.indexOf(':', keyIndex); + if (colonIndex < 0) return -1.0f; + + 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++; + + String numStr = ""; + while (i < (int)body.length() && + (body[i] == '-' || body[i] == '.' || (body[i] >= '0' && body[i] <= '9'))) { + numStr += body[i++]; + } + if (numStr.length() == 0) return -1.0f; + return numStr.toFloat(); +} + +// -------------------- appendStatusJsonFields -------------------- + +void appendStatusJsonFields(String &json) { + uint8_t pct = (uint8_t)((hatchPosition / HATCH_LIMIT_MAX) * 100.0f + 0.5f); + if (pct > 100) pct = 100; + + json += ",\"hatch\":{"; + json += "\"state\":\""; + json += (hatchPosition > 0.0f) ? "open" : "closed"; + json += "\","; + json += "\"position_sec\":"; + json += String(hatchPosition, 2); + json += ",\"position_pct\":"; + json += String(pct); + json += "}"; +} + +// -------------------- appendAboutJsonFields -------------------- + +void appendAboutJsonFields(String &json) { + json += ",\"channels\":" + String(CHANNEL_NUM); +} + +// -------------------- deviceHandleAction -------------------- + +bool deviceHandleAction(const String &action, + const String ¶msJson, + String &errorCode, + String &errorMessage) +{ + if (action == "open") { + float timeSec = extractJsonFloatValue(paramsJson, "time"); + if (timeSec <= 0.0f) { + errorCode = "IllegalActionOrParams"; + errorMessage = "Parameter 'time' must be > 0"; + return false; + } + + HatchActionResult res = hatchOpen(timeSec); + + switch (res) { + case HATCH_OK: + return true; + + case HATCH_ERR_ALREADY_AT_LIMIT: + errorCode = "IllegalActionOrParams"; + errorMessage = "Hatch is already fully open"; + return false; + + case HATCH_ERR_CALIBRATION_FAILED: + errorCode = "CalibrationFailed"; + errorMessage = "Limit switch did not trigger during calibration"; + return false; + + default: + errorCode = "IllegalActionOrParams"; + errorMessage = "Open failed"; + return false; + } + } + + if (action == "close") { + float timeSec = extractJsonFloatValue(paramsJson, "time"); + if (timeSec <= 0.0f) { + errorCode = "IllegalActionOrParams"; + errorMessage = "Parameter 'time' must be > 0"; + return false; + } + + HatchActionResult res = hatchClose(timeSec); + + switch (res) { + case HATCH_OK: + return true; + + case HATCH_ERR_ALREADY_CLOSED: + errorCode = "IllegalActionOrParams"; + errorMessage = "Hatch is already closed"; + return false; + + default: + errorCode = "IllegalActionOrParams"; + errorMessage = "Close failed"; + return false; + } + } + + errorCode = "IllegalActionOrParams"; + errorMessage = "Unknown action. Supported: open, close"; + return false; +} + +// -------------------- deviceHandleReset -------------------- + +void deviceHandleReset() { + hatchPosition = 0.0f; + hatchSavePosition(); + + // Сбрасываем EEPROM устройства + EEPROM.begin(EEPROM_SIZE); + for (uint16_t addr = DEVICE_EEPROM_START; addr < EEPROM_SIZE; addr++) { + EEPROM.write(addr, 0xFF); + } + EEPROM.commit(); + EEPROM.end(); +} + +// -------------------- setup / loop -------------------- + +void setup() { + coreSetup(); + + // Инициализация пинов управления + for (uint8_t ch = 0; ch < CHANNEL_NUM; ch++) { + uint8_t pin = sh_channel_pin(ch); + if (pin == SH_PIN_UNUSED) continue; + pinMode(pin, OUTPUT); + digitalWrite(pin, LOW); + } + + // Инициализация пина концевика + uint8_t fbPin = sh_channel_feedback_pin(HATCH_CH_CLOSE); + if (fbPin != SH_PIN_UNUSED) { + pinMode(fbPin, INPUT_PULLDOWN); + } + + // Загрузка сохранённой позиции + hatchLoadPosition(); + + Serial.print(F("Hatch position loaded: ")); + Serial.print(hatchPosition, 2); + Serial.println(F(" sec")); +} + +void loop() { + coreLoop(); +} diff --git a/devices/relay/README.md b/devices/relay/README.md new file mode 100644 index 0000000..3b19578 --- /dev/null +++ b/devices/relay/README.md @@ -0,0 +1,42 @@ +Для 8ми канального реле у меня получилась схема вот такая: +```json +{ + "status": "ok", + "schema": [ + 1, + 0, + 255, + 1, + 5, + 1, + 255, + 1, + 3, + 2, + 255, + 1, + 4, + 3, + 255, + 1, + 12, + 255, + 255, + 1, + 13, + 255, + 255, + 1, + 14, + 255, + 255, + 1, + 16, + 255, + 255, + 1 + ] +} +``` + +Это для esp8266 в исполнении модуля d1 mini \ No newline at end of file