main.cpp
// Sparrow Zigbee nodes: switch (end device) and light (router)
 
#include <Arduino.h>
#include <esp_sleep.h>
#include "driver/gpio.h"
#include "Zigbee.h"
 
constexpr uint8_t SWITCH_PIN = 9;
constexpr uint32_t FACTORY_RESET_HOLD_MS = 3000;
constexpr uint32_t BUTTON_DEBOUNCE_MS = 50;
 
#if defined(SPARROW_SWITCH)
 
#include "ep/ZigbeeSwitch.h"
#include "zcl/esp_zigbee_zcl_command.h"
#include "zcl/esp_zigbee_zcl_on_off.h"
 
constexpr uint8_t SWITCH_ENDPOINT = 1;
constexpr uint32_t COMMISSIONING_AWAKE_MS = 15000;
constexpr uint32_t POST_COMMAND_HOLD_MS = 250;
constexpr uint32_t BUTTON_RELEASE_TIMEOUT_MS = 5000;
 
ZigbeeSwitch switchEp(SWITCH_ENDPOINT);
 
RTC_DATA_ATTR bool switchIsOn = false;
 
static void waitForButtonRelease() {
  uint32_t start = millis();
  while (digitalRead(SWITCH_PIN) == LOW) {
    uint32_t heldMs = millis() - start;
    if (heldMs >= FACTORY_RESET_HOLD_MS) {
      Serial.println("Factory reset Zigbee (switch)...");
      Zigbee.factoryReset(true);  // does not return
    }
    if (heldMs >= BUTTON_RELEASE_TIMEOUT_MS) {
      break;
    }
    delay(10);
  }
}
 
static void enterDeepSleep(uint32_t graceMs = 0) {
  if (graceMs > 0) {
    delay(graceMs);
  }
  Serial.println("Switch entering deep sleep…");
  Serial.flush();
  esp_deep_sleep_start();
}
 
static bool sendOnOffCommand(bool turnOn) {
  esp_zb_zcl_on_off_cmd_t cmd_req = {};
  cmd_req.address_mode = ESP_ZB_APS_ADDR_MODE_DST_ADDR_ENDP_NOT_PRESENT;
  cmd_req.zcl_basic_cmd.src_endpoint = SWITCH_ENDPOINT;
  cmd_req.on_off_cmd_id = turnOn ? ESP_ZB_ZCL_CMD_ON_OFF_ON_ID : ESP_ZB_ZCL_CMD_ON_OFF_OFF_ID;
 
  esp_zb_lock_acquire(portMAX_DELAY);
  esp_err_t err = esp_zb_zcl_on_off_cmd_req(&cmd_req);
  esp_zb_lock_release();
  if (err != ESP_OK) {
    Serial.printf("Failed to send on/off command: 0x%x\n", err);
    return false;
  }
  return true;
}
 
void setup() {
  Serial.begin(115200);
  delay(20);
  pinMode(SWITCH_PIN, INPUT_PULLUP);
 
  esp_err_t pdCfg = esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
  if (pdCfg != ESP_OK) {
    Serial.printf("RTC peripheral power config failed: 0x%x\n", pdCfg);
  }
  esp_err_t wakeCfg = esp_deep_sleep_enable_gpio_wakeup(1ULL << SWITCH_PIN, ESP_GPIO_WAKEUP_GPIO_LOW);
  if (wakeCfg != ESP_OK) {
    Serial.printf("Failed to enable EXT0 wakeup: 0x%x\n", wakeCfg);
  }
 
  switchEp.setManufacturerAndModel("Sparrow", "Switch");
  switchEp.setPowerSource(ZB_POWER_SOURCE_BATTERY, 100);
 
  Zigbee.addEndpoint(&switchEp);
  Serial.println("Starting Zigbee (switch)...");
  if (!Zigbee.begin()) {
    Serial.println("Zigbee failed to start, restarting…");
    delay(1000);
    ESP.restart();
  }
 
  Serial.print("Joining network");
  uint32_t joinStart = millis();
  while (!Zigbee.connected()) {
    Serial.print(".");
    delay(200);
    if (millis() - joinStart > 20000) {
      Serial.println("\nJoin timeout, restarting…");
      ESP.restart();
    }
  }
  Serial.println("\nJoined!");
 
  esp_sleep_wakeup_cause_t wakeCause = esp_sleep_get_wakeup_cause();
  bool wokeFromButton = (wakeCause == ESP_SLEEP_WAKEUP_EXT0);
 
  if (wokeFromButton) {
    switchIsOn = !switchIsOn;
    Serial.printf("Switch state → %s\n", switchIsOn ? "ON" : "OFF");
    if (sendOnOffCommand(switchIsOn)) {
      Serial.println("On/Off command sent.");
    }
    waitForButtonRelease();
    enterDeepSleep(POST_COMMAND_HOLD_MS);
  } else {
    Serial.println("Cold boot: staying awake for commissioning window.");
    delay(COMMISSIONING_AWAKE_MS);
    waitForButtonRelease();
    enterDeepSleep();
  }
}
 
void loop() {
  enterDeepSleep();
}
 
