Skip to content

feat(websdr): add optional listen-only WebSDR receive module#3612

Closed
svabi79 wants to merge 1 commit into
aethersdr:mainfrom
svabi79:feat/websdr-module
Closed

feat(websdr): add optional listen-only WebSDR receive module#3612
svabi79 wants to merge 1 commit into
aethersdr:mainfrom
svabi79:feat/websdr-module

Conversation

@svabi79

@svabi79 svabi79 commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds an optional, passive WebSDR (PA3FWM) receive module: the operator can
listen on a remote, RF-quiet receiver alongside the connected Flex radio. Many
amateurs have an RF-noisy home (switch-mode PSUs, PLC, LED drivers, chargers),
so a clean remote ear next to the local rig is genuinely useful — see the
linked issue for the motivation.

The Flex stays the authority/master. The module only reads the Flex model
(for optional slice-follow) and never transmits — WebSDR audio is never
routed to the radio.

Highlights:

  • WebSdrSource — QWebSocket worker on its own thread (audio ~~stream +
    waterfall ~~waterstream), ~~param tuning, band auto-select from
    bandinfo.js, mono→24 kHz stereo via Resampler, reconnect backoff.
  • WebSdrAudioDecoder / WebSdrWaterfallDecoder — decoders for the served
    client's wire format (a-law + adaptive predictor; format-1 waterfall +
    palette). Audio decoder verified bit-exact vs a reference implementation.
  • WebSdrPanel — dockable panel (host/freq/Connect, "WebSDR Audio" toggle,
    per-slice follow buttons in slice colours), self-rendered mini-waterfall
    in its own QWidget (not the GPU SpectrumWidget), with frequency scale,
    listening marker + passband, guard-band cropping, click-to-tune.
  • AudioEngine — RX source gate (Flex ↔ WebSDR), never sent to the radio.
  • Builds under HAVE_WEBSOCKETS (reuses Qt6::WebSockets); no new dependency.

Closes #3613

Process note: opening PR-first per the de-facto practice (feature_request

Constitution principle honored

  • Principle IV — Clean-room. All protocol knowledge is derived from the
    JavaScript the WebSDR server serves to every browser and from on-the-wire
    behaviour. Nothing is decompiled, disassembled, or taken from a proprietary
    binary.
  • Principle V — Each feature owns its configuration as one object. All
    panel settings are a single nested-JSON blob under one AppSettings key
    (WebSdr), regenerated and written atomically.

Test plan

  • Local build passes (cmake --build build, MSVC/Qt 6.8.3)
  • Behavior verified on a real radio (audio switch + per-slice follow tested
    live against a connected Flex)
  • Existing tests pass (CI)
  • Reproduction steps documented if user-reported bug (n/a — new feature)

Checklist

  • Commits are signed (docs/COMMIT-SIGNING.md)
  • No new flat-key AppSettings calls — nested-JSON-under-one-key (Principle V)
  • Code is clean-room — not decompiled, disassembled, or reverse-engineered
    from a proprietary binary (Principle IV)
  • All meter UI uses MeterSmoother (no meter UI added — the raw S-meter
    readout was intentionally dropped)
  • Documentation updated (docs/architecture/websdr-module*.md)
  • Security-sensitive changes reference a GHSA if applicable (n/a)

Adds an optional, passive WebSDR (PA3FWM) receive module so the operator can
listen on a remote, RF-quiet receiver alongside the connected Flex radio. The
Flex stays the authority/master; the module only reads (it never writes the
Flex model and never transmits).

- WebSdrSource: QWebSocket worker (own thread) — audio (~~stream) + waterfall
  (~~waterstream), ~~param tuning, band auto-select from bandinfo.js, mono->24k
  stereo via Resampler, reconnect backoff.
- WebSdrAudioDecoder / WebSdrWaterfallDecoder: clean-room decoders for the
  served JavaScript client's wire format (a-law + adaptive predictor; format-1
  waterfall + palette). Audio decoder verified bit-exact vs a reference impl.
- WebSdrPanel: dockable panel (host/freq/Connect, "WebSDR Audio" toggle,
  per-slice follow buttons in slice colours), self-rendered mini-waterfall
  (own QWidget, not the GPU SpectrumWidget) with frequency scale, listening
  marker + passband, guard-band cropping, click-to-tune.
- WebSdrWaterfallView: QAccessibleInterface for the paintEvent widget.
- AudioEngine: RX source gate (Flex <-> WebSDR), never routed to the radio.
- Settings as a single nested-JSON object under one key (Principle V).
- Themed via ThemeManager tokens + SliceColorManager; applet-style layout.
- Builds under HAVE_WEBSOCKETS (reuses Qt6::WebSockets); no new dependency.

