Compare commits
2 Commits
5de105960b
...
90cace679b
Author | SHA1 | Date | |
---|---|---|---|
90cace679b | |||
99cbaf3b9f |
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,
|
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 {
|
||||||
|
@@ -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,
|
||||||
|
@@ -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]),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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 {
|
||||||
|
@@ -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,
|
||||||
|
@@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -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']);
|
||||||
|
@@ -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">
|
||||||
|
@@ -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>
|
||||||
|
@@ -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
|
||||||
|
@@ -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}
|
||||||
|
@@ -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>
|
||||||
|
@@ -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: () => { },
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
@@ -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,
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user