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 })}
{label}
{icon}
{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-400" /> } color={basicInfo && basicInfo.current > 0 ? "text-green-400" : basicInfo && basicInfo.current < 0 ? "text-red-400" : "text-gray-400"} subValue={power !== 0 ? `${power.toFixed(0)} W` : undefined} /> } // If Low SOC: Blink Red Icon, otherwise green/gray color={isLowSoc ? "text-red-500 animate-pulse" : (basicInfo ? "text-green-400" : "text-gray-400")} // If Low SOC: Blink Red Value Text, otherwise default white valueClassName={isLowSoc ? "text-red-500 animate-pulse" : undefined} subValue={basicInfo ? `${basicInfo.remainingCapacity.toFixed(2)} Ah` : undefined} /> } color="text-blue-400" /> } color={deltaVoltage > 0.03 ? "text-red-400" : "text-emerald-400"} /> } color="text-purple-400" />
{/* 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="#4b5563" 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;