Refactor AgvControls and update manual operation UI

This commit is contained in:
backuppc
2025-12-19 10:35:19 +09:00
parent b2a26bc67d
commit a43c2d769e
3 changed files with 167 additions and 234 deletions

15
App.tsx
View File

@@ -4,7 +4,7 @@ import { INITIAL_MAP, MAX_SPEED } from './constants';
import EditorToolbar from './components/EditorToolbar'; import EditorToolbar from './components/EditorToolbar';
import SimulationCanvas from './components/SimulationCanvas'; import SimulationCanvas from './components/SimulationCanvas';
import SerialConsole from './components/SerialConsole'; import SerialConsole from './components/SerialConsole';
import AgvControls from './components/AgvControls'; import AgvManualControls from './components/AgvManualControls';
import BmsPanel from './components/BmsPanel'; import BmsPanel from './components/BmsPanel';
import AcsControls from './components/AcsControls'; import AcsControls from './components/AcsControls';
import AgvStatusPanel from './components/AgvStatusPanel'; import AgvStatusPanel from './components/AgvStatusPanel';
@@ -1326,16 +1326,15 @@ const App: React.FC = () => {
{/* Top: Controls (Scrollable if needed) */} {/* Top: Controls (Scrollable if needed) */}
<div className="flex-1 overflow-y-auto"> <div className="flex-1 overflow-y-auto">
<AgvControls {/* Manual Controls */}
<AgvManualControls
agvState={agvState} agvState={agvState}
setMotion={(m) => setAgvState(s => ({ ...s, motionState: m }))} setMotion={(state) => setAgvState(s => ({ ...s, motionState: state }))}
setLift={(h) => setAgvState(s => ({ ...s, liftHeight: h }))} setRunConfig={(config) => setAgvState(s => ({ ...s, runConfig: config }))}
setRunConfig={(c) => setAgvState(s => ({ ...s, runConfig: c }))} setError={(err) => setAgvState(s => ({ ...s, error: err }))}
setError={(e) => setAgvState(s => ({ ...s, error: e }))}
onTurn180={handleTurn180} onTurn180={handleTurn180}
setMagnet={(isOn) => setAgvState(s => ({ ...s, magnetOn: isOn }))}
setLiftStatus={(status) => setAgvState(s => ({ ...s, liftStatus: status }))} setLiftStatus={(status) => setAgvState(s => ({ ...s, liftStatus: status }))}
setLidar={(isOn) => setAgvState(s => ({ ...s, lidarEnabled: isOn, sensorStatus: isOn ? '1' : '0' }))} setMagnet={(isOn) => setAgvState(s => ({ ...s, magnetOn: isOn }))}
/> />
</div> </div>

View File

@@ -1,145 +0,0 @@
import React from 'react';
import { StopCircle, Play, Square, AlertTriangle, ChevronsUp, ChevronsDown, Magnet, Radar, ArrowLeft, ArrowRight } from 'lucide-react';
import { AgvState, AgvMotionState, AgvRunConfig } from '../types';
import AgvManualControls from './AgvManualControls';
interface AgvControlsProps {
agvState: AgvState;
setMotion: (state: AgvMotionState) => void;
setLift: (val: number) => void;
setRunConfig: (config: AgvRunConfig) => void;
setError: (error: string | null) => void;
onTurn180: (direction: 'LEFT' | 'RIGHT') => void;
setMagnet: (isOn: boolean) => void;
setLiftStatus: (status: 'IDLE' | 'UP' | 'DOWN') => void;
setLidar: (isOn: boolean) => void;
}
const AgvControls: React.FC<AgvControlsProps> = ({ agvState, setMotion, setLift, setRunConfig, setError, onTurn180, setMagnet, setLiftStatus, setLidar }) => {
const isRunning = agvState.motionState === AgvMotionState.RUNNING || agvState.motionState === AgvMotionState.MARK_STOPPING;
const isError = agvState.error !== null;
const updateRunConfig = (key: keyof AgvRunConfig, value: any) => {
if (isError) return;
setRunConfig({
...agvState.runConfig,
[key]: value
});
};
const handleMarkStop = () => {
if (agvState.motionState === AgvMotionState.RUNNING) {
setRunConfig({
...agvState.runConfig,
speedLevel: 'L'
});
setMotion(AgvMotionState.MARK_STOPPING);
}
};
const resetError = () => {
setError(null);
};
const handleLiftSliderChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (agvState.liftStatus !== 'IDLE') {
setLiftStatus('IDLE');
}
setLift(parseInt(e.target.value));
};
return (
<div className="flex flex-col gap-4 p-3 bg-gray-900 border-b border-gray-700">
{/* Error Overlay */}
{isError && (
<div className="bg-red-900/80 border border-red-600 p-3 rounded text-center animate-pulse">
<div className="text-red-100 font-bold text-sm flex items-center justify-center gap-2 mb-2">
<AlertTriangle size={18} />
{agvState.error}
</div>
<button onClick={resetError} className="bg-red-600 hover:bg-red-500 text-white text-xs font-bold px-3 py-1 rounded shadow-sm">
RESET ERROR
</button>
</div>
)}
{/* Manual Operation (분리된 콤포넌트) */}
<AgvManualControls
agvState={agvState}
setMotion={setMotion}
updateRunConfig={updateRunConfig}
onTurn180={onTurn180}
handleMarkStop={handleMarkStop}
isRunning={isRunning}
isError={isError}
/>
{/* Lift & Magnet */}
<div className={`px-1 ${isError ? 'opacity-50 pointer-events-none' : ''}`}>
<div className="flex justify-between text-xs text-gray-400 mb-1">
<span className="flex items-center gap-1"><ChevronsUp size={12} /> Lift Height</span>
<span className="font-mono text-white">{Math.round(agvState.liftHeight)}%</span>
</div>
<div className="flex items-center gap-2 mb-2">
<button
onClick={() => setLiftStatus('DOWN')}
disabled={agvState.liftHeight === 0}
className={`p-1.5 rounded disabled:opacity-30 transition-colors ${agvState.liftStatus === 'DOWN'
? 'bg-blue-600 text-white shadow-[0_0_10px_rgba(37,99,235,0.5)]'
: 'bg-gray-700 hover:bg-gray-600 text-gray-300'
}`}
title="Lower Lift"
>
<ChevronsDown size={16} />
</button>
<input
type="range"
min="0"
max="100"
value={agvState.liftHeight}
onChange={handleLiftSliderChange}
className="flex-1 h-1.5 bg-gray-700 rounded-lg appearance-none cursor-pointer accent-blue-500"
/>
<button
onClick={() => setLiftStatus('UP')}
disabled={agvState.liftHeight === 100}
className={`p-1.5 rounded disabled:opacity-30 transition-colors ${agvState.liftStatus === 'UP'
? 'bg-blue-600 text-white shadow-[0_0_10px_rgba(37,99,235,0.5)]'
: 'bg-gray-700 hover:bg-gray-600 text-gray-300'
}`}
title="Raise Lift"
>
<ChevronsUp size={16} />
</button>
</div>
<button
onClick={() => setMagnet(!agvState.magnetOn)}
className={`w-full py-1.5 text-xs rounded border flex items-center justify-center gap-2 transition-colors ${agvState.magnetOn
? 'bg-orange-600 text-white border-orange-500'
: 'bg-gray-800 text-gray-400 border-gray-600 hover:bg-gray-700'
}`}
>
<Magnet size={14} className={agvState.magnetOn ? "animate-pulse" : ""} />
MAGNET {agvState.magnetOn ? 'ON' : 'OFF'}
</button>
</div>
<div className="h-px bg-gray-800" />
{/* Auto Run Controls */}
{/* Auto Run Controls (분리된 콤포넌트) */}
</div>
);
};
export default AgvControls;

