CWSD

July 8, 2026 · View on GitHub

Part of the hamtools suite — see the hub for how cwsd, xlog2 and usb-paddles combine into an integrated (remote) station.

CW sender daemon.

cwsd runs on Linux next to your transceiver (developed against the Icom IC-7300), links against hamlib, and exposes several independent network services so logging and contest software can control the rig, send Morse, stream receiver audio, and even key the rig with a real paddle over the internet. Each service is individually toggled in the config and listens on its own port.

Services

ServiceConfig sectionDefault portProtocolEnabled by defaultPurpose
rigctldrigctldTCP 4532hamlib rigctldyesQuery/set frequency, mode, PTT, VFO — for WSJT-X, fldigi, etc.
cwdaemoncwdaemonUDP 6789cwdaemonyesReceive text and key it as Morse (DTR=key, RTS=PTT).
audio streamaudioUDP 7355Opus over UDPnoCapture rig audio (ALSA), Opus-encode, fan out to subscribers.
remote keyremote_keyUDP 6790timestamped edgesnoReplay real paddle keying over the internet (DTR=key, RTS=PTT).

cwsd service architecture

Diagram source: docs/architecture.dot — regenerate with dot -Tsvg docs/architecture.dot -o docs/architecture.svg.

Note: cwdaemon and remote_key both drive the same DTR/RTS control lines. You can enable both on the same serial device, but don't key through them at once — they are alternative keying front-ends, not meant to run simultaneously. The audio stream has no configured target; clients subscribe (and NAT-punch) by sending any datagram to its port, then must send a periodic keepalive.

Running WSJT-X (or other soundcard apps) from another machine? cwsd handles CAT/PTT via rigctld, but its audio stream is RX-only. For full-duplex remote audio (RX and TX) over the LAN, see docs/wsjtx-remote-audio.md, a step-by-step runbook for bridging the rig's USB soundcard with PipeWire-Pulse.

Install (Ubuntu PPA)

Prebuilt packages for current Ubuntu releases are on the ppa:benishor/hamtools PPA — no compiling:

sudo add-apt-repository ppa:benishor/hamtools
sudo apt update
sudo apt install cwsd

This installs the binary, a systemd service (see below) and /etc/cwsd/cwsdrc.sample. On other distributions — or Raspberry Pi OS — grab a self-contained binary from the releases (static x86_64/arm64 tarballs), or build from source below.

Build from source

mkdir build
cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
make -j $(nproc)
sudo make install

Build requirements: CMake ≥ 3.25, a C++20 compiler, hamlib dev headers/libs, and — for the audio stream — ALSA and Opus dev libs. On Debian/Ubuntu:

sudo apt install libhamlib-dev libasound2-dev libopus-dev

Configuration

By default cwsd will read configuration from ~/.config/cwsdrc when run by hand. Installed from a package and started via systemd, it instead reads the system config /etc/cwsd/cwsdrc (see Installing as a system service below) — the unit runs as a DynamicUser with no home directory. Despite the rc name it is YAML. The rig.model field is a hamlib rig model number (e.g. 3073 = IC-7300). Each service has its own enabled flag and port. See cwsdrc.sample for the fully-commented template. A typical configuration:

rig:
  port: /dev/icom7300      # stable symlink from the udev rule below
  model: 3073              # hamlib rig model: Icom IC-7300
  # serial_speed: 115200   # override only if hamlib's backend default rate is
                           # unusable (e.g. the QMX model 2057 defaults to 256000,
                           # which many termios layers reject -> rig_open fails)
cwdaemon:
  enabled: true
  port: 6789
  initial_wpm: 40
rigctld:
  enabled: true
  port: 4532
audio:                     # Opus receiver-audio stream (off by default)
  enabled: false
  device: pipewire         # RX via PipeWire (shared w/ WSJT-X); or plughw:0,0 for the raw card
  port: 7355               # UDP port to bind; clients subscribe by sending here
  sample_rate: 8000        # opus rates only: 8000/12000/16000/24000/48000
  channels: 1
  bitrate: 96000           # opus target bitrate in bits/s
  frame_ms: 20             # opus frame size: 2.5/5/10/20/40/60
  client_timeout_ms: 10000 # drop subscribers silent longer than this
  fec_loss_perc: 10        # expected packet loss %; drives Opus in-band FEC (0 off)
