diff --git a/README.md b/README.md index a13fedfa..efb0b078 100644 --- a/README.md +++ b/README.md @@ -653,13 +653,13 @@ Operators at shelters or EOCs can encode structured resource data in their APRS Token format (within the 67-character APRS comment field): -``` -[Key Value] — quantity (e.g., [Food 50]) -[Key Current/Max] — capacity (e.g., [Beds 30/100]) -[Key -Value] — resource NEEDED (e.g., [Water -100]) -[Key OK] — status nominal -[Key !] — critical alert -``` +| Token. | Content | +| ----------------- | ------------------------------------ | +| [Key Value] | quantity (e.g., [Food 50]) | +| [Key Current/Max] | capacity (e.g., [Beds 30/100]) | +| [Key -Value] | resource NEEDED (e.g., [Water -100]) | +| [Key OK] | status nominal | +| [Key !] | critical alert | Built-in token keys with icons: `Beds`, `Water`, `Food`, `Power`, `Fuel`, `Med`, `Staff`, `Evac`, `Comms`, `Gen`. The parser accepts any key — unknown keys display with a generic icon. diff --git a/server/routes/wsjtx.js b/server/routes/wsjtx.js index f12f5106..42bed652 100644 --- a/server/routes/wsjtx.js +++ b/server/routes/wsjtx.js @@ -1145,6 +1145,18 @@ module.exports = function (app, ctx) { error: 'Session ID required — download from the OpenHamClock dashboard', }); } + // Get the multicast address if we were passed one + const multicastAddress = req.query.multicast; + + // Check to see if it is valid + if (multicastAddress) { + const parts = multicastAddress.split('.').map(Number); + if (parts.length !== 4 || parts[0] < 224 || parts[0] > 239) { + return res.status(400).json({ + error: `${multicastAddress}: Invalid multicast address (must be 224.0.0.0–239.255.255.255)`, + }); + } + } // SECURITY: Validate platform parameter if (!['linux', 'mac', 'windows'].includes(platform)) { @@ -1159,6 +1171,7 @@ module.exports = function (app, ctx) { const safeServerURL = sanitizeForShell(serverURL); const safeSessionId = sanitizeForShell(sessionId); const safeRelayKey = sanitizeForShell(WSJTX_RELAY_KEY); + const safeMulticastAddress = multicastAddress ? sanitizeForShell(multicastAddress) : ''; if (platform === 'linux' || platform === 'mac') { // Build bash script with relay.js embedded as heredoc @@ -1172,7 +1185,7 @@ module.exports = function (app, ctx) { '# Requires: Node.js 14+ (https://nodejs.org)', '#', '# In WSJT-X: Settings > Reporting > UDP Server', - '# Address: 127.0.0.1 Port: 2237', + '# Address: ' + (multicastAddress ? safeMulticastAddress : '127.0.0.1') + ' Port: 2237', '', 'set -e', '', @@ -1190,11 +1203,11 @@ module.exports = function (app, ctx) { ' exit 1', 'fi', '', - '# Write relay agent to temp file', - 'RELAY_FILE=$(mktemp /tmp/ohc-relay-XXXXXX.js)', + '# Write relay agent to temp file (unfortunately mktemp on macOS does not work the same as on Linux', + 'RELAY_FILE=$([[ "$(uname -s)" == Linux ]] && mktemp /tmp/ohc-relay-XXXXXX.js || echo /tmp/ohc-relay-$$.js)', 'trap "rm -f $RELAY_FILE" EXIT', '', - 'cat > "$RELAY_FILE" << \'OPENHAMCLOCK_RELAY_EOF\'', + 'cat > "${RELAY_FILE}" << \'OPENHAMCLOCK_RELAY_EOF\'', relayJs, 'OPENHAMCLOCK_RELAY_EOF', '', @@ -1202,6 +1215,7 @@ module.exports = function (app, ctx) { 'exec node "$RELAY_FILE" \\', ' --url "' + safeServerURL + '" \\', ' --key "' + safeRelayKey + '" \\', + multicastAddress ? ' --multicast "' + safeMulticastAddress + '" \\' : '', ' --session "' + safeSessionId + '"', ]; @@ -1302,19 +1316,22 @@ module.exports = function (app, ctx) { 'echo Relay agent ready.', 'echo.', 'echo In WSJT-X: Settings ^> Reporting ^> UDP Server', - 'echo Address: 127.0.0.1 Port: 2237', + 'echo Address: ' + (multicastAddress ? safeMulticastAddress : '127.0.0.1') + ' Port: 2237', 'echo.', 'echo Press Ctrl+C to stop', 'echo.', '', ':: Run relay', '%NODE_EXE% "%TEMP%\\ohc-relay.js" --url "' + - safeServerURL + - '" --key "' + - safeRelayKey + - '" --session "' + - safeSessionId + - '"', + safeServerURL + + '" --key "' + + safeRelayKey + + '" --session "' + + safeSessionId + + '"' + + multicastAddress + ? ' --multicast "' + safeMulticastAddress + "'" + : '', '', 'echo.', 'echo Relay stopped.', diff --git a/src/DockableApp.jsx b/src/DockableApp.jsx index 56318449..a62b56cf 100644 --- a/src/DockableApp.jsx +++ b/src/DockableApp.jsx @@ -833,6 +833,7 @@ export const DockableApp = ({ wsjtxSessionId={wsjtx.sessionId} showWSJTXOnMap={mapLayersEff.showWSJTX} onToggleWSJTXMap={toggleWSJTXEff} + wsjtxRelayMulticast={config.wsjtxRelayMulticast} /> ); break; diff --git a/src/components/PSKReporterPanel.jsx b/src/components/PSKReporterPanel.jsx index 86e81e2f..98cd5862 100644 --- a/src/components/PSKReporterPanel.jsx +++ b/src/components/PSKReporterPanel.jsx @@ -40,6 +40,7 @@ const PSKReporterPanel = ({ wsjtxSessionId, showWSJTXOnMap, onToggleWSJTXMap, + wsjtxRelayMulticast = { enabled: false, address: '224.0.0.1' }, }) => { const { t } = useTranslation(); const [panelMode, setPanelMode] = useState(() => { @@ -146,6 +147,10 @@ const PSKReporterPanel = ({ const activeClients = Object.entries(wsjtxClients); const primaryClient = activeClients[0]?.[1] || null; const isWSPRMode = primaryClient?.mode?.toUpperCase() === 'WSPR'; + const wsjtxDownloadParams = + 'session=' + + (wsjtxSessionId ? wsjtxSessionId : '') + + (wsjtxRelayMulticast.enabled ? `&multicast=${wsjtxRelayMulticast.address}` : ''); // WSPR decodes filtered by age const filteredWspr = useMemo(() => { @@ -643,9 +648,23 @@ const PSKReporterPanel = ({ ) : (
{t('pskReporterPanel.wsjtx.downloadRelay')}
+ {wsjtxRelayMulticast.enabled ? ( +
Multicast Address: {wsjtxRelayMulticast.address}
+ ) : ( + '' + )} +
+ + Installation Instructions + +
{ @@ -428,7 +432,7 @@ export const SettingsPanel = ({ // units, allUnits: { dist: distUnits, temp: tempUnits, press: pressUnits }, propagation: { mode: propMode, power: parseFloat(propPower) || 100 }, - + wsjtxRelayMulticast: { enabled: wsjtxMulticastEnabled, address: wsjtxMulticastAddress }, rigControl: { enabled: rigEnabled, host: rigHost, @@ -1123,6 +1127,50 @@ export const SettingsPanel = ({
+ {/* WSJTX Relay Multicast Options */} +
+ +
+ setWsjtxMulticastEnabled(e.target.checked)} + style={{ marginRight: '8px' }} + /> + Use multicast address   + setWsjtxMulticastAddress(e.target.value.toUpperCase())} + style={{ + width: '10%', + marginLeft: '8px', + padding: '8px 12px', + background: 'var(--bg-primary)', + border: '1px solid var(--border-color)', + borderRadius: '4px', + color: wsjtxMulticastEnabled ? 'var(--text-primary)' : 'var(--text-secondary', + fontSize: '12px', + fontFamily: 'JetBrains Mono, monospace', + boxSizing: 'border-box', + }} + /> + If you are going to run a wsjt-x relay, define here if you need a multicast listener and what address it + should be using. +
+
+ {/* Rig Control Settings */}