import React, { useState, useEffect, useRef, useCallback } from 'react'; import { SerialManager } from './services/serialService'; import { SerialLogEntry, ViewMode, NavigatorSerial } from './types'; import { generateId, formatTime, uInt8ArrayToHex, uInt8ArrayToAscii } from './utils'; import Terminal from './components/Terminal'; import ControlPanel from './components/ControlPanel'; import InputBar from './components/InputBar'; const BAUD_RATES = [9600, 19200, 38400, 57600, 115200]; const App: React.FC = () => { // Check for Browser Support const [isSupported, setIsSupported] = useState(true); // State for Configuration const [isDualMode, setIsDualMode] = useState(false); const [isAttached, setIsAttached] = useState(false); // Refs for State Access inside Callbacks (Fixes stale closures) const isAttachedRef = useRef(isAttached); const isDualModeRef = useRef(isDualMode); // Independent View Modes const [viewModeA, setViewModeA] = useState(ViewMode.ASCII); const [viewModeB, setViewModeB] = useState(ViewMode.ASCII); // Independent Baud Rates const [baudRateA, setBaudRateA] = useState(115200); const [baudRateB, setBaudRateB] = useState(115200); // State for Connections const [isConnectedA, setIsConnectedA] = useState(false); const [isConnectedB, setIsConnectedB] = useState(false); // Logs const [logsA, setLogsA] = useState([]); const [logsB, setLogsB] = useState([]); // Refs to hold SerialManager instances (persisted across renders) const serialA = useRef(new SerialManager('A')); const serialB = useRef(new SerialManager('B')); useEffect(() => { // Check if Web Serial is supported const nav = navigator as unknown as { serial: NavigatorSerial }; if (!nav.serial) { setIsSupported(false); console.warn("Web Serial API is not supported in this browser."); } return () => { serialA.current.disconnect(); serialB.current.disconnect(); }; }, []); // Sync Refs with State useEffect(() => { isAttachedRef.current = isAttached; }, [isAttached]); useEffect(() => { isDualModeRef.current = isDualMode; }, [isDualMode]); // Handle Bridging Logic useEffect(() => { if (isAttached && isDualMode) { // Connect bridges in the Service layer console.log("Bridging Enabled: A <-> B"); serialA.current.setBridgeTarget(serialB.current); serialB.current.setBridgeTarget(serialA.current); } else { // Disconnect bridges if (serialA.current['bridgeTarget'] !== null) console.log("Bridging Disabled"); serialA.current.setBridgeTarget(null); serialB.current.setBridgeTarget(null); } }, [isAttached, isDualMode]); // Safety: If Dual Mode is disabled, automatically detach useEffect(() => { if (!isDualMode && isAttached) { setIsAttached(false); } }, [isDualMode]); // --- Helper to add logs --- const addLog = useCallback((port: 'A' | 'B', direction: 'TX' | 'RX', data: Uint8Array) => { const entry: SerialLogEntry = { id: generateId(), timestamp: new Date(), direction, data }; if (port === 'A') { setLogsA(prev => [...prev.slice(-1000), entry]); // Keep last 1000 logs } else { setLogsB(prev => [...prev.slice(-1000), entry]); } }, []); // --- Log Saving Logic --- const handleSaveLogs = (portName: string, logs: SerialLogEntry[]) => { if (logs.length === 0) { alert(`No logs to save for ${portName}.`); return; } // Header let content = `SerialNexus Log - Port ${portName}\n`; content += `Exported at: ${new Date().toLocaleString()}\n`; content += `--------------------------------------------------------------------------------\n`; content += `TIMESTAMP | DIR | HEX DATA | ASCII\n`; content += `--------------------------------------------------------------------------------\n`; // Data content += logs.map(log => { const time = formatTime(log.timestamp).padEnd(23, ' '); const dir = log.direction.padEnd(3, ' '); const hex = uInt8ArrayToHex(log.data).padEnd(40, ' '); // simple padding const ascii = uInt8ArrayToAscii(log.data); return `${time} | ${dir} | ${hex} | ${ascii}`; }).join('\n'); const blob = new Blob([content], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `serial-log-${portName.toLowerCase()}-${new Date().toISOString().slice(0, 19).replace(/[:T]/g, '-')}.txt`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }; // --- Connection Handlers --- const handleConnectA = async () => { try { console.log("Connect A Clicked"); setLogsA([]); await serialA.current.connect(baudRateA); setIsConnectedA(true); serialA.current.startReading((data) => { // Log RX on A addLog('A', 'RX', data); // Visual Feedback for Bridge: If Attached, we assume it was sent to B // Note: The actual sending happens in SerialManager, here we just update the UI if (isAttachedRef.current && isDualModeRef.current && serialB.current.port) { addLog('B', 'TX', data); } }); } catch (err) { console.error("Connection Error A:", err); alert("Failed to connect to Port A. Check console for details."); } }; const handleDisconnectA = async () => { await serialA.current.disconnect(); setIsConnectedA(false); }; const handleConnectB = async () => { try { console.log("Connect B Clicked"); setLogsB([]); await serialB.current.connect(baudRateB); setIsConnectedB(true); serialB.current.startReading((data) => { // Log RX on B addLog('B', 'RX', data); // Visual Feedback for Bridge: If Attached, we assume it was sent to A if (isAttachedRef.current && isDualModeRef.current && serialA.current.port) { addLog('A', 'TX', data); } }); } catch (err) { console.error("Connection Error B:", err); alert("Failed to connect to Port B. Check console for details."); } }; const handleDisconnectB = async () => { await serialB.current.disconnect(); setIsConnectedB(false); }; // --- Send Handlers (Receives Uint8Array from InputBar) --- const handleSendA = async (data: Uint8Array) => { if (!isConnectedA) return; await serialA.current.send(data); addLog('A', 'TX', data); }; const handleSendB = async (data: Uint8Array) => { if (!isConnectedB) return; await serialB.current.send(data); addLog('B', 'TX', data); }; // --- Helper Component for Controls --- const renderControls = ( baud: number, setBaud: (r: number) => void, connected: boolean, onConnect: () => void, onDisconnect: () => void, viewMode: ViewMode, setViewMode: (v: ViewMode) => void ) => ( <>
Baud
{connected ? ( ) : ( )} {/* View Mode Toggles */}
); return (
Google AdSense Space (728x90)
{!isSupported && (
⚠️ Your browser does not support the Web Serial API. Please use Chrome, Edge, or Opera.
)} {/* Main Content Area */}
{/* Port A Terminal */}
setLogsA([])} onSave={() => handleSaveLogs("A", logsA)} className="flex-1" headerControls={renderControls( baudRateA, setBaudRateA, isConnectedA, handleConnectA, handleDisconnectA, viewModeA, setViewModeA )} />
{/* Port B Terminal (Only if Dual Mode) */} {isDualMode && (
setLogsB([])} onSave={() => handleSaveLogs("B", logsB)} className="flex-1 border-indigo-900/50 shadow-indigo-900/10" headerControls={renderControls( baudRateB, setBaudRateB, isConnectedB, handleConnectB, handleDisconnectB, viewModeB, setViewModeB )} />
)}
{/* Footer / Status */}
© 2026 SIMP {isDualMode ? (isAttached ? "MODE: DUAL MONITOR (ATTACHED: A <-> B)" : "MODE: DUAL MONITOR (INDEPENDENT)") : "MODE: SINGLE MONITOR (A)"}
); }; export default App;