From 6e917029a558eea6cab19bbb05a43c0a04381462 Mon Sep 17 00:00:00 2001 From: ceotjoe Date: Sun, 8 Mar 2026 00:44:06 +0100 Subject: [PATCH 1/2] Update rig-bridge README with mock and WSJT-X relay plugin docs Co-Authored-By: Claude Sonnet 4.6 --- rig-bridge/README.md | 64 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 58 insertions(+), 6 deletions(-) diff --git a/rig-bridge/README.md b/rig-bridge/README.md index 6da7afd7..82e5b76d 100644 --- a/rig-bridge/README.md +++ b/rig-bridge/README.md @@ -35,6 +35,14 @@ TCI (Transceiver Control Interface) is a WebSocket-based protocol used by modern | **flrig** | XML-RPC | 12345 | | **rigctld** | TCP | 4532 | +### For Testing (No Hardware Required) + +| Type | Description | +| ------------------- | -------------------------------------------------------------------- | +| **Simulated Radio** | Fake radio that drifts through several bands — no serial port needed | + +Enable by setting `radio.type = "mock"` in `rig-bridge-config.json` or selecting **Simulated Radio** in the setup UI. + --- ## Quick Start @@ -135,6 +143,47 @@ The bridge auto-reconnects every 5 s if the connection drops — just restart yo --- +## WSJT-X Relay + +The WSJT-X Relay is an **integration plugin** (not a radio plugin) that listens for WSJT-X UDP packets on the local machine and forwards decoded messages to an OpenHamClock server in real-time. This lets OpenHamClock display your FT8/FT4 decodes as DX spots without any manual intervention. + +### Setup + +Edit `rig-bridge-config.json`: + +```json +{ + "wsjtxRelay": { + "enabled": true, + "url": "https://openhamclock.com", + "key": "your-relay-key", + "session": "your-session-id", + "udpPort": 2237, + "batchInterval": 2000, + "verbose": false + } +} +``` + +| Field | Description | Default | +| --------------- | ------------------------------------------------ | -------------------------- | +| `enabled` | Activate the relay on startup | `false` | +| `url` | OpenHamClock server URL | `https://openhamclock.com` | +| `key` | Relay authentication key (from your OHC account) | — | +| `session` | Browser session ID for per-user isolation | — | +| `udpPort` | UDP port WSJT-X is sending to | `2237` | +| `batchInterval` | How often decoded messages are sent (ms) | `2000` | +| `verbose` | Log every decoded message to the console | `false` | + +### In WSJT-X + +Make sure WSJT-X is configured to send UDP packets to `localhost` on the same port as `udpPort` (default `2237`): +**File → Settings → Reporting → UDP Server → `127.0.0.1:2237`** + +The relay runs alongside your radio plugin — you can use direct USB or TCI at the same time. + +--- + ## OpenHamClock Setup Once the bridge is running and showing your frequency: @@ -215,13 +264,15 @@ rig-bridge/ │ └── plugins/ ├── usb/ - │ ├── index.js # USB serial lifecycle (open, reconnect, poll) - │ ├── protocol-yaesu.js # Yaesu CAT ASCII protocol - │ ├── protocol-kenwood.js# Kenwood ASCII protocol - │ └── protocol-icom.js # Icom CI-V binary protocol + │ ├── index.js # USB serial lifecycle (open, reconnect, poll) + │ ├── protocol-yaesu.js # Yaesu CAT ASCII protocol + │ ├── protocol-kenwood.js # Kenwood ASCII protocol + │ └── protocol-icom.js # Icom CI-V binary protocol + ├── tci.js # TCI/SDR WebSocket plugin (Thetis, ExpertSDR, etc.) ├── rigctld.js # rigctld TCP plugin ├── flrig.js # flrig XML-RPC plugin - └── tci.js # TCI/SDR WebSocket plugin (Thetis, ExpertSDR, etc.) + ├── mock.js # Simulated radio for testing (no hardware needed) + └── wsjtx-relay.js # WSJT-X UDP listener → OpenHamClock relay ``` --- @@ -234,7 +285,7 @@ Each plugin exports an object with the following shape: module.exports = { id: 'my-plugin', // Unique identifier (matches config.radio.type) name: 'My Plugin', // Human-readable name - category: 'rig', // 'rig' | 'rotator' | 'logger' | 'other' + category: 'rig', // 'rig' | 'integration' | 'rotator' | 'logger' | 'other' configKey: 'radio', // Which config section this plugin reads create(config, { updateState, state }) { @@ -267,6 +318,7 @@ module.exports = { **Categories:** - `rig` — radio control; the bridge dispatches `/freq`, `/mode`, `/ptt` to the active rig plugin +- `integration` — background service plugins (e.g. WSJT-X relay); started via `registry.connectIntegrations()` - `rotator`, `logger`, `other` — use `registerRoutes(app)` to expose their own endpoints To register a plugin at startup, call `registry.register(descriptor)` in `rig-bridge.js` before `registry.connectActive()`. From 4e660a7589b11d2a18efd87e45440f29d3bb99d2 Mon Sep 17 00:00:00 2001 From: ceotjoe Date: Mon, 9 Mar 2026 08:45:34 +0100 Subject: [PATCH 2/2] Add UDP multicast support to WSJT-X relay plugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enables the WSJT-X relay to join a UDP multicast group so multiple applications on the same machine or LAN can receive WSJT-X packets simultaneously, rather than competing for a single unicast socket. Changes: - core/config.js: Add three new defaults to DEFAULT_CONFIG.wsjtxRelay: multicast (bool, default false), multicastGroup ('224.0.0.1'), multicastInterface ('' = OS picks NIC). Deep-merge in loadConfig() picks these up automatically with no structural changes. - plugins/wsjtx-relay.js: · Derive mcEnabled / mcGroup / mcInterface constants at create() time · In the 'listening' handler, call socket.addMembership() after bind completes (the only valid moment per the Node.js dgram API). Wrapped in try/catch — failure logs a clear error and degrades gracefully to unicast; the plugin keeps running. · In disconnect(), call socket.dropMembership() before socket.close() to cleanly release the IGMP membership. Failure is non-fatal and logged but does not block socket teardown. · getStatus() now exposes multicast and multicastGroup for the status API and any future UI status display. - core/server.js (Integrations tab UI): · Multicast checkbox with onchange toggle handler · Collapsible wsjtxMulticastOpts div containing Multicast Group and Multicast Interface text inputs, shown only when checkbox is checked · populateIntegrations() populates all three new fields on load · saveIntegrations() collects and POSTs the new values; the existing POST /api/config handler merges and restarts the plugin automatically - rig-bridge-config.example.json: Add multicast, multicastGroup, multicastInterface fields (all at their default/off values) - README.md: Extend wsjtxRelay config JSON example and field table with the three new options; add 'Multicast Mode' subsection explaining when and how to use it (WSJT-X UDP server change, multicastInterface for multi-homed systems, link-local scope note); add troubleshooting row. Feature is strictly opt-in (multicast: false by default) — zero impact on existing unicast configurations. Co-Authored-By: Claude Sonnet 4.6 --- rig-bridge/README.md | 74 ++++++++++++++++------- rig-bridge/core/config.js | 3 + rig-bridge/core/server.js | 34 +++++++++++ rig-bridge/plugins/wsjtx-relay.js | 37 +++++++++++- rig-bridge/rig-bridge-config.example.json | 5 +- 5 files changed, 127 insertions(+), 26 deletions(-) diff --git a/rig-bridge/README.md b/rig-bridge/README.md index 82e5b76d..f64f3a9c 100644 --- a/rig-bridge/README.md +++ b/rig-bridge/README.md @@ -160,20 +160,26 @@ Edit `rig-bridge-config.json`: "session": "your-session-id", "udpPort": 2237, "batchInterval": 2000, - "verbose": false + "verbose": false, + "multicast": false, + "multicastGroup": "224.0.0.1", + "multicastInterface": "" } } ``` -| Field | Description | Default | -| --------------- | ------------------------------------------------ | -------------------------- | -| `enabled` | Activate the relay on startup | `false` | -| `url` | OpenHamClock server URL | `https://openhamclock.com` | -| `key` | Relay authentication key (from your OHC account) | — | -| `session` | Browser session ID for per-user isolation | — | -| `udpPort` | UDP port WSJT-X is sending to | `2237` | -| `batchInterval` | How often decoded messages are sent (ms) | `2000` | -| `verbose` | Log every decoded message to the console | `false` | +| Field | Description | Default | +| -------------------- | ------------------------------------------------------- | -------------------------- | +| `enabled` | Activate the relay on startup | `false` | +| `url` | OpenHamClock server URL | `https://openhamclock.com` | +| `key` | Relay authentication key (from your OHC account) | — | +| `session` | Browser session ID for per-user isolation | — | +| `udpPort` | UDP port WSJT-X is sending to | `2237` | +| `batchInterval` | How often decoded messages are sent (ms) | `2000` | +| `verbose` | Log every decoded message to the console | `false` | +| `multicast` | Join a UDP multicast group to receive WSJT-X packets | `false` | +| `multicastGroup` | Multicast group IP address to join | `224.0.0.1` | +| `multicastInterface` | Local NIC IP for multi-homed systems; `""` = OS default | `""` | ### In WSJT-X @@ -182,6 +188,29 @@ Make sure WSJT-X is configured to send UDP packets to `localhost` on the same po The relay runs alongside your radio plugin — you can use direct USB or TCI at the same time. +### Multicast Mode + +By default the relay uses **unicast** — WSJT-X sends packets directly to `127.0.0.1` and only this process receives them. + +If you want multiple applications on the same machine or LAN to receive WSJT-X packets simultaneously, enable multicast: + +1. In WSJT-X: **File → Settings → Reporting → UDP Server** — set the address to `224.0.0.1` +2. In `rig-bridge-config.json` (or via the setup UI at `http://localhost:5555`): + +```json +{ + "wsjtxRelay": { + "multicast": true, + "multicastGroup": "224.0.0.1", + "multicastInterface": "" + } +} +``` + +Leave `multicastInterface` blank unless you have multiple network adapters and need to specify which one to use (enter its local IP, e.g. `"192.168.1.100"`). + +> `224.0.0.1` is the WSJT-X conventional multicast group. It is link-local — packets are not routed across subnet boundaries. + --- ## OpenHamClock Setup @@ -216,18 +245,19 @@ Executables are output to the `dist/` folder. ## Troubleshooting -| Problem | Solution | -| ------------------------- | -------------------------------------------------------------------------------- | -| No COM ports found | Install USB driver (Silicon Labs CP210x for Yaesu, FTDI for some Kenwood) | -| Port opens but no data | Check baud rate matches radio's CAT Rate setting | -| Icom not responding | Verify CI-V address matches your radio model | -| CORS errors in browser | The bridge allows all origins by default | -| Port already in use | Close flrig/rigctld if running — you don't need them anymore | -| PTT not responsive | Enable **Hardware Flow (RTS/CTS)** (especially for FT-991A/FT-710) | -| macOS Comms Failure | The bridge automatically applies a `stty` fix for CP210x drivers. | -| TCI: Connection refused | Enable TCI in your SDR app (Thetis → Setup → CAT Control → Enable TCI Server) | -| TCI: No frequency updates | Check `trx` / `vfo` index in config match the active transceiver in your SDR app | -| TCI: Remote SDR | Set `tci.host` to the IP of the machine running the SDR application | +| Problem | Solution | +| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | +| No COM ports found | Install USB driver (Silicon Labs CP210x for Yaesu, FTDI for some Kenwood) | +| Port opens but no data | Check baud rate matches radio's CAT Rate setting | +| Icom not responding | Verify CI-V address matches your radio model | +| CORS errors in browser | The bridge allows all origins by default | +| Port already in use | Close flrig/rigctld if running — you don't need them anymore | +| PTT not responsive | Enable **Hardware Flow (RTS/CTS)** (especially for FT-991A/FT-710) | +| macOS Comms Failure | The bridge automatically applies a `stty` fix for CP210x drivers. | +| TCI: Connection refused | Enable TCI in your SDR app (Thetis → Setup → CAT Control → Enable TCI Server) | +| TCI: No frequency updates | Check `trx` / `vfo` index in config match the active transceiver in your SDR app | +| TCI: Remote SDR | Set `tci.host` to the IP of the machine running the SDR application | +| Multicast: no packets | Verify `multicastGroup` matches what WSJT-X sends to; check OS firewall allows multicast UDP; set `multicastInterface` to the correct NIC IP if multi-homed | --- diff --git a/rig-bridge/core/config.js b/rig-bridge/core/config.js index c09b5649..40aeccfe 100644 --- a/rig-bridge/core/config.js +++ b/rig-bridge/core/config.js @@ -47,6 +47,9 @@ const DEFAULT_CONFIG = { udpPort: 2237, // UDP port to listen on for WSJT-X packets batchInterval: 2000, // Batch send interval in ms verbose: false, // Log all decoded messages + multicast: false, // Join a multicast group instead of unicast + multicastGroup: '224.0.0.1', // WSJT-X conventional multicast group + multicastInterface: '', // Local NIC IP for multi-homed systems; '' = let OS choose }, }; diff --git a/rig-bridge/core/server.js b/rig-bridge/core/server.js index c866ae85..84abe5f4 100644 --- a/rig-bridge/core/server.js +++ b/rig-bridge/core/server.js @@ -554,6 +554,28 @@ function buildSetupHtml(version) { +
+ + Enable Multicast +
+
+ Join a UDP multicast group so multiple apps can receive WSJT-X packets simultaneously. + In WSJT-X set UDP Server to 224.0.0.1 instead of 127.0.0.1. +
+ + +
Status:
@@ -622,7 +644,11 @@ function buildSetupHtml(version) { document.getElementById('wsjtxSession').value = w.session || ''; document.getElementById('wsjtxPort').value = w.udpPort || 2237; document.getElementById('wsjtxInterval').value = w.batchInterval || 2000; + document.getElementById('wsjtxMulticast').checked = !!w.multicast; + document.getElementById('wsjtxMulticastGroup').value = w.multicastGroup || '224.0.0.1'; + document.getElementById('wsjtxMulticastInterface').value = w.multicastInterface || ''; toggleWsjtxOpts(); + toggleWsjtxMulticastOpts(); } function toggleWsjtxOpts() { @@ -630,6 +656,11 @@ function buildSetupHtml(version) { document.getElementById('wsjtxOpts').style.display = enabled ? 'block' : 'none'; } + function toggleWsjtxMulticastOpts() { + const on = document.getElementById('wsjtxMulticast').checked; + document.getElementById('wsjtxMulticastOpts').style.display = on ? 'block' : 'none'; + } + async function saveIntegrations() { const wsjtxRelay = { enabled: document.getElementById('wsjtxEnabled').checked, @@ -638,6 +669,9 @@ function buildSetupHtml(version) { session: document.getElementById('wsjtxSession').value.trim(), udpPort: parseInt(document.getElementById('wsjtxPort').value) || 2237, batchInterval: parseInt(document.getElementById('wsjtxInterval').value) || 2000, + multicast: document.getElementById('wsjtxMulticast').checked, + multicastGroup: document.getElementById('wsjtxMulticastGroup').value.trim() || '224.0.0.1', + multicastInterface: document.getElementById('wsjtxMulticastInterface').value.trim(), }; try { const res = await fetch('/api/config', { diff --git a/rig-bridge/plugins/wsjtx-relay.js b/rig-bridge/plugins/wsjtx-relay.js index af813604..26fb174a 100644 --- a/rig-bridge/plugins/wsjtx-relay.js +++ b/rig-bridge/plugins/wsjtx-relay.js @@ -10,9 +10,12 @@ * url string OpenHamClock server URL (e.g. https://openhamclock.com) * key string Relay authentication key * session string Browser session ID for per-user isolation - * udpPort number UDP port to listen on (default: 2237) - * batchInterval number Batch send interval in ms (default: 2000) - * verbose boolean Log all decoded messages (default: false) + * udpPort number UDP port to listen on (default: 2237) + * batchInterval number Batch send interval in ms (default: 2000) + * verbose boolean Log all decoded messages (default: false) + * multicast boolean Join a multicast group (default: false) + * multicastGroup string Multicast group IP (default: '224.0.0.1') + * multicastInterface string Local NIC IP for multi-homed systems; '' = OS default */ const dgram = require('dgram'); @@ -249,6 +252,10 @@ const descriptor = { const serverUrl = (cfg.url || '').replace(/\/$/, ''); const relayEndpoint = `${serverUrl}/api/wsjtx/relay`; + const mcEnabled = !!cfg.multicast; + const mcGroup = cfg.multicastGroup || '224.0.0.1'; + const mcInterface = cfg.multicastInterface || undefined; // undefined → OS picks NIC + let socket = null; let batchTimer = null; let heartbeatInterval = null; @@ -426,6 +433,19 @@ const descriptor = { console.log(`[WsjtxRelay] Listening for WSJT-X on UDP ${addr.address}:${addr.port}`); console.log(`[WsjtxRelay] Relaying to ${serverUrl}`); + if (mcEnabled) { + try { + socket.addMembership(mcGroup, mcInterface); + const ifaceLabel = mcInterface || '0.0.0.0 (OS default)'; + console.log(`[WsjtxRelay] Joined multicast group ${mcGroup} on interface ${ifaceLabel}`); + } catch (err) { + console.error(`[WsjtxRelay] Failed to join multicast group ${mcGroup}: ${err.message}`); + console.error( + `[WsjtxRelay] Falling back to unicast — check that ${mcGroup} is a valid multicast address and your OS supports multicast on this interface`, + ); + } + } + scheduleBatch(); // Initial health check then heartbeat @@ -469,6 +489,15 @@ const descriptor = { healthInterval = null; } if (socket) { + if (mcEnabled) { + try { + socket.dropMembership(mcGroup, mcInterface); + console.log(`[WsjtxRelay] Left multicast group ${mcGroup}`); + } catch (err) { + // Socket may already be closing or membership was never joined — safe to ignore + console.error(`[WsjtxRelay] dropMembership failed (non-fatal): ${err.message}`); + } + } try { socket.close(); } catch (e) {} @@ -488,6 +517,8 @@ const descriptor = { consecutiveErrors, udpPort: cfg.udpPort || 2237, serverUrl, + multicast: mcEnabled, + multicastGroup: mcEnabled ? mcGroup : null, }; } diff --git a/rig-bridge/rig-bridge-config.example.json b/rig-bridge/rig-bridge-config.example.json index 14882186..fa0c2fc5 100644 --- a/rig-bridge/rig-bridge-config.example.json +++ b/rig-bridge/rig-bridge-config.example.json @@ -26,6 +26,9 @@ "session": "", "udpPort": 2237, "batchInterval": 2000, - "verbose": false + "verbose": false, + "multicast": false, + "multicastGroup": "224.0.0.1", + "multicastInterface": "" } }