This commit is contained in:
backuppc
2026-01-23 11:54:38 +09:00
parent 8b1bfb205d
commit 3396104011
2 changed files with 2083 additions and 217 deletions

384
App.tsx
View File

@@ -8,7 +8,7 @@ import { QuickTestPanel } from './components/QuickTestPanel';
import { ConnectionStatus, TagData, ReaderCommand, FrequencyBand, MemoryBank, LogEntry, SerialPort, ReaderInfo, QuickTestConfig } from './types'; import { ConnectionStatus, TagData, ReaderCommand, FrequencyBand, MemoryBank, LogEntry, SerialPort, ReaderInfo, QuickTestConfig } from './types';
import { RfidProtocol } from './services/rfidService'; import { RfidProtocol } from './services/rfidService';
import { calculateCRC16, bytesToHexString, hexStringToBytes, hexToAscii, asciiToHex } from './utils/crc16'; import { calculateCRC16, bytesToHexString, hexStringToBytes, hexToAscii, asciiToHex } from './utils/crc16';
import { Terminal, Settings, LayoutDashboard, Database, Zap, ChevronUp, ChevronDown } from 'lucide-react'; import { Terminal, Settings, LayoutDashboard, Database, Zap, ChevronUp, ChevronDown, Wifi, WifiOff } from 'lucide-react';
const App: React.FC = () => { const App: React.FC = () => {
// Serial State // Serial State
@@ -54,7 +54,7 @@ const App: React.FC = () => {
const currentTidReadEpc = useRef<string | null>(null); const currentTidReadEpc = useRef<string | null>(null);
// Stale Closure Fix: Ref to hold the latest processFrame function // Stale Closure Fix: Ref to hold the latest processFrame function
const processFrameRef = useRef<(frame: Uint8Array) => void>(() => {}); const processFrameRef = useRef<(frame: Uint8Array) => void>(() => { });
const addLog = (type: LogEntry['type'], message: string, data?: string) => { const addLog = (type: LogEntry['type'], message: string, data?: string) => {
setLogs(prev => [{ setLogs(prev => [{
@@ -76,26 +76,26 @@ const App: React.FC = () => {
// Auto-read settings when connected (With Warm-up Sequence) // Auto-read settings when connected (With Warm-up Sequence)
useEffect(() => { useEffect(() => {
if (status === ConnectionStatus.CONNECTED && port) { if (status === ConnectionStatus.CONNECTED && port) {
// Start a warm-up sequence to stabilize connection // Start a warm-up sequence to stabilize connection
const warmup = async () => { const warmup = async () => {
// 1. Initial delay for hardware port opening // 1. Initial delay for hardware port opening
await new Promise(r => setTimeout(r, 300)); await new Promise(r => setTimeout(r, 300));
// 2. Send a "Sacrificial" command. // 2. Send a "Sacrificial" command.
// Devices often error on the very first byte received after connection/power-up due to sync issues. // 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). // We send this to clear the pipe, expecting it might fail (silent retry).
addLog('INFO', 'Initializing Handshake...'); addLog('INFO', 'Initializing Handshake...');
await sendCommand(ReaderCommand.GET_INFO); await sendCommand(ReaderCommand.GET_INFO);
// 3. Wait for the error/response to clear // 3. Wait for the error/response to clear
await new Promise(r => setTimeout(r, 500)); await new Promise(r => setTimeout(r, 500));
// 4. Send the actual Sync command // 4. Send the actual Sync command
addLog('INFO', 'Syncing Reader Info...'); addLog('INFO', 'Syncing Reader Info...');
handleGetInfo(); handleGetInfo();
}; };
warmup(); warmup();
} }
}, [status, port]); }, [status, port]);
@@ -163,7 +163,7 @@ const App: React.FC = () => {
isConnectedRef.current = false; isConnectedRef.current = false;
try { try {
if (readerRef.current) { if (readerRef.current) {
await readerRef.current.cancel().catch(() => {}); await readerRef.current.cancel().catch(() => { });
readerRef.current = null; readerRef.current = null;
} }
if (writerRef.current) { if (writerRef.current) {
@@ -207,8 +207,8 @@ const App: React.FC = () => {
while (buffer.length > 0) { while (buffer.length > 0) {
const lenByte = buffer[0]; const lenByte = buffer[0];
if (lenByte === 0) { if (lenByte === 0) {
buffer = buffer.slice(1); buffer = buffer.slice(1);
continue; continue;
} }
const totalFrameSize = lenByte + 1; const totalFrameSize = lenByte + 1;
@@ -226,7 +226,7 @@ const App: React.FC = () => {
} catch (error) { } catch (error) {
console.error("Read Error:", error); console.error("Read Error:", error);
if (isConnectedRef.current) { if (isConnectedRef.current) {
addLog('ERROR', 'Read Loop Error', String(error)); addLog('ERROR', 'Read Loop Error', String(error));
} }
break; break;
} finally { } finally {
@@ -239,25 +239,25 @@ const App: React.FC = () => {
const sendCommand = async (cmd: ReaderCommand, data: number[] = []) => { const sendCommand = async (cmd: ReaderCommand, data: number[] = []) => {
if (!port || !port.writable) { if (!port || !port.writable) {
addLog('ERROR', 'Port not writable or disconnected'); addLog('ERROR', 'Port not writable or disconnected');
return; return;
} }
try { try {
let targetAddr = address; let targetAddr = address;
if (address === 0xFF && readerInfo && readerInfo.address !== 0xFF) { if (address === 0xFF && readerInfo && readerInfo.address !== 0xFF) {
targetAddr = readerInfo.address; targetAddr = readerInfo.address;
} }
const frame = RfidProtocol.buildCommand(targetAddr, cmd, data); const frame = RfidProtocol.buildCommand(targetAddr, cmd, data);
addLog('TX', `CMD: 0x${cmd.toString(16).toUpperCase()}`, bytesToHexString(frame)); addLog('TX', `CMD: 0x${cmd.toString(16).toUpperCase()}`, bytesToHexString(frame));
const writer = port.writable.getWriter(); const writer = port.writable.getWriter();
writerRef.current = writer; writerRef.current = writer;
await writer.write(frame); await writer.write(frame);
writer.releaseLock(); writer.releaseLock();
} catch (e: any) { } catch (e: any) {
addLog('ERROR', 'Send Failed', e.message); addLog('ERROR', 'Send Failed', e.message);
} }
}; };
@@ -266,8 +266,8 @@ const App: React.FC = () => {
const processFrame = (frame: Uint8Array) => { const processFrame = (frame: Uint8Array) => {
try { try {
if (frame.length < 4) { if (frame.length < 4) {
addLog('ERROR', 'Incomplete Frame', bytesToHexString(frame)); addLog('ERROR', 'Incomplete Frame', bytesToHexString(frame));
return; return;
} }
const len = frame[0]; const len = frame[0];
@@ -283,67 +283,67 @@ const App: React.FC = () => {
handleGetInfoResponse(data, addr); handleGetInfoResponse(data, addr);
} else if (reCmd === ReaderCommand.READ_DATA_G2) { } else if (reCmd === ReaderCommand.READ_DATA_G2) {
if (data.length > 0 && data[0] === 0x00) { if (data.length > 0 && data[0] === 0x00) {
data = data.slice(1); data = data.slice(1);
} }
const hexData = bytesToHexString(data); const hexData = bytesToHexString(data);
let displayData = hexData; let displayData = hexData;
if (quickTestConfig.format === 'ascii') { if (quickTestConfig.format === 'ascii') {
displayData = hexToAscii(hexData); displayData = hexToAscii(hexData);
} }
setReadResult(displayData); setReadResult(displayData);
if (activeTab === 'quicktest') { if (activeTab === 'quicktest') {
setQuickWriteInput(displayData); setQuickWriteInput(displayData);
} }
addLog('INFO', 'Memory Read Success', hexData); addLog('INFO', 'Memory Read Success', hexData);
if (currentTidReadEpc.current) { if (currentTidReadEpc.current) {
const targetEpc = currentTidReadEpc.current; const targetEpc = currentTidReadEpc.current;
setTags(prev => prev.map(t => setTags(prev => prev.map(t =>
t.epc === targetEpc ? { ...t, tid: hexData } : t t.epc === targetEpc ? { ...t, tid: hexData } : t
)); ));
} }
// === Verification Logic === // === Verification Logic ===
if (performingQuickWriteRef.current) { if (performingQuickWriteRef.current) {
// Compare Read Data (Hex) with Expected Write Data (Hex) // Compare Read Data (Hex) with Expected Write Data (Hex)
const normalize = (s: string) => s.replace(/[\s-]/g, '').toUpperCase(); const normalize = (s: string) => s.replace(/[\s-]/g, '').toUpperCase();
const readHex = normalize(hexData); const readHex = normalize(hexData);
const expectedHex = normalize(quickWriteDataRef.current); const expectedHex = normalize(quickWriteDataRef.current);
if (readHex === expectedHex) { if (readHex === expectedHex) {
addLog('INFO', 'VERIFICATION SUCCESS: Data matches written value.'); addLog('INFO', 'VERIFICATION SUCCESS: Data matches written value.');
performingQuickWriteRef.current = false; performingQuickWriteRef.current = false;
verificationRetriesRef.current = 0; verificationRetriesRef.current = 0;
alert("Quick Test Successful: Write verified."); alert("Quick Test Successful: Write verified.");
} else { } else {
if (verificationRetriesRef.current < 5) { if (verificationRetriesRef.current < 5) {
verificationRetriesRef.current += 1; verificationRetriesRef.current += 1;
addLog('WARN', `Verification Mismatch (${verificationRetriesRef.current}/5). Retrying read...`, `Read: ${readHex}, Exp: ${expectedHex}`); addLog('WARN', `Verification Mismatch (${verificationRetriesRef.current}/5). Retrying read...`, `Read: ${readHex}, Exp: ${expectedHex}`);
setTimeout(() => { setTimeout(() => {
handleQuickRead(); handleQuickRead();
}, 500); }, 500);
} else { } else {
addLog('ERROR', 'VERIFICATION FAILED: Data mismatch after 5 attempts.'); addLog('ERROR', 'VERIFICATION FAILED: Data mismatch after 5 attempts.');
performingQuickWriteRef.current = false; performingQuickWriteRef.current = false;
verificationRetriesRef.current = 0; verificationRetriesRef.current = 0;
alert("Quick Test Failed: Verification mismatch after 5 attempts."); alert("Quick Test Failed: Verification mismatch after 5 attempts.");
} }
} }
} }
} else if (reCmd === ReaderCommand.WRITE_DATA_G2) { } else if (reCmd === ReaderCommand.WRITE_DATA_G2) {
addLog('INFO', 'Memory Write Success'); addLog('INFO', 'Memory Write Success');
if (performingQuickWriteRef.current) { if (performingQuickWriteRef.current) {
addLog('INFO', 'Verifying written data in 0.5s...'); addLog('INFO', 'Verifying written data in 0.5s...');
setTimeout(() => { setTimeout(() => {
handleQuickRead(); handleQuickRead();
}, 500); }, 500);
} }
} else if (reCmd === ReaderCommand.WRITE_EPC_G2) { } else if (reCmd === ReaderCommand.WRITE_EPC_G2) {
@@ -353,22 +353,22 @@ const App: React.FC = () => {
} }
} }
else if (reCmd === ReaderCommand.INVENTORY_G2) { else if (reCmd === ReaderCommand.INVENTORY_G2) {
if (status === 0x01 || status === 0x02 || status === 0x03 || status === 0x04) { if (status === 0x01 || status === 0x02 || status === 0x03 || status === 0x04) {
handleInventoryResponse(data); handleInventoryResponse(data);
} }
} else { } else {
let errorDetail = RfidProtocol.getStatusDescription(status); let errorDetail = RfidProtocol.getStatusDescription(status);
if (status === 0xFC && data.length > 0) { if (status === 0xFC && data.length > 0) {
errorDetail += ` - ${RfidProtocol.getTagErrorDescription(data[0])}`; errorDetail += ` - ${RfidProtocol.getTagErrorDescription(data[0])}`;
} }
addLog('ERROR', `Command 0x${reCmd.toString(16).toUpperCase()} Failed`, errorDetail); addLog('ERROR', `Command 0x${reCmd.toString(16).toUpperCase()} Failed`, errorDetail);
if (reCmd === ReaderCommand.WRITE_DATA_G2 && performingQuickWriteRef.current) { if (reCmd === ReaderCommand.WRITE_DATA_G2 && performingQuickWriteRef.current) {
performingQuickWriteRef.current = false; performingQuickWriteRef.current = false;
verificationRetriesRef.current = 0; verificationRetriesRef.current = 0;
addLog('ERROR', 'Quick Write Verification Cancelled due to Write Error'); addLog('ERROR', 'Quick Write Verification Cancelled due to Write Error');
alert("Quick Test Failed during Write operation."); alert("Quick Test Failed during Write operation.");
} }
} }
} catch (err) { } catch (err) {
@@ -410,10 +410,10 @@ const App: React.FC = () => {
} }
if (data.length >= 8) { if (data.length >= 8) {
if (data[3] >= 0 && data[3] <= 4) band = data[3] as FrequencyBand; if (data[3] >= 0 && data[3] <= 4) band = data[3] as FrequencyBand;
if (data.length > 6) power = data[6]; if (data.length > 6) power = data[6];
} else { } else {
if (data.length > 2) power = data[2]; if (data.length > 2) power = data[2];
} }
setReaderInfo({ setReaderInfo({
@@ -459,10 +459,10 @@ const App: React.FC = () => {
const handleQuickRead = () => { const handleQuickRead = () => {
if (status !== ConnectionStatus.CONNECTED) { if (status !== ConnectionStatus.CONNECTED) {
if (!performingQuickWriteRef.current) { if (!performingQuickWriteRef.current) {
alert("Reader is not connected! Please connect via serial port first."); alert("Reader is not connected! Please connect via serial port first.");
} }
return; return;
} }
setReadResult(null); setReadResult(null);
@@ -475,13 +475,13 @@ const App: React.FC = () => {
const handleQuickWrite = (inputValue: string) => { const handleQuickWrite = (inputValue: string) => {
if (status !== ConnectionStatus.CONNECTED) { if (status !== ConnectionStatus.CONNECTED) {
alert("Reader is not connected! Please connect via serial port first."); alert("Reader is not connected! Please connect via serial port first.");
return; return;
} }
let finalHexData = inputValue; let finalHexData = inputValue;
if (quickTestConfig.format === 'ascii') { if (quickTestConfig.format === 'ascii') {
finalHexData = asciiToHex(inputValue); finalHexData = asciiToHex(inputValue);
} }
// Store in State for UI // Store in State for UI
@@ -497,15 +497,15 @@ const App: React.FC = () => {
}; };
const handleCancelQuickAction = () => { const handleCancelQuickAction = () => {
if (scanIntervalRef.current) { if (scanIntervalRef.current) {
clearInterval(scanIntervalRef.current); clearInterval(scanIntervalRef.current);
scanIntervalRef.current = null; scanIntervalRef.current = null;
} }
setIsScanning(false); setIsScanning(false);
setPendingQuickAction(null); setPendingQuickAction(null);
performingQuickWriteRef.current = false; performingQuickWriteRef.current = false;
verificationRetriesRef.current = 0; verificationRetriesRef.current = 0;
addLog('INFO', 'Quick Test Cancelled by user'); addLog('INFO', 'Quick Test Cancelled by user');
}; };
const clearTags = () => setTags([]); const clearTags = () => setTags([]);
@@ -525,45 +525,45 @@ const App: React.FC = () => {
}) => { }) => {
await sendCommand(ReaderCommand.SET_POWER, [settings.power]); await sendCommand(ReaderCommand.SET_POWER, [settings.power]);
if (settings.address !== address) { if (settings.address !== address) {
await sendCommand(ReaderCommand.SET_ADDRESS, [settings.address]); await sendCommand(ReaderCommand.SET_ADDRESS, [settings.address]);
setAddress(settings.address); setAddress(settings.address);
} }
addLog('INFO', 'Parameters update command sent'); addLog('INFO', 'Parameters update command sent');
}; };
const handleFactoryReset = async () => { const handleFactoryReset = async () => {
addLog('INFO', 'Sending Factory Reset...'); addLog('INFO', 'Sending Factory Reset...');
}; };
const getEpcData = (epc: string) => { const getEpcData = (epc: string) => {
const bytes = hexStringToBytes(epc); const bytes = hexStringToBytes(epc);
if (!bytes) return { bytes: [], words: 0 }; if (!bytes) return { bytes: [], words: 0 };
return { return {
bytes: Array.from(bytes), bytes: Array.from(bytes),
words: Math.ceil(bytes.length / 2) words: Math.ceil(bytes.length / 2)
}; };
}; };
const handleFetchTids = async () => { const handleFetchTids = async () => {
if (isScanning) { if (isScanning) {
if (scanIntervalRef.current) clearInterval(scanIntervalRef.current); if (scanIntervalRef.current) clearInterval(scanIntervalRef.current);
scanIntervalRef.current = null; scanIntervalRef.current = null;
setIsScanning(false); setIsScanning(false);
} }
if (tags.length === 0) return; if (tags.length === 0) return;
addLog('INFO', 'Starting Batch TID Read...'); addLog('INFO', 'Starting Batch TID Read...');
for (const tag of tags) { for (const tag of tags) {
currentTidReadEpc.current = tag.epc; currentTidReadEpc.current = tag.epc;
const { bytes: epcBytes, words: epcWords } = getEpcData(tag.epc); const { bytes: epcBytes, words: epcWords } = getEpcData(tag.epc);
if (epcBytes.length === 0) continue; if (epcBytes.length === 0) continue;
const payload = [ const payload = [
epcWords, ...epcBytes, MemoryBank.TID, 0, 6, 0,0,0,0, 0, 0 epcWords, ...epcBytes, MemoryBank.TID, 0, 6, 0, 0, 0, 0, 0, 0
]; ];
addLog('INFO', `Reading TID for ${tag.epc}...`); addLog('INFO', `Reading TID for ${tag.epc}...`);
await sendCommand(ReaderCommand.READ_DATA_G2, payload); await sendCommand(ReaderCommand.READ_DATA_G2, payload);
await new Promise(r => setTimeout(r, 100)); await new Promise(r => setTimeout(r, 100));
} }
currentTidReadEpc.current = null; currentTidReadEpc.current = null;
addLog('INFO', 'Batch TID Read Complete'); addLog('INFO', 'Batch TID Read Complete');
@@ -571,10 +571,10 @@ const App: React.FC = () => {
const handleMemoryRead = async (bank: MemoryBank, ptr: number, len: number, pwd: string, targetEpc: string) => { const handleMemoryRead = async (bank: MemoryBank, ptr: number, len: number, pwd: string, targetEpc: string) => {
const { bytes: epcBytes, words: epcWords } = getEpcData(targetEpc); const { bytes: epcBytes, words: epcWords } = getEpcData(targetEpc);
let pwdBytes = hexStringToBytes(pwd) || new Uint8Array([0,0,0,0]); let pwdBytes = hexStringToBytes(pwd) || new Uint8Array([0, 0, 0, 0]);
const payload = [ const payload = [
epcWords, ...epcBytes, bank, ptr & 0xFF, len & 0xFF, ...Array.from(pwdBytes), 0, 0 epcWords, ...epcBytes, bank, ptr & 0xFF, len & 0xFF, ...Array.from(pwdBytes), 0, 0
]; ];
addLog('INFO', `Read Req: Bank ${bank}, Ptr ${ptr}, Len ${len}`); addLog('INFO', `Read Req: Bank ${bank}, Ptr ${ptr}, Len ${len}`);
@@ -583,7 +583,7 @@ const App: React.FC = () => {
const handleMemoryWrite = async (bank: MemoryBank, ptr: number, dataStr: string, pwd: string, targetEpc: string) => { const handleMemoryWrite = async (bank: MemoryBank, ptr: number, dataStr: string, pwd: string, targetEpc: string) => {
const { bytes: epcBytes, words: epcWords } = getEpcData(targetEpc); const { bytes: epcBytes, words: epcWords } = getEpcData(targetEpc);
let pwdBytes = hexStringToBytes(pwd) || new Uint8Array([0,0,0,0]); let pwdBytes = hexStringToBytes(pwd) || new Uint8Array([0, 0, 0, 0]);
const dataBytes = hexStringToBytes(dataStr); const dataBytes = hexStringToBytes(dataStr);
if (!dataBytes) { if (!dataBytes) {
@@ -593,7 +593,7 @@ const App: React.FC = () => {
const wNum = Math.ceil(dataBytes.length / 2); const wNum = Math.ceil(dataBytes.length / 2);
const payload = [ const payload = [
wNum, epcWords, ...epcBytes, bank, ptr & 0xFF, ...Array.from(dataBytes), ...Array.from(pwdBytes), 0, 0 wNum, epcWords, ...epcBytes, bank, ptr & 0xFF, ...Array.from(dataBytes), ...Array.from(pwdBytes), 0, 0
]; ];
addLog('INFO', `Write Req: Bank ${bank}, Ptr ${ptr}, Data ${dataStr}`); addLog('INFO', `Write Req: Bank ${bank}, Ptr ${ptr}, Data ${dataStr}`);
@@ -606,7 +606,7 @@ const App: React.FC = () => {
addLog('ERROR', 'Invalid EPC Data'); addLog('ERROR', 'Invalid EPC Data');
return; return;
} }
let pwdBytes = hexStringToBytes(pwd) || new Uint8Array([0,0,0,0]); let pwdBytes = hexStringToBytes(pwd) || new Uint8Array([0, 0, 0, 0]);
const payload = [epcWords, ...Array.from(pwdBytes), ...epcBytes]; const payload = [epcWords, ...Array.from(pwdBytes), ...epcBytes];
addLog('INFO', `Write EPC (0x04): ${newEpc}`); addLog('INFO', `Write EPC (0x04): ${newEpc}`);
@@ -622,7 +622,7 @@ const App: React.FC = () => {
return ( return (
<div className="flex h-screen bg-slate-100"> <div className="flex h-screen bg-slate-100">
{/* Sidebar */} {/* Sidebar */}
<div className="w-64 bg-white border-r border-slate-200 flex flex-col z-10"> <div className="hidden md:flex w-64 bg-white border-r border-slate-200 flex-col z-10">
<div className="p-6 border-b border-slate-100"> <div className="p-6 border-b border-slate-100">
<h1 className="text-xl font-bold text-slate-800 flex items-center gap-2"> <h1 className="text-xl font-bold text-slate-800 flex items-center gap-2">
<Database className="text-blue-600" /> <Database className="text-blue-600" />
@@ -634,33 +634,29 @@ const App: React.FC = () => {
<div className="p-4 space-y-2 flex-1"> <div className="p-4 space-y-2 flex-1">
<button <button
onClick={() => setActiveTab('quicktest')} onClick={() => setActiveTab('quicktest')}
className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium transition-colors ${ className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium transition-colors ${activeTab === 'quicktest' ? 'bg-indigo-50 text-indigo-700' : 'text-slate-600 hover:bg-slate-50'
activeTab === 'quicktest' ? 'bg-indigo-50 text-indigo-700' : 'text-slate-600 hover:bg-slate-50' }`}
}`}
> >
<Zap className="w-5 h-5" /> Quick Test <Zap className="w-5 h-5" /> Quick Test
</button> </button>
<button <button
onClick={() => setActiveTab('inventory')} onClick={() => setActiveTab('inventory')}
className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium transition-colors ${ className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium transition-colors ${activeTab === 'inventory' ? 'bg-blue-50 text-blue-700' : 'text-slate-600 hover:bg-slate-50'
activeTab === 'inventory' ? 'bg-blue-50 text-blue-700' : 'text-slate-600 hover:bg-slate-50' }`}
}`}
> >
<LayoutDashboard className="w-5 h-5" /> Inventory <LayoutDashboard className="w-5 h-5" /> Inventory
</button> </button>
<button <button
onClick={() => setActiveTab('memory')} onClick={() => setActiveTab('memory')}
className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium transition-colors ${ className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium transition-colors ${activeTab === 'memory' ? 'bg-blue-50 text-blue-700' : 'text-slate-600 hover:bg-slate-50'
activeTab === 'memory' ? 'bg-blue-50 text-blue-700' : 'text-slate-600 hover:bg-slate-50' }`}
}`}
> >
<Database className="w-5 h-5" /> Read / Write <Database className="w-5 h-5" /> Read / Write
</button> </button>
<button <button
onClick={() => setActiveTab('settings')} onClick={() => setActiveTab('settings')}
className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium transition-colors ${ className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium transition-colors ${activeTab === 'settings' ? 'bg-blue-50 text-blue-700' : 'text-slate-600 hover:bg-slate-50'
activeTab === 'settings' ? 'bg-blue-50 text-blue-700' : 'text-slate-600 hover:bg-slate-50' }`}
}`}
> >
<Settings className="w-5 h-5" /> Settings <Settings className="w-5 h-5" /> Settings
</button> </button>
@@ -681,6 +677,61 @@ const App: React.FC = () => {
{/* Main Content */} {/* Main Content */}
<div className="flex-1 flex flex-col overflow-hidden relative"> <div className="flex-1 flex flex-col overflow-hidden relative">
{/* Mobile Top Navigation Bar */}
<div className="md:hidden bg-white border-b border-slate-200 p-2 flex items-center justify-between shrink-0 overflow-x-auto">
<div className="flex items-center gap-3 pr-4 border-r border-slate-100 mr-2 shrink-0">
<div className="flex items-center gap-1">
<Database className="text-blue-600 w-5 h-5" />
<span className="font-bold text-slate-800 text-sm">UHF</span>
</div>
{/* Connection Status Indicator */}
<button
onClick={() => setActiveTab('settings')}
className={`flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium transition-colors ${status === ConnectionStatus.CONNECTED
? 'bg-emerald-50 text-emerald-600 border border-emerald-100'
: 'bg-red-50 text-red-600 border border-red-100 animate-pulse'
}`}
>
{status === ConnectionStatus.CONNECTED ? <Wifi className="w-3 h-3" /> : <WifiOff className="w-3 h-3" />}
<span>{status === ConnectionStatus.CONNECTED ? 'On' : 'Connect'}</span>
</button>
</div>
<div className="flex items-center gap-1 flex-1 justify-around">
<button
onClick={() => setActiveTab('quicktest')}
className={`p-2 rounded-lg transition-colors flex flex-col items-center justify-center gap-1 ${activeTab === 'quicktest' ? 'bg-indigo-50 text-indigo-700' : 'text-slate-600 hover:bg-slate-50'
}`}
>
<Zap className="w-5 h-5" />
<span className="text-[10px] font-medium leading-none">Quick</span>
</button>
<button
onClick={() => setActiveTab('inventory')}
className={`p-2 rounded-lg transition-colors flex flex-col items-center justify-center gap-1 ${activeTab === 'inventory' ? 'bg-blue-50 text-blue-700' : 'text-slate-600 hover:bg-slate-50'
}`}
>
<LayoutDashboard className="w-5 h-5" />
<span className="text-[10px] font-medium leading-none">Inv.</span>
</button>
<button
onClick={() => setActiveTab('memory')}
className={`p-2 rounded-lg transition-colors flex flex-col items-center justify-center gap-1 ${activeTab === 'memory' ? 'bg-blue-50 text-blue-700' : 'text-slate-600 hover:bg-slate-50'
}`}
>
<Database className="w-5 h-5" />
<span className="text-[10px] font-medium leading-none">R/W</span>
</button>
<button
onClick={() => setActiveTab('settings')}
className={`p-2 rounded-lg transition-colors flex flex-col items-center justify-center gap-1 ${activeTab === 'settings' ? 'bg-blue-50 text-blue-700' : 'text-slate-600 hover:bg-slate-50'
}`}
>
<Settings className="w-5 h-5" />
<span className="text-[10px] font-medium leading-none">Set</span>
</button>
</div>
</div>
<main className="flex-1 p-6 overflow-auto"> <main className="flex-1 p-6 overflow-auto">
{activeTab === 'inventory' && ( {activeTab === 'inventory' && (
<InventoryPanel <InventoryPanel
@@ -692,17 +743,17 @@ const App: React.FC = () => {
/> />
)} )}
{activeTab === 'quicktest' && ( {activeTab === 'quicktest' && (
<QuickTestPanel <QuickTestPanel
onQuickRead={handleQuickRead} onQuickRead={handleQuickRead}
onQuickWrite={handleQuickWrite} onQuickWrite={handleQuickWrite}
onCancel={handleCancelQuickAction} onCancel={handleCancelQuickAction}
writeInput={quickWriteInput} writeInput={quickWriteInput}
onWriteInputChange={setQuickWriteInput} onWriteInputChange={setQuickWriteInput}
result={readResult} result={readResult}
isPending={pendingQuickAction !== null} isPending={pendingQuickAction !== null}
isScanning={isScanning} isScanning={isScanning}
config={quickTestConfig} config={quickTestConfig}
/> />
)} )}
{activeTab === 'memory' && ( {activeTab === 'memory' && (
<MemoryPanel <MemoryPanel
@@ -731,15 +782,15 @@ const App: React.FC = () => {
{/* Log Terminal */} {/* Log Terminal */}
<div className={`${getLogHeightClass()} bg-white border-t border-slate-200 flex flex-col transition-all duration-300 ease-in-out`}> <div className={`${getLogHeightClass()} bg-white border-t border-slate-200 flex flex-col transition-all duration-300 ease-in-out`}>
<div <div
className="px-4 py-2 bg-slate-50 border-b border-slate-100 flex items-center justify-between cursor-pointer hover:bg-slate-100" className="px-4 py-2 bg-slate-50 border-b border-slate-100 flex items-center justify-between cursor-pointer hover:bg-slate-100"
onClick={() => setIsLogExpanded(!isLogExpanded)} onClick={() => setIsLogExpanded(!isLogExpanded)}
> >
<div className="flex items-center gap-2 text-xs font-semibold text-slate-700 uppercase tracking-wider"> <div className="flex items-center gap-2 text-xs font-semibold text-slate-700 uppercase tracking-wider">
<Terminal className="w-3 h-3" /> System Logs <Terminal className="w-3 h-3" /> System Logs
</div> </div>
<div> <div>
{isLogExpanded ? <ChevronDown className="w-4 h-4 text-slate-400"/> : <ChevronUp className="w-4 h-4 text-slate-400"/>} {isLogExpanded ? <ChevronDown className="w-4 h-4 text-slate-400" /> : <ChevronUp className="w-4 h-4 text-slate-400" />}
</div> </div>
</div> </div>
<div className="flex-1 overflow-auto p-4 font-mono text-xs space-y-1 text-slate-600"> <div className="flex-1 overflow-auto p-4 font-mono text-xs space-y-1 text-slate-600">
@@ -747,12 +798,11 @@ const App: React.FC = () => {
{logs.map(log => ( {logs.map(log => (
<div key={log.id} className="flex gap-2"> <div key={log.id} className="flex gap-2">
<span className="text-slate-400">[{log.timestamp.toLocaleTimeString()}]</span> <span className="text-slate-400">[{log.timestamp.toLocaleTimeString()}]</span>
<span className={`font-bold ${ <span className={`font-bold ${log.type === 'TX' ? 'text-blue-600' :
log.type === 'TX' ? 'text-blue-600' :
log.type.includes('RX') ? 'text-emerald-600' : log.type.includes('RX') ? 'text-emerald-600' :
log.type === 'ERROR' ? 'text-red-600' : log.type === 'ERROR' ? 'text-red-600' :
log.type === 'WARN' ? 'text-amber-600' : 'text-slate-700' log.type === 'WARN' ? 'text-amber-600' : 'text-slate-700'
}`}> }`}>
{log.type} {log.type}
</span> </span>
<span className="text-slate-800">{log.message}</span> <span className="text-slate-800">{log.message}</span>

1816
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff