wsjtx support enabled

pull/53/head
accius 2 days ago
parent abdcf095df
commit 344cff6758

@ -3661,15 +3661,51 @@ const WSJTX_MSG = {
CONFIGURE: 15, CONFIGURE: 15,
}; };
// In-memory store // In-memory store (for local UDP — no session)
const wsjtxState = { const wsjtxState = {
clients: {}, // clientId -> { status, lastSeen } clients: {}, // clientId -> { status, lastSeen }
decodes: [], // decoded messages (ring buffer) decodes: [], // decoded messages (ring buffer)
qsos: [], // logged QSOs qsos: [], // logged QSOs
wspr: [], // WSPR decodes wspr: [], // WSPR decodes
relay: null, // { lastSeen, version, port } — set by relay heartbeat relay: null, // not used for local UDP
}; };
// Per-session relay storage — each browser gets its own isolated data
const wsjtxRelaySessions = {}; // sessionId -> { clients, decodes, qsos, wspr, relay, lastAccess }
const WSJTX_SESSION_MAX_AGE = 60 * 60 * 1000; // 1 hour inactive expiry
const WSJTX_MAX_SESSIONS = 50; // prevent memory abuse
function getRelaySession(sessionId) {
if (!sessionId) return null;
if (!wsjtxRelaySessions[sessionId]) {
// Check session limit
if (Object.keys(wsjtxRelaySessions).length >= WSJTX_MAX_SESSIONS) {
// Evict oldest session
let oldestId = null, oldestTime = Infinity;
for (const [id, s] of Object.entries(wsjtxRelaySessions)) {
if (s.lastAccess < oldestTime) { oldestTime = s.lastAccess; oldestId = id; }
}
if (oldestId) delete wsjtxRelaySessions[oldestId];
}
wsjtxRelaySessions[sessionId] = {
clients: {}, decodes: [], qsos: [], wspr: [],
relay: null, lastAccess: Date.now()
};
}
wsjtxRelaySessions[sessionId].lastAccess = Date.now();
return wsjtxRelaySessions[sessionId];
}
// Cleanup expired sessions every 5 minutes
setInterval(() => {
const now = Date.now();
for (const [id, session] of Object.entries(wsjtxRelaySessions)) {
if (now - session.lastAccess > WSJTX_SESSION_MAX_AGE) {
delete wsjtxRelaySessions[id];
}
}
}, 5 * 60 * 1000);
/** /**
* QDataStream binary reader for WSJT-X protocol * QDataStream binary reader for WSJT-X protocol
* Reads big-endian Qt-serialized data types * Reads big-endian Qt-serialized data types
@ -3938,14 +3974,17 @@ function freqToBand(freqHz) {
/** /**
* Handle incoming WSJT-X messages * Handle incoming WSJT-X messages
* @param {Object} msg - parsed WSJT-X message
* @param {Object} state - state object to update (wsjtxState for local, session for relay)
*/ */
function handleWSJTXMessage(msg) { function handleWSJTXMessage(msg, state) {
if (!msg) return; if (!msg) return;
if (!state) state = wsjtxState;
switch (msg.type) { switch (msg.type) {
case WSJTX_MSG.HEARTBEAT: { case WSJTX_MSG.HEARTBEAT: {
wsjtxState.clients[msg.id] = { state.clients[msg.id] = {
...(wsjtxState.clients[msg.id] || {}), ...(state.clients[msg.id] || {}),
version: msg.version, version: msg.version,
lastSeen: msg.timestamp lastSeen: msg.timestamp
}; };
@ -3953,8 +3992,8 @@ function handleWSJTXMessage(msg) {
} }
case WSJTX_MSG.STATUS: { case WSJTX_MSG.STATUS: {
wsjtxState.clients[msg.id] = { state.clients[msg.id] = {
...(wsjtxState.clients[msg.id] || {}), ...(state.clients[msg.id] || {}),
lastSeen: msg.timestamp, lastSeen: msg.timestamp,
dialFrequency: msg.dialFrequency, dialFrequency: msg.dialFrequency,
mode: msg.mode, mode: msg.mode,
@ -3973,7 +4012,7 @@ function handleWSJTXMessage(msg) {
} }
case WSJTX_MSG.DECODE: { case WSJTX_MSG.DECODE: {
const clientStatus = wsjtxState.clients[msg.id] || {}; const clientStatus = state.clients[msg.id] || {};
const parsed = parseDecodeMessage(msg.message); const parsed = parseDecodeMessage(msg.message);
const decode = { const decode = {
@ -4006,13 +4045,13 @@ function handleWSJTXMessage(msg) {
// Only keep new decodes (not replays) // Only keep new decodes (not replays)
if (msg.isNew) { if (msg.isNew) {
wsjtxState.decodes.push(decode); state.decodes.push(decode);
// Trim old decodes // Trim old decodes
const cutoff = Date.now() - WSJTX_MAX_AGE; const cutoff = Date.now() - WSJTX_MAX_AGE;
while (wsjtxState.decodes.length > WSJTX_MAX_DECODES || while (state.decodes.length > WSJTX_MAX_DECODES ||
(wsjtxState.decodes.length > 0 && wsjtxState.decodes[0].timestamp < cutoff)) { (state.decodes.length > 0 && state.decodes[0].timestamp < cutoff)) {
wsjtxState.decodes.shift(); state.decodes.shift();
} }
} }
break; break;
@ -4020,12 +4059,12 @@ function handleWSJTXMessage(msg) {
case WSJTX_MSG.CLEAR: { case WSJTX_MSG.CLEAR: {
// WSJT-X cleared its band activity - optionally clear our decodes for this client // WSJT-X cleared its band activity - optionally clear our decodes for this client
wsjtxState.decodes = wsjtxState.decodes.filter(d => d.clientId !== msg.id); state.decodes = state.decodes.filter(d => d.clientId !== msg.id);
break; break;
} }
case WSJTX_MSG.QSO_LOGGED: { case WSJTX_MSG.QSO_LOGGED: {
const clientStatus = wsjtxState.clients[msg.id] || {}; const clientStatus = state.clients[msg.id] || {};
const qso = { const qso = {
clientId: msg.id, clientId: msg.id,
dxCall: msg.dxCall, dxCall: msg.dxCall,
@ -4044,9 +4083,9 @@ function handleWSJTXMessage(msg) {
const coords = gridToLatLon(msg.dxGrid); const coords = gridToLatLon(msg.dxGrid);
if (coords) { qso.lat = coords.latitude; qso.lon = coords.longitude; } if (coords) { qso.lat = coords.latitude; qso.lon = coords.longitude; }
} }
wsjtxState.qsos.push(qso); state.qsos.push(qso);
// Keep last 50 QSOs // Keep last 50 QSOs
if (wsjtxState.qsos.length > 50) wsjtxState.qsos.shift(); if (state.qsos.length > 50) state.qsos.shift();
break; break;
} }
@ -4065,14 +4104,14 @@ function handleWSJTXMessage(msg) {
timestamp: msg.timestamp, timestamp: msg.timestamp,
}; };
if (msg.isNew) { if (msg.isNew) {
wsjtxState.wspr.push(wsprDecode); state.wspr.push(wsprDecode);
if (wsjtxState.wspr.length > 100) wsjtxState.wspr.shift(); if (state.wspr.length > 100) state.wspr.shift();
} }
break; break;
} }
case WSJTX_MSG.CLOSE: { case WSJTX_MSG.CLOSE: {
delete wsjtxState.clients[msg.id]; delete state.clients[msg.id];
break; break;
} }
} }
@ -4106,16 +4145,21 @@ if (WSJTX_ENABLED) {
// API endpoint: get WSJT-X data // API endpoint: get WSJT-X data
app.get('/api/wsjtx', (req, res) => { app.get('/api/wsjtx', (req, res) => {
const sessionId = req.query.session || '';
// Use session-specific state for relay mode, or global state for local UDP
const state = (sessionId && WSJTX_RELAY_KEY) ? (wsjtxRelaySessions[sessionId] || { clients: {}, decodes: [], qsos: [], wspr: [], relay: null }) : wsjtxState;
const clients = {}; const clients = {};
for (const [id, client] of Object.entries(wsjtxState.clients)) { for (const [id, client] of Object.entries(state.clients)) {
// Only include clients seen in last 5 minutes // Only include clients seen in last 5 minutes
if (Date.now() - client.lastSeen < 5 * 60 * 1000) { if (Date.now() - client.lastSeen < 5 * 60 * 1000) {
clients[id] = client; clients[id] = client;
} }
} }
// Relay is "connected" if seen in last 60 seconds // Relay is "connected" if this session's relay was seen in last 60 seconds
const relayConnected = wsjtxState.relay && (Date.now() - wsjtxState.relay.lastSeen < 60000); const relayConnected = state.relay && (Date.now() - state.relay.lastSeen < 60000);
res.json({ res.json({
enabled: WSJTX_ENABLED, enabled: WSJTX_ENABLED,
@ -4123,13 +4167,13 @@ app.get('/api/wsjtx', (req, res) => {
relayEnabled: !!WSJTX_RELAY_KEY, relayEnabled: !!WSJTX_RELAY_KEY,
relayConnected: !!relayConnected, relayConnected: !!relayConnected,
clients, clients,
decodes: wsjtxState.decodes.slice(-100), // last 100 decodes: state.decodes.slice(-100), // last 100
qsos: wsjtxState.qsos.slice(-20), // last 20 qsos: state.qsos.slice(-20), // last 20
wspr: wsjtxState.wspr.slice(-50), // last 50 wspr: state.wspr.slice(-50), // last 50
stats: { stats: {
totalDecodes: wsjtxState.decodes.length, totalDecodes: state.decodes.length,
totalQsos: wsjtxState.qsos.length, totalQsos: state.qsos.length,
totalWspr: wsjtxState.wspr.length, totalWspr: state.wspr.length,
activeClients: Object.keys(clients).length, activeClients: Object.keys(clients).length,
} }
}); });
@ -4137,10 +4181,13 @@ app.get('/api/wsjtx', (req, res) => {
// API endpoint: get just decodes (lightweight polling) // API endpoint: get just decodes (lightweight polling)
app.get('/api/wsjtx/decodes', (req, res) => { app.get('/api/wsjtx/decodes', (req, res) => {
const sessionId = req.query.session || '';
const state = (sessionId && WSJTX_RELAY_KEY) ? (wsjtxRelaySessions[sessionId] || { decodes: [] }) : wsjtxState;
const since = parseInt(req.query.since) || 0; const since = parseInt(req.query.since) || 0;
const decodes = since const decodes = since
? wsjtxState.decodes.filter(d => d.timestamp > since) ? state.decodes.filter(d => d.timestamp > since)
: wsjtxState.decodes.slice(-100); : state.decodes.slice(-100);
res.json({ decodes, timestamp: Date.now() }); res.json({ decodes, timestamp: Date.now() });
}); });
@ -4160,9 +4207,17 @@ app.post('/api/wsjtx/relay', (req, res) => {
return res.status(401).json({ error: 'Invalid relay key' }); return res.status(401).json({ error: 'Invalid relay key' });
} }
// Relay heartbeat — just registers the relay as alive // Session ID is required for relay — isolates data per browser
const sessionId = req.body.session || req.headers['x-relay-session'] || '';
if (!sessionId) {
return res.status(400).json({ error: 'Session ID required' });
}
const session = getRelaySession(sessionId);
// Relay heartbeat — just registers the relay as alive for this session
if (req.body && req.body.relay === true) { if (req.body && req.body.relay === true) {
wsjtxState.relay = { session.relay = {
lastSeen: Date.now(), lastSeen: Date.now(),
version: req.body.version || '1.0.0', version: req.body.version || '1.0.0',
port: req.body.port || 2237, port: req.body.port || 2237,
@ -4177,7 +4232,7 @@ app.post('/api/wsjtx/relay', (req, res) => {
} }
// Update relay last seen on every batch too // Update relay last seen on every batch too
wsjtxState.relay = { ...(wsjtxState.relay || {}), lastSeen: Date.now() }; session.relay = { ...(session.relay || {}), lastSeen: Date.now() };
// Rate limit: max 100 messages per request // Rate limit: max 100 messages per request
const batch = messages.slice(0, 100); const batch = messages.slice(0, 100);
@ -4189,7 +4244,7 @@ app.post('/api/wsjtx/relay', (req, res) => {
if (!msg.timestamp || Math.abs(Date.now() - msg.timestamp) > 5 * 60 * 1000) { if (!msg.timestamp || Math.abs(Date.now() - msg.timestamp) > 5 * 60 * 1000) {
msg.timestamp = Date.now(); msg.timestamp = Date.now();
} }
handleWSJTXMessage(msg); handleWSJTXMessage(msg, session);
processed++; processed++;
} }
} }
@ -4231,6 +4286,12 @@ app.get('/api/wsjtx/relay/download/:platform', (req, res) => {
const host = req.headers['x-forwarded-host'] || req.headers.host; const host = req.headers['x-forwarded-host'] || req.headers.host;
const serverURL = proto + '://' + host; const serverURL = proto + '://' + host;
// Session ID from query param — ties this relay to the downloading browser
const sessionId = req.query.session || '';
if (!sessionId) {
return res.status(400).json({ error: 'Session ID required — download from the OpenHamClock dashboard' });
}
if (platform === 'linux' || platform === 'mac') { if (platform === 'linux' || platform === 'mac') {
// Build bash script with relay.js embedded as heredoc // Build bash script with relay.js embedded as heredoc
const lines = [ const lines = [
@ -4272,7 +4333,8 @@ app.get('/api/wsjtx/relay/download/:platform', (req, res) => {
'# Run relay', '# Run relay',
'exec node "$RELAY_FILE" \\', 'exec node "$RELAY_FILE" \\',
' --url "' + serverURL + '" \\', ' --url "' + serverURL + '" \\',
' --key "' + WSJTX_RELAY_KEY + '"', ' --key "' + WSJTX_RELAY_KEY + '" \\',
' --session "' + sessionId + '"',
]; ];
const script = lines.join('\n') + '\n'; const script = lines.join('\n') + '\n';
@ -4371,7 +4433,7 @@ app.get('/api/wsjtx/relay/download/:platform', (req, res) => {
'echo.', 'echo.',
'', '',
':: Run relay', ':: Run relay',
'%NODE_EXE% "%TEMP%\\ohc-relay.js" --url "' + serverURL + '" --key "' + WSJTX_RELAY_KEY + '"', '%NODE_EXE% "%TEMP%\\ohc-relay.js" --url "' + serverURL + '" --key "' + WSJTX_RELAY_KEY + '" --session "' + sessionId + '"',
'', '',
'echo.', 'echo.',
'echo Relay stopped.', 'echo Relay stopped.',

@ -718,6 +718,7 @@ const App = () => {
wsjtxPort={wsjtx.port} wsjtxPort={wsjtx.port}
wsjtxRelayEnabled={wsjtx.relayEnabled} wsjtxRelayEnabled={wsjtx.relayEnabled}
wsjtxRelayConnected={wsjtx.relayConnected} wsjtxRelayConnected={wsjtx.relayConnected}
wsjtxSessionId={wsjtx.sessionId}
showWSJTXOnMap={mapLayers.showWSJTX} showWSJTXOnMap={mapLayers.showWSJTX}
onToggleWSJTXMap={toggleWSJTX} onToggleWSJTXMap={toggleWSJTX}
/> />

@ -28,6 +28,7 @@ const PSKReporterPanel = ({
wsjtxPort, wsjtxPort,
wsjtxRelayEnabled, wsjtxRelayEnabled,
wsjtxRelayConnected, wsjtxRelayConnected,
wsjtxSessionId,
showWSJTXOnMap, showWSJTXOnMap,
onToggleWSJTXMap onToggleWSJTXMap
}) => { }) => {
@ -355,9 +356,7 @@ const PSKReporterPanel = ({
<span style={{ color: '#4ade80', fontWeight: 600 }}>Relay connected</span> <span style={{ color: '#4ade80', fontWeight: 600 }}>Relay connected</span>
</div> </div>
<div style={{ fontSize: '9px', opacity: 0.5 }}> <div style={{ fontSize: '9px', opacity: 0.5 }}>
Waiting for WSJT-X decodes... WSJT-X decodes will appear here when the station is active
<br />
In WSJT-X: Settings Reporting UDP 127.0.0.1:2237
</div> </div>
</div> </div>
) : ( ) : (
@ -366,19 +365,19 @@ const PSKReporterPanel = ({
Download the relay agent for your PC: Download the relay agent for your PC:
</div> </div>
<div style={{ display: 'flex', gap: '4px', justifyContent: 'center', flexWrap: 'wrap' }}> <div style={{ display: 'flex', gap: '4px', justifyContent: 'center', flexWrap: 'wrap' }}>
<a href="/api/wsjtx/relay/download/linux" <a href={`/api/wsjtx/relay/download/linux?session=${wsjtxSessionId || ''}`}
style={{ style={{
padding: '4px 10px', borderRadius: '4px', fontSize: '10px', fontWeight: '600', padding: '4px 10px', borderRadius: '4px', fontSize: '10px', fontWeight: '600',
background: 'rgba(167,139,250,0.2)', border: '1px solid #a78bfa55', background: 'rgba(167,139,250,0.2)', border: '1px solid #a78bfa55',
color: '#a78bfa', textDecoration: 'none', cursor: 'pointer', color: '#a78bfa', textDecoration: 'none', cursor: 'pointer',
}}>🐧 Linux</a> }}>🐧 Linux</a>
<a href="/api/wsjtx/relay/download/mac" <a href={`/api/wsjtx/relay/download/mac?session=${wsjtxSessionId || ''}`}
style={{ style={{
padding: '4px 10px', borderRadius: '4px', fontSize: '10px', fontWeight: '600', padding: '4px 10px', borderRadius: '4px', fontSize: '10px', fontWeight: '600',
background: 'rgba(167,139,250,0.2)', border: '1px solid #a78bfa55', background: 'rgba(167,139,250,0.2)', border: '1px solid #a78bfa55',
color: '#a78bfa', textDecoration: 'none', cursor: 'pointer', color: '#a78bfa', textDecoration: 'none', cursor: 'pointer',
}}>🍎 Mac</a> }}>🍎 Mac</a>
<a href="/api/wsjtx/relay/download/windows" <a href={`/api/wsjtx/relay/download/windows?session=${wsjtxSessionId || ''}`}
style={{ style={{
padding: '4px 10px', borderRadius: '4px', fontSize: '10px', fontWeight: '600', padding: '4px 10px', borderRadius: '4px', fontSize: '10px', fontWeight: '600',
background: 'rgba(167,139,250,0.2)', border: '1px solid #a78bfa55', background: 'rgba(167,139,250,0.2)', border: '1px solid #a78bfa55',

@ -4,6 +4,8 @@
* *
* WSJT-X sends decoded FT8/FT4/JT65/WSPR messages over UDP. * WSJT-X sends decoded FT8/FT4/JT65/WSPR messages over UDP.
* The server listens on the configured port and this hook fetches the results. * The server listens on the configured port and this hook fetches the results.
*
* Each browser gets a unique session ID so relay data is per-user.
*/ */
import { useState, useEffect, useCallback, useRef } from 'react'; import { useState, useEffect, useCallback, useRef } from 'react';
@ -11,7 +13,26 @@ const POLL_INTERVAL = 2000; // Poll every 2 seconds for near-real-time feel
const API_URL = '/api/wsjtx'; const API_URL = '/api/wsjtx';
const DECODES_URL = '/api/wsjtx/decodes'; const DECODES_URL = '/api/wsjtx/decodes';
// Generate or retrieve persistent session ID
function getSessionId() {
const KEY = 'ohc-wsjtx-session';
try {
let id = localStorage.getItem(KEY);
if (id && id.length >= 16) return id;
// Generate a random ID
id = (typeof crypto !== 'undefined' && crypto.randomUUID)
? crypto.randomUUID()
: Math.random().toString(36).substring(2) + Date.now().toString(36) + Math.random().toString(36).substring(2);
localStorage.setItem(KEY, id);
return id;
} catch {
// Fallback for privacy browsers that block localStorage
return Math.random().toString(36).substring(2) + Date.now().toString(36) + Math.random().toString(36).substring(2);
}
}
export function useWSJTX(enabled = true) { export function useWSJTX(enabled = true) {
const [sessionId] = useState(getSessionId);
const [data, setData] = useState({ const [data, setData] = useState({
clients: {}, clients: {},
decodes: [], decodes: [],
@ -30,9 +51,11 @@ export function useWSJTX(enabled = true) {
const pollDecodes = useCallback(async () => { const pollDecodes = useCallback(async () => {
if (!enabled) return; if (!enabled) return;
try { try {
const url = lastTimestamp.current const base = lastTimestamp.current
? `${DECODES_URL}?since=${lastTimestamp.current}` ? `${DECODES_URL}?since=${lastTimestamp.current}`
: DECODES_URL; : DECODES_URL;
const sep = base.includes('?') ? '&' : '?';
const url = `${base}${sep}session=${sessionId}`;
const res = await fetch(url); const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`); if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = await res.json(); const json = await res.json();
@ -54,13 +77,13 @@ export function useWSJTX(enabled = true) {
} catch (e) { } catch (e) {
// Silent fail for lightweight polls // Silent fail for lightweight polls
} }
}, [enabled]); }, [enabled, sessionId]);
// Full fetch - get everything including status, QSOs, clients // Full fetch - get everything including status, QSOs, clients
const fetchFull = useCallback(async () => { const fetchFull = useCallback(async () => {
if (!enabled) return; if (!enabled) return;
try { try {
const res = await fetch(API_URL); const res = await fetch(`${API_URL}?session=${sessionId}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`); if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = await res.json(); const json = await res.json();
setData(json); setData(json);
@ -71,7 +94,7 @@ export function useWSJTX(enabled = true) {
setError(e.message); setError(e.message);
setLoading(false); setLoading(false);
} }
}, [enabled]); }, [enabled, sessionId]);
// Initial full fetch // Initial full fetch
useEffect(() => { useEffect(() => {
@ -99,6 +122,7 @@ export function useWSJTX(enabled = true) {
...data, ...data,
loading, loading,
error, error,
sessionId,
refresh: fetchFull, refresh: fetchFull,
}; };
} }

@ -34,6 +34,7 @@ function parseArgs() {
const config = { const config = {
url: process.env.OPENHAMCLOCK_URL || '', url: process.env.OPENHAMCLOCK_URL || '',
key: process.env.RELAY_KEY || process.env.OPENHAMCLOCK_RELAY_KEY || '', key: process.env.RELAY_KEY || process.env.OPENHAMCLOCK_RELAY_KEY || '',
session: process.env.RELAY_SESSION || '',
port: parseInt(process.env.WSJTX_UDP_PORT || '2237'), port: parseInt(process.env.WSJTX_UDP_PORT || '2237'),
batchInterval: parseInt(process.env.BATCH_INTERVAL || '2000'), batchInterval: parseInt(process.env.BATCH_INTERVAL || '2000'),
verbose: process.env.VERBOSE === 'true', verbose: process.env.VERBOSE === 'true',
@ -43,6 +44,7 @@ function parseArgs() {
switch (args[i]) { switch (args[i]) {
case '--url': case '-u': config.url = args[++i]; break; case '--url': case '-u': config.url = args[++i]; break;
case '--key': case '-k': config.key = args[++i]; break; case '--key': case '-k': config.key = args[++i]; break;
case '--session': case '-s': config.session = args[++i]; break;
case '--port': case '-p': config.port = parseInt(args[++i]); break; case '--port': case '-p': config.port = parseInt(args[++i]); break;
case '--interval': case '-i': config.batchInterval = parseInt(args[++i]); break; case '--interval': case '-i': config.batchInterval = parseInt(args[++i]); break;
case '--verbose': case '-v': config.verbose = true; break; case '--verbose': case '-v': config.verbose = true; break;
@ -56,6 +58,7 @@ a remote OpenHamClock server.
Options: Options:
--url, -u <url> OpenHamClock server URL (required) --url, -u <url> OpenHamClock server URL (required)
--key, -k <key> Relay authentication key (required) --key, -k <key> Relay authentication key (required)
--session, -s <id> Browser session ID (required for per-user isolation)
--port, -p <port> Local UDP port to listen on (default: 2237) --port, -p <port> Local UDP port to listen on (default: 2237)
--interval, -i <ms> Batch send interval in ms (default: 2000) --interval, -i <ms> Batch send interval in ms (default: 2000)
--verbose, -v Show all decoded messages --verbose, -v Show all decoded messages
@ -64,6 +67,7 @@ Options:
Environment variables: Environment variables:
OPENHAMCLOCK_URL Same as --url OPENHAMCLOCK_URL Same as --url
RELAY_KEY Same as --key RELAY_KEY Same as --key
RELAY_SESSION Same as --session
WSJTX_UDP_PORT Same as --port WSJTX_UDP_PORT Same as --port
BATCH_INTERVAL Same as --interval BATCH_INTERVAL Same as --interval
VERBOSE Set to 'true' for verbose output VERBOSE Set to 'true' for verbose output
@ -91,6 +95,11 @@ if (!config.key) {
console.error(' Run with --help for usage info'); console.error(' Run with --help for usage info');
process.exit(1); process.exit(1);
} }
if (!config.session) {
console.error('❌ Error: --session is required (auto-generated by the download script)');
console.error(' Re-download the relay from your OpenHamClock dashboard');
process.exit(1);
}
// Normalize URL // Normalize URL
const serverUrl = config.url.replace(/\/$/, ''); const serverUrl = config.url.replace(/\/$/, '');
@ -293,7 +302,7 @@ function sendBatch() {
const batch = messageQueue.splice(0, messageQueue.length); const batch = messageQueue.splice(0, messageQueue.length);
sendInFlight = true; sendInFlight = true;
const body = JSON.stringify({ messages: batch }); const body = JSON.stringify({ messages: batch, session: config.session });
const parsed = new URL(relayEndpoint); const parsed = new URL(relayEndpoint);
const transport = parsed.protocol === 'https:' ? https : http; const transport = parsed.protocol === 'https:' ? https : http;
@ -433,7 +442,7 @@ socket.on('listening', () => {
// Send relay heartbeat immediately, then every 30s // Send relay heartbeat immediately, then every 30s
// This tells the server the relay is alive even before WSJT-X sends any packets // This tells the server the relay is alive even before WSJT-X sends any packets
function sendHeartbeat() { function sendHeartbeat() {
const body = JSON.stringify({ relay: true, version: '1.0.0', port: config.port }); const body = JSON.stringify({ relay: true, version: '1.0.0', port: config.port, session: config.session });
const parsed = new URL(relayEndpoint); const parsed = new URL(relayEndpoint);
const transport = parsed.protocol === 'https:' ? https : http; const transport = parsed.protocol === 'https:' ? https : http;

Loading…
Cancel
Save

Powered by TurnKey Linux.