via train tracking
This commit is contained in:
parent
8748d6543b
commit
09f78647d5
29
.vscode/tasks.json
vendored
Normal file
29
.vscode/tasks.json
vendored
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"type": "cargo",
|
||||||
|
"command": "build",
|
||||||
|
"problemMatcher": [
|
||||||
|
"$rustc"
|
||||||
|
],
|
||||||
|
"group": "build",
|
||||||
|
"label": "rust: cargo build"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "cargo",
|
||||||
|
"command": "run",
|
||||||
|
"problemMatcher": [
|
||||||
|
"$rustc"
|
||||||
|
],
|
||||||
|
"label": "rust: cargo run",
|
||||||
|
"args": [
|
||||||
|
"--",
|
||||||
|
"-c",
|
||||||
|
"VA3UJF-1",
|
||||||
|
"-p",
|
||||||
|
"23728"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
17
Cargo.toml
17
Cargo.toml
@ -1,17 +1,22 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "<crate_name>"
|
name = "aprs-trains"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
authors = ["Evan Pratten <ewpratten@gmail.com>"]
|
authors = ["Evan Pratten <ewpratten@gmail.com>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "<description>"
|
description = "Converts train locations to APRS objects"
|
||||||
documentation = "https://docs.rs/<crate_name>"
|
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
homepage = "https://github.com/ewpratten/<repo_name>"
|
homepage = "https://github.com/ewpratten/aprs-trains"
|
||||||
repository = "https://github.com/ewpratten/<repo_name>"
|
repository = "https://github.com/ewpratten/aprs-trains"
|
||||||
license = "GPL-3.0"
|
license = "GPL-3.0"
|
||||||
keywords = []
|
keywords = []
|
||||||
categories = []
|
categories = []
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
reqwest = { version = "^0.11.13", features = ["json"] }
|
||||||
|
serde = { version = "^1.0.126", features = ["derive"] }
|
||||||
|
tokio = { version = "^1.23.0", features = ["macros", "rt-multi-thread"] }
|
||||||
|
clap = { version = "^4.0.29", features = ["derive"] }
|
||||||
|
# chrono = "^0.4.23"
|
||||||
|
aprs-encode = "^0.1.2"
|
||||||
|
arrayvec = "^0.7"
|
||||||
|
12
README.md
12
README.md
@ -1,8 +1,8 @@
|
|||||||
# <repo_name>
|
# aprs-trains
|
||||||
[](https://crates.io/crates/<crate_name>)
|
[](https://crates.io/crates/aprs-trains)
|
||||||
[](https://docs.rs/<crate_name>)
|
[](https://docs.rs/aprs-trains)
|
||||||
[](https://github.com/Ewpratten/<repo_name>/actions/workflows/build.yml)
|
[](https://github.com/Ewpratten/aprs-trains/actions/workflows/build.yml)
|
||||||
[](https://github.com/Ewpratten/<repo_name>/actions/workflows/clippy.yml)
|
[](https://github.com/Ewpratten/aprs-trains/actions/workflows/clippy.yml)
|
||||||
|
|
||||||
|
|
||||||
repo description
|
repo description
|
||||||
@ -12,5 +12,5 @@ repo description
|
|||||||
This crate can be installed via `cargo` with:
|
This crate can be installed via `cargo` with:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cargo install <crate_name>
|
cargo install aprs-trains
|
||||||
```
|
```
|
||||||
|
14
src/cli.rs
Normal file
14
src/cli.rs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
use clap::Parser;
|
||||||
|
|
||||||
|
/// Simple program to greet a person
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[command(author, version, about, long_about = None)]
|
||||||
|
pub struct Args {
|
||||||
|
/// The callsign to send reports from
|
||||||
|
#[clap(short, long)]
|
||||||
|
pub callsign: String,
|
||||||
|
|
||||||
|
/// The password to use for the callsign
|
||||||
|
#[clap(short, long)]
|
||||||
|
pub password: String,
|
||||||
|
}
|
66
src/lib.rs
66
src/lib.rs
@ -1,66 +0,0 @@
|
|||||||
#![doc = include_str!("../README.md")]
|
|
||||||
#![deny(unsafe_code)]
|
|
||||||
#![warn(
|
|
||||||
clippy::all,
|
|
||||||
clippy::await_holding_lock,
|
|
||||||
clippy::char_lit_as_u8,
|
|
||||||
clippy::checked_conversions,
|
|
||||||
clippy::dbg_macro,
|
|
||||||
clippy::debug_assert_with_mut_call,
|
|
||||||
clippy::doc_markdown,
|
|
||||||
clippy::empty_enum,
|
|
||||||
clippy::enum_glob_use,
|
|
||||||
clippy::exit,
|
|
||||||
clippy::expl_impl_clone_on_copy,
|
|
||||||
clippy::explicit_deref_methods,
|
|
||||||
clippy::explicit_into_iter_loop,
|
|
||||||
clippy::fallible_impl_from,
|
|
||||||
clippy::filter_map_next,
|
|
||||||
clippy::float_cmp_const,
|
|
||||||
clippy::fn_params_excessive_bools,
|
|
||||||
clippy::if_let_mutex,
|
|
||||||
clippy::implicit_clone,
|
|
||||||
clippy::imprecise_flops,
|
|
||||||
clippy::inefficient_to_string,
|
|
||||||
clippy::invalid_upcast_comparisons,
|
|
||||||
clippy::large_types_passed_by_value,
|
|
||||||
clippy::let_unit_value,
|
|
||||||
clippy::linkedlist,
|
|
||||||
clippy::lossy_float_literal,
|
|
||||||
clippy::macro_use_imports,
|
|
||||||
clippy::manual_ok_or,
|
|
||||||
clippy::map_err_ignore,
|
|
||||||
clippy::map_flatten,
|
|
||||||
clippy::map_unwrap_or,
|
|
||||||
clippy::match_on_vec_items,
|
|
||||||
clippy::match_same_arms,
|
|
||||||
clippy::match_wildcard_for_single_variants,
|
|
||||||
clippy::mem_forget,
|
|
||||||
clippy::mismatched_target_os,
|
|
||||||
clippy::mut_mut,
|
|
||||||
clippy::mutex_integer,
|
|
||||||
clippy::needless_borrow,
|
|
||||||
clippy::needless_continue,
|
|
||||||
clippy::option_option,
|
|
||||||
clippy::path_buf_push_overwrite,
|
|
||||||
clippy::ptr_as_ptr,
|
|
||||||
clippy::ref_option_ref,
|
|
||||||
clippy::rest_pat_in_fully_bound_structs,
|
|
||||||
clippy::same_functions_in_if_condition,
|
|
||||||
clippy::semicolon_if_nothing_returned,
|
|
||||||
clippy::string_add_assign,
|
|
||||||
clippy::string_add,
|
|
||||||
clippy::string_lit_as_bytes,
|
|
||||||
clippy::string_to_string,
|
|
||||||
clippy::todo,
|
|
||||||
clippy::trait_duplication_in_bounds,
|
|
||||||
clippy::unimplemented,
|
|
||||||
clippy::unnested_or_patterns,
|
|
||||||
clippy::unused_self,
|
|
||||||
clippy::useless_transmute,
|
|
||||||
clippy::verbose_file_reads,
|
|
||||||
clippy::zero_sized_map_values,
|
|
||||||
future_incompatible,
|
|
||||||
nonstandard_style,
|
|
||||||
rust_2018_idioms
|
|
||||||
)]
|
|
58
src/main.rs
Normal file
58
src/main.rs
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use clap::Parser;
|
||||||
|
|
||||||
|
mod cli;
|
||||||
|
mod sources;
|
||||||
|
mod train;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
pub async fn main() {
|
||||||
|
// Get CLI args
|
||||||
|
let args = cli::Args::parse();
|
||||||
|
|
||||||
|
// Get all train locations
|
||||||
|
println!("Fetching train locations");
|
||||||
|
let trains = sources::get_trains().await.unwrap();
|
||||||
|
println!("Found {} trains", trains.len());
|
||||||
|
|
||||||
|
// Set up a request client
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
// For each train, construct, log, and send its position report
|
||||||
|
for train in trains {
|
||||||
|
// Crude packet creation
|
||||||
|
let packet = format!(
|
||||||
|
"{}>APRS,qAS:;{}*111111z{}={}",
|
||||||
|
args.callsign,
|
||||||
|
train.identifier_padded(),
|
||||||
|
train.location_ddm(),
|
||||||
|
train.status
|
||||||
|
);
|
||||||
|
println!("{}", packet);
|
||||||
|
|
||||||
|
// Push the packet to the server
|
||||||
|
let response = client
|
||||||
|
.post("http://rotate.aprs.net:8080/")
|
||||||
|
.header("Accept-Type", "text/plain")
|
||||||
|
.header("Content-Type", "application/octet-stream")
|
||||||
|
.body(format!(
|
||||||
|
"user {} pass {} vers aprs-trains 0.1.0\n{}",
|
||||||
|
args.callsign, args.password, packet
|
||||||
|
))
|
||||||
|
.timeout(Duration::from_secs(3))
|
||||||
|
.send()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if let Ok(response) = response {
|
||||||
|
println!("<- {}", response.status());
|
||||||
|
} else {
|
||||||
|
let err = response.unwrap_err();
|
||||||
|
if err.is_timeout() {
|
||||||
|
println!("<- TIMED OUT");
|
||||||
|
} else {
|
||||||
|
println!("<- ERR: {:?}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
14
src/sources/mod.rs
Normal file
14
src/sources/mod.rs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
use crate::train::TrainInfo;
|
||||||
|
|
||||||
|
mod viarail;
|
||||||
|
|
||||||
|
pub async fn get_trains() -> Result<Vec<TrainInfo>, reqwest::Error> {
|
||||||
|
// Build output
|
||||||
|
let mut output = Vec::new();
|
||||||
|
|
||||||
|
// Collect data
|
||||||
|
output.extend(viarail::get_trains().await?);
|
||||||
|
|
||||||
|
// Return output
|
||||||
|
Ok(output)
|
||||||
|
}
|
49
src/sources/viarail.rs
Normal file
49
src/sources/viarail.rs
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use crate::train::TrainInfo;
|
||||||
|
|
||||||
|
/// Data format used by VIA for their train tracker
|
||||||
|
#[derive(Debug, serde::Deserialize)]
|
||||||
|
struct ViaTrainInfo {
|
||||||
|
lat: Option<f32>,
|
||||||
|
lng: Option<f32>,
|
||||||
|
speed: Option<f64>,
|
||||||
|
direction: Option<f32>,
|
||||||
|
from: Option<String>,
|
||||||
|
to: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all trains from VIA Rail
|
||||||
|
pub async fn get_trains() -> Result<Vec<TrainInfo>, reqwest::Error> {
|
||||||
|
// Make a request to the tsimobile API
|
||||||
|
let response = reqwest::get("https://tsimobile.viarail.ca/data/allData.json").await?;
|
||||||
|
|
||||||
|
// Convert to something workable
|
||||||
|
let parsed_data: HashMap<String, ViaTrainInfo> = response.json().await?;
|
||||||
|
|
||||||
|
// Convert to TrainInfo format
|
||||||
|
let mut output = Vec::new();
|
||||||
|
|
||||||
|
for (id, train) in parsed_data {
|
||||||
|
if let (Some(lat), Some(lng), Some(speed), Some(from), Some(to)) =
|
||||||
|
(train.lat, train.lng, train.speed, train.from, train.to)
|
||||||
|
{
|
||||||
|
// Skip bracketed train names for my sanity
|
||||||
|
if id.contains("(") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
output.push(TrainInfo {
|
||||||
|
identifier: format!("VIA-{}", id),
|
||||||
|
speed,
|
||||||
|
latitude: lat,
|
||||||
|
longitude: lng,
|
||||||
|
course: train.direction,
|
||||||
|
status: format!("{} to {}", from, to),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return output
|
||||||
|
Ok(output)
|
||||||
|
}
|
57
src/train.rs
Normal file
57
src/train.rs
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
use aprs_encode::{ddm::{DdmLongitude, DegreeMinutes, DdmLatitude, CardinalDirection}, stack_str::PackArrayString};
|
||||||
|
use arrayvec::ArrayString;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct TrainInfo {
|
||||||
|
/// Identifier for the train
|
||||||
|
pub identifier: String,
|
||||||
|
/// Current speed in km/h
|
||||||
|
pub speed: f64,
|
||||||
|
/// Current latitude
|
||||||
|
pub latitude: f32,
|
||||||
|
/// Current longitude
|
||||||
|
pub longitude: f32,
|
||||||
|
/// Current course of the train
|
||||||
|
pub course: Option<f32>,
|
||||||
|
/// Status message
|
||||||
|
pub status: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TrainInfo {
|
||||||
|
/// Get the location in DDMM.hhN/DDDMM.hhW format
|
||||||
|
pub fn location_ddm(&self) -> String {
|
||||||
|
let mut ddm_longitude = ArrayString::<128>::new();
|
||||||
|
DdmLongitude {
|
||||||
|
ddm: DegreeMinutes::from(self.longitude.abs()),
|
||||||
|
direction: if self.longitude >= 0.0 {
|
||||||
|
CardinalDirection::East
|
||||||
|
} else {
|
||||||
|
CardinalDirection::West
|
||||||
|
},
|
||||||
|
}
|
||||||
|
.pack_into(&mut ddm_longitude)
|
||||||
|
.unwrap();
|
||||||
|
let mut ddm_latitude = ArrayString::<128>::new();
|
||||||
|
DdmLatitude {
|
||||||
|
ddm: DegreeMinutes::from(self.latitude.abs()),
|
||||||
|
direction: if self.latitude >= 0.0 {
|
||||||
|
CardinalDirection::North
|
||||||
|
} else {
|
||||||
|
CardinalDirection::South
|
||||||
|
},
|
||||||
|
}
|
||||||
|
.pack_into(&mut ddm_latitude)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
format!("{}/{}", ddm_latitude, ddm_longitude)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the identifier padded with spaces to 9 characters
|
||||||
|
pub fn identifier_padded(&self) -> String {
|
||||||
|
let mut output = self.identifier.clone();
|
||||||
|
while output.len() < 9 {
|
||||||
|
output.push(' ');
|
||||||
|
}
|
||||||
|
output
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user