import {Aedes} from 'aedes'; import {createServer} from 'net'; import {cfg} from './config.mjs'; const HISTORY_LIMIT = 500; const TTL_MS = 60_000; const satellites = new Map(); function pruneStale() { const now = Date.now(); for (const [key, bucket] of satellites) { if (now - bucket.lastSeen > TTL_MS) satellites.delete(key); } } function safeJson(buf) { try { return JSON.parse(buf.toString()); } catch { return null; } } /** * Best-effort field mapping — adjust once real TinyGS payload shape is confirmed * from the console logs. Unknown fields are kept under `raw` for inspection. */ function extractReading(topic, payload) { const data = safeJson(payload) ?? {raw: payload.toString()}; return { timestamp: Date.now(), topic, satellite: data.satellite ?? data.name ?? data.sat ?? 'unknown', norad: data.norad ?? data.noradId ?? data.NORAD ?? null, freqMHz: data.freq ?? data.frequency ?? null, packetRssi: data.rssi ?? null, packetSnr: data.snr ?? null, freqError: data.frequency_error ?? data.freqError ?? null, crcOk: data.crc_error !== undefined ? !data.crc_error : null, raw: data, }; } function handlePacket(topic, payload) { const reading = extractReading(topic, payload); const key = reading.norad ? `${reading.satellite}-${reading.norad}` : reading.satellite; let bucket = satellites.get(key); if (!bucket) { bucket = {history: [], lastSeen: 0}; satellites.set(key, bucket); } bucket.lastSeen = reading.timestamp; bucket.history.push(reading); if (bucket.history.length > HISTORY_LIMIT) bucket.history.shift(); pruneStale(); } export async function startTinyGSServer() { const {TINYGS_MQTT_PORT = 1883, TINYGS_MQTT_USER, TINYGS_MQTT_PASS} = cfg(); const broker = await Aedes.createBroker(); const server = createServer(broker.handle); if (TINYGS_MQTT_USER) { broker.authenticate = (client, username, password, cb) => { const ok = username === TINYGS_MQTT_USER && password?.toString() === TINYGS_MQTT_PASS; if (!ok) console.warn(`[tinygs] rejected auth from ${client.id}`); cb(null, ok); }; } broker.on('publish', (packet, client) => { if (!client || packet.topic.startsWith('$SYS')) return; console.info(`[tinygs] ${packet.topic}`, packet.payload.toString().slice(0, 200)); handlePacket(packet.topic, packet.payload); }); broker.on('client', client => console.info(`[tinygs] connected: ${client.id}`)); broker.on('clientDisconnect', client => console.info(`[tinygs] disconnected: ${client.id}`)); server.listen(TINYGS_MQTT_PORT, () => console.info(`🛜 MQTT broker - http://localhost:${TINYGS_MQTT_PORT}`)); } export const getTinyGSData = () => { pruneStale(); return Array.from(satellites.values()).map(bucket => ({ ...bucket.history.at(-1), history: bucket.history.slice(0, -1), })); }