huesmith 0.1.0

Hue-compatible Zigbee light library for ESP32-C6/H2 (ESP-IDF)
use esp_idf_hal::gpio::{AnyInputPin, AnyOutputPin, Pull};
use esp_idf_hal::peripherals::Peripherals;
use huesmith_core::hue::identity::DeviceIdentity;
use huesmith_core::light::state::LightState;
use huesmith_core::light::LightOutput;

use crate::light::{cct::CctPwm, simple::SimpleLed, ws2812::Ws2812b};
use crate::{Light, LightKind};

pub(crate) fn launch(mut light: Light) -> ! {
    esp_idf_svc::sys::link_patches();
    esp_idf_svc::log::EspLogger::initialize_default();

    unsafe {
        let ret = esp_idf_sys::nvs_flash_init();
        if ret == esp_idf_sys::ESP_ERR_NVS_NO_FREE_PAGES
            || ret == esp_idf_sys::ESP_ERR_NVS_NEW_VERSION_FOUND
        {
            esp_idf_sys::nvs_flash_erase();
            let retry = esp_idf_sys::nvs_flash_init();
            if retry != esp_idf_sys::ESP_OK {
                log::error!(
                    "nvs_flash_init failed after erase: {retry} — Zigbee state may not persist"
                );
            }
        }
    }

    log::info!("╔═══════════════════════════════════════════════╗");
    log::info!("║  huesmith v{:<35}║", env!("CARGO_PKG_VERSION"));
    log::info!("║  Hue-compatible Zigbee Light                  ║");
    log::info!("╚═══════════════════════════════════════════════╝");
    log::info!(
        "Device: {} {} ({:?})",
        light.identity.manufacturer_name,
        light.identity.model_identifier,
        light.kind
    );

    let p = Peripherals::take().expect("peripherals already taken");

    // Check reset pin before initializing hardware: if the pin is held low at
    // boot, erase Zigbee NVRAM so the device re-enters pairing mode.
    let nvram_erase = if let Some(reset_gpio) = light.reset_pin {
        let pin = unsafe { AnyInputPin::steal(reset_gpio) };
        match esp_idf_hal::gpio::PinDriver::input(pin, Pull::Up) {
            Ok(driver) => {
                let is_low = driver.is_low();
                if is_low {
                    log::warn!(
                        "Factory reset: GPIO{} held low at boot — erasing Zigbee NVRAM",
                        reset_gpio
                    );
                }
                is_low
            }
            Err(e) => {
                log::error!("Failed to configure reset pin GPIO{}: {e}", reset_gpio);
                false
            }
        }
    } else {
        false
    };

    // Build the hardware LED output from the builder kind + gpio numbers.
    //
    // SAFETY: AnyOutputPin::steal() bypasses the Rust peripheral ownership
    // system to allow runtime GPIO selection. This is safe because:
    //   1. Peripherals::take() above guarantees exclusive peripheral access.
    //   2. Each GPIO number is used at most once (user error otherwise).
    //   3. The resulting outputs live for the entire program lifetime.
    let led_output: Box<dyn LightOutput> = match light.kind {
        LightKind::Dimmable { gpio } | LightKind::OnOff { gpio } => {
            log::info!(
                "LED: PWM on GPIO{}{}",
                gpio,
                if light.inverted { " (inverted)" } else { "" }
            );
            let pin = unsafe { AnyOutputPin::steal(gpio) };
            Box::new(SimpleLed::new(
                p.ledc.channel0,
                p.ledc.timer0,
                pin,
                light.inverted,
            ))
        }
        LightKind::Cct {
            cool_gpio,
            warm_gpio,
        } => {
            log::info!(
                "LED: CCT PWM cool=GPIO{} warm=GPIO{}{}",
                cool_gpio,
                warm_gpio,
                if light.inverted { " (inverted)" } else { "" }
            );
            let cool_pin = unsafe { AnyOutputPin::steal(cool_gpio) };
            let warm_pin = unsafe { AnyOutputPin::steal(warm_gpio) };
            Box::new(CctPwm::new(
                p.ledc.timer0,
                p.ledc.channel0,
                cool_pin,
                p.ledc.channel1,
                warm_pin,
                light.inverted,
            ))
        }
        LightKind::Color { gpio } => {
            log::info!(
                "LED: WS2812B on GPIO{} ({} LED{})",
                gpio,
                light.leds,
                if light.leds != 1 { "s" } else { "" },
            );
            let pin = unsafe { AnyOutputPin::steal(gpio) };
            // `channel0` is deprecated only because the legacy RMT API is; see ws2812.rs.
            #[allow(deprecated)]
            let channel = p.rmt.channel0;
            let active = light.active_leds.unwrap_or(light.leds).min(light.leds);
            Box::new(Ws2812b::new(channel, pin, light.leds, active))
        }
        LightKind::Custom { device_type } => {
            log::info!("LED: user-supplied LightOutput backend ({:?})", device_type);
            light
                .custom_output
                .take()
                .expect("Light::custom always sets custom_output")
        }
    };

    let light_state = if light.power_on {
        LightState::new_powered_on(led_output)
    } else {
        LightState::new(led_output)
    };

    // Leak identity and config into 'static so the Zigbee stack can hold
    // references for the entire program lifetime (zigbee::run never returns).
    let identity: &'static DeviceIdentity = Box::leak(Box::new(light.identity));

    let config: &'static crate::zigbee::Config = Box::leak(Box::new(crate::zigbee::Config {
        device_type: light.kind.device_type(),
        identity,
        mac: light.mac,
        channel: light.channel,
        endpoint_id: 11,
    }));

    log::info!(
        "Zigbee: endpoint {}, {:?}, starting...",
        config.endpoint_id,
        config.device_type,
    );

    crate::zigbee::run(config, light_state, nvram_erase)
}