aquarium_control/beacon/
ws2812b.rs

1/* Copyright 2025 Uwe Martin
2
3Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
5The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
7THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8*/
9//! Provides a driver for controlling WS2812B RGB LEDs via SPI on Linux.
10//!
11//! This module serves as a high-level wrapper around the `ws2818-rgb-led-spi-driver`
12//! crate, simplifying the process of controlling a strip of WS2812B LEDs. It is
13//! specifically designed for hardware interaction and is only compiled when the
14//! `target_os` is "linux" and the `target_hw` feature is enabled.
15//!
16//! ## Key Features
17//!
18//! - **Simple API**: Offers a straightforward `new()` constructor and a `set_color()`
19//!   method to control the entire LED strip.
20//! - **Configuration Driven**: Uses a `Ws2812BConfig` struct, deserialized
21//!   from a configuration file, to specify the SPI device and the number of LEDs.
22//! - **Correct Color Encoding**: Automatically handles the common Green-Red-Blue (GRB)
23//!   byte order expected by WS2812B LEDs.
24//! - **Error Handling**: Provides a dedicated `Ws2812BError` enum for clear and
25//!   actionable error reporting during setup or operation.
26cfg_if::cfg_if! {
27    if #[cfg(all(target_os = "linux", feature = "target_hw"))] {
28        use serde_derive::Deserialize;
29        use ws2818_rgb_led_spi_driver::adapter_gen::WS28xxAdapter;
30        use ws2818_rgb_led_spi_driver::adapter_spi::WS28xxSpiAdapter;
31        use ws2818_rgb_led_spi_driver::encoding::encode_rgb;
32        use thiserror::Error;
33        use std::fmt;
34    }
35}
36
37/// Represents the primary colors for an RGB LED.
38#[cfg(all(target_os = "linux", feature = "target_hw"))]
39#[derive(Copy, Clone, Debug, PartialEq, Eq)]
40pub enum RgbLedColor {
41    /// The color Red.
42    Red,
43    /// The color Green.
44    Green,
45    /// The color Blue.
46    Blue,
47}
48
49#[cfg(all(target_os = "linux", feature = "target_hw"))]
50impl RgbLedColor {
51    /// Maps an `RgbLedColor` enum to its corresponding GRB byte representation for a WS2812B LED.
52    ///
53    /// WS2812B LEDs typically expect color data in Green-Red-Blue (GRB) order. This function
54    /// provides the correct byte array for each primary color.
55    ///
56    /// # Returns
57    /// A static reference to a 3-byte array `[G, R, B]`.
58    pub fn get_ws2812b_color_bytes(&self) -> &'static [u8; 3] {
59        match self {
60            // WS2812B is commonly GRB: Green, Red, Blue
61            // [G, R, B]
62            RgbLedColor::Red => &[0, 255, 0], // Green=0, Red=255, Blue=0
63            RgbLedColor::Green => &[255, 0, 0], // Green=255, Red=0, Blue=0
64            RgbLedColor::Blue => &[0, 0, 255], // Green=0, Red=0, Blue=255
65        }
66    }
67}
68
69#[cfg(all(target_os = "linux", feature = "target_hw"))]
70impl fmt::Display for RgbLedColor {
71    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72        match self {
73            RgbLedColor::Red => write!(f, "Red"),
74            RgbLedColor::Green => write!(f, "Green"),
75            RgbLedColor::Blue => write!(f, "Blue"),
76        }
77    }
78}
79
80/// Holds the configuration for the WS2812B RGB LED driver.
81#[derive(Deserialize)]
82#[cfg(all(target_os = "linux", feature = "target_hw"))]
83pub struct Ws2812BConfig {
84    /// The name of the SPI device file (e.g., "/dev/spidev0.0").
85    pub device_name: String,
86
87    /// The total number of LEDs on the connected strip.
88    pub number_leds: usize,
89
90    /// A flag to enable or disable the functionality of this module.
91    pub active: bool,
92}
93
94/// Defines errors that can occur within the `Ws2812B` module.
95#[cfg(all(target_os = "linux", feature = "target_hw"))]
96#[derive(Error, Debug)]
97pub enum Ws2812BError {
98    /// Failed to set up the underlying SPI adapter for the Ws2812B driver.
99    #[error("[{0}] Failed to setup adapter for Ws2812B: {1}")]
100    AdapterSetupFailure(String, String),
101
102    /// Failed to set the color of the Ws2812B LEDs.
103    #[error("[{0}] Failed to set color of Ws2812B to {1}: {2}")]
104    AdapterSetColorFailure(String, RgbLedColor, String),
105}
106
107/// A driver for controlling a strip of WS2812B RGB LEDs via an SPI interface.
108#[cfg(all(target_os = "linux", feature = "target_hw"))]
109pub struct Ws2812B {
110    /// The underlying SPI adapter from the `ws2818-rgb-led-spi-driver` crate.
111    adapter: WS28xxSpiAdapter,
112
113    /// The configuration for this driver instance.
114    config: Ws2812BConfig,
115}
116
117#[cfg(all(target_os = "linux", feature = "target_hw"))]
118impl Ws2812B {
119    /// Creates a new `Ws2812B` driver instance.
120    ///
121    /// This constructor initializes the connection to the SPI device specified in the
122    /// configuration.
123    ///
124    /// # Arguments
125    /// * `config` - The `Ws2812BConfig` containing the device name and number of LEDs.
126    ///
127    /// # Returns
128    /// A `Result` containing a new `Ws2812B` instance on success.
129    ///
130    /// # Errors
131    /// This function will return an `Err(Ws2812BError::AdapterSetupFailure)` if the
132    /// underlying SPI adapter from the `ws2818-rgb-led-spi-driver` crate fails
133    /// to initialize. This can happen if the specified SPI device file does not
134    /// exist or if there are permission issues.
135    pub fn new(config: Ws2812BConfig) -> Result<Self, Ws2812BError> {
136        let adapter = WS28xxSpiAdapter::new(&config.device_name)
137            .map_err(|e| Ws2812BError::AdapterSetupFailure(module_path!().to_string(), e))?;
138        Ok(Ws2812B { adapter, config })
139    }
140
141    /// Sets the color of all LEDs on the strip.
142    ///
143    /// This function encodes the desired `RgbLedColor` into the correct byte format
144    /// for the entire LED strip and writes the data to the SPI device.
145    ///
146    /// # Arguments
147    /// * `color` - The `RgbLedColor` to set for all LEDs.
148    ///
149    /// # Returns
150    /// An empty `Result` on successful transmission of the color data.
151    ///
152    /// # Errors
153    /// This function will return an `Err(Ws2812BError::AdapterSetColorFailure)` if
154    /// writing the encoded color data to the SPI device fails. This could be due
155    /// to an I/O error with the underlying hardware.
156    pub fn set_color(&mut self, color: RgbLedColor) -> Result<(), Ws2812BError> {
157        let raw_color_bytes = color.get_ws2812b_color_bytes(); // This is [G, R, B]
158
159        // Create a vector to hold the encoded data for all LEDs
160        // Each LED needs 3 bytes (GRB) in input, which gets expanded by the driver
161        let mut spi_encoded_rgb_bits = Vec::with_capacity(self.config.number_leds * 3);
162
163        // Repeat the color for each LED on the strip
164        for _i in 0..self.config.number_leds {
165            spi_encoded_rgb_bits.extend_from_slice(&encode_rgb(
166                raw_color_bytes[1], // Red component
167                raw_color_bytes[0], // Green component
168                raw_color_bytes[2], // Blue component
169            ));
170        }
171
172        self.adapter
173            .write_encoded_rgb(&spi_encoded_rgb_bits)
174            .map_err(|e| {
175                Ws2812BError::AdapterSetColorFailure(module_path!().to_string(), color, e)
176            })?;
177
178        Ok(())
179    }
180}
181
182#[cfg(test)]
183#[cfg(all(target_os = "linux", feature = "target_hw"))]
184pub mod tests {
185    use crate::beacon::ws2812b::{RgbLedColor, Ws2812B};
186    use crate::utilities::config::{read_config_file, ConfigData};
187    use spin_sleep::SpinSleeper;
188    use std::time::Duration;
189
190    #[test]
191    #[ignore]
192    pub fn test_ws2821b_set_colors() {
193        let config: ConfigData =
194            read_config_file("/config/aquarium_control_test_generic.toml".to_string())
195                .expect("Failed to read config in test");
196        let spin_sleeper = SpinSleeper::default();
197        let sleep_duration = Duration::from_millis(500);
198        let mut ws2812b = Ws2812B::new(config.ws2812b).expect("Failed to create Ws2812B in test");
199
200        ws2812b
201            .set_color(RgbLedColor::Green)
202            .expect("Failed to set color to Green");
203        spin_sleeper.sleep(sleep_duration);
204
205        ws2812b
206            .set_color(RgbLedColor::Blue)
207            .expect("Failed to set color to Blue");
208        spin_sleeper.sleep(sleep_duration);
209
210        ws2812b
211            .set_color(RgbLedColor::Red)
212            .expect("Failed to set color to Red");
213    }
214}