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
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
39 changes: 28 additions & 11 deletions server/routes/wsjtx.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand All @@ -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
Expand All @@ -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',
'',
Expand All @@ -1190,18 +1203,19 @@ 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',
'',
'# Run relay',
'exec node "$RELAY_FILE" \\',
' --url "' + safeServerURL + '" \\',
' --key "' + safeRelayKey + '" \\',
multicastAddress ? ' --multicast "' + safeMulticastAddress + '" \\' : '',
' --session "' + safeSessionId + '"',
];

Expand Down Expand Up @@ -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.',
Expand Down
1 change: 1 addition & 0 deletions src/DockableApp.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -833,6 +833,7 @@ export const DockableApp = ({
wsjtxSessionId={wsjtx.sessionId}
showWSJTXOnMap={mapLayersEff.showWSJTX}
onToggleWSJTXMap={toggleWSJTXEff}
wsjtxRelayMulticast={config.wsjtxRelayMulticast}
/>
);
break;
Expand Down
25 changes: 22 additions & 3 deletions src/components/PSKReporterPanel.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const PSKReporterPanel = ({
wsjtxSessionId,
showWSJTXOnMap,
onToggleWSJTXMap,
wsjtxRelayMulticast = { enabled: false, address: '224.0.0.1' },
}) => {
const { t } = useTranslation();
const [panelMode, setPanelMode] = useState(() => {
Expand Down Expand Up @@ -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(() => {
Expand Down Expand Up @@ -643,9 +648,23 @@ const PSKReporterPanel = ({
) : (
<div style={{ fontSize: '10px', opacity: 0.8, lineHeight: 1.6 }}>
<div style={{ marginBottom: '8px' }}>{t('pskReporterPanel.wsjtx.downloadRelay')}</div>
{wsjtxRelayMulticast.enabled ? (
<div style={{ marginBottom: '8px' }}>Multicast Address: {wsjtxRelayMulticast.address}</div>
) : (
''
)}
<div style={{ marginBottom: '8px' }}>
<a
style={{ color: 'var(--text-primary)' }}
target="_blank"
href="https://github.com/accius/openhamclock/blob/main/wsjtx-relay/README.md"
>
Installation Instructions
</a>
</div>
<div style={{ display: 'flex', gap: '4px', justifyContent: 'center', flexWrap: 'wrap' }}>
<a
href={`/api/wsjtx/relay/download/linux?session=${wsjtxSessionId || ''}`}
href={`/api/wsjtx/relay/download/linux?${wsjtxDownloadParams}`}
style={{
padding: '4px 10px',
borderRadius: '4px',
Expand All @@ -661,7 +680,7 @@ const PSKReporterPanel = ({
{t('pskReporterPanel.wsjtx.platformLinux')}
</a>
<a
href={`/api/wsjtx/relay/download/mac?session=${wsjtxSessionId || ''}`}
href={`/api/wsjtx/relay/download/mac?${wsjtxDownloadParams}`}
style={{
padding: '4px 10px',
borderRadius: '4px',
Expand All @@ -677,7 +696,7 @@ const PSKReporterPanel = ({
{t('pskReporterPanel.wsjtx.platformMac')}
</a>
<a
href={`/api/wsjtx/relay/download/windows?session=${wsjtxSessionId || ''}`}
href={`/api/wsjtx/relay/download/windows?${wsjtxDownloadParams}`}
style={{
padding: '4px 10px',
borderRadius: '4px',
Expand Down
50 changes: 49 additions & 1 deletion src/components/SettingsPanel.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ export const SettingsPanel = ({
return false;
}
});
const [wsjtxMulticastEnabled, setWsjtxMulticastEnabled] = useState(config?.wsjtxRelayMulticast.enabled || false);
const [wsjtxMulticastAddress, setWsjtxMulticastAddress] = useState(
config?.wsjtxRelayMulticast.address || '224.0.0.1',
);

// Local-only integration flags
const [n3fjpEnabled, setN3fjpEnabled] = useState(() => {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -1123,6 +1127,50 @@ export const SettingsPanel = ({
</div>
</div>

{/* WSJTX Relay Multicast Options */}
<div style={{ marginBottom: '20px' }}>
<label
style={{
display: 'block',
marginBottom: '8px',
color: 'var(--text-muted)',
fontSize: '11px',
textTransform: 'uppercase',
letterSpacing: '1px',
}}
>
🔁 WSJTX Relay Multicast Options
</label>
<div style={{ fontSize: '11px', color: 'var(--text-muted)', marginBottom: '8px', lineHeight: 1.4 }}>
<input
type="checkbox"
checked={wsjtxMulticastEnabled}
onChange={(e) => setWsjtxMulticastEnabled(e.target.checked)}
style={{ marginRight: '8px' }}
/>
<span style={{ color: 'var(--text-primary)', fontSize: '14px' }}>Use multicast address &nbsp;</span>
<input
type="text"
value={wsjtxMulticastAddress}
onChange={(e) => 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.
</div>
</div>

{/* Rig Control Settings */}
<div style={{ marginBottom: '20px' }}>
<label
Expand Down
1 change: 1 addition & 0 deletions src/layouts/ModernLayout.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,7 @@ export default function ModernLayout(props) {
wsjtxSessionId={wsjtx.sessionId}
showWSJTXOnMap={mapLayers.showWSJTX}
onToggleWSJTXMap={toggleWSJTX}
wsjtxRelayMulticast={config.wsjtxRelayMulticast}
/>
);

Expand Down
1 change: 1 addition & 0 deletions src/utils/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export const DEFAULT_CONFIG = {
dxClusterSource: 'dxspider-proxy',
customDxCluster: { enabled: false, host: '', port: 7300 },
udpDxCluster: { host: '', port: 12060 },
wsjtxRelayMulticast: { enabled: false, address: '224.0.0.1' },
};

// Cache for server config
Expand Down
Loading