huesmith 0.1.0

Hue-compatible Zigbee light library for ESP32-C6/H2 (ESP-IDF)
use super::ffi;

/// BDB signal types we handle during the Zigbee lifecycle.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BdbSignal {
    SkipStartup,
    DeviceFirstStart { ok: bool },
    DeviceReboot { ok: bool },
    Steering { ok: bool },
    TouchlinkStarted,
    TouchlinkJoinedRouter,
    CanSleep,
    Leave,
    DeviceAnnounce,
    MacSplitBoot,
    NlmeStatusIndication,
    PermitJoinStatus,
    DeviceUnavailable,
    Unknown(u32),
}

impl BdbSignal {
    pub fn from_raw(signal_type: u32, status: i32) -> Self {
        let ok = status == 0; // ESP_OK

        match signal_type {
            ffi::ESP_ZB_ZDO_SIGNAL_SKIP_STARTUP => Self::SkipStartup,
            ffi::ESP_ZB_BDB_SIGNAL_DEVICE_FIRST_START => Self::DeviceFirstStart { ok },
            ffi::ESP_ZB_BDB_SIGNAL_DEVICE_REBOOT => Self::DeviceReboot { ok },
            ffi::ESP_ZB_BDB_SIGNAL_STEERING => Self::Steering { ok },
            ffi::ESP_ZB_BDB_SIGNAL_TOUCHLINK_NWK_STARTED => Self::TouchlinkStarted,
            ffi::ESP_ZB_BDB_SIGNAL_TOUCHLINK_NWK_JOINED_ROUTER => Self::TouchlinkJoinedRouter,
            ffi::ESP_ZB_COMMON_SIGNAL_CAN_SLEEP => Self::CanSleep,
            ffi::ESP_ZB_ZDO_SIGNAL_LEAVE => Self::Leave,
            ffi::ESP_ZB_ZDO_SIGNAL_DEVICE_ANNCE => Self::DeviceAnnounce,
            ffi::ESP_ZB_MACSPLIT_DEVICE_BOOT => Self::MacSplitBoot,
            ffi::ESP_ZB_NLME_STATUS_INDICATION => Self::NlmeStatusIndication,
            ffi::ESP_ZB_NWK_SIGNAL_PERMIT_JOIN_STATUS => Self::PermitJoinStatus,
            ffi::ESP_ZB_ZDO_DEVICE_UNAVAILABLE => Self::DeviceUnavailable,
            other => Self::Unknown(other),
        }
    }
}

/// Counts consecutive `DeviceReboot { ok: false }` signals to gate the 60 s
/// rejoin window before opening for new-Hub steering.
static mut REJOIN_ATTEMPT: u8 = 0;

/// Scheduler callback for retrying commissioning. `mode` is passed as the
/// alarm parameter so both BDB_MODE_INITIALIZATION and BDB_MODE_NETWORK_STEERING
/// retries share this callback.
unsafe extern "C" fn steering_retry_cb(mode: u8) {
    esp_zb_bdb_start_top_level_commissioning_checked(mode);
}

fn esp_zb_bdb_start_top_level_commissioning_checked(mode: u8) {
    let ret = unsafe { ffi::esp_zb_bdb_start_top_level_commissioning(mode) };
    if ret != 0 {
        log::error!("Failed to start commissioning (mode {}): {}", mode, ret);
    }
}

// ── Joined hook ──────────────────────────────────────────────────────────────
// Called once when network steering succeeds. Sensors and switches register a
// hook here to start their polling alarm after the network join.

static mut JOINED_HOOK: unsafe fn() = default_joined_hook;
unsafe fn default_joined_hook() {}

/// Register a function to be called once when network steering succeeds.
///
/// SAFETY: must be called from a single-task context before `esp_zb_start`.
pub unsafe fn set_joined_hook(hook: unsafe fn()) {
    JOINED_HOOK = hook;
}

