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:
140
docs/hardware-protocol.md
Normal file
140
docs/hardware-protocol.md
Normal 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
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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]),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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<LedCountControlItemProps> = (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<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);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="card bg-base-100 border border-base-300/50 p-2">
|
||||
<div class="flex flex-col gap-1">
|
||||
@ -107,6 +121,18 @@ const LedCountControlItem: Component<LedCountControlItemProps> = (props) => {
|
||||
+
|
||||
</button>
|
||||
</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>
|
||||
);
|
||||
|
@ -74,6 +74,8 @@ export const LedStripPart: Component<LedStripPartProps> = (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<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 (
|
||||
<section
|
||||
@ -140,7 +154,7 @@ export const LedStripPart: Component<LedStripPartProps> = (props) => {
|
||||
stripConfiguration.selectedStripPart?.displayId ===
|
||||
localProps.config?.display_id,
|
||||
}}
|
||||
|
||||
onWheel={onWheel}
|
||||
>
|
||||
<For each={colors()}>{(item) => <Pixel color={item} />}</For>
|
||||
</section>
|
||||
|
@ -266,10 +266,19 @@ export const WhiteBalance = () => {
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-semibold text-base-content/70">白色 (W)</span>
|
||||
<div class="badge badge-outline badge-sm">暂未启用</div>
|
||||
<span class="label-text font-semibold text-amber-500">白色 (W)</span>
|
||||
<Value value={ledStripStore.colorCalibration.w} />
|
||||
</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>
|
||||
|
||||
@ -400,6 +409,23 @@ export const WhiteBalance = () => {
|
||||
/>
|
||||
</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">
|
||||
<label class="label">
|
||||
<span class="label-text font-semibold text-base-content/70">白色 (W)</span>
|
||||
|
@ -1,5 +1,10 @@
|
||||
import { Borders } from '../constants/border';
|
||||
|
||||
export enum LedType {
|
||||
RGB = 'RGB',
|
||||
RGBW = 'RGBW',
|
||||
}
|
||||
|
||||
export type LedStripPixelMapper = {
|
||||
start: number;
|
||||
end: number;
|
||||
@ -10,6 +15,7 @@ export class ColorCalibration {
|
||||
r: number = 1;
|
||||
g: number = 1;
|
||||
b: number = 1;
|
||||
w: number = 1;
|
||||
}
|
||||
|
||||
export type LedStripConfigContainer = {
|
||||
@ -23,5 +29,6 @@ export class LedStripConfig {
|
||||
public readonly display_id: number,
|
||||
public readonly border: Borders,
|
||||
public len: number,
|
||||
public led_type: LedType = LedType.RGB,
|
||||
) {}
|
||||
}
|
||||
|
Reference in New Issue
Block a user