diff --git a/App.tsx b/App.tsx index f0a3e24..47e9d2a 100644 --- a/App.tsx +++ b/App.tsx @@ -8,7 +8,7 @@ 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, Wifi, WifiOff } from 'lucide-react'; +import { Terminal, Settings, LayoutDashboard, Database, Zap, ChevronUp, ChevronDown, Wifi, WifiOff, AlertCircle, AlertTriangle } from 'lucide-react'; const App: React.FC = () => { // Serial State @@ -18,7 +18,7 @@ const App: React.FC = () => { const [address, setAddress] = useState(0xFF); // Broadcast address default // App State - Set Quick Test as Default - const [activeTab, setActiveTab] = useState<'connect' | 'inventory' | 'settings' | 'memory' | 'quicktest'>('quicktest'); + const [activeTab, setActiveTab] = useState<'connect' | 'inventory' | 'settings' | 'memory' | 'quicktest'>('connect'); const [logs, setLogs] = useState([]); const [tags, setTags] = useState([]); const [isScanning, setIsScanning] = useState(false); @@ -28,6 +28,21 @@ const App: React.FC = () => { // Log Visibility State const [isLogExpanded, setIsLogExpanded] = useState(false); + // Global Error Message State + const [globalMessage, setGlobalMessage] = useState<{ text: string, type: 'error' | 'info' } | null>(null); + const messageTimerRef = useRef(null); + + const showTemporaryMessage = (text: string, type: 'error' | 'info' = 'error') => { + if (messageTimerRef.current) { + window.clearTimeout(messageTimerRef.current); + } + setGlobalMessage({ text, type }); + messageTimerRef.current = window.setTimeout(() => { + setGlobalMessage(null); + messageTimerRef.current = null; + }, 5000); + }; + // Quick Action Config (Persisted in localStorage) const [quickTestConfig, setQuickTestConfig] = useState(() => { const saved = localStorage.getItem('quickTestConfig'); @@ -357,20 +372,26 @@ const App: React.FC = () => { if (status === 0x01 || status === 0x02 || status === 0x03 || status === 0x04) { handleInventoryResponse(data); } else if (status === 0xFB) { - addLog('INFO', RfidProtocol.getStatusDescription(status)); + const msg = RfidProtocol.getStatusDescription(status); + addLog('INFO', msg); + showTemporaryMessage(msg, 'info'); // Show as Info } else { let errorDetail = RfidProtocol.getStatusDescription(status); addLog('WARN', `Inventory Failed: ${errorDetail}`, `Status: 0x${status.toString(16).toUpperCase()}`); + showTemporaryMessage(`Inventory Failed: ${errorDetail}`, 'error'); } } else { if (status === 0xFB) { - addLog('INFO', RfidProtocol.getStatusDescription(status)); + const msg = RfidProtocol.getStatusDescription(status); + addLog('INFO', msg); + showTemporaryMessage(msg, 'info'); } 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); + showTemporaryMessage(`Command Failed: ${errorDetail}`, 'error'); if (reCmd === ReaderCommand.WRITE_DATA_G2 && performingQuickWriteRef.current) { performingQuickWriteRef.current = false; @@ -455,6 +476,10 @@ const App: React.FC = () => { // --- Operations --- const toggleScanning = useCallback(() => { + if (status !== ConnectionStatus.CONNECTED) { + alert("리더기가 연결되지 않았습니다! 먼저 시리얼 포트를 통해 연결해주세요."); + return; + } if (isScanning) { if (scanIntervalRef.current) clearInterval(scanIntervalRef.current); scanIntervalRef.current = null; @@ -465,13 +490,11 @@ const App: React.FC = () => { runScan(); scanIntervalRef.current = window.setInterval(runScan, 500); } - }, [isScanning, address]); + }, [isScanning, address, status]); const handleQuickRead = () => { if (status !== ConnectionStatus.CONNECTED) { - if (!performingQuickWriteRef.current) { - alert("Reader is not connected! Please connect via serial port first."); - } + alert("리더기가 연결되지 않았습니다! 먼저 시리얼 포트를 통해 연결해주세요."); return; } @@ -485,7 +508,7 @@ const App: React.FC = () => { const handleQuickWrite = (inputValue: string) => { if (status !== ConnectionStatus.CONNECTED) { - alert("Reader is not connected! Please connect via serial port first."); + alert("리더기가 연결되지 않았습니다! 먼저 시리얼 포트를 통해 연결해주세요."); return; } @@ -515,12 +538,16 @@ const App: React.FC = () => { setPendingQuickAction(null); performingQuickWriteRef.current = false; verificationRetriesRef.current = 0; - addLog('INFO', 'Quick Test Cancelled by user'); + addLog('INFO', '사용자에 의해 퀵 테스트 취소됨'); }; const clearTags = () => setTags([]); const handleGetInfo = async () => { + if (status !== ConnectionStatus.CONNECTED) { + alert("리더기가 연결되지 않았습니다! 먼저 시리얼 포트를 통해 연결해주세요."); + return; + } addLog('INFO', 'Requesting Reader Info...'); await sendCommand(ReaderCommand.GET_INFO); }; @@ -533,6 +560,10 @@ const App: React.FC = () => { maxFreq: number; band: FrequencyBand; }) => { + if (status !== ConnectionStatus.CONNECTED) { + alert("리더기가 연결되지 않았습니다! 먼저 시리얼 포트를 통해 연결해주세요."); + return; + } await sendCommand(ReaderCommand.SET_POWER, [settings.power]); if (settings.address !== address) { await sendCommand(ReaderCommand.SET_ADDRESS, [settings.address]); @@ -542,6 +573,10 @@ const App: React.FC = () => { }; const handleFactoryReset = async () => { + if (status !== ConnectionStatus.CONNECTED) { + alert("리더기가 연결되지 않았습니다! 먼저 시리얼 포트를 통해 연결해주세요."); + return; + } addLog('INFO', 'Sending Factory Reset...'); }; @@ -555,6 +590,10 @@ const App: React.FC = () => { }; const handleFetchTids = async () => { + if (status !== ConnectionStatus.CONNECTED) { + alert("리더기가 연결되지 않았습니다! 먼저 시리얼 포트를 통해 연결해주세요."); + return; + } if (isScanning) { if (scanIntervalRef.current) clearInterval(scanIntervalRef.current); scanIntervalRef.current = null; @@ -580,6 +619,10 @@ const App: React.FC = () => { }; const handleMemoryRead = async (bank: MemoryBank, ptr: number, len: number, pwd: string, targetEpc: string) => { + if (status !== ConnectionStatus.CONNECTED) { + alert("Reader is not connected! Please connect via serial port first."); + return; + } const { bytes: epcBytes, words: epcWords } = getEpcData(targetEpc); let pwdBytes = hexStringToBytes(pwd) || new Uint8Array([0, 0, 0, 0]); @@ -592,6 +635,10 @@ const App: React.FC = () => { }; const handleMemoryWrite = async (bank: MemoryBank, ptr: number, dataStr: string, pwd: string, targetEpc: string) => { + if (status !== ConnectionStatus.CONNECTED) { + alert("Reader is not connected! Please connect via serial port first."); + return; + } const { bytes: epcBytes, words: epcWords } = getEpcData(targetEpc); let pwdBytes = hexStringToBytes(pwd) || new Uint8Array([0, 0, 0, 0]); const dataBytes = hexStringToBytes(dataStr); @@ -637,7 +684,7 @@ const App: React.FC = () => { UHF Reader -

Web Serial Controller

+

RFID Read & Write Tester

@@ -732,7 +779,18 @@ const App: React.FC = () => {
-
+
+ {/* Global Status/Error Banner */} + {globalMessage && ( +
+ {globalMessage.type === 'error' ? : } + {globalMessage.text} +
+ )} + {activeTab === 'connect' && (

Connection Manager

diff --git a/services/rfidService.ts b/services/rfidService.ts index ae4a0e9..59fff0f 100644 --- a/services/rfidService.ts +++ b/services/rfidService.ts @@ -7,53 +7,53 @@ export class RfidProtocol { */ static buildCommand(address: number, cmd: ReaderCommand, data: number[] = []): Uint8Array { const len = data.length + 4; // Len byte itself is not included in length value logic usually, but manual says: "Len equals length of Data[] plus 4" - + // Frame except CRC const frameHeader = [len, address, cmd, ...data]; - + // Calculate CRC on [Len, Adr, Cmd, Data...] const crc = calculateCRC16(new Uint8Array(frameHeader)); const [lsb, msb] = getCRCBytes(crc); - + return new Uint8Array([...frameHeader, lsb, msb]); } /** - * Helper to interpret status codes from the reader + * Helper to interpret status codes from the reader (Translated to Korean) */ static getStatusDescription(status: number): string { switch (status) { - case 0x00: return "Success"; - case 0x01: return "Return before Inventory finished"; - case 0x02: return "Inventory Timeout"; - case 0x03: return "More data follows"; - case 0x04: return "Reader memory full"; - case 0x05: return "Access Password Error"; - case 0x09: return "Kill Tag Error"; - case 0x0A: return "Kill Password cannot be zero"; - case 0x0B: return "Tag does not support command"; - case 0x0C: return "Access Password cannot be zero (Locked)"; - case 0x0D: return "Tag is protected, cannot set again"; - case 0x0E: return "Tag is unprotected, no need to reset"; - case 0x10: return "Locked bytes, write fail"; - case 0x11: return "Cannot lock tag"; - case 0x12: return "Already locked, cannot lock again"; - case 0x13: return "Save Parameter Failed"; - case 0x14: return "Cannot adjust power"; - case 0x15: return "Return before Inventory finished (6B)"; - case 0x16: return "Inventory Timeout (6B)"; - case 0x17: return "More data follows (6B)"; - case 0x18: return "Reader memory full (6B)"; - case 0x19: return "Not Support Command or Access Password Error"; - case 0xF9: return "Command Execution Error"; - case 0xFA: return "Poor Communication / Tag Inoperable"; - case 0xFB: return "No Tag Operable"; - case 0xFC: return "Tag Returned Error Code"; // Check data for specific code - case 0xFD: return "Command Length Wrong"; - case 0xFE: return "Illegal Command / CRC Error"; - case 0xFF: return "Parameter Error"; - case 0x30: return "Communication Error"; - default: return `Unknown Status (0x${status.toString(16).toUpperCase().padStart(2, '0')})`; + case 0x00: return "성공"; + case 0x01: return "인벤토리 완료 전 반환됨"; + case 0x02: return "인벤토리 시간 초과"; + case 0x03: return "추가 데이터 있음"; + case 0x04: return "리더 메모리 가득 참"; + case 0x05: return "액세스 비밀번호 오류"; + case 0x09: return "태그 킬(Kill) 오류"; + case 0x0A: return "킬(Kill) 비밀번호는 0일 수 없음"; + case 0x0B: return "태그가 명령을 지원하지 않음"; + case 0x0C: return "액세스 비밀번호는 0일 수 없음 (잠김)"; + case 0x0D: return "태그가 보호되어 있어 다시 설정 불가"; + case 0x0E: return "태그가 보호되지 않아 재설정 필요 없음"; + case 0x10: return "잠긴 바이트 존재, 쓰기 실패"; + case 0x11: return "태그를 잠글 수 없음"; + case 0x12: return "이미 잠겨 있어 다시 잠글 수 없음"; + case 0x13: return "파라미터 저장 실패"; + case 0x14: return "전력을 조절할 수 없음"; + case 0x15: return "인벤토리 완료 전 반환됨 (6B)"; + case 0x16: return "인벤토리 시간 초과 (6B)"; + case 0x17: return "추가 데이터 있음 (6B)"; + case 0x18: return "리더 메모리 가득 참 (6B)"; + case 0x19: return "명령 미지원 또는 액세스 비밀번호 오류"; + case 0xF9: return "명령 실행 오류"; + case 0xFA: return "통신 불량 / 태그 작동 불가"; + case 0xFB: return "태그 없음 (감지 불가)"; + case 0xFC: return "태그 오류 코드 반환"; // Check data for specific code + case 0xFD: return "명령 길이 오류"; + case 0xFE: return "잘못된 명령 / CRC 오류"; + case 0xFF: return "파라미터 오류"; + case 0x30: return "통신 오류"; + default: return `알 수 없는 상태 (0x${status.toString(16).toUpperCase().padStart(2, '0')})`; } } @@ -62,12 +62,12 @@ export class RfidProtocol { */ static getTagErrorDescription(errorCode: number): string { switch (errorCode) { - case 0x00: return "Other error"; - case 0x03: return "Memory overrun or EPC length not supported"; - case 0x04: return "Memory locked"; - case 0x0B: return "Insufficient power"; - case 0x0F: return "Non-specific error"; - default: return `Unknown Tag Error (0x${errorCode.toString(16).toUpperCase().padStart(2, '0')})`; + case 0x00: return "기타 오류"; + case 0x03: return "메모리 초과 또는 EPC 길이 미지원"; + case 0x04: return "메모리 잠김"; + case 0x0B: return "전력 부족"; + case 0x0F: return "불특정 오류"; + default: return `알 수 없는 태그 오류 (0x${errorCode.toString(16).toUpperCase().padStart(2, '0')})`; } } } \ No newline at end of file