# Contributing to AetherSDR 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](GOVERNANCE.md) for project roles, decision-making, and the RFC process for significant changes. --- ## Quick Start 1. Browse [open issues](https://github.com/aethersdr/AetherSDR/issues) — issues labeled `good first issue` are great starting points. 2. Fork the repo and create a feature branch from `main`. 3. Implement the fix or feature (one issue per PR). 4. Open a pull request referencing the issue number (`Fixes #42`). --- ## Reporting Bugs - Use the **lightbulb button** in AetherSDR's title bar for AI-assisted bug reports, or open a [GitHub issue](https://github.com/aethersdr/AetherSDR/issues/new) 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. ## Suggesting Features - 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. --- ## Submitting Code **Development tool:** AetherSDR is developed using [Claude Code](https://claude.com/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. 1. **Fork the repo** and create a feature branch from `main`. 2. **Read the [AetherSDR Constitution](CONSTITUTION.md).** (Canonical source: [`.specify/memory/constitution.md`](.specify/memory/constitution.md); the root [`CONSTITUTION.md`](CONSTITUTION.md) is 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](https://github.com/CiscoDevNet/foundry-security-spec/blob/main/constitution.md) (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 or `Principle X.` for verified-base patch generation). 3. **One issue per PR.** Keep changes focused and reviewable. 4. **Follow the coding conventions** below. 5. **Test your changes** against a real FlexRadio if possible. 6. **Sign your commits** (required by branch protection — SSH or GPG; see [Commit Signing](#commit-signing) below). 7. **Open a pull request** against `main` with a clear description. --- ## Project Architecture The full architecture is documented in [CLAUDE.md](CLAUDE.md) including the complete file tree, data pipelines, thread architecture (12 threads), protocol specification, and implementation patterns. Read it before making changes. ### Key Patterns - **Model → Radio**: Model setters emit `commandReady(cmd)` → `RadioModel` sends to radio via TCP. - **Radio → Model**: Status messages (`S` lines) → `RadioModel::onStatusReceived()` → routes to model's `applyStatus()`. - **Model → GUI**: Models emit signals → GUI widgets update via slots. - **GUI → Model**: GUI widgets call model setters. Use `QSignalBlocker` or `m_updatingFromModel` guards to prevent echo loops. - **Settings**: Use `AppSettings`, **never** `QSettings`. Keys are PascalCase. Booleans are `"True"` / `"False"` strings. - **Radio-authoritative**: Never persist or override settings the radio manages (frequency, mode, filter, step size, AGC, antennas, TX power). ### Working in MainWindow `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`](docs/architecture/mainwindow-decomposition.md) — read it before touching anything named `MainWindow*`. ### Thread Architecture | 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. ### Multi-Flex (Multi-Client) Safety 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. --- ## SmartSDR Protocol Reference 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\|\n` | | `R` | Radio→Client | Response: `R\|\|` | | `S` | Radio→Client | Status: `S\| key=val ...` | | `M` | Radio→Client | Informational message | ### FlexLib Reference 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`. --- ## Coding Conventions ### C++ Style - **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. - **`QSignalBlocker`** to prevent feedback loops. - **Keep classes small** and single-responsibility. ### Naming - Classes: `PascalCase` (`SliceModel`, `SpectrumWidget`) - Methods: `camelCase` (`setFrequency()`, `applyStatus()`) - Members: `m_camelCase` (`m_frequency`, `m_sliceId`) - Signals: past tense (`frequencyChanged`, `commandReady`) - AppSettings keys: `PascalCase` (`LastConnectedRadioSerial`) ### Widget Guidelines - All GUI follows the dark theme: `#0f0f1a` background, `#c8d8e8` text, `#00b4d8` accent, `#203040` borders. - Use `GuardedSlider` (from `GuardedSlider.h`) instead of `QSlider` — it prevents wheel events from leaking to parent widgets. - Use `GuardedComboBox` for combo boxes in scrollable areas. - Disable `autoDefault` on QPushButtons inside QDialogs. ### Optional Dependencies 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. ### Commit Messages - Imperative mood: "Add band stacking" not "Added band stacking". - First line under 72 characters. - Reference issues: `Fixes #42` or `Closes #42`. ### Commit Signing 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`](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. #### Quick reference (SSH signing, the simple path) ```bash # 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`](docs/COMMIT-SIGNING.md). --- ## Reviews and merging PR review responsibility is divided into three tiers via [`.github/CODEOWNERS`](.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 | |---|---|---| | **Default** | Everything not listed below | @ten9876, @jensenpat | | **Mechanical / safe** | `tests/`, `docs/`, `*.md`, `.github/dependabot.yml`, `.github/docker/`, `.github/ISSUE_TEMPLATE/` | @ten9876, @jensenpat, @AetherClaude | | **Maintainer-only** | `src/gui/MainWindow.{h,cpp}`, `src/core/RadioModel.{h,cpp}`, `src/core/AudioEngine.{h,cpp}`, `src/core/PanadapterStream.{h,cpp}`, `CMakeLists.txt`, `CLAUDE.md`, `CONTRIBUTING.md`, `.github/CODEOWNERS`, `.github/workflows/` | @ten9876 | The maintainer-only tier covers *direction-impacting* paths: visual/UX, threading and central-state architecture, protocol bedrock, build configuration, and project policy. Per [CLAUDE.md](CLAUDE.md#autonomous-agent-boundaries), changes here need maintainer eyes regardless of who wrote them. Note the boundary inside `src/gui/`: only the core **`MainWindow.{h,cpp}`** is maintainer-only. The **`MainWindow_*.cpp` sibling TUs** from the #3351 decomposition (`MainWindow_DigitalModes.cpp`, `MainWindow_Wiring.cpp`, `MainWindow_Controllers.cpp`, etc.) are **deliberately at the Default reviewer tier** — widening review/approval of extracted feature code to more of the team was a primary goal of the decomposition, so feature work that lands in a sibling TU does not bottleneck on a maintainer. The mechanical tier exists so the @AetherClaude bot can land low-risk changes (test additions, documentation tweaks, dependency bumps, template updates) without queueing on human review. ### Draft PR conventions 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. ### Stale-branch policy 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 `main` runs on every commit (see [`.github/workflows/ci.yml`](.github/workflows/ci.yml) and [`.github/workflows/codeql.yml`](.github/workflows/codeql.yml)), so semantic conflicts that slip through three-way merge are caught on the merged result within ~10 min. ### Recovering from a red main If post-merge CI on `main` fails after a merge: 1. Check the failing workflow run linked from the email/GitHub notification. Identify the offending merge commit. 2. **Prefer fix-forward** if the issue is small (one or two file edits): open a normal PR titled `fix(ci): repair main after ` and let it land through the usual flow. 3. **Use revert** if fix-forward isn't obvious or the regression is broad: `git revert -m 1 ` on a new branch, push, open a PR, merge. Never force-push `main`. 4. For agent-authored regressions: re-open the source issue, remove the `aetherclaude-eligible` label, then re-add it. The orchestrator's State Override C (`failed` → `implement` re-entry) creates a fresh worktree from current `main` and retries the implementation. ## What We Will Not Accept - **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](GOVERNANCE.md). --- ## AI-Assisted Feature Requests **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. ### What makes a good request - **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.** --- ## Notes for AI Agents Read [CLAUDE.md](CLAUDE.md) first — it is the authoritative project context. ### Quick reference | 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 | ### AI-to-AI coordination 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. --- ## Code of Conduct Be respectful, constructive, and patient. Ham radio has a long tradition of helping each other learn — bring that spirit here. 73 de KK7GWY ## License By contributing to AetherSDR, you agree that your contributions will be licensed under the [GNU General Public License v3.0](LICENSE).