Clean up APRS location logic and add spotter location.

This commit is contained in:
mattbk
2025-10-25 22:46:43 -05:00
parent d353b1b055
commit f1d0f6dc31

View File

@@ -65,7 +65,7 @@ struct Cli {
/// Spotter callsign (e.g. W1CDN) /// Spotter callsign (e.g. W1CDN)
#[arg(short = 's', long)] #[arg(short = 's', long)]
spotter: Option<String>, my_call: Option<String>,
/// Spot UI frames to a Spothole server /// Spot UI frames to a Spothole server
#[arg(short = 'o', long, default_value_t = false)] #[arg(short = 'o', long, default_value_t = false)]
@@ -79,6 +79,14 @@ struct Cli {
#[arg(short = 'f', long, default_value_t = 14105000)] #[arg(short = 'f', long, default_value_t = 14105000)]
freq: u32, freq: u32,
/// Spotter latitude DD.dddd
#[arg(short = 'y', long, default_value_t = -9999.0_f64)]
my_lat: f64,
/// Spotter longitude DD.dddd; to send negaitve (W or S), use = e.g. --my-lon=-97.1
#[arg(short = 'x', long, default_value_t = -9999.0_f64)]
my_lon: f64,
} }
/// Convert a byte slice into a hex-dump string for debugging purposes. /// Convert a byte slice into a hex-dump string for debugging purposes.
@@ -379,9 +387,10 @@ fn handle_frame(frame: &AgwFrame, cli: &Cli, buffers: &mut BufferManager) {
let source = hdr.callfrom_str(); let source = hdr.callfrom_str();
let basic_destination = hdr.callto_str(); let basic_destination = hdr.callto_str();
let timestamp = Local::now().format("%H:%M:%S").to_string(); let timestamp = Local::now().format("%H:%M:%S").to_string();
let spotter = &cli.spotter; let my_call = &cli.my_call;
let freq = &cli.freq; let freq = &cli.freq;
//let spothole_alt = &cli.spothole_alt; let my_lat = &cli.my_lat;
let my_lon = &cli.my_lon;
// If user provides an alternate Spothole URL, use that one // If user provides an alternate Spothole URL, use that one
let spothole_url = match &cli.spothole_alt { let spothole_url = match &cli.spothole_alt {
@@ -399,9 +408,12 @@ fn handle_frame(frame: &AgwFrame, cli: &Cli, buffers: &mut BufferManager) {
} else { } else {
basic_destination.clone() basic_destination.clone()
}; };
// Extract location from APRS format // Extract location from APRS format
let (lat, lon) = aprs_loc(&text); let (lat, lon) = aprs_loc(&text);
//println!("{}, {}", lat, lon); // Only send good locations on
let json_lat = if lat > -9999.0_f64 && lon > -9999.0_f64 { lat.to_string() } else { "".to_string() }; // send nothing
let json_lon = if lat > -9999.0_f64 && lon > -9999.0_f64 { lon.to_string() } else { "".to_string() }; // send nothing
// Ignore frames where the basic destination contains "NODES" (caseinsensitive). // Ignore frames where the basic destination contains "NODES" (caseinsensitive).
if basic_destination.to_uppercase().contains("NODES") { if basic_destination.to_uppercase().contains("NODES") {
@@ -430,34 +442,27 @@ fn handle_frame(frame: &AgwFrame, cli: &Cli, buffers: &mut BufferManager) {
// If Spothole is enabled // If Spothole is enabled
if summary == "UI" && cli.spothole { if summary == "UI" && cli.spothole {
// curl --request POST --header "Content-Type: application/json" // POST JSON
// --data '{"dx_call":"M0TRT","time":1760019539, "freq":14200000, let packet = json!({
// "comment":"Test spot please ignore", "de_call":"M0TRT"}' https://spothole.app/api/v1/spot "dx_call": &source,
"de_call": &my_call,
println!("spothole_url: {}", spothole_url); "de_latitude": &my_lat,
"de_longitude": &my_lon,
"freq": &freq,
// POST JSON "comment": &text,
let packet = json!({ "dx_latitude": &json_lat,
"dx_call": &source, "dx_longitude": &json_lon,
"de_call": &spotter, "mode": "PKT",
"freq": &freq, "mode_type": "DATA",
"comment": &text, "mode_source": "SPOT",
"dx_latitude": &lat, "time": SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(),
"dx_longitude": &lon, });
"mode": "PKT", let client = reqwest::blocking::Client::new();
"mode_type": "DATA", let res = client.post(spothole_url)
"mode_source": "SPOT",
"time": SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(),
});
let client = reqwest::blocking::Client::new();
let res = client.post(spothole_url)
.json(&packet) .json(&packet)
.send(); .send();
println!("sent = {}", packet); println!("sent to {} = {}", spothole_url, packet);
println!("res = {res:?}"); println!("res = {res:?}");
} }
// Send UDP // Send UDP
@@ -468,11 +473,13 @@ fn handle_frame(frame: &AgwFrame, cli: &Cli, buffers: &mut BufferManager) {
let packet = json!({ let packet = json!({
"final_destination": &final_destination, "final_destination": &final_destination,
"source": &source, "source": &source,
"spotter": &spotter, "spotter": &my_call,
"spotter_latitude": &my_lat,
"spotter_longitude": &my_lon,
"summary": &summary, "summary": &summary,
"text": &text, "text": &text,
"dx_latitude": &lat, "dx_latitude": &json_lat,
"dx_longitude": &lon, "dx_longitude": &json_lon,
"freq": &freq, "freq": &freq,
"timestamp": SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(), "timestamp": SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(),
"type": "data" "type": "data"
@@ -491,6 +498,51 @@ fn handle_frame(frame: &AgwFrame, cli: &Cli, buffers: &mut BufferManager) {
} }
} }
fn aprs_loc(packet: &str) -> (f64, f64) {
// Capture different pieces of the location stringf
let re_loc = Regex::new(r"(?P<latd>\d{2})(?P<latm>[\d ]{2}\.[\d ]{2})(?P<ns>[nsNS])/(?P<lond>\d{3})(?P<lonm>[\d ]{2}\.[\d ]{2})(?P<ew>[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)
}
}
}
/// Main entry point: /// Main entry point:
/// - Parses CLI options. /// - Parses CLI options.
/// - Connects to the AGWPE server and sends the monitor command. /// - Connects to the AGWPE server and sends the monitor command.
@@ -580,49 +632,4 @@ fn main() -> Result<()> {
println!("Disconnected. Reconnecting in {} ms...", reconnect_delay_ms); println!("Disconnected. Reconnecting in {} ms...", reconnect_delay_ms);
sleep(Duration::from_millis(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<latd>\d{2})(?P<latm>[\d ]{2}\.[\d ]{2})(?P<ns>[nsNS])/(?P<lond>\d{3})(?P<lonm>[\d ]{2}\.[\d ]{2})(?P<ew>[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)
}
}
} }