Files
RFID/components/MemoryPanel.tsx
2026-01-23 11:41:59 +09:00

308 lines
13 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { MemoryBank, TagData } from '../types';
import { Lock, HardDriveDownload, HardDriveUpload, Search, RefreshCw, ChevronDown, Tag } from 'lucide-react';
import { hexStringToBytes, hexToAscii, asciiToHex } from '../utils/crc16';
interface Props {
tags: TagData[];
onScan: () => void;
isScanning: boolean;
onRead: (bank: MemoryBank, ptr: number, len: number, password: string, targetEpc: string) => Promise<void>;
onWrite: (bank: MemoryBank, ptr: number, data: string, password: string, targetEpc: string) => Promise<void>;
onWriteEpc: (newEpc: string, password: string) => Promise<void>;
selectedEpc: string; // From parent (legacy), we will use local state mainly
readResult: string | null;
}
export const MemoryPanel: React.FC<Props> = ({ tags, onScan, isScanning, onRead, onWrite, onWriteEpc, selectedEpc: initialEpc, readResult }) => {
const [targetEpc, setTargetEpc] = useState(initialEpc);
const [bank, setBank] = useState<MemoryBank>(MemoryBank.EPC);
const [ptr, setPtr] = useState(2); // Default to 2 for EPC bank
const [len, setLen] = useState(4);
const [password, setPassword] = useState("00000000");
const [writeData, setWriteData] = useState("");
// Unified Data Mode: Controls both Read display and Write input interpretation
const [dataMode, setDataMode] = useState<'hex' | 'ascii'>('hex');
// Update local state if parent prop changes (optional sync)
useEffect(() => {
if (initialEpc) setTargetEpc(initialEpc);
}, [initialEpc]);
// If tags list updates and we don't have a target, auto-select the first one
useEffect(() => {
if (!targetEpc && tags.length > 0) {
setTargetEpc(tags[0].epc);
}
}, [tags, targetEpc]);
// Auto-update Pointer default based on Bank to prevent Parameter Errors
useEffect(() => {
if (bank === MemoryBank.EPC) {
setPtr(2); // Start at 2 to skip CRC (0) and PC (1) which are often protected
} else {
setPtr(0);
}
}, [bank]);
const handleRead = () => {
if (!targetEpc) {
alert("Please select or scan a tag first.");
return;
}
onRead(bank, ptr, len, password, targetEpc);
};
const toggleDataMode = (newMode: 'hex' | 'ascii') => {
if (newMode === dataMode) return;
// Convert current Write Input Data to match new mode
// This prevents the user from having to manually convert what they just typed
if (newMode === 'ascii') {
// Switching Hex -> Ascii
setWriteData(hexToAscii(writeData));
} else {
// Switching Ascii -> Hex
setWriteData(asciiToHex(writeData));
}
setDataMode(newMode);
};
const getHexForWrite = () => {
if (dataMode === 'ascii') {
return asciiToHex(writeData);
}
return writeData;
};
const handleWrite = () => {
if (!targetEpc) {
alert("Please select or scan a tag first.");
return;
}
const hexPayload = getHexForWrite();
onWrite(bank, ptr, hexPayload, password, targetEpc);
};
const handleInitializeEpc = () => {
const hexPayload = getHexForWrite();
// Validate locally but allow error logging
if (!hexStringToBytes(hexPayload)) {
// Pass invalid data to parent to trigger the "Invalid EPC Data" log
onWriteEpc(hexPayload, password);
return;
}
// Confirm with user only if data looks valid
if (confirm("This will overwrite the EPC of a single tag in the field using Command 0x04. Proceed?")) {
onWriteEpc(hexPayload, password);
}
};
return (
<div className="bg-white p-6 rounded-xl shadow-sm border border-slate-200">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-6">
<h2 className="text-lg font-bold text-slate-800 flex items-center gap-2">
<HardDriveDownload className="text-blue-600" />
Memory Operations
</h2>
{/* Unified Mode Toggle */}
<div className="bg-slate-100 p-1 rounded-lg flex items-center self-start sm:self-auto">
<button
onClick={() => toggleDataMode('hex')}
className={`px-3 py-1.5 text-xs font-bold rounded-md transition-all ${dataMode === 'hex' ? 'bg-white text-blue-600 shadow-sm' : 'text-slate-500 hover:text-slate-700'}`}
>
HEX Mode
</button>
<button
onClick={() => toggleDataMode('ascii')}
className={`px-3 py-1.5 text-xs font-bold rounded-md transition-all ${dataMode === 'ascii' ? 'bg-white text-blue-600 shadow-sm' : 'text-slate-500 hover:text-slate-700'}`}
>
ASCII Mode
</button>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Configuration Column */}
<div className="space-y-5">
{/* Tag Selection Area */}
<div className="bg-slate-50 p-4 rounded-lg border border-slate-200">
<label className="block text-sm font-bold text-slate-700 mb-2">Target Tag (Filter)</label>
<div className="flex gap-2 mb-2">
<div className="relative flex-1">
<input
type="text"
value={targetEpc}
onChange={(e) => setTargetEpc(e.target.value)}
placeholder="Scan or type EPC..."
className="w-full bg-white border border-slate-300 text-slate-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2.5 font-mono"
/>
{tags.length > 0 && (
<div className="absolute right-2 top-2.5 pointer-events-none text-slate-400">
<ChevronDown className="w-4 h-4" />
</div>
)}
{/* Invisible Select overlay for dropdown behavior */}
{tags.length > 0 && (
<select
onChange={(e) => setTargetEpc(e.target.value)}
value={targetEpc}
className="absolute inset-0 opacity-0 cursor-pointer"
>
<option value="" disabled>Select from list...</option>
{tags.map(t => (
<option key={t.epc} value={t.epc}>{t.epc} (Count: {t.count})</option>
))}
</select>
)}
</div>
<button
type="button"
onClick={onScan}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors flex items-center gap-2 whitespace-nowrap ${
isScanning
? 'bg-amber-100 text-amber-700 border border-amber-200'
: 'bg-blue-600 text-white hover:bg-blue-700'
}`}
>
{isScanning ? <RefreshCw className="w-4 h-4 animate-spin" /> : <Search className="w-4 h-4" />}
{isScanning ? 'Stop' : 'Scan'}
</button>
</div>
<p className="text-xs text-slate-500">
{tags.length > 0
? `${tags.length} tags found. Select one to target.`
: "Click Scan to find nearby tags."}
</p>
</div>
<div>
<label className="block text-sm font-medium text-slate-600 mb-1">Access Password (8 Hex Chars)</label>
<div className="relative">
<input
type="text"
value={password}
onChange={e => setPassword(e.target.value)}
maxLength={8}
className="w-full bg-white border border-slate-300 text-slate-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2.5 pl-9 font-mono"
/>
<Lock className="w-4 h-4 text-slate-400 absolute left-3 top-3" />
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-slate-600 mb-1">Memory Bank</label>
<select
value={bank}
onChange={e => setBank(Number(e.target.value))}
className="w-full bg-white border border-slate-300 text-slate-900 text-sm rounded-lg p-2.5"
>
<option value={MemoryBank.RESERVED}>Reserved</option>
<option value={MemoryBank.EPC}>EPC</option>
<option value={MemoryBank.TID}>TID</option>
<option value={MemoryBank.USER}>User</option>
</select>
</div>
<div className="grid grid-cols-2 gap-2">
<div>
<label className="block text-xs font-medium text-slate-500 mb-1">Start (Word)</label>
<input
type="number"
value={ptr}
onChange={e => setPtr(Number(e.target.value))}
className="w-full bg-white border border-slate-300 text-sm rounded-lg p-2.5"
/>
</div>
<div>
<label className="block text-xs font-medium text-slate-500 mb-1">Len (Word)</label>
<input
type="number"
value={len}
onChange={e => setLen(Number(e.target.value))}
className="w-full bg-white border border-slate-300 text-sm rounded-lg p-2.5"
/>
</div>
</div>
</div>
{bank === MemoryBank.EPC && (
<div className="text-xs text-amber-600 bg-amber-50 p-2 rounded border border-amber-100">
<strong>Note:</strong> Start Address 0 is CRC, 1 is PC. Writing to these may fail (Error 0xFF). EPC data typically starts at address 2.
</div>
)}
</div>
{/* Action Column */}
<div className="flex flex-col gap-4">
{/* Read Section */}
<div className="p-4 bg-slate-50 rounded-lg border border-slate-200 flex-1 flex flex-col">
<label className="text-sm font-semibold text-slate-700 mb-2">
Read Data <span className="text-xs font-normal text-slate-400">({dataMode.toUpperCase()})</span>
</label>
<div className="flex-1 bg-white border border-slate-300 rounded p-2 mb-2 font-mono text-xs text-slate-600 break-all overflow-y-auto min-h-[60px]">
{readResult
? (dataMode === 'hex' ? readResult : hexToAscii(readResult))
: "No data read..."
}
</div>
<button
type="button"
onClick={handleRead}
className="w-full bg-white border border-slate-300 hover:bg-slate-50 text-slate-700 font-medium py-2 rounded-lg text-sm transition-colors flex items-center justify-center gap-2"
>
<HardDriveDownload className="w-4 h-4" /> Read Memory
</button>
</div>
{/* Write Section */}
<div className="p-4 bg-slate-50 rounded-lg border border-slate-200">
<label className="text-sm font-semibold text-slate-700 mb-2">
Write Data <span className="text-xs font-normal text-slate-400">({dataMode.toUpperCase()})</span>
</label>
<input
type="text"
value={writeData}
onChange={e => setWriteData(e.target.value)}
placeholder={dataMode === 'hex' ? "e.g., A1 B2 C3..." : "Type text here..."}
className="w-full bg-white border border-slate-300 text-sm rounded p-2 mb-2 font-mono"
/>
<div className="grid grid-cols-2 gap-2">
<button
type="button"
onClick={handleWrite}
className="bg-slate-800 hover:bg-slate-900 text-white font-medium py-2 rounded-lg text-sm transition-colors flex items-center justify-center gap-2"
title="Writes data to the selected bank/address (Cmd 0x03)"
>
<HardDriveUpload className="w-4 h-4" /> Write Mem
</button>
{bank === MemoryBank.EPC && (
<button
type="button"
onClick={handleInitializeEpc}
className="bg-indigo-600 hover:bg-indigo-700 text-white font-medium py-2 rounded-lg text-sm transition-colors flex items-center justify-center gap-2"
title="Overwrites EPC of a SINGLE tag in field (Cmd 0x04)"
>
<Tag className="w-4 h-4" /> Write EPC
</button>
)}
</div>
</div>
</div>
</div>
</div>
);
};