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

// -------------------- Ограничения --------------------
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