remote_key:                # real paddle keying over the internet (off by default)
  enabled: false           # shares DTR/RTS with cwdaemon; you can run both, just don't key through both at once
  port: 6790               # UDP port to bind for the timestamped edge stream
  # device: /dev/icom7300  # serial device with DTR=key/RTS=PTT; defaults to rig.port
  playout_ms: 150          # jitter-buffer depth; the rig lags the operator by this much
  silence_ms: 250          # force key-up if no packet arrives for this long
  max_key_down_ms: 5000    # hard watchdog: never hold the key down longer than this
  ptt_lead_ms: 10          # assert PTT this long before the first key-down
  ptt_tail_ms: 100         # hold PTT this long after the last key-up
logging:
  level: info
  filename: /tmp/cwsd.log
  max_size: 1048576

ALSA capture device (audio stream)

The device string is passed straight to ALSA. Two sensible choices:

  • pipewire — read RX through PipeWire, sharing the rig's USB codec with other apps (e.g. WSJT-X) instead of grabbing the raw card exclusively. Needs the PipeWire ALSA plugin, and cwsd must have access to the session where PipeWire runs (so this suits a desktop / user-session cwsd more than the hardened DynamicUser service).
  • plughw:0,0 — capture the raw card directly. Prefer this numeric form over the by-name plughw:CARD=CODEC,DEV=0; name resolution can fail when cwsd runs under systemd (Cannot get card index for CODEC).

Either way, the user running cwsd must be in the audio group to open /dev/snd/*. List capture devices with arecord -l.

Usage:

cwsd will simply run in foreground. Config is loaded from ~/.config/cwsdrc by default.

cwsd -c /path/to/cwsdrc (or --config) reads config from an explicit path — used by the system service, which reads /etc/cwsd/cwsdrc.

cwsd -d will make it daemonize.

cwsd --version will print the current running version.

  • sudo cp shared/80-ic7300.rules /etc/udev/rules.d/
  • sudo udevadm control --reload-rules && sudo udevadm trigger

After plugging in the Icom 7300 USB cable a /dev/icom7300 symlink will be created pointing to the actual /dev/ttyUSBx device that the rig was allocated. The rule matches a specific rig serial — edit ID_SERIAL_SHORT for your unit (find it with udevadm info -q property -n /dev/ttyUSB0 | grep ID_SERIAL_SHORT). The user running cwsd must also be in the dialout group to open the serial port.

Installing as a system service

make install (or the packages/tarballs) installs a systemd unit to /usr/lib/systemd/system/cwsd.service and a sample config to /etc/cwsd/cwsdrc.sample. The unit runs cwsd as a transient, unprivileged system user (DynamicUser=yes) — no account to create and no username baked into the unit — added to the dialout and audio groups so it can reach the rig's serial line and the sound card. It reads /etc/cwsd/cwsdrc (a system path, not a per-user ~/.config).

sudo install -m644 /etc/cwsd/cwsdrc.sample /etc/cwsd/cwsdrc   # then edit rig.port / rig.model
sudo systemctl daemon-reload
sudo systemctl enable --now cwsd
journalctl -u cwsd -f                                          # logs

The unit grants CAP_SYS_NICE/CAP_IPC_LOCK (with LimitRTPRIO/LimitMEMLOCK) so the keyer thread can run at real-time priority for jitter-free element timing. Logs go to the journal; set logging.filename to /var/log/cwsd/cwsd.log for an on-disk copy (the unit provisions that directory via LogsDirectory).

For an interactive/desktop setup you can instead just run cwsd yourself (it reads ~/.config/cwsdrc) — the system service is for always-on / headless stations.

Authors

Part of a small suite of Linux ham-radio tools:

  • xlog2 — logging application that drives cwsd's rigctld (rig control) and cwdaemon (network CW), plays its Opus rig-audio stream, and does real paddle keying through its remote_key service.
  • usb-paddles — USB Morse-paddle firmware; pairs with cwsd's remote_key for paddle keying over the internet.

License

cwsd is licensed under the GNU General Public License v3.0 or later (GPL-3.0-or-later). See LICENSE for the full text.

Bundled third-party libraries under src/libs/ keep their own licenses: