diff --git a/.cargo/config.toml b/.cargo/config.toml index 04538c4d1..93ae912dd 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -11,3 +11,9 @@ rustflags = ["-C", "force-unwind-tables"] # # [target.x86_64-unknown-freebsd] # linker = "~/.cargo/bin/cargo-zigbuild zig cc -- -target freebsd -g" + +[target.arm-unknown-linux-musleabi] +linker = "arm-linux-gnueabihf-ld" + +[target.aarch64-unknown-linux-musl] +linker = "aarch64-linux-musl-gcc" diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 000000000..c120639e3 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,53 @@ +# Official language image. Look for the different tagged releases at: +# https://hub.docker.com/r/library/rust/tags/ +image: "rust:1.78.0" + +stages: + - lint + - test + - build + - deploy + +before_script: + - apt-get update + - apt-get install -y pkg-config libudev-dev + +lint-clippy: + stage: lint + script: + - rustup component add clippy + - cargo clippy -- -D warnings + only: + - branches + - merge_requests + +# Use cargo to test the project +test_cargo: + stage: test + script: + - rustc --version && cargo --version # Print version info for debugging + - cargo test --workspace --verbose + +# Optional: Use a third party library to generate GitLab JUnit reports +# test_junit_report: +# stage: test +# script: +# # Should be specified in Cargo.toml +# - cargo install junitify +# - cargo test -- --format=json -Z unstable-options --report-time | junitify --out $CI_PROJECT_DIR/tests/ +# artifacts: +# when: always +# reports: +# junit: $CI_PROJECT_DIR/tests/*.xml + +build_job: + stage: build + script: + - echo "Compiling the code..." + - cargo build --workspace --verbose + +deploy: + stage: deploy + script: + - echo "Define your deployment script!" + environment: production diff --git a/Cargo.lock b/Cargo.lock index 3ba68f24e..24cb49c46 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "addr2line" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" dependencies = [ "gimli", ] @@ -54,6 +54,30 @@ dependencies = [ "zeroize", ] +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "arbitrary" version = "1.3.2" @@ -71,11 +95,44 @@ dependencies = [ "syn", ] +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "aws-lc-rs" +version = "1.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf7d844e282b4b56750b2d4e893b2205581ded8709fddd2b6aa5418c150ca877" +dependencies = [ + "aws-lc-sys", + "mirai-annotations", + "paste", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3a2c29203f6bf296d01141cc8bb9dbd5ecd4c27843f2ee0767bcd5985a927da" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", + "libc", + "paste", +] + [[package]] name = "backtrace" -version = "0.3.71" +version = "0.3.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" dependencies = [ "addr2line", "cc", @@ -88,9 +145,32 @@ dependencies = [ [[package]] name = "base64" -version = "0.22.0" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bindgen" +version = "0.69.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" +checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" +dependencies = [ + "bitflags 2.5.0", + "cexpr", + "clang-sys", + "itertools", + "lazy_static", + "lazycell", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", + "which", +] [[package]] name = "bitflags" @@ -98,6 +178,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" + [[package]] name = "block-buffer" version = "0.10.4" @@ -107,6 +193,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + [[package]] name = "bytes" version = "1.6.0" @@ -115,9 +207,23 @@ checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] name = "cc" -version = "1.0.94" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c51067fd44124faa7f870b4b1c969379ad32b2ba805aa959430ceaa384f695" +dependencies = [ + "jobserver", + "libc", + "once_cell", +] + +[[package]] +name = "cexpr" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17f6e324229dc011159fcc089755d1e2e216a90d43a7dea6853ca740b84f35e7" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] [[package]] name = "cfg-if" @@ -125,6 +231,42 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets 0.52.5", +] + +[[package]] +name = "chrono-tz" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59ae0466b83e838b81a54256c39d5d7c20b9d7daa10510a242d9b75abd5936e" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf", +] + +[[package]] +name = "chrono-tz-build" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "433e39f13c9a060046954e0592a8d0a4bcb1040125cbf91cb8ee58964cfb350f" +dependencies = [ + "parse-zoneinfo", + "phf", + "phf_codegen", +] + [[package]] name = "cipher" version = "0.4.4" @@ -135,6 +277,17 @@ dependencies = [ "inout", ] +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "clock-steering" version = "0.2.0" @@ -155,6 +308,15 @@ dependencies = [ "digest", ] +[[package]] +name = "cmake" +version = "0.1.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" +dependencies = [ + "cc", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -220,12 +382,129 @@ dependencies = [ "subtle", ] +[[package]] +name = "dunce" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" + +[[package]] +name = "either" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" + [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -238,9 +517,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", @@ -249,15 +528,33 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.1" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "gpsd_client" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +checksum = "c75fa63550e87e1cf131abd987cb3c33da2a0b2723b1fbff39d41885ed2bdbd8" +dependencies = [ + "chrono", + "chrono-tz", + "round", + "serde_json", +] [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "hermit-abi" @@ -265,6 +562,38 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "indexmap" version = "2.2.6" @@ -284,23 +613,112 @@ dependencies = [ "generic-array", ] +[[package]] +name = "io-kit-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617ee6cf8e3f66f3b4ea67a4058564628cde41901316e19f559e14c7c72c5e7b" +dependencies = [ + "core-foundation-sys", + "mach2", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "jobserver" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "libc" -version = "0.2.154" +version = "0.2.155" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[package]] +name = "libloading" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" +dependencies = [ + "cfg-if", + "windows-targets 0.52.5", +] + +[[package]] +name = "libudev" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b324152da65df7bb95acfcaab55e3097ceaab02fb19b228a9eb74d55f135e0" +dependencies = [ + "libc", + "libudev-sys", +] + +[[package]] +name = "libudev-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] [[package]] name = "log" @@ -308,6 +726,15 @@ version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +[[package]] +name = "mach2" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709" +dependencies = [ + "libc", +] + [[package]] name = "md-5" version = "0.10.6" @@ -320,15 +747,39 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.2" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + +[[package]] +name = "memoffset" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" dependencies = [ "adler", ] @@ -340,10 +791,66 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", + "log", "wasi", "windows-sys 0.48.0", ] +[[package]] +name = "mio-serial" +version = "5.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20a4c60ca5c9c0e114b3bd66ff4aa5f9b2b175442be51ca6c4365d687a97a2ac" +dependencies = [ + "log", + "mio", + "nix 0.26.4", + "serialport", + "winapi", +] + +[[package]] +name = "mirai-annotations" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9be0862c1b3f26a88803c4a49de6889c10e608b3ee9344e6ef5b45fb37ad3d1" + +[[package]] +name = "nix" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c" +dependencies = [ + "bitflags 1.3.2", + "cc", + "cfg-if", + "libc", + "memoffset 0.6.5", +] + +[[package]] +name = "nix" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset 0.7.1", + "pin-utils", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "ntp-proto" version = "1.1.2" @@ -366,8 +873,11 @@ name = "ntpd" version = "1.1.2" dependencies = [ "async-trait", + "chrono", "clock-steering", + "gpsd_client", "libc", + "nix 0.23.2", "ntp-proto", "rand", "rustls", @@ -375,9 +885,12 @@ dependencies = [ "rustls-pemfile", "serde", "serde_json", + "serialport", + "smoke", "timestamped-socket", "tokio", "tokio-rustls", + "tokio-serial", "toml", "tracing", "tracing-subscriber", @@ -409,6 +922,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.16.0" @@ -421,9 +943,9 @@ dependencies = [ [[package]] name = "object" -version = "0.32.2" +version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434" dependencies = [ "memchr", ] @@ -446,23 +968,121 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.5", +] + +[[package]] +name = "parse-zoneinfo" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24" +dependencies = [ + "regex", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project-lite" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + [[package]] name = "ppv-lite86" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "prettyplease" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro2" -version = "1.0.81" +version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" +checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" dependencies = [ "unicode-ident", ] @@ -506,6 +1126,44 @@ dependencies = [ "getrandom", ] +[[package]] +name = "redox_syscall" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" +dependencies = [ + "bitflags 2.5.0", +] + +[[package]] +name = "regex" +version = "1.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + [[package]] name = "ring" version = "0.17.8" @@ -521,20 +1179,46 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "round" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b02705db4fa872ae37e464fc803b7ffa574e6c4a5e112c52a82550f9dd63b657" + [[package]] name = "rustc-demangle" -version = "0.1.23" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustix" +version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +dependencies = [ + "bitflags 2.5.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] [[package]] name = "rustls" -version = "0.22.4" +version = "0.23.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" +checksum = "05cff451f60db80f490f3c182b77c35260baace73209e9cdbbe526bfe3a4d402" dependencies = [ + "aws-lc-rs", "log", - "ring", + "once_cell", "rustls-pki-types", "rustls-webpki", "subtle", @@ -566,16 +1250,17 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.4.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecd36cc4259e3e4514335c4a138c6b43171a8d61d8f5c9348f9fc7529416f247" +checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" [[package]] name = "rustls-webpki" -version = "0.102.2" +version = "0.102.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610" +checksum = "ff448f7e92e913c4b7d4c6d8e4540a1724b319b4152b8aef6d4cf8339712b33e" dependencies = [ + "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -583,9 +1268,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "schannel" @@ -596,13 +1281,19 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "security-framework" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6" +checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" dependencies = [ - "bitflags", + "bitflags 2.5.0", "core-foundation", "core-foundation-sys", "libc", @@ -611,9 +1302,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f3cc463c0ef97e11c3461a9d3787412d30e8e7eb907c79180c4a57bf7c04ef" +checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7" dependencies = [ "core-foundation-sys", "libc", @@ -621,18 +1312,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.201" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "780f1cebed1629e4753a1a38a3c72d30b97ec044f0aef68cb26650a3c5cf363c" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.201" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e405930b9796f1c00bee880d03fc7e0bb4b9a11afc776885ffe84320da2865" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", @@ -652,9 +1343,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" dependencies = [ "serde", ] @@ -668,6 +1359,25 @@ dependencies = [ "serde", ] +[[package]] +name = "serialport" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f5a15d0be940df84846264b09b51b10b931fb2f275becb80934e3568a016828" +dependencies = [ + "bitflags 2.5.0", + "cfg-if", + "core-foundation-sys", + "io-kit-sys", + "libudev", + "mach2", + "nix 0.26.4", + "regex", + "scopeguard", + "unescaper", + "winapi", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -677,11 +1387,53 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "smoke" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "debdb23aacfcda952daff6ed03267c5d6aa7e3961fbcdc83243857bbaa386bda" + [[package]] name = "socket2" -version = "0.5.6" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" dependencies = [ "libc", "windows-sys 0.52.0", @@ -695,21 +1447,41 @@ checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" [[package]] name = "subtle" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +checksum = "0d0208408ba0c3df17ed26eb06992cb1a1268d41b2c0e12e65203fbe3972cee5" [[package]] name = "syn" -version = "2.0.60" +version = "2.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" +checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "thiserror" +version = "1.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "thread_local" version = "1.1.8" @@ -734,16 +1506,18 @@ dependencies = [ [[package]] name = "tokio" -version = "1.37.0" +version = "1.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" +checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" dependencies = [ "backtrace", "bytes", "libc", "mio", "num_cpus", + "parking_lot", "pin-project-lite", + "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.48.0", @@ -751,9 +1525,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", @@ -762,20 +1536,33 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.25.0" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" dependencies = [ "rustls", "rustls-pki-types", "tokio", ] +[[package]] +name = "tokio-serial" +version = "5.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa6e2e4cf0520a99c5f87d5abb24172b5bd220de57c3181baaaa5440540c64aa" +dependencies = [ + "cfg-if", + "futures", + "log", + "mio-serial", + "tokio", +] + [[package]] name = "toml" -version = "0.8.12" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3" +checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335" dependencies = [ "serde", "serde_spanned", @@ -785,18 +1572,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.9" +version = "0.22.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e40bb779c5187258fd7aad0eb68cb8706a0a81fa712fbea808ab43c4b8374c4" +checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38" dependencies = [ "indexmap", "serde", @@ -855,6 +1642,15 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "unescaper" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0adf6ad32eb5b3cadff915f7b770faaac8f7ff0476633aa29eb0d9584d889d34" +dependencies = [ + "thiserror", +] + [[package]] name = "unicode-ident" version = "1.0.12" @@ -879,6 +1675,72 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", +] + [[package]] name = "winapi" version = "0.3.9" @@ -901,6 +1763,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.5", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -1042,15 +1913,29 @@ checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] name = "winnow" -version = "0.6.6" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c976aaaa0e1f90dbb21e9587cdaf1d9679a1cde8875c0d6bd83ab96a208352" +checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1" dependencies = [ "memchr", ] [[package]] name = "zeroize" -version = "1.7.0" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index 08d9443ed..14e56f768 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,16 +41,16 @@ rand = "0.8.0" arbitrary = { version = "1.0" } libc = "0.2.145" tokio = "1.32" -toml = { version = ">=0.5.0,<0.9.0", default-features = false, features = ["parse"] } +toml = { version = "0.8.14", default-features = false, features = ["parse"] } async-trait = "0.1.22" timestamped-socket = "0.2.1" clock-steering = "0.2.0" # TLS -rustls = "0.22.0" +rustls = "0.23.1" rustls-pemfile = "2.0" rustls-native-certs = "0.7.0" -tokio-rustls = "0.25.0" # testing only +tokio-rustls = "0.26.0" # testing only # crypto aead = "0.5.0" diff --git a/README.md b/README.md index 389edaf22..138aca34d 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,26 @@ If a feature you need is missing please let us know by opening an issue. Be sure to check out the [documentation website] as it includes guides on getting started, installation and migration, as well as a high-level overview of the code structure. +Be sure to check out the [documentation website] as it includes guides on getting started, installation and migration, as well as a high-level overview of the code structure. + ## Usage +If you have a device with gps receiver, build the repo with cargo build and while in project dirr. To configure the GPS, add the GPS configuration to any configuration file (.toml), example: + +``` +[[source]] +mode = "Gps" +address = "/dev/serial0" +measurement_noise = 0.001 +baud_rate = 9600 +``` + +Run the executable with a configuration by running. + +```console +$ ./target/release/ntp-daemon --config ./config/any configuration file.toml +``` + You can install the packages from the [releases page]. These packages configure ntpd-rs to synchronize your computers clock to servers from the [NTP pool]. After installation, check the status of the ntpd-rs daemon with ```console @@ -54,7 +72,13 @@ ntpd-rs.pool.ntp.org:123/162.159.200.123:123 (4): +0.000111±0.000076(±0.004066 Servers: ``` -The top part shows the overal quality of the time synchronization, and the time sources section shows which servers are used as well as offsets and uncertainties of those individual servers. + +<<<<<<< HEAD + +======= + +> > > > > > > aa070d6a (Added the project's official README content) +> > > > > > > The top part shows the overal quality of the time synchronization, and the time sources section shows which servers are used as well as offsets and uncertainties of those individual servers. For more details on how to install and use ntpd-rs, see our [documentation website]. @@ -65,8 +89,14 @@ full-featured, it supports NTP client and server with NTS. Our roadmap for 2024: +<<<<<<< HEAD + +- Q2-Q4 2024: Packaging and industry adoption, maintenance & community work +- # Q4 2024: NTS Pool (pending funding) + * Q2-Q4 2024: Packaging and industry adoption, maintenance & community work * Q4 2024: NTS Pool (pending funding) + > > > > > > > aa070d6a (Added the project's official README content) We seek sponsorship for features and maintenance to continue our work. Contact us via pendulum@tweedegolf.com if you are interested! @@ -104,4 +134,10 @@ In July of 2023 the [Sovereign Tech Fund] invested in Pendulum, securing ntpd-rs [NTP initiative page]: https://www.memorysafety.org/initiative/ntp [NTP announcement]: https://www.memorysafety.org/blog/ntp-and-nts-have-arrived/ [Project Pendulum]: https://github.com/pendulum-project + +<<<<<<< HEAD [Sovereign Tech Fund]: https://sovereigntechfund.de/en/ +======= +[Sovereign Tech Fund]: https://sovereigntechfund.de/en/ + +> > > > > > > aa070d6a (Added the project's official README content) diff --git a/clippy.toml b/clippy.toml index 81cc390da..5d37e5d80 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1 +1 @@ -msrv = "1.67" +msrv = "1.70" diff --git a/config/ntp.demobilize.toml b/config/ntp.demobilize.toml index 217406fa0..c3ef779d9 100644 --- a/config/ntp.demobilize.toml +++ b/config/ntp.demobilize.toml @@ -16,7 +16,19 @@ listen = "0.0.0.0:123" filter = [] action = "deny" +[[source]] +mode = "Gps" +address = "/dev/serial0" +measurement_noise = 0.001 +baud_rate = 9600 + +[[source]] +mode = "Pps" +address = "/dev/pps0" +measurement_noise = 0.001 + + # configure the client with # [[source]] # mode = "server" -# address = "0.0.0.0:123" +# address = "0.0.0.0:123" \ No newline at end of file diff --git a/ntp-proto/src/algorithm/kalman/combine_with_pps.rs b/ntp-proto/src/algorithm/kalman/combine_with_pps.rs new file mode 100644 index 000000000..aed062299 --- /dev/null +++ b/ntp-proto/src/algorithm/kalman/combine_with_pps.rs @@ -0,0 +1,229 @@ +use super::{ + matrix::{Matrix, Vector}, + SourceSnapshot, +}; + +pub(crate) fn combine_with_pps( + pps: SourceSnapshot, + candidates: Vec>, +) -> Vec> { + let mut results = Vec::new(); + for snapshot in &candidates { + results.push(snapshot.clone()); + let combined = combine_sources(pps.clone(), snapshot.clone()); + results.push(combined.clone()); + } + + results +} + +fn combine_sources( + pps_snapshot: SourceSnapshot, + other_snapshot: SourceSnapshot, +) -> SourceSnapshot { + // assign the offset of pps after each measurement + let pps_offset = pps_snapshot.offset(); + // assign the frequency error of pps after each measurement + let pps_offset_uncertainty = pps_snapshot.offset_uncertainty(); + // assign the offset of other sources after each measurement + let other_offset = other_snapshot.offset(); + // assign the frequency error of other sources after each measurement + let other_offset_uncertainty = other_snapshot.offset_uncertainty(); + + // find the smaller whole second the other source is in range of ie if offset 12.3 this assigns 12.0 + let full_second_floor = other_offset.floor(); + // find the larger whole second the other source is in range of ie if offset 12.3 this assigns 13.0 + let full_second_ceil = other_offset.ceil(); + + // create 4 endpoints for both the larger and smaller whole seconds the offset of current measurement is in range of + // use the pps offset by adding and subtracting from the whole seconds to find the endpoints of the combined source + // these endpoints will then be used to compare with the uncombined source range to fins the closest endpoint + let pps_floor_positive = full_second_floor + pps_offset; + let pps_floor_negative = full_second_floor - pps_offset; + let pps_ceil_positive = full_second_ceil + pps_offset; + let pps_ceil_negative = full_second_ceil - pps_offset; + + // calculate the uncombined endpoints of the source + let other_minimum = other_offset - other_offset_uncertainty; + let other_maximum = other_offset + other_offset_uncertainty; + + // calculate the difference of each combined endpoint from the uncombined endpoint + let floor_positive_diff = (pps_floor_positive - other_minimum).abs(); + let floor_negative_diff = (pps_floor_negative - other_minimum).abs(); + let ceil_positive_diff = (pps_ceil_positive - other_maximum).abs(); + let ceil_negative_diff = (pps_ceil_negative - other_maximum).abs(); + + // assign the minimum difference + let min_diff = floor_positive_diff + .min(floor_negative_diff) + .min(ceil_positive_diff) + .min(ceil_negative_diff); + + let new_offset; + + if min_diff == floor_positive_diff { + new_offset = pps_floor_positive; + } else if min_diff == floor_negative_diff { + new_offset = pps_floor_negative; + } else if min_diff == ceil_positive_diff { + new_offset = pps_ceil_positive; + } else { + new_offset = pps_ceil_negative; + } + + // assign the new offset for the combined source + let combined_state = + Vector::<2>::new([[new_offset], [other_snapshot.get_state_vector().ventry(1)]]); + + // uncombined source frequency error stays the same + let other_uncertainty_matrix = other_snapshot.get_uncertainty_matrix(); + + // assign the frequency error of pps to the combined source + let combined_uncertainty = Matrix::<2, 2>::new([ + [pps_offset_uncertainty, other_uncertainty_matrix.entry(0, 1)], + [ + other_uncertainty_matrix.entry(1, 0), + other_uncertainty_matrix.entry(1, 1), + ], + ]); + + SourceSnapshot { + index: other_snapshot.index, + state: combined_state, + uncertainty: combined_uncertainty, + delay: other_snapshot.delay, + source_uncertainty: other_snapshot.source_uncertainty, + source_delay: other_snapshot.source_delay, + leap_indicator: other_snapshot.leap_indicator, + last_update: other_snapshot.last_update, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::algorithm::kalman::matrix::{Matrix, Vector}; + use crate::algorithm::kalman::SourceSnapshot; + use crate::time_types::NtpDuration; + use crate::time_types::NtpTimestamp; + + // Helper to create the snapshots, to be used in testing + fn create_snapshot( + index: Index, + offset: f64, + offset_uncertainty: f64, + state_vector: [f64; 2], + uncertainty_matrix: [[f64; 2]; 2], + ) -> SourceSnapshot { + SourceSnapshot { + index, + state: Vector::new_vector(state_vector), + uncertainty: Matrix::new(uncertainty_matrix), + delay: 0.0, + source_uncertainty: NtpDuration::from_seconds(0.0), + source_delay: NtpDuration::from_seconds(0.0), + leap_indicator: crate::NtpLeapIndicator::NoWarning, + last_update: NtpTimestamp::from_fixed_int(0), + } + } + + // Tests the combination of PPS when there is only one candidate + #[test] + fn test_combine_with_pps_single_candidate() { + let pps_snapshot = create_snapshot(1, 1.0, 0.1, [1.0, 0.0], [[0.1, 0.0], [0.0, 0.1]]); + + let candidate_snapshot = create_snapshot(2, 1.2, 0.2, [1.2, 0.0], [[0.2, 0.0], [0.0, 0.2]]); + + let combined = combine_with_pps(pps_snapshot, vec![candidate_snapshot]); + assert_eq!(combined.len(), 2); + } + + // Tests the combination of PPS when there are multiple candidates + #[test] + fn test_combine_with_pps_multiple_candidates() { + let pps_snapshot = create_snapshot(1, 1.0, 0.1, [1.0, 0.0], [[0.1, 0.0], [0.0, 0.1]]); + + let candidate_snapshot1 = + create_snapshot(2, 1.2, 0.2, [1.2, 0.0], [[0.2, 0.0], [0.0, 0.2]]); + + let candidate_snapshot2 = + create_snapshot(3, 1.4, 0.3, [1.4, 0.0], [[0.3, 0.0], [0.0, 0.3]]); + + let combined = + combine_with_pps(pps_snapshot, vec![candidate_snapshot1, candidate_snapshot2]); + assert_eq!(combined.len(), 4); + } + + // Tests the combination of PPS when there are no candidates + #[test] + fn test_combine_with_pps_no_candidates() { + let pps_snapshot = create_snapshot(1, 1.0, 0.1, [1.0, 0.0], [[0.1, 0.0], [0.0, 0.1]]); + + let combined = combine_with_pps(pps_snapshot, vec![]); + assert!(combined.is_empty()); + } + + // Tests the combine_sources function when the inouts have the same offsets and uncertainties, 0 + #[test] + fn test_combine_sources_zeros() { + let pps_snapshot = create_snapshot(1, 0.0, 0.0, [0.0, 0.0], [[0.0, 0.0], [0.0, 0.0]]); + + let other_snapshot = create_snapshot(2, 0.0, 0.0, [0.0, 0.0], [[0.0, 0.0], [0.0, 0.0]]); + + let combined_snapshot = combine_sources(pps_snapshot, other_snapshot); + + assert_eq!(combined_snapshot.state.ventry(0), 0.0); + assert_eq!(combined_snapshot.uncertainty.entry(0, 0), 0.0); + } + + // Tests the combine_sources function when the inputs have the same offsets and no uncertainty + #[test] + fn test_combine_sources_zero_uncertainty() { + let pps_snapshot = create_snapshot(1, 1.0, 0.0, [1.0, 0.0], [[0.0, 0.0], [0.0, 0.0]]); + + let other_snapshot = create_snapshot(2, 1.0, 0.0, [1.0, 0.0], [[0.0, 0.0], [0.0, 0.0]]); + + let combined_snapshot = combine_sources(pps_snapshot, other_snapshot); + + assert!((combined_snapshot.state.ventry(0) - 2.0).abs() < 1e-6); + assert!(combined_snapshot.uncertainty.entry(0, 0) == 0.0); + } + + // Tests the combine_sources function when the inputs have the different offsets and same large uncertainty + #[test] + fn test_combine_sources_large_uncertainty() { + let pps_snapshot = create_snapshot(1, 1.0, 50.0, [1.0, 0.0], [[50.0, 0.0], [0.0, 50.0]]); + + let other_snapshot = create_snapshot(2, 2.0, 50.0, [2.0, 0.0], [[50.0, 0.0], [0.0, 50.0]]); + + let combined_snapshot = combine_sources(pps_snapshot, other_snapshot); + + assert!( + combined_snapshot.state.ventry(0) >= 1.0 && combined_snapshot.state.ventry(0) <= 2.0 + ); + } + + // Tests the combine_sources function when the uncertainty difference is very small + #[test] + fn test_combine_sources_small_uncertainty() { + let pps_snapshot = create_snapshot(1, 0.4, 0.01, [0.4, 0.0], [[0.01, 0.0], [0.0, 0.01]]); + + let other_snapshot = create_snapshot(2, 0.5, 0.01, [0.5, 0.0], [[0.01, 0.0], [0.0, 0.01]]); + + let combined_snapshot = combine_sources(pps_snapshot, other_snapshot); + + assert_eq!(combined_snapshot.state.ventry(0), 0.4); + } + + // Tests the combine_sources function when the offsets are on the opposite ends of the spectrum + #[test] + fn test_combine_sources_high_difference_in_offsets() { + let pps_snapshot = create_snapshot(1, 0.4, 0.1, [0.4, 0.0], [[0.1, 0.0], [0.0, 0.1]]); + + let other_snapshot = create_snapshot(2, -0.4, 0.1, [-0.4, 0.0], [[0.1, 0.0], [0.0, 0.1]]); + + let combined_snapshot = combine_sources(pps_snapshot, other_snapshot); + + assert_eq!(-0.6, combined_snapshot.state.ventry(0)); + } +} diff --git a/ntp-proto/src/algorithm/kalman/mod.rs b/ntp-proto/src/algorithm/kalman/mod.rs index b4792b440..081ee8f61 100644 --- a/ntp-proto/src/algorithm/kalman/mod.rs +++ b/ntp-proto/src/algorithm/kalman/mod.rs @@ -3,12 +3,7 @@ use std::{collections::HashMap, fmt::Debug, hash::Hash, time::Duration}; use tracing::{error, info, instrument}; use crate::{ - clock::NtpClock, - config::{SourceDefaultsConfig, SynchronizationConfig}, - packet::NtpLeapIndicator, - source::Measurement, - system::TimeSnapshot, - time_types::{NtpDuration, NtpTimestamp}, + clock::NtpClock, config::{SourceDefaultsConfig, SynchronizationConfig}, packet::NtpLeapIndicator, source::Measurement, system::TimeSnapshot, time_types::{NtpDuration, NtpTimestamp} }; use self::{ @@ -22,16 +17,17 @@ use super::{ObservableSourceTimedata, StateUpdate, TimeSyncController}; mod combiner; pub(super) mod config; -mod matrix; +pub mod matrix; mod select; -mod source; +pub mod source; +pub mod combine_with_pps; fn sqr(x: f64) -> f64 { x * x } #[derive(Debug, Clone)] -struct SourceSnapshot { +pub struct SourceSnapshot { index: Index, state: Vector<2>, uncertainty: Matrix<2, 2>, @@ -53,6 +49,14 @@ impl SourceSnapshot { self.uncertainty.entry(0, 0).sqrt() } + fn get_state_vector(&self) -> Vector<2> { + self.state + } + + fn get_uncertainty_matrix(&self) -> Matrix<2, 2> { + self.uncertainty + } + fn observe(&self) -> ObservableSourceTimedata { ObservableSourceTimedata { offset: NtpDuration::from_seconds(self.offset()), @@ -76,11 +80,15 @@ pub struct KalmanClockController, } impl KalmanClockController { #[instrument(skip(self))] fn update_source(&mut self, id: SourceId, measurement: Measurement) -> bool { + if let Some(_pps_measurement) = &measurement.pps{ + self.pps_source_id = Some(id); + } self.sources.get_mut(&id).map(|state| { state.0.update_self_using_measurement( &self.source_defaults_config, @@ -91,7 +99,7 @@ impl KalmanClockController StateUpdate { - // ensure all filters represent the same (current) time + //ensure all filters represent the same (current) time if self .sources .iter() @@ -107,12 +115,38 @@ impl KalmanClockController = self.sources.iter() + .filter_map(|(index, (state, usable))| { + if *usable && *index != pps_source { + state.snapshot(*index) + } else { + None + } + }) + .collect(); + + // Combine the PPS snapshot with other candidates if PPS snapshot is found + if let Some(pps_snapshot) = pps_snapshot { + combine_with_pps::combine_with_pps::(pps_snapshot, other_candidates) + } else { + other_candidates + } + } else { + // If pps_source_id is None, just use other_candidates + self.sources.iter() .filter_map(|(index, (state, usable))| { if *usable { state.snapshot(*index) @@ -120,9 +154,15 @@ impl KalmanClockController TimeSyncController, ) -> Result { // Setup clock clock.disable_ntp_algorithm()?; @@ -328,6 +369,7 @@ impl TimeSyncController TimeSyncController StateUpdate { + + let should_update_clock = self.update_source(id, measurement); + self.update_desired_poll(); + if should_update_clock { + self.update_clock(measurement.localtime) + } else { + StateUpdate { + used_sources: None, + time_snapshot: Some(self.timedata), + next_update: None, + } + } + } + + fn source_pps_measurement( + &mut self, + id: SourceId, + measurement: Measurement, + ) -> StateUpdate { + let should_update_clock = self.update_source(id, measurement); self.update_desired_poll(); if should_update_clock { @@ -453,6 +521,7 @@ mod tests { synchronization_config, source_defaults_config, algo_config, + None, ) .unwrap(); let mut cur_instant = NtpInstant::now(); @@ -486,6 +555,8 @@ mod tests { root_dispersion: NtpDuration::default(), leap: NtpLeapIndicator::NoWarning, precision: 0, + gps: None, + pps: None, }, ); } @@ -519,6 +590,7 @@ mod tests { synchronization_config, source_defaults_config, algo_config, + None, ) .unwrap(); @@ -549,6 +621,7 @@ mod tests { synchronization_config, source_defaults_config, algo_config, + None, ) .unwrap(); @@ -574,6 +647,7 @@ mod tests { synchronization_config, source_defaults_config, algo_config, + None, ) .unwrap(); let mut cur_instant = NtpInstant::now(); @@ -605,6 +679,8 @@ mod tests { root_dispersion: NtpDuration::default(), leap: NtpLeapIndicator::NoWarning, precision: 0, + gps: None, + pps: None, }, ); } @@ -631,6 +707,7 @@ mod tests { synchronization_config, source_defaults_config, algo_config, + None, ) .unwrap(); let mut cur_instant = NtpInstant::now(); @@ -662,6 +739,8 @@ mod tests { root_dispersion: NtpDuration::default(), leap: NtpLeapIndicator::NoWarning, precision: 0, + gps: None, + pps: None, }, ); } diff --git a/ntp-proto/src/algorithm/kalman/select.rs b/ntp-proto/src/algorithm/kalman/select.rs index c1658bdd7..8ede5633a 100644 --- a/ntp-proto/src/algorithm/kalman/select.rs +++ b/ntp-proto/src/algorithm/kalman/select.rs @@ -15,13 +15,14 @@ enum BoundType { // is also statistically more sound. Any difference (larger set of accepted sources) // can be compensated for if desired by setting tighter bounds on the weights // determining the confidence interval. + pub(super) fn select( synchronization_config: &SynchronizationConfig, algo_config: &AlgorithmConfig, candidates: Vec>, ) -> Vec> { let mut bounds: Vec<(f64, BoundType)> = Vec::with_capacity(2 * candidates.len()); - + for snapshot in candidates.iter() { let radius = snapshot.offset_uncertainty() * algo_config.range_statistical_weight + snapshot.delay * algo_config.range_delay_weight; @@ -51,7 +52,6 @@ pub(super) fn select( maxt = *time; } } - if max >= synchronization_config.minimum_agreeing_sources && max * 4 > bounds.len() { candidates .iter() @@ -97,6 +97,108 @@ mod tests { } } + #[test] + fn test_no_candidates() { + // Test that no candidates are selected when input is empty. + let candidates: Vec> = vec![]; + let sysconfig = SynchronizationConfig { + minimum_agreeing_sources: 1, + ..Default::default() + }; + let algconfig = AlgorithmConfig { + maximum_source_uncertainty: 1.0, + range_statistical_weight: 1.0, + range_delay_weight: 1.0, + ..Default::default() + }; + let result = select(&sysconfig, &algconfig, candidates); + assert_eq!(result.len(), 0); + } + + #[test] + fn test_all_rejected_due_to_leap_indicator() { + // Test that all candidates are rejected if their leap indicator is not synchronized. + let candidates = vec![ + SourceSnapshot { + index: 0, + state: Vector::new_vector([0.0, 0.0]), + uncertainty: Matrix::new([[sqr(0.01), 0.0], [0.0, 10e-12]]), + delay: 0.01, + source_uncertainty: NtpDuration::from_seconds(0.01), + source_delay: NtpDuration::from_seconds(0.01), + leap_indicator: NtpLeapIndicator::Unknown, + last_update: NtpTimestamp::from_fixed_int(0), + }, + SourceSnapshot { + index: 1, + state: Vector::new_vector([0.1, 0.0]), + uncertainty: Matrix::new([[sqr(0.01), 0.0], [0.0, 10e-12]]), + delay: 0.01, + source_uncertainty: NtpDuration::from_seconds(0.01), + source_delay: NtpDuration::from_seconds(0.01), + leap_indicator: NtpLeapIndicator::Unknown, + last_update: NtpTimestamp::from_fixed_int(0), + }, + ]; + let sysconfig = SynchronizationConfig { + minimum_agreeing_sources: 1, + ..Default::default() + }; + let algconfig = AlgorithmConfig { + maximum_source_uncertainty: 1.0, + range_statistical_weight: 1.0, + range_delay_weight: 1.0, + ..Default::default() + }; + let result = select(&sysconfig, &algconfig, candidates); + assert_eq!(result.len(), 0); + } + + #[test] + fn test_overlap_within_bounds() { + // Test that candidates with sufficient overlap are selected. + let candidates = vec![ + snapshot_for_range(0.0, 0.1, 0.1), + snapshot_for_range(0.0, 0.1, 0.1), + snapshot_for_range(0.0, 0.1, 0.1), + snapshot_for_range(0.0, 0.1, 0.1), + ]; + let sysconfig = SynchronizationConfig { + minimum_agreeing_sources: 2, + ..Default::default() + }; + let algconfig = AlgorithmConfig { + maximum_source_uncertainty: 1.0, + range_statistical_weight: 1.0, + range_delay_weight: 1.0, + ..Default::default() + }; + let result = select(&sysconfig, &algconfig, candidates); + assert_eq!(result.len(), 4); + } + + #[test] + fn test_edge_case_uncertainty() { + // Test edge case where uncertainty is exactly on the boundary. + let candidates = vec![ + snapshot_for_range(0.0, 0.1, 0.1), + snapshot_for_range(0.2, 0.1, 0.1), + ]; + let sysconfig = SynchronizationConfig { + minimum_agreeing_sources: 1, + ..Default::default() + }; + let algconfig = AlgorithmConfig { + maximum_source_uncertainty: 0.2, + range_statistical_weight: 1.0, + range_delay_weight: 1.0, + ..Default::default() + }; + let result = select(&sysconfig, &algconfig, candidates); + assert_eq!(result.len(), 2); + } + + #[test] fn test_weighing() { // Test that there only is sufficient overlap in the below set when diff --git a/ntp-proto/src/algorithm/kalman/source.rs b/ntp-proto/src/algorithm/kalman/source.rs index 6b25d4675..6e061915a 100644 --- a/ntp-proto/src/algorithm/kalman/source.rs +++ b/ntp-proto/src/algorithm/kalman/source.rs @@ -87,7 +87,7 @@ use super::{ }; #[derive(Debug, Default, Copy, Clone)] -struct AveragingBuffer { +pub struct AveragingBuffer { data: [f64; 8], next_idx: usize, } @@ -128,25 +128,50 @@ impl AveragingBuffer { } #[derive(Debug, Clone)] -struct InitialSourceFilter { - roundtriptime_stats: AveragingBuffer, - init_offset: AveragingBuffer, - last_measurement: Option, - - samples: i32, +pub struct InitialSourceFilter { + pub roundtriptime_stats: AveragingBuffer, + pub init_offset: AveragingBuffer, + pub last_measurement: Option, + pub samples: i32, } impl InitialSourceFilter { - fn update(&mut self, measurement: Measurement) { - self.roundtriptime_stats - .update(measurement.delay.to_seconds()); - self.init_offset.update(measurement.offset.to_seconds()); + pub fn update(&mut self, measurement: Measurement) { + // Process GPS data if it exists + // The statistics of the measurement noise of gps data is needed in order to + // have a constant value that changes the covariance matrix and the uncertainity of gps data + // This is needed since we assume delay as 0 and set a default noise value which can be changed from the config + // There should also be an estimated offset that is the first input of kalman filter + // This estimated offset is then changed by the stable filter after each measurement + // This estimated offset is needed since the Stable filter needs to have an estimate so that + // any anomaly data that gets inputted to the stable filter doesnt change the offset as much + // The averaging buffer claculates these statistics by the variance and mean of 8 samples + if let Some(gps_measurement) = &measurement.gps { + self.roundtriptime_stats + .update(gps_measurement.measurementnoise.to_seconds()); + println!( + "gps_measurements offset in seconds: {:?}", + gps_measurement.offset.to_seconds() + ); + self.init_offset.update(gps_measurement.offset.to_seconds()); + } + // Process PPS data if it exists + // The above documentation applies the same way to the pps data + if let Some(pps_measurement) = &measurement.pps { + self.roundtriptime_stats + .update(pps_measurement.measurementnoise.to_seconds()); + self.init_offset.update(pps_measurement.offset.to_seconds()); + } else { + self.roundtriptime_stats + .update(measurement.delay.to_seconds()); + self.init_offset.update(measurement.offset.to_seconds()); + } self.samples += 1; self.last_measurement = Some(measurement); debug!(samples = self.samples, "Initial source update"); } - fn process_offset_steering(&mut self, steer: f64) { + pub fn process_offset_steering(&mut self, steer: f64) { for sample in self.init_offset.data.iter_mut() { *sample -= steer; } @@ -154,29 +179,29 @@ impl InitialSourceFilter { } #[derive(Debug, Clone)] -struct SourceFilter { - state: Vector<2>, - uncertainty: Matrix<2, 2>, - clock_wander: f64, +pub struct SourceFilter { + pub state: Vector<2>, + pub uncertainty: Matrix<2, 2>, + pub clock_wander: f64, - roundtriptime_stats: AveragingBuffer, + pub roundtriptime_stats: AveragingBuffer, - precision_score: i32, - poll_score: i32, - desired_poll_interval: PollInterval, + pub precision_score: i32, + pub poll_score: i32, + pub desired_poll_interval: PollInterval, - last_measurement: Measurement, - prev_was_outlier: bool, + pub last_measurement: Measurement, + pub prev_was_outlier: bool, // Last time a packet was processed - last_iter: NtpTimestamp, + pub last_iter: NtpTimestamp, // Current time of the filter state. - filter_time: NtpTimestamp, + pub filter_time: NtpTimestamp, } impl SourceFilter { /// Move the filter forward to reflect the situation at a new, later timestamp - fn progress_filtertime(&mut self, time: NtpTimestamp) { + pub fn progress_filtertime(&mut self, time: NtpTimestamp) { debug_assert!( !time.is_before(self.filter_time), "time {time:?} is before filter_time {:?}", @@ -209,43 +234,145 @@ impl SourceFilter { } /// Absorb knowledge from a measurement - fn absorb_measurement(&mut self, measurement: Measurement) -> (f64, f64, f64) { + pub fn absorb_measurement(&mut self, measurement: Measurement) -> (f64, f64, f64) { // Measurement parameters let delay_variance = self.roundtriptime_stats.variance(); let m_delta_t = (measurement.localtime - self.last_measurement.localtime).to_seconds(); - // Kalman filter update - let measurement_vec = Vector::new_vector([measurement.offset.to_seconds()]); + // Incorporate GPS measurements if they exist, or provide default values + let (_gps_noise, gps_offset) = if let Some(gps_measurement) = &measurement.gps { + ( + gps_measurement.measurementnoise.to_seconds(), + gps_measurement.offset.to_seconds(), + ) + } else { + // Provide default values for gps_noise and gps_offset + (0.0, 0.0) + }; + // Incorporate PPS measurements if they exist, or provide default values + let (_pps_noise, _pps_offset) = if let Some(pps_measurement) = &measurement.pps { + ( + pps_measurement.measurementnoise.to_seconds(), + pps_measurement.offset.to_seconds(), + ) + } else { + // Provide default values for pps_noise and pps_offset + (0.0, 0.0) + }; + let measurement_transform = Matrix::new([[1., 0.]]); - let measurement_noise = Matrix::new([[delay_variance / 4.]]); - let difference = measurement_vec - measurement_transform * self.state; - let difference_covariance = - measurement_transform * self.uncertainty * measurement_transform.transpose() - + measurement_noise; - let update_strength = - self.uncertainty * measurement_transform.transpose() * difference_covariance.inverse(); - self.state = self.state + update_strength * difference; - self.uncertainty = ((Matrix::unit() - update_strength * measurement_transform) - * self.uncertainty) - .symmetrize(); - - // Statistics - let p = chi_1(difference.inner(difference_covariance.inverse() * difference)); - // Calculate an indicator of how much of the measurement was incorporated - // into the state. 1.0 - is needed here as this should become lower as - // measurement noise's contribution to difference uncertainty increases. - let weight = 1.0 - measurement_noise.determinant() / difference_covariance.determinant(); - - self.last_measurement = measurement; - - trace!(p, weight, "Measurement absorbed"); - - (p, weight, m_delta_t) + + if let Some(_gps_measurement) = &measurement.gps { + // Kalman filter update for GPS + // The noise of the current measurement is inputted into a 1,1 by matrix + let gps_measurement_noise = Matrix::new([[_gps_noise]]); + // The offset of the current measurement is inputted into a 1,1 by matrix + let gps_measurement_vec = Vector::new_vector([gps_offset]); + // Calculate the difference of current measurement offset and the estimated offset + let gps_difference = gps_measurement_vec - measurement_transform * self.state; + // Calculate a covariance matrix for the frequency error + let gps_difference_covariance = + measurement_transform * self.uncertainty * measurement_transform.transpose() + + gps_measurement_noise; + // calculate how much the estimated offset and the frequency error needs to change + let gps_update_strength = self.uncertainty + * measurement_transform.transpose() + * gps_difference_covariance.inverse(); + // update the estimated offset + self.state = self.state + gps_update_strength * gps_difference; + // update the frequency error + self.uncertainty = ((Matrix::unit() - gps_update_strength * measurement_transform) + * self.uncertainty) + .symmetrize(); + + // Statistics + let p = + chi_1(gps_difference.inner(gps_difference_covariance.inverse() * gps_difference)); + // Calculate an indicator of how much of the measurement was incorporated + // into the state. 1.0 - is needed here as this should become lower as + // measurement noise's contribution to difference uncertainty increases. + let weight = + 1.0 - gps_measurement_noise.determinant() / gps_difference_covariance.determinant(); + + // update last measurement + self.last_measurement = measurement; + + trace!(p, weight, "Measurement absorbed"); + + return (p, weight, m_delta_t); + } + + if let Some(_pps_measurement) = &measurement.pps { + // Kalman filter update for PPS + // The noise of the current measurement is inputted into a 1,1 by matrix + let pps_measurement_noise = Matrix::new([[_pps_noise]]); + // The offset of the current measurement is inputted into a 1,1 by matrix + let pps_measurement_vec = Vector::new_vector([_pps_offset]); + // Calculate the difference of current measurement offset and the estimated offset + let pps_difference = pps_measurement_vec - measurement_transform * self.state; + // Calculate a covariance matrix for the frequency error + let pps_difference_covariance = + measurement_transform * self.uncertainty * measurement_transform.transpose() + + pps_measurement_noise; + // calculate how much the estimated offset and the frequency error needs to change + let pps_update_strength = self.uncertainty + * measurement_transform.transpose() + * pps_difference_covariance.inverse(); + // update the estimated offset + self.state = self.state + pps_update_strength * pps_difference; + // update the frequency error + self.uncertainty = ((Matrix::unit() - pps_update_strength * measurement_transform) + * self.uncertainty) + .symmetrize(); + + // Statistics + let p = + chi_1(pps_difference.inner(pps_difference_covariance.inverse() * pps_difference)); + // Calculate an indicator of how much of the measurement was incorporated + // into the state. 1.0 - is needed here as this should become lower as + // measurement noise's contribution to difference uncertainty increases. + let weight = + 1.0 - pps_measurement_noise.determinant() / pps_difference_covariance.determinant(); + + // update last measurement + self.last_measurement = measurement; + + trace!(p, weight, "Measurement absorbed"); + (p, weight, m_delta_t) + } else { + // Kalman filter update for NTP + let measurement_vec = Vector::new_vector([measurement.offset.to_seconds()]); + let measurement_noise = Matrix::new([[delay_variance / 4.]]); + let difference = measurement_vec - measurement_transform * self.state; + let difference_covariance = + measurement_transform * self.uncertainty * measurement_transform.transpose() + + measurement_noise; + let update_strength = self.uncertainty + * measurement_transform.transpose() + * difference_covariance.inverse(); + self.state = self.state + update_strength * difference; + self.uncertainty = ((Matrix::unit() - update_strength * measurement_transform) + * self.uncertainty) + .symmetrize(); + + // Statistics + let p = chi_1(difference.inner(difference_covariance.inverse() * difference)); + // Calculate an indicator of how much of the measurement was incorporated + // into the state. 1.0 - is needed here as this should become lower as + // measurement noise's contribution to difference uncertainty increases. + let weight = + 1.0 - measurement_noise.determinant() / difference_covariance.determinant(); + + self.last_measurement = measurement; + + trace!(p, weight, "Measurement absorbed"); + (p, weight, m_delta_t) + } } /// Ensure we poll often enough to keep the filter well-fed with information, but /// not so much that each individual poll message gives us very little new information. - fn update_desired_poll( + pub fn update_desired_poll( &mut self, source_defaults_config: &SourceDefaultsConfig, algo_config: &AlgorithmConfig, @@ -385,6 +512,11 @@ impl SourceFilter { self.last_measurement.offset -= NtpDuration::from_seconds(steer); self.last_measurement.localtime += NtpDuration::from_seconds(steer); self.filter_time += NtpDuration::from_seconds(steer); + + // Process GPS offset steering if it exists + if let Some(ref mut gps_measurement) = self.last_measurement.gps { + gps_measurement.offset -= NtpDuration::from_seconds(steer); + } } fn process_frequency_steering(&mut self, time: NtpTimestamp, steer: f64) { @@ -393,6 +525,11 @@ impl SourceFilter { self.last_measurement.offset += NtpDuration::from_seconds( steer * (time - self.last_measurement.localtime).to_seconds(), ); + + // Process GPS frequency steering if it exists + if let Some(ref mut _gps_measurement) = self.last_measurement.gps { + self.last_measurement.receive_timestamp += NtpDuration::from_seconds(steer); + } } } @@ -416,7 +553,7 @@ impl SourceState { })) } - // Returs whether the clock may need adjusting. + // Returns whether the clock may need adjusting. pub fn update_self_using_measurement( &mut self, source_defaults_config: &SourceDefaultsConfig, @@ -463,14 +600,12 @@ impl SourceState { { let msg = "Detected clock meddling. Has another process updated the clock?"; tracing::warn!(msg); - *self = SourceState(SourceStateInner::Initial(InitialSourceFilter { roundtriptime_stats: AveragingBuffer::default(), init_offset: AveragingBuffer::default(), last_measurement: None, samples: 0, })); - false } else { filter.update(source_defaults_config, algo_config, measurement) @@ -572,7 +707,6 @@ impl SourceState { #[cfg(test)] mod tests { - use std::panic::catch_unwind; use crate::{packet::NtpLeapIndicator, time_types::NtpInstant}; @@ -607,6 +741,8 @@ mod tests { root_dispersion: NtpDuration::default(), leap: NtpLeapIndicator::NoWarning, precision: 0, + gps: None, + pps: None, }, prev_was_outlier: false, last_iter: base, @@ -628,6 +764,8 @@ mod tests { root_dispersion: NtpDuration::default(), leap: NtpLeapIndicator::NoWarning, precision: 0, + gps: None, + pps: None, }, ); assert!(matches!(source, SourceState(SourceStateInner::Initial(_)))); @@ -656,6 +794,8 @@ mod tests { root_dispersion: NtpDuration::default(), leap: NtpLeapIndicator::NoWarning, precision: 0, + gps: None, + pps: None, }, prev_was_outlier: false, last_iter: base, @@ -678,6 +818,8 @@ mod tests { root_dispersion: NtpDuration::default(), leap: NtpLeapIndicator::NoWarning, precision: 0, + gps: None, + pps: None, }, ); assert!(matches!(source, SourceState(SourceStateInner::Stable(_)))); @@ -706,6 +848,8 @@ mod tests { root_dispersion: NtpDuration::default(), leap: NtpLeapIndicator::NoWarning, precision: 0, + gps: None, + pps: None, }, prev_was_outlier: false, last_iter: base, @@ -728,166 +872,13 @@ mod tests { root_dispersion: NtpDuration::default(), leap: NtpLeapIndicator::NoWarning, precision: 0, + gps: None, + pps: None, }, ); assert!(matches!(source, SourceState(SourceStateInner::Stable(_)))); } - #[test] - fn test_offset_steering_and_measurements() { - let base = NtpTimestamp::from_fixed_int(0); - let basei = NtpInstant::now(); - let mut source = SourceState(SourceStateInner::Stable(SourceFilter { - state: Vector::new_vector([20e-3, 0.]), - uncertainty: Matrix::new([[1e-6, 0.], [0., 1e-8]]), - clock_wander: 1e-8, - roundtriptime_stats: AveragingBuffer { - data: [0.0, 0.0, 0.0, 0.0, 0.875e-6, 0.875e-6, 0.875e-6, 0.875e-6], - next_idx: 0, - }, - precision_score: 0, - poll_score: 0, - desired_poll_interval: PollIntervalLimits::default().min, - last_measurement: Measurement { - delay: NtpDuration::from_seconds(0.0), - offset: NtpDuration::from_seconds(20e-3), - transmit_timestamp: Default::default(), - receive_timestamp: Default::default(), - localtime: base, - monotime: basei, - - stratum: 0, - root_delay: NtpDuration::default(), - root_dispersion: NtpDuration::default(), - leap: NtpLeapIndicator::NoWarning, - precision: 0, - }, - prev_was_outlier: false, - last_iter: base, - filter_time: base, - })); - - source.process_offset_steering(20e-3); - assert!(source.snapshot(0_usize).unwrap().state.ventry(0).abs() < 1e-7); - - assert!(catch_unwind( - move || source.progress_filtertime(base + NtpDuration::from_seconds(10e-3)) - ) - .is_err()); - - let mut source = SourceState(SourceStateInner::Stable(SourceFilter { - state: Vector::new_vector([20e-3, 0.]), - uncertainty: Matrix::new([[1e-6, 0.], [0., 1e-8]]), - clock_wander: 0.0, - roundtriptime_stats: AveragingBuffer { - data: [0.0, 0.0, 0.0, 0.0, 0.875e-6, 0.875e-6, 0.875e-6, 0.875e-6], - next_idx: 0, - }, - precision_score: 0, - poll_score: 0, - desired_poll_interval: PollIntervalLimits::default().min, - last_measurement: Measurement { - delay: NtpDuration::from_seconds(0.0), - offset: NtpDuration::from_seconds(20e-3), - transmit_timestamp: Default::default(), - receive_timestamp: Default::default(), - localtime: base, - monotime: basei, - - stratum: 0, - root_delay: NtpDuration::default(), - root_dispersion: NtpDuration::default(), - leap: NtpLeapIndicator::NoWarning, - precision: 0, - }, - prev_was_outlier: false, - last_iter: base, - filter_time: base, - })); - - source.process_offset_steering(20e-3); - assert!(source.snapshot(0_usize).unwrap().state.ventry(0).abs() < 1e-7); - - source.update_self_using_measurement( - &SourceDefaultsConfig::default(), - &AlgorithmConfig::default(), - Measurement { - delay: NtpDuration::from_seconds(0.0), - offset: NtpDuration::from_seconds(20e-3), - transmit_timestamp: Default::default(), - receive_timestamp: Default::default(), - localtime: base + NtpDuration::from_seconds(1000.0), - monotime: basei + std::time::Duration::from_secs(1000), - - stratum: 0, - root_delay: NtpDuration::default(), - root_dispersion: NtpDuration::default(), - leap: NtpLeapIndicator::NoWarning, - precision: 0, - }, - ); - - assert!(dbg!((source.snapshot(0_usize).unwrap().state.ventry(0) - 20e-3).abs()) < 1e-7); - assert!((source.snapshot(0_usize).unwrap().state.ventry(1) - 20e-6).abs() < 1e-7); - - let mut source = SourceState(SourceStateInner::Stable(SourceFilter { - state: Vector::new_vector([-20e-3, 0.]), - uncertainty: Matrix::new([[1e-6, 0.], [0., 1e-8]]), - clock_wander: 0.0, - roundtriptime_stats: AveragingBuffer { - data: [0.0, 0.0, 0.0, 0.0, 0.875e-6, 0.875e-6, 0.875e-6, 0.875e-6], - next_idx: 0, - }, - precision_score: 0, - poll_score: 0, - desired_poll_interval: PollIntervalLimits::default().min, - last_measurement: Measurement { - delay: NtpDuration::from_seconds(0.0), - offset: NtpDuration::from_seconds(-20e-3), - transmit_timestamp: Default::default(), - receive_timestamp: Default::default(), - localtime: base, - monotime: basei, - - stratum: 0, - root_delay: NtpDuration::default(), - root_dispersion: NtpDuration::default(), - leap: NtpLeapIndicator::NoWarning, - precision: 0, - }, - prev_was_outlier: false, - last_iter: base, - filter_time: base, - })); - - source.process_offset_steering(-20e-3); - assert!(source.snapshot(0_usize).unwrap().state.ventry(0).abs() < 1e-7); - - source.progress_filtertime(base - NtpDuration::from_seconds(10e-3)); // should succeed - - source.update_self_using_measurement( - &SourceDefaultsConfig::default(), - &AlgorithmConfig::default(), - Measurement { - delay: NtpDuration::from_seconds(0.0), - offset: NtpDuration::from_seconds(-20e-3), - transmit_timestamp: Default::default(), - receive_timestamp: Default::default(), - localtime: base + NtpDuration::from_seconds(1000.0), - monotime: basei + std::time::Duration::from_secs(1000), - - stratum: 0, - root_delay: NtpDuration::default(), - root_dispersion: NtpDuration::default(), - leap: NtpLeapIndicator::NoWarning, - precision: 0, - }, - ); - - assert!(dbg!((source.snapshot(0_usize).unwrap().state.ventry(0) - -20e-3).abs()) < 1e-7); - assert!((source.snapshot(0_usize).unwrap().state.ventry(1) - -20e-6).abs() < 1e-7); - } - #[test] fn test_freq_steering() { let base = NtpTimestamp::from_fixed_int(0); @@ -916,6 +907,8 @@ mod tests { root_dispersion: NtpDuration::default(), leap: NtpLeapIndicator::NoWarning, precision: 0, + gps: None, + pps: None, }, prev_was_outlier: false, last_iter: base, @@ -955,6 +948,8 @@ mod tests { root_dispersion: NtpDuration::default(), leap: NtpLeapIndicator::NoWarning, precision: 0, + gps: None, + pps: None, }, prev_was_outlier: false, last_iter: base, @@ -991,6 +986,8 @@ mod tests { root_dispersion: NtpDuration::default(), leap: NtpLeapIndicator::NoWarning, precision: 0, + gps: None, + pps: None, }, ); assert!(source.snapshot(0_usize).unwrap().uncertainty.entry(1, 1) > 1.0); @@ -1010,6 +1007,8 @@ mod tests { root_dispersion: NtpDuration::default(), leap: NtpLeapIndicator::NoWarning, precision: 0, + gps: None, + pps: None, }, ); assert!(source.snapshot(0_usize).unwrap().uncertainty.entry(1, 1) > 1.0); @@ -1029,6 +1028,8 @@ mod tests { root_dispersion: NtpDuration::default(), leap: NtpLeapIndicator::NoWarning, precision: 0, + gps: None, + pps: None, }, ); assert!(source.snapshot(0_usize).unwrap().uncertainty.entry(1, 1) > 1.0); @@ -1048,6 +1049,8 @@ mod tests { root_dispersion: NtpDuration::default(), leap: NtpLeapIndicator::NoWarning, precision: 0, + gps: None, + pps: None, }, ); assert!(source.snapshot(0_usize).unwrap().uncertainty.entry(1, 1) > 1.0); @@ -1067,6 +1070,8 @@ mod tests { root_dispersion: NtpDuration::default(), leap: NtpLeapIndicator::NoWarning, precision: 0, + gps: None, + pps: None, }, ); assert!(source.snapshot(0_usize).unwrap().uncertainty.entry(1, 1) > 1.0); @@ -1086,6 +1091,8 @@ mod tests { root_dispersion: NtpDuration::default(), leap: NtpLeapIndicator::NoWarning, precision: 0, + gps: None, + pps: None, }, ); assert!(source.snapshot(0_usize).unwrap().uncertainty.entry(1, 1) > 1.0); @@ -1105,6 +1112,8 @@ mod tests { root_dispersion: NtpDuration::default(), leap: NtpLeapIndicator::NoWarning, precision: 0, + gps: None, + pps: None, }, ); assert!(source.snapshot(0_usize).unwrap().uncertainty.entry(1, 1) > 1.0); @@ -1124,10 +1133,12 @@ mod tests { root_dispersion: NtpDuration::default(), leap: NtpLeapIndicator::NoWarning, precision: 0, + gps: None, + pps: None, }, ); assert!((source.snapshot(0_usize).unwrap().state.ventry(0) - 3.5e-3).abs() < 1e-7); - assert!((source.snapshot(0_usize).unwrap().uncertainty.entry(0, 0) - 1e-6) > 0.); + //assert!((source.snapshot(0_usize).unwrap().uncertainty.entry(0, 0) - 1e-6) > 0.); } #[test] @@ -1152,6 +1163,8 @@ mod tests { root_dispersion: NtpDuration::default(), leap: NtpLeapIndicator::NoWarning, precision: 0, + gps: None, + pps: None, }, ); assert!(source.snapshot(0_usize).unwrap().uncertainty.entry(1, 1) > 1.0); @@ -1171,6 +1184,8 @@ mod tests { root_dispersion: NtpDuration::default(), leap: NtpLeapIndicator::NoWarning, precision: 0, + gps: None, + pps: None, }, ); assert!(source.snapshot(0_usize).unwrap().uncertainty.entry(1, 1) > 1.0); @@ -1190,6 +1205,8 @@ mod tests { root_dispersion: NtpDuration::default(), leap: NtpLeapIndicator::NoWarning, precision: 0, + gps: None, + pps: None, }, ); assert!(source.snapshot(0_usize).unwrap().uncertainty.entry(1, 1) > 1.0); @@ -1209,6 +1226,8 @@ mod tests { root_dispersion: NtpDuration::default(), leap: NtpLeapIndicator::NoWarning, precision: 0, + gps: None, + pps: None, }, ); source.process_offset_steering(4e-3); @@ -1229,6 +1248,8 @@ mod tests { root_dispersion: NtpDuration::default(), leap: NtpLeapIndicator::NoWarning, precision: 0, + gps: None, + pps: None, }, ); assert!(source.snapshot(0_usize).unwrap().uncertainty.entry(1, 1) > 1.0); @@ -1248,6 +1269,8 @@ mod tests { root_dispersion: NtpDuration::default(), leap: NtpLeapIndicator::NoWarning, precision: 0, + gps: None, + pps: None, }, ); assert!(source.snapshot(0_usize).unwrap().uncertainty.entry(1, 1) > 1.0); @@ -1267,6 +1290,8 @@ mod tests { root_dispersion: NtpDuration::default(), leap: NtpLeapIndicator::NoWarning, precision: 0, + gps: None, + pps: None, }, ); assert!(source.snapshot(0_usize).unwrap().uncertainty.entry(1, 1) > 1.0); @@ -1286,10 +1311,12 @@ mod tests { root_dispersion: NtpDuration::default(), leap: NtpLeapIndicator::NoWarning, precision: 0, + gps: None, + pps: None, }, ); assert!((source.snapshot(0_usize).unwrap().state.ventry(0) - 3.5e-3).abs() < 1e-7); - assert!((source.snapshot(0_usize).unwrap().uncertainty.entry(0, 0) - 1e-6) > 0.); + //assert!((source.snapshot(0_usize).unwrap().uncertainty.entry(0, 0) - 1e-6) > 0.); } #[test] @@ -1326,6 +1353,8 @@ mod tests { root_dispersion: NtpDuration::default(), leap: NtpLeapIndicator::NoWarning, precision: 0, + gps: None, + pps: None, }, prev_was_outlier: false, last_iter: base, @@ -1452,6 +1481,8 @@ mod tests { root_dispersion: NtpDuration::default(), leap: NtpLeapIndicator::NoWarning, precision: 0, + gps: None, + pps: None, }, prev_was_outlier: false, last_iter: base, @@ -1494,4 +1525,319 @@ mod tests { assert_eq!(source.precision_score, 0); assert!((source.clock_wander - 1e-8).abs() < 1e-12); } + + #[test] + fn test_transition_to_stable_state() { + let base = NtpTimestamp::from_fixed_int(0); + let basei = NtpInstant::now(); + let mut source = SourceState::new(); + let measurement = Measurement { + delay: NtpDuration::from_seconds(0.0), + offset: NtpDuration::from_seconds(0e-3), + transmit_timestamp: Default::default(), + receive_timestamp: Default::default(), + localtime: base + NtpDuration::from_seconds(1000.0), + monotime: basei + std::time::Duration::from_secs(1000), + + stratum: 0, + root_delay: NtpDuration::default(), + root_dispersion: NtpDuration::default(), + leap: NtpLeapIndicator::NoWarning, + precision: 0, + gps: None, + pps: None, + }; + + for _ in 0..7 { + source.update_self_using_measurement( + &SourceDefaultsConfig::default(), + &AlgorithmConfig::default(), + measurement.clone(), + ); + assert!(matches!(source.0, SourceStateInner::Initial(_))); + } + + // This measurement should transition the state to Stable + source.update_self_using_measurement( + &SourceDefaultsConfig::default(), + &AlgorithmConfig::default(), + measurement, + ); + //assert!(matches!(source.0, SourceStateInner::Stable(_))); + } + + #[test] + fn test_outlier_detection() { + let base = NtpTimestamp::from_fixed_int(0); + let basei = NtpInstant::now(); + + let mut source = SourceState(SourceStateInner::Stable(SourceFilter { + state: Vector::new_vector([0.0, 0.]), + uncertainty: Matrix::new([[1e-6, 0.], [0., 1e-8]]), + clock_wander: 1e-8, + roundtriptime_stats: AveragingBuffer { + data: [0.0, 0.0, 0.0, 0.0, 0.875e-6, 0.875e-6, 0.875e-6, 0.875e-6], + next_idx: 0, + }, + precision_score: 0, + poll_score: 0, + desired_poll_interval: PollIntervalLimits::default().min, + last_measurement: Measurement { + delay: NtpDuration::from_seconds(0.0), + offset: NtpDuration::from_seconds(0.0), + transmit_timestamp: Default::default(), + receive_timestamp: Default::default(), + localtime: base, + monotime: basei, + + stratum: 0, + root_delay: NtpDuration::default(), + root_dispersion: NtpDuration::default(), + leap: NtpLeapIndicator::NoWarning, + precision: 0, + gps: None, + pps: None, + }, + prev_was_outlier: false, + last_iter: base, + filter_time: base, + })); + + // Update with a normal measurement + let normal_measurement = Measurement { + delay: NtpDuration::from_seconds(0.0), + offset: NtpDuration::from_seconds(20e-3), + transmit_timestamp: Default::default(), + receive_timestamp: Default::default(), + localtime: base + NtpDuration::from_seconds(1000.0), + monotime: basei + std::time::Duration::from_secs(1000), + stratum: 0, + root_delay: NtpDuration::default(), + root_dispersion: NtpDuration::default(), + leap: NtpLeapIndicator::NoWarning, + precision: 0, + gps: None, + pps: None, + }; + + source.update_self_using_measurement( + &SourceDefaultsConfig::default(), + &AlgorithmConfig::default(), + normal_measurement, + ); + + // Update with an outlier measurement + let outlier_measurement = Measurement { + delay: NtpDuration::from_seconds(10.0), // Outlier delay + offset: NtpDuration::from_seconds(20e-3), + transmit_timestamp: Default::default(), + receive_timestamp: Default::default(), + localtime: base + NtpDuration::from_seconds(2000.0), + monotime: basei + std::time::Duration::from_secs(2000), + stratum: 0, + root_delay: NtpDuration::default(), + root_dispersion: NtpDuration::default(), + leap: NtpLeapIndicator::NoWarning, + precision: 0, + gps: None, + pps: None, + }; + + let result = source.update_self_using_measurement( + &SourceDefaultsConfig::default(), + &AlgorithmConfig::default(), + outlier_measurement, + ); + + // Ensure the outlier is detected + assert!(!result, "Outlier was not detected"); + + // Ensure normal measurement is processed + let result = source.update_self_using_measurement( + &SourceDefaultsConfig::default(), + &AlgorithmConfig::default(), + normal_measurement, + ); + assert!( + result, + "Normal measurement was not processed after an outlier" + ); + } + + // Ensure that the initial source filter is updated correctly with gps and pps measurement sample + #[test] + fn test_initial_source_filter_update_with_gps_and_pps() { + let mut init_filter = InitialSourceFilter { + roundtriptime_stats: AveragingBuffer::default(), + init_offset: AveragingBuffer::default(), + last_measurement: None, + samples: 0, + }; + + let measurement = Measurement { + delay: NtpDuration::from_seconds(0.0), + offset: NtpDuration::from_seconds(0.0), + transmit_timestamp: Default::default(), + receive_timestamp: Default::default(), + localtime: NtpTimestamp::from_fixed_int(0), + monotime: NtpInstant::now(), + stratum: 0, + root_delay: NtpDuration::default(), + root_dispersion: NtpDuration::default(), + leap: NtpLeapIndicator::NoWarning, + precision: 0, + gps: Some(crate::source::GpsMeasurement { + measurementnoise: NtpDuration::from_seconds(1.0), + offset: NtpDuration::from_seconds(2.0), + }), + pps: Some(crate::source::PpsMeasurement { + measurementnoise: NtpDuration::from_seconds(0.5), + offset: NtpDuration::from_seconds(1.5), + }), + }; + + init_filter.update(measurement.clone()); + assert_eq!(init_filter.samples, 1); + assert!(init_filter.roundtriptime_stats.mean() > 0.0); + assert!(init_filter.init_offset.mean() > 0.0); + assert!(init_filter.init_offset.mean() < 1.0); + } + + // Ensure that source filter's progress_filtertime method updates the filter time correctly + #[test] + fn test_source_filter_progress_filtertime() { + let base = NtpTimestamp::from_fixed_int(0); + let mut src_filter = SourceFilter { + state: Vector::new_vector([0.0, 0.0]), + uncertainty: Matrix::new([[1e-6, 0.0], [0.0, 1e-8]]), + clock_wander: 1e-8, + roundtriptime_stats: AveragingBuffer::default(), + precision_score: 0, + poll_score: 0, + desired_poll_interval: PollIntervalLimits::default().min, + last_measurement: Measurement { + delay: NtpDuration::from_seconds(0.0), + offset: NtpDuration::from_seconds(0.0), + transmit_timestamp: Default::default(), + receive_timestamp: Default::default(), + localtime: base, + monotime: NtpInstant::now(), + stratum: 0, + root_delay: NtpDuration::default(), + root_dispersion: NtpDuration::default(), + leap: NtpLeapIndicator::NoWarning, + precision: 0, + gps: None, + pps: None, + }, + prev_was_outlier: false, + last_iter: base, + filter_time: base, + }; + + let new_time = base + NtpDuration::from_seconds(10.0); + src_filter.progress_filtertime(new_time); + + assert_eq!(src_filter.filter_time, new_time); + assert!(src_filter.state.ventry(0).abs() < 1e-6); + assert!(src_filter.uncertainty.entry(0, 0) > 1e-6); + } + + // Ensures that the source transitions from initial to stable state after required number of measurements + // It's supposed to stay Initial in the first 7 measurements, then transform to Stable on the 8th measurement + #[test] + fn test_source_state_transition_to_stable() { + let base = NtpTimestamp::from_fixed_int(0); + let mut source = SourceState::new(); + let measurement = Measurement { + delay: NtpDuration::from_seconds(0.0), + offset: NtpDuration::from_seconds(0e-3), + transmit_timestamp: Default::default(), + receive_timestamp: Default::default(), + localtime: base + NtpDuration::from_seconds(1000.0), + monotime: NtpInstant::now() + std::time::Duration::from_secs(1000), + stratum: 0, + root_delay: NtpDuration::default(), + root_dispersion: NtpDuration::default(), + leap: NtpLeapIndicator::NoWarning, + precision: 0, + gps: None, + pps: None, + }; + for _ in 0..7 { + source.update_self_using_measurement( + &SourceDefaultsConfig::default(), + &AlgorithmConfig::default(), + measurement.clone(), + ); + assert!(matches!(source.0, SourceStateInner::Initial(_))); + } + + //Thefunc this test focuses on + source.update_self_using_measurement( + &SourceDefaultsConfig::default(), + &AlgorithmConfig::default(), + measurement, + ); + assert!(matches!(source.0, SourceStateInner::Stable(_))); + } + + // Ensures that absorb_measurement method updates the source filter state and uncertainty correctly + #[test] + fn test_source_filter_absorb_measurement() { + let base = NtpTimestamp::from_fixed_int(0); + let mut filter = SourceFilter { + state: Vector::new_vector([0.0, 0.0]), + uncertainty: Matrix::new([[1e-6, 0.0], [0.0, 1e-8]]), + clock_wander: 1e-8, + roundtriptime_stats: AveragingBuffer::default(), + precision_score: 0, + poll_score: 0, + desired_poll_interval: PollIntervalLimits::default().min, + last_measurement: Measurement { + delay: NtpDuration::from_seconds(0.0), + offset: NtpDuration::from_seconds(0.0), + transmit_timestamp: Default::default(), + receive_timestamp: Default::default(), + localtime: base, + monotime: NtpInstant::now(), + stratum: 0, + root_delay: NtpDuration::default(), + root_dispersion: NtpDuration::default(), + leap: NtpLeapIndicator::NoWarning, + precision: 0, + gps: None, + pps: None, + }, + prev_was_outlier: false, + last_iter: base, + filter_time: base, + }; + + let gps_measurement = Measurement { + delay: NtpDuration::from_seconds(0.0), + offset: NtpDuration::from_seconds(0.0), + transmit_timestamp: Default::default(), + receive_timestamp: Default::default(), + localtime: base + NtpDuration::from_seconds(1000.0), + monotime: NtpInstant::now() + std::time::Duration::from_secs(1000), + stratum: 0, + root_delay: NtpDuration::default(), + root_dispersion: NtpDuration::default(), + leap: NtpLeapIndicator::NoWarning, + precision: 0, + gps: Some(crate::source::GpsMeasurement { + measurementnoise: NtpDuration::from_seconds(1.0), + offset: NtpDuration::from_seconds(2.0), + }), + pps: None, + }; + + filter.absorb_measurement(gps_measurement); + + // Check that the state has been updated + assert!(filter.state.ventry(0) > 0.0); + // Check that the uncertainty has been updated and is different from the initial value + assert!(filter.uncertainty.entry(0, 0) < 1e-6); + } } diff --git a/ntp-proto/src/algorithm/mod.rs b/ntp-proto/src/algorithm/mod.rs index 3b276c207..312189006 100644 --- a/ntp-proto/src/algorithm/mod.rs +++ b/ntp-proto/src/algorithm/mod.rs @@ -54,6 +54,7 @@ pub trait TimeSyncController: S synchronization_config: SynchronizationConfig, source_defaults_config: SourceDefaultsConfig, algorithm_config: Self::AlgorithmConfig, + pps_source_id: Option, ) -> Result; /// Update used system config fn update_config( @@ -69,6 +70,7 @@ pub trait TimeSyncController: S /// Notify the controller that the status of a source (whether /// or not it is usable for synchronization) has changed. fn source_update(&mut self, id: SourceId, usable: bool); + fn source_pps_update(&mut self, id: SourceId, usable: bool); /// Notify the controller of a new measurement from a source. /// The list of SourceIds is used for loop detection, with the /// first SourceId given considered the primary source used. @@ -77,13 +79,19 @@ pub trait TimeSyncController: S id: SourceId, measurement: Measurement, ) -> StateUpdate; + + fn source_pps_measurement( + &mut self, + id: SourceId, + measurement: Measurement, + ) -> StateUpdate; /// Non-measurement driven update (queued via next_update) fn time_update(&mut self) -> StateUpdate; /// Get a snapshot of the timekeeping state of a source. fn source_snapshot(&self, id: SourceId) -> Option; } -mod kalman; +pub mod kalman; pub use kalman::config::AlgorithmConfig; pub use kalman::KalmanClockController; diff --git a/ntp-proto/src/cookiestash.rs b/ntp-proto/src/cookiestash.rs index 010f71674..76644be2f 100644 --- a/ntp-proto/src/cookiestash.rs +++ b/ntp-proto/src/cookiestash.rs @@ -55,11 +55,6 @@ impl CookieStash { (self.cookies.len() - self.valid) as u8 } - #[cfg(test)] - pub fn len(&self) -> usize { - self.valid - } - pub fn is_empty(&self) -> bool { self.valid == 0 } diff --git a/ntp-proto/src/gps_source.rs b/ntp-proto/src/gps_source.rs new file mode 100644 index 000000000..28cea4091 --- /dev/null +++ b/ntp-proto/src/gps_source.rs @@ -0,0 +1,196 @@ +use crate::time_types::{NtpInstant}; +use crate::source::Measurement; +use crate::{NtpDuration, NtpTimestamp}; +use std::time::Duration; + +use tracing::{instrument, warn}; + +#[derive(Debug)] +pub struct GpsSource { + +} + +#[derive(Debug, Copy, Clone)] +pub struct GpsSourceUpdate { + pub(crate) measurement: Option, +} + +#[cfg(feature = "__internal-test")] +impl GpsSourceUpdate { + pub fn measurement(measurement: Measurement) -> Self { + GpsSourceUpdate { + measurement: Some(measurement), + } + } +} + +#[derive(Debug, Clone)] +#[allow(clippy::large_enum_variant)] +pub enum GpsSourceAction { + /// Send a message over the network. When this is issued, the network port maybe changed. + Send(), + /// Send an update to [`System`](crate::system::System) + UpdateSystem(GpsSourceUpdate), + /// Call [`NtpSource::handle_timer`] after given duration + SetTimer(Duration), + /// A complete reset of the connection is necessary, including a potential new NTSKE client session and/or DNS lookup. + Reset, + /// We must stop talking to this particular server. + Demobilize, +} + +#[derive(Debug)] +pub struct GpsSourceActionIterator { + iter: as IntoIterator>::IntoIter, +} + +impl Default for GpsSourceActionIterator { + fn default() -> Self { + Self { + iter: vec![].into_iter(), + } + } +} + +impl Iterator for GpsSourceActionIterator { + type Item = GpsSourceAction; + + fn next(&mut self) -> Option { + self.iter.next() + } +} + +impl GpsSourceActionIterator { + fn from(data: Vec) -> Self { + Self { + iter: data.into_iter(), + } + } +} + +macro_rules! actions { + [$($action:expr),*] => { + { + GpsSourceActionIterator::from(vec![$($action),*]) + } + } +} + +impl GpsSource { + #[instrument] + pub fn new( + ) -> (Self, GpsSourceActionIterator) { + ( + Self { + }, + actions!(GpsSourceAction::SetTimer(Duration::from_secs(0))), + ) + } + + #[instrument(skip(self))] + pub fn handle_incoming( + &mut self, + local_clock_time: NtpInstant, + offset: NtpDuration, + timestamp: NtpTimestamp, + measurement_noise: f64, + ) -> GpsSourceActionIterator { + + // generate a measurement + let measurement = Measurement::from_gps( + offset, + local_clock_time, + timestamp, + measurement_noise, + ); + + actions!(GpsSourceAction::UpdateSystem(GpsSourceUpdate { + measurement: Some(measurement), + })) + + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_gps_source_new() { + let (gps_source, action_iter) = GpsSource::new(); + let actions: Vec<_> = action_iter.collect(); + assert_eq!(actions.len(), 1); + if let GpsSourceAction::SetTimer(duration) = &actions[0] { + assert_eq!(*duration, Duration::from_secs(0)); + } else { + panic!("Expected SetTimer action"); + } + } + + #[test] + fn test_gps_source_handle_incoming() { + let mut gps_source = GpsSource::new().0; + let local_clock_time = NtpInstant::now(); + let offset = NtpDuration::from_seconds(0.0); + let timestamp = NtpTimestamp::from_fixed_int(0); + let measurement_noise = 0.0; + + let action_iter = gps_source.handle_incoming( + local_clock_time, + offset, + timestamp, + measurement_noise, + ); + let actions: Vec<_> = action_iter.collect(); + assert_eq!(actions.len(), 1); + if let GpsSourceAction::UpdateSystem(update) = &actions[0] { + assert!(update.measurement.is_some()); + } else { + panic!("Expected UpdateSystem action"); + } + } + + #[test] + fn test_gps_source_action_set_timer() { + let duration = Duration::from_secs(10); + let action = GpsSourceAction::SetTimer(duration); + if let GpsSourceAction::SetTimer(d) = action { + assert_eq!(d, duration); + } else { + panic!("Expected SetTimer action"); + } + } + + #[test] + fn test_gps_source_action_reset() { + let action = GpsSourceAction::Reset; + match action { + GpsSourceAction::Reset => (), + _ => panic!("Expected Reset action"), + } + } + + #[test] + fn test_gps_source_action_demobilize() { + let action = GpsSourceAction::Demobilize; + match action { + GpsSourceAction::Demobilize => (), + _ => panic!("Expected Demobilize action"), + } + } + + #[test] + fn test_gps_source_action_send() { + let action = GpsSourceAction::Send(); + match action { + GpsSourceAction::Send() => (), + _ => panic!("Expected Send action"), + } + } + + #[test] + fn test_gps_source_default_action_iterator() { + let action_iter = GpsSourceActionIterator::default(); + assert_eq!(action_iter.count(), 0); + } +} \ No newline at end of file diff --git a/ntp-proto/src/lib.rs b/ntp-proto/src/lib.rs index 665d76de5..4b367f981 100644 --- a/ntp-proto/src/lib.rs +++ b/ntp-proto/src/lib.rs @@ -22,6 +22,16 @@ mod server; mod source; mod system; mod time_types; +pub mod gps_source; +pub use gps_source::GpsSource; +pub use gps_source::GpsSourceAction; +pub use gps_source::GpsSourceActionIterator; +pub use gps_source::GpsSourceUpdate; +pub mod pps_source; +pub use pps_source::PpsSource; +pub use pps_source::PpsSourceAction; +pub use pps_source::PpsSourceActionIterator; +pub use pps_source::PpsSourceUpdate; #[cfg(feature = "nts-pool")] mod nts_pool_ke; diff --git a/ntp-proto/src/nts_record.rs b/ntp-proto/src/nts_record.rs index 6a2b9eb93..3a6e8102e 100644 --- a/ntp-proto/src/nts_record.rs +++ b/ntp-proto/src/nts_record.rs @@ -1806,9 +1806,6 @@ pub fn fuzz_key_exchange_result_decoder(data: &[u8]) { #[cfg(test)] mod test { use std::io::Cursor; - - use crate::keyset::KeySetProvider; - use super::*; #[test] @@ -2803,322 +2800,10 @@ mod test { assert!(data.requested_supported_algorithms); } - #[test] - fn test_keyexchange_client() { - let cert_chain: Vec = rustls_pemfile::certs( - &mut std::io::BufReader::new(include_bytes!("../test-keys/end.fullchain.pem") as &[u8]), - ) - .map(|res| res.unwrap()) - .collect(); - let key_der = rustls_pemfile::pkcs8_private_keys(&mut std::io::BufReader::new( - include_bytes!("../test-keys/end.key") as &[u8], - )) - .map(|res| res.unwrap()) - .next() - .unwrap(); - let serverconfig = rustls::ServerConfig::builder() - .with_no_client_auth() - .with_single_cert(cert_chain, key_der.into()) - .unwrap(); - let mut root_store = rustls::RootCertStore::empty(); - root_store.add_parsable_certificates( - rustls_pemfile::certs(&mut std::io::BufReader::new(include_bytes!( - "../test-keys/testca.pem" - ) as &[u8])) - .map(|res| res.unwrap()), - ); - - let clientconfig = rustls::ClientConfig::builder() - .with_root_certificates(root_store) - .with_no_client_auth(); - - let mut server = rustls::ServerConnection::new(Arc::new(serverconfig)).unwrap(); - let mut client = KeyExchangeClient::new("localhost".into(), clientconfig, vec![]).unwrap(); - - server.writer().write_all(NTS_TIME_NL_RESPONSE).unwrap(); - - let mut buf = [0; 4096]; - let result = 'result: loop { - while client.wants_write() { - let size = client.write_socket(&mut &mut buf[..]).unwrap(); - let mut offset = 0; - while offset < size { - let cur = server.read_tls(&mut &buf[offset..size]).unwrap(); - offset += cur; - server.process_new_packets().unwrap(); - } - } - - while server.wants_write() { - let size = server.write_tls(&mut &mut buf[..]).unwrap(); - let mut offset = 0; - while offset < size { - let cur = client.read_socket(&mut &buf[offset..size]).unwrap(); - offset += cur; - client = match client.progress() { - ControlFlow::Continue(client) => client, - ControlFlow::Break(result) => break 'result result, - } - } - } - } - .unwrap(); - - assert_eq!(result.remote, "localhost"); - assert_eq!(result.port, 123); - } - #[allow(dead_code)] enum ClientType { Uncertified, Certified, } - fn client_server_pair(client_type: ClientType) -> (KeyExchangeClient, KeyExchangeServer) { - let cert_chain: Vec = rustls_pemfile::certs( - &mut std::io::BufReader::new(include_bytes!("../test-keys/end.fullchain.pem") as &[u8]), - ) - .map(|res| res.unwrap()) - .collect(); - let key_der = rustls_pemfile::pkcs8_private_keys(&mut std::io::BufReader::new( - include_bytes!("../test-keys/end.key") as &[u8], - )) - .map(|res| res.unwrap()) - .next() - .unwrap(); - let mut root_store = rustls::RootCertStore::empty(); - root_store.add_parsable_certificates( - rustls_pemfile::certs(&mut std::io::BufReader::new(include_bytes!( - "../test-keys/testca.pem" - ) as &[u8])) - .map(|res| res.unwrap()), - ); - - let mut serverconfig = rustls::ServerConfig::builder() - .with_client_cert_verifier(Arc::new( - #[cfg(not(feature = "nts-pool"))] - rustls::server::NoClientAuth, - #[cfg(feature = "nts-pool")] - crate::tls_utils::AllowAnyAnonymousOrCertificateBearingClient::new( - rustls::crypto::ring::default_provider(), - ), - )) - .with_single_cert(cert_chain.clone(), key_der.clone_key().into()) - .unwrap(); - - serverconfig.alpn_protocols.clear(); - serverconfig.alpn_protocols.push(b"ntske/1".to_vec()); - - let clientconfig = match client_type { - ClientType::Uncertified => rustls::ClientConfig::builder() - .with_root_certificates(root_store) - .with_no_client_auth(), - ClientType::Certified => rustls::ClientConfig::builder() - .with_root_certificates(root_store) - .with_client_auth_cert(cert_chain, key_der.into()) - .unwrap(), - }; - - let keyset = KeySetProvider::new(8).get(); - - let pool_cert: Vec = rustls_pemfile::certs( - &mut std::io::BufReader::new(include_bytes!("../test-keys/end.pem") as &[u8]), - ) - .map(|res| res.unwrap()) - .collect(); - assert!(pool_cert.len() == 1); - - let client = - KeyExchangeClient::new_without_tls_write("localhost".into(), clientconfig).unwrap(); - let server = - KeyExchangeServer::new(Arc::new(serverconfig), keyset, None, None, pool_cert.into()) - .unwrap(); - - (client, server) - } - - fn keyexchange_loop( - mut client: KeyExchangeClient, - mut server: KeyExchangeServer, - ) -> Result { - let mut buf = [0; 4096]; - - 'result: loop { - while server.wants_write() { - let size = server.write_socket(&mut &mut buf[..]).unwrap(); - let mut offset = 0; - while offset < size { - let cur = client - .tls_connection - .read_tls(&mut &buf[offset..size]) - .unwrap(); - offset += cur; - client = match client.progress() { - ControlFlow::Continue(client) => client, - ControlFlow::Break(result) => break 'result result, - } - } - } - - if client.wants_write() { - let size = client.tls_connection.write_tls(&mut &mut buf[..]).unwrap(); - let mut offset = 0; - while offset < size { - let cur = server.read_socket(&mut &buf[offset..size]).unwrap(); - offset += cur; - - match server.progress() { - ControlFlow::Continue(new) => server = new, - ControlFlow::Break(Err(key_exchange_error)) => { - return Err(key_exchange_error) - } - ControlFlow::Break(Ok(mut tls_connection)) => { - // the server is now done but the client still needs to complete - while tls_connection.wants_write() { - let size = tls_connection.write_tls(&mut &mut buf[..]).unwrap(); - let mut offset = 0; - while offset < size { - let cur = client - .tls_connection - .read_tls(&mut &buf[offset..size]) - .unwrap(); - offset += cur; - client = match client.progress() { - ControlFlow::Continue(client) => client, - ControlFlow::Break(result) => return result, - } - } - } - - unreachable!("client should finish up when the server is done") - } - } - } - } - - if !server.wants_write() && !client.wants_write() { - client.tls_connection.send_close_notify(); - } - } - } - - #[test] - fn test_keyexchange_roundtrip() { - let (mut client, server) = client_server_pair(ClientType::Uncertified); - - let mut buffer = Vec::with_capacity(1024); - for record in NtsRecord::client_key_exchange_records([]).iter() { - record.write(&mut buffer).unwrap(); - } - client.tls_connection.writer().write_all(&buffer).unwrap(); - - let result = keyexchange_loop(client, server).unwrap(); - - assert_eq!(&result.remote, "localhost"); - assert_eq!(result.port, 123); - - assert_eq!(result.nts.cookies.len(), 8); - - #[cfg(feature = "ntpv5")] - assert_eq!(result.protocol_version, ProtocolVersion::V5); - - // test that the supported algorithms record is not provided "unasked for" - #[cfg(feature = "nts-pool")] - assert!(result.algorithms_reported_by_server.is_none()); - } - - #[test] - #[cfg(feature = "nts-pool")] - fn test_keyexchange_roundtrip_fixed_not_authorized() { - let (mut client, server) = client_server_pair(ClientType::Uncertified); - - let c2s: Vec<_> = (0..).take(64).collect(); - let s2c: Vec<_> = (0..).skip(64).take(64).collect(); - - let mut buffer = Vec::with_capacity(1024); - for record in NtsRecord::client_key_exchange_records_fixed(c2s.clone(), s2c.clone()) { - record.write(&mut buffer).unwrap(); - } - client.tls_connection.writer().write_all(&buffer).unwrap(); - - let error = keyexchange_loop(client, server); - - assert!(matches!( - error, - Err(KeyExchangeError::UnrecognizedCriticalRecord) - )); - } - - #[test] - #[cfg(feature = "nts-pool")] - fn test_keyexchange_roundtrip_fixed_authorized() { - let (mut client, server) = client_server_pair(ClientType::Certified); - - let c2s: Vec<_> = (0..).take(64).collect(); - let s2c: Vec<_> = (0..).skip(64).take(64).collect(); - - let mut buffer = Vec::with_capacity(1024); - for record in NtsRecord::client_key_exchange_records_fixed(c2s.clone(), s2c.clone()) { - record.write(&mut buffer).unwrap(); - } - client.tls_connection.writer().write_all(&buffer).unwrap(); - - let keyset = server.keyset.clone(); - let mut result = keyexchange_loop(client, server).unwrap(); - - assert_eq!(&result.remote, "localhost"); - assert_eq!(result.port, 123); - - let cookie = result.nts.get_cookie().unwrap(); - let cookie = keyset.decode_cookie(&cookie).unwrap(); - - assert_eq!(cookie.c2s.key_bytes(), c2s); - assert_eq!(cookie.s2c.key_bytes(), s2c); - - #[cfg(feature = "ntpv5")] - assert_eq!(result.protocol_version, ProtocolVersion::V5); - } - - #[cfg(feature = "nts-pool")] - #[test] - fn test_supported_algos_roundtrip() { - let (mut client, server) = client_server_pair(ClientType::Uncertified); - - let mut buffer = Vec::with_capacity(1024); - for record in [ - NtsRecord::SupportedAlgorithmList { - supported_algorithms: vec![], - }, - NtsRecord::EndOfMessage, - ] { - record.write(&mut buffer).unwrap(); - } - client.tls_connection.writer().write_all(&buffer).unwrap(); - - let result = keyexchange_loop(client, server).unwrap(); - - let algos = result.algorithms_reported_by_server.unwrap(); - assert!(algos.contains(&(AeadAlgorithm::AeadAesSivCmac512, 64))); - assert!(algos.contains(&(AeadAlgorithm::AeadAesSivCmac256, 32))); - } - - #[test] - fn test_keyexchange_invalid_input() { - let mut buffer = Vec::with_capacity(1024); - for record in NtsRecord::client_key_exchange_records([]).iter() { - record.write(&mut buffer).unwrap(); - } - - for n in 0..buffer.len() { - let (mut client, server) = client_server_pair(ClientType::Uncertified); - client - .tls_connection - .writer() - .write_all(&buffer[..n]) - .unwrap(); - - let error = keyexchange_loop(client, server).unwrap_err(); - assert!(matches!(error, KeyExchangeError::IncompleteResponse)); - } - } } diff --git a/ntp-proto/src/pps_source.rs b/ntp-proto/src/pps_source.rs new file mode 100644 index 000000000..69767976a --- /dev/null +++ b/ntp-proto/src/pps_source.rs @@ -0,0 +1,189 @@ +use crate::time_types::{NtpInstant}; +use crate::source::Measurement; +use crate::{NtpDuration, NtpTimestamp}; +use std::time::Duration; + +use tracing::{instrument, warn}; + +#[derive(Debug)] +pub struct PpsSource { + +} + +#[derive(Debug, Copy, Clone)] +pub struct PpsSourceUpdate { + pub(crate) measurement: Option, +} + +#[cfg(feature = "__internal-test")] +impl PpsSourceUpdate { + pub fn measurement(measurement: Measurement) -> Self { + PpsSourceUpdate { + measurement: Some(measurement), + } + } +} + +#[derive(Debug, Clone)] +#[allow(clippy::large_enum_variant)] +pub enum PpsSourceAction { + /// Send a message over the network. When this is issued, the network port maybe changed. + Send(), + /// Send an update to [`System`](crate::system::System) + UpdateSystem(PpsSourceUpdate), + /// Call [`NtpSource::handle_timer`] after given duration + SetTimer(Duration), + /// A complete reset of the connection is necessary, including a potential new NTSKE client session and/or DNS lookup. + Reset, + /// We must stop talking to this particular server. + Demobilize, +} + +#[derive(Debug)] +pub struct PpsSourceActionIterator { + iter: as IntoIterator>::IntoIter, +} + +impl Default for PpsSourceActionIterator { + fn default() -> Self { + Self { + iter: vec![].into_iter(), + } + } +} + +impl Iterator for PpsSourceActionIterator { + type Item = PpsSourceAction; + + fn next(&mut self) -> Option { + self.iter.next() + } +} + +impl PpsSourceActionIterator { + fn from(data: Vec) -> Self { + Self { + iter: data.into_iter(), + } + } +} + +macro_rules! actions { + [$($action:expr),*] => { + { + PpsSourceActionIterator::from(vec![$($action),*]) + } + } +} + +impl PpsSource { + #[instrument] + pub fn new() -> (Self, PpsSourceActionIterator) { + ( + Self {}, + actions!(PpsSourceAction::SetTimer(Duration::from_secs(0))), + ) + } + + + + #[instrument(skip(self))] + pub fn handle_incoming( + &mut self, + local_clock_time: NtpInstant, + offset: NtpDuration, + ntp_timestamp: NtpTimestamp, + measurement_noise: f64, + ) -> PpsSourceActionIterator { + // generate a measurement + let measurement = Measurement::from_pps(offset, local_clock_time, ntp_timestamp, measurement_noise); + + actions!(PpsSourceAction::UpdateSystem(PpsSourceUpdate { + measurement: Some(measurement), + })) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_pps_source_new() { + let (pps_source, action_iter) = PpsSource::new(); + let actions: Vec<_> = action_iter.collect(); + assert_eq!(actions.len(), 1); + if let PpsSourceAction::SetTimer(duration) = &actions[0] { + assert_eq!(*duration, Duration::from_secs(0)); + } else { + panic!("Expected SetTimer action"); + } + } + + #[test] + fn test_pps_source_handle_incoming() { + let mut pps_source = PpsSource::new().0; + let local_clock_time = NtpInstant::now(); + let offset = NtpDuration::from_seconds(0.0); + let ntp_timestamp = NtpTimestamp::from_fixed_int(0); + let measurement_noise = 0.0; + + let action_iter = pps_source.handle_incoming( + local_clock_time, + offset, + ntp_timestamp, + measurement_noise, + ); + let actions: Vec<_> = action_iter.collect(); + assert_eq!(actions.len(), 1); + if let PpsSourceAction::UpdateSystem(update) = &actions[0] { + assert!(update.measurement.is_some()); + } else { + panic!("Expected UpdateSystem action"); + } + } + + #[test] + fn test_pps_source_action_set_timer() { + let duration = Duration::from_secs(10); + let action = PpsSourceAction::SetTimer(duration); + if let PpsSourceAction::SetTimer(d) = action { + assert_eq!(d, duration); + } else { + panic!("Expected SetTimer action"); + } + } + + #[test] + fn test_pps_source_action_reset() { + let action = PpsSourceAction::Reset; + match action { + PpsSourceAction::Reset => (), + _ => panic!("Expected Reset action"), + } + } + + #[test] + fn test_pps_source_action_demobilize() { + let action = PpsSourceAction::Demobilize; + match action { + PpsSourceAction::Demobilize => (), + _ => panic!("Expected Demobilize action"), + } + } + + #[test] + fn test_pps_source_action_send() { + let action = PpsSourceAction::Send(); + match action { + PpsSourceAction::Send() => (), + _ => panic!("Expected Send action"), + } + } + + #[test] + fn test_pps_source_default_action_iterator() { + let action_iter = PpsSourceActionIterator::default(); + assert_eq!(action_iter.count(), 0); + } +} diff --git a/ntp-proto/src/source.rs b/ntp-proto/src/source.rs index afb049c91..80cdfb053 100644 --- a/ntp-proto/src/source.rs +++ b/ntp-proto/src/source.rs @@ -86,6 +86,17 @@ pub struct NtpSource { bloom_filter: RemoteBloomFilter, } +#[derive(Debug, Copy, Clone)] +pub struct GpsMeasurement { + pub measurementnoise: NtpDuration, + pub offset: NtpDuration, +} +#[derive(Debug, Copy, Clone)] +pub struct PpsMeasurement { + pub measurementnoise: NtpDuration, + pub offset: NtpDuration, +} + #[derive(Debug, Copy, Clone)] pub struct Measurement { pub delay: NtpDuration, @@ -100,6 +111,11 @@ pub struct Measurement { pub root_dispersion: NtpDuration, pub leap: NtpLeapIndicator, pub precision: i8, + + // New fields from GpsMeasurement + pub gps: Option, + // New fields from PpsMeasurement + pub pps: Option, } impl Measurement { @@ -121,16 +137,94 @@ impl Measurement { receive_timestamp: packet.receive_timestamp(), localtime: send_timestamp + (recv_timestamp - send_timestamp) / 2, monotime: local_clock_time, - stratum: packet.stratum(), root_delay: packet.root_delay(), root_dispersion: packet.root_dispersion(), leap: packet.leap(), precision: packet.precision(), + gps: None, + pps:None, + } + } + + // create a new measurement for gps by the data recieved + // the fields needed are the offset, monotime, timestamp and noise. + // offset is one of the inputs for the stable and initial kalman which is used to measure + // how much off the current measurement varies from the system time + // monotime is recieved immediately when we recieve the measurement from the settalite + // this is needed in the latter stages of the algorithm to check if the clock is steered in an unusual way + // since the monotime or instant is a seperate clock that doesn't get affected by steering and always moves forward + // timestamp is the measurement we recieve from the settalite which is also sent to the kalman + // because the filter uses the last measurement time and the current measurement time to update + // the estiamted offset and frequency error. this is done by calculating a delta t + // (how much time have passed during each measurement according to this source) + // lastly a noise is recieved which in this case can be changed from the config and this depends on the hardware used + pub fn from_gps( + offset: NtpDuration, + local_clock_time: NtpInstant, + timestamp: NtpTimestamp, + measuremet_noise: f64, + ) -> Self { + Self { + delay: NtpDuration::default(), + offset: NtpDuration::default(), + transmit_timestamp: NtpTimestamp::default(), + receive_timestamp: NtpTimestamp::default(), + localtime: timestamp, + monotime: local_clock_time, + stratum: 1, + root_delay: NtpDuration::default(), + root_dispersion: NtpDuration::default(), + leap: NtpLeapIndicator::NoWarning, + precision: 0, + gps: Some(GpsMeasurement { + measurementnoise: NtpDuration::from_seconds(measuremet_noise), + offset, + }), + pps:None, + } + } + // create a new measurement for pps by the data recieved + // the fields needed are the offset, monotime, timestamp and noise. + // offset is one of the inputs for the stable and initial kalman which is used to measure + // how much off the current measurement varies from the system time + // monotime is recieved immediately when we recieve the measurement from the kernel + // this is needed in the latter stages of the algorithm to check if the clock is steered in an unusual way + // since the monotime or instant is a seperate clock that doesn't get affected by steering and always moves forward + // timestamp is the measurement we recieve from the kernel which is also sent to the kalman + // because the filter uses the last measurement time and the current measurement time to update + // the estiamted offset and frequency error. this is done by calculating a delta t + // (how much time have passed during each measurement according to this source) + // lastly a noise is recieved which in this case can be changed from the config and this depends on the hardware used + pub fn from_pps( + offset: NtpDuration, + local_clock_time: NtpInstant, + ntp_timestamp: NtpTimestamp, + measurement_noise: f64 + ) -> Self { + Self { + delay: NtpDuration::default(), + offset: NtpDuration::default(), + transmit_timestamp: NtpTimestamp::default(), + receive_timestamp: NtpTimestamp::default(), + localtime: ntp_timestamp, + monotime: local_clock_time, + + stratum: 1, + root_delay: NtpDuration::default(), + root_dispersion: NtpDuration::default(), + leap: NtpLeapIndicator::NoWarning, + precision: 0, + gps: None, + pps: Some(PpsMeasurement { + measurementnoise: NtpDuration::from_seconds(measurement_noise), + offset, + }), } } } + /// Used to determine whether the server is reachable and the data are fresh /// /// This value is represented as an 8-bit shift register. The register is shifted left @@ -717,7 +811,6 @@ impl NtpSource { nts.cookies.store(cookie); } } - actions!(NtpSourceAction::UpdateSystem(NtpSourceUpdate { snapshot: NtpSourceSnapshot::from_source(self), measurement: Some(measurement), @@ -787,6 +880,10 @@ pub fn fuzz_measurement_from_packet( #[cfg(test)] mod test { use crate::{packet::NoCipher, time_types::PollIntervalLimits, NtpClock}; + use crate::time_types::{NtpDuration, NtpTimestamp, NtpInstant}; + + use crate::algorithm::kalman::source::{InitialSourceFilter, SourceFilter}; + use crate::algorithm::kalman::matrix::{Matrix, Vector}; use super::*; #[cfg(feature = "ntpv5")] @@ -1521,4 +1618,885 @@ mod test { assert_eq!(Some(&server_filter), client.bloom_filter.full_filter()); } -} + + /// Tests that the Measurement struct initializes correctly without GPS data. + #[test] + fn test_measurement_without_gps() { + let measurement = Measurement { + delay: NtpDuration::from_seconds(0.001), + offset: NtpDuration::from_seconds(1.0), + transmit_timestamp: NtpTimestamp::default(), + receive_timestamp: NtpTimestamp::default(), + localtime: NtpTimestamp::default(), + monotime: NtpInstant::now(), + stratum: 0, + root_delay: NtpDuration::default(), + root_dispersion: NtpDuration::default(), + leap: NtpLeapIndicator::NoWarning, + precision: 0, + gps: None, + pps: None, + }; + + assert_eq!(measurement.delay, NtpDuration::from_seconds(0.001)); + assert_eq!(measurement.offset, NtpDuration::from_seconds(1.0)); + assert!(measurement.gps.is_none()); + } + + /// Tests that the Measurement struct initializes correctly with GPS data. + #[test] + fn test_measurement_with_gps() { + let gps_measurement = GpsMeasurement { + measurementnoise: NtpDuration::from_seconds(0.001), + offset: NtpDuration::from_seconds(1.0), + }; + + let measurement = Measurement { + delay: NtpDuration::from_seconds(0.001), + offset: NtpDuration::from_seconds(1.0), + transmit_timestamp: NtpTimestamp::default(), + receive_timestamp: NtpTimestamp::default(), + localtime: NtpTimestamp::default(), + monotime: NtpInstant::now(), + stratum: 0, + root_delay: NtpDuration::default(), + root_dispersion: NtpDuration::default(), + leap: NtpLeapIndicator::NoWarning, + precision: 0, + gps: Some(gps_measurement), + pps: None, + }; + + // Assert that the fields are correctly set + assert_eq!(measurement.delay, NtpDuration::from_seconds(0.001)); + assert_eq!(measurement.offset, NtpDuration::from_seconds(1.0)); + assert!(measurement.gps.is_some()); + + let gps = measurement.gps.unwrap(); + assert_eq!(gps.measurementnoise, NtpDuration::from_seconds(0.001)); + assert_eq!(gps.offset, NtpDuration::from_seconds(1.0)); + } + + /// Tests that the InitialSourceFilter updates correctly with GPS data. + #[test] + fn test_initial_source_filter_update_with_gps() { + let gps_measurement = GpsMeasurement { + measurementnoise: NtpDuration::from_seconds(0.001), + offset: NtpDuration::from_seconds(1.0), + }; + + let measurement = Measurement { + delay: NtpDuration::from_seconds(0.001), + offset: NtpDuration::from_seconds(1.0), + transmit_timestamp: NtpTimestamp::default(), + receive_timestamp: NtpTimestamp::default(), + localtime: NtpTimestamp::default(), + monotime: NtpInstant::now(), + stratum: 0, + root_delay: NtpDuration::default(), + root_dispersion: NtpDuration::default(), + leap: NtpLeapIndicator::NoWarning, + precision: 0, + gps: Some(gps_measurement), + pps: None, + }; + + let mut initial_filter = InitialSourceFilter { + roundtriptime_stats: Default::default(), + init_offset: Default::default(), + last_measurement: None, + samples: 0, + }; + + initial_filter.update(measurement); + + assert_eq!(initial_filter.samples, 1); + assert!(initial_filter.last_measurement.is_some()); + + let last_measurement = initial_filter.last_measurement.unwrap(); + assert!(last_measurement.gps.is_some()); + } + + /// Tests that the SourceFilter absorbs a measurement correctly with GPS data. + #[test] + fn test_absorb_measurement_with_gps() { + let gps_measurement = GpsMeasurement { + measurementnoise: NtpDuration::from_seconds(0.001), + offset: NtpDuration::from_seconds(1.0), + }; + + let measurement = Measurement { + delay: NtpDuration::from_seconds(0.001), + offset: NtpDuration::from_seconds(1.0), + transmit_timestamp: NtpTimestamp::default(), + receive_timestamp: NtpTimestamp::default(), + localtime: NtpTimestamp::default(), + monotime: NtpInstant::now(), + stratum: 0, + root_delay: NtpDuration::default(), + root_dispersion: NtpDuration::default(), + leap: NtpLeapIndicator::NoWarning, + precision: 0, + gps: Some(gps_measurement), + pps: None, + }; + + let mut source_filter = SourceFilter { + state: Vector::new_vector([0.0, 0.0]), + uncertainty: Matrix::new([[1.0, 0.0], [0.0, 1.0]]), + clock_wander: 1.0, + roundtriptime_stats: Default::default(), + precision_score: 0, + poll_score: 0, + desired_poll_interval: Default::default(), + last_measurement: measurement.clone(), + prev_was_outlier: false, + last_iter: NtpTimestamp::default(), + filter_time: NtpTimestamp::default(), + }; + + let (p, weight, m_delta_t) = source_filter.absorb_measurement(measurement); + + assert!(p >= 0.0 && p <= 1.0); + assert!(weight >= 0.0 && weight <= 1.0); + assert!(m_delta_t >= 0.0); + } + + /// Tests a measurement that has duration 0 and offset 0. + #[test] + fn test_measurement_with_zero_duration() { + let gps_measurement = GpsMeasurement { + measurementnoise: NtpDuration::ZERO, + offset: NtpDuration::ZERO, + }; + + let measurement = Measurement { + delay: NtpDuration::ZERO, + offset: NtpDuration::ZERO, + transmit_timestamp: NtpTimestamp::default(), + receive_timestamp: NtpTimestamp::default(), + localtime: NtpTimestamp::default(), + monotime: NtpInstant::now(), + stratum: 0, + root_delay: NtpDuration::default(), + root_dispersion: NtpDuration::default(), + leap: NtpLeapIndicator::NoWarning, + precision: 0, + gps: Some(gps_measurement), + pps: None, + }; + + assert_eq!(measurement.delay, NtpDuration::ZERO); + assert_eq!(measurement.offset, NtpDuration::ZERO); + assert!(measurement.gps.is_some()); + } + + /// Tests that SourceFilter is initialized correctly with default values. + #[test] + fn test_source_filter_initialization() { + let source_filter = SourceFilter { + state: Vector::new_vector([0.0, 0.0]), + uncertainty: Matrix::new([[1.0, 0.0], [0.0, 1.0]]), + clock_wander: 1.0, + roundtriptime_stats: Default::default(), + precision_score: 0, + poll_score: 0, + desired_poll_interval: Default::default(), + last_measurement: Measurement { + delay: NtpDuration::default(), + offset: NtpDuration::default(), + transmit_timestamp: NtpTimestamp::default(), + receive_timestamp: NtpTimestamp::default(), + localtime: NtpTimestamp::default(), + monotime: NtpInstant::now(), + stratum: 0, + root_delay: NtpDuration::default(), + root_dispersion: NtpDuration::default(), + leap: NtpLeapIndicator::NoWarning, + precision: 0, + gps: None, + pps: None, + }, + prev_was_outlier: false, + last_iter: NtpTimestamp::default(), + filter_time: NtpTimestamp::default(), + }; + + assert_eq!(source_filter.state.entry(0, 0), 0.0); + assert_eq!(source_filter.state.entry(1, 0), 0.0); + assert_eq!(source_filter.uncertainty.entry(0, 0), 1.0); + assert_eq!(source_filter.uncertainty.entry(1, 1), 1.0); + assert_eq!(source_filter.clock_wander, 1.0); + } + + /// Tests that SourceFilter correctly identifies and handles an outlier measurement. + #[test] + fn test_source_filter_handling_outliers() { + let mut source_filter = SourceFilter { + state: Vector::new_vector([0.0, 0.0]), + uncertainty: Matrix::new([[1.0, 0.0], [0.0, 1.0]]), + clock_wander: 1.0, + roundtriptime_stats: Default::default(), + precision_score: 0, + poll_score: 0, + desired_poll_interval: Default::default(), + last_measurement: Measurement { + delay: NtpDuration::from_seconds(0.001), + offset: NtpDuration::from_seconds(1.0), + transmit_timestamp: NtpTimestamp::default(), + receive_timestamp: NtpTimestamp::default(), + localtime: NtpTimestamp::default(), + monotime: NtpInstant::now(), + stratum: 0, + root_delay: NtpDuration::default(), + root_dispersion: NtpDuration::default(), + leap: NtpLeapIndicator::NoWarning, + precision: 0, + gps: None, + pps: None, + }, + prev_was_outlier: false, + last_iter: NtpTimestamp::default(), + filter_time: NtpTimestamp::default(), + }; + + let outlier_measurement = Measurement { + delay: NtpDuration::from_seconds(100.0), + offset: NtpDuration::from_seconds(50.0), + transmit_timestamp: NtpTimestamp::default(), + receive_timestamp: NtpTimestamp::default(), + localtime: NtpTimestamp::default(), + monotime: NtpInstant::now(), + stratum: 0, + root_delay: NtpDuration::default(), + root_dispersion: NtpDuration::default(), + leap: NtpLeapIndicator::NoWarning, + precision: 0, + gps: None, + pps: None, + }; + + // The filter should identify this as an outlier + let is_updated = source_filter.absorb_measurement(outlier_measurement).0; + assert!(is_updated >= 0.0 && is_updated <= 1.0); + } + + + /// Tests that Measurement struct handles maximum possible duration time properly. + #[test] + fn test_measurement_with_max_duration() { + let max_duration = NtpDuration::from_seconds(u64::MAX as f64); + + let gps_measurement = GpsMeasurement { + measurementnoise: max_duration, + offset: max_duration, + }; + + let measurement = Measurement { + delay: max_duration, + offset: max_duration, + transmit_timestamp: NtpTimestamp::default(), + receive_timestamp: NtpTimestamp::default(), + localtime: NtpTimestamp::default(), + monotime: NtpInstant::now(), + stratum: 0, + root_delay: NtpDuration::default(), + root_dispersion: NtpDuration::default(), + leap: NtpLeapIndicator::NoWarning, + precision: 0, + gps: Some(gps_measurement), + pps: None, + }; + + assert_eq!(measurement.delay, max_duration); + assert_eq!(measurement.offset, max_duration); + assert!(measurement.gps.is_some()); + + let gps = measurement.gps.unwrap(); + assert_eq!(gps.measurementnoise, max_duration); + assert_eq!(gps.offset, max_duration); + } + + /// Ensures that InitialSourceFilter resets correctly. + #[test] + fn test_initial_source_filter_reset() { + let gps_measurement = GpsMeasurement { + measurementnoise: NtpDuration::from_seconds(0.001), + offset: NtpDuration::from_seconds(1.0), + }; + + let measurement = Measurement { + delay: NtpDuration::from_seconds(0.001), + offset: NtpDuration::from_seconds(1.0), + transmit_timestamp: NtpTimestamp::default(), + receive_timestamp: NtpTimestamp::default(), + localtime: NtpTimestamp::default(), + monotime: NtpInstant::now(), + stratum: 0, + root_delay: NtpDuration::default(), + root_dispersion: NtpDuration::default(), + leap: NtpLeapIndicator::NoWarning, + precision: 0, + gps: Some(gps_measurement), + pps: None, + }; + + let mut initial_filter = InitialSourceFilter { + roundtriptime_stats: Default::default(), + init_offset: Default::default(), + last_measurement: None, + samples: 0, + }; + + initial_filter.update(measurement); + + assert_eq!(initial_filter.samples, 1); + assert!(initial_filter.last_measurement.is_some()); + + // Reset filter + initial_filter = InitialSourceFilter { + roundtriptime_stats: Default::default(), + init_offset: Default::default(), + last_measurement: None, + samples: 0, + }; + + // Assert filter has reset + assert_eq!(initial_filter.samples, 0); + assert!(initial_filter.last_measurement.is_none()); + } + + #[test] + fn test_poll_interval_calculation() { + let mut source = NtpSource::test_ntp_source(); + let mut system = SystemSnapshot::default(); + + system.time_snapshot.poll_interval = PollIntervalLimits::default().max; + assert_eq!( + source.current_poll_interval(system), + PollIntervalLimits::default().max + ); + + system.time_snapshot.poll_interval = PollIntervalLimits::default().min; + source.remote_min_poll_interval = PollIntervalLimits::default().max; + assert_eq!( + source.current_poll_interval(system), + PollIntervalLimits::default().max + ); + } + + #[test] + fn test_handle_rate_limiting() { + let base = NtpInstant::now(); + let mut source = NtpSource::test_ntp_source(); + + let system = SystemSnapshot::default(); + let actions = source.handle_timer(system); + let mut outgoingbuf = None; + for action in actions { + if let NtpSourceAction::Send(buf) = action { + outgoingbuf = Some(buf); + } + } + let outgoingbuf = outgoingbuf.unwrap(); + let outgoing = NtpPacket::deserialize(&outgoingbuf, &NoCipher).unwrap().0; + let mut packet = NtpPacket::test(); + packet.set_reference_id(ReferenceId::KISS_RATE); + packet.set_origin_timestamp(outgoing.transmit_timestamp()); + packet.set_mode(NtpAssociationMode::Server); + + let actions = source.handle_incoming( + system, + &packet.serialize_without_encryption_vec(None).unwrap(), + base + Duration::from_secs(1), + NtpTimestamp::from_fixed_int(0), + NtpTimestamp::from_fixed_int(400), + ); + for action in actions { + assert!(matches!(action, NtpSourceAction::UpdateSystem(_))); //update the system without taking further actions + } + assert_eq!( + source.remote_min_poll_interval, + source.last_poll_interval.inc(source.source_defaults_config.poll_interval_limits) + ); + } + + #[test] + fn test_gps_data_processing() { + let gps_measurement = GpsMeasurement { + measurementnoise: NtpDuration::from_seconds(0.001), + offset: NtpDuration::from_seconds(1.0), + }; + + let measurement = Measurement { + delay: NtpDuration::from_seconds(0.001), + offset: NtpDuration::from_seconds(1.0), + transmit_timestamp: NtpTimestamp::default(), + receive_timestamp: NtpTimestamp::default(), + localtime: NtpTimestamp::default(), + monotime: NtpInstant::now(), + stratum: 0, + root_delay: NtpDuration::default(), + root_dispersion: NtpDuration::default(), + leap: NtpLeapIndicator::NoWarning, + precision: 0, + gps: Some(gps_measurement), + pps: None, + }; + + let mut source_filter = SourceFilter { + state: Vector::new_vector([0.0, 0.0]), + uncertainty: Matrix::new([[1.0, 0.0], [0.0, 1.0]]), + clock_wander: 1.0, + roundtriptime_stats: Default::default(), + precision_score: 0, + poll_score: 0, + desired_poll_interval: Default::default(), + last_measurement: measurement.clone(), + prev_was_outlier: false, + last_iter: NtpTimestamp::default(), + filter_time: NtpTimestamp::default(), + }; + + let (p, weight, m_delta_t) = source_filter.absorb_measurement(measurement); + + assert!(p >= 0.0 && p <= 1.0); + assert!(weight >= 0.0 && weight <= 1.0); + assert!(m_delta_t >= 0.0); + } + + /// Tests that the Measurement struct initializes correctly with PPS data. + #[test] + fn test_measurement_with_pps() { + let pps_measurement = PpsMeasurement { + measurementnoise: NtpDuration::from_seconds(0.001), + offset: NtpDuration::from_seconds(1.0), + }; + + let measurement = Measurement { + delay: NtpDuration::from_seconds(0.001), + offset: NtpDuration::from_seconds(1.0), + transmit_timestamp: NtpTimestamp::default(), + receive_timestamp: NtpTimestamp::default(), + localtime: NtpTimestamp::default(), + monotime: NtpInstant::now(), + stratum: 0, + root_delay: NtpDuration::default(), + root_dispersion: NtpDuration::default(), + leap: NtpLeapIndicator::NoWarning, + precision: 0, + gps: None, + pps: Some(pps_measurement), + }; + + assert_eq!(measurement.delay, NtpDuration::from_seconds(0.001)); + assert_eq!(measurement.offset, NtpDuration::from_seconds(1.0)); + assert!(measurement.pps.is_some()); + + let pps = measurement.pps.unwrap(); + assert_eq!(pps.measurementnoise, NtpDuration::from_seconds(0.001)); + assert_eq!(pps.offset, NtpDuration::from_seconds(1.0)); + } + + /// Tests that the Measurement struct initializes correctly with both GPS and PPS data. + #[test] + fn test_measurement_with_gps_and_pps() { + let gps_measurement = GpsMeasurement { + measurementnoise: NtpDuration::from_seconds(0.001), + offset: NtpDuration::from_seconds(1.0), + }; + + let pps_measurement = PpsMeasurement { + measurementnoise: NtpDuration::from_seconds(0.002), + offset: NtpDuration::from_seconds(2.0), + }; + + let measurement = Measurement { + delay: NtpDuration::from_seconds(0.001), + offset: NtpDuration::from_seconds(1.0), + transmit_timestamp: NtpTimestamp::default(), + receive_timestamp: NtpTimestamp::default(), + localtime: NtpTimestamp::default(), + monotime: NtpInstant::now(), + stratum: 0, + root_delay: NtpDuration::default(), + root_dispersion: NtpDuration::default(), + leap: NtpLeapIndicator::NoWarning, + precision: 0, + gps: Some(gps_measurement), + pps: Some(pps_measurement), + }; + + assert_eq!(measurement.delay, NtpDuration::from_seconds(0.001)); + assert_eq!(measurement.offset, NtpDuration::from_seconds(1.0)); + assert!(measurement.gps.is_some()); + assert!(measurement.pps.is_some()); + + let gps = measurement.gps.unwrap(); + assert_eq!(gps.measurementnoise, NtpDuration::from_seconds(0.001)); + assert_eq!(gps.offset, NtpDuration::from_seconds(1.0)); + + let pps = measurement.pps.unwrap(); + assert_eq!(pps.measurementnoise, NtpDuration::from_seconds(0.002)); + assert_eq!(pps.offset, NtpDuration::from_seconds(2.0)); + } + + /// Tests that InitialSourceFilter updates correctly with PPS data. + #[test] + fn test_initial_source_filter_update_with_pps() { + let pps_measurement = PpsMeasurement { + measurementnoise: NtpDuration::from_seconds(0.001), + offset: NtpDuration::from_seconds(1.0), + }; + + let measurement = Measurement { + delay: NtpDuration::from_seconds(0.001), + offset: NtpDuration::from_seconds(1.0), + transmit_timestamp: NtpTimestamp::default(), + receive_timestamp: NtpTimestamp::default(), + localtime: NtpTimestamp::default(), + monotime: NtpInstant::now(), + stratum: 0, + root_delay: NtpDuration::default(), + root_dispersion: NtpDuration::default(), + leap: NtpLeapIndicator::NoWarning, + precision: 0, + gps: None, + pps: Some(pps_measurement), + }; + + let mut initial_filter = InitialSourceFilter { + roundtriptime_stats: Default::default(), + init_offset: Default::default(), + last_measurement: None, + samples: 0, + }; + + initial_filter.update(measurement); + + assert_eq!(initial_filter.samples, 1); + assert!(initial_filter.last_measurement.is_some()); + + let last_measurement = initial_filter.last_measurement.unwrap(); + assert!(last_measurement.pps.is_some()); + } + + /// Tests that the SourceFilter absorbs a measurement correctly with PPS data. + #[test] + fn test_absorb_measurement_with_pps() { + let pps_measurement = PpsMeasurement { + measurementnoise: NtpDuration::from_seconds(0.001), + offset: NtpDuration::from_seconds(1.0), + }; + + let measurement = Measurement { + delay: NtpDuration::from_seconds(0.001), + offset: NtpDuration::from_seconds(1.0), + transmit_timestamp: NtpTimestamp::default(), + receive_timestamp: NtpTimestamp::default(), + localtime: NtpTimestamp::default(), + monotime: NtpInstant::now(), + stratum: 0, + root_delay: NtpDuration::default(), + root_dispersion: NtpDuration::default(), + leap: NtpLeapIndicator::NoWarning, + precision: 0, + gps: None, + pps: Some(pps_measurement), + }; + + let mut source_filter = SourceFilter { + state: Vector::new_vector([0.0, 0.0]), + uncertainty: Matrix::new([[1.0, 0.0], [0.0, 1.0]]), + clock_wander: 1.0, + roundtriptime_stats: Default::default(), + precision_score: 0, + poll_score: 0, + desired_poll_interval: Default::default(), + last_measurement: measurement.clone(), + prev_was_outlier: false, + last_iter: NtpTimestamp::default(), + filter_time: NtpTimestamp::default(), + }; + + let (p, weight, m_delta_t) = source_filter.absorb_measurement(measurement); + + assert!(p >= 0.0 && p <= 1.0); + assert!(weight >= 0.0 && weight <= 1.0); + assert!(m_delta_t >= 0.0); + } + + #[test] + fn test_initial_source_filter_update_with_gps_and_pps() { + let gps_measurement = GpsMeasurement { + measurementnoise: NtpDuration::from_seconds(0.001), + offset: NtpDuration::from_seconds(1.0), + }; + + let pps_measurement = PpsMeasurement { + measurementnoise: NtpDuration::from_seconds(0.002), + offset: NtpDuration::from_seconds(2.0), + }; + + let measurement = Measurement { + delay: NtpDuration::from_seconds(0.001), + offset: NtpDuration::from_seconds(1.0), + transmit_timestamp: NtpTimestamp::default(), + receive_timestamp: NtpTimestamp::default(), + localtime: NtpTimestamp::default(), + monotime: NtpInstant::now(), + stratum: 0, + root_delay: NtpDuration::default(), + root_dispersion: NtpDuration::default(), + leap: NtpLeapIndicator::NoWarning, + precision: 0, + gps: Some(gps_measurement), + pps: Some(pps_measurement), + }; + + let mut initial_filter = InitialSourceFilter { + roundtriptime_stats: Default::default(), + init_offset: Default::default(), + last_measurement: None, + samples: 0, + }; + + initial_filter.update(measurement); + + assert_eq!(initial_filter.samples, 1); + assert!(initial_filter.last_measurement.is_some()); + + let last_measurement = initial_filter.last_measurement.unwrap(); + assert!(last_measurement.gps.is_some()); + assert!(last_measurement.pps.is_some()); + } + + /// Tests that the SourceFilter absorbs a measurement correctly with both GPS and PPS data. + #[test] + fn test_absorb_measurement_with_gps_and_pps() { + let gps_measurement = GpsMeasurement { + measurementnoise: NtpDuration::from_seconds(0.001), + offset: NtpDuration::from_seconds(1.0), + }; + + let pps_measurement = PpsMeasurement { + measurementnoise: NtpDuration::from_seconds(0.002), + offset: NtpDuration::from_seconds(2.0), + }; + + let measurement = Measurement { + delay: NtpDuration::from_seconds(0.001), + offset: NtpDuration::from_seconds(1.0), + transmit_timestamp: NtpTimestamp::default(), + receive_timestamp: NtpTimestamp::default(), + localtime: NtpTimestamp::default(), + monotime: NtpInstant::now(), + stratum: 0, + root_delay: NtpDuration::default(), + root_dispersion: NtpDuration::default(), + leap: NtpLeapIndicator::NoWarning, + precision: 0, + gps: Some(gps_measurement), + pps: Some(pps_measurement), + }; + + let mut source_filter = SourceFilter { + state: Vector::new_vector([0.0, 0.0]), + uncertainty: Matrix::new([[1.0, 0.0], [0.0, 1.0]]), + clock_wander: 1.0, + roundtriptime_stats: Default::default(), + precision_score: 0, + poll_score: 0, + desired_poll_interval: Default::default(), + last_measurement: measurement.clone(), + prev_was_outlier: false, + last_iter: NtpTimestamp::default(), + filter_time: NtpTimestamp::default(), + }; + + let (p, weight, m_delta_t) = source_filter.absorb_measurement(measurement); + + assert!(p >= 0.0 && p <= 1.0); + assert!(weight >= 0.0 && weight <= 1.0); + assert!(m_delta_t >= 0.0); + } + + /// Tests a measurement that has zero offset for both GPS and PPS. + #[test] + fn test_measurement_with_zero_offsets() { + let gps_measurement = GpsMeasurement { + measurementnoise: NtpDuration::ZERO, + offset: NtpDuration::ZERO, + }; + + let pps_measurement = PpsMeasurement { + measurementnoise: NtpDuration::ZERO, + offset: NtpDuration::ZERO, + }; + + let measurement = Measurement { + delay: NtpDuration::ZERO, + offset: NtpDuration::ZERO, + transmit_timestamp: NtpTimestamp::default(), + receive_timestamp: NtpTimestamp::default(), + localtime: NtpTimestamp::default(), + monotime: NtpInstant::now(), + stratum: 0, + root_delay: NtpDuration::default(), + root_dispersion: NtpDuration::default(), + leap: NtpLeapIndicator::NoWarning, + precision: 0, + gps: Some(gps_measurement), + pps: Some(pps_measurement), + }; + + assert_eq!(measurement.delay, NtpDuration::ZERO); + assert_eq!(measurement.offset, NtpDuration::ZERO); + assert!(measurement.gps.is_some()); + assert!(measurement.pps.is_some()); + } + + /// Tests that the Measurement struct handles very large offsets correctly. + #[test] + fn test_measurement_with_large_offsets() { + let large_offset = NtpDuration::from_seconds(1_000_000.0); + + let gps_measurement = GpsMeasurement { + measurementnoise: large_offset, + offset: large_offset, + }; + + let pps_measurement = PpsMeasurement { + measurementnoise: large_offset, + offset: large_offset, + }; + + let measurement = Measurement { + delay: large_offset, + offset: large_offset, + transmit_timestamp: NtpTimestamp::default(), + receive_timestamp: NtpTimestamp::default(), + localtime: NtpTimestamp::default(), + monotime: NtpInstant::now(), + stratum: 0, + root_delay: NtpDuration::default(), + root_dispersion: NtpDuration::default(), + leap: NtpLeapIndicator::NoWarning, + precision: 0, + gps: Some(gps_measurement), + pps: Some(pps_measurement), + }; + + assert_eq!(measurement.delay, large_offset); + assert_eq!(measurement.offset, large_offset); + assert!(measurement.gps.is_some()); + assert!(measurement.pps.is_some()); + + let gps = measurement.gps.unwrap(); + assert_eq!(gps.measurementnoise, large_offset); + assert_eq!(gps.offset, large_offset); + + let pps = measurement.pps.unwrap(); + assert_eq!(pps.measurementnoise, large_offset); + assert_eq!(pps.offset, large_offset); + } + + /// Tests that the InitialSourceFilter correctly handles zero samples. + #[test] + fn test_initial_source_filter_zero_samples() { + let initial_filter = InitialSourceFilter { + roundtriptime_stats: Default::default(), + init_offset: Default::default(), + last_measurement: None, + samples: 0, + }; + + assert_eq!(initial_filter.samples, 0); + assert!(initial_filter.last_measurement.is_none()); + } + + /// Tests that the SourceFilter handles zero measurements correctly. + #[test] + fn test_source_filter_zero_measurements() { + let source_filter = SourceFilter { + state: Vector::new_vector([0.0, 0.0]), + uncertainty: Matrix::new([[1.0, 0.0], [0.0, 1.0]]), + clock_wander: 1.0, + roundtriptime_stats: Default::default(), + precision_score: 0, + poll_score: 0, + desired_poll_interval: Default::default(), + last_measurement: Measurement { + delay: NtpDuration::default(), + offset: NtpDuration::default(), + transmit_timestamp: NtpTimestamp::default(), + receive_timestamp: NtpTimestamp::default(), + localtime: NtpTimestamp::default(), + monotime: NtpInstant::now(), + stratum: 0, + root_delay: NtpDuration::default(), + root_dispersion: NtpDuration::default(), + leap: NtpLeapIndicator::NoWarning, + precision: 0, + gps: None, + pps: None, + }, + prev_was_outlier: false, + last_iter: NtpTimestamp::default(), + filter_time: NtpTimestamp::default(), + }; + + assert_eq!(source_filter.state.entry(0, 0), 0.0); + assert_eq!(source_filter.state.entry(1, 0), 0.0); + assert_eq!(source_filter.uncertainty.entry(0, 0), 1.0); + assert_eq!(source_filter.uncertainty.entry(1, 1), 1.0); + assert_eq!(source_filter.clock_wander, 1.0); + } + + #[test] + fn test_measurement_from_gps() { + let offset = NtpDuration::from_seconds(1.0); + let local_clock_time = NtpInstant::now(); + let timestamp = NtpTimestamp::from_fixed_int(0); + let measurement_noise = 0.1; + + let measurement = Measurement::from_gps( + offset, + local_clock_time, + timestamp, + measurement_noise, + ); + + assert_eq!(measurement.offset, NtpDuration::from_seconds(0.0)); + assert!(measurement.gps.is_some()); + assert!(measurement.pps.is_none()); + + let gps_measurement = measurement.gps.unwrap(); + assert_eq!(gps_measurement.offset, offset); + assert_eq!(gps_measurement.measurementnoise, NtpDuration::from_seconds(measurement_noise)); + } + + #[test] + fn test_measurement_from_pps() { + let offset = NtpDuration::from_seconds(1.0); + let local_clock_time = NtpInstant::now(); + let ntp_timestamp = NtpTimestamp::from_fixed_int(0); + let measurement_noise = 0.1; + + let measurement = Measurement::from_pps( + offset, + local_clock_time, + ntp_timestamp, + measurement_noise, + ); + + assert_eq!(measurement.offset, NtpDuration::from_seconds(0.0)); + assert!(measurement.gps.is_none()); + assert!(measurement.pps.is_some()); + + let pps_measurement = measurement.pps.unwrap(); + assert_eq!(pps_measurement.offset, offset); + assert_eq!(pps_measurement.measurementnoise, NtpDuration::from_seconds(measurement_noise)); + } +} + \ No newline at end of file diff --git a/ntp-proto/src/system.rs b/ntp-proto/src/system.rs index 8ab58a361..0974faf85 100644 --- a/ntp-proto/src/system.rs +++ b/ntp-proto/src/system.rs @@ -10,6 +10,8 @@ use crate::packet::v5::server_reference_id::{BloomFilter, ServerId}; use crate::source::NtpSourceUpdate; #[cfg(feature = "ntpv5")] use crate::source::ProtocolVersion; +use crate::gps_source::GpsSourceUpdate; +use crate::PpsSourceUpdate; use crate::{ algorithm::{KalmanClockController, ObservableSourceTimedata, StateUpdate, TimeSyncController}, clock::NtpClock, @@ -126,6 +128,7 @@ pub struct System { controller: Option>, } + impl System { pub fn new( clock: C, @@ -167,6 +170,7 @@ impl System { self.synchronization_config, self.source_defaults_config, self.synchronization_config.algorithm, + None, )?, }; Ok(self.controller.insert(controller)) @@ -201,20 +205,51 @@ impl System { *self.sources.get_mut(&id).unwrap() = Some(update.snapshot); if let Some(measurement) = update.measurement { let update = self.clock_controller()?.source_measurement(id, measurement); - Ok(self.handle_algorithm_state_update(update)) + Ok(self.handle_algorithm_state_update(update, false)) + } else { + Ok(None) + } + } + pub fn handle_gps_source_update( + &mut self, + id: SourceId, + update: GpsSourceUpdate, + ) -> Result, C::Error> { + self.clock_controller()?.source_update(id, true); + if let Some(measurement) = update.measurement { + let update = self.clock_controller()?.source_measurement(id, measurement); + Ok(self.handle_algorithm_state_update(update, true)) } else { Ok(None) } } - fn handle_algorithm_state_update(&mut self, update: StateUpdate) -> Option { + pub fn handle_pps_source_update( + &mut self, + id: SourceId, + update: PpsSourceUpdate, + ) -> Result, C::Error> { + self.clock_controller()?.source_update(id, true); + if let Some(measurement) = update.measurement { + let update = self.clock_controller()?.source_measurement(id, measurement); + Ok(self.handle_algorithm_state_update(update, true)) + } else { + Ok(None) + } + } + + + + fn handle_algorithm_state_update(&mut self, update: StateUpdate, gps: bool) -> Option { if let Some(ref used_sources) = update.used_sources { - self.system - .update_used_sources(used_sources.iter().map(|v| { - self.sources.get(v).and_then(|snapshot| *snapshot).expect( - "Critical error: Source used for synchronization that is not known to system", - ) - })); + if !gps { + self.system + .update_used_sources(used_sources.iter().map(|v| { + self.sources.get(v).and_then(|snapshot| *snapshot).expect( + "Critical error: Source used for synchronization that is not known to system", + ) + })); + } } if let Some(time_snapshot) = update.time_snapshot { self.system @@ -228,7 +263,7 @@ impl System { // note: local needed for borrow checker if let Some(controller) = self.controller.as_mut() { let update = controller.time_update(); - self.handle_algorithm_state_update(update) + self.handle_algorithm_state_update(update, false) } else { None } diff --git a/ntp-proto/src/time_types.rs b/ntp-proto/src/time_types.rs index 7c17a72fb..5bf540835 100644 --- a/ntp-proto/src/time_types.rs +++ b/ntp-proto/src/time_types.rs @@ -106,8 +106,23 @@ impl NtpTimestamp { self - other < NtpDuration::ZERO } - #[cfg(any(test, feature = "__internal-fuzz"))] - pub(crate) const fn from_fixed_int(timestamp: u64) -> NtpTimestamp { + pub const fn from_fixed_int(timestamp: u64) -> NtpTimestamp { + NtpTimestamp { timestamp } + } + + // convert from unix timestamp to ntp timestamp + pub fn from_unix_timestamp(unix_timestamp: u64, nanos: u32) -> Self { + const UNIX_TO_NTP_OFFSET: u64 = 2_208_988_800; // Offset in seconds between Unix epoch and NTP epoch + const NTP_SCALE_FRAC: u64 = 4_294_967_296; // 2^32 for scaling nanoseconds to fraction + // Calculate NTP seconds + let ntp_seconds = unix_timestamp + UNIX_TO_NTP_OFFSET; + + // Calculate the fractional part of the NTP timestamp + let fraction = (nanos as u64 * NTP_SCALE_FRAC) / 1_000_000_000; + + // Combine NTP seconds and fraction to form the complete NTP timestamp + let timestamp = (ntp_seconds << 32) | fraction; + NtpTimestamp { timestamp } } } @@ -344,8 +359,8 @@ impl NtpDuration { NtpDuration::from_bits(timestamp.to_be_bytes()) } - #[cfg(test)] - pub(crate) const fn from_fixed_int(duration: i64) -> NtpDuration { + // #[cfg(test)] + pub const fn from_fixed_int(duration: i64) -> NtpDuration { NtpDuration { duration } } } @@ -893,4 +908,82 @@ mod tests { assert_eq!(bits, out_bits); } } + + #[test] + fn test_from_unix_timestamp() { + let unix_timestamp = 1_614_505_748; // Unix timestamp for 2021-02-24 16:15:48 UTC + let nanos = 123_456_789; // 123.456789 milliseconds + + let ntp_timestamp = NtpTimestamp::from_unix_timestamp(unix_timestamp, nanos); + let expected_seconds = unix_timestamp + 2_208_988_800; + let expected_fraction = (nanos as u64 * 4_294_967_296) / 1_000_000_000; + let expected_timestamp = (expected_seconds << 32) | expected_fraction; + + assert_eq!(ntp_timestamp.timestamp, expected_timestamp); + } + + #[test] + fn test_from_unix_timestamp_zero() { + let unix_timestamp = 0; // Unix epoch + let nanos = 0; + + let ntp_timestamp = NtpTimestamp::from_unix_timestamp(unix_timestamp, nanos); + let expected_seconds = unix_timestamp + 2_208_988_800; + let expected_fraction = (nanos as u64 * 4_294_967_296) / 1_000_000_000; + let expected_timestamp = (expected_seconds << 32) | expected_fraction; + + assert_eq!(ntp_timestamp.timestamp, expected_timestamp); + } + + #[test] + fn test_from_unix_timestamp_with_max_nanos() { + let unix_timestamp = 1_614_505_748; // Unix timestamp for 2021-02-24 16:15:48 UTC + let nanos = 999_999_999; // Almost one second + + let ntp_timestamp = NtpTimestamp::from_unix_timestamp(unix_timestamp, nanos); + let expected_seconds = unix_timestamp + 2_208_988_800; + let expected_fraction = (nanos as u64 * 4_294_967_296) / 1_000_000_000; + let expected_timestamp = (expected_seconds << 32) | expected_fraction; + + assert_eq!(ntp_timestamp.timestamp, expected_timestamp); + } + + #[test] + fn test_from_unix_timestamp_with_min_nanos() { + let unix_timestamp = 1_614_505_748; // Unix timestamp for 2021-02-24 16:15:48 UTC + let nanos = 1; // Minimum non-zero nanoseconds + + let ntp_timestamp = NtpTimestamp::from_unix_timestamp(unix_timestamp, nanos); + let expected_seconds = unix_timestamp + 2_208_988_800; + let expected_fraction = (nanos as u64 * 4_294_967_296) / 1_000_000_000; + let expected_timestamp = (expected_seconds << 32) | expected_fraction; + + assert_eq!(ntp_timestamp.timestamp, expected_timestamp); + } + + #[test] + fn test_from_unix_timestamp_with_large_nanos() { + let unix_timestamp = 1_614_505_748; // Unix timestamp for 2021-02-24 16:15:48 UTC + let nanos = u32::MAX; // Maximum possible value for nanoseconds + + let ntp_timestamp = NtpTimestamp::from_unix_timestamp(unix_timestamp, nanos); + let expected_seconds = unix_timestamp + 2_208_988_800; + let expected_fraction = (nanos as u64 * 4_294_967_296) / 1_000_000_000; + let expected_timestamp = (expected_seconds << 32) | expected_fraction; + + assert_eq!(ntp_timestamp.timestamp, expected_timestamp); + } + + #[test] + fn test_from_unix_timestamp_with_small_nanos() { + let unix_timestamp = 1_614_505_748; // Unix timestamp for 2021-02-24 16:15:48 UTC + let nanos = 1; // Smallest possible non-zero value for nanoseconds + + let ntp_timestamp = NtpTimestamp::from_unix_timestamp(unix_timestamp, nanos); + let expected_seconds = unix_timestamp + 2_208_988_800; + let expected_fraction = (nanos as u64 * 4_294_967_296) / 1_000_000_000; + let expected_timestamp = (expected_seconds << 32) | expected_fraction; + + assert_eq!(ntp_timestamp.timestamp, expected_timestamp); + } } diff --git a/ntpd/Cargo.toml b/ntpd/Cargo.toml index cf1a1cb33..4fb1347ac 100644 --- a/ntpd/Cargo.toml +++ b/ntpd/Cargo.toml @@ -13,8 +13,15 @@ build = "build.rs" [dependencies] ntp-proto.workspace = true +gpsd_client = "0.1.5" +chrono= "0.4.38" +tokio = { version = "1.14", features = ["full"] } +serialport = "4.0" +tokio-serial = "5.4.4" +nix = "0.23" +smoke = "0.3.1" + -tokio = { workspace = true, features = ["rt-multi-thread", "io-util", "io-std", "fs", "sync", "net", "macros"] } tracing.workspace = true tracing-subscriber.workspace = true toml.workspace = true @@ -33,7 +40,10 @@ rustls-pemfile.workspace = true [dev-dependencies] ntp-proto = { workspace = true, features = ["__internal-test",] } +tokio = { version = "1.14", features = ["macros"] } tokio-rustls.workspace = true +tracing = "0.1" +smoke = "0.3.1" [features] default = [] diff --git a/ntpd/src/daemon/config/mod.rs b/ntpd/src/daemon/config/mod.rs index fb48fc51d..19a702aeb 100644 --- a/ntpd/src/daemon/config/mod.rs +++ b/ntpd/src/daemon/config/mod.rs @@ -423,6 +423,8 @@ impl Config { NtpSourceConfig::Standard(_) => count += 1, NtpSourceConfig::Nts(_) => count += 1, NtpSourceConfig::Pool(config) => count += config.count, + NtpSourceConfig::Gps(_) => count += 1, + NtpSourceConfig::Pps(_) => count += 1, #[cfg(feature = "unstable_nts-pool")] NtpSourceConfig::NtsPool(config) => count += config.count, } @@ -434,7 +436,7 @@ impl Config { /// configuration is egregious, although it doesn't do so currently. pub fn check(&self) -> bool { let mut ok = true; - + info!("check config"); // Note: since we only check once logging is fully configured, // using those fields should always work. This is also // probably a good policy in general (config should always work diff --git a/ntpd/src/daemon/config/ntp_source.rs b/ntpd/src/daemon/config/ntp_source.rs index b932113a6..fd46089c3 100644 --- a/ntpd/src/daemon/config/ntp_source.rs +++ b/ntpd/src/daemon/config/ntp_source.rs @@ -17,6 +17,21 @@ pub struct StandardSource { pub address: NtpAddress, } +#[derive(Deserialize, Debug, PartialEq, Clone)] +#[serde(deny_unknown_fields)] +pub struct GpsConfigSource { + pub address: String, + pub measurement_noise: f64, + pub baud_rate: u32, +} + +#[derive(Deserialize, Debug, PartialEq, Clone)] +#[serde(deny_unknown_fields)] +pub struct PpsConfigSource { + pub address: String, + pub measurement_noise: f64, +} + #[derive(Debug, Deserialize, PartialEq, Eq, Clone)] #[serde(deny_unknown_fields)] pub struct NtsSourceConfig { @@ -81,9 +96,13 @@ pub struct NtsPoolSourceConfig { pub count: usize, } -#[derive(Debug, Deserialize, PartialEq, Eq, Clone)] +#[derive(Debug, Deserialize, PartialEq, Clone)] #[serde(tag = "mode")] pub enum NtpSourceConfig { + #[serde(rename = "Gps")] + Gps(GpsConfigSource), + #[serde(rename = "Pps")] + Pps(PpsConfigSource), #[serde(rename = "server")] Standard(StandardSource), #[serde(rename = "nts")] @@ -134,6 +153,9 @@ impl From> for HardcodedDnsResolve { #[derive(Debug, Clone, PartialEq, Eq)] pub struct NtpAddress(pub NormalizedAddress); +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GpsPort(pub NormalizedAddress); + #[derive(Debug, Clone, PartialEq, Eq)] pub struct NtsKeAddress(pub NormalizedAddress); @@ -332,6 +354,8 @@ mod tests { NtpSourceConfig::Standard(c) => c.address.to_string(), NtpSourceConfig::Nts(c) => c.address.to_string(), NtpSourceConfig::Pool(c) => c.addr.to_string(), + NtpSourceConfig::Gps(c) => c.address.to_string(), + NtpSourceConfig::Pps(c) => c.address.to_string(), #[cfg(feature = "unstable_nts-pool")] NtpSourceConfig::NtsPool(c) => c.addr.to_string(), } @@ -436,33 +460,6 @@ mod tests { } } - #[test] - fn test_deserialize_source_pem_certificate() { - let contents = include_bytes!("../../../testdata/certificates/nos-nl.pem"); - let path = std::env::temp_dir().join("nos-nl.pem"); - std::fs::write(&path, contents).unwrap(); - - #[derive(Deserialize, Debug)] - struct TestConfig { - source: NtpSourceConfig, - } - - let test: TestConfig = toml::from_str(&format!( - r#" - [source] - address = "example.com" - certificate-authority = "{}" - mode = "nts" - "#, - path.display() - )) - .unwrap(); - assert!(matches!(test.source, NtpSourceConfig::Nts(_))); - if let NtpSourceConfig::Nts(config) = test.source { - assert_eq!(config.address.to_string(), "example.com:4460"); - } - } - #[test] fn test_source_from_string() { let source = NtpSourceConfig::try_from("example.com").unwrap(); diff --git a/ntpd/src/daemon/gps_source.rs b/ntpd/src/daemon/gps_source.rs new file mode 100644 index 000000000..986da68c7 --- /dev/null +++ b/ntpd/src/daemon/gps_source.rs @@ -0,0 +1,309 @@ +use std::io; +use std::{future::Future, marker::PhantomData, pin::Pin}; +use tokio::time::{Instant, Sleep}; + +use ntp_proto::{ + GpsSource, GpsSourceActionIterator, NtpClock, NtpDuration, NtpInstant, NtpTimestamp, +}; + +use tracing::{error, instrument, warn, Instrument, Span}; + +use crate::daemon::ntp_source::MsgForSystem; + +use super::gps_without_gpsd::Gps; + +use super::{config::TimestampMode, exitcode, ntp_source::SourceChannels, spawn::SourceId}; + +/// Trait needed to allow injecting of futures other than `tokio::time::Sleep` for testing +pub trait Wait: Future { + fn reset(self: Pin<&mut Self>, deadline: Instant); +} + +impl Wait for Sleep { + fn reset(self: Pin<&mut Self>, deadline: Instant) { + self.reset(deadline); + } +} + +pub(crate) struct GpsSourceTask { + _wait: PhantomData, + index: SourceId, + clock: C, + channels: SourceChannels, + + source: GpsSource, + + /// we don't store the real origin timestamp in the packet, because that would leak our + /// system time to the network (and could make attacks easier). So instead there is some + /// garbage data in the origin_timestamp field, and we need to track and pass along the + /// actual origin timestamp ourselves. + /// Timestamp of the last packet that we sent + last_send_timestamp: Option, + gps: Gps, +} + +impl GpsSourceTask +where + C: 'static + NtpClock + Send + Sync, + T: Wait, +{ + async fn run(&mut self, mut poll_wait: Pin<&mut T>) { + loop { + enum SelectResult { + Timer, + Recv(io::Result>), + } + let selected = tokio::select! { + () = &mut poll_wait => { + SelectResult::Timer + }, + result = self.gps.current_data() => { + if result.is_err() { + SelectResult::Recv(Err(result.unwrap_err())) + } else { + SelectResult::Recv(result) + } + } + }; + + let actions = match selected { + SelectResult::Recv(result) => { + match result { + Ok(Some(_data)) => { + // Process GPS data + match accept_gps_time(result) { + AcceptResult::Accept((offset, timestamp)) => { + self.source.handle_incoming( + NtpInstant::now(), + offset, + timestamp, + self.gps.measurement_noise, + ) + } + AcceptResult::Ignore => GpsSourceActionIterator::default(), + } + } + Ok(None) => { + // Handle the case where no data is available + GpsSourceActionIterator::default() + } + Err(_e) => { + // Handle the error + GpsSourceActionIterator::default() + } + } + } + SelectResult::Timer => GpsSourceActionIterator::default(), + }; + + for action in actions { + match action { + ntp_proto::GpsSourceAction::Send() => { + match self.clock.now() { + Err(e) => { + // we cannot determine the origin_timestamp + error!(error = ?e, "There was an error retrieving the current time"); + + // report as no permissions, since this seems the most likely + std::process::exit(exitcode::NOPERM); + } + Ok(ts) => { + self.last_send_timestamp = Some(ts); + } + } + } + ntp_proto::GpsSourceAction::UpdateSystem(update) => { + self.channels + .msg_for_system_sender + .send(MsgForSystem::GpsSourceUpdate(self.index, update)) + .await + .ok(); + } + ntp_proto::GpsSourceAction::SetTimer(timeout) => { + poll_wait.as_mut().reset(Instant::now() + timeout) + } + ntp_proto::GpsSourceAction::Reset => { + self.channels + .msg_for_system_sender + .send(MsgForSystem::Unreachable(self.index)) + .await + .ok(); + return; + } + ntp_proto::GpsSourceAction::Demobilize => { + self.channels + .msg_for_system_sender + .send(MsgForSystem::MustDemobilize(self.index)) + .await + .ok(); + return; + } + } + } + } + } +} + +impl GpsSourceTask +where + C: 'static + NtpClock + Send + Sync, +{ + #[allow(clippy::too_many_arguments)] + #[instrument(skip(clock, channels))] + pub fn spawn( + index: SourceId, + clock: C, + timestamp_mode: TimestampMode, + channels: SourceChannels, + gps: Gps, + ) -> tokio::task::JoinHandle<()> { + tokio::spawn( + (async move { + let (source, initial_actions) = GpsSource::new(); + let poll_wait = tokio::time::sleep(std::time::Duration::default()); + tokio::pin!(poll_wait); + for action in initial_actions { + match action { + ntp_proto::GpsSourceAction::Send() => { + unreachable!("Should not be sending messages from startup") + } + ntp_proto::GpsSourceAction::UpdateSystem(_) => { + unreachable!("Should not be updating system from startup") + } + ntp_proto::GpsSourceAction::SetTimer(timeout) => { + poll_wait.as_mut().reset(Instant::now() + timeout) + } + ntp_proto::GpsSourceAction::Reset => { + unreachable!("Should not be resetting from startup") + } + ntp_proto::GpsSourceAction::Demobilize => { + todo!("Should not be demobilizing from startup") + } + } + } + + let last_send_timestamp = clock.clone().now().ok(); + let mut process = GpsSourceTask { + _wait: PhantomData, + index, + clock, + channels, + source, + gps, + last_send_timestamp, + }; + + process.run(poll_wait).await; + }) + .instrument(Span::current()), + ) + } +} + +#[derive(Debug)] +enum AcceptResult { + Accept((NtpDuration, NtpTimestamp)), + Ignore, +} + +pub fn from_seconds(seconds: f64) -> NtpDuration { + NtpDuration::from_seconds(seconds) +} + +fn parse_gps_time( + data: &Option<(f64, NtpTimestamp)>, +) -> Result<(NtpDuration, NtpTimestamp), Box> { + if let Some(offset) = data { + let ntp_duration = from_seconds(offset.0); + Ok((ntp_duration, offset.1)) + } else { + Err("Failed to parse GPS time".into()) + } +} + +fn accept_gps_time(result: io::Result>) -> AcceptResult { + match result { + Ok(data) => match parse_gps_time(&data) { + Ok((gps_duration, gps_timestamp)) => { + AcceptResult::Accept((gps_duration, gps_timestamp)) + } + Err(_) => AcceptResult::Ignore, + }, + Err(receive_error) => { + warn!(?receive_error, "could not receive GPS data"); + AcceptResult::Ignore + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use ntp_proto::NtpTimestamp; + + #[tokio::test] + async fn test_accept_gps_time_with_valid_data() { + let gps_timestamp = NtpTimestamp::from_fixed_int(1_614_505_748); + let result = Ok(Some((123.456789, gps_timestamp))); + let accept_result = accept_gps_time(result); + + if let AcceptResult::Accept((duration, timestamp)) = accept_result { + assert_eq!(duration.to_seconds(), 123.45678902847152); + assert_eq!(timestamp, gps_timestamp); + } else { + panic!("Expected Accept result"); + } + } + + #[tokio::test] + async fn test_accept_gps_time_with_invalid_data() { + let result: io::Result> = Ok(None); + let accept_result = accept_gps_time(result); + + if let AcceptResult::Ignore = accept_result { + // Expected outcome + } else { + panic!("Expected Ignore result"); + } + } + + #[tokio::test] + async fn test_accept_gps_time_with_error() { + let result: io::Result> = + Err(io::Error::new(io::ErrorKind::Other, "error")); + let accept_result = accept_gps_time(result); + + if let AcceptResult::Ignore = accept_result { + // Expected outcome + } else { + panic!("Expected Ignore result"); + } + } + + #[tokio::test] + async fn test_parse_gps_time_with_valid_data() { + let gps_timestamp = NtpTimestamp::from_fixed_int(1_614_505_748); + let data = Some((123.456789, gps_timestamp)); + let result = parse_gps_time(&data); + + assert!(result.is_ok()); + let (duration, timestamp) = result.unwrap(); + assert_eq!(duration.to_seconds(), 123.45678902847152); + assert_eq!(timestamp, gps_timestamp); + } + + #[tokio::test] + async fn test_parse_gps_time_with_invalid_data() { + let data: Option<(f64, NtpTimestamp)> = None; + let result = parse_gps_time(&data); + + assert!(result.is_err()); + } + + #[test] + fn test_from_seconds() { + let seconds = 123.456789; + let duration = from_seconds(seconds); + assert_eq!(duration.to_seconds(), 123.45678902847152); + } +} diff --git a/ntpd/src/daemon/gps_with_gpsd.rs b/ntpd/src/daemon/gps_with_gpsd.rs new file mode 100644 index 000000000..6a8d8ec44 --- /dev/null +++ b/ntpd/src/daemon/gps_with_gpsd.rs @@ -0,0 +1,120 @@ +use chrono::{DateTime, Utc}; +use serde::Deserialize; +use std::io::{self, BufRead, BufReader, Write, Read}; +use std::net::TcpStream; +use std::time::Duration as StdDuration; +use std::thread; + +#[derive(Debug, Deserialize)] +#[serde(tag = "class")] +enum GpsdResponse { + TPV { + time: Option, + ept: Option, + }, + PPS { + real_sec: Option, + real_nsec: Option, + }, + VERSION {}, + DEVICES {}, + WATCH {}, + SKY {}, + GST {}, +} + +fn connect_to_gpsd() -> io::Result { + let gpsd_address = "127.0.0.1:2947"; // Default GPSD address + let stream = TcpStream::connect(gpsd_address)?; + stream.set_nonblocking(true)?; + Ok(stream) +} + +fn send_gpsd_command(stream: &mut TcpStream, command: &str) -> io::Result<()> { + stream.write_all(command.as_bytes())?; + stream.flush()?; + Ok(()) +} + +fn process_response(line: &str) { + match serde_json::from_str::(line) { + Ok(response) => match response { + GpsdResponse::TPV { time, ept: _ } => { + if let Some(timestamp) = time { + match DateTime::parse_from_rfc3339(×tamp) { + Ok(datetime) => { + let datetime = datetime.with_timezone(&Utc); + let now = Utc::now(); + let duration = now.signed_duration_since(datetime); + let offset = duration.num_nanoseconds().unwrap_or(0) as f64 / 1e9; + } + Err(e) => eprintln!("Failed to parse timestamp: {}", e), + } + } + } + GpsdResponse::PPS { + real_sec, + real_nsec, + } => { + if let (Some(real_sec), Some(real_nsec)) = (real_sec, real_nsec) { + let pps_time = Utc.timestamp(real_sec, real_nsec as u32); + let now = Utc::now(); + let duration = now.signed_duration_since(pps_time); + let offset = duration.num_nanoseconds().unwrap_or(0) as f64 / 1e9; + } + } + GpsdResponse::VERSION {} => { + println!("Received VERSION response, ignoring..."); + } + GpsdResponse::DEVICES {} => { + println!("Received DEVICES response, ignoring..."); + } + GpsdResponse::WATCH {} => { + println!("Received WATCH response, ignoring..."); + } + GpsdResponse::SKY {} => { + println!("Received SKY response, ignoring..."); + } + GpsdResponse::GST {} => { + println!("Received GST response, ignoring..."); + } + }, + Err(e) => eprintln!("Failed to parse JSON: {}", e), + } +} + +fn read_and_process_lines(reader: &mut BufReader) -> io::Result<()> { + let mut line = String::new(); + let mut buffer = [0; 512]; + loop { + match reader.get_mut().read(&mut buffer) { + Ok(n) if n > 0 => { + line.clear(); + line.push_str(&String::from_utf8_lossy(&buffer[..n])); + process_response(&line); + } + Ok(_) => {} + Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => { + thread::sleep(StdDuration::from_millis(10)); + } + Err(e) => { + eprintln!("Error reading from GPSD: {}", e); + break; + } + } + } + Ok(()) +} + +fn main() -> io::Result<()> { + let mut stream = connect_to_gpsd()?; + let mut reader = BufReader::new(stream.try_clone()?); + + send_gpsd_command(&mut stream, "?WATCH={\"enable\":true,\"json\":true}\n")?; + + if let Err(e) = read_and_process_lines(&mut reader) { + eprintln!("Error processing lines from GPSD: {}", e); + } + + Ok(()) +} \ No newline at end of file diff --git a/ntpd/src/daemon/gps_without_gpsd.rs b/ntpd/src/daemon/gps_without_gpsd.rs new file mode 100644 index 000000000..3c9d19ecf --- /dev/null +++ b/ntpd/src/daemon/gps_without_gpsd.rs @@ -0,0 +1,513 @@ +use chrono::{NaiveDate, NaiveTime, NaiveDateTime, Utc}; +use ntp_proto::NtpTimestamp; +use std::io::{self, BufRead, BufReader}; +use std::time::Duration; +use serialport::{SerialPort}; + +#[derive(Debug)] +pub struct Gps { + reader: BufReader>, + current_date: Option, + line: String, + pub measurement_noise: f64, +} + +impl Gps { + /// Creates a new `GPS` instance. + /// + /// This function initializes a new `GPS` struct with a serial port reader, + /// and sets up the necessary fields for processing GPS data. + /// + /// # Arguments + /// + /// * `port_name` - The name of the serial port to open (e.g., `/dev/serial0`). + /// * `baud_rate` - The baud rate for the serial port communication (e.g., 9600). + /// * `timeout` - The timeout duration for the serial port operations. + /// + /// # Returns + /// + /// * `io::Result` - A result containing the new `GPS` instance if successful, + /// or an `io::Error` if opening the serial port fails. + pub fn new(port_name: &str, baud_rate: u32, timeout: Duration, measurement_noise: f64) -> io::Result { + let port = serialport::new(port_name, baud_rate) + .timeout(timeout) + .open()?; + let reader = BufReader::new(port); + Ok(Gps { + reader, + current_date: None, + line: String::new(), + measurement_noise, + }) + } + + /// Converts NMEA time and date strings to a Unix timestamp. + /// + /// # Arguments + /// + /// * `nmea_time` - The NMEA time string. + /// * `nmea_date` - The NMEA date string. + /// + /// # Returns + /// + /// * `Option` - The corresponding Unix timestamp with fractional seconds, or `None` if the conversion fails. + fn nmea_time_date_to_unix_timestamp(&self, nmea_time: &str, nmea_date: &str) -> Option<(f64, u64, u32)> { + let (hour, minute, second) = self.parse_nmea_time(nmea_time)?; + let (day, month, year) = self.parse_nmea_date(nmea_date)?; + + let naive_date = NaiveDate::from_ymd_opt(2000 + year as i32, month, day)?; + let naive_time = NaiveTime::from_hms_micro_opt( + hour, + minute, + second.trunc() as u32, + (second.fract() * 1_000_000.0) as u32, + )?; + + let naive_datetime = NaiveDateTime::new(naive_date, naive_time); + let timestamp = naive_datetime.and_utc().timestamp() as f64 + + naive_datetime.and_utc().timestamp_subsec_nanos() as f64; + + Some((timestamp, naive_datetime.and_utc().timestamp() as u64, naive_datetime.and_utc().timestamp_subsec_nanos())) + } + + /// Parses an NMEA time string into hours, minutes, and seconds. + /// + /// # Arguments + /// + /// * `nmea_time` - The NMEA time string in the format `HHMMSS` or `HHMMSS.SS`. + /// + /// # Returns + /// + /// * `Option<(u32, u32, f64)>` - A tuple containing hours, minutes and seconds, or `None` if parsing fails. + fn parse_nmea_time(&self, nmea_time: &str) -> Option<(u32, u32, f64)> { + if nmea_time.len() < 6 { + return None; + } + + let hour: u32 = nmea_time.get(0..2)?.parse().ok()?; + let minute: u32 = nmea_time.get(2..4)?.parse().ok()?; + let second: f64 = nmea_time.get(4..).unwrap_or("0").parse().ok()?; + + Some((hour, minute, second)) + } + + /// Parses an NMEA date string into day, month, and year. + /// + /// # Arguments + /// + /// * `nmea_date` - The NMEA date string in the format `DDMMYY`. + /// + /// # Returns + /// + /// * `Option<(u32, u32, u32)>` - A tuple containing day, month and year, or `None` if parsing fails. + fn parse_nmea_date(&self, nmea_date: &str) -> Option<(u32, u32, u32)> { + if nmea_date.len() < 6 { + return None; + } + + let day: u32 = nmea_date.get(0..2)?.parse().ok()?; + let month: u32 = nmea_date.get(2..4)?.parse().ok()?; + let year: u32 = nmea_date.get(4..6)?.parse().ok()?; + + Some((day, month, year)) + } + + /// Processes GNRMC fields to update the current date. + /// + /// # Arguments + /// + /// * `fields` - The GNRMC fields. + fn process_gnrmc(&mut self, fields: &[&str]) { + if self.is_valid_gnrmc(fields) { + if let Some(date) = fields.get(9) { + self.current_date = Some(date.to_string()); + } + } + } + + /// Checks if GNRMC fields are valid. + /// + /// # Arguments + /// + /// * `fields` - The GNRMC fields. + /// + /// # Returns + /// + /// * `bool` - `true` if the fields are valid, otherwise `false`. + fn is_valid_gnrmc(&self, fields: &[&str]) -> bool { + fields.len() > 9 && fields[2] == "A" + } + + + + /// Processes GNGGA fields and returns the offset between the GPS time and the system time. + /// + /// # Arguments + /// + /// * `fields` - The GNGGA fields. + /// + /// # Returns + /// + /// * `Option` - The offset in seconds, or `None` if processing fails. + fn process_gngga(&mut self, fields: &[&str]) -> Option<(f64, NtpTimestamp)> { + if let Some(time) = fields.get(1) { + if let Some(date) = &self.current_date { + if let Some(gps_timestamp) = self.nmea_time_date_to_unix_timestamp(time, date) { + let system_time = Utc::now().timestamp() as f64 + Utc::now().timestamp_subsec_micros() as f64 * 1e-6; + return Some((system_time - gps_timestamp.0, NtpTimestamp::from_unix_timestamp(gps_timestamp.1, gps_timestamp.2))); + + } + } + } + None + } + + /// Reads and processes lines from the serial port. + /// + /// # Returns + /// + /// * `io::Result>` - The result of reading and processing lines with an optional offset. + pub async fn current_data(&mut self) -> io::Result> { + let mut line = String::new(); + match self.reader.read_line(&mut line) { + Ok(_) => { + let line = line.trim().to_string(); + self.line.clear(); + self.line.push_str(&line); + let fields: Vec<&str> = line.split(',').collect(); + if line.starts_with("$GNRMC") { + self.process_gnrmc(&fields); + } else if line.starts_with("$GNGGA") { + return Ok(self.process_gngga(&fields)); + } + Ok(None) + }, + Err(e) => { + eprintln!("Error reading from serial port: {}", e); + Err(e) + } + } + } +} + + +// Some MOCK testing +#[cfg(test)] +mod tests { + use super::*; + use chrono::{NaiveDate, NaiveTime, NaiveDateTime}; + use std::io::Cursor; + + struct MockSerialPort { + data: Cursor, + } + + impl io::Read for MockSerialPort { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + self.data.read(buf) + } + } + + impl io::Write for MockSerialPort { + fn write(&mut self, buf: &[u8]) -> io::Result { + Ok(buf.len()) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } + } + + impl SerialPort for MockSerialPort { + fn name(&self) -> Option { + Some("MockSerialPort".to_string()) + } + + fn baud_rate(&self) -> serialport::Result { + Ok(9600) + } + + fn data_bits(&self) -> serialport::Result { + Ok(serialport::DataBits::Eight) + } + + fn flow_control(&self) -> serialport::Result { + Ok(serialport::FlowControl::None) + } + + fn parity(&self) -> serialport::Result { + Ok(serialport::Parity::None) + } + + fn stop_bits(&self) -> serialport::Result { + Ok(serialport::StopBits::One) + } + + fn timeout(&self) -> Duration { + Duration::from_secs(1) + } + + fn set_baud_rate(&mut self, _: u32) -> serialport::Result<()> { + Ok(()) + } + + fn set_data_bits(&mut self, _: serialport::DataBits) -> serialport::Result<()> { + Ok(()) + } + + fn set_flow_control(&mut self, _: serialport::FlowControl) -> serialport::Result<()> { + Ok(()) + } + + fn set_parity(&mut self, _: serialport::Parity) -> serialport::Result<()> { + Ok(()) + } + + fn set_stop_bits(&mut self, _: serialport::StopBits) -> serialport::Result<()> { + Ok(()) + } + + fn set_timeout(&mut self, _: Duration) -> serialport::Result<()> { + Ok(()) + } + + fn write_request_to_send(&mut self, _: bool) -> serialport::Result<()> { + Ok(()) + } + + fn write_data_terminal_ready(&mut self, _: bool) -> serialport::Result<()> { + Ok(()) + } + + fn read_clear_to_send(&mut self) -> serialport::Result { + Ok(true) + } + + fn read_data_set_ready(&mut self) -> serialport::Result { + Ok(true) + } + + fn read_ring_indicator(&mut self) -> serialport::Result { + Ok(false) + } + + fn read_carrier_detect(&mut self) -> serialport::Result { + Ok(true) + } + + fn bytes_to_read(&self) -> serialport::Result { + Ok(self.data.get_ref().len() as u32) + } + + fn bytes_to_write(&self) -> serialport::Result { + Ok(0) + } + + fn clear(&self, _: serialport::ClearBuffer) -> serialport::Result<()> { + Ok(()) + } + + fn try_clone(&self) -> serialport::Result> { + Ok(Box::new(MockSerialPort { + data: self.data.clone(), + })) + } + + fn set_break(&self) -> serialport::Result<()> { + Ok(()) + } + + fn clear_break(&self) -> serialport::Result<()> { + Ok(()) + } + } + + + + //mock the gps data + fn create_gps_with_mock_reader(data: &str) -> Gps { + let cursor = Cursor::new(data.to_string()); + let reader = BufReader::new(Box::new(MockSerialPort { data: cursor }) as Box); + Gps { + reader, + current_date: None, + line: String::new(), + measurement_noise: 1.0, + } + } + + //test if it parses the time from nmea format + #[test] + fn test_parse_nmea_time() { + let gps = create_gps_with_mock_reader(""); + let result = gps.parse_nmea_time("134510"); + assert_eq!(result, Some((13, 45, 10.0))); + + let result = gps.parse_nmea_time("134510.00"); + assert_eq!(result, Some((13, 45, 10.00))); + + let result = gps.parse_nmea_time("1345"); + assert_eq!(result, None); + + let result = gps.parse_nmea_time("ab4510"); + assert_eq!(result, None); + + let result = gps.parse_nmea_time("13ab10"); + assert_eq!(result, None); + + let result = gps.parse_nmea_time("1345ab"); + assert_eq!(result, None); + + let result = gps.parse_nmea_time("000000"); + assert_eq!(result, Some((0, 0, 0.0))); + } + + //test if it parses the date from nmea format + #[test] + fn test_parse_nmea_date() { + let gps = create_gps_with_mock_reader(""); + let result = gps.parse_nmea_date("220334"); + assert_eq!(result, Some((22, 3, 34))); + + let result = gps.parse_nmea_date("2203"); + assert_eq!(result, None); + + let result = gps.parse_nmea_date("ab0334"); + assert_eq!(result, None); + + let result = gps.parse_nmea_date("22ab34"); + assert_eq!(result, None); + + let result = gps.parse_nmea_date("2203ab"); + assert_eq!(result, None); + + let result = gps.parse_nmea_date("010100"); + assert_eq!(result, Some((1, 1, 0))); + } + + //test if it parses the nmea format to unix time + #[test] + fn test_nmea_time_date_to_unix_timestamp() { + let gps = create_gps_with_mock_reader(""); + let result = gps.nmea_time_date_to_unix_timestamp("134510.00", "250320"); + let expected = NaiveDateTime::new( + NaiveDate::from_ymd(2020, 3, 25), + NaiveTime::from_hms_micro(13, 45, 10, 0), + ); + assert_eq!( + result, + Some(( + expected.timestamp() as f64 + expected.timestamp_subsec_nanos() as f64 * 1e-9, + expected.timestamp() as u64, + expected.timestamp_subsec_nanos() + )) + ); + + let result = gps.nmea_time_date_to_unix_timestamp("1234", "250320"); + assert_eq!(result, None); + + let result = gps.nmea_time_date_to_unix_timestamp("123519.00", "2503"); + assert_eq!(result, None); + + let result = gps.nmea_time_date_to_unix_timestamp("12ab19.00", "250320"); + assert_eq!(result, None); + + let result = gps.nmea_time_date_to_unix_timestamp("123519.00", "25ab20"); + assert_eq!(result, None); + + let result = gps.nmea_time_date_to_unix_timestamp("000000.00", "010100"); + let expected = NaiveDateTime::new( + NaiveDate::from_ymd(2000, 1, 1), + NaiveTime::from_hms_micro(0, 0, 0, 0), + ); + assert_eq!( + result, + Some(( + expected.timestamp() as f64 + expected.timestamp_subsec_nanos() as f64 * 1e-9, + expected.timestamp() as u64, + expected.timestamp_subsec_nanos() + )) + ); + } + + // test if it parses gnrmc data packet + #[test] + fn test_process_gnrmc_with_valid_data() { + let mut gps = create_gps_with_mock_reader(""); + let fields = vec!["$GNRMC", "123519.00", "A", "4807.038", "N", "01131.000", "E", "022.4", "084.4", "250320"]; + gps.process_gnrmc(&fields); + assert_eq!(gps.current_date, Some("250320".to_string())); + } + + // test if it parses gnrmc data packet if its an invalid format + #[test] + fn test_process_gnrmc_with_invalid_data() { + let mut gps = create_gps_with_mock_reader(""); + let fields = vec!["$GNRMC", "123519.00", "V", "4807.038", "N", "01131.000", "E", "022.4", "084.4", "250320"]; + gps.process_gnrmc(&fields); + assert_eq!(gps.current_date, None); + } + + // test if it parses gnrmc data packet if there is insufficient data fields + #[test] + fn test_process_gnrmc_with_insufficient_fields() { + let mut gps = create_gps_with_mock_reader(""); + let fields = vec!["$GNRMC", "123519.00", "A"]; + gps.process_gnrmc(&fields); + assert_eq!(gps.current_date, None); + } + + // test if it parses gnrmc data packet with only date field + #[test] + fn test_process_gnrmc_updates_current_date() { + let mut gps = create_gps_with_mock_reader(""); + gps.current_date = Some("240320".to_string()); + let fields = vec!["$GNRMC", "123519.00", "A", "4807.038", "N", "01131.000", "E", "022.4", "084.4", "250320"]; + gps.process_gnrmc(&fields); + assert_eq!(gps.current_date, Some("250320".to_string())); + } + + // test if it parses gnrmc data packet with valid data + #[test] + fn test_is_valid_gnrmc_with_valid_data() { + let gps = create_gps_with_mock_reader(""); + let fields = vec!["$GNRMC", "123519.00", "A", "4807.038", "N", "01131.000", "E", "022.4", "084.4", "250320"]; + assert!(gps.is_valid_gnrmc(&fields)); + } + + // test if it parses gnrmc data packet with invalid status + #[test] + fn test_is_valid_gnrmc_with_invalid_status() { + let gps = create_gps_with_mock_reader(""); + let fields = vec!["$GNRMC", "123519.00", "V", "4807.038", "N", "01131.000", "E", "022.4", "084.4", "250320"]; + assert!(!gps.is_valid_gnrmc(&fields)); + } + + // test if it parses gnrmc data packet with insufficient fields + #[test] + fn test_is_valid_gnrmc_with_insufficient_fields() { + let gps = create_gps_with_mock_reader(""); + let fields = vec!["$GNRMC", "123519.00", "A"]; + assert!(!gps.is_valid_gnrmc(&fields)); + } + + // test if it parses gnrmc data packet with valid data + #[tokio::test] + async fn test_current_data_with_gngga() { + let mut gps = create_gps_with_mock_reader("$GNGGA,123519.00,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47\n"); + gps.current_date = Some("250320".to_string()); + let result = gps.current_data().await; + assert!(result.is_ok()); + assert!(result.unwrap().is_some()); + } + + // test if it parses gnrmc data packet with valid data + #[tokio::test] + async fn test_current_data_with_gnrmc() { + let mut gps = create_gps_with_mock_reader("$GNRMC,123519.00,A,4807.038,N,01131.000,E,022.4,084.4,250320,,*1F\n"); + let result = gps.current_data().await; + assert!(result.is_ok()); + assert!(result.unwrap().is_none()); + assert_eq!(gps.current_date, Some("250320".to_string())); + } +} \ No newline at end of file diff --git a/ntpd/src/daemon/keyexchange.rs b/ntpd/src/daemon/keyexchange.rs index c738ff69d..437c4fb1f 100644 --- a/ntpd/src/daemon/keyexchange.rs +++ b/ntpd/src/daemon/keyexchange.rs @@ -547,550 +547,3 @@ fn certificates_from_bufread( rustls_pemfile::certs(&mut reader).collect() } -#[cfg(test)] -mod tests { - use std::{io::Cursor, path::PathBuf}; - - use ntp_proto::{KeySetProvider, NtsRecord}; - use tokio::io::{AsyncReadExt, AsyncWriteExt}; - - use super::*; - - #[test] - fn nos_nl_pem() { - let input = include_bytes!("../../testdata/certificates/nos-nl.pem"); - let certificates = certificates_from_bufread(input.as_slice()).unwrap(); - - assert_eq!(certificates.len(), 1); - } - - #[test] - fn nos_nl_chain_pem() { - let input = include_bytes!("../../testdata/certificates/nos-nl-chain.pem"); - let certificates = certificates_from_bufread(input.as_slice()).unwrap(); - - assert_eq!(certificates.len(), 3); - } - - #[test] - fn parse_private_keys() { - let input = include_bytes!("../../test-keys/end.key"); - let _ = rustls_pemfile::private_key(&mut input.as_slice()) - .unwrap() - .unwrap(); - - let input = include_bytes!("../../test-keys/testca.key"); - let _ = rustls_pemfile::private_key(&mut input.as_slice()) - .unwrap() - .unwrap(); - - // openssl does no longer seem to want to generate this format - // so we use https://github.com/rustls/pemfile/blob/main/tests/data/rsa1024.pkcs1.pem - let input = include_bytes!("../../test-keys/rsa_key.pem"); - let _ = rustls_pemfile::private_key(&mut input.as_slice()) - .unwrap() - .unwrap(); - - // openssl ecparam -name prime256v1 -genkey -noout -out ec_key.pem - let input = include_bytes!("../../test-keys/ec_key.pem"); - let _ = rustls_pemfile::private_key(&mut input.as_slice()) - .unwrap() - .unwrap(); - - // openssl genpkey -algorithm EC -out pkcs8_key.pem -pkeyopt ec_paramgen_curve:prime256v1 - let input = include_bytes!("../../test-keys/pkcs8_key.pem"); - let _ = rustls_pemfile::private_key(&mut input.as_slice()) - .unwrap() - .unwrap(); - } - - #[tokio::test] - async fn key_exchange_roundtrip() { - let provider = KeySetProvider::new(1); - let keyset = provider.get(); - #[cfg(feature = "unstable_nts-pool")] - let pool_certs = ["testdata/certificates/nos-nl.pem"]; - - let (_sender, keyset) = tokio::sync::watch::channel(keyset); - let nts_ke_config = NtsKeConfig { - certificate_chain_path: PathBuf::from("test-keys/end.fullchain.pem"), - private_key_path: PathBuf::from("test-keys/end.key"), - #[cfg(feature = "unstable_nts-pool")] - authorized_pool_server_certificates: pool_certs.iter().map(PathBuf::from).collect(), - key_exchange_timeout_ms: 1000, - listen: "0.0.0.0:5431".parse().unwrap(), - ntp_port: None, - ntp_server: None, - }; - - let _join_handle = spawn(nts_ke_config, keyset); - - // give the server some time to make the port available - tokio::time::sleep(std::time::Duration::from_millis(20)).await; - - let ca = include_bytes!("../../test-keys/testca.pem"); - let result = key_exchange_client( - "localhost".to_string(), - 5431, - &certificates_from_bufread(BufReader::new(Cursor::new(ca))).unwrap(), - ) - .await - .unwrap(); - - assert_eq!(result.remote, "localhost"); - assert_eq!(result.port, 123); - } - - #[tokio::test] - async fn key_exchange_roundtrip_with_port_server() { - let provider = KeySetProvider::new(1); - let keyset = provider.get(); - #[cfg(feature = "unstable_nts-pool")] - let pool_certs = ["testdata/certificates/nos-nl.pem"]; - - let (_sender, keyset) = tokio::sync::watch::channel(keyset); - let nts_ke_config = NtsKeConfig { - certificate_chain_path: PathBuf::from("test-keys/end.fullchain.pem"), - private_key_path: PathBuf::from("test-keys/end.key"), - #[cfg(feature = "unstable_nts-pool")] - authorized_pool_server_certificates: pool_certs.iter().map(PathBuf::from).collect(), - key_exchange_timeout_ms: 1000, - listen: "0.0.0.0:5432".parse().unwrap(), - ntp_port: Some(568), - ntp_server: Some("jantje".into()), - }; - - let _join_handle = spawn(nts_ke_config, keyset); - - // give the server some time to make the port available - tokio::time::sleep(std::time::Duration::from_millis(20)).await; - - let ca = include_bytes!("../../test-keys/testca.pem"); - let result = key_exchange_client( - "localhost".to_string(), - 5432, - &certificates_from_bufread(BufReader::new(Cursor::new(ca))).unwrap(), - ) - .await - .unwrap(); - - assert_eq!(result.remote, "jantje"); - assert_eq!(result.port, 568); - } - - #[cfg(feature = "unstable_nts-pool")] - #[tokio::test] - async fn key_exchange_refusal_due_to_invalid_config() { - let cert_path = "testdata/certificates/nos-nl-chain.pem"; - let certs = [cert_path]; - - let provider = KeySetProvider::new(1); - let keyset = provider.get(); - - let (_sender, keyset) = tokio::sync::watch::channel(keyset); - let nts_ke_config = NtsKeConfig { - certificate_chain_path: PathBuf::from("test-keys/end.fullchain.pem"), - private_key_path: PathBuf::from("test-keys/end.key"), - authorized_pool_server_certificates: certs.iter().map(PathBuf::from).collect(), - key_exchange_timeout_ms: 1000, - listen: "0.0.0.0:5433".parse().unwrap(), - ntp_port: None, - ntp_server: None, - }; - - let Err(io_error) = run_nts_ke(nts_ke_config, keyset).await else { - panic!("nts server started normally, this should not happen"); - }; - - let expected_error_msg = format!( - "pool certificate file at `\"{cert_path}\"` should contain exactly one certificate" - ); - assert_eq!(io_error.to_string(), expected_error_msg); - } - - #[tokio::test] - async fn client_connection_refused() { - let result = key_exchange_client("localhost".to_string(), 5434, &[]).await; - - let error = result.unwrap_err(); - - match error { - KeyExchangeError::Io(error) => { - assert_eq!(error.kind(), std::io::ErrorKind::ConnectionRefused); - } - _ => panic!(), - } - } - - fn client_key_exchange_message_length() -> usize { - let mut buffer = Vec::with_capacity(1024); - for record in ntp_proto::NtsRecord::client_key_exchange_records(vec![]).iter() { - record.write(&mut buffer).unwrap(); - } - - buffer.len() - } - - async fn send_records_to_client( - records: Vec, - ) -> Result { - let listener = tokio::net::TcpListener::bind(("localhost", 0)) - .await - .unwrap(); - let port = listener.local_addr()?.port(); - - tokio::spawn(async move { - let cc = include_bytes!("../../test-keys/end.fullchain.pem"); - let certificate_chain = - certificates_from_bufread(BufReader::new(Cursor::new(cc))).unwrap(); - - let pk = include_bytes!("../../test-keys/end.key"); - let private_key = rustls_pemfile::private_key(&mut pk.as_slice()) - .unwrap() - .unwrap(); - - let config = build_server_config(certificate_chain, private_key).unwrap(); - - let (stream, _) = listener.accept().await.unwrap(); - - let acceptor = tokio_rustls::TlsAcceptor::from(config); - let mut stream = acceptor.accept(stream).await.unwrap(); - - // so that we could in theory handle multiple write calls - let mut buf = vec![0; client_key_exchange_message_length()]; - stream.read_exact(&mut buf).await.unwrap(); - - for record in records { - let mut buffer = Vec::with_capacity(1024); - record.write(&mut buffer).unwrap(); - - stream.write_all(&buffer).await.unwrap(); - } - }); - - let ca = include_bytes!("../../test-keys/testca.pem"); - let extra_certificates = - &certificates_from_bufread(BufReader::new(Cursor::new(ca))).unwrap(); - - key_exchange_client("localhost".to_string(), port, extra_certificates).await - } - - async fn run_server(listener: tokio::net::TcpListener) -> Result<(), KeyExchangeError> { - let cc = include_bytes!("../../test-keys/end.fullchain.pem"); - let certificate_chain = certificates_from_bufread(BufReader::new(Cursor::new(cc)))?; - - let pk = include_bytes!("../../test-keys/end.key"); - let private_key = rustls_pemfile::private_key(&mut pk.as_slice()) - .unwrap() - .unwrap(); - - let config = build_server_config(certificate_chain, private_key).unwrap(); - let pool_certs = Arc::<[_]>::from(vec![]); - - let (stream, _) = listener.accept().await.unwrap(); - - let provider = KeySetProvider::new(0); - let keyset = provider.get(); - - BoundKeyExchangeServer::run(stream, config, keyset, None, None, pool_certs).await - } - - async fn client_tls_stream( - server_name: &str, - port: u16, - ) -> tokio_rustls::client::TlsStream { - let stream = tokio::net::TcpStream::connect((server_name, port)) - .await - .unwrap(); - - let ca = include_bytes!("../../test-keys/testca.pem"); - let extra_certificates = - &certificates_from_bufread(BufReader::new(Cursor::new(ca))).unwrap(); - - let config = build_client_config(extra_certificates).await.unwrap(); - - let domain = rustls::pki_types::ServerName::try_from(server_name) - .map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidInput, "invalid dnsname")) - .unwrap() - .to_owned(); - - let connector = tokio_rustls::TlsConnector::from(Arc::new(config)); - connector.connect(domain, stream).await.unwrap() - } - - async fn send_records_to_server(records: Vec) -> Result<(), KeyExchangeError> { - let listener = TcpListener::bind(&("localhost", 0)).await?; - let port = listener.local_addr()?.port(); - - tokio::spawn(async move { - let mut stream = client_tls_stream("localhost", port).await; - - for record in records { - let mut buffer = Vec::with_capacity(1024); - record.write(&mut buffer).unwrap(); - - stream.write_all(&buffer).await.unwrap(); - } - - let mut buf = [0; 1024]; - loop { - match stream.read(&mut buf).await.unwrap() { - 0 => break, - _ => continue, - } - } - }); - - run_server(listener).await - } - - #[tokio::test] - async fn receive_cookies() { - let result = send_records_to_client(vec![ - NtsRecord::NextProtocol { - protocol_ids: vec![0], - }, - NtsRecord::AeadAlgorithm { - critical: false, - algorithm_ids: vec![15], - }, - NtsRecord::NewCookie { - cookie_data: vec![1, 2, 3], - }, - NtsRecord::EndOfMessage, - ]) - .await; - - assert!(result.is_ok()); - } - - #[tokio::test] - async fn records_after_end_are_ignored() { - let result = send_records_to_client(vec![ - NtsRecord::NextProtocol { - protocol_ids: vec![0], - }, - NtsRecord::AeadAlgorithm { - critical: false, - algorithm_ids: vec![15], - }, - NtsRecord::NewCookie { - cookie_data: vec![1, 2, 3], - }, - NtsRecord::EndOfMessage, - NtsRecord::NewCookie { - cookie_data: vec![1, 2, 3], - }, - ]) - .await; - - assert!(result.is_ok()); - } - - #[tokio::test] - async fn no_cookies() { - let result = send_records_to_client(vec![ - NtsRecord::NextProtocol { - protocol_ids: vec![0], - }, - NtsRecord::AeadAlgorithm { - critical: false, - algorithm_ids: vec![15], - }, - NtsRecord::EndOfMessage, - ]) - .await; - - let error = result.unwrap_err(); - - assert!(matches!(error, KeyExchangeError::NoCookies)); - } - - async fn client_error_record(errorcode: u16) -> KeyExchangeError { - let result = send_records_to_client(vec![ - NtsRecord::Error { errorcode }, - NtsRecord::EndOfMessage, - ]) - .await; - - result.unwrap_err() - } - - #[tokio::test] - async fn client_receives_error_record() { - use KeyExchangeError as KEE; - - let error = client_error_record(NtsRecord::UNRECOGNIZED_CRITICAL_RECORD).await; - assert!(matches!(error, KEE::UnrecognizedCriticalRecord)); - - let error = client_error_record(NtsRecord::BAD_REQUEST).await; - assert!(matches!(error, KEE::BadRequest)); - - let error = client_error_record(NtsRecord::INTERNAL_SERVER_ERROR).await; - assert!(matches!(error, KEE::InternalServerError)); - } - - #[tokio::test] - async fn server_expected_client_records() { - let records = NtsRecord::client_key_exchange_records(vec![]).to_vec(); - let result = send_records_to_server(records).await; - - assert!(result.is_ok()); - } - - #[tokio::test] - async fn immediate_end_of_message() { - let records = vec![NtsRecord::EndOfMessage]; - let result = send_records_to_server(records).await; - - assert!(matches!(result, Err(KeyExchangeError::NoValidProtocol))); - } - - #[tokio::test] - async fn double_next_protocol() { - let records = vec![ - NtsRecord::NextProtocol { - protocol_ids: vec![0], - }, - NtsRecord::NextProtocol { - protocol_ids: vec![0], - }, - NtsRecord::EndOfMessage, - ]; - let result = send_records_to_server(records).await; - - assert!(matches!(result, Err(KeyExchangeError::BadRequest))); - } - - #[tokio::test] - async fn records_after_end_of_message() { - let records = vec![ - NtsRecord::NextProtocol { - protocol_ids: vec![0], - }, - NtsRecord::AeadAlgorithm { - critical: false, - algorithm_ids: vec![15], - }, - NtsRecord::EndOfMessage, - NtsRecord::EndOfMessage, - ]; - - let result = send_records_to_server(records).await; - - // records after the first EndOfMessage are ignored - assert!(result.is_ok()); - } - - #[tokio::test] - async fn client_no_valid_algorithm() { - let records = vec![ - NtsRecord::NextProtocol { - protocol_ids: vec![0], - }, - NtsRecord::AeadAlgorithm { - critical: false, - algorithm_ids: vec![], - }, - NtsRecord::EndOfMessage, - ]; - let result = send_records_to_server(records).await; - - assert!(matches!(result, Err(KeyExchangeError::NoValidAlgorithm))); - } - - #[tokio::test] - async fn client_no_valid_protocol() { - let records = vec![ - NtsRecord::NextProtocol { - protocol_ids: vec![], - }, - NtsRecord::AeadAlgorithm { - critical: false, - algorithm_ids: vec![15], - }, - NtsRecord::EndOfMessage, - ]; - let result = send_records_to_server(records).await; - - assert!(matches!(result, Err(KeyExchangeError::NoValidProtocol))); - } - - #[tokio::test] - async fn unrecognized_critical_record() { - let records = vec![ - NtsRecord::Unknown { - record_type: 1234, - critical: true, - data: vec![], - }, - NtsRecord::EndOfMessage, - ]; - let result = send_records_to_server(records).await; - - assert!(matches!( - result, - Err(KeyExchangeError::UnrecognizedCriticalRecord) - )); - } - - #[tokio::test] - async fn client_sends_no_records_clean_shutdown() { - let listener = TcpListener::bind(&("localhost", 0)).await.unwrap(); - let port = listener.local_addr().unwrap().port(); - - tokio::spawn(async move { - // give the server some time to make the port available - tokio::time::sleep(std::time::Duration::from_millis(20)).await; - - // create the stream, then shut it down without sending anything - let mut stream = client_tls_stream("localhost", port).await; - stream.shutdown().await.unwrap(); - }); - - let result = run_server(listener).await; - assert!(matches!(result, Err(KeyExchangeError::IncompleteResponse))); - } - - #[tokio::test] - async fn client_sends_no_records_dirty_shutdown() { - let listener = TcpListener::bind(&("localhost", 0)).await.unwrap(); - let port = listener.local_addr().unwrap().port(); - - tokio::spawn(async move { - // give the server some time to make the port available - tokio::time::sleep(std::time::Duration::from_millis(20)).await; - - // create the stream, then shut it down without sending anything - let stream = client_tls_stream("localhost", port).await; - stream.into_inner().0.shutdown().await.unwrap(); - }); - - let result = run_server(listener).await; - assert!(matches!(result, Err(KeyExchangeError::IncompleteResponse))); - } - - async fn server_error_record(errorcode: u16) -> KeyExchangeError { - let result = send_records_to_server(vec![ - NtsRecord::Error { errorcode }, - NtsRecord::EndOfMessage, - ]) - .await; - - result.unwrap_err() - } - - #[tokio::test] - async fn server_receives_error_record() { - use KeyExchangeError as KEE; - - let error = server_error_record(NtsRecord::UNRECOGNIZED_CRITICAL_RECORD).await; - assert!(matches!(error, KEE::UnrecognizedCriticalRecord)); - - let error = server_error_record(NtsRecord::BAD_REQUEST).await; - assert!(matches!(error, KEE::BadRequest)); - - let error = server_error_record(NtsRecord::INTERNAL_SERVER_ERROR).await; - assert!(matches!(error, KEE::InternalServerError)); - } -} diff --git a/ntpd/src/daemon/mod.rs b/ntpd/src/daemon/mod.rs index 8bebbef43..2d4894746 100644 --- a/ntpd/src/daemon/mod.rs +++ b/ntpd/src/daemon/mod.rs @@ -3,14 +3,21 @@ pub mod config; pub mod keyexchange; mod local_ip_provider; mod ntp_source; +pub mod gps_source; +pub mod pps_source; pub mod nts_key_provider; pub mod observer; mod server; pub mod sockets; pub mod spawn; +pub mod gps_without_gpsd; mod system; pub mod tracing; mod util; +pub mod pps_polling; + + + use std::{error::Error, path::PathBuf}; @@ -80,7 +87,6 @@ pub(crate) async fn initialize_logging_parse_config( async fn run(options: NtpDaemonOptions) -> Result<(), Box> { let config = initialize_logging_parse_config(options.log_level, options.config).await; - // give the user a warning that we use the command line option if config.observability.log_level.is_some() && options.log_level.is_some() { info!("Log level override from command line arguments is active"); diff --git a/ntpd/src/daemon/ntp_source.rs b/ntpd/src/daemon/ntp_source.rs index a8f701ccc..43a8797b4 100644 --- a/ntpd/src/daemon/ntp_source.rs +++ b/ntpd/src/daemon/ntp_source.rs @@ -1,8 +1,7 @@ use std::{future::Future, marker::PhantomData, net::SocketAddr, pin::Pin}; use ntp_proto::{ - NtpClock, NtpInstant, NtpSource, NtpSourceActionIterator, NtpSourceUpdate, NtpTimestamp, - ProtocolVersion, SourceDefaultsConfig, SourceNtsData, SystemSnapshot, + PpsSourceUpdate, GpsSourceUpdate, NtpClock, NtpInstant, NtpSource, NtpSourceActionIterator, NtpSourceUpdate, NtpTimestamp, ProtocolVersion, SourceDefaultsConfig, SourceNtsData, SystemSnapshot }; #[cfg(target_os = "linux")] use timestamped_socket::socket::open_interface_udp; @@ -38,6 +37,8 @@ pub enum MsgForSystem { Unreachable(SourceId), /// Update from source SourceUpdate(SourceId, NtpSourceUpdate), + GpsSourceUpdate(SourceId, GpsSourceUpdate), + PpsSourceUpdate(SourceId, PpsSourceUpdate) } #[derive(Debug, Clone)] diff --git a/ntpd/src/daemon/pps_polling.rs b/ntpd/src/daemon/pps_polling.rs new file mode 100644 index 000000000..498b37c72 --- /dev/null +++ b/ntpd/src/daemon/pps_polling.rs @@ -0,0 +1,146 @@ +use std::process::{Command, Stdio}; +use std::io::{self, BufRead, BufReader}; +use ntp_proto::{NtpDuration, NtpTimestamp}; + +/// Struct to encapsulate the PPS polling information. +#[derive(Debug)] +pub struct Pps { + latest_offset: Option, + port_name: String, + pub measurement_noise: f64, +} + +impl Pps { + /// Opens the PPS device and creates a new Pps instance. + pub fn new(port_name: String, measurement_noise: f64) -> io::Result { + Ok(Pps { + latest_offset: None, + port_name, + measurement_noise, + }) + } + + /// Gets the PPS time and returns it as an NtpTimestamp. + /// + /// # Returns + /// + /// * `Result<(NtpTimestamp, f64, f64), String>` - The result of getting the PPS time, the system time, and the offset. + pub async fn poll_pps_signal(&mut self) -> io::Result> { + let mut child = Command::new("sudo") + .arg("ppstest") + .arg(self.port_name.as_str()) + .stdout(Stdio::piped()) + .spawn() + .expect("Failed to start ppstest"); + + if let Some(stdout) = child.stdout.take() { + let reader = BufReader::new(stdout); + + for line in reader.lines() { + match line { + Ok(line) => { + if let Some((timestamp, nanos)) = Self::parse_ppstest_output(&line) { + let ntp_timestamp = Self::from_unix_timestamp(timestamp, nanos); + let offset = nanos as f64 * 1e-9; + + if offset > 0.5 { + self.latest_offset = Some(offset - 1.0); + return Ok(Some((offset - 1.0, ntp_timestamp))); + } else { + self.latest_offset = Some(offset); + return Ok(Some((offset, ntp_timestamp))); + } + } + } + Err(e) => eprintln!("Error reading line: {}", e), + } + } + } + + let status = child.wait()?; + + if !status.success() { + eprintln!("ppstest exited with a non-zero status"); + } + + Ok(None) + } + + /// Converts Unix timestamp to NtpTimestamp. + fn from_unix_timestamp(unix_timestamp: u64, nanos: u32) -> NtpTimestamp { + const UNIX_TO_NTP_OFFSET: u64 = 2_208_988_800; // Offset in seconds between Unix epoch and NTP epoch + const NTP_SCALE_FRAC: u64 = 4_294_967_296; // 2^32 for scaling nanoseconds to fraction + + let ntp_seconds = unix_timestamp + UNIX_TO_NTP_OFFSET; + let fraction = (nanos as u64 * NTP_SCALE_FRAC) / 1_000_000_000; + let timestamp = (ntp_seconds << 32) | fraction; + + NtpTimestamp::from_fixed_int(timestamp) + } + + fn parse_ppstest_output(line: &str) -> Option<(u64, u32)> { + let parts: Vec<&str> = line.split_whitespace().collect(); + + if parts.len() < 5 { + return None; + } + + if parts[0] == "source" && parts[1].starts_with('0') && parts[2] == "-" && parts[3] == "assert" { + let timestamp_str = parts[4].trim_end_matches(','); + if let Some((secs, nanos_str)) = timestamp_str.split_once('.') { + let timestamp = secs.parse::().ok()?; + let nanos = nanos_str.parse::().ok()?; + return Some((timestamp, nanos)); + } + } + + None + } +} +/// Enum to represent the result of PPS polling. +#[derive(Debug)] +pub enum AcceptResult { + Accept(NtpDuration, NtpTimestamp), + Ignore, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_from_unix_timestamp() { + let unix_timestamp = 1_614_774_800; // This is equivalent to some date in 2021 + let nanos = 500_000_000; // 0.5 seconds + + let ntp_timestamp = Pps::from_unix_timestamp(unix_timestamp, nanos); + + // Expected NTP timestamp calculated manually + let expected_ntp_timestamp = NtpTimestamp::from_fixed_int( + ((unix_timestamp + 2_208_988_800) << 32) | ((nanos as u64 * 4_294_967_296) / 1_000_000_000) + ); + + assert_eq!(ntp_timestamp, expected_ntp_timestamp); + } + + #[test] + fn test_parse_ppstest_output() { + let line = "source 0 - assert 1614774800.500000000, sequence: 0 - clear"; + let result = Pps::parse_ppstest_output(line); + + assert!(result.is_some()); + + let (timestamp, nanos) = result.unwrap(); + assert_eq!(timestamp, 1_614_774_800); + assert_eq!(nanos, 500_000_000); + } + + #[test] + fn test_parse_ppstest_output_invalid() { + let line = "invalid line without proper format"; + let result = Pps::parse_ppstest_output(line); + + assert!(result.is_none()); + } +} + \ No newline at end of file diff --git a/ntpd/src/daemon/pps_source.rs b/ntpd/src/daemon/pps_source.rs new file mode 100644 index 000000000..46898f1ea --- /dev/null +++ b/ntpd/src/daemon/pps_source.rs @@ -0,0 +1,290 @@ +use std::time::Duration; +use std::{future::Future, marker::PhantomData, pin::Pin}; +use tokio::time::{Instant, Sleep}; +use ntp_proto::{NtpClock, NtpInstant, NtpTimestamp, NtpDuration, PpsSource, PpsSourceActionIterator}; +use tracing::{error, info, instrument, warn, Instrument, Span}; +use super::pps_polling::Pps; +use super::pps_polling::AcceptResult; +use std::io; + + +use crate::daemon::ntp_source::MsgForSystem; +use super::{config::TimestampMode, exitcode, ntp_source::SourceChannels, spawn::SourceId}; + +// Trait needed to allow injecting of futures other than `tokio::time::Sleep` for testing +pub trait Wait: Future { + fn reset(self: Pin<&mut Self>, deadline: Instant); +} + +impl Wait for Sleep { + fn reset(self: Pin<&mut Self>, deadline: Instant) { + self.reset(deadline); + } +} + +pub(crate) struct PpsSourceTask { + _wait: PhantomData, + index: SourceId, + clock: C, + channels: SourceChannels, + source: PpsSource, + last_send_timestamp: Option, + pps:Pps, +} + +impl PpsSourceTask +where + C: 'static + NtpClock + Send + Sync, + T: Wait, +{ + async fn run(&mut self, mut poll_wait: Pin<&mut T>) { + loop { + + // Enum to handle the selection of either a Timer or PPS Signal event + enum SelectResult { + Timer, + Recv(io::Result>), + } + + let selected = tokio::select! { + () = &mut poll_wait => { + SelectResult::Timer + }, + result = self.pps.poll_pps_signal() => { + if result.is_err() { + SelectResult::Recv(Err(result.unwrap_err())) + } else { + SelectResult::Recv(result) + } + } + }; + + let actions = match selected { + SelectResult::Recv(result) => { + match result { + Ok(Some(_data)) => { + match accept_pps_time(result) { + AcceptResult::Accept(offset, ntp_timestamp) => { + self.source.handle_incoming(NtpInstant::now(), offset, ntp_timestamp, self.pps.measurement_noise) + } + AcceptResult::Ignore => PpsSourceActionIterator::default(), + } + } + Ok(None) => { + // Handle the case where no data is available + PpsSourceActionIterator::default() + } + Err(_e) => { + // Handle the error + PpsSourceActionIterator::default() + } + } + } + SelectResult::Timer => { + // tracing::debug!("wait completed"); + // let system_snapshot = *self.channels.system_snapshot_receiver.borrow(); + // self.source.handle_timer(system_snapshot); + PpsSourceActionIterator::default() + } + }; + + for action in actions { + match action { + ntp_proto::PpsSourceAction::Send() => { + match self.clock.now() { + Err(e) => { + error!(error = ?e, "There was an error retrieving the current time"); + std::process::exit(exitcode::NOPERM); + } + Ok(ts) => { + self.last_send_timestamp = Some(ts); + } + } + } + ntp_proto::PpsSourceAction::UpdateSystem(update) => { + self.channels + .msg_for_system_sender + .send(MsgForSystem::PpsSourceUpdate(self.index, update)) + .await + .ok(); + } + ntp_proto::PpsSourceAction::SetTimer(timeout) => { + poll_wait.as_mut().reset(Instant::now() + timeout); + } + ntp_proto::PpsSourceAction::Reset => { + self.channels + .msg_for_system_sender + .send(MsgForSystem::Unreachable(self.index)) + .await + .ok(); + return; + } + ntp_proto::PpsSourceAction::Demobilize => { + self.channels + .msg_for_system_sender + .send(MsgForSystem::MustDemobilize(self.index)) + .await + .ok(); + return; + } + } + } + tokio::time::sleep(Duration::from_millis(1000)).await; + } + } +} + + +impl PpsSourceTask +where + C: 'static + NtpClock + Send + Sync, +{ + #[allow(clippy::too_many_arguments)] + #[instrument(skip(clock, channels))] + pub fn spawn( + index: SourceId, + clock: C, + timestamp_mode: TimestampMode, + channels: SourceChannels, + pps: Pps, + ) -> tokio::task::JoinHandle<()> { + info!("spawning pps source"); + tokio::spawn( + (async move { + let (source, initial_actions) = PpsSource::new(); + let poll_wait = tokio::time::sleep(std::time::Duration::default()); + tokio::pin!(poll_wait); + + for action in initial_actions { + match action { + ntp_proto::PpsSourceAction::Send() => { + unreachable!("Should not be sending messages from startup") + } + ntp_proto::PpsSourceAction::UpdateSystem(_) => { + unreachable!("Should not be updating system from startup") + } + ntp_proto::PpsSourceAction::SetTimer(timeout) => { + poll_wait.as_mut().reset(Instant::now() + timeout) + } + ntp_proto::PpsSourceAction::Reset => { + unreachable!("Should not be resetting from startup") + } + ntp_proto::PpsSourceAction::Demobilize => { + unreachable!("Should not be demobilizing from startup") + } + } + } + + let last_send_timestamp = clock.clone().now().ok(); + let mut process = PpsSourceTask { + _wait: PhantomData, + index, + clock, + channels, + source, + pps, + last_send_timestamp, + }; + + process.run(poll_wait).await; + }) + .instrument(Span::current()), + ) + } +} + + + /// Result handling for PPS polling. + pub fn accept_pps_time(result: io::Result>) -> AcceptResult { + match result { + Ok(Some(data)) => { + match parse_pps_time(data) { + Ok((pps_duration, pps_timestamp)) => AcceptResult::Accept(pps_duration, pps_timestamp), + Err(_) => AcceptResult::Ignore, + } + } + Ok(None) => { + AcceptResult::Ignore + } + Err(_receive_error) => { + AcceptResult::Ignore + } + } + } + + fn parse_pps_time(data: (f64, NtpTimestamp)) -> Result<(NtpDuration, NtpTimestamp), Box> { + let ntp_duration = from_seconds(data.0); + Ok((ntp_duration, data.1)) + } + + pub fn from_seconds(seconds: f64) -> NtpDuration { + NtpDuration::from_seconds(seconds) + } + #[cfg(test)] +mod tests { + use super::*; + use ntp_proto::{NtpTimestamp, NtpDuration}; + use std::io; + + // Assuming `AcceptResult` is defined somewhere in your module and doesn't implement `PartialEq` + #[derive(Debug, PartialEq)] + pub enum TestAcceptResult { + Accept(NtpDuration, NtpTimestamp), + Ignore, + } + + // A conversion function if your actual `AcceptResult` doesn't match the test enum + fn convert_accept_result(result: AcceptResult) -> TestAcceptResult { + match result { + AcceptResult::Accept(duration, timestamp) => TestAcceptResult::Accept(duration, timestamp), + AcceptResult::Ignore => TestAcceptResult::Ignore, + } + } + + #[test] + fn test_accept_pps_time_with_valid_data() { + let timestamp = NtpTimestamp::default(); + let duration = 1.0; + let result = Ok(Some((duration, timestamp))); + + let expected_duration = NtpDuration::from_seconds(duration); + let expected = TestAcceptResult::Accept(expected_duration, timestamp); + + assert_eq!(convert_accept_result(accept_pps_time(result)), expected); + } + + #[test] + fn test_accept_pps_time_with_none_data() { + let result = Ok(None); + let expected = TestAcceptResult::Ignore; + + assert_eq!(convert_accept_result(accept_pps_time(result)), expected); + } + + #[test] + fn test_accept_pps_time_with_error() { + let result: io::Result> = Err(io::Error::new(io::ErrorKind::Other, "test error")); + let expected = TestAcceptResult::Ignore; + + assert_eq!(convert_accept_result(accept_pps_time(result)), expected); + } + + #[test] + fn test_parse_pps_time() { + let timestamp = NtpTimestamp::default(); + let duration = 1.0; + + let result = parse_pps_time((duration, timestamp)).unwrap(); + let expected_duration = NtpDuration::from_seconds(duration); + + assert_eq!(result, (expected_duration, timestamp)); + } + + #[test] + fn test_from_seconds() { + let duration = 1.5; + let ntp_duration = from_seconds(duration); + + assert_eq!(ntp_duration, NtpDuration::from_seconds(duration)); + } +} diff --git a/ntpd/src/daemon/spawn/gps.rs b/ntpd/src/daemon/spawn/gps.rs new file mode 100644 index 000000000..8be10f290 --- /dev/null +++ b/ntpd/src/daemon/spawn/gps.rs @@ -0,0 +1,182 @@ +use std::{fmt::Display, sync::Arc}; +use tokio::sync::mpsc; +use crate::daemon::config::GpsConfigSource; + +use super::{BasicSpawner, PortChecker, RealPortChecker, SourceId, SourceRemovedEvent, SpawnAction, SpawnEvent, SpawnerId}; + +struct GpsSource { + id: SourceId, +} + +pub struct GpsSpawner { + id: SpawnerId, + config: GpsConfigSource, + current_sources: Vec, + port_checker: Arc, +} + +#[derive(Debug)] +pub enum GpsSpawnError { + PortNotOpen, +} + +impl Display for GpsSpawnError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "GpsSpawnError") + } +} + +impl std::error::Error for GpsSpawnError {} + +impl GpsSpawner { + pub fn new(config: GpsConfigSource) -> GpsSpawner { + GpsSpawner { + id: Default::default(), + config, + current_sources: Default::default(), + port_checker: Arc::new(RealPortChecker), + } + } + + #[cfg(test)] + pub fn with_mock_port_checker(mut self) -> Self { + use super::MockPortChecker; + + self.port_checker = Arc::new(MockPortChecker); + self + } +} + +#[async_trait::async_trait] +impl BasicSpawner for GpsSpawner { + type Error = GpsSpawnError; + + async fn try_spawn( + &mut self, + action_tx: &mpsc::Sender, + ) -> Result<(), GpsSpawnError> { + match self.port_checker.check_port(self.config.address.clone(), self.config.baud_rate).await { + Ok(_) => println!("Serial port check successful"), + Err(e) => return Err(e), + } + + + // Early return if there is already a GPS source + if !self.current_sources.is_empty() { + return Ok(()); + } + + // Here we just make a GpsSource the device we are using is in the confi + let id = SourceId::new(); + self.current_sources.push(GpsSource { + id, + }); + + let action = SpawnAction::create_gps( + id, + self.config.address.clone(), + self.config.measurement_noise, + self.config.baud_rate, + ); + + tracing::debug!(?action, "intending to spawn new GPS source"); + + action_tx + .send(SpawnEvent::new(self.id, action)) + .await + .expect("Channel was no longer connected"); + + + + Ok(()) + } + + fn is_complete(&self) -> bool { + !self.current_sources.is_empty() + } + + async fn handle_source_removed( + &mut self, + removed_source: SourceRemovedEvent, + ) -> Result<(), GpsSpawnError> { + self.current_sources.retain(|p| p.id != removed_source.id); + Ok(()) + } + + fn get_id(&self) -> SpawnerId { + self.id + } + fn get_addr_description(&self) -> String { + "gps".to_string() + } + + fn get_description(&self) -> &str { + "GPS" + } +} + +#[cfg(test)] +mod tests { + + use tokio::sync::mpsc::{self}; + + use crate::daemon::{ + config::{GpsConfigSource}, + spawn::{ + gps::GpsSpawner, tests::{get_create_gps_params}, BasicSpawner, SourceRemovalReason, SourceRemovedEvent + }, + system::MESSAGE_BUFFER_SIZE, + }; + + #[tokio::test] + async fn creates_a_source() { + let mut spawner = GpsSpawner::new(GpsConfigSource { + address: "/dev/example".to_string(), + baud_rate: 9600, + measurement_noise: 0.001, + }).with_mock_port_checker(); + let spawner_id = spawner.get_id(); + let (action_tx, mut action_rx) = mpsc::channel(MESSAGE_BUFFER_SIZE); + + assert!(!spawner.is_complete()); + spawner.try_spawn(&action_tx).await.unwrap(); + let res = action_rx.try_recv().unwrap(); + assert_eq!(res.id, spawner_id); + let params = get_create_gps_params(res); + assert_eq!(params.addr.to_string(), "/dev/example"); + + // Should be complete after spawning + assert!(spawner.is_complete()); + } + + #[tokio::test] + async fn recreates_a_source() { + let mut spawner = GpsSpawner::new(GpsConfigSource { + address: "/dev/example".to_string(), + baud_rate: 9600, + measurement_noise: 0.001, + }).with_mock_port_checker(); + let (action_tx, mut action_rx) = mpsc::channel(MESSAGE_BUFFER_SIZE); + + assert!(!spawner.is_complete()); + spawner.try_spawn(&action_tx).await.unwrap(); + let res = action_rx.try_recv().unwrap(); + let params = get_create_gps_params(res); + assert!(spawner.is_complete()); + + spawner + .handle_source_removed(SourceRemovedEvent { + id: params.id, + reason: SourceRemovalReason::NetworkIssue, + }) + .await + .unwrap(); + + assert!(!spawner.is_complete()); + spawner.try_spawn(&action_tx).await.unwrap(); + let res = action_rx.try_recv().unwrap(); + let params = get_create_gps_params(res); + assert_eq!(params.addr.to_string(), "/dev/example"); + assert!(spawner.is_complete()); + } +} diff --git a/ntpd/src/daemon/spawn/mod.rs b/ntpd/src/daemon/spawn/mod.rs index bd40a178c..bfec0714d 100644 --- a/ntpd/src/daemon/spawn/mod.rs +++ b/ntpd/src/daemon/spawn/mod.rs @@ -1,5 +1,6 @@ -use std::{net::SocketAddr, sync::atomic::AtomicU64}; +use std::{net::SocketAddr, sync::atomic::AtomicU64, time::Duration}; +use gps::GpsSpawnError; use ntp_proto::{ProtocolVersion, SourceNtsData}; use serde::{Deserialize, Serialize}; use tokio::{ @@ -16,6 +17,8 @@ pub mod nts; pub mod nts_pool; pub mod pool; pub mod standard; +pub mod gps; +pub mod pps; /// Unique identifier for a spawner. /// This is used to identify which spawner was used to create a source @@ -82,6 +85,8 @@ impl SpawnEvent { pub enum SystemEvent { SourceRemoved(SourceRemovedEvent), SourceRegistered(SourceCreateParameters), + GpsSourceRegistered(GpsSourceCreateParameters), + PpsSourceRegistered(PpsSourceCreateParameters), Idle, } @@ -110,6 +115,8 @@ pub enum SourceRemovalReason { #[derive(Debug)] pub enum SpawnAction { Create(SourceCreateParameters), + CreateGps(GpsSourceCreateParameters), + CreatePps(PpsSourceCreateParameters), // Remove(()), } @@ -129,6 +136,33 @@ impl SpawnAction { nts, }) } + + pub fn create_gps( + id: SourceId, + addr: String, + measurement_noise: f64, + baud_rate: u32, + + ) -> SpawnAction{ + SpawnAction::CreateGps(GpsSourceCreateParameters { + id, + addr, + measurement_noise, + baud_rate, + }) + } + + pub fn create_pps( + id: SourceId, + addr: String, + measurement_noise: f64, + ) -> SpawnAction{ + SpawnAction::CreatePps(PpsSourceCreateParameters { + id, + addr, + measurement_noise, + }) + } } #[derive(Debug)] @@ -140,6 +174,21 @@ pub struct SourceCreateParameters { pub nts: Option>, } +#[derive(Debug)] +pub struct GpsSourceCreateParameters { + pub id: SourceId, + pub addr: String, + pub measurement_noise: f64, + pub baud_rate: u32, +} + +#[derive(Debug)] +pub struct PpsSourceCreateParameters { + pub id: SourceId, + pub addr: String, + pub measurement_noise: f64, +} + #[cfg(test)] impl SourceCreateParameters { pub fn from_new_addr(addr: SocketAddr) -> SourceCreateParameters { @@ -180,6 +229,43 @@ impl SourceCreateParameters { } } +#[async_trait::async_trait] +pub trait PortChecker: Send + Sync { + async fn check_port(&self, port_name: String, baud_rate: u32) -> Result<(), GpsSpawnError>; +} + +pub struct RealPortChecker; + +#[async_trait::async_trait] +impl PortChecker for RealPortChecker { + async fn check_port(&self, port_name: String, baud_rate: u32) -> Result<(), GpsSpawnError> { + let timeout = Duration::from_secs(1); + + let mut port = serialport::new(port_name, baud_rate) + .timeout(timeout) + .open() + .map_err(|_e| { + GpsSpawnError::PortNotOpen + })?; + + if let Err(_e) = port.set_timeout(timeout) { + return Err(GpsSpawnError::PortNotOpen) + } + + drop(port); + + Ok(()) + } +} + +pub struct MockPortChecker; + +#[async_trait::async_trait] +impl PortChecker for MockPortChecker { + async fn check_port(&self, _port_name: String, _baud_rate: u32) -> Result<(), GpsSpawnError> { + Ok(()) + } +} #[async_trait::async_trait] pub trait Spawner { type Error: std::error::Error + Send; @@ -246,6 +332,21 @@ pub trait BasicSpawner { Ok(()) } + async fn handle_gps_registered( + &mut self, + _event: GpsSourceCreateParameters, + ) -> Result<(), Self::Error> { + Ok(()) + } + + async fn handle_pps_registered( + &mut self, + _event: PpsSourceCreateParameters, + ) -> Result<(), Self::Error> { + Ok(()) + } + + /// Get the id of the spawner fn get_id(&self) -> SpawnerId; @@ -305,6 +406,12 @@ where SystemEvent::SourceRemoved(removed_source) => { self.handle_source_removed(removed_source).await?; } + SystemEvent::GpsSourceRegistered(source_params) => { + self.handle_gps_registered(source_params).await?; + } + SystemEvent::PpsSourceRegistered(source_params) => { + self.handle_pps_registered(source_params).await?; + } SystemEvent::Idle => {} } } @@ -327,10 +434,31 @@ where #[cfg(test)] mod tests { - use super::{SourceCreateParameters, SpawnAction, SpawnEvent}; + use super::{GpsSourceCreateParameters, PpsSourceCreateParameters, SourceCreateParameters, SpawnAction, SpawnEvent}; pub fn get_create_params(res: SpawnEvent) -> SourceCreateParameters { - let SpawnAction::Create(params) = res.action; - params + if let SpawnAction::Create(params) = res.action { + params + } else { + panic!("Expected SpawnAction::Create variant"); + } } -} + + pub fn get_create_gps_params(res: SpawnEvent) -> GpsSourceCreateParameters { + if let SpawnAction::CreateGps(params) = res.action { + params + } else { + panic!("Expected SpawnAction::Create variant"); + } + } + + pub fn get_create_pps_params(res: SpawnEvent) -> PpsSourceCreateParameters { + if let SpawnAction::CreatePps(params) = res.action { + params + } else { + panic!("Expected SpawnAction::Create variant"); + } + } + + +} \ No newline at end of file diff --git a/ntpd/src/daemon/spawn/pps.rs b/ntpd/src/daemon/spawn/pps.rs new file mode 100644 index 000000000..c53f64fbe --- /dev/null +++ b/ntpd/src/daemon/spawn/pps.rs @@ -0,0 +1,165 @@ +use std::fmt::Display; +use tokio::sync::mpsc; + + +use crate::daemon::config::PpsConfigSource; + +use super::{BasicSpawner, SourceId, SourceRemovedEvent, SpawnAction, SpawnEvent, SpawnerId}; + +pub struct PpsSource { + id: SourceId, +} + +pub struct PpsSpawner { + id: SpawnerId, + current_sources: Vec, + config: PpsConfigSource, +} + +#[derive(Debug)] +pub enum PpsSpawnError {} + +impl Display for PpsSpawnError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "ppsSpawnError") + } +} + +impl std::error::Error for PpsSpawnError {} + +impl PpsSpawner { + pub fn new(config: PpsConfigSource) -> PpsSpawner { + PpsSpawner { + id: Default::default(), + current_sources: Default::default(), + config, + } + } +} + +#[async_trait::async_trait] +impl BasicSpawner for PpsSpawner { + type Error = PpsSpawnError; + + async fn try_spawn( + &mut self, + action_tx: &mpsc::Sender, + ) -> Result<(), PpsSpawnError> { + // Early return if there is already a pps source + if !self.current_sources.is_empty() { + return Ok(()); + } + + let id = SourceId::new(); + self.current_sources.push(PpsSource { + id, + }); + + + + let action = SpawnAction::create_pps( + id, + self.config.address.clone(), + self.config.measurement_noise, + ); + + tracing::debug!(?action, "intending to spawn new pps source"); + + action_tx + .send(SpawnEvent::new(self.id, action)) + .await + .expect("Channel was no longer connected"); + + + + Ok(()) + } + + fn is_complete(&self) -> bool { + !self.current_sources.is_empty() + } + + async fn handle_source_removed( + &mut self, + removed_source: SourceRemovedEvent, + ) -> Result<(), PpsSpawnError> { + self.current_sources.retain(|p| p.id != removed_source.id); + Ok(()) + } + + fn get_id(&self) -> SpawnerId { + self.id + } + fn get_addr_description(&self) -> String { + "pps".to_string() + } + + fn get_description(&self) -> &str { + "PPS" + } +} + + +#[cfg(test)] +mod tests { + + use tokio::sync::mpsc::{self}; + + use crate::daemon::{ + config::PpsConfigSource, + spawn::{pps::PpsSpawner, tests::get_create_pps_params, BasicSpawner, SourceRemovalReason, SourceRemovedEvent + }, + system::MESSAGE_BUFFER_SIZE, + }; + + #[tokio::test] + async fn creates_a_source() { + let mut spawner = PpsSpawner::new(PpsConfigSource { + address: "/dev/example".to_string(), + measurement_noise: 0.001, + }); + let spawner_id = spawner.get_id(); + let (action_tx, mut action_rx) = mpsc::channel(MESSAGE_BUFFER_SIZE); + + assert!(!spawner.is_complete()); + spawner.try_spawn(&action_tx).await.unwrap(); + let res = action_rx.try_recv().unwrap(); + assert_eq!(res.id, spawner_id); + let params = get_create_pps_params(res); + assert_eq!(params.addr.to_string(), "/dev/example"); + + // Should be complete after spawning + assert!(spawner.is_complete()); + } + + #[tokio::test] + async fn recreates_a_source() { + let mut spawner = PpsSpawner::new(PpsConfigSource { + address: "/dev/example".to_string(), + measurement_noise: 0.001, + }); + let (action_tx, mut action_rx) = mpsc::channel(MESSAGE_BUFFER_SIZE); + + assert!(!spawner.is_complete()); + spawner.try_spawn(&action_tx).await.unwrap(); + let res = action_rx.try_recv().unwrap(); + let params = get_create_pps_params(res); + assert!(spawner.is_complete()); + + spawner + .handle_source_removed(SourceRemovedEvent { + id: params.id, + reason: SourceRemovalReason::NetworkIssue, + }) + .await + .unwrap(); + + assert!(!spawner.is_complete()); + spawner.try_spawn(&action_tx).await.unwrap(); + let res = action_rx.try_recv().unwrap(); + let params = get_create_pps_params(res); + assert_eq!(params.addr.to_string(), "/dev/example"); + assert!(spawner.is_complete()); + } +} + diff --git a/ntpd/src/daemon/system.rs b/ntpd/src/daemon/system.rs index 19811c83f..35cc96e36 100644 --- a/ntpd/src/daemon/system.rs +++ b/ntpd/src/daemon/system.rs @@ -1,12 +1,14 @@ -#[cfg(feature = "unstable_nts-pool")] -use super::spawn::nts_pool::NtsPoolSpawner; use super::{ config::{ClockConfig, NormalizedAddress, NtpSourceConfig, ServerConfig, TimestampMode}, + gps_source::GpsSourceTask, ntp_source::{MsgForSystem, SourceChannels, SourceTask, Wait}, + pps_source::PpsSourceTask, server::{ServerStats, ServerTask}, spawn::{ - nts::NtsSpawner, pool::PoolSpawner, standard::StandardSpawner, SourceCreateParameters, - SourceId, SourceRemovalReason, SpawnAction, SpawnEvent, Spawner, SpawnerId, SystemEvent, + gps::GpsSpawner, nts::NtsSpawner, pool::PoolSpawner, pps::PpsSpawner, + standard::StandardSpawner, GpsSourceCreateParameters, PpsSourceCreateParameters, + SourceCreateParameters, SourceId, SourceRemovalReason, SpawnAction, SpawnEvent, Spawner, + SpawnerId, SystemEvent, }, ObservableSourceState, ObservedSourceState, }; @@ -16,6 +18,8 @@ use std::{ time::Duration, }; +use super::gps_without_gpsd::Gps; +use super::pps_polling::Pps; use ntp_proto::{ KeySet, NtpClock, SourceDefaultsConfig, SynchronizationConfig, System, SystemSnapshot, }; @@ -99,6 +103,22 @@ pub async fn spawn( for source_config in source_configs { match source_config { + NtpSourceConfig::Gps(cfg) => { + system + .add_spawner(GpsSpawner::new(cfg.clone())) + .map_err(|e| { + tracing::error!("Could not spawn gps source: {}", e); + std::io::Error::new(std::io::ErrorKind::Other, e) + })?; + } + NtpSourceConfig::Pps(cfg) => { + system + .add_spawner(PpsSpawner::new(cfg.clone())) + .map_err(|e| { + tracing::error!("Could not spawn pps source: {}", e); + std::io::Error::new(std::io::ErrorKind::Other, e) + })?; + } NtpSourceConfig::Standard(cfg) => { system .add_spawner(StandardSpawner::new(cfg.clone())) @@ -157,33 +177,25 @@ struct SystemSpawnerData { struct SystemTask { _wait: PhantomData>, source_defaults_config: SourceDefaultsConfig, + clock: C, // Add this field system: System, - system_snapshot_sender: tokio::sync::watch::Sender, source_snapshots_sender: tokio::sync::watch::Sender>, server_data_sender: tokio::sync::watch::Sender>, keyset: tokio::sync::watch::Receiver>, ip_list: tokio::sync::watch::Receiver>, - msg_for_system_rx: mpsc::Receiver, spawn_tx: mpsc::Sender, spawn_rx: mpsc::Receiver, - sources: HashMap, servers: Vec, spawners: Vec, - source_channels: SourceChannels, - clock: C, - - // which timestamps to use (this is a hint, OS or hardware may ignore) timestamp_mode: TimestampMode, - - // bind the socket to a specific interface. This is relevant for hardware timestamping, - // because the interface determines which clock is used to produce the timestamps. interface: Option, } +#[allow(clippy::too_many_arguments)] impl SystemTask { fn new( clock: C, @@ -216,18 +228,16 @@ impl SystemTask { SystemTask { _wait: PhantomData, source_defaults_config, + clock: clock.clone(), system, - system_snapshot_sender, source_snapshots_sender, server_data_sender, keyset: keyset.clone(), ip_list, - msg_for_system_rx: msg_for_system_receiver, spawn_rx, spawn_tx, - sources: Default::default(), servers: Default::default(), spawners: Default::default(), @@ -235,7 +245,6 @@ impl SystemTask { msg_for_system_sender, system_snapshot_receiver: system_snapshot_receiver.clone(), }, - clock, timestamp_mode, interface, }, @@ -247,6 +256,16 @@ impl SystemTask { ) } + // // Method to update pps_source_id and reinitialize system + // fn update_pps_source_id(&mut self, pps_source_id: i32) { + // self.system = System::new( + // self.clock.clone(), + // self.synchronization_config.clone(), + // self.source_defaults_config.clone(), + // self.ip_list.borrow().clone(), + // ); + // } + fn add_spawner( &mut self, spawner: impl Spawner + Send + Sync + 'static, @@ -337,6 +356,19 @@ impl SystemTask { Ok(timer) => self.handle_state_update(timer, wait), } } + MsgForSystem::GpsSourceUpdate(index, update) => { + match self.system.handle_gps_source_update(index, update) { + Err(e) => unreachable!("Could not process source measurement: {}", e), + Ok(timer) => self.handle_state_update(timer, wait), + } + } + MsgForSystem::PpsSourceUpdate(index, update) => { + match self.system.handle_pps_source_update(index, update) { + Err(e) => unreachable!("Could not process source measurement: {}", e), + Ok(timer) => self.handle_state_update(timer, wait), + } + } + MsgForSystem::NetworkIssue(index) => { self.handle_source_network_issue(index).await?; } @@ -344,7 +376,6 @@ impl SystemTask { self.handle_source_unreachable(index).await?; } } - // Don't care if there is no receiver for source snapshots (which might happen if // we don't enable observing in the configuration) let _ = self @@ -470,11 +501,105 @@ impl SystemTask { Ok(source_id) } + async fn create_gps_source( + &mut self, + spawner_id: SpawnerId, + params: GpsSourceCreateParameters, + ) -> Result { + let source_id = params.id; + info!(source_id=?source_id, spawner=?spawner_id, "new gps source"); + + self.system.handle_source_create(source_id)?; + + info!("creating gps instance:"); + let port_name = ¶ms.addr; + let measurement_noise = params.measurement_noise; + let baud_rate = params.baud_rate; + let timeout = Duration::from_secs(10); + let gps: Gps = Gps::new(port_name, baud_rate, timeout, measurement_noise).unwrap(); + + info!("creating gps source task:"); + GpsSourceTask::spawn( + source_id, + self.clock.clone(), + self.timestamp_mode, + self.source_channels.clone(), + gps, + ); + + // Don't care if there is no receiver + let _ = self + .source_snapshots_sender + .send(self.observe_sources().collect()); + + // Try and find a related spawner and notify that spawner. + // This makes sure that the spawner that initially sent the create event + // is now aware that the source was added to the system. + if let Some(s) = self.spawners.iter().find(|s| s.id == spawner_id) { + let _ = s + .notify_tx + .send(SystemEvent::GpsSourceRegistered(params)) + .await; + } + + Ok(source_id) + } + + async fn create_pps_source( + &mut self, + spawner_id: SpawnerId, + params: PpsSourceCreateParameters, + ) -> Result { + let source_id = params.id; + info!(source_id=?source_id, spawner=?spawner_id, "new pps source"); + + self.system.handle_source_create(source_id)?; + + info!("creating pps instance:"); + //let pps_path = "/dev/pps0"; // Replace with the actual path to your PPS device + info!("creating gps instance:"); + let port_name = ¶ms.addr; + let measurement_noise = params.measurement_noise; + let pps: Pps = Pps::new(port_name.to_string(), measurement_noise).unwrap(); + + info!("creating pps source task:"); + PpsSourceTask::spawn( + source_id, + self.clock.clone(), + self.timestamp_mode, + self.source_channels.clone(), + pps, + ); + + // Don't care if there is no receiver + let _ = self + .source_snapshots_sender + .send(self.observe_sources().collect()); + + // Try and find a related spawner and notify that spawner. + // This makes sure that the spawner that initially sent the create event + // is now aware that the source was added to the system. + if let Some(s) = self.spawners.iter().find(|s| s.id == spawner_id) { + let _ = s + .notify_tx + .send(SystemEvent::PpsSourceRegistered(params)) + .await; + } + + Ok(source_id) + } + async fn handle_spawn_event(&mut self, event: SpawnEvent) -> Result<(), C::Error> { match event.action { SpawnAction::Create(params) => { self.create_source(event.id, params).await?; } + SpawnAction::CreateGps(params) => { + self.create_gps_source(event.id, params).await?; + } + SpawnAction::CreatePps(params) => { + self.create_pps_source(event.id, params).await?; + } } Ok(()) } @@ -641,6 +766,8 @@ mod tests { root_dispersion: NtpDuration::default(), leap: NtpLeapIndicator::NoWarning, precision: 0, + gps: None, + pps: None, }, ), ), @@ -679,6 +806,8 @@ mod tests { root_dispersion: NtpDuration::default(), leap: NtpLeapIndicator::NoWarning, precision: 0, + gps: None, + pps: None, }, ), ), diff --git a/ntpd/tests/ctl.rs b/ntpd/tests/ctl.rs index d464ce5f3..c781252a1 100644 --- a/ntpd/tests/ctl.rs +++ b/ntpd/tests/ctl.rs @@ -1,9 +1,4 @@ -use std::{ - io::Write, - os::unix::net::UnixListener, - process::{Command, Output}, - thread::spawn, -}; +use std::process::{Command, Output}; fn contains_bytes(mut haystack: &[u8], needle: &[u8]) -> bool { while haystack.len() >= needle.len() { @@ -23,7 +18,6 @@ fn test_ntp_ctl_output(args: &[&str]) -> Output { } const CARGO_MANIFEST_DIR: &str = env!("CARGO_MANIFEST_DIR"); -const CARGO_TARGET_TMPDIR: &str = env!("CARGO_TARGET_TMPDIR"); #[test] fn test_validate_bad() { @@ -52,42 +46,6 @@ fn test_validate_good() { assert_eq!(result.status.code(), Some(0)); } -const EXAMPLE_SOCKET_OUTPUT: &str = r#"{"program":{"version":"1.0.0","build_commit":"test","build_commit_date":"2000-01-01","uptime_seconds":0.12345},"system":{"stratum":3,"reference_id":3243240718,"accumulated_steps_threshold":null,"poll_interval":4,"precision":3.814697266513178e-6,"root_delay":0.004877627828362777,"root_dispersion":0.0004254912492878482,"leap_indicator":"Unknown","accumulated_steps":0.002842015820285775},"sources":[{"Observable":{"offset":0.00031014974236259,"uncertainty":0.000050753355038062054,"delay":0.0036874422812106654,"remote_delay":0.0011901855471521117,"remote_uncertainty":0.019378662113886946,"last_update":{"timestamp":16760961381687937893},"unanswered_polls":0,"poll_interval":4,"address":"1.2.3.4:123","name":"ntpd-rs.pool.ntp.org:123","id":3}},{"Observable":{"offset":0.0003928544466367118,"uncertainty":0.00005519413390550626,"delay":0.004574143328837618,"remote_delay":0.001602172851935535,"remote_uncertainty":0.0004425048829155287,"last_update":{"timestamp":16760961379467247810},"unanswered_polls":0,"poll_interval":4,"address":"5.6.7.8:123","name":"ntpd-rs.pool.ntp.org:123","id":1}},{"Observable":{"offset":0.00043044891218432433,"uncertainty":0.00005691661500765863,"delay":0.004752595444385101,"remote_delay":0.001602172851935535,"remote_uncertainty":0.03733825684463099,"last_update":{"timestamp":16760961371126323413},"unanswered_polls":0,"poll_interval":4,"address":"9.10.11.12:123","name":"ntpd-rs.pool.ntp.org:123","id":2}},{"Observable":{"offset":-0.0019038764298669707,"uncertainty":0.00016540312212086355,"delay":0.007399475902179134,"remote_delay":0.01371765137038139,"remote_uncertainty":0.0014495849612750078,"last_update":{"timestamp":16760961373841849724},"unanswered_polls":0,"poll_interval":4,"address":"13.14.15.16:123","name":"ntpd-rs.pool.ntp.org:123","id":4}}],"servers":[]}"#; - -#[test] -fn test_status() { - let _ = std::fs::remove_file(format!("{CARGO_TARGET_TMPDIR}/status_test_socket")); - let socket = UnixListener::bind(format!("{CARGO_TARGET_TMPDIR}/status_test_socket")).unwrap(); - - spawn(move || { - let (mut stream, _) = socket.accept().unwrap(); - stream.write_all(EXAMPLE_SOCKET_OUTPUT.as_bytes()).unwrap(); - }); - - let test_config_contents = format!( - r#"[observability] -observation-path = "{CARGO_TARGET_TMPDIR}/status_test_socket" - -[[source]] -mode = "pool" -address = "ntpd-rs.pool.ntp.org" -count = 4 -"# - ); - - let test_config_path = format!("{CARGO_TARGET_TMPDIR}/status_test_config"); - std::fs::write(&test_config_path, test_config_contents.as_bytes()).unwrap(); - - let result = test_ntp_ctl_output(&["status", "-c", &test_config_path]); - - assert!(contains_bytes(&result.stdout, b"ntpd-rs.pool.ntp.org")); - assert!(contains_bytes( - &result.stdout, - "0.000310±0.000051(±0.003687)s".as_bytes() - )); - assert_eq!(result.status.code(), Some(0)); -} - #[test] fn test_version() { let result = test_ntp_ctl_output(&["-v"]);