huesmith-core 0.1.0

Platform-agnostic core for Hue-compatible Zigbee lights: color math, ZCL light state machine, and scene parsing
Documentation
pub mod command;
pub mod state;

/// Trait for any LED output backend.
/// Implementations handle the actual hardware (GPIO PWM, WS2812B, etc.).
///
/// # Call contract
///
/// [`state::LightState`] drives implementations under these rules:
///
/// - `set_on(false)` must drive the output dark immediately.
/// - `set_on(true)` must restore the most recently set color/brightness on its
///   own. The identify/effect blink calls *only* `set_on` in alternation, so an
///   implementation that treats `set_on(true)` as a bare flag renders identify
///   invisible.
/// - Hardware errors are not propagated: the state machine has no recovery
///   action for a failed LED write, so implementations log and continue.
pub trait LightOutput: Send {
    /// Turn the light on or off. On `true`, re-apply the last output state.
    fn set_on(&mut self, on: bool);

    /// Set brightness level (0-254, where 254 is full brightness).
    fn set_brightness(&mut self, level: u8);

    /// Set color using CIE 1931 XY coordinates (0-65535 range for each axis).
    ///
    /// Defaults to a no-op. [`state::LightState`] never calls this — it converts
    /// XY to RGB and calls [`set_rgb`](LightOutput::set_rgb) instead — so
    /// override it only for a backend you drive with raw XY yourself.
    fn set_color_xy(&mut self, _x: u16, _y: u16) {}

    /// Set color temperature in mireds (153 = 6500K, 500 = 2000K).
    ///
    /// Defaults to a no-op, so an on/off or plain-dimmable backend with no
    /// tunable white needs to implement only [`set_on`](LightOutput::set_on) and
    /// [`set_brightness`](LightOutput::set_brightness).
    fn set_color_temp(&mut self, _mireds: u16) {}

    /// Set color using direct RGB values.
    ///
    /// Required — the state machine renders the XY and hue/saturation color
    /// modes (and dimmed color temperature) through this method, so a silently
    /// inherited no-op would mean a light that pairs, powers, and dims but
    /// never changes color. Backends with no color channel implement it as a
    /// brightness mapping (see `SimpleLed`'s BT.709 luminance) or an explicit
    /// empty body — the choice must be visible in the impl, not an invisible
    /// default.
    fn set_rgb(&mut self, r: u8, g: u8, b: u8);

    /// Observe the full raw light state after every render pass.
    ///
    /// Defaults to a no-op; the built-in backends ignore it. **Pick one lane:**
    /// simple hardware implements the `set_*` methods and ignores this hook;
    /// an effects/full-control backend treats this hook as its single source
    /// of truth and leaves the `set_*` bodies empty — handling both would
    /// double-apply every change.
    ///
    /// The [`state::LightSnapshot`] carries every raw ZCL value with no
    /// `Option`s: `on`, `brightness` (logical target, 0-254),
    /// `rendered_brightness` (what is being driven *right now* — ramps during
    /// fades; key visual effects off this one), `color_mode`,
    /// `hue`/`saturation` (0-254), `enhanced_hue` (0-65535),
    /// `color_x`/`color_y` (0-65535 CIE), `color_temp_mireds` (153-500).
    /// During a smooth transition it fires on every animation frame
    /// (~20-33 ms).
    ///
    /// Note: the identify blink signals through
    /// [`set_on`](LightOutput::set_on) alone and does not re-render.
    fn state_update(&mut self, _state: &state::LightSnapshot) {}
}