Files
intelligence-terminal/apis/sources/kiwisdr.mjs
calesthio ef2c6470fb Initial release — Crucix Intelligence Engine v2.0.0
26-source OSINT intelligence engine with live Jarvis dashboard,
auto-refresh via SSE, optional LLM layer (4 providers), delta/memory
system, and Telegram breaking news alerts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 23:45:46 -07:00

307 lines
11 KiB
JavaScript

// KiwiSDR Network — Global software-defined radio receiver network
// No auth required. ~900 public HF receivers worldwide (0-30 MHz).
// Useful for SIGINT awareness: HF band activity, receiver distribution,
// detecting unusual radio configurations in conflict zones.
// Data source: receiverbook.de (embeds full receiver list as JS variable)
import { safeFetch } from '../utils/fetch.mjs';
const RECEIVERBOOK_URL = 'https://www.receiverbook.de/map?type=kiwisdr';
// Fetch the full list of public KiwiSDR receivers from receiverbook.de
export async function getAllReceivers() {
try {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 20000);
const res = await fetch(RECEIVERBOOK_URL, {
headers: { 'User-Agent': 'Crucix/1.0' },
signal: controller.signal,
});
clearTimeout(timer);
if (!res.ok) return { error: `HTTP ${res.status}` };
const html = await res.text();
// Extract embedded JS: var receivers = [...];
const match = html.match(/var\s+receivers\s*=\s*(\[[\s\S]*?\]);/);
if (!match) return { error: 'Could not parse receiver data from page' };
const sites = JSON.parse(match[1]);
// Flatten: each site has a .receivers[] array of individual SDRs
const flat = [];
for (const site of sites) {
const [lon, lat] = site.location?.coordinates || [NaN, NaN];
const country = site.label?.split(',').pop()?.trim() || '';
for (const rx of (site.receivers || [site])) {
flat.push({
name: rx.label || site.label || '',
location: site.label || '',
lat, lon,
country,
url: rx.url || site.url || '',
version: rx.version || '',
antenna: '',
users: 0, usersMax: 0,
offline: false,
snr: NaN,
tdoa: null,
bands: '',
});
}
}
return flat;
} catch (e) {
return { error: e.message };
}
}
// Regions of intelligence interest with bounding boxes
const REGIONS_OF_INTEREST = {
middleEast: { lamin: 12, lomin: 30, lamax: 42, lomax: 65, label: 'Middle East' },
ukraine: { lamin: 44, lomin: 22, lamax: 53, lomax: 41, label: 'Ukraine / Eastern Europe' },
taiwan: { lamin: 20, lomin: 115, lamax: 28, lomax: 125, label: 'Taiwan Strait' },
baltics: { lamin: 53, lomin: 19, lamax: 60, lomax: 29, label: 'Baltic Region' },
southChinaSea: { lamin: 5, lomin: 105, lamax: 23, lomax: 122, label: 'South China Sea' },
koreanPeninsula:{ lamin: 33, lomin: 124, lamax: 43, lomax: 132, label: 'Korean Peninsula' },
iran: { lamin: 25, lomin: 44, lamax: 40, lomax: 63, label: 'Iran' },
sahel: { lamin: 10, lomin: -17, lamax: 20, lomax: 25, label: 'Sahel / West Africa' },
};
// HF band classifications for intelligence relevance
const HF_BANDS = {
vlf: { min: 0, max: 0.3, label: 'VLF (submarine/military comms)' },
lf: { min: 0.3, max: 0.5, label: 'LF (navigation/time signals)' },
mf: { min: 0.5, max: 1.8, label: 'MF (AM broadcast/maritime)' },
hf160m: { min: 1.8, max: 2.0, label: '160m amateur' },
hf80m: { min: 3.5, max: 4.0, label: '80m amateur' },
hf60m: { min: 5.3, max: 5.4, label: '60m amateur/utility' },
hf49m: { min: 5.9, max: 6.2, label: '49m shortwave broadcast' },
hf40m: { min: 7.0, max: 7.3, label: '40m amateur' },
hf31m: { min: 9.4, max: 9.9, label: '31m shortwave broadcast' },
hf30m: { min: 10.1, max: 10.15,label: '30m amateur' },
hf25m: { min: 11.6, max: 12.1, label: '25m shortwave broadcast' },
hf20m: { min: 14.0, max: 14.35,label: '20m amateur' },
hf17m: { min: 18.068,max: 18.168,label: '17m amateur' },
hf15m: { min: 21.0, max: 21.45,label: '15m amateur' },
hf11m: { min: 25.67, max: 26.1, label: '11m broadcast/CB' },
hfMilitary:{ min: 2.0, max: 30.0, label: 'HF military/utility (general)' },
};
// Check if a receiver falls within a bounding box
function inBounds(rx, box) {
if (isNaN(rx.lat) || isNaN(rx.lon)) return false;
return rx.lat >= box.lamin && rx.lat <= box.lamax && rx.lon >= box.lomin && rx.lon <= box.lomax;
}
// Map a receiver to a continent based on coordinates
function getContinent(lat, lon) {
if (isNaN(lat) || isNaN(lon)) return 'Unknown';
if (lat >= 15 && lat <= 72 && lon >= -170 && lon <= -50) return 'North America';
if (lat >= -60 && lat < 15 && lon >= -90 && lon <= -30) return 'South America';
if (lat >= 35 && lat <= 72 && lon >= -25 && lon <= 45) return 'Europe';
if (lat >= -35 && lat <= 37 && lon >= -25 && lon <= 55) return 'Africa';
if (lat >= 0 && lat <= 72 && lon >= 45 && lon <= 180) return 'Asia';
if (lat >= -50 && lat <= 0 && lon >= 95 && lon <= 180) return 'Oceania';
if (lat >= 35 && lat < 45 && lon >= 25 && lon <= 45) return 'Middle East';
return 'Other';
}
// Classify the frequency range of a receiver
function classifyFrequency(rx) {
// KiwiSDR receivers typically cover 0-30 MHz
// Some entries have frequency info in various fields
const maxFreq = parseFloat(rx.max_freq ?? rx.sdr_hu?.max_freq ?? 30);
const minFreq = parseFloat(rx.min_freq ?? rx.sdr_hu?.min_freq ?? 0);
return { minFreq, maxFreq };
}
// Normalize receiver data (already flat from getAllReceivers)
function normalizeReceiver(rx, idx) {
return {
name: (rx.name || `Receiver-${idx}`).slice(0, 100),
location: (rx.location || '').slice(0, 80),
lat: parseFloat(rx.lat) || NaN,
lon: parseFloat(rx.lon) || NaN,
users: parseInt(rx.users ?? 0, 10),
usersMax: parseInt(rx.usersMax ?? 0, 10),
antenna: (rx.antenna || '').slice(0, 80),
bands: (rx.bands || '').slice(0, 60),
offline: rx.offline === true,
snr: parseFloat(rx.snr ?? NaN),
tdoa: rx.tdoa ?? null,
country: rx.country || '',
};
}
// Briefing — analyze the global KiwiSDR network
export async function briefing() {
const raw = await getAllReceivers();
// Handle errors
if (raw?.error) {
return {
source: 'KiwiSDR',
timestamp: new Date().toISOString(),
status: 'error',
message: raw.error,
};
}
// The API may return an array directly or an object with a receivers list
let rxList;
if (Array.isArray(raw)) {
rxList = raw;
} else if (raw && typeof raw === 'object') {
// Try common keys
rxList = raw.receivers || raw.rx || raw.sdrs || raw.data || Object.values(raw);
// If the object values are receiver objects, flatten
if (!Array.isArray(rxList)) {
rxList = Object.values(raw).filter(v => v && typeof v === 'object' && !Array.isArray(v));
}
} else {
return {
source: 'KiwiSDR',
timestamp: new Date().toISOString(),
status: 'error',
message: 'Unexpected data format from KiwiSDR API',
};
}
// Normalize all receivers
const allRx = rxList.map((rx, i) => normalizeReceiver(rx, i));
const onlineRx = allRx.filter(r => !r.offline);
const offlineRx = allRx.filter(r => r.offline);
// --- Geographic distribution by country ---
const byCountry = {};
for (const rx of onlineRx) {
const c = rx.country || 'Unknown';
byCountry[c] = (byCountry[c] || 0) + 1;
}
// Sort by count descending, take top 20
const topCountries = Object.entries(byCountry)
.sort((a, b) => b[1] - a[1])
.slice(0, 20)
.map(([country, count]) => ({ country, count }));
// --- Continental distribution ---
const byContinent = {};
for (const rx of onlineRx) {
const continent = getContinent(rx.lat, rx.lon);
byContinent[continent] = (byContinent[continent] || 0) + 1;
}
// --- Receivers in regions of interest ---
const conflictZoneReceivers = {};
for (const [key, box] of Object.entries(REGIONS_OF_INTEREST)) {
const rxInRegion = onlineRx.filter(rx => inBounds(rx, box));
conflictZoneReceivers[key] = {
region: box.label,
count: rxInRegion.length,
receivers: rxInRegion.slice(0, 10).map(rx => ({
name: rx.name,
location: rx.location,
lat: rx.lat,
lon: rx.lon,
users: rx.users,
antenna: rx.antenna,
country: rx.country,
})),
};
}
// --- Activity analysis (users connected) ---
const activeRx = onlineRx
.filter(r => r.users > 0)
.sort((a, b) => b.users - a.users);
const totalUsers = onlineRx.reduce((sum, r) => sum + r.users, 0);
const totalCapacity = onlineRx.reduce((sum, r) => sum + r.usersMax, 0);
const topActive = activeRx.slice(0, 15).map(rx => ({
name: rx.name,
location: rx.location,
country: rx.country,
users: rx.users,
usersMax: rx.usersMax,
lat: rx.lat,
lon: rx.lon,
antenna: rx.antenna,
}));
// --- TDOA-capable receivers (direction finding / geolocation) ---
const tdoaCapable = onlineRx.filter(r => r.tdoa !== null && r.tdoa > 0);
// --- Antenna analysis (identify unusual/specialized setups) ---
const antennaTypes = {};
for (const rx of onlineRx) {
if (rx.antenna) {
const key = rx.antenna.toLowerCase().trim();
antennaTypes[key] = (antennaTypes[key] || 0) + 1;
}
}
// --- Utilization metrics ---
const utilizationPct = totalCapacity > 0
? ((totalUsers / totalCapacity) * 100).toFixed(1)
: '0.0';
const highUtilization = onlineRx
.filter(r => r.usersMax > 0 && (r.users / r.usersMax) >= 0.8)
.map(rx => ({
name: rx.name,
location: rx.location,
country: rx.country,
users: rx.users,
usersMax: rx.usersMax,
}));
// --- Generate signals ---
const signals = [];
// High user count (unusual listening activity)
if (totalUsers > onlineRx.length * 0.5) {
signals.push(`HIGH LISTENER ACTIVITY: ${totalUsers} total users across ${onlineRx.length} receivers (${utilizationPct}% utilization)`);
}
// Conflict zone coverage
for (const [key, info] of Object.entries(conflictZoneReceivers)) {
if (info.count > 0) {
const activeInZone = info.receivers.filter(r => r.users > 0);
if (activeInZone.length > 0) {
signals.push(`ACTIVE LISTENING in ${info.region}: ${activeInZone.length}/${info.count} receivers have users connected`);
}
}
}
// High utilization receivers
if (highUtilization.length > 5) {
signals.push(`${highUtilization.length} receivers at >80% capacity — elevated HF monitoring demand`);
}
return {
source: 'KiwiSDR',
timestamp: new Date().toISOString(),
status: 'active',
network: {
totalReceivers: allRx.length,
online: onlineRx.length,
offline: offlineRx.length,
totalUsers,
totalCapacity,
utilizationPct: parseFloat(utilizationPct),
tdoaCapable: tdoaCapable.length,
},
geographic: {
byContinent,
topCountries,
},
conflictZones: conflictZoneReceivers,
topActive,
highUtilization: highUtilization.slice(0, 10),
signals,
};
}
if (process.argv[1]?.endsWith('kiwisdr.mjs')) {
const data = await briefing();
console.log(JSON.stringify(data, null, 2));
}