Files
JBD_BMS_Tools/App.tsx

266 lines
10 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { serialService } from './services/serialService';
import { JBDProtocol, CMD_BASIC_INFO, CMD_CELL_INFO, CMD_HW_VERSION } from './services/jbdProtocol';
import { BMSBasicInfo, BMSCellInfo, ConnectionState } from './types';
import Dashboard from './components/Dashboard';
import Settings from './components/Settings';
import Terminal from './components/Terminal';
import { LayoutDashboard, Settings as SettingsIcon, Usb, AlertCircle, RefreshCw, PanelRightClose, PanelRightOpen } from 'lucide-react';
const App: React.FC = () => {
const [connectionState, setConnectionState] = useState<ConnectionState>(ConnectionState.DISCONNECTED);
const [basicInfo, setBasicInfo] = useState<BMSBasicInfo | null>(null);
const [cellInfo, setCellInfo] = useState<BMSCellInfo | null>(null);
const [hwVersion, setHwVersion] = useState<string | null>(null);
const [activeTab, setActiveTab] = useState<'dashboard' | 'settings'>('dashboard');
const [errorMsg, setErrorMsg] = useState<string | null>(null);
const [isTerminalOpen, setIsTerminalOpen] = useState(true);
// Polling Logic
useEffect(() => {
try {
// @ts-ignore
(window.adsbygoogle = window.adsbygoogle || []).push({});
} catch (e) {
console.error("AdSense error", e);
}
}, []);
useEffect(() => {
let isMounted = true;
let timeoutId: number;
const runPoll = async () => {
// ONLY POLL IF ON DASHBOARD AND CONNECTED
if (connectionState !== ConnectionState.CONNECTED || activeTab !== 'dashboard') return;
try {
// 1. Get Basic Info
const basicData = await serialService.sendCommand(CMD_BASIC_INFO);
const parsedBasic = JBDProtocol.parseBasicInfo(basicData);
if (isMounted) setBasicInfo(parsedBasic);
// Delay 500ms
await new Promise(r => setTimeout(r, 500));
if (!isMounted) return;
// 2. Get Cell Info
const cellData = await serialService.sendCommand(CMD_CELL_INFO);
const parsedCells = JBDProtocol.parseCellInfo(cellData);
if (isMounted) setCellInfo(parsedCells);
// Delay 500ms
await new Promise(r => setTimeout(r, 500));
if (!isMounted) return;
// 3. Get Hardware Version (CMD 0x05)
const hwData = await serialService.sendCommand(CMD_HW_VERSION);
// CMD 0x05 usually returns raw ASCII string in payload
const versionStr = new TextDecoder().decode(hwData);
if (isMounted) setHwVersion(versionStr);
if (isMounted) setErrorMsg(null);
} catch (e: any) {
console.error("Polling error:", e);
// Log to terminal for debugging
serialService.log('error', `Poll Fail: ${e.message}`);
} finally {
// Schedule next poll cycle in 500ms
if (isMounted && connectionState === ConnectionState.CONNECTED && activeTab === 'dashboard') {
timeoutId = window.setTimeout(runPoll, 500);
}
}
};
if (connectionState === ConnectionState.CONNECTED && activeTab === 'dashboard') {
runPoll();
}
return () => {
isMounted = false;
window.clearTimeout(timeoutId);
};
}, [connectionState, activeTab]);
const handleConnect = async () => {
try {
setConnectionState(ConnectionState.CONNECTING);
await serialService.connect();
setConnectionState(ConnectionState.CONNECTED);
setErrorMsg(null);
} catch (e: any) {
console.error(e);
setConnectionState(ConnectionState.ERROR);
setErrorMsg(e.message || "시리얼 포트 연결 실패");
}
};
const handleDisconnect = async () => {
try {
await serialService.disconnect();
setConnectionState(ConnectionState.DISCONNECTED);
setBasicInfo(null);
setCellInfo(null);
setHwVersion(null);
} catch (e) {
console.error(e);
}
};
const toggleMosfet = async (type: 'charge' | 'discharge', currentState: boolean) => {
if (!basicInfo) return;
try {
const newCharge = type === 'charge' ? !currentState : basicInfo.mosfetStatus.charge;
const newDischarge = type === 'discharge' ? !currentState : basicInfo.mosfetStatus.discharge;
await serialService.toggleMosfet(newCharge, newDischarge);
} catch (e: any) {
alert("MOSFET 제어 실패: " + e.message);
}
};
const renderContent = () => {
switch (activeTab) {
case 'dashboard':
return <Dashboard basicInfo={basicInfo} cellInfo={cellInfo} hwVersion={hwVersion} onToggleMosfet={toggleMosfet} />;
case 'settings':
return <Settings />;
default:
return null;
}
};
return (
<div className="flex h-screen w-full bg-gray-50 text-gray-900 overflow-hidden">
{/* Sidebar */}
<div className="w-20 lg:w-64 flex-shrink-0 bg-white border-r border-gray-200 flex flex-col z-20">
<div className="p-6 flex items-center justify-center lg:justify-start">
<div className="h-8 w-8 bg-blue-600 rounded-lg flex items-center justify-center font-bold shadow-lg shadow-blue-900/50">
J
</div>
<span className="ml-3 font-bold text-xl hidden lg:block tracking-tight text-gray-800">JBD Tool</span>
</div>
<nav className="flex-1 px-4 space-y-2 mt-4">
<button
onClick={() => setActiveTab('dashboard')}
className={`w-full flex items-center p-3 rounded-xl transition-all ${activeTab === 'dashboard'
? 'bg-blue-600 text-white shadow-lg shadow-blue-900/20'
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-900'
}`}
>
<LayoutDashboard size={20} />
<span className="ml-3 hidden lg:block font-medium">Dashboard</span>
</button>
<button
onClick={() => setActiveTab('settings')}
className={`w-full flex items-center p-3 rounded-xl transition-all ${activeTab === 'settings'
? 'bg-blue-600 text-white shadow-lg shadow-blue-900/20'
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-900'
}`}
>
<SettingsIcon size={20} />
<span className="ml-3 hidden lg:block font-medium">EEPROM</span>
</button>
</nav>
{/* AdSense Unit */}
<div className="px-4 pb-2">
<div className="text-[10px] text-gray-600 text-center mb-1">Sponsored</div>
<ins className="adsbygoogle"
style={{ display: "block" }}
data-ad-client="ca-pub-4444852135420953"
data-ad-slot="7799405796"
data-ad-format="auto"
data-full-width-responsive="true"></ins>
</div>
<div className="p-4 border-t border-gray-200 flex-1 flex flex-col justify-end">
{/* AdSense Area - Expanded */}
</div>
</div>
{/* Main Content Area */}
<div className="flex-1 flex flex-col h-full overflow-hidden relative">
{/* Header */}
<header className="h-16 bg-white/80 backdrop-blur border-b border-gray-200 flex items-center justify-between px-6 z-10 flex-shrink-0">
<h1 className="text-xl font-semibold text-gray-800">
{activeTab === 'dashboard' ? 'Overview' : 'Configuration'}
</h1>
<div className="flex items-center gap-4">
{connectionState === ConnectionState.CONNECTED ? (
<button
onClick={handleDisconnect}
className="py-1.5 px-3 bg-red-100 text-red-600 border border-red-200 rounded-lg hover:bg-red-200 transition-colors flex items-center text-sm font-semibold"
>
<Usb size={16} className="mr-2" />
Disconnect
</button>
) : (
<button
onClick={handleConnect}
disabled={connectionState === ConnectionState.CONNECTING}
className="py-1.5 px-3 bg-green-600 text-white rounded-lg hover:bg-green-500 transition-colors flex items-center text-sm font-semibold shadow-sm"
>
{connectionState === ConnectionState.CONNECTING ? <RefreshCw className="animate-spin mr-2" size={16} /> : <Usb size={16} className="mr-2" />}
Connect
</button>
)}
<button
onClick={() => setIsTerminalOpen(!isTerminalOpen)}
className="p-2 text-gray-500 hover:bg-gray-100 rounded-lg transition-colors"
title={isTerminalOpen ? "Hide Terminal" : "Show Terminal"}
>
{isTerminalOpen ? <PanelRightClose size={20} /> : <PanelRightOpen size={20} />}
</button>
</div>
</header>
{/* Content Body with Right Sidebar */}
<div className="flex-1 flex overflow-hidden">
{/* Main View */}
<main className="flex-1 overflow-hidden bg-gray-50 relative">
{errorMsg && (
<div className="bg-red-900/80 text-white px-6 py-2 flex items-center justify-center text-sm font-medium backdrop-blur absolute top-0 left-0 right-0 z-30 animate-in slide-in-from-top-2">
<AlertCircle size={16} className="mr-2" />
{errorMsg}
</div>
)}
{renderContent()}
{/* Empty State Overlay */}
{connectionState !== ConnectionState.CONNECTED && (
<div className="absolute inset-0 bg-white/80 backdrop-blur-sm z-10 flex flex-col items-center justify-center p-6 text-center">
<div className="w-16 h-16 bg-gray-100 rounded-2xl flex items-center justify-center mb-6 shadow-xl border border-gray-200">
<Usb size={32} className="text-gray-500" />
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-2"> </h2>
<p className="text-gray-500 max-w-md mb-8">
JBD BMS를 UART-to-USB .
</p>
</div>
)}
</main>
{/* Right Terminal Panel */}
<aside className={`${isTerminalOpen ? 'w-80 lg:w-96 border-l' : 'w-0 border-l-0'} transition-all duration-300 ease-in-out border-gray-200 bg-white flex flex-col flex-shrink-0 z-20 overflow-hidden`}>
<div className="w-80 lg:w-96 h-full flex flex-col">
<Terminal />
</div>
</aside>
</div>
</div>
</div>
);
};
export default App;