This commit is contained in:
2026-09-10 22:52:44 -04:00
parent 2c25251864
commit 79acad8c07
9 changed files with 314 additions and 30 deletions

View File

@@ -10,7 +10,9 @@ export function cfg() {
return {
PORT: process.env.PORT || 3000,
ADSB_URL: process.env.ADSB_URL || '',
DB_HOST: process.env.DB_HOST || 'http://localhost:8428',
TINYGS_URL: process.env.TINYGS_URL || '',
TINYGS_AUTH: process.env.TINYGS_AUTH || '',
DB_HOST: process.env.DB_HOST || 'http://localhost:8428',
EMAIL: process.env.EMAIL,
LATITUDE: parseFloat(process.env.LATITUDE || '0'),
LONGITUDE: parseFloat(process.env.LONGITUDE || '0'),

69
server/src/sats.mjs Normal file
View File

@@ -0,0 +1,69 @@
import {cfg} from './config.mjs';
import {isEqual} from '@ztimson/utils';
const HISTORY_LIMIT = 500
const POLL_MS = 5_000
const TTL_MS = 15 * 60_000 // drop a satellite's bucket if nothing new for 15 min
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();
function pruneStale() {
const now = Date.now();
for (const [name, bucket] of satellites) {
if (now - bucket.lastSeen > TTL_MS) satellites.delete(name);
}
}
export async function pollTinyGS() {
const { TINYGS_URL, TINYGS_AUTH } = cfg();
try {
const raw = await fetch(TINYGS_URL + '/wm', {
headers: TINYGS_AUTH ? {Authorization: `Basic ${TINYGS_AUTH}`} : {}
}).then(r => r.text());
const reading = parseWm(raw);
if (!reading.satellite || reading.satellite === '-') return;
let bucket = satellites.get(reading.satellite);
if (!bucket) {
bucket = { history: [], lastSeen: 0 };
satellites.set(reading.satellite, bucket);
}
bucket.lastSeen = reading.timestamp;
if (!isEqual(bucket.history.at(-1), reading)) {
bucket.history.push(reading);
if (bucket.history.length > HISTORY_LIMIT) bucket.history.shift();
}
pruneStale();
} catch (err) {
console.error('[tinygs] poll failed', err);
}
}
setInterval(pollTinyGS, POLL_MS);
export const getTinyGSData = () => {
pruneStale();
return Array.from(satellites.entries()).map(([name, bucket]) => ({
...bucket.history.at(-1),
history: bucket.history.slice(0, -1),
}));
}

View File

@@ -108,11 +108,14 @@ export async function queryDaily(start, end) {
export async function getCoords() {
const c = cfg();
if(c.LATITUDE !== 0 && c.LONGITUDE !== 0 && c.ALTITUDE !== 0)
return {latitude: c.LATITUDE, longitude: c.LONGITUDE, altitude: c.ALTITUDE};
const results = await queryInstant(c, `{__name__=~"latitude|longitude|altitude"}`);
const fields = metricToFields(results);
if (fields.latitude && fields.longitude) {
if(fields.latitude && fields.longitude) {
return {latitude: fields.latitude, longitude: fields.longitude, altitude: fields.altitude || c.ALTITUDE};
}
return {latitude: c.LATITUDE, longitude: c.LONGITUDE, altitude: c.ALTITUDE};
return {latitude: 0, longitude: 0, altitude: 0};
}

View File

@@ -12,6 +12,7 @@ import {getADSBImage, getADSB, getADSBHistory, getADSBRange, initAircraftDb} fro
import {fetchIcon} from './openweather.mjs';
import {getForecast, getWeatherCondition, forecastTTL} from './forecast.mjs';
import {dailyWeather, hourlyWeather} from './openmeteo.mjs';
import {getTinyGSData} from './sats.mjs';
// ── Uncaught error handlers ───────────────────────────────────────────────────
@@ -152,7 +153,7 @@ app.get('/api/icon/:icon', asyncHandler(async (req, res, next) => {
// res.json(filterFields(await getSpaceWeather(), fields));
// });
// ── ADSB/AIS Proxy ────────────────────────────────────────────────────────────
// ── ADSB/AIS/SAT Proxy ────────────────────────────────────────────────────────────
app.get('/api/adsb', asyncHandler(async (req, res) => res.json(await getADSB())));
app.get('/api/adsb-image/:icao', asyncHandler(async (req, res) => {
@@ -170,6 +171,7 @@ app.get('/api/ais-image/:mmsi', asyncHandler(async (req, res) => {
res.contentType(blob.type).send(buffer);
}));
app.get('/api/range', asyncHandler(async (req, res) => res.json(await getADSBRange())));
app.get('/api/sats', (_req, res) => res.json(getTinyGSData()));
// ── DOCS ──────────────────────────────────────────────────────────────────────