diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4ea5374..9c28035 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,14 +10,8 @@ jobs: - name: Install Dependencies run: sudo apt-get update && sudo apt-get install libdbus-1-dev - uses: actions/checkout@v4 - - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true - - uses: actions-rs/cargo@v1 - with: - command: check + - uses: dtolnay/rust-toolchain@stable + - run: cargo check test: name: Test Suite @@ -26,32 +20,18 @@ jobs: - name: Install Dependencies run: sudo apt-get update && sudo apt-get install libdbus-1-dev - uses: actions/checkout@v4 - - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true - - uses: actions-rs/cargo@v1 - with: - command: test + - uses: dtolnay/rust-toolchain@stable + - run: cargo test fmt: name: Rustfmt runs-on: ubuntu-latest steps: - - name: Install Dependencies - run: sudo apt-get update && sudo apt-get install libdbus-1-dev - uses: actions/checkout@v4 - - uses: actions-rs/toolchain@v1 + - uses: dtolnay/rust-toolchain@stable with: - profile: minimal - toolchain: stable - override: true - - run: rustup component add rustfmt - - uses: actions-rs/cargo@v1 - with: - command: fmt - args: --all -- --check + components: rustfmt + - run: cargo fmt --all -- --check clippy: name: Clippy @@ -60,13 +40,7 @@ jobs: - name: Install Dependencies run: sudo apt-get update && sudo apt-get install libdbus-1-dev - uses: actions/checkout@v4 - - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true - - run: rustup component add clippy - - uses: actions-rs/cargo@v1 + - uses: dtolnay/rust-toolchain@stable with: - command: clippy - args: --all-targets --all-features -- -D warnings \ No newline at end of file + components: clippy + - run: cargo clippy --all-targets --all-features -- -D warnings \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1537e8c..1a78c20 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,25 +11,27 @@ jobs: strategy: fail-fast: false matrix: - target: [x86_64-pc-windows-gnu, x86_64-unknown-linux-musl, x86_64-apple-darwin] + target: + - x86_64-pc-windows-gnu + - x86_64-unknown-linux-musl + - x86_64-apple-darwin + - aarch64-apple-darwin steps: - name: Install Dependencies run: sudo apt-get update && sudo apt-get install libdbus-1-dev - - uses: actions/checkout@master + - uses: actions/checkout@v4 - name: Compile and release - uses: rust-build/rust-build.action@latest + uses: rust-build/rust-build.action@v1.4.5 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} RUSTTARGET: ${{ matrix.target }} + crates-publish: name: Crates Publish runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - override: true + - uses: dtolnay/rust-toolchain@stable - uses: katyo/publish-crates@v2 with: registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }} \ No newline at end of file diff --git a/.run/uplift help.run.xml b/.run/uplift help.run.xml new file mode 100644 index 0000000..1cfab15 --- /dev/null +++ b/.run/uplift help.run.xml @@ -0,0 +1,22 @@ + + + + \ No newline at end of file diff --git a/.run/uplift query.run.xml b/.run/uplift query.run.xml new file mode 100644 index 0000000..fabdb3e --- /dev/null +++ b/.run/uplift query.run.xml @@ -0,0 +1,22 @@ + + + + \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 8558253..4f163fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,83 +4,106 @@ version = 4 [[package]] name = "aho-corasick" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "anstream" -version = "0.6.11" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", + "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.8" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.2" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.0" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.1" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", - "windows-sys 0.48.0", + "once_cell_polyfill", + "windows-sys 0.61.2", ] [[package]] name = "anyhow" -version = "1.0.94" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "async-trait" -version = "0.1.83" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + [[package]] name = "autocfg" -version = "1.1.0" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" @@ -90,9 +113,30 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.6.0" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] [[package]] name = "block2" @@ -105,12 +149,11 @@ dependencies = [ [[package]] name = "bluez-async" -version = "0.7.2" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ce7d4413c940e8e3cb6afc122d3f4a07096aca259d286781128683fc9f39d9b" +checksum = "84ae4213cc2a8dc663acecac67bbdad05142be4d8ef372b6903abf878b0c690a" dependencies = [ - "async-trait", - "bitflags 2.6.0", + "bitflags 2.13.0", "bluez-generated", "dbus", "dbus-tokio", @@ -119,52 +162,66 @@ dependencies = [ "log", "serde", "serde-xml-rs", - "thiserror", + "thiserror 2.0.18", "tokio", "uuid", ] [[package]] name = "bluez-generated" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d1c659dbc82f0b8ca75606c91a371e763589b7f6acf36858eeed0c705afe367" +checksum = "9676783265eadd6f11829982792c6f303f3854d014edfba384685dcf237dd062" dependencies = [ "dbus", ] [[package]] name = "btleplug" -version = "0.11.6" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82837101dad9a257a3ffe35fbc2ef4df9a27aecbe5343ce83ac233bcab283394" +checksum = "52c3264dbe2c8e29381e4e95aa2d2783ad0b9192b511240f3755b7e5e3cee87e" dependencies = [ "async-trait", - "bitflags 2.6.0", + "bitflags 2.13.0", "bluez-async", - "dashmap 6.1.0", + "dashmap", "dbus", "futures", "jni", - "jni-utils", "log", "objc2", "objc2-core-bluetooth", "objc2-foundation", "once_cell", + "serde", + "serde_bytes", "static_assertions", - "thiserror", + "thiserror 2.0.18", "tokio", "tokio-stream", "uuid", "windows", + "windows-future", ] [[package]] -name = "bytes" +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" [[package]] name = "cesu8" @@ -174,15 +231,26 @@ checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chacha20" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core", +] [[package]] name = "clap" -version = "4.5.23" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -190,9 +258,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.23" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -202,9 +270,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.18" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck", "proc-macro2", @@ -214,54 +282,139 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" [[package]] name = "colorchoice" -version = "1.0.0" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "combine" -version = "4.6.6" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" dependencies = [ "bytes", "memchr", ] +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "confy" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8807c397789cbe02bbdb1a27ea5f345584132808697b2a3f957c829829ee4814" +dependencies = [ + "etcetera 0.10.0", + "lazy_static", + "serde", + "thiserror 2.0.18", + "toml", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" -version = "0.8.20" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] -name = "dashmap" -version = "5.5.3" +name = "crypto-common" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ - "cfg-if", - "hashbrown", - "lock_api", - "once_cell", - "parking_lot_core", + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", ] [[package]] name = "dashmap" -version = "6.1.0" +version = "6.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" dependencies = [ "cfg-if", "crossbeam-utils", - "hashbrown", + "hashbrown 0.14.5", "lock_api", "once_cell", "parking_lot_core", @@ -269,15 +422,15 @@ dependencies = [ [[package]] name = "dbus" -version = "0.9.7" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bb21987b9fb1613058ba3843121dd18b163b254d8a6e797e144cbac14d96d1b" +checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e" dependencies = [ "futures-channel", "futures-util", "libc", "libdbus-sys", - "winapi", + "windows-sys 0.61.2", ] [[package]] @@ -291,17 +444,95 @@ dependencies = [ "tokio", ] +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror 2.0.18", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common 0.1.6", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "crypto-common 0.2.2", + "ctutils", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "downcast" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" + [[package]] name = "either" -version = "1.9.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +dependencies = [ + "serde", +] [[package]] name = "env_filter" -version = "0.1.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea" +checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217" dependencies = [ "log", "regex", @@ -309,22 +540,95 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.5" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" +checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6" dependencies = [ "anstream", "anstyle", "env_filter", - "humantime", + "jiff", "log", ] +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "etcetera" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26c7b13d0780cb82722fd59f6f57f925e143427e4a75313a6c77243bf5326ae6" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.59.0", +] + +[[package]] +name = "etcetera" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de48cc4d1c1d97a20fd819def54b890cadde72ed3ad0c614822a0a433361be96" +dependencies = [ + "cfg-if", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "flume" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fragile" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8878864ba14bb86e818a412bfd6f18f9eabd4ec0f008a28e8f7eb61db532fcf9" +dependencies = [ + "futures-core", +] + [[package]] name = "futures" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -337,9 +641,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -347,32 +651,43 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", "futures-util", ] +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", @@ -381,21 +696,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -405,76 +720,321 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] [[package]] -name = "hashbrown" -version = "0.14.2" +name = "generic-array" +version = "0.14.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", +] [[package]] -name = "heck" -version = "0.5.0" +name = "getrandom" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "rand_core", +] [[package]] -name = "hermit-abi" -version = "0.3.9" +name = "hashbrown" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] -name = "humantime" -version = "2.1.0" +name = "hashbrown" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] [[package]] -name = "itertools" -version = "0.10.5" +name = "hashbrown" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" -dependencies = [ - "either", -] +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] -name = "jni" -version = "0.19.0" +name = "hashlink" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" +checksum = "824e001ac4f3012dd16a264bec811403a67ca9deb6c102fc5049b32c4574b35f" dependencies = [ - "cesu8", - "combine", - "jni-sys", - "log", - "thiserror", - "walkdir", + "hashbrown 0.16.1", ] [[package]] -name = "jni-sys" -version = "0.3.0" +name = "heck" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] -name = "jni-utils" -version = "0.1.1" +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "259e9f2c3ead61de911f147000660511f07ab00adeed1d84f5ac4d0386e7a6c4" +checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018" dependencies = [ - "dashmap 5.5.3", - "futures", - "jni", + "hmac", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "hybrid-array" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +dependencies = [ + "typenum", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jiff" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccfe6121cbe750cf81efa362d85c0bde7ea298ec43092d3a193baca59cdbd634" +dependencies = [ + "defmt", + "jiff-static", "log", - "once_cell", - "static_assertions", - "uuid", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e165e897f662d428f3cd3828a919dbe067c2d42bb1031eede74ef9d27ecdedd2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "jni" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" +dependencies = [ + "cesu8", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", ] [[package]] @@ -485,51 +1045,160 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.179" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libdbus-sys" -version = "0.2.5" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06085512b750d640299b79be4bad3d2fa90a9c00b1fd9e1b46364f66f0485c72" +checksum = "b1f111c8c41e7c61a49cd34e44c7619462967221a6443b0ec299e0ac30cfb9b1" dependencies = [ "pkg-config", + "vcpkg", ] +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + [[package]] name = "lock_api" -version = "0.4.11" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.22" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "md-5" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" +dependencies = [ + "cfg-if", + "digest 0.11.3", +] [[package]] name = "memchr" -version = "2.6.4" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" [[package]] name = "mio" -version = "1.0.2" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" dependencies = [ - "hermit-abi", "libc", "wasi", - "windows-sys 0.52.0", + "windows-sys 0.61.2", +] + +[[package]] +name = "mockall" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39a6bfcc6c8c7eed5ee98b9c3e33adc726054389233e201c95dab2d41a3839d2" +dependencies = [ + "cfg-if", + "downcast", + "fragile", + "mockall_derive", + "predicates", + "predicates-tree", +] + +[[package]] +name = "mockall_derive" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ca3004c2efe9011bd4e461bd8256445052b9615405b4f7ea43fc8ca5c20898" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", + "serde", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", + "serde", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-integer", + "num-traits", + "serde", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", ] [[package]] @@ -554,16 +1223,16 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a644b62ffb826a5277f536cf0f701493de420b13d40e700c452c36567771111" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.13.0", "objc2", "objc2-foundation", ] [[package]] name = "objc2-encode" -version = "4.0.3" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7891e71393cd1f227313c9379a26a584ff3d7e6e7159e988851f0934c993f0f8" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" [[package]] name = "objc2-foundation" @@ -571,7 +1240,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.13.0", "block2", "libc", "objc2", @@ -579,73 +1248,168 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.20.2" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "parking" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] [[package]] name = "parking_lot_core" -version = "0.9.9" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.48.5", + "windows-link", ] [[package]] -name = "pin-project-lite" -version = "0.2.13" +name = "percent-encoding" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] -name = "pin-utils" -version = "0.1.0" +name = "pin-project-lite" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pkg-config" -version = "0.3.27" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "predicates" +version = "3.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" +dependencies = [ + "anstyle", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144" + +[[package]] +name = "predicates-tree" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2" +dependencies = [ + "predicates-core", + "termtree", +] [[package]] name = "proc-macro2" -version = "1.0.81" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.36" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "redox_syscall" -version = "0.4.1" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.13.0", ] [[package]] name = "regex" -version = "1.10.2" +version = "1.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" dependencies = [ "aho-corasick", "memchr", @@ -655,9 +1419,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.3" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -666,9 +1430,34 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.2" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rmp" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "rmp-serde" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155" +dependencies = [ + "rmp", + "serde", +] + +[[package]] +name = "rustversion" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "same-file" @@ -687,109 +1476,440 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.190" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ + "serde_core", "serde_derive", ] [[package]] name = "serde-xml-rs" -version = "0.6.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb3aa78ecda1ebc9ec9847d5d3aba7d618823446a049ba2491940506da6e2782" +checksum = "cc2215ce3e6a77550b80a1c37251b7d294febaf42e36e21b7b411e0bf54d540d" dependencies = [ "log", "serde", - "thiserror", - "xml-rs", + "thiserror 2.0.18", + "xml", +] + +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.190" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "sha1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "sqlx" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "378620ccc25c62c89d8be1c819e76a88d59bdcc3304733330788948e619bfd71" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b44e85bf579a8eeb4ceaa77a3a523baf2bf0e9bac7e40f405d537b5d2d5ccb" +dependencies = [ + "base64", + "bytes", + "cfg-if", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.16.1", + "hashlink", + "indexmap", + "log", + "memchr", + "percent-encoding", + "serde", + "serde_json", + "sha2 0.10.9", + "smallvec", + "thiserror 2.0.18", + "tracing", + "url", +] + +[[package]] +name = "sqlx-macros" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd2b84f2bc39a5705ef27ec785a11c934a41bbd4a24941e257927cddc26b60bf" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb8d96de5fdc85a5c4ec813432b523ec637e80ba98f046555f75f7908ddac7c3" +dependencies = [ + "cfg-if", + "dotenvy", + "either", + "heck", + "hex", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2 0.10.9", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn", + "thiserror 2.0.18", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90b8020fe17c5f2c245bfa2505d7ef59c5604839527c740266ad2214acebea27" +dependencies = [ + "bitflags 2.13.0", + "byteorder", + "bytes", + "crc", + "digest 0.11.3", + "dotenvy", + "either", + "futures-core", + "futures-util", + "generic-array", + "log", + "percent-encoding", + "serde", + "sha1", + "sha2 0.11.0", + "sqlx-core", + "thiserror 2.0.18", + "tracing", +] + +[[package]] +name = "sqlx-postgres" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87a2bdd6e83f6b3ea525ca9fee568030508b58355a43d0b2c1674d5f79dcd65e" +dependencies = [ + "atoi", + "base64", + "bitflags 2.13.0", + "byteorder", + "crc", + "dotenvy", + "etcetera 0.11.0", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "rand", + "serde", + "serde_json", + "sha2 0.11.0", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.18", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488e99c397a62007e4229aec669a179816339afc6d2620ca6fa420dbee2e982c" +dependencies = [ + "atoi", + "flume", + "form_urlencoded", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "sqlx-core", + "thiserror 2.0.18", + "tracing", + "url", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" dependencies = [ "proc-macro2", "quote", - "syn", + "unicode-ident", ] [[package]] -name = "slab" -version = "0.4.9" +name = "synstructure" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ - "autocfg", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "smallvec" -version = "1.11.1" +name = "termtree" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" [[package]] -name = "socket2" -version = "0.6.1" +name = "thiserror" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "libc", - "windows-sys 0.60.2", + "thiserror-impl 1.0.69", ] [[package]] -name = "static_assertions" -version = "1.1.0" +name = "thiserror" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] [[package]] -name = "strsim" -version = "0.11.0" +name = "thiserror-impl" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] -name = "syn" -version = "2.0.60" +name = "thiserror-impl" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "unicode-ident", + "syn", ] [[package]] -name = "thiserror" -version = "1.0.64" +name = "tinystr" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ - "thiserror-impl", + "displaydoc", + "zerovec", ] [[package]] -name = "thiserror-impl" -version = "1.0.64" +name = "tinyvec" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ - "proc-macro2", - "quote", - "syn", + "tinyvec_macros", ] +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" -version = "1.49.0" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "libc", "mio", @@ -801,9 +1921,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", @@ -812,9 +1932,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.16" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" dependencies = [ "futures-core", "pin-project-lite", @@ -824,9 +1944,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.10" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", @@ -835,44 +1955,215 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.3", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "uom" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "a739f83872836c82a4f2527d4e54b37007b3de68cafe7edde95fd695968bf4b9" +dependencies = [ + "num-bigint", + "num-complex", + "num-rational", + "num-traits", + "serde_core", + "typenum", +] [[package]] name = "uplift-cli" -version = "0.3.0" +version = "0.5.0" dependencies = [ "anyhow", "btleplug", "clap", + "confy", "env_logger", "futures", - "lazy_static", "log", + "serde", + "tokio", + "uom", + "uplift-lib", + "uuid", +] + +[[package]] +name = "uplift-lib" +version = "0.0.1" +dependencies = [ + "async-trait", + "btleplug", + "either", + "futures", + "log", + "mockall", + "nom", + "rmp-serde", + "serde", + "serde_json", + "sqlx", "tokio", + "tokio-stream", + "uom", "uuid", ] +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.11.0" +version = "1.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" +dependencies = [ + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "walkdir" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", @@ -880,68 +2171,120 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "winapi" -version = "0.3.9" +name = "wasm-bindgen" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", ] [[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" +name = "wasm-bindgen-macro" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] [[package]] -name = "winapi-util" -version = "0.1.6" +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ - "winapi", + "unicode-ident", ] [[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" +name = "whoami" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998767ef88740d1f5b0682a9c53c24431453923962269c2db68ee43788c5a40d" + +[[package]] +name = "winapi-util" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] [[package]] name = "windows" -version = "0.57.0" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" dependencies = [ "windows-core", - "windows-targets 0.52.5", ] [[package]] name = "windows-core" -version = "0.57.0" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", + "windows-link", "windows-result", - "windows-targets 0.52.5", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", ] [[package]] name = "windows-implement" -version = "0.57.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", @@ -950,9 +2293,9 @@ dependencies = [ [[package]] name = "windows-interface" -version = "0.57.0" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", @@ -966,39 +2309,40 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] -name = "windows-result" -version = "0.1.2" +name = "windows-numerics" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" dependencies = [ - "windows-targets 0.52.5", + "windows-core", + "windows-link", ] [[package]] -name = "windows-sys" -version = "0.48.0" +name = "windows-result" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-targets 0.48.5", + "windows-link", ] [[package]] -name = "windows-sys" -version = "0.52.0" +name = "windows-strings" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-targets 0.52.5", + "windows-link", ] [[package]] name = "windows-sys" -version = "0.60.2" +version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets 0.53.5", + "windows-targets", ] [[package]] @@ -1012,192 +2356,180 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - -[[package]] -name = "windows-targets" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.5", - "windows_aarch64_msvc 0.52.5", - "windows_i686_gnu 0.52.5", - "windows_i686_gnullvm 0.52.5", - "windows_i686_msvc 0.52.5", - "windows_x86_64_gnu 0.52.5", - "windows_x86_64_gnullvm 0.52.5", - "windows_x86_64_msvc 0.52.5", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] -name = "windows-targets" -version = "0.53.5" +name = "windows-threading" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" dependencies = [ "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", ] [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" -version = "0.53.1" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" -version = "0.48.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] -name = "windows_i686_gnu" -version = "0.52.5" +name = "windows_i686_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] -name = "windows_i686_gnu" -version = "0.53.1" +name = "windows_i686_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] -name = "windows_i686_gnullvm" -version = "0.52.5" +name = "windows_x86_64_gnu" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" +name = "windows_x86_64_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] -name = "windows_i686_msvc" -version = "0.48.5" +name = "windows_x86_64_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "windows_i686_msvc" -version = "0.52.5" +name = "winnow" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" [[package]] -name = "windows_i686_msvc" -version = "0.53.1" +name = "winnow" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" [[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" +name = "writeable" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] -name = "windows_x86_64_gnu" -version = "0.52.5" +name = "xml" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" +checksum = "636f85e5ca6488e96401b61eb7de54f4e44755c988af0f52cf90230c312a1a89" [[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" +name = "yoke" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] [[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" +name = "yoke-derive" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] [[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.5" +name = "zerofrom" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] [[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" +name = "zerofrom-derive" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] [[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" +name = "zerotrie" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] [[package]] -name = "windows_x86_64_msvc" -version = "0.52.5" +name = "zerovec" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] [[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" +name = "zerovec-derive" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] -name = "xml-rs" -version = "0.8.19" +name = "zmij" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fcb9cbac069e033553e8bb871be2fbdffcab578eb25bd0f7c508cedc6dcd75a" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml old mode 100755 new mode 100644 index 6fbf805..21c0ffe --- a/Cargo.toml +++ b/Cargo.toml @@ -1,33 +1,26 @@ -[package] -name = "uplift-cli" -version = "0.3.0" -authors = ["Dylan Owen "] -description = "A cli tool to control a bluetooth enabled uplift desk" -readme = "Readme.md" -license = "MPL-2.0" -homepage = "https://github.com/dylanowen/uplift-cli" -repository = "https://github.com/dylanowen/uplift-cli" -edition = "2021" - -[[bin]] -name = "uplift" -path = "src/main.rs" - -[dependencies] -log = "0.4.21" -env_logger = "0.11.3" -lazy_static = "1.4" - -# async -tokio = { version = "1.37", features = ["macros", "time", "rt-multi-thread"] } -futures = "0.3.30" +[workspace] + +resolver = "2" + +members = [ + "crates/*" +] + +[workspace.dependencies] # Bluetooth support -btleplug = "0.11.5" +btleplug = "0.12" uuid = "1.8" -# Command line parsing -clap = { version = "4.5.4", features = ["derive", "env"] } +# Async +tokio = "1.49" +tokio-stream = "0.1" +futures = "0.3.30" + +# Units +uom = "0.38" + +serde = "1.0" -# Error handling -anyhow = "1.0.82" +log = "0.4" +env_logger = "0.11" \ No newline at end of file diff --git a/Readme.md b/Readme.md index c9e5fb8..b5b3e11 100755 --- a/Readme.md +++ b/Readme.md @@ -1,34 +1,45 @@ # Uplift CLI ### Sit + ```bash uplift sit ``` + ```bash uplift sit save ``` ### Stand + ```bash uplift stand ``` + ```bash uplift stand save ``` ### Query + Get the current height + ```bash uplift query ``` ### Listen + Continuously get the height + ```bash uplift listen ``` ## References + * https://github.com/justintout/uplift-reconnect * https://github.com/justintout/uplift-reconnect-web -* https://github.com/deadman96385/uplift_desk_controller_app/blob/master/app/src/main/java/com/deadman/uplift/BluetoothHandler.java#L69 \ No newline at end of file +* https://github.com/deadman96385/uplift_desk_controller_app/blob/master/app/src/main/java/com/deadman/uplift/BluetoothHandler.java#L69 +* https://gitlab.com/pimp-my-desk/desk-control/jiecang-reverse-engineering +* https://github.com/Rocka84/esphome_components/blob/master/components/jiecang_desk_controller/jiecang_desk_controller.cpp \ No newline at end of file diff --git a/crates/uplift-cli/Cargo.toml b/crates/uplift-cli/Cargo.toml new file mode 100755 index 0000000..ac2070d --- /dev/null +++ b/crates/uplift-cli/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "uplift-cli" +version = "0.5.0" +authors = ["Dylan Owen "] +description = "CLI for controlling Uplift standing desks via Bluetooth" +readme = "Readme.md" +license = "MPL-2.0" +homepage = "https://github.com/dylanowen/uplift-cli" +repository = "https://github.com/dylanowen/uplift-cli" +edition = "2024" + +[[bin]] +name = "uplift" +path = "src/main.rs" + +[dependencies] +uplift-lib = { path = "../uplift-lib", version = "0.0.1", features = ["serde"] } + +# Bluetooth support +btleplug.workspace = true +uuid.workspace = true + +# async +tokio = { workspace = true, features = ["rt-multi-thread"] } +futures.workspace = true + +# Units +uom = { workspace = true, features = ["serde"] } + +clap = { version = "4.6", features = ["derive", "env"] } +confy = "2.0" +serde = { workspace = true, features = ["derive"] } + +# Error handling +anyhow = "1.0" + +env_logger.workspace = true +log.workspace = true \ No newline at end of file diff --git a/crates/uplift-cli/Readme.md b/crates/uplift-cli/Readme.md new file mode 100644 index 0000000..3020b12 --- /dev/null +++ b/crates/uplift-cli/Readme.md @@ -0,0 +1,43 @@ +# uplift-cli + +A CLI for controlling Uplift standing desks over Bluetooth. + +## Install + +```sh +cargo install uplift-cli +``` + +## Commands + +```sh +# Get the current desk height +uplift query + +# Move to sitting position +uplift sit + +# Save current height as sitting position +uplift sit save + +# Move to standing position +uplift stand + +# Save current height as standing position +uplift stand save + +# Toggle between sit and stand based on current height +uplift toggle + +# Retry sit until the desk reaches the target height +uplift force-sit + +# Retry stand until the desk reaches the target height +uplift force-stand + +# Retry toggle until the desk reaches the target height +uplift force-toggle + +# Stream height updates continuously +uplift listen +``` \ No newline at end of file diff --git a/crates/uplift-cli/src/lib.rs b/crates/uplift-cli/src/lib.rs new file mode 100644 index 0000000..4e46be3 --- /dev/null +++ b/crates/uplift-cli/src/lib.rs @@ -0,0 +1,136 @@ +use clap::{Parser, Subcommand}; +use log::{debug, error}; +use serde::{Deserialize, Serialize}; +use std::any::type_name; +use std::collections::HashSet; +use std::fmt::{Debug, Formatter}; +use uom::fmt::DisplayStyle; +use uom::si::SI; +use uom::si::f32::Length; +use uom::si::fmt::QuantityArguments; +use uom::si::length::{Dimension, inch, pica_computer}; +use uplift_lib::UpliftDeskId; + +const CONF_NAME: &str = env!("CARGO_PKG_NAME"); + +#[derive(Parser, Debug)] +#[clap(author, version, about, long_about = None)] +#[clap(propagate_version = true)] +pub struct Args { + #[clap(subcommand)] + pub command: Commands, + /// Set the timeout in seconds, 0 for infinite + #[clap(long, default_value_t = 60)] + pub timeout: u64, + /// Choose the desk to interact with + #[clap(long, value_parser=UpliftDeskId::parse)] + pub desk: Option, + /// Set the environment log level + #[clap(long, env = env_logger::DEFAULT_FILTER_ENV, default_value_t = String::from("info"))] + pub log_level: String, + /// Set the environment log style + #[clap(long, env = env_logger::DEFAULT_WRITE_STYLE_ENV)] + pub log_style: Option, +} + +#[derive(Subcommand, Debug)] +pub enum Commands { + /// Get the estimated desk height in inches + Query, + /// Sit or use `--save` to store the current height + Sit { + #[clap(long)] + save: bool, + }, + /// Stand or use `--save` to store the current height + Stand { + #[clap(long)] + save: bool, + }, + /// Sit -> Stand or Stand -> Sit + Toggle, + /// Retry the Sit operation 5 times if the desk doesn't complete it + ForceSit, + /// Retry the Stand operation 5 times if the desk doesn't complete it + ForceStand, + /// Retry the Toggle operation 5 times if the desk doesn't complete it + ForceToggle, + /// Set the current height to the upper limit + SetUpperLimit, + /// Set the current height to the lower limit + SetLowerLimit, + /// Clear the desk's lower limit + ClearUpperLimit, + /// Clear the desk's upper limit + ClearLowerLimit, + /// Listen for height changes + Listen, + /// Add a known desk to prioritize when connecting + AddKnownDesk { + #[clap(value_parser=UpliftDeskId::parse)] + desk: UpliftDeskId, + }, + /// Remove a known desk + RemoveKnownDesk { + #[clap(value_parser=UpliftDeskId::parse)] + desk: UpliftDeskId, + }, +} + +#[derive(Serialize, Deserialize)] +pub struct Config { + pub known_desks: HashSet, + pub sit_height: Length, + pub stand_height: Length, +} + +impl Default for Config { + fn default() -> Self { + Self { + known_desks: HashSet::new(), + sit_height: Length::new::(26.), + stand_height: Length::new::(42.), + } + } +} + +impl Debug for Config { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct(type_name::()) + .field("known_desks", &self.known_desks) + .field("sit_height", &format_height(self.sit_height)) + .field("stand_height", &format_height(self.stand_height)) + .finish() + } +} + +pub fn load_conf() -> Config { + let config_path = confy::get_configuration_file_path(CONF_NAME, None).unwrap(); + debug!("loading uplift config @ {config_path:?}",); + + let conf = confy::load(CONF_NAME, None).unwrap_or_else(|e| { + error!("Failed to load config: {}", e); + Default::default() + }); + + debug!("Loaded config @ {config_path:?}: {conf:#?}",); + + conf +} + +pub fn save_conf(conf: &Config) -> Result<(), anyhow::Error> { + confy::store(CONF_NAME, None, conf)?; + + debug!( + "Saved config @ {:?}: {conf:#?}", + confy::get_configuration_file_path(CONF_NAME, None)? + ); + + Ok(()) +} + +pub fn format_height(length: Length) -> QuantityArguments, f32, inch> { + length + .trunc::() + .into_format_args(inch, DisplayStyle::Abbreviation) +} diff --git a/crates/uplift-cli/src/main.rs b/crates/uplift-cli/src/main.rs new file mode 100644 index 0000000..3d33cbf --- /dev/null +++ b/crates/uplift-cli/src/main.rs @@ -0,0 +1,320 @@ +use anyhow::{Context, anyhow}; +use btleplug::api::Manager as _; +use btleplug::platform::{Adapter, Manager}; +use clap::Parser; +use futures::StreamExt; +use log::trace; +use std::collections::HashMap; +use std::time::Duration; +use tokio::time::{Instant, sleep, timeout, timeout_at}; +use uplift_cli::{Args, Commands, Config, format_height, load_conf, save_conf}; +use uplift_lib::{Command, LimitDirection, TouchMode, UpliftDeskScanner}; + +use uom::si::f32::Length; +use uom::si::length::inch; +use uplift_lib::{UpliftDesk, UpliftDeskId}; + +const FORCE_ATTEMPTS: usize = 20; +const SCAN_TIMEOUT: Duration = Duration::from_secs(2); +const PACKET_DELAY: Duration = Duration::from_millis(200); + +#[tokio::main] +async fn main() -> Result<(), anyhow::Error> { + let args = Args::parse(); + setup_logging(&args)?; + + let conf = load_conf(); + + let timeout_duration = Duration::from_secs(args.timeout); + + let runner = run_command(conf, args); + if !timeout_duration.is_zero() { + timeout(timeout_duration, runner) + .await + .context("Operation timed out")??; + } else { + runner.await?; + } + + Ok(()) +} + +async fn run_command(mut conf: Config, args: Args) -> Result<(), anyhow::Error> { + let manager = Manager::new().await?; + + let adapters = manager.adapters().await?; + let adapter = adapters + .into_iter() + .next() + .ok_or_else(|| anyhow!("No adapters found"))?; + + let mut desk = choose_desk(&conf, adapter.clone(), args.desk).await?; + + desk.connect().await?; + + match args.command { + Commands::Query => { + query(desk).await?; + } + Commands::Sit { save } => { + if save { + desk.command(Command::SaveSit).await?; + + // let the packet actually send + sleep(PACKET_DELAY).await; + + let height = desk.height().await?; + conf.sit_height = height; + save_conf(&conf)?; + + println!("Saved sitting height @ {:.1}", format_height(height)); + } else { + desk.command_retry(Command::GotoSit).await?; + } + } + Commands::Stand { save } => { + if save { + desk.command_retry(Command::SaveStand).await?; + + let height = desk.height().await?; + conf.stand_height = height; + save_conf(&conf)?; + + println!("Saved standing height @ {:.1}", format_height(height)); + } else { + desk.command_retry(Command::GotoStand).await?; + } + } + Commands::ForceSit => { + force(Command::GotoSit, conf.sit_height, &mut desk).await?; + } + Commands::ForceStand => { + force(Command::GotoStand, conf.stand_height, &mut desk).await?; + } + Commands::Toggle => { + let height = desk.height().await?; + + let mid = (conf.stand_height + conf.sit_height) / 2.; + + if height > mid { + desk.command_retry(Command::GotoSit).await?; + } else { + desk.command_retry(Command::GotoStand).await?; + } + + // let the packet actually send + sleep(PACKET_DELAY).await; + } + Commands::ForceToggle => { + let height = desk.height().await?; + + let mid = (conf.stand_height + conf.sit_height) / 2.; + + if height > mid { + force(Command::GotoSit, conf.sit_height, &mut desk).await?; + } else { + force(Command::GotoStand, conf.stand_height, &mut desk).await?; + } + } + Commands::SetUpperLimit => { + desk.command_retry(Command::SaveUpLimit).await?; + + query(desk).await?; + } + Commands::SetLowerLimit => { + desk.command_retry(Command::SaveDownLimit).await?; + + query(desk).await?; + } + Commands::ClearUpperLimit => { + desk.command_retry(Command::ClearLimit { + direction: LimitDirection::Upper, + }) + .await?; + + query(desk).await?; + } + Commands::ClearLowerLimit => { + desk.command_retry(Command::ClearLimit { + direction: LimitDirection::Lower, + }) + .await?; + + query(desk).await?; + } + Commands::Listen => { + let mut heights = desk.height_stream().await?; + loop { + if let Some(height) = heights.next().await { + trace!("Height {:.1}", format_height(height)); + } else { + return Err(anyhow!("Height stream disconnected")); + } + } + } + Commands::AddKnownDesk { desk } => { + conf.known_desks.insert(desk); + save_conf(&conf)?; + } + Commands::RemoveKnownDesk { desk } => { + conf.known_desks.remove(&desk); + save_conf(&conf)?; + } + } + + Ok(()) +} + +async fn query(mut desk: UpliftDesk) -> anyhow::Result<()> { + let subscription = desk.get_subscription().await?; + let height = subscription.height().await?; + let touch_mode = subscription.touch_mode(); + let touch_mode_warning = match touch_mode { + TouchMode::OneTouch => "", + TouchMode::Continuous => "\n\t⚠️ Change your desk to one touch mode to fully use this tool", + }; + let limits = subscription.physical_limits(); + let limit_status = subscription.limited_status(); + let rssi = desk.rssi().await?; + println!( + "desk: {}\n\ + height: {}\n\ + touch mode: {touch_mode:?}{touch_mode_warning}\n\ + physical min limit: {:?}\n\ + physical max limit: {:?}\n\ + limited status: {limit_status:?}\n\ + rssi: {rssi:?}", + desk.id(), + format_height(height), + limits.map(|l| format_height(l.0)), + limits.map(|l| format_height(l.1)), + ); + + Ok(()) +} + +async fn force(command: Command, goal: Length, desk: &mut UpliftDesk) -> anyhow::Result<()> { + const HEIGHT_STREAM_TIMEOUT: Duration = Duration::from_secs(1); + let height_tolerance: Length = Length::new::(1.); + + let mut attempts = 0; + let mut previous_height = desk.height().await?; + + let mut heights = desk.height_stream().await?; + while attempts < FORCE_ATTEMPTS { + attempts += 1; + trace!("Running forced attempt {attempts}"); + desk.command(command).await?; + + sleep(PACKET_DELAY).await; + + 'query_height: loop { + if let Ok(Some(next_height)) = timeout(HEIGHT_STREAM_TIMEOUT, heights.next()).await { + trace!( + "Height moved from: {:.1} -> {:.1}", + format_height(previous_height), + format_height(next_height) + ); + + if previous_height == next_height { + // we've stopped moving so check our height + if (goal - next_height).abs() <= height_tolerance { + return Ok(()); + } else { + break 'query_height; + } + } else { + previous_height = next_height; + } + } else { + break 'query_height; + } + } + } + + Err(anyhow!( + "Failed to force the desk to the intended height after {attempts} attempts" + )) +} + +async fn choose_desk( + conf: &Config, + adapter: Adapter, + desk_uuid: Option, +) -> Result { + let mut scanner = UpliftDeskScanner::new(adapter).await?; + let deadline = Instant::now() + SCAN_TIMEOUT; + + if let Some(desk_uuid) = desk_uuid { + loop { + match timeout_at(deadline, scanner.next()).await { + Ok(Some(desk)) if desk_uuid == desk.id() => { + return Ok(desk); + } + Ok(Some(_)) => continue, + _ => { + return Err(anyhow!("Timed out waiting for desk: {desk_uuid:?}")); + } + } + } + } else { + let mut desks = HashMap::new(); + while let Ok(Some(desk)) = timeout_at(deadline, scanner.next()).await { + // TODO this won't support 2 known desks close to each other + if conf.known_desks.contains(&desk.id()) { + return Ok(desk); + } + + desks.insert(desk.id(), desk); + } + + if desks.len() > 1 { + async fn get_rssi(desk: &mut UpliftDesk) -> i16 { + async fn inner(desk: &mut UpliftDesk) -> btleplug::Result { + desk.connect().await?; + desk.rssi().await?.ok_or(btleplug::Error::NotConnected) + } + let result = inner(desk).await.unwrap_or(i16::MIN); + let _ = desk.disconnect().await; + result + } + + let mut iter = desks.into_values(); + let mut desk = iter.next().unwrap(); + let mut rssi = get_rssi(&mut desk).await; + trace!("{:?} @ rssi: {rssi}", desk.id()); + for mut next_desk in iter { + let next_rssi = get_rssi(&mut next_desk).await; + trace!("{:?} @ rssi: {next_rssi}", next_desk.id()); + if next_rssi > rssi { + rssi = next_rssi; + desk = next_desk; + } + } + + trace!("Chosen desk: {:?}", desk.id()); + + // let everything cool down after all the connecting + sleep(Duration::from_secs(1)).await; + + Ok(desk) + } else { + desks + .into_values() + .next() + .ok_or_else(|| anyhow!("No desk found")) + } + } +} + +fn setup_logging(args: &Args) -> Result<(), anyhow::Error> { + let mut builder = env_logger::Builder::new(); + builder.parse_filters(&args.log_level); + // builder.filter_module("uplift_lib", log::LevelFilter::Trace); + + if let Some(s) = &args.log_style { + builder.parse_write_style(s); + } + + builder.try_init().context("Failed to setup logger") +} diff --git a/crates/uplift-lib/Cargo.toml b/crates/uplift-lib/Cargo.toml new file mode 100755 index 0000000..04b9c25 --- /dev/null +++ b/crates/uplift-lib/Cargo.toml @@ -0,0 +1,47 @@ +[package] +name = "uplift-lib" +version = "0.0.1" +authors = ["Dylan Owen "] +description = "Rust library for controlling Uplift standing desks via Bluetooth" +license = "MPL-2.0" +homepage = "https://github.com/dylanowen/uplift-cli" +repository = "https://github.com/dylanowen/uplift-cli" +edition = "2024" + +[lib] + +[features] +serde = ["dep:serde", "btleplug/serde", "dep:rmp-serde", "dep:serde_json"] +sqlx = ["dep:sqlx", "dep:rmp-serde", "serde"] + +[dependencies] + +# Bluetooth support +btleplug.workspace = true +uuid.workspace = true + +# async +tokio = { workspace = true, features = ["macros", "sync", "time"] } +tokio-stream.workspace = true +futures.workspace = true + +# Units +uom = { workspace = true, features = ["f32", "si", "std"] } + +# Parsing +nom = "8" + +serde = { workspace = true, features = ["derive"], optional = true } + +sqlx = { version = "0.9", optional = true } +rmp-serde = { version = "1.3", optional = true } +serde_json = { version = "1", optional = true } + +either = "1.13" + +log.workspace = true + +[dev-dependencies] +async-trait = "0.1" +mockall = "0.13" +tokio = { workspace = true, features = ["rt-multi-thread"] } \ No newline at end of file diff --git a/crates/uplift-lib/src/api/command.rs b/crates/uplift-lib/src/api/command.rs new file mode 100644 index 0000000..fe85ee9 --- /dev/null +++ b/crates/uplift-lib/src/api/command.rs @@ -0,0 +1,245 @@ +use crate::api::{serialize_height, serialize_payload}; +use btleplug::Result; +use btleplug::api; +use btleplug::api::{Characteristic, WriteType}; +use std::borrow::Cow; + +/// https://gitlab.com/pimp-my-desk/desk-control/jiecang-reverse-engineering#protocol +/// https://github.com/phord/Jarvis#hsx-control-lines +#[derive(Copy, Clone, Debug)] +#[non_exhaustive] +pub enum Command { + Up, + Down, + Stop, + SaveSit, + SaveStand, + SaveMemory1, + SaveMemory2, + SaveMemory3, + SaveMemory4, + GotoSit, + GotoStand, + GotoMemory1, + GotoMemory2, + GotoMemory3, + GotoMemory4, + FetchHeightValue, + FetchHeightRange, + GotoHeight { height_mm: u16 }, + LockInfo, + SetMemoryClickMode { during_move: bool }, + FetchHighLowLimit, + SaveUpLimit, + SaveDownLimit, + ClearLimit { direction: LimitDirection }, + Patch, + FetchStandTime, + FetchAllTime, + SetMemoryHeight { memory_location: u8, height_mm: u16 }, + Handshake, + Unknown { cmd: u8, data: &'static [u8] }, +} + +#[derive(Copy, Clone, Debug)] +pub enum LimitDirection { + All, + Upper, + Lower, +} + +pub const UP: [u8; 6] = simple_desk_command(0x01); +pub const DOWN: [u8; 6] = simple_desk_command(0x02); +/// Save Sit +pub const SAVE_MEMORY_1: [u8; 6] = simple_desk_command(0x03); +/// Save Stand +pub const SAVE_MEMORY_2: [u8; 6] = simple_desk_command(0x04); +/// Goto Sit +pub const GOTO_MEMORY_1: [u8; 6] = simple_desk_command(0x05); +/// Goto Stand +pub const GOTO_MEMORY_2: [u8; 6] = simple_desk_command(0x06); +pub const FETCH_HEIGHT_VALUE: [u8; 6] = simple_desk_command(0x07); +pub const FETCH_HEIGHT_RANGE: [u8; 6] = simple_desk_command(0x0C); + +pub fn goto_height(height_mm: u16) -> Vec { + advanced_desk_command(0x1b, &serialize_height(height_mm)) +} + +// Unknown what this does +pub fn lock_info() -> Vec { + // I'm not sure what the data denotes here + advanced_desk_command(0x1f, &[0x0]) +} + +pub fn set_memory_click_mode(mode: u8) -> Vec { + advanced_desk_command(0x19, &[mode]) +} + +pub const FETCH_HIGH_LOW_LIMIT: [u8; 6] = simple_desk_command(0x20); + +pub const SAVE_UP_LIMIT: [u8; 6] = simple_desk_command(0x21); +pub const SAVE_DOWN_LIMIT: [u8; 6] = simple_desk_command(0x22); + +pub fn clear_limit(dir: LimitDirection) -> Vec { + advanced_desk_command( + 0x23, + &[match dir { + LimitDirection::All => 0x0, + LimitDirection::Upper => 0x1, + LimitDirection::Lower => 0x2, + }], + ) +} + +pub const SAVE_MEMORY_3: [u8; 6] = simple_desk_command(0x25); +pub const SAVE_MEMORY_4: [u8; 6] = simple_desk_command(0x26); +pub const GOTO_MEMORY_3: [u8; 6] = simple_desk_command(0x27); +pub const GOTO_MEMORY_4: [u8; 6] = simple_desk_command(0x28); + +pub const STOP: [u8; 6] = simple_desk_command(0x2b); + +/// Unknown what this does +pub const PATCH: [u8; 6] = simple_desk_command(0xa0); + +pub const FETCH_STAND_TIME: [u8; 6] = simple_desk_command(0xa2); +pub const FETCH_ALL_TIME: [u8; 6] = simple_desk_command(0xaa); + +pub fn set_memory_height(memory_location: u8, height_mm: u16) -> Vec { + assert!(memory_location > 0 && memory_location <= 3); + let height_bytes = serialize_height(height_mm); + advanced_desk_command(0xad, &[memory_location, height_bytes[0], height_bytes[1]]) +} + +pub const HANDSHAKE: [u8; 6] = simple_desk_command(0xfe); + +impl Command { + pub(crate) fn payload(self) -> Cow<'static, [u8]> { + match self { + Command::Up => Cow::from(&UP), + Command::Down => Cow::from(&DOWN), + Command::Stop => Cow::from(&STOP), + Command::SaveSit | Command::SaveMemory1 => Cow::from(&SAVE_MEMORY_1), + Command::SaveStand | Command::SaveMemory2 => Cow::from(&SAVE_MEMORY_2), + Command::SaveMemory3 => Cow::from(&SAVE_MEMORY_3), + Command::SaveMemory4 => Cow::from(&SAVE_MEMORY_4), + Command::GotoSit | Command::GotoMemory1 => Cow::from(&GOTO_MEMORY_1), + Command::GotoStand | Command::GotoMemory2 => Cow::from(&GOTO_MEMORY_2), + Command::GotoMemory3 => Cow::from(&GOTO_MEMORY_3), + Command::GotoMemory4 => Cow::from(&GOTO_MEMORY_4), + Command::FetchHeightValue => Cow::from(&FETCH_HEIGHT_VALUE), + Command::FetchHeightRange => Cow::from(&FETCH_HEIGHT_RANGE), + Command::GotoHeight { height_mm } => Cow::from(goto_height(height_mm)), + Command::LockInfo => Cow::from(lock_info()), + Command::SetMemoryClickMode { during_move } => { + Cow::from(set_memory_click_mode(if during_move { 1 } else { 0 })) + } + Command::FetchHighLowLimit => Cow::from(&FETCH_HIGH_LOW_LIMIT), + Command::SaveUpLimit => Cow::from(&SAVE_UP_LIMIT), + Command::SaveDownLimit => Cow::from(&SAVE_DOWN_LIMIT), + Command::ClearLimit { direction } => Cow::from(clear_limit(direction)), + Command::Patch => Cow::from(&PATCH), + Command::FetchStandTime => Cow::from(&FETCH_STAND_TIME), + Command::FetchAllTime => Cow::from(&FETCH_ALL_TIME), + Command::SetMemoryHeight { + memory_location, + height_mm, + } => Cow::from(set_memory_height(memory_location, height_mm)), + Command::Handshake => Cow::from(&HANDSHAKE), + Command::Unknown { cmd, data } => Cow::from(advanced_desk_command(cmd, data)), + } + } +} + +impl From for Cow<'static, [u8]> { + fn from(val: Command) -> Self { + val.payload() + } +} + +const CMD_PREFIX: [u8; 2] = [0xf1, 0xf1]; + +/// A simple command with no data +const fn simple_desk_command(cmd: u8) -> [u8; 6] { + [0xf1, 0xf1, cmd, 0x00, cmd, 0x7e] +} + +pub fn advanced_desk_command(cmd: u8, data: &[u8]) -> Vec { + serialize_payload(&CMD_PREFIX, cmd, data) +} + +pub(crate) trait EnhancedPeripheral { + async fn command( + &self, + command: Command, + characteristic: &Characteristic, + write_type: WriteType, + ) -> Result<()>; +} + +impl EnhancedPeripheral for P { + async fn command( + &self, + command: Command, + characteristic: &Characteristic, + write_type: WriteType, + ) -> Result<()> { + let payload = command.payload(); + log::trace!( + "{{{}}} - Sending Command: {command:?} @ {payload:02x?}", + self.id() + ); + self.write(characteristic, payload.as_ref(), write_type) + .await + } +} + +#[cfg(test)] +mod tests { + pub use super::*; + + #[test] + fn test_commands() { + // assert_eq!(UP, &[0xf1, 0xf1, 0x01, 0x00, 0x01, 0x7e]); + assert_eq!(advanced_desk_command(0x01, &[]), simple_desk_command(0x01)); + assert_eq!( + goto_height(0x0123), + [ + 0xf1, + 0xf1, + 0x1b, + 2, + 0x01, + 0x23, + 0x1b + 2 + 0x01 + 0x23, + 0x7e + ] + ); + assert_eq!( + goto_height(0xfe12), + [ + 0xf1, + 0xf1, + 0x1b, + 2, + 0xfe, + 0x12, + 0x1bu8.wrapping_add(2).wrapping_add(0xfe).wrapping_add(0x12), + 0x7e + ] + ); + assert_eq!( + set_memory_height(0x01, 0x0102), + [ + 0xf1, + 0xf1, + 0xad, + 3, + 0x01, + 0x01, + 0x02, + 0xad + 3 + 0x01 + 0x01 + 0x02, + 0x7e + ] + ); + } +} diff --git a/crates/uplift-lib/src/api/message.rs b/crates/uplift-lib/src/api/message.rs new file mode 100644 index 0000000..b1302c0 --- /dev/null +++ b/crates/uplift-lib/src/api/message.rs @@ -0,0 +1,324 @@ +use crate::api::{PAYLOAD_END, deserialize_height}; +use nom::IResult; +use nom::Parser; +use nom::branch::alt; +use nom::bytes::complete::{tag, take}; +use nom::multi::many0; +use std::fmt::{Debug, Formatter}; + +type NomResult<'a, O> = Result>>; + +const MESSAGE_PREFIX: [u8; 2] = [0xf2, 0xf2]; + +const HEIGHT_CODE: u8 = 0x01; +const PHYSICAL_LIMITS_CODE: u8 = 0x07; +#[allow(dead_code)] +const ID_CODE: u8 = 0x0f; // not sure what this does but it seems to be related to iD in Calibration? +const HEIGHT_UNIT_CODE: u8 = 0x0e; +#[allow(dead_code)] +const IA_CODE: u8 = 0x10; // Probably stores a height, maybe some minimum? +const TOUCH_MODE_CODE: u8 = 0x19; +const LIMITED_STATUS_CODE: u8 = 0x20; +#[allow(dead_code)] +const HEIGHT_MAX_CODE: u8 = 0x21; +#[allow(dead_code)] +const HEIGHT_MIN_CODE: u8 = 0x22; + +#[derive(Eq, PartialEq, Clone)] +pub enum Message { + Height(u16), + PhysicalLimits { min: u16, max: u16 }, + HeightUnit { unit: HeightUnit }, + TouchMode { mode: TouchMode }, + LimitedStatus { status: LimitedDirection }, + Unknown { code: u8, data: Vec }, +} + +#[derive(Eq, PartialEq, Clone, Copy, Debug)] +pub enum HeightUnit { + Cm, + Inch, +} + +#[derive(Eq, PartialEq, Clone, Copy, Debug)] +pub enum TouchMode { + /// You press the desk buttons once to move it + OneTouch, + /// You need to keep pressing the desk buttons to move it + Continuous, +} + +#[derive(Eq, PartialEq, Clone, Copy, Debug)] +pub enum LimitedDirection { + All, + Upper, + Lower, + None, +} + +impl Message { + pub fn parse(raw_message: &[u8]) -> IResult<&[u8], Vec> { + many0( + ( + tag(MESSAGE_PREFIX.as_ref()), + parse_message_data, + tag([PAYLOAD_END].as_ref()), + ) + .map(|(_, message, _)| message), + ) + .parse(raw_message) + } +} + +fn parse_message_data(initial_i: &[u8]) -> IResult<&[u8], Message> { + let (i, cmd_len) = take(2usize)(initial_i)?; + let cmd = cmd_len[0]; + let len = cmd_len[1]; + let (i, data) = take(len)(i)?; + let computed_checksum = data + .iter() + .fold(cmd.wrapping_add(len), |c, d| c.wrapping_add(*d)); + let (i, _) = tag([computed_checksum].as_ref())(i)?; + + let message = match cmd { + HEIGHT_CODE => parse_height_message(data)?, + PHYSICAL_LIMITS_CODE => parse_physical_limits_message(data)?, + HEIGHT_UNIT_CODE => parse_height_unit_message(data)?, + TOUCH_MODE_CODE => parse_touch_mode_message(data)?, + LIMITED_STATUS_CODE => parse_limited_status_message(data)?, + _ => Message::Unknown { + code: cmd, + data: data.into(), + }, + }; + + Ok((i, message)) +} + +fn parse_height_message(data: &[u8]) -> NomResult<'_, Message> { + // TODO what does the bonus byte mean? + let (_, data) = take(2usize)(data)?; + + let height = deserialize_height(data); + + Ok(Message::Height(height)) +} + +fn parse_height_unit_message(data: &[u8]) -> NomResult<'_, Message> { + let (_, unit) = alt(( + tag([0x00].as_ref()).map(|_| HeightUnit::Cm), + tag([0x01].as_ref()).map(|_| HeightUnit::Inch), + )) + .parse(data)?; + + Ok(Message::HeightUnit { unit }) +} + +fn parse_physical_limits_message(data: &[u8]) -> NomResult<'_, Message> { + let (data, max) = take(2usize)(data)?; + let max = deserialize_height(max); + let (_, min) = take(2usize)(data)?; + let min = deserialize_height(min); + + Ok(Message::PhysicalLimits { min, max }) +} + +fn parse_touch_mode_message(data: &[u8]) -> NomResult<'_, Message> { + let (_, mode) = alt(( + tag([0x0].as_ref()).map(|_| TouchMode::OneTouch), + tag([0x1].as_ref()).map(|_| TouchMode::Continuous), + )) + .parse(data)?; + + Ok(Message::TouchMode { mode }) +} + +fn parse_limited_status_message(data: &[u8]) -> NomResult<'_, Message> { + let (_, status) = alt(( + tag([0x00].as_ref()).map(|_| LimitedDirection::None), + tag([0x01].as_ref()).map(|_| LimitedDirection::Upper), + tag([0x10].as_ref()).map(|_| LimitedDirection::Lower), + tag([0x11].as_ref()).map(|_| LimitedDirection::All), + )) + .parse(data)?; + + Ok(Message::LimitedStatus { status }) +} + +impl Debug for Message { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Message::Height(height) => f + .debug_tuple("Height") + .field(&format_hex(HEIGHT_CODE)) + .field(height) + .finish(), + Message::PhysicalLimits { min, max } => f + .debug_tuple("PhysicalLimits") + .field(&format_hex(PHYSICAL_LIMITS_CODE)) + .field(min) + .field(max) + .finish(), + Message::HeightUnit { unit } => f + .debug_tuple("HeightUnit") + .field(&format_hex(HEIGHT_UNIT_CODE)) + .field(unit) + .finish(), + Message::TouchMode { mode } => f + .debug_struct("TouchMode") + .field("code", &format_hex(TOUCH_MODE_CODE)) + .field("mode", &mode) + .finish(), + Message::LimitedStatus { status } => f + .debug_struct("LimitedStatus") + .field("code", &format_hex(LIMITED_STATUS_CODE)) + .field("status", &status) + .finish(), + Message::Unknown { code, data } => f + .debug_struct("Unknown") + .field("code", &format_hex(code)) + .field("data", &format_hex(data)) + .finish(), + } + } +} + +#[inline] +fn format_hex(value: V) -> String { + format!("{value:02x?}") +} + +#[cfg(test)] +mod tests { + pub use super::*; + use crate::api::serialize_payload; + + #[test] + fn test_height_message() { + let data = [0xf2, 0xf2, HEIGHT_CODE, 0x03, 0x01, 0x17, 0x03, 0x1f, 0x7e]; + let (i, result) = Message::parse(&data).unwrap(); + assert_eq!(i.len(), 0); + assert_eq!(result, vec![Message::Height(0x0117)]); + } + + #[test] + fn test_height_unit_message() { + let data = [ + 0xf2, + 0xf2, + HEIGHT_UNIT_CODE, + 0x01, + 0x00, + HEIGHT_UNIT_CODE.wrapping_add(0x01), + 0x7e, + ]; + let (i, result) = Message::parse(&data).unwrap(); + assert_eq!(i.len(), 0); + assert_eq!( + result, + vec![Message::HeightUnit { + unit: HeightUnit::Cm + }] + ); + let data = [ + 0xf2, + 0xf2, + HEIGHT_UNIT_CODE, + 0x01, + 0x01, + HEIGHT_UNIT_CODE.wrapping_add(0x02), + 0x7e, + ]; + let (i, result) = Message::parse(&data).unwrap(); + assert_eq!(i.len(), 0); + assert_eq!( + result, + vec![Message::HeightUnit { + unit: HeightUnit::Inch + }] + ); + } + + #[test] + fn test_physical_limits_message() { + let data = serialize_payload(&MESSAGE_PREFIX, 0x07, &[0x04, 0x56, 0x01, 0x23]); + let (i, result) = Message::parse(&data).unwrap(); + assert_eq!(i.len(), 0); + assert_eq!( + result, + vec![Message::PhysicalLimits { + min: 0x0123, + max: 0x0456 + }] + ); + } + + #[test] + fn test_parse_many() { + let data = [ + serialize_payload( + &MESSAGE_PREFIX, + PHYSICAL_LIMITS_CODE, + &[0x04, 0x56, 0x01, 0x23], + ), + serialize_payload(&MESSAGE_PREFIX, HEIGHT_CODE, &[0x01, 0x17, 0x03]), + serialize_payload( + &MESSAGE_PREFIX, + PHYSICAL_LIMITS_CODE, + &[0x04, 0x56, 0x01, 0x23], + ), + ] + .concat(); + let (i, result) = Message::parse(&data).unwrap(); + + assert_eq!(i.len(), 0); + assert_eq!( + result, + vec![ + Message::PhysicalLimits { + min: 0x0123, + max: 0x0456 + }, + Message::Height(0x0117), + Message::PhysicalLimits { + min: 0x0123, + max: 0x0456 + } + ] + ); + } + + #[test] + fn test_parse_partial() { + let mut data = serialize_payload( + &MESSAGE_PREFIX, + PHYSICAL_LIMITS_CODE, + &[0x04, 0x56, 0x01, 0x23], + ); + data.pop(); + let (i, result) = Message::parse(&data).unwrap(); + assert_eq!(i.len(), data.len()); + assert_eq!(result, vec![]); + let first_message = serialize_payload( + &MESSAGE_PREFIX, + PHYSICAL_LIMITS_CODE, + &[0x04, 0x56, 0x01, 0x23], + ); + let mut data = [ + first_message.clone(), + serialize_payload(&MESSAGE_PREFIX, HEIGHT_CODE, &[0x01, 0x17, 0x03]), + ] + .concat(); + data.pop(); + let (i, result) = Message::parse(&data).unwrap(); + + assert_eq!(i.len(), data.len() - first_message.len()); + assert_eq!( + result, + vec![Message::PhysicalLimits { + min: 0x0123, + max: 0x0456 + }] + ); + } +} diff --git a/crates/uplift-lib/src/api/mod.rs b/crates/uplift-lib/src/api/mod.rs new file mode 100644 index 0000000..99b05eb --- /dev/null +++ b/crates/uplift-lib/src/api/mod.rs @@ -0,0 +1,84 @@ +mod command; +mod message; + +use btleplug::api::bleuuid; +pub use command::*; +pub use message::*; +use uuid::Uuid; + +#[derive(Copy, Clone, Debug)] +pub struct DeskUuids { + pub service_uuid: Uuid, + pub data_in: Uuid, + pub data_out: Uuid, + pub descriptors_out_uuid: Uuid, + pub name_uuid: Uuid, +} + +const DESK_1: DeskUuids = DeskUuids { + service_uuid: bleuuid::uuid_from_u16(0xff12), + data_in: bleuuid::uuid_from_u16(0xff01), + data_out: bleuuid::uuid_from_u16(0xff02), + descriptors_out_uuid: bleuuid::uuid_from_u16(0x2902), + name_uuid: bleuuid::uuid_from_u16(0xff06), +}; + +const DESK_3: DeskUuids = DeskUuids { + service_uuid: bleuuid::uuid_from_u16(0xfe60), + data_in: bleuuid::uuid_from_u16(0xfe61), + data_out: bleuuid::uuid_from_u16(0xfe62), + descriptors_out_uuid: bleuuid::uuid_from_u16(0x2902), + name_uuid: bleuuid::uuid_from_u16(0xfe63), +}; + +const DESK_4: DeskUuids = DeskUuids { + service_uuid: bleuuid::uuid_from_u16(0xff00), + data_in: bleuuid::uuid_from_u16(0xff01), + data_out: bleuuid::uuid_from_u16(0xff02), + descriptors_out_uuid: bleuuid::uuid_from_u16(0x2902), + name_uuid: bleuuid::uuid_from_u16(0xfe63), +}; + +const DESK_5: DeskUuids = DeskUuids { + service_uuid: bleuuid::uuid_from_u16(0x00ff), + data_in: bleuuid::uuid_from_u16(0x01ff), + data_out: bleuuid::uuid_from_u16(0x02ff), + descriptors_out_uuid: bleuuid::uuid_from_u16(0x2902), + name_uuid: bleuuid::uuid_from_u16(0x36ef), +}; + +pub const DESK_UUIDS: [DeskUuids; 4] = [DESK_1, DESK_3, DESK_4, DESK_5]; + +/// Denotes the end of a message or command +const PAYLOAD_END: u8 = 0x7e; + +#[inline] +const fn serialize_height(height: u16) -> [u8; 2] { + height.to_be_bytes() +} + +#[inline] +const fn deserialize_height(raw_height: &[u8]) -> u16 { + u16::from_be_bytes([raw_height[0], raw_height[1]]) +} + +#[inline] +fn serialize_payload(prefix: &[u8], cmd: u8, data: &[u8]) -> Vec { + let data_len = data.len() as u8; + // prefix_length + cmd + len + data + checksum + payload_end + let mut payload = Vec::with_capacity(prefix.len() + 1 + 1 + data_len as usize + 1 + 1); + payload.extend_from_slice(prefix); + + // since it's a checksum assume we just wrap when adding + let mut checksum = cmd.wrapping_add(data_len); + payload.push(cmd); + payload.push(data_len); + for d in data { + payload.push(*d); + checksum = checksum.wrapping_add(*d); + } + payload.push(checksum); + payload.push(PAYLOAD_END); + + payload +} diff --git a/crates/uplift-lib/src/desk/mod.rs b/crates/uplift-lib/src/desk/mod.rs new file mode 100644 index 0000000..ce7d327 --- /dev/null +++ b/crates/uplift-lib/src/desk/mod.rs @@ -0,0 +1,438 @@ +mod subscription; + +use crate::api::{Command, EnhancedPeripheral as _, HeightUnit}; +use crate::desk::subscription::DeskSubscription; +use crate::{DESK_UUIDS, DeskUuids, LimitedDirection, UpliftDeskId}; +use btleplug::api::{Characteristic, Peripheral, WriteType}; +use btleplug::{Error, Result, platform}; +use either::Either; +use std::collections::BTreeSet; +use std::fmt::{Debug, Formatter}; +use std::mem; +use std::time::Duration; +use tokio::sync::oneshot; +use tokio::time::sleep; +use tokio_stream::Stream; +use uom::si::f32::Length; + +const RETRY_DELAY: Duration = Duration::from_millis(800); + +#[allow(clippy::large_enum_variant)] +pub enum UpliftDesk { + Connected { + peripheral: P, + data_in_characteristic: Characteristic, + data_out_characteristic: Characteristic, + name_characteristic: Characteristic, + subscription: Either, DeskSubscription

>, + cancel_subscription_tx: oneshot::Sender<()>, + disconnect_on_drop: bool, + }, + Disconnected { + peripheral: P, + }, +} + +impl UpliftDesk

{ + pub fn new(peripheral: P) -> Self { + Self::Disconnected { peripheral } + } + + pub async fn connect(&mut self) -> Result<()> { + if !matches!(self, UpliftDesk::Connected { peripheral, .. } if peripheral.is_connected().await?) + { + let peripheral = self.peripheral().clone(); + + log::debug!("{{{}}} - Connecting to desk", peripheral.id()); + + peripheral.connect().await?; + peripheral.discover_services().await?; + + let (service, desk_uuids) = peripheral.services().iter().find_map(|service| { + for desk in DESK_UUIDS { + if desk.service_uuid == service.uuid { + return Some((service.clone(), desk)); + } + } + None + }).ok_or_else(|| { + log::warn!("{{{}}} - Attempted to connect to desk that isn't advertising the necessary Service or Characteristics", peripheral.id()); + Error::DeviceNotFound + })?; + + let (data_in_characteristic, data_out_characteristic, name_characteristic) = + get_characteristics(desk_uuids, service.characteristics)?; + + let (cancel_subscription_tx, cancel_subscription_rx) = oneshot::channel(); + + // we're about to drop our non-connected self, we don't want to drop our connection as we're establishing it + if let UpliftDesk::Connected { + disconnect_on_drop, .. + } = self + { + *disconnect_on_drop = true; + } + + *self = Self::Connected { + peripheral, + disconnect_on_drop: false, + data_in_characteristic, + data_out_characteristic, + name_characteristic, + subscription: Either::Left(cancel_subscription_rx), + cancel_subscription_tx, + }; + } + + Ok(()) + } + + pub fn id(&self) -> UpliftDeskId { + UpliftDeskId(self.peripheral().id()) + } + + pub async fn rssi(&self) -> Result> { + match self { + UpliftDesk::Connected { peripheral, .. } => { + Ok(peripheral.properties().await?.and_then(|p| p.rssi)) + } + UpliftDesk::Disconnected { .. } => Err(Error::NotConnected), + } + } + + pub async fn name(&self) -> Result> { + match self { + UpliftDesk::Connected { peripheral, .. } => { + Ok(peripheral.properties().await?.and_then(|p| p.local_name)) + } + UpliftDesk::Disconnected { .. } => Err(Error::NotConnected), + } + } + + pub async fn command(&self, command: Command) -> Result<()> { + match self { + UpliftDesk::Connected { + peripheral, + data_in_characteristic, + .. + } if peripheral.is_connected().await? => { + peripheral + .command(command, data_in_characteristic, WriteType::WithoutResponse) + .await + } + _ => Err(Error::NotConnected), + } + } + + pub async fn command_retry(&self, command: Command) -> Result<()> { + self.command(command).await?; + + sleep(RETRY_DELAY).await; + + self.command(command).await?; + + Ok(()) + } + + pub async fn height(&mut self) -> Result { + self.get_subscription().await?.height().await + } + + pub async fn physical_limits(&mut self) -> Result> { + Ok(self.get_subscription().await?.physical_limits()) + } + + pub async fn height_unit(&mut self) -> Result { + Ok(self.get_subscription().await?.height_unit()) + } + + pub async fn limited_status(&mut self) -> Result { + Ok(self.get_subscription().await?.limited_status()) + } + + pub async fn height_stream(&mut self) -> Result + use

> { + self.get_subscription().await?.height_stream().await + } + + /// It would be nice to return our desk here but btleplug drops any knowledge of this peripheral on disconnect + pub async fn disconnect(&mut self) -> Result<()> { + if let UpliftDesk::Connected { + peripheral, + cancel_subscription_tx, + disconnect_on_drop: dont_disconnect, + .. + } = self + { + disconnect( + peripheral, + mem::replace(cancel_subscription_tx, oneshot::channel().0), + ) + .await?; + // we've already disconnected, we don't need a background job when we drop + *dont_disconnect = true; + } + let peripheral = self.peripheral().clone(); + *self = UpliftDesk::Disconnected { peripheral }; + + Ok(()) + } + + pub async fn get_subscription(&mut self) -> Result<&DeskSubscription

> { + match self { + UpliftDesk::Connected { + peripheral, + data_in_characteristic, + data_out_characteristic, + subscription, + .. + } if peripheral.is_connected().await? => { + // subscription.as_ref() + + if let Either::Left(cancel_rx) = subscription.as_mut() { + let cancel_rx = mem::replace(cancel_rx, oneshot::channel().1); + + *subscription = Either::Right( + DeskSubscription::new( + peripheral.id(), + data_in_characteristic.clone(), + data_out_characteristic.clone(), + peripheral.clone(), + cancel_rx, + ) + .await?, + ) + } + + let subscription = subscription.as_ref().right().unwrap(); + if !subscription.is_live() { + // our subscription exists but the main loop isn't running + Err(Error::NotConnected)? + } + + Ok(subscription) + } + _ => Err(Error::NotConnected), + } + } + + fn peripheral(&self) -> &P { + match self { + UpliftDesk::Connected { peripheral, .. } => peripheral, + UpliftDesk::Disconnected { peripheral } => peripheral, + } + } +} + +impl Debug for UpliftDesk

{ + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("UpliftDesk") + .field("id", &self.id()) + .finish() + } +} + +impl Drop for UpliftDesk

{ + fn drop(&mut self) { + if let UpliftDesk::Connected { + peripheral, + cancel_subscription_tx, + disconnect_on_drop: false, + .. + } = self + { + let peripheral = peripheral.clone(); + let cancel = mem::replace(cancel_subscription_tx, oneshot::channel().0); + tokio::spawn(async move { + if let Err(e) = disconnect(&peripheral, cancel).await { + log::warn!("{{{}}} - Error while dropping desk: {e:?}", peripheral.id()); + } + }); + } + } +} + +async fn disconnect( + peripheral: &P, + cancel_subscription_tx: oneshot::Sender<()>, +) -> Result<()> { + log::debug!("{{{}}} - Disconnecting from Desk", peripheral.id()); + if cancel_subscription_tx.send(()).is_err() { + log::error!( + "{{{}}} - We have already cancelled our subscription", + peripheral.id() + ); + } + peripheral.disconnect().await +} + +fn get_characteristics( + desk_characteristic: DeskUuids, + characteristics: BTreeSet, +) -> Result<(Characteristic, Characteristic, Characteristic)> { + let mut data_in_characteristic = None; + let mut data_out_characteristic = None; + let mut name_characteristic = None; + + for characteristic in characteristics.into_iter() { + if desk_characteristic.data_in == characteristic.uuid { + data_in_characteristic = Some(characteristic); + } else if desk_characteristic.data_out == characteristic.uuid { + data_out_characteristic = Some(characteristic); + } else if desk_characteristic.name_uuid == characteristic.uuid { + name_characteristic = Some(characteristic); + } + } + + Ok(( + data_in_characteristic.ok_or_else(|| Error::NoSuchCharacteristic)?, + data_out_characteristic.ok_or_else(|| Error::NoSuchCharacteristic)?, + name_characteristic.ok_or_else(|| Error::NoSuchCharacteristic)?, + )) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mock_btle::*; + use btleplug::api::{CharPropFlags, Characteristic, Service}; + use tokio::runtime::Runtime; + + fn valid_characteristics() -> BTreeSet { + [ + DESK_UUIDS[0].data_in, + DESK_UUIDS[0].data_out, + DESK_UUIDS[0].name_uuid, + ] + .into_iter() + .map(|uuid| Characteristic { + uuid, + service_uuid: DESK_UUIDS[0].service_uuid, + properties: CharPropFlags::empty(), + descriptors: BTreeSet::new(), + }) + .collect() + } + + #[test] + fn test_new_desk() { + let runtime = Runtime::new().unwrap(); + + runtime.block_on(async { + let mut peripheral_a = MockPeripheral::new(); + + let mut disconnect_peripheral = MockPeripheral::new(); + disconnect_peripheral + .expect_disconnect() + .returning(|| Ok(())); + + let mut peripheral_b = MockPeripheral::new(); + peripheral_b.expect_connect().returning(|| Ok(())); + peripheral_b.expect_discover_services().returning(|| Ok(())); + peripheral_b.expect_services().returning(|| { + [Service { + uuid: DESK_UUIDS[0].service_uuid, + primary: true, + characteristics: valid_characteristics(), + }] + .into_iter() + .collect() + }); + peripheral_b + .expect_clone() + .return_once(move || disconnect_peripheral); + peripheral_a + .expect_clone() + .return_once(move || peripheral_b); + + let mut desk = UpliftDesk::new(peripheral_a); + desk.connect().await.unwrap(); + drop(desk); + }); + + // This will log panics from tokio threads, but it won't fail our test :( we need + // https://docs.rs/tokio/latest/tokio/runtime/struct.Builder.html#method.unhandled_panic + drop(runtime); + } + + #[tokio::test] + async fn test_invalid_peripheral() { + let mut peripheral = MockPeripheral::new(); + + let mut drop_peripheral = MockPeripheral::new(); + drop_peripheral.expect_connect().returning(|| Ok(())); + drop_peripheral + .expect_discover_services() + .returning(|| Ok(())); + drop_peripheral.expect_services().returning(BTreeSet::new); + + peripheral + .expect_clone() + .return_once(move || drop_peripheral); + + assert!(matches!( + UpliftDesk::new(peripheral).connect().await.err().unwrap(), + Error::DeviceNotFound + )); + } + + // fn mock_central(setup_peripheral: F) -> MockCentral + // where + // F: FnOnce(&mut MockPeripheral), + // { + // let mut central = MockCentral::new(); + // let mut peripheral = MockPeripheral::new(); + // + // peripheral.expect_connect().returning(|| Ok(())); + // peripheral.expect_discover_services().returning(|| Ok(())); + // peripheral.expect_properties().returning(|| { + // Ok(Some(PeripheralProperties { + // services: vec![DESK_UUIDS[0].service_uuid], + // ..Default::default() + // })) + // }); + // peripheral.expect_services().returning(move || { + // let mut services = BTreeSet::new(); + // services.insert(Service { + // uuid: DESK_UUIDS[0].service_uuid, + // primary: true, + // characteristics: valid_characteristics(), + // }); + // + // services + // }); + // + // let mut drop_peripheral = MockPeripheral::new(); + // drop_peripheral + // .expect_disconnect() + // .once() + // .returning(|| Ok(())); + // peripheral + // .expect_clone() + // .return_once(move || drop_peripheral); + // + // setup_peripheral(&mut peripheral); + // + // central.expect_peripheral().return_once(|_| Ok(peripheral)); + // + // central + // } + // + // fn valid_characteristics() -> BTreeSet { + // let mut characteristics = BTreeSet::new(); + // + // for uuid in [ + // DESK_UUIDS[0].data_in, + // DESK_UUIDS[0].data_out, + // DESK_UUIDS[0].name_uuid, + // ] { + // characteristics.insert(Characteristic { + // uuid, + // service_uuid: DESK_UUIDS[0].service_uuid, + // properties: CharPropFlags::empty(), + // descriptors: BTreeSet::new(), + // }); + // } + // + // characteristics + // } +} diff --git a/crates/uplift-lib/src/desk/subscription.rs b/crates/uplift-lib/src/desk/subscription.rs new file mode 100644 index 0000000..6befd64 --- /dev/null +++ b/crates/uplift-lib/src/desk/subscription.rs @@ -0,0 +1,305 @@ +use crate::api::{Command, EnhancedPeripheral as _, HeightUnit, Message}; +use crate::{LimitedDirection, TouchMode}; +use btleplug::Error; +use btleplug::api::{Characteristic, Peripheral, ValueNotification, WriteType}; +use btleplug::platform::PeripheralId; +use std::future::Future; +use std::ops::Deref; +use std::time::Duration; +use tokio::sync::{oneshot, watch}; +use tokio::task::JoinHandle; +use tokio::time::timeout; +use tokio::{select, time}; +use tokio_stream::wrappers::WatchStream; +use tokio_stream::{Stream, StreamExt}; +use uom::si::f32::Length; +use uom::si::length::{centimeter, inch}; + +pub struct DeskSubscription { + height_rx: watch::Receiver>, + physical_limits_rx: watch::Receiver>, + height_unit_rx: watch::Receiver, + touch_mode_rx: watch::Receiver, + limited_status_rx: watch::Receiver, + data_in_characteristic: Characteristic, + peripheral: P, + subscription_live: JoinHandle>, +} + +/// The app starts up by sending +/// +/// t:400 [Command::FetchHighLowLimit] +/// t:500 [Command::FetchHighLowLimit] +/// t:600 [Command::FetchHeightValue] +/// t:700 [Command::FetchHeightRange] +/// t:? [Command::LockInfo] +/// t:? [Command::Handshake] +/// t:1100 [Command::Handshake] +impl DeskSubscription

{ + pub(super) async fn new( + id: PeripheralId, + data_in_characteristic: Characteristic, + data_out_characteristic: Characteristic, + peripheral: P, + cancel_rx: oneshot::Receiver<()>, + ) -> btleplug::Result { + let (height_tx, height_rx) = watch::channel(None); + let (physical_limits_tx, physical_limits_rx) = watch::channel(None); + // The app seems to default to inches + let (height_unit_tx, height_unit_rx) = watch::channel(HeightUnit::Inch); + let (touch_mode_tx, touch_mode_rx) = watch::channel(TouchMode::Continuous); + let (limited_status_tx, limited_status_rx) = watch::channel(LimitedDirection::None); + + let receiver = peripheral.notifications().await?; + peripheral.subscribe(&data_out_characteristic).await?; + + // spawn the initial commands + tokio::spawn({ + let peripheral = peripheral.clone(); + let data_in_characteristic = data_in_characteristic.clone(); + async move { + // The app specifically sleeps for 100ms between commands + let mut interval = time::interval(Duration::from_millis(100)); + for cmd in [ + Command::Handshake, + Command::FetchHighLowLimit, + Command::FetchHighLowLimit, + Command::FetchHeightRange, + Command::FetchHeightValue, + Command::Handshake, + ] { + interval.tick().await; + if let Err(e) = peripheral + .command(cmd, &data_in_characteristic, WriteType::WithoutResponse) + .await + { + log::warn!("Failed to write `{cmd:?}`: {e:?}"); + break; + } + } + } + }); + + let subscription_live = tokio::spawn({ + let peripheral = peripheral.clone(); + async move { + let result = subscription_main_loop( + receiver, + height_tx, + physical_limits_tx, + height_unit_tx, + touch_mode_tx, + limited_status_tx, + cancel_rx, + data_out_characteristic, + peripheral, + ) + .await; + + if let Err(e) = &result { + log::error!("{{{id}}} - Error in our subscription task: {e:?}") + } + + result + } + }); + + Ok(Self { + height_rx, + physical_limits_rx, + height_unit_rx, + touch_mode_rx, + limited_status_rx, + data_in_characteristic, + peripheral, + subscription_live, + }) + } + + pub fn height(&self) -> impl Future> + 'static { + let unit_watch = self.height_unit_rx.clone(); + let height_future = self.wait_for_some_height(); + async move { + Ok(desk_height_to_length( + height_future.await?, + *unit_watch.borrow(), + )) + } + } + + pub fn physical_limits(&self) -> Option<(Length, Length)> { + let (min, max) = (*self.physical_limits_rx.borrow())?; + Some(( + // For some reason these units don't match the reported unit? + desk_height_to_length(min, HeightUnit::Cm), + desk_height_to_length(max, HeightUnit::Cm), + )) + } + + pub fn height_unit(&self) -> HeightUnit { + *self.height_unit_rx.borrow() + } + + pub fn touch_mode(&self) -> TouchMode { + *self.touch_mode_rx.borrow() + } + + pub fn limited_status(&self) -> LimitedDirection { + *self.limited_status_rx.borrow() + } + + pub(super) async fn height_stream( + &self, + ) -> btleplug::Result + use

> { + let unit_watch = self.height_unit_rx.clone(); + self.wait_for_some_height().await?; + Ok( + WatchStream::new(self.height_rx.clone()).filter_map(move |maybe_height| { + maybe_height.map(|height| desk_height_to_length(height, *unit_watch.borrow())) + }), + ) + } + + fn wait_for_some_height(&self) -> impl Future> + Send + 'static { + let peripheral = self.peripheral.clone(); + let data_in_characteristic = self.data_in_characteristic.clone(); + wait_for_some(self.height_rx.clone(), move || async move { + peripheral + .command( + Command::FetchHeightValue, + &data_in_characteristic, + WriteType::WithoutResponse, + ) + .await + }) + } + + pub(super) fn is_live(&self) -> bool { + !self.subscription_live.is_finished() + } +} + +#[allow(clippy::too_many_arguments)] +async fn subscription_main_loop( + mut receiver: R, + height_tx: watch::Sender>, + physical_limits_tx: watch::Sender>, + height_unit_tx: watch::Sender, + touch_mode_tx: watch::Sender, + limited_status_tx: watch::Sender, + mut cancel_rx: oneshot::Receiver<()>, + data_out_characteristic: Characteristic, + peripheral: P, +) -> btleplug::Result<()> +where + R: Stream + Unpin, + P: Peripheral, +{ + let id = peripheral.id(); + log::trace!("{{{id}}} - Spawning Subscription"); + let mut partial_message = vec![]; + loop { + select! { + event = receiver.next() => { + match event { + Some(ValueNotification { value: raw_message, .. }) => { + log::trace!("{{{id}}} - Received Data {}{raw_message:02x?}", if !partial_message.is_empty() { format!("previous({partial_message:02x?}) ") } else { String::new() }); + let complete_message = [partial_message, raw_message].concat(); + match Message::parse(&complete_message) { + Ok((remainder, messages)) => { + partial_message = remainder.to_vec(); + for message in messages { + log::debug!("{{{id}}} - Received Message {message:?}"); + match message { + Message::Height(height) => { + height_tx.send(Some(height)).map_err(|_| Error::RuntimeError("Couldn't send latest value".to_string()))?; + } + Message::PhysicalLimits{ min, max } => { + physical_limits_tx.send(Some((min, max))).map_err(|_| Error::RuntimeError("Couldn't send latest value".to_string()))?; + } + Message::HeightUnit { unit } => { + height_unit_tx.send(unit).map_err(|_| Error::RuntimeError("Couldn't send latest value".to_string()))?; + } + Message::TouchMode{ mode } => { + touch_mode_tx.send(mode).map_err(|_| Error::RuntimeError("Couldn't send latest value".to_string()))?; + } + Message::LimitedStatus { status } => { + limited_status_tx.send(status).map_err(|_| Error::RuntimeError("Couldn't send latest value".to_string()))?; + } + Message::Unknown{ .. } => {} + } + } + } + Err(e) => { + log::trace!("{{{id}}} - Couldn't parse message {e:?} @ {complete_message:02x?}"); + partial_message = vec![]; + } + } + } + None => break, + } + } + _ = &mut cancel_rx => { + log::trace!("{{{id}}} - Cancelling Background Subscription"); + break + } + } + } + // Drop here to signal ASAP that our subscription is closing + drop(height_tx); + drop(height_unit_tx); + + // We MUST time out here as btleplug may never call us back + if let Err(e) = timeout( + Duration::from_secs(10), + peripheral.unsubscribe(&data_out_characteristic), + ) + .await + .map_err(|_| "Timed Out".to_string()) + .and_then(|r| r.map_err(|e| format!("{e:?}"))) + { + log::debug!( + "{{{id}}} - Error Unsubscribing from {{{}}}: {e}", + data_out_characteristic.uuid + ); + } + + log::trace!("{{{id}}} - Exiting Subscription"); + + Ok(()) +} + +async fn wait_for_some( + mut rx: watch::Receiver>, + on_none: F, +) -> btleplug::Result +where + T: Clone, + F: FnOnce() -> Fut, + Fut: Future>, +{ + let maybe_result = rx.borrow_and_update().deref().clone(); + match maybe_result { + Some(result) => Ok(result), + None => { + on_none().await?; + + Ok(rx + .wait_for(Option::is_some) + .await + .map_err(|_| Error::NotConnected)? + .clone() + .unwrap()) + } + } +} + +fn desk_height_to_length(raw_height: u16, height_unit: HeightUnit) -> Length { + let height = match height_unit { + HeightUnit::Cm => Length::new::(raw_height as f32), + HeightUnit::Inch => Length::new::(raw_height as f32), + }; + + // The desk seems to have its values 10x greater than the unit + height / 10. +} diff --git a/crates/uplift-lib/src/id.rs b/crates/uplift-lib/src/id.rs new file mode 100644 index 0000000..5f65f0e --- /dev/null +++ b/crates/uplift-lib/src/id.rs @@ -0,0 +1,125 @@ +use btleplug::platform::PeripheralId; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +use std::fmt::{Debug, Display, Formatter}; + +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Eq, Hash, Ord, PartialEq, PartialOrd, Clone, Debug)] +pub struct UpliftDeskId(pub(crate) PeripheralId); + +impl UpliftDeskId { + pub fn new(id: I) -> Self + where + I: Into, + { + Self(id.into()) + } + + #[cfg(feature = "serde")] + pub fn parse(id: &str) -> Result { + serde_json::from_value::(serde_json::Value::String(id.to_string())) + .map(UpliftDeskId) + } +} + +impl From for PeripheralId { + fn from(val: UpliftDeskId) -> Self { + val.0 + } +} + +#[cfg(feature = "serde")] +impl Display for UpliftDeskId { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + if let Ok(serde_json::Value::String(s)) = serde_json::to_value(&self.0) { + write!(f, "{s}") + } else { + Err(std::fmt::Error) + } + } +} + +#[cfg(feature = "serde")] +mod serde_feature { + use super::*; + + impl From> for UpliftDeskId { + fn from(value: Vec) -> Self { + rmp_serde::from_slice(&value).expect("Failed to deserialize desk id") + } + } + + // impl TryFrom> for UpliftDeskId { + // type Error = rmp_serde::decode::Error; + // + // fn try_from(value: Vec) -> result::Result { + // rmp_serde::from_slice(&value) + // } + // } +} + +#[cfg(feature = "sqlx")] +mod sqlx_feature { + use super::*; + use sqlx::encode::IsNull; + use sqlx::error::BoxDynError; + use sqlx::{Database, Decode, Encode, Type}; + + impl Type for UpliftDeskId + where + [u8]: Type, + { + fn type_info() -> DB::TypeInfo { + <&[u8] as Type>::type_info() + } + + fn compatible(ty: &DB::TypeInfo) -> bool { + <&[u8] as Type>::compatible(ty) + } + } + + impl<'r, DB: Database> Decode<'r, DB> for UpliftDeskId + where + // make sure our DB supports binary + Vec: Decode<'r, DB>, + { + fn decode(value: ::ValueRef<'r>) -> Result { + let raw_value = as Decode>::decode(value)?; + + Ok(rmp_serde::from_slice(&raw_value)?) + } + } + + impl<'q, DB: Database> Encode<'q, DB> for UpliftDeskId + where + // make sure our DB supports binary + Vec: Encode<'q, DB>, + { + fn encode_by_ref( + &self, + buf: &mut ::ArgumentBuffer, + ) -> Result { + rmp_serde::to_vec(self)?.encode(buf) + } + } +} + +#[cfg(all(test, target_os = "macos", feature = "serde"))] +mod tests { + use super::*; + + const TEST_UUID: &str = "12345678-1234-1234-1234-123456789012"; + + #[test] + fn test_parse_raw_uuid() { + let id = UpliftDeskId::parse(TEST_UUID).unwrap(); + assert_eq!(id.to_string(), TEST_UUID); + } + + #[test] + fn test_parse_round_trip() { + let id = UpliftDeskId::parse(TEST_UUID).unwrap(); + let id2 = UpliftDeskId::parse(&id.to_string()).unwrap(); + assert_eq!(id, id2); + } +} diff --git a/crates/uplift-lib/src/lib.rs b/crates/uplift-lib/src/lib.rs new file mode 100644 index 0000000..6af039a --- /dev/null +++ b/crates/uplift-lib/src/lib.rs @@ -0,0 +1,24 @@ +mod api; +mod desk; +mod id; +#[cfg(test)] +mod mock_btle; +mod scanner; + +pub use crate::api::*; +pub use crate::desk::*; +pub use crate::id::*; +pub use crate::scanner::*; + +// // we shouldn't need greater precision than millimeter +// pub type SI = dyn Units< +// V, +// length = millimeter, +// mass = kilogram, +// time = second, +// electric_current = ampere, +// thermodynamic_temperature = kelvin, +// amount_of_substance = mole, +// luminous_intensity = candela, +// >; +// pub type Length = si::length::Length, f32>; diff --git a/crates/uplift-lib/src/mock_btle.rs b/crates/uplift-lib/src/mock_btle.rs new file mode 100644 index 0000000..e16fb31 --- /dev/null +++ b/crates/uplift-lib/src/mock_btle.rs @@ -0,0 +1,97 @@ +use btleplug::api; +use btleplug::api::{ + BDAddr, Characteristic, Descriptor, PeripheralProperties, Service, ValueNotification, WriteType, +}; +use btleplug::platform::PeripheralId; +use mockall::mock; + +use async_trait::async_trait; +use btleplug::Result; +use btleplug::api::CentralEvent; +use btleplug::api::CentralState; +use btleplug::api::ScanFilter; +use futures::stream::Stream; +use std::collections::BTreeSet; +use std::fmt; +use std::fmt::{Debug, Formatter}; +use std::pin::Pin; + +#[cfg(test)] +mock! { + pub Central {} + + #[async_trait] + impl api::Central for Central { + type Peripheral = MockPeripheral; + + async fn events(&self) -> Result + Send>>>; + + async fn start_scan(&self, filter: ScanFilter) -> Result<()>; + + async fn stop_scan(&self) -> Result<()>; + + async fn peripherals(&self) -> Result>; + + async fn peripheral(&self, id: &PeripheralId) -> Result; + + async fn add_peripheral(&self, address: &PeripheralId) -> Result; + + async fn clear_peripherals(&self) -> Result<()>; + + async fn adapter_info(&self) -> Result; + + async fn adapter_state(&self) -> Result; + } + + impl Clone for Central { + fn clone(&self) -> Self; + } +} + +#[cfg(test)] +mock! { + pub Peripheral {} + + #[async_trait] + impl api::Peripheral for Peripheral { + fn id(&self) -> PeripheralId; + + fn address(&self) -> BDAddr; + + fn mtu(&self) -> u16; + + async fn properties(&self) -> Result>; + + fn services(&self) -> BTreeSet; + + async fn is_connected(&self) -> Result; + + async fn connect(&self) -> Result<()>; + + async fn disconnect(&self) -> Result<()>; + + async fn discover_services(&self) -> Result<()>; + + async fn write(&self, characteristic: &Characteristic, data: &[u8], write_type: WriteType) -> Result<()>; + + async fn read(&self, characteristic: &Characteristic) -> Result>; + + async fn subscribe(&self, characteristic: &Characteristic) -> Result<()>; + + async fn unsubscribe(&self, characteristic: &Characteristic) -> Result<()>; + + async fn notifications(&self) -> Result + Send>>>; + + async fn write_descriptor(&self, descriptor: &Descriptor, data: &[u8]) -> Result<()>; + + async fn read_descriptor(&self, descriptor: &Descriptor) -> Result>; + } + + impl Clone for Peripheral { + fn clone(&self) -> Self; + } + + impl Debug for Peripheral { + fn fmt<'a>(&self, f: &mut Formatter<'a>) -> fmt::Result; + } +} diff --git a/crates/uplift-lib/src/scanner.rs b/crates/uplift-lib/src/scanner.rs new file mode 100644 index 0000000..4fb1588 --- /dev/null +++ b/crates/uplift-lib/src/scanner.rs @@ -0,0 +1,114 @@ +use crate::{DESK_UUIDS, UpliftDesk}; +use btleplug::api::CentralEvent::{DeviceConnected, DeviceDiscovered, DeviceUpdated}; +use btleplug::api::{Central, CentralEvent, Peripheral, ScanFilter}; +use futures::{Stream, ready}; +use log::debug; +use std::future::Future; +use std::pin::Pin; +use std::task::{Context, Poll}; + +#[must_use = "streams do nothing unless polled"] +pub struct UpliftDeskScanner +where + // Workaround for the macro https://github.com/taiki-e/pin-project-lite/issues/2#issuecomment-745190950 + C: Central, + C: Unpin, + C: 'static, +{ + events: Pin + Send>>, + pending: Option>>>, + central: C, +} + +impl Drop for UpliftDeskScanner +where + C: Central, + C: Unpin, + C: 'static, +{ + fn drop(&mut self) { + let central = self.central.clone(); + tokio::spawn(async move { + if let Err(error) = central.stop_scan().await { + log::error!("Failed to stop scanning: {error:?}"); + } else { + log::trace!("Stopped Scanning") + } + }); + } +} + +impl UpliftDeskScanner +where + C: Central + Unpin, +{ + pub async fn new(central: C) -> btleplug::Result { + let events = central.events().await?; + + central + .start_scan(ScanFilter { + services: DESK_UUIDS.iter().map(|d| d.service_uuid).collect(), + }) + .await?; + + log::trace!("Started Scanning"); + + Ok(Self { + events, + pending: None, + central, + }) + } +} + +impl Stream for UpliftDeskScanner +where + C: Central + Unpin, +{ + type Item = UpliftDesk; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let next_desk = loop { + if let Some(ref mut p) = self.pending { + // We have a peripheral in progress, poll that until it's done + let peripheral = ready!(p.as_mut().poll(cx)); + self.pending = None; + + debug!("Discovered peripheral {:?}", peripheral.id()); + + break Some(UpliftDesk::new(peripheral)); + } else { + match ready!(self.events.as_mut().poll_next(cx)) { + Some(DeviceDiscovered(id) | DeviceUpdated(id) | DeviceConnected(id)) => { + let central = self.central.clone(); + self.pending = Some(Box::pin(async move { + central.peripheral(&id).await.unwrap_or_else(|_| { + panic!("{{{id}}} - Couldn't find just-reported Peripheral") + }) + })); + } + Some(event) => log::trace!("Unhandled Event: {:?}", event), + None => break None, + } + } + }; + + Poll::Ready(next_desk) + } + + fn size_hint(&self) -> (usize, Option) { + // We don't know the lower bound + (0, self.events.size_hint().1) + } +} + +// impl FusedStream for FilterMap +// where +// St: Stream + FusedStream, +// F: FnMut1, +// Fut: Future>, +// { +// fn is_terminated(&self) -> bool { +// self.pending.is_none() && self.stream.is_terminated() +// } +// } diff --git a/justfile b/justfile index 2bac652..8316973 100644 --- a/justfile +++ b/justfile @@ -1,36 +1,35 @@ - target_config := '--all-targets --all-features' default: build fix: - cargo fix {{target_config}} --allow-staged - cargo clippy --fix {{target_config}} --allow-staged + cargo fix {{ target_config }} --allow-staged + cargo clippy --fix {{ target_config }} --allow-staged fmt: - cargo fmt --all + cargo fmt --all lint: - cargo fmt --all -- --check - cargo clippy {{target_config}} -- -D warnings - -cargo audit + cargo fmt --all -- --check + cargo clippy {{ target_config }} -- -D warnings + -cargo audit check: - cargo check + cargo check build: - cargo build + cargo build release: - cargo build --release + cargo build --release test: - cargo test + cargo test pre-commit: fix fmt lint test release install: - cargo install --force --path . + cargo install --bin uplift --force --path ./crates/uplift-cli clean: - cargo clean + cargo clean diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..f0002e2 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "1.96.1" +components = ["clippy", "rustfmt"] \ No newline at end of file diff --git a/src/desk.rs b/src/desk.rs deleted file mode 100644 index 231028c..0000000 --- a/src/desk.rs +++ /dev/null @@ -1,307 +0,0 @@ -use std::collections::BTreeSet; -use std::sync::atomic::AtomicIsize; -use std::sync::atomic::AtomicU8; -use std::sync::atomic::Ordering; -use std::sync::Arc; -use std::time::Duration; - -use anyhow::{anyhow, Context}; -use btleplug::api::CentralEvent::{DeviceConnected, DeviceDiscovered, DeviceUpdated}; -use btleplug::api::{ - bleuuid, Central, Characteristic, Manager as _, Peripheral as _, ScanFilter, ValueNotification, - WriteType, -}; -use btleplug::platform::{Manager, Peripheral}; -use futures::{executor, StreamExt}; -use tokio::time; -use uuid::Uuid; - -// const UP_PACKET: [u8; 6] = [0xf1, 0xf1, 0x01, 0x00, 0x01, 0x7e]; -// const DOWN_PACKET: [u8; 6] = [0xf1, 0xf1, 0x02, 0x00, 0x02, 0x7e]; -const SAVE_SIT_PACKET: [u8; 6] = [0xf1, 0xf1, 0x03, 0x00, 0x03, 0x7e]; -const SAVE_STAND_PACKET: [u8; 6] = [0xf1, 0xf1, 0x04, 0x00, 0x04, 0x7e]; -const SIT_PACKET: [u8; 6] = [0xf1, 0xf1, 0x05, 0x00, 0x05, 0x7e]; -const STAND_PACKET: [u8; 6] = [0xf1, 0xf1, 0x06, 0x00, 0x06, 0x7e]; -// const STOP_PACKET: [u8; 6] = [0xf1, 0xf1, 0x02, 0x00, 0x2b, 0x7e]; -const QUERY_PACKET: [u8; 6] = [0xf1, 0xf1, 0x07, 0x00, 0x07, 0x7e]; - -pub const DESK_SERVICE_UUID: Uuid = bleuuid::uuid_from_u16(0xff12); - -const DESK_DATA_IN_UUID: Uuid = bleuuid::uuid_from_u16(0xff01); -const DESK_DATA_OUT_UUID: Uuid = bleuuid::uuid_from_u16(0xff02); -const DESK_NAME_UUID: Uuid = bleuuid::uuid_from_u16(0xff06); - -pub struct Desk { - height: Arc, - raw_height: Arc<(AtomicU8, AtomicU8)>, - data_in_characteristic: Characteristic, - peripheral: Peripheral, - _manager: Manager, -} - -impl Desk { - pub async fn new() -> Result { - let (manager, peripheral) = connect().await?; - - log::debug!("{:?} - Connected to peripheral", peripheral.address()); - - // start discovering characteristics on our peripheral - peripheral - .discover_services() - .await - .with_context(|| format!("{:?} - Discovering Services", peripheral.address()))?; - - let (data_in_characteristic, data_out_characteristic, _name_characteristic) = - get_characteristics(peripheral.characteristics())?; - - let height = Arc::new(AtomicIsize::new(-1)); - let raw_height = Arc::new((AtomicU8::new(0), AtomicU8::new(0))); - - // subscribe to events (height) on our peripheral - { - let updated_height = height.clone(); - let updated_raw_height = raw_height.clone(); - - let mut height_receiver = peripheral.notifications().await?; - peripheral - .subscribe(&data_out_characteristic) - .await - .with_context(|| { - format!("{:?} - Subscribing to desk updates", peripheral.address()) - })?; - - let address = peripheral.address(); - tokio::spawn(async move { - while let Some(ValueNotification { value, .. }) = height_receiver.next().await { - let last_height = updated_height.load(Ordering::Relaxed); - let (low, high) = get_raw_height(&value); - let height = estimate_height((low, high), last_height); - - log::trace!( - "{:?} - Updated Height: ({:x},{:x}) -> {:x}", - address, - low, - high, - height - ); - updated_height.store(height, Ordering::Relaxed); - updated_raw_height.0.store(low, Ordering::Relaxed); - updated_raw_height.1.store(high, Ordering::Relaxed); - } - }); - } - - let desk = Desk { - height, - raw_height, - data_in_characteristic, - peripheral, - _manager: manager, - }; - - // we need to do an initial query to actually write anything, so just get that out of the way - desk.write(&desk.data_in_characteristic, &QUERY_PACKET) - .await?; - - Ok(desk) - } - - pub fn height(&self) -> isize { - self.height.load(Ordering::Relaxed) - } - - pub fn raw_height(&self) -> (u8, u8) { - ( - self.raw_height.0.load(Ordering::Relaxed), - self.raw_height.1.load(Ordering::Relaxed), - ) - } - - pub async fn save_sit(&self) -> Result<(), anyhow::Error> { - log::debug!("{:?} - Save sit", self.peripheral.address()); - - self.write(&self.data_in_characteristic, &SAVE_SIT_PACKET) - .await - .with_context(|| format!("{:?} - Saving Sit", self.peripheral.address())) - } - - pub async fn save_stand(&self) -> Result<(), anyhow::Error> { - log::debug!("{:?} - Save stand", self.peripheral.address()); - - self.write(&self.data_in_characteristic, &SAVE_STAND_PACKET) - .await - .with_context(|| format!("{:?} - Saving Stand", self.peripheral.address())) - } - - pub async fn sit(&self) -> Result<(), anyhow::Error> { - log::debug!("{:?} - Sit", self.peripheral.address()); - - self.write(&self.data_in_characteristic, &SIT_PACKET) - .await - .with_context(|| format!("{:?} - Sitting", self.peripheral.address())) - } - - pub async fn stand(&self) -> Result<(), anyhow::Error> { - log::debug!("{:?} - Stand", self.peripheral.address()); - - self.write(&self.data_in_characteristic, &STAND_PACKET) - .await - .with_context(|| format!("{:?} - Standing", self.peripheral.address())) - } - - pub async fn query_height(&self) -> Result { - // since we're querying, clear our height so we can check if it's updated - self.height.store(-1, Ordering::Relaxed); - self.write(&self.data_in_characteristic, &QUERY_PACKET) - .await - .with_context(|| format!("{:?} - Querying", self.peripheral.address()))?; - - // wait for our height to update (is there a better way than polling?) - while self.height.load(Ordering::Relaxed) <= 0 { - time::sleep(Duration::from_millis(100)).await; - } - - Ok(self.height.load(Ordering::Relaxed)) - } - - async fn write( - &self, - characteristic: &Characteristic, - data: &[u8], - ) -> Result<(), anyhow::Error> { - self.peripheral - .write(characteristic, data, WriteType::WithoutResponse) - .await - .with_context(|| format!("{:?} - Failed to write data", self.peripheral.address())) - } -} - -fn get_raw_height(data: &[u8]) -> (u8, u8) { - (data[5], data[7]) -} - -// 25.2" -pub const MIN_PHYSICAL_HEIGHT: isize = 252; -// 25.2" + 0xff -pub const MAX_PHYSICAL_HEIGHT: isize = MIN_PHYSICAL_HEIGHT + 0xff; -pub const MID_PHYSICAL_HEIGHT: isize = (MIN_PHYSICAL_HEIGHT + MAX_PHYSICAL_HEIGHT) / 2; -// 26.0" based on a 5'6" person -pub const AVG_SITTING_HEIGHT: isize = 260; -// 40.5" based on a 5'6" person -pub const AVG_STANDING_HEIGHT: isize = 405; -pub const AVG_MID_HEIGHT: isize = (AVG_SITTING_HEIGHT + AVG_STANDING_HEIGHT) / 2; - -/// The height ranges from 0x00 to 0xff. 0x01 roughly seems to be 0.1" -fn estimate_height((low, high): (u8, u8), last_height: isize) -> isize { - let low = low as isize; - let high = high as isize; - - let raw_height = if low >= 0xfd { - // anything outside of this range seems to be "special" - if last_height < MID_PHYSICAL_HEIGHT { - high - } else { - low - } - } else { - low - }; - - MIN_PHYSICAL_HEIGHT + raw_height -} - -impl Drop for Desk { - fn drop(&mut self) { - executor::block_on(self.peripheral.disconnect()).unwrap(); - } -} - -async fn connect() -> Result<(Manager, Peripheral), anyhow::Error> { - log::debug!("Connecting to Bluetooth Manager"); - let manager = Manager::new().await?; - - let adapters = manager.adapters().await?; - let central = adapters - .into_iter() - .next() - .ok_or_else(|| anyhow!("Couldn't find an adapter"))?; - - log::debug!("Using adapter: {:?}", central.adapter_info().await?); - - let mut events = central.events().await?; - - // scan for our desk service - central - .start_scan(ScanFilter { - services: vec![DESK_SERVICE_UUID], - }) - .await?; - - let mut result = Err(anyhow!("Our adapter stopped looking for peripherals")); - while let Some(event) = events.next().await { - match event { - DeviceDiscovered(id) | DeviceUpdated(id) | DeviceConnected(id) => { - let peripheral = central - .peripheral(&id) - .await - .context(format!("{id:?} - Couldn't get our Peripheral"))?; - - log::trace!("{:?} - Discovered peripheral", peripheral.address()); - - let properties = peripheral.properties().await.context(format!( - "{:?} - Couldn't get properties", - peripheral.address() - ))?; - - if let Some(properties) = &properties { - // even with the ScanFilter we still get initial unmatched devices, filter those out - if properties.services.contains(&DESK_SERVICE_UUID) { - log::debug!("{:?} - Attempting to connect", peripheral.address()); - - peripheral - .connect() - .await - .context(format!("{:?} - Connection failed", peripheral.address()))?; - - result = Ok((manager, peripheral)); - break; - } - } - - log::trace!( - "{:?} - Peripheral didn't contain the Desk Service", - properties - ); - } - event => log::trace!("Unhandled Event: {:?}", event), - } - } - - central.stop_scan().await?; - - result -} - -fn get_characteristics( - characteristics: BTreeSet, -) -> Result<(Characteristic, Characteristic, Characteristic), anyhow::Error> { - let mut data_in_characteristic = None; - let mut data_out_characteristic = None; - let mut name_characteristic = None; - - for characteristic in characteristics.into_iter() { - if DESK_DATA_IN_UUID == characteristic.uuid { - data_in_characteristic = Some(characteristic); - } else if DESK_DATA_OUT_UUID == characteristic.uuid { - data_out_characteristic = Some(characteristic); - } else if DESK_NAME_UUID == characteristic.uuid { - name_characteristic = Some(characteristic); - } - } - - Ok(( - data_in_characteristic.ok_or_else(|| anyhow!("Couldn't get data-in characteristic"))?, - data_out_characteristic.ok_or_else(|| anyhow!("Couldn't find data-out characteristic"))?, - name_characteristic.ok_or_else(|| anyhow!("Couldn't find name characteristic"))?, - )) -} diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 6b06f29..0000000 --- a/src/main.rs +++ /dev/null @@ -1,218 +0,0 @@ -use std::convert::identity; -use std::future::Future; -use std::time::Duration; - -use anyhow::{anyhow, Context}; -use clap::{Parser, Subcommand}; -use tokio::time; -use tokio::time::timeout; - -use crate::desk::{Desk, AVG_MID_HEIGHT, AVG_SITTING_HEIGHT, AVG_STANDING_HEIGHT}; - -mod desk; - -const FORCE_ATTEMPTS: usize = 5; - -#[derive(Parser, Debug)] -#[clap(author, version, about, long_about = None)] -#[clap(propagate_version = true)] -struct Args { - #[clap(subcommand)] - command: Commands, - /// Set the timeout in seconds, 0 for infinite - #[clap(long, default_value_t = 60)] - timeout: u64, - /// Set the environment log level - #[clap(long, env = env_logger::DEFAULT_FILTER_ENV, default_value_t = String::from("info"))] - log_level: String, - /// Set the environment log style - #[clap(long, env = env_logger::DEFAULT_WRITE_STYLE_ENV)] - log_style: Option, -} - -#[derive(Subcommand, Debug)] -enum Commands { - /// Sit or use `save` to store the current height - Sit { - #[clap(subcommand)] - save: Option, - }, - /// Retry the Sit operation 5 times if the desk doesn't complete it - ForceSit, - /// Stand or use `save` to store the current height - Stand { - #[clap(subcommand)] - save: Option, - }, - /// Retry the Stand operation 5 times if the desk doesn't complete it - ForceStand, - /// Get the estimated desk height in inches - Query, - /// Sit -> Stand or Stand -> Sit - Toggle, - /// Retry the Toggle operation 5 times if the desk doesn't complete it - ForceToggle, - /// Listen for height changes - Listen, -} - -#[derive(Subcommand, Debug)] -enum SaveCommand { - Save, -} - -#[tokio::main] -async fn main() -> Result<(), anyhow::Error> { - let args = Args::parse(); - - setup_logging(&args)?; - - let runner = run_command(&args); - if args.timeout > 0 { - timeout(Duration::from_secs(args.timeout), runner) - .await - .context("Operation timed out") - .and_then(identity)? - } else { - runner.await? - } - - Ok(()) -} - -fn setup_logging(args: &Args) -> Result<(), anyhow::Error> { - let mut builder = env_logger::Builder::new(); - builder.parse_filters(&args.log_level); - - if let Some(s) = &args.log_style { - builder.parse_write_style(s); - } - - builder.try_init().context("Failed to setup logger") -} - -async fn run_command(args: &Args) -> Result<(), anyhow::Error> { - let desk = Desk::new().await?; - - match &args.command { - Commands::Sit { save } => { - if save.is_some() { - desk.save_sit().await?; - } else { - desk.sit().await?; - } - - // let the packet actually send - desk.query_height().await?; - } - Commands::ForceSit => { - force_sit(&desk).await?; - } - Commands::Stand { save } => { - if save.is_some() { - desk.save_stand().await?; - } else { - desk.stand().await?; - } - - // let the packet actually send - desk.query_height().await?; - } - Commands::ForceStand => { - force_stand(&desk).await?; - } - Commands::Query => { - println!("{}", desk.query_height().await? as f32 / 10.0); - } - Commands::Toggle => { - let height = desk.query_height().await?; - if height > AVG_MID_HEIGHT { - desk.sit().await?; - } else { - desk.stand().await?; - } - - // let the packet actually send - desk.query_height().await?; - } - Commands::ForceToggle => { - let height = desk.query_height().await?; - if height > AVG_MID_HEIGHT { - force_sit(&desk).await?; - } else { - force_stand(&desk).await?; - } - } - Commands::Listen => { - let mut height = 0; - loop { - let next_height = desk.height(); - if height != next_height { - let (low, high) = desk.raw_height(); - println!("height: ({low:x},{high:x}) -> {next_height}"); - } - height = next_height; - - time::sleep(Duration::from_millis(100)).await; - } - } - } - - Ok(()) -} - -async fn force_sit(desk: &Desk) -> Result<(), anyhow::Error> { - force( - || async { desk.sit().await }, - |height| height < (AVG_MID_HEIGHT + AVG_SITTING_HEIGHT) / 2, - desk, - ) - .await -} - -async fn force_stand(desk: &Desk) -> Result<(), anyhow::Error> { - force( - || async { desk.stand().await }, - |height| height > (AVG_MID_HEIGHT + AVG_STANDING_HEIGHT) / 2, - desk, - ) - .await -} - -async fn force( - mut action: impl FnMut() -> AFut, - mut done: impl FnMut(isize) -> bool, - desk: &Desk, -) -> Result<(), anyhow::Error> -where - AFut: Future>, -{ - let mut attempts = 0; - let mut previous_height = desk.query_height().await?; - - while attempts < FORCE_ATTEMPTS { - attempts += 1; - log::trace!("Running forced attempt {attempts}"); - action().await?; - - 'query_height: loop { - time::sleep(Duration::from_millis(1000)).await; - let next_height = desk.height(); - log::trace!("Height moved from: {previous_height} -> {next_height}"); - - // we've stopped moving so check our height - if previous_height == next_height { - if done(next_height) { - return Ok(()); - } else { - break 'query_height; - } - } - previous_height = next_height; - } - } - - Err(anyhow!( - "Failed to force the desk to the intended height after {attempts} attempts" - )) -}