diff --git a/.gitattributes b/.gitattributes index d379db36b..38d481005 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,4 @@ "console_backend/tests/data/*" filter=lfs diff=lfs merge=lfs -text "pyside-wheels/*.whl" filter=lfs diff=lfs merge=lfs -text installers/Windows/NSIS/*.zip filter=lfs diff=lfs merge=lfs -text +"binaries/**" filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore index 19b41f0fe..593513982 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,5 @@ installers/Windows/*.exe installers/Linux/*.deb installers/Windows/NSIS/* !installers/Windows/NSIS/*.zip +.idea +resources/base diff --git a/Cargo.lock b/Cargo.lock index d1d2148d6..4043e8f62 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -57,9 +57,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "0.7.18" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" dependencies = [ "memchr", ] @@ -70,15 +70,6 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" -[[package]] -name = "ansi_term" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" -dependencies = [ - "winapi", -] - [[package]] name = "anyhow" version = "1.0.57" @@ -151,15 +142,13 @@ checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" [[package]] name = "bindgen" -version = "0.59.2" +version = "0.64.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bd2a9a458e8f4304c52c43ebb0cfbd520289f8379a52e329a38afda99bf8eb8" +checksum = "c4243e6031260db77ede97ad86c27e501d646a27ab57b59a574f725d98ab1fb4" dependencies = [ "bitflags", "cexpr", "clang-sys", - "clap 2.34.0", - "env_logger", "lazy_static", "lazycell", "log", @@ -169,6 +158,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", + "syn", "which", ] @@ -259,9 +249,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.0.72" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" [[package]] name = "cexpr" @@ -327,30 +317,15 @@ dependencies = [ [[package]] name = "clang-sys" -version = "1.3.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa66045b9cb23c2e9c1520732030608b02ee07e5cfaa5a521ec15ded7fa24c90" +checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f" dependencies = [ "glob", "libc", "libloading", ] -[[package]] -name = "clap" -version = "2.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" -dependencies = [ - "ansi_term", - "atty", - "bitflags", - "strsim 0.8.0", - "textwrap 0.11.0", - "unicode-width", - "vec_map", -] - [[package]] name = "clap" version = "3.2.5" @@ -363,9 +338,9 @@ dependencies = [ "clap_lex", "indexmap", "once_cell", - "strsim 0.10.0", + "strsim", "termcolor", - "textwrap 0.15.0", + "textwrap", ] [[package]] @@ -392,9 +367,9 @@ dependencies = [ [[package]] name = "cmake" -version = "0.1.46" +version = "0.1.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7b858541263efe664aead4a5209a4ae5c5d2811167d4ed4ee0944503f8d2089" +checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" dependencies = [ "cc", ] @@ -459,7 +434,7 @@ dependencies = [ "capnp", "capnpc", "chrono", - "clap 3.2.5", + "clap", "criterion", "crossbeam", "csv", @@ -609,7 +584,7 @@ dependencies = [ "atty", "cast", "ciborium", - "clap 3.2.5", + "clap", "criterion-plot", "itertools", "lazy_static", @@ -725,16 +700,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "ctor" -version = "0.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccc0a48a9b826acdf4028595adc9db92caea352f7af011a3034acd172a52a0aa" -dependencies = [ - "quote", - "syn", -] - [[package]] name = "cty" version = "0.2.2" @@ -743,9 +708,9 @@ checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" [[package]] name = "curl" -version = "0.4.43" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37d855aeef205b43f65a5001e0997d81f8efca7badad4fad7d897aa7f0d0651f" +checksum = "509bd11746c7ac09ebd19f0b17782eae80aadee26237658a6b4808afb5c11a22" dependencies = [ "curl-sys", "libc", @@ -758,9 +723,9 @@ dependencies = [ [[package]] name = "curl-sys" -version = "0.4.53+curl-7.82.0" +version = "0.4.61+curl-8.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8092905a5a9502c312f223b2775f57ec5c5b715f9a15ee9d2a8591d1364a0352" +checksum = "14d05c10f541ae6f3bc5b3d923c20001f47db7d5f0b2bc6ad16490133842db79" dependencies = [ "cc", "libc", @@ -792,7 +757,7 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "strsim 0.10.0", + "strsim", "syn", ] @@ -880,9 +845,9 @@ checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" [[package]] name = "either" -version = "1.6.1" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" [[package]] name = "encode_unicode" @@ -892,9 +857,9 @@ checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" [[package]] name = "env_logger" -version = "0.9.0" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" +checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" dependencies = [ "atty", "humantime", @@ -1092,9 +1057,9 @@ checksum = "78cc372d058dcf6d5ecd98510e7fbc9e5aec4d21de70f65fea8fecebcd881bd4" [[package]] name = "glob" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "half" @@ -1288,15 +1253,15 @@ checksum = "7efd1d698db0759e6ef11a7cd44407407399a910c774dd804c64c032da7826ff" [[package]] name = "libc" -version = "0.2.125" +version = "0.2.146" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5916d2ae698f6de9bfb891ad7a8d65c09d232dc58cc4ac433c7da3b2fd84bc2b" +checksum = "f92be4933c13fd498862a9e02a3055f8a8d9c039ce33db97306fd5a6caa7f29b" [[package]] name = "libloading" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afe203d669ec979b7128619bae5a63b7b42e9203c1b29146079ee05e2f604b52" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" dependencies = [ "cfg-if 1.0.0", "winapi", @@ -1360,11 +1325,10 @@ dependencies = [ [[package]] name = "log" -version = "0.4.17" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +checksum = "518ef76f2f87365916b142844c16d8fefd85039bc5699050210a7778ee1cd1de" dependencies = [ - "cfg-if 1.0.0", "value-bag", ] @@ -1416,9 +1380,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "memmap2" @@ -1605,13 +1569,12 @@ dependencies = [ [[package]] name = "nom" -version = "7.1.0" +version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1d11e1ef389c76fe5b81bcaf2ea32cf88b62bc494e19f493d0b30e7a930109" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ "memchr", "minimal-lexical", - "version_check", ] [[package]] @@ -2002,11 +1965,11 @@ checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" [[package]] name = "proc-macro2" -version = "1.0.34" +version = "1.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f84e92c0f7c9d58328b85a78557813e4bd845130db68d7184635344399423b1" +checksum = "dec2b086b7a862cf4de201096214fa870344cf922b2b30c167badb3af3195406" dependencies = [ - "unicode-xid", + "unicode-ident", ] [[package]] @@ -2070,9 +2033,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.10" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" +checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" dependencies = [ "proc-macro2", ] @@ -2178,9 +2141,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.5.6" +version = "1.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1" +checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" dependencies = [ "aho-corasick", "memchr", @@ -2195,9 +2158,9 @@ checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" [[package]] name = "regex-syntax" -version = "0.6.26" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64" +checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" [[package]] name = "remove_dir_all" @@ -2677,12 +2640,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" -[[package]] -name = "strsim" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" - [[package]] name = "strsim" version = "0.10.0" @@ -2723,18 +2680,19 @@ dependencies = [ [[package]] name = "swiftnav" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "596f5dddddeb2895b7d0c81eeb65f1a3d5b64e349c73efa8c3d83e9ead19c5fe" +checksum = "a53e196c909138c6fcc82673d31da225f97a8014030aa5132c1826af94f8c9e4" dependencies = [ + "rustversion", "swiftnav-sys", ] [[package]] name = "swiftnav-sys" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb495c3322f6a34303e872e75bd2dade8d39898bbdd2bb1ddba2c8f560a0539" +checksum = "af6ab0c6a3fdf57ef76186df18cec083d060c532f5f3b0caadf07aecbb1a32fb" dependencies = [ "bindgen", "cmake", @@ -2742,13 +2700,13 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.92" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ff7c592601f11445996a06f8ad0c27f094a58857c2f89e97974ab9235b92c52" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", - "unicode-xid", + "unicode-ident", ] [[package]] @@ -2788,9 +2746,9 @@ dependencies = [ [[package]] name = "termcolor" -version = "1.1.2" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" dependencies = [ "winapi-util", ] @@ -2805,15 +2763,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "textwrap" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" -dependencies = [ - "unicode-width", -] - [[package]] name = "textwrap" version = "0.15.0" @@ -2928,16 +2877,10 @@ dependencies = [ ] [[package]] -name = "unicode-width" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" - -[[package]] -name = "unicode-xid" -version = "0.2.2" +name = "unicode-ident" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" +checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" [[package]] name = "unindent" @@ -2959,13 +2902,9 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "value-bag" -version = "1.0.0-alpha.9" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2209b78d1249f7e6f3293657c9779fe31ced465df091bbd433a1cf88e916ec55" -dependencies = [ - "ctor", - "version_check", -] +checksum = "a4d330786735ea358f3bc09eea4caa098569c1c93f342d9aca0514915022fe7e" [[package]] name = "vcpkg" @@ -2973,12 +2912,6 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" -[[package]] -name = "vec_map" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" - [[package]] name = "version-compare" version = "0.1.0" @@ -2987,9 +2920,9 @@ checksum = "fe88247b92c1df6b6de80ddc290f3976dbdf2f5f5d3fd049a9fb598c6dd5ca73" [[package]] name = "version_check" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "walkdir" @@ -3181,13 +3114,13 @@ checksum = "9c97e489d8f836838d497091de568cf16b117486d529ec5579233521065bd5e4" [[package]] name = "which" -version = "4.2.2" +version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea187a8ef279bc014ec368c27a920da2024d2a711109bfbe3440585d5cf27ad9" +checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" dependencies = [ "either", - "lazy_static", "libc", + "once_cell", ] [[package]] diff --git a/Makefile.toml b/Makefile.toml index 9cdf85571..a7c59771f 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -226,7 +226,7 @@ git describe --tags --always > console_backend/src/version.txt [tasks.install-backend] command = "${PYTHON}" -args = ["-m", "pip", "install", "-e", "./console_backend"] +args = ["-m", "pip", "install", "-v", "-e", "./console_backend"] [tasks.prod-install-backend] command = "${PYTHON}" @@ -343,7 +343,7 @@ script_runner = "@duckscript" script = ''' wget -O imagemagick.tool.7.1.0.nupkg https://github.com/swift-nav/swift-toolchains/releases/download/imagemagick-7.1.0/imagemagick.tool.7.1.0.nupkg exec --fail-on-error choco install -q -y --side-by-side vcredist2010 --version=10.0.40219.32503 -exec --fail-on-error choco install -q -y ./imagemagick.tool.7.1.0.nupkg +exec --fail-on-error choco install imagemagick.tool --version="7.1.0" --source="./" --pre -y -q rm imagemagick.tool.7.1.0.nupkg ''' @@ -455,6 +455,9 @@ args = ["-m", "pip", "install", "console_backend/dist/${BACKEND_WHEEL}"] script_runner = "@duckscript" script = ''' cp src/main/resources py39-dist/ + +os = os_family +glob_cp binaries/${os}/rtcm3tosbp* py39-dist/binaries/${os} ''' [tasks.build-dist-freeze] diff --git a/binaries/linux/rtcm3tosbp b/binaries/linux/rtcm3tosbp new file mode 100755 index 000000000..e90014df4 --- /dev/null +++ b/binaries/linux/rtcm3tosbp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7e6c5a8c70d9a15689037b4b6f98fce0f647caa3b6d4448731951bffe27ce291 +size 809440 diff --git a/binaries/mac/rtcm3tosbp b/binaries/mac/rtcm3tosbp new file mode 100755 index 000000000..707d4e521 --- /dev/null +++ b/binaries/mac/rtcm3tosbp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f31913dd402a2c98f0ea792a1ae4a58608ee98557a38525b80da3fc59b26624a +size 795824 diff --git a/binaries/windows/rtcm3tosbp.exe b/binaries/windows/rtcm3tosbp.exe new file mode 100644 index 000000000..aac10bdb1 --- /dev/null +++ b/binaries/windows/rtcm3tosbp.exe @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c01f35d1c955c418590e2c87d328f54c0c067a0d8af8773968211e7a50031981 +size 814080 diff --git a/console_backend/Cargo.toml b/console_backend/Cargo.toml index 8977ac540..87ce53a11 100644 --- a/console_backend/Cargo.toml +++ b/console_backend/Cargo.toml @@ -48,10 +48,10 @@ indicatif = { version = "0.16", optional = true } [target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies] serialport = { git = "https://github.com/swift-nav/serialport-rs.git" } -curl = { version = "0.4", features = ["ssl", "static-curl"] } +curl = { version = "^0.4.44", features = ["ssl", "static-curl"] } [target.'cfg(target_os = "linux")'.dependencies] -curl = { version = "0.4", features = ["rustls", "static-curl"] } +curl = { version = "^0.4.44", features = ["rustls", "static-curl"] } [target.'cfg(target_os = "windows")'.dependencies] windows = { version = ">=0.24", features = [ diff --git a/console_backend/src/cli_options.rs b/console_backend/src/cli_options.rs index 7b515de56..9345055d0 100644 --- a/console_backend/src/cli_options.rs +++ b/console_backend/src/cli_options.rs @@ -199,6 +199,10 @@ pub struct CliOptions { /// Set the width of the main window. #[clap(long)] pub width: Option, + + /// Enable ntrip client + #[clap(long)] + pub enable_ntrip: bool, } impl CliOptions { diff --git a/console_backend/src/common_constants.rs b/console_backend/src/common_constants.rs index 4e451f8d5..d6eaedaee 100644 --- a/console_backend/src/common_constants.rs +++ b/console_backend/src/common_constants.rs @@ -297,6 +297,8 @@ pub enum Keys { NOTIFICATION, #[strum(serialize = "SOLUTION_LINE")] SOLUTION_LINE, + #[strum(serialize = "NTRIP_DISPLAY")] + NTRIP_DISPLAY, } #[derive(Clone, Debug, Display, EnumString, EnumVariantNames, Eq, Hash, PartialEq)] diff --git a/console_backend/src/connection.rs b/console_backend/src/connection.rs index 4d9d0d75f..b5b1cbaeb 100644 --- a/console_backend/src/connection.rs +++ b/console_backend/src/connection.rs @@ -163,6 +163,7 @@ fn conn_manager_thd( ConnectionState::Connected { conn: conn.clone(), stop_token, + msg_sender: msg_sender.clone(), }, &client_sender, ); diff --git a/console_backend/src/lib.rs b/console_backend/src/lib.rs index 46c317cce..75b0fa4c8 100644 --- a/console_backend/src/lib.rs +++ b/console_backend/src/lib.rs @@ -21,6 +21,8 @@ pub mod formatters; pub mod fusion_status_flags; pub mod log_panel; pub mod main_tab; +pub mod ntrip_output; +pub mod ntrip_tab; pub mod observation_tab; pub mod output; pub mod piksi_tools_constants; @@ -45,6 +47,9 @@ pub mod watch; use std::sync::Mutex; +use crate::client_sender::BoxedClientSender; +use crate::shared_state::SharedState; +use crate::types::MsgSender; use crate::{ advanced_imu_tab::AdvancedImuTab, advanced_magnetometer_tab::AdvancedMagnetometerTab, advanced_networking_tab::AdvancedNetworkingTab, @@ -75,14 +80,14 @@ struct Tabs { pub status_bar: Mutex, pub update: Mutex, pub settings: Option, - pub shared_state: shared_state::SharedState, + pub shared_state: SharedState, } impl Tabs { fn new( - shared_state: shared_state::SharedState, - client_sender: client_sender::BoxedClientSender, - msg_sender: types::MsgSender, + shared_state: SharedState, + client_sender: BoxedClientSender, + msg_sender: MsgSender, ) -> Self { Self { main: MainTab::new(shared_state.clone(), client_sender.clone()).into(), @@ -121,9 +126,9 @@ impl Tabs { } fn with_settings( - shared_state: shared_state::SharedState, - client_sender: client_sender::BoxedClientSender, - msg_sender: types::MsgSender, + shared_state: SharedState, + client_sender: BoxedClientSender, + msg_sender: MsgSender, ) -> Self { let mut tabs = Self::new( shared_state.clone(), diff --git a/console_backend/src/ntrip_output.rs b/console_backend/src/ntrip_output.rs new file mode 100644 index 000000000..292e8f5e3 --- /dev/null +++ b/console_backend/src/ntrip_output.rs @@ -0,0 +1,103 @@ +use crate::ntrip_tab::OutputType; +use crate::types::Result; +use crate::utils::pythonhome_dir; +use anyhow::Context; +use crossbeam::channel::Receiver; +use log::{error, info}; +use std::io::Write; +use std::process::{Command, Stdio}; +use std::{io, thread}; + +pub struct MessageConverter { + in_rx: Receiver>, + output_type: OutputType, +} + +impl MessageConverter { + pub fn new(in_rx: Receiver>, output_type: OutputType) -> Self { + Self { in_rx, output_type } + } + + pub fn start(&mut self, out: W) -> Result<()> { + match self.output_type { + OutputType::RTCM => self.output_rtcm(out), + OutputType::SBP => self.output_sbp(out), + } + } + + /// Just redirects directly to writer + fn output_rtcm(&mut self, mut out: W) -> Result<()> { + let in_rx = self.in_rx.clone(); + thread::spawn(move || loop { + if let Ok(data) = in_rx.recv() { + if let Err(e) = out.write(&data) { + error!("failed to write to device {e}"); + } + } + }); + Ok(()) + } + + /// Runs rtcm3tosbp converter + fn output_sbp(&mut self, mut out: W) -> Result<()> { + let mut child = if cfg!(target_os = "windows") { + let mut cmd = Command::new("cmd"); + let rtcm = pythonhome_dir()? + .join("binaries") + .join("windows") + .join("rtcm3tosbp.exe") + .to_string_lossy() + .to_string(); + info!("running rtcm3tosbp from \"{}\"", rtcm); + cmd.args(["/C", &rtcm]); + cmd + } else if cfg!(target_os = "macos") { + let mut cmd = Command::new("sh"); + let rtcm = pythonhome_dir()? + .join("binaries") + .join("mac") + .join("rtcm3tosbp") + .to_string_lossy() + .to_string(); + info!("running rtcm3tosbp from \"{}\"", rtcm); + cmd.args(["-c", &rtcm]); + cmd + } else { + let mut cmd = Command::new("sh"); + let rtcm = pythonhome_dir()? + .join("binaries") + .join("linux") + .join("rtcm3tosbp") + .to_string_lossy() + .to_string(); + info!("running rtcm3tosbp from \"{}\"", rtcm); + cmd.args(["-c", &rtcm]); + cmd + }; + let mut child = child + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .spawn() + .context("rtcm converter process failed")?; + + let mut child_in = child.stdin.take().context("rtcm3tosbp stdin missing")?; + let mut child_out = child.stdout.take().context("rtcm3tosbp stdout missing")?; + let in_rx = self.in_rx.clone(); + + thread::spawn(move || { + if let Err(e) = io::copy(&mut child_out, &mut out) { + error!("failed to write to device {e}"); + } + }); + thread::spawn(move || { + while let Ok(data) = in_rx.recv() { + if let Err(e) = child_in.write_all(&data) { + error!("failed to write to rtcm3tosbp {e}") + } + } + }); + thread::spawn(move || child.wait()); + Ok(()) + } +} diff --git a/console_backend/src/ntrip_tab.rs b/console_backend/src/ntrip_tab.rs new file mode 100644 index 000000000..fff955583 --- /dev/null +++ b/console_backend/src/ntrip_tab.rs @@ -0,0 +1,399 @@ +use crate::ntrip_output::MessageConverter; +use crate::status_bar::Heartbeat; +use crate::types::{ArcBool, MsgSender, PosLLH}; +use anyhow::Context; +use chrono::{DateTime, Utc}; +use crossbeam::channel; +use crossbeam::channel::Sender; +use crossbeam::channel::TryRecvError; +use curl::easy::{Easy, HttpVersion, List, ReadError}; +use log::error; +use std::cell::RefCell; +use std::io::Write; +use std::rc::Rc; +use std::str::FromStr; +use std::sync::{Arc, Mutex}; +use std::thread::JoinHandle; +use std::time::{Duration, SystemTime}; +use std::{iter, thread}; +use strum_macros::EnumString; + +#[derive(Debug, Default)] +pub struct NtripState { + pub(crate) connected_thd: Option>, + pub(crate) options: NtripOptions, + pub(crate) is_running: ArcBool, + last_data: Arc>, +} + +#[derive(Debug, Default, Copy, Clone)] +struct LastData { + lat: f64, + lon: f64, + alt: f64, +} + +#[derive(Debug, Default, Clone)] +pub enum PositionMode { + #[default] + Dynamic, + Static { + lat: f64, + lon: f64, + alt: f64, + }, +} + +#[derive(Debug, Clone, EnumString)] +pub enum OutputType { + RTCM, + SBP, +} + +#[derive(Debug, Default, Clone)] +pub struct NtripOptions { + pub(crate) url: String, + pub(crate) username: Option, + pub(crate) password: Option, + pub(crate) nmea_period: u64, + pub(crate) pos_mode: PositionMode, + pub(crate) client_id: String, + pub(crate) output_type: Option, +} + +impl NtripOptions { + pub fn new( + url: String, + username: String, + password: String, + pos_mode: Option<(f64, f64, f64)>, + nmea_period: u64, + output_type: &str, + ) -> Self { + let pos_mode = pos_mode + .map(|(lat, lon, alt)| PositionMode::Static { lat, lon, alt }) + .unwrap_or(PositionMode::Dynamic); + + let username = Some(username).filter(|s| !s.is_empty()); + let password = Some(password).filter(|s| !s.is_empty()); + NtripOptions { + url, + username, + password, + pos_mode, + nmea_period, + client_id: "00000000-0000-0000-0000-000000000000".to_string(), + output_type: OutputType::from_str(output_type).ok(), + } + } +} + +#[derive(Debug, Clone, Copy, serde::Deserialize)] +#[serde(rename_all = "lowercase")] +enum Message { + Gga { lat: f64, lon: f64, height: f64 }, +} + +fn build_gga(opts: &NtripOptions, last_data: &Arc>) -> Command { + let (lat, lon, height) = match opts.pos_mode { + PositionMode::Dynamic => { + let guard = last_data.lock().unwrap(); + (guard.lat, guard.lon, guard.alt) + } + PositionMode::Static { lat, lon, alt } => (lat, lon, alt), + }; + Command { + epoch: None, + after: 0, + crc: None, + message: Message::Gga { lat, lon, height }, + } +} + +#[derive(Debug, Clone, Copy, serde::Deserialize)] +struct Command { + #[serde(default = "default_after")] + after: u64, + epoch: Option, + crc: Option, + #[serde(flatten)] + message: Message, +} + +const fn default_after() -> u64 { + 10 +} + +impl Command { + fn to_bytes(self) -> Vec { + self.to_string().into_bytes() + } +} + +impl std::fmt::Display for Command { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let now = self.epoch.map_or_else(SystemTime::now, |e| { + SystemTime::UNIX_EPOCH + Duration::from_secs(e) + }); + let message = self.message.format(now.into()); + let checksum = self.crc.unwrap_or_else(|| checksum(message.as_bytes())); + write!(f, "{message}*{checksum:X}") + } +} + +fn checksum(buf: &[u8]) -> u8 { + let mut sum = 0; + for c in &buf[1..] { + sum ^= c; + } + sum +} + +impl Message { + fn format(&self, time: DateTime) -> String { + match *self { + Message::Gga { lat, lon, height } => { + let time = time.format("%H%M%S.00"); + + let latn = ((lat * 1e8).round() / 1e8).abs(); + let lonn = ((lon * 1e8).round() / 1e8).abs(); + + let lat_deg = latn as u16; + let lon_deg = lonn as u16; + + let lat_min = (latn - (lat_deg as f64)) * 60.0; + let lon_min = (lonn - (lon_deg as f64)) * 60.0; + + let lat_dir = if lat < 0.0 { 'S' } else { 'N' }; + let lon_dir = if lon < 0.0 { 'W' } else { 'E' }; + + 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, height + ) + } + } + } +} + +fn get_commands( + opt: NtripOptions, + last_data: Arc>, +) -> anyhow::Result + Send>> { + if opt.nmea_period == 0 { + return Ok(Box::new(iter::empty())); + } + let first = build_gga(&opt, &last_data); + let rest = iter::repeat(Command { + after: opt.nmea_period, + ..first + }); + Ok(Box::new(iter::once(first).chain(rest))) +} + +#[derive(Default)] +struct Progress { + ul_tot: f64, + dl_tot: f64, + ul_tot_old: f64, + dl_tot_old: f64, +} + +impl Progress { + /// Fetch changed download and modify last changed + pub fn tick_dl(&mut self) -> f64 { + let diff = self.dl_tot - self.dl_tot_old; + self.dl_tot_old = self.dl_tot; + diff + } + + /// Fetch changed upload and modify last changed + pub fn tick_ul(&mut self) -> f64 { + let diff = self.ul_tot - self.ul_tot_old; + self.ul_tot_old = self.ul_tot; + diff + } +} + +fn main( + mut heartbeat: Heartbeat, + opt: NtripOptions, + last_data: Arc>, + is_running: ArcBool, + rtcm_tx: Sender>, +) -> anyhow::Result<()> { + let mut curl = Easy::new(); + let mut headers = List::new(); + headers.append("Transfer-Encoding:")?; + headers.append("Ntrip-Version: Ntrip/2.0")?; + headers.append(&format!("X-SwiftNav-Client-Id: {}", opt.client_id))?; + + let gga = build_gga(&opt, &last_data); + headers.append(&format!("Ntrip-GGA: {gga}"))?; + + 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 let Some(username) = &opt.username { + curl.username(username)?; + } + + if let Some(password) = &opt.password { + curl.password(password)?; + } + let (tx, rx) = channel::bounded::>(1); + let transfer = Rc::new(RefCell::new(curl.transfer())); + + let progress = Arc::new(Mutex::new(Progress::default())); + let progress_clone = progress.clone(); + let progress_thd = thread::spawn({ + let running = is_running.clone(); + move || loop { + { + if !running.get() { + return false; + } + } + { + let mut progress = progress_clone.lock().unwrap(); + heartbeat.set_ntrip_ul(progress.tick_ul()); + heartbeat.set_ntrip_dl(progress.tick_dl()); + } + thread::park_timeout(Duration::from_secs(1)) + } + }); + + transfer.borrow_mut().progress_function({ + let rx = ℞ + let transfer = Rc::clone(&transfer); + move |_dlnow, dltot, _ulnow, ultot| { + { + if !is_running.get() { + return false; + } + } + { + let mut progress = progress.lock().unwrap(); + progress.ul_tot = ultot; + progress.dl_tot = dltot; + } + if !rx.is_empty() { + if let Err(e) = transfer.borrow().unpause_read() { + error!("ntrip unpause error: {e}"); + return false; + } + } + true + } + })?; + + transfer.borrow_mut().write_function(move |data| { + if let Err(e) = rtcm_tx.send(data.to_owned()) { + error!("ntrip write error: {e}"); + return Ok(0); + } + Ok(data.len()) + })?; + transfer.borrow_mut().read_function(|mut data: &mut [u8]| { + let mut bytes = match rx.try_recv() { + Ok(bytes) => bytes, + Err(TryRecvError::Empty) => return Err(ReadError::Pause), + Err(TryRecvError::Disconnected) => return Err(ReadError::Abort), + }; + bytes.extend_from_slice(b"\r\n"); + if let Err(e) = data.write_all(&bytes) { + error!("ntrip read error: {e}"); + return Err(ReadError::Abort); + } + Ok(bytes.len()) + })?; + + let commands = get_commands(opt.clone(), last_data)?; + let handle = thread::spawn(move || { + for cmd in commands { + if cmd.after > 0 { + // need to unpark thread (?) + thread::park_timeout(Duration::from_secs(cmd.after)); + } + if tx.send(cmd.to_bytes()).is_err() { + break; + } + } + Ok(()) + }); + + transfer + .borrow() + .perform() + .context("ntrip curl perform errored")?; + if !handle.is_finished() { + Ok(()) + } else { + // an error stopped the thread early + progress_thd.join().expect("could not join progress thread"); + handle.join().expect("could not join on handle thread") + } +} + +impl NtripState { + pub fn connect( + &mut self, + msg_sender: MsgSender, + mut heartbeat: Heartbeat, + options: NtripOptions, + ) { + if self.connected_thd.is_some() && heartbeat.get_ntrip_connected() { + // is already connected + return; + } + + self.options = options.clone(); + let last_data = self.last_data.clone(); + self.is_running.set(true); + heartbeat.set_ntrip_connected(true); + let thd = thread::spawn({ + let running = self.is_running.clone(); + move || { + let (conv_tx, conv_rx) = channel::unbounded::>(); + let output_type = options.output_type.clone().unwrap_or(OutputType::RTCM); + let mut output_converter = MessageConverter::new(conv_rx, output_type); + if let Err(e) = output_converter.start(msg_sender).and(main( + heartbeat.clone(), + options, + last_data, + running.clone(), + conv_tx, + )) { + error!("{e}"); + } + running.set(false); + heartbeat.set_ntrip_connected(false); + } + }); + + self.connected_thd = Some(thd); + } + + pub fn disconnect(&mut self) { + self.is_running.set(false); + if let Some(thd) = self.connected_thd.take() { + let _ = thd.join(); + } + } + + /// Update data required for dynamic mode. + /// Currently used for position data, potentially epoch in the future. + pub fn set_last_data(&mut self, val: PosLLH) { + let fields = val.fields(); + let mut guard = self.last_data.lock().unwrap(); + guard.lat = fields.lat; + guard.lon = fields.lon; + guard.alt = fields.height; + } +} diff --git a/console_backend/src/process_messages.rs b/console_backend/src/process_messages.rs index 2b5a5432c..b67a59fc3 100644 --- a/console_backend/src/process_messages.rs +++ b/console_backend/src/process_messages.rs @@ -219,7 +219,12 @@ fn register_events(link: sbp::link::Link) { }); link.register(|tabs: &Tabs, msg: PosLLH| { tabs.solution.lock().unwrap().handle_pos_llh(msg.clone()); - tabs.status_bar.lock().unwrap().handle_pos_llh(msg); + tabs.status_bar.lock().unwrap().handle_pos_llh(msg.clone()); + + // ntrip tab dynamic position + let mut guard = tabs.shared_state.lock(); + let ntrip = &mut guard.ntrip_tab; + ntrip.set_last_data(msg); }); link.register(|tabs: &Tabs, msg: MsgPosLlhCov| { tabs.solution.lock().unwrap().handle_pos_llh_cov(msg); diff --git a/console_backend/src/server_recv_thread.rs b/console_backend/src/server_recv_thread.rs index 56d680cfb..65850a9e1 100644 --- a/console_backend/src/server_recv_thread.rs +++ b/console_backend/src/server_recv_thread.rs @@ -14,9 +14,10 @@ use crate::errors::{ SOLUTION_POSITION_UNIT_SELECTION_NOT_AVAILABLE, }; use crate::log_panel::LogLevel; +use crate::ntrip_tab::NtripOptions; use crate::output::CsvLogging; use crate::settings_tab; -use crate::shared_state::{AdvancedNetworkingState, SharedState}; +use crate::shared_state::{AdvancedNetworkingState, ConnectionState, SharedState}; use crate::solution_tab::LatLonUnits; use crate::types::{FlowControl, RealtimeDelay}; use crate::update_tab::UpdateTabUpdate; @@ -322,6 +323,54 @@ pub fn server_recv_thread( m::message::ConfirmInsChange(Ok(_)) => { shared_state.set_settings_confirm_ins_change(true); } + m::message::NtripConnect(Ok(cv_in)) => { + let url = cv_in + .get_url() + .expect(CAP_N_PROTO_DESERIALIZATION_FAILURE) + .to_string(); + let usr = cv_in + .get_username() + .expect(CAP_N_PROTO_DESERIALIZATION_FAILURE) + .to_string(); + let pwd = cv_in + .get_password() + .expect(CAP_N_PROTO_DESERIALIZATION_FAILURE) + .to_string(); + let gga_period = cv_in.get_gga_period(); + let output_type = cv_in + .get_output_type() + .expect(CAP_N_PROTO_DESERIALIZATION_FAILURE) + .to_string(); + let position: Option<(f64, f64, f64)> = match cv_in.get_position().which() { + Ok(m::ntrip_connect::position::Pos(Ok(pos))) => { + Some((pos.get_lat(), pos.get_lon(), pos.get_alt())) + } + Err(e) => { + error!("{}", e); + None + } + _ => None, + }; + let mut guard = shared_state.lock(); + let heartbeat = guard.heartbeat_data.clone(); + match guard.conn.get() { + ConnectionState::Connected { msg_sender, .. } => { + let options = NtripOptions::new( + url, + usr, + pwd, + position, + gga_period, + &output_type, + ); + guard.ntrip_tab.connect(msg_sender, heartbeat, options); + } + _ => error!("ntrip unable to find connected device"), + } + } + m::message::NtripDisconnect(Ok(_)) => { + shared_state.lock().ntrip_tab.disconnect(); + } _ => { error!("unknown message from front-end"); } diff --git a/console_backend/src/shared_state.rs b/console_backend/src/shared_state.rs index 5c2d55199..a79b2b1e2 100644 --- a/console_backend/src/shared_state.rs +++ b/console_backend/src/shared_state.rs @@ -11,6 +11,7 @@ use std::{ }; use anyhow::{Context, Result as AHResult}; + use chrono::{DateTime, Utc}; use crossbeam::channel::Sender; use directories::{ProjectDirs, UserDirs}; @@ -28,10 +29,12 @@ use crate::constants::{ }; use crate::errors::CONVERT_TO_STR_FAILURE; use crate::log_panel::LogLevel; +use crate::ntrip_tab::NtripState; use crate::output::{CsvLogging, CsvSerializer}; use crate::process_messages::StopToken; use crate::settings_tab; use crate::solution_tab::LatLonUnits; +use crate::types::MsgSender; use crate::update_tab::UpdateTabUpdate; use crate::utils::send_conn_state; use crate::watch::{WatchReceiver, Watched}; @@ -333,6 +336,13 @@ impl SharedState { pub fn heartbeat_data(&self) -> Heartbeat { self.lock().heartbeat_data.clone() } + + pub fn msg_sender(&self) -> Option { + match self.connection() { + ConnectionState::Connected { msg_sender, .. } => Some(msg_sender), + _ => None, + } + } } impl Deref for SharedState { @@ -358,6 +368,7 @@ impl Clone for SharedState { pub struct SharedStateInner { pub(crate) logging_bar: LoggingBarState, pub(crate) log_panel: LogPanelState, + pub(crate) ntrip_tab: NtripState, pub(crate) tracking_tab: TrackingTabState, pub(crate) connection_history: ConnectionHistory, pub(crate) conn: Watched, @@ -384,6 +395,7 @@ impl SharedStateInner { SharedStateInner { logging_bar: LoggingBarState::new(log_directory), log_panel: LogPanelState::new(), + ntrip_tab: NtripState::default(), tracking_tab: TrackingTabState::new(), debug: false, connection_history, @@ -883,6 +895,7 @@ pub enum ConnectionState { Connected { conn: Connection, stop_token: StopToken, + msg_sender: MsgSender, }, /// Attempting to connect diff --git a/console_backend/src/status_bar.rs b/console_backend/src/status_bar.rs index b4141f5aa..75dd91097 100644 --- a/console_backend/src/status_bar.rs +++ b/console_backend/src/status_bar.rs @@ -63,6 +63,9 @@ pub struct StatusBarUpdate { ant_status: String, port: String, version: String, + ntrip_connected: bool, + ntrip_upload_bytes: f64, + ntrip_download_bytes: f64, } impl StatusBarUpdate { pub fn new() -> StatusBarUpdate { @@ -77,6 +80,9 @@ impl StatusBarUpdate { ant_status: String::from(EMPTY_STR), port: String::from(""), version: String::from(""), + ntrip_connected: false, + ntrip_upload_bytes: 0.0, + ntrip_download_bytes: 0.0, } } } @@ -172,6 +178,9 @@ impl StatusBar { sb_update.port + " - " }; status_bar_status.set_title(&format!("{}Swift Console {}", port, sb_update.version)); + status_bar_status.set_ntrip_upload(sb_update.ntrip_upload_bytes); + status_bar_status.set_ntrip_download(sb_update.ntrip_download_bytes); + status_bar_status.set_ntrip_connected(sb_update.ntrip_connected); client_sender.send_data(serialize_capnproto_builder(builder)); } @@ -318,6 +327,9 @@ pub struct HeartbeatInner { last_bytes_read: usize, last_time_bytes_read: Instant, version: String, + ntrip_connected: bool, + ntrip_upload_bytes: f64, + ntrip_download_bytes: f64, } impl HeartbeatInner { pub fn new() -> HeartbeatInner { @@ -351,6 +363,9 @@ impl HeartbeatInner { last_bytes_read: 0, last_time_bytes_read: Instant::now(), version: String::from(""), + ntrip_download_bytes: 0.0, + ntrip_upload_bytes: 0.0, + ntrip_connected: false, } } @@ -488,6 +503,9 @@ impl HeartbeatInner { solid_connection: self.solid_connection, port: self.port.clone(), version: self.version.clone(), + ntrip_connected: self.ntrip_connected, + ntrip_upload_bytes: self.ntrip_upload_bytes, + ntrip_download_bytes: self.ntrip_download_bytes, } } else { let packet = StatusBarUpdate { @@ -495,6 +513,9 @@ impl HeartbeatInner { ant_status: self.ant_status.clone(), num_sats: self.llh_num_sats, version: self.version.clone(), + ntrip_connected: self.ntrip_connected, + ntrip_upload_bytes: self.ntrip_upload_bytes, + ntrip_download_bytes: self.ntrip_download_bytes, ..Default::default() }; self.llh_num_sats = 0; @@ -546,6 +567,26 @@ impl Heartbeat { pub fn reset(&mut self) { *self.lock().expect(HEARTBEAT_LOCK_MUTEX_FAILURE) = HeartbeatInner::new(); } + pub fn set_ntrip_ul(&mut self, ul: f64) { + self.lock() + .expect(HEARTBEAT_LOCK_MUTEX_FAILURE) + .ntrip_upload_bytes = ul + } + pub fn set_ntrip_dl(&mut self, dl: f64) { + self.lock() + .expect(HEARTBEAT_LOCK_MUTEX_FAILURE) + .ntrip_download_bytes = dl + } + pub fn set_ntrip_connected(&mut self, connected: bool) { + self.lock() + .expect(HEARTBEAT_LOCK_MUTEX_FAILURE) + .ntrip_connected = connected + } + pub fn get_ntrip_connected(&self) -> bool { + self.lock() + .expect(HEARTBEAT_LOCK_MUTEX_FAILURE) + .ntrip_connected + } } impl Deref for Heartbeat { diff --git a/console_backend/src/types.rs b/console_backend/src/types.rs index 2beee375b..519a9839b 100644 --- a/console_backend/src/types.rs +++ b/console_backend/src/types.rs @@ -17,6 +17,7 @@ use crate::piksi_tools_constants::{ }; use crate::utils::{mm_to_m, ms_to_sec}; + use anyhow::Context; use chrono::{DateTime, Utc}; use ordered_float::OrderedFloat; @@ -33,9 +34,10 @@ use sbp::messages::{ piksi::{Latency, MsgSpecan, MsgSpecanDep, MsgUartState, MsgUartStateDepa, Period}, ConcreteMessage, }; -use sbp::{Sbp, SbpEncoder, SbpMessage}; +use sbp::{Sbp, SbpMessage}; use serialport::FlowControl as SPFlowControl; -use std::io; +use std::fmt::Formatter; +use std::io::Write; use std::{ cmp::{Eq, PartialEq}, collections::HashMap, @@ -49,13 +51,20 @@ use std::{ Arc, Mutex, }, }; + pub type Error = anyhow::Error; pub type Result = anyhow::Result; pub type UtcDateTime = DateTime; -/// Sends Sbp messages to the connected device +/// Sends messages to the connected device pub struct MsgSender { - inner: Arc>>>, + inner: Arc>>, +} + +impl Debug for MsgSender { + fn fmt(&self, _f: &mut Formatter<'_>) -> fmt::Result { + Ok(()) + } } impl MsgSender { @@ -63,23 +72,30 @@ impl MsgSender { const SENDER_ID: u16 = 42; const LOCK_FAILURE: &'static str = "failed to aquire sender lock"; - pub fn new(writer: W) -> Self - where - W: io::Write + Send + 'static, - { + pub fn new(writer: W) -> Self { Self { - inner: Arc::new(Mutex::new(SbpEncoder::new(Box::new(writer)))), + inner: Arc::new(Mutex::new(Box::new(writer))), } } + /// Send SBP Message pub fn send(&self, msg: impl Into) -> Result<()> { let mut msg = msg.into(); if msg.sender_id().is_none() { msg.set_sender_id(Self::SENDER_ID); } let mut framed = self.inner.lock().expect(Self::LOCK_FAILURE); - framed.send(&msg).context("while sending a message")?; - Ok(()) + Ok(framed.write_all(&sbp::to_vec(&msg).context("while serializing into bytes")?)?) + } +} + +impl Write for MsgSender { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.inner.lock().expect(MsgSender::LOCK_FAILURE).write(buf) + } + + fn flush(&mut self) -> std::io::Result<()> { + self.inner.lock().expect(MsgSender::LOCK_FAILURE).flush() } } diff --git a/console_backend/src/utils.rs b/console_backend/src/utils.rs index 1b8c90f70..0a39f342b 100644 --- a/console_backend/src/utils.rs +++ b/console_backend/src/utils.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; use std::ops::Index; +use std::path::{Path, PathBuf}; use capnp::message::Builder; use capnp::message::HeapAllocator; @@ -16,6 +17,46 @@ use crate::errors::*; use crate::shared_state::{ConnectionState, SerialConfig, SharedState}; use crate::types::SignalCodes; +pub fn app_dir() -> crate::types::Result { + std::env::current_exe()? + .parent() + .ok_or(anyhow::format_err!("no parent directory")) + .map(Path::to_path_buf) +} + +/// Returns directory to packaged python, or workspace when ran locally in dev environment +/// This is used to locate rtcm3tosbp +pub fn pythonhome_dir() -> crate::types::Result { + let app_dir = app_dir()?; + // If dev environment, hard code check to py39 path "${WORKSPACE}\\py39" + // Mac and Linux both share python3 in "${WORKSPACE}/py39/bin" + let py39 = if cfg!(target_os = "windows") { + Some(app_dir.as_path()) + } else { + app_dir.parent() + }; + if let Some(py39) = py39 { + // if we are in the "${WORKSPACE}/py39" directory, + // we are in dev environment, move up one folder. + if py39.file_name().filter(|&x| x.eq("py39")).is_some() { + let workspace = py39 + .parent() + .ok_or(anyhow::format_err!("no workspace found?")); + return workspace.map(Path::to_path_buf); + } + // if compiled on mac, exe should be in "Swift Console.app/MacOS/Swift Console" + // app_dir gives "Swift Console.app/MacOS" + // returns "Swift Console.app/Resources/lib" + if cfg!(target_os = "macos") { + let resources = py39.join("Resources/lib"); + if resources.exists() { + return Ok(py39.join("Resources")); + } + } + } + Ok(app_dir) +} + /// Create a new SbpString of L size with T termination. pub fn fixed_sbp_string(data: &str) -> SbpString<[u8; L], T> { let mut arr = [0u8; L]; diff --git a/resources/AdvancedTab.qml b/resources/AdvancedTab.qml index 5f410edea..07e3441e8 100644 --- a/resources/AdvancedTab.qml +++ b/resources/AdvancedTab.qml @@ -8,7 +8,7 @@ import QtQuick.Layouts 1.15 MainTab { id: advancedTab - subTabNames: ["System Monitor", "IMU", "Magnetometer", "Networking", "Spectrum Analyzer", "INS"] + subTabNames: Globals.enableNtrip ? ["System Monitor", "IMU", "Magnetometer", "Networking", "Spectrum Analyzer", "INS", "NTRIP"] : ["System Monitor", "IMU", "Magnetometer", "Networking", "Spectrum Analyzer", "INS"] curSubTabIndex: 0 StackLayout { @@ -35,6 +35,9 @@ MainTab { AdvancedTabComponents.AdvancedInsTab { } + AdvancedTabComponents.NtripClientTab { + } + } } diff --git a/resources/AdvancedTabComponents/NtripClientTab.qml b/resources/AdvancedTabComponents/NtripClientTab.qml new file mode 100644 index 000000000..8ba17b9a8 --- /dev/null +++ b/resources/AdvancedTabComponents/NtripClientTab.qml @@ -0,0 +1,249 @@ +import "../BaseComponents" +import "../Constants" +import QtCharts 2.15 +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import SwiftConsole 1.0 + +Item { + id: ntripClientTab + + property bool connected: false + property var floatValidator + property var intValidator + property var stringValidator + + RowLayout { + anchors.fill: parent + + ColumnLayout { + Repeater { + id: generalRepeater + + model: ["Url", "Username", "Password", "GGA Period"] + + RowLayout { + height: 30 + + Label { + text: modelData + ": " + Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft + } + + TextField { + width: 400 + Layout.fillWidth: true + text: { + if (modelData == "Url") + return "na.skylark.swiftnav.com:2101"; + + if (modelData == "GGA Period") + return "10"; + + return ""; + } + placeholderText: modelData + font.family: Constants.genericTable.fontFamily + font.pixelSize: Constants.largePixelSize + selectByMouse: true + Layout.alignment: Qt.AlignVCenter | Qt.AlignRight + validator: { + if (modelData == "GGA Period") + return intValidator; + + return stringValidator; + } + readOnly: connected + } + + } + + } + + Repeater { + id: positionRepeater + + model: ["Lat", "Lon", "Alt"] + + RowLayout { + height: 30 + visible: staticRadio.checked + + Label { + text: modelData + ": " + Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft + } + + TextField { + id: textField + + width: 400 + Layout.fillWidth: true + placeholderText: modelData + font.family: Constants.genericTable.fontFamily + font.pixelSize: Constants.largePixelSize + selectByMouse: true + Layout.alignment: Qt.AlignVCenter | Qt.AlignRight + validator: floatValidator + text: { + if (modelData == "Lat") + return "37.77101999622968"; + + if (modelData == "Lon") + return "-122.40315159140708"; + + if (modelData == "Alt") + return "-5.549358852471994"; + + return ""; + } + readOnly: connected + } + + } + + } + + } + + ColumnLayout { + RadioButton { + checked: true + text: "Dynamic" + ToolTip.visible: hovered + ToolTip.text: "Allow automatically fetching position from device" + enabled: !connected + } + + RadioButton { + id: staticRadio + + text: "Static" + ToolTip.visible: hovered + ToolTip.text: "Allow user input position" + enabled: !connected + } + + ComboBox { + id: outputType + + editable: false + + model: ListModel { + ListElement { + text: "RTCM" + } + + ListElement { + text: "SBP" + } + + } + + } + + } + + ColumnLayout { + Label { + id: inputErrorLabel + + visible: false + text: "" + font.family: Constants.genericTable.fontFamily + font.pixelSize: Constants.largePixelSize + color: "red" + } + + RowLayout { + SwiftButton { + invertColor: true + icon.width: 10 + icon.height: 10 + icon.source: Constants.icons.playPath + icon.color: Constants.materialGrey + ToolTip.visible: hovered + ToolTip.text: "Start" + enabled: !connected + onClicked: { + let url = generalRepeater.itemAt(0).children[1].text; + if (!url) { + inputErrorLabel.text = "URL is not provided!"; + inputErrorLabel.visible = true; + return ; + } + let username = generalRepeater.itemAt(1).children[1].text; + let password = generalRepeater.itemAt(2).children[1].text; + let ggaPeriod = generalRepeater.itemAt(3).children[1].text; + if (!ggaPeriod) { + inputErrorLabel.text = "GGA Period is not provided!"; + inputErrorLabel.visible = true; + return ; + } + let lat = null; + let lon = null; + let alt = null; + if (staticRadio.checked) { + lat = positionRepeater.itemAt(0).children[1].text; + lon = positionRepeater.itemAt(1).children[1].text; + alt = positionRepeater.itemAt(2).children[1].text; + if (!lat || !lon || !alt) { + inputErrorLabel.text = "Position missing!"; + inputErrorLabel.visible = true; + return ; + } + } + let output_type = outputType.currentText; + backend_request_broker.ntrip_connect(url, username, password, ggaPeriod, lat, lon, alt, output_type); + connected = true; + inputErrorLabel.visible = false; + } + } + + SwiftButton { + invertColor: true + icon.width: 10 + icon.height: 10 + icon.source: Constants.icons.pauseButtonUrl + icon.color: Constants.materialGrey + ToolTip.visible: hovered + ToolTip.text: "Stop" + enabled: connected + onClicked: { + backend_request_broker.ntrip_disconnect(); + connected = false; + inputErrorLabel.visible = false; + } + } + + } + + } + + } + + NtripStatusData { + id: ntripStatusData + + signal ntrip_connected(bool connected) + + function setConnection(connected) { + ntripClientTab.connected = connected; + } + + Component.onCompleted: { + ntripStatusData.ntrip_connected.connect(setConnection); + } + } + + floatValidator: DoubleValidator { + } + + intValidator: IntValidator { + } + + stringValidator: RegExpValidator { + } + +} diff --git a/resources/Constants/Constants.qml b/resources/Constants/Constants.qml index b83effbb1..6a2c97fba 100644 --- a/resources/Constants/Constants.qml +++ b/resources/Constants/Constants.qml @@ -293,6 +293,7 @@ QtObject { readonly property string corrAgeLabel: "Correction Age:" readonly property string insLabel: "INS:" readonly property string antennaLabel: "Antenna:" + readonly property string ntripLabel: "Ntrip:" readonly property string defaultValue: "--" } diff --git a/resources/Constants/Globals.qml b/resources/Constants/Globals.qml index b0499719b..c799a1f73 100644 --- a/resources/Constants/Globals.qml +++ b/resources/Constants/Globals.qml @@ -7,6 +7,7 @@ QtObject { property bool useOpenGL: false property bool useAntiAliasing: true property bool showPrompts: true + property bool enableNtrip: false property int initialMainTabIndex: 0 // Tracking property int initialSubTabIndex: 0 // Signals property bool showCsvLog: false diff --git a/resources/StatusBar.qml b/resources/StatusBar.qml index 2096ebaef..6bcd2419e 100644 --- a/resources/StatusBar.qml +++ b/resources/StatusBar.qml @@ -15,6 +15,7 @@ Rectangle { property real dataRate: 0 property bool solidConnection: false property string title: "" + property string ntrip: "off" property int verticalPadding: Constants.statusBar.verticalPadding color: Constants.swiftOrange @@ -53,12 +54,16 @@ Rectangle { }, { "labelText": Constants.statusBar.antennaLabel, "valueText": antennaStatus + }, { + "labelText": Constants.statusBar.ntripLabel, + "valueText": ntrip }] RowLayout { spacing: Constants.statusBar.keyValueSpacing Label { + visible: modelData.valueText topPadding: Constants.statusBar.verticalPadding bottomPadding: Constants.statusBar.verticalPadding text: modelData.labelText @@ -69,6 +74,7 @@ Rectangle { Label { id: statusBarPos + visible: modelData.valueText Layout.minimumWidth: Constants.statusBar.valueMinimumWidth topPadding: Constants.statusBar.verticalPadding bottomPadding: Constants.statusBar.verticalPadding @@ -100,6 +106,7 @@ Rectangle { dataRate = statusBarData.data_rate; solidConnection = statusBarData.solid_connection; title = statusBarData.title; + ntrip = statusBarData.ntrip_display; } } } diff --git a/resources/console_resources.qrc b/resources/console_resources.qrc index 4cf148dc6..2eeb33caa 100644 --- a/resources/console_resources.qrc +++ b/resources/console_resources.qrc @@ -43,6 +43,7 @@ AdvancedTabComponents/UnknownStatus.qml AdvancedTabComponents/WarningStatus.qml AdvancedTabComponents/OkStatus.qml + AdvancedTabComponents/NtripClientTab.qml BaselineTab.qml BaselineTabComponents/BaselinePlot.qml SettingsTab.qml diff --git a/src/main/resources/base/console_backend.capnp b/src/main/resources/base/console_backend.capnp index fef5df80d..846b22ffd 100644 --- a/src/main/resources/base/console_backend.capnp +++ b/src/main/resources/base/console_backend.capnp @@ -181,6 +181,9 @@ struct StatusBarStatus { dataRate @6: Float64; solidConnection @7: Bool; title @8: Text; + ntripConnected @9: Bool; + ntripDownload @10: Float64; + ntripUpload @11: Float64; } struct BaselinePlotStatus { @@ -452,6 +455,28 @@ struct AutoSurveyRequest { request @0 :Void = void; } +struct Position { + lat @0: Float64; + lon @1: Float64; + alt @2: Float64; +} + +struct NtripConnect { + url @0 :Text; + username @1 :Text; + password @2 :Text; + ggaPeriod @3 :UInt64; + position :union { + pos @4 :Position; + none @5 :Void; + } + outputType @6: Text; +} + +struct NtripDisconnect { + +} + struct Message { union { solutionVelocityStatus @0 :SolutionVelocityStatus; @@ -509,5 +534,7 @@ struct Message { connectionNotification @52 : ConnectionNotification; settingsNotification @53 : SettingsNotification; connectionDialogStatus @54 :ConnectionDialogStatus; + ntripConnect @55 :NtripConnect; + ntripDisconnect @56 :NtripDisconnect; } } diff --git a/swiftnav_console/backend_request_broker.py b/swiftnav_console/backend_request_broker.py index 64a53bd8c..064b45db9 100644 --- a/swiftnav_console/backend_request_broker.py +++ b/swiftnav_console/backend_request_broker.py @@ -299,3 +299,40 @@ def auto_survey_request(self) -> None: msg.autoSurveyRequest = msg.init(Message.Union.AutoSurveyRequest) buffer = msg.to_bytes() self.endpoint.send_message(buffer) + + @Slot(str, str, str, int, QTKeys.QVARIANT, QTKeys.QVARIANT, QTKeys.QVARIANT, str) # type: ignore + def ntrip_connect( + self, + url: str, + username: str, + password: str, + gga_period: int, + lat: Optional[float], + lon: Optional[float], + alt: Optional[float], + output_type: str, + ) -> None: + Message = self.messages.Message + msg = self.messages.Message() + msg.ntripConnect = msg.init(Message.Union.NtripConnect) + msg.ntripConnect.url = url + msg.ntripConnect.username = username + msg.ntripConnect.password = password + msg.ntripConnect.ggaPeriod = gga_period + msg.ntripConnect.outputType = output_type + if lat is not None and lon is not None and alt is not None: + msg.ntripConnect.position.pos.lat = float(lat) + msg.ntripConnect.position.pos.lon = float(lon) + msg.ntripConnect.position.pos.alt = float(alt) + else: + msg.ntripConnect.position.none = None + buffer = msg.to_bytes() + self.endpoint.send_message(buffer) + + @Slot() # type: ignore + def ntrip_disconnect(self): + Message = self.messages.Message + msg = self.messages.Message() + msg.ntripDisconnect = msg.init(Message.Union.NtripDisconnect) + buffer = msg.to_bytes() + self.endpoint.send_message(buffer) diff --git a/swiftnav_console/constants.py b/swiftnav_console/constants.py index 037a240c0..984d72110 100644 --- a/swiftnav_console/constants.py +++ b/swiftnav_console/constants.py @@ -141,6 +141,7 @@ class Keys(str, Enum): CONNECTION_MESSAGE = "CONNECTION_MESSAGE" NOTIFICATION = "NOTIFICATION" SOLUTION_LINE = "SOLUTION_LINE" + NTRIP_DISPLAY = "NTRIP_DISPLAY" class ConnectionState(str, Enum): diff --git a/swiftnav_console/main.py b/swiftnav_console/main.py index e8a443dc3..d8073486b 100644 --- a/swiftnav_console/main.py +++ b/swiftnav_console/main.py @@ -33,6 +33,8 @@ from .backend_request_broker import BackendRequestBroker +from .ntrip_status import NtripStatusData + from .log_panel import ( log_panel_update, LogPanelData, @@ -225,7 +227,6 @@ }, } - capnp.remove_import_hook() # pylint: disable=no-member @@ -474,6 +475,23 @@ def _process_message_buffer(self, buffer): data[Keys.SOLID_CONNECTION] = m.statusBarStatus.solidConnection data[Keys.TITLE] = m.statusBarStatus.title data[Keys.ANTENNA_STATUS] = m.statusBarStatus.antennaStatus + + up = m.statusBarStatus.ntripUpload + down = m.statusBarStatus.ntripDownload + down_units = "B/s" + + if down >= 1000: + down /= 1000 + down = round(down, 1) + down_units = "KB/s" + + connected = m.statusBarStatus.ntripConnected + if connected: + data[Keys.NTRIP_DISPLAY] = f"{up}B/s ⬆ {down}{down_units} ⬇" + NtripStatusData.post_connected(True) + else: + data[Keys.NTRIP_DISPLAY] = "" + NtripStatusData.post_connected(False) StatusBarData.post_data_update(data) elif m.which == Message.Union.ConnectionStatus: data = connection_update() @@ -640,6 +658,8 @@ def handle_cli_arguments(args: argparse.Namespace, globals_: QObject): globals_.setProperty("width", args.width) # type: ignore if args.show_file_connection: globals_.setProperty("showFileConnection", True) # type: ignore + if args.enable_ntrip: + globals_.setProperty("enableNtrip", True) # type: ignore def start_splash_linux(): @@ -699,6 +719,7 @@ def main(passed_args: Optional[Tuple[str, ...]] = None) -> int: parser.add_argument("--show-csv-log", action="store_true") parser.add_argument("--height", type=int) parser.add_argument("--width", type=int) + parser.add_argument("--enable-ntrip", action="store_true") args_main, unknown_args = parser.parse_known_args() if args_main.debug_with_no_backend and args_main.read_capnp_recording is None: @@ -749,6 +770,7 @@ def main(passed_args: Optional[Tuple[str, ...]] = None) -> int: qmlRegisterType(SolutionTableEntries, "SwiftConsole", 1, 0, "SolutionTableEntries") # type: ignore qmlRegisterType(SolutionVelocityPoints, "SwiftConsole", 1, 0, "SolutionVelocityPoints") # type: ignore qmlRegisterType(StatusBarData, "SwiftConsole", 1, 0, "StatusBarData") # type: ignore + qmlRegisterType(NtripStatusData, "SwiftConsole", 1, 0, "NtripStatusData") # type: ignore qmlRegisterType(TrackingSignalsPoints, "SwiftConsole", 1, 0, "TrackingSignalsPoints") # type: ignore qmlRegisterType(TrackingSkyPlotPoints, "SwiftConsole", 1, 0, "TrackingSkyPlotPoints") # type: ignore qmlRegisterType(ObservationRemoteTableModel, "SwiftConsole", 1, 0, "ObservationRemoteTableModel") # type: ignore diff --git a/swiftnav_console/ntrip_status.py b/swiftnav_console/ntrip_status.py new file mode 100644 index 000000000..6ebd9deef --- /dev/null +++ b/swiftnav_console/ntrip_status.py @@ -0,0 +1,18 @@ +"""Ntrip Status QObjects. +""" + +from PySide2.QtCore import QObject, SignalInstance + + +class NtripStatusData(QObject): # pylint: disable=too-many-instance-attributes + _instance: "NtripStatusData" + ntrip_connected: SignalInstance + + def __init__(self): + super().__init__() + assert getattr(self.__class__, "_instance", None) is None + self.__class__._instance = self + + @classmethod + def post_connected(cls, connected: bool) -> None: + cls._instance.ntrip_connected.emit(connected) diff --git a/swiftnav_console/status_bar.py b/swiftnav_console/status_bar.py index 31ad245af..e49edd760 100644 --- a/swiftnav_console/status_bar.py +++ b/swiftnav_console/status_bar.py @@ -19,13 +19,14 @@ def status_bar_update() -> Dict[str, Any]: Keys.SOLID_CONNECTION: bool, Keys.TITLE: str, Keys.ANTENNA_STATUS: str, + Keys.NTRIP_DISPLAY: str, } STATUS_BAR: List[Dict[str, Any]] = [status_bar_update()] -class StatusBarData(QObject): # pylint: disable=too-many-instance-attributes +class StatusBarData(QObject): # pylint: disable=too-many-instance-attributes, too-many-public-methods _pos: str = "" _rtk: str = "" @@ -37,6 +38,7 @@ class StatusBarData(QObject): # pylint: disable=too-many-instance-attributes _title: str = "" _antenna_status: str = "" _data_updated = Signal() + _ntrip_display: str = "" status_bar: Dict[str, Any] = {} def __init__(self): @@ -127,6 +129,14 @@ def set_antenna_status(self, antenna_status: str) -> None: antenna_status = Property(str, get_antenna_status, set_antenna_status) + def get_ntrip_display(self) -> str: + return self._ntrip_display + + def set_ntrip_display(self, ntrip_display: str) -> None: + self._ntrip_display = ntrip_display + + ntrip_display = Property(str, get_ntrip_display, set_ntrip_display) + class StatusBarModel(QObject): # pylint: disable=too-few-public-methods @Slot(StatusBarData) # type: ignore @@ -140,4 +150,5 @@ def fill_data(self, cp: StatusBarData) -> StatusBarData: # pylint:disable=no-se cp.set_solid_connection(cp.status_bar[Keys.SOLID_CONNECTION]) cp.set_title(cp.status_bar[Keys.TITLE]) cp.set_antenna_status(cp.status_bar[Keys.ANTENNA_STATUS]) + cp.set_ntrip_display(cp.status_bar[Keys.NTRIP_DISPLAY]) return cp