Thanks for your interest in AetherSDR! We're building a native SmartSDR client for FlexRadio on Linux, macOS, and Windows. Community contributions are welcome.
See GOVERNANCE.md for project roles, decision-making, and the RFC process for significant changes.
- Browse open issues —
issues labeled
good first issueare great starting points. - Fork the repo and create a feature branch from
main. - Implement the fix or feature (one issue per PR).
- Open a pull request referencing the issue number (
Fixes #42).
- Use the lightbulb button in AetherSDR's title bar for AI-assisted bug reports, or open a GitHub issue directly.
- Include: OS/distro, AetherSDR version, radio model, firmware version.
- Attach logs (
~/.config/AetherSDR/aethersdr.log) or use Help → Support → Send to Support. - Check existing issues first to avoid duplicates.
- Open a GitHub issue or use the lightbulb button for an AI-assisted feature request.
- Describe the problem you're solving, not just the solution.
- Reference SmartSDR behavior where applicable — screenshots help.
- One feature per issue.
Development tool: AetherSDR is developed using Claude Code
as the primary development environment. We strongly encourage all contributors to use
Claude Code — it has full codebase context via CLAUDE.md and naturally produces code
that matches our conventions.
- Fork the repo and create a feature branch from
main. - Read the AetherSDR Constitution. (Canonical
source:
.specify/memory/constitution.md; the rootCONSTITUTION.mdis a byte-identical mirror.) 14 principles total (constitution v2.0.0): 7 AetherSDR-specific (FlexLib authority, radio-authoritative live state, radio-persistable settings, clean-room contributions, per-feature config ownership, transmit-on-intent, boundary input validation) + 7 defensive engineering principles adopted from Cisco's Foundry Constitution (Evidence Over Assertion, Surface Only What Survives, Atomic Claims, Demonstrated Fixes, Infra Sandbox, Operator Outranks Agents, Atomic Persistence). Your PR's commit message will cite the principle it honors (e.g.Principle V.for nested-JSON persistence orPrinciple X.for verified-base patch generation). - One issue per PR. Keep changes focused and reviewable.
- Follow the coding conventions below.
- Test your changes against a real FlexRadio if possible.
- Sign your commits (required by branch protection — SSH or GPG; see Commit Signing below).
- Open a pull request against
mainwith a clear description.
The full architecture is documented in CLAUDE.md including the complete file tree, data pipelines, thread architecture (12 threads), protocol specification, and implementation patterns. Read it before making changes.
- Model → Radio: Model setters emit
commandReady(cmd)→RadioModelsends to radio via TCP. - Radio → Model: Status messages (
Slines) →RadioModel::onStatusReceived()→ routes to model'sapplyStatus(). - Model → GUI: Models emit signals → GUI widgets update via slots.
- GUI → Model: GUI widgets call model setters. Use
QSignalBlockerorm_updatingFromModelguards to prevent echo loops. - Settings: Use
AppSettings, neverQSettings. Keys are PascalCase. Booleans are"True"/"False"strings. - Radio-authoritative: Never persist or override settings the radio manages
(frequency, mode, filter, step size, AGC, squelch, DSP flags, antennas, TX
power, panadapter count and per-pan state — including FFT
average/FPS/weighted-average and waterfall line duration). Never write a
radio-echoed status value into a setter that also persists to
AppSettings(the recurring #4261 anti-pattern). SeeAGENTS.mdfor the full list.
MainWindow was a ~19,500-line monolith; #3351 decomposed it into one class
spread across MainWindow.cpp + a family of nine MainWindow_*.cpp sibling TUs
(controllers, menus, shortcuts, wiring, digital modes, SWR sweep, spots,
session, DSP applets). It's still one class — the siblings hold MainWindow::
method bodies.
Don't add new feature code to MainWindow.cpp. Put a feature's
lifecycle/handlers in the matching sibling TU, signal wiring in
MainWindow_Wiring.cpp, and reserve MainWindow.{h,cpp} for genuinely
cross-cutting code. The full TU map and a "where does my change go?" table are in
docs/architecture/mainwindow-decomposition.md
— read it before touching anything named MainWindow*.
| Thread | Components |
|---|---|
| Main | GUI rendering, RadioModel, all sub-models, user input |
| Connection | RadioConnection (TCP 4992 I/O) |
| Audio | AudioEngine (RX/TX audio; NR2/RN2/NR4/DFNR/BNR/MNR DSP) |
| Network | PanadapterStream (VITA-49 UDP parsing) |
| ExtControllers | FlexControl, MIDI, SerialPort |
| Spot | DX Cluster, RBN, WSJT-X, POTA, FreeDV clients |
Cross-thread communication uses auto-queued signals exclusively.
When another client (SmartSDR, Maestro) is connected, filter all status
updates and VITA-49 packets by client_handle. Do not process data from
other clients' slices or panadapters.
ASCII over TCP (port 4992) + VITA-49 binary over UDP.
| Prefix | Direction | Meaning |
|---|---|---|
V |
Radio→Client | Firmware version |
H |
Radio→Client | Client handle (hex) |
C |
Client→Radio | Command: C<seq>|<cmd>\n |
R |
Radio→Client | Response: R<seq>|<hex_code>|<body> |
S |
Radio→Client | Status: S<handle>|<object> key=val ... |
M |
Radio→Client | Informational message |
The FlexLib C# source at ~/build/FlexLib/ is the authoritative protocol
reference. Use it to understand behavior, but write clean-room C++ —
do not copy-paste.
Key files: Slice.cs, Radio.cs, Panadapter.cs, Transmit.cs,
Meter.cs, APD.cs, TNF.cs, CWX.cs, DVK.cs.
- C++20 / Qt6 — modern idioms (
std::ranges,auto, structured bindings). - RAII everywhere. No naked
new/delete. Use Qt parent-child ownership. - Qt signals/slots for cross-object communication.
QSignalBlockerto prevent feedback loops.- Keep classes small and single-responsibility.
- Classes:
PascalCase(SliceModel,SpectrumWidget) - Methods:
camelCase(setFrequency(),applyStatus()) - Members:
m_camelCase(m_frequency,m_sliceId) - Signals: past tense (
frequencyChanged,commandReady) - AppSettings keys:
PascalCase(LastConnectedRadioSerial)
- All GUI follows the dark theme:
#0f0f1abackground,#c8d8e8text,#00b4d8accent,#203040borders. - Use
GuardedSlider(fromGuardedSlider.h) instead ofQSlider— it prevents wheel events from leaking to parent widgets. - Use
GuardedComboBoxfor combo boxes in scrollable areas. - Disable
autoDefaulton QPushButtons inside QDialogs.
Features gated behind compile-time flags:
| Flag | Package | Feature |
|---|---|---|
HAVE_SERIALPORT |
Qt6::SerialPort |
FlexControl, serial PTT/CW |
HAVE_WEBSOCKETS |
Qt6::WebSockets |
FreeDV Reporter, TCI server |
HAVE_KEYCHAIN |
Qt6Keychain |
SmartLink credential persistence |
HAVE_MIDI |
Bundled RtMidi | MIDI controller mapping |
HAVE_RADE |
Bundled RADE/Opus | FreeDV digital voice |
HAVE_SPECBLEACH |
libspecbleach (clang-cl on Win) | NR4 spectral noise reduction |
HAVE_DFNR |
Bundled DeepFilterNet3 | DFNR neural noise reduction |
HAVE_BNR |
NVIDIA NIM container | GPU noise removal |
HAVE_MQTT |
Bundled libmosquitto | MQTT applet |
Use #ifdef HAVE_* guards. Features must degrade gracefully when unavailable.
- Imperative mood: "Add band stacking" not "Added band stacking".
- First line under 72 characters.
- Reference issues:
Fixes #42orCloses #42.
All commits to main must be signed (branch protection enforces this).
SSH and GPG signing are both supported; SSH signing is recommended
if you already push via SSH because it reuses your existing key.
Full setup guide: docs/COMMIT-SIGNING.md
— covers Windows, macOS, Linux, WSL, and Raspberry Pi OS, with both
SSH and GPG paths. The top of that doc has explicit AI-assistant
instructions, so if you'd rather have your AI coding assistant walk
you through setup, just tell it
"read docs/COMMIT-SIGNING.md and help me set up commit signing"
and it will follow the algorithm there.
# 1. Confirm or generate an SSH key
ls -la ~/.ssh/id_ed25519.pub || ssh-keygen -t ed25519 -C "you@example.com"
# 2. Configure git to sign with it
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git config --global commit.gpgsign true
git config --global tag.gpgsign true
git config --global user.email "you@example.com" # must match GitHub
# 3. Register the key on GitHub
cat ~/.ssh/id_ed25519.pub
# Paste at GitHub > Settings > SSH and GPG keys > New SSH key
# Set Key Type: "Signing Key" (NOT Authentication — that's a different
# role on the same key; you may need both entries for the same pubkey)
# 4. Verify
git commit --allow-empty -m "signing test"
git log --show-signature -1 # expect "Good \"git\" signature"For GPG, Windows-specific tweaks, Touch ID integration on macOS, or
troubleshooting "Unverified" badges, see
docs/COMMIT-SIGNING.md.
PR review responsibility is divided into three tiers via
.github/CODEOWNERS. Self-approval is blocked by
GitHub on every tier — your own PR always needs review from someone else.
| Tier | Paths | Who can approve |
|---|---|---|
| Source (Tier 3) | Everything not listed below — all of src/, including the whole of MainWindow |
@aethersdr/reviewers (@ten9876, @jensenpat, @NF0T, @rfoust, @chibondking) |
| Infrastructure (Tier 2) | tests/, docs/, *.md, CMakeLists.txt, the routine .github/workflows/, .github/dependabot.yml, .github/docker/, .github/ISSUE_TEMPLATE/ |
@aethersdr/infrastructure (@ten9876, @jensenpat) |
| Maintainer-only (Tier 1) | governance/security docs (CONSTITUTION.md, GOVERNANCE.md, CONTRIBUTING.md, SECURITY*, LICENSE, ROADMAP.md, CODE_OF_CONDUCT.md), .github/CODEOWNERS, .github/codeql/, the release signing/publish + CodeQL-scan workflows (sign-release.yml, codeql.yml, macos-dmg.yml, build-macos-qt.yml, windows-installer.yml, appimage.yml, docker-ci-image.yml, streamdeck-plugins.yml), and the AI-instruction files (AGENTS.md, CLAUDE.md, GEMINI.md, .claude/commands/) |
@aethersdr/maintainers (@ten9876) |
The maintainer-only tier covers governance and security-critical paths: project policy and governance docs, the CODEOWNERS file and CodeQL config, the release signing/publish pipeline, and the AI-instruction files. Per CLAUDE.md, changes here need maintainer eyes regardless of who wrote them. (The routine CI workflows and the build config sit at Tier 2 so day-to-day CI iteration isn't maintainer-gated; only the security-sensitive workflows are carved back to Tier 1.)
MainWindow is not maintainer-gated. With the #3351 decomposition
complete, the core MainWindow.{h,cpp} and its extracted MainWindow_*.cpp
sibling TUs (MainWindow_DigitalModes.cpp, MainWindow_Wiring.cpp,
MainWindow_Controllers.cpp, etc.) all sit at the Source (Tier 3) reviewer
tier, so the whole MainWindow surface shares the broad reviewer roster — a
primary goal of the decomposition was widening review of that code to the team.
Bot-opened PRs (e.g. @AetherClaude's) still require a human reviewer regardless of tier — the bot is intentionally not a code owner. The Infrastructure tier simply means low-risk changes (test additions, documentation tweaks, dependency bumps, template updates) need an infrastructure owner rather than a maintainer.
Draft status carries different meaning depending on who opened the PR:
- Human-authored draft — work-in-progress; reviewers should skip these until the author marks Ready for Review.
@AetherClaude/aethersdr-agent[bot]draft — auto-generated from an issue and awaiting human review. The draft state holds the PR back from auto-merge; it is not "WIP". Treat it like a ready-to-review PR for triage purposes.
Triage scripts and review agents should include bot drafts in their sweep and skip only human drafts.
We do not require PR branches to be up to date with main before
merging. The reasoning:
- Squash-merge already runs a fresh three-way merge against
main, so textual conflicts are caught at merge time regardless of branch age. - Forcing every PR to rebase after every other merge cost ~15–25 min of CI per stale PR per batch day, which adds up fast when AetherClaude is processing a queue of triaged issues.
- Post-merge CI on
mainruns on every commit (see.github/workflows/ci.ymland.github/workflows/codeql.yml), so semantic conflicts that slip through three-way merge are caught on the merged result within ~10 min.
If post-merge CI on main fails after a merge:
- Check the failing workflow run linked from the email/GitHub notification. Identify the offending merge commit.
- Prefer fix-forward if the issue is small (one or two file edits):
open a normal PR titled
fix(ci): repair main after <SHA>and let it land through the usual flow. - Use revert if fix-forward isn't obvious or the regression is
broad:
git revert -m 1 <merge-sha>on a new branch, push, open a PR, merge. Never force-pushmain. - For agent-authored regressions: re-open the source issue, remove
the
aetherclaude-eligiblelabel, then re-add it. The orchestrator's State Override C (failed→implementre-entry) creates a fresh worktree from currentmainand retries the implementation.
- Wine/Crossover workarounds. The goal is fully native.
- Copied proprietary code. Clean-room implementations from observed protocol behavior and FlexLib source are fine.
- Changes that break the core RX path. Test: discovery → connect → FFT display → audio output.
- Large reformatting PRs. Fix style only in files you're modifying.
- UX, visual, or architecture changes without an approved RFC. Open
a
[RFC]issue first — see GOVERNANCE.md.
You don't need to be a developer to contribute. Click the lightbulb button in AetherSDR's title bar — it copies a structured prompt to your clipboard and opens your choice of AI assistant. Describe your idea in plain English, and the AI generates a well-structured GitHub issue.
- Be specific. "Add a noise gate with adjustable threshold" not "better audio."
- Describe the problem. Tell us why, not just what.
- Reference SmartSDR. Screenshots of the Windows client are very helpful.
- One feature per issue.
Read CLAUDE.md first — it is the authoritative project context.
| Task | Start here |
|---|---|
| New slice property | SliceModel.h/.cpp — getter/setter/signal, parse in applyStatus() |
| New TX property | TransmitModel.h/.cpp — same pattern |
| New GUI control | RxApplet.cpp for patterns, VfoWidget.cpp for tab panels |
| New applet | Copy EqApplet as template, register in AppletPanel |
| New overlay sub-menu | SpectrumOverlayMenu.cpp — buildBandPanel() as template |
| New status object | RadioModel::onStatusReceived() — add routing |
| New meter display | MeterModel parses all meters — wire to a gauge |
| New Radio Setup tab | RadioSetupDialog.cpp — follow existing tab patterns |
| New spot source | DxClusterDialog.cpp — follow existing tab patterns |
| Protocol command | Check FlexLib for syntax, test with radio logs |
If your AI agent hits an issue requiring maintainer coordination, open a GitHub issue with: your analysis, relevant log output, code references, and proposed fix. The maintainer's Claude instance monitors issues and will respond.
Be respectful, constructive, and patient. Ham radio has a long tradition of helping each other learn — bring that spirit here.
73 de KK7GWY
By contributing to AetherSDR, you agree that your contributions will be licensed under the GNU General Public License v3.0.