sn3218-hal 0.2.0

Embedded Rust driver for SN3218 18-channel LED controller with gamma correction
Documentation
//! SN3218 [`embedded-hal`] driver for 18-channel LED driver SN3218
//!
//! The SN3218 is an 18-channel LED driver with PWM control, commonly arranged as 6 RGB channels.
//! This LED driver is used on the Raspberry Pi GFX HAT by Pimoroni and other SN3218-based boards.
//!
//! # Features
//!
//! - 18-channel PWM LED control (typically 6 RGB LEDs)
//! - Automatic gamma correction for natural brightness curves
//! - Simple enable/disable control
//! - Mask-based channel selection
//! - Built on `embedded-hal` for platform independence
//!
//! # Examples
//!
//! ```no_run
//! use sn3218::SN3218;
//! # use embedded_hal_mock::i2c::Mock as I2c;
//! # let expectations = [];
//! # let i2c = I2c::new(&expectations);
//!
//! let mut led_driver = SN3218::new(i2c);
//!
//! // Enable output and all LEDs
//! led_driver.enable().unwrap();
//! led_driver.enable_leds(0x3FFFF).unwrap(); // All 18 channels
//!
//! // Set RGB values for 6 LEDs (18 channels total)
//! let values = [
//!     255, 0, 0,     // LED 0: Red
//!     0, 255, 0,     // LED 1: Green
//!     0, 0, 255,     // LED 2: Blue
//!     255, 255, 0,   // LED 3: Yellow
//!     255, 0, 255,   // LED 4: Magenta
//!     0, 255, 255,   // LED 5: Cyan
//! ];
//! led_driver.output(&values).unwrap();
//! ```
//!
//! # Channel Layout
//!
//! The 18 channels are organized as 6 RGB LEDs:
//! - LED 0: channels 0 (R), 1 (G), 2 (B)
//! - LED 1: channels 3 (R), 4 (G), 5 (B)
//! - LED 2: channels 6 (R), 7 (G), 8 (B)
//! - LED 3: channels 9 (R), 10 (G), 11 (B)
//! - LED 4: channels 12 (R), 13 (G), 14 (B)
//! - LED 5: channels 15 (R), 16 (G), 17 (B)
//!
//! Based on the Python implementation: <https://github.com/pimoroni/sn3218>
//!

use embedded_hal::i2c::I2c;

/// SN3218 LED driver instance
///
/// Controls an 18-channel SN3218 LED driver over I2C. The driver includes
/// automatic gamma correction for natural brightness perception.
///
/// # Type Parameters
///
/// * `T` - An I2C implementation that satisfies the `embedded_hal::i2c::I2c` trait
pub struct SN3218<T: I2c> {
    i2c: T,
    gamma_table: [u8; 256],
}

const I2C_ADDRESS: u8 = 0x54;
const CMD_ENABLE_OUTPUT: u8 = 0x00;
const CMD_SET_PWM_VALUES: u8 = 0x01;
const CMD_ENABLE_LEDS: u8 = 0x13;
const CMD_UPDATE: u8 = 0x16;
const CMD_RESET: u8 = 0x17;

const BUF_CMD_ENABLE_ENABLE: [u8; 1] = [0x01];
const BUF_CMD_ENABLE_DISABLE: [u8; 1] = [0x00];
const BUF_CMD_255: [u8; 1] = [0xFF];

impl<T: I2c> SN3218<T> {
    /// Creates a new SN3218 driver instance
    ///
    /// Initializes the driver with automatic gamma correction. The gamma table
    /// is pre-calculated using the formula: `255^(x/255)` for natural brightness curves.
    ///
    /// # Parameters
    ///
    /// * `i2c` - An I2C interface implementation
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use sn3218::SN3218;
    /// # use embedded_hal_mock::i2c::Mock as I2c;
    /// # let expectations = [];
    /// # let i2c = I2c::new(&expectations);
    ///
    /// let led_driver = SN3218::new(i2c);
    /// ```
    pub fn new(i2c: T) -> Self {
        let mut gamma_table: [u8; 256] = [0; 256];
        for i in 0..256 {
            gamma_table[i] = (255f64.powf(i as f64 / 255f64)) as u8;
        }
        Self { i2c, gamma_table }
    }

    /// Enables the LED driver output
    ///
    /// This must be called before the LEDs will produce any light.
    /// Use [`disable`](Self::disable) to turn off all outputs.
    ///
    /// # Errors
    ///
    /// Returns the I2C implementation's error type if communication fails.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// # use sn3218::SN3218;
    /// # use embedded_hal_mock::i2c::Mock as I2c;
    /// # let expectations = [];
    /// # let i2c = I2c::new(&expectations);
    /// let mut driver = SN3218::new(i2c);
    /// driver.enable().unwrap();
    /// ```
    pub fn enable(&mut self) -> Result<(), T::Error> {
        self.i2c
            .cmd_write(CMD_ENABLE_OUTPUT, &BUF_CMD_ENABLE_ENABLE)
    }

