diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index dbbb183..70fd122 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4432,6 +4432,7 @@ dependencies = [ "tokio", "tokio-stream", "tokio-tungstenite", + "tokio-util", "toml 0.7.8", "url-build-parse", ] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index a549e4e..6d4d505 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -23,6 +23,7 @@ core-graphics = "0.22.3" display-info = "0.4.1" anyhow = "1.0.69" tokio = {version = "1.26.0", features = ["full"] } +tokio-util = "0.7" paris = { version = "1.5", features = ["timestamps", "macros"] } log = "0.4.17" env_logger = "0.10.0" diff --git a/src-tauri/src/ambient_light/publisher.rs b/src-tauri/src/ambient_light/publisher.rs index bea78d8..83a0b08 100644 --- a/src-tauri/src/ambient_light/publisher.rs +++ b/src-tauri/src/ambient_light/publisher.rs @@ -26,6 +26,7 @@ pub struct LedColorsPublisher { colors_rx: Arc>>>, colors_tx: Arc>>>, inner_tasks_version: Arc>, + test_mode_active: Arc>, } impl LedColorsPublisher { @@ -44,6 +45,7 @@ impl LedColorsPublisher { colors_rx: Arc::new(RwLock::new(rx)), colors_tx: Arc::new(RwLock::new(tx)), inner_tasks_version: Arc::new(RwLock::new(0)), + test_mode_active: Arc::new(RwLock::new(false)), } }) .await @@ -81,12 +83,20 @@ impl LedColorsPublisher { let mappers = mappers.clone(); - match Self::send_colors_by_display(colors, mappers, &strips, &color_calibration).await { - Ok(_) => { - // log::info!("sent colors: #{: >15}", display_id); - } - Err(err) => { - warn!("Failed to send colors: #{: >15}\t{}", display_id, err); + // 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 { + Ok(_) => { + // log::info!("sent colors: #{: >15}", display_id); + } + Err(err) => { + warn!("Failed to send colors: #{: >15}\t{}", display_id, err); + } } } @@ -532,6 +542,35 @@ impl LedColorsPublisher { pub async fn clone_colors_receiver(&self) -> watch::Receiver> { 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)] diff --git a/src-tauri/src/led_test_effects.rs b/src-tauri/src/led_test_effects.rs new file mode 100644 index 0000000..cbba028 --- /dev/null +++ b/src-tauri/src/led_test_effects.rs @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 + } +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 45de72a..8da5e4c 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -4,6 +4,7 @@ mod ambient_light; mod display; mod led_color; +mod led_test_effects; mod rpc; mod screenshot; mod screenshot_manager; @@ -13,6 +14,7 @@ mod volume; use ambient_light::{Border, ColorCalibration, LedStripConfig, LedStripConfigGroup, LedType}; use display::{DisplayManager, DisplayState}; use display_info::DisplayInfo; +use led_test_effects::{LedTestEffects, TestEffectConfig, TestEffectType}; use paris::{error, info, warn}; use rpc::{BoardInfo, UdpRpc}; use screenshot::Screenshot; @@ -24,6 +26,14 @@ use tauri::{Manager, Emitter, Runtime}; use regex; use tauri::http::{Request, Response}; 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>>>> = + tokio::sync::OnceCell::const_new(); +static CANCEL_TOKEN: tokio::sync::OnceCell>>> = + tokio::sync::OnceCell::const_new(); #[derive(Serialize, Deserialize)] #[serde(remote = "DisplayInfo")] struct DisplayInfoDef { @@ -140,10 +150,6 @@ async fn patch_led_strip_len(display_id: u32, border: Border, delta_len: i8) -> #[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) @@ -153,7 +159,6 @@ async fn patch_led_strip_type(display_id: u32, border: Border, led_type: LedType e.to_string() })?; - info!("patch_led_strip_type: ok"); Ok(()) } @@ -167,6 +172,193 @@ async fn send_colors(offset: u16, buffer: Vec) -> Result<(), String> { }) } +#[tauri::command] +async fn send_test_colors_to_board(board_address: String, offset: u16, buffer: Vec) -> 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 { + 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) -> Result<(), Box> { + 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] async fn move_strip_part( display_id: u32, @@ -356,20 +548,7 @@ fn handle_ambient_light_protocol( async fn main() { env_logger::init(); - // Debug: Print available displays - 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); - } - } + // Initialize display info (removed debug output) tokio::spawn(async move { let screenshot_manager = ScreenshotManager::global().await; @@ -404,6 +583,12 @@ async fn main() { patch_led_strip_len, patch_led_strip_type, 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, reverse_led_strip_part, set_color_calibration, diff --git a/src/App.tsx b/src/App.tsx index 31cb935..65c9b9e 100644 --- a/src/App.tsx +++ b/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 { 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 { setLedStripStore } from './stores/led-strip.store'; 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'; function App() { + const location = useLocation(); + const [previousPath, setPreviousPath] = createSignal(''); + + // 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(() => { invoke('read_config').then((config) => { setLedStripStore({ @@ -33,20 +57,22 @@ function App() { 环境光控制 diff --git a/src/components/led-strip-test/led-strip-test.tsx b/src/components/led-strip-test/led-strip-test.tsx new file mode 100644 index 0000000..ff4f81d --- /dev/null +++ b/src/components/led-strip-test/led-strip-test.tsx @@ -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([]); + const [selectedBoard, setSelectedBoard] = createSignal(null); + const [ledCount, setLedCount] = createSignal(60); + const [ledType, setLedType] = createSignal<'RGB' | 'RGBW'>('RGB'); + const [isRunning, setIsRunning] = createSignal(false); + const [currentPattern, setCurrentPattern] = createSignal(null); + const [animationSpeed, setAnimationSpeed] = createSignal(33); // ~30fps + + // Load available boards + createEffect(() => { + invoke('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 ( +
+
+
+

LED Strip Testing

+ + {/* Hardware Selection */} +
+ + +
+ + {/* LED Configuration */} +
+
+ + setLedCount(parseInt(e.target.value) || 60)} + /> +
+ +
+ + +
+ +
+ + setAnimationSpeed(parseInt(e.target.value) || 33)} + /> +
+
+
+
+ + {/* Test Patterns */} +
+
+

Test Patterns

+ +
+ + {(pattern) => ( +
+
+

{pattern.name}

+

{pattern.description}

+ +
+ startTest(pattern)} + disabled={!selectedBoard()} + > + Start Test + + } + > + + +
+
+
+ )} +
+
+ + +
+ + + + Test pattern "{currentPattern()?.name}" is running on {selectedBoard()?.host} +
+
+ + +
+ + + + Please select a hardware board to start testing +
+
+
+
+
+ ); +};