adjusting how dxcluster works

pull/27/head
accius 5 days ago
parent 9dab93ca06
commit e7788ef6be

@ -691,8 +691,7 @@
call: s.call || s.dx_call || 'UNKNOWN', call: s.call || s.dx_call || 'UNKNOWN',
comment: s.comment || s.info || '', comment: s.comment || s.info || '',
time: s.time || new Date().toISOString().substr(11, 5) + 'z', time: s.time || new Date().toISOString().substr(11, 5) + 'z',
spotter: s.spotter || '', spotter: s.spotter || ''
source: s.source || ''
}))); })));
} else { } else {
setData([{ setData([{
@ -2595,19 +2594,13 @@
}} }}
> >
<option value="auto">Auto (Best Available)</option> <option value="auto">Auto (Best Available)</option>
<option value="hamqth">HamQTH</option> <option value="hamqth">HamQTH (HTTP)</option>
<option value="iu1bow">IU1BOW DX Spider</option> <option value="dxspider">DX Spider (Telnet)</option>
<option value="spothole">Spothole</option>
<option value="dxheat">DXHeat</option>
<option value="dxsummit">DX Summit</option>
</select> </select>
<p style={{ fontSize: '12px', color: 'var(--text-muted)', marginTop: '8px' }}> <p style={{ fontSize: '12px', color: 'var(--text-muted)', marginTop: '8px' }}>
{dxClusterSource === 'auto' && '→ Automatically selects the best available source'} {dxClusterSource === 'auto' && '→ Tries HamQTH first, then DX Spider telnet'}
{dxClusterSource === 'hamqth' && '→ HamQTH.com DX Cluster CSV feed'} {dxClusterSource === 'hamqth' && '→ HamQTH.com CSV feed (works on all platforms)'}
{dxClusterSource === 'iu1bow' && '→ IU1BOW.it Spiderweb cluster (HTTP API)'} {dxClusterSource === 'dxspider' && '→ Telnet to dxspider.co.uk:7300 (works locally/Pi, may fail on cloud hosting)'}
{dxClusterSource === 'spothole' && '→ Spothole.app aggregated DX cluster'}
{dxClusterSource === 'dxheat' && '→ DXHeat.com real-time cluster'}
{dxClusterSource === 'dxsummit' && '→ DXSummit.fi cluster (may be slow)'}
</p> </p>
</div> </div>

@ -15,6 +15,7 @@ const express = require('express');
const cors = require('cors'); const cors = require('cors');
const path = require('path'); const path = require('path');
const fetch = require('node-fetch'); const fetch = require('node-fetch');
const net = require('net');
const app = express(); const app = express();
const PORT = process.env.PORT || 3000; const PORT = process.env.PORT || 3000;
@ -116,68 +117,64 @@ app.get('/api/hamqsl/conditions', async (req, res) => {
}); });
// DX Cluster proxy - fetches from selectable sources // DX Cluster proxy - fetches from selectable sources
// Query param: ?source=hamqth|dxheat|dxsummit|jo30|auto (default: auto) // Query param: ?source=hamqth|dxspider|auto (default: auto)
// Note: DX Spider uses telnet - works locally but may be blocked on cloud hosting
// Note: DX Spider (telnet) removed - doesn't work on hosted platforms // Cache for DX Spider telnet spots (to avoid excessive connections)
// Using HTTP-based APIs only for online compatibility let dxSpiderCache = { spots: [], timestamp: 0 };
const DXSPIDER_CACHE_TTL = 60000; // 60 seconds cache
app.get('/api/dxcluster/spots', async (req, res) => { app.get('/api/dxcluster/spots', async (req, res) => {
const source = (req.query.source || 'auto').toLowerCase(); const source = (req.query.source || 'auto').toLowerCase();
// Helper function for HamQTH // Helper function for HamQTH (HTTP-based, works everywhere)
async function fetchHamQTH() { async function fetchHamQTH() {
const controller = new AbortController(); const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 8000); const timeout = setTimeout(() => controller.abort(), 10000);
try { try {
const response = await fetch('https://www.hamqth.com/dxc_csv.php', { const response = await fetch('https://www.hamqth.com/dxc_csv.php?limit=25', {
headers: { 'User-Agent': 'OpenHamClock/3.4' }, headers: { 'User-Agent': 'OpenHamClock/3.5' },
signal: controller.signal signal: controller.signal
}); });
clearTimeout(timeout); clearTimeout(timeout);
if (response.ok) { if (response.ok) {
const text = await response.text(); const text = await response.text();
const lines = text.trim().split('\n').filter(line => line.trim() && !line.startsWith('#')); // HamQTH CSV format: Spotter^Frequency^DXCall^Comment^TimeDate^^^Continent^Band^Country^DXCC
// Example: KF0NYM^18070.0^TX5U^Correction, Good Sig MO, 73^2149 2025-05-27^^^EU^17M^France^227
const lines = text.trim().split('\n').filter(line => line.includes('^'));
if (lines.length > 0) { if (lines.length > 0) {
const spots = []; const spots = lines.slice(0, 25).map(line => {
for (const line of lines.slice(0, 25)) {
const parts = line.split('^'); const parts = line.split('^');
const spotter = parts[0] || '';
const freqKhz = parseFloat(parts[1]) || 0;
const dxCall = parts[2] || 'UNKNOWN';
const comment = parts[3] || '';
const timeDate = parts[4] || '';
if (parts.length >= 5) { // Frequency: convert from kHz to MHz
const spotter = parts[0] || ''; const freqMhz = freqKhz > 1000 ? (freqKhz / 1000).toFixed(3) : String(freqKhz);
const freqKhz = parts[1] || '';
const dxCall = parts[2] || ''; // Time: extract HHMM from "2149 2025-05-27" format
const comment = parts[3] || ''; let time = '';
const timeDate = parts[4] || ''; if (timeDate && timeDate.length >= 4) {
const band = parts[9] || ''; const timeStr = timeDate.substring(0, 4);
time = timeStr.substring(0, 2) + ':' + timeStr.substring(2, 4) + 'z';
const freqNum = parseFloat(freqKhz);
if (!isNaN(freqNum) && freqNum > 0 && dxCall) {
const freqMhz = (freqNum / 1000).toFixed(3);
let time = '';
if (timeDate && timeDate.length >= 4) {
const timeStr = timeDate.substring(0, 4);
time = timeStr.substring(0, 2) + ':' + timeStr.substring(2, 4) + 'z';
}
spots.push({
freq: freqMhz,
call: dxCall,
comment: comment + (band ? ' ' + band : ''),
time: time,
spotter: spotter,
source: 'HamQTH'
});
}
} }
}
return {
if (spots.length > 0) { freq: freqMhz,
console.log('[DX Cluster] HamQTH:', spots.length, 'spots'); call: dxCall,
return spots; comment: comment,
} time: time,
spotter: spotter,
source: 'HamQTH'
};
});
console.log('[DX Cluster] HamQTH:', spots.length, 'spots');
return spots;
} }
} }
} catch (error) { } catch (error) {
@ -189,194 +186,120 @@ app.get('/api/dxcluster/spots', async (req, res) => {
return null; return null;
} }
// Helper function for DXHeat // Helper function for DX Spider (telnet-based, works locally/Pi)
async function fetchDXHeat() { async function fetchDXSpider() {
const controller = new AbortController(); // Check cache first
const timeout = setTimeout(() => controller.abort(), 8000); if (Date.now() - dxSpiderCache.timestamp < DXSPIDER_CACHE_TTL && dxSpiderCache.spots.length > 0) {
console.log('[DX Cluster] DX Spider: returning', dxSpiderCache.spots.length, 'cached spots');
return dxSpiderCache.spots;
}
try { return new Promise((resolve) => {
const response = await fetch('https://dxheat.com/dxc/data.php', { const spots = [];
headers: { let buffer = '';
'User-Agent': 'OpenHamClock/3.4', let loginSent = false;
'Accept': 'application/json' let commandSent = false;
},
signal: controller.signal const client = new net.Socket();
client.setTimeout(15000);
// Try connecting to DX Spider node
client.connect(7300, 'dxspider.co.uk', () => {
console.log('[DX Cluster] DX Spider: connected to dxspider.co.uk:7300');
}); });
clearTimeout(timeout);
if (response.ok) { client.on('data', (data) => {
const text = await response.text(); buffer += data.toString();
const data = JSON.parse(text);
const spots = data.spots || data;
if (Array.isArray(spots) && spots.length > 0) { // Wait for login prompt
const mapped = spots.slice(0, 25).map(spot => ({ if (!loginSent && (buffer.includes('login:') || buffer.includes('Please enter your call') || buffer.includes('enter your callsign'))) {
freq: spot.f || spot.frequency || '0.000', loginSent = true;
call: spot.c || spot.dx || spot.callsign || 'UNKNOWN', client.write('GUEST\r\n');
comment: spot.i || spot.info || '', console.log('[DX Cluster] DX Spider: sent login');
time: spot.t ? String(spot.t).substring(11, 16) + 'z' : '', return;
spotter: spot.s || spot.spotter || '',
source: 'DXHeat'
}));
console.log('[DX Cluster] DXHeat:', mapped.length, 'spots');
return mapped;
} }
}
} catch (error) {
clearTimeout(timeout);
if (error.name !== 'AbortError') {
console.error('[DX Cluster] DXHeat error:', error.message);
}
}
return null;
}
// Helper function for DX Summit
async function fetchDXSummit() {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 8000);
try {
const response = await fetch('https://www.dxsummit.fi/api/v1/spots?limit=25', {
headers: {
'User-Agent': 'OpenHamClock/3.4 (Amateur Radio Dashboard)',
'Accept': 'application/json'
},
signal: controller.signal
});
clearTimeout(timeout);
if (response.ok) {
const text = await response.text();
const data = JSON.parse(text);
if (Array.isArray(data) && data.length > 0) { // Wait for prompt after login, then send command
const spots = data.slice(0, 25).map(spot => ({ if (loginSent && !commandSent && (buffer.includes('Hello') || buffer.includes('de ') || buffer.includes('>') || buffer.includes('GUEST'))) {
freq: spot.frequency ? String(spot.frequency) : '0.000', commandSent = true;
call: spot.dx_call || spot.dxcall || spot.callsign || 'UNKNOWN', setTimeout(() => {
comment: spot.info || spot.comment || '', client.write('sh/dx 25\r\n');
time: spot.time ? String(spot.time).substring(0, 5) + 'z' : '', console.log('[DX Cluster] DX Spider: sent sh/dx 25');
spotter: spot.spotter || spot.de || '', }, 1000);
source: 'DX Summit' return;
}));
console.log('[DX Cluster] DX Summit:', spots.length, 'spots');
return spots;
} }
}
} catch (error) {
clearTimeout(timeout);
if (error.name !== 'AbortError') {
console.error('[DX Cluster] DX Summit error:', error.message);
}
}
return null;
}
// Helper function for IU1BOW Spiderweb (HTTP-based DX Spider web interface)
async function fetchIU1BOW() {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 8000);
try {
const response = await fetch('https://www.iu1bow.it/spotlist', {
headers: {
'User-Agent': 'OpenHamClock/3.4',
'Accept': 'application/json'
},
signal: controller.signal
});
clearTimeout(timeout);
if (response.ok) {
const data = await response.json();
if (Array.isArray(data) && data.length > 0) { // Parse DX spots from the output
const spots = data.slice(0, 25).map(spot => { // Format: DX de W3LPL: 14195.0 TI5/AA8HH FT8 -09 dB 1234Z
// IU1BOW format varies, common fields: freq, spotcall/dx_call, spotter, time, comment const lines = buffer.split('\n');
const freqVal = spot.freq || spot.frequency || 0; for (const line of lines) {
const freqMhz = freqVal > 1000 ? (freqVal / 1000).toFixed(3) : String(freqVal).includes('.') ? String(freqVal) : (freqVal / 1000).toFixed(3); if (line.includes('DX de ')) {
let time = ''; const match = line.match(/DX de ([A-Z0-9\/\-]+):\s+(\d+\.?\d*)\s+([A-Z0-9\/\-]+)\s+(.+?)\s+(\d{4})Z/i);
if (spot.time) { if (match) {
// Time might be Unix timestamp or string const spotter = match[1].replace(':', '');
if (typeof spot.time === 'number') { const freqKhz = parseFloat(match[2]);
const d = new Date(spot.time * 1000); const dxCall = match[3];
time = d.toISOString().substring(11, 16) + 'z'; const comment = match[4].trim();
} else { const timeStr = match[5];
time = String(spot.time).substring(0, 5) + 'z';
if (!isNaN(freqKhz) && freqKhz > 0 && dxCall) {
const freqMhz = (freqKhz / 1000).toFixed(3);
const time = timeStr.substring(0, 2) + ':' + timeStr.substring(2, 4) + 'z';
// Avoid duplicates
if (!spots.find(s => s.call === dxCall && s.freq === freqMhz)) {
spots.push({
freq: freqMhz,
call: dxCall,
comment: comment,
time: time,
spotter: spotter,
source: 'DX Spider'
});
}
} }
} }
return { }
freq: freqMhz, }
call: spot.spotcall || spot.dx_call || spot.dx || 'UNKNOWN',
comment: spot.comment || spot.info || '', // If we have enough spots, close connection
time: time, if (spots.length >= 20) {
spotter: spot.spotter || spot.de || '', client.write('bye\r\n');
source: 'IU1BOW DX Spider' setTimeout(() => client.destroy(), 500);
};
});
console.log('[DX Cluster] IU1BOW:', spots.length, 'spots');
return spots;
} }
}
} catch (error) {
clearTimeout(timeout);
if (error.name !== 'AbortError') {
console.error('[DX Cluster] IU1BOW error:', error.message);
}
}
return null;
}
// Helper function for Spothole (aggregated DX cluster + xOTA spots)
async function fetchSpothole() {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 8000);
try {
// Spothole API endpoint - filter for DX cluster spots only
const response = await fetch('https://spothole.app/api/spots?sources=dxcluster&limit=25', {
headers: {
'User-Agent': 'OpenHamClock/3.4',
'Accept': 'application/json'
},
signal: controller.signal
}); });
clearTimeout(timeout);
if (response.ok) { client.on('timeout', () => {
const data = await response.json(); console.log('[DX Cluster] DX Spider: timeout');
const spotsList = data.spots || data; client.destroy();
});
if (Array.isArray(spotsList) && spotsList.length > 0) {
const spots = spotsList.slice(0, 25).map(spot => { client.on('error', (err) => {
// Spothole format: dx, frequency, mode, comment, de, time, etc. console.error('[DX Cluster] DX Spider error:', err.message);
const freqVal = spot.frequency || spot.freq || 0; client.destroy();
const freqMhz = freqVal > 1000 ? (freqVal / 1000).toFixed(3) : String(freqVal); });
let time = '';
if (spot.time || spot.timestamp) { client.on('close', () => {
const d = new Date(spot.time || spot.timestamp); if (spots.length > 0) {
time = d.toISOString().substring(11, 16) + 'z'; console.log('[DX Cluster] DX Spider:', spots.length, 'spots');
} dxSpiderCache = { spots: spots, timestamp: Date.now() };
return { resolve(spots);
freq: freqMhz, } else {
call: spot.dx || spot.call || spot.spotted || 'UNKNOWN', console.log('[DX Cluster] DX Spider: no spots received');
comment: spot.comment || spot.info || spot.mode || '', resolve(null);
time: time,
spotter: spot.de || spot.spotter || '',
source: 'Spothole'
};
});
console.log('[DX Cluster] Spothole:', spots.length, 'spots');
return spots;
} }
} });
} catch (error) {
clearTimeout(timeout); // Fallback timeout - close after 20 seconds regardless
if (error.name !== 'AbortError') { setTimeout(() => {
console.error('[DX Cluster] Spothole error:', error.message); if (spots.length > 0) {
} client.destroy();
} } else if (client.readable) {
return null; client.destroy();
resolve(null);
}
}, 20000);
});
} }
// Fetch based on selected source // Fetch based on selected source
@ -384,21 +307,19 @@ app.get('/api/dxcluster/spots', async (req, res) => {
if (source === 'hamqth') { if (source === 'hamqth') {
spots = await fetchHamQTH(); spots = await fetchHamQTH();
} else if (source === 'dxheat') { } else if (source === 'dxspider') {
spots = await fetchDXHeat(); spots = await fetchDXSpider();
} else if (source === 'dxsummit') { // Fallback to HamQTH if DX Spider fails
spots = await fetchDXSummit(); if (!spots) {
} else if (source === 'iu1bow') { console.log('[DX Cluster] DX Spider failed, falling back to HamQTH');
spots = await fetchIU1BOW(); spots = await fetchHamQTH();
} else if (source === 'spothole') { }
spots = await fetchSpothole();
} else { } else {
// Auto mode - try sources in order (most reliable first) // Auto mode - try HamQTH first (most reliable), then DX Spider
spots = await fetchHamQTH(); spots = await fetchHamQTH();
if (!spots) spots = await fetchIU1BOW(); if (!spots) {
if (!spots) spots = await fetchSpothole(); spots = await fetchDXSpider();
if (!spots) spots = await fetchDXHeat(); }
if (!spots) spots = await fetchDXSummit();
} }
res.json(spots || []); res.json(spots || []);
@ -407,12 +328,9 @@ app.get('/api/dxcluster/spots', async (req, res) => {
// Get available DX cluster sources // Get available DX cluster sources
app.get('/api/dxcluster/sources', (req, res) => { app.get('/api/dxcluster/sources', (req, res) => {
res.json([ res.json([
{ id: 'auto', name: 'Auto (Best Available)', description: 'Automatically selects the best available source' }, { id: 'auto', name: 'Auto (Best Available)', description: 'Tries HamQTH first, then DX Spider' },
{ id: 'hamqth', name: 'HamQTH', description: 'HamQTH.com DX Cluster CSV feed' }, { id: 'hamqth', name: 'HamQTH', description: 'HamQTH.com CSV feed (HTTP, works everywhere)' },
{ id: 'iu1bow', name: 'IU1BOW DX Spider', description: 'IU1BOW.it Spiderweb cluster (HTTP API)' }, { id: 'dxspider', name: 'DX Spider (G6NHU)', description: 'Telnet to dxspider.co.uk:7300 (works locally/Pi, may fail on cloud hosting)' }
{ id: 'spothole', name: 'Spothole', description: 'Spothole.app aggregated DX cluster' },
{ id: 'dxheat', name: 'DXHeat', description: 'DXHeat.com real-time cluster' },
{ id: 'dxsummit', name: 'DX Summit', description: 'DXSummit.fi cluster (may be slow)' }
]); ]);
}); });

Loading…
Cancel
Save

Powered by TurnKey Linux.