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:
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 { 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<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(() => {
|
||||
invoke<LedStripConfigContainer>('read_config').then((config) => {
|
||||
setLedStripStore({
|
||||
@ -33,20 +57,22 @@ function App() {
|
||||
</svg>
|
||||
</div>
|
||||
<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="/displays" 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="/info" 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="/white-balance" class="text-base-content">白平衡</A></li>
|
||||
<li><A href="/led-strip-test" class="text-base-content">灯带测试</A></li>
|
||||
</ul>
|
||||
</div>
|
||||
<a class="btn btn-ghost text-xl text-primary font-bold">环境光控制</a>
|
||||
</div>
|
||||
<div class="navbar-center hidden lg:flex">
|
||||
<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="/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="/white-balance" 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="/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="/led-strip-test" class="btn btn-ghost text-base-content hover:text-primary">灯带测试</A></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
@ -61,6 +87,7 @@ function App() {
|
||||
<Route path="/displays" component={DisplayStateIndex} />
|
||||
<Route path="/led-strips-configuration" component={LedStripConfiguration} />
|
||||
<Route path="/white-balance" component={WhiteBalance} />
|
||||
<Route path="/led-strip-test" element={<LedStripTest />} />
|
||||
</Routes>
|
||||
</main>
|
||||
</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