Skip to content

About

Unattended CW (Morse code) QSO recorder using SDR

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

17 Commits

Folders and files

Repository files navigation

CW QSO Recorder

Unattended Morse code (CW) recorder that monitors HF amateur radio bands via SDR. Automatically detects CW signals, validates them against non-CW interference, estimates character speed, and saves recordings as MP3 files with JSON metadata. Supports multiple SDR hardware via GNU Radio.

Features

  • Wideband monitoring - scans 200 kHz of bandwidth to find CW signals automatically
  • Ham band presets - --band 20m sets center frequency and scan limits for any HF band
  • CW validation - bimodal amplitude, Morse timing, and dit/dah ratio tests reject SSB, digital, and noise
  • Post-recording audio analysis - tone SNR and on/off contrast filter catches false positives
  • Morse timing estimation - K-means clustering of dit/dah durations, Farnsworth spacing detection
  • FT8 exclusion - automatically skips FT8 frequency windows on all bands
  • GUI waterfall - real-time pyqtgraph waterfall display with signal markers and scan region overlay
  • Terminal display - flicker-free fixed-height status display with signal tracking
  • Streaming audio - phase-coherent narrowband extraction with multi-stage IIR decimation, no clicks
  • Smart recording - 3-second lead-in capture, mid-QSO silence preservation, 2-second trailing trim
  • Persistent numbering - recording file numbers continue across sessions with no gaps

Supported SDRs

Device device_type Notes
SDRplay RSPdx rspdx Default. Two-stage gain, 3 antenna ports. Requires SDRplay API v3.x
RTL-SDR rtlsdr Cheap, widely available. 8-bit, ~2 MHz max bandwidth
HackRF One hackrf Wideband, 8-bit. Good for experimentation
LimeSDR lime 12-bit, flexible sample rates
ADALM-PlutoSDR pluto Compact AD9361-based. USB or network
Ettus USRP usrp Professional grade. Requires UHD drivers
Generic SoapySDR soapy Any SoapySDR-supported device. Set soapy_driver

Requirements

  • One of the supported SDRs listed above
  • Windows 10/11 (tested on Windows 10 Pro)
  • SDRplay API v3.x (only if using RSPdx)
  • Radioconda (provides GNU Radio, gr-sdrplay3, SoapySDR, UHD)

Installation

1. Install SDRplay API

Download and install the SDRplay API (v3.x) from sdrplay.com/downloads. This provides the driver for the RSPdx. Reboot after installation.

2. Install Radioconda

Download the radioconda installer for Windows from github.com/ryanvolz/radioconda. Run the .exe installer.

During installation:

  • Install for "Just Me" (recommended)
  • Add to PATH when prompted
  • Default install location is fine (C:\Users\<you>\radioconda)

3. Install Additional Dependencies

Open a Radioconda Prompt from the Start Menu:

conda install -y -c conda-forge pysoundfile websocket-client pyyaml scipy

For the GUI waterfall display (optional):

conda install -y -c conda-forge pyqtgraph pyside6

4. Clone and Verify

git clone https://github.com/km6am/morse-code-recorder.git
cd morse-code-recorder
python -m pytest tests/ -v

All tests should pass (one Windows file-locking error in cleanup is expected). No SDR hardware is needed for tests.

Quick Start

1. Connect your RSPdx

Connect the RSPdx via USB. Make sure SDRuno or other SDR software is closed (only one application can use the SDR at a time).

2. Run the recorder

From the project directory, activate radioconda and run:

activate.bat
python -m cw_recorder --band 20m

This monitors the 20m non-voice segment (14.000-14.150 MHz) and saves CW recordings to ./recordings/.

For an RTL-SDR, set device_type in config.yaml:

sdr:
  device_type: "rtlsdr"
  band: "20m"
  sample_rate_hz: 250000
  gain: 40

3. Stop recording

Press Ctrl+C. In-progress recordings are finalized and saved on shutdown.

Usage

Command Line Options

python -m cw_recorder [OPTIONS]

Options:
  --config PATH    Config file (default: config.yaml)
  --band BAND      Ham band: 160m 80m 60m 40m 30m 20m 17m 15m 12m 10m
  --display        Terminal status display (signal list, peaks, stats)
  --gui            GUI waterfall display (requires pyqtgraph + PySide6)

