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

#include <ESP8266WiFi.h>
#include <sh_core_esp8266.h>

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

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

// Debounce
static const uint32_t DEBOUNCE_MS = 50;

// Тайминги индикаций
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";

// лента
static Adafruit_NeoPixel pixels(BUTTON_CHANNEL_NUM, SIGNAL_LED, NEO_GRB + NEO_KHZ800);

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

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

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

// базовые индикаторы (что задаёт сервер)
IndicatorState channel_indicator[BUTTON_CHANNEL_NUM] = { IND_DISABLED, IND_DISABLED, IND_DISABLED, IND_DISABLED };
static const uint32_t IndicatorPosition[BUTTON_CHANNEL_NUM] = {0, 2, 3, 1};

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

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

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

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

static IndicatorState get_effective_indicator(uint8_t ch) {
  if (ch >= BUTTON_CHANNEL_NUM) 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() {
  uint32_t t = now_ms();

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

  // обычный режим: по каналам
  for (uint8_t ch = 0; ch < BUTTON_CHANNEL_NUM; ch++) {
    IndicatorState st = get_effective_indicator(IndicatorPosition[ch]);

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

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

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

  set_channel_waiting(ch);

  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() {
  // сенсорные модули: INPUT
  for (uint8_t ch = 0; ch < BUTTON_CHANNEL_NUM; ch++) {
    pinMode(BUTTON_PINS[ch], INPUT_PULLUP);
    digitalWrite(BUTTON_PINS[ch], HIGH);
    // pinMode(BUTTON_PINS[ch], INPUT);

    uint8_t r = digitalRead(BUTTON_PINS[ch]) ? 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;
  }

  pixels.begin();
  pixels.setBrightness(30);
  pixels.clear();
  pixels.show();
}

void button_logic_loop() {
  uint32_t t = now_ms();

  for (uint8_t ch = 0; ch < BUTTON_CHANNEL_NUM; ch++) {
    uint8_t r = digitalRead(BUTTON_PINS[ch]) ? 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);
}