From 8448d539375e20522a711b26966d9de231b9c336 Mon Sep 17 00:00:00 2001
From: Stephen D <webmaster@scd31.com>
Date: Sat, 14 Oct 2023 13:15:30 -0300
Subject: [PATCH 1/2] two-way FELINET <-> APRS

---
 Cargo.lock  | 299 ++++++++++++++++++++++++++++++++++++++++++++--------
 Cargo.toml  |   2 +-
 src/aprs.rs |  66 +++++++++---
 src/main.rs | 189 +++++++++++++++++++++++++++++++--
 4 files changed, 492 insertions(+), 64 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index d6859d7..23c2c78 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -19,9 +19,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
 
 [[package]]
 name = "aho-corasick"
-version = "1.1.1"
+version = "1.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ea5d730647d4fadd988536d06fecce94b7b4f2a7efdae548f1cf4b63205518ab"
+checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0"
 dependencies = [
  "memchr",
 ]
@@ -176,6 +176,12 @@ dependencies = [
  "wyz",
 ]
 
+[[package]]
+name = "bumpalo"
+version = "3.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"
+
 [[package]]
 name = "bytes"
 version = "1.5.0"
@@ -197,6 +203,22 @@ version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 
+[[package]]
+name = "core-foundation"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
+
 [[package]]
 name = "crc"
 version = "3.0.1"
@@ -247,25 +269,14 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
 
 [[package]]
 name = "errno"
-version = "0.3.4"
+version = "0.3.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "add4f07d43996f76ef320709726a556a9d4f965d9410d8d0271132d2f8293480"
+checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860"
 dependencies = [
- "errno-dragonfly",
  "libc",
  "windows-sys",
 ]
 
-[[package]]
-name = "errno-dragonfly"
-version = "0.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
-dependencies = [
- "cc",
- "libc",
-]
-
 [[package]]
 name = "fastrand"
 version = "2.0.1"
@@ -614,6 +625,15 @@ version = "1.0.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
 
+[[package]]
+name = "js-sys"
+version = "0.3.64"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a"
+dependencies = [
+ "wasm-bindgen",
+]
+
 [[package]]
 name = "labrador-ldpc"
 version = "1.1.1"
@@ -634,9 +654,9 @@ checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b"
 
 [[package]]
 name = "linux-raw-sys"
-version = "0.4.9"
+version = "0.4.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "45786cec4d5e54a224b15cb9f06751883103a27c19c93eda09b0b4f5f08fefac"
+checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f"
 
 [[package]]
 name = "lock_api"
@@ -723,6 +743,12 @@ version = "1.18.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
 
+[[package]]
+name = "openssl-probe"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
+
 [[package]]
 name = "parking_lot"
 version = "0.12.1"
