feat: Add RGBW LED support and hardware communication protocol

- Add RGBW LED type support alongside existing RGB LEDs
- Implement 4-channel RGBW data transmission (R,G,B,W bytes)
- Add RGBW visual preview with half-color, half-white gradient display
- Fix RGB color calibration bug in publisher (was not being applied)
- Create comprehensive hardware communication protocol documentation
- Support mixed RGB/RGBW LED strips on same display
- Add W channel color temperature adjustment in white balance page
- Hardware acts as simple UDP-to-WS2812 bridge without type distinction
This commit is contained in:
2025-07-05 02:46:31 +08:00
parent 5de105960b
commit 99cbaf3b9f
10 changed files with 392 additions and 20 deletions

View File

@ -16,6 +16,18 @@ pub enum Border {
Right,
}
#[derive(Clone, Copy, Serialize, Deserialize, Debug, PartialEq)]
pub enum LedType {
RGB,
RGBW,
}
impl Default for LedType {
fn default() -> Self {
LedType::RGB
}
}
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
pub struct LedStripConfig {
pub index: usize,
@ -23,6 +35,8 @@ pub struct LedStripConfig {
pub display_id: u32,
pub start_pos: usize,
pub len: usize,
#[serde(default)]
pub led_type: LedType,
}
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
@ -30,6 +44,12 @@ pub struct ColorCalibration {
r: f32,
g: f32,
b: f32,
#[serde(default = "default_w_value")]
w: f32,
}
fn default_w_value() -> f32 {
1.0
}
impl ColorCalibration {
@ -40,6 +60,15 @@ impl ColorCalibration {
(self.b * 255.0) as u8,
]
}
pub fn to_bytes_rgbw(&self) -> [u8; 4] {
[
(self.r * 255.0) as u8,
(self.g * 255.0) as u8,
(self.b * 255.0) as u8,
(self.w * 255.0) as u8,
]
}
}
#[derive(Clone, Serialize, Deserialize, Debug)]
@ -122,6 +151,7 @@ impl LedStripConfigGroup {
},
start_pos: j + i * 4 * 30,
len: 30,
led_type: LedType::RGB,
};
configs.push(item);
strips.push(item);
@ -136,6 +166,7 @@ impl LedStripConfigGroup {
r: 1.0,
g: 1.0,
b: 1.0,
w: 1.0,
};
Ok(Self {

View File

@ -5,7 +5,7 @@ use tokio::{sync::OnceCell, task::yield_now};
use crate::ambient_light::{config, LedStripConfigGroup};
use super::{Border, SamplePointMapper, ColorCalibration};
use super::{Border, SamplePointMapper, ColorCalibration, LedType};
pub struct ConfigManager {
config: Arc<RwLock<LedStripConfigGroup>>,
@ -94,6 +94,33 @@ impl ConfigManager {
Ok(())
}
pub async fn patch_led_strip_type(
&self,
display_id: u32,
border: Border,
led_type: LedType,
) -> anyhow::Result<()> {
let mut config = self.config.write().await;
for strip in config.strips.iter_mut() {
if strip.display_id == display_id && strip.border == border {
strip.led_type = led_type;
}
}
let cloned_config = config.clone();
drop(config);
self.update(&cloned_config).await?;
self.config_update_sender
.send(cloned_config)
.map_err(|e| anyhow::anyhow!("Failed to send config update: {}", e))?;
Ok(())
}
pub async fn move_strip_part(
&self,
display_id: u32,

View File

@ -18,7 +18,7 @@ use crate::{
use itertools::Itertools;
use super::{LedStripConfigGroup, SamplePointMapper};
use super::{LedStripConfigGroup, SamplePointMapper, LedStripConfig, ColorCalibration, LedType};
pub struct LedColorsPublisher {
sorted_colors_rx: Arc<RwLock<watch::Receiver<Vec<u8>>>>,
@ -56,6 +56,8 @@ impl LedColorsPublisher {
bound_scale_factor: f32,
mappers: Vec<SamplePointMapper>,
display_colors_tx: broadcast::Sender<(u32, Vec<u8>)>,
strips: Vec<LedStripConfig>,
color_calibration: ColorCalibration,
) {
let internal_tasks_version = self.inner_tasks_version.clone();
let screenshot_manager = ScreenshotManager::global().await;
@ -79,7 +81,7 @@ impl LedColorsPublisher {
let mappers = mappers.clone();
match Self::send_colors_by_display(colors, mappers).await {
match Self::send_colors_by_display(colors, mappers, &strips, &color_calibration).await {
Ok(_) => {
// log::info!("sent colors: #{: >15}", display_id);
}
@ -209,9 +211,9 @@ impl LedColorsPublisher {
}
}
async fn handle_config_change(&self, configs: LedStripConfigGroup) {
async fn handle_config_change(&self, original_configs: LedStripConfigGroup) {
let inner_tasks_version = self.inner_tasks_version.clone();
let configs = Self::get_colors_configs(&configs).await;
let configs = Self::get_colors_configs(&original_configs).await;
if let Err(err) = configs {
warn!("Failed to get configs: {}", err);
@ -231,12 +233,22 @@ impl LedColorsPublisher {
let display_id = sample_point_group.display_id;
let sample_points = sample_point_group.points;
let bound_scale_factor = sample_point_group.bound_scale_factor;
// Get strips for this display
let display_strips: Vec<LedStripConfig> = original_configs.strips
.iter()
.filter(|strip| strip.display_id == display_id)
.cloned()
.collect();
self.start_one_display_colors_fetcher(
display_id,
sample_points,
bound_scale_factor,
sample_point_group.mappers,
display_colors_tx.clone(),
display_strips,
original_configs.color_calibration,
)
.await;
}
@ -266,6 +278,8 @@ impl LedColorsPublisher {
pub async fn send_colors_by_display(
colors: Vec<LedColor>,
mappers: Vec<SamplePointMapper>,
strips: &[LedStripConfig],
color_calibration: &ColorCalibration,
) -> anyhow::Result<()> {
// let color_len = colors.len();
let display_led_offset = mappers
@ -282,7 +296,7 @@ impl LedColorsPublisher {
let udp_rpc = udp_rpc.as_ref().unwrap();
// let socket = UdpSocket::bind("0.0.0.0:0").await?;
for group in mappers.clone() {
for (group_index, group) in mappers.clone().iter().enumerate() {
if (group.start.abs_diff(group.end)) > colors.len() {
return Err(anyhow::anyhow!(
"get_sorted_colors: color_index out of range. color_index: {}, strip len: {}, colors.len(): {}",
@ -293,7 +307,20 @@ impl LedColorsPublisher {
}
let group_size = group.start.abs_diff(group.end);
let mut buffer = Vec::<u8>::with_capacity(group_size * 3);
// Find the corresponding LED strip config to get LED type
let led_type = if group_index < strips.len() {
strips[group_index].led_type
} else {
LedType::RGB // fallback to RGB
};
let bytes_per_led = match led_type {
LedType::RGB => 3,
LedType::RGBW => 4,
};
let mut buffer = Vec::<u8>::with_capacity(group_size * bytes_per_led);
if group.end > group.start {
// Prevent integer underflow by using saturating subtraction
@ -310,12 +337,37 @@ impl LedColorsPublisher {
for i in start_index..end_index {
if i < colors.len() {
let bytes = colors[i].as_bytes();
buffer.append(&mut bytes.to_vec());
let bytes = match led_type {
LedType::RGB => {
let calibration_bytes = color_calibration.to_bytes();
let color_bytes = colors[i].as_bytes();
// Apply calibration to RGB values
vec![
((color_bytes[0] as f32 * calibration_bytes[0] as f32 / 255.0) as u8),
((color_bytes[1] as f32 * calibration_bytes[1] as f32 / 255.0) as u8),
((color_bytes[2] as f32 * calibration_bytes[2] as f32 / 255.0) as u8),
]
}
LedType::RGBW => {
let calibration_bytes = color_calibration.to_bytes_rgbw();
let color_bytes = colors[i].as_bytes();
// Apply calibration to RGB values and use calibrated W
vec![
((color_bytes[0] as f32 * calibration_bytes[0] as f32 / 255.0) as u8),
((color_bytes[1] as f32 * calibration_bytes[1] as f32 / 255.0) as u8),
((color_bytes[2] as f32 * calibration_bytes[2] as f32 / 255.0) as u8),
calibration_bytes[3], // W channel
]
}
};
buffer.extend_from_slice(&bytes);
} else {
log::warn!("Index {} out of bounds for colors array of length {}", i, colors.len());
// Add black color as fallback
buffer.append(&mut vec![0, 0, 0]);
match led_type {
LedType::RGB => buffer.extend_from_slice(&[0, 0, 0]),
LedType::RGBW => buffer.extend_from_slice(&[0, 0, 0, 0]),
}
}
}
} else {
@ -333,12 +385,37 @@ impl LedColorsPublisher {
for i in (start_index..end_index).rev() {
if i < colors.len() {
let bytes = colors[i].as_bytes();
buffer.append(&mut bytes.to_vec());
let bytes = match led_type {
LedType::RGB => {
let calibration_bytes = color_calibration.to_bytes();
let color_bytes = colors[i].as_bytes();
// Apply calibration to RGB values
vec![
((color_bytes[0] as f32 * calibration_bytes[0] as f32 / 255.0) as u8),
((color_bytes[1] as f32 * calibration_bytes[1] as f32 / 255.0) as u8),
((color_bytes[2] as f32 * calibration_bytes[2] as f32 / 255.0) as u8),
]
}
LedType::RGBW => {
let calibration_bytes = color_calibration.to_bytes_rgbw();
let color_bytes = colors[i].as_bytes();
// Apply calibration to RGB values and use calibrated W
vec![
((color_bytes[0] as f32 * calibration_bytes[0] as f32 / 255.0) as u8),
((color_bytes[1] as f32 * calibration_bytes[1] as f32 / 255.0) as u8),
((color_bytes[2] as f32 * calibration_bytes[2] as f32 / 255.0) as u8),
calibration_bytes[3], // W channel
]
}
};
buffer.extend_from_slice(&bytes);
} else {
log::warn!("Index {} out of bounds for colors array of length {}", i, colors.len());
// Add black color as fallback
buffer.append(&mut vec![0, 0, 0]);
match led_type {
LedType::RGB => buffer.extend_from_slice(&[0, 0, 0]),
LedType::RGBW => buffer.extend_from_slice(&[0, 0, 0, 0]),
}
}
}
}

View File

@ -43,6 +43,10 @@ impl LedColor {
pub fn as_bytes (&self) -> [u8; 3] {
self.0
}
pub fn as_bytes_rgbw(&self, w: u8) -> [u8; 4] {
[self.0[0], self.0[1], self.0[2], w]
}
}
impl Serialize for LedColor {

View File

@ -10,7 +10,7 @@ mod screenshot_manager;
mod screen_stream;
mod volume;
use ambient_light::{Border, ColorCalibration, LedStripConfig, LedStripConfigGroup};
use ambient_light::{Border, ColorCalibration, LedStripConfig, LedStripConfigGroup, LedType};
use display::{DisplayManager, DisplayState};
use display_info::DisplayInfo;
use paris::{error, info, warn};
@ -138,6 +138,25 @@ async fn patch_led_strip_len(display_id: u32, border: Border, delta_len: i8) ->
Ok(())
}
#[tauri::command]
async fn patch_led_strip_type(display_id: u32, border: Border, led_type: LedType) -> Result<(), String> {
info!(
"patch_led_strip_type: {} {:?} {:?}",
display_id, border, led_type
);
let config_manager = ambient_light::ConfigManager::global().await;
config_manager
.patch_led_strip_type(display_id, border, led_type)
.await
.map_err(|e| {
error!("can not patch led strip type: {}", e);
e.to_string()
})?;
info!("patch_led_strip_type: ok");
Ok(())
}
#[tauri::command]
async fn send_colors(offset: u16, buffer: Vec<u8>) -> Result<(), String> {
ambient_light::LedColorsPublisher::send_colors(offset, buffer)
@ -383,6 +402,7 @@ async fn main() {
get_led_strips_sample_points,
get_one_edge_colors,
patch_led_strip_len,
patch_led_strip_type,
send_colors,
move_strip_part,
reverse_led_strip_part,