    /// Disables the LED driver output
    ///
    /// Turns off all LED outputs immediately. The PWM values and enabled
    /// channels are preserved and will resume when [`enable`](Self::enable) is called.
    ///
    /// # Errors
    ///
    /// Returns the I2C implementation's error type if communication fails.
    pub fn disable(&mut self) -> Result<(), T::Error> {
        self.i2c
            .cmd_write(CMD_ENABLE_OUTPUT, &BUF_CMD_ENABLE_DISABLE)
    }

    /// Resets the LED driver to its default state
    ///
    /// Clears all PWM values and LED enable states. After reset, you'll need to
    /// call [`enable`](Self::enable) and [`enable_leds`](Self::enable_leds) again.
    ///
    /// # Errors
    ///
    /// Returns the I2C implementation's error type if communication fails.
    pub fn reset(&mut self) -> Result<(), T::Error> {
        self.i2c.cmd_write(CMD_RESET, &BUF_CMD_255)
    }

    /// Enables specific LED channels using a bitmask
    ///
    /// Each bit in the mask corresponds to one of the 18 LED channels.
    /// Only enabled channels will produce light when PWM values are set.
    ///
    /// # Parameters
    ///
    /// * `mask` - 18-bit mask where each bit enables the corresponding channel
    ///
    /// # Examples
    ///
    /// ```no_run
    /// # use sn3218::SN3218;
    /// # use embedded_hal_mock::i2c::Mock as I2c;
    /// # let expectations = [];
    /// # let i2c = I2c::new(&expectations);
    /// let mut driver = SN3218::new(i2c);
    ///
    /// // Enable all 18 channels
    /// driver.enable_leds(0x3FFFF).unwrap();
    ///
    /// // Enable only the first RGB LED (channels 0, 1, 2)
    /// driver.enable_leds(0x07).unwrap();
    ///
    /// // Enable only red channels (0, 3, 6, 9, 12, 15)
    /// driver.enable_leds(0b001001001001001001).unwrap();
    /// ```
    ///
    /// # Errors
    ///
    /// Returns the I2C implementation's error type if communication fails.
    pub fn enable_leds(&mut self, mask: u32) -> Result<(), T::Error> {
        let buf = [
            (mask & 0x3F) as u8,
            ((mask >> 6) & 0x3F) as u8,
            ((mask >> 12) & 0x3F) as u8,
        ];
        self.i2c.cmd_write(CMD_ENABLE_LEDS, &buf)?;
        self.i2c.cmd_write(CMD_UPDATE, &BUF_CMD_255)
    }

    /// Sets PWM values for all 18 LED channels
    ///
    /// Values are automatically gamma-corrected for natural brightness perception.
    /// The array must contain exactly 18 elements, typically arranged as 6 RGB triplets.
    ///
    /// # Parameters
    ///
    /// * `values` - Array of 18 PWM values (0-255) for each channel
    ///
    /// # Panics
    ///
    /// Panics if the values array is not exactly 18 elements long.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// # use sn3218::SN3218;
    /// # use embedded_hal_mock::i2c::Mock as I2c;
    /// # let expectations = [];
    /// # let i2c = I2c::new(&expectations);
    /// let mut driver = SN3218::new(i2c);
    ///
    /// // Set RGB values for 6 LEDs
    /// let values = [
    ///     255, 0, 0,     // LED 0: Full red
    ///     0, 255, 0,     // LED 1: Full green
    ///     0, 0, 255,     // LED 2: Full blue
    ///     128, 128, 128, // LED 3: Half brightness white
    ///     255, 128, 64,  // LED 4: Orange
    ///     0, 0, 0,       // LED 5: Off
    /// ];
    /// driver.output(&values).unwrap();
    /// ```
    ///
    /// # Errors
    ///
    /// Returns the I2C implementation's error type if communication fails.
    pub fn output(&mut self, values: &[u8]) -> Result<(), T::Error> {
        if values.len() != 18 {
            panic!(
                "values array must be exactly 18 elements long (got {})",
                values.len()
            );
        }
        let mut buf = [0u8; 18];
        for i in 0..18 {
            buf[i] = self.gamma_table[values[i] as usize];
        }
        self.i2c.cmd_write(CMD_SET_PWM_VALUES, &buf)?;
        self.i2c.cmd_write(CMD_UPDATE, &BUF_CMD_255)
    }
}

/// Internal trait for sending commands to the SN3218 device
///
/// This trait extends the I2C interface with a helper method for sending
/// commands with data to the SN3218 chip.
trait SN3218CmdWrite<T: I2c> {
    /// Writes a command followed by data to the SN3218
    fn cmd_write(&mut self, command: u8, buf: &[u8]) -> Result<(), T::Error>;
}

impl<T: I2c> SN3218CmdWrite<T> for T {
    /// Sends a command byte followed by data bytes to the SN3218
    ///
    /// This method constructs a message with the command byte first,
    /// followed by the provided data buffer, and sends it to the SN3218's
    /// I2C address (0x54).
    fn cmd_write(&mut self, command: u8, buffer: &[u8]) -> Result<(), T::Error> {
        let to_send: Vec<u8> = [command].iter().chain(buffer).copied().collect();
        self.write(I2C_ADDRESS, &to_send)
    }
}