rust-galmon-web/src/api/mod.rs

376 lines
11 KiB
Rust

use failure::Error;
use serde::{Serialize, Deserialize};
use std::collections::HashMap;
use std::time::SystemTime;
#[macro_use]
mod adapter_macro;
mod apiservice;
pub mod world_geo;
pub use self::apiservice::{APIService, FetchTask};
use crate::config::Config;
impl<Message: 'static> APIService<Message> {
pub fn api_world_geo<F>(&self, config: &Config, callback: F) -> Option<FetchTask>
where
F: FnOnce(Result<world_geo::FeatureCollection, Error>) -> Message + 'static,
{
self.api_get(config, format_args!("/geo/world.geojson"), callback)
}
}
pub type DateTime = chrono::DateTime<chrono::Utc>;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Global {
#[serde(rename = "gps-offset-ns")]
pub gps_offset_ns: f64,
#[serde(rename = "gps-utc-offset-ns")]
pub gps_utc_offset_ns: f64,
#[serde(rename = "last-seen", with = "chrono::serde::ts_seconds")]
pub last_seen: DateTime,
#[serde(rename = "leap-second-planned")]
pub leap_second_planned: bool,
#[serde(rename = "leap-seconds")]
pub leap_seconds: f64,
#[serde(rename = "utc-offset-ns")]
pub utc_offset_ns: f64,
}
impl<Message: 'static> APIService<Message> {
pub fn api_global<F>(&self, config: &Config, callback: F) -> Option<FetchTask>
where
F: FnOnce(Result<Global, Error>) -> Message + 'static,
{
self.api_get(config, format_args!("/global.json"), callback)
}
}
#[derive(Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash, Debug, serde_repr::Serialize_repr, serde_repr::Deserialize_repr)]
#[repr(u8)]
pub enum GNS {
GPS = 0, // US
Galileo = 2, // EU
BeiDou = 3, // CN
Glonass = 6, // RU
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AlmanacEntry {
// NOT in Glonass
#[serde(rename = "eph-ecefX")]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub eph_ecef_x: Option<f64>,
#[serde(rename = "eph-ecefY")]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub eph_ecef_y: Option<f64>,
#[serde(rename = "eph-ecefZ")]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub eph_ecef_z: Option<f64>,
// optional in Glonass
#[serde(rename = "eph-latitude")]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub eph_latitude: Option<f64>,
#[serde(rename = "eph-longitude")]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub eph_longitude: Option<f64>,
// NOT in Glonass
pub t: Option<f64>,
pub t0e: Option<f64>,
// all
pub gnssid: GNS,
pub name: String,
pub observed: bool,
pub inclination: f64,
}
pub type Almanac = HashMap<String, AlmanacEntry>;
impl<Message: 'static> APIService<Message> {
pub fn api_almanac<F>(&self, config: &Config, callback: F) -> Option<FetchTask>
where
F: FnOnce(Result<Almanac, Error>) -> Message + 'static,
{
self.api_get(config, format_args!("/almanac.json"), callback)
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Observer {
pub id: u32,
#[serde(rename = "last-seen", with = "chrono::serde::ts_seconds")]
pub last_seen: DateTime,
pub latitude: f64,
pub longitude: f64,
#[serde(rename = "svs")]
pub satellite_vehicles: HashMap<String, ObservedSatelliteVehicle>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ObservedSatelliteVehicle {
#[serde(rename = "age-s")]
pub age_s: f32, // u32?
pub azi: f64,
pub db: u32,
pub elev: f64,
#[serde(rename = "fullName")]
pub full_name: String,
pub gnss: GNS,
#[serde(rename = "last-seen")]
pub last_seen: i64,
pub name: String,
pub prres: f64,
pub sigid: u32,
pub sv: u32,
}
pub type ObserverList = Vec<Observer>;
impl<Message: 'static> APIService<Message> {
pub fn api_observers<F>(&self, config: &Config, callback: F) -> Option<FetchTask>
where
F: FnOnce(Result<ObserverList, Error>) -> Message + 'static,
{
self.api_get(config, format_args!("/observers.json"), callback)
}
}
#[derive(Clone, Copy, Debug)]
/// A certain point in time in some reference system, in seconds
pub struct Instant {
pub week_number: u16, // depending on source only might have rolled on 8-bit
pub time_of_week: u32,
}
impl Instant {
pub fn system_time(self) -> SystemTime {
SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(self.epoch())
}
pub fn unroll_weeknumber(self) -> u32 {
let current_epoch = SystemTime::now().duration_since(SystemTime::now()).expect("before 1970-01-01").as_secs();
let current_wn = (current_epoch / (86400*7)) as u32;
let wn = self.week_number as u32;
let wn_bits = (16 - self.week_number.leading_zeros()).min(8); // assume at least 8-bit precision in weeknumber
let wn_mask = !0u32 << wn_bits;
let round_up = 1 << (wn_bits - 1); // if we already reached "halftime" round up
wn + ((current_wn + round_up) & wn_mask) // add bits from current_wn
}
pub fn epoch(self) -> u64 {
self.unroll_weeknumber() as u64 * (86400*7) + self.time_of_week as u64
}
}
#[derive(Clone, Copy, Debug)]
pub struct Vec3 {
pub x: f64,
pub y: f64,
pub z: f64,
}
#[derive(Clone, Debug)]
pub struct ReferenceTimeOffset {
/// offset in 2**(-30) seconds at base Instant (close to nanoseconds)
pub base_offset: i32,
/// correction in 2**(-50) seconds per second since base Instant
pub correction: i32,
/// time at which constant offset was measured
pub base: Instant,
/// text describing delta for some "current" (last_seen) Instant in nanoseconds offset and change in nanoseconds per day.
pub delta: String,
}
mod sv {
use super::*;
adapter!{
LastSeen => Instant {
#[serde(rename = "wn")]
week_number: u16,
#[serde(rename = "tow")]
time_of_week: u32,
}
}
adapter!{
Position => Option<Vec3> {
x: f64,
y: f64,
z: f64,
}
}
adapter!{
UtcOffsetInstant => Option<Instant> {
#[serde(rename = "wn0t")]
week_number: u16,
#[serde(rename = "t0t")]
time_of_week: u32,
}
}
adapter!{
// bug https://github.com/ahupowerdns/galmon/issues/8: Glonass sends a0 and a1, but not the other values
UtcOffset => Option<ReferenceTimeOffset> {
#[serde(rename = "a0")]
base_offset: i32,
#[serde(rename = "a1")]
correction: i32,
#[serde(flatten, with = "UtcOffsetInstant")]
base: Instant,
#[serde(rename = "delta-utc")]
delta: String,
}
}
adapter!{
GpsOffsetInstant => Option<Instant> {
#[serde(rename = "wn0g")]
week_number: u16,
#[serde(rename = "t0g")]
time_of_week: u32,
}
}
adapter!{
GpsOffset => Option<ReferenceTimeOffset> {
#[serde(rename = "a0g")]
base_offset: i32,
#[serde(rename = "a1g")]
correction: i32,
#[serde(flatten, with = "GpsOffsetInstant")]
base: Instant,
#[serde(rename = "delta-gps")]
delta: String,
}
}
}
// TODO: "undo" flatten #[serde(default, skip_serializing_if = "Option::is_none")]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SatelliteVehicle {
/* Identification: */
gnssid: GNS,
svid: u32, /* vehicle id; can be moved between satellites */
sigid: u32, /* "signal", also appended as "@{sigid}" to the full name */
/* Data: */
/// Signal In Space Accuracy
#[serde(default, skip_serializing_if = "Option::is_none")]
sisa: Option<String>,
#[serde(rename = "eph-age-m", default, skip_serializing_if = "Option::is_none")]
/// Age of ephemeris in minutes
eph_age_m: Option<f32>,
#[serde(rename = "best-tle", default, skip_serializing_if = "Option::is_none")]
best_tle: Option<String>,
#[serde(rename = "best-tle-dist", default, skip_serializing_if = "Option::is_none")]
best_tle_dist: Option<f64>,
#[serde(rename = "best-tle-int-desig", default, skip_serializing_if = "Option::is_none")]
best_tle_int_desig: Option<String>,
#[serde(rename = "best-tle-norad", default, skip_serializing_if = "Option::is_none")]
best_tle_norad: Option<i32>,
#[serde(rename = "alma-dist", default, skip_serializing_if = "Option::is_none")]
alma_dist: Option<f64>, // distance from almanac position in kilometers
#[serde(default, skip_serializing_if = "Option::is_none")]
aode: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
aodc: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
iod: Option<u16>,
// IOD data:
#[serde(default, skip_serializing_if = "Option::is_none")]
af0: Option<i32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
af1: Option<i32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
af2: Option<u8>,
#[serde(default, skip_serializing_if = "Option::is_none")]
t0c: Option<u16>, // clock epoch
#[serde(flatten, with = "sv::Position")]
position: Option<Vec3>,
// utc offset (all but Glonass): combined data
#[serde(flatten, with = "sv::UtcOffset")]
utc_offset: Option<ReferenceTimeOffset>,
// GPS offset (only Galileo and BeiDou)
#[serde(flatten, with = "sv::GpsOffset")]
gpc_offset: Option<ReferenceTimeOffset>,
#[serde(rename = "dtLS")]
dt_ls: i8,
#[serde(default, skip_serializing_if = "Option::is_none")]
health: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
healthissue: Option<u32>, // some codes?
// Galileo only: Health flags for E1 (common) and E5 (uncommon) frequencies.
#[serde(default, skip_serializing_if = "Option::is_none")]
e1bhs: Option<u8>,
#[serde(default, skip_serializing_if = "Option::is_none")]
e1bdvs: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
e5bhs: Option<u8>,
#[serde(default, skip_serializing_if = "Option::is_none")]
e5bdvs: Option<bool>,
#[serde(rename = "latest-disco", default, skip_serializing_if = "Option::is_none")]
latest_disco: Option<f64>,
#[serde(rename = "latest-disco-age", default, skip_serializing_if = "Option::is_none")]
latest_disco_age: Option<f64>,
#[serde(rename = "time-disco", default, skip_serializing_if = "Option::is_none")]
time_disco: Option<f64>,
#[serde(flatten, with = "sv::LastSeen")]
last_seen: Instant,
/// Number of seconds since we've last received from this SV. A satellite can be out of sight for a long time
#[serde(rename = "last-seen-s")]
last_seen_s: i64,
#[serde(rename = "fullName")]
full_name: String, // format!("{}@{}", self.name, self.sigid)
name: String,
perrecv: HashMap<u32, SatelliteVehiclePerReceiver>, // keys: Observer `id`
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SatelliteVehiclePerReceiver {
#[serde(rename = "elev")]
elevation: f64,
#[serde(rename = "azi", default, skip_serializing_if = "Option::is_none")]
azimuth: Option<f64>,
db: i32,
#[serde(rename = "last-seen-s")]
last_seen_s: i64,
prres: f64,
delta_hz: Option<f64>,
delta_hz_corr: Option<f64>,
}
pub type SatelliteVehicles = HashMap<String, SatelliteVehicle>;
impl<Message: 'static> APIService<Message> {
pub fn api_satellite_vehicles<F>(&self, config: &Config, callback: F) -> Option<FetchTask>
where
F: FnOnce(Result<SatelliteVehicles, Error>) -> Message + 'static,
{
self.api_get(config, format_args!("/svs.json"), callback)
}
}