From 6874055b1cc3fa9c99f6eac62e16cf331ebb7a97 Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Tue, 10 Mar 2026 23:20:42 +0100 Subject: [PATCH] [feat](trx-frontend-http): add Background Decoding Scheduler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements a scheduler that retunes the rig automatically when no SSE clients are connected. Two modes are supported: - Grayline: tunes to per-period bookmarks (dawn/day/dusk/night) based on an inline NOAA solar algorithm given station lat/lon. - Time Span: tunes to bookmarks within user-defined UTC windows; midnight- spanning intervals supported. Backend: - SchedulerStore (PickleDB, sch:{rig_id} keys) in scheduler.rs - spawn_scheduler_task polls every 30 s, checks context.sse_clients == 0, sends SetFreq + SetMode via RigRequest with rig_id_override - HTTP API: GET/PUT/DELETE /scheduler/{rig_id}, GET …/status - sse_clients Arc added to FrontendRuntimeContext and shared with the SSE counter in build_server (single source of truth) - /scheduler/ added to Read auth routes (write requires Control) Frontend: - Scheduler tab (clock icon, 6th position) with Grayline/TimeSpan UI - scheduler.js plugin: loads config + bookmarks, live status polling every 15 s, write controls hidden for Rx-role users - CSS .sch-* component styles added to style.css - SCHEDULER.md design document at repo root Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Stan Grams --- SCHEDULER.md | 119 +++++ src/trx-client/trx-frontend/src/lib.rs | 3 + .../trx-frontend-http/assets/web/app.js | 17 +- .../trx-frontend-http/assets/web/index.html | 90 ++++ .../assets/web/plugins/scheduler.js | 485 +++++++++++++++++ .../trx-frontend-http/assets/web/style.css | 127 +++++ .../trx-frontend/trx-frontend-http/src/api.rs | 16 + .../trx-frontend-http/src/auth.rs | 1 + .../trx-frontend-http/src/scheduler.rs | 504 ++++++++++++++++++ .../trx-frontend-http/src/server.rs | 35 +- .../trx-frontend-http/src/status.rs | 1 + 11 files changed, 1393 insertions(+), 5 deletions(-) create mode 100644 SCHEDULER.md create mode 100644 src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/scheduler.js create mode 100644 src/trx-client/trx-frontend/trx-frontend-http/src/scheduler.rs diff --git a/SCHEDULER.md b/SCHEDULER.md new file mode 100644 index 0000000..503e03a --- /dev/null +++ b/SCHEDULER.md @@ -0,0 +1,119 @@ +# Background Decoding Scheduler + +## Overview + +The Background Decoding Scheduler automatically retunes the rig to pre-configured +bookmarks when no users are connected to the HTTP frontend. It runs as a background +tokio task inside `trx-frontend-http`, polling every 30 seconds. + +## Modes + +### Disabled (default) +Scheduler is inactive. Rig is not touched automatically. + +### Grayline +Retunes around the solar terminator (day/night boundary). + +The user provides: +- Station latitude and longitude (decimal degrees) +- Optional transition window width (minutes, default 20) +- Bookmark IDs for four periods: + - **Dawn** – window around sunrise (`sunrise ± window_min/2`) + - **Day** – after dawn until dusk + - **Dusk** – window around sunset (`sunset ± window_min/2`) + - **Night** – after dusk until next dawn + +Period precedence (most specific wins): Dawn > Dusk > Day > Night. + +If no bookmark is assigned to a period, the rig is not retuned for that period. + +Sunrise/sunset is computed inline using the NOAA simplified algorithm. +Polar regions (midnight sun / polar night) fall back to Day/Night accordingly. + +### TimeSpan +Retunes according to a list of user-defined time windows (UTC). + +Each entry specifies: +- `start_hhmm` – start of window (e.g. 600 = 06:00 UTC) +- `end_hhmm` – end of window (e.g. 700 = 07:00 UTC) +- `bookmark_id` – bookmark to apply +- `label` – optional human-readable description + +Windows that span midnight (`end_hhmm < start_hhmm`) are supported. +When multiple entries overlap, the first match (by list order) wins. + +## Storage + +Configuration is stored in PickleDB at `~/.config/trx-rs/scheduler.db`. + +Keys: `sch:{rig_id}` → JSON `SchedulerConfig`. + +## HTTP API + +All read endpoints are accessible at the **Rx** role level. +Write endpoints require the **Control** role. + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/scheduler/{rig_id}` | Get scheduler config for a rig | +| PUT | `/scheduler/{rig_id}` | Save scheduler config (Control only) | +| DELETE | `/scheduler/{rig_id}` | Reset config to Disabled (Control only) | +| GET | `/scheduler/{rig_id}/status` | Get last-applied bookmark and next event | + +## Activation logic + +Every 30 seconds the scheduler task checks: +1. `context.sse_clients.load() == 0` — no users connected +2. Active rig has a non-Disabled scheduler config +3. Current UTC time matches a scheduled window or grayline period +4. If the matching bookmark differs from `last_applied`, send `SetFreq` + `SetMode` + +The scheduler **does not** revert changes when users reconnect. Bookmarks serve as +a frequency map — the user can retune manually after connecting. + +## Data model (Rust) + +```rust +pub enum SchedulerMode { Disabled, Grayline, TimeSpan } + +pub struct GraylineConfig { + pub lat: f64, + pub lon: f64, + pub transition_window_min: u32, + pub day_bookmark_id: Option, + pub night_bookmark_id: Option, + pub dawn_bookmark_id: Option, + pub dusk_bookmark_id: Option, +} + +pub struct ScheduleEntry { + pub id: String, + pub start_hhmm: u32, + pub end_hhmm: u32, + pub bookmark_id: String, + pub label: Option, +} + +pub struct SchedulerConfig { + pub rig_id: String, + pub mode: SchedulerMode, + pub grayline: Option, + pub entries: Vec, +} +``` + +## UI (Scheduler tab) + +A dedicated sixth tab with a clock icon. + +- **Rig selector**: shows active rig (read-only). +- **Mode picker**: Disabled / Grayline / TimeSpan radio buttons. +- **Grayline section** (visible when mode = Grayline): + - Lat/lon inputs + - Transition window slider (5–60 min) + - Four bookmark selectors (Dawn / Day / Dusk / Night) +- **TimeSpan section** (visible when mode = TimeSpan): + - Table of entries with Start, End, Bookmark, Label, Remove button + - "Add Entry" row at the bottom +- **Status card**: last applied bookmark name and timestamp. +- Save button (Control only; form is read-only for Rx users). diff --git a/src/trx-client/trx-frontend/src/lib.rs b/src/trx-client/trx-frontend/src/lib.rs index 8f57a61..0c713a4 100644 --- a/src/trx-client/trx-frontend/src/lib.rs +++ b/src/trx-client/trx-frontend/src/lib.rs @@ -162,6 +162,8 @@ pub struct FrontendRuntimeContext { pub wspr_history: Arc>>, /// Authentication tokens for HTTP-JSON frontend pub auth_tokens: HashSet, + /// Active HTTP SSE clients (incremented on /events connect, decremented on disconnect). + pub sse_clients: Arc, /// Active rigctl TCP clients. pub rigctl_clients: Arc, /// rigctl listen endpoint, if enabled. @@ -222,6 +224,7 @@ impl FrontendRuntimeContext { ft8_history: Arc::new(Mutex::new(VecDeque::new())), wspr_history: Arc::new(Mutex::new(VecDeque::new())), auth_tokens: HashSet::new(), + sse_clients: Arc::new(AtomicUsize::new(0)), rigctl_clients: Arc::new(AtomicUsize::new(0)), rigctl_listen_addr: Arc::new(Mutex::new(None)), decode_collector_started: AtomicBool::new(false), diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js index 4f5d5ad..6bceed6 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js @@ -3347,7 +3347,7 @@ if (spectrumBwSweetBtn) { } // --- Tab navigation --- -const TAB_ORDER = ["main", "bookmarks", "decoders", "map", "about"]; +const TAB_ORDER = ["main", "bookmarks", "decoders", "map", "scheduler", "about"]; function navigateToTab(name) { if (authEnabled && !authRole && name !== "main") return; @@ -3415,6 +3415,10 @@ document.querySelector(".tab-bar").addEventListener("click", (e) => { window.addEventListener("resize", () => { scheduleSpectrumLayout(); }); // --- Auth startup sequence --- +function getAvailableRigIds() { + return lastRigIds || []; +} + async function initializeApp() { showAuthGate(false); const authStatus = await checkAuthStatus(); @@ -3426,6 +3430,7 @@ async function initializeApp() { updateAuthUI(); connect(); connectDecode(); + initSchedulerUI(); resizeHeaderSignalCanvas(); startHeaderSignalSampling(); return; @@ -3439,6 +3444,7 @@ async function initializeApp() { applyAuthRestrictions(); connect(); connectDecode(); + initSchedulerUI(); resizeHeaderSignalCanvas(); startHeaderSignalSampling(); } else { @@ -3449,6 +3455,13 @@ async function initializeApp() { } } +function initSchedulerUI() { + if (typeof initScheduler === "function") { + initScheduler(lastActiveRigId, authRole); + wireSchedulerEvents(); + } +} + // Setup auth form document.getElementById("auth-form").addEventListener("submit", async (e) => { e.preventDefault(); @@ -3466,6 +3479,7 @@ document.getElementById("auth-form").addEventListener("submit", async (e) => { applyAuthRestrictions(); connect(); connectDecode(); + initSchedulerUI(); resizeHeaderSignalCanvas(); startHeaderSignalSampling(); } catch (err) { @@ -3488,6 +3502,7 @@ if (guestBtn) { applyAuthRestrictions(); connect(); connectDecode(); + initSchedulerUI(); resizeHeaderSignalCanvas(); startHeaderSignalSampling(); }); diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html index dd2e98b..74de8e7 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html @@ -46,6 +46,10 @@ Map + + + + + +
+ + +
+ + +
+
Last Activity
+
No activity yet.
+
+ +