huesmith 0.1.0

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

use esp_idf_hal::gpio::OutputPin;
use esp_idf_hal::ledc::{LedcChannel, LedcDriver, LedcTimer, LedcTimerDriver};

use super::electrical_duty;

/// Single-LED PWM output. Supports on/off and brightness dimming.
/// Color commands are mapped to perceived luminance (grayscale).
///
/// `inverted = false` (default): active-high — LED wired GPIO-high = on, or a
/// low-side switch (N-MOSFET/transistor). `inverted = true`: the GPIO sinks
/// current directly from the LED's cathode (LED `+` → 3V3, `−` → GPIO), so the
/// electrical duty is reversed.
pub struct SimpleLed<'d> {
    driver: LedcDriver<'d>,
    max_duty: u32,
    /// Last requested level (0-254). Kept so `set_on(true)` can restore the
    /// output on its own, as the `LightOutput` contract requires (identify
    /// blinking toggles `set_on` with no render call in between).
    level: u8,
    on: bool,
    inverted: bool,
}

impl<'d> SimpleLed<'d> {
    pub fn new<C, T, P>(channel: C, timer: T, pin: P, inverted: bool) -> Self
    where
        C: LedcChannel + 'd,
        T: LedcTimer<SpeedMode = C::SpeedMode> + 'd,
        P: OutputPin + 'd,
    {
        let timer_driver =
            LedcTimerDriver::new(timer, &esp_idf_hal::ledc::config::TimerConfig::default())
                .expect("LEDC timer init failed");

        let driver = LedcDriver::new(channel, timer_driver, pin).expect("LEDC driver init failed");
        let max_duty = driver.get_max_duty();

        let mut this = Self {
            driver,
            max_duty,
            level: 0,
            on: false,
            inverted,
        };
        // Park the pin in the electrical "off" state (high when inverted).
        this.apply_level();
        this
    }

    fn apply_level(&mut self) {
        let level = if self.on { self.level } else { 0 };
        let duty = electrical_duty(level, self.max_duty, self.inverted);
        if let Err(e) = self.driver.set_duty(duty) {
            log::error!("LEDC set_duty failed: {e}");
        }
    }
}

impl<'d> LightOutput for SimpleLed<'d> {
    fn set_on(&mut self, on: bool) {
        self.on = on;
        self.apply_level();
    }

    fn set_brightness(&mut self, level: u8) {
        self.level = level.min(254);
        self.apply_level();
    }

    fn set_color_xy(&mut self, _x: u16, _y: u16) {
        // Single LED: color is ignored, brightness is already set separately.
    }

    fn set_color_temp(&mut self, _mireds: u16) {
        // Single LED: color temperature is ignored.
    }

    fn set_rgb(&mut self, r: u8, g: u8, b: u8) {
        // Map RGB to perceived brightness using BT.709 luminance coefficients.
        let luminance = 0.2126 * f32::from(r) + 0.7152 * f32::from(g) + 0.0722 * f32::from(b);
        self.level = (luminance / 255.0 * 254.0).round() as u8;
        self.apply_level();
    }
}