Implement LED test effects with proper cleanup
- Add LED test effects page with multiple test patterns (solid colors, rainbow, breathing, flowing) - Implement Rust backend for LED test effects with proper task management - Add automatic cleanup when navigating away from test page using onCleanup hook - Ensure test mode is properly disabled to resume normal ambient lighting - Clean up debug logging for production readiness - Fix menu navigation issues by using SolidJS router components Features: - Multiple test patterns: solid colors, rainbow cycle, breathing effect, flowing lights - Configurable animation speed - Automatic cleanup prevents LED conflicts with ambient lighting - Responsive UI with proper error handling
This commit is contained in:
1
src-tauri/Cargo.lock
generated
1
src-tauri/Cargo.lock
generated
@ -4432,6 +4432,7 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"tokio-tungstenite",
|
"tokio-tungstenite",
|
||||||
|
"tokio-util",
|
||||||
"toml 0.7.8",
|
"toml 0.7.8",
|
||||||
"url-build-parse",
|
"url-build-parse",
|
||||||
]
|
]
|
||||||
|
@ -23,6 +23,7 @@ core-graphics = "0.22.3"
|
|||||||
display-info = "0.4.1"
|
display-info = "0.4.1"
|
||||||
anyhow = "1.0.69"
|
anyhow = "1.0.69"
|
||||||
tokio = {version = "1.26.0", features = ["full"] }
|
tokio = {version = "1.26.0", features = ["full"] }
|
||||||
|
tokio-util = "0.7"
|
||||||
paris = { version = "1.5", features = ["timestamps", "macros"] }
|
paris = { version = "1.5", features = ["timestamps", "macros"] }
|
||||||
log = "0.4.17"
|
log = "0.4.17"
|
||||||
env_logger = "0.10.0"
|
env_logger = "0.10.0"
|
||||||
|
@ -26,6 +26,7 @@ pub struct LedColorsPublisher {
|
|||||||
colors_rx: Arc<RwLock<watch::Receiver<Vec<u8>>>>,
|
colors_rx: Arc<RwLock<watch::Receiver<Vec<u8>>>>,
|
||||||
colors_tx: Arc<RwLock<watch::Sender<Vec<u8>>>>,
|
colors_tx: Arc<RwLock<watch::Sender<Vec<u8>>>>,
|
||||||
inner_tasks_version: Arc<RwLock<usize>>,
|
inner_tasks_version: Arc<RwLock<usize>>,
|
||||||
|
test_mode_active: Arc<RwLock<bool>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LedColorsPublisher {
|
impl LedColorsPublisher {
|
||||||
@ -44,6 +45,7 @@ impl LedColorsPublisher {
|
|||||||
colors_rx: Arc::new(RwLock::new(rx)),
|
colors_rx: Arc::new(RwLock::new(rx)),
|
||||||
colors_tx: Arc::new(RwLock::new(tx)),
|
colors_tx: Arc::new(RwLock::new(tx)),
|
||||||
inner_tasks_version: Arc::new(RwLock::new(0)),
|
inner_tasks_version: Arc::new(RwLock::new(0)),
|
||||||
|
test_mode_active: Arc::new(RwLock::new(false)),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
@ -81,6 +83,13 @@ impl LedColorsPublisher {
|
|||||||
|
|
||||||
let mappers = mappers.clone();
|
let mappers = mappers.clone();
|
||||||
|
|
||||||
|
// Check if test mode is active before sending normal colors
|
||||||
|
let test_mode_active = {
|
||||||
|
let publisher = LedColorsPublisher::global().await;
|
||||||
|
*publisher.test_mode_active.read().await
|
||||||
|
};
|
||||||
|
|
||||||
|
if !test_mode_active {
|
||||||
match Self::send_colors_by_display(colors, mappers, &strips, &color_calibration).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);
|
||||||
@ -89,6 +98,7 @@ impl LedColorsPublisher {
|
|||||||
warn!("Failed to send colors: #{: >15}\t{}", display_id, err);
|
warn!("Failed to send colors: #{: >15}\t{}", display_id, err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
match display_colors_tx.send((
|
match display_colors_tx.send((
|
||||||
display_id,
|
display_id,
|
||||||
@ -532,6 +542,35 @@ impl LedColorsPublisher {
|
|||||||
pub async fn clone_colors_receiver(&self) -> watch::Receiver<Vec<u8>> {
|
pub async fn clone_colors_receiver(&self) -> watch::Receiver<Vec<u8>> {
|
||||||
self.colors_rx.read().await.clone()
|
self.colors_rx.read().await.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Enable test mode - this will pause normal LED data publishing
|
||||||
|
pub async fn enable_test_mode(&self) {
|
||||||
|
let mut test_mode = self.test_mode_active.write().await;
|
||||||
|
*test_mode = true;
|
||||||
|
log::info!("Test mode enabled - normal LED publishing paused");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Disable test mode - this will resume normal LED data publishing
|
||||||
|
pub async fn disable_test_mode(&self) {
|
||||||
|
let mut test_mode = self.test_mode_active.write().await;
|
||||||
|
*test_mode = false;
|
||||||
|
log::info!("Test mode disabled - normal LED publishing resumed");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Disable test mode with a delay to ensure clean transition
|
||||||
|
pub async fn disable_test_mode_with_delay(&self, delay_ms: u64) {
|
||||||
|
// Wait for the specified delay
|
||||||
|
tokio::time::sleep(tokio::time::Duration::from_millis(delay_ms)).await;
|
||||||
|
|
||||||
|
let mut test_mode = self.test_mode_active.write().await;
|
||||||
|
*test_mode = false;
|
||||||
|
log::info!("Test mode disabled with delay - normal LED publishing resumed");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if test mode is currently active
|
||||||
|
pub async fn is_test_mode_active(&self) -> bool {
|
||||||
|
*self.test_mode_active.read().await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
239
src-tauri/src/led_test_effects.rs
Normal file
239
src-tauri/src/led_test_effects.rs
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
use std::f64::consts::PI;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub enum TestEffectType {
|
||||||
|
FlowingRainbow,
|
||||||
|
GroupCounting,
|
||||||
|
SingleScan,
|
||||||
|
Breathing,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct TestEffectConfig {
|
||||||
|
pub effect_type: TestEffectType,
|
||||||
|
pub led_count: u32,
|
||||||
|
pub led_type: LedType,
|
||||||
|
pub speed: f64, // Speed multiplier
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub enum LedType {
|
||||||
|
RGB,
|
||||||
|
RGBW,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LedTestEffects;
|
||||||
|
|
||||||
|
impl LedTestEffects {
|
||||||
|
/// Generate LED colors for a specific test effect at a given time
|
||||||
|
pub fn generate_colors(config: &TestEffectConfig, time_ms: u64) -> Vec<u8> {
|
||||||
|
let time_seconds = time_ms as f64 / 1000.0;
|
||||||
|
|
||||||
|
match config.effect_type {
|
||||||
|
TestEffectType::FlowingRainbow => {
|
||||||
|
Self::flowing_rainbow(config.led_count, config.led_type.clone(), time_seconds, config.speed)
|
||||||
|
}
|
||||||
|
TestEffectType::GroupCounting => {
|
||||||
|
Self::group_counting(config.led_count, config.led_type.clone())
|
||||||
|
}
|
||||||
|
TestEffectType::SingleScan => {
|
||||||
|
Self::single_scan(config.led_count, config.led_type.clone(), time_seconds, config.speed)
|
||||||
|
}
|
||||||
|
TestEffectType::Breathing => {
|
||||||
|
Self::breathing(config.led_count, config.led_type.clone(), time_seconds, config.speed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Flowing rainbow effect - smooth rainbow colors flowing along the strip
|
||||||
|
fn flowing_rainbow(led_count: u32, led_type: LedType, time: f64, speed: f64) -> Vec<u8> {
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
let time_offset = (time * speed * 60.0) % 360.0; // 60 degrees per second at speed 1.0
|
||||||
|
|
||||||
|
for i in 0..led_count {
|
||||||
|
// Create longer wavelength for smoother color transitions
|
||||||
|
let hue = ((i as f64 * 720.0 / led_count as f64) + time_offset) % 360.0;
|
||||||
|
let rgb = Self::hsv_to_rgb(hue, 1.0, 1.0);
|
||||||
|
|
||||||
|
buffer.push(rgb.0);
|
||||||
|
buffer.push(rgb.1);
|
||||||
|
buffer.push(rgb.2);
|
||||||
|
|
||||||
|
if matches!(led_type, LedType::RGBW) {
|
||||||
|
buffer.push(0); // White channel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Group counting effect - every 10 LEDs have different colors
|
||||||
|
fn group_counting(led_count: u32, led_type: LedType) -> Vec<u8> {
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
|
||||||
|
let group_colors = [
|
||||||
|
(255, 0, 0), // Red (1-10)
|
||||||
|
(0, 255, 0), // Green (11-20)
|
||||||
|
(0, 0, 255), // Blue (21-30)
|
||||||
|
(255, 255, 0), // Yellow (31-40)
|
||||||
|
(255, 0, 255), // Magenta (41-50)
|
||||||
|
(0, 255, 255), // Cyan (51-60)
|
||||||
|
(255, 128, 0), // Orange (61-70)
|
||||||
|
(128, 255, 0), // Lime (71-80)
|
||||||
|
(255, 255, 255), // White (81-90)
|
||||||
|
(128, 128, 128), // Gray (91-100)
|
||||||
|
];
|
||||||
|
|
||||||
|
for i in 0..led_count {
|
||||||
|
let group_index = (i / 10) % group_colors.len() as u32;
|
||||||
|
let color = group_colors[group_index as usize];
|
||||||
|
|
||||||
|
buffer.push(color.0);
|
||||||
|
buffer.push(color.1);
|
||||||
|
buffer.push(color.2);
|
||||||
|
|
||||||
|
if matches!(led_type, LedType::RGBW) {
|
||||||
|
buffer.push(0); // White channel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Single LED scan effect - one LED moves along the strip
|
||||||
|
fn single_scan(led_count: u32, led_type: LedType, time: f64, speed: f64) -> Vec<u8> {
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
let scan_period = 2.0 / speed; // 2 seconds per full scan at speed 1.0
|
||||||
|
let active_index = ((time / scan_period * led_count as f64) as u32) % led_count;
|
||||||
|
|
||||||
|
for i in 0..led_count {
|
||||||
|
if i == active_index {
|
||||||
|
// Bright white LED
|
||||||
|
buffer.push(255);
|
||||||
|
buffer.push(255);
|
||||||
|
buffer.push(255);
|
||||||
|
|
||||||
|
if matches!(led_type, LedType::RGBW) {
|
||||||
|
buffer.push(255); // White channel
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Off
|
||||||
|
buffer.push(0);
|
||||||
|
buffer.push(0);
|
||||||
|
buffer.push(0);
|
||||||
|
|
||||||
|
if matches!(led_type, LedType::RGBW) {
|
||||||
|
buffer.push(0); // White channel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Breathing effect - entire strip breathes with white light
|
||||||
|
fn breathing(led_count: u32, led_type: LedType, time: f64, speed: f64) -> Vec<u8> {
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
let breathing_period = 4.0 / speed; // 4 seconds per breath at speed 1.0
|
||||||
|
let brightness = ((time / breathing_period * 2.0 * PI).sin() * 0.5 + 0.5) * 255.0;
|
||||||
|
let brightness = brightness as u8;
|
||||||
|
|
||||||
|
for _i in 0..led_count {
|
||||||
|
buffer.push(brightness);
|
||||||
|
buffer.push(brightness);
|
||||||
|
buffer.push(brightness);
|
||||||
|
|
||||||
|
if matches!(led_type, LedType::RGBW) {
|
||||||
|
buffer.push(brightness); // White channel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert HSV to RGB
|
||||||
|
/// H: 0-360, S: 0-1, V: 0-1
|
||||||
|
/// Returns: (R, G, B) where each component is 0-255
|
||||||
|
fn hsv_to_rgb(h: f64, s: f64, v: f64) -> (u8, u8, u8) {
|
||||||
|
let c = v * s;
|
||||||
|
let x = c * (1.0 - ((h / 60.0) % 2.0 - 1.0).abs());
|
||||||
|
let m = v - c;
|
||||||
|
|
||||||
|
let (r_prime, g_prime, b_prime) = if h < 60.0 {
|
||||||
|
(c, x, 0.0)
|
||||||
|
} else if h < 120.0 {
|
||||||
|
(x, c, 0.0)
|
||||||
|
} else if h < 180.0 {
|
||||||
|
(0.0, c, x)
|
||||||
|
} else if h < 240.0 {
|
||||||
|
(0.0, x, c)
|
||||||
|
} else if h < 300.0 {
|
||||||
|
(x, 0.0, c)
|
||||||
|
} else {
|
||||||
|
(c, 0.0, x)
|
||||||
|
};
|
||||||
|
|
||||||
|
let r = ((r_prime + m) * 255.0).round() as u8;
|
||||||
|
let g = ((g_prime + m) * 255.0).round() as u8;
|
||||||
|
let b = ((b_prime + m) * 255.0).round() as u8;
|
||||||
|
|
||||||
|
(r, g, b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_hsv_to_rgb() {
|
||||||
|
// Test red
|
||||||
|
let (r, g, b) = LedTestEffects::hsv_to_rgb(0.0, 1.0, 1.0);
|
||||||
|
assert_eq!((r, g, b), (255, 0, 0));
|
||||||
|
|
||||||
|
// Test green
|
||||||
|
let (r, g, b) = LedTestEffects::hsv_to_rgb(120.0, 1.0, 1.0);
|
||||||
|
assert_eq!((r, g, b), (0, 255, 0));
|
||||||
|
|
||||||
|
// Test blue
|
||||||
|
let (r, g, b) = LedTestEffects::hsv_to_rgb(240.0, 1.0, 1.0);
|
||||||
|
assert_eq!((r, g, b), (0, 0, 255));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_flowing_rainbow() {
|
||||||
|
let config = TestEffectConfig {
|
||||||
|
effect_type: TestEffectType::FlowingRainbow,
|
||||||
|
led_count: 10,
|
||||||
|
led_type: LedType::RGB,
|
||||||
|
speed: 1.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let colors = LedTestEffects::generate_colors(&config, 0);
|
||||||
|
assert_eq!(colors.len(), 30); // 10 LEDs * 3 bytes each
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_group_counting() {
|
||||||
|
let config = TestEffectConfig {
|
||||||
|
effect_type: TestEffectType::GroupCounting,
|
||||||
|
led_count: 20,
|
||||||
|
led_type: LedType::RGB,
|
||||||
|
speed: 1.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let colors = LedTestEffects::generate_colors(&config, 0);
|
||||||
|
assert_eq!(colors.len(), 60); // 20 LEDs * 3 bytes each
|
||||||
|
|
||||||
|
// First 10 should be red
|
||||||
|
assert_eq!(colors[0], 255); // R
|
||||||
|
assert_eq!(colors[1], 0); // G
|
||||||
|
assert_eq!(colors[2], 0); // B
|
||||||
|
|
||||||
|
// Next 10 should be green
|
||||||
|
assert_eq!(colors[30], 0); // R
|
||||||
|
assert_eq!(colors[31], 255); // G
|
||||||
|
assert_eq!(colors[32], 0); // B
|
||||||
|
}
|
||||||
|
}
|
@ -4,6 +4,7 @@
|
|||||||
mod ambient_light;
|
mod ambient_light;
|
||||||
mod display;
|
mod display;
|
||||||
mod led_color;
|
mod led_color;
|
||||||
|
mod led_test_effects;
|
||||||
mod rpc;
|
mod rpc;
|
||||||
mod screenshot;
|
mod screenshot;
|
||||||
mod screenshot_manager;
|
mod screenshot_manager;
|
||||||
@ -13,6 +14,7 @@ mod volume;
|
|||||||
use ambient_light::{Border, ColorCalibration, LedStripConfig, LedStripConfigGroup, LedType};
|
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 led_test_effects::{LedTestEffects, TestEffectConfig, TestEffectType};
|
||||||
use paris::{error, info, warn};
|
use paris::{error, info, warn};
|
||||||
use rpc::{BoardInfo, UdpRpc};
|
use rpc::{BoardInfo, UdpRpc};
|
||||||
use screenshot::Screenshot;
|
use screenshot::Screenshot;
|
||||||
@ -24,6 +26,14 @@ use tauri::{Manager, Emitter, Runtime};
|
|||||||
use regex;
|
use regex;
|
||||||
use tauri::http::{Request, Response};
|
use tauri::http::{Request, Response};
|
||||||
use volume::VolumeManager;
|
use volume::VolumeManager;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
|
// Global static variables for LED test effect management
|
||||||
|
static EFFECT_HANDLE: tokio::sync::OnceCell<Arc<RwLock<Option<tokio::task::JoinHandle<()>>>>> =
|
||||||
|
tokio::sync::OnceCell::const_new();
|
||||||
|
static CANCEL_TOKEN: tokio::sync::OnceCell<Arc<RwLock<Option<tokio_util::sync::CancellationToken>>>> =
|
||||||
|
tokio::sync::OnceCell::const_new();
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
#[serde(remote = "DisplayInfo")]
|
#[serde(remote = "DisplayInfo")]
|
||||||
struct DisplayInfoDef {
|
struct DisplayInfoDef {
|
||||||
@ -140,10 +150,6 @@ async fn patch_led_strip_len(display_id: u32, border: Border, delta_len: i8) ->
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn patch_led_strip_type(display_id: u32, border: Border, led_type: LedType) -> Result<(), String> {
|
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;
|
let config_manager = ambient_light::ConfigManager::global().await;
|
||||||
config_manager
|
config_manager
|
||||||
.patch_led_strip_type(display_id, border, led_type)
|
.patch_led_strip_type(display_id, border, led_type)
|
||||||
@ -153,7 +159,6 @@ async fn patch_led_strip_type(display_id: u32, border: Border, led_type: LedType
|
|||||||
e.to_string()
|
e.to_string()
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
info!("patch_led_strip_type: ok");
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -167,6 +172,193 @@ async fn send_colors(offset: u16, buffer: Vec<u8>) -> Result<(), String> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn send_test_colors_to_board(board_address: String, offset: u16, buffer: Vec<u8>) -> Result<(), String> {
|
||||||
|
use tokio::net::UdpSocket;
|
||||||
|
|
||||||
|
let socket = UdpSocket::bind("0.0.0.0:0").await.map_err(|e| {
|
||||||
|
error!("Failed to bind UDP socket: {}", e);
|
||||||
|
e.to_string()
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut packet = vec![0x02]; // Header
|
||||||
|
packet.push((offset >> 8) as u8); // Offset high
|
||||||
|
packet.push((offset & 0xff) as u8); // Offset low
|
||||||
|
packet.extend_from_slice(&buffer); // Color data
|
||||||
|
|
||||||
|
socket.send_to(&packet, &board_address).await.map_err(|e| {
|
||||||
|
error!("Failed to send test colors to board {}: {}", board_address, e);
|
||||||
|
e.to_string()
|
||||||
|
})?;
|
||||||
|
|
||||||
|
info!("Sent test colors to board {} with offset {} and {} bytes", board_address, offset, buffer.len());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn enable_test_mode() -> Result<(), String> {
|
||||||
|
let publisher = ambient_light::LedColorsPublisher::global().await;
|
||||||
|
publisher.enable_test_mode().await;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn disable_test_mode() -> Result<(), String> {
|
||||||
|
info!("🔄 disable_test_mode command called from frontend");
|
||||||
|
let publisher = ambient_light::LedColorsPublisher::global().await;
|
||||||
|
publisher.disable_test_mode().await;
|
||||||
|
info!("✅ disable_test_mode command completed");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn is_test_mode_active() -> Result<bool, String> {
|
||||||
|
let publisher = ambient_light::LedColorsPublisher::global().await;
|
||||||
|
Ok(publisher.is_test_mode_active().await)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn start_led_test_effect(
|
||||||
|
board_address: String,
|
||||||
|
effect_config: TestEffectConfig,
|
||||||
|
update_interval_ms: u64,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
use tokio::time::{interval, Duration};
|
||||||
|
|
||||||
|
// Enable test mode first
|
||||||
|
let publisher = ambient_light::LedColorsPublisher::global().await;
|
||||||
|
publisher.enable_test_mode().await;
|
||||||
|
|
||||||
|
let handle_storage = EFFECT_HANDLE.get_or_init(|| async {
|
||||||
|
Arc::new(RwLock::new(None))
|
||||||
|
}).await;
|
||||||
|
|
||||||
|
let cancel_storage = CANCEL_TOKEN.get_or_init(|| async {
|
||||||
|
Arc::new(RwLock::new(None))
|
||||||
|
}).await;
|
||||||
|
|
||||||
|
// Stop any existing effect
|
||||||
|
{
|
||||||
|
let mut cancel_guard = cancel_storage.write().await;
|
||||||
|
if let Some(token) = cancel_guard.take() {
|
||||||
|
token.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut handle_guard = handle_storage.write().await;
|
||||||
|
if let Some(handle) = handle_guard.take() {
|
||||||
|
let _ = handle.await; // Wait for graceful shutdown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start new effect
|
||||||
|
let effect_config = Arc::new(effect_config);
|
||||||
|
let board_address = Arc::new(board_address);
|
||||||
|
let start_time = std::time::Instant::now();
|
||||||
|
|
||||||
|
// Create new cancellation token
|
||||||
|
let cancel_token = tokio_util::sync::CancellationToken::new();
|
||||||
|
let cancel_token_clone = cancel_token.clone();
|
||||||
|
|
||||||
|
let handle = tokio::spawn(async move {
|
||||||
|
let mut interval = interval(Duration::from_millis(update_interval_ms));
|
||||||
|
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
_ = interval.tick() => {
|
||||||
|
let elapsed_ms = start_time.elapsed().as_millis() as u64;
|
||||||
|
let colors = LedTestEffects::generate_colors(&effect_config, elapsed_ms);
|
||||||
|
|
||||||
|
// Send to board
|
||||||
|
if let Err(e) = send_test_colors_to_board_internal(&board_address, 0, colors).await {
|
||||||
|
error!("Failed to send test effect colors: {}", e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = cancel_token_clone.cancelled() => {
|
||||||
|
info!("LED test effect cancelled gracefully");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
info!("LED test effect task ended");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Store the handle and cancel token
|
||||||
|
{
|
||||||
|
let mut handle_guard = handle_storage.write().await;
|
||||||
|
*handle_guard = Some(handle);
|
||||||
|
|
||||||
|
let mut cancel_guard = cancel_storage.write().await;
|
||||||
|
*cancel_guard = Some(cancel_token);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn stop_led_test_effect(board_address: String, led_count: u32, led_type: led_test_effects::LedType) -> Result<(), String> {
|
||||||
|
// Stop the effect task first
|
||||||
|
|
||||||
|
info!("🛑 Stopping LED test effect - board: {}", board_address);
|
||||||
|
|
||||||
|
// Cancel the task gracefully first
|
||||||
|
if let Some(cancel_storage) = CANCEL_TOKEN.get() {
|
||||||
|
let mut cancel_guard = cancel_storage.write().await;
|
||||||
|
if let Some(token) = cancel_guard.take() {
|
||||||
|
info!("🔄 Cancelling test effect task gracefully");
|
||||||
|
token.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for the task to finish
|
||||||
|
if let Some(handle_storage) = EFFECT_HANDLE.get() {
|
||||||
|
let mut handle_guard = handle_storage.write().await;
|
||||||
|
if let Some(handle) = handle_guard.take() {
|
||||||
|
info!("⏳ Waiting for test effect task to finish");
|
||||||
|
match handle.await {
|
||||||
|
Ok(_) => info!("✅ Test effect task finished successfully"),
|
||||||
|
Err(e) => warn!("⚠️ Test effect task finished with error: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Turn off all LEDs
|
||||||
|
let bytes_per_led = match led_type {
|
||||||
|
led_test_effects::LedType::RGB => 3,
|
||||||
|
led_test_effects::LedType::RGBW => 4,
|
||||||
|
};
|
||||||
|
let buffer = vec![0u8; (led_count * bytes_per_led) as usize];
|
||||||
|
|
||||||
|
send_test_colors_to_board_internal(&board_address, 0, buffer).await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
info!("💡 Sent LED off command");
|
||||||
|
|
||||||
|
// Disable test mode to resume normal publishing
|
||||||
|
let publisher = ambient_light::LedColorsPublisher::global().await;
|
||||||
|
publisher.disable_test_mode().await;
|
||||||
|
|
||||||
|
info!("🔄 Test mode disabled, normal publishing resumed");
|
||||||
|
info!("✅ LED test effect stopped completely");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal helper function
|
||||||
|
async fn send_test_colors_to_board_internal(board_address: &str, offset: u16, buffer: Vec<u8>) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
use tokio::net::UdpSocket;
|
||||||
|
|
||||||
|
let socket = UdpSocket::bind("0.0.0.0:0").await?;
|
||||||
|
|
||||||
|
let mut packet = vec![0x02]; // Header
|
||||||
|
packet.push((offset >> 8) as u8); // Offset high
|
||||||
|
packet.push((offset & 0xff) as u8); // Offset low
|
||||||
|
packet.extend_from_slice(&buffer); // Color data
|
||||||
|
|
||||||
|
socket.send_to(&packet, board_address).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn move_strip_part(
|
async fn move_strip_part(
|
||||||
display_id: u32,
|
display_id: u32,
|
||||||
@ -356,20 +548,7 @@ fn handle_ambient_light_protocol<R: Runtime>(
|
|||||||
async fn main() {
|
async fn main() {
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
|
|
||||||
// Debug: Print available displays
|
// Initialize display info (removed debug output)
|
||||||
match display_info::DisplayInfo::all() {
|
|
||||||
Ok(displays) => {
|
|
||||||
println!("=== AVAILABLE DISPLAYS ===");
|
|
||||||
for (index, display) in displays.iter().enumerate() {
|
|
||||||
println!(" Display {}: ID={}, Scale={}, Width={}, Height={}",
|
|
||||||
index, display.id, display.scale_factor, display.width, display.height);
|
|
||||||
}
|
|
||||||
println!("=== END DISPLAYS ===");
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
println!("Error getting display info: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let screenshot_manager = ScreenshotManager::global().await;
|
let screenshot_manager = ScreenshotManager::global().await;
|
||||||
@ -404,6 +583,12 @@ async fn main() {
|
|||||||
patch_led_strip_len,
|
patch_led_strip_len,
|
||||||
patch_led_strip_type,
|
patch_led_strip_type,
|
||||||
send_colors,
|
send_colors,
|
||||||
|
send_test_colors_to_board,
|
||||||
|
enable_test_mode,
|
||||||
|
disable_test_mode,
|
||||||
|
is_test_mode_active,
|
||||||
|
start_led_test_effect,
|
||||||
|
stop_led_test_effect,
|
||||||
move_strip_part,
|
move_strip_part,
|
||||||
reverse_led_strip_part,
|
reverse_led_strip_part,
|
||||||
set_color_calibration,
|
set_color_calibration,
|
||||||
|
47
src/App.tsx
47
src/App.tsx
@ -1,7 +1,8 @@
|
|||||||
import { Routes, Route } from '@solidjs/router';
|
import { Routes, Route, useLocation, A } from '@solidjs/router';
|
||||||
import { LedStripConfiguration } from './components/led-strip-configuration/led-strip-configuration';
|
import { LedStripConfiguration } from './components/led-strip-configuration/led-strip-configuration';
|
||||||
import { WhiteBalance } from './components/white-balance/white-balance';
|
import { WhiteBalance } from './components/white-balance/white-balance';
|
||||||
import { createEffect } from 'solid-js';
|
import { LedStripTest } from './components/led-strip-test/led-strip-test';
|
||||||
|
import { createEffect, createSignal } from 'solid-js';
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
import { setLedStripStore } from './stores/led-strip.store';
|
import { setLedStripStore } from './stores/led-strip.store';
|
||||||
import { LedStripConfigContainer } from './models/led-strip-config';
|
import { LedStripConfigContainer } from './models/led-strip-config';
|
||||||
@ -9,6 +10,29 @@ import { InfoIndex } from './components/info/info-index';
|
|||||||
import { DisplayStateIndex } from './components/displays/display-state-index';
|
import { DisplayStateIndex } from './components/displays/display-state-index';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
const location = useLocation();
|
||||||
|
const [previousPath, setPreviousPath] = createSignal<string>('');
|
||||||
|
|
||||||
|
// Monitor route changes and cleanup LED tests when leaving the test page
|
||||||
|
createEffect(() => {
|
||||||
|
const currentPath = location.pathname;
|
||||||
|
const prevPath = previousPath();
|
||||||
|
|
||||||
|
// Check if we're leaving the LED test page
|
||||||
|
const isLeavingTestPage = prevPath === '/led-strip-test' && currentPath !== '/led-strip-test';
|
||||||
|
|
||||||
|
if (isLeavingTestPage) {
|
||||||
|
// The LED test component will handle stopping the test effect via onCleanup
|
||||||
|
// We just need to ensure test mode is disabled to resume normal LED publishing
|
||||||
|
invoke('disable_test_mode').catch((error) => {
|
||||||
|
console.error('Failed to disable test mode:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update previousPath after the condition check
|
||||||
|
setPreviousPath(currentPath);
|
||||||
|
});
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
invoke<LedStripConfigContainer>('read_config').then((config) => {
|
invoke<LedStripConfigContainer>('read_config').then((config) => {
|
||||||
setLedStripStore({
|
setLedStripStore({
|
||||||
@ -33,20 +57,22 @@ function App() {
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<ul tabindex="0" class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52">
|
<ul tabindex="0" class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52">
|
||||||
<li><a href="/info" class="text-base-content">基本信息</a></li>
|
<li><A href="/info" class="text-base-content">基本信息</A></li>
|
||||||
<li><a href="/displays" class="text-base-content">显示器信息</a></li>
|
<li><A href="/displays" class="text-base-content">显示器信息</A></li>
|
||||||
<li><a href="/led-strips-configuration" class="text-base-content">灯条配置</a></li>
|
<li><A href="/led-strips-configuration" class="text-base-content">灯条配置</A></li>
|
||||||
<li><a href="/white-balance" class="text-base-content">白平衡</a></li>
|
<li><A href="/white-balance" class="text-base-content">白平衡</A></li>
|
||||||
|
<li><A href="/led-strip-test" class="text-base-content">灯带测试</A></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<a class="btn btn-ghost text-xl text-primary font-bold">环境光控制</a>
|
<a class="btn btn-ghost text-xl text-primary font-bold">环境光控制</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="navbar-center hidden lg:flex">
|
<div class="navbar-center hidden lg:flex">
|
||||||
<ul class="menu menu-horizontal px-1">
|
<ul class="menu menu-horizontal px-1">
|
||||||
<li><a href="/info" class="btn btn-ghost text-base-content hover:text-primary">基本信息</a></li>
|
<li><A href="/info" class="btn btn-ghost text-base-content hover:text-primary">基本信息</A></li>
|
||||||
<li><a href="/displays" class="btn btn-ghost text-base-content hover:text-primary">显示器信息</a></li>
|
<li><A href="/displays" class="btn btn-ghost text-base-content hover:text-primary">显示器信息</A></li>
|
||||||
<li><a href="/led-strips-configuration" class="btn btn-ghost text-base-content hover:text-primary">灯条配置</a></li>
|
<li><A href="/led-strips-configuration" class="btn btn-ghost text-base-content hover:text-primary">灯条配置</A></li>
|
||||||
<li><a href="/white-balance" class="btn btn-ghost text-base-content hover:text-primary">白平衡</a></li>
|
<li><A href="/white-balance" class="btn btn-ghost text-base-content hover:text-primary">白平衡</A></li>
|
||||||
|
<li><A href="/led-strip-test" class="btn btn-ghost text-base-content hover:text-primary">灯带测试</A></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="navbar-end">
|
<div class="navbar-end">
|
||||||
@ -61,6 +87,7 @@ function App() {
|
|||||||
<Route path="/displays" component={DisplayStateIndex} />
|
<Route path="/displays" component={DisplayStateIndex} />
|
||||||
<Route path="/led-strips-configuration" component={LedStripConfiguration} />
|
<Route path="/led-strips-configuration" component={LedStripConfiguration} />
|
||||||
<Route path="/white-balance" component={WhiteBalance} />
|
<Route path="/white-balance" component={WhiteBalance} />
|
||||||
|
<Route path="/led-strip-test" element={<LedStripTest />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
293
src/components/led-strip-test/led-strip-test.tsx
Normal file
293
src/components/led-strip-test/led-strip-test.tsx
Normal file
@ -0,0 +1,293 @@
|
|||||||
|
import { createSignal, createEffect, For, Show, onCleanup } from 'solid-js';
|
||||||
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
|
||||||
|
interface BoardInfo {
|
||||||
|
fullname: string;
|
||||||
|
host: string;
|
||||||
|
address: string;
|
||||||
|
port: number;
|
||||||
|
connect_status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TestPattern {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
effect_type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TestEffectConfig {
|
||||||
|
effect_type: string;
|
||||||
|
led_count: number;
|
||||||
|
led_type: string;
|
||||||
|
speed: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LedStripTest = () => {
|
||||||
|
const [boards, setBoards] = createSignal<BoardInfo[]>([]);
|
||||||
|
const [selectedBoard, setSelectedBoard] = createSignal<BoardInfo | null>(null);
|
||||||
|
const [ledCount, setLedCount] = createSignal(60);
|
||||||
|
const [ledType, setLedType] = createSignal<'RGB' | 'RGBW'>('RGB');
|
||||||
|
const [isRunning, setIsRunning] = createSignal(false);
|
||||||
|
const [currentPattern, setCurrentPattern] = createSignal<TestPattern | null>(null);
|
||||||
|
const [animationSpeed, setAnimationSpeed] = createSignal(33); // ~30fps
|
||||||
|
|
||||||
|
// Load available boards
|
||||||
|
createEffect(() => {
|
||||||
|
invoke<BoardInfo[]>('get_boards').then((boardList) => {
|
||||||
|
setBoards(boardList);
|
||||||
|
if (boardList.length > 0 && !selectedBoard()) {
|
||||||
|
setSelectedBoard(boardList[0]);
|
||||||
|
}
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error('Failed to load boards:', error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cleanup when component is unmounted
|
||||||
|
onCleanup(() => {
|
||||||
|
if (isRunning() && selectedBoard()) {
|
||||||
|
// Stop the test effect in backend
|
||||||
|
invoke('stop_led_test_effect', {
|
||||||
|
boardAddress: `${selectedBoard()!.address}:${selectedBoard()!.port}`,
|
||||||
|
ledCount: ledCount(),
|
||||||
|
ledType: ledType()
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error('Failed to stop test during cleanup:', error);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update local state immediately
|
||||||
|
setIsRunning(false);
|
||||||
|
setCurrentPattern(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Test patterns
|
||||||
|
const testPatterns: TestPattern[] = [
|
||||||
|
{
|
||||||
|
name: '流光效果',
|
||||||
|
description: '彩虹色流光,用于测试灯带方向',
|
||||||
|
effect_type: 'FlowingRainbow'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '十个一组计数',
|
||||||
|
description: '每十个LED一组不同颜色,用于快速计算灯珠数量',
|
||||||
|
effect_type: 'GroupCounting'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '单色扫描',
|
||||||
|
description: '单个LED依次点亮,用于精确测试每个LED位置',
|
||||||
|
effect_type: 'SingleScan'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '呼吸灯',
|
||||||
|
description: '整条灯带呼吸效果,用于测试整体亮度',
|
||||||
|
effect_type: 'Breathing'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Test effect management - now handled by Rust backend
|
||||||
|
|
||||||
|
const startTest = async (pattern: TestPattern) => {
|
||||||
|
if (isRunning()) {
|
||||||
|
await stopTest();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selectedBoard()) {
|
||||||
|
console.error('No board selected');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const effectConfig: TestEffectConfig = {
|
||||||
|
effect_type: pattern.effect_type,
|
||||||
|
led_count: ledCount(),
|
||||||
|
led_type: ledType(),
|
||||||
|
speed: 1.0 / (animationSpeed() / 50) // Convert animation speed to effect speed
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start the test effect in Rust backend
|
||||||
|
await invoke('start_led_test_effect', {
|
||||||
|
boardAddress: `${selectedBoard()!.address}:${selectedBoard()!.port}`,
|
||||||
|
effectConfig: effectConfig,
|
||||||
|
updateIntervalMs: animationSpeed()
|
||||||
|
});
|
||||||
|
|
||||||
|
setCurrentPattern(pattern);
|
||||||
|
setIsRunning(true);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to start test effect:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopTest = async () => {
|
||||||
|
if (!selectedBoard()) {
|
||||||
|
setIsRunning(false);
|
||||||
|
setCurrentPattern(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Stop the test effect in Rust backend
|
||||||
|
await invoke('stop_led_test_effect', {
|
||||||
|
boardAddress: `${selectedBoard()!.address}:${selectedBoard()!.port}`,
|
||||||
|
ledCount: ledCount(),
|
||||||
|
ledType: ledType()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Only update UI state after successful backend call
|
||||||
|
setIsRunning(false);
|
||||||
|
setCurrentPattern(null);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to stop test effect:', error);
|
||||||
|
// Still update UI state even if backend call fails
|
||||||
|
setIsRunning(false);
|
||||||
|
setCurrentPattern(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="container mx-auto p-6 space-y-6">
|
||||||
|
<div class="card bg-base-200 shadow-xl">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title text-2xl mb-4">LED Strip Testing</h2>
|
||||||
|
|
||||||
|
{/* Hardware Selection */}
|
||||||
|
<div class="form-control w-full max-w-xs">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text">Select Hardware Board</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
class="select select-bordered w-full max-w-xs"
|
||||||
|
value={selectedBoard()?.host || ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
const board = boards().find(b => b.host === e.target.value);
|
||||||
|
setSelectedBoard(board || null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option disabled value="">Choose a board</option>
|
||||||
|
<For each={boards()}>
|
||||||
|
{(board) => (
|
||||||
|
<option value={board.host}>
|
||||||
|
{board.host} ({board.address}:{board.port})
|
||||||
|
</option>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* LED Configuration */}
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mt-4">
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text">LED Count</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
class="input input-bordered w-full text-center text-lg"
|
||||||
|
value={ledCount()}
|
||||||
|
min="1"
|
||||||
|
max="1000"
|
||||||
|
onInput={(e) => setLedCount(parseInt(e.target.value) || 60)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text">LED Type</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
class="select select-bordered w-full"
|
||||||
|
value={ledType()}
|
||||||
|
onChange={(e) => setLedType(e.target.value as 'RGB' | 'RGBW')}
|
||||||
|
>
|
||||||
|
<option value="RGB">RGB</option>
|
||||||
|
<option value="RGBW">RGBW</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text">Animation Speed (ms)</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
class="input input-bordered w-full text-center"
|
||||||
|
value={animationSpeed()}
|
||||||
|
min="16"
|
||||||
|
max="200"
|
||||||
|
step="1"
|
||||||
|
onInput={(e) => setAnimationSpeed(parseInt(e.target.value) || 33)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Test Patterns */}
|
||||||
|
<div class="card bg-base-200 shadow-xl">
|
||||||
|
<div class="card-body">
|
||||||
|
<h3 class="card-title text-xl mb-4">Test Patterns</h3>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<For each={testPatterns}>
|
||||||
|
{(pattern) => (
|
||||||
|
<div class="card bg-base-100 shadow-md">
|
||||||
|
<div class="card-body">
|
||||||
|
<h4 class="card-title text-lg">{pattern.name}</h4>
|
||||||
|
<p class="text-sm opacity-70 mb-4">{pattern.description}</p>
|
||||||
|
|
||||||
|
<div class="card-actions justify-end">
|
||||||
|
<Show
|
||||||
|
when={currentPattern() === pattern && isRunning()}
|
||||||
|
fallback={
|
||||||
|
<button
|
||||||
|
class="btn btn-primary"
|
||||||
|
onClick={() => startTest(pattern)}
|
||||||
|
disabled={!selectedBoard()}
|
||||||
|
>
|
||||||
|
Start Test
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn btn-error"
|
||||||
|
onClick={() => stopTest()}
|
||||||
|
>
|
||||||
|
Stop Test
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when={isRunning()}>
|
||||||
|
<div class="alert alert-info mt-4">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||||
|
</svg>
|
||||||
|
<span>Test pattern "{currentPattern()?.name}" is running on {selectedBoard()?.host}</span>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show when={!selectedBoard()}>
|
||||||
|
<div class="alert alert-warning mt-4">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.728-.833-2.498 0L3.732 16c-.77.833.192 2.5 1.732 2.5z" />
|
||||||
|
</svg>
|
||||||
|
<span>Please select a hardware board to start testing</span>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
Reference in New Issue
Block a user