summaryrefslogtreecommitdiff
path: root/stacks/crdop/docs
diff options
context:
space:
mode:
authorinfo@mode42.com <info@mode42.com>2026-08-07 18:23:28 +0000
committerinfo@mode42.com <info@mode42.com>2026-08-07 18:23:28 +0000
commitfa05a5f8238e9e1417235711656e5062ccc843a7 (patch)
tree46fbbd18de54492b59307c16efcc7f3152723238 /stacks/crdop/docs
Initial push
Diffstat (limited to 'stacks/crdop/docs')
-rw-r--r--stacks/crdop/docs/ACOUSTIC-TEST-PROTOCOL.md84
-rw-r--r--stacks/crdop/docs/AUDIO-ARCHITECTURE.md80
-rw-r--r--stacks/crdop/docs/BUILD.md99
-rw-r--r--stacks/crdop/docs/CHANGELOG.md25
-rw-r--r--stacks/crdop/docs/CONFIG.md50
-rw-r--r--stacks/crdop/docs/DEVELOPER.md188
-rw-r--r--stacks/crdop/docs/EXAMPLES.md91
-rw-r--r--stacks/crdop/docs/FEC-SPEC.md99
-rw-r--r--stacks/crdop/docs/G3RUH-DESIGN.md72
-rw-r--r--stacks/crdop/docs/HARDWARE-INTERFACE.md194
-rw-r--r--stacks/crdop/docs/HOST-PROTOCOL-SPEC.md103
-rw-r--r--stacks/crdop/docs/INDEX.md77
-rw-r--r--stacks/crdop/docs/LICENSE-USAGE.md116
-rw-r--r--stacks/crdop/docs/MAX25-USAGE.md292
-rw-r--r--stacks/crdop/docs/PROTOCOL.md36
-rw-r--r--stacks/crdop/docs/README.md31
-rw-r--r--stacks/crdop/docs/SOFTMODEM.md127
-rw-r--r--stacks/crdop/docs/SOUNDCARD-QUALIFICATION.md95
18 files changed, 1859 insertions, 0 deletions
diff --git a/stacks/crdop/docs/ACOUSTIC-TEST-PROTOCOL.md b/stacks/crdop/docs/ACOUSTIC-TEST-PROTOCOL.md
new file mode 100644
index 0000000..9656c2a
--- /dev/null
+++ b/stacks/crdop/docs/ACOUSTIC-TEST-PROTOCOL.md
@@ -0,0 +1,84 @@
+# Acoustic bench test protocol — P0 bench acceptance
+
+**Purpose:** Close CRDOP P0 bench without RF. All steps runnable offline in CI or on a dev host.
+
+**Related:** [HOST-PROTOCOL-SPEC.md](HOST-PROTOCOL-SPEC.md) · [DEVELOPER.md](DEVELOPER.md) · [ROADMAP.md](../ROADMAP.md) P0 bench.
+
+---
+
+## Prerequisites
+
+| Item | Requirement |
+|------|-------------|
+| Build | `MAX25_BUILD_CRDOP=ON`, `cmake --install` or source tree |
+| Python | 3.10+ with repo on `PYTHONPATH` |
+| ALSA | Optional for tests 3–4; loopback tests need no hardware |
+| PulseAudio | **Off** or bypassed (`no_pulse=yes` in INI) |
+
+---
+
+## Test matrix
+
+| ID | Name | RF | Pass criteria |
+|----|------|-----|---------------|
+| **T0** | Bell 202 round-trip | No | `test_bell202_line_code.py` green |
+| **T1** | HDLC + CRC | No | `hdlc_codec` round-trip in unit tests |
+| **T2** | Software loopback | No | `max25-signal-sniffer.py --loopback` decodes AX.25 UI |
+| **T3** | `acoustic_engine` self-test | No | `AcousticEngine.loopback_self_test()` returns True |
+| **T4** | `audio-dummyd` + host TCP | No | Connect :8515/:8516, `PROTOCOLMODE KISS` → `OK` |
+| **T5** | `max25d` audio-dummy backend | No | `test_audio_dummy_backend.py` green |
+| **T6** | `max25d` crdop-tcp backend | No | `test_crdop_backend.py` green |
+| **T7** | ALSA loopback cable | No* | Sniffer decodes tone from `aplay`/`arecord` pair |
+| **T8** | WAV fixture | No | Sniffer `--wav` decodes recorded Bell 202 capture |
+
+\*Requires `snd-aloop` or physical loopback interface.
+
+---
+
+## Commands (copy-paste)
+
+```bash
+cd MAX25-Stack
+./scripts/build.sh
+
+# T0–T1
+python3 -m pytest stacks/crdop/test_bell202_line_code.py -q
+python3 -m pytest stacks/daemon/test_crdop_backend.py stacks/daemon/test_audio_dummy_backend.py -q
+
+# T2
+python3 stacks/crdop/tools/max25-signal-sniffer.py --loopback
+
+# T4 (background)
+python3 stacks/crdop/tools/audio-dummyd.py --ctrl-port 8515 --data-port 8516 &
+sleep 1
+python3 -c "
+import socket
+c=socket.create_connection(('127.0.0.1',8515),timeout=2)
+c.sendall(b'PROTOCOLMODE KISS\n')
+print(c.recv(64))
+"
+```
+
+---
+
+## P0 bench exit gate
+
+**100 % P0 bench** when:
+
+- [x] T0, T1, T2, T5, T6 pass in `release-check.sh`
+- [ ] T3 documented green in CI log (or explicit skip reason)
+- [ ] T4 manual or scripted smoke in release-check
+- [ ] Host ports unified: launcher, `audio-dummyd`, `max25d` use [HOST-PROTOCOL-SPEC.md](HOST-PROTOCOL-SPEC.md) defaults
+
+P0 on-air starts only after P0 bench exit — see [docs/HARDWARE-ACCEPTANCE.md](../../docs/HARDWARE-ACCEPTANCE.md).
+
+---
+
+## Failure triage
+
+| Symptom | Check |
+|---------|-------|
+| Wrong tones | Mark 1200 Hz / Space 2200 Hz, sample rate 48000 |
+| CRC fail | `ax25_codec.ax25_crc` vs `hdlc_codec` |
+| TCP refuse | Port 8515 free; `audio-dummyd` running for host tests |
+| ALSA xrun | `period_frames`, cable levels — [SOUNDCARD-QUALIFICATION.md](SOUNDCARD-QUALIFICATION.md) |
diff --git a/stacks/crdop/docs/AUDIO-ARCHITECTURE.md b/stacks/crdop/docs/AUDIO-ARCHITECTURE.md
new file mode 100644
index 0000000..6320253
--- /dev/null
+++ b/stacks/crdop/docs/AUDIO-ARCHITECTURE.md
@@ -0,0 +1,80 @@
+# CRDOP audio architecture — kernel ALSA only
+
+**CRDOP** = stack acronym for **MAX25-SoftModem** (device id `soft-crdop`).
+
+MAX25-SoftModem (CRDOP) talks to the radio **through the sound hardware directly**. There is **no PulseAudio**, **no PipeWire default route**, and **no desktop sound server** in the path.
+
+## Stack layers
+
+```
+┌─────────────┐ M25/1 TCP ┌──────────────┐ host TCP ┌─────────────┐
+│ max25d │ ◄────────────► │ CRDOP modem │ ◄──────────► │ max25- │
+│ (stack) │ │ (DSP/AX.25) │ │ terminal │
+└─────────────┘ └──────┬───────┘ └─────────────┘
+ │
+ MAX25 sound-proxy
+ (buffer, timing, duplex)
+ │
+ ▼
+ libasound (userspace)
+ │
+ ▼
+ Linux kernel ALSA
+ (/dev/snd/*, drivers)
+ │
+ ▼
+ Sound hardware (USB/PCI codec)
+ │
+ ▼
+ Radio interface (line / acoustic)
+```
+
+| Layer | Role | Allowed |
+|-------|------|---------|
+| **max25d** | Device lifecycle, `CrdopTcpBackend`, no audio I/O | — |
+| **CRDOP modem** | AFSK encode/decode, AX.25 framing, duplex policy | — |
+| **MAX25 sound-proxy** | **Only** audio shim between modem and ALSA — period size, xrun recovery, hw params, PTT timing hooks | MAX25-owned |
+| **ALSA userspace** | `snd_pcm_*` on **`hw:` / `plughw:`** devices | Direct libasound |
+| **Kernel ALSA** | Driver, DMA, card registry | Required |
+| **PulseAudio / PipeWire** | — | **Forbidden** in production path |
+
+The sound-proxy is **not** a second network hop — it is the in-process (or co-process) module that owns ALSA opens and keeps modem timing off the desktop audio stack.
+
+## Why no PulseAudio
+
+PulseAudio and PipeWire insert mixing, resampling, and variable latency between applications and the kernel. CRDOP must:
+
+- Reproduce **exact mark/space frequencies** on playback
+- Sample capture with **stable phase** for demodulation
+- Meet **tighter deadlines** as baud rises toward 19200
+
+A userspace mixer breaks those guarantees. Operators must bind **card and device explicitly** (`hw:Card,Device` or tested `plughw:`).
+
+## Operator setup
+
+1. Identify hardware: `arecord -l` and `aplay -l` (kernel cards, not `pulse` pseudo-devices).
+2. Set INI `[audio] capture` and `playback` to **hardware ALSA names**.
+3. Ensure no session steals the device (`pasuspender` / stop PulseAudio on dedicated hosts).
+4. Verify loopback before on-air traffic.
+
+Example INI:
+
+```ini
+[audio]
+backend = alsa-kernel
+no_pulse = yes
+capture = hw:1,0
+playback = hw:1,0
+```
+
+Dedicated packet-radio hosts: prefer **single-purpose Linux** without a desktop sound daemon, or `PULSE_SERVER=` / `PIPEWIRE_RUNTIME_DIR=` unset for the `crdop` service unit.
+
+## Development (Eigenentwicklung)
+
+Native CRDOP will implement the sound-proxy in-tree (`include/crdop/sound_proxy.h`). Legacy optional vendor builds used `ALSASound.c` with the same rule: **open ALSA devices directly**.
+
+## See also
+
+- [SOFTMODEM.md](SOFTMODEM.md) — product scope, baud, duplex
+- [CONFIG.md](CONFIG.md) — INI keys
+- [BUILD.md](BUILD.md) — `libasound2-dev` dependency
diff --git a/stacks/crdop/docs/BUILD.md b/stacks/crdop/docs/BUILD.md
new file mode 100644
index 0000000..e72fac0
--- /dev/null
+++ b/stacks/crdop/docs/BUILD.md
@@ -0,0 +1,99 @@
+# Build
+
+Standalone CRDOP (**MAX25-SoftModem** — MAX25-SoftModem) — embedded `vendor/ardopcf/`, no submodules.
+
+## Requirements
+
+- CMake ≥ 3.16
+- **GCC** or **Clang / LLVM**
+- Platform libraries (see table below)
+- Optional tests: **cmocka** (Linux, *BSD)
+
+## Platform matrix (tested)
+
+| OS | Toolchain | Audio / libs | Status |
+|----|-----------|----------------|--------|
+| **Linux** | GCC, Clang | `libasound2-dev`, `pkg-config` | tested |
+| **FreeBSD** | GCC, Clang | `alsa-lib`, `pkgconf` | tested |
+| **OpenBSD / NetBSD** | GCC, Clang | `alsa-lib` if available | experimental |
+| **Windows** | MinGW-w64, MSVC | WinMM (built-in) | **untested** |
+| **macOS** | Clang (Xcode) | CoreAudio (built-in) | **untested** |
+
+Other operating systems are not tested.
+
+### Debian / Ubuntu
+
+```bash
+sudo apt install build-essential cmake libasound2-dev pkg-config
+sudo apt install libcmocka-dev # optional unit tests
+```
+
+### FreeBSD
+
+```bash
+pkg install cmake alsa-lib pkgconf cmocka
+```
+
+### macOS
+
+```bash
+xcode-select --install
+# or: brew install cmake
+```
+
+### Windows (MinGW-w64 on Linux host)
+
+```bash
+sudo apt install mingw-w64
+CRDOP_TOOLCHAIN=x86_64-w64-mingw32 ./scripts/build-crdop.sh
+# → build/crdopc.exe
+```
+
+Native Windows: Visual Studio 2022+ with CMake, or MSYS2 MinGW.
+
+## Build
+
+```bash
+./scripts/build-crdop.sh
+CC=clang ./scripts/build-crdop.sh
+./scripts/test-all.sh # smoke + cmocka (Unix)
+```
+
+Output: `build/crdopc` (Windows: `build/crdopc.exe`)
+
+## Install
+
+```bash
+./scripts/install-crdop.sh # → /usr/local
+CRDOP_PREFIX=$HOME/.local ./scripts/install-crdop.sh
+```
+
+Installs:
+
+| Path | Contents |
+|------|----------|
+| `bin/crdopc` | Modem binary |
+| `bin/crdop` | Profile launcher (`scripts/crdopc`) |
+| `share/crdop/` | INI examples, `VERSION` |
+
+Run: `crdop` or `CRDOP_INI=~/.config/crdop/crdop.ini crdop`
+
+## Cross-compile (Linux host)
+
+```bash
+CRDOP_TOOLCHAIN=aarch64-linux-gnu ./scripts/build-crdop.sh
+CRDOP_TOOLCHAIN=arm-linux-gnueabihf ./scripts/build-crdop.sh
+CRDOP_TOOLCHAIN=x86_64-w64-mingw32 ./scripts/build-crdop.sh
+```
+
+## Troubleshooting
+
+| Problem | Fix |
+|---------|-----|
+| `vendor/ardopcf missing` | Full repo checkout |
+| No ALSA (Linux/BSD) | Install dev package / `alsa-lib` port |
+| macOS build fails | Xcode CLI tools; CMake ≥ 3.16 |
+| Tests skipped | Install cmocka dev package |
+| Stale build | `rm -rf build && ./scripts/build-crdop.sh` |
+
+Vendor refresh (maintainers): `./scripts/refresh-vendor-ardopcf.sh`
diff --git a/stacks/crdop/docs/CHANGELOG.md b/stacks/crdop/docs/CHANGELOG.md
new file mode 100644
index 0000000..2bbfbf6
--- /dev/null
+++ b/stacks/crdop/docs/CHANGELOG.md
@@ -0,0 +1,25 @@
+# Changelog
+
+## [CUR999] — dev track (current)
+
+**CUR999** — internal dev track id for MAX25-SoftModem inside MAX25-Stack v1.0.0. Dev track `CUR999`; ships with MAX25-Stack v1.0.0.
+
+- Dev track id `CUR999` in `$SRC/stacks/crdop/VERSION`
+- Native modem DSP in development; optional legacy vendor build (`-DCRDOP_VENDOR_ARDOPCF=ON`); **ARDOP-plugin** documented separately
+- `max25d` `crdop-tcp` backend, INI scaffold, launcher, HyBBX attach examples
+
+## [0.5.0] — planned (not yet released)
+
+Target standalone CRDOP v0.5 after v1.0.0 stack ship.
+
+- Standalone CRDOP modem (`crdopc`) with native DSP
+- Embedded optional `vendor/ardopcf` (MIT) for dev-only legacy builds only
+- CB / dual / amateur profiles; Linux and *BSD tested
+
+## [0.1.0-l2-cb] — 2026-07-03
+
+Experimental CB profile bootstrap.
+
+## [0.0.0-bootstrap]
+
+Repository scaffold.
diff --git a/stacks/crdop/docs/CONFIG.md b/stacks/crdop/docs/CONFIG.md
new file mode 100644
index 0000000..821fd1c
--- /dev/null
+++ b/stacks/crdop/docs/CONFIG.md
@@ -0,0 +1,50 @@
+# Configuration
+
+`scripts/crdopc` reads INI → starts native `audio-dummyd` (M25/KISS host).
+
+## INI search order
+
+1. `$CRDOP_INI`
+2. `~/.config/crdop/crdop.ini`
+3. `share/crdop.ini.example`
+
+Templates: `share/crdop.ini.example` · `share/crdop-dual.ini.example` · `share/crdop-amateur.ini.example`
+
+## Keys
+
+### `[profile]`
+
+| Key | Default | Values |
+|-----|---------|--------|
+| `radio_profile` | `cb` | `cb` · `dual` · `amateur` |
+
+### `[modem]`
+
+| Key | Default | Notes |
+|-----|---------|-------|
+| `arq_bandwidth` | profile | `500MAX` (cb/dual) · `1000MAX` (amateur) |
+| `duplex` | `half` | `half` · `full` (`full` → `EXTRADELAY 0`) |
+| `extra_delay_ms` | _(auto)_ | Override delay; empty = profile default |
+
+### `[host]` · `[mycall]` · `[audio]`
+
+| Section | Key | Default | Notes |
+|---------|-----|---------|-------|
+| host | `port` | `8515` | TCP control |
+| mycall | `call` | `NOCALL-0` | On-air ID |
+| audio | `backend` | `alsa-kernel` | Kernel ALSA only — see [AUDIO-ARCHITECTURE.md](AUDIO-ARCHITECTURE.md) |
+| audio | `no_pulse` | `yes` | Reject PulseAudio/PipeWire pseudo devices |
+| audio | `capture` | _(required)_ | e.g. `hw:1,0` from `arecord -l` |
+| audio | `playback` | _(required)_ | e.g. `hw:1,0` from `aplay -l` |
+| audio | `sample_rate` | modem default | Hz |
+| audio | `period_frames` | auto | Buffer tuning |
+
+**No PulseAudio** in the production path. The **MAX25 sound-proxy** opens ALSA directly; `scripts/crdopc` clears Pulse/PipeWire session env when `no_pulse=yes`.
+
+## Environment
+
+| Variable | Purpose |
+|----------|---------|
+| `CRDOP_INI` | Config file path |
+
+Examples: [EXAMPLES.md](EXAMPLES.md)
diff --git a/stacks/crdop/docs/DEVELOPER.md b/stacks/crdop/docs/DEVELOPER.md
new file mode 100644
index 0000000..af43252
--- /dev/null
+++ b/stacks/crdop/docs/DEVELOPER.md
@@ -0,0 +1,188 @@
+# CRDOP developer guide
+
+**CRDOP** = **MAX25-SoftModem**. In-house GPLv3 sound-card modem (`stacks/crdop/`). Project rule: [docs/CRDOP.md](../../../docs/CRDOP.md).
+
+---
+
+## Source tree
+
+```
+stacks/crdop/
+├── lib/ # Native DSP (Python — leads validation)
+│ ├── bell202_line_code.py
+│ ├── afsk_modulator.py
+│ ├── afsk_demodulator.py
+│ ├── hdlc_codec.py
+│ ├── sound_proxy.py
+│ ├── m25_host_protocol.py
+│ ├── acoustic_engine.py
+│ └── test_bell202_line_code.py
+├── include/crdop/ # C API scaffold (future native binary)
+│ ├── sound_proxy.h
+│ └── version.h
+├── tools/
+│ ├── audio-dummyd.py # M25 host TCP daemon
+│ └── max25-signal-sniffer.py
+├── scripts/
+│ ├── crdopc # Launcher → audio-dummyd by default
+│ ├── build-crdop.sh
+│ └── install-crdop.sh
+├── share/ # crdop.ini.example (+ dual, amateur)
+├── cmake/ # Platform, vendor (dev-only), tests
+├── docs/ # This tree
+├── ROADMAP.md
+└── CMakeLists.txt
+```
+
+**Daemon integration:** `stacks/daemon/device_backends.py` → `CrdopTcpBackend` (`soft-crdop`), `AudioDummyBackend` (`audio-dummy`).
+
+**AX.25 codec:** `stacks/daemon/ax25_codec.py` — shared with on-air framing in `acoustic_engine.py`.
+
+---
+
+## Module map
+
+| Module | Layer | Responsibility |
+|--------|-------|----------------|
+| `bell202_line_code.py` | Line code | Bell 202 frequency-toggle: bit 0 → tone change; mark 1200 Hz / space 2200 Hz |
+| `afsk_modulator.py` | PHY TX | Continuous-phase AFSK PCM @ configurable sample rate (default 48 kHz) |
+| `afsk_demodulator.py` | PHY RX | Per-symbol Goertzel demod (1200 baud class) |
+| `hdlc_codec.py` | Framing | HDLC flags `0x7E`, bit-stuffing, CRC-16-CCITT |
+| `sound_proxy.py` | Audio I/O | ALSA capture/playback via `arecord`/`aplay`; buffer config |
+| `m25_host_protocol.py` | Host | TCP ctrl (:8515) + data (:8516); M25-family + KISS-semantics |
+| `acoustic_engine.py` | Bench | Full path: AX.25 UI → HDLC → AFSK → demod → parse; loopback self-test |
+
+**Host vs on-air:** `m25_host_protocol.py` carries AX.25 UI on the data port **without** HDLC wrapping. `acoustic_engine.encode_ax25_ui()` builds full on-air HDLC + AFSK for RF/audio output.
+
+**Not yet in `lib/`:** G3RUH scrambler/FSK (P1), 300 baud HF AFSK (P2), optional short-block FEC.
+
+---
+
+## Running tests
+
+### MAX25-Stack (recommended)
+
+```bash
+./scripts/build.sh
+cmake --build build --target max25_test # all offline tests
+cmake --build build --target max25_daemon_smoke # daemon + CRDOP subset
+```
+
+CRDOP-related tests in `max25_daemon_smoke`:
+
+| Test file | What it checks |
+|-----------|----------------|
+| `stacks/crdop/lib/test_bell202_line_code.py` | Bell 202 encode/decode round-trip |
+| `stacks/daemon/test_crdop_backend.py` | `CrdopTcpBackend` attach, TX (native M25/KISS) |
+| `stacks/daemon/test_audio_dummy_backend.py` | `audio-dummy` loopback backend |
+| `stacks/daemon/test_multi_device.py` | Multi-device registry incl. `soft-crdop` |
+
+### Direct (no CMake)
+
+```bash
+python3 stacks/crdop/lib/test_bell202_line_code.py
+python3 stacks/daemon/test_crdop_backend.py
+python3 stacks/daemon/test_audio_dummy_backend.py
+```
+
+### Sniffer loopback (no audio hardware)
+
+```bash
+python3 stacks/crdop/tools/max25-signal-sniffer.py --loopback
+# or after install:
+max25-signal-sniffer --loopback
+```
+
+Exit 0 when loopback decode produces AX.25 UI lines.
+
+### audio-dummyd bench
+
+```bash
+python3 stacks/crdop/tools/audio-dummyd.py --ctrl-port 8515 --data-port 8516
+# Terminal probe (separate shell):
+max25-terminal -H 127.0.0.1 -P 8515
+```
+
+### Standalone CRDOP vendor tests (dev-only)
+
+When built with `-DCRDOP_VENDOR_ARDOPCF=ON` and cmocka installed:
+
+```bash
+./scripts/build-crdop.sh
+CRDOP_BUILD_TESTS=ON ./scripts/test-all.sh
+```
+
+Vendor tree is **never** installed in MAX25 releases.
+
+---
+
+## Extending the modem
+
+### Add a modulation mode
+
+1. Add PHY module under `lib/` (e.g. `g3ruh_fsk.py`).
+2. Wire into `acoustic_engine.py` — select by `baud` / `modulation` parameter.
+3. Extend `crdop.ini` `[modem]` keys in `share/crdop.ini.example` and [CONFIG.md](CONFIG.md).
+4. Add unit tests beside the module (`test_*.py`).
+5. Update [ROADMAP.md](../ROADMAP.md) implementation status table.
+
+### Extend host protocol
+
+1. Edit `m25_host_protocol.py` — keep ctrl/data port split.
+2. Mirror changes in `stacks/daemon/device_backends.py` (`CrdopTcpBackend`).
+3. Document in [PROTOCOL.md](PROTOCOL.md).
+4. Add cases to `test_crdop_backend.py`.
+
+### C port path
+
+`include/crdop/sound_proxy.h` is the C ABI target. Python `lib/` leads until acoustic bench gates close; then port validated algorithms to native `crdopc` binary.
+
+---
+
+## Build modes
+
+| Mode | CMake flag | Output |
+|------|------------|--------|
+| **MAX25-Stack (default)** | `MAX25_BUILD_CRDOP=ON` | `bin/crdop`, `bin/audio-dummyd`, `bin/max25-signal-sniffer`, `share/crdop/` |
+| **MAX25 without CRDOP** | `-DMAX25_BUILD_CRDOP=OFF` | No CRDOP install artifacts |
+| **Standalone CRDOP** | `stacks/crdop/scripts/build-crdop.sh` | Same scaffold; optional vendor `crdopc` with `-DCRDOP_VENDOR_ARDOPCF=ON` |
+| **Dev vendor ARDOP** | `-DCRDOP_VENDOR_ARDOPCF=ON` | Legacy ardopcf binary — local only, never released |
+
+### Install paths (MAX25 `cmake --install`)
+
+| Artifact | Destination |
+|----------|-------------|
+| `crdop` (launcher) | `${CMAKE_INSTALL_PREFIX}/bin/` |
+| `audio-dummyd`, `max25-signal-sniffer` | `${CMAKE_INSTALL_PREFIX}/bin/` |
+| `crdop.ini.example`, `lib/*.py`, `VERSION` | `${CMAKE_INSTALL_PREFIX}/share/crdop/` |
+
+---
+
+## Dependencies
+
+| Component | Requirement |
+|-----------|-------------|
+| Runtime | Python 3, ALSA utils (`arecord`/`aplay`) on Linux |
+| Build | CMake ≥ 3.16 |
+| Audio | `libasound` — kernel ALSA direct opens only |
+| Tests | None beyond Python 3 (cmocka optional for vendor C tests) |
+
+---
+
+## Conventions
+
+- **English** for all shipped docs and user-facing strings.
+- **No PulseAudio / PipeWire** in production audio path.
+- **ARDOP** is a separate optional plugin — not part of CRDOP — see [plugins/external/ardop/README.md](../../../plugins/external/ardop/README.md).
+- Baud ceiling: **19200** (P1 G3RUH direct FSK). Above that: out of scope.
+
+---
+
+## Related
+
+| Doc | Topic |
+|-----|--------|
+| [MAX25-USAGE.md](MAX25-USAGE.md) | Operator workflow in MAX25-Stack |
+| [HARDWARE-INTERFACE.md](HARDWARE-INTERFACE.md) | Radio interface spec for builders |
+| [AUDIO-ARCHITECTURE.md](AUDIO-ARCHITECTURE.md) | Sound-proxy design |
+| [ROADMAP.md](../ROADMAP.md) | P0/P1/P2 milestones and module status |
diff --git a/stacks/crdop/docs/EXAMPLES.md b/stacks/crdop/docs/EXAMPLES.md
new file mode 100644
index 0000000..b6d32c5
--- /dev/null
+++ b/stacks/crdop/docs/EXAMPLES.md
@@ -0,0 +1,91 @@
+# Examples
+
+MAX25-Stack integration (plugins, `max25d`, terminal): [MAX25-USAGE.md](MAX25-USAGE.md).
+
+## Build
+
+```bash
+./scripts/build-crdop.sh
+CC=clang ./scripts/build-crdop.sh
+CRDOP_BUILD_TESTS=ON ./scripts/test-all.sh # needs libcmocka-dev
+```
+
+Cross-build (Linux host): see [BUILD.md](BUILD.md).
+
+## Run — CB (default, native M25 host)
+
+```bash
+./scripts/crdopc
+# or explicit example INI:
+CRDOP_INI=share/crdop.ini.example ./scripts/crdopc
+```
+
+This starts `audio-dummyd` with native M25/KISS host on TCP :8515/:8516.
+
+Copy config for daily use:
+
+```bash
+mkdir -p ~/.config/crdop
+cp share/crdop.ini.example ~/.config/crdop/crdop.ini
+$EDITOR ~/.config/crdop/crdop.ini
+./scripts/crdopc
+```
+
+## Run — dual (CB ↔ amateur)
+
+```bash
+CRDOP_INI=share/crdop-dual.ini.example ./scripts/crdopc
+```
+
+## Run — amateur (secondary)
+
+```bash
+CRDOP_INI=share/crdop-amateur.ini.example ./scripts/crdopc
+```
+
+## Full-duplex CB
+
+`share/crdop.ini.example` with:
+
+```ini
+[modem]
+duplex = full
+arq_bandwidth = 500MAX
+```
+
+## Custom call / ALSA devices
+
+```ini
+[mycall]
+call = CB01-0
+
+[audio]
+capture = plughw:1,0
+playback = plughw:1,0
+```
+
+Pass extra args after port (reserved for future native modem flags):
+
+```bash
+./scripts/crdopc 8515
+```
+
+## Direct audio-dummyd (no launcher)
+
+```bash
+./build/bin/audio-dummyd --ctrl-port 8515 --data-port 8516
+```
+
+## max25d integration
+
+```ini
+[devices]
+soft-crdop = crdop:default
+
+[device.soft-crdop]
+host = 127.0.0.1
+port = 8515
+listen = yes
+```
+
+See [../../docs/PLUGINS-DEVICE-MODEL.md](../../docs/PLUGINS-DEVICE-MODEL.md).
diff --git a/stacks/crdop/docs/FEC-SPEC.md b/stacks/crdop/docs/FEC-SPEC.md
new file mode 100644
index 0000000..53b1f93
--- /dev/null
+++ b/stacks/crdop/docs/FEC-SPEC.md
@@ -0,0 +1,99 @@
+# CRDOP FEC and duplex — implementation spec
+
+**Principle:** *Send more often with shorter frames rather than one long payload.*
+
+**Related:** [ROADMAP.md](../ROADMAP.md) FEC section · [docs/CRDOP.md](../../../docs/CRDOP.md)
+
+**Status:** MAX25-Stack **v1.0.0** / dev track `CUR999` — L1 optional FEC **planned**; operator-layer strategy **active now**.
+
+---
+
+## Layer model
+
+| Layer | Mechanism | v1.0.0 |
+|-------|-----------|--------|
+| **L3 Application** | Short beacons, staggered intervals | HyBBX / `max25d.ini` |
+| **L2 AX.25** | UI = no ARQ; I-frames optional ARQ via host | `ax25_codec.py` |
+| **L1 Modem** | CRC-16-CCITT mandatory; optional repeat/FEC blocks | `hdlc_codec.py` — CRC done |
+| **PHY** | Bell 202 AFSK (P0) | `afsk_*` |
+
+---
+
+## Half-duplex (CB default)
+
+| Parameter | Value | INI / host |
+|-----------|-------|------------|
+| Duplex | half | `[modem] duplex = half` |
+| CSMA persist | 255 | KISS `0x02` / CB profile |
+| Extra TX delay | 150 ms class | `extra_delay_ms` / `EXTRADELAY` |
+| Max UI info | **≤128 bytes** recommended | Application |
+| PTT | explicit | No VOX on CB |
+
+### Retransmission strategy (no L2 ARQ on UI)
+
+1. Prefer **N small identical beacons** over one large burst.
+2. Stagger dual-radio sites (`ax25_auto_interval` + offset).
+3. Optional L1: **2–3 repeats** of same short codeword vs one RS block >256 B.
+
+---
+
+## Full-duplex
+
+| Parameter | Value |
+|-----------|-------|
+| `duplex` | `full` |
+| `EXTRADELAY` | `0` |
+| FEC | Stronger codes allowed when bandwidth permits |
+| Audio | Full-duplex sound card; echo control required |
+
+---
+
+## Reference protocols (not on-air targets)
+
+| Protocol | Lesson for CRDOP |
+|----------|------------------|
+| **FX.25** | RS wrapper around AX.25; bit-stuffing hurts naive FEC |
+| **IL2P** | No bit-stuffing; packet-sync scramble — reference for future L2 |
+| **VARA DSP** | Multi-stage FEC — PHY reference only |
+
+---
+
+## Planned L1 FEC (post P0 on-air)
+
+| Requirement | Target |
+|-------------|--------|
+| Block size | ≤64 B payload + CRC per repeat unit |
+| Latency | <300 ms added on 1200 bd half-duplex |
+| Compatibility | Plain AX.25 receivers must not break (FX.25-style optional path) |
+| Test | AWGN + FM clip simulator before on-air |
+
+### INI keys (future)
+
+```ini
+[fec]
+enabled = no # default off v1.0.0
+repeat_count = 0 # 0 = no repeat; 2–3 for CB marginal
+profile = half # half | full
+```
+
+---
+
+## CB 1200 parameters (normative operator)
+
+| Parameter | Value |
+|-----------|-------|
+| Baud | 1200 |
+| Modulation | Bell 202 (1200/2200 Hz) |
+| Frame | AX.25 UI |
+| persist | 255 |
+| Beacon | 300 s + stagger (site INI) |
+
+---
+
+## Implementation checklist
+
+- [x] CRC-16 in `hdlc_codec.py`
+- [x] Duplex INI keys in `crdop.ini.example`
+- [ ] `fec.enabled` INI + launcher
+- [ ] Offline BER vs frame-length simulator
+- [ ] Document measured CB SNR thresholds (field)
diff --git a/stacks/crdop/docs/G3RUH-DESIGN.md b/stacks/crdop/docs/G3RUH-DESIGN.md
new file mode 100644
index 0000000..ae1a164
--- /dev/null
+++ b/stacks/crdop/docs/G3RUH-DESIGN.md
@@ -0,0 +1,72 @@
+# G3RUH FSK — CRDOP P1 design spec
+
+**Scope:** 9600–**19200** baud direct FSK. **Hard maximum 19200** — nothing above.
+
+**Prerequisite:** P0 on-air (1200 AFSK) stable. **Not** CB FM mic path.
+
+See [ROADMAP.md](../ROADMAP.md) P1 milestone (stack scheduling: **DEV-Level 4**).
+
+---
+
+## PHY difference from P0
+
+| | 1200 AFSK (P0) | G3RUH FSK (P1) |
+|---|----------------|----------------|
+| Modulation | Audio tones 1200/2200 Hz | Scrambled baseband FSK |
+| RF path | Mic / speaker / line | **Direct** varactor + discriminator |
+| CB FM voice channel | ✅ | ❌ |
+| VHF backbone | Access only at 1200 | ✅ at 9600–19200 |
+
+---
+
+## Parameters (normative)
+
+| Baud | Peak deviation | RF bandwidth (~-60 dB) |
+|------|----------------|------------------------|
+| 4800 | ±1.5 kHz | ~8 kHz |
+| **9600** | **±3 kHz** | **~20 kHz** |
+| **19200** | **±6 kHz** | **~30 kHz** |
+
+CRDOP implements **up to 19200** only.
+
+---
+
+## Radio interface requirements
+
+| Requirement | Detail |
+|-------------|--------|
+| TX | Direct modulation input (varactor), not acoustic |
+| RX | Discriminator / data output, wide IF |
+| Filter | Flat amplitude 10 Hz–5 kHz, constant group delay |
+| Scrambler | G3RUH compatible — interoperate with standard 9600 peers |
+
+Standard narrow FM voice audio **fails** eye-diagram tests at 9600+.
+
+---
+
+## Software modules (planned)
+
+| Module | Path | Status |
+|--------|------|--------|
+| Scrambler | `stacks/crdop/lib/g3ruh_scrambler.py` | Not started |
+| FSK modulator | `stacks/crdop/lib/g3ruh_modulator.py` | Not started |
+| FSK demodulator | `stacks/crdop/lib/g3ruh_demodulator.py` | Not started |
+| HDLC/framing | Reuse `hdlc_codec.py` | Done |
+
+Host protocol unchanged — same M25/KISS TCP; baud selected via INI `speed_baud` (future).
+
+---
+
+## P1 exit criteria
+
+1. Loopback with recorded baseband fixtures at 9600.
+2. Over-the-air exchange with reference 9600 FSK peer.
+3. 19200 optional stretch after 9600 stable.
+
+---
+
+## Out of scope
+
+- 38400+ satellite/backbone rates
+- Acoustic coupling at 9600+
+- CB 27 MHz deployment
diff --git a/stacks/crdop/docs/HARDWARE-INTERFACE.md b/stacks/crdop/docs/HARDWARE-INTERFACE.md
new file mode 100644
index 0000000..9c36e21
--- /dev/null
+++ b/stacks/crdop/docs/HARDWARE-INTERFACE.md
@@ -0,0 +1,194 @@
+# CRDOP hardware interface specification
+
+Generic specification for builders who manufacture **radio ↔ sound-card interfaces** for CRDOP (MAX25-SoftModem). **No product-specific wiring examples** — apply these rules to your own design.
+
+**CRDOP** = **MAX25-SoftModem**. Project rule: [docs/CRDOP.md](../../../docs/CRDOP.md).
+
+---
+
+## Scope
+
+CRDOP connects a host sound card to a transceiver via one of:
+
+| Path | Typical use | Max practical baud |
+|------|-------------|-------------------|
+| **Audio AFSK** | Mic IN / speaker OUT / line level | **1200** (P0) |
+| **Direct FSK** | Varactor TX + FM discriminator RX | **9600–19200** (P1) |
+| **SSB audio** | USB/LSB suppressed carrier | **300** (P2 optional) |
+| **Acoustic coupling** | Speaker ↔ microphone (test / field) | **1200** |
+
+RF path matrix and delivery milestones: [ROADMAP.md](../ROADMAP.md).
+
+---
+
+## Audio levels
+
+### Line level (preferred)
+
+| Signal | Typical level | Notes |
+|--------|---------------|-------|
+| **Playback → radio mic IN** | −20 to −10 dBV (adjust per rig sensitivity) | Use potentiometer or fixed attenuator; verify with sniffer or oscilloscope |
+| **Radio speaker/line → capture** | 0.1–1.0 V RMS typical | Attenuate hot speaker outputs before codec input |
+| **Full-scale digital** | Avoid clipping on TX and RX | Leave 6–12 dB headroom |
+
+### Mic level
+
+Some transceivers expose mic-level inputs only. Attenuate line output to mic sensitivity (often 5–20 mV). Use coupling capacitor if DC bias is present on the mic jack.
+
+### Isolation
+
+| Method | Purpose |
+|--------|---------|
+| **Audio transformer** (1:1 or step-down) | Galvanic isolation; breaks ground loops |
+| **Opto-isolator** (digital PTT path) | Isolate PTT from radio ground |
+| **Separate ground reference** | Star ground at interface PCB; one tie point to radio chassis if required |
+
+**Ground loops** cause hum, false transitions, and decode failures. Always isolate PC USB ground from radio chassis when using direct cable coupling.
+
+---
+
+## PTT control
+
+CRDOP is **half-duplex by default**. The host must key the transmitter before playback and unkey after tail.
+
+| Method | Interface | Caveats |
+|--------|-----------|---------|
+| **GPIO** | USB-serial RTS/DTR, parallel port, dedicated GPIO | Cleanest; software-controlled timing |
+| **Serial CAT** | Radio command port | Latency depends on radio firmware |
+| **VOX** | Audio-derived keying | **Not recommended** for packet — tail timing unpredictable; false keys from noise |
+| **Manual** | Operator PTT | Acceptable for bench only |
+
+### Timing parameters
+
+Configure in `crdop.ini` / `max25d.ini`:
+
+| Parameter | Typical half-duplex | Full-duplex |
+|-----------|--------------------|-------------|
+| Pre-TX delay (`extra_delay_ms`) | 80–200 ms | 0 |
+| TXTAIL | Match radio requirement | N/A |
+| PTT release | After last space tone + margin | N/A |
+
+**Rule:** explicit GPIO/serial PTT beats VOX for AX.25 UI bursts.
+
+---
+
+## Acoustic coupling
+
+Valid for **bench validation** and some field setups:
+
+```
+[Host speaker] ──air gap──► [Radio mic]
+[Radio speaker] ──air gap──► [Host mic]
+```
+
+| Principle | Detail |
+|-----------|--------|
+| **Distance** | Minimize path loss; foam gasket reduces room noise |
+| **Frequency response** | FM voice channel ≈ 3 kHz — sufficient for 1200/2200 Hz Bell 202 |
+| **Level** | Low volume on playback; avoid speaker distortion |
+| **Duplex** | Half-duplex only — acoustic feedback if TX and RX paths overlap |
+| **Test** | Run `max25-signal-sniffer --loopback` on host before acoustic trial |
+
+Acoustic coupling does **not** scale to G3RUH direct FSK (P1).
+
+---
+
+## Direct FSK path (P1 — G3RUH)
+
+When audio AFSK is insufficient (9600–19200 baud):
+
+| Element | Requirement |
+|---------|-------------|
+| **TX** | Discriminator input bypassed; varactor or dedicated FSK input on data port |
+| **RX** | Flat FM discriminator output (de-emphasis off or compensated) |
+| **Bandwidth** | ~20 kHz IF for 9600; wider for 19200 |
+| **Scrambler** | G3RUH polynomial — implemented in future `lib/` module |
+| **Radio** | Data-port capable; standard FM mic/speaker path **not** sufficient |
+
+CB FM rigs at 27 MHz are generally **not** direct-FSK capable at 9600+ — P1 targets VHF/UHF amateur backbone paths.
+
+---
+
+## Sound card requirements
+
+| Requirement | 1200 baud (P0) | 9600–19200 (P1) |
+|-------------|----------------|-----------------|
+| **Sample rate** | ≥ 48 kHz recommended | ≥ 48 kHz; 96 kHz preferred |
+| **Bit depth** | 16-bit mono | 16-bit mono |
+| **Duplex** | Full-duplex USB codec or half-duplex with PTT | Full-duplex, low jitter |
+| **Clock** | Stable crystal; avoid drift during TX | Jitter < 50 ppm class |
+| **Driver** | Kernel ALSA (`hw:` / `plughw:`) | Same |
+| **Isolation** | Transformer on radio side | Same + verified loopback |
+
+### Unsuitable interfaces
+
+- Onboard laptop mic/speaker without external interface
+- Bluetooth audio bridges
+- PulseAudio / PipeWire virtual devices
+- Consumer "USB sound sticks" without galvanic isolation
+
+Configure devices in `crdop.ini`:
+
+```ini
+[audio]
+backend = alsa-kernel
+no_pulse = yes
+capture = plughw:1,0
+playback = plughw:1,0
+sample_rate = 48000
+```
+
+Verify with:
+
+```bash
+max25-signal-sniffer --loopback
+max25-signal-sniffer -D plughw:1,0 -t 2.0
+```
+
+---
+
+## Compliance and operator responsibility
+
+| Topic | Policy |
+|-------|--------|
+| **Transmit licence** | Operator must hold valid authorization for the band and mode |
+| **CB (27 MHz EU)** | EN 300 433 / national rules — voice primary; data may be restricted or channel-specific |
+| **Amateur bands** | Band plan, power limits, identification |
+| **EMC** | Builder responsible for conducted/radiated emissions from interface hardware |
+| **CRDOP software** | GPLv3 — no warranty; see [LICENSE-USAGE.md](LICENSE-USAGE.md) |
+
+CRDOP documentation does **not** constitute legal advice. Check local regulations before on-air operation.
+
+---
+
+## Acceptance checklist (builder)
+
+| Step | Pass criterion |
+|------|----------------|
+| Loopback | `max25-signal-sniffer --loopback` decodes test UI |
+| Level | Mark/space tones within radio spec; no clipping |
+| Isolation | No hum on decode when PC and radio share mains |
+| PTT | Key before TX audio; clean tail; no truncation |
+| On-air | AX.25 UI exchange with known-good peer at 1200 baud |
+| Duplex | Half-duplex CSMA behaviour matches `persist` / `extra_delay` INI |
+
+---
+
+## Related in-repo docs
+
+| Topic | Document |
+|-------|----------|
+| Sound-card requirements | [SOUNDCARD-QUALIFICATION.md](SOUNDCARD-QUALIFICATION.md) |
+| Bell 202 / 1200 AFSK (P0) | [ROADMAP.md](../ROADMAP.md) · [SOFTMODEM.md](SOFTMODEM.md) |
+| G3RUH 9600–19200 (P1) | [G3RUH-DESIGN.md](G3RUH-DESIGN.md) |
+| AX.25 / packet context | [docs/PACKET-RADIO.md](../../../docs/PACKET-RADIO.md) |
+
+---
+
+## Related
+
+| Doc | Topic |
+|-----|--------|
+| [AUDIO-ARCHITECTURE.md](AUDIO-ARCHITECTURE.md) | Kernel ALSA, sound-proxy |
+| [MAX25-USAGE.md](MAX25-USAGE.md) | Stack integration and bench modes |
+| [ROADMAP.md](../ROADMAP.md) | P0/P1/P2 phases |
diff --git a/stacks/crdop/docs/HOST-PROTOCOL-SPEC.md b/stacks/crdop/docs/HOST-PROTOCOL-SPEC.md
new file mode 100644
index 0000000..d45c7e5
--- /dev/null
+++ b/stacks/crdop/docs/HOST-PROTOCOL-SPEC.md
@@ -0,0 +1,103 @@
+# M25 SoftModem host protocol — implementation spec (frozen for v1.0.0)
+
+**CRDOP** = **MAX25-SoftModem** · Native host wire for MAX25-SoftModem · **Not** ARDOP FEC/ARQ.
+
+**Normative code:** `stacks/crdop/lib/m25_host_protocol.py` · **Consumer:** `max25d` `CrdopTcpBackend`, `audio-dummyd`, `crdopc` launcher.
+
+---
+
+## Transport
+
+| Channel | Default port | Wire |
+|---------|--------------|------|
+| **Control** | **8515** | Line-oriented ASCII, **`\n`** terminated (native) |
+| **Data** | **8516** (= ctrl + 1) | Binary AX.25 UI body **without** HDLC flags or FCS |
+
+| Mode | Control terminator | Data semantics |
+|------|-------------------|----------------|
+| **Native M25/KISS** | `\n` | KISS DATA equivalent — 16+ byte UI body |
+
+### Bench alternate ports
+
+When `soft-crdop` and `audio-dummyd` run on one host, bench daemon may use **8520/8521** via CLI. Production `crdopc` and INI default **8515/8516**.
+
+---
+
+## Control commands (native)
+
+All commands are single lines. Reply is one line ending `\n`.
+
+| Command | Args | Reply | Behaviour |
+|---------|------|-------|-----------|
+| `INITIALIZE` | — | `OK` | Reset session state |
+| `PROTOCOLMODE` | `KISS` | `OK` | Select KISS-semantics data channel |
+| `MYCALL` | `CALL-SSID` | `OK` | Store station ID (uppercase) |
+| `LISTEN` | `TRUE`/`FALSE` | `OK` | RX enable (default true) |
+| `PING` | — | `OK` | Liveness |
+| `STATUS` | — | `STATUS ready mycall=…` | State snapshot |
+| *(unknown)* | — | `ERR unknown command` | |
+
+**Not supported on native path:** `PROTOCOLMODE FEC`, `FECMODE`, ARQ bandwidth commands — ARDOP is a separate optional plugin, not part of CRDOP.
+
+---
+
+## Data channel
+
+### TX (host → modem)
+
+1. TCP connect to data port.
+2. Send **one** AX.25 UI frame body: `dest(7) + src(7) + ctrl(1) + pid(1) + info(0..256)`.
+3. Modem responds `OK` or `ERR …` (ASCII line).
+
+Modem builds on-air: HDLC flags, bit-stuffing, frequency-toggle line code, AFSK PHY — see `acoustic_engine.encode_ax25_ui()`.
+
+### RX (modem → host)
+
+Native bench: optional `STATUS` lines on control channel. Production: `max25d` parses demodulated UI and displays `[CRDOP AX25 UI src>dst] text`.
+
+---
+
+## max25d integration
+
+```ini
+[devices]
+soft-crdop = crdop:default
+
+[device.soft-crdop]
+host = 127.0.0.1
+port = 8515
+listen = yes
+```
+
+`CrdopTcpBackend` on `open()`:
+
+1. TCP connect `host:port` (ctrl), `host:port+1` (data).
+2. Send `INITIALIZE`, `PROTOCOLMODE KISS`, `MYCALL …`, `LISTEN TRUE` (each `\n`).
+
+---
+
+## Layer separation
+
+| Layer | On data TCP | On air |
+|-------|-------------|--------|
+| AX.25 UI body | ✅ | Inside HDLC |
+| HDLC + CRC-16 | ❌ | ✅ |
+| Bell 202 AFSK | ❌ | ✅ |
+
+Same rule as KISS `0x00` DATA frames in `kiss_bridge.py`.
+
+---
+
+## Compliance tests
+
+| Test | Command |
+|------|---------|
+| Unit | `pytest stacks/daemon/test_crdop_backend.py` |
+| Bench host | `python3 stacks/crdop/tools/audio-dummyd.py --ctrl-port 8515 --data-port 8516` |
+| Loopback DSP | `pytest stacks/crdop/test_bell202_line_code.py` |
+
+---
+
+## Version
+
+Frozen for **MAX25-Stack v1.0.0** (dev track `CUR999`). Changes require bump in this file + `PROTOCOL.md` + `test_crdop_backend.py`.
diff --git a/stacks/crdop/docs/INDEX.md b/stacks/crdop/docs/INDEX.md
new file mode 100644
index 0000000..91a75b4
--- /dev/null
+++ b/stacks/crdop/docs/INDEX.md
@@ -0,0 +1,77 @@
+# CRDOP documentation index
+
+**CRDOP** = stack acronym for **MAX25-SoftModem** (device id `soft-crdop`). Product: **MAX25-SoftModem** (`stacks/crdop/`). Version: MAX25-Stack **v1.0.0**; dev track `CUR999` in `$SRC/stacks/crdop/VERSION`.
+
+**Project rule (authoritative):** [docs/CRDOP.md](../../../docs/CRDOP.md)
+**AI agent map:** [DEVELOPMENT.md](../../../docs/DEVELOPMENT.md) (repo root)
+
+---
+
+## Start here
+
+| Audience | Doc | One-line purpose |
+|----------|-----|------------------|
+| Everyone | [SOFTMODEM.md](SOFTMODEM.md) | What CRDOP is — baud scope, duplex, ALSA path |
+| MAX25 operator | [MAX25-USAGE.md](MAX25-USAGE.md) | Build, `max25d`, plugins, terminal, HyBBX attach |
+| Developer | [DEVELOPER.md](DEVELOPER.md) | Source tree, modules, tests, extending the modem |
+| Hardware builder | [HARDWARE-INTERFACE.md](HARDWARE-INTERFACE.md) | Generic radio/audio interface specification |
+| Legal / distribution | [LICENSE-USAGE.md](LICENSE-USAGE.md) | GPLv3 — private and commercial use rights |
+
+## Implementation specs (v1.0.0)
+
+| Doc | One-line purpose |
+|-----|------------------|
+| [HOST-PROTOCOL-SPEC.md](HOST-PROTOCOL-SPEC.md) | **Frozen** M25/KISS TCP :8515/:8516 — normative host wire |
+| [ACOUSTIC-TEST-PROTOCOL.md](ACOUSTIC-TEST-PROTOCOL.md) | P0 bench acceptance matrix |
+| [FEC-SPEC.md](FEC-SPEC.md) | Duplex + FEC strategy — implementable parameters |
+| [SOUNDCARD-QUALIFICATION.md](SOUNDCARD-QUALIFICATION.md) | Interface qualification tests (class A/B/C) |
+| [G3RUH-DESIGN.md](G3RUH-DESIGN.md) | P1 — 9600–19200 direct FSK (DEV-Level 4) |
+
+---
+
+## Technical reference
+
+| Doc | One-line purpose |
+|-----|------------------|
+| [AUDIO-ARCHITECTURE.md](AUDIO-ARCHITECTURE.md) | Kernel ALSA only — sound-proxy, no PulseAudio |
+| [PROTOCOL.md](PROTOCOL.md) | M25 host summary — **normative:** [HOST-PROTOCOL-SPEC.md](HOST-PROTOCOL-SPEC.md) |
+| [CONFIG.md](CONFIG.md) | `crdop.ini` keys and launcher behaviour |
+| [BUILD.md](BUILD.md) | Standalone CMake build and cross-compile |
+| [EXAMPLES.md](EXAMPLES.md) | INI profiles, launcher, `max25d` snippets |
+| [CHANGELOG.md](CHANGELOG.md) | Release notes |
+
+---
+
+## Planning
+
+| Doc | One-line purpose |
+|-----|------------------|
+| [ROADMAP.md](../ROADMAP.md) | P0/P1/P2 modulation, RF paths, FEC, delivery milestones |
+
+---
+
+## Plugin registry
+
+| Id | Type | Role |
+|----|------|------|
+| `soft-crdop` | device | Production sound-card modem (`soft-modems` hardware) |
+| `audio-dummy` | device | Acoustic bench — loopback / ALSA / host (`acoustic-bench`) |
+| `soft-modems` | hardware | CRDOP device family |
+| `acoustic-bench` | hardware | Dev/test bench — no RF required |
+| `ardop-plugin` | optional | Third-party ARDOP host registry (separate from CRDOP) |
+
+---
+
+## Install tree (MAX25-Stack build)
+
+| Path | Contents |
+|------|----------|
+| `bin/crdop` | Launcher (`scripts/crdopc`) |
+| `bin/audio-dummyd` | M25 host + acoustic engine |
+| `bin/max25-signal-sniffer` | Bell 202 analysis tool |
+| `share/crdop/` | INI examples, `lib/*.py`, `VERSION` |
+| `share/hybbx/crdop-host.ini.example` | HyBBX Secondary attach |
+
+---
+
+*Compact index — depth in linked docs. Roadmap: [ROADMAP.md](../ROADMAP.md).*
diff --git a/stacks/crdop/docs/LICENSE-USAGE.md b/stacks/crdop/docs/LICENSE-USAGE.md
new file mode 100644
index 0000000..84ad5ff
--- /dev/null
+++ b/stacks/crdop/docs/LICENSE-USAGE.md
@@ -0,0 +1,116 @@
+# CRDOP license and usage rights
+
+**CRDOP** (MAX25-SoftModem) is **free software** under the **GNU General Public License version 3** (GPL-3.0-or-later).
+
+License files:
+
+| File | Scope |
+|------|-------|
+| [stacks/crdop/LICENSE](../LICENSE) | CRDOP subproject |
+| [LICENSE](../../../LICENSE) | MainAX25-Stack repository root |
+
+---
+
+## Summary (not legal advice)
+
+GPLv3 grants everyone — **private individuals and commercial entities** — the right to:
+
+| Right | Meaning |
+|-------|---------|
+| **Use** | Run CRDOP for any purpose (personal, club, business, government) |
+| **Study** | Read and learn from source code |
+| **Modify** | Change code to suit your needs |
+| **Distribute** | Share originals or modified versions |
+| **Commercial use** | Sell services, products, or support built on CRDOP |
+
+**Copyleft obligation:** When you **distribute** CRDOP (or a combined work based on it), you must:
+
+1. Provide **corresponding source** (or a written offer valid ≥ 3 years).
+2. License derivatives under **GPLv3** (or later, per "or later" clause).
+3. Preserve copyright notices and license text.
+4. Document changes if you modify files.
+5. Pass through patent retaliation and anti-tivoization terms as GPLv3 requires.
+
+**No warranty:** Software is provided **AS IS** — no implied fitness for a particular purpose.
+
+---
+
+## Private use
+
+| Scenario | Obligation |
+|----------|------------|
+| Run CRDOP on your own computer | None beyond GPLv3 acceptance |
+| Modify for personal use only | No distribution → no source-offer obligation |
+| Internal company use (no binary redistribution) | Same as private — no copyleft trigger until you distribute |
+
+---
+
+## Commercial use
+
+| Scenario | Obligation |
+|----------|------------|
+| Sell pre-built PCs with CRDOP installed | Provide source (or written offer) to recipients |
+| Ship hardware interface **with** CRDOP binaries | GPL applies to distributed software; hardware schematics you create separately may use another license if they are not a derivative of the GPL code |
+| SaaS / hosted service (no binary to users) | AGPL would impose network copyleft — **CRDOP is GPL, not AGPL**; consult counsel for your deployment model |
+| OEM integration in a product | Ensure GPL compliance for all distributed GPL components; document how users obtain source |
+
+**Practical path:** Include `LICENSE`, offer source at your support URL or ship `stacks/crdop/` tree, and state "based on CRDOP (GPLv3)".
+
+---
+
+## Combined works (MAX25-Stack)
+
+MAX25-Stack bundles CRDOP with daemon, terminal, and plugins. Distribution of the **combined stack** triggers GPLv3 obligations for the GPL-licensed portions. Other components may carry their own licenses — check root `LICENSE` and per-directory notices.
+
+| Component | Typical license |
+|-----------|-----------------|
+| `stacks/crdop/` | GPL-3.0-or-later |
+| MAX25 daemon / terminal | GPL-3.0-or-later (repository default) |
+
+---
+
+## What you may not do
+
+| Restriction | Reason |
+|-------------|--------|
+| Relicense CRDOP under proprietary terms | GPLv3 copyleft |
+| Remove copyright / license notices | GPLv3 §4 |
+| Imply warranty or liability from authors | GPLv3 §15–16 |
+| Distribute without source (when distributing binaries) | GPLv3 §6 |
+
+Patent suits against GPL users can terminate your license (GPLv3 §8).
+
+---
+
+## ARDOP-plugin (separate)
+
+**ARDOP-plugin** is an optional MAX25-Stack registry entry — **not** part of the CRDOP GPLv3 tree.
+
+| Item | Policy |
+|------|--------|
+| CRDOP coupling | **None** — third-party ARDOP host software |
+| Registry | [plugins/external/ardop/README.md](../../../plugins/external/ardop/README.md) |
+
+CRDOP GPLv3 obligations apply to **CRDOP code**, not to operator ARDOP host software.
+
+---
+
+## Contributor grants
+
+By contributing to `stacks/crdop/`, you agree that contributions are licensed under **GPL-3.0-or-later**, consistent with the repository [CONTRIBUTING.md](../../../CONTRIBUTING.md).
+
+---
+
+## Full license text
+
+- Online: <https://www.gnu.org/licenses/gpl-3.0.html>
+- In tree: [stacks/crdop/LICENSE](../LICENSE) · [LICENSE](../../../LICENSE)
+
+---
+
+## Related
+
+| Doc | Topic |
+|-----|--------|
+| [docs/CRDOP.md](../../../docs/CRDOP.md) | Project rule §5 License |
+| [INDEX.md](INDEX.md) | Documentation index |
diff --git a/stacks/crdop/docs/MAX25-USAGE.md b/stacks/crdop/docs/MAX25-USAGE.md
new file mode 100644
index 0000000..616093c
--- /dev/null
+++ b/stacks/crdop/docs/MAX25-USAGE.md
@@ -0,0 +1,292 @@
+# CRDOP in MAX25-Stack — usage guide
+
+Complete operator and integrator workflow for **MAX25-SoftModem (CRDOP)** inside MainAX25-Stack.
+
+**CRDOP** = stack acronym for **MAX25-SoftModem** (device id `soft-crdop`). Project rule: [docs/CRDOP.md](../../../docs/CRDOP.md).
+
+**Versions:** MAX25-Stack **v1.0.0** · CRDOP dev track **CUR999** (`$SRC/stacks/crdop/VERSION`).
+
+**Dependency:** CRDOP sources are standalone-capable in principle, but **MAX25-Stack is required** for build, `max25d`, INI, operator tooling, and plugin registry until **CRDOP-v1.0.0** marks a mature standalone release.
+
+---
+
+## Quick start
+
+```bash
+# 1. Build (CRDOP ON by default)
+./scripts/build.sh
+
+# 2. Configure daemon
+sudo cp share/max25/max25d.ini.example /etc/max25/max25d.ini
+# Edit: enable soft-crdop block (see below)
+
+# 3. Start stack
+./scripts/max25-ctl start --hardware soft-modems --device soft-crdop
+
+# 4. Terminal session
+max25-terminal -U /run/max25/modem.sock
+# or TCP: max25-terminal -H 127.0.0.1 -P 7325
+```
+
+Inside terminal: `SET DEVICE soft-crdop` → `CONNECT` → `SEND …`
+
+---
+
+## Build and install
+
+### CMake options
+
+| Option | Default | Effect |
+|--------|---------|--------|
+| `MAX25_BUILD_CRDOP` | **ON** | Build/install CRDOP scaffold + tools |
+| `MAX25_BUILD_DAEMON` | ON | `max25d` with `CrdopTcpBackend` |
+| `MAX25_BUILD_TERMINAL` | ON | `max25-terminal` / `max25-client` |
+| `CRDOP_VENDOR_ARDOPCF` | OFF | Dev-only legacy ARDOP — **never** in releases |
+
+Disable CRDOP only when not needed:
+
+```bash
+cmake -B build -DMAX25_BUILD_CRDOP=OFF
+cmake --build build -j$(nproc)
+```
+
+### Install tree
+
+After `cmake --install` (or `./scripts/build.sh` + local prefix):
+
+| Path | Contents |
+|------|----------|
+| `bin/crdop` | Launcher — starts `audio-dummyd` with INI |
+| `bin/audio-dummyd` | M25 host TCP daemon + acoustic engine |
+| `bin/max25-signal-sniffer` | Bell 202 analysis |
+| `share/crdop/crdop.ini.example` | CB profile template |
+| `share/crdop/crdop-dual.ini.example` | CB ↔ amateur preset |
+| `share/crdop/crdop-amateur.ini.example` | Amateur secondary preset |
+| `share/crdop/lib/*.py` | DSP modules (installed for tooling) |
+| `share/crdop/VERSION` | `CUR999` |
+| `share/hybbx/crdop-host.ini.example` | HyBBX Secondary attach |
+| `share/clients/soft-crdop.yaml` | Terminal device profile |
+
+---
+
+## Plugin model
+
+| Plugin id | Type | Hardware family | Role |
+|-----------|------|-----------------|------|
+| `soft-crdop` | device | `soft-modems` | Production sound-card modem |
+| `audio-dummy` | device | `acoustic-bench` | Bench — loopback / ALSA / host |
+| `soft-modems` | hardware | — | CRDOP device family |
+| `acoustic-bench` | hardware | — | Dev/test without RF |
+
+Registry: `plugins/manifest.yaml`. Discovery:
+
+```bash
+./scripts/discover-plugins.sh --json | jq '.devices[] | select(.id|test("crdop|audio-dummy"))'
+```
+
+---
+
+## max25d configuration
+
+### Production path — `soft-crdop`
+
+```ini
+[daemon]
+hardware = soft-modems
+device = soft-crdop
+
+[devices]
+default = soft-crdop
+soft-crdop = crdop:default
+
+[device.soft-crdop]
+host = 127.0.0.1
+port = 8515
+listen = yes
+```
+
+`max25-ctl start` launches `crdop` (→ `audio-dummyd`) when `auto_start = yes` in `[stack]`.
+
+### Bench path — `audio-dummy`
+
+No RF; validates DSP loopback or ALSA capture:
+
+```ini
+[daemon]
+hardware = acoustic-bench
+device = audio-dummy
+
+[devices]
+default = audio-dummy
+audio-dummy = audio:loopback
+```
+
+Modes for `audio-dummy` spec:
+
+| Spec | Behaviour |
+|------|-----------|
+| `audio:loopback` | Internal DSP encode/decode |
+| `audio:alsa:plughw:N,M` | Sniff live ALSA capture |
+| `audio:host` | Attach to running `audio-dummyd` on :8515 |
+
+Start bench:
+
+```bash
+./scripts/max25-ctl start --hardware acoustic-bench --device audio-dummy
+max25-terminal -U /run/max25/modem.sock
+```
+
+### Sniffer (no daemon)
+
+```bash
+max25-signal-sniffer --loopback
+max25-signal-sniffer -D plughw:1,0 -t 3.0
+max25-signal-sniffer --mark # 1200 Hz calibration tone
+max25-signal-sniffer --space # 2200 Hz calibration tone
+```
+
+---
+
+## crdop.ini (modem side)
+
+Copy and edit:
+
+```bash
+mkdir -p ~/.config/crdop
+cp share/crdop/crdop.ini.example ~/.config/crdop/crdop.ini
+```
+
+Key sections — full reference: [CONFIG.md](CONFIG.md).
+
+```ini
+[profile]
+radio_profile = cb ; cb | dual | amateur
+
+[modem]
+duplex = half ; half | full
+arq_bandwidth = 500MAX
+
+[mycall]
+call = CB01-0
+
+[audio]
+backend = alsa-kernel
+no_pulse = yes
+capture = plughw:1,0
+playback = plughw:1,0
+
+[host]
+port = 8515
+```
+
+Launch:
+
+```bash
+CRDOP_INI=~/.config/crdop/crdop.ini ./scripts/crdopc
+# or after install:
+crdop
+```
+
+---
+
+## max25-terminal / M25/1
+
+CRDOP uses the same M25/1 session flow as other `max25d` device backends:
+
+```
+SET DEVICE soft-crdop
+CONNECT
+SEND Hello packet world
+```
+
+| Item | Value |
+|------|-------|
+| Daemon socket | `/run/max25/modem.sock` (Linux) |
+| Daemon TCP | `7325` (configurable in `max25d.ini`) |
+| Modem host TCP | `8515` ctrl, `8516` data (CRDOP native) |
+| Client profile | `share/clients/soft-crdop.yaml` |
+
+Protocol reference: [PROTOCOL.md](PROTOCOL.md) · [include/max25/protocol.md](../../../include/max25/protocol.md).
+
+When `ax25_ui = yes` in `max25d.ini`, terminal `SEND` lines encode as AX.25 UI frames.
+
+---
+
+## HyBBX attach
+
+HyBBX is **external** — attach after MAX25 stack is up.
+
+1. Start CRDOP path:
+
+ ```bash
+ ./scripts/max25-ctl start --hardware soft-modems --device soft-crdop
+ ```
+
+2. Merge `share/hybbx/crdop-host.ini.example` into HyBBX `hybbx.ini` on Secondary:
+
+ ```ini
+ [networks]
+ crdop = yes
+
+ [transport.crdop1]
+ enabled = yes
+ modem_host = 127.0.0.1
+ modem_port = 8515
+ mycall = CB-0
+ listen = yes
+ circuit_host = main.example.com
+ circuit_port = 7323
+ ```
+
+MAX25 owns modem lifecycle; HyBBX owns sessions. See [docs/HYBBX.md](../../../docs/HYBBX.md).
+
+---
+
+## Operator profiles
+
+| INI template | Profile | Use |
+|--------------|---------|-----|
+| `crdop.ini.example` | CB | Primary — K24/K25 class channels |
+| `crdop-dual.ini.example` | dual | CB ↔ amateur switching |
+| `crdop-amateur.ini.example` | amateur | Amateur secondary |
+
+```bash
+CRDOP_INI=share/crdop-dual.ini.example ./scripts/crdopc
+```
+
+---
+
+## Testing in MAX25-Stack
+
+```bash
+./scripts/test.sh # cmake --build build --target max25_test
+cmake --build build --target max25_daemon_smoke
+./scripts/release-check.sh # install + policy checks
+```
+
+CRDOP-specific offline tests: `test_crdop_backend.py`, `test_audio_dummy_backend.py`, `test_bell202_line_code.py`.
+
+---
+
+## Troubleshooting
+
+| Symptom | Check |
+|---------|-------|
+| `crdopc not found` | Run `./scripts/build.sh`; launcher uses `audio-dummyd` from install or source tree |
+| No decode | `max25-signal-sniffer --loopback`; verify ALSA devices in INI |
+| PulseAudio hijack | `no_pulse = yes`; use `hw:` / `plughw:` explicitly |
+| Port conflict | Default :8515/:8516 — change `[host] port` + `[device.soft-crdop] port` |
+| Terminal no device | `SET DEVICE soft-crdop`; check `[devices]` in `max25d.ini` |
+
+---
+
+## Related
+
+| Doc | Topic |
+|-----|--------|
+| [INDEX.md](INDEX.md) | Full doc table |
+| [DEVELOPER.md](DEVELOPER.md) | Source, modules, extending |
+| [HARDWARE-INTERFACE.md](HARDWARE-INTERFACE.md) | Radio interface spec |
+| [docs/PLUGINS-DEVICE-MODEL.md](../../../docs/PLUGINS-DEVICE-MODEL.md) | Unified device workflow |
+| [docs/LINUX-HOST-SETUP.md](../../../docs/LINUX-HOST-SETUP.md) | Host prerequisites |
+| [ROADMAP.md](../ROADMAP.md) | P0/P1/P2 milestones and modulation priorities |
diff --git a/stacks/crdop/docs/PROTOCOL.md b/stacks/crdop/docs/PROTOCOL.md
new file mode 100644
index 0000000..2d28bd4
--- /dev/null
+++ b/stacks/crdop/docs/PROTOCOL.md
@@ -0,0 +1,36 @@
+# Protocol
+
+Normative wire: **[HOST-PROTOCOL-SPEC.md](HOST-PROTOCOL-SPEC.md)** (frozen v1.0.0).
+
+CRDOP (**MAX25-SoftModem**) uses the **native MAX25 M25/KISS host protocol** on TCP :8515 (control) / :8516 (data).
+
+## Stack (default)
+
+```
+[Radio] ↔ [audio/ALSA] ↔ audio-dummyd / native modem ↔ TCP :8515 ctrl, :8516 data ↔ max25d
+```
+
+## Over-the-air
+
+AX.25-compatible AFSK tones (1200 baud primary, up to 19200 baud design range). See [SOFTMODEM.md](SOFTMODEM.md).
+
+## Host TCP (native M25)
+
+| Port | Role |
+|------|------|
+| 8515 | Control (line-oriented ASCII, `\n` terminated) |
+| 8516 | Data (AX.25 UI body without HDLC/FCS) |
+
+Common commands: `INITIALIZE` · `PROTOCOLMODE KISS` · `MYCALL` · `LISTEN` · `STATUS` · `PING`
+
+Implementation reference: `stacks/crdop/lib/m25_host_protocol.py`
+
+## CRDOP launch presets
+
+| Profile | Role |
+|---------|------|
+| `cb` | Default — CB / 500MAX class |
+| `dual` | CB ↔ amateur turnaround |
+| `amateur` | Amateur bandwidth class |
+
+Runtime overrides via host TCP or `crdop.ini` always apply.
diff --git a/stacks/crdop/docs/README.md b/stacks/crdop/docs/README.md
new file mode 100644
index 0000000..4a763a2
--- /dev/null
+++ b/stacks/crdop/docs/README.md
@@ -0,0 +1,31 @@
+# CRDOP documentation
+
+**CRDOP** = stack acronym for **MAX25-SoftModem** (device id `soft-crdop`).
+
+**MAX25-SoftModem** — in-house sound-card modem subproject (`stacks/crdop/`).
+
+## Entry point
+
+**[INDEX.md](INDEX.md)** — complete table of all CRDOP docs with one-line purpose.
+
+Project rule: [docs/CRDOP.md](../../../docs/CRDOP.md)
+
+## Policy
+
+| Rule | |
+|------|--|
+| Standard | Native MAX25 M25/KISS host on TCP :8515/:8516 |
+| Focus | CB-first; `dual` and `amateur` are presets |
+| Scope | Modem only — no host application |
+
+## Dev-only vendor tree
+
+Legacy **[pflarue/ardop](https://github.com/pflarue/ardop)** (ardopcf, MIT) may exist under `vendor/ardopcf/` for local dev builds (`-DCRDOP_VENDOR_ARDOPCF=ON`). **Never installed in MAX25 releases.**
+
+Pin: `a7c9228` · record: `vendor/ardopcf.ref`
+
+## Platforms (tested)
+
+Linux · FreeBSD — **tested** (GCC, Clang)
+
+Windows · macOS — build supported, **not yet tested**
diff --git a/stacks/crdop/docs/SOFTMODEM.md b/stacks/crdop/docs/SOFTMODEM.md
new file mode 100644
index 0000000..fef0a0e
--- /dev/null
+++ b/stacks/crdop/docs/SOFTMODEM.md
@@ -0,0 +1,127 @@
+# MAX25-SoftModem — product definition
+
+> **Authoritative rule:** [docs/CRDOP.md](../../../docs/CRDOP.md) · **Doc index:** [INDEX.md](INDEX.md)
+
+## Name and role
+
+- **Product:** MAX25-SoftModem (stack acronym **CRDOP**; plugin id `soft-crdop`)
+- **Stack version:** MAX25-Stack **v1.0.0** · **Dev track:** `CUR999` in `$SRC/stacks/crdop/VERSION`
+- **Stack:** MainAX25-Stack — **standard** component (built/installed unless `MAX25_BUILD_CRDOP=OFF`)
+- **Nature:** Pure MAX25 in-house development — in **development and test phase**, openly documented
+
+## Use like a hardware modem
+
+CRDOP uses the host **sound card IN and OUT** plus a **suitable radio** (or acoustic coupling to existing RF hardware). In `max25d` it registers like any other device backend — operators use the same M25/1 flow (`SET DEVICE`, `CONNECT`, `SEND`).
+
+```
+┌──────────┐ line or acoustic ┌─────────────┐ TCP ┌─────────┐
+│ Radio │ ◄──────────────────► │ CRDOP/ALSA │ ◄──────► │ max25d │
+└──────────┘ └─────────────┘ └─────────┘
+ │
+ 1200–19200 baud
+ half or full duplex
+```
+
+### Acoustic AX.25 compatibility
+
+On the **acoustic / RF layer**, CRDOP targets **AX.25 Packet Radio** interoperability at the PHY level:
+
+- **1200 baud Bell 202 AFSK** (1200/2200 Hz) — standard VHF/UHF and CB packet class
+- **Half- and full-duplex** per radio and audio capability
+- **Acoustic or line-level** coupling — valid test and field configuration
+
+CRDOP is an **alternative modem** when the sound card is the RF interface. It does not replace an existing UART/KISS path when that path is already in use.
+
+Interface specification for builders: [HARDWARE-INTERFACE.md](HARDWARE-INTERFACE.md). MAX25-Stack usage: [MAX25-USAGE.md](MAX25-USAGE.md).
+
+## Protocol stack
+
+| Layer | Policy |
+|-------|--------|
+| On-air / acoustic | AX.25-compatible AFSK tones (1200 baud primary) |
+| Host to max25d | MAX25-native TCP control + data (see [PROTOCOL.md](PROTOCOL.md)) |
+
+We study legacy stacks; we implement our own modem and document it for **reproducible hardware builds**.
+
+## Sound path — kernel ALSA only
+
+CRDOP does **not** use PulseAudio, PipeWire-as-default, or any desktop sound server.
+
+| Layer | Function |
+|-------|----------|
+| **MAX25 sound-proxy** | Sole shim between modem DSP and ALSA — buffers, timing, duplex, xrun recovery |
+| **libasound** | Direct `hw:` / `plughw:` opens |
+| **Kernel ALSA** | Driver + `/dev/snd/*` |
+| **Hardware** | Sound card → radio interface |
+
+Configure explicit devices in `crdop.ini` `[audio]` (`capture`, `playback`). See [AUDIO-ARCHITECTURE.md](AUDIO-ARCHITECTURE.md).
+
+## Baud scope
+
+| Priority | Range | Status |
+|----------|-------|--------|
+| **P0** | **1200 baud** Bell 202 AFSK | Primary — CB + VHF on-air |
+| **P1** | **9600–19200** G3RUH direct FSK | Planned after 1200 stable (VHF backbone) |
+| **P2** | **300** HF AFSK | Optional |
+| — | **>19200 baud** | **Not implemented** — do not configure or expect |
+
+Full modulation matrix, RF paths, band context, delivery milestones: **[ROADMAP.md](../ROADMAP.md)**.
+
+Higher baud rates demand **proportionally better** audio hardware (sample rate, jitter, SNR, isolation). G3RUH requires a **direct FSK radio path** — not the CB FM mic/speaker route.
+
+## Duplex
+
+| Mode | Status |
+|------|--------|
+| **Half-duplex** | Standard CB / packet-radio (PTT or VOX-style timing) |
+| **Full-duplex** | Supported when radio and audio hardware allow simultaneous TX/RX paths |
+
+Configure in `crdop.ini`: `[modem] duplex = half|full`.
+
+## Sound card — required (not optional)
+
+CRDOP must:
+
+1. **Generate** precise mark/space frequencies on **playback**.
+2. **Decode** those tones reliably on **capture**.
+
+Requirements **scale with baud**:
+
+| Baud class | Audio expectation |
+|------------|-------------------|
+| 1200 | Stable full-duplex or half-duplex USB codec or PCI interface; galvanic isolation to radio |
+| 9600–19200 | Low-jitter interface, adequate bandwidth, verified loopback before on-air |
+
+**Unsuitable:** onboard laptop mic/speaker shortcuts, anonymous USB “sound sticks”, Bluetooth bridges, unisolated consumer line inputs.
+
+Document your ALSA device in INI (`[audio] capture` / `playback`) and verify with loopback or sidetone before traffic.
+
+## Open hardware
+
+Documentation targets builders who want to **manufacture radio ↔ sound-card interfaces**:
+
+- Level matching (radio mic/speaker ↔ line level)
+- Galvanic isolation (transformers / opto)
+- PTT integration (GPIO, serial — VOX not recommended for packet)
+- Acoustic coupler fixtures for bench validation
+
+Generic interface specification: [HARDWARE-INTERFACE.md](HARDWARE-INTERFACE.md). Delivery milestones: [ROADMAP.md](../ROADMAP.md).
+
+## Build / release (MAX25-Stack-v1.0.0)
+
+**CRDOP dev track:** `CUR999` in `$SRC/stacks/crdop/VERSION` (ships with MAX25-Stack v1.0.0).
+
+| Item | Shipped |
+|------|---------|
+| CRDOP scaffold (INI, launcher, install) | **Yes** — default CMake |
+| `max25d` `crdop-tcp` backend | **Yes** — offline-tested |
+| Native `crdopc` DSP binary | **In development** — in-house modem DSP |
+| ARDOP / ardopcf vendor tree | **Never** in release tarball |
+
+## HyBBX
+
+When CRDOP is active, HyBBX Secondary attaches via `share/hybbx/crdop-host.ini.example`. HyBBX remains an external project.
+
+## Testing phase
+
+Current work: loopback audio, acoustic bench validation, field on-air trials, acceptable sound-card criteria. Report results via project issues — RF acceptance remains manual until automated gates exist. Developer tests: [DEVELOPER.md](DEVELOPER.md).
diff --git a/stacks/crdop/docs/SOUNDCARD-QUALIFICATION.md b/stacks/crdop/docs/SOUNDCARD-QUALIFICATION.md
new file mode 100644
index 0000000..b35d55b
--- /dev/null
+++ b/stacks/crdop/docs/SOUNDCARD-QUALIFICATION.md
@@ -0,0 +1,95 @@
+# Sound card qualification — CRDOP / MAX25-SoftModem
+
+**Purpose:** Measurable criteria for interface hardware used with CRDOP. No product endorsements — class-based requirements.
+
+See also [ROADMAP.md](../ROADMAP.md) (P0/P1 audio requirements).
+
+---
+
+## Mandatory architecture
+
+```
+CRDOP DSP → sound_proxy → libasound (hw:) → codec → radio interface
+```
+
+| Forbidden in production path | Reason |
+|------------------------------|--------|
+| PulseAudio default route | Mixing, resampling, latency |
+| PipeWire default | Variable buffer |
+| Bluetooth A2DP/HFP | Codec + AGC |
+
+INI: `[audio] backend = alsa-kernel`, `no_pulse = yes`.
+
+---
+
+## Sample rate
+
+| Baud target | Minimum rate | CRDOP default |
+|-------------|--------------|---------------|
+| 1200 AFSK | 48000 Hz | **48000** |
+| 9600 G3RUH (audio tap) | 48000–96000 | 48000 + tuning |
+| 19200 | 96000 Hz | 96000 if hardware supports |
+
+---
+
+## Qualification tests
+
+Run on candidate interface **before** field deployment.
+
+| ID | Test | Pass |
+|----|------|------|
+| **Q1** | Loopback cable, 1200 Hz sine 2 s | THD acceptable by ear + FFT peak at 1200 ±2 Hz |
+| **Q2** | Loopback Bell 202 sniffer | `max25-signal-sniffer --loopback` or ALSA loop |
+| **Q3** | Mark/space calibration tones | `--mark` / `--space` tools within ±5 Hz |
+| **Q4** | 60 s full-duplex record+play | No xrun in `dmesg` / ALSA log |
+| **Q5** | Clock drift | 10 min loopback: no cumulative symbol slip at 1200 bd |
+
+---
+
+## Interface classes
+
+| Class | Isolation | 1200 AFSK | 19200 path |
+|-------|-----------|-----------|------------|
+| **A** | Galvanic line I/O, fixed gain | Required minimum | Test Q1–Q5 |
+| **B** | USB codec, headphone/mic level | Acceptable with level match | Not recommended |
+| **C** | Onboard mic/speaker only | Acoustic bench only | Fail |
+
+CRDOP production path: **Class A or B** with line-level radio interface — see [HARDWARE-INTERFACE.md](HARDWARE-INTERFACE.md).
+
+---
+
+## Level guidelines
+
+| Path | Typical level |
+|------|---------------|
+| Line OUT → radio mic in | 100–300 mV RMS (adjust pad) |
+| Radio speaker/line → Line IN | Attenuate to avoid clipping |
+| DC offset | AC coupling required |
+
+---
+
+## Baud scaling
+
+Requirements **rise faster than linear** above 1200 bd:
+
+| Baud | Clock jitter tolerance | Filter flatness |
+|------|------------------------|-----------------|
+| 1200 | moderate | 300–3000 Hz FM audio path OK |
+| 9600 | strict | 10 Hz–5 kHz flat, constant group delay |
+| 19200 | professional | Direct FSK tap — not soundcard mic path |
+
+---
+
+## Failure actions
+
+| Symptom | Action |
+|---------|--------|
+| xruns | Increase `period_frames`; reduce system load |
+| Wrong tones | Verify 48000 Hz; check resampler not inserted |
+| Intermittent decode | Replace Class C with A/B; check ground loop |
+
+---
+
+## Community list (planned)
+
+P0 bench exit: publish operator-submitted **class + Q1–Q2 result** table — no vendor ranking.
git clone -b <branch> https://cgit.mode42.com/<repo>.git
git clone -b <branch> git://cgit.mode42.com/<repo>.git

info@mode42.com