new mqtt server for tinygs

This commit is contained in:
2026-09-11 11:08:38 -04:00
parent 688552d0d3
commit 033e34a971
5 changed files with 339 additions and 50 deletions

267
server/package-lock.json generated
View File

@@ -11,6 +11,7 @@
"@scalar/express-api-reference": "^0.10.4", "@scalar/express-api-reference": "^0.10.4",
"@ztimson/utils": "^0.29.5", "@ztimson/utils": "^0.29.5",
"adm-zip": "^0.5.17", "adm-zip": "^0.5.17",
"aedes": "^1.1.2",
"better-sqlite3": "^12.11.1", "better-sqlite3": "^12.11.1",
"cheerio": "^1.2.0", "cheerio": "^1.2.0",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
@@ -90,6 +91,24 @@
"node": ">=20" "node": ">=20"
} }
}, },
"node_modules/@types/node": {
"version": "22.20.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.2.tgz",
"integrity": "sha512-xlvWf4Vs9n1PEVYwP1n4vvG07M6y8WgvJ2t0vbrWTmijsIHp1cS+uJ2kMIRdY3nHZK0nCYKrPeD171+SzF4/zw==",
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/@types/readable-stream": {
"version": "4.0.24",
"resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.24.tgz",
"integrity": "sha512-NRvUNC/JFGPJvqdAfEve8oginbM6V08u5NzLWpG8MwA2kTPOLnqk+wpwuPT+mp3aUsxyuT6m2gnrPuHYCruzEg==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@ztimson/utils": { "node_modules/@ztimson/utils": {
"version": "0.29.5", "version": "0.29.5",
"resolved": "https://registry.npmjs.org/@ztimson/utils/-/utils-0.29.5.tgz", "resolved": "https://registry.npmjs.org/@ztimson/utils/-/utils-0.29.5.tgz",
@@ -99,6 +118,18 @@
"var-persist": "^1.0.1" "var-persist": "^1.0.1"
} }
}, },
"node_modules/abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
"license": "MIT",
"dependencies": {
"event-target-shim": "^5.0.0"
},
"engines": {
"node": ">=6.5"
}
},
"node_modules/accepts": { "node_modules/accepts": {
"version": "1.3.8", "version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@@ -121,6 +152,66 @@
"node": ">=12.0" "node": ">=12.0"
} }
}, },
"node_modules/aedes": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/aedes/-/aedes-1.1.2.tgz",
"integrity": "sha512-prT8Zr4JkqpJMb5rMlLrW54oRyKPNgpRgqrF6PWQedL4ju+1YIwzkraxr16Rm4QyEkpqmZUdOEkfGp7zaDd5Lw==",
"license": "MIT",
"dependencies": {
"aedes-packet": "^4.0.1",
"aedes-persistence": "^10.3.1",
"mqemitter": "^7.1.0",
"mqtt-packet": "^9.0.2"
},
"engines": {
"node": ">=20"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/aedes"
},
"peerDependencies": {
"aedes-persistence-level": "^9.1.2",
"aedes-persistence-mongodb": "^9.3.1",
"aedes-persistence-redis": "^11.2.1"
},
"peerDependenciesMeta": {
"aedes-persistence-level": {
"optional": true
},
"aedes-persistence-mongodb": {
"optional": true
},
"aedes-persistence-redis": {
"optional": true
}
}
},
"node_modules/aedes-packet": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/aedes-packet/-/aedes-packet-4.0.1.tgz",
"integrity": "sha512-PLjKJElXEYB66RzHhJCw0SrRLfSmGRbbg2mtO5t/6FAbFp8vC0cG5ErhvPvfa9iKBrKZwoluWLgn8doBU8sRAw==",
"license": "MIT",
"dependencies": {
"mqtt-packet": "^9.0.0"
},
"engines": {
"node": ">=20"
}
},
"node_modules/aedes-persistence": {
"version": "10.3.1",
"resolved": "https://registry.npmjs.org/aedes-persistence/-/aedes-persistence-10.3.1.tgz",
"integrity": "sha512-C/z0zLEXY9PkUH+7pz31H5gGzlQZxqhFONK1lt5VON0hiHXQq6tr4cSIAtFUVQ1kyUdd7SsfkeKLAECp3gjjpw==",
"license": "MIT",
"dependencies": {
"aedes-packet": "^4.0.1",
"qlobber": "^8.0.1"
},
"engines": {
"node": ">=20"
}
},
"node_modules/array-flatten": { "node_modules/array-flatten": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
@@ -633,6 +724,24 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/event-target-shim": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
"license": "MIT",
"engines": {
"node": ">=0.8.x"
}
},
"node_modules/expand-template": { "node_modules/expand-template": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
@@ -688,6 +797,16 @@
"url": "https://opencollective.com/express" "url": "https://opencollective.com/express"
} }
}, },
"node_modules/fastparallel": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/fastparallel/-/fastparallel-2.4.1.tgz",
"integrity": "sha512-qUmhxPgNHmvRjZKBFUNI0oZuuH9OlSIOXmJ98lhKPxMZZ7zS/Fi0wRHOihDSz0R1YiIOjxzOY4bq65YTcdBi2Q==",
"license": "ISC",
"dependencies": {
"reusify": "^1.0.4",
"xtend": "^4.0.2"
}
},
"node_modules/file-uri-to-path": { "node_modules/file-uri-to-path": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
@@ -1024,6 +1143,105 @@
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/mqemitter": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/mqemitter/-/mqemitter-7.1.0.tgz",
"integrity": "sha512-GnBDNz3lxmllW201ne0mrmdy5tPOTnc79jjVcsfUa2LG2pUGeyGWVeiae6ZysfC/64XrYOqCKRAQYrB7pGyBVQ==",
"license": "ISC",
"dependencies": {
"fastparallel": "^2.4.1",
"qlobber": "^8.0.1"
},
"engines": {
"node": ">=20"
}
},
"node_modules/mqtt-packet": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-9.0.2.tgz",
"integrity": "sha512-MvIY0B8/qjq7bKxdN1eD+nrljoeaai+qjLJgfRn3TiMuz0pamsIWY2bFODPZMSNmabsLANXsLl4EMoWvlaTZWA==",
"license": "MIT",
"dependencies": {
"bl": "^6.0.8",
"debug": "^4.3.4",
"process-nextick-args": "^2.0.1"
}
},
"node_modules/mqtt-packet/node_modules/bl": {
"version": "6.1.6",
"resolved": "https://registry.npmjs.org/bl/-/bl-6.1.6.tgz",
"integrity": "sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg==",
"license": "MIT",
"dependencies": {
"@types/readable-stream": "^4.0.0",
"buffer": "^6.0.3",
"inherits": "^2.0.4",
"readable-stream": "^4.2.0"
}
},
"node_modules/mqtt-packet/node_modules/buffer": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.2.1"
}
},
"node_modules/mqtt-packet/node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/mqtt-packet/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/mqtt-packet/node_modules/readable-stream": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
"integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
"license": "MIT",
"dependencies": {
"abort-controller": "^3.0.0",
"buffer": "^6.0.3",
"events": "^3.3.0",
"process": "^0.11.10",
"string_decoder": "^1.3.0"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/ms": { "node_modules/ms": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@@ -1211,6 +1429,21 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/process": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
"license": "MIT",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"license": "MIT"
},
"node_modules/proxy-addr": { "node_modules/proxy-addr": {
"version": "2.0.7", "version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -1234,6 +1467,15 @@
"once": "^1.3.1" "once": "^1.3.1"
} }
}, },
"node_modules/qlobber": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/qlobber/-/qlobber-8.0.1.tgz",
"integrity": "sha512-O+Wd1chXj5YE1DwmD+ae0bXiSLehmnS3czlC1R9FL/Nt/3q8uMS1bIHmg2lJfCoiimCxClWM8AAuJrF0EvNiog==",
"license": "MIT",
"engines": {
"node": ">= 16"
}
},
"node_modules/qs": { "node_modules/qs": {
"version": "6.15.3", "version": "6.15.3",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz",
@@ -1303,6 +1545,16 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/reusify": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
"integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
"license": "MIT",
"engines": {
"iojs": ">=1.0.0",
"node": ">=0.10.0"
}
},
"node_modules/safe-buffer": { "node_modules/safe-buffer": {
"version": "5.2.1", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -1634,6 +1886,12 @@
"node": ">=20.18.1" "node": ">=20.18.1"
} }
}, },
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"license": "MIT"
},
"node_modules/unpipe": { "node_modules/unpipe": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@@ -1713,6 +1971,15 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"license": "MIT",
"engines": {
"node": ">=0.4"
}
},
"node_modules/yaml": { "node_modules/yaml": {
"version": "2.9.0", "version": "2.9.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",

View File

@@ -10,6 +10,7 @@
"@scalar/express-api-reference": "^0.10.4", "@scalar/express-api-reference": "^0.10.4",
"@ztimson/utils": "^0.29.5", "@ztimson/utils": "^0.29.5",
"adm-zip": "^0.5.17", "adm-zip": "^0.5.17",
"aedes": "^1.1.2",
"better-sqlite3": "^12.11.1", "better-sqlite3": "^12.11.1",
"cheerio": "^1.2.0", "cheerio": "^1.2.0",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",

View File

@@ -12,6 +12,9 @@ export function cfg() {
ADSB_URL: process.env.ADSB_URL || '', ADSB_URL: process.env.ADSB_URL || '',
TINYGS_URL: process.env.TINYGS_URL || '', TINYGS_URL: process.env.TINYGS_URL || '',
TINYGS_AUTH: process.env.TINYGS_AUTH || '', TINYGS_AUTH: process.env.TINYGS_AUTH || '',
TINYGS_MQTT_PORT: process.env.TINYGS_MQTT_PORT || 1883,
TINYGS_MQTT_USER: process.env.TINYGS_MQTT_USER || 'admin',
TINYGS_MQTT_PASS: process.env.TINYGS_MQTT_PASS || 'admin',
DB_HOST: process.env.DB_HOST || 'http://localhost:8428', DB_HOST: process.env.DB_HOST || 'http://localhost:8428',
EMAIL: process.env.EMAIL, EMAIL: process.env.EMAIL,
LATITUDE: parseFloat(process.env.LATITUDE || '0'), LATITUDE: parseFloat(process.env.LATITUDE || '0'),

View File

@@ -1,69 +1,86 @@
import {Aedes} from 'aedes';
import {createServer} from 'net';
import {cfg} from './config.mjs'; import {cfg} from './config.mjs';
import {isEqual} from '@ztimson/utils';
const HISTORY_LIMIT = 500; const HISTORY_LIMIT = 500;
const POLL_MS = 5_000;
const TTL_MS = 60_000; const TTL_MS = 60_000;
function parseWm(raw) {
const f = raw.replace(/<[^>]+>/g, '').split(',').map(s => s.trim());
const [latitude, longitude] = f[15].split('/').map(s => parseFloat(s));
const [az, el] = f[16].split('/').map(s => parseFloat(s));
return {
timestamp: Date.now(),
freqMHz: parseFloat(f[3]),
satellite: f[14],
latitude, longitude,
az, el,
packetRssi: parseFloat(f[21]),
packetSnr: parseFloat(f[22]),
freqError: parseFloat(f[23]),
crcOk: !/CRC ERROR/i.test(f[24]),
}
}
const satellites = new Map(); const satellites = new Map();
function pruneStale() { function pruneStale() {
const now = Date.now(); const now = Date.now();
for (const [name, bucket] of satellites) { for (const [key, bucket] of satellites) {
if (now - bucket.lastSeen > TTL_MS) satellites.delete(name); if (now - bucket.lastSeen > TTL_MS) satellites.delete(key);
} }
} }
export async function pollTinyGS() { function safeJson(buf) {
const { TINYGS_URL, TINYGS_AUTH } = cfg(); try { return JSON.parse(buf.toString()); } catch { return null; }
try { }
const raw = await fetch(TINYGS_URL + '/wm', {
headers: TINYGS_AUTH ? {Authorization: `Basic ${TINYGS_AUTH}`} : {} /**
}).then(r => r.text()); * Best-effort field mapping — adjust once real TinyGS payload shape is confirmed
const reading = parseWm(raw); * from the console logs. Unknown fields are kept under `raw` for inspection.
if (!reading.satellite || reading.satellite === '-') return; */
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;
const key = reading.satellite + '-' + reading.freqMHz;
let bucket = satellites.get(key); let bucket = satellites.get(key);
if (!bucket) { if (!bucket) {
bucket = {history: [], lastSeen: 0}; bucket = {history: [], lastSeen: 0};
satellites.set(key, bucket); satellites.set(key, bucket);
} }
bucket.lastSeen = reading.timestamp; bucket.lastSeen = reading.timestamp;
if (!isEqual(bucket.history.at(-1), reading)) {
bucket.history.push(reading); bucket.history.push(reading);
if (bucket.history.length > HISTORY_LIMIT) bucket.history.shift(); if (bucket.history.length > HISTORY_LIMIT) bucket.history.shift();
}
pruneStale(); pruneStale();
} catch (err) {
console.error('[tinygs] poll failed', err);
}
} }
setInterval(pollTinyGS, POLL_MS); 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 = () => { export const getTinyGSData = () => {
pruneStale(); pruneStale();
return Array.from(satellites.entries()).map(([name, bucket]) => ({ return Array.from(satellites.values()).map(bucket => ({
...bucket.history.at(-1), ...bucket.history.at(-1),
history: bucket.history.slice(0, -1), history: bucket.history.slice(0, -1),
})); }));

View File

@@ -12,7 +12,7 @@ import {getADSBImage, getADSB, getADSBHistory, getADSBRange, initAircraftDb} fro
import {fetchIcon} from './openweather.mjs'; import {fetchIcon} from './openweather.mjs';
import {getForecast, getWeatherCondition, forecastTTL} from './forecast.mjs'; import {getForecast, getWeatherCondition, forecastTTL} from './forecast.mjs';
import {dailyWeather, hourlyWeather} from './openmeteo.mjs'; import {dailyWeather, hourlyWeather} from './openmeteo.mjs';
import {getTinyGSData} from './sats.mjs'; import {getTinyGSData, startTinyGSServer} from './sats.mjs';
// ── Uncaught error handlers ─────────────────────────────────────────────────── // ── Uncaught error handlers ───────────────────────────────────────────────────
@@ -209,6 +209,7 @@ const c = cfg();
setTimeout(getForecast, 1) setTimeout(getForecast, 1)
setInterval(getForecast, forecastTTL) setInterval(getForecast, forecastTTL)
await initAircraftDb(); await initAircraftDb();
await startTinyGSServer();
app.listen(c.PORT, () => { app.listen(c.PORT, () => {
console.log(`⛅ Weather API — http://localhost:${c.PORT}`) console.log(`⛅ Weather API — http://localhost:${c.PORT}`)