When the user changes frequency, applyLocalTunedFrequency sets lastFreqHz
optimistically. But the SSE state stream could push back the old server
frequency before set_freq completes, causing the marker to snap back then
forward. Add a sequence-guarded _freqOptimisticHz that suppresses stale
SSE frequency updates while a set_freq is in flight.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Replace synchronous drawSignalOverlay() calls in freq/BW change handlers
with lightweight CSS div elements repositioned via transform: translateX().
This is GPU-composited with zero layout/paint cost, making frequency and
bandwidth changes appear instantaneous. The full WebGL overlay catches up
on the next requestAnimationFrame.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Call drawSignalOverlay() synchronously on frequency and bandwidth changes instead of deferring entirely to requestAnimationFrame. Also make bookmark apply fire-and-forget so the click handler returns immediately after optimistic UI updates.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Replace single-connection relay with per-rig audio manager that spawns independent TCP connections for each rig. Each rig gets its own broadcast channel, stream info, and vchan command routing. Selected rig mirrors to global channels for backward compat. Also fix bookmark apply to update spectrum marker instantly and fire all requests in parallel.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
When ?rig_id= is specified on /audio, don't fall back to the global broadcast (which carries whichever rig is connected). Return 404 for rigs without an active audio connection instead of silently delivering the wrong rig's audio. Also create per-rig audio channels for all known rigs eagerly so connected rigs are instantly subscribable.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Channel SSE events were broadcast to all tabs regardless of rig, causing tabs to display wrong rig's channels. Per-rig audio info_rx override caused WebSocket to hang waiting for stream info that never arrives.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Each browser tab can now subscribe to a specific rig's spectrum and
audio independently via ?rig_id= query params on /spectrum and /audio.
The remote client polls spectrum for all rigs with active subscribers
and routes responses to per-rig watch channels. Virtual channel
commands are routed through per-rig senders with global fallback.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
When a new tab connects and receives the initial channels event,
automatically subscribe to channel 0 (primary) so the session joins
the same tuned channel as other users on that rig. Uses a lightweight
auto-join that skips scheduler control takeover since audio isn't
started yet at this point.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
select_rig no longer mutates remote_active_rig_id — only the per-session
mapping is updated. This eliminates cross-tab leaking entirely: each tab
carries its own rig_id via the session manager, /events?rig_id, and
/status?rig_id query params.
The global remote_active_rig_id now only serves as the startup default
for brand-new sessions that have no rig_id yet.
Also fix the About section to show the per-tab lastActiveRigId instead
of the server's global active_rig_id.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
pollFreshSnapshot() fetches GET /status on every SSE connect/reconnect,
but /status always returned the global selected rig's state, overwriting
the per-tab display with whichever rig was last switched to globally.
Now pollFreshSnapshot passes rig_id as a query param and the /status
endpoint uses the per-rig watch channel when provided, matching the
/events behavior for true per-tab rig isolation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
The previous commit conditionally skipped updating remote_active_rig_id
when session_id was provided, but the remote client reads the global to
route commands to the correct rig on trx-server. Restore the
unconditional global update; cross-tab SSE isolation is handled by the
rig_id query param on /events and the JS-side guard in applyRigList.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
After select_rig stopped mutating the global remote_active_rig_id for
session-aware clients, SSE reconnects would fall back to the old global
default instead of the newly selected rig. Now connect() passes
lastActiveRigId as a rig_id query param to /events, and the server
prefers it over the global default when present.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
The .audio-active state was using --accent-green which is actually
orange (#c24b1a). Match the regular play button's hardcoded #00d17f
green so the header button visually matches.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
select_rig was unconditionally updating the global remote_active_rig_id,
causing all SSE sessions to see the changed rig. Now only the per-session
mapping is updated when session_id is provided; the global default is
only changed for non-session-aware clients.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Bump selector specificity to .header-bar-btn.header-audio-btn so the
padding: 0 rule wins over the generic .header-bar-btn padding, and
switch the SVG to width/height: 100% so it expands to fill the button.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add per-rig watch::Sender<RigState> map to FrontendRuntimeContext,
populated by refresh_remote_snapshot for every rig returned by GetRigs.
The SSE /events endpoint now subscribes to the session's rig-specific
watch channel instead of the single global one, allowing different
browser tabs to independently view different rigs. The JS frontend
reconnects SSE on rig switch to pick up the new channel.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add SessionRigManager to track per-SSE-session rig_id so different
browser tabs can independently select rigs without interfering.
The /events SSE stream filters state updates by session rig (falling
back to the global active rig), and /select_rig accepts an optional
session_id to update the per-session mapping.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Rig switch needs the server call so SSE/audio follow the selected rig. Play button now uses a fixed triangle icon sized to match header controls.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
The /select_rig endpoint sets global server state which affects all tabs. Since postPath() already sends rig_id with every command, the rig picker now just sets the local lastActiveRigId variable.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
postPath() was duplicating rig_id on /select_rig calls, causing deserialization failure and silently dropping the rig switch.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
All POST command endpoints now accept an optional ?rig_id= parameter so each browser tab can independently target a specific rig. The JS frontend tracks the active rig per-tab and auto-appends rig_id to every request.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add checkbox column to bookmark table with select-all support and a
Delete Selected button for batch removal. New POST /bookmarks/batch_delete
API endpoint accepts an array of IDs and removes them in one request.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Broadcast virtual channel list for newly selected rig from select_rig so SSE
clients receive correct channels immediately. Detect rig changes in render()
and reset stale decoder state (RDS, spectrum, decoder status indicators) from
the previous rig.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add Maidenhead locator and reverse-geocoded city/country to the header.
Uses Nominatim API to resolve nearest city asynchronously.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
When Device::new(args) fails, enumerate all available SoapySDR devices and
include them in the error message. Also hint that args are case-sensitive.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
When specific SoapySDR device args are provided (e.g. with a serial number),
fail hard instead of silently falling back to Device::new("") which opens
the first available device. This caused multi-device setups to bind both
rig instances to the same physical device.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add weakest decoded signal panel showing top 5 weakest SNR signals. Make all
stat tiles (longest decode, strongest signal, weakest signal) clickable to
highlight the corresponding locator on the map.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Reflect the common/ft8/ft4/ft2 directory reorganization in the
architecture diagram, file tree, and signal flow description.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Move ft2_encode from ft4/ to ft2/ where it belongs. Remove all
module-level #[allow] suppressions and fix the underlying issues:
- Remove dead code: wf_mag_at, xor_rows, unused Monitor IFFT fields, OsdBox.size
- Gate encode174_to_bits with #[cfg(test)] (only used in tests)
- Convert 40+ C-style index loops to idiomatic iterators
- Add targeted #[allow(clippy::too_many_arguments)] on two OSD functions
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Move ft2/osd.rs, ft2/bitmetrics.rs, ft2/downsample.rs, ft2/sync.rs
out of the ft2/ directory into src/ as top-level modules. Convert
ft2/mod.rs to ft2.rs. Update all imports from super:: to crate::ft2::.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Replace FTX_CRATE.md with README.md documenting upstream origins
(kgoba/ft8_lib for FT8/FT4, iu8lmc/Decodium-3.0 for FT2) and a
Mermaid diagram of the crate architecture.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Remove normalize_llr which was undoing the scalefac=2.83 scaling,
causing LLRs to be 2.83x too small for the BP+OSD decoder. Align
sync thresholds with reference: coarse 0.50->0.40, decode 0.65->0.55,
sync quality 10->9, maxosd 3->4. Revert norm_sqr back to norm in
bitmetrics since the metric difference is nonlinear.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Replace Vec<Vec<f32>> with flat stack arrays in ldpc_decode (~114KB),
convert 19+ Vec allocations to stack arrays in osd174_91, eliminate
per-call temp Vec in nextpat91 via in-place mutation, and replace
norm() with norm_sqr() in bitmetrics hot loop (~5.4M calls/frame).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Eliminate duplicated code between FT2 and FT8/shared modules:
- Share parity8() from encode.rs, remove copies in ft2/mod.rs and osd.rs
- Share pack_bits() from decode.rs, remove pack_bits91() from osd.rs
- Add verify_crc_and_build_message() to decode.rs, used by both FT8 and FT2
- Add normalize_llr() to decode.rs, replacing per-module normalization
- Make encode174() pub(crate), add encode174_to_bits() for bit-array output
- Wire FT2 decode_hit to use full BP+OSD decoder from osd.rs instead of
separate BP + sum-product + OSD-lite flow
- Align LLR scale factor to 2.83 matching reference implementation
Net -178 lines removed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
add() and lookup() had no wrap-around guard in their linear-probe loops.
Once 256 unique callsigns filled the table, any subsequent add or lookup
for an absent hash would cycle through all 256 slots forever, hanging the
FT8 decoder task permanently inside block_in_place. On a busy band this
could happen within a few minutes of operation.
- add(): evict the probe-start slot when a full cycle completes
- lookup(): return None after a full probe cycle
- reset(): call cleanup(10) each slot boundary to age out stale entries
- Add regression tests for both infinite-loop scenarios
Also includes cargo fmt reformatting of pre-existing style issues.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>