Refactor AgvControls and update manual operation UI
This commit is contained in:
15
App.tsx
15
App.tsx
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -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,6 +54,22 @@ const AgvManualControls: React.FC<AgvManualControlsProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className={`${isError ? 'opacity-50 pointer-events-none' : ''}`}>
|
<div className={`${isError ? 'opacity-50 pointer-events-none' : ''}`}>
|
||||||
<div className="flex justify-between items-center mb-2 px-1">
|
<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>
|
<h3 className="text-xs font-bold text-gray-400 uppercase tracking-wider">Manual Operation</h3>
|
||||||
@@ -37,7 +77,16 @@ const AgvManualControls: React.FC<AgvManualControlsProps> = ({
|
|||||||
|
|
||||||
<div className="bg-gray-800 rounded-lg p-3 border border-gray-700 shadow-inner">
|
<div className="bg-gray-800 rounded-lg p-3 border border-gray-700 shadow-inner">
|
||||||
<div className="grid grid-cols-3 gap-2 mb-3">
|
<div className="grid grid-cols-3 gap-2 mb-3">
|
||||||
<div />
|
<button
|
||||||
|
onClick={() => setLiftStatus('UP')}
|
||||||
|
disabled={agvState.liftHeight === 100}
|
||||||
|
className={`h-10 rounded flex items-center justify-center transition-colors shadow ${agvState.liftStatus === 'UP'
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'bg-gray-700 hover:bg-gray-600 text-gray-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<ChevronsUp size={20} />
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
disabled={isRunning}
|
disabled={isRunning}
|
||||||
onMouseDown={() => setManualMotion(AgvMotionState.FORWARD, 'FWD')}
|
onMouseDown={() => setManualMotion(AgvMotionState.FORWARD, 'FWD')}
|
||||||
@@ -46,7 +95,16 @@ const AgvManualControls: React.FC<AgvManualControlsProps> = ({
|
|||||||
>
|
>
|
||||||
<ArrowUp size={24} />
|
<ArrowUp size={24} />
|
||||||
</button>
|
</button>
|
||||||
<div />
|
<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
|
<button
|
||||||
disabled={isRunning}
|
disabled={isRunning}
|
||||||
@@ -71,7 +129,16 @@ const AgvManualControls: React.FC<AgvManualControlsProps> = ({
|
|||||||
<RotateCw size={20} />
|
<RotateCw size={20} />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div />
|
<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
|
<button
|
||||||
disabled={isRunning}
|
disabled={isRunning}
|
||||||
onMouseDown={() => setManualMotion(AgvMotionState.BACKWARD, 'BWD')}
|
onMouseDown={() => setManualMotion(AgvMotionState.BACKWARD, 'BWD')}
|
||||||
@@ -80,7 +147,19 @@ const AgvManualControls: React.FC<AgvManualControlsProps> = ({
|
|||||||
>
|
>
|
||||||
<ArrowDown size={24} />
|
<ArrowDown size={24} />
|
||||||
</button>
|
</button>
|
||||||
<div />
|
<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>
|
</div>
|
||||||
|
|
||||||
{/* Extra Actions */}
|
{/* Extra Actions */}
|
||||||
@@ -102,8 +181,7 @@ const AgvManualControls: React.FC<AgvManualControlsProps> = ({
|
|||||||
<button
|
<button
|
||||||
disabled={!isRunning || agvState.motionState === AgvMotionState.MARK_STOPPING}
|
disabled={!isRunning || agvState.motionState === AgvMotionState.MARK_STOPPING}
|
||||||
onClick={handleMarkStop}
|
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 ${
|
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
|
||||||
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'
|
||||||
}`}
|
}`}
|
||||||
@@ -113,6 +191,7 @@ const AgvManualControls: React.FC<AgvManualControlsProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user