initial commit
This commit is contained in:
commit
44ef27b2b6
|
@ -0,0 +1 @@
|
|||
/target
|
|
@ -0,0 +1,8 @@
|
|||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/vfsm.iml" filepath="$PROJECT_DIR$/.idea/vfsm.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="CPP_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,17 @@
|
|||
[package]
|
||||
name = "vfsm"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
log = "0.4.17"
|
||||
simple_logger = "4.1.0"
|
||||
url = { version = "2.3.1", features = ["serde"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1.0.96"
|
||||
chrono = { version = "0.4.24", features = ["serde"] }
|
||||
reqwest = { version = "0.11.17", features = ["json"] }
|
||||
tokio = { version = "1", features = ["full"]}
|
||||
rand = "0.8.5"
|
|
@ -0,0 +1,119 @@
|
|||
use rand::Rng;
|
||||
use crate::vatlink::data_v3::VatlinkDataV3FlightPlan;
|
||||
|
||||
/*
|
||||
┌┬┐
|
||||
│││
|
||||
├┼┤
|
||||
│││
|
||||
└┴┘
|
||||
|
||||
──
|
||||
*/
|
||||
|
||||
pub const BOTTOM_RIGHT: char = '┌';
|
||||
pub const LEFT_BOTTOM_RIGHT: char = '┬';
|
||||
pub const LEFT_BOTTOM: char = '┐';
|
||||
|
||||
|
||||
pub fn print_fps(fp: &VatlinkDataV3FlightPlan, callsign: &str) {
|
||||
let ac_type = fp.aircraft_faa.clone();
|
||||
let flight_type = fp.flight_rules.clone();
|
||||
let blocks_leftmost = [callsign.len(), ac_type.len(), flight_type.len()];
|
||||
let longest_block_leftmost = blocks_leftmost.iter().max().unwrap();
|
||||
let ft_offset = longest_block_leftmost - 3;
|
||||
let cid = rand::thread_rng().gen_range(0..999);
|
||||
let leftmost_with_padding = longest_block_leftmost + 2;
|
||||
|
||||
let squawk = fp.assigned_transponder.clone();
|
||||
let departure_time = format!("P{}", fp.deptime);
|
||||
let cruise_alt = fp.altitude.clone();
|
||||
let blocks_2nd_leftmost = [squawk.len(), departure_time.len(), cruise_alt.len()];
|
||||
let longest_block_2nd_leftmost = blocks_2nd_leftmost.iter().max().unwrap();
|
||||
let tnd_leftmost_with_padding = longest_block_2nd_leftmost + 2;
|
||||
|
||||
let departure = fp.departure.clone();
|
||||
let arrival = fp.arrival.clone();
|
||||
let alternate = fp.alternate.clone();
|
||||
|
||||
let MAIN_WRAP = 68;
|
||||
|
||||
let mut route = fp.route.clone();
|
||||
|
||||
if fp.route.len() > 68 * 3 {
|
||||
route = route[..68*3-3].to_string() + "...";
|
||||
}
|
||||
|
||||
let route_chars: Vec<char> = route.chars().collect();
|
||||
let mut route_split = &mut route_chars.chunks(MAIN_WRAP)
|
||||
.map(|chunk| chunk.iter().collect::<String>())
|
||||
.collect::<Vec<_>>();
|
||||
route_split.push("".to_string());
|
||||
route_split.push("".to_string());
|
||||
route_split.push("".to_string());
|
||||
|
||||
let mut rem = fp.remarks.clone();
|
||||
|
||||
let room_for_remarks = if fp.route.len() < 68 {
|
||||
4
|
||||
} else if fp.route.len() < 68 * 2 {
|
||||
3
|
||||
} else {
|
||||
2
|
||||
};
|
||||
|
||||
if fp.remarks.len() > 68 * room_for_remarks {
|
||||
rem = rem[..68*room_for_remarks-3].to_string() + "...";
|
||||
}
|
||||
|
||||
let rem_chars: Vec<char> = rem.chars().collect();
|
||||
let mut rem_split = &mut rem_chars.chunks(MAIN_WRAP)
|
||||
.map(|chunk| chunk.iter().collect::<String>())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
|
||||
|
||||
let rem_01;
|
||||
let rem_02;
|
||||
let rem_03;
|
||||
let rem_04;
|
||||
|
||||
|
||||
|
||||
if rem_split.len() == 1 {
|
||||
rem_01 = "".to_string();
|
||||
rem_02 = "".to_string();
|
||||
rem_03 = "".to_string();
|
||||
rem_04 = rem_split[0].clone();
|
||||
} else if rem_split.len() == 2 {
|
||||
rem_01 = "".to_string();
|
||||
rem_02 = "".to_string();
|
||||
rem_03 = rem_split[0].clone();
|
||||
rem_04 = rem_split[1].clone();
|
||||
} else if rem_split.len() == 3 {
|
||||
rem_01 = "".to_string();
|
||||
rem_02 = rem_split[0].clone();
|
||||
rem_03 = rem_split[1].clone();
|
||||
rem_04 = rem_split[2].clone();
|
||||
} else if rem_split.is_empty() {
|
||||
rem_01 = "".to_string();
|
||||
rem_02 = "".to_string();
|
||||
rem_03 = "".to_string();
|
||||
rem_04 = "".to_string();
|
||||
} else {
|
||||
rem_01 = rem_split[0].clone();
|
||||
rem_02 = rem_split[1].clone();
|
||||
rem_03 = rem_split[2].clone();
|
||||
rem_04 = rem_split[3].clone();
|
||||
}
|
||||
|
||||
println!("--- FLIGHT PROGRESS STRIP READOUT FOR {} ---", callsign);
|
||||
println!("┌{:─>leftmost_with_padding$}┬{:─>tnd_leftmost_with_padding$}┬──────┬──────────────────────────────────────────────────────────────────────┬──────┬──────┬──────┐", "", "");
|
||||
println!("│ {: >longest_block_leftmost$} │ {: >longest_block_2nd_leftmost$} │ {: >4} │ {: <MAIN_WRAP$} │ │ │ │", callsign, squawk, departure, route_split[0]);
|
||||
println!("│{: >leftmost_with_padding$}├{:─>tnd_leftmost_with_padding$}┤ {: >4} │ {: <MAIN_WRAP$} │──────┼──────┼──────┤", "", "", arrival, route_split[1].to_string()+rem_01.as_str());
|
||||
println!("│ {: >longest_block_leftmost$} │ {: >longest_block_2nd_leftmost$} │ {: >4} │ {: <MAIN_WRAP$} │ │ │ │", ac_type, departure_time, alternate, route_split[2].to_string()+rem_02.as_str());
|
||||
println!("│{: >leftmost_with_padding$}├{:─>tnd_leftmost_with_padding$}┤ │ {: <MAIN_WRAP$} │──────┼──────┼──────┤", "", "", rem_03);
|
||||
println!("│ {:0>3}{: >ft_offset$} │ {: >longest_block_2nd_leftmost$} │ │ {: <MAIN_WRAP$} │ │ │ │", cid, flight_type, cruise_alt, rem_04);
|
||||
println!("└{:─>leftmost_with_padding$}┴{:─>tnd_leftmost_with_padding$}┴──────┴──────────────────────────────────────────────────────────────────────┴──────┴──────┴──────┘", "", "");
|
||||
println!("--- END FLIGHT PROGRESS STRIP READOUT FOR {} ---", callsign);
|
||||
}
|
|
@ -0,0 +1,160 @@
|
|||
use std::error::Error;
|
||||
use std::{io, thread};
|
||||
use std::io::Write;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use log::{debug, error, info, Level, warn};
|
||||
use tokio::select;
|
||||
use tokio::sync::mpsc::{Receiver, channel};
|
||||
use tokio::sync::RwLock;
|
||||
use tokio::time::{sleep, sleep_until};
|
||||
use crate::fps::print_fps;
|
||||
|
||||
use crate::vatlink::{vatlink_init, VATLINK_STATUS_URL, VATLINK_VERSION};
|
||||
use crate::vatlink::data_v3::VATLINK_DATAV3_UPDATE_INTERVAL;
|
||||
use crate::vatlink::data_v3_client::DataClientV3;
|
||||
|
||||
|
||||
pub mod vatlink;
|
||||
pub mod fps;
|
||||
|
||||
pub const VFSM_VERSION: &str = "0.1.0";
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
simple_logger::init_with_level(Level::Debug).unwrap();
|
||||
|
||||
info!("VATSIM Flight System Manager - By coredoesdev - Version {}, built with vatlink {}", VFSM_VERSION, VATLINK_VERSION);
|
||||
info!("Initializing vatlink...");
|
||||
|
||||
// pull vatlink server data
|
||||
let vatlink_init_data = vatlink_init(VATLINK_STATUS_URL).await?;
|
||||
|
||||
info!("Retrieved vatlink initialization data, connecting to servers...");
|
||||
|
||||
let mut datav3_client = DataClientV3::new(vatlink_init_data.data.v3).await?;
|
||||
let mut client = Arc::new(RwLock::new(datav3_client));
|
||||
|
||||
{
|
||||
debug!("connected: {} pilots online", client.read().await.last_saved_data().pilots.len());
|
||||
}
|
||||
|
||||
let (update_shutdown_tx, update_shutdown_rx) = channel::<()>(128);
|
||||
let update_client = client.clone();
|
||||
|
||||
let update_task = tokio::spawn(update_main(update_client, update_shutdown_rx));
|
||||
|
||||
let sector_lat: f64 = 0.0;
|
||||
let sector_long: f64 = 0.0;
|
||||
let sector_loaded: bool = false;
|
||||
|
||||
loop {
|
||||
let mut input = String::new();
|
||||
print!("vfsm> ");
|
||||
|
||||
io::stdout().flush()?;
|
||||
|
||||
/*
|
||||
Commands:
|
||||
s/callsign - Show a flight strip (i.e s/AF1)
|
||||
p/callsign - Print a flight strip (i.e. p/AF1)
|
||||
w/callsign - Watch a specific aircraft. Will show flight strip, print it, and add it to your watchlist, where changes will be automatically shown and printed. (i.e. w/AF1)
|
||||
u/callsign - Unwatch a specific aircraft.
|
||||
d/ - Download new vatlink data
|
||||
:/sector - Load a sector file
|
||||
c/lat/long/range/sector - Create and load a sector file
|
||||
l/ - List aircraft in sector
|
||||
h/ - Help
|
||||
*/
|
||||
|
||||
let _ = io::stdin().read_line(&mut input).is_ok();
|
||||
|
||||
let input = input.trim().to_string();
|
||||
|
||||
let input_split = input.split('/').collect::<Vec<&str>>();
|
||||
let command = input_split[0];
|
||||
|
||||
match command {
|
||||
"s" => {
|
||||
if input_split.len() != 2 {
|
||||
error!("syntax s/<callsign>");
|
||||
continue;
|
||||
}
|
||||
let callsign = input_split[1];
|
||||
|
||||
if !sector_loaded {
|
||||
warn!("no sector loaded");
|
||||
}
|
||||
|
||||
{
|
||||
let client = client.read().await;
|
||||
let maybe_pilot = client.last_saved_data().pilots.iter().find(|u| u.callsign == callsign);
|
||||
match maybe_pilot {
|
||||
Some(pilot) => {
|
||||
if let Some(fp) = &pilot.flight_plan {
|
||||
print_fps(fp, callsign);
|
||||
} else {
|
||||
error!("no active flight plan for '{}'", callsign);
|
||||
}
|
||||
},
|
||||
None => {
|
||||
error!("no active pilot by callsign '{}'", callsign);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"p" => todo!(),
|
||||
"w" => todo!(),
|
||||
"u" => todo!(),
|
||||
"d" => todo!(),
|
||||
":" => todo!(),
|
||||
"c" => todo!(),
|
||||
"l" => todo!(),
|
||||
"h" => {
|
||||
info!("vfsm: Commands:");
|
||||
info!("s/callsign - Show a flight strip (s/AF1)");
|
||||
info!("p/callsign - Print a flight strip (p/AF1)");
|
||||
info!("w/callsign - Watch a specific aircraft. Will show flight strip, print it, and add it to your watchlist, where changes will be automatically shown and printed. (i.e. w/AF1)");
|
||||
info!("u/callsign - Unwatch a specific aircraft");
|
||||
info!("d/ - Download new vatlink data");
|
||||
info!(":/sector - Load a sector file");
|
||||
info!("c/lat/long/range/sector - Create and load a sector file");
|
||||
info!("l/ - List aircraft in sector");
|
||||
info!("h/ - Help");
|
||||
},
|
||||
_unknown => {
|
||||
error!("unknown command '{}'", _unknown);
|
||||
info!("vfsm: Commands:");
|
||||
info!("s/callsign - Show a flight strip (s/AF1)");
|
||||
info!("p/callsign - Print a flight strip (p/AF1)");
|
||||
info!("w/callsign - Watch a specific aircraft. Will show flight strip, print it, and add it to your watchlist, where changes will be automatically shown and printed. (i.e. w/AF1)");
|
||||
info!("u/callsign - Unwatch a specific aircraft");
|
||||
info!("d/ - Download new vatlink data");
|
||||
info!(":/sector - Load a sector file");
|
||||
info!("c/lat/long/range/sector - Create and load a sector file");
|
||||
info!("l/ - List aircraft in sector");
|
||||
info!("h/ - Help");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
update_shutdown_tx.send(()).await?;
|
||||
update_task.await?.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_main(datav3_client: Arc<RwLock<DataClientV3>>, mut rx: Receiver<()>) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
loop {
|
||||
select! {
|
||||
_ = sleep(Duration::from_secs(VATLINK_DATAV3_UPDATE_INTERVAL as u64)) => (),
|
||||
x = rx.recv() => {
|
||||
return Ok(()); // if anything happens (channel closed OR we recv a shutdown) we should exit
|
||||
}
|
||||
}
|
||||
{
|
||||
datav3_client.write().await.update().await?;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,166 @@
|
|||
use chrono::{DateTime, Utc};
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
pub const VATLINK_DATAV3_UPDATE_INTERVAL: i32 = 15;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct VatlinkDataV3 {
|
||||
pub general: VatlinkDataV3General,
|
||||
pub pilots: Vec<VatlinkDataV3Pilot>,
|
||||
pub controllers: Vec<VatlinkDataV3Controller>,
|
||||
pub atis: Vec<VatlinkDataV3Atis>,
|
||||
pub servers: Vec<VatlinkDataV3Server>,
|
||||
pub prefiles: Vec<VatlinkDataV3Prefile>,
|
||||
pub facilities: Vec<VatlinkDataV3Facility>,
|
||||
pub ratings: Vec<VatlinkDataV3Rating>,
|
||||
pub pilot_ratings: Vec<VatlinkDataV3PilotRating>,
|
||||
pub military_ratings: Vec<VatlinkDataV3MilitaryRating>
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct VatlinkDataV3General {
|
||||
pub version: i32, // 3
|
||||
pub reload: i32, // Seemingly always 1
|
||||
#[serde(with = "vatlink_data_v3_timestamp_format_01")]
|
||||
pub update: DateTime<Utc>, //YYYYMMDDHHMMSS
|
||||
pub update_timestamp: DateTime<Utc>, //YYYY-MM-DDTHH:MM:SS.6fZ
|
||||
pub connected_clients: i32,
|
||||
pub unique_users: i32
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct VatlinkDataV3Pilot {
|
||||
pub cid: i32,
|
||||
pub name: String,
|
||||
pub callsign: String,
|
||||
pub server: String,
|
||||
pub pilot_rating: i32,
|
||||
pub military_rating: i32,
|
||||
pub latitude: f64,
|
||||
pub longitude: f64,
|
||||
pub altitude: i64,
|
||||
pub groundspeed: i64,
|
||||
pub transponder: String,
|
||||
pub heading: i32,
|
||||
pub qnh_i_hg: f32,
|
||||
pub qnh_mb: i32,
|
||||
pub flight_plan: Option<VatlinkDataV3FlightPlan>,
|
||||
pub logon_time: DateTime<Utc>, // YYYY-MM-DDTHH:MM:SS.3fZ
|
||||
pub last_updated: DateTime<Utc>, // YYYY-MM-DDTHH:MM:SS.3fZ
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct VatlinkDataV3FlightPlan {
|
||||
pub flight_rules: String,
|
||||
pub aircraft: String, // ICAO aircraft code (i.e B789/H-SDE1E2E3FGHIJ2J3J4J5M1RWXY/LB1D1)
|
||||
pub aircraft_faa: String, // FAA aircraft code (i.e H/B789/L)
|
||||
pub aircraft_short: String, // FAA aircraft code middle (i.e B789),
|
||||
pub departure: String, // ICAO airfield code (e.g. KCLT)
|
||||
pub arrival: String, // ICAO airfield code (e.g. KATL)
|
||||
pub alternate: String, // ICAO airfield code (e.g. KJQF)
|
||||
pub cruise_tas: String, // Cruise KTAS, as a string though.
|
||||
pub altitude: String, // Cruise altitude in ft, as a string though.
|
||||
pub deptime: String, // Departure time, HHMM
|
||||
pub enroute_time: String, // Enroute time, HHMM
|
||||
pub fuel_time: String, // Fuel on board, HHMM
|
||||
pub remarks: String, // Flight plan remarks
|
||||
pub route: String, // Flight plan route
|
||||
pub revision_id: i32,
|
||||
pub assigned_transponder: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct VatlinkDataV3Controller {
|
||||
pub cid: i32,
|
||||
pub name: String,
|
||||
pub callsign: String,
|
||||
pub frequency: String, // frequency, ex: 121.900
|
||||
pub facility: i32,
|
||||
pub rating: i32,
|
||||
pub server: String,
|
||||
pub visual_range: i32,
|
||||
pub text_atis: Option<Vec<String>>,
|
||||
pub last_updated: DateTime<Utc>, // YYYY-MM-DDTHH:MM:SS.3fZ
|
||||
pub logon_time: DateTime<Utc>, // YYYY-MM-DDTHH:MM:SS.3fZ
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct VatlinkDataV3Atis {
|
||||
pub cid: i32,
|
||||
pub name: String,
|
||||
pub callsign: String,
|
||||
pub frequency: String, // frequency, ex: 121.900
|
||||
pub facility: i32,
|
||||
pub rating: i32,
|
||||
pub server: String,
|
||||
pub visual_range: i32,
|
||||
pub atis_code: Option<String>,
|
||||
pub text_atis: Option<Vec<String>>,
|
||||
pub last_updated: DateTime<Utc>, // YYYY-MM-DDTHH:MM:SS.3fZ
|
||||
pub logon_time: DateTime<Utc> // YYYY-MM-DDTHH:MM:SS.3fZ
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct VatlinkDataV3Server {
|
||||
pub ident: String,
|
||||
pub hostname_or_ip: String,
|
||||
pub location: String,
|
||||
pub name: String,
|
||||
pub clients_connection_allowed: i32,
|
||||
pub client_connections_allowed: bool,
|
||||
pub is_sweatbox: bool
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct VatlinkDataV3Prefile {
|
||||
pub cid: i32,
|
||||
pub name: String,
|
||||
pub callsign: String,
|
||||
pub flight_plan: VatlinkDataV3FlightPlan,
|
||||
pub last_updated: DateTime<Utc>, // YYYY-MM-DDTHH:MM:SS.3fZ
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct VatlinkDataV3Facility {
|
||||
pub id: i32,
|
||||
pub short: String,
|
||||
pub long: String
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct VatlinkDataV3Rating {
|
||||
pub id: i32,
|
||||
pub short: String,
|
||||
pub long: String
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct VatlinkDataV3PilotRating {
|
||||
pub id: i32,
|
||||
pub short_name: String,
|
||||
pub long_name: String
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct VatlinkDataV3MilitaryRating {
|
||||
pub id: i32,
|
||||
pub short_name: String,
|
||||
pub long_name: String
|
||||
}
|
||||
|
||||
mod vatlink_data_v3_timestamp_format_01 {
|
||||
use chrono::{DateTime, Utc, TimeZone};
|
||||
use serde::{self, Deserialize, Serializer, Deserializer};
|
||||
|
||||
const FORMAT: &'static str = "%Y%m%d%H%M%S";
|
||||
|
||||
pub fn serialize<S>(date: &DateTime<Utc>, serializer: S) -> Result<S::Ok, S::Error> where S: Serializer {
|
||||
let s = format!("{}", date.format(FORMAT));
|
||||
serializer.serialize_str(&s)
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error> where D: Deserializer<'de> {
|
||||
let s = String::deserialize(deserializer)?;
|
||||
Utc.datetime_from_str(&s, FORMAT).map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use log::{debug, trace};
|
||||
use reqwest::{Client, ClientBuilder, header};
|
||||
use tokio::sync::oneshot::channel;
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio::time::sleep;
|
||||
use url::Url;
|
||||
use crate::vatlink::data_v3::VatlinkDataV3;
|
||||
use crate::vatlink::{VATLINK_VERSION, VatlinkServer};
|
||||
use crate::VFSM_VERSION;
|
||||
|
||||
pub struct DataClientV3 {
|
||||
client: Client,
|
||||
last_data: VatlinkDataV3,
|
||||
last_data_update: DateTime<Utc>,
|
||||
next_data_update: DateTime<Utc>,
|
||||
data_v3_url: Url,
|
||||
}
|
||||
impl DataClientV3 {
|
||||
pub async fn new(server: VatlinkServer) -> Result<Self, Box<dyn Error>> {
|
||||
if server.is_empty() {
|
||||
return Err(DataClientV3Error::NoServerProvided { options: server }.into());
|
||||
}
|
||||
|
||||
let server = server[0].clone();
|
||||
|
||||
let mut headers = header::HeaderMap::new();
|
||||
headers.insert("X-Legal-Contact", header::HeaderValue::from_static("email:core(at)e3t(dot)cc"));
|
||||
headers.insert("X-Who-Is-This", header::HeaderValue::from_static("emailLcore(at)e3t(dot)cc"));
|
||||
headers.insert("X-Whoami", header::HeaderValue::from_static("emailLcore(at)e3t(dot)cc"));
|
||||
|
||||
let client = ClientBuilder::new().user_agent(format!("vatlink/{} vfsm/{}", VATLINK_VERSION, VFSM_VERSION)).default_headers(headers).build()?;
|
||||
|
||||
let resp: VatlinkDataV3 = client.get(server.clone()).send().await?.json().await?;
|
||||
|
||||
Ok(Self {
|
||||
client,
|
||||
last_data: resp.clone(),
|
||||
last_data_update: resp.general.update_timestamp,
|
||||
next_data_update: resp.general.update_timestamp + Duration::seconds(15),
|
||||
data_v3_url: server.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn last_saved_data(&self) -> &VatlinkDataV3 {
|
||||
&self.last_data
|
||||
}
|
||||
|
||||
pub async fn current_data(&mut self) -> Result<&VatlinkDataV3, Box<dyn Error + Send + Sync>> {
|
||||
let cur_time = Utc::now();
|
||||
let sleep_time = self.next_data_update - cur_time;
|
||||
if sleep_time < Duration::milliseconds(500) {
|
||||
debug!("fetch current data: current data expires in <500ms, delaying until new data is available");
|
||||
self.update().await?;
|
||||
}
|
||||
Ok(&self.last_data)
|
||||
}
|
||||
|
||||
pub async fn update(&mut self) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
trace!("updating stored data");
|
||||
let cur_time = Utc::now();
|
||||
let sleep_time = (self.next_data_update + Duration::seconds(1)) - cur_time;
|
||||
if (self.next_data_update + Duration::seconds(1)) > cur_time {
|
||||
trace!("delaying {} until next data is available", sleep_time);
|
||||
sleep(sleep_time.to_std()?).await;
|
||||
}
|
||||
|
||||
let resp: VatlinkDataV3 = self.client.get(self.data_v3_url.clone()).send().await?.json().await?;
|
||||
|
||||
self.last_data = resp.clone();
|
||||
self.last_data_update = resp.general.update_timestamp;
|
||||
self.next_data_update = resp.general.update_timestamp + Duration::seconds(15);
|
||||
|
||||
trace!("data updated to {}, next update available at {}", self.last_data_update, self.next_data_update);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
enum DataClientUpdateThreadMsg {
|
||||
Shutdown,
|
||||
UpdateNow
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum DataClientV3Error {
|
||||
NoServerProvided { options: VatlinkServer }
|
||||
}
|
||||
impl Display for DataClientV3Error {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::NoServerProvided { options } => write!(f, "No valid server URL provided (from options {})", options.iter().map(|u| format!("'{}'", u.to_string())).collect::<Vec<String>>().join(", "))
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Error for DataClientV3Error {}
|
|
@ -0,0 +1,34 @@
|
|||
use std::error::Error;
|
||||
use log::debug;
|
||||
use url::Url;
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
pub const VATLINK_VERSION: &str = "0.1.0";
|
||||
|
||||
pub mod data_v3;
|
||||
pub mod data_v3_client;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct VatlinkStatus {
|
||||
pub data: VatlinkStatusData,
|
||||
pub user: VatlinkServer
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct VatlinkStatusData {
|
||||
pub v3: VatlinkServer,
|
||||
pub transceivers: VatlinkServer,
|
||||
pub servers: VatlinkServer,
|
||||
pub servers_sweatbox: VatlinkServer,
|
||||
pub servers_all: VatlinkServer
|
||||
}
|
||||
|
||||
pub type VatlinkServer = Vec<Url>;
|
||||
|
||||
pub const VATLINK_STATUS_URL: &str = "https://status.vatsim.net/status.json";
|
||||
|
||||
pub async fn vatlink_init(url: &str) -> Result<VatlinkStatus, Box<dyn Error>> {
|
||||
debug!("retrieving VATSIMv3 network status from {}", url);
|
||||
let resp: VatlinkStatus = reqwest::get(url).await?.json().await?;
|
||||
Ok(resp)
|
||||
}
|
Loading…
Reference in New Issue