initial commit
This commit is contained in:
307
components/MemoryPanel.tsx
Normal file
307
components/MemoryPanel.tsx
Normal file
@@ -0,0 +1,307 @@
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user