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]
|
||||
name = "<crate_name>"
|
||||
name = "aprs-trains"
|
||||
version = "0.1.0"
|
||||
authors = ["Evan Pratten <ewpratten@gmail.com>"]
|
||||
edition = "2021"
|
||||
description = "<description>"
|
||||
documentation = "https://docs.rs/<crate_name>"
|
||||
description = "Converts train locations to APRS objects"
|
||||
readme = "README.md"
|
||||
homepage = "https://github.com/ewpratten/<repo_name>"
|
||||
repository = "https://github.com/ewpratten/<repo_name>"
|
||||
homepage = "https://github.com/ewpratten/aprs-trains"
|
||||
repository = "https://github.com/ewpratten/aprs-trains"
|
||||
license = "GPL-3.0"
|
||||
keywords = []
|
||||
categories = []
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[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>
|
||||
[](https://crates.io/crates/<crate_name>)
|
||||
[](https://docs.rs/<crate_name>)
|
||||
[](https://github.com/Ewpratten/<repo_name>/actions/workflows/build.yml)
|
||||
[](https://github.com/Ewpratten/<repo_name>/actions/workflows/clippy.yml)
|
||||
# aprs-trains
|
||||
[](https://crates.io/crates/aprs-trains)
|
||||
[](https://docs.rs/aprs-trains)
|
||||
[](https://github.com/Ewpratten/aprs-trains/actions/workflows/build.yml)
|
||||
[](https://github.com/Ewpratten/aprs-trains/actions/workflows/clippy.yml)
|
||||
|
||||
|
||||
repo description
|
||||
@ -12,5 +12,5 @@ repo description
|
||||
This crate can be installed via `cargo` with:
|
||||
|
||||
```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