feat(rotator): multiple rotors of any type, plus a CAT PTT hotkey
Settings -> Rotator is now a list like the amplifiers: add/remove rotors, mix PstRotator / Rotator Genius / ARCO, and a Rotator Genius can drive both its ports (one entry = two rotors). The compass gains a rotor selector, and a per-rotor motorized-antenna flag so the boom/pattern paths show only for the rotor carrying the Ultrabeam/SteppIR. Also in this batch: a keyboard PTT hotkey (hold-to-talk or toggle, reusing the audio PTT method with a CAT fallback), and TCI spot push to the panadapter with click-to-fill of the callsign.
This commit is contained in:
@@ -10,7 +10,7 @@ import {
|
||||
GetListsSettings, SaveListsSettings,
|
||||
GetCATSettings, SaveCATSettings, DiscoverFlexRadios,
|
||||
ListProfiles, GetActiveProfile, SaveProfile, DeleteProfile, ActivateProfile, DuplicateProfile,
|
||||
GetRotatorSettings, SaveRotatorSettings, TestRotator, RotatorPark, RotatorStop,
|
||||
GetRotators, SaveRotators, TestRotatorDevice, RotatorPark, RotatorStop,
|
||||
GetUltrabeamSettings, SaveUltrabeamSettings, TestUltrabeam,
|
||||
GetAntGeniusSettings, SaveAntGeniusSettings,
|
||||
GetTunerGeniusSettings, SaveTunerGeniusSettings,
|
||||
@@ -80,7 +80,7 @@ type StationSettings = StationSettingsForm;
|
||||
type ListsSettings = ListsSettingsForm;
|
||||
type ModePreset = ModePresetForm;
|
||||
type CATSettings = Omit<mainModels.CATSettings, 'convertValues'>;
|
||||
type RotatorSettings = Omit<mainModels.RotatorSettings, 'convertValues'>;
|
||||
type RotatorDevice = Omit<mainModels.RotatorDevice, 'convertValues'>;
|
||||
type ClusterServer = Omit<clusterModels.ServerConfig, 'convertValues'>;
|
||||
type ClusterServerStatus = Omit<clusterModels.ServerStatus, 'convertValues'>;
|
||||
type Profile = Omit<profileModels.Profile, 'convertValues'>;
|
||||
@@ -1140,10 +1140,11 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
icom_port: '', icom_baud: 115200, icom_addr: 0x98, icom_net_host: '', icom_net_user: '', icom_net_pass: '', icom_net_audio: false,
|
||||
tci_host: '', tci_port: 40001, tci_spots: false, poll_ms: 250, delay_ms: 0,
|
||||
digital_default: 'FT8', share_enabled: false, share_port: 4532,
|
||||
ptt_hotkey_enabled: false, ptt_hotkey: '', ptt_hotkey_toggle: false,
|
||||
});
|
||||
const [rotator, setRotator] = useState<RotatorSettings>({
|
||||
enabled: false, type: 'pst', host: '127.0.0.1', port: 12000, has_elevation: false, rotator_num: 1,
|
||||
} as any);
|
||||
// While true, the next key press is captured as the PTT hotkey.
|
||||
const [capturingPtt, setCapturingPtt] = useState(false);
|
||||
const [rotors, setRotors] = useState<RotatorDevice[]>([]);
|
||||
const [rotatorTesting, setRotatorTesting] = useState(false);
|
||||
const [rotatorTest, setRotatorTest] = useState<{ ok: boolean; msg: string } | null>(null);
|
||||
|
||||
@@ -1498,7 +1499,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
try {
|
||||
const [l, ls, c, ap, r, b, qd, es] = await Promise.all([
|
||||
GetLookupSettings(), GetListsSettings(), GetCATSettings(), GetActiveProfile(),
|
||||
GetRotatorSettings(), GetBackupSettings(), GetQSLDefaults(), GetExternalServices(),
|
||||
GetRotators(), GetBackupSettings(), GetQSLDefaults(), GetExternalServices(),
|
||||
]);
|
||||
setLookup(l);
|
||||
setActiveProfile(ap as Profile);
|
||||
@@ -1511,7 +1512,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
await reloadProfiles();
|
||||
await reloadClusterServers();
|
||||
setCatCfg(c);
|
||||
setRotator(r);
|
||||
setRotors((r ?? []) as any);
|
||||
try { setUltrabeam(await GetUltrabeamSettings() as any); } catch {}
|
||||
try { setAntgenius(await GetAntGeniusSettings() as any); } catch {}
|
||||
try { setTunergenius(await GetTunerGeniusSettings() as any); } catch {}
|
||||
@@ -1552,7 +1553,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
try { setActiveProfile(await GetActiveProfile() as Profile); } catch {}
|
||||
try { setLookup(await GetLookupSettings() as any); } catch {}
|
||||
try { setCatCfg(await GetCATSettings() as any); } catch {}
|
||||
try { setRotator(await GetRotatorSettings() as any); } catch {}
|
||||
try { setRotors(((await GetRotators()) ?? []) as any); } catch {}
|
||||
try { setUltrabeam(await GetUltrabeamSettings() as any); } catch {}
|
||||
try { setAntgenius(await GetAntGeniusSettings() as any); } catch {}
|
||||
try { setTunergenius(await GetTunerGeniusSettings() as any); } catch {}
|
||||
@@ -1744,7 +1745,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
}
|
||||
await SaveLookupSettings(lookup as any);
|
||||
await SaveCATSettings(catCfg as any);
|
||||
await SaveRotatorSettings(rotator as any);
|
||||
await SaveRotators(rotors as any);
|
||||
await SaveUltrabeamSettings(ultrabeam as any);
|
||||
await SaveAntGeniusSettings(antgenius as any);
|
||||
await SaveTunerGeniusSettings(tunergenius as any);
|
||||
@@ -2811,6 +2812,54 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* PTT hotkey — a keyboard key that keys the rig while OpsLog is focused.
|
||||
Uses the Audio → PTT method (CAT / RTS / DTR), falling back to CAT. */}
|
||||
<div className="border-t border-border/60 pt-3 space-y-2">
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||
<Checkbox
|
||||
checked={!!(catCfg as any).ptt_hotkey_enabled}
|
||||
onCheckedChange={(c) => setCatCfg((s) => ({ ...s, ptt_hotkey_enabled: !!c } as any))}
|
||||
/>
|
||||
{t('cat.pttKey')}
|
||||
</label>
|
||||
{(catCfg as any).ptt_hotkey_enabled && (
|
||||
<>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onKeyDown={(e) => {
|
||||
if (!capturingPtt) return;
|
||||
e.preventDefault();
|
||||
if (e.code === 'Escape') { setCapturingPtt(false); return; }
|
||||
setCatCfg((s) => ({ ...s, ptt_hotkey: e.code } as any));
|
||||
setCapturingPtt(false);
|
||||
}}
|
||||
onClick={() => setCapturingPtt(true)}
|
||||
className="min-w-[140px] rounded-md border border-border bg-muted/40 px-3 py-1.5 text-sm font-mono hover:bg-muted focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
>
|
||||
{capturingPtt ? t('cat.pttKeyPress') : ((catCfg as any).ptt_hotkey || t('cat.pttKeyNone'))}
|
||||
</button>
|
||||
{(catCfg as any).ptt_hotkey && !capturingPtt && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCatCfg((s) => ({ ...s, ptt_hotkey: '' } as any))}
|
||||
className="text-xs text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{t('cat.pttKeyClear')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||
<Checkbox
|
||||
checked={!!(catCfg as any).ptt_hotkey_toggle}
|
||||
onCheckedChange={(c) => setCatCfg((s) => ({ ...s, ptt_hotkey_toggle: !!c } as any))}
|
||||
/>
|
||||
{t('cat.pttKeyToggle')}
|
||||
</label>
|
||||
<p className="text-[11px] text-muted-foreground">{t('cat.pttKeyHint')}</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{catCfg.backend === 'omnirig' && (
|
||||
<>
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||
@@ -2830,12 +2879,12 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
);
|
||||
}
|
||||
|
||||
async function testRotator() {
|
||||
async function testRotator(dev: RotatorDevice, sub = 1) {
|
||||
setRotatorTesting(true);
|
||||
setRotatorTest(null);
|
||||
try {
|
||||
await TestRotator(rotator as any);
|
||||
setRotatorTest({ ok: true, msg: (rotator as any).type === 'rotgenius' ? t('rot.testOkRG') : t('cat.rotatorOk') });
|
||||
await TestRotatorDevice(dev as any, sub);
|
||||
setRotatorTest({ ok: true, msg: (dev as any).type === 'rotgenius' ? t('rot.testOkRG') : t('cat.rotatorOk') });
|
||||
} catch (e: any) {
|
||||
setRotatorTest({ ok: false, msg: String(e?.message ?? e) });
|
||||
} finally {
|
||||
@@ -3305,130 +3354,195 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
}
|
||||
|
||||
function RotatorPanel() {
|
||||
const isRG = (rotator as any).type === 'rotgenius';
|
||||
const isARCO = (rotator as any).type === 'arco';
|
||||
// Rotors are a list, like amplifiers: add with +, remove with the trash icon.
|
||||
// A Rotator Genius with "two rotors" contributes 2 addressable rotors.
|
||||
const patch = (i: number, p: Partial<RotatorDevice>) => setRotors((l) => l.map((d, j) => (j === i ? { ...d, ...p } : d)) as any);
|
||||
const addRotor = () => setRotors((l) => [...l, {
|
||||
id: '', name: '', type: 'pst', host: '127.0.0.1', port: 12000, has_elevation: false,
|
||||
rotator_num: 1, dual: false, motorized: true, name2: '', motorized2: false,
|
||||
transport: 'tcp', com_port: '', baud: 9600,
|
||||
} as any]);
|
||||
const removeRotor = (i: number) => setRotors((l) => l.filter((_, j) => j !== i));
|
||||
const anyPst = rotors.some((d) => ((d as any).type ?? 'pst') === 'pst');
|
||||
// The motorized-antenna choice only matters once more than one rotor exists.
|
||||
const multi = rotors.length > 1 || rotors.some((d) => (d as any).type === 'rotgenius' && (d as any).dual);
|
||||
return (
|
||||
<>
|
||||
<SectionHeader
|
||||
title="Rotator"
|
||||
hint={isRG || isARCO ? undefined : t('rot.hint')}
|
||||
/>
|
||||
<SectionHeader title="Rotator" hint={anyPst ? t('rot.hint') : undefined} />
|
||||
<div className="space-y-4 max-w-xl">
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||
<Checkbox checked={rotator.enabled} onCheckedChange={(c) => setRotator((s) => ({ ...s, enabled: !!c }))} />
|
||||
{t('rot.enable')}
|
||||
</label>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1">
|
||||
<Label>{t('rot.type')}</Label>
|
||||
{/* Each backend gets its default port: Rotator Genius 9006, ARCO 4001
|
||||
(placeholder — must match the ARCO's LAN menu), PstRotator 12000. */}
|
||||
<Select value={(rotator as any).type ?? 'pst'}
|
||||
onValueChange={(v) => setRotator((s) => ({ ...s, type: v, port: v === 'rotgenius' ? 9006 : v === 'arco' ? 4001 : 12000 } as any))}>
|
||||
<SelectTrigger className="h-9"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="pst">PstRotator (UDP)</SelectItem>
|
||||
<SelectItem value="rotgenius">Rotator Genius (4O3A, native)</SelectItem>
|
||||
<SelectItem value="arco">GS-232A controller (microHAM ARCO, ERC…)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{isRG && (
|
||||
<div className="space-y-1">
|
||||
<Label>{t('rot.rotatorNum')}</Label>
|
||||
<Select value={String((rotator as any).rotator_num ?? 1)}
|
||||
onValueChange={(v) => setRotator((s) => ({ ...s, rotator_num: parseInt(v, 10) || 1 } as any))}>
|
||||
<SelectTrigger className="h-9"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1">1</SelectItem>
|
||||
<SelectItem value="2">2</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{rotors.length === 0 && <p className="text-sm text-muted-foreground">{t('rot.none')}</p>}
|
||||
{rotors.map((d, i) => {
|
||||
const dev = d as any;
|
||||
const isRG = dev.type === 'rotgenius';
|
||||
const isARCO = dev.type === 'arco';
|
||||
const transport = dev.transport ?? 'tcp';
|
||||
return (
|
||||
<div key={dev.id || i} className="rounded-xl border border-border bg-card/40 p-3 space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Compass className="size-4 text-primary shrink-0" />
|
||||
<Input className="h-8 flex-1" value={dev.name ?? ''} placeholder={`Rotor ${i + 1}`}
|
||||
onChange={(e) => patch(i, { name: e.target.value })} />
|
||||
<Button size="icon" variant="ghost" className="size-8 text-muted-foreground hover:text-destructive"
|
||||
onClick={() => removeRotor(i)} title={t('rot.remove')}>
|
||||
<Trash2 className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1">
|
||||
<Label>{t('rot.type')}</Label>
|
||||
{/* Each backend gets its default port: Rotator Genius 9006, ARCO 4001
|
||||
(placeholder — must match the ARCO's LAN menu), PstRotator 12000. */}
|
||||
<Select value={dev.type ?? 'pst'}
|
||||
onValueChange={(v) => patch(i, { type: v as any, port: v === 'rotgenius' ? 9006 : v === 'arco' ? 4001 : 12000 })}>
|
||||
<SelectTrigger className="h-9"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="pst">PstRotator (UDP)</SelectItem>
|
||||
<SelectItem value="rotgenius">Rotator Genius (4O3A, native)</SelectItem>
|
||||
<SelectItem value="arco">GS-232A controller (microHAM ARCO, ERC…)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{isRG && !dev.dual && (
|
||||
<div className="space-y-1">
|
||||
<Label>{t('rot.rotatorNum')}</Label>
|
||||
<Select value={String(dev.rotator_num ?? 1)}
|
||||
onValueChange={(v) => patch(i, { rotator_num: parseInt(v, 10) || 1 })}>
|
||||
<SelectTrigger className="h-9"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1">1</SelectItem>
|
||||
<SelectItem value="2">2</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
{/* The ARCO is reachable over the LAN (TCP) or its USB virtual COM. */}
|
||||
{isARCO && (
|
||||
<div className="space-y-1">
|
||||
<Label>Connection</Label>
|
||||
<Select value={transport} onValueChange={(v) => patch(i, { transport: v as any })}>
|
||||
<SelectTrigger className="h-9"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="tcp">Network (LAN)</SelectItem>
|
||||
<SelectItem value="serial">USB (serial COM)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* Rotator Genius drives both its ports — one entry = two rotors. */}
|
||||
{isRG && (
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||
<Checkbox checked={!!dev.dual} onCheckedChange={(c) => patch(i, { dual: !!c })} />
|
||||
{t('rot.rgDual')}
|
||||
</label>
|
||||
)}
|
||||
{isARCO && transport === 'serial' ? (
|
||||
<div className="space-y-1 max-w-xs">
|
||||
<Label>COM port</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Select value={dev.com_port || '_'} onValueChange={(v) => patch(i, { com_port: v === '_' ? '' : v })}>
|
||||
<SelectTrigger className="h-9 flex-1"><SelectValue placeholder="— COM —" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{wkPorts.length === 0 && <SelectItem value="_" disabled>No ports found</SelectItem>}
|
||||
{wkPorts.map((p) => <SelectItem key={p} value={p}>{p}</SelectItem>)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button size="sm" variant="outline" className="h-9" onClick={() => ListSerialPorts().then((p) => setWkPorts((p ?? []) as string[])).catch(() => {})}>
|
||||
<ArrowDown className="size-3.5 rotate-90" />
|
||||
</Button>
|
||||
<Select value={String(dev.baud || 9600)} onValueChange={(v) => patch(i, { baud: Number(v) })}>
|
||||
<SelectTrigger className="h-9 w-28"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{[4800, 9600, 19200, 38400, 57600].map((b) => (
|
||||
<SelectItem key={b} value={String(b)}>{b} baud</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div className="space-y-1 col-span-2">
|
||||
<Label>Host / IP</Label>
|
||||
<Input value={dev.host ?? ''} onChange={(e) => patch(i, { host: e.target.value })}
|
||||
placeholder={isRG || isARCO ? '192.168.1.60' : '127.0.0.1'} className="font-mono" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label>{isRG || isARCO ? 'TCP port' : 'UDP port'}</Label>
|
||||
<PortInput value={dev.port} onChange={(n) => patch(i, { port: n })}
|
||||
fallback={isRG ? 9006 : isARCO ? 4001 : 12000} className="font-mono" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!isRG && !isARCO && (
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||
<Checkbox checked={!!dev.has_elevation} onCheckedChange={(c) => patch(i, { has_elevation: !!c })} />
|
||||
This rotator supports elevation (VHF / satellite)
|
||||
</label>
|
||||
)}
|
||||
{isRG && <p className="text-xs text-muted-foreground">{t('rot.rgHint')}</p>}
|
||||
{isARCO && <p className="text-xs text-muted-foreground">{t('rot.arcoHint')}</p>}
|
||||
{/* Which antenna this rotor carries — only relevant with >1 rotor. */}
|
||||
{multi && (
|
||||
<div className="space-y-1 max-w-xs">
|
||||
<Label>{isRG && dev.dual ? t('rot.antenna1') : t('rot.antenna')}</Label>
|
||||
<Select value={dev.motorized ? 'motor' : 'std'} onValueChange={(v) => patch(i, { motorized: v === 'motor' })}>
|
||||
<SelectTrigger className="h-9"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="std">{t('rot.antStd')}</SelectItem>
|
||||
<SelectItem value="motor">{t('rot.antMotor')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
{/* Second logical rotor of a dual Rotator Genius: its own name + antenna. */}
|
||||
{isRG && dev.dual && (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1">
|
||||
<Label>{t('rot.name2')}</Label>
|
||||
<Input value={dev.name2 ?? ''} placeholder={`${dev.name || 'Rotor'} 2`}
|
||||
onChange={(e) => patch(i, { name2: e.target.value })} />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label>{t('rot.antenna2')}</Label>
|
||||
<Select value={dev.motorized2 ? 'motor' : 'std'} onValueChange={(v) => patch(i, { motorized2: v === 'motor' })}>
|
||||
<SelectTrigger className="h-9"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="std">{t('rot.antStd')}</SelectItem>
|
||||
<SelectItem value="motor">{t('rot.antMotor')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm" onClick={() => testRotator(d, 1)} disabled={rotatorTesting}>
|
||||
{rotatorTesting ? t('hw.sending') : (isRG && dev.dual ? t('rot.testRotor1') : t('hw.testRotator'))}
|
||||
</Button>
|
||||
{isRG && dev.dual && (
|
||||
<Button variant="outline" size="sm" onClick={() => testRotator(d, 2)} disabled={rotatorTesting}>
|
||||
{t('rot.testRotor2')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* The ARCO is reachable over the LAN (TCP) or its USB virtual COM. */}
|
||||
{isARCO && (
|
||||
<div className="space-y-1">
|
||||
<Label>Connection</Label>
|
||||
<Select value={(rotator as any).transport ?? 'tcp'}
|
||||
onValueChange={(v) => setRotator((s) => ({ ...s, transport: v } as any))}>
|
||||
<SelectTrigger className="h-9"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="tcp">Network (LAN)</SelectItem>
|
||||
<SelectItem value="serial">USB (serial COM)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isARCO && (rotator as any).transport === 'serial' ? (
|
||||
<div className="space-y-1 max-w-xs">
|
||||
<Label>COM port</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Select value={(rotator as any).com_port || '_'}
|
||||
onValueChange={(v) => setRotator((s) => ({ ...s, com_port: v === '_' ? '' : v } as any))}>
|
||||
<SelectTrigger className="h-9 flex-1"><SelectValue placeholder="— COM —" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{wkPorts.length === 0 && <SelectItem value="_" disabled>No ports found</SelectItem>}
|
||||
{wkPorts.map((p) => <SelectItem key={p} value={p}>{p}</SelectItem>)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button size="sm" variant="outline" className="h-9" onClick={() => ListSerialPorts().then((p) => setWkPorts((p ?? []) as string[])).catch(() => {})}>
|
||||
<ArrowDown className="size-3.5 rotate-90" />
|
||||
</Button>
|
||||
<Select value={String((rotator as any).baud || 9600)}
|
||||
onValueChange={(v) => setRotator((s) => ({ ...s, baud: Number(v) } as any))}>
|
||||
<SelectTrigger className="h-9 w-28"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{[4800, 9600, 19200, 38400, 57600].map((b) => (
|
||||
<SelectItem key={b} value={String(b)}>{b} baud</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div className="space-y-1 col-span-2">
|
||||
<Label>Host / IP</Label>
|
||||
<Input
|
||||
value={rotator.host ?? ''}
|
||||
onChange={(e) => setRotator((s) => ({ ...s, host: e.target.value }))}
|
||||
placeholder={isRG || isARCO ? '192.168.1.60' : '127.0.0.1'}
|
||||
className="font-mono"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label>{isRG || isARCO ? 'TCP port' : 'UDP port'}</Label>
|
||||
<PortInput
|
||||
value={rotator.port}
|
||||
onChange={(n) => setRotator((s) => ({ ...s, port: n }))} fallback={isRG ? 9006 : isARCO ? 4001 : 12000}
|
||||
className="font-mono"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!isRG && !isARCO && (
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||
<Checkbox checked={rotator.has_elevation} onCheckedChange={(c) => setRotator((s) => ({ ...s, has_elevation: !!c }))} />
|
||||
This rotator supports elevation (VHF / satellite)
|
||||
</label>
|
||||
)}
|
||||
{isRG && <p className="text-xs text-muted-foreground">{t('rot.rgHint')}</p>}
|
||||
{isARCO && <p className="text-xs text-muted-foreground">{t('rot.arcoHint')}</p>}
|
||||
<div className="flex items-center gap-2 pt-2">
|
||||
<Button variant="outline" size="sm" onClick={testRotator} disabled={rotatorTesting}>
|
||||
{rotatorTesting ? t('hw.sending') : t('hw.testRotator')}
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={() => RotatorStop().catch((e) => setErr(String(e?.message ?? e)))} disabled={!rotator.enabled}>
|
||||
Stop
|
||||
</Button>
|
||||
{!isRG && (
|
||||
<Button variant="outline" size="sm" onClick={() => RotatorPark().catch((e) => setErr(String(e?.message ?? e)))} disabled={!rotator.enabled}>
|
||||
Park
|
||||
);
|
||||
})}
|
||||
<Button variant="outline" size="sm" onClick={addRotor}>
|
||||
<Plus className="size-3.5 mr-1" /> {t('rot.add')}
|
||||
</Button>
|
||||
{rotors.length > 0 && (
|
||||
<div className="flex items-center gap-2 pt-2">
|
||||
<Button variant="outline" size="sm" onClick={() => RotatorStop().catch((e) => setErr(String(e?.message ?? e)))}>
|
||||
Stop
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{anyPst && (
|
||||
<Button variant="outline" size="sm" onClick={() => RotatorPark().catch((e) => setErr(String(e?.message ?? e)))}>
|
||||
Park
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{rotatorTest && (
|
||||
<div className={cn(
|
||||
'text-xs rounded-md p-2.5 border',
|
||||
@@ -3440,8 +3554,9 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
</div>
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground">
|
||||
From the main entry strip, click the bearing pill to rotate to the short-path azimuth.
|
||||
Shift+click for long-path, Ctrl+click to stop.
|
||||
From the main entry strip, click the bearing pill to rotate the active rotor to the short-path
|
||||
azimuth. Shift+click for long-path, Ctrl+click to stop. With several rotors, pick the active one
|
||||
on the compass widget.
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user