Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 100 additions & 18 deletions rig-bridge/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -135,6 +143,76 @@ 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,
"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` |
| `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

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.

### 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

Once the bridge is running and showing your frequency:
Expand Down Expand Up @@ -167,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 |

---

Expand Down Expand Up @@ -215,13 +294,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
```

---
Expand All @@ -234,7 +315,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 }) {
Expand Down Expand Up @@ -267,6 +348,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()`.
3 changes: 3 additions & 0 deletions rig-bridge/core/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
};

Expand Down
34 changes: 34 additions & 0 deletions rig-bridge/core/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,28 @@ function buildSetupHtml(version) {
</div>
</div>

<div class="checkbox-row">
<input type="checkbox" id="wsjtxMulticast" onchange="toggleWsjtxMulticastOpts()">
<span>Enable Multicast</span>
</div>
<div class="help-text" style="margin-top:-8px; margin-bottom:10px;">
Join a UDP multicast group so multiple apps can receive WSJT-X packets simultaneously.
In WSJT-X set UDP Server to <code>224.0.0.1</code> instead of <code>127.0.0.1</code>.
</div>

<div id="wsjtxMulticastOpts" style="display:none;">
<div class="row">
<div>
<label>Multicast Group</label>
<input type="text" id="wsjtxMulticastGroup" placeholder="224.0.0.1">
</div>
<div>
<label>Multicast Interface</label>
<input type="text" id="wsjtxMulticastInterface" placeholder="Leave blank for OS default">
</div>
</div>
</div>

<div style="font-size:12px; color:#6b7280; margin-bottom:14px;">
Status: <span id="wsjtxStatusText" style="color:#c4c9d4;">—</span>
</div>
Expand Down Expand Up @@ -622,14 +644,23 @@ 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() {
const enabled = document.getElementById('wsjtxEnabled').checked;
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,
Expand All @@ -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', {
Expand Down
37 changes: 34 additions & 3 deletions rig-bridge/plugins/wsjtx-relay.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {}
Expand All @@ -488,6 +517,8 @@ const descriptor = {
consecutiveErrors,
udpPort: cfg.udpPort || 2237,
serverUrl,
multicast: mcEnabled,
multicastGroup: mcEnabled ? mcGroup : null,
};
}

Expand Down
5 changes: 4 additions & 1 deletion rig-bridge/rig-bridge-config.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@
"session": "",
"udpPort": 2237,
"batchInterval": 2000,
"verbose": false
"verbose": false,
"multicast": false,
"multicastGroup": "224.0.0.1",
"multicastInterface": ""
}
}