88 lines
2.7 KiB
JavaScript
88 lines
2.7 KiB
JavaScript
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),
|
|
}));
|
|
}
|