CodeMonkey WebSocket API
CodeMonkey exposes a full WebSocket API on TCP port 2104 (default, configurable in Settings). Everything you can do from the GUI can be done from the API.
Connection
ws://127.0.0.1:2104
The server accepts plain-text WebSocket connections. All messages are UTF-8 JSON text frames.
Settings → WebSocket API controls:
- ►Enable/disable the API
- ►Interface:
127.0.0.1(localhost only) or0.0.0.0(all interfaces) - ►Port (default 2104)
Message Format
Client → Server (commands)
{
"type": "cmd",
"id": "optional-correlation-id",
"cmd": "command.name",
"data": { ... }
}
id is optional; any string you provide is echoed back in the reply. Useful for correlating async replies.
data is optional for commands that take no parameters.
Server → Client (replies)
{ "type": "reply", "id": "...", "ok": true, "data": { ... } }
{ "type": "reply", "id": "...", "ok": false, "error": "description" }
Server → All Clients (events)
{ "type": "event", "event": "event.name", "data": { ... } }
Events are pushed unsolicited whenever state changes.
On Connect
Immediately after connecting you receive two messages:
- 1.
helloevent — identifies the server - 2.
statusevent — current state snapshot
Commands
status.get
Returns the current status.
Response data:
{
"radio_connected": false,
"radio_freq_khz": 0.0,
"ptt": false,
"transmitting": false,
"audio_running": true,
"decoder_freq_hz": 800.0,
"decoder_wpm": 20.0,
"decoder_snr": 15.0,
"tx_freq_hz": 800.0,
"tx_wpm": 20,
"ws_port": 2104,
"ws_clients": 1
}
config.get
Returns all settings.
Response data:
{
"toneFreqHz": 800.0,
"toneFreqMinHz": 300.0,
"toneFreqMaxHz": 1200.0,
"wpmMin": 5.0,
"wpmMax": 60.0,
"useAFC": true,
"squelch": 0.0,
"txToneFreqHz": 800.0,
"txWpm": 20,
"txAmplitude": 0.8,
"useFarnsworth": false,
"farnsworthWpm": 10,
"audioInputDevice": "",
"audioOutputDevice": "",
"waterfallGain": 0.0,
"waterfallFreqMin": 200.0,
"waterfallFreqMax": 2000.0,
"rigModel": 1,
"rigPort": "/dev/ttyUSB0",
"rigBaud": 9600,
"wsEnabled": true,
"wsPort": 2104,
"wsHost": "127.0.0.1"
}
config.set
Set one or more settings. Any subset of config.get keys is accepted. Omitted keys are unchanged.
Request data (example — tune decoder to 700 Hz and enable Farnsworth):
{
"toneFreqHz": 700.0,
"useFarnsworth": true,
"farnsworthWpm": 12
}
Supported fields: toneFreqHz, toneFreqMinHz, toneFreqMaxHz, wpmMin, wpmMax, useAFC, squelch, txToneFreqHz, txWpm, useFarnsworth, farnsworthWpm, audioInputDevice, waterfallFreqMin, waterfallFreqMax, waterfallGain. Changes take effect immediately. A config.changed event is pushed to all clients.
decoder.frequency.get
Response data:
{ "freq_hz": 800.0 }
decoder.frequency.set
Lock the decoder to a specific audio frequency.
Request data:
{ "freq_hz": 700.0 }
Response data:
{ "freq_hz": 700.0 }
tx.send
Transmit CW text. Raises PTT (if rig connected), plays audio, lowers PTT when done.
Request data:
{ "text": "CQ CQ DE W5XYZ W5XYZ K" }
Events emitted: tx.started, ptt.changed (true), tx.finished, ptt.changed (false).
tx.abort
Abort the current transmission immediately.
radio.connect
Connect to the rig using the configured radio settings.
radio.disconnect
Disconnect from the rig.
radio.frequency.set
Set the rig VFO frequency.
Request data:
{ "freq_khz": 14025.0 }
radio.center
Center the rig so that the currently-tracked CW signal lands at the TX tone frequency. Equivalent to clicking the Center button in the GUI.
radio.ptt.set
Manually control PTT (use with caution — tx.send/tx.abort manage PTT automatically).
{ "ptt": true }
audio.devices
List available audio input devices.
Response data:
{ "devices": ["Built-in Microphone", "USB Audio CODEC", "..."] }
version.get
Response data:
{
"version": "0.1.0-ALPHA",
"app": "CodeMonkey",
"build_time": 1748000000
}
OTA Bridge Commands
These commands are used by OTA’s built-in bridge (Radio → CodeMonkey Bridge…). Custom controllers may also use them.
link.announce
Register this connection as the OTA bridge controller. CodeMonkey enters “linked” mode: the status bar shows a ● OTA linked indicator.
Request data:
{ "controller": "OTA" }
Response data:
{ "active": true, "controller": "OTA" }
link.release
Release the bridge link. CodeMonkey exits linked mode. The indicator is hidden. No request data required.
Response data:
{ "active": false }
link.tune
Tune the radio to the given frequency and mode. When mode is CW (or empty), CodeMonkey automatically offsets the VFO so the incoming signal falls under the decoder tone marker:
VFO = freq_khz − (decoder_tone_hz / 1000.0)
This means OTA always passes the spot frequency; CodeMonkey handles the CW offset.
Request data:
{ "freq_khz": 14025.0, "mode": "CW" }
Response data:
{}
Events
hello
Sent immediately on connect.
{ "version": "0.1.0-ALPHA", "port": 2104, "app": "CodeMonkey" }
status
Pushed at 1 Hz. Same fields as status.get.
char.decoded
A CW character was decoded.
{ "char": "A", "confidence": 0.95 }
status.update
Decoder status update (fires several times per second during reception).
{ "freq_hz": 802.3, "wpm": 20.1, "snr": 14.7 }
radio.connected
{ "freq_khz": 14025.0 }
radio.disconnected
{}
radio.frequency
Rig VFO changed (polled at ~2 Hz while connected).
{ "freq_khz": 14028.5 }
tx.started
{}
tx.finished
{}
ptt.changed
{ "ptt": true }
config.changed
Fired when settings are changed (via GUI or API). Same shape as config.get.
link.state
Pushed when the OTA bridge link state changes.
{ "active": true, "controller": "OTA" }
{ "active": false, "controller": "" }
Also included in every status heartbeat as link_active and link_controller fields.
error
{ "message": "description of error" }
Example Session
import asyncio, json, websockets
async def main():
async with websockets.connect("ws://127.0.0.1:2104") as ws:
# hello + status events arrive automatically
hello = json.loads(await ws.recv())
status = json.loads(await ws.recv())
print("Connected to", hello["data"]["app"], hello["data"]["version"])
print("Decoder at", status["data"]["decoder_freq_hz"], "Hz")
# Tune to 750 Hz
await ws.send(json.dumps({
"type": "cmd", "id": "1",
"cmd": "decoder.frequency.set",
"data": {"freq_hz": 750.0}
}))
reply = json.loads(await ws.recv())
print("Tuned:", reply)
# Send CW
await ws.send(json.dumps({
"type": "cmd", "id": "2",
"cmd": "tx.send",
"data": {"text": "CQ DE W5XYZ K"}
}))
# Stream decoded characters
async for msg in ws:
ev = json.loads(msg)
if ev.get("event") == "char.decoded":
print(ev["data"]["char"], end="", flush=True)
asyncio.run(main())
Integration with OTA
Built-in Bridge (recommended)
OTA has a built-in CodeMonkey bridge under Radio → CodeMonkey Bridge…. When enabled:
- ►OTA connects to CodeMonkey (default
127.0.0.1:2104) and sendslink.announce. - ►OTA suspends its own Hamlib and routes all radio commands through CodeMonkey.
- ►When tuning to a CW spot, OTA sends
link.tunewith the spot frequency; CodeMonkey automatically offsets the VFO by the current decoder tone:VFO = freq_khz − (tone_hz / 1000.0) - ►If OTA disconnects or the bridge is disabled, CodeMonkey receives
link.releaseand the link indicator clears. - ►CodeMonkey broadcasts
link.stateevents whenever link state changes.
Monitoring link state from Python
import asyncio, json, websockets
async def monitor():
async with websockets.connect("ws://127.0.0.1:2104") as ws:
hello = json.loads(await ws.recv())
status = json.loads(await ws.recv())
print("Link active:", status["data"].get("link_active", False))
async for msg in ws:
ev = json.loads(msg)
if ev.get("event") == "link.state":
d = ev["data"]
if d["active"]:
print(f"OTA bridge linked: controller={d['controller']}")
else:
print("OTA bridge released")
elif ev.get("event") == "radio.frequency":
print(f"VFO: {ev['data']['freq_khz']:.3f} kHz")
asyncio.run(monitor())