This commit is contained in:
Kai Vogelgesang 2023-04-16 02:42:41 +02:00
commit 56d058c2fd
Signed by: kai
GPG Key ID: 3FC8578CC818A9EB
6 changed files with 1895 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
.env

1190
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

13
Cargo.toml Normal file
View File

@ -0,0 +1,13 @@
[package]
name = "satellit-ansage"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dotenv = "0.15.0"
reqwest = { version = "0.11.16", features = ["json"] }
serde = { version = "1.0.160", features = ["derive"] }
serde_json = "1.0.96"
tokio = { version = "1.27.0", features = ["full"] }

19
src/main.rs Normal file
View File

@ -0,0 +1,19 @@
use std::{env, error::Error};
use crate::n2yo::N2YO;
mod n2yo;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
dotenv::dotenv().ok();
let api_key = env::var("N2YO_API_KEY")?;
let n2yo = N2YO::new(api_key);
let tle = n2yo.tle(25544).await?;
println!("{}", tle.tle);
Ok(())
}

199
src/n2yo/mod.rs Normal file
View File

@ -0,0 +1,199 @@
use reqwest::Client;
use serde::de::DeserializeOwned;
use self::proto::{Above, Positions, RadioPasses, VisualPasses, TLE};
mod proto;
const BASE_URL: &str = "https://api.n2yo.com/rest/v1/satellite";
pub struct N2YO {
api_key: String,
client: Client,
}
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
impl N2YO {
pub fn new(api_key: String) -> Self {
Self {
api_key,
client: Client::new(),
}
}
async fn query<T: DeserializeOwned>(&self, s: &str) -> Result<T> {
let result = self
.client
.get(s)
.query(&[("apiKey", &self.api_key)])
.send()
.await?
.json()
.await?;
Ok(result)
}
/// Get TLE
///
/// Retrieve the Two Line Elements (TLE) for a satellite identified by NORAD id.
#[allow(dead_code)]
pub async fn tle(&self, id: u64) -> Result<TLE> {
self.query(&format!("{BASE_URL}/tle/{id}")).await
}
/// Get satellite positions
///
/// Retrieve the future positions of any satellite as groundtrack (latitude, longitude) to display
/// orbits on maps. Also return the satellite's azimuth and elevation with respect to the observer
/// location. Each element in the response array is one second of calculation. First element is
/// calculated for current UTC time.
#[allow(dead_code)]
pub async fn positions(
&self,
id: u64,
lat: f64,
lng: f64,
alt: f64,
seconds: u16,
) -> Result<Positions> {
self.query(&format!(
"{BASE_URL}/positions/{id}/{lat}/{lng}/{alt}/{seconds}"
))
.await
}
/// Get visual passes
///
/// Get predicted visual passes for any satellite relative to a location on Earth. A "visual pass"
/// is a pass that should be optically visible on the entire (or partial) duration of crossing the
/// sky. For that to happen, the satellite must be above the horizon, illumintaed by Sun (not in
/// Earth shadow), and the sky dark enough to allow visual satellite observation.
#[allow(dead_code)]
pub async fn visual_passes(
&self,
id: u64,
lat: f64,
lng: f64,
alt: f64,
days: u8,
min_visibility: u64,
) -> Result<VisualPasses> {
self.query(&format!(
"{BASE_URL}/positions/{id}/{lat}/{lng}/{alt}/{days}/{min_visibility}"
))
.await
}
/// Get radio passes
///
/// The "radio passes" are similar to "visual passes", the only difference being the requirement
/// for the objects to be optically visible for observers. This function is useful mainly for
/// predicting satellite passes to be used for radio communications. The quality of the pass depends
/// essentially on the highest elevation value during the pass, which is one of the input parameters.
#[allow(dead_code)]
pub async fn radio_passes(
&self,
id: u64,
lat: f64,
lng: f64,
alt: f64,
days: u8,
min_elevation: u64,
) -> Result<RadioPasses> {
self.query(&format!(
"{BASE_URL}/positions/{id}/{lat}/{lng}/{alt}/{days}/{min_elevation}"
))
.await
}
/// What's up?
///
/// The "above" function will return all objects within a given search radius above observer's location.
/// The radius (θ), expressed in degrees, is measured relative to the point in the sky directly above
/// an observer (azimuth). This image may offer a better explanation:
///
/// ![](https://www.n2yo.com/img/above.png)
///
/// The search radius range is 0 to 90 degrees, nearly 0 meaning to show only satellites passing exactly
/// above the observer location, while 90 degrees to return all satellites above the horizon. Since there
/// are many satellites and debris in the sky at any point in time, the result could be filtered by
/// satellite category (integer). The following categories are currently available at n2yo.com:
///
/// | Category | id |
/// |----------|---:|
/// |Amateur radio|18|
/// |Beidou Navigation System|35|
/// |Brightest|1|
/// |Celestis|45|
/// |Chinese Space Station|54|
/// |CubeSats|32|
/// |Disaster monitoring|8|
/// |Earth resources|6|
/// |Education|29|
/// |Engineering|28|
/// |Experimental|19|
/// |Flock|48|
/// |Galileo|22|
/// |Geodetic|27|
/// |Geostationary|10|
/// |Global Positioning System (GPS) Constellation|50|
/// |Global Positioning System (GPS) Operational|20|
/// |Globalstar|17|
/// |Glonass Constellation|51|
/// |Glonass Operational|21|
/// |GOES|5|
/// |Gonets|40|
/// |Gorizont|12|
/// |Intelsat|11|
/// |Iridium|15|
/// IRNSS|46|
/// ISS|2|
/// Lemur|49|
/// Military|30|
/// Molniya|14|
/// Navy Navigation Satellite System|24|
/// NOAA|4|
/// O3B Networks|43|
/// OneWeb|53|
/// Orbcomm|16|
/// Parus|38|
/// QZSS|47|
/// Radar Calibration|31|
/// Raduga|13|
/// Russian LEO Navigation|25|
/// Satellite-Based Augmentation System|23|
/// Search & rescue|7|
/// Space & Earth Science|26|
/// Starlink|52|
/// Strela|39|
/// Tracking and Data Relay Satellite System|9|
/// Tselina|44|
/// Tsikada|42|
/// Tsiklon|41|
/// TV|34|
/// Weather|3|
/// Westford Needles|37|
/// XM and Sirius|33|
/// Yaogan|36|
///
/// Please use this function responsably as there is a lot of CPU needed in order to calculate exact
/// positions for all satellites in the sky. The function will return altitude, latitude and longitude
/// of satellites footprints to be displayed on a map, and some minimal information to identify the object.
#[allow(dead_code)]
pub async fn above(
&self,
lat: f64,
lng: f64,
alt: f64,
search_radius: u8,
category_id: u8,
) -> Result<Above> {
self.query(&format!(
"{BASE_URL}/above/{lat}/{lng}/{alt}/{search_radius}/{category_id}"
))
.await
}
}

