initial commit
This commit is contained in:
121
components/ConnectionPanel.tsx
Normal file
121
components/ConnectionPanel.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { ConnectionStatus } from '../types';
|
||||
import { Wifi, WifiOff, Activity } from 'lucide-react';
|
||||
|
||||
interface Props {
|
||||
status: ConnectionStatus;
|
||||
onConnect: () => void;
|
||||
onDisconnect: () => void;
|
||||
baudRate: number;
|
||||
setBaudRate: (rate: number) => void;
|
||||
address: number;
|
||||
setAddress: (addr: number) => void;
|
||||
}
|
||||
|
||||
export const ConnectionPanel: React.FC<Props> = ({
|
||||
status,
|
||||
onConnect,
|
||||
onDisconnect,
|
||||
baudRate,
|
||||
setBaudRate,
|
||||
address,
|
||||
setAddress
|
||||
}) => {
|
||||
const isConnected = status === ConnectionStatus.CONNECTED;
|
||||
|
||||
// Local state for address input to allow smooth typing without auto-formatting interference
|
||||
const [localAddress, setLocalAddress] = useState(address.toString(16).toUpperCase().padStart(2, '0'));
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
|
||||
// Sync from props only when not editing
|
||||
useEffect(() => {
|
||||
if (!isFocused) {
|
||||
setLocalAddress(address.toString(16).toUpperCase().padStart(2, '0'));
|
||||
}
|
||||
}, [address, isFocused]);
|
||||
|
||||
const handleAddressChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const val = e.target.value.toUpperCase();
|
||||
// Validate Hex and Length
|
||||
if (/^[0-9A-F]*$/.test(val) && val.length <= 2) {
|
||||
setLocalAddress(val);
|
||||
// Update parent state if valid
|
||||
if (val !== '') {
|
||||
setAddress(parseInt(val, 16));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
setIsFocused(false);
|
||||
// On blur, ensure the display matches the formatted prop value (pads single digits, etc.)
|
||||
setLocalAddress(address.toString(16).toUpperCase().padStart(2, '0'));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white p-4 rounded-xl shadow-sm border border-slate-200">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
{isConnected ? <Wifi className="text-emerald-500" /> : <WifiOff className="text-slate-400" />}
|
||||
<h2 className="text-lg font-semibold text-slate-800">Connection</h2>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-600 mb-1">Device Model</label>
|
||||
<select
|
||||
disabled={isConnected}
|
||||
className="w-full bg-slate-50 border border-slate-300 text-slate-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2.5 disabled:opacity-50"
|
||||
>
|
||||
<option value="CR100">현승코리아 CR-100</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-600 mb-1">Baud Rate</label>
|
||||
<select
|
||||
disabled={isConnected}
|
||||
value={baudRate}
|
||||
onChange={(e) => setBaudRate(Number(e.target.value))}
|
||||
className="w-full bg-slate-50 border border-slate-300 text-slate-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2.5 disabled:opacity-50"
|
||||
>
|
||||
<option value={9600}>9600</option>
|
||||
<option value={19200}>19200</option>
|
||||
<option value={38400}>38400</option>
|
||||
<option value={57600}>57600 (Default)</option>
|
||||
<option value={115200}>115200</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-600 mb-1">Reader Address (Hex)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={localAddress}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={handleBlur}
|
||||
onChange={handleAddressChange}
|
||||
className="w-full bg-slate-50 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"
|
||||
maxLength={2}
|
||||
placeholder="FF"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={isConnected ? onDisconnect : onConnect}
|
||||
className={`w-full flex items-center justify-center gap-2 font-medium rounded-lg text-sm px-5 py-2.5 transition-colors ${
|
||||
isConnected
|
||||
? 'bg-red-50 text-red-600 hover:bg-red-100 border border-red-200'
|
||||
: 'bg-blue-600 text-white hover:bg-blue-700 shadow-md shadow-blue-500/20'
|
||||
}`}
|
||||
>
|
||||
{isConnected ? 'Disconnect' : 'Connect Serial'}
|
||||
{status === ConnectionStatus.CONNECTING && <Activity className="animate-spin h-4 w-4" />}
|
||||
</button>
|
||||
|
||||
<div className="text-xs text-slate-400 text-center">
|
||||
{status}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
179
components/InventoryPanel.tsx
Normal file
179
components/InventoryPanel.tsx
Normal file
@@ -0,0 +1,179 @@
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { TagData } from '../types';
|
||||
import { Play, Pause, Trash2, Download, Fingerprint, ScanBarcode, CircuitBoard } from 'lucide-react';
|
||||
|
||||
interface Props {
|
||||
tags: TagData[];
|
||||
isScanning: boolean;
|
||||
onToggleScan: () => void;
|
||||
onClear: () => void;
|
||||
onFetchTids: () => void;
|
||||
}
|
||||
|
||||
export const InventoryPanel: React.FC<Props> = ({
|
||||
tags,
|
||||
isScanning,
|
||||
onToggleScan,
|
||||
onClear,
|
||||
onFetchTids
|
||||
}) => {
|
||||
const [viewMode, setViewMode] = useState<'epc' | 'tid'>('epc');
|
||||
|
||||
const exportCsv = () => {
|
||||
const csvContent = "data:text/csv;charset=utf-8,"
|
||||
+ "EPC,TID,Count,Timestamp\n"
|
||||
+ tags.map(t => `${t.epc},${t.tid || ''},${t.count},${new Date(t.timestamp).toISOString()}`).join("\n");
|
||||
const encodedUri = encodeURI(csvContent);
|
||||
const link = document.createElement("a");
|
||||
link.setAttribute("href", encodedUri);
|
||||
link.setAttribute("download", "rfid_inventory.csv");
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full gap-4">
|
||||
|
||||
{/* Main Inventory Table Card */}
|
||||
<div className="flex flex-col flex-1 bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
|
||||
{/* Header / Controls */}
|
||||
<div className="p-4 border-b border-slate-100 flex flex-col xl:flex-row items-start xl:items-center justify-between bg-slate-50/50 gap-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex flex-col">
|
||||
<h1 className="text-xl font-bold text-slate-800">EPC C1G2 Inventory</h1>
|
||||
<span className="text-xs text-slate-500">{tags.length} Unique Tags Found</span>
|
||||
</div>
|
||||
|
||||
{/* View Mode Switcher */}
|
||||
<div className="bg-slate-200 p-1 rounded-lg flex items-center gap-1 ml-4">
|
||||
<button
|
||||
onClick={() => setViewMode('epc')}
|
||||
className={`px-3 py-1.5 rounded-md text-xs font-bold flex items-center gap-2 transition-all ${viewMode === 'epc' ? 'bg-white text-blue-600 shadow-sm' : 'text-slate-500 hover:text-slate-700'}`}
|
||||
>
|
||||
<ScanBarcode className="w-3.5 h-3.5" /> EPC
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode('tid')}
|
||||
className={`px-3 py-1.5 rounded-md text-xs font-bold flex items-center gap-2 transition-all ${viewMode === 'tid' ? 'bg-white text-indigo-600 shadow-sm' : 'text-slate-500 hover:text-slate-700'}`}
|
||||
>
|
||||
<Fingerprint className="w-3.5 h-3.5" /> TID
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{viewMode === 'tid' && (
|
||||
<button
|
||||
onClick={onFetchTids}
|
||||
disabled={isScanning || tags.length === 0}
|
||||
className="flex items-center gap-2 px-3 py-2 bg-indigo-50 text-indigo-700 border border-indigo-200 rounded-lg text-sm font-medium hover:bg-indigo-100 disabled:opacity-50 disabled:cursor-not-allowed transition-colors mr-2"
|
||||
title="Read TID memory for all listed tags"
|
||||
>
|
||||
<CircuitBoard className="w-4 h-4" />
|
||||
Read TIDs
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={onToggleScan}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all ${
|
||||
isScanning
|
||||
? 'bg-amber-100 text-amber-700 hover:bg-amber-200 border border-amber-200'
|
||||
: 'bg-emerald-600 text-white hover:bg-emerald-700 shadow-md shadow-emerald-500/20'
|
||||
}`}
|
||||
>
|
||||
{isScanning ? <Pause className="w-4 h-4" /> : <Play className="w-4 h-4" />}
|
||||
{isScanning ? 'Stop Scan' : 'Start Scan'}
|
||||
</button>
|
||||
|
||||
<div className="h-6 w-px bg-slate-300 mx-1"></div>
|
||||
|
||||
<button
|
||||
onClick={onClear}
|
||||
className="p-2 text-slate-600 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors"
|
||||
title="Clear List"
|
||||
>
|
||||
<Trash2 className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={exportCsv}
|
||||
disabled={tags.length === 0}
|
||||
className="p-2 text-slate-600 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-colors disabled:opacity-50"
|
||||
title="Export CSV"
|
||||
>
|
||||
<Download className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Table Area */}
|
||||
<div className="flex-1 overflow-auto">
|
||||
<table className="w-full text-sm text-left text-slate-600">
|
||||
<thead className="text-xs text-slate-700 uppercase bg-slate-50 sticky top-0 z-10 shadow-sm">
|
||||
<tr>
|
||||
<th scope="col" className="px-6 py-3 w-16">#</th>
|
||||
<th scope="col" className="px-6 py-3">
|
||||
{viewMode === 'epc' ? 'EPC ID' : 'TID (Tag Identifier)'}
|
||||
</th>
|
||||
{viewMode === 'tid' && <th scope="col" className="px-6 py-3 text-slate-400">EPC Reference</th>}
|
||||
<th scope="col" className="px-6 py-3 w-32">Length (Bits)</th>
|
||||
<th scope="col" className="px-6 py-3 w-32 text-right">Count</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{tags.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={viewMode === 'tid' ? 5 : 4} className="px-6 py-12 text-center text-slate-400 italic">
|
||||
No tags scanned yet. Press "Start Scan" to begin.
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
tags.map((tag, index) => (
|
||||
<tr key={tag.epc} className="bg-white border-b border-slate-100 hover:bg-slate-50 transition-colors">
|
||||
<td className="px-6 py-4 font-mono text-slate-400">{index + 1}</td>
|
||||
|
||||
{/* Main ID Column */}
|
||||
<td className="px-6 py-4 font-mono font-medium text-slate-900">
|
||||
{viewMode === 'epc' ? (
|
||||
tag.epc
|
||||
) : (
|
||||
tag.tid ? (
|
||||
<span className="text-indigo-700">{tag.tid}</span>
|
||||
) : (
|
||||
<span className="text-slate-300 italic text-xs">Not Read (Click "Read TIDs")</span>
|
||||
)
|
||||
)}
|
||||
</td>
|
||||
|
||||
{/* EPC Reference Column (Only in TID view) */}
|
||||
{viewMode === 'tid' && (
|
||||
<td className="px-6 py-4 font-mono text-xs text-slate-400">
|
||||
{tag.epc}
|
||||
</td>
|
||||
)}
|
||||
|
||||
{/* Length Column */}
|
||||
<td className="px-6 py-4">
|
||||
{viewMode === 'epc'
|
||||
? tag.epc.replace(/\s/g, '').length * 4
|
||||
: (tag.tid ? tag.tid.replace(/\s/g, '').length * 4 : '-')
|
||||
}
|
||||
</td>
|
||||
|
||||
<td className="px-6 py-4 text-right">
|
||||
<span className="bg-blue-100 text-blue-800 text-xs font-medium px-2.5 py-0.5 rounded-full">
|
||||
{tag.count}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
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>
|
||||
);
|
||||
};
|
||||
127
components/QuickTestPanel.tsx
Normal file
127
components/QuickTestPanel.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
|
||||
import React from 'react';
|
||||
import { Zap, HardDriveUpload, HardDriveDownload, Activity, CheckCircle, AlertCircle, XCircle } from 'lucide-react';
|
||||
import { QuickTestConfig } from '../types';
|
||||
|
||||
interface Props {
|
||||
onQuickRead: () => void;
|
||||
onQuickWrite: (data: string) => void;
|
||||
onCancel: () => void;
|
||||
writeInput: string;
|
||||
onWriteInputChange: (value: string) => void;
|
||||
result: string | null;
|
||||
isPending: boolean;
|
||||
isScanning: boolean;
|
||||
config: QuickTestConfig;
|
||||
}
|
||||
|
||||
export const QuickTestPanel: React.FC<Props> = ({
|
||||
onQuickRead,
|
||||
onQuickWrite,
|
||||
onCancel,
|
||||
writeInput,
|
||||
onWriteInputChange,
|
||||
result,
|
||||
isPending,
|
||||
isScanning,
|
||||
config
|
||||
}) => {
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col items-center justify-center p-6 max-w-4xl mx-auto">
|
||||
<div className="text-center mb-10">
|
||||
<div className="inline-flex items-center justify-center p-3 bg-indigo-100 rounded-full mb-4">
|
||||
<Zap className="w-8 h-8 text-indigo-600" />
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold text-slate-800 mb-2">Quick Test Mode</h1>
|
||||
<p className="text-slate-500">
|
||||
Automatically detects the first tag in the field and performs a Read or Write operation.<br/>
|
||||
Target: <strong>EPC Bank</strong>, Start Address: <strong>2</strong><br/>
|
||||
Length: <strong>{config.length} Words</strong>, Format: <strong>{config.format.toUpperCase()}</strong>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 w-full">
|
||||
|
||||
{/* Read Card */}
|
||||
<div className="bg-white p-8 rounded-2xl shadow-sm border border-slate-200 hover:shadow-md transition-shadow flex flex-col items-center text-center">
|
||||
<div className="mb-6 p-4 bg-blue-50 text-blue-600 rounded-full">
|
||||
<HardDriveDownload className="w-8 h-8" />
|
||||
</div>
|
||||
<h2 className="text-xl font-bold text-slate-800 mb-2">Auto Read</h2>
|
||||
|
||||
<div className="w-full bg-slate-50 border border-slate-200 rounded-xl p-4 mb-6 min-h-[80px] flex items-center justify-center font-mono text-lg text-slate-700 break-all">
|
||||
{isPending ? (
|
||||
<div className="flex items-center gap-2 text-amber-500 animate-pulse">
|
||||
<Activity className="w-5 h-5" /> Scanning & Reading...
|
||||
</div>
|
||||
) : (
|
||||
result ? (
|
||||
<span className="text-emerald-600 font-bold">{result}</span>
|
||||
) : (
|
||||
<span className="text-slate-300">No Data Read</span>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isPending ? (
|
||||
<button
|
||||
onClick={onCancel}
|
||||
className="w-full py-4 bg-red-500 hover:bg-red-600 text-white rounded-xl font-bold text-lg shadow-lg shadow-red-500/30 transition-all flex items-center justify-center gap-2"
|
||||
>
|
||||
<XCircle className="w-5 h-5" /> Cancel
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={onQuickRead}
|
||||
disabled={isScanning && !isPending}
|
||||
className="w-full py-4 bg-blue-600 hover:bg-blue-700 disabled:bg-slate-300 disabled:cursor-not-allowed text-white rounded-xl font-bold text-lg shadow-lg shadow-blue-500/30 transition-all flex items-center justify-center gap-2"
|
||||
>
|
||||
Start Read Test
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Write Card */}
|
||||
<div className="bg-white p-8 rounded-2xl shadow-sm border border-slate-200 hover:shadow-md transition-shadow flex flex-col items-center text-center">
|
||||
<div className="mb-6 p-4 bg-purple-50 text-purple-600 rounded-full">
|
||||
<HardDriveUpload className="w-8 h-8" />
|
||||
</div>
|
||||
<h2 className="text-xl font-bold text-slate-800 mb-2">Auto Write</h2>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
value={writeInput}
|
||||
onChange={(e) => onWriteInputChange(e.target.value)}
|
||||
placeholder={`${config.format === 'hex' ? 'Hex Data (e.g. A1 B2...)' : 'ASCII String'}`}
|
||||
className="w-full bg-slate-50 border border-slate-300 text-center text-lg rounded-xl p-4 mb-6 font-mono focus:ring-2 focus:ring-purple-500 outline-none"
|
||||
/>
|
||||
|
||||
{isPending ? (
|
||||
<button
|
||||
onClick={onCancel}
|
||||
className="w-full py-4 bg-red-500 hover:bg-red-600 text-white rounded-xl font-bold text-lg shadow-lg shadow-red-500/30 transition-all flex items-center justify-center gap-2"
|
||||
>
|
||||
<XCircle className="w-5 h-5" /> Cancel
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => onQuickWrite(writeInput)}
|
||||
disabled={(isScanning && !isPending) || !writeInput}
|
||||
className="w-full py-4 bg-slate-800 hover:bg-slate-900 disabled:bg-slate-300 disabled:cursor-not-allowed text-white rounded-xl font-bold text-lg shadow-lg shadow-slate-500/30 transition-all flex items-center justify-center gap-2"
|
||||
>
|
||||
Start Write Test
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{isScanning && !isPending && (
|
||||
<div className="mt-8 flex items-center gap-2 px-4 py-2 bg-amber-50 text-amber-700 border border-amber-200 rounded-full text-sm animate-pulse">
|
||||
<Activity className="w-4 h-4" /> Background scanning is active in Inventory...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
371
components/SettingsPanel.tsx
Normal file
371
components/SettingsPanel.tsx
Normal file
@@ -0,0 +1,371 @@
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { FrequencyBand, ReaderInfo, QuickTestConfig } from '../types';
|
||||
import { RefreshCw, Save, RotateCcw, Cpu, Radio, Zap, Activity, HardDrive, Settings2 } from 'lucide-react';
|
||||
|
||||
interface Props {
|
||||
readerInfo: ReaderInfo | null;
|
||||
onGetInfo: () => void;
|
||||
onSetParameters: (settings: {
|
||||
address: number;
|
||||
baudRate: number;
|
||||
power: number;
|
||||
minFreq: number;
|
||||
maxFreq: number;
|
||||
band: FrequencyBand;
|
||||
}) => void;
|
||||
onFactoryReset: () => void;
|
||||
quickTestConfig: QuickTestConfig;
|
||||
onSaveQuickTestConfig: (config: QuickTestConfig) => void;
|
||||
}
|
||||
|
||||
export const SettingsPanel: React.FC<Props> = ({
|
||||
readerInfo,
|
||||
onGetInfo,
|
||||
onSetParameters,
|
||||
onFactoryReset,
|
||||
quickTestConfig,
|
||||
onSaveQuickTestConfig
|
||||
}) => {
|
||||
// Local state for the editable form
|
||||
const [address, setAddress] = useState(0x00);
|
||||
const [baudRate, setBaudRate] = useState(57600);
|
||||
const [power, setPower] = useState(13);
|
||||
const [maxResponseTime, setMaxResponseTime] = useState(0);
|
||||
const [minFreq, setMinFreq] = useState(0);
|
||||
const [maxFreq, setMaxFreq] = useState(0);
|
||||
const [isSingleFreq, setIsSingleFreq] = useState(false);
|
||||
const [band, setBand] = useState<FrequencyBand>(FrequencyBand.US);
|
||||
|
||||
// Quick Test Local State
|
||||
const [qtLength, setQtLength] = useState<3 | 4>(4);
|
||||
const [qtFormat, setQtFormat] = useState<'hex' | 'ascii'>('hex');
|
||||
|
||||
// Initialize form with reader info when available
|
||||
useEffect(() => {
|
||||
if (readerInfo) {
|
||||
setAddress(readerInfo.address);
|
||||
setPower(readerInfo.power);
|
||||
setMinFreq(readerInfo.minFreq);
|
||||
setMaxFreq(readerInfo.maxFreq);
|
||||
setMaxResponseTime(readerInfo.maxResponseTime);
|
||||
setBand(readerInfo.band);
|
||||
setIsSingleFreq(readerInfo.minFreq === readerInfo.maxFreq);
|
||||
}
|
||||
}, [readerInfo]);
|
||||
|
||||
// Sync Quick Test config
|
||||
useEffect(() => {
|
||||
setQtLength(quickTestConfig.length);
|
||||
setQtFormat(quickTestConfig.format);
|
||||
}, [quickTestConfig]);
|
||||
|
||||
const handleApplyReaderSettings = () => {
|
||||
onSetParameters({
|
||||
address,
|
||||
baudRate,
|
||||
power,
|
||||
minFreq,
|
||||
maxFreq,
|
||||
band
|
||||
});
|
||||
};
|
||||
|
||||
const handleSaveLocalSettings = () => {
|
||||
onSaveQuickTestConfig({
|
||||
length: qtLength,
|
||||
format: qtFormat
|
||||
});
|
||||
};
|
||||
|
||||
const baudRates = [9600, 19200, 38400, 57600, 115200];
|
||||
const powerLevels = Array.from({ length: 31 }, (_, i) => i); // 0-30 dBm
|
||||
|
||||
const isLoaded = !!readerInfo;
|
||||
|
||||
return (
|
||||
<div className="bg-white p-8 rounded-xl shadow-sm border border-slate-200 h-full overflow-auto flex flex-col">
|
||||
|
||||
{/* Header */}
|
||||
<div className="mb-8 pb-6 border-b border-slate-100">
|
||||
<h2 className="text-2xl font-bold text-slate-800">System Configuration</h2>
|
||||
<p className="text-slate-500 mt-1">
|
||||
Manage device parameters and local browser settings.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 1. Quick Test Settings Card */}
|
||||
<div className="mb-10">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Settings2 className="w-5 h-5 text-indigo-600" />
|
||||
<h3 className="text-lg font-bold text-slate-800">Quick Test Settings (Local)</h3>
|
||||
</div>
|
||||
|
||||
<div className="bg-indigo-50/50 p-6 rounded-xl border border-indigo-100">
|
||||
<p className="text-sm text-indigo-900 mb-6 border-l-4 border-indigo-400 pl-3">
|
||||
These settings are stored in your browser's local storage and control the behavior of the "Quick Test" tab.
|
||||
</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 mb-6">
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-medium text-slate-600">Read/Write Length (Words)</label>
|
||||
<div className="flex items-center gap-4">
|
||||
<label className="flex items-center gap-2 cursor-pointer bg-white px-4 py-2 rounded-lg border border-indigo-100 shadow-sm">
|
||||
<input
|
||||
type="radio"
|
||||
checked={qtLength === 3}
|
||||
onChange={() => setQtLength(3)}
|
||||
className="w-4 h-4 text-indigo-600 focus:ring-indigo-500 border-gray-300"
|
||||
/>
|
||||
<span className="text-sm text-slate-700">3 Words (6 Bytes)</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 cursor-pointer bg-white px-4 py-2 rounded-lg border border-indigo-100 shadow-sm">
|
||||
<input
|
||||
type="radio"
|
||||
checked={qtLength === 4}
|
||||
onChange={() => setQtLength(4)}
|
||||
className="w-4 h-4 text-indigo-600 focus:ring-indigo-500 border-gray-300"
|
||||
/>
|
||||
<span className="text-sm text-slate-700">4 Words (8 Bytes)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-medium text-slate-600">Data Display Format</label>
|
||||
<div className="flex items-center gap-4">
|
||||
<label className="flex items-center gap-2 cursor-pointer bg-white px-4 py-2 rounded-lg border border-indigo-100 shadow-sm">
|
||||
<input
|
||||
type="radio"
|
||||
checked={qtFormat === 'hex'}
|
||||
onChange={() => setQtFormat('hex')}
|
||||
className="w-4 h-4 text-indigo-600 focus:ring-indigo-500 border-gray-300"
|
||||
/>
|
||||
<span className="text-sm text-slate-700">HEX</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 cursor-pointer bg-white px-4 py-2 rounded-lg border border-indigo-100 shadow-sm">
|
||||
<input
|
||||
type="radio"
|
||||
checked={qtFormat === 'ascii'}
|
||||
onChange={() => setQtFormat('ascii')}
|
||||
className="w-4 h-4 text-indigo-600 focus:ring-indigo-500 border-gray-300"
|
||||
/>
|
||||
<span className="text-sm text-slate-700">ASCII</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleSaveLocalSettings}
|
||||
className="flex items-center justify-center gap-2 px-5 py-2.5 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 shadow-sm hover:shadow transition-colors font-medium"
|
||||
title="Save these settings to browser"
|
||||
>
|
||||
<Save className="w-4 h-4" />
|
||||
Save Local Settings
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr className="border-slate-100 mb-10" />
|
||||
|
||||
{/* 2. Reader Configuration */}
|
||||
<div className="flex-1">
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<HardDrive className="w-5 h-5 text-blue-600" />
|
||||
<h3 className="text-lg font-bold text-slate-800">Reader Configuration (Device)</h3>
|
||||
</div>
|
||||
|
||||
{/* Device Actions */}
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<button
|
||||
onClick={onGetInfo}
|
||||
className="flex items-center justify-center gap-2 px-4 py-2 bg-slate-100 text-slate-700 border border-slate-200 rounded-lg hover:bg-slate-200 hover:text-slate-900 transition-all font-medium"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${!isLoaded ? 'animate-pulse' : ''}`} />
|
||||
Read Config
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleApplyReaderSettings}
|
||||
className="flex items-center justify-center gap-2 px-5 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 shadow-sm hover:shadow transition-colors font-medium"
|
||||
>
|
||||
<Save className="w-4 h-4" />
|
||||
Apply Config
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={onFactoryReset}
|
||||
className="flex items-center justify-center gap-2 px-4 py-2 bg-white border border-red-200 text-red-600 rounded-lg hover:bg-red-50 hover:text-red-700 hover:border-red-300 transition-colors font-medium"
|
||||
>
|
||||
<RotateCcw className="w-4 h-4" />
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status Info */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
|
||||
<div className="bg-slate-50 border border-slate-100 rounded-lg p-4 flex items-center gap-3">
|
||||
<div className="p-2 bg-white rounded-md shadow-sm text-blue-600"><Cpu className="w-5 h-5" /></div>
|
||||
<div>
|
||||
<div className="text-xs text-slate-400 font-semibold uppercase">Model</div>
|
||||
<div className="text-sm font-bold text-slate-700">{readerInfo?.model || '--'}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-slate-50 border border-slate-100 rounded-lg p-4 flex items-center gap-3">
|
||||
<div className="p-2 bg-white rounded-md shadow-sm text-emerald-600"><Activity className="w-5 h-5" /></div>
|
||||
<div>
|
||||
<div className="text-xs text-slate-400 font-semibold uppercase">Firmware</div>
|
||||
<div className="text-sm font-bold text-slate-700">{readerInfo?.version || '--'}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-slate-50 border border-slate-100 rounded-lg p-4 flex items-center gap-3">
|
||||
<div className="p-2 bg-white rounded-md shadow-sm text-violet-600"><Radio className="w-5 h-5" /></div>
|
||||
<div>
|
||||
<div className="text-xs text-slate-400 font-semibold uppercase">Protocol</div>
|
||||
<div className="text-sm font-bold text-slate-700">{readerInfo?.protocol || 'EPCC1-G2'}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-slate-50 border border-slate-100 rounded-lg p-4 flex items-center gap-3">
|
||||
<div className="p-2 bg-white rounded-md shadow-sm text-amber-600"><Zap className="w-5 h-5" /></div>
|
||||
<div>
|
||||
<div className="text-xs text-slate-400 font-semibold uppercase">Power</div>
|
||||
<div className="text-sm font-bold text-slate-700">{readerInfo ? `${readerInfo.power} dBm` : '--'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Device Settings Form */}
|
||||
<div className={`space-y-8 transition-all duration-300 ${isLoaded ? 'opacity-100' : 'opacity-50 pointer-events-none grayscale'}`}>
|
||||
{/* 1. Communication Settings */}
|
||||
<div>
|
||||
<h3 className="text-sm font-bold text-slate-900 uppercase tracking-wider mb-4 flex items-center gap-2">
|
||||
<span className="w-1 h-4 bg-blue-500 rounded-full"></span>
|
||||
Communication Parameters
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm font-medium text-slate-600">Reader Address (Hex)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={address.toString(16).toUpperCase()}
|
||||
onChange={e => {
|
||||
const val = parseInt(e.target.value, 16);
|
||||
if (!isNaN(val) && val >= 0 && val <= 0xFF) setAddress(val);
|
||||
}}
|
||||
className="w-full bg-white border border-slate-300 rounded-lg px-3 py-2 text-slate-900 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all"
|
||||
maxLength={2}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm font-medium text-slate-600">Baud Rate</label>
|
||||
<select
|
||||
value={baudRate}
|
||||
onChange={e => setBaudRate(Number(e.target.value))}
|
||||
className="w-full bg-white border border-slate-300 rounded-lg px-3 py-2 text-slate-900 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all"
|
||||
>
|
||||
{baudRates.map(b => <option key={b} value={b}>{b} bps</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm font-medium text-slate-600">Max Response Time</label>
|
||||
<select
|
||||
value={maxResponseTime}
|
||||
onChange={e => setMaxResponseTime(Number(e.target.value))}
|
||||
className="w-full bg-white border border-slate-300 rounded-lg px-3 py-2 text-slate-900 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all"
|
||||
>
|
||||
<option value={0}>Auto</option>
|
||||
<option value={10}>10ms</option>
|
||||
<option value={20}>20ms</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 2. RF Settings */}
|
||||
<div>
|
||||
<h3 className="text-sm font-bold text-slate-900 uppercase tracking-wider mb-4 flex items-center gap-2">
|
||||
<span className="w-1 h-4 bg-emerald-500 rounded-full"></span>
|
||||
RF Configuration
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm font-medium text-slate-600">Power Output</label>
|
||||
<select
|
||||
value={power}
|
||||
onChange={e => setPower(Number(e.target.value))}
|
||||
className="w-full bg-white border border-slate-300 rounded-lg px-3 py-2 text-slate-900 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all"
|
||||
>
|
||||
{powerLevels.map(p => <option key={p} value={p}>{p} dBm</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm font-medium text-slate-600">Min Frequency</label>
|
||||
<select
|
||||
value={minFreq}
|
||||
onChange={e => setMinFreq(Number(e.target.value))}
|
||||
disabled={isSingleFreq}
|
||||
className="w-full bg-white border border-slate-300 rounded-lg px-3 py-2 text-slate-900 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all disabled:bg-slate-50 disabled:text-slate-400"
|
||||
>
|
||||
<option value={0}>Default</option>
|
||||
<option value={1}>Custom 1</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between items-center">
|
||||
<label className="text-sm font-medium text-slate-600">Max Frequency</label>
|
||||
<label className="flex items-center gap-1.5 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isSingleFreq}
|
||||
onChange={e => setIsSingleFreq(e.target.checked)}
|
||||
className="w-3.5 h-3.5 rounded text-blue-600 focus:ring-blue-500 border-slate-300"
|
||||
/>
|
||||
<span className="text-xs text-slate-500">Single Freq</span>
|
||||
</label>
|
||||
</div>
|
||||
<select
|
||||
value={maxFreq}
|
||||
onChange={e => setMaxFreq(Number(e.target.value))}
|
||||
disabled={isSingleFreq}
|
||||
className="w-full bg-white border border-slate-300 rounded-lg px-3 py-2 text-slate-900 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all disabled:bg-slate-50 disabled:text-slate-400"
|
||||
>
|
||||
<option value={0}>Default</option>
|
||||
<option value={1}>Custom 1</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-slate-50/80 rounded-lg p-5 border border-slate-200">
|
||||
<label className="text-xs font-bold text-slate-500 uppercase tracking-wide mb-3 block">Frequency Region / Band</label>
|
||||
<div className="flex flex-wrap gap-x-8 gap-y-4">
|
||||
{[
|
||||
{ label: 'User Defined', value: FrequencyBand.USER },
|
||||
{ label: 'Chinese Band', value: FrequencyBand.CHINESE },
|
||||
{ label: 'US Band', value: FrequencyBand.US },
|
||||
{ label: 'Korean Band', value: FrequencyBand.KOREAN },
|
||||
{ label: 'EU Band', value: FrequencyBand.EU },
|
||||
].map((item) => (
|
||||
<label key={item.value} className="flex items-center gap-2 cursor-pointer group">
|
||||
<div className={`w-4 h-4 rounded-full border flex items-center justify-center transition-colors ${band === item.value ? 'border-blue-600 bg-blue-600' : 'border-slate-300 bg-white group-hover:border-blue-400'}`}>
|
||||
{band === item.value && <div className="w-1.5 h-1.5 rounded-full bg-white" />}
|
||||
</div>
|
||||
<input
|
||||
type="radio"
|
||||
name="band"
|
||||
checked={band === item.value}
|
||||
onChange={() => setBand(item.value)}
|
||||
className="hidden"
|
||||
/>
|
||||
<span className={`text-sm ${band === item.value ? 'text-slate-900 font-medium' : 'text-slate-600'}`}>{item.label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user