View File

@@ -1,27 +1,51 @@
import React from 'react'; import React from 'react';
import { ArrowUp, ArrowDown, RotateCcw, RotateCw, StopCircle, Disc } from 'lucide-react'; import { ArrowUp, ArrowDown, RotateCcw, RotateCw, StopCircle, Disc, ChevronsUp, ChevronsDown, Magnet, AlertTriangle } from 'lucide-react';
import { AgvState, AgvMotionState, AgvRunConfig } from '../types'; import { AgvState, AgvMotionState, AgvRunConfig } from '../types';
interface AgvManualControlsProps { interface AgvManualControlsProps {
agvState: AgvState; agvState: AgvState;
setMotion: (state: AgvMotionState) => void; setMotion: (state: AgvMotionState) => void;
updateRunConfig: (key: keyof AgvRunConfig, value: any) => void; setRunConfig: (config: AgvRunConfig) => void;
setError: (error: string | null) => void;
onTurn180: (direction: 'LEFT' | 'RIGHT') => void; onTurn180: (direction: 'LEFT' | 'RIGHT') => void;
handleMarkStop: () => void; setLiftStatus: (status: 'IDLE' | 'UP' | 'DOWN') => void;
isRunning: boolean; setMagnet: (isOn: boolean) => void;
isError: boolean;
} }
const AgvManualControls: React.FC<AgvManualControlsProps> = ({ const AgvManualControls: React.FC<AgvManualControlsProps> = ({
agvState, agvState,
setMotion, setMotion,
updateRunConfig, setRunConfig,
setError,
onTurn180, onTurn180,
handleMarkStop, setLiftStatus,
isRunning, setMagnet,
isError,
}) => { }) => {
const isRunning = agvState.motionState === AgvMotionState.RUNNING || agvState.motionState === AgvMotionState.MARK_STOPPING;
const isError = agvState.error !== null;
const updateRunConfig = (key: keyof AgvRunConfig, value: any) => {
if (isError) return;
setRunConfig({
...agvState.runConfig,
[key]: value
});
};
const handleMarkStop = () => {
if (agvState.motionState === AgvMotionState.RUNNING) {
setRunConfig({
...agvState.runConfig,
speedLevel: 'L'
});
setMotion(AgvMotionState.MARK_STOPPING);
}
};
const resetError = () => {
setError(null);
};
const setManualMotion = (motion: AgvMotionState, direction?: 'FWD' | 'BWD') => { const setManualMotion = (motion: AgvMotionState, direction?: 'FWD' | 'BWD') => {
setMotion(motion); setMotion(motion);
if (direction) { if (direction) {
@@ -30,86 +54,141 @@ const AgvManualControls: React.FC<AgvManualControlsProps> = ({
}; };
return ( return (
<div className={`${isError ? 'opacity-50 pointer-events-none' : ''}`}>
<div className="flex justify-between items-center mb-2 px-1">
<h3 className="text-xs font-bold text-gray-400 uppercase tracking-wider">Manual Operation</h3>
</div>
<div className="bg-gray-800 rounded-lg p-3 border border-gray-700 shadow-inner"> <div className="flex flex-col gap-4 p-3 bg-gray-900 border-b border-gray-700">
<div className="grid grid-cols-3 gap-2 mb-3">
<div />
<button
disabled={isRunning}
onMouseDown={() => setManualMotion(AgvMotionState.FORWARD, 'FWD')}
onMouseUp={() => setManualMotion(AgvMotionState.IDLE)}
className="h-10 bg-gray-700 hover:bg-gray-600 rounded flex items-center justify-center active:bg-blue-600 disabled:opacity-30 transition-colors shadow"
>
<ArrowUp size={24} />
</button>
<div />
<button {/* Error Overlay */}
disabled={isRunning} {isError && (
onMouseDown={() => setManualMotion(AgvMotionState.TURN_LEFT)} <div className="bg-red-900/80 border border-red-600 p-3 rounded text-center animate-pulse">
onMouseUp={() => setManualMotion(AgvMotionState.IDLE)} <div className="text-red-100 font-bold text-sm flex items-center justify-center gap-2 mb-2">
className="h-10 bg-gray-700 hover:bg-gray-600 rounded flex items-center justify-center active:bg-blue-600 disabled:opacity-30 transition-colors shadow" <AlertTriangle size={18} />
> {agvState.error}
<RotateCcw size={20} /> </div>
</button> <button onClick={resetError} className="bg-red-600 hover:bg-red-500 text-white text-xs font-bold px-3 py-1 rounded shadow-sm">
<button RESET ERROR
onClick={() => setManualMotion(AgvMotionState.IDLE)}
className="h-10 bg-red-800 hover:bg-red-700 rounded flex items-center justify-center text-white shadow"
>
<StopCircle size={24} />
</button>
<button
disabled={isRunning}
onMouseDown={() => setManualMotion(AgvMotionState.TURN_RIGHT)}
onMouseUp={() => setManualMotion(AgvMotionState.IDLE)}
className="h-10 bg-gray-700 hover:bg-gray-600 rounded flex items-center justify-center active:bg-blue-600 disabled:opacity-30 transition-colors shadow"
>
<RotateCw size={20} />
</button> </button>
</div>
)}
<div /> <div className={`${isError ? 'opacity-50 pointer-events-none' : ''}`}>
<button <div className="flex justify-between items-center mb-2 px-1">
disabled={isRunning} <h3 className="text-xs font-bold text-gray-400 uppercase tracking-wider">Manual Operation</h3>
onMouseDown={() => setManualMotion(AgvMotionState.BACKWARD, 'BWD')}
onMouseUp={() => setManualMotion(AgvMotionState.IDLE)}
className="h-10 bg-gray-700 hover:bg-gray-600 rounded flex items-center justify-center active:bg-blue-600 disabled:opacity-30 transition-colors shadow"
>
<ArrowDown size={24} />
</button>
<div />
</div> </div>
{/* Extra Actions */} <div className="bg-gray-800 rounded-lg p-3 border border-gray-700 shadow-inner">
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-3 gap-2 mb-3">
<button <button
disabled={isRunning} onClick={() => setLiftStatus('UP')}
onClick={() => onTurn180('LEFT')} disabled={agvState.liftHeight === 100}
className="text-xs py-1.5 bg-gray-700 rounded hover:bg-gray-600 flex items-center justify-center gap-1 disabled:opacity-30 shadow-sm" className={`h-10 rounded flex items-center justify-center transition-colors shadow ${agvState.liftStatus === 'UP'
> ? 'bg-blue-600 text-white'
<RotateCcw size={12} /> L-Turn 180 : 'bg-gray-700 hover:bg-gray-600 text-gray-300'
</button> }`}
<button >
disabled={isRunning} <ChevronsUp size={20} />
onClick={() => onTurn180('RIGHT')} </button>
className="text-xs py-1.5 bg-gray-700 rounded hover:bg-gray-600 flex items-center justify-center gap-1 disabled:opacity-30 shadow-sm" <button
> disabled={isRunning}
<RotateCw size={12} /> R-Turn 180 onMouseDown={() => setManualMotion(AgvMotionState.FORWARD, 'FWD')}
</button> onMouseUp={() => setManualMotion(AgvMotionState.IDLE)}
<button className="h-10 bg-gray-700 hover:bg-gray-600 rounded flex items-center justify-center active:bg-blue-600 disabled:opacity-30 transition-colors shadow"
disabled={!isRunning || agvState.motionState === AgvMotionState.MARK_STOPPING} >
onClick={handleMarkStop} <ArrowUp size={24} />
className={`col-span-2 text-xs py-1.5 border rounded flex items-center justify-center gap-1 shadow-sm transition-colors disabled:opacity-30 ${ </button>
agvState.motionState === AgvMotionState.MARK_STOPPING <button
onClick={() => setMagnet(true)}
disabled={agvState.magnetOn}
className={`h-10 rounded flex items-center justify-center transition-colors shadow ${agvState.magnetOn
? 'bg-orange-600 text-white opacity-50 cursor-not-allowed'
: 'bg-gray-700 hover:bg-gray-600 text-gray-300'
}`}
>
<Magnet size={20} />
</button>
<button
disabled={isRunning}
onMouseDown={() => setManualMotion(AgvMotionState.TURN_LEFT)}
onMouseUp={() => setManualMotion(AgvMotionState.IDLE)}
className="h-10 bg-gray-700 hover:bg-gray-600 rounded flex items-center justify-center active:bg-blue-600 disabled:opacity-30 transition-colors shadow"
>
<RotateCcw size={20} />
</button>
<button
onClick={() => setManualMotion(AgvMotionState.IDLE)}
className="h-10 bg-red-800 hover:bg-red-700 rounded flex items-center justify-center text-white shadow"
>
<StopCircle size={24} />
</button>
<button
disabled={isRunning}
onMouseDown={() => setManualMotion(AgvMotionState.TURN_RIGHT)}
onMouseUp={() => setManualMotion(AgvMotionState.IDLE)}
className="h-10 bg-gray-700 hover:bg-gray-600 rounded flex items-center justify-center active:bg-blue-600 disabled:opacity-30 transition-colors shadow"
>
<RotateCw size={20} />
</button>
<button
onClick={() => setLiftStatus('DOWN')}
disabled={agvState.liftHeight === 0}
className={`h-10 rounded flex items-center justify-center transition-colors shadow ${agvState.liftStatus === 'DOWN'
? 'bg-blue-600 text-white'
: 'bg-gray-700 hover:bg-gray-600 text-gray-300'
}`}
>
<ChevronsDown size={20} />
</button>
<button
disabled={isRunning}
onMouseDown={() => setManualMotion(AgvMotionState.BACKWARD, 'BWD')}
onMouseUp={() => setManualMotion(AgvMotionState.IDLE)}
className="h-10 bg-gray-700 hover:bg-gray-600 rounded flex items-center justify-center active:bg-blue-600 disabled:opacity-30 transition-colors shadow"
>
<ArrowDown size={24} />
</button>
<button
onClick={() => setMagnet(false)}
disabled={!agvState.magnetOn}
className={`h-10 rounded flex items-center justify-center transition-colors shadow relative ${!agvState.magnetOn
? 'bg-gray-600 text-gray-500 cursor-not-allowed opacity-50'
: 'bg-gray-700 hover:bg-gray-600 text-gray-300'
}`}
>
<Magnet size={20} />
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
<div className="w-6 h-0.5 bg-red-500 rotate-45 transform origin-center"></div>
</div>
</button>
</div>
{/* Extra Actions */}
<div className="grid grid-cols-2 gap-2">
<button
disabled={isRunning}
onClick={() => onTurn180('LEFT')}
className="text-xs py-1.5 bg-gray-700 rounded hover:bg-gray-600 flex items-center justify-center gap-1 disabled:opacity-30 shadow-sm"
>
<RotateCcw size={12} /> L-Turn 180
</button>
<button
disabled={isRunning}
onClick={() => onTurn180('RIGHT')}
className="text-xs py-1.5 bg-gray-700 rounded hover:bg-gray-600 flex items-center justify-center gap-1 disabled:opacity-30 shadow-sm"
>
<RotateCw size={12} /> R-Turn 180
</button>
<button
disabled={!isRunning || agvState.motionState === AgvMotionState.MARK_STOPPING}
onClick={handleMarkStop}
className={`col-span-2 text-xs py-1.5 border rounded flex items-center justify-center gap-1 shadow-sm transition-colors disabled:opacity-30 ${agvState.motionState === AgvMotionState.MARK_STOPPING
? 'bg-yellow-600 text-black border-yellow-500 font-bold' ? 'bg-yellow-600 text-black border-yellow-500 font-bold'
: 'bg-yellow-900/50 text-yellow-200 border-yellow-800/50 hover:bg-yellow-900/70' : 'bg-yellow-900/50 text-yellow-200 border-yellow-800/50 hover:bg-yellow-900/70'
}`} }`}
> >
<Disc size={12} /> {agvState.motionState === AgvMotionState.MARK_STOPPING ? 'Stopping at Mark...' : 'Drive to Next Mark'} <Disc size={12} /> {agvState.motionState === AgvMotionState.MARK_STOPPING ? 'Stopping at Mark...' : 'Drive to Next Mark'}
</button> </button>
</div>
</div> </div>
</div> </div>
</div> </div>