import React, { useMemo, useState, useRef, useEffect } from 'react';
import { BMSBasicInfo, BMSCellInfo, ProtectionStatus } from '../types';
import { LineChart, Line, BarChart, Bar, Cell, ResponsiveContainer, XAxis, YAxis, Tooltip, CartesianGrid, ReferenceLine } from 'recharts';
import { Activity, Battery, Thermometer, Zap, AlertTriangle, PlayCircle, StopCircle, Download, RotateCw, RefreshCw, Gauge, TrendingUp } from 'lucide-react';
interface DashboardProps {
basicInfo: BMSBasicInfo | null;
cellInfo: BMSCellInfo | null;
hwVersion: string | null;
onToggleMosfet: (type: 'charge' | 'discharge', currentState: boolean) => void;
}
const COLORS = [
'#ef4444', '#f97316', '#f59e0b', '#84cc16', '#10b981',
'#06b6d4', '#3b82f6', '#6366f1', '#8b5cf6', '#d946ef',
'#f43f5e', '#fbbf24', '#a3e635', '#34d399', '#22d3ee',
'#94a3b8'
];
// Updated MetricCard to accept valueClassName for custom styling (e.g., blinking)
const MetricCard: React.FC<{ label: string; value: string; unit?: string; icon: React.ReactNode; color?: string; subValue?: string; valueClassName?: string }> = ({ label, value, unit, icon, color = "text-blue-400", subValue, valueClassName }) => (
{React.isValidElement(icon) && React.cloneElement(icon as React.ReactElement
, { size: 48 })}
{value}
{unit && {unit}}
{subValue &&
{subValue}
}
);
const MosfetSwitch: React.FC<{ label: string; active: boolean; onClick: () => void; disabled: boolean }> = ({ label, active, onClick, disabled }) => (
);
const ProtectionBadge: React.FC<{ label: string; active: boolean }> = ({ label, active }) => (
{label}
);
const Dashboard: React.FC = ({ basicInfo, cellInfo, hwVersion, onToggleMosfet }) => {
const [isRecording, setIsRecording] = useState(false);
const logDataRef = useRef([]);
const [logCount, setLogCount] = useState(0);
const [voltageHistory, setVoltageHistory] = useState([]);
// Initialize/Clear history on connect/disconnect
useEffect(() => {
if (!cellInfo) {
setVoltageHistory([]);
}
}, [cellInfo === null]);
// Update Voltage History
useEffect(() => {
if (cellInfo) {
setVoltageHistory(prev => {
const now = new Date();
const timeLabel = now.toLocaleTimeString('en-US', { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" });
const newEntry: any = {
time: timeLabel,
...cellInfo.voltages.reduce((acc, v, i) => ({ ...acc, [`cell${i + 1}`]: v }), {})
};
// Keep approx 60 points (at ~0.5s interval -> ~30s history, user asked for ~1min so increase buffer)
// If polling is 500ms, 120 points = 60 seconds
const newHistory = [...prev, newEntry];
if (newHistory.length > 120) return newHistory.slice(newHistory.length - 120);
return newHistory;
});
}
}, [cellInfo]);
// Data logging effect
useEffect(() => {
if (isRecording && basicInfo) {
logDataRef.current.push({
timestamp: new Date().toISOString(),
voltage: basicInfo.packVoltage,
current: basicInfo.current,
capacity: basicInfo.remainingCapacity,
rsoc: basicInfo.rsoc,
maxCell: cellInfo ? Math.max(...cellInfo.voltages) : 0,
minCell: cellInfo ? Math.min(...cellInfo.voltages) : 0,
temp1: basicInfo.ntcTemps[0] || 0
});
setLogCount(prev => prev + 1);
}
}, [basicInfo, isRecording, cellInfo]);
const toggleRecording = () => {
if (isRecording) {
// Stop and download
const headers = ['Timestamp', 'PackVoltage(V)', 'Current(A)', 'Capacity(Ah)', 'RSOC(%)', 'MaxCell(V)', 'MinCell(V)', 'Temp1(C)'];
const csvContent = "data:text/csv;charset=utf-8,"
+ headers.join(",") + "\n"
+ logDataRef.current.map(row => Object.values(row).join(",")).join("\n");
const encodedUri = encodeURI(csvContent);
const link = document.createElement("a");
link.setAttribute("href", encodedUri);
link.setAttribute("download", `bms_log_${new Date().toISOString()}.csv`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
setIsRecording(false);
logDataRef.current = [];
setLogCount(0);
} else {
setIsRecording(true);
logDataRef.current = [];
setLogCount(0);
}
};
// Safe data extraction (Use defaults if null)
const voltages = cellInfo?.voltages || [];
const cellCount = voltages.length;
const maxVoltage = cellCount > 0 ? Math.max(...voltages) : 0;
const minVoltage = cellCount > 0 ? Math.min(...voltages) : 0;
const avgVoltage = cellCount > 0 ? voltages.reduce((a, b) => a + b, 0) / cellCount : 0;
const deltaVoltage = (maxVoltage - minVoltage);
const power = basicInfo ? basicInfo.packVoltage * basicInfo.current : 0;
// Data for Bar Chart
const cellChartData = voltages.map((v, i) => ({
name: `${i + 1}`,
voltage: v,
isMax: v === maxVoltage && maxVoltage > 0,
isMin: v === minVoltage && minVoltage > 0
}));
const protections = basicInfo?.protectionStatus || {
covp: false, cuvp: false, povp: false, puvp: false,
chgot: false, chgut: false, dsgot: false, dsgut: false,
chgoc: false, dsgoc: false, sc: false, afe: false
};
const protectionList = [
{ key: 'covp', label: 'COVP' }, { key: 'cuvp', label: 'CUVP' },
{ key: 'povp', label: 'POVP' }, { key: 'puvp', label: 'PUVP' },
{ key: 'chgot', label: 'CHG OT' }, { key: 'chgut', label: 'CHG UT' },
{ key: 'dsgot', label: 'DSG OT' }, { key: 'dsgut', label: 'DSG UT' },
{ key: 'chgoc', label: 'CHG OC' }, { key: 'dsgoc', label: 'DSG OC' },
{ key: 'sc', label: 'SHORT' }, { key: 'afe', label: 'AFE' },
];
// Logic for blinking red when SOC < 30
const isLowSoc = basicInfo && basicInfo.rsoc < 30;
return (
{/* Top Metrics Row */}
}
color="text-yellow-600"
/>
}
color={basicInfo && basicInfo.current > 0 ? "text-green-600" : basicInfo && basicInfo.current < 0 ? "text-red-600" : "text-gray-500"}
subValue={power !== 0 ? `${power.toFixed(0)} W` : undefined}
/>
}
// If Low SOC: Blink Red Icon, otherwise green/gray
color={isLowSoc ? "text-red-600 animate-pulse" : (basicInfo ? "text-green-600" : "text-gray-500")}
// If Low SOC: Blink Red Value Text, otherwise default white
valueClassName={isLowSoc ? "text-red-600 animate-pulse" : undefined}
subValue={basicInfo ? `${basicInfo.remainingCapacity.toFixed(2)} Ah` : undefined}
/>
}
color="text-blue-600"
/>
}
color={deltaVoltage > 0.03 ? "text-red-600" : "text-emerald-600"}
/>
}
color="text-purple-600"
/>
{/* Left Column: Charts & Status */}
{/* Combined Cell Voltage Monitor */}
{/* Header */}
Cell Monitor (Instant & Trend)
Max: {maxVoltage.toFixed(3)}V
Min: {minVoltage.toFixed(3)}V
{/* 1. Bar Chart (Instant) */}
Instant Voltage
0 ? minVoltage - 0.05 : 2.5, maxVoltage > 0 ? maxVoltage + 0.05 : 4.5]} stroke="#9ca3af" tick={{ fontSize: 10 }} />
[value.toFixed(3) + ' V', 'Voltage']}
/>
{cellChartData.map((entry, index) => (
|
))}
{/* 2. Line Chart (Trend) */}
Voltage History (~1 min)
{/* Hide XAxis ticks as requested */}
[value.toFixed(3) + ' V', name]}
/>
{/* Dynamically create thin lines for each cell */}
{Array.from({ length: cellCount }).map((_, i) => (
))}
{/* Protections & Temps */}
{/* Protections */}
Protection Status
{protectionList.map((p) => (
// @ts-ignore
))}
{/* Temperatures */}
Temperatures
{basicInfo && basicInfo.ntcTemps.length > 0 ? basicInfo.ntcTemps.map((t, i) => (
NTC {i + 1}
{t.toFixed(1)}°C
)) : (
// Placeholder
NTC 1
--.-°C
)}
{/* Right Column: Controls & Capacity */}
{/* MOSFET Control */}
MOSFET Control
onToggleMosfet('charge', basicInfo?.mosfetStatus.charge ?? false)}
/>
onToggleMosfet('discharge', basicInfo?.mosfetStatus.discharge ?? false)}
/>
{/* Capacity Info */}
Capacity Info
Design Capacity
{basicInfo ? basicInfo.fullCapacity.toFixed(2) : '-.--'} Ah
Remaining Cap
{basicInfo ? basicInfo.remainingCapacity.toFixed(2) : '-.--'} Ah
Mfg Date
{basicInfo ? basicInfo.productionDate : 'YYYY-MM-DD'}
Device Ver
{hwVersion || '--'}
{/* Logger */}
Data Logger
{isRecording && ● REC ({logCount})}
);
};
export default Dashboard;