|
|
|
@ -1,12 +1,12 @@
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
* usePSKReporter Hook
|
|
|
|
* usePSKReporter Hook
|
|
|
|
* Fetches PSKReporter data showing where your digital mode signals are being received
|
|
|
|
* Fetches PSKReporter data via MQTT WebSocket connection
|
|
|
|
*
|
|
|
|
*
|
|
|
|
* Uses HTTP API with server-side caching to respect PSKReporter rate limits.
|
|
|
|
* Uses real-time MQTT feed from mqtt.pskreporter.info for live spots
|
|
|
|
*
|
|
|
|
* No HTTP API calls - direct WebSocket connection from browser
|
|
|
|
* For real-time MQTT updates, see mqtt.pskreporter.info (requires mqtt.js library)
|
|
|
|
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
import { useState, useEffect, useCallback } from 'react';
|
|
|
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
|
|
|
|
|
|
import mqtt from 'mqtt';
|
|
|
|
|
|
|
|
|
|
|
|
// Convert grid square to lat/lon
|
|
|
|
// Convert grid square to lat/lon
|
|
|
|
function gridToLatLon(grid) {
|
|
|
|
function gridToLatLon(grid) {
|
|
|
|
@ -31,116 +31,287 @@ function gridToLatLon(grid) {
|
|
|
|
return { lat: finalLat, lon: finalLon };
|
|
|
|
return { lat: finalLat, lon: finalLon };
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Get band name from frequency in Hz
|
|
|
|
|
|
|
|
function getBandFromHz(freqHz) {
|
|
|
|
|
|
|
|
const freqMHz = freqHz / 1000000;
|
|
|
|
|
|
|
|
if (freqMHz >= 1.8 && freqMHz <= 2) return '160m';
|
|
|
|
|
|
|
|
if (freqMHz >= 3.5 && freqMHz <= 4) return '80m';
|
|
|
|
|
|
|
|
if (freqMHz >= 5.3 && freqMHz <= 5.4) return '60m';
|
|
|
|
|
|
|
|
if (freqMHz >= 7 && freqMHz <= 7.3) return '40m';
|
|
|
|
|
|
|
|
if (freqMHz >= 10.1 && freqMHz <= 10.15) return '30m';
|
|
|
|
|
|
|
|
if (freqMHz >= 14 && freqMHz <= 14.35) return '20m';
|
|
|
|
|
|
|
|
if (freqMHz >= 18.068 && freqMHz <= 18.168) return '17m';
|
|
|
|
|
|
|
|
if (freqMHz >= 21 && freqMHz <= 21.45) return '15m';
|
|
|
|
|
|
|
|
if (freqMHz >= 24.89 && freqMHz <= 24.99) return '12m';
|
|
|
|
|
|
|
|
if (freqMHz >= 28 && freqMHz <= 29.7) return '10m';
|
|
|
|
|
|
|
|
if (freqMHz >= 50 && freqMHz <= 54) return '6m';
|
|
|
|
|
|
|
|
if (freqMHz >= 144 && freqMHz <= 148) return '2m';
|
|
|
|
|
|
|
|
if (freqMHz >= 420 && freqMHz <= 450) return '70cm';
|
|
|
|
|
|
|
|
return 'Unknown';
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export const usePSKReporter = (callsign, options = {}) => {
|
|
|
|
export const usePSKReporter = (callsign, options = {}) => {
|
|
|
|
const {
|
|
|
|
const {
|
|
|
|
minutes = 15, // Time window in minutes (default 15)
|
|
|
|
minutes = 15, // Time window to keep spots
|
|
|
|
enabled = true, // Enable/disable fetching
|
|
|
|
enabled = true, // Enable/disable fetching
|
|
|
|
refreshInterval = 300000, // Refresh every 5 minutes (PSKReporter friendly)
|
|
|
|
maxSpots = 100 // Max spots to keep
|
|
|
|
maxSpots = 100 // Max spots to display
|
|
|
|
|
|
|
|
} = options;
|
|
|
|
} = options;
|
|
|
|
|
|
|
|
|
|
|
|
const [txReports, setTxReports] = useState([]);
|
|
|
|
const [txReports, setTxReports] = useState([]);
|
|
|
|
const [rxReports, setRxReports] = useState([]);
|
|
|
|
const [rxReports, setRxReports] = useState([]);
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
const [error, setError] = useState(null);
|
|
|
|
const [error, setError] = useState(null);
|
|
|
|
|
|
|
|
const [connected, setConnected] = useState(false);
|
|
|
|
const [lastUpdate, setLastUpdate] = useState(null);
|
|
|
|
const [lastUpdate, setLastUpdate] = useState(null);
|
|
|
|
|
|
|
|
const [source, setSource] = useState('connecting');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const clientRef = useRef(null);
|
|
|
|
|
|
|
|
const txReportsRef = useRef([]);
|
|
|
|
|
|
|
|
const rxReportsRef = useRef([]);
|
|
|
|
|
|
|
|
const mountedRef = useRef(true);
|
|
|
|
|
|
|
|
|
|
|
|
const fetchData = useCallback(async () => {
|
|
|
|
// Clean old spots (older than specified minutes)
|
|
|
|
if (!callsign || callsign === 'N0CALL' || !enabled) {
|
|
|
|
const cleanOldSpots = useCallback((spots, maxAgeMinutes) => {
|
|
|
|
setTxReports([]);
|
|
|
|
const cutoff = Date.now() - (maxAgeMinutes * 60 * 1000);
|
|
|
|
setRxReports([]);
|
|
|
|
return spots.filter(s => s.timestamp > cutoff).slice(0, maxSpots);
|
|
|
|
setLoading(false);
|
|
|
|
}, [maxSpots]);
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Process incoming MQTT message
|
|
|
|
|
|
|
|
const processMessage = useCallback((topic, message) => {
|
|
|
|
|
|
|
|
if (!mountedRef.current) return;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
setError(null);
|
|
|
|
const data = JSON.parse(message.toString());
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// PSKReporter MQTT message format
|
|
|
|
|
|
|
|
// sa=sender callsign, sl=sender locator, ra=receiver callsign, rl=receiver locator
|
|
|
|
|
|
|
|
// f=frequency, md=mode, rp=snr (report), t=timestamp
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
|
|
|
sa: senderCallsign,
|
|
|
|
|
|
|
|
sl: senderLocator,
|
|
|
|
|
|
|
|
ra: receiverCallsign,
|
|
|
|
|
|
|
|
rl: receiverLocator,
|
|
|
|
|
|
|
|
f: frequency,
|
|
|
|
|
|
|
|
md: mode,
|
|
|
|
|
|
|
|
rp: snr,
|
|
|
|
|
|
|
|
t: timestamp
|
|
|
|
|
|
|
|
} = data;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!senderCallsign || !receiverCallsign) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const senderLoc = gridToLatLon(senderLocator);
|
|
|
|
|
|
|
|
const receiverLoc = gridToLatLon(receiverLocator);
|
|
|
|
|
|
|
|
const freq = parseInt(frequency) || 0;
|
|
|
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const report = {
|
|
|
|
|
|
|
|
sender: senderCallsign,
|
|
|
|
|
|
|
|
senderGrid: senderLocator,
|
|
|
|
|
|
|
|
receiver: receiverCallsign,
|
|
|
|
|
|
|
|
receiverGrid: receiverLocator,
|
|
|
|
|
|
|
|
freq,
|
|
|
|
|
|
|
|
freqMHz: freq ? (freq / 1000000).toFixed(3) : '?',
|
|
|
|
|
|
|
|
band: getBandFromHz(freq),
|
|
|
|
|
|
|
|
mode: mode || 'Unknown',
|
|
|
|
|
|
|
|
snr: snr !== undefined ? parseInt(snr) : null,
|
|
|
|
|
|
|
|
timestamp: timestamp ? timestamp * 1000 : now,
|
|
|
|
|
|
|
|
age: 0,
|
|
|
|
|
|
|
|
lat: null,
|
|
|
|
|
|
|
|
lon: null
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Fetch combined endpoint from our server (handles caching)
|
|
|
|
const upperCallsign = callsign?.toUpperCase();
|
|
|
|
const response = await fetch(`/api/pskreporter/${encodeURIComponent(callsign)}?minutes=${minutes}`);
|
|
|
|
if (!upperCallsign) return;
|
|
|
|
|
|
|
|
|
|
|
|
if (response.ok) {
|
|
|
|
// If I'm the sender, this is a TX report (someone heard me)
|
|
|
|
const data = await response.json();
|
|
|
|
if (senderCallsign.toUpperCase() === upperCallsign) {
|
|
|
|
|
|
|
|
report.lat = receiverLoc?.lat;
|
|
|
|
|
|
|
|
report.lon = receiverLoc?.lon;
|
|
|
|
|
|
|
|
|
|
|
|
// Process TX reports (where I'm being heard)
|
|
|
|
// Add to front, dedupe by receiver+freq, limit size
|
|
|
|
const txData = data.tx?.reports || [];
|
|
|
|
txReportsRef.current = [report, ...txReportsRef.current]
|
|
|
|
const processedTx = txData
|
|
|
|
.filter((r, i, arr) =>
|
|
|
|
.map(r => ({
|
|
|
|
i === arr.findIndex(x => x.receiver === r.receiver && Math.abs(x.freq - r.freq) < 1000)
|
|
|
|
...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);
|
|
|
|
.slice(0, maxSpots);
|
|
|
|
|
|
|
|
|
|
|
|
// Process RX reports (what I'm hearing)
|
|
|
|
setTxReports(cleanOldSpots([...txReportsRef.current], minutes));
|
|
|
|
const rxData = data.rx?.reports || [];
|
|
|
|
setLastUpdate(new Date());
|
|
|
|
const processedRx = rxData
|
|
|
|
}
|
|
|
|
.map(r => ({
|
|
|
|
|
|
|
|
...r,
|
|
|
|
// If I'm the receiver, this is an RX report (I heard someone)
|
|
|
|
lat: r.lat || (r.senderGrid ? gridToLatLon(r.senderGrid)?.lat : null),
|
|
|
|
if (receiverCallsign.toUpperCase() === upperCallsign) {
|
|
|
|
lon: r.lon || (r.senderGrid ? gridToLatLon(r.senderGrid)?.lon : null),
|
|
|
|
report.lat = senderLoc?.lat;
|
|
|
|
age: r.age || Math.floor((Date.now() - r.timestamp) / 60000)
|
|
|
|
report.lon = senderLoc?.lon;
|
|
|
|
}))
|
|
|
|
|
|
|
|
.filter(r => r.lat && r.lon)
|
|
|
|
rxReportsRef.current = [report, ...rxReportsRef.current]
|
|
|
|
|
|
|
|
.filter((r, i, arr) =>
|
|
|
|
|
|
|
|
i === arr.findIndex(x => x.sender === r.sender && Math.abs(x.freq - r.freq) < 1000)
|
|
|
|
|
|
|
|
)
|
|
|
|
.slice(0, maxSpots);
|
|
|
|
.slice(0, maxSpots);
|
|
|
|
|
|
|
|
|
|
|
|
setTxReports(processedTx);
|
|
|
|
setRxReports(cleanOldSpots([...rxReportsRef.current], minutes));
|
|
|
|
setRxReports(processedRx);
|
|
|
|
|
|
|
|
setLastUpdate(new Date());
|
|
|
|
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) {
|
|
|
|
} catch (err) {
|
|
|
|
console.error('PSKReporter fetch error:', err);
|
|
|
|
// Silently ignore parse errors - malformed messages happen
|
|
|
|
setError(err.message);
|
|
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}, [callsign, minutes, enabled, maxSpots]);
|
|
|
|
}, [callsign, minutes, maxSpots, cleanOldSpots]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Connect to MQTT
|
|
|
|
useEffect(() => {
|
|
|
|
useEffect(() => {
|
|
|
|
fetchData();
|
|
|
|
mountedRef.current = true;
|
|
|
|
|
|
|
|
|
|
|
|
if (enabled && refreshInterval > 0) {
|
|
|
|
if (!callsign || callsign === 'N0CALL' || !enabled) {
|
|
|
|
const interval = setInterval(fetchData, refreshInterval);
|
|
|
|
setTxReports([]);
|
|
|
|
return () => clearInterval(interval);
|
|
|
|
setRxReports([]);
|
|
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
|
|
setSource('disabled');
|
|
|
|
|
|
|
|
setConnected(false);
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}, [fetchData, enabled, refreshInterval]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Computed stats
|
|
|
|
const upperCallsign = callsign.toUpperCase();
|
|
|
|
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);
|
|
|
|
// Clear old data
|
|
|
|
|
|
|
|
txReportsRef.current = [];
|
|
|
|
const stats = {
|
|
|
|
rxReportsRef.current = [];
|
|
|
|
txCount: txReports.length,
|
|
|
|
setTxReports([]);
|
|
|
|
rxCount: rxReports.length,
|
|
|
|
setRxReports([]);
|
|
|
|
txBands,
|
|
|
|
setLoading(true);
|
|
|
|
txModes,
|
|
|
|
setError(null);
|
|
|
|
bestSnr: txReports.length > 0
|
|
|
|
setSource('connecting');
|
|
|
|
? txReports.reduce((max, r) => (r.snr || -99) > (max?.snr || -99) ? r : max, null)
|
|
|
|
|
|
|
|
: null
|
|
|
|
console.log(`[PSKReporter MQTT] Connecting for ${upperCallsign}...`);
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Connect to PSKReporter MQTT via WebSocket
|
|
|
|
|
|
|
|
const client = mqtt.connect('wss://mqtt.pskreporter.info:1886/mqtt', {
|
|
|
|
|
|
|
|
clientId: `ohc_${upperCallsign}_${Math.random().toString(16).substr(2, 6)}`,
|
|
|
|
|
|
|
|
clean: true,
|
|
|
|
|
|
|
|
connectTimeout: 15000,
|
|
|
|
|
|
|
|
reconnectPeriod: 60000,
|
|
|
|
|
|
|
|
keepalive: 60
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
clientRef.current = client;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
client.on('connect', () => {
|
|
|
|
|
|
|
|
if (!mountedRef.current) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
console.log('[PSKReporter MQTT] Connected!');
|
|
|
|
|
|
|
|
setConnected(true);
|
|
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
|
|
setSource('mqtt');
|
|
|
|
|
|
|
|
setError(null);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Subscribe to spots where we are the sender (being heard by others)
|
|
|
|
|
|
|
|
// Topic format: pskr/filter/v2/{mode}/{band}/{senderCall}/{senderLoc}/{rxCall}/{rxLoc}/{freq}/{snr}
|
|
|
|
|
|
|
|
const txTopic = `pskr/filter/v2/+/+/${upperCallsign}/#`;
|
|
|
|
|
|
|
|
client.subscribe(txTopic, { qos: 0 }, (err) => {
|
|
|
|
|
|
|
|
if (err) {
|
|
|
|
|
|
|
|
console.error('[PSKReporter MQTT] TX subscribe error:', err);
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
console.log(`[PSKReporter MQTT] Subscribed TX: ${txTopic}`);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Subscribe to spots where we are the receiver (hearing others)
|
|
|
|
|
|
|
|
const rxTopic = `pskr/filter/v2/+/+/+/+/${upperCallsign}/#`;
|
|
|
|
|
|
|
|
client.subscribe(rxTopic, { qos: 0 }, (err) => {
|
|
|
|
|
|
|
|
if (err) {
|
|
|
|
|
|
|
|
console.error('[PSKReporter MQTT] RX subscribe error:', err);
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
console.log(`[PSKReporter MQTT] Subscribed RX: ${rxTopic}`);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
client.on('message', processMessage);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
client.on('error', (err) => {
|
|
|
|
|
|
|
|
if (!mountedRef.current) return;
|
|
|
|
|
|
|
|
console.error('[PSKReporter MQTT] Error:', err.message);
|
|
|
|
|
|
|
|
setError('Connection error');
|
|
|
|
|
|
|
|
setConnected(false);
|
|
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
client.on('close', () => {
|
|
|
|
|
|
|
|
if (!mountedRef.current) return;
|
|
|
|
|
|
|
|
console.log('[PSKReporter MQTT] Disconnected');
|
|
|
|
|
|
|
|
setConnected(false);
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
client.on('offline', () => {
|
|
|
|
|
|
|
|
if (!mountedRef.current) return;
|
|
|
|
|
|
|
|
console.log('[PSKReporter MQTT] Offline');
|
|
|
|
|
|
|
|
setConnected(false);
|
|
|
|
|
|
|
|
setSource('offline');
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
client.on('reconnect', () => {
|
|
|
|
|
|
|
|
if (!mountedRef.current) return;
|
|
|
|
|
|
|
|
console.log('[PSKReporter MQTT] Reconnecting...');
|
|
|
|
|
|
|
|
setSource('reconnecting');
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Cleanup on unmount or callsign change
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
|
|
mountedRef.current = false;
|
|
|
|
|
|
|
|
if (client) {
|
|
|
|
|
|
|
|
console.log('[PSKReporter MQTT] Cleaning up...');
|
|
|
|
|
|
|
|
client.end(true);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
}, [callsign, enabled, processMessage]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Periodically clean old spots and update ages
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
|
|
if (!enabled) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const interval = setInterval(() => {
|
|
|
|
|
|
|
|
// Update ages and clean old spots
|
|
|
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
setTxReports(prev => prev.map(r => ({
|
|
|
|
|
|
|
|
...r,
|
|
|
|
|
|
|
|
age: Math.floor((now - r.timestamp) / 60000)
|
|
|
|
|
|
|
|
})).filter(r => r.age <= minutes));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
setRxReports(prev => prev.map(r => ({
|
|
|
|
|
|
|
|
...r,
|
|
|
|
|
|
|
|
age: Math.floor((now - r.timestamp) / 60000)
|
|
|
|
|
|
|
|
})).filter(r => r.age <= minutes));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}, 30000); // Every 30 seconds
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return () => clearInterval(interval);
|
|
|
|
|
|
|
|
}, [enabled, minutes]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Manual refresh - force reconnect
|
|
|
|
|
|
|
|
const refresh = useCallback(() => {
|
|
|
|
|
|
|
|
if (clientRef.current) {
|
|
|
|
|
|
|
|
clientRef.current.end(true);
|
|
|
|
|
|
|
|
clientRef.current = null;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
setConnected(false);
|
|
|
|
|
|
|
|
setLoading(true);
|
|
|
|
|
|
|
|
setSource('reconnecting');
|
|
|
|
|
|
|
|
// useEffect will reconnect due to state change
|
|
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
return {
|
|
|
|
txReports,
|
|
|
|
txReports,
|
|
|
|
txCount: txReports.length,
|
|
|
|
txCount: txReports.length,
|
|
|
|
rxReports,
|
|
|
|
rxReports,
|
|
|
|
rxCount: rxReports.length,
|
|
|
|
rxCount: rxReports.length,
|
|
|
|
stats,
|
|
|
|
|
|
|
|
loading,
|
|
|
|
loading,
|
|
|
|
error,
|
|
|
|
error,
|
|
|
|
connected: false, // HTTP mode - not real-time connected
|
|
|
|
connected,
|
|
|
|
source: 'http',
|
|
|
|
source,
|
|
|
|
lastUpdate,
|
|
|
|
lastUpdate,
|
|
|
|
refresh: fetchData
|
|
|
|
refresh
|
|
|
|
};
|
|
|
|
};
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|