From d9d9e10ede65cb48ef976b56548d96b27db79d4a Mon Sep 17 00:00:00 2001 From: notoriaga Date: Tue, 6 Dec 2022 11:37:47 -0800 Subject: [PATCH 1/8] misc cleanup --- src/main.rs | 40 ++++++++++++++++++---------------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/src/main.rs b/src/main.rs index a8b1694..bdc6e85 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,20 +17,20 @@ struct Cli { url: String, /// Receiver latitude to report, in degrees - #[clap(long, default_value = "37.77101999622968", allow_hyphen_values = true)] - lat: String, + #[clap(long, default_value_t = 37.77101999622968, allow_hyphen_values = true)] + lat: f64, /// Receiver longitude to report, in degrees #[clap( long, - default_value = "-122.40315159140708", + default_value_t = -122.40315159140708, allow_hyphen_values = true )] - lon: String, + lon: f64, /// Receiver height to report, in meters - #[clap(long, default_value = "-5.549358852471994", allow_hyphen_values = true)] - height: String, + #[clap(long, default_value_t = -5.549358852471994, allow_hyphen_values = true)] + height: f64, /// Client ID #[clap( @@ -56,12 +56,12 @@ struct Cli { password: Option, /// GGA update period, in seconds. 0 means to never send a GGA - #[clap(long, default_value = "10")] + #[clap(long, default_value_t = 10)] gga_period: u64, /// Request counter allows correlation between message sent and acknowledgment response from corrections stream - #[clap(long)] - request_counter: Option, + #[clap(long, default_value_t = 0)] + request_counter: u8, /// Area ID to be used in generation of CRA message. If this flag is set, ntripping outputs messages of type CRA rather than the default GGA #[clap(long)] @@ -94,12 +94,9 @@ fn checksum(buf: &[u8]) -> u8 { fn main() -> Result<()> { let opt = Cli::parse(); - let latf: f64 = opt.lat.parse::()?; - let lonf: f64 = opt.lon.parse::()?; - let heightf: f64 = opt.height.parse::()?; - - let latn = ((latf * 1e8).round() / 1e8).abs(); - let lonn = ((lonf * 1e8).round() / 1e8).abs(); + let latn = ((opt.lat * 1e8).round() / 1e8).abs(); + let lonn = ((opt.lon * 1e8).round() / 1e8).abs(); + let height = opt.height; let lat_deg: u16 = latn as u16; let lon_deg: u16 = lonn as u16; @@ -107,10 +104,10 @@ fn main() -> Result<()> { let lat_min: f64 = (latn - (lat_deg as f64)) * 60.0; let lon_min: f64 = (lonn - (lon_deg as f64)) * 60.0; - let lat_dir = if latf < 0.0 { 'S' } else { 'N' }; - let lon_dir = if lonf < 0.0 { 'W' } else { 'E' }; + let lat_dir = if opt.lat < 0.0 { 'S' } else { 'N' }; + let lon_dir = if opt.lon < 0.0 { 'W' } else { 'E' }; - let mut request_counter = opt.request_counter.unwrap_or(0); + let mut request_counter = opt.request_counter; CURL.with(|curl| -> Result<()> { let mut curl = curl.borrow_mut(); @@ -179,14 +176,13 @@ fn main() -> Result<()> { Some(solution_id) => solution_id.to_string(), None => String::new() }; - format!("$PSWTCRA,{},{},{},{}", request_counter, area_id, corrections_mask, solution_id) + format!("$PSWTCRA,{request_counter},{area_id},{corrections_mask},{solution_id}") }, None => { - format!("$GPGGA,{},{:02}{:010.7},{},{:03}{:010.7},{},4,12,1.3,{:.2},M,0.0,M,1.7,0078", - time, lat_deg, lat_min, lat_dir, lon_deg, lon_min, lon_dir, heightf) + format!("$GPGGA,{time},{lat_deg:02}{lat_min:010.7},{lat_dir},{lon_deg:03}{lon_min:010.7},{lon_dir},4,12,1.3,{height:.2},M,0.0,M,1.7,0078") } }; - request_counter = request_counter.overflowing_add(1).0; + request_counter = request_counter.wrapping_add(1); let checksum = checksum(message.as_bytes()); let message = format!("{}*{:X}\r\n", message, checksum); buf.write_all(message.as_bytes()).unwrap(); From 75c8dadb4920036caed6615cb1bb9ab5ab24837e Mon Sep 17 00:00:00 2001 From: notoriaga Date: Sat, 10 Dec 2022 13:53:11 -0800 Subject: [PATCH 2/8] split into two crates/use async --- Cargo.lock | 622 ++++++++++++++++++++++------- Cargo.toml | 16 +- ntripping-cli/Cargo.toml | 21 + build.rs => ntripping-cli/build.rs | 0 ntripping-cli/src/main.rs | 183 +++++++++ ntripping/Cargo.toml | 16 + ntripping/src/error.rs | 42 ++ ntripping/src/header.rs | 7 + ntripping/src/lib.rs | 358 +++++++++++++++++ ntripping/src/sentence.rs | 321 +++++++++++++++ src/main.rs | 201 ---------- 11 files changed, 1436 insertions(+), 351 deletions(-) create mode 100644 ntripping-cli/Cargo.toml rename build.rs => ntripping-cli/build.rs (100%) create mode 100644 ntripping-cli/src/main.rs create mode 100644 ntripping/Cargo.toml create mode 100644 ntripping/src/error.rs create mode 100644 ntripping/src/header.rs create mode 100644 ntripping/src/lib.rs create mode 100644 ntripping/src/sentence.rs delete mode 100644 src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 3d10639..eb4714f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,27 +24,39 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.0.0" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "base64" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "bitflags" -version = "1.2.1" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bumpalo" -version = "3.10.0" +version = "3.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" + +[[package]] +name = "bytes" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" +checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" [[package]] name = "cc" -version = "1.0.58" +version = "1.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a06fb2e53271d7c279ec1efea6ab691c35a2ae67ec0d91d7acec0caf13b518" +checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4" [[package]] name = "cfg-if" @@ -104,6 +116,16 @@ dependencies = [ "os_str_bytes", ] +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + [[package]] name = "core-foundation-sys" version = "0.8.3" @@ -111,33 +133,107 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" [[package]] -name = "curl" -version = "0.4.44" +name = "cxx" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "509bd11746c7ac09ebd19f0b17782eae80aadee26237658a6b4808afb5c11a22" +checksum = "bdf07d07d6531bfcdbe9b8b739b104610c6508dcc4d63b410585faf338241daf" dependencies = [ - "curl-sys", - "libc", - "openssl-probe", - "openssl-sys", - "schannel", - "socket2", - "winapi", + "cc", + "cxxbridge-flags", + "cxxbridge-macro", + "link-cplusplus", ] [[package]] -name = "curl-sys" -version = "0.4.56+curl-7.83.1" +name = "cxx-build" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6093e169dd4de29e468fa649fbae11cdcd5551c81fe5bf1b0677adad7ef3d26f" +checksum = "d2eb5b96ecdc99f72657332953d4d9c50135af1bac34277801cc3937906ebd39" dependencies = [ "cc", - "libc", - "libz-sys", - "openssl-sys", - "pkg-config", - "vcpkg", - "winapi", + "codespan-reporting", + "once_cell", + "proc-macro2", + "quote", + "scratch", + "syn", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac040a39517fd1674e0f32177648334b0f4074625b5588a64519804ba0553b12" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1362b0ddcfc4eb0a1f57b68bd77dd99f0e826958a96abd0ae9bd092e114ffed6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "futures-channel" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ba265a92256105f45b719605a571ffe2d1f0fea3807304b522c1d778f79eed" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac" + +[[package]] +name = "futures-macro" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39c15cf1a4aa79df40f1bb462fb39676d0ad9e366c2a33b590d7c66f4f81fcf9" + +[[package]] +name = "futures-task" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea" + +[[package]] +name = "futures-util" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6" +dependencies = [ + "futures-core", + "futures-macro", + "futures-sink", + "futures-task", + "pin-project-lite", + "pin-utils", + "slab", ] [[package]] @@ -148,32 +244,102 @@ checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" [[package]] name = "hermit-abi" -version = "0.1.15" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3deed196b6e7f9e44a2ae8d94225d80302d81208b1bb673fd21fe634645c85a9" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" dependencies = [ "libc", ] +[[package]] +name = "http" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.0-rc1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f038884e63a5a85612eeea789f5e0f54c3ada7306234502543b23d98744781f0" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + +[[package]] +name = "hyper" +version = "1.0.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d85cfb5d312f278a651d0567873b9c32b28d8ced5603a9242c73c54c93fec814" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "tokio", + "tracing", + "want", +] + [[package]] name = "iana-time-zone" -version = "0.1.48" +version = "0.1.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "237a0714f28b1ee39ccec0770ccb544eb02c9ef2c82bb096230eefcffa6468b0" +checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" dependencies = [ "android_system_properties", "core-foundation-sys", + "iana-time-zone-haiku", "js-sys", - "once_cell", "wasm-bindgen", "winapi", ] +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +dependencies = [ + "cxx", + "cxx-build", +] + +[[package]] +name = "itoa" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" + [[package]] name = "js-sys" -version = "0.3.59" +version = "0.3.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "258451ab10b34f8af53416d1fdab72c22e805f0c92a1136d59470ec0b11138b2" +checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" dependencies = [ "wasm-bindgen", ] @@ -191,15 +357,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64de3cc433455c14174d42e554d4027ee631c4d046d43e3ecc6efc4636cdc7a7" [[package]] -name = "libz-sys" -version = "1.1.0" +name = "link-cplusplus" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af67924b8dd885cccea261866c8ce5b74d239d272e154053ff927dae839f5ae9" +checksum = "9272ab7b96c9046fbc5bc56c06c117cb639fe2d509df0c421cad82d2915cf369" dependencies = [ "cc", - "libc", - "pkg-config", - "vcpkg", ] [[package]] @@ -211,21 +374,66 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "mio" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" +dependencies = [ + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys", +] + [[package]] name = "ntripping" version = "1.4.0" +dependencies = [ + "base64", + "bytes", + "chrono", + "futures-channel", + "futures-util", + "hyper", + "tokio", + "tracing", +] + +[[package]] +name = "ntripping-cli" +version = "1.4.0" dependencies = [ "chrono", "clap", - "curl", + "futures-util", + "ntripping", + "tokio", + "tracing-subscriber", "vergen", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num-integer" -version = "0.1.43" +version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d59457e662d541ba17869cf51cf177c0b5f0cbf476c66bdc90bf1edac4f875b" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" dependencies = [ "autocfg", "num-traits", @@ -233,9 +441,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.12" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac267bcc07f48ee5f8935ab0d24f316fb722d7a1292e2913f0cc196b29ffd611" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" dependencies = [ "autocfg", ] @@ -247,50 +455,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0" [[package]] -name = "openssl-probe" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" - -[[package]] -name = "openssl-sys" -version = "0.9.75" +name = "os_str_bytes" +version = "6.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5f9bd0c2710541a3cda73d6f9ac4f1b240de4ae261065d309dbe73d9dceb42f" -dependencies = [ - "autocfg", - "cc", - "libc", - "pkg-config", - "vcpkg", -] +checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" [[package]] -name = "os_str_bytes" -version = "6.0.0" +name = "overload" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] -name = "pest" -version = "2.1.3" +name = "pin-project-lite" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" -dependencies = [ - "ucd-trie", -] +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" [[package]] -name = "pkg-config" -version = "0.3.18" +name = "pin-utils" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d36492546b6af1463394d46f0c834346f31548646f6ba10849802c9c9a27ac33" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "proc-macro-error" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc175e9777c3116627248584e8f8b3e2987405cabe1c0adf7d1dd28f09dc7880" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ "proc-macro-error-attr", "proc-macro2", @@ -301,77 +493,83 @@ dependencies = [ [[package]] name = "proc-macro-error-attr" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cc9795ca17eb581285ec44936da7fc2335a3f34f2ddd13118b6f4d515435c50" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ "proc-macro2", "quote", - "syn", - "syn-mid", "version_check", ] [[package]] name = "proc-macro2" -version = "1.0.46" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b" +checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.18" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" dependencies = [ "proc-macro2", ] [[package]] name = "rustc_version" -version = "0.3.3" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ "semver", ] [[package]] -name = "schannel" -version = "0.1.19" +name = "scratch" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" -dependencies = [ - "lazy_static", - "winapi", -] +checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898" [[package]] name = "semver" -version = "0.11.0" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4" + +[[package]] +name = "sharded-slab" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" dependencies = [ - "semver-parser", + "lazy_static", ] [[package]] -name = "semver-parser" -version = "0.10.2" +name = "slab" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7" +checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" dependencies = [ - "pest", + "autocfg", ] +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + [[package]] name = "socket2" -version = "0.4.0" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e3dfc207c526015c632472a77be09cf1b6e46866581aecae5cc38fb4235dea2" +checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" dependencies = [ "libc", "winapi", @@ -385,50 +583,123 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" -version = "1.0.92" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ff7c592601f11445996a06f8ad0c27f094a58857c2f89e97974ab9235b92c52" +checksum = "60b9b43d45702de4c839cb9b51d9f529c5dd26a4aff255b42b1ebc03e88ee908" dependencies = [ "proc-macro2", "quote", - "unicode-xid", + "unicode-ident", ] [[package]] -name = "syn-mid" -version = "0.5.0" +name = "termcolor" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7be3539f6c128a931cf19dcee741c1af532c7fd387baa739c03dd2e96479338a" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" dependencies = [ - "proc-macro2", - "quote", - "syn", + "winapi-util", ] [[package]] -name = "termcolor" -version = "1.1.3" +name = "thread_local" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" dependencies = [ - "winapi-util", + "once_cell", ] [[package]] name = "time" -version = "0.1.43" +version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" dependencies = [ "libc", + "wasi 0.10.0+wasi-snapshot-preview1", "winapi", ] [[package]] -name = "ucd-trie" +name = "tokio" +version = "1.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eab6d665857cc6ca78d6e80303a02cea7a7851e85dfbd77cbdc09bd129f1ef46" +dependencies = [ + "autocfg", + "bytes", + "libc", + "memchr", + "mio", + "pin-project-lite", + "socket2", + "windows-sys", +] + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" +checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" [[package]] name = "unicode-ident" @@ -437,16 +708,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" [[package]] -name = "unicode-xid" -version = "0.2.3" +name = "unicode-width" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" [[package]] -name = "vcpkg" -version = "0.2.10" +name = "valuable" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6454029bf181f092ad1b853286f23e2c507d8e8194d01d92da4a55c274a5508c" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" [[package]] name = "vergen" @@ -461,15 +732,37 @@ dependencies = [ [[package]] name = "version_check" -version = "0.9.2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.82" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7652e3f6c4706c8d9cd54832c4a4ccb9b5336e2c3bd154d5cccfbf1c1f5f7d" +checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -477,9 +770,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.82" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "662cd44805586bd52971b9586b1df85cdbbd9112e4ef4d8f41559c334dc6ac3f" +checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" dependencies = [ "bumpalo", "log", @@ -492,9 +785,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.82" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b260f13d3012071dfb1512849c033b1925038373aea48ced3012c09df952c602" +checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -502,9 +795,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.82" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da" +checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" dependencies = [ "proc-macro2", "quote", @@ -515,9 +808,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.82" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a" +checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" [[package]] name = "winapi" @@ -549,3 +842,60 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" diff --git a/Cargo.toml b/Cargo.toml index 0865ab6..b8df1bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,14 +1,2 @@ -[package] -name = "ntripping" -version = "1.4.0" -authors = ["Swift Navigation "] -edition = "2018" -publish = true - -[dependencies] -curl = "^0.4.44" -clap = { version = "4", features = ["derive"] } -chrono = "0.4" - -[build-dependencies] -vergen = "3" +[workspace] +members = ["ntripping", "ntripping-cli"] diff --git a/ntripping-cli/Cargo.toml b/ntripping-cli/Cargo.toml new file mode 100644 index 0000000..6bd1536 --- /dev/null +++ b/ntripping-cli/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "ntripping-cli" +version = "1.4.0" +authors = ["Swift Navigation "] +edition = "2021" +publish = true + +[[bin]] +name = "ntripping" +path = "src/main.rs" + +[dependencies] +chrono = "0.4.23" +clap = { version = "4", features = ["derive"] } +futures-util = "0.3.25" +ntripping = { path = "../ntripping" } +tokio = { version = "1.23.0", features = ["rt", "time", "io-std", "io-util"] } +tracing-subscriber = "0.3.16" + +[build-dependencies] +vergen = "3" diff --git a/build.rs b/ntripping-cli/build.rs similarity index 100% rename from build.rs rename to ntripping-cli/build.rs diff --git a/ntripping-cli/src/main.rs b/ntripping-cli/src/main.rs new file mode 100644 index 0000000..15c3b46 --- /dev/null +++ b/ntripping-cli/src/main.rs @@ -0,0 +1,183 @@ +use std::time::{Duration, SystemTime}; + +use chrono::{DateTime, Utc}; +use clap::Parser; +use futures_util::{SinkExt, TryStreamExt}; +use tokio::io::{self, AsyncWriteExt}; +use tracing_subscriber::filter::LevelFilter; + +use ntripping::{ + sentence::{Cra, Gga, Sentence}, + Auth, Client, +}; + +#[derive(Debug, clap::Parser)] +#[clap( + name = "ntripping", + about = "NTRIP command line client.", + version = env!("VERGEN_SEMVER_LIGHTWEIGHT"), +)] +struct Cli { + /// URL of the NTRIP caster + #[clap(long, default_value = "na.skylark.swiftnav.com:2101/CRS")] + url: String, + + /// Receiver latitude to report, in degrees + #[clap(long, default_value_t = 37.77101999622968, allow_hyphen_values = true)] + lat: f64, + + /// Receiver longitude to report, in degrees + #[clap( + long, + default_value_t = -122.40315159140708, + allow_hyphen_values = true + )] + lon: f64, + + /// Receiver height to report, in meters + #[clap(long, default_value_t = -5.549358852471994, allow_hyphen_values = true)] + height: f64, + + /// Client ID + #[clap( + long, + default_value = "00000000-0000-0000-0000-000000000000", + alias = "client" + )] + client_id: String, + + /// Verbosity level, can be specified multiple times + #[clap(short, long, action = clap::ArgAction::Count)] + verbose: u8, + + /// Receiver time to report, as a Unix time + #[clap(long)] + epoch: Option, + + /// Username credentials + #[clap(long)] + username: Option, + + /// Password credentials + #[clap(long)] + password: Option, + + /// GGA update period, in seconds. 0 means to never send a GGA + #[clap(long, default_value_t = 10)] + gga_period: u64, + + /// Set the ntrip-gga header + #[clap(long)] + gga_header: bool, + + /// Request counter allows correlation between message sent and acknowledgment response from corrections stream + #[clap(long, default_value_t = 0)] + request_counter: u8, + + /// Area ID to be used in generation of CRA message. If this flag is set, ntripping outputs messages of type CRA rather than the default GGA + #[clap(long)] + area_id: Option, + + /// Field specifying which types of corrections are to be received + #[clap(long)] + corrections_mask: Option, + + /// Solution ID, the identifier of the connection stream to reconnect to in the event of disconnections + #[clap(long)] + solution_id: Option, +} + +type Result = std::result::Result>; + +async fn run(opt: Cli) -> Result<()> { + let now = || -> DateTime { + if let Some(epoch) = opt.epoch { + SystemTime::UNIX_EPOCH + Duration::from_secs(epoch.into()) + } else { + SystemTime::now() + } + .into() + }; + + let auth = if let (Some(username), Some(password)) = (opt.username, opt.password) { + Some(Auth::new(username, password)) + } else { + None + }; + + let msg: Sentence = if let Some(area_id) = opt.area_id { + Cra::new() + .with_area_id(area_id) + .with_corrections_mask(opt.corrections_mask) + .with_solution_id(opt.solution_id) + .into() + } else { + Gga::new() + .with_time(now()) + .with_lat(opt.lat) + .with_lon(opt.lon) + .with_height(opt.height) + .into() + }; + + let client = { + let client = Client::new().with_client_id(opt.client_id).with_auth(auth); + if opt.gga_header { + client.with_ntrip_gga(msg) + } else { + client + } + }; + + let (mut sink, mut stream) = { + let url = if !opt.url.starts_with("http://") && !opt.url.starts_with("https://") { + format!("https://{}", opt.url) + } else { + opt.url + }; + let uri = url.parse()?; + client.connect(uri).await?.split() + }; + + let writer_task = tokio::spawn(async move { + let mut out = io::stdout(); + while let Some(data) = stream.try_next().await? { + out.write_all(&data).await?; + } + Result::Ok(()) + }); + + if opt.gga_period == 0 { + return writer_task.await?; + } + + let mut request_counter = opt.request_counter; + let mut gga_interval = tokio::time::interval(Duration::from_secs(opt.gga_period)); + loop { + if writer_task.is_finished() { + break; + } + gga_interval.tick().await; + let sentence = msg.with_time(now()).with_request_counter(request_counter); + let _ = sink.send(sentence).await; + request_counter = request_counter.wrapping_add(1); + } + + Ok(()) +} + +fn main() -> Result<()> { + let opt = Cli::parse(); + tracing_subscriber::fmt::fmt() + .with_max_level(match opt.verbose { + 0 => LevelFilter::WARN, + 1 => LevelFilter::DEBUG, + _ => LevelFilter::TRACE, + }) + .compact() + .init(); + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build()? + .block_on(run(opt)) +} diff --git a/ntripping/Cargo.toml b/ntripping/Cargo.toml new file mode 100644 index 0000000..ed38816 --- /dev/null +++ b/ntripping/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "ntripping" +version = "1.4.0" +authors = ["Swift Navigation "] +edition = "2021" +publish = true + +[dependencies] +base64 = "0.13" +bytes = "1.0" +chrono = "0.4" +futures-channel = { version = "0.3", features = ["sink"] } +futures-util = { version = "0.3", default-features = false, features = ["sink"] } +hyper = { version = "1.0.0-rc.1", features = ["client", "http1"] } +tokio = { version = "1.23", features = ["net", "rt"] } +tracing = "0.1" diff --git a/ntripping/src/error.rs b/ntripping/src/error.rs new file mode 100644 index 0000000..4f858dc --- /dev/null +++ b/ntripping/src/error.rs @@ -0,0 +1,42 @@ +use std::io; + +#[derive(Debug)] +pub enum Error { + InvalidUri(&'static str), + Io(io::Error), + Hyper(hyper::Error), + Http(hyper::http::Error), + BadStatus(hyper::StatusCode), +} + +impl From for Error { + fn from(e: io::Error) -> Self { + Self::Io(e) + } +} + +impl From for Error { + fn from(e: hyper::Error) -> Self { + Self::Hyper(e) + } +} + +impl From for Error { + fn from(e: hyper::http::Error) -> Self { + Self::Http(e) + } +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::InvalidUri(msg) => write!(f, "invalid uri: {msg}"), + Self::Io(e) => write!(f, "io error: {e}"), + Self::Hyper(e) => write!(f, "hyper error: {e}"), + Self::Http(e) => write!(f, "http error: {e}"), + Self::BadStatus(status) => write!(f, "bad status: {status}"), + } + } +} + +impl std::error::Error for Error {} diff --git a/ntripping/src/header.rs b/ntripping/src/header.rs new file mode 100644 index 0000000..b4c4d7a --- /dev/null +++ b/ntripping/src/header.rs @@ -0,0 +1,7 @@ +#![allow(clippy::declare_interior_mutable_const)] + +pub use hyper::http::header::*; + +pub const NTRIP_GGA: HeaderName = HeaderName::from_static("ntrip-gga"); +pub const NTRIP_VERSION: HeaderName = HeaderName::from_static("ntrip-version"); +pub const SWIFT_CLIENT_ID: HeaderName = HeaderName::from_static("x-swiftnav-client-id"); diff --git a/ntripping/src/lib.rs b/ntripping/src/lib.rs new file mode 100644 index 0000000..b5b9b7b --- /dev/null +++ b/ntripping/src/lib.rs @@ -0,0 +1,358 @@ +mod error; +mod header; +pub mod sentence; + +use std::{ + convert::Infallible, + fmt, + pin::Pin, + task::{Context, Poll}, +}; + +use bytes::Bytes; +use futures_channel::mpsc; +use futures_util::{Sink, Stream}; +use hyper::{ + body::{self, Body, Frame}, + client::conn::http1 as conn, + Request, StatusCode, Uri, +}; +use tokio::net::TcpStream; + +pub use error::Error; + +use crate::sentence::Sentence; + +#[derive(Debug, Default)] +pub struct Client { + auth: Option, + client_id: Option, + ntrip_gga: Option, +} + +impl Client { + pub fn new() -> Self { + Self::default() + } + + pub fn with_auth(mut self, auth: impl Into>) -> Self { + self.auth = auth.into(); + self + } + + pub fn with_client_id(mut self, client_id: impl Into>) -> Self { + self.client_id = client_id.into(); + self + } + + pub fn with_ntrip_gga(mut self, ntrip_gga: impl Into>) -> Self { + self.ntrip_gga = ntrip_gga.into(); + self + } + + pub async fn connect(&self, uri: Uri) -> Result { + connect(uri, self.headers()).await + } + + fn headers(&self) -> impl Iterator { + let auth = self.auth.as_ref().map(|auth| { + let auth = base64::encode(format!("{}:{}", auth.username, auth.password)); + (header::AUTHORIZATION, format!("Basic {auth}")) + }); + let gga = self + .ntrip_gga + .map(|gga| (header::NTRIP_GGA, gga.to_string(false))); + + std::iter::once(( + header::SWIFT_CLIENT_ID, + self.client_id + .as_deref() + .unwrap_or("00000000-0000-0000-0000-000000000000") + .to_owned(), + )) + .chain(auth) + .chain(gga) + } +} + +pub struct Auth { + username: String, + password: String, +} + +impl Auth { + pub fn new(username: impl Into, password: impl Into) -> Self { + Self { + username: username.into(), + password: password.into(), + } + } +} + +impl fmt::Debug for Auth { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Auth") + .field("username", &self.username) + .field("password", &"***") + .finish() + } +} + +#[derive(Debug)] +pub struct Connection { + send: SendHalf, + recv: ReceiveHalf, +} + +impl Connection { + pub fn split(self) -> (SendHalf, ReceiveHalf) { + (self.send, self.recv) + } +} + +impl Stream for Connection { + type Item = Result; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + Pin::new(&mut self.recv).poll_next(cx) + } +} + +impl Sink for Connection +where + T: Into, +{ + type Error = SendError; + + fn poll_ready(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + >::poll_ready(Pin::new(&mut self.send), cx) + } + + fn start_send(mut self: Pin<&mut Self>, item: T) -> Result<(), Self::Error> { + >::start_send(Pin::new(&mut self.send), item) + } + + fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + >::poll_flush(Pin::new(&mut self.send), cx) + } + + fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + >::poll_close(Pin::new(&mut self.send), cx) + } +} + +#[derive(Debug)] +pub struct ReceiveHalf { + response: hyper::Response, +} + +impl Stream for ReceiveHalf { + type Item = Result; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + match Pin::new(self.response.body_mut()).poll_frame(cx) { + Poll::Ready(Some(Ok(frame))) => { + let chunk = frame.into_data(); + if let Some(chunk) = &chunk { + tracing::trace!(length = chunk.len(), "recv"); + } else { + tracing::trace!("recv eof"); + } + Poll::Ready(chunk.map(Ok)) + } + Poll::Ready(Some(Err(e))) => Poll::Ready(Some(Err(ReceiveError(e)))), + Poll::Ready(None) => Poll::Ready(None), + Poll::Pending => Poll::Pending, + } + } +} + +#[derive(Debug)] +pub struct ReceiveError(hyper::Error); + +impl std::fmt::Display for ReceiveError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "receive error: {}", self.0) + } +} + +impl std::error::Error for ReceiveError {} + +#[derive(Debug)] +pub struct SendHalf { + tx: mpsc::Sender, +} + +impl Sink for SendHalf +where + T: Into, +{ + type Error = SendError; + + fn poll_ready(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + Pin::new(&mut self.tx).poll_ready(cx).map_err(|_| SendError) + } + + fn start_send(mut self: Pin<&mut Self>, item: T) -> Result<(), Self::Error> { + Pin::new(&mut self.tx) + .start_send(item.into()) + .map_err(|_| SendError) + } + + fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + Pin::new(&mut self.tx).poll_flush(cx).map_err(|_| SendError) + } + + fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + Pin::new(&mut self.tx).poll_close(cx).map_err(|_| SendError) + } +} + +#[derive(Debug)] +pub struct SendError; + +impl fmt::Display for SendError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("failed to send message") + } +} + +impl std::error::Error for SendError {} + +struct ChannelBody { + rx: mpsc::Receiver, +} + +impl Body for ChannelBody { + type Data = Bytes; + type Error = Infallible; + + fn poll_frame( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll, Self::Error>>> { + match Pin::new(&mut self.rx).poll_next(cx) { + Poll::Ready(Some(chunk)) => { + tracing::trace!(length = chunk.len(), "frame ready"); + Poll::Ready(Some(Ok(Frame::data(chunk)))) + } + Poll::Ready(None) => Poll::Ready(None), + Poll::Pending => Poll::Pending, + } + } +} + +async fn connect( + uri: Uri, + headers: impl Iterator, +) -> Result { + let Some(authority) = uri.authority().cloned() else { + return Err(Error::InvalidUri("invalid authority")); + }; + + let addr = format!("{}:{}", authority.host(), uri.port_u16().unwrap_or(2101)); + tracing::debug!(%addr, "connect"); + let stream = TcpStream::connect(addr).await?; + let (mut sender, conn) = conn::Builder::new() + .http09_responses(true) + .handshake(stream) + .await?; + tokio::spawn(conn); + + let mut builder = Request::builder() + .uri(uri) + .header(header::HOST, authority.as_str()) + .header(header::USER_AGENT, "NTRIP ntrip-client/1.0") + .header(header::NTRIP_VERSION, "Ntrip/2.0") + .method("GET"); + + for (key, value) in headers { + builder = builder.header(key, value); + } + + if let Some(i) = authority.as_str().find('@') { + if !builder + .headers_ref() + .unwrap() + .contains_key(&header::AUTHORIZATION) + { + let auth = base64::encode(&authority.as_str()[..i]); + builder = builder.header(header::AUTHORIZATION, format!("Basic {auth}")); + } + } + + let (tx, rx) = mpsc::channel::(1); + let request = builder.body(ChannelBody { rx })?; + let response = sender.send_request(request).await?; + if response.status() != StatusCode::OK { + return Err(Error::BadStatus(response.status())); + } + Ok(Connection { + send: SendHalf { tx }, + recv: ReceiveHalf { response }, + }) +} + +#[cfg(test)] +mod tests { + use crate::sentence::Cra; + + use super::*; + + #[test] + fn client_headers() { + let client = Client::new(); + assert_eq!( + client.headers().collect::>(), + vec![( + header::SWIFT_CLIENT_ID, + "00000000-0000-0000-0000-000000000000".to_string() + )] + ); + + let client = client.with_client_id("123".to_string()); + assert_eq!( + client.headers().collect::>(), + vec![(header::SWIFT_CLIENT_ID, "123".to_string()),] + ); + + let client = client.with_auth(Auth::new("user", "secret")); + assert_eq!( + client.headers().collect::>(), + vec![ + (header::SWIFT_CLIENT_ID, "123".to_string()), + (header::AUTHORIZATION, "Basic dXNlcjpzZWNyZXQ=".to_string()) + ] + ); + + let client = client.with_ntrip_gga(Sentence::CRA( + Cra::new().with_request_counter(0).with_area_id(1), + )); + assert_eq!( + client.headers().collect::>(), + vec![ + (header::SWIFT_CLIENT_ID, "123".to_string()), + (header::AUTHORIZATION, "Basic dXNlcjpzZWNyZXQ=".to_string()), + (header::NTRIP_GGA, "$PSWTCRA,0,1,,*51".to_string()) + ] + ); + + let client = client + .with_auth(None) + .with_ntrip_gga(None) + .with_client_id(None); + assert_eq!( + client.headers().collect::>(), + vec![( + header::SWIFT_CLIENT_ID, + "00000000-0000-0000-0000-000000000000".to_string() + )] + ); + } + + #[test] + fn auth_debug_hidden() { + let auth = format!("{:?}", Auth::new("user", "secret")); + assert!(!auth.contains("secret")); + } +} diff --git a/ntripping/src/sentence.rs b/ntripping/src/sentence.rs new file mode 100644 index 0000000..9d97abb --- /dev/null +++ b/ntripping/src/sentence.rs @@ -0,0 +1,321 @@ +use std::fmt::Write; + +use bytes::Bytes; +use chrono::{DateTime, Utc}; + +#[derive(Debug, Clone, Copy)] +pub enum Sentence { + GGA(Gga), + CRA(Cra), +} + +impl Sentence { + pub fn with_time(mut self, time: impl Into>>) -> Self { + if let Self::GGA(ref mut gga) = self { + gga.time = time.into() + } + self + } + + pub fn with_request_counter(mut self, request_counter: impl Into>) -> Self { + if let Self::CRA(ref mut cra) = self { + cra.request_counter = request_counter.into() + } + self + } + + pub(crate) fn to_string(self, carriage_return: bool) -> String { + let mut buf = String::from('$'); + match self { + Sentence::GGA(g) => g.write_string(&mut buf), + Sentence::CRA(c) => c.write_string(&mut buf), + } + write!(buf, "*{:X}", checksum(buf.as_bytes())).unwrap(); + if carriage_return { + buf.push_str("\r\n"); + } + buf + } +} + +impl From for Sentence { + fn from(gga: Gga) -> Self { + Self::GGA(gga) + } +} + +impl From for Sentence { + fn from(cra: Cra) -> Self { + Self::CRA(cra) + } +} + +impl From for Bytes { + fn from(s: Sentence) -> Self { + Bytes::from(s.to_string(true)) + } +} + +#[derive(Default, Debug, Clone, Copy)] +pub struct Gga { + time: Option>, + lat: Option, + lon: Option, + fix_type: Option, + num_satellites: Option, + hdop: Option, + height: Option, + geoid_height: Option, + age_of_corrections: Option, + station_id: Option, +} + +impl Gga { + pub fn new() -> Self { + Self::default() + } + + pub fn with_time(mut self, time: impl Into>>) -> Self { + self.time = time.into(); + self + } + + pub fn with_lat(mut self, lat: impl Into>) -> Self { + self.lat = lat.into(); + self + } + + pub fn with_lon(mut self, lon: impl Into>) -> Self { + self.lon = lon.into(); + self + } + + pub fn with_fix_type(mut self, fix_type: impl Into>) -> Self { + self.fix_type = fix_type.into(); + self + } + + pub fn with_num_satellites(mut self, num_satellites: impl Into>) -> Self { + self.num_satellites = num_satellites.into(); + self + } + + pub fn with_hdop(mut self, hdop: impl Into>) -> Self { + self.hdop = hdop.into(); + self + } + + pub fn with_height(mut self, height: impl Into>) -> Self { + self.height = height.into(); + self + } + + pub fn with_geoid_height(mut self, geoid_height: impl Into>) -> Self { + self.geoid_height = geoid_height.into(); + self + } + + pub fn with_age_of_corrections(mut self, age_of_corrections: impl Into>) -> Self { + self.age_of_corrections = age_of_corrections.into(); + self + } + + pub fn with_station_id(mut self, station_id: impl Into>) -> Self { + self.station_id = station_id.into(); + self + } + + pub(crate) fn write_string(&self, buf: &mut String) { + buf.push_str("GPGGA,"); + + if let Some(time) = self.time { + write!(buf, "{},", time.format("%H%M%S.00")).unwrap(); + } else { + buf.push(','); + } + + if let Some(lat) = self.lat { + let latn = ((lat * 1e8).round() / 1e8).abs(); + let lat_deg = latn as u16; + let lat_min = (latn - (lat_deg as f64)) * 60.0; + let lat_dir = if lat < 0.0 { 'S' } else { 'N' }; + write!(buf, "{lat_deg:02}{lat_min:010.7},{lat_dir},").unwrap() + } else { + buf.push_str(",,"); + } + + if let Some(lon) = self.lon { + let lonn = ((lon * 1e8).round() / 1e8).abs(); + let lon_deg = lonn as u16; + let lon_min = (lonn - (lon_deg as f64)) * 60.0; + let lon_dir = if lon < 0.0 { 'W' } else { 'E' }; + write!(buf, "{lon_deg:03}{lon_min:010.7},{lon_dir},").unwrap() + } else { + buf.push_str(",,"); + } + + if let Some(fix_type) = self.fix_type { + write!(buf, "{fix_type},").unwrap(); + } else { + buf.push(','); + } + + if let Some(satellites) = self.num_satellites { + write!(buf, "{satellites},").unwrap(); + } else { + buf.push(','); + } + + if let Some(hdop) = self.hdop { + write!(buf, "{hdop:.1},").unwrap(); + } else { + buf.push(','); + } + + if let Some(height) = self.height { + write!(buf, "{height:.2},M,").unwrap(); + } else { + buf.push_str(",M,"); + } + + if let Some(geoid_height) = self.geoid_height { + write!(buf, "{geoid_height:.1},M,").unwrap(); + } else { + buf.push_str(",M,"); + } + + if let Some(age_of_corrections) = self.age_of_corrections { + write!(buf, "{age_of_corrections:.1},").unwrap(); + } else { + buf.push(','); + } + + if let Some(corrections_station_id) = self.station_id { + write!(buf, "{corrections_station_id:04}").unwrap(); + } + } +} + +#[derive(Default, Debug, Clone, Copy)] +pub struct Cra { + request_counter: Option, + area_id: Option, + corrections_mask: Option, + solution_id: Option, +} + +impl Cra { + pub fn new() -> Self { + Self::default() + } + + pub fn with_request_counter(mut self, request_counter: impl Into>) -> Self { + self.request_counter = request_counter.into(); + self + } + + pub fn with_area_id(mut self, area_id: impl Into>) -> Self { + self.area_id = area_id.into(); + self + } + + pub fn with_corrections_mask(mut self, corrections_mask: impl Into>) -> Self { + self.corrections_mask = corrections_mask.into(); + self + } + + pub fn with_solution_id(mut self, solution_id: impl Into>) -> Self { + self.solution_id = solution_id.into(); + self + } + + pub(crate) fn write_string(&self, buf: &mut String) { + buf.push_str("PSWTCRA,"); + + if let Some(request_counter) = self.request_counter { + write!(buf, "{request_counter},").unwrap(); + } else { + buf.push(','); + } + + if let Some(area_id) = self.area_id { + write!(buf, "{area_id},").unwrap(); + } else { + buf.push(','); + } + if let Some(corrections_mask) = self.corrections_mask { + write!(buf, "{corrections_mask},").unwrap(); + } else { + buf.push(','); + } + + if let Some(solution_id) = self.solution_id { + write!(buf, "{solution_id}").unwrap(); + } + } +} + +fn checksum(buf: &[u8]) -> u8 { + let mut sum: u8 = 0; + for c in &buf[1..] { + sum ^= c; + } + sum +} + +#[cfg(test)] +mod tests { + use chrono::TimeZone; + + use super::*; + + #[test] + fn gga() { + let test_data = [ + (Gga::new(), "$GPGGA,,,,,,,,,,M,,M,,*56"), + ( + Gga::new() + + .with_time(Utc.with_ymd_and_hms(2020, 1, 1, 18, 59, 40).unwrap()) + .with_lat(37.77103777) + .with_lon(-122.40316335) + .with_fix_type(5) + .with_num_satellites(10) + .with_hdop(0.9) + .with_height(-8.09) + .with_geoid_height(0.0) + .with_age_of_corrections(1.3) + .with_station_id(0), + "$GPGGA,185940.00,3746.2622662,N,12224.1898010,W,5,10,0.9,-8.09,M,0.0,M,1.3,0000*7D", + ), + ].map(|(gga, expected)| (Sentence::from(gga), expected.to_string())); + + for (gga, expected) in test_data { + assert_eq!(gga.to_string(false), expected); + } + } + + #[test] + fn cra() { + let test_data = [ + (Cra::new(), "$PSWTCRA,,,,*50"), + (Cra::new().with_request_counter(0), "$PSWTCRA,0,,,*60"), + (Cra::new().with_area_id(0), "$PSWTCRA,,0,,*60"), + (Cra::new().with_corrections_mask(0), "$PSWTCRA,,,0,*60"), + (Cra::new().with_solution_id(0), "$PSWTCRA,,,,0*60"), + ( + Cra::new() + .with_area_id(0) + .with_request_counter(0) + .with_corrections_mask(0) + .with_solution_id(0), + "$PSWTCRA,0,0,0,0*50", + ), + ] + .map(|(gga, expected)| (Sentence::from(gga), expected.to_string())); + + for (gga, expected) in test_data { + assert_eq!(gga.to_string(false), expected); + } + } +} diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index bdc6e85..0000000 --- a/src/main.rs +++ /dev/null @@ -1,201 +0,0 @@ -use std::boxed::Box; -use std::cell::RefCell; -use std::error::Error; -use std::io::{self, Write}; -use std::ops::Add; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; - -use chrono::{DateTime, Utc}; -use clap::Parser; -use curl::easy::{Easy, HttpVersion, List, ReadError}; - -#[derive(Debug, Parser)] -#[clap(name = "ntripping", about = "NTRIP command line client.", version = env!("VERGEN_SEMVER_LIGHTWEIGHT"))] -struct Cli { - /// URL of the NTRIP caster - #[clap(long, default_value = "na.skylark.swiftnav.com:2101/CRS")] - url: String, - - /// Receiver latitude to report, in degrees - #[clap(long, default_value_t = 37.77101999622968, allow_hyphen_values = true)] - lat: f64, - - /// Receiver longitude to report, in degrees - #[clap( - long, - default_value_t = -122.40315159140708, - allow_hyphen_values = true - )] - lon: f64, - - /// Receiver height to report, in meters - #[clap(long, default_value_t = -5.549358852471994, allow_hyphen_values = true)] - height: f64, - - /// Client ID - #[clap( - long, - default_value = "00000000-0000-0000-0000-000000000000", - alias = "client" - )] - client_id: String, - - #[clap(short, long)] - verbose: bool, - - /// Receiver time to report, as a Unix time - #[clap(long)] - epoch: Option, - - /// Username credentials - #[clap(long)] - username: Option, - - /// Password credentials - #[clap(long)] - password: Option, - - /// GGA update period, in seconds. 0 means to never send a GGA - #[clap(long, default_value_t = 10)] - gga_period: u64, - - /// Request counter allows correlation between message sent and acknowledgment response from corrections stream - #[clap(long, default_value_t = 0)] - request_counter: u8, - - /// Area ID to be used in generation of CRA message. If this flag is set, ntripping outputs messages of type CRA rather than the default GGA - #[clap(long)] - area_id: Option, - - /// Field specifying which types of corrections are to be received - #[clap(long)] - corrections_mask: Option, - - /// Solution ID, the identifier of the connection stream to reconnect to in the event of disconnections - #[clap(long)] - solution_id: Option, -} - -type Result = std::result::Result>; - -thread_local! { - static CURL: RefCell = RefCell::new(Easy::new()); - static LAST: RefCell = RefCell::new(UNIX_EPOCH); -} - -fn checksum(buf: &[u8]) -> u8 { - let mut sum: u8 = 0; - for c in &buf[1..] { - sum ^= c; - } - sum -} - -fn main() -> Result<()> { - let opt = Cli::parse(); - - let latn = ((opt.lat * 1e8).round() / 1e8).abs(); - let lonn = ((opt.lon * 1e8).round() / 1e8).abs(); - let height = opt.height; - - let lat_deg: u16 = latn as u16; - let lon_deg: u16 = lonn as u16; - - let lat_min: f64 = (latn - (lat_deg as f64)) * 60.0; - let lon_min: f64 = (lonn - (lon_deg as f64)) * 60.0; - - let lat_dir = if opt.lat < 0.0 { 'S' } else { 'N' }; - let lon_dir = if opt.lon < 0.0 { 'W' } else { 'E' }; - - let mut request_counter = opt.request_counter; - - CURL.with(|curl| -> Result<()> { - let mut curl = curl.borrow_mut(); - - let mut headers = List::new(); - let mut client_header = "X-SwiftNav-Client-Id: ".to_string(); - client_header.push_str(&opt.client_id); - - headers.append("Transfer-Encoding:")?; - headers.append("Ntrip-Version: Ntrip/2.0")?; - headers.append(&client_header)?; - - curl.http_headers(headers)?; - curl.useragent("NTRIP ntrip-client/1.0")?; - curl.url(&opt.url)?; - curl.progress(true)?; - curl.put(true)?; - curl.custom_request("GET")?; - curl.http_version(HttpVersion::Any)?; - curl.http_09_allowed(true)?; - - if opt.verbose { - curl.verbose(true)?; - } - - if let Some(username) = &opt.username { - curl.username(username)?; - } - - if let Some(password) = &opt.password { - curl.password(password)?; - } - - curl.write_function(|buf| Ok(io::stdout().write_all(buf).map_or(0, |_| buf.len())))?; - - curl.progress_function(|_dltot, _dlnow, _ultot, _ulnow| { - let now = SystemTime::now(); - let elapsed = LAST.with(|last| { - let dur = now.duration_since(*last.borrow()); - dur.unwrap_or_else(|_| Duration::from_secs(0)).as_secs() - }); - if elapsed > 10 { - CURL.with(|curl| curl.borrow().unpause_read().unwrap()); - } - true - })?; - - curl.read_function(move |mut buf: &mut [u8]| { - let now = if let Some(epoch) = opt.epoch { - SystemTime::UNIX_EPOCH.add(Duration::from_secs(epoch.into())) - } else { - SystemTime::now() - }; - let elapsed = LAST.with(|last| { - let dur = now.duration_since(*last.borrow()); - dur.unwrap_or_else(|_| Duration::from_secs(0)).as_secs() - }); - if opt.gga_period > 0 && elapsed > opt.gga_period { - LAST.with(|last| *last.borrow_mut() = now); - let datetime: DateTime = now.into(); - let time = datetime.format("%H%M%S.00"); - let message = match &opt.area_id { - Some(area_id) => { - let corrections_mask = &opt.corrections_mask.unwrap_or(0); - let solution_id = match &opt.solution_id { - Some(solution_id) => solution_id.to_string(), - None => String::new() - }; - format!("$PSWTCRA,{request_counter},{area_id},{corrections_mask},{solution_id}") - }, - None => { - format!("$GPGGA,{time},{lat_deg:02}{lat_min:010.7},{lat_dir},{lon_deg:03}{lon_min:010.7},{lon_dir},4,12,1.3,{height:.2},M,0.0,M,1.7,0078") - } - }; - request_counter = request_counter.wrapping_add(1); - let checksum = checksum(message.as_bytes()); - let message = format!("{}*{:X}\r\n", message, checksum); - buf.write_all(message.as_bytes()).unwrap(); - Ok(buf.len()) - } else { - Err(ReadError::Pause) - } - })?; - - Ok(()) - })?; - - CURL.with(|curl| -> Result<()> { Ok(curl.borrow().perform()?) })?; - - Ok(()) -} From a2c576d387934bec03266d586bf0d496f2e50c75 Mon Sep 17 00:00:00 2001 From: notoriaga Date: Sat, 10 Dec 2022 14:09:05 -0800 Subject: [PATCH 3/8] cleanup --- ntripping/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ntripping/src/lib.rs b/ntripping/src/lib.rs index b5b9b7b..e2fac7e 100644 --- a/ntripping/src/lib.rs +++ b/ntripping/src/lib.rs @@ -19,9 +19,9 @@ use hyper::{ }; use tokio::net::TcpStream; -pub use error::Error; +use sentence::Sentence; -use crate::sentence::Sentence; +pub use error::Error; #[derive(Debug, Default)] pub struct Client { From f86d18082f5201172a1ed2486a1408ae6773a376 Mon Sep 17 00:00:00 2001 From: notoriaga Date: Sat, 10 Dec 2022 14:25:05 -0800 Subject: [PATCH 4/8] clean up errors --- ntripping/src/error.rs | 42 --------------------------------- ntripping/src/lib.rs | 53 +++++++++++++++++++++++++++++++++++------- 2 files changed, 45 insertions(+), 50 deletions(-) delete mode 100644 ntripping/src/error.rs diff --git a/ntripping/src/error.rs b/ntripping/src/error.rs deleted file mode 100644 index 4f858dc..0000000 --- a/ntripping/src/error.rs +++ /dev/null @@ -1,42 +0,0 @@ -use std::io; - -#[derive(Debug)] -pub enum Error { - InvalidUri(&'static str), - Io(io::Error), - Hyper(hyper::Error), - Http(hyper::http::Error), - BadStatus(hyper::StatusCode), -} - -impl From for Error { - fn from(e: io::Error) -> Self { - Self::Io(e) - } -} - -impl From for Error { - fn from(e: hyper::Error) -> Self { - Self::Hyper(e) - } -} - -impl From for Error { - fn from(e: hyper::http::Error) -> Self { - Self::Http(e) - } -} - -impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::InvalidUri(msg) => write!(f, "invalid uri: {msg}"), - Self::Io(e) => write!(f, "io error: {e}"), - Self::Hyper(e) => write!(f, "hyper error: {e}"), - Self::Http(e) => write!(f, "http error: {e}"), - Self::BadStatus(status) => write!(f, "bad status: {status}"), - } - } -} - -impl std::error::Error for Error {} diff --git a/ntripping/src/lib.rs b/ntripping/src/lib.rs index e2fac7e..c3dac7b 100644 --- a/ntripping/src/lib.rs +++ b/ntripping/src/lib.rs @@ -1,4 +1,3 @@ -mod error; mod header; pub mod sentence; @@ -17,12 +16,10 @@ use hyper::{ client::conn::http1 as conn, Request, StatusCode, Uri, }; -use tokio::net::TcpStream; +use tokio::{io, net::TcpStream}; use sentence::Sentence; -pub use error::Error; - #[derive(Debug, Default)] pub struct Client { auth: Option, @@ -50,7 +47,7 @@ impl Client { self } - pub async fn connect(&self, uri: Uri) -> Result { + pub async fn connect(&self, uri: Uri) -> Result { connect(uri, self.headers()).await } @@ -245,9 +242,9 @@ impl Body for ChannelBody { async fn connect( uri: Uri, headers: impl Iterator, -) -> Result { +) -> Result { let Some(authority) = uri.authority().cloned() else { - return Err(Error::InvalidUri("invalid authority")); + return Err(ConnectionError::InvalidUri("invalid authority")); }; let addr = format!("{}:{}", authority.host(), uri.port_u16().unwrap_or(2101)); @@ -285,7 +282,7 @@ async fn connect( let request = builder.body(ChannelBody { rx })?; let response = sender.send_request(request).await?; if response.status() != StatusCode::OK { - return Err(Error::BadStatus(response.status())); + return Err(ConnectionError::BadStatus(response.status())); } Ok(Connection { send: SendHalf { tx }, @@ -293,6 +290,46 @@ async fn connect( }) } +#[derive(Debug)] +pub enum ConnectionError { + InvalidUri(&'static str), + BadStatus(hyper::StatusCode), + Io(io::Error), + // hyper or http + Http(Box), +} + +impl From for ConnectionError { + fn from(e: io::Error) -> Self { + Self::Io(e) + } +} + +impl From for ConnectionError { + fn from(e: hyper::Error) -> Self { + Self::Http(Box::new(e)) + } +} + +impl From for ConnectionError { + fn from(e: hyper::http::Error) -> Self { + Self::Http(Box::new(e)) + } +} + +impl std::fmt::Display for ConnectionError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::InvalidUri(msg) => write!(f, "invalid uri: {msg}"), + Self::Io(e) => write!(f, "io error: {e}"), + Self::Http(e) => write!(f, "http error: {e}"), + Self::BadStatus(status) => write!(f, "bad status: {status}"), + } + } +} + +impl std::error::Error for ConnectionError {} + #[cfg(test)] mod tests { use crate::sentence::Cra; From 4eca2c2e58283e40110abc1a0765263f8efd4eca Mon Sep 17 00:00:00 2001 From: notoriaga Date: Sat, 10 Dec 2022 14:54:18 -0800 Subject: [PATCH 5/8] more cleanup --- ntripping/src/lib.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ntripping/src/lib.rs b/ntripping/src/lib.rs index c3dac7b..a3d25a1 100644 --- a/ntripping/src/lib.rs +++ b/ntripping/src/lib.rs @@ -140,14 +140,14 @@ where #[derive(Debug)] pub struct ReceiveHalf { - response: hyper::Response, + body: body::Incoming, } impl Stream for ReceiveHalf { type Item = Result; fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - match Pin::new(self.response.body_mut()).poll_frame(cx) { + match Pin::new(&mut self.body).poll_frame(cx) { Poll::Ready(Some(Ok(frame))) => { let chunk = frame.into_data(); if let Some(chunk) = &chunk { @@ -280,13 +280,13 @@ async fn connect( let (tx, rx) = mpsc::channel::(1); let request = builder.body(ChannelBody { rx })?; - let response = sender.send_request(request).await?; - if response.status() != StatusCode::OK { - return Err(ConnectionError::BadStatus(response.status())); + let (parts, body) = sender.send_request(request).await?.into_parts(); + if parts.status != StatusCode::OK { + return Err(ConnectionError::BadStatus(parts.status)); } Ok(Connection { send: SendHalf { tx }, - recv: ReceiveHalf { response }, + recv: ReceiveHalf { body }, }) } From b28f172e3633f29cd1c01ff1a33b150f53501767 Mon Sep 17 00:00:00 2001 From: notoriaga Date: Thu, 15 Dec 2022 13:38:44 -0800 Subject: [PATCH 6/8] comment for lint --- ntripping/src/header.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/ntripping/src/header.rs b/ntripping/src/header.rs index b4c4d7a..7e36985 100644 --- a/ntripping/src/header.rs +++ b/ntripping/src/header.rs @@ -1,3 +1,4 @@ +// https://github.com/rust-lang/rust-clippy/issues/9776 #![allow(clippy::declare_interior_mutable_const)] pub use hyper::http::header::*; From a9ba3d183527c18a7f15d86dbaba59247d84f7e8 Mon Sep 17 00:00:00 2001 From: notoriaga Date: Thu, 15 Dec 2022 15:40:24 -0800 Subject: [PATCH 7/8] change default url --- ntripping-cli/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ntripping-cli/src/main.rs b/ntripping-cli/src/main.rs index 15c3b46..ff342f1 100644 --- a/ntripping-cli/src/main.rs +++ b/ntripping-cli/src/main.rs @@ -19,7 +19,7 @@ use ntripping::{ )] struct Cli { /// URL of the NTRIP caster - #[clap(long, default_value = "na.skylark.swiftnav.com:2101/CRS")] + #[clap(long, default_value = "na.skylark.swiftnav.com:2101")] url: String, /// Receiver latitude to report, in degrees From af4d1217f3b69327e8ec38aad393af06fc1a60c2 Mon Sep 17 00:00:00 2001 From: notoriaga Date: Fri, 16 Dec 2022 12:00:58 -0800 Subject: [PATCH 8/8] log to stderr --- ntripping-cli/src/main.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/ntripping-cli/src/main.rs b/ntripping-cli/src/main.rs index ff342f1..a04da7e 100644 --- a/ntripping-cli/src/main.rs +++ b/ntripping-cli/src/main.rs @@ -174,6 +174,7 @@ fn main() -> Result<()> { 1 => LevelFilter::DEBUG, _ => LevelFilter::TRACE, }) + .with_writer(std::io::stderr) .compact() .init(); tokio::runtime::Builder::new_current_thread()