827 lines
31 KiB
TypeScript
827 lines
31 KiB
TypeScript
|
|
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, Wifi, WifiOff } from 'lucide-react';
|
|
|
|
const App: React.FC = () => {
|
|
// Serial State
|
|
const [port, setPort] = useState<SerialPort | null>(null);
|
|
const [status, setStatus] = useState<ConnectionStatus>(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<'connect' | 'inventory' | 'settings' | 'memory' | 'quicktest'>('quicktest');
|
|
const [logs, setLogs] = useState<LogEntry[]>([]);
|
|
const [tags, setTags] = useState<TagData[]>([]);
|
|
const [isScanning, setIsScanning] = useState(false);
|
|
const [readerInfo, setReaderInfo] = useState<ReaderInfo | null>(null);
|
|
const [readResult, setReadResult] = useState<string | null>(null);
|
|
|
|
// Log Visibility State
|
|
const [isLogExpanded, setIsLogExpanded] = useState(false);
|
|
|
|
// Quick Action Config (Persisted in localStorage)
|
|
const [quickTestConfig, setQuickTestConfig] = useState<QuickTestConfig>(() => {
|
|
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<string>(""); // Actual Hex data to write (State for UI consistency)
|
|
const [quickWriteInput, setQuickWriteInput] = useState<string>(""); // UI Input state
|
|
|
|
// References for streams and state tracking
|
|
const readerRef = useRef<ReadableStreamDefaultReader<Uint8Array> | null>(null);
|
|
const writerRef = useRef<WritableStreamDefaultWriter<Uint8Array> | null>(null);
|
|
const scanIntervalRef = useRef<number | null>(null);
|
|
const isConnectedRef = useRef<boolean>(false);
|
|
|
|
// Track if a Quick Write is in progress to trigger auto-read verification
|
|
const performingQuickWriteRef = useRef<boolean>(false);
|
|
const quickWriteDataRef = useRef<string>(""); // Ref to hold write data for verification inside async loop
|
|
const verificationRetriesRef = useRef<number>(0); // Track retries
|
|
|
|
// Track which EPC is currently being queried for TID
|
|
const currentTidReadEpc = useRef<string | null>(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}`);
|
|
setActiveTab('quicktest');
|
|
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-[30vh]';
|
|
return 'h-12';
|
|
};
|
|
|
|
return (
|
|
<div className="flex h-screen bg-slate-100">
|
|
{/* Sidebar */}
|
|
<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">
|
|
<h1 className="text-xl font-bold text-slate-800 flex items-center gap-2">
|
|
<Database className="text-blue-600" />
|
|
UHF Reader
|
|
</h1>
|
|
<p className="text-xs text-slate-400 mt-1">Web Serial Controller</p>
|
|
</div>
|
|
|
|
<div className="p-4 space-y-2 flex-1">
|
|
<button
|
|
onClick={() => setActiveTab('connect')}
|
|
className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium transition-colors ${activeTab === 'connect' ? 'bg-emerald-50 text-emerald-700' : 'text-slate-600 hover:bg-slate-50'
|
|
}`}
|
|
>
|
|
<Wifi className="w-5 h-5" /> Connection
|
|
</button>
|
|
<button
|
|
onClick={() => setActiveTab('quicktest')}
|
|
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'
|
|
}`}
|
|
>
|
|
<Zap className="w-5 h-5" /> Quick Test
|
|
</button>
|
|
<button
|
|
onClick={() => setActiveTab('inventory')}
|
|
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'
|
|
}`}
|
|
>
|
|
<LayoutDashboard className="w-5 h-5" /> Inventory
|
|
</button>
|
|
<button
|
|
onClick={() => setActiveTab('memory')}
|
|
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'
|
|
}`}
|
|
>
|
|
<Database className="w-5 h-5" /> Read / Write
|
|
</button>
|
|
<button
|
|
onClick={() => setActiveTab('settings')}
|
|
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'
|
|
}`}
|
|
>
|
|
<Settings className="w-5 h-5" /> Settings
|
|
</button>
|
|
</div>
|
|
|
|
{/* Connection Panel removed from here and moved to main content */}
|
|
</div>
|
|
|
|
{/* Main Content */}
|
|
<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-1 pr-4 border-r border-slate-100 mr-2 shrink-0">
|
|
<Database className="text-blue-600 w-5 h-5" />
|
|
<span className="font-bold text-slate-800 text-sm">UHF</span>
|
|
</div>
|
|
<div className="flex items-center gap-1 flex-1 justify-around">
|
|
<button
|
|
onClick={() => setActiveTab('connect')}
|
|
className={`p-2 rounded-lg transition-colors flex flex-col items-center justify-center gap-1 ${activeTab === 'connect' ? 'bg-emerald-50 text-emerald-700' : 'text-slate-600 hover:bg-slate-50'
|
|
}`}
|
|
>
|
|
<Wifi className="w-5 h-5" />
|
|
<span className="text-[10px] font-medium leading-none">Connect</span>
|
|
</button>
|
|
<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">
|
|
{activeTab === 'connect' && (
|
|
<div className="space-y-4 max-w-2xl mx-auto">
|
|
<h2 className="text-lg font-bold text-slate-800">Connection Manager</h2>
|
|
<div className="bg-white rounded-xl border border-slate-200 shadow-sm p-6">
|
|
<ConnectionPanel
|
|
status={status}
|
|
onConnect={connectSerial}
|
|
onDisconnect={disconnectSerial}
|
|
baudRate={baudRate}
|
|
setBaudRate={setBaudRate}
|
|
address={address}
|
|
setAddress={setAddress}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
{activeTab === 'inventory' && (
|
|
<InventoryPanel
|
|
tags={tags}
|
|
isScanning={isScanning}
|
|
onToggleScan={toggleScanning}
|
|
onClear={clearTags}
|
|
onFetchTids={handleFetchTids}
|
|
/>
|
|
)}
|
|
{activeTab === 'quicktest' && (
|
|
<QuickTestPanel
|
|
onQuickRead={handleQuickRead}
|
|
onQuickWrite={handleQuickWrite}
|
|
onCancel={handleCancelQuickAction}
|
|
writeInput={quickWriteInput}
|
|
onWriteInputChange={setQuickWriteInput}
|
|
result={readResult}
|
|
isPending={pendingQuickAction !== null}
|
|
isScanning={isScanning}
|
|
config={quickTestConfig}
|
|
/>
|
|
)}
|
|
{activeTab === 'memory' && (
|
|
<MemoryPanel
|
|
tags={tags}
|
|
isScanning={isScanning}
|
|
onScan={toggleScanning}
|
|
onRead={handleMemoryRead}
|
|
onWrite={handleMemoryWrite}
|
|
onWriteEpc={handleWriteEpc}
|
|
selectedEpc={tags.length > 0 ? tags[0].epc : ""}
|
|
readResult={readResult}
|
|
/>
|
|
)}
|
|
{activeTab === 'settings' && (
|
|
<SettingsPanel
|
|
readerInfo={readerInfo}
|
|
onGetInfo={handleGetInfo}
|
|
onSetParameters={handleSetParameters}
|
|
onFactoryReset={handleFactoryReset}
|
|
quickTestConfig={quickTestConfig}
|
|
onSaveQuickTestConfig={saveQuickTestConfig}
|
|
/>
|
|
)}
|
|
</main>
|
|
|
|
{/* Log Terminal */}
|
|
<div className={`${getLogHeightClass()} bg-white border-t border-slate-200 flex flex-col transition-all duration-300 ease-in-out`}>
|
|
<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"
|
|
onClick={() => setIsLogExpanded(!isLogExpanded)}
|
|
>
|
|
<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
|
|
</div>
|
|
<div>
|
|
{isLogExpanded ? <ChevronDown className="w-4 h-4 text-slate-400" /> : <ChevronUp className="w-4 h-4 text-slate-400" />}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex-1 overflow-auto p-4 font-mono text-xs space-y-1 text-slate-600">
|
|
{logs.length === 0 && <span className="text-slate-400 italic">No logs yet...</span>}
|
|
{logs.map(log => (
|
|
<div key={log.id} className="flex gap-2">
|
|
<span className="text-slate-400">[{log.timestamp.toLocaleTimeString()}]</span>
|
|
<span className={`font-bold ${log.type === 'TX' ? 'text-blue-600' :
|
|
log.type.includes('RX') ? 'text-emerald-600' :
|
|
log.type === 'ERROR' ? 'text-red-600' :
|
|
log.type === 'WARN' ? 'text-amber-600' : 'text-slate-700'
|
|
}`}>
|
|
{log.type}
|
|
</span>
|
|
<span className="text-slate-800">{log.message}</span>
|
|
{log.data && <span className="text-slate-500 ml-2">{log.data}</span>}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default App;
|