@@ -818,9 +844,9 @@ dependencies = [
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.68"
+version = "1.0.69"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b1106fec09662ec6dd98ccac0f81cef56984d0b49f75c92d8cbad76e20c005c"
+checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da"
 dependencies = [
  "unicode-ident",
 ]
@@ -958,9 +984,9 @@ dependencies = [
 
 [[package]]
 name = "regex"
-version = "1.9.6"
+version = "1.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ebee201405406dbf528b8b672104ae6d6d63e6d118cb10e4d51abbc7b58044ff"
+checksum = "d119d7c7ca818f8a53c300863d4f87566aac09943aef5b355bb83969dae75d87"
 dependencies = [
  "aho-corasick",
  "memchr",
@@ -970,9 +996,9 @@ dependencies = [
 
 [[package]]
 name = "regex-automata"
-version = "0.3.9"
+version = "0.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "59b23e92ee4318893fa3fe3e6fb365258efbfe6ac6ab30f090cdcbb7aa37efa9"
+checksum = "465c6fc0621e4abc4187a2bda0937bfd4f722c2730b29562e19689ea796c9a4b"
 dependencies = [
  "aho-corasick",
  "memchr",
@@ -981,9 +1007,24 @@ dependencies = [
 
 [[package]]
 name = "regex-syntax"
-version = "0.7.5"
+version = "0.8.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
+checksum = "56d84fdd47036b038fc80dd333d10b6aab10d5d31f4a366e20014def75328d33"
+
+[[package]]
+name = "ring"
+version = "0.16.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc"
+dependencies = [
+ "cc",
+ "libc",
+ "once_cell",
+ "spin",
+ "untrusted",
+ "web-sys",
+ "winapi",
+]
 
 [[package]]
 name = "rustc-demangle"
@@ -993,9 +1034,9 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
 
 [[package]]
 name = "rustix"
-version = "0.38.17"
+version = "0.38.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f25469e9ae0f3d0047ca8b93fc56843f38e6774f0914a107ff8b41be8be8e0b7"
+checksum = "745ecfa778e66b2b63c88a61cb36e0eea109e803b0b86bf9879fbc77c70e86ed"
 dependencies = [
  "bitflags 2.4.0",
  "errno",
@@ -1004,32 +1045,117 @@ dependencies = [
  "windows-sys",
 ]
 
+[[package]]
+name = "rustls"
+version = "0.21.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8"
+dependencies = [
+ "log",
+ "ring",
+ "rustls-webpki",
+ "sct",
+]
+
+[[package]]
+name = "rustls-native-certs"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00"
+dependencies = [
+ "openssl-probe",
+ "rustls-pemfile",
+ "schannel",
+ "security-framework",
+]
+
+[[package]]
+name = "rustls-pemfile"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2"
+dependencies = [
+ "base64",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.101.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c7d5dece342910d9ba34d259310cae3e0154b873b35408b787b59bce53d34fe"
+dependencies = [
+ "ring",
+ "untrusted",
+]
+
 [[package]]
 name = "rustversion"
 version = "1.0.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
 
+[[package]]
+name = "schannel"
+version = "0.1.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88"
+dependencies = [
+ "windows-sys",
+]
+
 [[package]]
 name = "scopeguard"
 version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
 
+[[package]]
+name = "sct"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4"
+dependencies = [
+ "ring",
+ "untrusted",
+]
+
+[[package]]
+name = "security-framework"
+version = "2.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de"
+dependencies = [
+ "bitflags 1.3.2",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
 [[package]]
 name = "serde"
-version = "1.0.188"
+version = "1.0.189"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e"
+checksum = "8e422a44e74ad4001bdc8eede9a4570ab52f71190e9c076d14369f38b9200537"
 dependencies = [
  "serde_derive",
 ]
 
 [[package]]
 name = "serde_derive"
-version = "1.0.188"
+version = "1.0.189"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2"
+checksum = "1e48d1f918009ce3145511378cf68d613e3b3d9137d67272562080d68a2b32d5"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1127,6 +1253,12 @@ dependencies = [
  "windows-sys",
 ]
 
+[[package]]
+name = "spin"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
+
 [[package]]
 name = "syn"
 version = "1.0.109"
@@ -1196,9 +1328,9 @@ dependencies = [
 
 [[package]]
 name = "tokio"
-version = "1.32.0"
+version = "1.33.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9"
+checksum = "4f38200e3ef7995e5ef13baec2f432a6da0aa9ac495b2c0e8f3b7eec2c92d653"
 dependencies = [
  "backtrace",
  "bytes",
@@ -1234,6 +1366,16 @@ dependencies = [
  "syn 2.0.38",
 ]
 
+[[package]]
+name = "tokio-rustls"
+version = "0.24.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081"
+dependencies = [
+ "rustls",
+ "tokio",
+]
+
 [[package]]
 name = "tokio-stream"
 version = "0.1.14"
@@ -1312,7 +1454,11 @@ dependencies = [
  "percent-encoding",
  "pin-project",
  "prost 0.12.1",
+ "rustls",
+ "rustls-native-certs",
+ "rustls-pemfile",
  "tokio",
+ "tokio-rustls",
  "tokio-stream",
  "tower",
  "tower-layer",
@@ -1367,11 +1513,10 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
 
 [[package]]
 name = "tracing"
-version = "0.1.37"
+version = "0.1.39"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8"
+checksum = "ee2ef2af84856a50c1d430afce2fdded0a4ec7eda868db86409b4543df0797f9"
 dependencies = [
- "cfg-if",
  "pin-project-lite",
  "tracing-attributes",
  "tracing-core",
@@ -1379,9 +1524,9 @@ dependencies = [
 
 [[package]]
 name = "tracing-attributes"
-version = "0.1.26"
+version = "0.1.27"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
+checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1390,9 +1535,9 @@ dependencies = [
 
 [[package]]
 name = "tracing-core"
-version = "0.1.31"
+version = "0.1.32"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a"
+checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
 dependencies = [
  "once_cell",
 ]
@@ -1409,6 +1554,12 @@ version = "1.0.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
 
+[[package]]
+name = "untrusted"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
+
 [[package]]
 name = "want"
 version = "0.3.1"
@@ -1424,6 +1575,70 @@ version = "0.11.0+wasi-snapshot-preview1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
 
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.87"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.87"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd"
+dependencies = [
+ "bumpalo",
+ "log",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.38",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.87"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.87"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.38",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.87"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1"
+
+[[package]]
+name = "web-sys"
+version = "0.3.64"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
 [[package]]
 name = "which"
 version = "4.4.2"
@@ -1526,9 +1741,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
 
 [[package]]
 name = "winnow"
-version = "0.5.16"
+version = "0.5.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "037711d82167854aff2018dfd193aa0fef5370f456732f0d5a0c59b0f1b4b907"
+checksum = "a3b801d0e0a6726477cc207f60162da452f3a95adb368399bef20a946e06f65c"
 dependencies = [
  "memchr",
 ]
diff --git a/Cargo.toml b/Cargo.toml
index 478008a..a940ad1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -29,7 +29,7 @@ serde = { version = "1.0.188", features = ["derive"] }
 simple-aprs = "0.3.2"
 tokio = { version = "1.32.0", features = ["full"] }
 toml = "0.8.2"
-tonic = "0.10.2"
+tonic = { version = "0.10.2", features = ["tls", "tls-roots"] }
 ham-cats = { git = "https://gitlab.scd31.com/cats/ham-cats"}
 half = "2.3.1"
 geoutils = "0.5.1"
diff --git a/src/aprs.rs b/src/aprs.rs
index 5c9e281..8147482 100644
--- a/src/aprs.rs
+++ b/src/aprs.rs
@@ -3,13 +3,20 @@ use std::time::Duration;
 use aprs_parser::AprsPacket;
 use futures::StreamExt;
 use simple_aprs::{ISConnection, ISSettings, RawPacket};
-use tokio::sync::broadcast;
+use tokio::{
+    pin, select,
+    sync::{broadcast, mpsc, oneshot},
+};
 
 use crate::config::AprsConfig;
 
-pub async fn connect(config: &AprsConfig, tx: broadcast::Sender<AprsPacket>) {
+pub async fn connect(
+    config: &AprsConfig,
+    tx: broadcast::Sender<AprsPacket>,
+    mut rx: mpsc::Receiver<AprsPacket>,
+) {
     loop {
-        tokio::time::sleep(Duration::from_millis(100)).await;
+        tokio::time::sleep(Duration::from_millis(1000)).await;
 
         println!("Connecting to APRS...");
 
@@ -33,17 +40,52 @@ pub async fn connect(config: &AprsConfig, tx: broadcast::Sender<AprsPacket>) {
 
         println!("Connected to APRS.");
 
-        let (mut aprs_read, aprs_write) = aprs_is.split();
+        let (mut aprs_read, mut aprs_write) = aprs_is.split();
 
-        aprs_read
-            .stream()
-            .for_each(|x| async {
-                match x {
-                    Ok(packet) => handle_aprs_packet(&packet, &tx).await,
-                    Err(e) => eprintln!("APRS-IS error: {e}"),
+        // if this task dies we need a way to restart the loop
+        // we use a oneshot for this
+        let (reader_died_tx, reader_died_rx) = oneshot::channel();
+        let (writer_died_tx, writer_died_rx) = oneshot::channel();
+
+        {
+            let tx = tx.clone();
+
+            tokio::task::spawn(async move {
+                let stream = aprs_read.stream();
+                pin!(stream);
+
+                select! {
+                    _ = async {
+                        while let Some(x) = stream.next().await {
+                            match x {
+                                Ok(packet) => handle_aprs_packet(&packet, &tx).await,
+                                Err(e) => {
+                                    eprintln!("APRS-IS error: {e}");
+                                    reader_died_tx.send(()).ok();
+                                    break;
+                                }
+                            }
+                        }
+                    } => {}
+
+                    _ = writer_died_rx => {}
                 }
-            })
-            .await;
+            });
+        }
+
+        select! {
+            _ = async {
+                while let Some(pkt) = rx.recv().await {
+                    if let Err(e) = aprs_write.send(&pkt).await {
+                        eprintln!("APRS-IS error: {e}");
+                        writer_died_tx.send(()).ok();
+                        break;
+                    }
+                }
+            } => {},
+
+            _ = reader_died_rx => {}
+        };
     }
 }
 
diff --git a/src/main.rs b/src/main.rs
index 01130be..a7ad7a0 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,22 +1,31 @@
 use std::time::Duration;
 
-use anyhow::{anyhow, Context};
-use aprs_parser::{AprsData, AprsPacket};
+use anyhow::{anyhow, bail, Context};
+use aprs_parser::{
+    AprsCst, AprsData, AprsMessage, AprsPacket, Callsign, Latitude, Longitude, Precision,
+    QConstruct, Via,
+};
 use async_stream::stream;
 use config::{Config, FelinetConfig};
-use felinet::{handler_client::HandlerClient, Packet as SemiPacket};
+use felinet::{
+    handler_client::HandlerClient, packet_filter::Filter, Packet as SemiPacket, PacketFilter,
+};
+use futures::StreamExt;
 use geoutils::Location;
 use half::f16;
 use ham_cats::{
+    error::CommentError,
     packet::Packet as CatsPacket,
-    whisker::{Destination, Gps, Identification, Route},
+    whisker::{Arbitrary, Destination, Gps, Identification, Route, RouteNode},
 };
-use tokio::sync::broadcast;
+use tokio::sync::{broadcast, mpsc};
 use tonic::Request;
 
 mod aprs;
 mod config;
 
+const MAX_PACKET_LEN: usize = 8191;
+
 pub mod felinet {
     tonic::include_proto!("felinet");
 }
@@ -24,19 +33,36 @@ pub mod felinet {
 #[tokio::main]
 async fn main() -> anyhow::Result<()> {
     let config = Config::load()?;
+    let aprs_callsign = Callsign::new(&config.aprs.callsign)
+        .context("Given APRS callsign is not a valid callsign")?;
 
     let aprs_recv_tx = broadcast::Sender::new(1024);
+    let (aprs_send_tx, aprs_send_rx) = mpsc::channel(1024);
 
     {
+        let config = config.clone();
         let aprs_recv_tx = aprs_recv_tx.clone();
         tokio::task::spawn(async move {
-            aprs::connect(&config.aprs, aprs_recv_tx).await;
+            aprs::connect(&config.aprs, aprs_recv_tx, aprs_send_rx).await;
+        });
+    }
+
+    {
+        let config = config.clone();
+        tokio::task::spawn(async move {
+            loop {
+                if let Err(e) = felinet_tx_forever(&config.felinet, aprs_recv_tx.subscribe()).await
+                {
+                    eprintln!("FELINET TX error: {e}");
+                    tokio::time::sleep(Duration::from_secs(1)).await;
+                }
+            }
         });
     }
 
     loop {
-        if let Err(e) = felinet_tx_forever(&config.felinet, aprs_recv_tx.subscribe()).await {
-            eprintln!("FELINET error: {e}");
+        if let Err(e) = felinet_rx_forever(&config.felinet, &aprs_callsign, &aprs_send_tx).await {
+            eprintln!("FELINET RX error: {e}");
             tokio::time::sleep(Duration::from_secs(1)).await;
         }
     }
@@ -62,12 +88,57 @@ async fn felinet_tx_forever(
 
     client.push_packets(Request::new(felinet_stream)).await?;
 
+    bail!("TX stream ended");
+}
+
+async fn felinet_rx_forever(
+    config: &FelinetConfig,
+    aprs_callsign: &Callsign,
+    aprs_send_tx: &mpsc::Sender<AprsPacket>,
+) -> anyhow::Result<()> {
+    let mut client = HandlerClient::connect(config.address.clone()).await?;
+
+    let mut stream = client
+        .get_packets(Request::new(PacketFilter {
+            filter: Some(Filter::All(0)),
+        }))
+        .await?
+        .into_inner();
+
+    while let Some(semi) = stream.next().await {
+        handle_semi(semi?, aprs_callsign, aprs_send_tx).await?;
+    }
+
+    bail!("RX stream ended");
+}
+
+async fn handle_semi(
+    semi: SemiPacket,
+    aprs_callsign: &Callsign,
+    aprs_send_tx: &mpsc::Sender<AprsPacket>,
+) -> anyhow::Result<()> {
+    let pkt = CatsPacket::<MAX_PACKET_LEN>::semi_decode(
+        semi.raw[..].try_into().ok().context("Capacity error")?,
+    )?;
+
+    if pkt.arbitrary_iter().any(|a| a.0[..] == [0xC0]) {
+        // packet came from APRS initially
+        return Ok(());
+    }
+
+    let aprs = felinet_to_aprs(pkt, aprs_callsign)?;
+
+    for p in aprs {
+        aprs_send_tx.send(p).await?;
+    }
+
     Ok(())
 }
 
 fn aprs_to_felinet(aprs: &AprsPacket) -> anyhow::Result<Option<SemiPacket>> {
-    let mut cats: CatsPacket<8191> = CatsPacket::default();
+    let mut cats: CatsPacket<MAX_PACKET_LEN> = CatsPacket::default();
 
+    // TODO map icons between standards?
     cats.add_identification(
         Identification::new(
             0,
@@ -78,10 +149,15 @@ fn aprs_to_felinet(aprs: &AprsPacket) -> anyhow::Result<Option<SemiPacket>> {
     )?;
 
     // 3 is arbitrary
+    // TODO could be a bit smarter by using the packet via
     let mut r = Route::new(3);
     r.push_internet();
     cats.add_route(r)?;
 
+    // used so that we can tell which CATS packets came from APRS
+    // so that we don't digipeat them back to APRS
+    cats.add_arbitrary(Arbitrary::new(&[0xC0]).unwrap())?;
+
     // TODO can definitely make this better
     match &aprs.data {
         // TODO should do something with CST in here
@@ -162,3 +238,98 @@ fn aprs_to_felinet(aprs: &AprsPacket) -> anyhow::Result<Option<SemiPacket>> {
 
     Ok(Some(semi))
 }
+
+// TODO need a way to prevent loops like FELINET -> APRS -> FELINET
+// Can probably use the destination field, except we'll need to do something special for Mic-E
+
+// CATS can encode things in one packet that may require separate packets in APRS.
+// The solution is to create multiple APRS packets
+fn felinet_to_aprs(
+    cats: CatsPacket<MAX_PACKET_LEN>,
+    aprs_callsign: &Callsign,
+) -> anyhow::Result<Vec<AprsPacket>> {
+    // (comment & destination) -> (message)
+    // (comment & destination & gps) -> (message, position)
+    // (comment & gps) -> (position)
+    let mut out = vec![];
+
+    let ident = match cats.identification() {
+        Some(x) => x,
+        None => return Ok(out),
+    };
+    let from = Callsign::new_with_ssid(ident.callsign.as_str(), format!("{}", ident.ssid));
+
+    let mut via = vec![];
+
+    if let Some(r) = cats.route() {
+        for x in r.iter() {
+            match x {
+                RouteNode::Internet => {
+                    via.push(Via::QConstruct(QConstruct::Ar));
+                    via.push(Via::Callsign(aprs_callsign.to_owned(), true));
+                }
+                RouteNode::Identity(callsign, ssid, false) => {
+                    via.push(Via::Callsign(
+                        Callsign::new_with_ssid(callsign, format!("{}", ssid)),
+                        true,
+                    ));
+                }
+                RouteNode::Identity(_, _, true) => {}
+            }
+        }
+    };
+
+    let mut buf = [0; MAX_PACKET_LEN];
+    let comment = match cats.comment(&mut buf) {
+        Ok(c) => Some(c),
+        Err(CommentError::NoComment) => None,
+        // buffer is as long as the max packet length
+        Err(CommentError::BufferOverflow) => unreachable!(),
+    };
+
+    // one message packet per destination
+    if let Some(text) = comment {
+        for dest in cats.destination_iter() {
+            let id = match dest.ack_num() {
+                0 => None,
+                x => Some(format!("{}", x).into_bytes()),
+            };
+
+            out.push(AprsPacket {
+                from: from.clone(),
+                via: via.clone(),
+                data: AprsData::Message(AprsMessage {
+                    to: Callsign::new_no_ssid("APRS"),
+                    addressee: format!("{}-{}", ident.callsign, ident.ssid).into_bytes(),
+                    text: format!("[CATS] {}", text).into_bytes(),
+                    id,
+                }),
+            });
+        }
+    }
+
+    if let Some(gps) = cats.gps() {
+        let text = format!("[CATS] {}", comment.unwrap_or(""));
+
+        out.push(AprsPacket {
+            from,
+            via,
+            data: AprsData::Position(aprs_parser::AprsPosition {
+                to: Callsign::new_no_ssid("APRS"),
+                timestamp: None,
+                messaging_supported: true,
+                latitude: Latitude::new(gps.latitude()).context("Invalid latitude")?,
+                longitude: Longitude::new(gps.longitude()).context("Invalid longitude")?,
+                precision: Precision::TenthMinute,
+                // TODO map icons between standards?
+                symbol_table: '/',
+                symbol_code: 'p',
+
+                comment: text.into(),
+                cst: AprsCst::Uncompressed,
+            }),
+        });
+    }
+
+    Ok(out)
+}
-- 
GitLab


From 1b714bf3f73ffe6f6fb23c5c399a92b404f331d0 Mon Sep 17 00:00:00 2001
From: Stephen D <webmaster@scd31.com>
Date: Sat, 14 Oct 2023 13:24:56 -0300
Subject: [PATCH 2/2] prevent FELINET -> APRS -> FELINET loop

---
 src/main.rs | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index a7ad7a0..a03b550 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -136,6 +136,11 @@ async fn handle_semi(
 }
 
 fn aprs_to_felinet(aprs: &AprsPacket) -> anyhow::Result<Option<SemiPacket>> {
+    // Was a FELINET packet initially, so we don't want to repeat it again
+    if aprs.to() == Some(&Callsign::new_no_ssid("KITTY")) {
+        return Ok(None);
+    }
+
     let mut cats: CatsPacket<MAX_PACKET_LEN> = CatsPacket::default();
 
     // TODO map icons between standards?
@@ -239,9 +244,6 @@ fn aprs_to_felinet(aprs: &AprsPacket) -> anyhow::Result<Option<SemiPacket>> {
     Ok(Some(semi))
 }
 
-// TODO need a way to prevent loops like FELINET -> APRS -> FELINET
-// Can probably use the destination field, except we'll need to do something special for Mic-E
-
 // CATS can encode things in one packet that may require separate packets in APRS.
 // The solution is to create multiple APRS packets
 fn felinet_to_aprs(
@@ -299,7 +301,7 @@ fn felinet_to_aprs(
                 from: from.clone(),
                 via: via.clone(),
                 data: AprsData::Message(AprsMessage {
-                    to: Callsign::new_no_ssid("APRS"),
+                    to: Callsign::new_no_ssid("KITTY"),
                     addressee: format!("{}-{}", ident.callsign, ident.ssid).into_bytes(),
                     text: format!("[CATS] {}", text).into_bytes(),
                     id,
@@ -315,7 +317,7 @@ fn felinet_to_aprs(
             from,
             via,
             data: AprsData::Position(aprs_parser::AprsPosition {
-                to: Callsign::new_no_ssid("APRS"),
+                to: Callsign::new_no_ssid("KITTY"),
                 timestamp: None,
                 messaging_supported: true,
                 latitude: Latitude::new(gps.latitude()).context("Invalid latitude")?,
-- 
GitLab