forked from n6cta/mwtchahrd
Compare commits
3 Commits
f452e72ef9
...
aprs_loc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
863657f61f | ||
|
|
f1d0f6dc31 | ||
|
|
d353b1b055 |
@@ -8,6 +8,8 @@ authors = ["Chris, N6CTA <mail@n6cta.com>"]
|
|||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
|
rand = "0.9.2"
|
||||||
|
regex = "1.12.2"
|
||||||
reqwest = { version = "0.12.24", features = ["json", "blocking"] }
|
reqwest = { version = "0.12.24", features = ["json", "blocking"] }
|
||||||
serde_json = "1.0.145"
|
serde_json = "1.0.145"
|
||||||
socket2 = "0.5"
|
socket2 = "0.5"
|
||||||
|
|||||||
132
src/main.rs
132
src/main.rs
@@ -12,6 +12,9 @@ use std::net::UdpSocket;
|
|||||||
use std::net::Ipv4Addr;
|
use std::net::Ipv4Addr;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use reqwest;
|
use reqwest;
|
||||||
|
use regex::Regex;
|
||||||
|
use rand::Rng;
|
||||||
|
|
||||||
|
|
||||||
/// Validate that the provided port string can be parsed into a u16 and is nonzero.
|
/// Validate that the provided port string can be parsed into a u16 and is nonzero.
|
||||||
fn validate_port(port: &str) -> Result<u16, String> {
|
fn validate_port(port: &str) -> Result<u16, String> {
|
||||||
@@ -62,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)]
|
||||||
@@ -76,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.
|
||||||
@@ -367,7 +378,7 @@ impl BufferManager {
|
|||||||
/// - Filters out frames destined for "NODES" and frames with an XID payload.
|
/// - Filters out frames destined for "NODES" and frames with an XID payload.
|
||||||
/// - Optionally filters to only UI frames if requested.
|
/// - Optionally filters to only UI frames if requested.
|
||||||
/// - Buffers multi-line frames and prints a formatted session line.
|
/// - Buffers multi-line frames and prints a formatted session line.
|
||||||
fn handle_frame(frame: &AgwFrame, cli: &Cli, buffers: &mut BufferManager) {
|
fn handle_frame(frame: &AgwFrame, cli: &Cli, buffers: &mut BufferManager, loc_store: &mut HashMap<String, Vec<String>>) {
|
||||||
let hdr = &frame.header;
|
let hdr = &frame.header;
|
||||||
// Process only frames on the specified channel.
|
// Process only frames on the specified channel.
|
||||||
if hdr.port != cli.channel as i32 {
|
if hdr.port != cli.channel as i32 {
|
||||||
@@ -376,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 {
|
||||||
@@ -397,6 +409,29 @@ fn handle_frame(frame: &AgwFrame, cli: &Cli, buffers: &mut BufferManager) {
|
|||||||
basic_destination.clone()
|
basic_destination.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Extract location from APRS format
|
||||||
|
let (lat, lon) = aprs_loc(&text);
|
||||||
|
|
||||||
|
// Store the location
|
||||||
|
// Only update location store if there is a real location
|
||||||
|
if lat > -9999.0_f64 && lon > -9999.0_f64 {
|
||||||
|
let loc = vec![lat.to_string(), lon.to_string()];
|
||||||
|
loc_store.insert(source.clone(), loc);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look up a stored location
|
||||||
|
// If it doesn't exist, set to empty
|
||||||
|
let stored_loc = match loc_store.get(&source) {
|
||||||
|
Some(loc_value) => loc_value,
|
||||||
|
None => &vec!["".to_string(), "".to_string()],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only send good locations on
|
||||||
|
let json_lat = stored_loc[0].clone() ;
|
||||||
|
let json_lon = stored_loc[1].clone() ;
|
||||||
|
//let json_lat = if lat > -9999.0_f64 && lon > -9999.0_f64 { lat.to_string() } else { old_loc[0].clone() };
|
||||||
|
//let json_lon = if lat > -9999.0_f64 && lon > -9999.0_f64 { lon.to_string() } else { old_loc[1].clone() };
|
||||||
|
|
||||||
// Ignore frames where the basic destination contains "NODES" (case‑insensitive).
|
// Ignore frames where the basic destination contains "NODES" (case‑insensitive).
|
||||||
if basic_destination.to_uppercase().contains("NODES") {
|
if basic_destination.to_uppercase().contains("NODES") {
|
||||||
return;
|
return;
|
||||||
@@ -422,21 +457,30 @@ fn handle_frame(frame: &AgwFrame, cli: &Cli, buffers: &mut BufferManager) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// In non-debug mode, print the session line and any additional lines.
|
||||||
|
if !cli.debug {
|
||||||
|
print_session_line(×tamp, &source, &final_destination, &summary);
|
||||||
|
if lines.len() > 1 {
|
||||||
|
for line in &lines[1..] {
|
||||||
|
println!("{}", line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Stored location: {} {}", stored_loc[0].clone(), stored_loc[1].clone());
|
||||||
|
|
||||||
// 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"
|
|
||||||
// --data '{"dx_call":"M0TRT","time":1760019539, "freq":14200000,
|
|
||||||
// "comment":"Test spot please ignore", "de_call":"M0TRT"}' https://spothole.app/api/v1/spot
|
|
||||||
|
|
||||||
println!("spothole_url: {}", spothole_url);
|
|
||||||
|
|
||||||
|
|
||||||
// POST JSON
|
// POST JSON
|
||||||
let packet = json!({
|
let packet = json!({
|
||||||
"dx_call": &source,
|
"dx_call": &source,
|
||||||
"de_call": &spotter,
|
"de_call": &my_call,
|
||||||
|
"de_latitude": &my_lat,
|
||||||
|
"de_longitude": &my_lon,
|
||||||
"freq": &freq,
|
"freq": &freq,
|
||||||
"comment": &text,
|
"comment": &text,
|
||||||
|
"dx_latitude": &json_lat,
|
||||||
|
"dx_longitude": &json_lon,
|
||||||
"mode": "PKT",
|
"mode": "PKT",
|
||||||
"mode_type": "DATA",
|
"mode_type": "DATA",
|
||||||
"mode_source": "SPOT",
|
"mode_source": "SPOT",
|
||||||
@@ -446,9 +490,8 @@ fn handle_frame(frame: &AgwFrame, cli: &Cli, buffers: &mut BufferManager) {
|
|||||||
let res = client.post(spothole_url)
|
let res = client.post(spothole_url)
|
||||||
.json(&packet)
|
.json(&packet)
|
||||||
.send();
|
.send();
|
||||||
|
println!("sent to {} = {}", spothole_url, packet);
|
||||||
println!("res = {res:?}");
|
println!("res = {res:?}");
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send UDP
|
// Send UDP
|
||||||
@@ -459,23 +502,62 @@ 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": &json_lat,
|
||||||
|
"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"
|
||||||
});
|
});
|
||||||
let _ = socket.expect("REASON").send_to(packet.to_string().as_bytes(), uaddr);
|
let _ = socket.expect("REASON").send_to(packet.to_string().as_bytes(), uaddr);
|
||||||
}
|
}
|
||||||
|
|
||||||
// In non-debug mode, print the session line and any additional lines.
|
|
||||||
if !cli.debug {
|
|
||||||
print_session_line(×tamp, &source, &final_destination, &summary);
|
|
||||||
if lines.len() > 1 {
|
|
||||||
for line in &lines[1..] {
|
|
||||||
println!("{}", line);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -491,6 +573,10 @@ fn main() -> Result<()> {
|
|||||||
let uaddr = format!("{}:{}", cli.uip, cli.uport);
|
let uaddr = format!("{}:{}", cli.uip, cli.uport);
|
||||||
let reconnect_delay_ms = 5000;
|
let reconnect_delay_ms = 5000;
|
||||||
|
|
||||||
|
// Set up the location store
|
||||||
|
//let mut loc_store = HashMap:: new();
|
||||||
|
let mut loc_store: HashMap<String, Vec<String>> = HashMap::new();
|
||||||
|
|
||||||
if cli.uport != 55555 {
|
if cli.uport != 55555 {
|
||||||
// Bind the client socket to any available address and port
|
// Bind the client socket to any available address and port
|
||||||
let socket = UdpSocket::bind("0.0.0.0:0")?;
|
let socket = UdpSocket::bind("0.0.0.0:0")?;
|
||||||
@@ -538,7 +624,7 @@ fn main() -> Result<()> {
|
|||||||
while buffer.len() >= 36 {
|
while buffer.len() >= 36 {
|
||||||
match parse_frame(&buffer, cli.debug) {
|
match parse_frame(&buffer, cli.debug) {
|
||||||
Ok((consumed, frame)) => {
|
Ok((consumed, frame)) => {
|
||||||
handle_frame(&frame, &cli, &mut buffers);
|
handle_frame(&frame, &cli, &mut buffers, &mut loc_store);
|
||||||
// Remove the processed frame from the buffer.
|
// Remove the processed frame from the buffer.
|
||||||
buffer.drain(0..consumed);
|
buffer.drain(0..consumed);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user