From abdcf095dff4d98ae7280df87260a1cc819fbdd4 Mon Sep 17 00:00:00 2001 From: accius Date: Mon, 2 Feb 2026 22:10:40 -0500 Subject: [PATCH] relay --- server.js | 79 +++++++++++++++++++++++++---- src/App.jsx | 1 + src/components/PSKReporterPanel.jsx | 67 ++++++++++++++---------- wsjtx-relay/relay.js | 37 ++++++++++++++ 4 files changed, 149 insertions(+), 35 deletions(-) diff --git a/server.js b/server.js index ce6d16a..01c9c76 100644 --- a/server.js +++ b/server.js @@ -3667,6 +3667,7 @@ const wsjtxState = { decodes: [], // decoded messages (ring buffer) qsos: [], // logged QSOs wspr: [], // WSPR decodes + relay: null, // { lastSeen, version, port } — set by relay heartbeat }; /** @@ -4113,10 +4114,14 @@ app.get('/api/wsjtx', (req, res) => { } } + // Relay is "connected" if seen in last 60 seconds + const relayConnected = wsjtxState.relay && (Date.now() - wsjtxState.relay.lastSeen < 60000); + res.json({ enabled: WSJTX_ENABLED, port: WSJTX_UDP_PORT, relayEnabled: !!WSJTX_RELAY_KEY, + relayConnected: !!relayConnected, clients, decodes: wsjtxState.decodes.slice(-100), // last 100 qsos: wsjtxState.qsos.slice(-20), // last 20 @@ -4155,11 +4160,25 @@ app.post('/api/wsjtx/relay', (req, res) => { return res.status(401).json({ error: 'Invalid relay key' }); } + // Relay heartbeat — just registers the relay as alive + if (req.body && req.body.relay === true) { + wsjtxState.relay = { + lastSeen: Date.now(), + version: req.body.version || '1.0.0', + port: req.body.port || 2237, + }; + return res.json({ ok: true, timestamp: Date.now() }); + } + + // Regular message batch const { messages } = req.body || {}; if (!Array.isArray(messages) || messages.length === 0) { return res.status(400).json({ error: 'No messages provided' }); } + // Update relay last seen on every batch too + wsjtxState.relay = { ...(wsjtxState.relay || {}), lastSeen: Date.now() }; + // Rate limit: max 100 messages per request const batch = messages.slice(0, 100); let processed = 0; @@ -4263,10 +4282,16 @@ app.get('/api/wsjtx/relay/download/:platform', (req, res) => { return res.send(script); } else if (platform === 'windows') { - // Simple .bat that downloads relay.js then runs with node - // No PowerShell, no execution policy issues + // .bat that auto-downloads portable Node.js if needed, then runs relay + // No install, no admin, no PowerShell execution policy issues + const NODE_VERSION = 'v22.13.1'; // LTS + const NODE_ZIP = 'node-' + NODE_VERSION + '-win-x64.zip'; + const NODE_DIR = 'node-' + NODE_VERSION + '-win-x64'; + const NODE_URL = 'https://nodejs.org/dist/' + NODE_VERSION + '/' + NODE_ZIP; + const batLines = [ '@echo off', + 'setlocal', 'title OpenHamClock WSJT-X Relay', 'echo.', 'echo =========================================', @@ -4274,25 +4299,61 @@ app.get('/api/wsjtx/relay/download/:platform', (req, res) => { 'echo =========================================', 'echo.', '', - ':: Check for Node.js', + ':: Check for Node.js (system-installed or portable)', + 'set "NODE_EXE=node"', + 'set "PORTABLE_DIR=%TEMP%\\ohc-node"', + '', 'where node >nul 2>nul', + 'if not errorlevel 1 (', + ' for /f "tokens=*" %%i in (\'node -v\') do echo Found Node.js %%i', + ' goto :have_node', + ')', + '', + ':: Check for previously downloaded portable Node.js', + 'if exist "%PORTABLE_DIR%\\' + NODE_DIR + '\\node.exe" (', + ' set "NODE_EXE=%PORTABLE_DIR%\\' + NODE_DIR + '\\node.exe"', + ' echo Found portable Node.js', + ' goto :have_node', + ')', + '', + ':: Download portable Node.js', + 'echo Node.js not found. Downloading portable version...', + 'echo (This is a one-time ~30MB download^)', + 'echo.', + '', + 'if not exist "%PORTABLE_DIR%" mkdir "%PORTABLE_DIR%"', + '', + 'powershell -Command "try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; Invoke-WebRequest -Uri \'' + NODE_URL + '\' -OutFile \'%PORTABLE_DIR%\\' + NODE_ZIP + '\' } catch { Write-Host $_.Exception.Message; exit 1 }"', 'if errorlevel 1 (', - ' echo Node.js is not installed!', ' echo.', - ' echo Download it from: https://nodejs.org', - ' echo Install the LTS version, then run this script again.', + ' echo Failed to download Node.js!', + ' echo Check your internet connection and try again.', ' echo.', ' pause', ' exit /b 1', ')', '', - 'for /f "tokens=*" %%i in (\'node -v\') do echo Found Node.js %%i', + 'echo Extracting...', + 'powershell -Command "Expand-Archive -Path \'%PORTABLE_DIR%\\' + NODE_ZIP + '\' -DestinationPath \'%PORTABLE_DIR%\' -Force"', + 'if errorlevel 1 (', + ' echo Failed to extract Node.js!', + ' echo.', + ' pause', + ' exit /b 1', + ')', + '', + 'del "%PORTABLE_DIR%\\' + NODE_ZIP + '" >nul 2>nul', + 'set "NODE_EXE=%PORTABLE_DIR%\\' + NODE_DIR + '\\node.exe"', + 'echo Portable Node.js ready.', + 'echo.', + '', + ':have_node', 'echo Server: ' + serverURL, 'echo.', '', ':: Download relay agent', 'echo Downloading relay agent...', - 'powershell -Command "Invoke-WebRequest -Uri \'' + serverURL + '/api/wsjtx/relay/agent.js\' -OutFile \'%TEMP%\\ohc-relay.js\'"', + 'powershell -Command "try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; Invoke-WebRequest -Uri \'' + serverURL + '/api/wsjtx/relay/agent.js\' -OutFile \'%TEMP%\\ohc-relay.js\' } catch { Write-Host $_.Exception.Message; exit 1 }"', 'if errorlevel 1 (', ' echo Failed to download relay agent!', ' echo Check your internet connection and try again.', @@ -4310,7 +4371,7 @@ app.get('/api/wsjtx/relay/download/:platform', (req, res) => { 'echo.', '', ':: Run relay', - 'node "%TEMP%\\ohc-relay.js" --url "' + serverURL + '" --key "' + WSJTX_RELAY_KEY + '"', + '%NODE_EXE% "%TEMP%\\ohc-relay.js" --url "' + serverURL + '" --key "' + WSJTX_RELAY_KEY + '"', '', 'echo.', 'echo Relay stopped.', diff --git a/src/App.jsx b/src/App.jsx index d84ee42..38f8170 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -717,6 +717,7 @@ const App = () => { wsjtxEnabled={wsjtx.enabled} wsjtxPort={wsjtx.port} wsjtxRelayEnabled={wsjtx.relayEnabled} + wsjtxRelayConnected={wsjtx.relayConnected} showWSJTXOnMap={mapLayers.showWSJTX} onToggleWSJTXMap={toggleWSJTX} /> diff --git a/src/components/PSKReporterPanel.jsx b/src/components/PSKReporterPanel.jsx index 5b5aac9..c96d657 100644 --- a/src/components/PSKReporterPanel.jsx +++ b/src/components/PSKReporterPanel.jsx @@ -27,6 +27,7 @@ const PSKReporterPanel = ({ wsjtxEnabled, wsjtxPort, wsjtxRelayEnabled, + wsjtxRelayConnected, showWSJTXOnMap, onToggleWSJTXMap }) => { @@ -347,34 +348,48 @@ const PSKReporterPanel = ({ }}>
Waiting for WSJT-X...
{wsjtxRelayEnabled ? ( -
-
- Download the relay agent for your PC: + wsjtxRelayConnected ? ( +
+
+ + Relay connected +
+
+ Waiting for WSJT-X decodes... +
+ In WSJT-X: Settings → Reporting → UDP → 127.0.0.1:2237 +
-
- 🐧 Linux - 🍎 Mac - 🪟 Windows + ) : ( +
+
+ Download the relay agent for your PC: +
+ +
+ Requires Node.js · Run the script, then start WSJT-X +
-
- Requires Node.js · Run the script, then start WSJT-X -
-
+ ) ) : (
In WSJT-X: Settings → Reporting → UDP Server diff --git a/wsjtx-relay/relay.js b/wsjtx-relay/relay.js index 9a4b786..bfc818c 100644 --- a/wsjtx-relay/relay.js +++ b/wsjtx-relay/relay.js @@ -430,6 +430,43 @@ socket.on('listening', () => { // Start batch relay loop scheduleBatch(); + // Send relay heartbeat immediately, then every 30s + // This tells the server the relay is alive even before WSJT-X sends any packets + function sendHeartbeat() { + const body = JSON.stringify({ relay: true, version: '1.0.0', port: config.port }); + const parsed = new URL(relayEndpoint); + const transport = parsed.protocol === 'https:' ? https : http; + + const reqOpts = { + hostname: parsed.hostname, + port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80), + path: parsed.pathname, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(body), + 'Authorization': `Bearer ${config.key}`, + 'X-Relay-Heartbeat': 'true', + }, + timeout: 10000, + }; + + const req = transport.request(reqOpts, (res) => { + res.resume(); + if (res.statusCode === 200 && consecutiveErrors > 0) { + console.log('\n ✅ Server connection restored'); + consecutiveErrors = 0; + } + }); + req.on('error', () => {}); + req.on('timeout', () => req.destroy()); + req.write(body); + req.end(); + } + + sendHeartbeat(); + setInterval(sendHeartbeat, 30000); + // Periodic health check — verify server is reachable setInterval(() => { const parsed = new URL(`${serverUrl}/api/wsjtx`);