import React, { useState, useEffect, useRef, useCallback } from 'react'; import { ConnectionPanel } from './components/ConnectionPanel'; import { InventoryPanel } from './components/InventoryPanel'; import { SettingsPanel } from './components/SettingsPanel'; import { MemoryPanel } from './components/MemoryPanel'; import { QuickTestPanel } from './components/QuickTestPanel'; import { ConnectionStatus, TagData, ReaderCommand, FrequencyBand, MemoryBank, LogEntry, SerialPort, ReaderInfo, QuickTestConfig } from './types'; import { RfidProtocol } from './services/rfidService'; import { calculateCRC16, bytesToHexString, hexStringToBytes, hexToAscii, asciiToHex } from './utils/crc16'; import { Terminal, Settings, LayoutDashboard, Database, Zap, ChevronUp, ChevronDown } from 'lucide-react'; const App: React.FC = () => { // Serial State const [port, setPort] = useState(null); const [status, setStatus] = useState(ConnectionStatus.DISCONNECTED); const [baudRate, setBaudRate] = useState(57600); const [address, setAddress] = useState(0xFF); // Broadcast address default // App State - Set Quick Test as Default const [activeTab, setActiveTab] = useState<'inventory' | 'settings' | 'memory' | 'quicktest'>('quicktest'); const [logs, setLogs] = useState([]); const [tags, setTags] = useState([]); const [isScanning, setIsScanning] = useState(false); const [readerInfo, setReaderInfo] = useState(null); const [readResult, setReadResult] = useState(null); // Log Visibility State const [isLogExpanded, setIsLogExpanded] = useState(false); // Quick Action Config (Persisted in localStorage) const [quickTestConfig, setQuickTestConfig] = useState(() => { const saved = localStorage.getItem('quickTestConfig'); return saved ? JSON.parse(saved) : { length: 3, format: 'ascii' }; }); // Quick Action State const [pendingQuickAction, setPendingQuickAction] = useState<'read' | 'write' | null>(null); const [quickWriteData, setQuickWriteData] = useState(""); // Actual Hex data to write (State for UI consistency) const [quickWriteInput, setQuickWriteInput] = useState(""); // UI Input state // References for streams and state tracking const readerRef = useRef | null>(null); const writerRef = useRef | null>(null); const scanIntervalRef = useRef(null); const isConnectedRef = useRef(false); // Track if a Quick Write is in progress to trigger auto-read verification const performingQuickWriteRef = useRef(false); const quickWriteDataRef = useRef(""); // Ref to hold write data for verification inside async loop const verificationRetriesRef = useRef(0); // Track retries // Track which EPC is currently being queried for TID const currentTidReadEpc = useRef(null); // Stale Closure Fix: Ref to hold the latest processFrame function const processFrameRef = useRef<(frame: Uint8Array) => void>(() => {}); const addLog = (type: LogEntry['type'], message: string, data?: string) => { setLogs(prev => [{ id: Math.random().toString(36).substr(2, 9), timestamp: new Date(), type, message, data }, ...prev].slice(0, 100)); // Keep last 100 logs }; const saveQuickTestConfig = (config: QuickTestConfig) => { setQuickTestConfig(config); localStorage.setItem('quickTestConfig', JSON.stringify(config)); addLog('INFO', `Quick Test Settings Saved to Browser: Len=${config.length}, Fmt=${config.format}`); alert("Quick Test settings saved to browser storage."); }; // Auto-read settings when connected (With Warm-up Sequence) useEffect(() => { if (status === ConnectionStatus.CONNECTED && port) { // Start a warm-up sequence to stabilize connection const warmup = async () => { // 1. Initial delay for hardware port opening await new Promise(r => setTimeout(r, 300)); // 2. Send a "Sacrificial" command. // Devices often error on the very first byte received after connection/power-up due to sync issues. // We send this to clear the pipe, expecting it might fail (silent retry). addLog('INFO', 'Initializing Handshake...'); await sendCommand(ReaderCommand.GET_INFO); // 3. Wait for the error/response to clear await new Promise(r => setTimeout(r, 500)); // 4. Send the actual Sync command addLog('INFO', 'Syncing Reader Info...'); handleGetInfo(); }; warmup(); } }, [status, port]); // Quick Action Logic: Watch tags list updates useEffect(() => { if (pendingQuickAction && tags.length > 0) { const targetTag = tags[0]; // Pick the first tag found const action = pendingQuickAction; const dataToWrite = quickWriteData; // 1. Stop Scanning immediately if (scanIntervalRef.current) { clearInterval(scanIntervalRef.current); scanIntervalRef.current = null; setIsScanning(false); } // 2. Clear pending state so we don't fire again setPendingQuickAction(null); setQuickWriteData(""); // 3. Execute Memory Operation with small delay to ensure bus is clear setTimeout(() => { if (action === 'read') { addLog('INFO', `Quick Read detected tag: ${targetTag.epc}`); handleMemoryRead(MemoryBank.EPC, 2, quickTestConfig.length, "00000000", targetTag.epc); } else if (action === 'write') { // Set Flag to trigger verification read on success performingQuickWriteRef.current = true; verificationRetriesRef.current = 0; // Reset retries addLog('INFO', `Quick Write detected tag: ${targetTag.epc}`); handleMemoryWrite(MemoryBank.EPC, 2, dataToWrite, "00000000", targetTag.epc); } }, 200); } }, [tags, pendingQuickAction, quickWriteData, quickTestConfig]); const connectSerial = async () => { try { setStatus(ConnectionStatus.CONNECTING); // @ts-ignore const p = await navigator.serial.requestPort(); await p.open({ baudRate }); setPort(p); isConnectedRef.current = true; setStatus(ConnectionStatus.CONNECTED); addLog('INFO', `Connected to serial port at ${baudRate}`); startReading(p); } catch (err: any) { console.error(err); setStatus(ConnectionStatus.ERROR); addLog('ERROR', 'Failed to connect: ' + err.message); } }; const disconnectSerial = async () => { if (scanIntervalRef.current) { clearInterval(scanIntervalRef.current); scanIntervalRef.current = null; setIsScanning(false); } isConnectedRef.current = false; try { if (readerRef.current) { await readerRef.current.cancel().catch(() => {}); readerRef.current = null; } if (writerRef.current) { await writerRef.current.releaseLock(); writerRef.current = null; } if (port) { await port.close(); setPort(null); } setStatus(ConnectionStatus.DISCONNECTED); addLog('INFO', 'Disconnected'); } catch (err: any) { addLog('ERROR', 'Error disconnecting: ' + err.message); } }; // --- Serial I/O --- const startReading = async (p: SerialPort) => { if (!p.readable) return; while (p.readable && isConnectedRef.current) { const reader = p.readable.getReader(); readerRef.current = reader; try { let buffer = new Uint8Array(0); while (true) { const { value, done } = await reader.read(); if (done) break; if (value && value.length > 0) { addLog('INFO', 'RAW RX', bytesToHexString(value)); const newBuffer = new Uint8Array(buffer.length + value.length); newBuffer.set(buffer); newBuffer.set(value, buffer.length); buffer = newBuffer; while (buffer.length > 0) { const lenByte = buffer[0]; if (lenByte === 0) { buffer = buffer.slice(1); continue; } const totalFrameSize = lenByte + 1; if (buffer.length >= totalFrameSize) { const frame = buffer.slice(0, totalFrameSize); // Call the latest logic via Ref processFrameRef.current(frame); buffer = buffer.slice(totalFrameSize); } else { break; } } } } } catch (error) { console.error("Read Error:", error); if (isConnectedRef.current) { addLog('ERROR', 'Read Loop Error', String(error)); } break; } finally { try { reader.releaseLock(); } catch (e) { } } } }; const sendCommand = async (cmd: ReaderCommand, data: number[] = []) => { if (!port || !port.writable) { addLog('ERROR', 'Port not writable or disconnected'); return; } try { let targetAddr = address; if (address === 0xFF && readerInfo && readerInfo.address !== 0xFF) { targetAddr = readerInfo.address; } const frame = RfidProtocol.buildCommand(targetAddr, cmd, data); addLog('TX', `CMD: 0x${cmd.toString(16).toUpperCase()}`, bytesToHexString(frame)); const writer = port.writable.getWriter(); writerRef.current = writer; await writer.write(frame); writer.releaseLock(); } catch (e: any) { addLog('ERROR', 'Send Failed', e.message); } }; // --- Logic --- const processFrame = (frame: Uint8Array) => { try { if (frame.length < 4) { addLog('ERROR', 'Incomplete Frame', bytesToHexString(frame)); return; } const len = frame[0]; const addr = frame[1]; const reCmd = frame[2]; const status = frame[3]; let data = frame.length > 6 ? frame.slice(4, frame.length - 2) : new Uint8Array(0); addLog('RX', `Cmd:0x${reCmd.toString(16).toUpperCase()} Stat:0x${status.toString(16).toUpperCase()}`, bytesToHexString(frame)); if (status === 0x00) { if (reCmd === ReaderCommand.GET_INFO) { handleGetInfoResponse(data, addr); } else if (reCmd === ReaderCommand.READ_DATA_G2) { if (data.length > 0 && data[0] === 0x00) { data = data.slice(1); } const hexData = bytesToHexString(data); let displayData = hexData; if (quickTestConfig.format === 'ascii') { displayData = hexToAscii(hexData); } setReadResult(displayData); if (activeTab === 'quicktest') { setQuickWriteInput(displayData); } addLog('INFO', 'Memory Read Success', hexData); if (currentTidReadEpc.current) { const targetEpc = currentTidReadEpc.current; setTags(prev => prev.map(t => t.epc === targetEpc ? { ...t, tid: hexData } : t )); } // === Verification Logic === if (performingQuickWriteRef.current) { // Compare Read Data (Hex) with Expected Write Data (Hex) const normalize = (s: string) => s.replace(/[\s-]/g, '').toUpperCase(); const readHex = normalize(hexData); const expectedHex = normalize(quickWriteDataRef.current); if (readHex === expectedHex) { addLog('INFO', 'VERIFICATION SUCCESS: Data matches written value.'); performingQuickWriteRef.current = false; verificationRetriesRef.current = 0; alert("Quick Test Successful: Write verified."); } else { if (verificationRetriesRef.current < 5) { verificationRetriesRef.current += 1; addLog('WARN', `Verification Mismatch (${verificationRetriesRef.current}/5). Retrying read...`, `Read: ${readHex}, Exp: ${expectedHex}`); setTimeout(() => { handleQuickRead(); }, 500); } else { addLog('ERROR', 'VERIFICATION FAILED: Data mismatch after 5 attempts.'); performingQuickWriteRef.current = false; verificationRetriesRef.current = 0; alert("Quick Test Failed: Verification mismatch after 5 attempts."); } } } } else if (reCmd === ReaderCommand.WRITE_DATA_G2) { addLog('INFO', 'Memory Write Success'); if (performingQuickWriteRef.current) { addLog('INFO', 'Verifying written data in 0.5s...'); setTimeout(() => { handleQuickRead(); }, 500); } } else if (reCmd === ReaderCommand.WRITE_EPC_G2) { addLog('INFO', 'EPC Write Success (Cmd 0x04)'); } else if (reCmd === ReaderCommand.SET_ACCESS_EPC_MATCH) { addLog('INFO', 'Tag Selected Successfully'); } } else if (reCmd === ReaderCommand.INVENTORY_G2) { if (status === 0x01 || status === 0x02 || status === 0x03 || status === 0x04) { handleInventoryResponse(data); } } else { let errorDetail = RfidProtocol.getStatusDescription(status); if (status === 0xFC && data.length > 0) { errorDetail += ` - ${RfidProtocol.getTagErrorDescription(data[0])}`; } addLog('ERROR', `Command 0x${reCmd.toString(16).toUpperCase()} Failed`, errorDetail); if (reCmd === ReaderCommand.WRITE_DATA_G2 && performingQuickWriteRef.current) { performingQuickWriteRef.current = false; verificationRetriesRef.current = 0; addLog('ERROR', 'Quick Write Verification Cancelled due to Write Error'); alert("Quick Test Failed during Write operation."); } } } catch (err) { console.error("Frame Process Error", err); addLog('ERROR', 'Frame Parse Error', bytesToHexString(frame)); } }; // Sync the latest processFrame to the ref so the async loop uses the latest closure useEffect(() => { processFrameRef.current = processFrame; }); const handleInventoryResponse = (data: Uint8Array) => { if (data.length === 0) return; let numTags = data[0]; let offset = 1; for (let i = 0; i < numTags; i++) { if (offset >= data.length) break; const epcLen = data[offset]; if (offset + 1 + epcLen > data.length) break; const epcBytes = data.slice(offset + 1, offset + 1 + epcLen); const epcString = bytesToHexString(epcBytes); updateTag(epcString); offset += 1 + epcLen; } }; const handleGetInfoResponse = (data: Uint8Array, sourceAddr: number) => { let version = '1.0'; let protocol: ReaderInfo['protocol'] = 'EPCC1-G2'; let power = 0; let band: FrequencyBand = FrequencyBand.US; if (data.length > 0) { const maj = data[0]; const min = data.length > 1 ? data[1] : 0; version = `${maj}.${min.toString(16).toUpperCase().padStart(2, '0')}`; } if (data.length >= 8) { if (data[3] >= 0 && data[3] <= 4) band = data[3] as FrequencyBand; if (data.length > 6) power = data[6]; } else { if (data.length > 2) power = data[2]; } setReaderInfo({ model: 'RRU9809', version: version, address: sourceAddr, power: power, minFreq: 0, maxFreq: 0, protocol: protocol, maxResponseTime: 0, band: band }); addLog('INFO', 'Reader Info Parsed', `Ver:${version}, Pwr:${power}dBm`); }; const updateTag = (epc: string) => { setTags(prev => { const existing = prev.find(t => t.epc === epc); if (existing) { return prev.map(t => t.epc === epc ? { ...t, count: t.count + 1, timestamp: Date.now() } : t); } else { return [...prev, { epc, count: 1, timestamp: Date.now() }]; } }); }; // --- Operations --- const toggleScanning = useCallback(() => { if (isScanning) { if (scanIntervalRef.current) clearInterval(scanIntervalRef.current); scanIntervalRef.current = null; setIsScanning(false); } else { setIsScanning(true); const runScan = () => sendCommand(ReaderCommand.INVENTORY_G2, []); runScan(); scanIntervalRef.current = window.setInterval(runScan, 500); } }, [isScanning, address]); const handleQuickRead = () => { if (status !== ConnectionStatus.CONNECTED) { if (!performingQuickWriteRef.current) { alert("Reader is not connected! Please connect via serial port first."); } return; } setReadResult(null); setTags([]); setPendingQuickAction('read'); if (!isScanning) { toggleScanning(); } }; const handleQuickWrite = (inputValue: string) => { if (status !== ConnectionStatus.CONNECTED) { alert("Reader is not connected! Please connect via serial port first."); return; } let finalHexData = inputValue; if (quickTestConfig.format === 'ascii') { finalHexData = asciiToHex(inputValue); } // Store in State for UI setQuickWriteData(finalHexData); // Store in Ref for Async Logic quickWriteDataRef.current = finalHexData; setTags([]); setPendingQuickAction('write'); if (!isScanning) { toggleScanning(); } }; const handleCancelQuickAction = () => { if (scanIntervalRef.current) { clearInterval(scanIntervalRef.current); scanIntervalRef.current = null; } setIsScanning(false); setPendingQuickAction(null); performingQuickWriteRef.current = false; verificationRetriesRef.current = 0; addLog('INFO', 'Quick Test Cancelled by user'); }; const clearTags = () => setTags([]); const handleGetInfo = async () => { addLog('INFO', 'Requesting Reader Info...'); await sendCommand(ReaderCommand.GET_INFO); }; const handleSetParameters = async (settings: { address: number; baudRate: number; power: number; minFreq: number; maxFreq: number; band: FrequencyBand; }) => { await sendCommand(ReaderCommand.SET_POWER, [settings.power]); if (settings.address !== address) { await sendCommand(ReaderCommand.SET_ADDRESS, [settings.address]); setAddress(settings.address); } addLog('INFO', 'Parameters update command sent'); }; const handleFactoryReset = async () => { addLog('INFO', 'Sending Factory Reset...'); }; const getEpcData = (epc: string) => { const bytes = hexStringToBytes(epc); if (!bytes) return { bytes: [], words: 0 }; return { bytes: Array.from(bytes), words: Math.ceil(bytes.length / 2) }; }; const handleFetchTids = async () => { if (isScanning) { if (scanIntervalRef.current) clearInterval(scanIntervalRef.current); scanIntervalRef.current = null; setIsScanning(false); } if (tags.length === 0) return; addLog('INFO', 'Starting Batch TID Read...'); for (const tag of tags) { currentTidReadEpc.current = tag.epc; const { bytes: epcBytes, words: epcWords } = getEpcData(tag.epc); if (epcBytes.length === 0) continue; const payload = [ epcWords, ...epcBytes, MemoryBank.TID, 0, 6, 0,0,0,0, 0, 0 ]; addLog('INFO', `Reading TID for ${tag.epc}...`); await sendCommand(ReaderCommand.READ_DATA_G2, payload); await new Promise(r => setTimeout(r, 100)); } currentTidReadEpc.current = null; addLog('INFO', 'Batch TID Read Complete'); }; const handleMemoryRead = async (bank: MemoryBank, ptr: number, len: number, pwd: string, targetEpc: string) => { const { bytes: epcBytes, words: epcWords } = getEpcData(targetEpc); let pwdBytes = hexStringToBytes(pwd) || new Uint8Array([0,0,0,0]); const payload = [ epcWords, ...epcBytes, bank, ptr & 0xFF, len & 0xFF, ...Array.from(pwdBytes), 0, 0 ]; addLog('INFO', `Read Req: Bank ${bank}, Ptr ${ptr}, Len ${len}`); await sendCommand(ReaderCommand.READ_DATA_G2, payload); }; const handleMemoryWrite = async (bank: MemoryBank, ptr: number, dataStr: string, pwd: string, targetEpc: string) => { const { bytes: epcBytes, words: epcWords } = getEpcData(targetEpc); let pwdBytes = hexStringToBytes(pwd) || new Uint8Array([0,0,0,0]); const dataBytes = hexStringToBytes(dataStr); if (!dataBytes) { addLog('ERROR', 'Invalid Hex Data for Write'); return; } const wNum = Math.ceil(dataBytes.length / 2); const payload = [ wNum, epcWords, ...epcBytes, bank, ptr & 0xFF, ...Array.from(dataBytes), ...Array.from(pwdBytes), 0, 0 ]; addLog('INFO', `Write Req: Bank ${bank}, Ptr ${ptr}, Data ${dataStr}`); await sendCommand(ReaderCommand.WRITE_DATA_G2, payload); }; const handleWriteEpc = async (newEpc: string, pwd: string) => { const { bytes: epcBytes, words: epcWords } = getEpcData(newEpc); if (epcWords === 0) { addLog('ERROR', 'Invalid EPC Data'); return; } let pwdBytes = hexStringToBytes(pwd) || new Uint8Array([0,0,0,0]); const payload = [epcWords, ...Array.from(pwdBytes), ...epcBytes]; addLog('INFO', `Write EPC (0x04): ${newEpc}`); await sendCommand(ReaderCommand.WRITE_EPC_G2, payload); }; const getLogHeightClass = () => { if (isLogExpanded) return 'h-96'; if (activeTab === 'quicktest' || activeTab === 'settings') return 'h-12'; return 'h-48'; }; return (
{/* Sidebar */}

UHF Reader

Web Serial Controller

{/* Main Content */}
{activeTab === 'inventory' && ( )} {activeTab === 'quicktest' && ( )} {activeTab === 'memory' && ( 0 ? tags[0].epc : ""} readResult={readResult} /> )} {activeTab === 'settings' && ( )}
{/* Log Terminal */}
setIsLogExpanded(!isLogExpanded)} >
System Logs
{isLogExpanded ? : }
{logs.length === 0 && No logs yet...} {logs.map(log => (
[{log.timestamp.toLocaleTimeString()}] {log.type} {log.message} {log.data && {log.data}}
))}
); }; export default App;