Sats
This commit is contained in:
@@ -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
69
server/src/sats.mjs
Normal 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),
|
||||
}));
|
||||
}
|
||||
@@ -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};
|
||||
}
|
||||
|
||||
@@ -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 ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
Reference in New Issue
Block a user