Ordo Artificum / CodeMonkey / WebSocket API
← Back to CodeMonkey

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:

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. 1.hello event — identifies the server
  2. 2.status event — 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:

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())