/// Handle a BDB signal during the Zigbee lifecycle.
pub fn handle_bdb_signal(signal: BdbSignal) {
    match signal {
        BdbSignal::SkipStartup => {
            log::info!("Zigbee stack initialized, starting BDB initialization");
            esp_zb_bdb_start_top_level_commissioning_checked(ffi::BDB_MODE_INITIALIZATION);
        }
        BdbSignal::DeviceFirstStart { ok: true } | BdbSignal::DeviceReboot { ok: true } => {
            unsafe {
                REJOIN_ATTEMPT = 0;
            }
            let factory_new = unsafe { ffi::esp_zb_bdb_is_factory_new() };
            if factory_new {
                log::info!("Factory-new device, starting network steering");
                esp_zb_bdb_start_top_level_commissioning_checked(ffi::BDB_MODE_NETWORK_STEERING);
            } else {
                log::info!("Device rebooted (already commissioned) — resuming");
                unsafe { JOINED_HOOK() };
            }
        }
        BdbSignal::DeviceFirstStart { ok: false } => {
            // No saved credentials — retry steering in 1 s.
            log::warn!("BDB first start failed, retrying steering in 1s...");
            unsafe {
                ffi::esp_zb_scheduler_alarm(
                    steering_retry_cb,
                    ffi::BDB_MODE_NETWORK_STEERING,
                    1_000,
                );
            }
        }
        BdbSignal::DeviceReboot { ok: false } => {
            // Has saved credentials but couldn't find the network.
            // Retry init for up to 60 s before opening for a new Hub.
            // NVM is never erased here — old credentials survive until a new
            // network is actually joined (steering overwrites them on success).
            let attempt = unsafe {
                REJOIN_ATTEMPT = REJOIN_ATTEMPT.saturating_add(1);
                REJOIN_ATTEMPT
            };
            if attempt < 6 {
                log::warn!(
                    "Network not found (attempt {}/6) — retrying in 10s...",
                    attempt
                );
                unsafe {
                    ffi::esp_zb_scheduler_alarm(
                        steering_retry_cb,
                        ffi::BDB_MODE_INITIALIZATION,
                        10_000,
                    );
                }
            } else {
                log::warn!("Network not found after ~60s — opening for new Hub (NVM preserved)");
                unsafe {
                    REJOIN_ATTEMPT = 0;
                    ffi::esp_zb_scheduler_alarm(
                        steering_retry_cb,
                        ffi::BDB_MODE_NETWORK_STEERING,
                        1_000,
                    );
                }
            }
        }
        BdbSignal::Steering { ok: true } => {
            unsafe {
                REJOIN_ATTEMPT = 0;
            }
            let ch = unsafe { ffi::esp_zb_get_current_channel() };
            let pan = unsafe { ffi::esp_zb_get_pan_id() };
            let addr = unsafe { ffi::esp_zb_get_short_address() };
            log::info!(
                "Network steering succeeded — joined network! ch={} PAN=0x{:04X} addr=0x{:04X}",
                ch,
                pan,
                addr,
            );
            unsafe { JOINED_HOOK() };
        }
        BdbSignal::Steering { ok: false } => {
            log::warn!("Network steering failed, retrying in 1s...");
            unsafe {
                ffi::esp_zb_scheduler_alarm(
                    steering_retry_cb,
                    ffi::BDB_MODE_NETWORK_STEERING,
                    1000,
                );
            }
        }
        BdbSignal::TouchlinkStarted | BdbSignal::TouchlinkJoinedRouter => {
            log::info!("Touchlink commissioning event");
        }
        BdbSignal::CanSleep => {}
        BdbSignal::Leave => {
            log::warn!("Left network");
        }
        BdbSignal::DeviceAnnounce => {
            log::debug!("Device announcement received");
        }
        BdbSignal::MacSplitBoot => {
            log::debug!("MAC co-processor boot");
        }
        BdbSignal::NlmeStatusIndication => {
            // NWK-layer status (e.g. no route, delivery failure). Useful when the
            // Bridge stops responding — kept at debug so it does not spam.
            log::debug!("NWK status indication");
        }
        BdbSignal::PermitJoinStatus => {
            log::debug!("Permit-join status changed");
        }
        BdbSignal::DeviceUnavailable => {
            log::debug!("ZDO device unavailable");
        }
        BdbSignal::Unknown(sig) => {
            log::info!("ZDO signal: 0x{:02x}", sig);
        }
    }
}