commit
bccdac98f8
@ -0,0 +1,147 @@
|
||||
/**
|
||||
* usePSKReporter Hook
|
||||
* Fetches PSKReporter data showing where your digital mode signals are being received
|
||||
*
|
||||
* Uses HTTP API with server-side caching to respect PSKReporter rate limits.
|
||||
*
|
||||
* For real-time MQTT updates, see mqtt.pskreporter.info (requires mqtt.js library)
|
||||
*/
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
|
||||
// Convert grid square to lat/lon
|
||||
function gridToLatLon(grid) {
|
||||
if (!grid || grid.length < 4) return null;
|
||||
|
||||
const g = grid.toUpperCase();
|
||||
const lon = (g.charCodeAt(0) - 65) * 20 - 180;
|
||||
const lat = (g.charCodeAt(1) - 65) * 10 - 90;
|
||||
const lonMin = parseInt(g[2]) * 2;
|
||||
const latMin = parseInt(g[3]) * 1;
|
||||
|
||||
let finalLon = lon + lonMin + 1;
|
||||
let finalLat = lat + latMin + 0.5;
|
||||
|
||||
if (grid.length >= 6) {
|
||||
const lonSec = (g.charCodeAt(4) - 65) * (2/24);
|
||||
const latSec = (g.charCodeAt(5) - 65) * (1/24);
|
||||
finalLon = lon + lonMin + lonSec + (1/24);
|
||||
finalLat = lat + latMin + latSec + (0.5/24);
|
||||
}
|
||||
|
||||
return { lat: finalLat, lon: finalLon };
|
||||
}
|
||||
|
||||
export const usePSKReporter = (callsign, options = {}) => {
|
||||
const {
|
||||
minutes = 15, // Time window in minutes (default 15)
|
||||
enabled = true, // Enable/disable fetching
|
||||
refreshInterval = 300000, // Refresh every 5 minutes (PSKReporter friendly)
|
||||
maxSpots = 100 // Max spots to display
|
||||
} = options;
|
||||
|
||||
const [txReports, setTxReports] = useState([]);
|
||||
const [rxReports, setRxReports] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const [lastUpdate, setLastUpdate] = useState(null);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
if (!callsign || callsign === 'N0CALL' || !enabled) {
|
||||
setTxReports([]);
|
||||
setRxReports([]);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setError(null);
|
||||
|
||||
// Fetch combined endpoint from our server (handles caching)
|
||||
const response = await fetch(`/api/pskreporter/${encodeURIComponent(callsign)}?minutes=${minutes}`);
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
|
||||
// Process TX reports (where I'm being heard)
|
||||
const txData = data.tx?.reports || [];
|
||||
const processedTx = txData
|
||||
.map(r => ({
|
||||
...r,
|
||||
// Ensure we have location data
|
||||
lat: r.lat || (r.receiverGrid ? gridToLatLon(r.receiverGrid)?.lat : null),
|
||||
lon: r.lon || (r.receiverGrid ? gridToLatLon(r.receiverGrid)?.lon : null),
|
||||
age: r.age || Math.floor((Date.now() - r.timestamp) / 60000)
|
||||
}))
|
||||
.filter(r => r.lat && r.lon)
|
||||
.slice(0, maxSpots);
|
||||
|
||||
// Process RX reports (what I'm hearing)
|
||||
const rxData = data.rx?.reports || [];
|
||||
const processedRx = rxData
|
||||
.map(r => ({
|
||||
...r,
|
||||
lat: r.lat || (r.senderGrid ? gridToLatLon(r.senderGrid)?.lat : null),
|
||||
lon: r.lon || (r.senderGrid ? gridToLatLon(r.senderGrid)?.lon : null),
|
||||
age: r.age || Math.floor((Date.now() - r.timestamp) / 60000)
|
||||
}))
|
||||
.filter(r => r.lat && r.lon)
|
||||
.slice(0, maxSpots);
|
||||
|
||||
setTxReports(processedTx);
|
||||
setRxReports(processedRx);
|
||||
setLastUpdate(new Date());
|
||||
|
||||
// Check for errors in response
|
||||
if (data.error || data.tx?.error || data.rx?.error) {
|
||||
setError(data.error || data.tx?.error || data.rx?.error);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('PSKReporter fetch error:', err);
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [callsign, minutes, enabled, maxSpots]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
|
||||
if (enabled && refreshInterval > 0) {
|
||||
const interval = setInterval(fetchData, refreshInterval);
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
}, [fetchData, enabled, refreshInterval]);
|
||||
|
||||
// Computed stats
|
||||
const txBands = [...new Set(txReports.map(r => r.band))].filter(b => b && b !== 'Unknown');
|
||||
const txModes = [...new Set(txReports.map(r => r.mode))].filter(Boolean);
|
||||
|
||||
const stats = {
|
||||
txCount: txReports.length,
|
||||
rxCount: rxReports.length,
|
||||
txBands,
|
||||
txModes,
|
||||
bestSnr: txReports.length > 0
|
||||
? txReports.reduce((max, r) => (r.snr || -99) > (max?.snr || -99) ? r : max, null)
|
||||
: null
|
||||
};
|
||||
|
||||
return {
|
||||
txReports,
|
||||
txCount: txReports.length,
|
||||
rxReports,
|
||||
rxCount: rxReports.length,
|
||||
stats,
|
||||
loading,
|
||||
error,
|
||||
connected: false, // HTTP mode - not real-time connected
|
||||
source: 'http',
|
||||
lastUpdate,
|
||||
refresh: fetchData
|
||||
};
|
||||
};
|
||||
|
||||
export default usePSKReporter;
|
||||
Loading…
Reference in new issue