Examples

# Monitor 20m with terminal display
python -m cw_recorder --band 20m --display

# Monitor 40m with GUI waterfall
python -m cw_recorder --band 40m --gui

# Use a custom config file
python -m cw_recorder --config my_config.yaml

# Headless (log output only, good for unattended operation)
python -m cw_recorder --band 20m

Display Modes

Headless (default) - log output only. Best for unattended recording.

Terminal (--display) - fixed-height text display showing:

  • Active and silent signals with frequency, SNR, WPM, duration
  • Spectrum peaks with CW markers
  • Recording stats (saved/rejected/short counts)

GUI (--gui) - pyqtgraph window with:

  • Live spectrum plot with noise floor and signal markers
  • Scrolling waterfall with configurable colormap
  • Scan region overlay (shaded exclusion zones)
  • Signal table with real-time WPM updates

Configuration

Edit config.yaml or use --band to set the band. The --band flag overrides center frequency and scan limits from the config file.

sdr:
  device_type: "rspdx"         # "rspdx", "rtlsdr", "hackrf", "lime", "pluto", "usrp", "soapy"
  band: "20m"                   # Sets center freq + scan limits automatically
  sample_rate_hz: 200000        # 200 kHz bandwidth
  gain: 30                      # Single gain value (for non-RSPdx SDRs)
  antenna_port: "Antenna B"     # RSPdx antenna port (A, B, or C)
  if_gain_reduction_db: 40      # IF gain reduction (0-59)
  rf_gain_reduction_db: 0       # RF gain reduction
  agc_enabled: false            # Manual gain recommended for CW
  biasT: false                  # Bias-T power for active antennas

detection:
  snr_threshold_db: 6           # dB above noise floor to trigger detection
  max_signal_bandwidth_hz: 500  # Reject signals wider than this
  silence_timeout_s: 15         # End recording after this much silence
  min_recording_duration_s: 5   # Discard recordings shorter than this
  min_wpm: 5                    # WPM range for validation
  max_wpm: 50
  reject_ft8: true              # Skip FT8 frequency windows (default: on)
  # Override scan limits (normally set by band):
  # scan_freq_min_hz: 14000000
  # scan_freq_max_hz: 14150000

recording:
  output_dir: "./recordings"
  sample_rate_hz: 8000          # Audio sample rate
  sidetone_hz: 700              # BFO sidetone frequency
  max_disk_usage_mb: 1024       # Auto-prune oldest when exceeded

Available Bands

Band Center (MHz) Scan Range (kHz) Notes
160m 1.900 1800-2000 Nighttime, needs good antenna
80m 3.550 3500-3600 Nighttime
60m 5.359 5332-5405 Channelized, limited
40m 7.063 7000-7125 Day and night
30m 10.125 10100-10150 CW/digital only, 24hr
20m 14.075 14000-14150 Best daytime band
17m 18.090 18068-18110 Daytime
15m 21.075 21000-21200 Daytime, solar cycle dependent
12m 24.920 24890-24930 Daytime, narrow
10m 28.100 28000-28300 Daytime, solar cycle dependent

Output

Recordings are saved to ./recordings/ organized by date:

recordings/
  2026-04-11/
    0001_14043.6kHz_2026-04-11T19-29-41Z.mp3
    0001_14043.6kHz_2026-04-11T19-29-41Z.json
    0002_14028.4kHz_2026-04-11T19-30-53Z.mp3
    0002_14028.4kHz_2026-04-11T19-30-53Z.json

File numbers are sequential with no gaps and persist across sessions.

JSON Metadata

Each MP3 has a JSON sidecar with recording details:

{
  "signal_id": "a1b2c3d4e5f6",
  "freq_hz": 14043603.515625,
  "start_time_utc": "2026-04-11T19:29:41+00:00",
  "duration_s": 45.0,
  "active_duration_s": 32.5,
  "duty_cycle": 0.72,
  "snr_db": 15.0,
  "est_wpm_range": [32.0, 32.0],
  "sample_rate_hz": 8000,
  "sidetone_hz": 700,
  "timing": {
    "char_wpm": 32.0,
    "effective_wpm": 32.0,
    "dit_ms": 37.5,
    "dah_ms": 118.5,
    "ratio": 3.16,
    "confidence": 0.97,
    "is_farnsworth": false,
    "n_dits": 27,
    "n_dahs": 14,
    "n_elements": 41
  }
}

