\"feat_Enhance_BMS_Simulation_SystemLog_UI_and_ACS_Controls\"
This commit is contained in:
79
App.tsx
79
App.tsx
@@ -71,7 +71,8 @@ const App: React.FC = () => {
|
|||||||
// Lift & Magnet
|
// Lift & Magnet
|
||||||
magnetOn: false,
|
magnetOn: false,
|
||||||
liftStatus: 'IDLE',
|
liftStatus: 'IDLE',
|
||||||
lidarEnabled: true, // Default ON
|
lidarEnabled: false,
|
||||||
|
isCharging: false, // Initial state
|
||||||
|
|
||||||
// Protocol Flags Initial State
|
// Protocol Flags Initial State
|
||||||
system0: 0,
|
system0: 0,
|
||||||
@@ -80,8 +81,8 @@ const App: React.FC = () => {
|
|||||||
signalFlags: 0, // Will be updated by useEffect
|
signalFlags: 0, // Will be updated by useEffect
|
||||||
|
|
||||||
batteryLevel: 95,
|
batteryLevel: 95,
|
||||||
maxCapacity: 100, // 100Ah
|
maxCapacity: 100,
|
||||||
batteryTemp: 25,
|
batteryTemps: [25.0, 26.5],
|
||||||
cellVoltages: Array(8).fill(3.2),
|
cellVoltages: Array(8).fill(3.2),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -607,6 +608,26 @@ const App: React.FC = () => {
|
|||||||
const totalV = Math.floor(s.cellVoltages.reduce((a, b) => a + b, 0) * 100);
|
const totalV = Math.floor(s.cellVoltages.reduce((a, b) => a + b, 0) * 100);
|
||||||
resp[4] = (totalV >> 8) & 0xFF; resp[5] = totalV & 0xFF;
|
resp[4] = (totalV >> 8) & 0xFF; resp[5] = totalV & 0xFF;
|
||||||
|
|
||||||
|
// 6, 7: Current (mA)
|
||||||
|
// Power (W) = (mA / 1000) * V => mA = (W / V) * 1000.
|
||||||
|
// Discharging: ~50W, Charging: ~-450W.
|
||||||
|
const isCharging = s.isCharging; // Use manual state
|
||||||
|
const powerW = isCharging ? -450 : 50;
|
||||||
|
|
||||||
|
let currentMA = 0;
|
||||||
|
if (totalV > 0) {
|
||||||
|
const realV = totalV / 100; // Convert 0.01V to Volts
|
||||||
|
currentMA = Math.floor((powerW / totalV) * 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write Int16 (Little Endian or Big Endian? BMS usually Big Endian for these usually?
|
||||||
|
// Code above uses Big Endian for Voltage (resp[4] = high, resp[5] = low).
|
||||||
|
// Standard BMS protocols often BE. I will follow the pattern of Voltage.
|
||||||
|
// Handle negative numbers via Two's Complement for manual byte splitting if needed,
|
||||||
|
// but bitwise ops on standard JS numbers (treated as 32bit int) work fine for & 0xFF.
|
||||||
|
resp[6] = (currentMA >> 8) & 0xFF;
|
||||||
|
resp[7] = currentMA & 0xFF;
|
||||||
|
|
||||||
const remainAh = Math.floor(s.maxCapacity * (s.batteryLevel / 100));
|
const remainAh = Math.floor(s.maxCapacity * (s.batteryLevel / 100));
|
||||||
resp[8] = (remainAh >> 8) & 0xFF; resp[9] = remainAh & 0xFF;
|
resp[8] = (remainAh >> 8) & 0xFF; resp[9] = remainAh & 0xFF;
|
||||||
const maxAh = s.maxCapacity;
|
const maxAh = s.maxCapacity;
|
||||||
@@ -614,8 +635,8 @@ const App: React.FC = () => {
|
|||||||
|
|
||||||
resp[23] = Math.floor(s.batteryLevel);
|
resp[23] = Math.floor(s.batteryLevel);
|
||||||
|
|
||||||
const t1 = Math.floor((s.batteryTemp * 10) + 2731);
|
const t1 = Math.floor((s.batteryTemps[0] * 10) + 2731);
|
||||||
const t2 = Math.floor((s.batteryTemp * 10) + 2731);
|
const t2 = Math.floor((s.batteryTemps[1] * 10) + 2731);
|
||||||
|
|
||||||
resp[27] = (t1 >> 8) & 0xFF; resp[28] = t1 & 0xFF;
|
resp[27] = (t1 >> 8) & 0xFF; resp[28] = t1 & 0xFF;
|
||||||
resp[29] = (t2 >> 8) & 0xFF; resp[30] = t2 & 0xFF;
|
resp[29] = (t2 >> 8) & 0xFF; resp[30] = t2 & 0xFF;
|
||||||
@@ -1145,6 +1166,40 @@ const App: React.FC = () => {
|
|||||||
};
|
};
|
||||||
}, [agvState, agvConnected, sendTag]);
|
}, [agvState, agvConnected, sendTag]);
|
||||||
|
|
||||||
|
// BMS Simulation
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
setAgvState(s => {
|
||||||
|
const isCharging = s.isCharging;
|
||||||
|
let newLevel = s.batteryLevel;
|
||||||
|
|
||||||
|
if (isCharging) {
|
||||||
|
// Charge: 1% per min (faster than discharge)
|
||||||
|
newLevel = Math.min(100, s.batteryLevel + (1.0 / 60));
|
||||||
|
} else {
|
||||||
|
// Discharge: 5% per 10 min = 0.5% per min = 0.5 / 60 per sec
|
||||||
|
newLevel = Math.max(0, s.batteryLevel - (0.5 / 60));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fluctuate Voltages (+- 0.01V)
|
||||||
|
const newVoltages = s.cellVoltages.map(v => {
|
||||||
|
const delta = (Math.random() - 0.5) * 0.02;
|
||||||
|
let val = v + delta;
|
||||||
|
if (val < 2.8) val = 2.8;
|
||||||
|
if (val > 3.6) val = 3.6;
|
||||||
|
return val;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...s,
|
||||||
|
batteryLevel: newLevel,
|
||||||
|
cellVoltages: newVoltages
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
return () => clearInterval(timer);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex h-screen w-screen bg-gray-950 text-white overflow-hidden"
|
className="flex h-screen w-screen bg-gray-950 text-white overflow-hidden"
|
||||||
@@ -1162,14 +1217,14 @@ const App: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 2. Inner Left: Protocol Flags (Separated Sidebar) - Removed BMS from here */}
|
{/* 2. Inner Left: Protocol Flags (Separated Sidebar) - Removed BMS from here */}
|
||||||
<div className="w-80 border-r border-gray-800 bg-gray-900 flex flex-col shrink-0">
|
<div className="w-60 border-r border-gray-800 bg-gray-900 flex flex-col shrink-0">
|
||||||
<div className="flex-1 overflow-hidden">
|
<div className="flex-1 overflow-hidden">
|
||||||
<AgvStatusPanel agvState={agvState} />
|
<AgvStatusPanel agvState={agvState} />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Auto Run Controls (Left Sidebar) */}
|
{/* Auto Run Controls (Left Sidebar) */}
|
||||||
<div className="w-80 border-r border-gray-800 bg-gray-900 flex flex-col shrink-0">
|
<div className="w-60 border-r border-gray-800 bg-gray-900 flex flex-col shrink-0">
|
||||||
<div className="flex-1 overflow-hidden">
|
<div className="flex-1 overflow-hidden">
|
||||||
<AgvAutoRunControls
|
<AgvAutoRunControls
|
||||||
agvState={agvState}
|
agvState={agvState}
|
||||||
@@ -1267,7 +1322,7 @@ const App: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 4. Right: Controls & BMS & ACS */}
|
{/* 4. Right: Controls & BMS & ACS */}
|
||||||
<div className="w-80 border-l border-gray-800 bg-gray-900 flex flex-col shrink-0">
|
<div className="w-72 border-l border-gray-800 bg-gray-900 flex flex-col shrink-0">
|
||||||
|
|
||||||
{/* Top: Controls (Scrollable if needed) */}
|
{/* Top: Controls (Scrollable if needed) */}
|
||||||
<div className="flex-1 overflow-y-auto">
|
<div className="flex-1 overflow-y-auto">
|
||||||
@@ -1286,7 +1341,13 @@ const App: React.FC = () => {
|
|||||||
|
|
||||||
{/* Middle 1: BMS Panel (Fixed) */}
|
{/* Middle 1: BMS Panel (Fixed) */}
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<BmsPanel state={agvState} setBatteryLevel={(l) => setAgvState(s => ({ ...s, batteryLevel: l }))} />
|
<BmsPanel
|
||||||
|
state={agvState}
|
||||||
|
setBatteryLevel={(l) => setAgvState(s => ({ ...s, batteryLevel: l }))}
|
||||||
|
setIsCharging={(isConnected) => {
|
||||||
|
setAgvState(s => ({ ...s, isCharging: isConnected }));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bottom: System Logs (Fixed Height) */}
|
{/* Bottom: System Logs (Fixed Height) */}
|
||||||
|
|||||||
33
bms_sim_code.txt
Normal file
33
bms_sim_code.txt
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
// BMS Simulation
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
setAgvState(s => {
|
||||||
|
const isCharging = s.isCharging;
|
||||||
|
let newLevel = s.batteryLevel;
|
||||||
|
|
||||||
|
if (isCharging) {
|
||||||
|
// Charge: 1% per min (faster than discharge)
|
||||||
|
newLevel = Math.min(100, s.batteryLevel + (1.0 / 60));
|
||||||
|
} else {
|
||||||
|
// Discharge: 5% per 10 min = 0.5% per min = 0.5 / 60 per sec
|
||||||
|
newLevel = Math.max(0, s.batteryLevel - (0.5 / 60));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fluctuate Voltages (+- 0.01V)
|
||||||
|
const newVoltages = s.cellVoltages.map(v => {
|
||||||
|
const delta = (Math.random() - 0.5) * 0.02;
|
||||||
|
let val = v + delta;
|
||||||
|
if (val < 2.8) val = 2.8;
|
||||||
|
if (val > 3.6) val = 3.6;
|
||||||
|
return val;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...s,
|
||||||
|
batteryLevel: newLevel,
|
||||||
|
cellVoltages: newVoltages
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
return () => clearInterval(timer);
|
||||||
|
}, []);
|
||||||
@@ -1 +0,0 @@
|
|||||||
Update: Relocate AutoRun controls and cleanup
|
|
||||||
@@ -3,140 +3,167 @@ import { Navigation, Package, Zap, ChevronRight, Hash, Type } from 'lucide-react
|
|||||||
import { SimulationMap } from '../types';
|
import { SimulationMap } from '../types';
|
||||||
|
|
||||||
interface AcsControlsProps {
|
interface AcsControlsProps {
|
||||||
onSend: (cmd: number, data?: number[]) => void;
|
onSend: (cmd: number, data?: number[]) => void;
|
||||||
isConnected: boolean;
|
isConnected: boolean;
|
||||||
mapData: SimulationMap;
|
mapData: SimulationMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AcsControls: React.FC<AcsControlsProps> = ({ onSend, isConnected, mapData }) => {
|
const AcsControls: React.FC<AcsControlsProps> = ({ onSend, isConnected, mapData }) => {
|
||||||
const [tagId, setTagId] = useState('0001');
|
const [tagId, setTagId] = useState('0001');
|
||||||
const [alias, setAlias] = useState('charger1');
|
const [alias, setAlias] = useState('charger1');
|
||||||
|
|
||||||
const PRESET_ALIASES = [
|
const PRESET_ALIASES = [
|
||||||
'charger1', 'charger2',
|
'charger1', 'charger2',
|
||||||
'loader', 'unloader', 'cleaner',
|
'loader', 'unloader', 'cleaner',
|
||||||
'buffer1', 'buffer2', 'buffer3', 'buffer4', 'buffer5', 'buffer6'
|
'buffer1', 'buffer2', 'buffer3', 'buffer4', 'buffer5', 'buffer6'
|
||||||
];
|
];
|
||||||
|
|
||||||
const availableTags = useMemo(() => {
|
const availableTags = useMemo(() => {
|
||||||
// Extract unique, non-empty RFID tags from map nodes
|
// Extract unique, non-empty RFID tags from map nodes
|
||||||
const tags = mapData.nodes
|
const tags = mapData.nodes
|
||||||
.filter(n => n.rfidId && n.rfidId.trim() !== '')
|
.filter(n => n.rfidId && n.rfidId.trim() !== '')
|
||||||
.map(n => n.rfidId);
|
.map(n => n.rfidId);
|
||||||
return Array.from(new Set(tags)).sort();
|
return Array.from(new Set(tags)).sort();
|
||||||
}, [mapData]);
|
}, [mapData]);
|
||||||
|
|
||||||
const handleSend = (cmd: number, dataStr?: string, dataByte?: number) => {
|
const handleSend = (cmd: number, dataStr?: string, dataByte?: number) => {
|
||||||
if (!isConnected) return;
|
if (!isConnected) return;
|
||||||
|
|
||||||
let payload: number[] = [];
|
let payload: number[] = [];
|
||||||
|
|
||||||
if (dataStr !== undefined) {
|
if (dataStr !== undefined) {
|
||||||
// Convert string to ASCII bytes
|
// Convert string to ASCII bytes
|
||||||
for (let i = 0; i < dataStr.length; i++) {
|
for (let i = 0; i < dataStr.length; i++) {
|
||||||
payload.push(dataStr.charCodeAt(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 (
|
return (
|
||||||
<div className={`bg-gray-800 p-4 border-t border-gray-700 select-none ${!isConnected ? 'opacity-50 pointer-events-none' : ''}`}>
|
<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">
|
<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="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>
|
<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>
|
</h3>
|
||||||
|
|
||||||
{/* Navigation Commands */}
|
{/* Navigation Commands */}
|
||||||
<div className="space-y-2 mb-4">
|
<div className="space-y-2 mb-4">
|
||||||
{/* Goto Tag */}
|
{/* Goto Tag */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="relative flex-1">
|
<div className="relative flex-1">
|
||||||
<Hash size={12} className="absolute left-2 top-1/2 -translate-y-1/2 text-gray-500" />
|
<Hash size={12} className="absolute left-2 top-1/2 -translate-y-1/2 text-gray-500" />
|
||||||
<input
|
<input
|
||||||
list="tag-options"
|
list="tag-options"
|
||||||
type="text"
|
type="text"
|
||||||
value={tagId}
|
value={tagId}
|
||||||
onChange={(e) => setTagId(e.target.value)}
|
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"
|
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"
|
placeholder="Tag ID"
|
||||||
/>
|
/>
|
||||||
<datalist id="tag-options">
|
<datalist id="tag-options">
|
||||||
{availableTags.map(tag => (
|
{availableTags.map(tag => (
|
||||||
<option key={tag} value={tag} />
|
<option key={tag} value={tag} />
|
||||||
))}
|
))}
|
||||||
</datalist>
|
</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>
|
</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>
|
||||||
|
</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
|
<button
|
||||||
onClick={() => handleSend(0x81, tagId)}
|
onClick={() => handleSend(0x83, undefined, 1)}
|
||||||
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"
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Goto Alias */}
|
{/* Lift Controls (New) */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="mb-3">
|
||||||
<div className="relative flex-1">
|
<h4 className="text-[10px] text-gray-500 uppercase font-bold mb-1">Lift Control</h4>
|
||||||
<Type size={12} className="absolute left-2 top-1/2 -translate-y-1/2 text-gray-500" />
|
<div className="flex gap-1">
|
||||||
<input
|
<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>
|
||||||
list="alias-options"
|
<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>
|
||||||
type="text"
|
<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>
|
||||||
value={alias}
|
</div>
|
||||||
onChange={(e) => setAlias(e.target.value)}
|
</div>
|
||||||
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"
|
{/* System Controls (New) */}
|
||||||
/>
|
<div>
|
||||||
<datalist id="alias-options">
|
<h4 className="text-[10px] text-gray-500 uppercase font-bold mb-1">System Control</h4>
|
||||||
{PRESET_ALIASES.map(item => (
|
<div className="grid grid-cols-2 gap-2 mb-2">
|
||||||
<option key={item} value={item} />
|
<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>
|
||||||
</datalist>
|
</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>
|
</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>
|
</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;
|
export default AcsControls;
|
||||||
@@ -3,86 +3,119 @@ import { BatteryCharging, Zap, Thermometer, Sliders } from 'lucide-react';
|
|||||||
import { AgvState } from '../types';
|
import { AgvState } from '../types';
|
||||||
|
|
||||||
interface BmsPanelProps {
|
interface BmsPanelProps {
|
||||||
state: AgvState;
|
state: AgvState;
|
||||||
setBatteryLevel: (level: number) => void;
|
setBatteryLevel: (level: number) => void;
|
||||||
|
setIsCharging: (isCharging: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BmsPanel: React.FC<BmsPanelProps> = ({ state, setBatteryLevel }) => {
|
const BmsPanel: React.FC<BmsPanelProps> = ({ state, setBatteryLevel, setIsCharging }) => {
|
||||||
const currentCapacity = (state.maxCapacity * (state.batteryLevel / 100)).toFixed(1);
|
const currentCapacity = (state.maxCapacity * (state.batteryLevel / 100)).toFixed(1);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-gray-800 p-4 border-t border-gray-700 h-auto select-none">
|
<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">
|
<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="flex items-center gap-2"><BatteryCharging size={16} /> BMS Monitor</span>
|
||||||
<span className="text-xs font-mono text-gray-500">ID: 01</span>
|
<span className="text-xs font-mono text-gray-500">ID: 01</span>
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
{/* Main Info Grid */}
|
{/* Main Info Grid */}
|
||||||
<div className="grid grid-cols-2 gap-2 mb-3">
|
<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="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-[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">
|
<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>
|
{(state.cellVoltages.reduce((a, b) => a + b, 0)).toFixed(1)} <span className="text-xs text-gray-400">V</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-gray-700/50 p-2 rounded border border-gray-600">
|
<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">
|
<div className="text-[10px] text-gray-400 flex items-center gap-1 uppercase tracking-wider">
|
||||||
<Thermometer size={10} /> Temp
|
<Thermometer size={10} /> Temp 1 / 2
|
||||||
</div>
|
</div>
|
||||||
<div className="text-lg font-mono text-white font-bold flex items-baseline gap-1">
|
<div className="flex flex-col gap-0.5">
|
||||||
{state.batteryTemp.toFixed(1)} <span className="text-xs text-gray-400">°C</span>
|
<div className="text-sm font-mono text-white font-bold flex justify-between">
|
||||||
</div>
|
<span className="text-[10px] text-gray-500">1:</span> {state.batteryTemps?.[0]?.toFixed(1)}°C
|
||||||
</div>
|
</div>
|
||||||
|
<div className="text-sm font-mono text-white font-bold flex justify-between">
|
||||||
<div className="bg-gray-700/50 p-2 rounded border border-gray-600">
|
<span className="text-[10px] text-gray-500">2:</span> {state.batteryTemps?.[1]?.toFixed(1)}°C
|
||||||
<div className="text-[10px] text-gray-400 uppercase tracking-wider">Capacity (Ah)</div>
|
</div>
|
||||||
<div className="text-sm font-mono text-white">
|
</div>
|
||||||
<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>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Manual Slider */}
|
<div className="bg-gray-700/50 p-2 rounded border border-gray-600">
|
||||||
<div className="mb-4">
|
<div className="text-[10px] text-gray-400 uppercase tracking-wider">Capacity (Ah)</div>
|
||||||
<div className="flex justify-between text-xs text-gray-400 mb-1">
|
<div className="text-sm font-mono text-white">
|
||||||
<span className="flex items-center gap-1"><Sliders size={10}/> Manual Adjust</span>
|
<span className="text-yellow-400 font-bold">{currentCapacity}</span>
|
||||||
</div>
|
<span className="text-gray-500"> / </span>
|
||||||
<input
|
{state.maxCapacity}
|
||||||
type="range"
|
</div>
|
||||||
min="0"
|
</div>
|
||||||
max="100"
|
<div className="bg-gray-700/50 p-2 rounded border border-gray-600">
|
||||||
step="1"
|
<div className="text-[10px] text-gray-400 uppercase tracking-wider">Level (%)</div>
|
||||||
value={state.batteryLevel}
|
<div className={`text-sm font-mono font-bold ${state.batteryLevel < 20 ? 'text-red-400' : 'text-green-400'}`}>
|
||||||
onChange={(e) => setBatteryLevel(Number(e.target.value))}
|
{state.batteryLevel.toFixed(1)}%
|
||||||
className="w-full h-1.5 bg-gray-600 rounded-lg appearance-none cursor-pointer accent-green-500 hover:accent-green-400"
|
</div>
|
||||||
/>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Cells */}
|
<div className="bg-gray-700/50 p-2 rounded border border-gray-600">
|
||||||
<div className="space-y-1">
|
<div className="text-[10px] text-gray-400 uppercase tracking-wider">Current (A)</div>
|
||||||
<div className="text-[10px] text-gray-500 uppercase tracking-wider mb-1">Cell Voltages (V)</div>
|
<div className={`text-sm font-mono font-bold ${state.isCharging ? 'text-yellow-400' : 'text-blue-400'}`}>
|
||||||
<div className="grid grid-cols-4 gap-1">
|
{(state.isCharging ? -18.7 : 2.1).toFixed(1)} A
|
||||||
{state.cellVoltages.map((v, i) => (
|
</div>
|
||||||
<div key={i} className={`py-1 px-0.5 text-center rounded text-[10px] font-mono border ${
|
</div>
|
||||||
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' :
|
<div className="bg-gray-700/50 p-2 rounded border border-gray-600">
|
||||||
'bg-gray-700/30 border-gray-600 text-gray-300'
|
<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'}`}>
|
||||||
{v.toFixed(3)}
|
{state.isCharging ? -450 : 50} W
|
||||||
</div>
|
</div>
|
||||||
))}
|
</div>
|
||||||
</div>
|
</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;
|
export default BmsPanel;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const SystemLogPanel: React.FC<SystemLogPanelProps> = ({ logs, onClear }) => {
|
|||||||
// Force scroll to top
|
// Force scroll to top
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (logContainerRef.current) {
|
if (logContainerRef.current) {
|
||||||
logContainerRef.current.scrollTop = 0;
|
logContainerRef.current.scrollTop = 0;
|
||||||
}
|
}
|
||||||
}, [reversedLogs]);
|
}, [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">
|
<div className="flex flex-col h-full bg-gray-900 border-t border-gray-700 overflow-hidden relative">
|
||||||
{/* Header */}
|
{/* 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 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="flex items-center gap-2">
|
||||||
<div className="font-bold text-xs text-gray-300 flex items-center gap-1.5">
|
<div className="font-bold text-xs text-gray-300 flex items-center gap-1.5">
|
||||||
<Info size={12} className="text-gray-400" /> SYSTEM LOG
|
<Info size={12} className="text-gray-400" /> SYSTEM LOG
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={onClear}
|
onClick={onClear}
|
||||||
title="Clear Logs"
|
title="Clear Logs"
|
||||||
className="p-1 text-gray-500 hover:text-gray-200 hover:bg-gray-700 rounded transition-colors"
|
className="p-1 text-gray-500 hover:text-gray-200 hover:bg-gray-700 rounded transition-colors"
|
||||||
>
|
>
|
||||||
<Trash2 size={12} />
|
<Trash2 size={12} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Log Body */}
|
{/* Log Body */}
|
||||||
<div
|
<div
|
||||||
ref={logContainerRef}
|
ref={logContainerRef}
|
||||||
className="flex-1 overflow-y-auto p-2 font-mono text-[10px] space-y-0.5 leading-tight bg-gray-900"
|
className="flex-1 overflow-auto p-2 font-mono text-[10px] space-y-0.5 leading-tight bg-gray-900"
|
||||||
>
|
>
|
||||||
{reversedLogs.length === 0 && (
|
{reversedLogs.length === 0 && (
|
||||||
<div className="h-full flex flex-col items-center justify-center text-gray-700 space-y-1">
|
<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>
|
<span className="text-[9px]">No System Activity</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{reversedLogs.map((log, index) => (
|
{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">
|
<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] pt-0.5">{index + 1}</span>
|
<span className="text-gray-600 select-none min-w-[20px] text-right text-[9px]">{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="text-gray-500 select-none min-w-[45px] text-[9px]">{log.timestamp}</span>
|
||||||
<span className={`flex-1 font-mono break-all
|
<span className={`flex-1
|
||||||
${log.type === 'ERROR' ? 'text-red-400 font-bold' : 'text-gray-300'}
|
${log.type === 'ERROR' ? 'text-red-400 font-bold' : 'text-gray-300'}
|
||||||
${log.type === 'INFO' ? 'text-gray-400' : ''}
|
${log.type === 'INFO' ? 'text-gray-400' : ''}
|
||||||
`}>
|
`}>
|
||||||
{log.message}
|
{log.message}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
12
types.ts
12
types.ts
@@ -177,10 +177,10 @@ export enum AgvError {
|
|||||||
Charger_pos_error = 3,
|
Charger_pos_error = 3,
|
||||||
line_out_error = 4,
|
line_out_error = 4,
|
||||||
runerror_by_no_magent_line = 5,
|
runerror_by_no_magent_line = 5,
|
||||||
agv_system_error=6,
|
agv_system_error = 6,
|
||||||
battery_low_voltage=7,
|
battery_low_voltage = 7,
|
||||||
lift_time_over=9,
|
lift_time_over = 9,
|
||||||
lift_driver_ocr=10,
|
lift_driver_ocr = 10,
|
||||||
lift_driver_emg = 11,
|
lift_driver_emg = 11,
|
||||||
arrive_ctl_comm_error = 12,
|
arrive_ctl_comm_error = 12,
|
||||||
door_ctl_comm_error = 13,
|
door_ctl_comm_error = 13,
|
||||||
@@ -251,7 +251,9 @@ export interface AgvState {
|
|||||||
// New features
|
// New features
|
||||||
magnetOn: boolean;
|
magnetOn: boolean;
|
||||||
liftStatus: 'IDLE' | 'UP' | 'DOWN';
|
liftStatus: 'IDLE' | 'UP' | 'DOWN';
|
||||||
|
|
||||||
lidarEnabled: boolean; // 1=ON, 0=OFF
|
lidarEnabled: boolean; // 1=ON, 0=OFF
|
||||||
|
isCharging: boolean; // Manual charging state
|
||||||
|
|
||||||
// Protocol Flags (Integers representing bitmaps)
|
// Protocol Flags (Integers representing bitmaps)
|
||||||
system0: number;
|
system0: number;
|
||||||
@@ -262,7 +264,7 @@ export interface AgvState {
|
|||||||
// Battery
|
// Battery
|
||||||
batteryLevel: number; // Percentage 0-100
|
batteryLevel: number; // Percentage 0-100
|
||||||
maxCapacity: number; // Ah (Total Capacity)
|
maxCapacity: number; // Ah (Total Capacity)
|
||||||
batteryTemp: number;
|
batteryTemps: number[]; // [Temp1, Temp2]
|
||||||
cellVoltages: number[];
|
cellVoltages: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user