A lightweight C client for the CWCom / Morse-over-IP (MOIP) protocol.
IRMC connects to MorseKOB relay servers and lets morse code operators communicate over the internet — just like IRC, but in morse code. Plug in a telegraph key (via serial or GPIO), or simply listen to traffic on a channel.
Status: Experimental — works on Linux, macOS, and Raspberry Pi.
- Quick Start
- Building
- Usage
- The CWCom Protocol
- Architecture
- Hardware Interfaces
- Testing
- Changelog
- Authors
# Install dependencies (Debian/Ubuntu)
sudo apt-get install -y build-essential libportaudio-dev portaudio19-dev
# Build
cd src && make
# Connect to a channel and listen
./irmc -h morsecode.dc3.com -c 205sudo apt-get install -y build-essential libportaudio-dev portaudio19-dev
cd src
makesudo apt-get install -y build-essential libportaudio-dev portaudio19-dev wiringpi
cd src
make raspibrew install portaudio
cd src
make| Target | Description |
|---|---|
make |
Build irmc binary (Linux / macOS) |
make raspi |
Build with Raspberry Pi GPIO support |
make clean |
Remove build artifacts (*.o, binary) |
make install |
Copy binary to ~/bin |
make java |
Start local MorseKOB test server (Java) |
Dependencies: libportaudio, pthread, libm. Raspberry Pi builds additionally require wiringPi.
irmc [options]
| Option | Description | Default |
|---|---|---|
-h |
Server hostname | mtc-kob.dyndns.org |
-p |
Server port | 7890 |
-c |
Channel number | 103 |
-i |
Your callsign / ID | irmc-default |
-s |
Serial port device for telegraph key | (none) |
-? |
Show help |
# Connect with defaults (mtc-kob.dyndns.org:7890, channel 103)
./irmc
# Join channel 205 on a specific server
./irmc -h morsecode.dc3.com -c 205
# Identify yourself and attach a serial key
./irmc -h morsecode.dc3.com -c 113 -i W1AW -s /dev/ttyUSB0
# Use a local test server
./irmc -h localhost -c 100IRMC implements the CWCom protocol, an open UDP-based standard for transmitting morse code timing data between clients and relay servers. The protocol is documented in detail in doc/cwcom.pdf.
- Protocol: UDP (
SOCK_DGRAM) - Default server:
mtc-kob.dyndns.org - Default port:
7890 - Clients connect to a numbered channel — everyone on the same channel hears each other.
- A keep-alive packet (ID + connect) is sent every ~5 seconds to maintain the session.
The protocol uses two packet formats:
Used for connecting and disconnecting.
Offset Size Field Description
────── ───── ──────── ──────────────────────────────
0x00 2 command CON (0x0004) or DIS (0x0002)
0x02 2 channel Channel number
Used for identification, morse data transmission, and latch/unlatch signals.
Offset Size Field Description
────── ───── ───────── ──────────────────────────────────────────
0x00 2 command DAT (0x0003)
0x02 2 length Payload length (always 492)
0x04 128 id Sender callsign / identifier (null-terminated)
0x84 4 a1 Reserved
0x88 4 sequence Packet sequence number (monotonically increasing)
0x8C 4 a21 Magic number (1 for ID packets, 0 for TX)
0x90 4 a22 Magic number (755)
0x94 4 a23 Magic number (65535 for ID, 16777215 for TX)
0x98 204 code[51] Morse timing array (51 × signed 32-bit int)
0x164 4 n Number of valid elements in code[]
0x168 128 status Decoded character or interface version string
0x1E8 8 a4 Reserved
Total: 496 bytes — every data packet is exactly this size, regardless of content.
| Code | Value | Description |
|---|---|---|
DIS |
0x0002 |
Disconnect from channel |
DAT |
0x0003 |
Data packet (morse timing, ID, latch/unlatch) |
CON |
0x0004 |
Connect to channel |
ACK |
0x0005 |
Acknowledgement |
Morse code is encoded as an array of signed 32-bit integers in the code[] field:
| Value | Meaning |
|---|---|
| > 0 | Mark (key down / tone) — duration in ms |
| < 0 | Space (key up / silence) — duration in ms |
| = 1 | Special: latch signal (close circuit) |
| = 2 | Special: unlatch signal (open circuit) |
| = 0 | No-op (ignored) |
For example, the letter "A" (dit-dah) might be encoded as:
code[] = { 60, -60, 180 } n = 3
↑ ↑ ↑
dit gap dah
Values with abs(value) > 2000 are treated as invalid and ignored.
Client Server
│ │
│──── CON(channel) ─────────────────>│ 1. Connect to channel
│──── DAT(id_packet) ──────────────>│ 2. Send identification
│ │
│<─── DAT(morse data) ─────────────│ 3. Receive morse from others
│──── DAT(morse data) ─────────────>│ 4. Transmit own morse
│ │
│──── CON + DAT(id) ───────────────>│ 5. Keep-alive (every ~5s)
│ ... │
│──── DIS ──────────────────────────>│ 6. Disconnect
│ │
Each data packet is sent 5 times redundantly (UDP has no delivery guarantee).
If no morse key activity is detected for 240 seconds, the transmitter loop returns to idle. This prevents accidental channel blocking.
┌─────────────────────────────────────────────────────────────┐
│ irmc.c │
│ (main loop, CLI, networking) │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌───────────────────┐ │
│ │ cwprotocol.c │ │ beep.c │ │ util.c │ │
│ │ │ │ │ │ │ │
│ │ Packet build │ │ PortAudio │ │ fastclock() │ │
│ │ Connect/Disc │ │ sine wave │ │ kbhit() │ │
│ │ ID / TX / RX │ │ tone output │ │ get_in_addr() │ │
│ └──────────────┘ └──────────────┘ └───────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│ │ │
UDP Socket PortAudio POSIX / Mach
(network) (audio) (time, I/O)
| File | Lines | Responsibility |
|---|---|---|
irmc.c |
382 | Entry point, CLI parsing, UDP socket setup, main event loop, signal handling, TX/RX logic |
cwprotocol.c/h |
147 | CWCom packet construction (connect, disconnect, data, ID), global packet state, latch/unlatch |
beep.c/h |
203 | Audio output via PortAudio — square wave callback at 44.1 kHz sample rate, frequency/duration control |
util.c/h |
87 | Cross-platform millisecond timer (fastclock), non-blocking keyboard input (kbhit), IPv4/v6 address helper |
The core of IRMC is an infinite loop in main() that runs roughly every 50 microseconds:
- Receive — Read incoming UDP packets (non-blocking)
- Decode — If a 496-byte data packet arrived with new sequence number:
- Display decoded text (
statusfield) - Play back morse timing via audio (
beep()) - Handle latch/unlatch signals
- Display decoded text (
- Transmit — If local key input produced morse timing data:
- Send the data packet 5× redundantly
- Key input — Poll serial port (DSR line) or GPIO pin for key state changes
- Keep-alive — Every ~100 iterations, re-send connect + ID packets
- Flush — Discard any stale keyboard input
| Flag | Effect |
|---|---|
RASPI |
Enable Raspberry Pi GPIO input via wiringPi (pin 5), adjust audio latency ×6 |
TX_SERIAL |
Enable serial port key input (currently disabled in source) |
DEBUG |
Print received packet fields and TX events to stdout |
__MACH__ |
Use macOS Mach clock instead of clock_gettime |
A telegraph key or paddle can be connected via RS-232 serial or Raspberry Pi GPIO. For detailed build instructions, see the MorseKOB interface guide and the included loop interface PDF.
| RS232 Signal | DB9 Pin | Function |
|---|---|---|
| DTR | 4 | Manual key / paddle common |
| DSR | 6 | Manual key / dot paddle |
| CTS | 8 | Dash paddle |
| RTS | 7 | Sounder output |
| SG | 5 | Ground |
When built with make raspi, IRMC reads key input from GPIO pin 5 (wiringPi numbering). Connect your key between GPIO 5 and ground. The pin is configured as INPUT with the wiringPi library.
There is no automated test suite. Testing is manual:
# Start the included local test server (requires Java)
cd src && make java
# In another terminal, connect to it
./irmc -h localhost -c 100Or connect to a public server:
./irmc -h morsecode.dc3.com -c 205| Version | Changes |
|---|---|
| v0.3.3 | Current — minor fixes and cleanups |
| v0.3 | Command-line option cleanup |
| v0.2 | Ported to Debian Wheezy and OS X Yosemite (DG6FL) |
| v0.1 | Original version (VE4FEB) |
- Fernan Bolando (VE4FEB) — Original author
- Gerolf Ziegenhain (DG6FL) — Linux/macOS port, ongoing development
- Les Kerr — MorseKOB server and protocol
- John Samien (VK1EME) — CWCom protocol
- MOIP — Morse Code over IP — Protocol specification
- MorseKOB — Server software and interface guides
- CWCom Protocol Spec — Detailed packet documentation (PDF)
- Loop Interface Guide — Hardware telegraph loop interface (PDF)