#elif defined(SPARROW_LIGHT)
 
#include <Adafruit_NeoPixel.h>
#include "ep/ZigbeeLight.h"
 
constexpr uint8_t LIGHT_ENDPOINT = 1;
constexpr uint8_t NEOPIXEL_PIN = 3;
constexpr uint8_t NEOPIXEL_COUNT = 1;
constexpr uint8_t NEOPIXEL_BRIGHTNESS = 48;
constexpr uint32_t IDENTIFY_BLINK_INTERVAL_MS = 250;
constexpr uint32_t IDENTIFY_DEFAULT_DURATION_MS = 5000;
 
ZigbeeLight lightEp(LIGHT_ENDPOINT);
Adafruit_NeoPixel statusPixel(NEOPIXEL_COUNT, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800);
 
static bool lightState = false;
static bool lastButtonLevel = HIGH;
static uint32_t lastButtonChangeMs = 0;
static uint32_t pressStartMs = 0;
static bool longPressHandled = false;
 
static bool identifyActive = false;
static bool identifyBlinkOn = false;
static bool identifyReset = false;
static uint32_t identifyDeadlineMs = 0;
static uint32_t identifyLastToggleMs = 0;
 
static void refreshNeoPixel() {
  if (identifyActive) {
    if (identifyBlinkOn) {
      statusPixel.setPixelColor(0, statusPixel.Color(0, 0, 255));  // blue during identify
    } else {
      statusPixel.clear();
    }
  } else {
    if (lightState) {
      statusPixel.setPixelColor(0, statusPixel.Color(255, 200, 80));  // warm white
    } else {
      statusPixel.clear();
    }
  }
  statusPixel.show();
}
 
static void handleLightChange(bool on) {
  lightState = on;
  if (!identifyActive) {
    refreshNeoPixel();
  }
}
 
static void handleIdentify(uint16_t seconds) {
  if (seconds == 0) {
    identifyActive = false;
    identifyReset = true;
    return;
  }
 
  uint32_t durationMs = seconds > 0 ? static_cast<uint32_t>(seconds) * 1000UL : IDENTIFY_DEFAULT_DURATION_MS;
  if (durationMs == 0) {
    durationMs = IDENTIFY_DEFAULT_DURATION_MS;
  }
 
  identifyDeadlineMs = millis() + durationMs;
  identifyLastToggleMs = 0;
  identifyBlinkOn = false;
  identifyActive = true;
  identifyReset = true;
}
 
void setup() {
  Serial.begin(115200);
  pinMode(SWITCH_PIN, INPUT_PULLUP);
 
  statusPixel.begin();
  statusPixel.setBrightness(NEOPIXEL_BRIGHTNESS);
  statusPixel.clear();
  statusPixel.show();
 
  lightEp.setManufacturerAndModel("Sparrow", "Light");
  lightEp.onLightChange(handleLightChange);
  lightEp.onIdentify(handleIdentify);
 
  Zigbee.addEndpoint(&lightEp);
  Serial.println("Starting Zigbee (light)...");
  if (!Zigbee.begin()) {
    Serial.println("Zigbee failed to start, restarting…");
    delay(1000);
    ESP.restart();
  }
 
  Serial.print("Joining network");
  while (!Zigbee.connected()) {
    Serial.print(".");
    delay(200);
  }
  Serial.println("\nJoined!");
 
  lightEp.setLight(false);
}
 
void loop() {
  uint32_t now = millis();
 
  if (identifyReset) {
    identifyReset = false;
    refreshNeoPixel();
  }
 
  if (identifyActive) {
    if ((int32_t)(identifyDeadlineMs - now) <= 0) {
      identifyActive = false;
      refreshNeoPixel();
    } else if (identifyLastToggleMs == 0 || (now - identifyLastToggleMs) >= IDENTIFY_BLINK_INTERVAL_MS) {
      identifyLastToggleMs = now;
      identifyBlinkOn = !identifyBlinkOn;
      refreshNeoPixel();
    }
  }
 
  bool reading = digitalRead(SWITCH_PIN);
  if (reading != lastButtonLevel && (now - lastButtonChangeMs) >= BUTTON_DEBOUNCE_MS) {
    lastButtonChangeMs = now;
    lastButtonLevel = reading;
    if (reading == LOW) {
      pressStartMs = now;
      longPressHandled = false;
    } else {
      if (pressStartMs != 0 && !longPressHandled) {
        bool newState = !lightState;
        Serial.printf("Local light toggle → %s\n", newState ? "ON" : "OFF");
        lightEp.setLight(newState);
      }
      pressStartMs = 0;
    }
  }
 
  if (lastButtonLevel == LOW && pressStartMs != 0 && !longPressHandled && (now - pressStartMs) >= FACTORY_RESET_HOLD_MS) {
    longPressHandled = true;
    identifyActive = false;
    identifyReset = true;
    Serial.println("Factory reset Zigbee (light)...");
    Zigbee.factoryReset(true);
  }
 
  delay(10);
}
 
#else
 
#error "Define SPARROW_SWITCH or SPARROW_LIGHT for this firmware."
 
#endif