UNPKG

@ntrip/caster

Version:
296 lines (295 loc) 14 kB
"use strict"; /* * This file is part of the @ntrip/caster distribution (https://github.com/node-ntrip/caster). * Copyright (c) 2020 Nebojsa Cvetkovic. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.AutoSourceEntry = void 0; const stream = require("stream"); const ecefProjector = require("ecef-projector"); const countries = require("i18n-iso-countries"); const sourcetable_1 = require("../sourcetable"); const rtcm_1 = require("@gnss/rtcm"); const geonames_1 = require("@ntrip/geonames"); const BITRATE_UPDATE_INTERVAL = 30 * 60 * 1000; const BITRATE_WARMUP_SHIFT = 12; const CARRIER_SAMPLING_TIMEOUT = 15 * 1000; const CARRIER_RESET_INTERVAL = 15 * 60 * 1000; const LOCATION_ACCURACY_DECIMAL = 4; const MESSAGES_TIMING_ARRAY_SIZE = 25; const MESSAGES_TIMING_MINIMUM_COUNT = 3; const MESSAGES_TIMING_MAX_INTERVAL = 2 * 60 * 1000; const MESSAGES_TIMING_TRUNCATE_TIMEOUT = 10 * 60 * 1000; const MESSAGES_UPDATE_INTERVAL = 5 * 1000; const MESSAGES_TIMEOUT_INTERVAL = 30 * 60 * 1000; const NAV_SYSTEMS_SAMPLING_TIMEOUT = 15 * 1000; const NAV_SYSTEMS_RESET_INTERVAL = 15 * 60 * 1000; const RTCM_NAV_SYSTEM_MAP = { [rtcm_1.RtcmNavSystem.GPS]: sourcetable_1.Sourcetable.NavSystem.GPS, [rtcm_1.RtcmNavSystem.GLONASS]: sourcetable_1.Sourcetable.NavSystem.GLONASS, [rtcm_1.RtcmNavSystem.GALILEO]: sourcetable_1.Sourcetable.NavSystem.GALILEO, [rtcm_1.RtcmNavSystem.QZSS]: sourcetable_1.Sourcetable.NavSystem.QZSS, [rtcm_1.RtcmNavSystem.SBAS]: sourcetable_1.Sourcetable.NavSystem.SBAS, [rtcm_1.RtcmNavSystem.BEIDOU]: sourcetable_1.Sourcetable.NavSystem.BEIDOU, [rtcm_1.RtcmNavSystem.IRNSS]: sourcetable_1.Sourcetable.NavSystem.IRNSS, [rtcm_1.RtcmNavSystem.FUTURE]: undefined }; /** * Automatic sourcetable entry data filler * * Fills in missing fields in a mountpoint's sourcetable entry based on the data sent by the server. * Accepts a stream of {@code RtcmMessage} objects. */ class AutoSourceEntry extends stream.Writable { constructor(mountpoint, options) { var _a, _b, _c, _d; super({ objectMode: true }); this.mountpoint = mountpoint; this.options = { ignoreExisting: true, setDefault: true, geoNamesPlaces: undefined, carrier: true, country: true, format: true, formatDetails: true, identifier: true, generator: true, location: true, navSystems: true, bitrate: true }; this.carriers = { l1: false, l2: false }; this.carriersInitialSet = true; this.messages = new Map(); this.navSystems = []; this.navSystemsInitialSet = true; this.rtcmVersion = rtcm_1.RtcmVersion.V3_0; const sourceEntry = this.sourceEntry = mountpoint.sourceEntry; Object.assign(this.options, options); // Ignore values already in the source entry if (this.options.ignoreExisting) { this.options.carrier = sourceEntry.carrier === undefined; this.options.country = sourceEntry.country === undefined; this.options.format = sourceEntry.format === undefined; this.options.formatDetails = sourceEntry.formatDetails === undefined; this.options.identifier = sourceEntry.identifier === undefined; this.options.generator = sourceEntry.generator === undefined; this.options.location = sourceEntry.latitude === undefined && sourceEntry.longitude === undefined; this.options.navSystems = sourceEntry.navSystem === undefined; } // Set undefined values in source entry to their defaults if (this.options.setDefault) { this.sourceEntry.carrier = (_a = this.sourceEntry.carrier) !== null && _a !== void 0 ? _a : sourcetable_1.Sourcetable.CarrierPhaseInformation.None; this.sourceEntry.nmea = (_b = this.sourceEntry.nmea) !== null && _b !== void 0 ? _b : false; this.sourceEntry.solution = (_c = this.sourceEntry.solution) !== null && _c !== void 0 ? _c : sourcetable_1.Sourcetable.SolutionType.SingleBase; this.sourceEntry.compressionEncryption = (_d = this.sourceEntry.compressionEncryption) !== null && _d !== void 0 ? _d : 'none'; } // List of places to use for location/country if ((options === null || options === void 0 ? void 0 : options.geoNamesPlaces) !== undefined) this.geoNames = new geonames_1.GeoNames(options === null || options === void 0 ? void 0 : options.geoNamesPlaces); if (this.options.formatDetails) { setInterval(() => this.updateFormatDetails(), MESSAGES_UPDATE_INTERVAL); setInterval(() => this.truncateFormatDetails(), MESSAGES_TIMEOUT_INTERVAL); } if (this.options.carrier) { setInterval(() => { this.carriers.l1 = false; this.carriers.l2 = false; this.options.carrier = true; }, CARRIER_RESET_INTERVAL); setTimeout(() => { setInterval(() => { this.options.carrier = false; this.updateCarriers(); }, CARRIER_RESET_INTERVAL); this.options.carrier = false; this.carriersInitialSet = false; }, CARRIER_SAMPLING_TIMEOUT); } if (this.options.navSystems) { setInterval(() => { this.navSystems = []; this.options.navSystems = true; }, NAV_SYSTEMS_RESET_INTERVAL); setTimeout(() => { setInterval(() => { this.options.navSystems = false; this.updateNavSystems(); }, NAV_SYSTEMS_RESET_INTERVAL); this.options.navSystems = false; this.navSystemsInitialSet = false; }, NAV_SYSTEMS_SAMPLING_TIMEOUT); } if (this.options.bitrate) this.updateBitrate(this.mountpoint.stats.in, BITRATE_WARMUP_SHIFT); } _write(message, encoding, callback) { const constructor = message.constructor; // Update RTCM version to minimum necessary this.updateRtcmVersion(constructor.sinceVersion); // Include message type in format details this.addToFormatDetails(message.messageType); // Include GNSS system if message (if any) this.addToNavSystems(constructor.navSystem); // Set receiver type as data generator if (message instanceof rtcm_1.RtcmMessageReceiverAntennaDescriptor) this.updateGenerator(message.receiverTypeDescriptor); // Set station location (latitude/longitude, country, city) if (message instanceof rtcm_1.RtcmMessageStationArp || message instanceof rtcm_1.RtcmMessagePhysicalReferenceStationPosition) { const [latitude, longitude] = ecefProjector.unproject(message.arpEcefX / 10000, message.arpEcefY / 10000, message.arpEcefZ / 10000); this.updateLocation({ latitude: latitude, longitude: longitude }); } // Calculate RTK carrier if (this.options.carrier) { switch (message.messageType) { case rtcm_1.RtcmMessageType.GPS_L1_OBSERVATIONS: case rtcm_1.RtcmMessageType.GPS_L1_OBSERVATIONS_EXTENDED: case rtcm_1.RtcmMessageType.GLONASS_L1_OBSERVATIONS: case rtcm_1.RtcmMessageType.GLONASS_L1_OBSERVATIONS_EXTENDED: this.addToCarriers(true); break; case rtcm_1.RtcmMessageType.GPS_L1_L2_OBSERVATIONS: case rtcm_1.RtcmMessageType.GPS_L1_L2_OBSERVATIONS_EXTENDED: case rtcm_1.RtcmMessageType.GLONASS_L1_L2_OBSERVATIONS: case rtcm_1.RtcmMessageType.GLONASS_L1_L2_OBSERVATIONS_EXTENDED: this.addToCarriers(true, true); break; } if (message instanceof rtcm_1.RtcmMessageMsm) { const frequencySet = new Set(message.info.signalIds.map(id => rtcm_1.signalIdMapping[constructor.navSystem][id][0])); this.addToCarriers(frequencySet.has(1), frequencySet.has(2)); } } callback(); } updateLocation(val) { var _a, _b, _c; const accuracy = Math.pow(10, LOCATION_ACCURACY_DECIMAL); val.longitude = Math.round(val.longitude * accuracy) / accuracy; val.latitude = Math.round(val.latitude * accuracy) / accuracy; if (((_a = this.location) === null || _a === void 0 ? void 0 : _a.longitude) === val.longitude && ((_b = this.location) === null || _b === void 0 ? void 0 : _b.latitude) === val.latitude) return; this.location = val; if (this.options.location) { this.sourceEntry.latitude = val.latitude; this.sourceEntry.longitude = val.longitude; } // Update country and location identifier if places are available (_c = this.geoNames) === null || _c === void 0 ? void 0 : _c.nearest(val).then((place) => { if (place === undefined) return; if (this.options.country) this.sourceEntry.country = countries.alpha2ToAlpha3(place.countryCode); if (this.options.identifier) this.sourceEntry.identifier = place.asciiName; }); } updateGenerator(val) { if (this.options.generator) this.sourceEntry.generator = val; } addToFormatDetails(type) { if (!this.options.formatDetails) return; const current = Date.now(); if (!this.messages.has(type)) { this.messages.set(type, { last: current, timings: [] }); this.updateFormatDetails(); return; } const timing = this.messages.get(type); const time = (current - timing.last) / 1000; timing.last = current; timing.timings.push(time); if (timing.timings.length > MESSAGES_TIMING_ARRAY_SIZE) timing.timings.shift(); if (timing.timings.length === MESSAGES_TIMING_MINIMUM_COUNT) this.updateFormatDetails(); } updateFormatDetails() { const messages = []; for (const [messageType, timing] of this.messages) { const average = Math.round(timing.timings.reduce((a, b) => a + b, 0) / Math.max(timing.timings.length, 1)); // Ignore inconsistent intervals to avoid burst messages (e.g. ephemerides, MSM multi message) const includeInterval = average <= MESSAGES_TIMING_MAX_INTERVAL && average > 0 && timing.timings.length >= MESSAGES_TIMING_MINIMUM_COUNT && timing.timings.every(t => t < 5 * average); messages.push({ type: messageType, rate: includeInterval ? average : undefined }); } this.sourceEntry.formatDetails = messages.sort((a, b) => a.type - b.type); } truncateFormatDetails() { const cutoff = Date.now() - MESSAGES_TIMING_TRUNCATE_TIMEOUT; for (const [messageType, timing] of this.messages) if (timing.last < cutoff) this.messages.delete(messageType); } updateBitrate(old, warmup) { const current = this.mountpoint.stats.in; this.sourceEntry.bitrate = Math.round((current - old) * 8 * 1000 / (BITRATE_UPDATE_INTERVAL >> warmup)); setTimeout(() => { this.updateBitrate(current, Math.max(0, warmup - 1)); }, BITRATE_UPDATE_INTERVAL >> Math.max(0, warmup - 1)); } updateRtcmVersion(val) { if (!this.options.format) return; if (val <= this.rtcmVersion) return; if (val === rtcm_1.RtcmVersion.FUTURE) return; // Ignore future messages, version is currently unknown to us this.rtcmVersion = val; this.sourceEntry.format = `RTCM ${val.toFixed(1)}`; } addToNavSystems(rtcmSystem) { if (rtcmSystem === null) return; if (this.navSystems.includes(rtcmSystem)) return; this.navSystems.push(rtcmSystem); if (this.navSystemsInitialSet) this.updateNavSystems(); } updateNavSystems() { const navSystems = Object.values(sourcetable_1.Sourcetable.NavSystem); this.sourceEntry.navSystem = Array.from(this.navSystems.map(s => RTCM_NAV_SYSTEM_MAP[s])) .filter((s) => s !== undefined) .sort((a, b) => navSystems.indexOf(a) - navSystems.indexOf(b)); } addToCarriers(l1 = false, l2 = false) { if (!this.options.carrier) return; this.carriers.l1 = this.carriers.l1 || l1; this.carriers.l2 = this.carriers.l2 || l2; if (this.carriersInitialSet) this.updateCarriers(); } updateCarriers() { this.sourceEntry.carrier = this.carriers.l1 ? (this.carriers.l2 ? sourcetable_1.Sourcetable.CarrierPhaseInformation.L1_L2 : sourcetable_1.Sourcetable.CarrierPhaseInformation.L1) : sourcetable_1.Sourcetable.CarrierPhaseInformation.None; } } exports.AutoSourceEntry = AutoSourceEntry;