Newer
Older
smart-home-server / devices / button / ButtonLogic.cpp
#include "ButtonLogic.h"
#include <sh_core.h>

// Сенсорные модули обычно дают HIGH при касании.
// Если у тебя наоборот — поставь false.
static const bool TOUCH_ACTIVE_HIGH = true;

// Антиспам
static const uint32_t PRESS_ANTISPAM_MS = 20;

// Debounce
static const uint32_t DEBOUNCE_MS = 30;

// Тайминги индикаций
static const uint32_t WARNING_BLINK_MS = 500;
static const uint32_t ERROR_BLINK_MS   = 1500;
static const uint32_t GLOBAL_BLINK_MS  = 1000;

// Таймаут ожидания ответа сервера
static const uint32_t WAIT_TIMEOUT_MS = 1000;

// endpoint события (на сервере)
static const char* EVENT_PATH = "/events/new";

// NeoPixel создаём в setup (чтобы кол-во LED было CHANNEL_NUM)
static Adafruit_NeoPixel *pixels = nullptr;

// debounce/state
static uint8_t  last_raw[BUTTON_MAX_CHANNELS];
static uint8_t  stable_state[BUTTON_MAX_CHANNELS];
static uint32_t last_change_at[BUTTON_MAX_CHANNELS];

// антиспам
static uint32_t last_press_sent_at[BUTTON_MAX_CHANNELS];

// временные статусы
static bool temp_active[BUTTON_MAX_CHANNELS];
static IndicatorState temp_state[BUTTON_MAX_CHANNELS];
static uint32_t temp_until_ms[BUTTON_MAX_CHANNELS];

// базовые индикаторы (что задаёт сервер)
IndicatorState channel_indicator[BUTTON_MAX_CHANNELS] = {
  IND_DISABLED, IND_DISABLED, IND_DISABLED, IND_DISABLED,
  IND_DISABLED, IND_DISABLED, IND_DISABLED, IND_DISABLED
};

static uint32_t now_ms() { return millis(); }

static inline uint8_t chan_count() {
  uint8_t n = CHANNEL_NUM;
  if (n > BUTTON_MAX_CHANNELS) n = BUTTON_MAX_CHANNELS;
  if (n < 1) n = 1;
  return n;
}

static uint32_t color_rgb(uint8_t r, uint8_t g, uint8_t b) {
  if (!pixels) return 0;
  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 >= chan_count()) return;
  channel_indicator[ch] = st;
}

IndicatorState get_channel_indicator(uint8_t ch) {
  if (ch >= chan_count()) return IND_DISABLED;
  return channel_indicator[ch];
}

void set_channel_waiting(uint8_t ch) {
  if (ch >= chan_count()) 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 >= chan_count()) 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 >= chan_count()) return;
  temp_active[ch] = false;
}

static IndicatorState get_effective_indicator(uint8_t ch) {
  if (ch >= chan_count()) 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() {
  if (!pixels) return;

  uint32_t t = now_ms();
  uint8_t n = chan_count();

  // глобальные режимы перекрывают канальные
  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 < n; 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 < n; i++) pixels->setPixelColor(i, c);
    pixels->show();
    return;
  }

  // обычный режим: по каналам
  for (uint8_t ch = 0; ch < n; ch++) {
    uint8_t pos = sh_channel_indicator(ch);
    if (pos >= n) pos = ch;

    IndicatorState st = get_effective_indicator(pos);

    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 >= chan_count()) 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 >= chan_count()) return;

  // если канал в mute — игнорируем
  if (get_channel_indicator(ch) == IND_MUTE) return;

  // антиспам
  if (!antispam_allow_press(ch)) return;

  set_channel_waiting(ch);
  render_pixels();

  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() {
  uint8_t n = chan_count();

  // кнопки
  for (uint8_t ch = 0; ch < n; ch++) {
    uint8_t pin = sh_channel_pin(ch);
    if (pin == SH_PIN_UNUSED) continue;

    // сенсорные модули: INPUT_PULLUP (как у тебя было)
    pinMode(pin, INPUT_PULLUP);
    digitalWrite(pin, HIGH);

    uint8_t r = digitalRead(pin) ? 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;
  }

  // лента
  if (!pixels) {
    pixels = new Adafruit_NeoPixel(n, SIGNAL_LED, NEO_GRB + NEO_KHZ800);
  }

  pixels->begin();
  pixels->setBrightness(30);
  pixels->clear();
  pixels->show();
}

void button_logic_loop() {
  uint32_t t = now_ms();
  uint8_t n = chan_count();

  for (uint8_t ch = 0; ch < n; ch++) {
    uint8_t pin = sh_channel_pin(ch);
    if (pin == SH_PIN_UNUSED) continue;

    uint8_t r = digitalRead(pin) ? 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);
}