2 Commits

Author SHA1 Message Date
90cace679b Implement synchronized LED strip highlighting with theme colors and clean up debug logs
- Add three-way synchronized highlighting between LED strip components
- Implement hover and selection state synchronization across display borders, sorter, and control panels
- Replace hardcoded colors with DaisyUI theme colors (primary, warning, base-content)
- Use background highlighting for sorter to prevent interface jittering
- Reduce LED strip width from 24px to 20px for better visual appearance
- Clean up console.log statements and debug output for production readiness
- Maintain layout stability by avoiding size changes in highlighting effects
2025-07-05 14:32:31 +08:00
99cbaf3b9f 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
2025-07-05 02:46:31 +08:00
15 changed files with 467 additions and 107 deletions

140
docs/hardware-protocol.md Normal file
View File

@@ -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

View File

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

View File

@@ -5,7 +5,7 @@ use tokio::{sync::OnceCell, task::yield_now};
use crate::ambient_light::{config, LedStripConfigGroup}; use crate::ambient_light::{config, LedStripConfigGroup};
use super::{Border, SamplePointMapper, ColorCalibration}; use super::{Border, SamplePointMapper, ColorCalibration, LedType};
pub struct ConfigManager { pub struct ConfigManager {
config: Arc<RwLock<LedStripConfigGroup>>, config: Arc<RwLock<LedStripConfigGroup>>,
@@ -94,6 +94,33 @@ impl ConfigManager {
Ok(()) 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( pub async fn move_strip_part(
&self, &self,
display_id: u32, display_id: u32,

View File

@@ -18,7 +18,7 @@ use crate::{
use itertools::Itertools; use itertools::Itertools;
use super::{LedStripConfigGroup, SamplePointMapper}; use super::{LedStripConfigGroup, SamplePointMapper, LedStripConfig, ColorCalibration, LedType};
pub struct LedColorsPublisher { pub struct LedColorsPublisher {
sorted_colors_rx: Arc<RwLock<watch::Receiver<Vec<u8>>>>, sorted_colors_rx: Arc<RwLock<watch::Receiver<Vec<u8>>>>,
@@ -56,6 +56,8 @@ impl LedColorsPublisher {
bound_scale_factor: f32, bound_scale_factor: f32,
mappers: Vec<SamplePointMapper>, mappers: Vec<SamplePointMapper>,
display_colors_tx: broadcast::Sender<(u32, Vec<u8>)>, display_colors_tx: broadcast::Sender<(u32, Vec<u8>)>,
strips: Vec<LedStripConfig>,
color_calibration: ColorCalibration,
) { ) {
let internal_tasks_version = self.inner_tasks_version.clone(); let internal_tasks_version = self.inner_tasks_version.clone();
let screenshot_manager = ScreenshotManager::global().await; let screenshot_manager = ScreenshotManager::global().await;
@@ -79,7 +81,7 @@ impl LedColorsPublisher {
let mappers = mappers.clone(); 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(_) => { Ok(_) => {
// log::info!("sent colors: #{: >15}", display_id); // 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 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 { if let Err(err) = configs {
warn!("Failed to get configs: {}", err); warn!("Failed to get configs: {}", err);
@@ -231,12 +233,22 @@ impl LedColorsPublisher {
let display_id = sample_point_group.display_id; let display_id = sample_point_group.display_id;
let sample_points = sample_point_group.points; let sample_points = sample_point_group.points;
let bound_scale_factor = sample_point_group.bound_scale_factor; 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( self.start_one_display_colors_fetcher(
display_id, display_id,
sample_points, sample_points,
bound_scale_factor, bound_scale_factor,
sample_point_group.mappers, sample_point_group.mappers,
display_colors_tx.clone(), display_colors_tx.clone(),
display_strips,
original_configs.color_calibration,
) )
.await; .await;
} }
@@ -266,6 +278,8 @@ impl LedColorsPublisher {
pub async fn send_colors_by_display( pub async fn send_colors_by_display(
colors: Vec<LedColor>, colors: Vec<LedColor>,
mappers: Vec<SamplePointMapper>, mappers: Vec<SamplePointMapper>,
strips: &[LedStripConfig],
color_calibration: &ColorCalibration,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
// let color_len = colors.len(); // let color_len = colors.len();
let display_led_offset = mappers let display_led_offset = mappers
@@ -282,7 +296,7 @@ impl LedColorsPublisher {
let udp_rpc = udp_rpc.as_ref().unwrap(); let udp_rpc = udp_rpc.as_ref().unwrap();
// let socket = UdpSocket::bind("0.0.0.0:0").await?; // 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() { if (group.start.abs_diff(group.end)) > colors.len() {
return Err(anyhow::anyhow!( return Err(anyhow::anyhow!(
"get_sorted_colors: color_index out of range. color_index: {}, strip len: {}, colors.len(): {}", "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 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 { if group.end > group.start {
// Prevent integer underflow by using saturating subtraction // Prevent integer underflow by using saturating subtraction
@@ -310,12 +337,37 @@ impl LedColorsPublisher {
for i in start_index..end_index { for i in start_index..end_index {
if i < colors.len() { if i < colors.len() {
let bytes = colors[i].as_bytes(); let bytes = match led_type {
buffer.append(&mut bytes.to_vec()); 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 { } else {
log::warn!("Index {} out of bounds for colors array of length {}", i, colors.len()); log::warn!("Index {} out of bounds for colors array of length {}", i, colors.len());
// Add black color as fallback // 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 { } else {
@@ -333,12 +385,37 @@ impl LedColorsPublisher {
for i in (start_index..end_index).rev() { for i in (start_index..end_index).rev() {
if i < colors.len() { if i < colors.len() {
let bytes = colors[i].as_bytes(); let bytes = match led_type {
buffer.append(&mut bytes.to_vec()); 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 { } else {
log::warn!("Index {} out of bounds for colors array of length {}", i, colors.len()); log::warn!("Index {} out of bounds for colors array of length {}", i, colors.len());
// Add black color as fallback // 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] { pub fn as_bytes (&self) -> [u8; 3] {
self.0 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 { impl Serialize for LedColor {

View File

@@ -10,7 +10,7 @@ mod screenshot_manager;
mod screen_stream; mod screen_stream;
mod volume; mod volume;
use ambient_light::{Border, ColorCalibration, LedStripConfig, LedStripConfigGroup}; use ambient_light::{Border, ColorCalibration, LedStripConfig, LedStripConfigGroup, LedType};
use display::{DisplayManager, DisplayState}; use display::{DisplayManager, DisplayState};
use display_info::DisplayInfo; use display_info::DisplayInfo;
use paris::{error, info, warn}; use paris::{error, info, warn};
@@ -138,6 +138,25 @@ async fn patch_led_strip_len(display_id: u32, border: Border, delta_len: i8) ->
Ok(()) 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] #[tauri::command]
async fn send_colors(offset: u16, buffer: Vec<u8>) -> Result<(), String> { async fn send_colors(offset: u16, buffer: Vec<u8>) -> Result<(), String> {
ambient_light::LedColorsPublisher::send_colors(offset, buffer) ambient_light::LedColorsPublisher::send_colors(offset, buffer)
@@ -383,6 +402,7 @@ async fn main() {
get_led_strips_sample_points, get_led_strips_sample_points,
get_one_edge_colors, get_one_edge_colors,
patch_led_strip_len, patch_led_strip_len,
patch_led_strip_type,
send_colors, send_colors,
move_strip_part, move_strip_part,
reverse_led_strip_part, reverse_led_strip_part,

View File

@@ -11,14 +11,13 @@ import { DisplayStateIndex } from './components/displays/display-state-index';
function App() { function App() {
createEffect(() => { createEffect(() => {
invoke<LedStripConfigContainer>('read_config').then((config) => { invoke<LedStripConfigContainer>('read_config').then((config) => {
console.log('App: read config', config);
setLedStripStore({ setLedStripStore({
strips: config.strips, strips: config.strips,
mappers: config.mappers, mappers: config.mappers,
colorCalibration: config.color_calibration, colorCalibration: config.color_calibration,
}); });
}).catch((error) => { }).catch((error) => {
console.error('App: Failed to read config:', error); console.error('Failed to read config:', error);
}); });
}); });

View File

@@ -1,8 +1,10 @@
import { invoke } from '@tauri-apps/api/core'; import { invoke } from '@tauri-apps/api/core';
import { Component, createMemo, For, JSX, splitProps } from 'solid-js'; import { Component, createMemo, For, JSX, splitProps, useContext } from 'solid-js';
import { DisplayInfo } from '../../models/display-info.model'; import { DisplayInfo } from '../../models/display-info.model';
import { ledStripStore } from '../../stores/led-strip.store'; import { ledStripStore } from '../../stores/led-strip.store';
import { Borders } from '../../constants/border'; import { Borders } from '../../constants/border';
import { LedType } from '../../models/led-strip-config';
import { LedStripConfigurationContext } from '../../contexts/led-strip-configuration.context';
type LedCountControlItemProps = { type LedCountControlItemProps = {
displayId: number; displayId: number;
@@ -11,6 +13,8 @@ type LedCountControlItemProps = {
}; };
const LedCountControlItem: Component<LedCountControlItemProps> = (props) => { const LedCountControlItem: Component<LedCountControlItemProps> = (props) => {
const [stripConfiguration, { setHoveredStripPart }] = useContext(LedStripConfigurationContext);
const config = createMemo(() => { const config = createMemo(() => {
return ledStripStore.strips.find( return ledStripStore.strips.find(
(s) => s.display_id === props.displayId && s.border === props.border (s) => s.display_id === props.displayId && s.border === props.border
@@ -45,7 +49,7 @@ const LedCountControlItem: Component<LedCountControlItemProps> = (props) => {
const target = e.target as HTMLInputElement; const target = e.target as HTMLInputElement;
const newValue = parseInt(target.value); const newValue = parseInt(target.value);
const currentLen = config()?.len || 0; const currentLen = config()?.len || 0;
if (!isNaN(newValue) && newValue >= 0 && newValue <= 1000) { if (!isNaN(newValue) && newValue >= 0 && newValue <= 1000) {
const deltaLen = newValue - currentLen; const deltaLen = newValue - currentLen;
if (deltaLen !== 0) { if (deltaLen !== 0) {
@@ -65,8 +69,41 @@ const LedCountControlItem: Component<LedCountControlItemProps> = (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);
});
};
const onMouseEnter = () => {
setHoveredStripPart({
displayId: props.displayId,
border: props.border,
});
};
const onMouseLeave = () => {
setHoveredStripPart(null);
};
return ( return (
<div class="card bg-base-100 border border-base-300/50 p-2"> <div
class="card bg-base-100 border border-base-300/50 p-2 transition-all duration-200 cursor-pointer"
classList={{
'ring-2 ring-primary bg-primary/20 border-primary':
stripConfiguration.hoveredStripPart?.border === props.border &&
stripConfiguration.hoveredStripPart?.displayId === props.displayId,
}}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<div class="text-center"> <div class="text-center">
<span class="text-xs font-medium text-base-content"> <span class="text-xs font-medium text-base-content">
@@ -107,6 +144,18 @@ const LedCountControlItem: Component<LedCountControlItemProps> = (props) => {
+ +
</button> </button>
</div> </div>
<div class="mt-1">
<select
class="select select-xs w-full text-xs"
value={config()?.led_type || LedType.RGB}
onChange={handleLedTypeChange}
title="LED类型"
>
<option value={LedType.RGB}>RGB</option>
<option value={LedType.RGBW}>RGBW</option>
</select>
</div>
</div> </div>
</div> </div>
); );
@@ -114,7 +163,7 @@ const LedCountControlItem: Component<LedCountControlItemProps> = (props) => {
type LedCountControlPanelProps = { type LedCountControlPanelProps = {
display: DisplayInfo; display: DisplayInfo;
} & JSX.HTMLAttributes<HTMLElement>; } & JSX.HTMLAttributes<HTMLDivElement>;
export const LedCountControlPanel: Component<LedCountControlPanelProps> = (props) => { export const LedCountControlPanel: Component<LedCountControlPanelProps> = (props) => {
const [localProps, rootProps] = splitProps(props, ['display']); const [localProps, rootProps] = splitProps(props, ['display']);

View File

@@ -19,19 +19,17 @@ export const LedStripConfiguration = () => {
createEffect(() => { createEffect(() => {
invoke<string>('list_display_info').then((displays) => { invoke<string>('list_display_info').then((displays) => {
const parsedDisplays = JSON.parse(displays); const parsedDisplays = JSON.parse(displays);
console.log('LedStripConfiguration: Loaded displays:', parsedDisplays);
setDisplayStore({ setDisplayStore({
displays: parsedDisplays, displays: parsedDisplays,
}); });
}).catch((error) => { }).catch((error) => {
console.error('LedStripConfiguration: Failed to load displays:', error); console.error('Failed to load displays:', error);
}); });
invoke<LedStripConfigContainer>('read_led_strip_configs').then((configs) => { invoke<LedStripConfigContainer>('read_led_strip_configs').then((configs) => {
console.log('LedStripConfiguration: Loaded LED strip configs:', configs);
setLedStripStore(configs); setLedStripStore(configs);
}).catch((error) => { }).catch((error) => {
console.error('LedStripConfiguration: Failed to load LED strip configs:', error); console.error('Failed to load LED strip configs:', error);
}); });
}); });
@@ -86,6 +84,7 @@ export const LedStripConfiguration = () => {
LedStripConfigurationContextType[0] LedStripConfigurationContextType[0]
>({ >({
selectedStripPart: null, selectedStripPart: null,
hoveredStripPart: null,
}); });
const ledStripConfigurationContextValue: LedStripConfigurationContextType = [ const ledStripConfigurationContextValue: LedStripConfigurationContextType = [
@@ -96,6 +95,11 @@ export const LedStripConfiguration = () => {
selectedStripPart: v, selectedStripPart: v,
}); });
}, },
setHoveredStripPart: (v) => {
setLedStripConfiguration({
hoveredStripPart: v,
});
},
}, },
]; ];
@@ -135,10 +139,9 @@ export const LedStripConfiguration = () => {
</div> </div>
<div class="h-96 mb-4"> <div class="h-96 mb-4">
<DisplayListContainer> <DisplayListContainer>
{displayStore.displays.map((display) => { {displayStore.displays.map((display) => (
console.log('LedStripConfiguration: Rendering DisplayView for display:', display); <DisplayView display={display} />
return <DisplayView display={display} />; ))}
})}
</DisplayListContainer> </DisplayListContainer>
</div> </div>
<div class="text-xs text-base-content/50"> <div class="text-xs text-base-content/50">

View File

@@ -60,32 +60,16 @@ export const LedStripPart: Component<LedStripPartProps> = (props) => {
); );
if (index === -1) { if (index === -1) {
console.log('🔍 LED: Strip config not found', {
displayId: localProps.config.display_id,
border: localProps.config.border,
availableStrips: ledStripStore.strips.length
});
return; return;
} }
const mapper = ledStripStore.mappers[index]; const mapper = ledStripStore.mappers[index];
if (!mapper) { if (!mapper) {
console.log('🔍 LED: Mapper not found', { index, mappersCount: ledStripStore.mappers.length });
return; return;
} }
const offset = mapper.start * 3; const offset = mapper.start * 3;
console.log('🎨 LED: Updating colors', {
displayId: localProps.config.display_id,
border: localProps.config.border,
stripLength: localProps.config.len,
mapperPos: mapper.pos,
offset,
colorsArrayLength: ledStripStore.colors.length,
firstFewColors: Array.from(ledStripStore.colors.slice(offset, offset + 9))
});
const colors = new Array(localProps.config.len).fill(null).map((_, i) => { const colors = new Array(localProps.config.len).fill(null).map((_, i) => {
const index = offset + i * 3; const index = offset + i * 3;
const r = ledStripStore.colors[index] || 0; const r = ledStripStore.colors[index] || 0;
@@ -94,12 +78,6 @@ export const LedStripPart: Component<LedStripPartProps> = (props) => {
return `rgb(${r}, ${g}, ${b})`; return `rgb(${r}, ${g}, ${b})`;
}); });
console.log('🎨 LED: Generated colors', {
border: localProps.config.border,
colorsCount: colors.length,
sampleColors: colors.slice(0, 3)
});
setColors(colors); setColors(colors);
}); });
@@ -124,7 +102,19 @@ export const LedStripPart: Component<LedStripPartProps> = (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 ( return (
<section <section
@@ -140,7 +130,7 @@ export const LedStripPart: Component<LedStripPartProps> = (props) => {
stripConfiguration.selectedStripPart?.displayId === stripConfiguration.selectedStripPart?.displayId ===
localProps.config?.display_id, localProps.config?.display_id,
}} }}
onWheel={onWheel}
> >
<For each={colors()}>{(item) => <Pixel color={item} />}</For> <For each={colors()}>{(item) => <Pixel color={item} />}</For>
</section> </section>

View File

@@ -29,7 +29,7 @@ const SorterItem: Component<{ strip: LedStripConfig; mapper: LedStripPixelMapper
const [dragCurr, setDragCurr] = createSignal<{ x: number; y: number } | null>(null); const [dragCurr, setDragCurr] = createSignal<{ x: number; y: number } | null>(null);
const [dragStartIndex, setDragStartIndex] = createSignal<number>(0); const [dragStartIndex, setDragStartIndex] = createSignal<number>(0);
const [cellWidth, setCellWidth] = createSignal<number>(0); const [cellWidth, setCellWidth] = createSignal<number>(0);
const [, { setSelectedStripPart }] = useContext(LedStripConfigurationContext); const [stripConfiguration, { setSelectedStripPart, setHoveredStripPart }] = useContext(LedStripConfigurationContext);
const [rootWidth, setRootWidth] = createSignal<number>(0); const [rootWidth, setRootWidth] = createSignal<number>(0);
let root: HTMLDivElement; let root: HTMLDivElement;
@@ -38,9 +38,6 @@ const SorterItem: Component<{ strip: LedStripConfig; mapper: LedStripPixelMapper
if (targetStart === props.mapper.start) { if (targetStart === props.mapper.start) {
return; return;
} }
console.log(
`moving strip part ${props.strip.display_id} ${props.strip.border} from ${props.mapper.start} to ${targetStart}`,
);
invoke('move_strip_part', { invoke('move_strip_part', {
displayId: props.strip.display_id, displayId: props.strip.display_id,
border: props.strip.border, border: props.strip.border,
@@ -151,6 +148,17 @@ const SorterItem: Component<{ strip: LedStripConfig; mapper: LedStripPixelMapper
}).catch((err) => console.error(err)); }).catch((err) => console.error(err));
}; };
const onMouseEnter = () => {
setHoveredStripPart({
displayId: props.strip.display_id,
border: props.strip.border,
});
};
const onMouseLeave = () => {
setHoveredStripPart(null);
};
const setColor = (fullIndex: number, colorsIndex: number, fullLeds: string[]) => { const setColor = (fullIndex: number, colorsIndex: number, fullLeds: string[]) => {
const colors = ledStripStore.colors; const colors = ledStripStore.colors;
let c1 = `rgb(${Math.floor(colors[colorsIndex * 3] * 0.8)}, ${Math.floor( let c1 = `rgb(${Math.floor(colors[colorsIndex * 3] * 0.8)}, ${Math.floor(
@@ -162,7 +170,6 @@ const SorterItem: Component<{ strip: LedStripConfig; mapper: LedStripPixelMapper
)}, ${Math.min(Math.floor(colors[colorsIndex * 3 + 2] * 1.2), 255)})`; )}, ${Math.min(Math.floor(colors[colorsIndex * 3 + 2] * 1.2), 255)})`;
if (fullLeds.length <= fullIndex) { if (fullLeds.length <= fullIndex) {
console.error('out of range', fullIndex, fullLeds.length);
return; return;
} }
@@ -221,9 +228,16 @@ const SorterItem: Component<{ strip: LedStripConfig; mapper: LedStripPixelMapper
return ( return (
<div <div
class="flex mx-2 select-none cursor-ew-resize focus:cursor-ew-resize" class="flex mx-2 select-none cursor-ew-resize focus:cursor-ew-resize transition-colors duration-200"
classList={{
'bg-primary/20 rounded-lg':
stripConfiguration.hoveredStripPart?.border === props.strip.border &&
stripConfiguration.hoveredStripPart?.displayId === props.strip.display_id,
}}
onPointerDown={onPointerDown} onPointerDown={onPointerDown}
ondblclick={reverse} ondblclick={reverse}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
ref={root!} ref={root!}
> >
<div <div

View File

@@ -43,50 +43,35 @@ export const ScreenViewWebSocket: Component<ScreenViewWebSocketProps> = (props)
const connectWebSocket = () => { const connectWebSocket = () => {
if (!isMounted) { if (!isMounted) {
console.log('Component not mounted, skipping WebSocket connection');
return; return;
} }
const wsUrl = `ws://127.0.0.1:8765`; const wsUrl = `ws://127.0.0.1:8765`;
console.log('Connecting to WebSocket:', wsUrl, 'with displayId:', localProps.displayId);
setConnectionStatus('connecting'); setConnectionStatus('connecting');
websocket = new WebSocket(wsUrl); websocket = new WebSocket(wsUrl);
websocket.binaryType = 'arraybuffer'; websocket.binaryType = 'arraybuffer';
console.log('WebSocket object created:', websocket);
websocket.onopen = () => { websocket.onopen = () => {
console.log('WebSocket connected successfully!');
setConnectionStatus('connected'); setConnectionStatus('connected');
// Send initial configuration // Send initial configuration
const config = { const config = {
display_id: localProps.displayId, display_id: localProps.displayId,
width: localProps.width || 320, // Reduced from 400 for better performance width: localProps.width || 320,
height: localProps.height || 180, // Reduced from 225 for better performance height: localProps.height || 180,
quality: localProps.quality || 50 // Reduced from 75 for faster compression quality: localProps.quality || 50
}; };
console.log('Sending WebSocket configuration:', config);
websocket?.send(JSON.stringify(config)); websocket?.send(JSON.stringify(config));
}; };
websocket.onmessage = (event) => { websocket.onmessage = (event) => {
console.log('🔍 WebSocket message received:', {
type: typeof event.data,
isArrayBuffer: event.data instanceof ArrayBuffer,
size: event.data instanceof ArrayBuffer ? event.data.byteLength : 'N/A'
});
if (event.data instanceof ArrayBuffer) { if (event.data instanceof ArrayBuffer) {
console.log('📦 Processing ArrayBuffer frame, size:', event.data.byteLength);
handleJpegFrame(new Uint8Array(event.data)); handleJpegFrame(new Uint8Array(event.data));
} else {
console.log('⚠️ Received non-ArrayBuffer data:', event.data);
} }
}; };
websocket.onclose = (event) => { websocket.onclose = (event) => {
console.log('WebSocket closed:', event.code, event.reason);
setConnectionStatus('disconnected'); setConnectionStatus('disconnected');
websocket = null; websocket = null;
@@ -100,7 +85,6 @@ export const ScreenViewWebSocket: Component<ScreenViewWebSocketProps> = (props)
}; };
websocket.onerror = (error) => { websocket.onerror = (error) => {
console.error('WebSocket error:', error);
setConnectionStatus('error'); setConnectionStatus('error');
}; };
}; };
@@ -204,7 +188,6 @@ export const ScreenViewWebSocket: Component<ScreenViewWebSocketProps> = (props)
// Initialize canvas and resize observer // Initialize canvas and resize observer
onMount(() => { onMount(() => {
console.log('ScreenViewWebSocket mounted with displayId:', localProps.displayId);
const context = canvas.getContext('2d'); const context = canvas.getContext('2d');
setCtx(context); setCtx(context);
@@ -217,7 +200,6 @@ export const ScreenViewWebSocket: Component<ScreenViewWebSocketProps> = (props)
resizeObserver.observe(root); resizeObserver.observe(root);
// Connect WebSocket // Connect WebSocket
console.log('About to connect WebSocket...');
connectWebSocket(); connectWebSocket();
onCleanup(() => { onCleanup(() => {
@@ -227,17 +209,7 @@ export const ScreenViewWebSocket: Component<ScreenViewWebSocketProps> = (props)
}); });
}); });
// Debug function to list displays
const debugDisplays = async () => {
try {
const result = await invoke('list_display_info');
console.log('Available displays:', result);
alert(`Available displays: ${result}`);
} catch (error) {
console.error('Failed to get display info:', error);
alert(`Error: ${error}`);
}
};
// Status indicator // Status indicator
const getStatusColor = () => { const getStatusColor = () => {
@@ -275,13 +247,6 @@ export const ScreenViewWebSocket: Component<ScreenViewWebSocketProps> = (props)
{connectionStatus() === 'connected' && ( {connectionStatus() === 'connected' && (
<span>| {fps()} FPS | {frameCount()} frames</span> <span>| {fps()} FPS | {frameCount()} frames</span>
)} )}
<button
onClick={debugDisplays}
class="ml-2 px-1 py-0.5 bg-blue-600 hover:bg-blue-700 rounded text-xs"
title="Debug: Show available displays"
>
Debug
</button>
</div> </div>
{rootProps.children} {rootProps.children}

View File

@@ -38,7 +38,7 @@ export const WhiteBalance = () => {
setIsFullscreen(true); setIsFullscreen(true);
} }
} catch (error) { } catch (error) {
console.error('Failed to auto enter fullscreen:', error); // Silently handle fullscreen error
} }
}; };
@@ -101,7 +101,6 @@ export const WhiteBalance = () => {
const unlisten = listen('config_changed', (event) => { const unlisten = listen('config_changed', (event) => {
const { strips, mappers, color_calibration } = const { strips, mappers, color_calibration } =
event.payload as LedStripConfigContainer; event.payload as LedStripConfigContainer;
console.log(event.payload);
setLedStripStore({ setLedStripStore({
strips, strips,
mappers, mappers,
@@ -121,9 +120,9 @@ export const WhiteBalance = () => {
const calibration = { ...ledStripStore.colorCalibration }; const calibration = { ...ledStripStore.colorCalibration };
calibration[key] = value; calibration[key] = value;
setLedStripStore('colorCalibration', calibration); setLedStripStore('colorCalibration', calibration);
invoke('set_color_calibration', { calibration }).catch((error) => invoke('set_color_calibration', { calibration }).catch(() => {
console.log(error), // Silently handle error
); });
}; };
const toggleFullscreen = async () => { const toggleFullscreen = async () => {
@@ -138,7 +137,7 @@ export const WhiteBalance = () => {
setPanelPosition({ x: 0, y: 0 }); setPanelPosition({ x: 0, y: 0 });
} }
} catch (error) { } catch (error) {
console.error('Failed to toggle fullscreen:', error); // Silently handle fullscreen error
} }
}; };
@@ -156,7 +155,9 @@ export const WhiteBalance = () => {
const reset = () => { const reset = () => {
invoke('set_color_calibration', { invoke('set_color_calibration', {
calibration: new ColorCalibration(), calibration: new ColorCalibration(),
}).catch((error) => console.log(error)); }).catch(() => {
// Silently handle error
});
}; };
return ( return (
@@ -266,10 +267,19 @@ export const WhiteBalance = () => {
<div class="form-control"> <div class="form-control">
<label class="label"> <label class="label">
<span class="label-text font-semibold text-base-content/70"> (W)</span> <span class="label-text font-semibold text-amber-500"> (W)</span>
<div class="badge badge-outline badge-sm"></div> <Value value={ledStripStore.colorCalibration.w} />
</label> </label>
<ColorSlider class="from-yellow-50 to-cyan-50" disabled /> <ColorSlider
class="from-amber-100 to-amber-50"
value={ledStripStore.colorCalibration.w}
onInput={(ev) =>
updateColorCalibration(
'w',
(ev.target as HTMLInputElement).valueAsNumber ?? 1,
)
}
/>
</div> </div>
</div> </div>
@@ -400,6 +410,23 @@ export const WhiteBalance = () => {
/> />
</div> </div>
<div class="form-control">
<label class="label">
<span class="label-text font-semibold text-amber-500"> (W)</span>
<Value value={ledStripStore.colorCalibration.w} />
</label>
<ColorSlider
class="from-amber-100 to-amber-50"
value={ledStripStore.colorCalibration.w}
onInput={(ev) =>
updateColorCalibration(
'w',
(ev.target as HTMLInputElement).valueAsNumber ?? 1,
)
}
/>
</div>
<div class="form-control"> <div class="form-control">
<label class="label"> <label class="label">
<span class="label-text font-semibold text-base-content/70"> (W)</span> <span class="label-text font-semibold text-base-content/70"> (W)</span>

View File

@@ -7,9 +7,14 @@ export type LedStripConfigurationContextType = [
displayId: number; displayId: number;
border: Borders; border: Borders;
} | null; } | null;
hoveredStripPart: {
displayId: number;
border: Borders;
} | null;
}, },
{ {
setSelectedStripPart: (v: { displayId: number; border: Borders } | null) => void; setSelectedStripPart: (v: { displayId: number; border: Borders } | null) => void;
setHoveredStripPart: (v: { displayId: number; border: Borders } | null) => void;
}, },
]; ];
@@ -17,8 +22,10 @@ export const LedStripConfigurationContext =
createContext<LedStripConfigurationContextType>([ createContext<LedStripConfigurationContextType>([
{ {
selectedStripPart: null, selectedStripPart: null,
hoveredStripPart: null,
}, },
{ {
setSelectedStripPart: () => {}, setSelectedStripPart: () => {},
setHoveredStripPart: () => { },
}, },
]); ]);

View File

@@ -1,5 +1,10 @@
import { Borders } from '../constants/border'; import { Borders } from '../constants/border';
export enum LedType {
RGB = 'RGB',
RGBW = 'RGBW',
}
export type LedStripPixelMapper = { export type LedStripPixelMapper = {
start: number; start: number;
end: number; end: number;
@@ -10,6 +15,7 @@ export class ColorCalibration {
r: number = 1; r: number = 1;
g: number = 1; g: number = 1;
b: number = 1; b: number = 1;
w: number = 1;
} }
export type LedStripConfigContainer = { export type LedStripConfigContainer = {
@@ -23,5 +29,6 @@ export class LedStripConfig {
public readonly display_id: number, public readonly display_id: number,
public readonly border: Borders, public readonly border: Borders,
public len: number, public len: number,
public led_type: LedType = LedType.RGB,
) {} ) {}
} }