349 lines
12 KiB
TypeScript
349 lines
12 KiB
TypeScript
import { createSignal, createEffect, For, Show, onCleanup } from 'solid-js';
|
|
import { invoke } from '@tauri-apps/api/core';
|
|
import { listen } from '@tauri-apps/api/event';
|
|
import { useLanguage } from '../../i18n/index';
|
|
|
|
interface BoardInfo {
|
|
fullname: string;
|
|
host: string;
|
|
address: string;
|
|
port: number;
|
|
connect_status: 'Connected' | 'Disconnected' | { Connecting: number };
|
|
}
|
|
|
|
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 { t } = useLanguage();
|
|
const [boards, setBoards] = createSignal<BoardInfo[]>([]);
|
|
const [selectedBoard, setSelectedBoard] = createSignal<BoardInfo | null>(null);
|
|
const [ledCount, setLedCount] = createSignal(60);
|
|
const [ledType, setLedType] = createSignal<'WS2812B' | 'SK6812'>('WS2812B');
|
|
const [isRunning, setIsRunning] = createSignal(false);
|
|
const [currentPattern, setCurrentPattern] = createSignal<TestPattern | null>(null);
|
|
const [animationSpeed, setAnimationSpeed] = createSignal(33); // ~30fps
|
|
|
|
// Load available boards and listen for changes
|
|
createEffect(() => {
|
|
// Initial load
|
|
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);
|
|
});
|
|
|
|
// Listen for board changes
|
|
const unlisten = listen<BoardInfo[]>('boards_changed', (event) => {
|
|
const boardList = event.payload;
|
|
setBoards(boardList);
|
|
|
|
// If currently selected board is no longer available, select the first available one
|
|
const currentBoard = selectedBoard();
|
|
if (currentBoard) {
|
|
const stillExists = boardList.find(board =>
|
|
board.host === currentBoard.host &&
|
|
board.address === currentBoard.address &&
|
|
board.port === currentBoard.port
|
|
);
|
|
|
|
if (stillExists) {
|
|
// Update to the new board object to reflect any status changes
|
|
setSelectedBoard(stillExists);
|
|
} else {
|
|
// Current board is no longer available, select first available or null
|
|
setSelectedBoard(boardList.length > 0 ? boardList[0] : null);
|
|
}
|
|
} else if (boardList.length > 0) {
|
|
// No board was selected, select the first one
|
|
setSelectedBoard(boardList[0]);
|
|
}
|
|
});
|
|
|
|
// Cleanup listener when effect is disposed
|
|
onCleanup(() => {
|
|
unlisten.then((unlistenFn) => unlistenFn());
|
|
});
|
|
});
|
|
|
|
// 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: t('ledTest.flowingRainbow'),
|
|
description: t('ledTest.flowingRainbowDesc'),
|
|
effect_type: 'FlowingRainbow'
|
|
},
|
|
{
|
|
name: t('ledTest.groupCounting'),
|
|
description: t('ledTest.groupCountingDesc'),
|
|
effect_type: 'GroupCounting'
|
|
},
|
|
{
|
|
name: t('ledTest.singleScan'),
|
|
description: t('ledTest.singleScanDesc'),
|
|
effect_type: 'SingleScan'
|
|
},
|
|
{
|
|
name: t('ledTest.breathing'),
|
|
description: t('ledTest.breathingDesc'),
|
|
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">{t('ledTest.title')}</h2>
|
|
|
|
{/* Hardware Selection */}
|
|
<div class="form-control w-full max-w-xs">
|
|
<label class="label">
|
|
<span class="label-text">{t('ledTest.selectHardwareBoard')}</span>
|
|
<span class="label-text-alt">
|
|
{boards().length > 0 ? `${boards().length} ${t('ledTest.devicesFound')}` : t('ledTest.searching')}
|
|
</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="">
|
|
{boards().length > 0 ? t('ledTest.chooseBoard') : t('ledTest.noBoardsFound')}
|
|
</option>
|
|
<For each={boards()}>
|
|
{(board) => {
|
|
const getStatusIcon = (status: BoardInfo['connect_status']) => {
|
|
if (status === 'Connected') return '🟢';
|
|
if (typeof status === 'object' && 'Connecting' in status) return '🟡';
|
|
return '🔴';
|
|
};
|
|
|
|
const getStatusText = (status: BoardInfo['connect_status']) => {
|
|
if (status === 'Connected') return t('ledTest.connected');
|
|
if (typeof status === 'object' && 'Connecting' in status) return t('ledTest.connecting');
|
|
return t('ledTest.disconnected');
|
|
};
|
|
|
|
return (
|
|
<option value={board.host}>
|
|
{getStatusIcon(board.connect_status)} {board.host} ({board.address}:{board.port}) - {getStatusText(board.connect_status)}
|
|
</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">{t('ledTest.ledCount')}</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">{t('ledTest.ledType')}</span>
|
|
</label>
|
|
<select
|
|
class="select select-bordered w-full"
|
|
value={ledType()}
|
|
onChange={(e) => setLedType(e.target.value as 'WS2812B' | 'SK6812')}
|
|
>
|
|
<option value="WS2812B">WS2812B</option>
|
|
<option value="SK6812">SK6812</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-control">
|
|
<label class="label">
|
|
<span class="label-text">{t('ledTest.animationSpeed')}</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()}
|
|
>
|
|
{t('ledTest.startTestButton')}
|
|
</button>
|
|
}
|
|
>
|
|
<button
|
|
class="btn btn-error"
|
|
onClick={() => stopTest()}
|
|
>
|
|
{t('ledTest.stopTest')}
|
|
</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>
|
|
);
|
|
};
|