Skip to content

Commit 61435ff

Browse files
committed
feat: rotator settle delay and setup tab restyle
Add configurable settle delay (0-30s, default 5s) before park/slew after a pass ends, giving large antennas time to stop wobbling. Restyle rotator setup tab to match Settings window pattern: shared Select/Input components, 12px labels with --text-dim color. Remove unused rot-select styles.
1 parent 9bce2e4 commit 61435ff

2 files changed

Lines changed: 85 additions & 59 deletions

File tree

src/stores/rotator.svelte.ts

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ class RotatorStore {
4949
parkAz = $state(0);
5050
parkEl = $state(0);
5151
passEndAction = $state<PassEndAction>('nothing');
52+
settleDelaySec = $state(5);
5253

5354
// Runtime state
5455
status = $state<RotatorStatus>('disconnected');
@@ -146,6 +147,7 @@ class RotatorStore {
146147
}
147148

148149
async disconnect(): Promise<void> {
150+
if (this._settleTimer) { clearTimeout(this._settleTimer); this._settleTimer = null; }
149151
this.stopTimers();
150152
if (this.driver) {
151153
await this.driver.disconnect().catch(() => {});
@@ -231,23 +233,34 @@ class RotatorStore {
231233
this.targetEl = state.trackEl;
232234
}
233235

236+
private _settleTimer: ReturnType<typeof setTimeout> | null = null;
237+
234238
private handlePassEnd(noradId: number | null): void {
235-
if (this.passEndAction === 'slew-next' && noradId !== null) {
236-
// Find next pass for the same sat and pre-position to AOS
237-
const nextPass = this.findNextPass(noradId);
238-
if (nextPass) {
239-
this.goto(nextPass.aosAz, 0);
240-
this.nextAosEpoch = nextPass.aosEpoch;
241-
this.nextAosSatName = nextPass.satName;
242-
// Keep auto-track on so it picks up when sat rises
243-
return;
239+
if (this._settleTimer) { clearTimeout(this._settleTimer); this._settleTimer = null; }
240+
const act = () => {
241+
if (this.passEndAction === 'slew-next' && noradId !== null) {
242+
// Find next pass for the same sat and pre-position to AOS
243+
const nextPass = this.findNextPass(noradId);
244+
if (nextPass) {
245+
this.goto(nextPass.aosAz, 0);
246+
this.nextAosEpoch = nextPass.aosEpoch;
247+
this.nextAosSatName = nextPass.satName;
248+
// Keep auto-track on so it picks up when sat rises
249+
return;
250+
}
244251
}
245-
}
246-
this.autoTrack = false;
247-
this.targetAz = null;
248-
this.targetEl = null;
249-
if (this.passEndAction === 'park') {
250-
this.park();
252+
this.autoTrack = false;
253+
this.targetAz = null;
254+
this.targetEl = null;
255+
if (this.passEndAction === 'park') {
256+
this.park();
257+
}
258+
};
259+
const delayMs = this.settleDelaySec * 1000;
260+
if (delayMs > 0 && this.passEndAction !== 'nothing') {
261+
this._settleTimer = setTimeout(act, delayMs);
262+
} else {
263+
act();
251264
}
252265
}
253266

@@ -456,6 +469,8 @@ class RotatorStore {
456469
if (pEl) this.parkEl = Number(pEl);
457470
const endAction = g('pass_end_action');
458471
if (endAction === 'nothing' || endAction === 'park' || endAction === 'slew-next') this.passEndAction = endAction;
472+
const settle = g('settle_delay');
473+
if (settle) this.settleDelaySec = Number(settle);
459474
const tol = g('tolerance');
460475
if (tol) this.tolerance = Number(tol);
461476
// autoTrack and panelOpen are NOT restored — require explicit user action
@@ -524,6 +539,11 @@ class RotatorStore {
524539
this.save('pass_end_action', action);
525540
}
526541

542+
setSettleDelay(sec: number): void {
543+
this.settleDelaySec = Math.max(0, Math.min(30, sec));
544+
this.save('settle_delay', this.settleDelaySec);
545+
}
546+
527547
setTolerance(deg: number): void {
528548
this.tolerance = Math.max(0, Math.min(10, deg));
529549
this.save('tolerance', this.tolerance);

src/ui/RotatorWindow.svelte

Lines changed: 50 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import Button from './shared/Button.svelte';
55
import Checkbox from './shared/Checkbox.svelte';
66
import Slider from './shared/Slider.svelte';
7+
import Select from './shared/Select.svelte';
8+
import Input from './shared/Input.svelte';
79
import InfoTip from './shared/InfoTip.svelte';
810
import { uiStore } from '../stores/ui.svelte';
911
import { beamStore, isInsideBeam } from '../stores/beam.svelte';
@@ -890,22 +892,22 @@
890892
{:else}
891893
<div class="setup-panel">
892894
<h4 class="section-header">Antenna</h4>
893-
<div class="rot-row">
895+
<div class="row">
894896
<label>Preset<InfoTip>Sets beam width, tolerance, and update rate for common antenna types.</InfoTip></label>
895897
<div class="antenna-presets">
896898
{#each Object.entries(ANTENNA_PRESETS) as [key, p]}
897899
<Button size="xs" active={activeAntennaPreset === key} onclick={() => applyAntennaPreset(key)}>{p.label}</Button>
898900
{/each}
899901
</div>
900902
</div>
901-
<div class="rot-row antenna-summary">
903+
<div class="row antenna-summary">
902904
<span>Beam {beamStore.beamWidth}°</span>
903905
<span>Tolerance {rotatorStore.tolerance}°</span>
904906
<span>Rate {rateDisplay}</span>
905907
</div>
906908

907909
<h4 class="section-header">Connection</h4>
908-
<div class="rot-row">
910+
<div class="row">
909911
<label>Mode</label>
910912
<div class="rot-mode-btns">
911913
<Button size="xs" variant="ghost" active={rotatorStore.mode === 'serial'}
@@ -916,29 +918,29 @@
916918
</div>
917919

918920
{#if rotatorStore.mode === 'serial'}
919-
<div class="rot-row">
921+
<div class="row">
920922
<label>Protocol</label>
921-
<select class="rot-select" value={rotatorStore.serialProtocol}
922-
onchange={(e) => rotatorStore.setSerialProtocol((e.currentTarget as HTMLSelectElement).value as any)}>
923+
<Select size="xs" value={rotatorStore.serialProtocol}
924+
onchange={(e) => rotatorStore.setSerialProtocol((e.target as HTMLSelectElement).value as any)}>
923925
<option value="gs232">GS-232</option>
924926
<option value="easycomm">EasyComm II</option>
925-
</select>
927+
</Select>
926928
</div>
927-
<div class="rot-row">
929+
<div class="row">
928930
<label>Baud</label>
929-
<select class="rot-select" value={String(rotatorStore.baudRate)}
930-
onchange={(e) => rotatorStore.setBaudRate(Number((e.currentTarget as HTMLSelectElement).value))}>
931+
<Select size="xs" value={String(rotatorStore.baudRate)}
932+
onchange={(e) => rotatorStore.setBaudRate(Number((e.target as HTMLSelectElement).value))}>
931933
<option value="4800">4800</option>
932934
<option value="9600">9600</option>
933935
<option value="19200">19200</option>
934-
</select>
936+
</Select>
935937
</div>
936938
{/if}
937939

938940
{#if rotatorStore.mode === 'network'}
939-
<div class="rot-row">
941+
<div class="row">
940942
<label>URL</label>
941-
<input class="radar-input rot-url" type="text"
943+
<Input size="xs" class="rot-url" type="text"
942944
value={rotatorStore.wsUrl}
943945
onblur={(e) => rotatorStore.setWsUrl((e.currentTarget as HTMLInputElement).value)}
944946
onkeydown={(e) => e.key === 'Enter' && (e.target as HTMLInputElement).blur()} />
@@ -954,50 +956,56 @@
954956
min={0} max={10} step={0.5} value={rotatorStore.tolerance}
955957
oninput={(e) => rotatorStore.setTolerance(Number((e.target as HTMLInputElement).value))} />
956958

957-
<div class="rot-row">
958-
<label>Park position</label>
959-
<select class="rot-select" value={rotatorStore.parkPreset}
960-
onchange={(e) => rotatorStore.setParkPreset((e.currentTarget as HTMLSelectElement).value as ParkPreset)}>
959+
<div class="row">
960+
<label>Park Position</label>
961+
<Select size="xs" value={rotatorStore.parkPreset}
962+
onchange={(e) => rotatorStore.setParkPreset((e.target as HTMLSelectElement).value as ParkPreset)}>
961963
{#each Object.entries(PARK_PRESETS) as [key, p]}
962964
<option value={key}>{p.label}</option>
963965
{/each}
964966
<option value="custom">Custom</option>
965-
</select>
967+
</Select>
966968
</div>
967969
{#if rotatorStore.parkPreset === 'custom'}
968-
<div class="rot-row">
970+
<div class="row">
969971
<label>Park Az / El</label>
970972
<div class="park-custom">
971-
<input class="radar-input" type="number" min="0" max="360" step="0.1"
973+
<Input size="xs" type="number" min="0" max="360" step="0.1"
972974
value={rotatorStore.parkAz}
973975
onblur={(e) => rotatorStore.setParkPosition(Number((e.currentTarget as HTMLInputElement).value), rotatorStore.parkEl)}
974976
onkeydown={(e) => e.key === 'Enter' && (e.target as HTMLInputElement).blur()} />
975-
<span class="radar-unit">°</span>
976-
<input class="radar-input" type="number" min="0" max="90" step="0.1"
977+
<span class="unit">°</span>
978+
<Input size="xs" type="number" min="0" max="90" step="0.1"
977979
value={rotatorStore.parkEl}
978980
onblur={(e) => rotatorStore.setParkPosition(rotatorStore.parkAz, Number((e.currentTarget as HTMLInputElement).value))}
979981
onkeydown={(e) => e.key === 'Enter' && (e.target as HTMLInputElement).blur()} />
980-
<span class="radar-unit">°</span>
982+
<span class="unit">°</span>
981983
</div>
982984
</div>
983985
{/if}
984986

985-
<div class="rot-row">
986-
<label>After pass<InfoTip>Action when a tracked satellite goes below the horizon. Disables auto-slew automatically.</InfoTip></label>
987-
<select class="rot-select" value={rotatorStore.passEndAction}
988-
onchange={(e) => rotatorStore.setPassEndAction((e.currentTarget as HTMLSelectElement).value as PassEndAction)}>
987+
<div class="row">
988+
<label>After Pass<InfoTip>Action when a tracked satellite goes below the horizon. Disables auto-slew automatically.</InfoTip></label>
989+
<Select size="xs" value={rotatorStore.passEndAction}
990+
onchange={(e) => rotatorStore.setPassEndAction((e.target as HTMLSelectElement).value as PassEndAction)}>
989991
<option value="nothing">Do nothing</option>
990992
<option value="park">Park</option>
991993
<option value="slew-next">Slew to next AOS</option>
992-
</select>
994+
</Select>
993995
</div>
996+
{#if rotatorStore.passEndAction !== 'nothing'}
997+
{#snippet settleTip()}<InfoTip>Wait this many seconds after LOS before parking or slewing. Lets large antennas stop wobbling.</InfoTip>{/snippet}
998+
<Slider label="Settle Delay" display="{rotatorStore.settleDelaySec}s" tip={settleTip}
999+
min={0} max={30} step={1} value={rotatorStore.settleDelaySec}
1000+
oninput={(e) => rotatorStore.setSettleDelay(Number((e.target as HTMLInputElement).value))} />
1001+
{/if}
9941002

9951003
<h4 class="section-header">Visual</h4>
996-
<div class="rot-row">
997-
<label>Cone without rotator<InfoTip>Show the beam cone in the 3D view when no rotator is connected. The cone always follows the rotator when connected.</InfoTip></label>
1004+
<div class="row">
1005+
<label>Cone Without Rotator<InfoTip>Show the beam cone in the 3D view when no rotator is connected. The cone always follows the rotator when connected.</InfoTip></label>
9981006
<Checkbox checked={beamStore.coneVisible} onchange={() => beamStore.setConeVisible(!beamStore.coneVisible)} />
9991007
</div>
1000-
<div class="rot-row">
1008+
<div class="row">
10011009
<label>Radar VFX<InfoTip>Sweep line and phosphor afterglow effect on satellite blips.</InfoTip></label>
10021010
<Checkbox checked={uiStore.radarVfx} onchange={() => uiStore.setToggle('radarVfx', !uiStore.radarVfx)} />
10031011
</div>
@@ -1164,18 +1172,6 @@
11641172
display: flex;
11651173
gap: 2px;
11661174
}
1167-
.rot-select {
1168-
font-size: 10px;
1169-
font-family: 'Overpass Mono', monospace;
1170-
background: var(--ui-bg);
1171-
border: 1px solid var(--border);
1172-
color: var(--text-dim);
1173-
padding: 1px 3px;
1174-
border-radius: 2px;
1175-
}
1176-
.rot-select:hover { border-color: var(--border-hover); }
1177-
.rot-select:focus { border-color: var(--border-hover); outline: none; color: var(--text); }
1178-
.rot-url { width: 160px; }
11791175
.park-custom {
11801176
display: flex;
11811177
align-items: center;
@@ -1281,7 +1277,7 @@
12811277
display: flex;
12821278
gap: 2px;
12831279
}
1284-
.antenna-summary {
1280+
.antenna-summary.row {
12851281
gap: 10px;
12861282
justify-content: flex-start;
12871283
font-size: 9px;
@@ -1306,6 +1302,16 @@
13061302
border-bottom: 1px solid var(--border);
13071303
}
13081304
.section-header:first-child { margin-top: 0; }
1305+
.row {
1306+
display: flex;
1307+
align-items: center;
1308+
justify-content: space-between;
1309+
margin-bottom: 6px;
1310+
}
1311+
.row:last-child { margin-bottom: 0; }
1312+
.row label { color: var(--text-dim); font-size: 12px; }
1313+
.unit { color: var(--text-ghost); font-size: 11px; }
1314+
:global(.rot-url) { width: 160px; }
13091315
.guide-details {
13101316
margin-top: 8px;
13111317
}

0 commit comments

Comments
 (0)