Tools

Search and Filter Recordings

# Find high-SNR recordings around 20 WPM
python -m tools.find_recordings --wpm-min 18 --wpm-max 22 --snr-min 15

# Long recordings sorted by duration
python -m tools.find_recordings --dur-min 60 --sort dur

# Play matching recordings interactively
python -m tools.find_recordings --snr-min 20 --play

Label Recordings (for validator tuning)

python tools/label_recordings.py

Plays recordings and prompts for CW/NOT_CW labels. Results saved to tools/labels.json.

Capture Raw Spectrum Data

# Record 10 minutes of spectrum snapshots for offline analysis
python -m tools.capture_iq --config config.yaml --duration 600 --output capture.npz

Test with KiwiSDR (no local SDR needed)

python tools/test_kiwi_pipeline.py --host kiwisdr.briata.org --freq 7025 --bandwidth 40 --duration 120

Find public KiwiSDRs at kiwisdr.com/public.

Architecture

RSPdx SDR (GNU Radio flowgraph)
  |-- Path 1: FFT spectrum --> Scanner (find narrowband peaks)
  |-- Path 2: IQ ring buffer --> NarrowbandExtractor (per-signal, phase-coherent)
                                       |
                                 CW Validator (bimodal + timing + ratio, 3-of-3)
                                       |
                                 Signal Tracker (active --> silent --> ended)
                                       |
                                 Recorder (BFO demod, smooth AGC, streaming MP3)
                                       |
                                 Audio Validator (tone SNR + on/off contrast)
                                       |
                                 Morse Timing (K-means dit/dah, Farnsworth)
                                       |
                                 Output: MP3 + JSON sidecar

Detection Pipeline

  1. Scanner - FFT spectrum analysis finds narrowband peaks above the noise floor
  2. Validator - 1-second IQ snapshot tested for bimodal amplitude, Morse timing, and dit/dah ratio (all 3 must pass to start tracking)
  3. Tracker - maintains signal lifecycle; once validated, stays active as long as scanner sees a peak (no re-validation needed each cycle)
  4. Recorder - streams phase-coherent narrowband IQ through multi-stage IIR decimation, BFO demodulation, and smooth AGC to MP3
  5. Audio Validator - post-recording analysis rejects false positives based on tone SNR and on/off contrast
  6. Morse Timing - K-means clustering of accumulated dit/dah durations produces refined WPM estimate

Key Design Decisions

  • IQ ring buffer with streaming reads - get_new_iq() returns contiguous samples since last call using a monotonic cursor. No gaps, no overlap, no re-reading.
  • Per-signal NarrowbandExtractor - each tracked signal gets its own extractor maintaining frequency shift phase and IIR filter state across chunks. Eliminates clicks at chunk boundaries.
  • Multi-stage IIR decimation - 200 kHz to 500 Hz in 3 stages (10x, 10x, 4x) using 4th-order Butterworth filters. 9ms per signal vs 100ms with FIR.
  • Scanner-based tracking - validation is required once to start tracking. After that, any scanner peak within 200 Hz keeps the signal active. Natural CW pauses don't kill recordings.
  • Working directory - recordings write to .working/ during capture. Only validated recordings move to the output folder with gap-free numbering.

Troubleshooting

SDR unavailable

If the SDR is still held by a previous session, the recorder will retry once after a 5-second countdown. Close SDRuno or any other SDR software before starting.

Overload warnings

rspdx :warning: overload detected means the signal level is too high. Increase if_gain_reduction_db in the config (e.g., 40 to 50).

No signals detected

  • Check antenna connection and port setting (Antenna A, B, or C)
  • Try a different band - 20m is best during daytime, 40m/80m at night
  • Lower snr_threshold_db (default 6 dB)
  • Verify frequency with SDRuno to confirm the SDR is receiving

License

Copyright (C) 2026 Tony (KM6AM)

This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License v3. See LICENSE for details.

Tests failing

The one PermissionError in test_start_recording_creates_file is a known Windows file-locking issue during temp directory cleanup. All other tests should pass.

About

Unattended CW (Morse code) QSO recorder using SDR

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages