initial commit

This commit is contained in:
core 2023-06-14 23:02:44 -04:00
commit 44ef27b2b6
Signed by: core
GPG Key ID: FDBF740DADDCEECF
12 changed files with 1997 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

8
.idea/.gitignore vendored Normal file
View File

@ -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

8
.idea/modules.xml Normal file
View File

@ -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>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

11
.idea/vfsm.iml Normal file
View File

@ -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>

1369
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

17
Cargo.toml Normal file
View File

@ -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"

119
src/fps.rs Normal file
View File

@ -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);
}

160
src/main.rs Normal file
View File

@ -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?;
}
}
}

166
src/vatlink/data_v3.rs Normal file
View File

@ -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)
}
}

View File

@ -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 {}

34
src/vatlink/mod.rs Normal file
View File

@ -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)
}