diff --git a/docs/hardware-protocol.md b/docs/hardware-protocol.md new file mode 100644 index 0000000..242fa41 --- /dev/null +++ b/docs/hardware-protocol.md @@ -0,0 +1,140 @@ +# LED Hardware Communication Protocol + +## Overview + +UDP-based protocol for sending LED color data from desktop application to ambient light hardware boards. The hardware acts as a simple UDP-to-WS2812 bridge, directly forwarding color data without any processing or LED type distinction. + +## Connection + +- **Protocol**: UDP +- **Port**: 23042 +- **Discovery**: mDNS (`_ambient_light._udp.local.`) +- **Example Board**: `192.168.31.206:23042` + +## Packet Format + +``` +Byte 0: Header (0x02) +Byte 1: Offset High (upper 8 bits of LED start position) +Byte 2: Offset Low (lower 8 bits of LED start position) +Byte 3+: LED Color Data (variable length) +``` + +## LED Color Data + +### RGB LEDs (3 bytes per LED) + +``` +[R][G][B][R][G][B][R][G][B]... +``` + +### RGBW LEDs (4 bytes per LED) + +``` +[R][G][B][W][R][G][B][W][R][G][B][W]... +``` + +All values are 0-255. + +## Color Calibration + +Colors are calibrated before transmission: + +**RGB:** + +```rust +calibrated_r = (original_r * calibration_r) / 255 +calibrated_g = (original_g * calibration_g) / 255 +calibrated_b = (original_b * calibration_b) / 255 +``` + +**RGBW:** + +```rust +calibrated_r = (original_r * calibration_r) / 255 +calibrated_g = (original_g * calibration_g) / 255 +calibrated_b = (original_b * calibration_b) / 255 +calibrated_w = calibration_w // Direct value +``` + +## Packet Examples + +### RGB Example + +3 RGB LEDs starting at position 0: Red, Green, Blue + +``` +02 00 00 FF 00 00 00 FF 00 00 00 FF +│ │ │ └─────────────────────────── 9 bytes color data +│ │ └─ Offset Low (0) +│ └─ Offset High (0) +└─ Header (0x02) +``` + +### RGBW Example + +2 RGBW LEDs starting at position 10: White, Warm White + +``` +02 00 0A FF FF FF FF FF C8 96 C8 +│ │ │ └─────────────────────── 8 bytes color data +│ │ └─ Offset Low (10) +│ └─ Offset High (0) +└─ Header (0x02) +``` + +## Implementation Notes + +- **Byte Order**: Big-endian for multi-byte values (offset field) +- **Delivery**: Fire-and-forget UDP (no acknowledgment required) +- **Hardware Role**: Simple UDP-to-WS2812 bridge, no data processing +- **LED Type Logic**: Handled entirely on desktop side, not hardware +- **Mixed Types**: Same display can have both RGB and RGBW strips +- **Data Flow**: Desktop → UDP → Hardware → WS2812 (direct forward) + +## Hardware Implementation + +The hardware board acts as a simple UDP-to-WS2812 bridge, directly forwarding color data to the LED strips without any processing or type distinction. + +### Packet Processing + +1. **Validation**: Check minimum 3 bytes and header (0x02) +2. **Extract Offset**: Parse 16-bit LED start position +3. **Forward Data**: Send color data directly to WS2812 controller +4. **No Type Logic**: Hardware doesn't distinguish RGB/RGBW - just forwards bytes + +### Example C Code + +```c +void process_packet(uint8_t* data, size_t len) { + if (len < 3 || data[0] != 0x02) return; + + uint16_t offset = (data[1] << 8) | data[2]; + uint8_t* color_data = &data[3]; + size_t color_len = len - 3; + + // Direct forward to WS2812 - no RGB/RGBW distinction needed + ws2812_update(offset, color_data, color_len); +} +``` + +### Key Simplifications + +- **No LED Type Detection**: Hardware doesn't need to know RGB vs RGBW +- **Direct Data Forward**: Color bytes sent as-is to WS2812 controller +- **Desktop Handles Logic**: All RGB/RGBW processing done on desktop side +- **Simple Bridge**: Hardware is just a UDP-to-WS2812 data bridge + +## Troubleshooting + +**No Updates**: Check network connectivity, mDNS discovery, port 23042 +**Wrong Colors**: Verify calibration settings on desktop application +**Flickering**: Monitor packet rate, network congestion, power supply +**Partial Updates**: Check strip configuration, offset calculations +**Hardware Issues**: Verify WS2812 wiring, power supply, data signal integrity + +## Protocol Version + +- **Current**: 1.0 +- **Header**: 0x02 +- **Future**: Different headers for backward compatibility diff --git a/src-tauri/src/ambient_light/config.rs b/src-tauri/src/ambient_light/config.rs index 73eeba8..989aac8 100644 --- a/src-tauri/src/ambient_light/config.rs +++ b/src-tauri/src/ambient_light/config.rs @@ -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 { diff --git a/src-tauri/src/ambient_light/config_manager.rs b/src-tauri/src/ambient_light/config_manager.rs index 2d5a0eb..563e6f0 100644 --- a/src-tauri/src/ambient_light/config_manager.rs +++ b/src-tauri/src/ambient_light/config_manager.rs @@ -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>, @@ -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, diff --git a/src-tauri/src/ambient_light/publisher.rs b/src-tauri/src/ambient_light/publisher.rs index ef27d47..bea78d8 100644 --- a/src-tauri/src/ambient_light/publisher.rs +++ b/src-tauri/src/ambient_light/publisher.rs @@ -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>>>, @@ -56,6 +56,8 @@ impl LedColorsPublisher { bound_scale_factor: f32, mappers: Vec, display_colors_tx: broadcast::Sender<(u32, Vec)>, + strips: Vec, + 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 = 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, mappers: Vec, + 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::::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::::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]), + } } } } diff --git a/src-tauri/src/led_color.rs b/src-tauri/src/led_color.rs index 87d49dc..2593ae7 100644 --- a/src-tauri/src/led_color.rs +++ b/src-tauri/src/led_color.rs @@ -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 { diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 26941b7..45de72a 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -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) -> 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, diff --git a/src/components/led-strip-configuration/led-count-control-panel.tsx b/src/components/led-strip-configuration/led-count-control-panel.tsx index ed9d804..721efc2 100644 --- a/src/components/led-strip-configuration/led-count-control-panel.tsx +++ b/src/components/led-strip-configuration/led-count-control-panel.tsx @@ -3,6 +3,7 @@ import { Component, createMemo, For, JSX, splitProps } from 'solid-js'; import { DisplayInfo } from '../../models/display-info.model'; import { ledStripStore } from '../../stores/led-strip.store'; import { Borders } from '../../constants/border'; +import { LedType } from '../../models/led-strip-config'; type LedCountControlItemProps = { displayId: number; @@ -45,7 +46,7 @@ const LedCountControlItem: Component = (props) => { const target = e.target as HTMLInputElement; const newValue = parseInt(target.value); const currentLen = config()?.len || 0; - + if (!isNaN(newValue) && newValue >= 0 && newValue <= 1000) { const deltaLen = newValue - currentLen; if (deltaLen !== 0) { @@ -65,6 +66,19 @@ const LedCountControlItem: Component = (props) => { } }; + const handleLedTypeChange = (e: Event) => { + const target = e.target as HTMLSelectElement; + const newType = target.value as LedType; + + invoke('patch_led_strip_type', { + displayId: props.displayId, + border: props.border, + ledType: newType, + }).catch((e) => { + console.error(e); + }); + }; + return (
@@ -107,6 +121,18 @@ const LedCountControlItem: Component = (props) => { +
+ +
+ +
); diff --git a/src/components/led-strip-configuration/led-strip-part.tsx b/src/components/led-strip-configuration/led-strip-part.tsx index 1ce62c6..0f1a890 100644 --- a/src/components/led-strip-configuration/led-strip-part.tsx +++ b/src/components/led-strip-configuration/led-strip-part.tsx @@ -74,6 +74,8 @@ export const LedStripPart: Component = (props) => { return; } + // Frontend always uses RGB data (3 bytes per LED) for preview + // The backend sends RGB data to frontend regardless of LED type const offset = mapper.start * 3; console.log('🎨 LED: Updating colors', { @@ -124,7 +126,19 @@ export const LedStripPart: Component = (props) => { }, }); - + const onWheel = (e: WheelEvent) => { + if (localProps.config) { + invoke('patch_led_strip_len', { + displayId: localProps.config.display_id, + border: localProps.config.border, + deltaLen: e.deltaY > 0 ? 1 : -1, + }) + .then(() => {}) + .catch((e) => { + console.error(e); + }); + } + }; return (
= (props) => { stripConfiguration.selectedStripPart?.displayId === localProps.config?.display_id, }} - + onWheel={onWheel} > {(item) => }
diff --git a/src/components/white-balance/white-balance.tsx b/src/components/white-balance/white-balance.tsx index 7bc597f..a347242 100644 --- a/src/components/white-balance/white-balance.tsx +++ b/src/components/white-balance/white-balance.tsx @@ -266,10 +266,19 @@ export const WhiteBalance = () => {
- + + updateColorCalibration( + 'w', + (ev.target as HTMLInputElement).valueAsNumber ?? 1, + ) + } + />
@@ -400,6 +409,23 @@ export const WhiteBalance = () => { /> +
+ + + updateColorCalibration( + 'w', + (ev.target as HTMLInputElement).valueAsNumber ?? 1, + ) + } + /> +
+