Newer
Older
smart-home-server / devices / hatch / HatchLogic.h
#ifndef HATCH_LOGIC_H
#define HATCH_LOGIC_H

#include <Arduino.h>

// -------------------- Каналы --------------------
// Канал 0: пин "открыть"
// Канал 1: пин "закрыть", SH_CH_FEEDBACK — концевик "закрыто"

#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;

// -------------------- Состояние люка --------------------
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;

// -------------------- Низкоуровневое управление пинами --------------------

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);
}

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) == LOW);
}

// -------------------- Отправка событий --------------------

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;

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();
  }
}

// -------------------- Остановка --------------------

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;

  _hatchMoveDuration   = (uint32_t)(maxCloseTime * 1000.0f);
  _hatchMoveStart      = millis();
  _hatchOpenAfterCalib = openAfter;
  _hatchState          = HATCH_STATE_CALIBRATING;

  hatch_pin_close(true);
  hatch_power_relay(true);
}

// -------------------- Tick (вызывать из loop) --------------------

inline void hatchTick() {
  if (_hatchState == HATCH_STATE_IDLE) return;

  uint32_t elapsed = millis() - _hatchMoveStart;
  bool     timeout = (elapsed >= _hatchMoveDuration);

  // --- CALIBRATING ---
  if (_hatchState == HATCH_STATE_CALIBRATING) {
    if (hatch_read_limit_switch()) {
      hatch_stop_all();
      hatchPosition = 0.0f;
      hatchSavePosition();
      hatch_send_event("limit_switch_activated", "{}");

      if (_hatchOpenAfterCalib > 0.0f) {
        float target = _hatchOpenAfterCalib;
        if (target > HATCH_LIMIT_MAX) target = HATCH_LIMIT_MAX;
        hatch_start_open(target);
      }
      return;
    }

    if (timeout) {
      hatch_stop_all();
      hatchPosition = 0.0f;
      hatchSavePosition();
      hatch_send_event("calibration_failed", "{}");
    }
    return;
  }

  // --- OPENING ---
  if (_hatchState == HATCH_STATE_OPENING) {
    if (timeout) {
      hatch_stop_all();
      hatchPosition = _hatchMoveTarget;
      hatchSavePosition();
    }
    return;
  }

  // --- 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;
    }

    if (timeout) {
      hatch_stop_all();
      hatchPosition = _hatchMoveTarget;
      hatchSavePosition();
    }
    return;
  }
}

// -------------------- Публичные функции --------------------

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 (hatchIsBusy())                    return HATCH_ERR_BUSY;
  if (hatchPosition >= HATCH_LIMIT_MAX) return HATCH_ERR_ALREADY_AT_LIMIT;

  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", "{}");
    hatch_start_open(targetPos);
    return HATCH_OK;
  }

  if (hatchPosition == 0.0f) {
    hatch_start_calibrate(targetPos);
    return HATCH_OK;
  }

  hatch_start_open(targetPos);
  return HATCH_OK;
}

inline HatchActionResult hatchClose(float timeSec) {
  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;

  float targetPos = hatchPosition - timeSec;
  if (targetPos < HATCH_LIMIT_MIN) targetPos = HATCH_LIMIT_MIN;

  hatch_start_close(targetPos);
  return HATCH_OK;
}

#endif // HATCH_LOGIC_H