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.
| Service | Config section | Default port | Protocol | Enabled by default | Purpose |
|---|---|---|---|---|---|
| rigctld | rigctld |
TCP 4532 | hamlib rigctld | yes | Query/set frequency, mode, PTT, VFO — for WSJT-X, fldigi, etc. |
| cwdaemon | cwdaemon |
UDP 6789 | cwdaemon | yes | Receive text and key it as Morse (DTR=key, RTS=PTT). |
| audio stream | audio |
UDP 7355 | Opus over UDP | no | Capture rig audio (ALSA), Opus-encode, fan out to subscribers. |
| remote key | remote_key |
UDP 6790 | timestamped edges | no | Replay real paddle keying over the internet (DTR=key, RTS=PTT). |
Diagram source: docs/architecture.dot — regenerate with dot -Tsvg docs/architecture.dot -o docs/architecture.svg.
Note:
cwdaemonandremote_keyboth 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, seedocs/wsjtx-remote-audio.md, a step-by-step runbook for bridging the rig's USB soundcard with PipeWire-Pulse.
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.
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
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
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: 1048576The 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 hardenedDynamicUserservice).plughw:0,0— capture the raw card directly. Prefer this numeric form over the by-nameplughw: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.
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.
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 # logsThe 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
cwsdyourself (it reads~/.config/cwsdrc) — the system service is for always-on / headless stations.
- YO6SSW - Adrian Scripcă benishor@gmail.com
- YO3GEK - Matei Conovici mconovici@gmail.com
Part of a small suite of Linux ham-radio tools:
- xlog2 — logging application that drives
cwsd's
rigctld(rig control) andcwdaemon(network CW), plays its Opus rig-audio stream, and does real paddle keying through itsremote_keyservice. - usb-paddles — USB Morse-paddle
firmware; pairs with cwsd's
remote_keyfor paddle keying over the internet.
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:
- easylogging++ — MIT
- fkYAML (
node.hpp) — MIT