\"feat_Enhance_BMS_Simulation_SystemLog_UI_and_ACS_Controls\"

This commit is contained in:
2025-12-19 00:52:33 +09:00
parent 051138489b
commit b2a26bc67d
7 changed files with 396 additions and 241 deletions

View File

@@ -3,140 +3,167 @@ import { Navigation, Package, Zap, ChevronRight, Hash, Type } from 'lucide-react
import { SimulationMap } from '../types';
interface AcsControlsProps {
onSend: (cmd: number, data?: number[]) => void;
isConnected: boolean;
mapData: SimulationMap;
onSend: (cmd: number, data?: number[]) => void;
isConnected: boolean;
mapData: SimulationMap;
}
const AcsControls: React.FC<AcsControlsProps> = ({ onSend, isConnected, mapData }) => {
const [tagId, setTagId] = useState('0001');
const [alias, setAlias] = useState('charger1');
const [tagId, setTagId] = useState('0001');
const [alias, setAlias] = useState('charger1');
const PRESET_ALIASES = [
'charger1', 'charger2',
'loader', 'unloader', 'cleaner',
'buffer1', 'buffer2', 'buffer3', 'buffer4', 'buffer5', 'buffer6'
];
const PRESET_ALIASES = [
'charger1', 'charger2',
'loader', 'unloader', 'cleaner',
'buffer1', 'buffer2', 'buffer3', 'buffer4', 'buffer5', 'buffer6'
];
const availableTags = useMemo(() => {
// Extract unique, non-empty RFID tags from map nodes
const tags = mapData.nodes
.filter(n => n.rfidId && n.rfidId.trim() !== '')
.map(n => n.rfidId);
return Array.from(new Set(tags)).sort();
}, [mapData]);
const availableTags = useMemo(() => {
// Extract unique, non-empty RFID tags from map nodes
const tags = mapData.nodes
.filter(n => n.rfidId && n.rfidId.trim() !== '')
.map(n => n.rfidId);
return Array.from(new Set(tags)).sort();
}, [mapData]);
const handleSend = (cmd: number, dataStr?: string, dataByte?: number) => {
if (!isConnected) return;
let payload: number[] = [];
if (dataStr !== undefined) {
// Convert string to ASCII bytes
for (let i = 0; i < dataStr.length; i++) {
payload.push(dataStr.charCodeAt(i));
const handleSend = (cmd: number, dataStr?: string, dataByte?: number) => {
if (!isConnected) return;
let payload: number[] = [];
if (dataStr !== undefined) {
// Convert string to ASCII bytes
for (let i = 0; i < dataStr.length; i++) {
payload.push(dataStr.charCodeAt(i));
}
} else if (dataByte !== undefined) {
payload.push(dataByte);
}
} else if (dataByte !== undefined) {
payload.push(dataByte);
}
onSend(cmd, payload);
};
onSend(cmd, payload);
};
return (
<div className={`bg-gray-800 p-4 border-t border-gray-700 select-none ${!isConnected ? 'opacity-50 pointer-events-none' : ''}`}>
<h3 className="text-sm font-semibold mb-3 text-green-400 flex items-center justify-between">
<span className="flex items-center gap-2"><Navigation size={16} /> ACS Control</span>
<span className="text-[10px] bg-gray-900 px-1.5 py-0.5 rounded text-gray-500 border border-gray-700">EXT CMD</span>
</h3>
return (
<div className={`bg-gray-800 p-4 border-t border-gray-700 select-none ${!isConnected ? 'opacity-50 pointer-events-none' : ''}`}>
<h3 className="text-sm font-semibold mb-3 text-green-400 flex items-center justify-between">
<span className="flex items-center gap-2"><Navigation size={16} /> ACS Control</span>
<span className="text-[10px] bg-gray-900 px-1.5 py-0.5 rounded text-gray-500 border border-gray-700">EXT CMD</span>
</h3>
{/* Navigation Commands */}
<div className="space-y-2 mb-4">
{/* Goto Tag */}
<div className="flex items-center gap-2">
<div className="relative flex-1">
<Hash size={12} className="absolute left-2 top-1/2 -translate-y-1/2 text-gray-500" />
<input
list="tag-options"
type="text"
value={tagId}
onChange={(e) => setTagId(e.target.value)}
className="w-full bg-gray-900 border border-gray-600 rounded py-1 pl-7 pr-2 text-xs text-white font-mono focus:border-green-500 outline-none"
placeholder="Tag ID"
/>
<datalist id="tag-options">
{availableTags.map(tag => (
<option key={tag} value={tag} />
))}
</datalist>
{/* Navigation Commands */}
<div className="space-y-2 mb-4">
{/* Goto Tag */}
<div className="flex items-center gap-2">
<div className="relative flex-1">
<Hash size={12} className="absolute left-2 top-1/2 -translate-y-1/2 text-gray-500" />
<input
list="tag-options"
type="text"
value={tagId}
onChange={(e) => setTagId(e.target.value)}
className="w-full bg-gray-900 border border-gray-600 rounded py-1 pl-7 pr-2 text-xs text-white font-mono focus:border-green-500 outline-none"
placeholder="Tag ID"
/>
<datalist id="tag-options">
{availableTags.map(tag => (
<option key={tag} value={tag} />
))}
</datalist>
</div>
<button
onClick={() => handleSend(0x81, tagId)}
className="bg-gray-700 hover:bg-green-700 text-white text-xs px-3 py-1.5 rounded flex items-center gap-1 transition-colors border border-gray-600"
>
GO <ChevronRight size={10} />
</button>
</div>
<button
onClick={() => handleSend(0x81, tagId)}
className="bg-gray-700 hover:bg-green-700 text-white text-xs px-3 py-1.5 rounded flex items-center gap-1 transition-colors border border-gray-600"
{/* Goto Alias */}
<div className="flex items-center gap-2">
<div className="relative flex-1">
<Type size={12} className="absolute left-2 top-1/2 -translate-y-1/2 text-gray-500" />
<input
list="alias-options"
type="text"
value={alias}
onChange={(e) => setAlias(e.target.value)}
className="w-full bg-gray-900 border border-gray-600 rounded py-1 pl-7 pr-2 text-xs text-white font-mono focus:border-green-500 outline-none"
placeholder="Alias Name"
/>
<datalist id="alias-options">
{PRESET_ALIASES.map(item => (
<option key={item} value={item} />
))}
</datalist>
</div>
<button
onClick={() => handleSend(0x82, alias)}
className="bg-gray-700 hover:bg-green-700 text-white text-xs px-3 py-1.5 rounded flex items-center gap-1 transition-colors border border-gray-600"
>
GO <ChevronRight size={10} />
</button>
</div>
</div>
<div className="h-px bg-gray-700 my-3" />
{/* Action Buttons Grid */}
<div className="grid grid-cols-2 gap-2 mb-3">
<button
onClick={() => handleSend(0x83, undefined, 1)}
className="flex items-center justify-center gap-2 bg-blue-900/40 hover:bg-blue-800 border border-blue-800 text-blue-200 py-2 rounded text-xs transition-colors"
>
GO <ChevronRight size={10} />
<Package size={14} /> Pick ON
</button>
<button
onClick={() => handleSend(0x83, undefined, 0)}
className="flex items-center justify-center gap-2 bg-gray-700 hover:bg-gray-600 border border-gray-600 text-gray-300 py-2 rounded text-xs transition-colors"
>
<Package size={14} /> Pick OFF
</button>
<button
onClick={() => handleSend(0x84, undefined, 1)}
className="flex items-center justify-center gap-2 bg-yellow-900/40 hover:bg-yellow-800 border border-yellow-800 text-yellow-200 py-2 rounded text-xs transition-colors"
>
<Zap size={14} /> Charge ON
</button>
<button
onClick={() => handleSend(0x84, undefined, 0)}
className="flex items-center justify-center gap-2 bg-gray-700 hover:bg-gray-600 border border-gray-600 text-gray-300 py-2 rounded text-xs transition-colors"
>
<Zap size={14} /> Charge OFF
</button>
</div>
{/* Goto Alias */}
<div className="flex items-center gap-2">
<div className="relative flex-1">
<Type size={12} className="absolute left-2 top-1/2 -translate-y-1/2 text-gray-500" />
<input
list="alias-options"
type="text"
value={alias}
onChange={(e) => setAlias(e.target.value)}
className="w-full bg-gray-900 border border-gray-600 rounded py-1 pl-7 pr-2 text-xs text-white font-mono focus:border-green-500 outline-none"
placeholder="Alias Name"
/>
<datalist id="alias-options">
{PRESET_ALIASES.map(item => (
<option key={item} value={item} />
))}
</datalist>
{/* Lift Controls (New) */}
<div className="mb-3">
<h4 className="text-[10px] text-gray-500 uppercase font-bold mb-1">Lift Control</h4>
<div className="flex gap-1">
<button onClick={() => handleSend(0x85, undefined, 1)} className="flex-1 bg-gray-700 hover:bg-green-700 text-white text-xs py-1.5 rounded border border-gray-600">UP</button>
<button onClick={() => handleSend(0x85, undefined, 0)} className="flex-1 bg-gray-700 hover:bg-red-700 text-white text-xs py-1.5 rounded border border-gray-600">STOP</button>
<button onClick={() => handleSend(0x85, undefined, 2)} className="flex-1 bg-gray-700 hover:bg-green-700 text-white text-xs py-1.5 rounded border border-gray-600">DOWN</button>
</div>
</div>
{/* System Controls (New) */}
<div>
<h4 className="text-[10px] text-gray-500 uppercase font-bold mb-1">System Control</h4>
<div className="grid grid-cols-2 gap-2 mb-2">
<button onClick={() => handleSend(0x86)} className="bg-red-900/40 hover:bg-red-800 text-red-200 border border-red-800 text-xs py-1.5 rounded font-bold">EMG STOP</button>
<button onClick={() => handleSend(0x87)} className="bg-gray-700 hover:bg-gray-600 text-gray-200 border border-gray-600 text-xs py-1.5 rounded">RESET</button>
</div>
<div className="flex items-center justify-between bg-gray-900 p-2 rounded border border-gray-700">
<span className="text-xs text-gray-400">Mark Stop</span>
<div className="flex gap-1">
<button onClick={() => handleSend(0x88, undefined, 1)} className="px-2 py-0.5 text-[10px] bg-green-900 text-green-300 rounded border border-green-800 hover:bg-green-800">ON</button>
<button onClick={() => handleSend(0x88, undefined, 0)} className="px-2 py-0.5 text-[10px] bg-gray-700 text-gray-300 rounded border border-gray-600 hover:bg-gray-600">OFF</button>
</div>
</div>
<button
onClick={() => handleSend(0x82, alias)}
className="bg-gray-700 hover:bg-green-700 text-white text-xs px-3 py-1.5 rounded flex items-center gap-1 transition-colors border border-gray-600"
>
GO <ChevronRight size={10} />
</button>
</div>
</div>
<div className="h-px bg-gray-700 my-3" />
{/* Action Buttons Grid */}
<div className="grid grid-cols-2 gap-2">
<button
onClick={() => handleSend(0x83, undefined, 1)}
className="flex items-center justify-center gap-2 bg-blue-900/40 hover:bg-blue-800 border border-blue-800 text-blue-200 py-2 rounded text-xs transition-colors"
>
<Package size={14} /> Pick ON
</button>
<button
onClick={() => handleSend(0x83, undefined, 0)}
className="flex items-center justify-center gap-2 bg-gray-700 hover:bg-gray-600 border border-gray-600 text-gray-300 py-2 rounded text-xs transition-colors"
>
<Package size={14} /> Pick OFF
</button>
<button
onClick={() => handleSend(0x84, undefined, 1)}
className="flex items-center justify-center gap-2 bg-yellow-900/40 hover:bg-yellow-800 border border-yellow-800 text-yellow-200 py-2 rounded text-xs transition-colors"
>
<Zap size={14} /> Charge ON
</button>
<button
onClick={() => handleSend(0x84, undefined, 0)}
className="flex items-center justify-center gap-2 bg-gray-700 hover:bg-gray-600 border border-gray-600 text-gray-300 py-2 rounded text-xs transition-colors"
>
<Zap size={14} /> Charge OFF
</button>
</div>
</div>
);
);
};
export default AcsControls;

View File

@@ -3,86 +3,119 @@ import { BatteryCharging, Zap, Thermometer, Sliders } from 'lucide-react';
import { AgvState } from '../types';
interface BmsPanelProps {
state: AgvState;
setBatteryLevel: (level: number) => void;
state: AgvState;
setBatteryLevel: (level: number) => void;
setIsCharging: (isCharging: boolean) => void;
}
const BmsPanel: React.FC<BmsPanelProps> = ({ state, setBatteryLevel }) => {
const currentCapacity = (state.maxCapacity * (state.batteryLevel / 100)).toFixed(1);
const BmsPanel: React.FC<BmsPanelProps> = ({ state, setBatteryLevel, setIsCharging }) => {
const currentCapacity = (state.maxCapacity * (state.batteryLevel / 100)).toFixed(1);
return (
<div className="bg-gray-800 p-4 border-t border-gray-700 h-auto select-none">
<h3 className="text-sm font-semibold mb-3 text-gray-300 flex items-center justify-between">
<span className="flex items-center gap-2"><BatteryCharging size={16} /> BMS Monitor</span>
<span className="text-xs font-mono text-gray-500">ID: 01</span>
</h3>
{/* Main Info Grid */}
<div className="grid grid-cols-2 gap-2 mb-3">
<div className="bg-gray-700/50 p-2 rounded border border-gray-600">
<div className="text-[10px] text-gray-400 uppercase tracking-wider">Total Voltage</div>
<div className="text-lg font-mono text-white font-bold flex items-baseline gap-1">
{(state.cellVoltages.reduce((a,b)=>a+b, 0)).toFixed(1)} <span className="text-xs text-gray-400">V</span>
</div>
</div>
<div className="bg-gray-700/50 p-2 rounded border border-gray-600">
<div className="text-[10px] text-gray-400 flex items-center gap-1 uppercase tracking-wider">
<Thermometer size={10} /> Temp
</div>
<div className="text-lg font-mono text-white font-bold flex items-baseline gap-1">
{state.batteryTemp.toFixed(1)} <span className="text-xs text-gray-400">°C</span>
</div>
</div>
<div className="bg-gray-700/50 p-2 rounded border border-gray-600">
<div className="text-[10px] text-gray-400 uppercase tracking-wider">Capacity (Ah)</div>
<div className="text-sm font-mono text-white">
<span className="text-yellow-400 font-bold">{currentCapacity}</span>
<span className="text-gray-500"> / </span>
{state.maxCapacity}
</div>
</div>
<div className="bg-gray-700/50 p-2 rounded border border-gray-600">
<div className="text-[10px] text-gray-400 uppercase tracking-wider">Level (%)</div>
<div className={`text-sm font-mono font-bold ${state.batteryLevel < 20 ? 'text-red-400' : 'text-green-400'}`}>
{state.batteryLevel.toFixed(1)}%
return (
<div className="bg-gray-800 p-4 border-t border-gray-700 h-auto select-none">
<h3 className="text-sm font-semibold mb-3 text-gray-300 flex items-center justify-between">
<span className="flex items-center gap-2"><BatteryCharging size={16} /> BMS Monitor</span>
<span className="text-xs font-mono text-gray-500">ID: 01</span>
</h3>
{/* Main Info Grid */}
<div className="grid grid-cols-2 gap-2 mb-3">
<div className="bg-gray-700/50 p-2 rounded border border-gray-600">
<div className="text-[10px] text-gray-400 uppercase tracking-wider">Total Voltage</div>
<div className="text-lg font-mono text-white font-bold flex items-baseline gap-1">
{(state.cellVoltages.reduce((a, b) => a + b, 0)).toFixed(1)} <span className="text-xs text-gray-400">V</span>
</div>
</div>
<div className="bg-gray-700/50 p-2 rounded border border-gray-600">
<div className="text-[10px] text-gray-400 flex items-center gap-1 uppercase tracking-wider">
<Thermometer size={10} /> Temp 1 / 2
</div>
<div className="flex flex-col gap-0.5">
<div className="text-sm font-mono text-white font-bold flex justify-between">
<span className="text-[10px] text-gray-500">1:</span> {state.batteryTemps?.[0]?.toFixed(1)}°C
</div>
<div className="text-sm font-mono text-white font-bold flex justify-between">
<span className="text-[10px] text-gray-500">2:</span> {state.batteryTemps?.[1]?.toFixed(1)}°C
</div>
</div>
</div>
</div>
</div>
{/* Manual Slider */}
<div className="mb-4">
<div className="flex justify-between text-xs text-gray-400 mb-1">
<span className="flex items-center gap-1"><Sliders size={10}/> Manual Adjust</span>
</div>
<input
type="range"
min="0"
max="100"
step="1"
value={state.batteryLevel}
onChange={(e) => setBatteryLevel(Number(e.target.value))}
className="w-full h-1.5 bg-gray-600 rounded-lg appearance-none cursor-pointer accent-green-500 hover:accent-green-400"
/>
</div>
<div className="bg-gray-700/50 p-2 rounded border border-gray-600">
<div className="text-[10px] text-gray-400 uppercase tracking-wider">Capacity (Ah)</div>
<div className="text-sm font-mono text-white">
<span className="text-yellow-400 font-bold">{currentCapacity}</span>
<span className="text-gray-500"> / </span>
{state.maxCapacity}
</div>
</div>
<div className="bg-gray-700/50 p-2 rounded border border-gray-600">
<div className="text-[10px] text-gray-400 uppercase tracking-wider">Level (%)</div>
<div className={`text-sm font-mono font-bold ${state.batteryLevel < 20 ? 'text-red-400' : 'text-green-400'}`}>
{state.batteryLevel.toFixed(1)}%
</div>
</div>
{/* Cells */}
<div className="space-y-1">
<div className="text-[10px] text-gray-500 uppercase tracking-wider mb-1">Cell Voltages (V)</div>
<div className="grid grid-cols-4 gap-1">
{state.cellVoltages.map((v, i) => (
<div key={i} className={`py-1 px-0.5 text-center rounded text-[10px] font-mono border ${
v < 2.9 ? 'bg-red-900/30 border-red-800 text-red-300' :
v > 3.5 ? 'bg-blue-900/30 border-blue-800 text-blue-300' :
'bg-gray-700/30 border-gray-600 text-gray-300'
}`}>
{v.toFixed(3)}
</div>
))}
</div>
</div>
</div>
);
<div className="bg-gray-700/50 p-2 rounded border border-gray-600">
<div className="text-[10px] text-gray-400 uppercase tracking-wider">Current (A)</div>
<div className={`text-sm font-mono font-bold ${state.isCharging ? 'text-yellow-400' : 'text-blue-400'}`}>
{(state.isCharging ? -18.7 : 2.1).toFixed(1)} A
</div>
</div>
<div className="bg-gray-700/50 p-2 rounded border border-gray-600">
<div className="text-[10px] text-gray-400 uppercase tracking-wider">Power (W)</div>
<div className={`text-sm font-mono font-bold ${state.isCharging ? 'text-yellow-400' : 'text-blue-400'}`}>
{state.isCharging ? -450 : 50} W
</div>
</div>
</div>
{/* Charge Button */}
<div className="mb-3">
<button
onClick={() => setIsCharging(!state.isCharging)}
className={`w-full py-1.5 rounded flex items-center justify-center gap-2 text-xs font-bold transition-all ${state.isCharging
? 'bg-yellow-600 text-white shadow-[0_0_10px_rgba(202,138,4,0.5)] border border-yellow-500'
: 'bg-gray-700 text-gray-400 hover:bg-gray-600 border border-gray-600'
}`}
>
<Zap size={14} className={state.isCharging ? "fill-white animate-pulse" : ""} />
{state.isCharging ? 'CHARGING...' : 'START CHARGING'}
</button>
</div>
{/* Manual Slider */}
<div className="mb-4">
<div className="flex justify-between text-xs text-gray-400 mb-1">
<span className="flex items-center gap-1"><Sliders size={10} /> Manual Adjust</span>
</div>
<input
type="range"
min="0"
max="100"
step="1"
value={state.batteryLevel}
onChange={(e) => setBatteryLevel(Number(e.target.value))}
className="w-full h-1.5 bg-gray-600 rounded-lg appearance-none cursor-pointer accent-green-500 hover:accent-green-400"
/>
</div>
{/* Cells */}
<div className="space-y-1">
<div className="text-[10px] text-gray-500 uppercase tracking-wider mb-1">Cell Voltages (V)</div>
<div className="grid grid-cols-4 gap-1">
{state.cellVoltages.map((v, i) => (
<div key={i} className={`py-1 px-0.5 text-center rounded text-[10px] font-mono border ${v < 2.9 ? 'bg-red-900/30 border-red-800 text-red-300' :
v > 3.5 ? 'bg-blue-900/30 border-blue-800 text-blue-300' :
'bg-gray-700/30 border-gray-600 text-gray-300'
}`}>
{v.toFixed(3)}
</div>
))}
</div>
</div>
</div>
);
};
export default BmsPanel;

View File

@@ -15,7 +15,7 @@ const SystemLogPanel: React.FC<SystemLogPanelProps> = ({ logs, onClear }) => {
// Force scroll to top
useEffect(() => {
if (logContainerRef.current) {
logContainerRef.current.scrollTop = 0;
logContainerRef.current.scrollTop = 0;
}
}, [reversedLogs]);
@@ -23,44 +23,44 @@ const SystemLogPanel: React.FC<SystemLogPanelProps> = ({ logs, onClear }) => {
<div className="flex flex-col h-full bg-gray-900 border-t border-gray-700 overflow-hidden relative">
{/* Header */}
<div className="flex items-center justify-between px-3 py-1.5 bg-gray-800 border-b border-gray-700 shrink-0">
<div className="flex items-center gap-2">
<div className="font-bold text-xs text-gray-300 flex items-center gap-1.5">
<Info size={12} className="text-gray-400" /> SYSTEM LOG
</div>
<div className="flex items-center gap-2">
<div className="font-bold text-xs text-gray-300 flex items-center gap-1.5">
<Info size={12} className="text-gray-400" /> SYSTEM LOG
</div>
<button
onClick={onClear}
title="Clear Logs"
className="p-1 text-gray-500 hover:text-gray-200 hover:bg-gray-700 rounded transition-colors"
>
<Trash2 size={12} />
</button>
</div>
<button
onClick={onClear}
title="Clear Logs"
className="p-1 text-gray-500 hover:text-gray-200 hover:bg-gray-700 rounded transition-colors"
>
<Trash2 size={12} />
</button>
</div>
{/* Log Body */}
<div
ref={logContainerRef}
className="flex-1 overflow-y-auto p-2 font-mono text-[10px] space-y-0.5 leading-tight bg-gray-900"
<div
ref={logContainerRef}
className="flex-1 overflow-auto p-2 font-mono text-[10px] space-y-0.5 leading-tight bg-gray-900"
>
{reversedLogs.length === 0 && (
<div className="h-full flex flex-col items-center justify-center text-gray-700 space-y-1">
<span className="text-[9px]">No System Activity</span>
</div>
)}
{reversedLogs.map((log, index) => (
<div key={log.id} className="flex gap-2 break-all opacity-90 hover:opacity-100 hover:bg-gray-800/50 py-[1px] border-b border-gray-800/30 last:border-0 items-start">
<span className="text-gray-600 select-none min-w-[20px] text-right text-[9px] pt-0.5">{index + 1}</span>
<span className="text-gray-500 select-none whitespace-nowrap min-w-[45px] text-[9px] pt-0.5">{log.timestamp}</span>
<span className={`flex-1 font-mono break-all
{reversedLogs.length === 0 && (
<div className="h-full flex flex-col items-center justify-center text-gray-700 space-y-1">
<span className="text-[9px]">No System Activity</span>
</div>
)}
{reversedLogs.map((log, index) => (
<div key={log.id} className="flex gap-2 whitespace-nowrap opacity-90 hover:opacity-100 hover:bg-gray-800/50 py-[1px] border-b border-gray-800/30 last:border-0 items-center">
<span className="text-gray-600 select-none min-w-[20px] text-right text-[9px]">{index + 1}</span>
<span className="text-gray-500 select-none min-w-[45px] text-[9px]">{log.timestamp}</span>
<span className={`flex-1
${log.type === 'ERROR' ? 'text-red-400 font-bold' : 'text-gray-300'}
${log.type === 'INFO' ? 'text-gray-400' : ''}
`}>
{log.message}
</span>
</div>
))}
{log.message}
</span>
</div>
))}
</div>
</div>
);