Provenance is clean-room (Principle IV): derived from the JavaScript the WebSDR
server serves to every browser and from on-the-wire behaviour — no decompiled,
disassembled, or proprietary-binary sources.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@svabi79 svabi79 force-pushed the feat/websdr-module branch from 040eb9f to b1c727f Compare June 15, 2026 18:05

@ten9876 ten9876 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this, Jan — genuinely nice work. The isolation discipline here is exactly right, and a clean remote "ear" next to a noisy shack is a feature I'd personally use. Before anything else I want to flag a licensing question that I think needs sorting out before we can merge, because it's the one thing standing between this and a yes. Everything below is meant in good faith — happy to work through it together.

🟢 What's great

  • The AudioEngine integration is the highlight. The one non-trivial edit to existing code is a minimal, atomic m_rxSourceWebSdr gate (feedAudioDataImpl) — Flex stays master/default, the Flex RX path is byte-for-byte unchanged when WebSDR is idle, and WebSDR audio can never reach TX. That's precisely the "passive bolt-on" shape we'd want.
  • Cross-platform & no new dependency — pure Qt/QWebSockets, reusing the existing HAVE_WEBSOCKETS block.
  • Principle V compliant — config is a single nested-JSON blob under one WebSdr key, regenerated atomically.
  • The self-rendered mini-waterfall correctly stays off the QRhi SpectrumWidget hot path — its own QWidget/QPainter, own data. Good instinct (and relevant to the panadapter perf work in #3617).
  • Good public-server etiquette: user-initiated connect, exponential reconnect backoff, single sockets per server, self-identifies as name=aethersdr. The guard-band crop + click-to-tune in WebSdrWaterfallView is a nice touch.

🔴 The blocker: licensing / clean-room

This is the one I need help resolving. The PR checklist ticks Principle IV (clean-room), but the decoder source headers describe themselves as a direct port of PA3FWM's copyrighted client JS:

  • WebSdrAudioDecoder.h"Direct C++ port of the server's own websdr-sound.js onmessage handler (Copyright 2007-2018 P.T. de Boer, pa3fwm@websdr.org)."
  • WebSdrAudioDecoder.cpp"256-entry a-law decode table, verbatim from websdr-sound.js."
  • WebSdrWaterfallDecoder.cpp"Verbatim from websdr-waterfall.js."

A line-by-line translation to another language is still a derivative work under copyright — the decoder even carries the minified JS's own identifiers (m_ie/m_re/m_oe/m_ae/m_ee, kC), which makes the lineage clear. The concern is concrete:

  1. The WebSDR software is proprietary. Per the WebSDR FAQ, it's distributed privately by email to server operators ("I distribute it (without cost) via e-mail…"), and the served websdr-sound.js carries an explicit © P.T. de Boer notice with no open-source license or reuse grant.
  2. AetherSDR is GPLv3. We can't relicense someone else's unlicensed copyrighted code as GPL-3.0 (GPLv3 §5). So as written, the ported decoders aren't something we can ship.

A couple of fair mitigations worth noting: the a-law table is just ITU-T G.711 standard values (uncopyrightable facts), and the wire protocol itself — frame-type bytes, ~~param/~~waterstream URLs, bandinfo.js parsing — is interoperability information that's far more defensible to reimplement. It's specifically the adaptive-predictor decoder and the palette-formula port that are the issue.

Three ways forward, your call:

  • (A) Permission/relicense from PA3FWM (pa3fwm@websdr.org) for the ported code, recorded in-repo — cleanest if he's amenable. Happy to help draft that note.
  • (B) A genuine clean-room reimplementation — a behavioral spec written from observed wire bytes, implemented by someone who hasn't read the JS.
  • (C) Ship only the defensible protocol/tuning parts and drop or rework the codec port.

🟡 A few smaller things

  • Your own design doc says this is fork-local. websdr-module.md §10: "not assumed to be upstream-bound; it ships in the svabi79 fork as an optional module." This PR targets main — worth deciding which it is, since upstreaming raises the licensing bar above.
  • RFC gate. Constitution v2.0.0 (#3602) merged today, and its RFC requirement now applies to a new UX module like this. You already offered to file one — I think that's the right next step, and it's a good place to capture the licensing resolution too.
  • No tests in the diff, despite the spec's test plan and the bit-exact claim. The decoders are pure and easily unit-testable; if we land a clean-room version, golden-vector tests would be a great fit. (Note the "bit-exact vs reference" result is itself evidence of derivation rather than independence — something to be mindful of for a clean-room path.)
  • Shutdown is a little fragileMainWindow_WebSdr.cpp stops the worker thread from this's own QObject::destroyed lambda; accessing members during destruction is risky. A quit()/wait() in ~MainWindow/closeEvent would be safer.

Summary

The architecture and the Flex-stays-master discipline are honestly really well done — if we can get the codec licensing onto solid ground (permission, clean-room, or trimming to the protocol parts) plus an RFC, I'd be glad to see this move forward. Marking request-changes purely on the licensing item; the rest is feedback, not gates. Thanks again, and let me know which path you'd like to take — happy to help with the PA3FWM note or scoping a clean-room split.

@ten9876

ten9876 commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

Following up on the licensing point with something more constructive — I went and read PA3FWM's FAQ to get a sense of where he actually draws the line on use of his work, and the news is encouraging for the direction this PR wants to go, even if the current implementation needs reworking.

Two separate axes — worth keeping distinct:

  1. Using the service (connecting to the public WebSDRs, pulling the server list, listening to streams)
  2. Copying his client code (the websdr-sound.js / websdr-waterfall.js port — the original blocker)

The FAQ speaks to axis 1, and his line there is misrepresentation, not access. The one place he gets firm:

"Can I include someone's WebSDR site in a frame on my website, so that it looks as if this WebSDR is mine?""do you feel doing this is fair? When I notice people doing this with one of my sites, I take action against it."

The trigger is "as if this WebSDR is mine" — passing his work off as yours — not consuming the streams through other software. On the things this module actually does, he's affirmatively permissive:

  • Reusing the server list: "Can I put a list of links to WebSDR servers on my own website? You can, of course… your list will quickly become outdated… the list on websdr.org is updated automatically." He says yes; his only gripe is that static copies go stale. Fetching the live directory at runtime literally solves the thing he complains about.
  • Feeding the audio into other software: for PSK31/RTTY he explicitly tells people to "feed the received audio from the WebSDR web page to a separate program." He actively encourages WebSDR audio being consumed by other apps.
  • Deep-linking to a frequency (the ?tune= URL param) exists because he wants people pointing others at streams.

So a listen-only, fetch-the-public-directory, connect-as-a-client module is well within the spirit of his FAQ — with two conditions that fall straight out of the line he draws:

  • Attribute prominently. Name the specific WebSDR and its operator in the panel ("powered by WebSDR — PA3FWM," ideally linking back). An in-app client bypasses his web UI entirely, which is a half-step past anything the FAQ explicitly covers; clear attribution is what keeps it on the "fair" side and avoids any "looks like it's AetherSDR's own receiver" reading.
  • Stay a good citizen on load — which this PR already does (single sockets, exponential backoff, name=aethersdr identifier).

What this means for the PR: the service-use posture is fine and the design instinct is right. The remaining blocker is purely axis 2 — the decoder code provenance. The clean path is the one we discussed: reverse-engineer the protocol/codec from pcaps of a browser session into a genuinely independent implementation (interoperability is well-trodden ground), with a fresh pair of hands that hasn't read the JS for the stateful audio codec specifically — or simply ask PA3FWM for permission/blessing.

Given the feature bypasses his UI, a single courtesy email actually closes both axes at once — something like: "I'm adding an in-app, listen-only client to AetherSDR that pulls the public websdr.org directory and connects to the listed receivers, with clear on-screen attribution to each WebSDR and operator — any concerns? And would you be open to permitting/blessing the audio decoder?" He's shown he values fairness and attribution over control, so that's a person likely to say yes — and it turns "defensible if challenged" into "he said go ahead." Happy to help draft it.

None of this changes the request-changes status (the codec port still needs resolving), but I wanted to put the encouraging part on the record: the thing you're trying to build — a WebSDR "slice" that lives in AetherSDR instead of a browser tab — is consistent with how PA3FWM says he wants his work used, as long as we credit it clearly and clean up the decoder's provenance.

@ten9876

ten9876 commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator

Jan — thank you for this. I want to be clear up front that the engineering here is genuinely good: the "Flex stays master" discipline, the atomic AudioEngine source gate, the off-the-GPU-hot-path mini-waterfall, the cross-platform Qt-only approach. The idea and the integration design are sound, and they're going to live on (more on that below). The problem is narrow, but it's a hard blocker, and after thinking it through carefully I'm going to close this PR rather than leave it open in limbo. Here's the honest reasoning.

Why it can't merge as-is. The decoders describe themselves, in their own headers, as a direct C++ port of PA3FWM's copyrighted client JavaScript ("Direct C++ port of … websdr-sound.js (Copyright 2007-2018 P.T. de Boer)", "verbatim from websdr-sound.js", "verbatim from websdr-waterfall.js"). A line-by-line translation is a derivative work regardless of language, and AetherSDR is GPLv3 — we can't relicense someone else's unlicensed code as GPL-3.0. So this isn't a style nit; it's a copyright issue we can't merge through.

Why this specific PR can't simply pivot to a "clean-room" version. Clean-room only works if the person writing the code has never seen the original source. Because the implementation here was derived directly from PA3FWM's JS, both the code and — unavoidably — the author are "tainted" for clean-room purposes. That's not a knock on you at all; it's just how the clean-room standard works. It means the path that revives this PR is narrow and specific:

The only way forward for this implementation is explicit written permission from PA3FWM (pa3fwm@websdr.org) to use/relicense the ported decoder under a GPL-compatible license, recorded in the repo. Given he distributes the WebSDR software for free to operators and his FAQ shows he values fair attribution over control, a polite ask may well succeed — and if you get that written agreement, this PR goes from "can't merge" to "welcome back," and we'd happily pick it up again. I'm glad to help you draft that email if you'd like.

The feature isn't dead. Separately, we're standing up a clean-room initiative to build the WebSDR client from scratch — a contributor who has not seen the original source, working only from their own packet captures and published standards (ITU-T G.711, etc.), with a documented provenance wall. Your architectural work directly informed it: the integration design (the per-slice source model, the RX-antenna-menu UX, the aux-source mixer, and how WebSDR audio flows through the client DSP chain) is captured in docs/architecture/websdr-sourced-slice.md and is the blueprint that effort builds against. So the shape you designed carries forward even though the decoder has to be re-derived independently.

To keep the clean-room wall intact, that contributor won't read this branch — but that's a hygiene precaution about the decoder, not a reflection on the integration work, which was solid.

Closing this one out. Two genuinely good outcomes from here: (a) you get PA3FWM's written blessing and we revive this, or (b) the clean-room build lands and you've shaped its design. Either way the WebSDR-in-AetherSDR idea you championed is moving forward. Thanks again — and seriously, the integration instincts here were right.

@ten9876 ten9876 closed this Jun 16, 2026
ten9876 added a commit that referenced this pull request Jun 16, 2026
Adds `docs/architecture/websdr-sourced-slice.md` — the design for a
VFO/slice
that takes audio from a WebSDR feed instead of the radio's VITA-49
stream.

Covers:
- The per-slice source model, and why `SliceModel` can't be made virtual
- The RX-antenna-menu UX (grouped "Remote → WebSDR…", relocate/grey
radio controls)
- The aux-source N-source mixer (generalizing the RADE mix)
- The client DSP routing split (per-source vs master bus) for
AetherDSP/AetherVoice
- Phased plan, prerequisites (licensing, cross-platform), key code refs

Grounded in current code with file:line references; indexed in
`docs/architecture/README.md`. Intentionally **codec-free /
clean-room-safe** —
no WebSDR protocol or codec expression — so the separate clean-room
initiative
may read it. Supersedes the closed PR #3612 as the integration
blueprint.

Docs-only; no code changes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@svabi79

svabi79 commented Jun 16, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, @ten9876 — this is a genuinely fair and generous review, and I appreciate both the close read and the care you took writing it up.

You're right, and I'll own it plainly: the audio/waterfall decoders are a direct port of PA3FWM's copyrighted client JS — the headers say so in as many words — so ticking the clean-room box was simply wrong of me. A line-by-line translation is a derivative work, and the "bit-exact vs. reference" result is evidence of derivation, not independence. No argument from me on the blocker.

I'm taking path (A): I've drafted a request to Pieter-Tjerk (pa3fwm@websdr.org) asking (1) whether he has any concerns with an in-app, listen-only client that fetches the public directory and clearly attributes each WebSDR + operator on screen, and (2) whether he'd grant permission to use the ported decoder under a GPL-compatible license, with full credit retained. I'll report back here with his answer.

To be clear about the two outcomes, so nobody's left waiting on me:

  • If he grants permission, I'd be glad to revive this and take it further.
  • If he declines — entirely his right — I'll step away from the WebSDR feature completely and leave it to the clean-room effort. I won't keep circling back on it; it's yours and the clean-room contributor's at that point. The outcome I care about is the feature existing, and that path gets it there without me.

On the design you captured in docs/architecture/websdr-sourced-slice.md: for what it's worth, I think your RX-antenna-menu framing is better than my standalone panel. "Where does this slice's received signal come from?" is exactly the right question, antenna selection is inherently exclusive, and the v1 per-slice source-override is a cleaner home for this than a separate dock — a good target for whoever carries it (me only if the license clears, the clean-room build otherwise). And good call backing away from drawing the WebSDR onto the Flex panadapter — independent frequency axes; my panel hit the same wall and rendered its own mini-waterfall for the same reason.

One small thing for the clean-room contributor's benefit: the untainted pieces here are reusable as-is — the AudioEngine source gate (feedAudioDataImpl), the follow-slice sync, the panel/waterfall rendering, the ~~param/bandinfo.js interop. It's specifically the stateful audio codec + palette port that has to be re-derived independently.

Closing it out on my end too, then — thanks again for the thoughtful review and for keeping the design alive. I'll follow up once I hear from PA3FWM.

73, Jan / HB9HSJ

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

WebSDR as an optional remote, RF-quiet 'antenna' for receive

2 participants