472
src/n2yo/proto.rs Normal file
View File

@ -0,0 +1,472 @@
#![allow(clippy::upper_case_acronyms)]
#![allow(dead_code)]
use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub struct Info {
/// NORAD id used in input
#[serde(rename = "satid")]
pub sat_id: u64,
/// Satellite name
#[serde(rename = "satname")]
pub sat_name: String,
/// Count of transactions performed with this API key in last 60 minutes
#[serde(rename = "transactionscount")]
pub transaction_count: u64,
}
#[derive(Debug, Deserialize)]
pub struct TLE {
pub info: Info,
/// TLE on single line string. Split the line in two by \r\n to get original two lines
pub tle: String,
}
#[derive(Debug, Deserialize)]
pub struct Positions {
pub info: Info,
pub positions: Vec<Position>,
}
#[derive(Debug, Deserialize)]
pub struct Position {
/// Satellite footprint latitude (decimal degrees format)
#[serde(rename = "satlatitude")]
pub sat_latitude: f64,
/// Satellite footprint longitude (decimal degrees format)
#[serde(rename = "satlongitude")]
pub sat_longitude: f64,
#[serde(rename = "sataltitude")]
pub sat_altitude: f64,
/// Satellite azimuth with respect to observer's location (degrees)
pub azimuth: f64,
/// Satellite elevation with respect to observer's location (degrees)
pub elevation: f64,
/// Satellite right ascension (degrees)
pub ra: f64,
/// Satellite declination (degrees)
pub dec: f64,
/// Unix time for this position (seconds). You should convert this UTC value to observer's time zone
pub timestamp: u64,
}
#[derive(Debug, Deserialize)]
pub struct PassInfo {
/// NORAD id used in input
#[serde(rename = "satid")]
pub sat_id: u64,
/// Satellite name
#[serde(rename = "satname")]
pub sat_name: String,
/// Count of transactions performed with this API key in last 60 minutes
#[serde(rename = "transactionscount")]
pub transaction_count: u64,
/// Count of passes returned
#[serde(rename = "passescount")]
pub pass_count: u64,
}
#[derive(Debug, Deserialize)]
pub enum Compass {
N,
NNE,
NE,
ENE,
E,
ESE,
SE,
SSE,
S,
SSW,
SW,
WSW,
W,
WNW,
NW,
NNW,
}
#[derive(Debug, Deserialize)]
pub struct VisualPasses {
pub info: PassInfo,
pub passes: Option<Vec<VisualPass>>,
}
#[derive(Debug, Deserialize)]
pub struct VisualPass {
// start
/// Satellite azimuth for the start of this pass (relative to the observer, in degrees)
#[serde(rename = "startAz")]
pub start_az: f64,
/// Satellite azimuth for the start of this pass (relative to the observer). Possible values: N, NE, E, SE, S, SW, W, NW
#[serde(rename = "startAzCompass")]
pub start_az_compass: Compass,
/// Satellite elevation for the start of this pass (relative to the observer, in degrees)
#[serde(rename = "startEl")]
pub start_el: f64,
/// Unix time for the start of this pass. You should convert this UTC value to observer's time zone
#[serde(rename = "startUTC")]
pub start_utc: u64,
// max
/// Satellite azimuth for the max elevation of this pass (relative to the observer, in degrees)
#[serde(rename = "maxAz")]
pub max_az: f64,
/// Satellite azimuth for the max elevation of this pass (relative to the observer). Possible values: N, NE, E, SE, S, SW, W, NW
#[serde(rename = "maxAzCompass")]
pub max_az_compass: Compass,
/// Satellite max elevation for this pass (relative to the observer, in degrees)
#[serde(rename = "maxEl")]
pub max_el: f64,
/// Unix time for the max elevation of this pass. You should convert this UTC value to observer's time zone
#[serde(rename = "maxUTC")]
pub max_utc: u64,
// end
/// Satellite azimuth for the end of this pass (relative to the observer, in degrees)
#[serde(rename = "endAz")]
pub end_az: f64,
/// Satellite azimuth for the end of this pass (relative to the observer). Possible values: N, NE, E, SE, S, SW, W, NW
#[serde(rename = "endAzCompass")]
pub end_az_compass: Compass,
/// Satellite elevation for the end of this pass (relative to the observer, in degrees)
#[serde(rename = "endEl")]
pub end_el: f64,
/// Unix time for the end of this pass. You should convert this UTC value to observer's time zone
#[serde(rename = "endUTC")]
pub end_utc: u64,
/// Max visual magnitude of the pass, same scale as star brightness. If magnitude cannot be determined, the value is 100000
pub mag: f64,
/// Total visible duration of this pass (in seconds)
pub duration: u64,
}
#[derive(Debug, Deserialize)]
pub struct RadioPasses {
pub info: PassInfo,
pub passes: Option<Vec<RadioPass>>,
}
#[derive(Debug, Deserialize)]
pub struct RadioPass {
// start
/// Satellite azimuth for the start of this pass (relative to the observer, in degrees)
#[serde(rename = "startAz")]
pub start_az: f64,
/// Satellite azimuth for the start of this pass (relative to the observer). Possible values: N, NE, E, SE, S, SW, W, NW
#[serde(rename = "startAzCompass")]
pub start_az_compass: Compass,
/// Unix time for the start of this pass. You should convert this UTC value to observer's time zone
#[serde(rename = "startUTC")]
pub start_utc: u64,
// max
/// Satellite azimuth for the max elevation of this pass (relative to the observer, in degrees)
#[serde(rename = "maxAz")]
pub max_az: f64,
/// Satellite azimuth for the max elevation of this pass (relative to the observer). Possible values: N, NE, E, SE, S, SW, W, NW
#[serde(rename = "maxAzCompass")]
pub max_az_compass: Compass,
/// Satellite max elevation for this pass (relative to the observer, in degrees)
#[serde(rename = "maxEl")]
pub max_el: f64,
/// Unix time for the max elevation of this pass. You should convert this UTC value to observer's time zone
#[serde(rename = "maxUTC")]
pub max_utc: u64,
// end
/// Satellite azimuth for the end of this pass (relative to the observer, in degrees)
#[serde(rename = "endAz")]
pub end_az: f64,
/// Satellite azimuth for the end of this pass (relative to the observer). Possible values: N, NE, E, SE, S, SW, W, NW
#[serde(rename = "endAzCompass")]
pub end_az_compass: Compass,
/// Unix time for the end of this pass. You should convert this UTC value to observer's time zone
#[serde(rename = "endUTC")]
pub end_utc: u64,
}
#[derive(Debug, Deserialize)]
pub struct Above {
pub info: AboveInfo,
pub above: Option<Vec<AboveSat>>,
}
#[derive(Debug, Deserialize)]
pub struct AboveInfo {
/// Category name (ANY if category id requested was 0)
pub category: String,
/// Count of transactions performed with this API key in last 60 minutes
#[serde(rename = "transactionscount")]
pub transaction_count: u64,
/// Count of satellites returned
#[serde(rename = "satcount")]
pub sat_count: u64,
}
#[derive(Debug, Deserialize)]
pub struct AboveSat {
/// Satellite NORAD id
#[serde(rename = "satid")]
pub sat_id: u64,
/// Satellite international designator
#[serde(rename = "satname")]
pub sat_name: String,
/// Satellite name
#[serde(rename = "intDesignator")]
pub int_designator: String,
/// Satellite launch date (YYYY-MM-DD)
#[serde(rename = "launchDate")]
pub launch_date: String,
/// Satellite footprint latitude (decimal degrees format)
#[serde(rename = "satlat")]
pub sat_lat: f64,
/// Satellite footprint longitude (decimal degrees format)
#[serde(rename = "satlng")]
pub sat_lng: f64,
/// Satellite altitude (km)
#[serde(rename = "satalt")]
pub sat_alt: f64,
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_tle() {
let data = r#"{
"info": {
"satid": 25544,
"satname": "SPACE STATION",
"transactionscount": 4
},
"tle": "1 25544U 98067A 18077.09047010 .00001878 00000-0 35621-4 0 9999\r\n2 25544 51.6412 112.8495 0001928 208.4187 178.9720 15.54106440104358"
}"#;
let parsed = serde_json::from_str::<TLE>(data);
assert!(parsed.is_ok())
}
#[test]
fn test_positions() {
let data = r#"{
"info": {
"satname": "SPACE STATION",
"satid": 25544,
"transactionscount": 5
},
"positions": [
{
"satlatitude": -39.90318514,
"satlongitude": 158.28897924,
"sataltitude": 417.85,
"azimuth": 254.31,
"elevation": -69.09,
"ra": 44.77078138,
"dec": -43.99279118,
"timestamp": 1521354418
},
{
"satlatitude": -39.86493451,
"satlongitude": 158.35261287,
"sataltitude": 417.84,
"azimuth": 254.33,
"elevation": -69.06,
"ra": 44.81676119,
"dec": -43.98086419,
"timestamp": 1521354419
}
]
}"#;
let parsed = serde_json::from_str::<Positions>(data);
assert!(parsed.is_ok());
}
#[test]
fn test_visualpasses() {
let data = r#"{
"info": {
"satid": 25544,
"satname": "SPACE STATION",
"transactionscount": 4,
"passescount": 3
},
"passes": [
{
"startAz": 307.21,
"startAzCompass": "NW",
"startEl": 13.08,
"startUTC": 1521368025,
"maxAz": 225.45,
"maxAzCompass": "SW",
"maxEl": 78.27,
"maxUTC": 1521368345,
"endAz": 132.82,
"endAzCompass": "SE",
"endEl": 0,
"endUTC": 1521368660,
"mag": -2.4,
"duration": 485
},
{
"startAz": 311.56,
"startAzCompass": "NW",
"startEl": 50.94,
"startUTC": 1521451295,
"maxAz": 37.91,
"maxAzCompass": "NE",
"maxEl": 52.21,
"maxUTC": 1521451615,
"endAz": 118.61,
"endAzCompass": "ESE",
"endEl": 0,
"endUTC": 1521451925,
"mag": -2,
"duration": 325
},
{
"startAz": 291.06,
"startAzCompass": "WNW",
"startEl": 3.47,
"startUTC": 1521457105,
"maxAz": 231.58,
"maxAzCompass": "SW",
"maxEl": 14.75,
"maxUTC": 1521457380,
"endAz": 170.63,
"endAzCompass": "S",
"endEl": 0,
"endUTC": 1521457650,
"mag": -0.1,
"duration": 485
}
]
}"#;
let parsed = serde_json::from_str::<VisualPasses>(data);
assert!(parsed.is_ok());
}
#[test]
fn test_radiopasses() {
let data = r#"{
"info": {
"satid": 25544,
"satname": "SPACE STATION",
"transactionscount": 2,
"passescount": 2
},
"passes": [
{
"startAz": 311.57,
"startAzCompass": "NW",
"startUTC": 1521451295,
"maxAz": 37.98,
"maxAzCompass": "NE",
"maxEl": 52.19,
"maxUTC": 1521451615,
"endAz": 118.6,
"endAzCompass": "ESE",
"endUTC": 1521451925
},
{
"startAz": 242.34,
"startAzCompass": "WSW",
"startUTC": 1521600275,
"maxAz": 328.03,
"maxAzCompass": "NW",
"maxEl": 49.59,
"maxUTC": 1521600595,
"endAz": 47.97,
"endAzCompass": "NE",
"endUTC": 1521600905
}
]
}"#;
let parsed = serde_json::from_str::<RadioPasses>(data);
assert!(parsed.is_ok());
}
#[test]
fn test_above() {
let data = r#"{
"info": {
"category": "Amateur radio",
"transactionscount": 17,
"satcount": 3
},
"above": [
{
"satid": 20480,
"satname": "JAS 1B (FUJI 2)",
"intDesignator": "1990-013C",
"launchDate": "1990-02-07",
"satlat": 49.5744,
"satlng": -96.7081,
"satalt": 1227.9326
},
{
"satid": 26609,
"satname": "AMSAT OSCAR 40",
"intDesignator": "2000-072B",
"launchDate": "2000-11-16",
"satlat": 5.5105,
"satlng": -21.4478,
"satalt": 49678.6389
},
{
"satid": 40719,
"satname": "DEORBITSAIL",
"intDesignator": "2015-032E",
"launchDate": "2015-07-10",
"satlat": 43.8106,
"satlng": -90.3944,
"satalt": 657.5516
}
]
}"#;
let parsed = serde_json::from_str::<Above>(data);
assert!(parsed.is_ok());
}
}