378 lines
17 KiB
TypeScript
378 lines
17 KiB
TypeScript
|
|
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">
|
|
장치 매개변수 및 로컬 브라우저 설정을 관리합니다.
|
|
</p>
|
|
</div>
|
|
|
|
|
|
{/* 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
|
|
</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" />
|
|
Write
|
|
</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>
|
|
|
|
<hr className="border-slate-100 mb-10" />
|
|
|
|
|
|
{/* 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">
|
|
이 설정은 브라우저의 로컬 저장소에 저장되며 "Quick Test" 탭의 동작을 제어합니다.
|
|
</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</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</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>
|
|
|
|
|
|
|
|
|
|
</div>
|
|
);
|
|
};
|