Tune links: put the tuned frequency in the address bar #42

Merged
sjg merged 1 commits from feat/tune-links into main 2026-08-05 22:36:31 +02:00
Owner

Every public web SDR — OpenWebRX, KiwiSDR, WebSDR — can be linked to at a frequency, and that is largely how they spread. trx-rs could not: features/navigation/routes.ts routed the tab and passed the query string through untouched, so "listen to this" meant a screenshot and a frequency typed out by hand.

What it does

http://receiver.example:8080/?rig=sdr&f=14074000&mode=USB&bw=3000
Parameter Meaning
f Frequency. Hz canonically; 7074k and 14.074M are accepted from hand-written links.
mode USB, CW, WFM, …
bw Filter bandwidth in Hz.
rig Rig to select first, on a multi-rig client.

Opening one selects the rig, sets the mode, tunes, then applies the bandwidth — in that order, because a mode change carries its own default bandwidth and would otherwise overwrite an explicit bw. All four are optional; anything the rig cannot do is reported and the rest of the link still applies.

After load the address bar tracks the dial, which is what makes it copyable at any moment rather than only at load. It is rewritten with replaceState: tuning is not navigation, and a swept dial would otherwise bury the back button. A link button in the top bar copies the current link, and folds into the overflow menu before the theme toggle when the bar gets tight.

Decisions worth reviewing

  • A bare load fills the URL in with where the radio already is. That is what keeps the address bar always shareable, but it does mean every load rewrites the URL.
  • rx sessions do not apply links. Tuning needs control, so the page says the link was not applied rather than failing control calls one at a time.
  • Virtual channels do not publish. While a tab listens to a vchan the address is left alone rather than advertising a frequency the rig is not on.
  • bw is skipped in both directions on rigs without filter control, which would only refuse it.

Testing

Parsing and formatting are pure functions, tested directly: hand-written frequency forms, junk rejection, round-trip, canonicalisation, and that unrelated query parameters survive being rewritten.

New tests/tune-links.mjs covers the behaviour in a browser: a link tunes frequency, mode and bandwidth; the URL canonicalises in place; tuning by hand rewrites the link; no history entries accumulate; the parameters survive a tab change; rig= selects the rig; a bare load fills in. Added to npm run test:browser.

42 unit tests, four browser suites, typecheck, lint and the rebuilt bundle all pass.

One fixture change is load-bearing: web-fixture.mjs pinned every SSE frame's frequency to 100 MHz + jitter to keep consecutive frames distinct, so no test could ever observe the radio being tuned. The jitter now rides on the S-meter, and the fixture echoes set_freq/set_mode/set_bandwidth the way it already echoed squelch.

Documented under "Tune Links" in the User Manual.

Every public web SDR — OpenWebRX, KiwiSDR, WebSDR — can be linked to at a frequency, and that is largely how they spread. trx-rs could not: `features/navigation/routes.ts` routed the tab and passed the query string through untouched, so "listen to this" meant a screenshot and a frequency typed out by hand. ## What it does ``` http://receiver.example:8080/?rig=sdr&f=14074000&mode=USB&bw=3000 ``` | Parameter | Meaning | |-----------|---------| | `f` | Frequency. Hz canonically; `7074k` and `14.074M` are accepted from hand-written links. | | `mode` | `USB`, `CW`, `WFM`, … | | `bw` | Filter bandwidth in Hz. | | `rig` | Rig to select first, on a multi-rig client. | Opening one selects the rig, sets the mode, tunes, then applies the bandwidth — in that order, because a mode change carries its own default bandwidth and would otherwise overwrite an explicit `bw`. All four are optional; anything the rig cannot do is reported and the rest of the link still applies. After load the address bar tracks the dial, which is what makes it copyable at any moment rather than only at load. It is rewritten with `replaceState`: tuning is not navigation, and a swept dial would otherwise bury the back button. A link button in the top bar copies the current link, and folds into the `⋯` overflow menu before the theme toggle when the bar gets tight. ## Decisions worth reviewing - **A bare load fills the URL in** with where the radio already is. That is what keeps the address bar always shareable, but it does mean every load rewrites the URL. - **`rx` sessions do not apply links.** Tuning needs `control`, so the page says the link was not applied rather than failing control calls one at a time. - **Virtual channels do not publish.** While a tab listens to a vchan the address is left alone rather than advertising a frequency the rig is not on. - **`bw` is skipped in both directions** on rigs without filter control, which would only refuse it. ## Testing Parsing and formatting are pure functions, tested directly: hand-written frequency forms, junk rejection, round-trip, canonicalisation, and that unrelated query parameters survive being rewritten. New `tests/tune-links.mjs` covers the behaviour in a browser: a link tunes frequency, mode and bandwidth; the URL canonicalises in place; tuning by hand rewrites the link; no history entries accumulate; the parameters survive a tab change; `rig=` selects the rig; a bare load fills in. Added to `npm run test:browser`. 42 unit tests, four browser suites, typecheck, lint and the rebuilt bundle all pass. One fixture change is load-bearing: `web-fixture.mjs` pinned every SSE frame's frequency to `100 MHz + jitter` to keep consecutive frames distinct, so no test could ever observe the radio being tuned. The jitter now rides on the S-meter, and the fixture echoes `set_freq`/`set_mode`/`set_bandwidth` the way it already echoed squelch. Documented under "Tune Links" in the User Manual.
sjg added 1 commit 2026-08-05 22:31:40 +02:00
[feat](trx-frontend-http): put the tuned frequency in the address bar
CI / frontend (pull_request) Successful in 4m3s
CI / reuse (pull_request) Successful in 2s
CI / lint (push) Successful in 2m16s
CI / lint (pull_request) Successful in 2m16s
CI / test (pull_request) Successful in 8m11s
CI / test (push) Successful in 7m20s
CI / frontend (push) Successful in 3m10s
CI / reuse (push) Successful in 2s
a2c630a92b
A receiver spreads by being linked to, and there was nothing to link to:
the routes carried the tab and nothing else, so "listen to this" could
only ever mean a screenshot and a frequency typed out in a message.

The query string now carries the dial -- rig, frequency, mode and
bandwidth -- in both directions.  Opening a link selects the rig, sets
the mode, tunes, then applies the bandwidth: a mode change brings its
own default bandwidth with it, so an explicit bw has to land after it.
Frequencies are read the way someone writes them by hand (7074k,
14.074M) and written back as whole Hz, so what comes out of the address
bar is the same link in canonical form.

After that the address bar keeps up with the dial, which is what makes
it copyable at any moment rather than only at load.  It is rewritten
with replaceState -- tuning is not navigation, and a swept dial would
otherwise bury the back button.  A link button in the top bar copies
the current link; it folds into the overflow menu when the bar is tight.

Applying a link changes the radio, so an rx session says so instead of
failing control calls one at a time.  A tab listening to a virtual
channel leaves the address alone rather than publishing a frequency the
rig is not on, and bw is skipped in both directions on rigs without
filter control, which would only refuse it.

The fixture pinned every state frame to 100 MHz plus jitter to keep
frames distinct, so no test could observe tuning at all.  The jitter
moves to the S-meter and the fixture echoes set_freq/set_mode/
set_bandwidth, as it already did for squelch.

Signed-off-by: Stan Grams <sjg@haxx.space>
sjg merged commit a2c630a92b into main 2026-08-05 22:36:31 +02:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sjg/trx-rs#42