From d353b1b055970448cb69ee4ec95c7c9f2b97343b Mon Sep 17 00:00:00 2001 From: mattbk Date: Fri, 24 Oct 2025 23:16:19 -0500 Subject: [PATCH] Parse APRS location from packets that have that information. --- Cargo.toml | 2 ++ src/main.rs | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 4516ff0..bdc2fac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,8 @@ authors = ["Chris, N6CTA "] anyhow = "1.0" chrono = "0.4" clap = { version = "4", features = ["derive"] } +rand = "0.9.2" +regex = "1.12.2" reqwest = { version = "0.12.24", features = ["json", "blocking"] } serde_json = "1.0.145" socket2 = "0.5" diff --git a/src/main.rs b/src/main.rs index a4e3fa8..c9ecdd9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,6 +12,9 @@ use std::net::UdpSocket; use std::net::Ipv4Addr; use serde_json::json; use reqwest; +use regex::Regex; +use rand::Rng; + /// Validate that the provided port string can be parsed into a u16 and is nonzero. fn validate_port(port: &str) -> Result { @@ -396,6 +399,9 @@ fn handle_frame(frame: &AgwFrame, cli: &Cli, buffers: &mut BufferManager) { } else { basic_destination.clone() }; + // Extract location from APRS format + let (lat, lon) = aprs_loc(&text); + //println!("{}, {}", lat, lon); // Ignore frames where the basic destination contains "NODES" (case‑insensitive). if basic_destination.to_uppercase().contains("NODES") { @@ -437,6 +443,8 @@ fn handle_frame(frame: &AgwFrame, cli: &Cli, buffers: &mut BufferManager) { "de_call": &spotter, "freq": &freq, "comment": &text, + "dx_latitude": &lat, + "dx_longitude": &lon, "mode": "PKT", "mode_type": "DATA", "mode_source": "SPOT", @@ -446,6 +454,7 @@ fn handle_frame(frame: &AgwFrame, cli: &Cli, buffers: &mut BufferManager) { let res = client.post(spothole_url) .json(&packet) .send(); + println!("sent = {}", packet); println!("res = {res:?}"); @@ -461,7 +470,9 @@ fn handle_frame(frame: &AgwFrame, cli: &Cli, buffers: &mut BufferManager) { "source": &source, "spotter": &spotter, "summary": &summary, - "text": &text, + "text": &text, + "dx_latitude": &lat, + "dx_longitude": &lon, "freq": &freq, "timestamp": SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(), "type": "data" @@ -569,4 +580,49 @@ fn main() -> Result<()> { println!("Disconnected. Reconnecting in {} ms...", reconnect_delay_ms); sleep(Duration::from_millis(reconnect_delay_ms)); } +} + +fn aprs_loc(packet: &str) -> (f64, f64) { + + // Capture different pieces of the location stringf + let re_loc = Regex::new(r"(?P\d{2})(?P[\d ]{2}\.[\d ]{2})(?P[nsNS])/(?P\d{3})(?P[\d ]{2}\.[\d ]{2})(?P[ewEW])").unwrap(); + + // Only proceed if there were captures + match re_loc.captures(&packet) { + Some(_caps) => { + + // Break captures into named values + let loc = re_loc.captures(&packet).unwrap(); + + // Initiate randomness for ambiguity.. + let mut rng = rand::rng(); + + // Convert to decimal degrees. If ambiguity spaces are included (see APRS spec), replace them with random digits. + let mut lat_dec: f64 = &loc["latd"].trim().parse().expect("Expects a number!") + (&loc["latm"].replace(" ", &rng.random_range(0..9).to_string()).trim().parse().expect("Expects a number!") / 60.0); + // If south, make negative + if &loc["ns"] == "S" { + lat_dec = lat_dec * -1.0 + } + + // TODO if there are spaces in loc["latm"], the same spaces need to be in loc["lonm"] for proper ambiguity according to APRS spec + + let mut lon_dec: f64 = &loc["lond"].trim().parse().expect("Expects a number!") + (&loc["lonm"].replace(" ", &rng.random_range(0..9).to_string()).trim().parse().expect("Expects a number!") / 60.0); + // If west, make negative + if &loc["ew"] == "W" { + lon_dec = lon_dec * -1.0 + } + + // String to paste into map for testing + //println!("{}, {}", lat_dec, lon_dec); + + // Return + (lat_dec, lon_dec) + + } + // Otherwise if there were no captures, return bad data + None => { + // The regex did not match. Deal with it here! + (-9999.0_f64, -9999.0_f64) + } +} } \ No newline at end of file