\"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

79
App.tsx
View File

@@ -71,7 +71,8 @@ const App: React.FC = () => {
// Lift & Magnet
magnetOn: false,
liftStatus: 'IDLE',
lidarEnabled: true, // Default ON
lidarEnabled: false,
isCharging: false, // Initial state
// Protocol Flags Initial State
system0: 0,
@@ -80,8 +81,8 @@ const App: React.FC = () => {
signalFlags: 0, // Will be updated by useEffect
batteryLevel: 95,
maxCapacity: 100, // 100Ah
batteryTemp: 25,
maxCapacity: 100,
batteryTemps: [25.0, 26.5],
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);
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));
resp[8] = (remainAh >> 8) & 0xFF; resp[9] = remainAh & 0xFF;
const maxAh = s.maxCapacity;
@@ -614,8 +635,8 @@ const App: React.FC = () => {
resp[23] = Math.floor(s.batteryLevel);
const t1 = Math.floor((s.batteryTemp * 10) + 2731);
const t2 = Math.floor((s.batteryTemp * 10) + 2731);
const t1 = Math.floor((s.batteryTemps[0] * 10) + 2731);
const t2 = Math.floor((s.batteryTemps[1] * 10) + 2731);
resp[27] = (t1 >> 8) & 0xFF; resp[28] = t1 & 0xFF;
resp[29] = (t2 >> 8) & 0xFF; resp[30] = t2 & 0xFF;
@@ -1145,6 +1166,40 @@ const App: React.FC = () => {
};
}, [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 (
<div
className="flex h-screen w-screen bg-gray-950 text-white overflow-hidden"
@@ -1162,14 +1217,14 @@ const App: React.FC = () => {
</div>
{/* 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">
<AgvStatusPanel agvState={agvState} />
</div>
{/* 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">
<AgvAutoRunControls
agvState={agvState}
@@ -1267,7 +1322,7 @@ const App: React.FC = () => {
</div>
{/* 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) */}
<div className="flex-1 overflow-y-auto">
@@ -1286,7 +1341,13 @@ const App: React.FC = () => {
{/* Middle 1: BMS Panel (Fixed) */}
<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>
{/* Bottom: System Logs (Fixed Height) */}

33
bms_sim_code.txt Normal file
View 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);
}, []);

View File

@@ -1 +0,0 @@
Update: Relocate AutoRun controls and cleanup

View File

@@ -105,10 +105,11 @@ const AcsControls: React.FC<AcsControlsProps> = ({ onSend, isConnected, mapData
</div>
</div>
<div className="h-px bg-gray-700 my-3" />
{/* Action Buttons Grid */}
<div className="grid grid-cols-2 gap-2">
<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"
@@ -135,6 +136,32 @@ const AcsControls: React.FC<AcsControlsProps> = ({ onSend, isConnected, mapData
<Zap size={14} /> Charge OFF
</button>
</div>
{/* 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>
</div>
</div>
);
};

View File

@@ -5,9 +5,10 @@ import { AgvState } from '../types';
interface BmsPanelProps {
state: AgvState;
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);
return (
@@ -27,10 +28,15 @@ const BmsPanel: React.FC<BmsPanelProps> = ({ state, setBatteryLevel }) => {
</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
<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 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>
@@ -48,6 +54,34 @@ const BmsPanel: React.FC<BmsPanelProps> = ({ state, setBatteryLevel }) => {
{state.batteryLevel.toFixed(1)}%
</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 */}
@@ -71,8 +105,7 @@ const BmsPanel: React.FC<BmsPanelProps> = ({ state, setBatteryLevel }) => {
<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' :
<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'
}`}>

View File

@@ -41,7 +41,7 @@ const SystemLogPanel: React.FC<SystemLogPanelProps> = ({ logs, onClear }) => {
{/* 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"
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">
@@ -50,10 +50,10 @@ const SystemLogPanel: React.FC<SystemLogPanelProps> = ({ logs, onClear }) => {
)}
{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
<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' : ''}
`}>

View File

@@ -251,7 +251,9 @@ export interface AgvState {
// New features
magnetOn: boolean;
liftStatus: 'IDLE' | 'UP' | 'DOWN';
lidarEnabled: boolean; // 1=ON, 0=OFF
isCharging: boolean; // Manual charging state
// Protocol Flags (Integers representing bitmaps)
system0: number;
@@ -262,7 +264,7 @@ export interface AgvState {
// Battery
batteryLevel: number; // Percentage 0-100
maxCapacity: number; // Ah (Total Capacity)
batteryTemp: number;
batteryTemps: number[]; // [Temp1, Temp2]
cellVoltages: number[];
}