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}