Files
desktop/src/components/led-strip-test/led-strip-test.tsx
Ivan Li 7e2dafa3d2 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
2025-07-06 02:37:15 +08:00

294 lines
9.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
};