From 16550b322310b97f82acf292d34eff1e240e68d3 Mon Sep 17 00:00:00 2001 From: Dylan Owen Date: Thu, 26 Dec 2024 22:25:11 -0800 Subject: [PATCH 01/12] initial lib --- Cargo.lock | 1163 +++++++++++++------ Cargo.toml | 36 +- crates/uplift_lib/Cargo.toml | 36 + crates/uplift_lib/src/desk.rs | 372 ++++++ crates/uplift_lib/src/discovery.rs | 109 ++ crates/uplift_lib/src/error.rs | 22 + crates/uplift_lib/src/id.rs | 222 ++++ src/desk.rs => crates/uplift_lib/src/lib.rs | 149 +-- rust-toolchain.toml | 2 + src/main.rs | 218 ---- 10 files changed, 1625 insertions(+), 704 deletions(-) mode change 100755 => 100644 Cargo.toml create mode 100755 crates/uplift_lib/Cargo.toml create mode 100644 crates/uplift_lib/src/desk.rs create mode 100644 crates/uplift_lib/src/discovery.rs create mode 100644 crates/uplift_lib/src/error.rs create mode 100644 crates/uplift_lib/src/id.rs rename src/desk.rs => crates/uplift_lib/src/lib.rs (65%) create mode 100644 rust-toolchain.toml delete mode 100644 src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 8558253..bf262ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,61 +3,37 @@ version = 4 [[package]] -name = "aho-corasick" -version = "1.1.2" +name = "addr2line" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ - "memchr", -] - -[[package]] -name = "anstream" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "utf8parse", + "gimli", ] [[package]] -name = "anstyle" -version = "1.0.8" +name = "adler2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] -name = "anstyle-parse" -version = "0.2.2" +name = "ahash" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" -dependencies = [ - "windows-sys 0.48.0", + "cfg-if", + "once_cell", + "version_check", + "zerocopy", ] [[package]] -name = "anstyle-wincon" -version = "3.0.1" +name = "allocator-api2" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" -dependencies = [ - "anstyle", - "windows-sys 0.48.0", -] +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "anyhow" @@ -76,17 +52,35 @@ dependencies = [ "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.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] -name = "bitflags" -version = "1.3.2" +name = "backtrace" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] [[package]] name = "bitflags" @@ -94,6 +88,15 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +[[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 = "block2" version = "0.5.1" @@ -110,7 +113,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ce7d4413c940e8e3cb6afc122d3f4a07096aca259d286781128683fc9f39d9b" dependencies = [ "async-trait", - "bitflags 2.6.0", + "bitflags", "bluez-generated", "dbus", "dbus-tokio", @@ -119,7 +122,7 @@ dependencies = [ "log", "serde", "serde-xml-rs", - "thiserror", + "thiserror 1.0.69", "tokio", "uuid", ] @@ -140,7 +143,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82837101dad9a257a3ffe35fbc2ef4df9a27aecbe5343ce83ac233bcab283394" dependencies = [ "async-trait", - "bitflags 2.6.0", + "bitflags", "bluez-async", "dashmap 6.1.0", "dbus", @@ -152,8 +155,10 @@ dependencies = [ "objc2-core-bluetooth", "objc2-foundation", "once_cell", + "serde", + "serde_bytes", "static_assertions", - "thiserror", + "thiserror 1.0.69", "tokio", "tokio-stream", "uuid", @@ -161,10 +166,16 @@ dependencies = [ ] [[package]] -name = "bytes" +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.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" [[package]] name = "cesu8" @@ -179,66 +190,72 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] -name = "clap" -version = "4.5.23" +name = "combine" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" dependencies = [ - "clap_builder", - "clap_derive", + "bytes", + "memchr", ] [[package]] -name = "clap_builder" -version = "4.5.23" +name = "concurrent-queue" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", + "crossbeam-utils", ] [[package]] -name = "clap_derive" -version = "4.5.18" +name = "cpufeatures" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", + "libc", ] [[package]] -name = "clap_lex" -version = "0.7.4" +name = "crc" +version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +dependencies = [ + "crc-catalog", +] [[package]] -name = "colorchoice" -version = "1.0.0" +name = "crc-catalog" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] -name = "combine" -version = "4.6.6" +name = "crossbeam-queue" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" dependencies = [ - "bytes", - "memchr", + "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 = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] [[package]] name = "dashmap" @@ -247,7 +264,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if", - "hashbrown", + "hashbrown 0.14.5", "lock_api", "once_cell", "parking_lot_core", @@ -261,7 +278,7 @@ checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" dependencies = [ "cfg-if", "crossbeam-utils", - "hashbrown", + "hashbrown 0.14.5", "lock_api", "once_cell", "parking_lot_core", @@ -291,33 +308,60 @@ dependencies = [ "tokio", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "either" -version = "1.9.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +dependencies = [ + "serde", +] [[package]] -name = "env_filter" -version = "0.1.0" +name = "equivalent" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "event-listener" +version = "5.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" dependencies = [ - "log", - "regex", + "concurrent-queue", + "parking", + "pin-project-lite", ] [[package]] -name = "env_logger" -version = "0.11.5" +name = "form_urlencoded" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ - "anstream", - "anstyle", - "env_filter", - "humantime", - "log", + "percent-encoding", ] [[package]] @@ -362,6 +406,17 @@ dependencies = [ "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" @@ -409,29 +464,201 @@ dependencies = [ "slab", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + [[package]] name = "hashbrown" -version = "0.14.2" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] [[package]] -name = "heck" -version = "0.5.0" +name = "hashbrown" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" [[package]] -name = "hermit-abi" -version = "0.3.9" +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "hex" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] -name = "humantime" -version = "2.1.0" +name = "icu_collections" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" +dependencies = [ + "equivalent", + "hashbrown 0.15.2", +] [[package]] name = "itertools" @@ -452,7 +679,7 @@ dependencies = [ "combine", "jni-sys", "log", - "thiserror", + "thiserror 1.0.69", "walkdir", ] @@ -477,17 +704,11 @@ dependencies = [ "uuid", ] -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - [[package]] name = "libc" -version = "0.2.179" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "libdbus-sys" @@ -498,11 +719,17 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "litemap" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" + [[package]] name = "lock_api" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -516,22 +743,55 @@ checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "memchr" -version = "2.6.4" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" +dependencies = [ + "adler2", +] [[package]] name = "mio" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ - "hermit-abi", "libc", "wasi", "windows-sys 0.52.0", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "objc-sys" version = "0.3.5" @@ -554,7 +814,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a644b62ffb826a5277f536cf0f701493de420b13d40e700c452c36567771111" dependencies = [ - "bitflags 2.6.0", + "bitflags", "objc2", "objc2-foundation", ] @@ -571,36 +831,73 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "bitflags 2.6.0", + "bitflags", "block2", "libc", "objc2", ] +[[package]] +name = "object" +version = "0.36.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + [[package]] name = "parking_lot_core" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.48.5", + "windows-targets", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + [[package]] name = "pin-project-lite" -version = "0.2.13" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" [[package]] name = "pin-utils" @@ -610,65 +907,64 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.27" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "proc-macro2" -version = "1.0.81" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.36" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] [[package]] name = "redox_syscall" -version = "0.4.1" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ - "bitflags 1.3.2", + "bitflags", ] [[package]] -name = "regex" -version = "1.10.2" +name = "rmp" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4" dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", + "byteorder", + "num-traits", + "paste", ] [[package]] -name = "regex-automata" -version = "0.4.3" +name = "rmp-serde" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db" dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", + "byteorder", + "rmp", + "serde", ] [[package]] -name = "regex-syntax" -version = "0.8.2" +name = "rustc-demangle" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "same-file" @@ -687,9 +983,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.190" +version = "1.0.216" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7" +checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" dependencies = [ "serde_derive", ] @@ -702,21 +998,41 @@ checksum = "fb3aa78ecda1ebc9ec9847d5d3aba7d618823446a049ba2491940506da6e2782" dependencies = [ "log", "serde", - "thiserror", + "thiserror 1.0.69", "xml-rs", ] +[[package]] +name = "serde_bytes" +version = "0.11.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "387cc504cb06bb40a96c8e04e951fe01854cf6bc921053c954e4a606d9675c6a" +dependencies = [ + "serde", +] + [[package]] name = "serde_derive" -version = "1.0.190" +version = "1.0.216" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3" +checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "slab" version = "0.4.9" @@ -728,20 +1044,81 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.11.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" -version = "0.6.1" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.52.0", ] +[[package]] +name = "sqlformat" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790" +dependencies = [ + "nom", + "unicode_categories", +] + +[[package]] +name = "sqlx" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93334716a037193fac19df402f8571269c84a00852f6a7066b5d2616dcd64d3e" +dependencies = [ + "sqlx-core", +] + +[[package]] +name = "sqlx-core" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d8060b456358185f7d50c55d9b5066ad956956fddec42ee2e8567134a8936e" +dependencies = [ + "atoi", + "byteorder", + "bytes", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-channel", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.14.5", + "hashlink", + "hex", + "indexmap", + "log", + "memchr", + "once_cell", + "paste", + "percent-encoding", + "serde", + "sha2", + "smallvec", + "sqlformat", + "thiserror 1.0.69", + "tracing", + "url", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "static_assertions" version = "1.1.0" @@ -749,61 +1126,97 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] -name = "strsim" -version = "0.11.0" +name = "syn" +version = "2.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] [[package]] -name = "syn" -version = "2.0.60" +name = "synstructure" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "unicode-ident", + "syn", ] [[package]] name = "thiserror" -version = "1.0.64" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc" +dependencies = [ + "thiserror-impl 2.0.9", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ - "thiserror-impl", + "proc-macro2", + "quote", + "syn", ] [[package]] name = "thiserror-impl" -version = "1.0.64" +version = "2.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tokio" -version = "1.49.0" +version = "1.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" dependencies = [ + "backtrace", "libc", "mio", "pin-project-lite", "socket2", "tokio-macros", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", @@ -812,9 +1225,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" dependencies = [ "futures-core", "pin-project-lite", @@ -824,9 +1237,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.10" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" dependencies = [ "bytes", "futures-core", @@ -835,44 +1248,115 @@ dependencies = [ "tokio", ] +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] -name = "uplift-cli" -version = "0.3.0" +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + +[[package]] +name = "uplift-lib" +version = "0.0.1" dependencies = [ "anyhow", "btleplug", - "clap", - "env_logger", "futures", - "lazy_static", "log", + "rmp-serde", + "serde", + "sqlx", + "thiserror 2.0.9", "tokio", "uuid", ] [[package]] -name = "utf8parse" -version = "0.2.1" +name = "url" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +dependencies = [ + "serde", +] + +[[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", @@ -902,11 +1386,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.6" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "winapi", + "windows-sys 0.59.0", ] [[package]] @@ -922,7 +1406,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" dependencies = [ "windows-core", - "windows-targets 0.52.5", + "windows-targets", ] [[package]] @@ -934,7 +1418,7 @@ dependencies = [ "windows-implement", "windows-interface", "windows-result", - "windows-targets 0.52.5", + "windows-targets", ] [[package]] @@ -959,28 +1443,13 @@ dependencies = [ "syn", ] -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - [[package]] name = "windows-result" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" dependencies = [ - "windows-targets 0.52.5", -] - -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", + "windows-targets", ] [[package]] @@ -989,215 +1458,183 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.5", + "windows-targets", ] [[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", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" -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-targets", ] [[package]] name = "windows-targets" -version = "0.53.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 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", + "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_aarch64_gnullvm" -version = "0.48.5" +version = "0.52.6" 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" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" -version = "0.48.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] -name = "windows_i686_gnu" -version = "0.53.1" +name = "windows_i686_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] -name = "windows_i686_gnullvm" -version = "0.52.5" +name = "windows_i686_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" +name = "windows_x86_64_gnu" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] -name = "windows_i686_msvc" -version = "0.48.5" +name = "windows_x86_64_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] -name = "windows_i686_msvc" -version = "0.52.5" +name = "windows_x86_64_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "windows_i686_msvc" -version = "0.53.1" +name = "write16" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" [[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" +name = "writeable" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" [[package]] -name = "windows_x86_64_gnu" -version = "0.52.5" +name = "xml-rs" +version = "0.8.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" +checksum = "ea8b391c9a790b496184c29f7f93b9ed5b16abb306c05415b68bcc16e4d06432" [[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" +name = "yoke" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] [[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" +name = "yoke-derive" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] [[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.5" +name = "zerocopy" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "zerocopy-derive", +] [[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" +name = "zerocopy-derive" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" +name = "zerofrom" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] [[package]] -name = "windows_x86_64_msvc" -version = "0.52.5" +name = "zerofrom-derive" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] [[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" +name = "zerovec" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] [[package]] -name = "xml-rs" -version = "0.8.19" +name = "zerovec-derive" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fcb9cbac069e033553e8bb871be2fbdffcab578eb25bd0f7c508cedc6dcd75a" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml old mode 100755 new mode 100644 index 6fbf805..e0f17fb --- a/Cargo.toml +++ b/Cargo.toml @@ -1,33 +1,7 @@ -[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" +[workspace] -[[bin]] -name = "uplift" -path = "src/main.rs" +resolver = "2" -[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" - -# Bluetooth support -btleplug = "0.11.5" -uuid = "1.8" - -# Command line parsing -clap = { version = "4.5.4", features = ["derive", "env"] } - -# Error handling -anyhow = "1.0.82" +members = [ + "crates/uplift_lib" +] \ No newline at end of file diff --git a/crates/uplift_lib/Cargo.toml b/crates/uplift_lib/Cargo.toml new file mode 100755 index 0000000..7dee9d4 --- /dev/null +++ b/crates/uplift_lib/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "uplift-lib" +version = "0.0.1" +authors = ["Dylan Owen "] +description = "" +readme = "Readme.md" +license = "MPL-2.0" +homepage = "https://github.com/dylanowen/uplift-cli" +repository = "https://github.com/dylanowen/uplift-cli" +edition = "2021" + +[lib] + +[features] +serde = ["dep:serde", "btleplug/serde"] +sqlx = ["dep:sqlx", "dep:rmp-serde", "serde"] + +[dependencies] +log = "0.4.21" + +# async +tokio = { version = "1.37", features = ["macros", "sync", "time"] } +futures = "0.3.30" + +# Bluetooth support +btleplug = "0.11.5" +uuid = "1.8" + + +serde = { version = "1.0", features = ["derive"], default-features = false, optional = true } +sqlx = { version = "0.8", default-features = false, optional = true } +rmp-serde = { version = "1.3", optional = true } + +# Error handling +anyhow = "1.0.82" +thiserror = "2.0" diff --git a/crates/uplift_lib/src/desk.rs b/crates/uplift_lib/src/desk.rs new file mode 100644 index 0000000..b6ca383 --- /dev/null +++ b/crates/uplift_lib/src/desk.rs @@ -0,0 +1,372 @@ +use btleplug::api::{ + bleuuid, Central, Characteristic, Peripheral as _, PeripheralProperties, ValueNotification, + WriteType, +}; +use btleplug::platform::{Adapter, Peripheral, PeripheralId}; +use btleplug::{Error, Result}; +use std::collections::BTreeSet; + +use crate::id::UpliftDeskId; +use crate::{ + estimate_height, get_raw_height, DESK_DATA_IN_UUID, DESK_DATA_OUT_UUID, DESK_NAME_UUID, + DESK_SERVICE_UUID, MID_PHYSICAL_HEIGHT, MIN_PHYSICAL_HEIGHT, QUERY_PACKET, +}; +use anyhow::anyhow; +use futures::StreamExt; +use std::ops::Deref; +use std::sync::atomic::Ordering; +use std::sync::Arc; +use std::time::Duration; +use tokio::select; +use tokio::sync::broadcast::{Receiver, Sender}; +use tokio::sync::{broadcast, mpsc, RwLock}; +use tokio::time::sleep; +use uuid::Uuid; + +pub struct UpliftDesk { + properties: PeripheralProperties, + peripheral: Peripheral, + data_in_characteristic: Characteristic, + data_out_characteristic: Characteristic, + name_characteristic: Characteristic, + height_stream: Arc>>>, +} + +impl UpliftDesk { + pub async fn new(id: I, adapter: &Adapter) -> Result + where + I: Into, + { + let peripheral = adapter.peripheral(&id.into()).await?; + + peripheral.connect().await?; + peripheral.discover_services().await?; + + let (data_in_characteristic, data_out_characteristic, name_characteristic) = + get_characteristics(peripheral.characteristics())?; + + println!("new desk connecting"); + + match peripheral.properties().await? { + Some(properties) if properties.services.contains(&DESK_SERVICE_UUID) => { + Ok(UpliftDesk { + properties, + peripheral, + data_in_characteristic, + data_out_characteristic, + name_characteristic, + height_stream: Arc::new(RwLock::new(None)), + }) + } + _ => Err(btleplug::Error::DeviceNotFound), + } + } + + pub async fn name(&self) -> Result> { + Ok(self + .peripheral + .properties() + .await? + .and_then(|p| p.local_name)) + } + + pub async fn get_height(&self) -> Result { + let mut rx = self.stream_height().await?; + rx.recv().await.map_err(|e| Error::Other(Box::new(e))) + } + + pub async fn stream_height(&self) -> Result> { + let height_stream_read = self.height_stream.read().await; + let rx = if let Some(height_stream) = height_stream_read.as_ref() { + println!("read subscribe"); + height_stream.subscribe() + } else { + drop(height_stream_read); + let mut height_stream_write = self.height_stream.write().await; + if let Some(height_stream) = height_stream_write.as_ref() { + println!("write subscribe"); + height_stream.subscribe() + } else { + let (tx, rx) = broadcast::channel(10); + + let peripheral = self.peripheral.clone(); + let data_in_characteristic = self.data_in_characteristic.clone(); + let data_out_characteristic = self.data_out_characteristic.clone(); + + let mut height_receiver = peripheral.notifications().await?; + peripheral.subscribe(&data_out_characteristic).await?; + + tokio::spawn({ + let tx = tx.clone(); + let height_stream = self.height_stream.clone(); + + println!("spawning stream"); + + async move { + let mut received_message = false; + loop { + select! { + event = height_receiver.next() => { + match event { + Some(ValueNotification { value, .. }) => { + received_message = true; + let height = UpliftDeskHeight::new(&value); + println!("height: {height:?}"); + + if let Err(_) = tx.send(height) { + // no more receivers + break; + } + } + None => break, + } + } + _ = sleep(Duration::from_secs(1)) => { + if tx.receiver_count() <= 0 { + break; + } else if !received_message { + write(&QUERY_PACKET,&data_in_characteristic,&peripheral).await; + } + } + } + } + + if let Err(e) = peripheral.unsubscribe(&data_out_characteristic).await { + log::warn!("Error unsubscribing from Data Out Characteristic: {e:?}") + } + + *height_stream.write().await = None + } + }); + + *height_stream_write = Some(tx); + + rx + } + }; + + self.query_desk().await?; + Ok(rx) + } + + pub async fn test(&self) -> Result<()> { + self.peripheral.discover_services().await?; + + for service in self.peripheral.services() { + println!("service: {service:?}") + } + + Ok(()) + } + + pub async fn disconnect(self) -> UpliftDeskId { + // the Drop implementation of self will disconnect this + UpliftDeskId::new(self.peripheral.id()) + } + + async fn query_desk(&self) -> Result<()> { + write( + &QUERY_PACKET, + &self.data_in_characteristic, + &self.peripheral, + ) + .await + } + + // pub async fn new_old(id: I, adapter: &Adapter) -> Result> where I: Into { + // let id = id.into(); + // + // let peripheral = adapter + // .peripheral(&id) + // .await + // .context(format!("{id} - Couldn't connect to Desk"))?; + // + // let properties = peripheral.properties().await.context(format!( + // "{id} - Couldn't get properties", + // ))?; + // + // match properties { + // Some(properties) if properties.services.contains(&DESK_SERVICE_UUID) => { + // Ok(Some(UpliftDesk{ peripheral })) + // } + // _ => { + // Ok(None) + // } + // } + // } + // + // pub async fn scan_for_desks(adapter: &Adapter) -> Receiver> { + // let (tx, rx) = mpsc::channel(10); + // + // let adapter = adapter.clone(); + // tokio::spawn(async move { + // async fn inner( + // adapter: &Adapter, + // tx: &Sender>, + // ) -> Result<()> { + // let mut events = adapter.events().await?; + // + // // scan for our desk service + // adapter + // .start_scan(ScanFilter { + // services: vec![DESK_SERVICE_UUID], + // }) + // .await?; + // + // loop { + // select! { + // event = events.next() => { + // match event { + // Some(DeviceDiscovered(id) | DeviceUpdated(id) | DeviceConnected(id)) => { + // match UpliftDesk::new_old(id, adapter).await { + // Ok(Some(desk)) => { + // if let Err(error) = tx.send(Ok(desk)).await { + // break Err(error.into()) + // } + // } + // Ok(None) => (), + // Err(error) => log::warn!("{error}") + // } + // } + // Some(event ) => log::trace!("Unhandled Event: {:?}", event), + // None => { + // break Err(anyhow!("Our adapter stopped looking for peripherals")) + // } + // } + // } + // _ = tx.closed() => { + // break Ok(()); + // } + // } + // } + // } + // + // log::trace!("Started Scanning"); + // + // let result = inner(&adapter, &tx).await; + // if let Err(error) = adapter.stop_scan().await { + // log::error!("Failed to stop scanning: {error:?}"); + // } else { + // log::trace!("Stopped Scanning") + // } + // + // if let Err(error) = result { + // if let Err(error) = tx.send(Err(error)).await { + // // log::warn!("Error when sending final error: {:?}", error.0) + // } + // } + // }); + // + // rx + // } +} + +#[inline] +async fn write( + data: &[u8], + characteristic: &Characteristic, + peripheral: &Peripheral, +) -> Result<()> { + peripheral + .write(&characteristic, data, WriteType::WithoutResponse) + .await +} + +impl Deref for UpliftDesk { + type Target = Peripheral; + + fn deref(&self) -> &Self::Target { + &self.peripheral + } +} + +impl Drop for UpliftDesk { + fn drop(&mut self) { + let peripheral = self.peripheral.clone(); + // tokio::task::spawn(async move { + // let _ = peripheral.disconnect().await; + // }); + } +} + +#[derive(Clone, Debug)] +pub struct UpliftDeskHeight { + low: u8, + high: u8, +} + +impl UpliftDeskHeight { + fn new(data: &[u8]) -> Self { + Self { + low: data[5], + high: data[7], + } + } + + pub fn physical_height(&self) -> usize { + let low = self.low as usize; + let high = self.high as usize; + + // 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 + // }; + let raw_height = low; + + raw_height + } +} + +fn get_characteristics( + 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_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(|| Error::NoSuchCharacteristic)?, + data_out_characteristic.ok_or_else(|| Error::NoSuchCharacteristic)?, + name_characteristic.ok_or_else(|| Error::NoSuchCharacteristic)?, + )) +} + +#[cfg(test)] +mod tests { + pub use super::*; + use btleplug::api::Manager as _; + use btleplug::platform::Manager; + #[tokio::test] + async fn test() { + let manager = Manager::new().await.unwrap(); + + let adapters = manager.adapters().await.unwrap(); + let adapter = adapters.into_iter().next().unwrap(); + + let mut rx = UpliftDeskId::scan(&adapter).await; + let id = rx.recv().await.unwrap().unwrap(); + + let desk = id.connect(&adapter).await.unwrap(); + + println!( + "height: {}", + desk.get_height().await.unwrap().physical_height() + ) + } +} diff --git a/crates/uplift_lib/src/discovery.rs b/crates/uplift_lib/src/discovery.rs new file mode 100644 index 0000000..44c6068 --- /dev/null +++ b/crates/uplift_lib/src/discovery.rs @@ -0,0 +1,109 @@ +use std::collections::BTreeSet; +use std::pin::Pin; +use btleplug::api::{bleuuid, BDAddr, Central, Characteristic, Descriptor, Peripheral as _, PeripheralProperties, ScanFilter, Service, ValueNotification, WriteType}; +use btleplug::platform::{Peripheral, Adapter, Manager, PeripheralId}; +use tokio::select; +use tokio::sync::mpsc; +use tokio::sync::mpsc::{Receiver, Sender}; +use uuid::Uuid; + +use anyhow::{anyhow, Context, Result}; +use btleplug::api; +use btleplug::api::CentralEvent::{DeviceConnected, DeviceDiscovered, DeviceUpdated}; +use futures::{Stream, StreamExt}; + +const DESK_SERVICE_UUID: Uuid = bleuuid::uuid_from_u16(0xff12); + +pub trait DeskAdapter { + async fn scan_for_desks(&self) -> Receiver>; + + async fn get_desk_peripheral(&self, id: I) -> Result> where I: Into; +} + +impl DeskAdapter for Adapter { + async fn scan_for_desks(&self) -> Receiver> { + let (tx, rx) = mpsc::channel(10); + + let adapter = self.clone(); + tokio::spawn(async move { + async fn inner(adapter: &Adapter, tx: &Sender>) -> Result<()> { + let mut events = adapter.events().await?; + + // scan for our desk service + adapter + .start_scan(ScanFilter { + services: vec![DESK_SERVICE_UUID], + }) + .await?; + + loop { + select! { + event = events.next() => { + match event { + Some(DeviceDiscovered(id) | DeviceUpdated(id) | DeviceConnected(id)) => { + match adapter.get_desk_peripheral(id).await { + Ok(Some(peripheral)) => { + if let Err(error) = tx.send(Ok(peripheral)).await { + break Err(error.into()) + } + } + Ok(None) => (), + Err(error) => log::warn!("{error}") + } + } + Some(event ) => log::trace!("Unhandled Event: {:?}", event), + None => { + break Err(anyhow!("Our adapter stopped looking for peripherals")) + } + } + } + _ = tx.closed() => { + break Ok(()); + } + } + } + } + + log::trace!("Started Scanning"); + + let result = inner(&adapter, &tx).await; + if let Err(error) = adapter.stop_scan().await { + log::error!("Failed to stop scanning: {error:?}"); + } + else { + log::trace!("Stopped Scanning") + } + + if let Err(error) = result { + if let Err(error) = tx.send(Err(error)).await { + log::warn!("Error when sending final error: {:?}", error.0) + } + } + }); + + rx + } + + async fn get_desk_peripheral(&self, id: I) -> Result> where I: Into { + let id = id.into(); + + let peripheral = self + .peripheral(&id) + .await + .context(format!("{id} - Couldn't connect to Desk"))?; + + let properties = peripheral.properties().await.context(format!( + "{id} - Couldn't get properties", + ))?; + + match properties { + Some(properties) if properties.services.contains(&DESK_SERVICE_UUID) => { + Ok(Some(peripheral)) + } + _ => { + Ok(None) + } + } + } +} + diff --git a/crates/uplift_lib/src/error.rs b/crates/uplift_lib/src/error.rs new file mode 100644 index 0000000..34bb1d6 --- /dev/null +++ b/crates/uplift_lib/src/error.rs @@ -0,0 +1,22 @@ +use std::result; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error("Bluetooth Error: {0}")] + BluetoothError(btleplug::Error) +} + +pub type Result = result::Result; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn send_sync() { + fn ensure_send_sync(t: T) {} + + ensure_send_sync(Error::BluetoothError(btleplug::Error::DeviceNotFound)) + } +} \ No newline at end of file diff --git a/crates/uplift_lib/src/id.rs b/crates/uplift_lib/src/id.rs new file mode 100644 index 0000000..4e3be38 --- /dev/null +++ b/crates/uplift_lib/src/id.rs @@ -0,0 +1,222 @@ +use crate::{UpliftDesk, DESK_SERVICE_UUID}; +use anyhow::anyhow; +use btleplug::api::CentralEvent::{DeviceConnected, DeviceDiscovered, DeviceUpdated}; +use btleplug::api::{bleuuid, Central, Peripheral as _, ScanFilter}; +use btleplug::platform::{Adapter, Peripheral, PeripheralId}; +use btleplug::Result; +use futures::StreamExt; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +use std::error::Error; +use std::{mem, result}; +use tokio::select; +use tokio::sync::mpsc; +use tokio::sync::mpsc::{Receiver, Sender}; +use uuid::Uuid; + +#[cfg(feature = "sqlx")] +pub use sqlx_feature::*; + +#[cfg(feature = "serde")] +pub use serde_feture::*; + +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Eq, Hash, Ord, PartialEq, PartialOrd, Clone, Debug)] +pub struct UpliftDeskId(PeripheralId); + +impl UpliftDeskId { + pub(crate) fn new(id: I) -> Self + where + I: Into, + { + Self(id.into()) + } + + pub async fn scan(adapter: &Adapter) -> Receiver> { + let (tx, rx) = mpsc::channel(10); + + let adapter = adapter.clone(); + tokio::spawn(async move { + async fn inner(adapter: &Adapter, tx: &Sender>) -> Result<()> { + let mut events = adapter.events().await?; + + // scan for our desk service + adapter + .start_scan(ScanFilter { + services: vec![DESK_SERVICE_UUID], + }) + .await?; + + loop { + select! { + event = events.next() => { + match event { + Some(DeviceDiscovered(id) | DeviceUpdated(id) | DeviceConnected(id)) => { + if let Err(error) = tx.send(Ok(UpliftDeskId::new(id))).await { + // the receiver has been dropped + break Ok(()) + } + } + Some(event ) => log::trace!("Unhandled Event: {:?}", event), + None => { + // Our adapter stopped looking for peripherals + break Err(btleplug::Error::NotConnected) + } + } + } + _ = tx.closed() => { + break Ok(()); + } + } + } + } + + log::trace!("Started Scanning"); + + let result = inner(&adapter, &tx).await; + if let Err(error) = adapter.stop_scan().await { + log::error!("Failed to stop scanning: {error:?}"); + } else { + log::trace!("Stopped Scanning") + } + + if let Err(error) = result { + if let Err(error) = tx.send(Err(error)).await { + log::warn!("Error when sending final error: {error:?}") + } + } + }); + + rx + } + + pub async fn connect(&self, adapter: &Adapter) -> Result { + UpliftDesk::new(self.0.clone(), adapter).await + } +} + +impl From for UpliftDeskId { + fn from(value: Uuid) -> Self { + UpliftDeskId(value.into()) + } +} + +#[cfg(feature = "serde")] +mod serde_feture { + 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::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 + // &'q [u8]: Encode<'q, DB>, + // { + // fn encode_by_ref( + // &self, + // buf: &mut ::ArgumentBuffer<'q>, + // ) -> result::Result { + // rmp_serde::to_vec(self)?.encode(buf) + // } + // } + + 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<'q>, + ) -> result::Result { + rmp_serde::to_vec(self)?.encode(buf) + } + } + + // impl<'r, DB: Database> Decode<'r, DB> for UpliftDeskId + // where + // // make sure our DB supports binary + // &'r [u8]: Decode<'r, DB>, + // { + // fn decode( + // value: ::ValueRef<'r>, + // ) -> result::Result> { + // let raw_value = <&[u8] as Decode>::decode(value)?; + // + // Ok(rmp_serde::from_slice(raw_value)?) + // } + // } +} + +#[cfg(test)] +mod tests { + pub use super::*; + use btleplug::api::Manager as _; + use btleplug::platform::Manager; + #[tokio::test] + async fn test() { + let manager = Manager::new().await.unwrap(); + + let adapters = manager.adapters().await.unwrap(); + let adapter = adapters.into_iter().next().unwrap(); + + let mut rx = UpliftDeskId::scan(&adapter).await; + + let mut i = 10; + while let Some(result) = rx.recv().await { + println!("{result:?}"); + i -= 1; + + if i <= 0 { + break; + } + } + } +} diff --git a/src/desk.rs b/crates/uplift_lib/src/lib.rs similarity index 65% rename from src/desk.rs rename to crates/uplift_lib/src/lib.rs index 231028c..455cbd1 100644 --- a/src/desk.rs +++ b/crates/uplift_lib/src/lib.rs @@ -1,3 +1,8 @@ +mod desk; +mod discovery; +mod error; +mod id; + use std::collections::BTreeSet; use std::sync::atomic::AtomicIsize; use std::sync::atomic::AtomicU8; @@ -5,13 +10,16 @@ use std::sync::atomic::Ordering; use std::sync::Arc; use std::time::Duration; -use anyhow::{anyhow, Context}; +pub use crate::desk::*; +pub use crate::discovery::DeskAdapter; +pub use crate::id::*; +use anyhow::{anyhow, Context, Result}; 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 btleplug::platform::{Manager, Peripheral, PeripheralId}; use futures::{executor, StreamExt}; use tokio::time; use uuid::Uuid; @@ -31,7 +39,7 @@ 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 { +pub struct ConnectedUpliftDesk { height: Arc, raw_height: Arc<(AtomicU8, AtomicU8)>, data_in_characteristic: Characteristic, @@ -39,17 +47,40 @@ pub struct Desk { _manager: Manager, } -impl Desk { - pub async fn new() -> Result { - let (manager, peripheral) = connect().await?; +impl ConnectedUpliftDesk { + pub async fn new() -> Result { + log::debug!("Connecting to Bluetooth Manager"); + let manager = Manager::new().await?; + + let adapters = manager.adapters().await?; + let adapter = adapters + .into_iter() + .next() + .ok_or_else(|| anyhow!("Couldn't find an adapter"))?; + + // Grab the first desk + let peripheral = adapter + .scan_for_desks() + .await + .recv() + .await + .expect("Scanner unexpectedly stopped")?; + // let peripheral = adapter.get_desk_peripheral(Uuid::parse_str("0ab28845-04db-6dd1-ddf7-58a1b26ccf31").unwrap()).await?; + + log::debug!("{} - Attempting to connect", peripheral.id()); + + peripheral + .connect() + .await + .context(format!("{} - Connection failed", peripheral.id()))?; - log::debug!("{:?} - Connected to peripheral", peripheral.address()); + log::debug!("{} - Connected to peripheral", peripheral.id()); // start discovering characteristics on our peripheral peripheral .discover_services() .await - .with_context(|| format!("{:?} - Discovering Services", peripheral.address()))?; + .with_context(|| format!("{} - Discovering Services", peripheral.id()))?; let (data_in_characteristic, data_out_characteristic, _name_characteristic) = get_characteristics(peripheral.characteristics())?; @@ -66,11 +97,9 @@ impl Desk { peripheral .subscribe(&data_out_characteristic) .await - .with_context(|| { - format!("{:?} - Subscribing to desk updates", peripheral.address()) - })?; + .with_context(|| format!("{} - Subscribing to desk updates", peripheral.id()))?; - let address = peripheral.address(); + let id = peripheral.id(); tokio::spawn(async move { while let Some(ValueNotification { value, .. }) = height_receiver.next().await { let last_height = updated_height.load(Ordering::Relaxed); @@ -79,7 +108,7 @@ impl Desk { log::trace!( "{:?} - Updated Height: ({:x},{:x}) -> {:x}", - address, + id, low, high, height @@ -91,7 +120,7 @@ impl Desk { }); } - let desk = Desk { + let desk = ConnectedUpliftDesk { height, raw_height, data_in_characteristic, @@ -118,35 +147,35 @@ impl Desk { } pub async fn save_sit(&self) -> Result<(), anyhow::Error> { - log::debug!("{:?} - Save sit", self.peripheral.address()); + log::debug!("{} - Save sit", self.peripheral.id()); self.write(&self.data_in_characteristic, &SAVE_SIT_PACKET) .await - .with_context(|| format!("{:?} - Saving Sit", self.peripheral.address())) + .with_context(|| format!("{} - Saving Sit", self.peripheral.id())) } pub async fn save_stand(&self) -> Result<(), anyhow::Error> { - log::debug!("{:?} - Save stand", self.peripheral.address()); + log::debug!("{} - Save stand", self.peripheral.id()); self.write(&self.data_in_characteristic, &SAVE_STAND_PACKET) .await - .with_context(|| format!("{:?} - Saving Stand", self.peripheral.address())) + .with_context(|| format!("{} - Saving Stand", self.peripheral.id())) } pub async fn sit(&self) -> Result<(), anyhow::Error> { - log::debug!("{:?} - Sit", self.peripheral.address()); + log::debug!("{} - Sit", self.peripheral.id()); self.write(&self.data_in_characteristic, &SIT_PACKET) .await - .with_context(|| format!("{:?} - Sitting", self.peripheral.address())) + .with_context(|| format!("{} - Sitting", self.peripheral.id())) } pub async fn stand(&self) -> Result<(), anyhow::Error> { - log::debug!("{:?} - Stand", self.peripheral.address()); + log::debug!("{} - Stand", self.peripheral.id()); self.write(&self.data_in_characteristic, &STAND_PACKET) .await - .with_context(|| format!("{:?} - Standing", self.peripheral.address())) + .with_context(|| format!("{} - Standing", self.peripheral.id())) } pub async fn query_height(&self) -> Result { @@ -154,7 +183,7 @@ impl Desk { self.height.store(-1, Ordering::Relaxed); self.write(&self.data_in_characteristic, &QUERY_PACKET) .await - .with_context(|| format!("{:?} - Querying", self.peripheral.address()))?; + .with_context(|| format!("{} - Querying", self.peripheral.id()))?; // wait for our height to update (is there a better way than polling?) while self.height.load(Ordering::Relaxed) <= 0 { @@ -172,7 +201,7 @@ impl Desk { self.peripheral .write(characteristic, data, WriteType::WithoutResponse) .await - .with_context(|| format!("{:?} - Failed to write data", self.peripheral.address())) + .with_context(|| format!("{} - Failed to write data", self.peripheral.id())) } } @@ -193,6 +222,8 @@ 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 { + // TODO https://github.com/justintout/uplift-reconnect/blob/master/lib/ble.dart#L167 + let low = low as isize; let high = high as isize; @@ -210,81 +241,15 @@ fn estimate_height((low, high): (u8, u8), last_height: isize) -> isize { MIN_PHYSICAL_HEIGHT + raw_height } -impl Drop for Desk { +impl Drop for ConnectedUpliftDesk { 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> { +) -> anyhow::Result<(Characteristic, Characteristic, Characteristic)> { let mut data_in_characteristic = None; let mut data_out_characteristic = None; let mut name_characteristic = None; diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..861bb3e --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "1.83" \ No newline at end of file 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" - )) -} From 424ac966dcd4ba99662da0d4217d3d74ae4791f1 Mon Sep 17 00:00:00 2001 From: Dylan Owen Date: Wed, 12 Feb 2025 20:25:42 -0800 Subject: [PATCH 02/12] Initial main refactor --- Cargo.lock | 330 ++++++++---- Cargo.toml | 1 + Readme.md | 13 +- crates/uplift_cli/Cargo.toml | 29 ++ crates/uplift_cli/src/main.rs | 289 +++++++++++ crates/uplift_lib/Cargo.toml | 18 +- crates/uplift_lib/src/api/command.rs | 194 +++++++ crates/uplift_lib/src/api/message.rs | 256 ++++++++++ crates/uplift_lib/src/api/mod.rs | 47 ++ crates/uplift_lib/src/desk.rs | 722 +++++++++++++++++---------- crates/uplift_lib/src/discovery.rs | 109 ---- crates/uplift_lib/src/error.rs | 22 - crates/uplift_lib/src/id.rs | 208 ++++---- crates/uplift_lib/src/lib.rs | 282 +---------- crates/uplift_lib/src/mock_btle.rs | 91 ++++ 15 files changed, 1757 insertions(+), 854 deletions(-) create mode 100755 crates/uplift_cli/Cargo.toml create mode 100644 crates/uplift_cli/src/main.rs create mode 100644 crates/uplift_lib/src/api/command.rs create mode 100644 crates/uplift_lib/src/api/message.rs create mode 100644 crates/uplift_lib/src/api/mod.rs delete mode 100644 crates/uplift_lib/src/discovery.rs delete mode 100644 crates/uplift_lib/src/error.rs create mode 100644 crates/uplift_lib/src/mock_btle.rs diff --git a/Cargo.lock b/Cargo.lock index bf262ec..5612639 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,15 +18,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] -name = "ahash" -version = "0.8.11" +name = "aho-corasick" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy", + "memchr", ] [[package]] @@ -35,6 +32,56 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +dependencies = [ + "anstyle", + "once_cell", + "windows-sys 0.59.0", +] + [[package]] name = "anyhow" version = "1.0.94" @@ -52,15 +99,6 @@ dependencies = [ "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.4.0" @@ -189,6 +227,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + [[package]] name = "combine" version = "4.6.7" @@ -329,6 +373,12 @@ dependencies = [ "syn", ] +[[package]] +name = "downcast" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" + [[package]] name = "either" version = "1.13.0" @@ -338,6 +388,29 @@ dependencies = [ "serde", ] +[[package]] +name = "env_filter" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "humantime", + "log", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -355,6 +428,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "foldhash" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -364,6 +443,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fragile" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" + [[package]] name = "futures" version = "0.3.31" @@ -485,31 +570,32 @@ name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash", - "allocator-api2", -] [[package]] name = "hashbrown" version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] [[package]] name = "hashlink" -version = "0.9.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ - "hashbrown 0.14.5", + "hashbrown 0.15.2", ] [[package]] -name = "hex" -version = "0.4.3" +name = "humantime" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "icu_collections" @@ -660,6 +746,12 @@ dependencies = [ "hashbrown 0.15.2", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itertools" version = "0.10.5" @@ -747,12 +839,6 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - [[package]] name = "miniz_oxide" version = "0.8.2" @@ -773,14 +859,39 @@ dependencies = [ "windows-sys 0.52.0", ] +[[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 = "7.1.3" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" dependencies = [ "memchr", - "minimal-lexical", ] [[package]] @@ -911,6 +1022,32 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +[[package]] +name = "predicates" +version = "3.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" +dependencies = [ + "anstyle", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" + +[[package]] +name = "predicates-tree" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "proc-macro2" version = "1.0.92" @@ -938,6 +1075,35 @@ dependencies = [ "bitflags", ] +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + [[package]] name = "rmp" version = "0.8.14" @@ -1058,57 +1224,37 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "sqlformat" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790" -dependencies = [ - "nom", - "unicode_categories", -] - [[package]] name = "sqlx" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93334716a037193fac19df402f8571269c84a00852f6a7066b5d2616dcd64d3e" +version = "0.8.3" dependencies = [ "sqlx-core", ] [[package]] name = "sqlx-core" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d8060b456358185f7d50c55d9b5066ad956956fddec42ee2e8567134a8936e" +version = "0.8.3" dependencies = [ - "atoi", - "byteorder", "bytes", "crc", "crossbeam-queue", "either", "event-listener", - "futures-channel", "futures-core", "futures-intrusive", "futures-io", "futures-util", - "hashbrown 0.14.5", + "hashbrown 0.15.2", "hashlink", - "hex", "indexmap", "log", "memchr", "once_cell", - "paste", "percent-encoding", "serde", "sha2", "smallvec", - "sqlformat", - "thiserror 1.0.69", + "thiserror 2.0.9", "tracing", "url", ] @@ -1147,6 +1293,12 @@ dependencies = [ "syn", ] +[[package]] +name = "termtree" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" + [[package]] name = "thiserror" version = "1.0.69" @@ -1293,24 +1445,48 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] -name = "unicode_categories" -version = "0.1.1" +name = "uom" +version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" +checksum = "ffd36e5350a65d112584053ee91843955826bf9e56ec0d1351214e01f6d7cd9c" +dependencies = [ + "num-traits", + "typenum", +] [[package]] -name = "uplift-lib" +name = "uplift-cli" version = "0.0.1" dependencies = [ "anyhow", "btleplug", + "env_logger", "futures", "log", + "thiserror 2.0.9", + "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", "sqlx", - "thiserror 2.0.9", "tokio", + "tokio-stream", + "uom", "uuid", ] @@ -1337,6 +1513,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.11.0" @@ -1576,26 +1758,6 @@ dependencies = [ "synstructure", ] -[[package]] -name = "zerocopy" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "zerofrom" version = "0.1.5" diff --git a/Cargo.toml b/Cargo.toml index e0f17fb..107f4bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,5 +3,6 @@ resolver = "2" members = [ + "crates/uplift_cli", "crates/uplift_lib" ] \ 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..5cda540 --- /dev/null +++ b/crates/uplift_cli/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "uplift-cli" +version = "0.0.1" +authors = ["Dylan Owen "] +description = "" +readme = "Readme.md" +license = "MPL-2.0" +homepage = "https://github.com/dylanowen/uplift-cli" +repository = "https://github.com/dylanowen/uplift-cli" +edition = "2021" + +[dependencies] +uplift-lib = { path = "../uplift_lib" } +log = "0.4.21" +env_logger = "0.11" + +# async +tokio = { version = "1.37", features = ["rt-multi-thread"] } +futures = "0.3.30" + +# Bluetooth support +btleplug = "0.11.5" +uuid = "1.8" + +uom = "0.36" + +# Error handling +anyhow = "1.0.82" +thiserror = "2.0" diff --git a/crates/uplift_cli/src/main.rs b/crates/uplift_cli/src/main.rs new file mode 100644 index 0000000..14f1c84 --- /dev/null +++ b/crates/uplift_cli/src/main.rs @@ -0,0 +1,289 @@ +use btleplug::api::Manager as _; +use btleplug::platform::Manager; +use futures::StreamExt; +use log::LevelFilter; +use std::time::Duration; +use tokio::time::sleep; +use uom::fmt::DisplayStyle; +use uom::si::length::{foot, inch}; +use uplift_lib::Command; +use uplift_lib::UpliftDesk; +use uplift_lib::UpliftDeskId; +use uuid::Uuid; + +#[tokio::main] +async fn main() -> Result<(), anyhow::Error> { + env_logger::builder() + .filter_module("uplift_lib", LevelFilter::Trace) + .init(); + + let manager = Manager::new().await.unwrap(); + + let adapters = manager.adapters().await.unwrap(); + let adapter = adapters.into_iter().next().unwrap(); + + // let id = UpliftDeskId::scan(&adapter) + // .await + // .unwrap() + // .next() + // .await + // .unwrap(); + + let id = UpliftDeskId::from(Uuid::parse_str("0ab28845-04db-6dd1-ddf7-58a1b26ccf31")?); + + let mut desk = UpliftDesk::new(id, &adapter).await.unwrap(); + // let mut desk = UpliftDesk::new(id, &adapter).await.unwrap(); + + let mut height_stream = desk.height_stream().await?; + while let Some(height) = height_stream.next().await { + desk.command(Command::Unknown { + cmd: 0xfe, + data: &[], + }) + .await + .unwrap(); + let rssi = desk.rssi(); + + println!( + "height: {} {} rssi: {rssi:?}", + height + .trunc::() + .into_format_args(foot, DisplayStyle::Abbreviation), + height + .fract::() + .into_format_args(inch, DisplayStyle::Abbreviation) + ) + } + + // let mut interval = interval(Duration::from_millis(500)); + // for _ in 0..10 { + // interval.tick().await; + // println!("name: {:?}", desk.name().await.unwrap()); + // println!("height: {:?}", desk.height().await.unwrap()); + // } + + drop(desk); + + sleep(Duration::from_secs(20)).await; + + Ok(()) +} + +// 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" +// )) +// } diff --git a/crates/uplift_lib/Cargo.toml b/crates/uplift_lib/Cargo.toml index 7dee9d4..2f7b4bc 100755 --- a/crates/uplift_lib/Cargo.toml +++ b/crates/uplift_lib/Cargo.toml @@ -20,17 +20,27 @@ log = "0.4.21" # async tokio = { version = "1.37", features = ["macros", "sync", "time"] } +tokio-stream = "0.1" futures = "0.3.30" # Bluetooth support btleplug = "0.11.5" uuid = "1.8" +nom = "8" + +#autoconver +#uom.version = "0.36" +#uom = { version = "0.36", default-features = false, features = ["autoconvert", "usize", "f32", "si", "std", "serde"] } +uom = { version = "0.36", default-features = false, features = ["f32", "si", "std"] } serde = { version = "1.0", features = ["derive"], default-features = false, optional = true } -sqlx = { version = "0.8", default-features = false, optional = true } +sqlx = { version = "0.8", path = "../../../sqlx", default-features = false, optional = true } rmp-serde = { version = "1.3", optional = true } -# Error handling -anyhow = "1.0.82" -thiserror = "2.0" +either = "1.13" + +[dev-dependencies] +async-trait = "0.1" +mockall = "0.13" +tokio = { version = "1.37", 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..8f29bb6 --- /dev/null +++ b/crates/uplift_lib/src/api/command.rs @@ -0,0 +1,194 @@ +use crate::api::{serialize_height, serialize_payload}; +use btleplug::api; +use btleplug::api::{Characteristic, WriteType}; +use btleplug::Result; +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, + SaveMemory1, + SaveMemory2, + SaveMemory3, + SaveMemory4, + GotoMemory1, + GotoMemory2, + GotoMemory3, + GotoMemory4, + FetchHeightValue, + FetchHeightRange, + FetchHighLowLimit, + GotoHeight { height_mm: u16 }, + SetMemoryHeight { memory_location: u8, height_mm: u16 }, + Patch, + FetchStandTime, + FetchAllTime, + Unknown { cmd: u8, data: &'static [u8] }, +} + +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)) +} + +pub const FETCH_HIGH_LOW_LIMIT: [u8; 6] = simple_desk_command(0x20); + +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(0x2b); + +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]]) +} + +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::SaveMemory1 => Cow::from(&SAVE_MEMORY_1), + Command::SaveMemory2 => Cow::from(&SAVE_MEMORY_2), + Command::SaveMemory3 => Cow::from(&SAVE_MEMORY_3), + Command::SaveMemory4 => Cow::from(&SAVE_MEMORY_4), + Command::GotoMemory1 => Cow::from(&GOTO_MEMORY_1), + 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::FetchHighLowLimit => Cow::from(&FETCH_HIGH_LOW_LIMIT), + 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::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..c37dee8 --- /dev/null +++ b/crates/uplift_lib/src/api/message.rs @@ -0,0 +1,256 @@ +use crate::api::{deserialize_height, PAYLOAD_END}; +use nom::branch::alt; +use nom::bytes::complete::{tag, take}; +use nom::multi::many0; +use nom::Parser; +use nom::{IResult, Needed}; +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; +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; +const IA_CODE: u8 = 0x10; // Probably stores a height, maybe some minimum? +const HEIGHT_MAX_CODE: u8 = 0x21; +const HEIGHT_MIN_CODE: u8 = 0x22; + +#[derive(Eq, PartialEq, Clone)] +pub enum Message { + Height(u16), + PhysicalLimits { min: u16, max: u16 }, + HeightUnit(HeightUnit), + Unknown { code: u8, data: Vec }, +} + +#[derive(Eq, PartialEq, Clone, Copy, Debug)] +pub enum HeightUnit { + Cm, + Inch, +} + +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)?, + HEIGHT_UNIT_CODE => parse_height_unit_message(data)?, + PHYSICAL_LIMITS_CODE => parse_physical_limits_message(data)?, + _ => Message::Unknown { + code: cmd, + data: data.into(), + }, + }; + + Ok((i, message)) +} + +fn parse_height_message(data: &[u8]) -> NomResult { + // TODO what does the bonus byte mean? It always seems to be 0x03 + let (_, data) = take(2usize)(data)?; + + let height = deserialize_height(data); + + Ok(Message::Height(height)) +} + +fn parse_height_unit_message(data: &[u8]) -> NomResult { + let (_, height_unit) = alt(( + tag([0x00].as_ref()).map(|_| HeightUnit::Cm), + tag([0x01].as_ref()).map(|_| HeightUnit::Inch), + )) + .parse(data)?; + + Ok(Message::HeightUnit(height_unit)) +} + +fn parse_physical_limits_message(data: &[u8]) -> NomResult { + 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 }) +} + +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::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(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(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..0a35a26 --- /dev/null +++ b/crates/uplift_lib/src/api/mod.rs @@ -0,0 +1,47 @@ +mod command; +mod message; + +use btleplug::api::bleuuid; +pub use command::*; +pub use message::*; +use uuid::Uuid; + +pub const DESK_SERVICE_UUID: Uuid = bleuuid::uuid_from_u16(0xff12); + +pub const DESK_DATA_IN_UUID: Uuid = bleuuid::uuid_from_u16(0xff01); +pub const DESK_DATA_OUT_UUID: Uuid = bleuuid::uuid_from_u16(0xff02); +pub const DESK_NAME_UUID: Uuid = bleuuid::uuid_from_u16(0xff06); + +/// 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.rs b/crates/uplift_lib/src/desk.rs index b6ca383..b575da4 100644 --- a/crates/uplift_lib/src/desk.rs +++ b/crates/uplift_lib/src/desk.rs @@ -1,65 +1,84 @@ +use crate::api::{ + Command, EnhancedPeripheral as _, HeightUnit, Message, DESK_DATA_IN_UUID, DESK_DATA_OUT_UUID, + DESK_NAME_UUID, DESK_SERVICE_UUID, +}; +use crate::UpliftDeskId; use btleplug::api::{ - bleuuid, Central, Characteristic, Peripheral as _, PeripheralProperties, ValueNotification, - WriteType, + Central, Characteristic, Peripheral, PeripheralProperties, ValueNotification, WriteType, }; -use btleplug::platform::{Adapter, Peripheral, PeripheralId}; -use btleplug::{Error, Result}; +use btleplug::platform::PeripheralId; +use btleplug::{platform, Error, Result}; +use either::Either; use std::collections::BTreeSet; - -use crate::id::UpliftDeskId; -use crate::{ - estimate_height, get_raw_height, DESK_DATA_IN_UUID, DESK_DATA_OUT_UUID, DESK_NAME_UUID, - DESK_SERVICE_UUID, MID_PHYSICAL_HEIGHT, MIN_PHYSICAL_HEIGHT, QUERY_PACKET, -}; -use anyhow::anyhow; -use futures::StreamExt; +use std::fmt::{Debug, Formatter}; +use std::future::Future; +use std::mem; use std::ops::Deref; -use std::sync::atomic::Ordering; -use std::sync::Arc; use std::time::Duration; -use tokio::select; -use tokio::sync::broadcast::{Receiver, Sender}; -use tokio::sync::{broadcast, mpsc, RwLock}; -use tokio::time::sleep; -use uuid::Uuid; - -pub struct UpliftDesk { +use tokio::sync::{oneshot, watch}; +use tokio::task::JoinHandle; +use tokio::time::interval; +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 UpliftDesk { properties: PeripheralProperties, - peripheral: Peripheral, + peripheral: P, data_in_characteristic: Characteristic, data_out_characteristic: Characteristic, name_characteristic: Characteristic, - height_stream: Arc>>>, + subscription: Either, DeskSubscription

>, + cancel_subscription_tx: oneshot::Sender<()>, } -impl UpliftDesk { - pub async fn new(id: I, adapter: &Adapter) -> Result +impl UpliftDesk

{ + pub async fn new(id: I, central: &C) -> Result where I: Into, + C: Central, { - let peripheral = adapter.peripheral(&id.into()).await?; + let id = id.into(); + log::debug!("{{{id}}} - Connecting to desk"); + + let peripheral = central.peripheral(&id).await?; peripheral.connect().await?; peripheral.discover_services().await?; + let properties = peripheral.properties().await?.ok_or_else(|| { + log::warn!("{{{id}}} - No properties found for our service"); + Error::DeviceNotFound + })?; + let service = peripheral.services().iter().find(|s| s.uuid == DESK_SERVICE_UUID).cloned().ok_or_else(|| { + log::warn!("{{{id}}} - Attempted to connect to desk that isn't advertising the necessary Service or Characteristics"); + Error::DeviceNotFound + })?; + let (data_in_characteristic, data_out_characteristic, name_characteristic) = - get_characteristics(peripheral.characteristics())?; + get_characteristics(service.characteristics)?; + + let (cancel_subscription_tx, cancel_subscription_rx) = oneshot::channel(); + + Ok(Self { + properties, + peripheral, + data_in_characteristic, + data_out_characteristic, + name_characteristic, + subscription: Either::Left(cancel_subscription_rx), + cancel_subscription_tx, + }) + } - println!("new desk connecting"); + pub fn id(&self) -> UpliftDeskId { + UpliftDeskId(self.peripheral.id()) + } - match peripheral.properties().await? { - Some(properties) if properties.services.contains(&DESK_SERVICE_UUID) => { - Ok(UpliftDesk { - properties, - peripheral, - data_in_characteristic, - data_out_characteristic, - name_characteristic, - height_stream: Arc::new(RwLock::new(None)), - }) - } - _ => Err(btleplug::Error::DeviceNotFound), - } + pub fn rssi(&self) -> Option { + self.properties.rssi } pub async fn name(&self) -> Result> { @@ -70,257 +89,320 @@ impl UpliftDesk { .and_then(|p| p.local_name)) } - pub async fn get_height(&self) -> Result { - let mut rx = self.stream_height().await?; - rx.recv().await.map_err(|e| Error::Other(Box::new(e))) + pub async fn command(&self, command: Command) -> Result<()> { + self.peripheral + .command( + command, + &self.data_in_characteristic, + WriteType::WithoutResponse, + ) + .await } - pub async fn stream_height(&self) -> Result> { - let height_stream_read = self.height_stream.read().await; - let rx = if let Some(height_stream) = height_stream_read.as_ref() { - println!("read subscribe"); - height_stream.subscribe() - } else { - drop(height_stream_read); - let mut height_stream_write = self.height_stream.write().await; - if let Some(height_stream) = height_stream_write.as_ref() { - println!("write subscribe"); - height_stream.subscribe() - } else { - let (tx, rx) = broadcast::channel(10); - - let peripheral = self.peripheral.clone(); - let data_in_characteristic = self.data_in_characteristic.clone(); - let data_out_characteristic = self.data_out_characteristic.clone(); - - let mut height_receiver = peripheral.notifications().await?; - peripheral.subscribe(&data_out_characteristic).await?; - - tokio::spawn({ - let tx = tx.clone(); - let height_stream = self.height_stream.clone(); - - println!("spawning stream"); - - async move { - let mut received_message = false; - loop { - select! { - event = height_receiver.next() => { - match event { - Some(ValueNotification { value, .. }) => { - received_message = true; - let height = UpliftDeskHeight::new(&value); - println!("height: {height:?}"); - - if let Err(_) = tx.send(height) { - // no more receivers - break; - } - } - None => break, - } - } - _ = sleep(Duration::from_secs(1)) => { - if tx.receiver_count() <= 0 { - break; - } else if !received_message { - write(&QUERY_PACKET,&data_in_characteristic,&peripheral).await; - } - } - } - } + pub async fn height(&mut self) -> Result { + self.get_subscription().await?.get_height().await + } - if let Err(e) = peripheral.unsubscribe(&data_out_characteristic).await { - log::warn!("Error unsubscribing from Data Out Characteristic: {e:?}") - } + pub async fn height_unit(&mut self) -> Result { + Ok(self.get_subscription().await?.get_height_unit()) + } - *height_stream.write().await = None - } - }); + pub async fn height_stream(&mut self) -> Result> { + self.get_subscription().await?.height_stream().await + } - *height_stream_write = Some(tx); + pub fn disconnect(self) -> UpliftDeskId { + // Drop takes care of disconnecting + self.id() + } - rx + async fn get_subscription(&mut self) -> Result<&DeskSubscription

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

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

{ + fn drop(&mut self) { + let cancel = mem::replace(&mut self.cancel_subscription_tx, oneshot::channel().0); + let _ = cancel.send(()); + + let peripheral = self.peripheral.clone(); + tokio::spawn(async move { + if let Err(e) = peripheral.disconnect().await { + log::warn!("{{{}}} - Error while dropping desk: {e:?}", peripheral.id()); + } + }); + } +} + +struct DeskSubscription { + height_rx: watch::Receiver>, + height_unit_rx: watch::Receiver, + data_in_characteristic: Characteristic, + peripheral: P, + subscription_live: JoinHandle>, +} - pub async fn test(&self) -> Result<()> { - self.peripheral.discover_services().await?; +/// The app starts up by sending +/// +/// t:400 [Command::FetchHighLowLimit] +/// t:500 [Command::FetchHighLowLimit] +/// t:600 [Command::FetchHeightValue] +/// t:700 [Command::FetchHeightRange] +/// t:? F1F11F0100207E Advanced Command: 0x1F/31 data: 00 +/// t:? F1F1FE00FE7E Simple command: 0xFE/-2 +/// t:1100 F1F1FE00FE7E +impl DeskSubscription

{ + async fn new( + id: PeripheralId, + data_in_characteristic: Characteristic, + data_out_characteristic: Characteristic, + peripheral: P, + cancel_rx: oneshot::Receiver<()>, + ) -> Self { + let (height_tx, height_rx) = watch::channel(None); + // The app seems to default to inches + let (height_unit_tx, height_unit_rx) = watch::channel(HeightUnit::Inch); + + let receiver = peripheral.notifications().await.unwrap(); + peripheral + .subscribe(&data_out_characteristic) + .await + .unwrap(); + + // 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::FetchHighLowLimit, + Command::FetchHighLowLimit, + Command::FetchHeightValue, + ] { + 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, + height_unit_tx, + cancel_rx, + data_out_characteristic, + peripheral, + ) + .await; + + if let Err(e) = &result { + log::error!("{{{id}}} - Error in our subscription task: {e:?}") + } - for service in self.peripheral.services() { - println!("service: {service:?}") + result + } + }); + + Self { + height_rx, + height_unit_rx, + data_in_characteristic, + peripheral, + subscription_live, } + } - Ok(()) + fn get_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().clone(), + )) + } } - pub async fn disconnect(self) -> UpliftDeskId { - // the Drop implementation of self will disconnect this - UpliftDeskId::new(self.peripheral.id()) + fn get_height_unit(&self) -> HeightUnit { + // I don't know how to ask the desk for this info + self.height_unit_rx.borrow().clone() } - async fn query_desk(&self) -> Result<()> { - write( - &QUERY_PACKET, - &self.data_in_characteristic, - &self.peripheral, + async fn height_stream(&self) -> Result> { + 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().clone())) + }), ) - .await } - // pub async fn new_old(id: I, adapter: &Adapter) -> Result> where I: Into { - // let id = id.into(); - // - // let peripheral = adapter - // .peripheral(&id) - // .await - // .context(format!("{id} - Couldn't connect to Desk"))?; - // - // let properties = peripheral.properties().await.context(format!( - // "{id} - Couldn't get properties", - // ))?; - // - // match properties { - // Some(properties) if properties.services.contains(&DESK_SERVICE_UUID) => { - // Ok(Some(UpliftDesk{ peripheral })) - // } - // _ => { - // Ok(None) - // } - // } - // } - // - // pub async fn scan_for_desks(adapter: &Adapter) -> Receiver> { - // let (tx, rx) = mpsc::channel(10); - // - // let adapter = adapter.clone(); - // tokio::spawn(async move { - // async fn inner( - // adapter: &Adapter, - // tx: &Sender>, - // ) -> Result<()> { - // let mut events = adapter.events().await?; - // - // // scan for our desk service - // adapter - // .start_scan(ScanFilter { - // services: vec![DESK_SERVICE_UUID], - // }) - // .await?; - // - // loop { - // select! { - // event = events.next() => { - // match event { - // Some(DeviceDiscovered(id) | DeviceUpdated(id) | DeviceConnected(id)) => { - // match UpliftDesk::new_old(id, adapter).await { - // Ok(Some(desk)) => { - // if let Err(error) = tx.send(Ok(desk)).await { - // break Err(error.into()) - // } - // } - // Ok(None) => (), - // Err(error) => log::warn!("{error}") - // } - // } - // Some(event ) => log::trace!("Unhandled Event: {:?}", event), - // None => { - // break Err(anyhow!("Our adapter stopped looking for peripherals")) - // } - // } - // } - // _ = tx.closed() => { - // break Ok(()); - // } - // } - // } - // } - // - // log::trace!("Started Scanning"); - // - // let result = inner(&adapter, &tx).await; - // if let Err(error) = adapter.stop_scan().await { - // log::error!("Failed to stop scanning: {error:?}"); - // } else { - // log::trace!("Stopped Scanning") - // } - // - // if let Err(error) = result { - // if let Err(error) = tx.send(Err(error)).await { - // // log::warn!("Error when sending final error: {:?}", error.0) - // } - // } - // }); - // - // rx - // } -} + 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 + }) + } -#[inline] -async fn write( - data: &[u8], - characteristic: &Characteristic, - peripheral: &Peripheral, -) -> Result<()> { - peripheral - .write(&characteristic, data, WriteType::WithoutResponse) - .await + fn is_live(&self) -> bool { + !self.subscription_live.is_finished() + } } -impl Deref for UpliftDesk { - type Target = Peripheral; - - fn deref(&self) -> &Self::Target { - &self.peripheral +async fn subscription_main_loop( + mut receiver: R, + height_tx: watch::Sender>, + height_unit_tx: watch::Sender, + mut cancel_rx: oneshot::Receiver<()>, + data_out_characteristic: Characteristic, + peripheral: P, +) -> 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.len() > 0 { 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{ .. } => {} + Message::HeightUnit(height_unit) => { + height_unit_tx.send(height_unit).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 => { + break + } + } } -} -impl Drop for UpliftDesk { - fn drop(&mut self) { - let peripheral = self.peripheral.clone(); - // tokio::task::spawn(async move { - // let _ = peripheral.disconnect().await; - // }); + if let Err(e) = peripheral.unsubscribe(&data_out_characteristic).await { + log::warn!( + "{{{id}}} - Error Unsubscribing from {{{}}}: {e:?}", + data_out_characteristic.uuid + ); } -} -#[derive(Clone, Debug)] -pub struct UpliftDeskHeight { - low: u8, - high: u8, + log::trace!("{{{id}}} - Exiting Subscription"); + + Ok(()) } -impl UpliftDeskHeight { - fn new(data: &[u8]) -> Self { - Self { - low: data[5], - high: data[7], +async fn wait_for_some(mut rx: watch::Receiver>, on_none: F) -> 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.clone()), + None => { + on_none().await?; + + Ok(rx + .wait_for(Option::is_some) + .await + .map_err(|_| Error::NotConnected)? + .clone() + .unwrap()) } } +} - pub fn physical_height(&self) -> usize { - let low = self.low as usize; - let high = self.high as usize; - - // 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 - // }; - let raw_height = low; - - raw_height - } +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. } fn get_characteristics( @@ -349,24 +431,116 @@ fn get_characteristics( #[cfg(test)] mod tests { - pub use super::*; - use btleplug::api::Manager as _; - use btleplug::platform::Manager; + use super::*; + use crate::mock_btle::*; + use btleplug::api::{CharPropFlags, Service}; + use tokio::runtime::Runtime; + use uuid::Uuid; + + #[test] + fn test_new_desk() { + let runtime = Runtime::new().unwrap(); + + runtime.block_on(async { + let central = mock_peripheral(|_| ()); + + let id: PeripheralId = Uuid::default().into(); + let desk = UpliftDesk::new(id, ¢ral).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() { - let manager = Manager::new().await.unwrap(); + async fn test_invalid_peripheral() { + 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_SERVICE_UUID], + ..Default::default() + })) + }); + peripheral.expect_services().returning(BTreeSet::new); + + let mut drop_peripheral = MockPeripheral::new(); + drop_peripheral + .expect_disconnect() + .once() + .returning(|| Ok(())); + peripheral + .expect_clone() + .return_once(move || drop_peripheral); + + central.expect_peripheral().return_once(|_| Ok(peripheral)); + + let id: PeripheralId = Uuid::default().into(); + assert!(matches!( + UpliftDesk::new(id, ¢ral).await.err().unwrap(), + Error::DeviceNotFound + )); + } - let adapters = manager.adapters().await.unwrap(); - let adapter = adapters.into_iter().next().unwrap(); + fn mock_peripheral(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_SERVICE_UUID], + ..Default::default() + })) + }); + peripheral.expect_services().returning(move || { + let mut services = BTreeSet::new(); + services.insert(Service { + uuid: DESK_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 + } - let mut rx = UpliftDeskId::scan(&adapter).await; - let id = rx.recv().await.unwrap().unwrap(); + fn valid_characteristics() -> BTreeSet { + let mut characteristics = BTreeSet::new(); - let desk = id.connect(&adapter).await.unwrap(); + for uuid in [DESK_DATA_IN_UUID, DESK_DATA_OUT_UUID, DESK_NAME_UUID] { + characteristics.insert(Characteristic { + uuid, + service_uuid: DESK_SERVICE_UUID, + properties: CharPropFlags::empty(), + descriptors: BTreeSet::new(), + }); + } - println!( - "height: {}", - desk.get_height().await.unwrap().physical_height() - ) + characteristics } } diff --git a/crates/uplift_lib/src/discovery.rs b/crates/uplift_lib/src/discovery.rs deleted file mode 100644 index 44c6068..0000000 --- a/crates/uplift_lib/src/discovery.rs +++ /dev/null @@ -1,109 +0,0 @@ -use std::collections::BTreeSet; -use std::pin::Pin; -use btleplug::api::{bleuuid, BDAddr, Central, Characteristic, Descriptor, Peripheral as _, PeripheralProperties, ScanFilter, Service, ValueNotification, WriteType}; -use btleplug::platform::{Peripheral, Adapter, Manager, PeripheralId}; -use tokio::select; -use tokio::sync::mpsc; -use tokio::sync::mpsc::{Receiver, Sender}; -use uuid::Uuid; - -use anyhow::{anyhow, Context, Result}; -use btleplug::api; -use btleplug::api::CentralEvent::{DeviceConnected, DeviceDiscovered, DeviceUpdated}; -use futures::{Stream, StreamExt}; - -const DESK_SERVICE_UUID: Uuid = bleuuid::uuid_from_u16(0xff12); - -pub trait DeskAdapter { - async fn scan_for_desks(&self) -> Receiver>; - - async fn get_desk_peripheral(&self, id: I) -> Result> where I: Into; -} - -impl DeskAdapter for Adapter { - async fn scan_for_desks(&self) -> Receiver> { - let (tx, rx) = mpsc::channel(10); - - let adapter = self.clone(); - tokio::spawn(async move { - async fn inner(adapter: &Adapter, tx: &Sender>) -> Result<()> { - let mut events = adapter.events().await?; - - // scan for our desk service - adapter - .start_scan(ScanFilter { - services: vec![DESK_SERVICE_UUID], - }) - .await?; - - loop { - select! { - event = events.next() => { - match event { - Some(DeviceDiscovered(id) | DeviceUpdated(id) | DeviceConnected(id)) => { - match adapter.get_desk_peripheral(id).await { - Ok(Some(peripheral)) => { - if let Err(error) = tx.send(Ok(peripheral)).await { - break Err(error.into()) - } - } - Ok(None) => (), - Err(error) => log::warn!("{error}") - } - } - Some(event ) => log::trace!("Unhandled Event: {:?}", event), - None => { - break Err(anyhow!("Our adapter stopped looking for peripherals")) - } - } - } - _ = tx.closed() => { - break Ok(()); - } - } - } - } - - log::trace!("Started Scanning"); - - let result = inner(&adapter, &tx).await; - if let Err(error) = adapter.stop_scan().await { - log::error!("Failed to stop scanning: {error:?}"); - } - else { - log::trace!("Stopped Scanning") - } - - if let Err(error) = result { - if let Err(error) = tx.send(Err(error)).await { - log::warn!("Error when sending final error: {:?}", error.0) - } - } - }); - - rx - } - - async fn get_desk_peripheral(&self, id: I) -> Result> where I: Into { - let id = id.into(); - - let peripheral = self - .peripheral(&id) - .await - .context(format!("{id} - Couldn't connect to Desk"))?; - - let properties = peripheral.properties().await.context(format!( - "{id} - Couldn't get properties", - ))?; - - match properties { - Some(properties) if properties.services.contains(&DESK_SERVICE_UUID) => { - Ok(Some(peripheral)) - } - _ => { - Ok(None) - } - } - } -} - diff --git a/crates/uplift_lib/src/error.rs b/crates/uplift_lib/src/error.rs deleted file mode 100644 index 34bb1d6..0000000 --- a/crates/uplift_lib/src/error.rs +++ /dev/null @@ -1,22 +0,0 @@ -use std::result; -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum Error { - #[error("Bluetooth Error: {0}")] - BluetoothError(btleplug::Error) -} - -pub type Result = result::Result; - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn send_sync() { - fn ensure_send_sync(t: T) {} - - ensure_send_sync(Error::BluetoothError(btleplug::Error::DeviceNotFound)) - } -} \ No newline at end of file diff --git a/crates/uplift_lib/src/id.rs b/crates/uplift_lib/src/id.rs index 4e3be38..0e7edb6 100644 --- a/crates/uplift_lib/src/id.rs +++ b/crates/uplift_lib/src/id.rs @@ -1,108 +1,129 @@ -use crate::{UpliftDesk, DESK_SERVICE_UUID}; -use anyhow::anyhow; +use crate::UpliftDesk; use btleplug::api::CentralEvent::{DeviceConnected, DeviceDiscovered, DeviceUpdated}; -use btleplug::api::{bleuuid, Central, Peripheral as _, ScanFilter}; -use btleplug::platform::{Adapter, Peripheral, PeripheralId}; +use btleplug::api::{Central, CentralEvent, ScanFilter}; +use btleplug::platform::PeripheralId; use btleplug::Result; -use futures::StreamExt; +use futures::{ready, Stream}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; -use std::error::Error; -use std::{mem, result}; -use tokio::select; -use tokio::sync::mpsc; -use tokio::sync::mpsc::{Receiver, Sender}; +use std::pin::Pin; +use std::task::{Context, Poll}; use uuid::Uuid; -#[cfg(feature = "sqlx")] -pub use sqlx_feature::*; - -#[cfg(feature = "serde")] -pub use serde_feture::*; +use crate::api::DESK_SERVICE_UUID; #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Eq, Hash, Ord, PartialEq, PartialOrd, Clone, Debug)] -pub struct UpliftDeskId(PeripheralId); +pub struct UpliftDeskId(pub(crate) PeripheralId); impl UpliftDeskId { - pub(crate) fn new(id: I) -> Self + pub fn new(id: I) -> Self where I: Into, { Self(id.into()) } - pub async fn scan(adapter: &Adapter) -> Receiver> { - let (tx, rx) = mpsc::channel(10); + pub async fn scan(central: &C) -> Result> + where + C: Central + Unpin + 'static, + { + UpliftDeskIdStream::new(central).await + } - let adapter = adapter.clone(); - tokio::spawn(async move { - async fn inner(adapter: &Adapter, tx: &Sender>) -> Result<()> { - let mut events = adapter.events().await?; - - // scan for our desk service - adapter - .start_scan(ScanFilter { - services: vec![DESK_SERVICE_UUID], - }) - .await?; - - loop { - select! { - event = events.next() => { - match event { - Some(DeviceDiscovered(id) | DeviceUpdated(id) | DeviceConnected(id)) => { - if let Err(error) = tx.send(Ok(UpliftDeskId::new(id))).await { - // the receiver has been dropped - break Ok(()) - } - } - Some(event ) => log::trace!("Unhandled Event: {:?}", event), - None => { - // Our adapter stopped looking for peripherals - break Err(btleplug::Error::NotConnected) - } - } - } - _ = tx.closed() => { - break Ok(()); - } - } - } - } + pub async fn connect(&self, adapter: &C) -> Result> + where + C: Central, + ::Peripheral: 'static, + { + UpliftDesk::new(self.0.clone(), adapter).await + } +} - log::trace!("Started Scanning"); +impl From for UpliftDeskId { + fn from(value: Uuid) -> Self { + UpliftDeskId(value.into()) + } +} - let result = inner(&adapter, &tx).await; - if let Err(error) = adapter.stop_scan().await { - log::error!("Failed to stop scanning: {error:?}"); - } else { - log::trace!("Stopped Scanning") - } +impl From for PeripheralId { + fn from(val: UpliftDeskId) -> Self { + val.0 + } +} + +pub struct UpliftDeskIdStream +where + C: Central + Unpin + 'static, +{ + events: Pin + Send>>, + central: C, +} + +impl UpliftDeskIdStream +where + C: Central + Unpin, +{ + async fn new(central: &C) -> Result { + let central = central.clone(); + let events = central.events().await?; - if let Err(error) = result { - if let Err(error) = tx.send(Err(error)).await { - log::warn!("Error when sending final error: {error:?}") + central + .start_scan(ScanFilter { + services: vec![DESK_SERVICE_UUID], + }) + .await?; + + log::trace!("Started Scanning"); + + Ok(Self { events, central }) + } +} + +impl Stream for UpliftDeskIdStream +where + C: Central + Unpin, +{ + type Item = UpliftDeskId; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let next_id = loop { + match ready!(self.events.as_mut().poll_next(cx)) { + Some(DeviceDiscovered(id) | DeviceUpdated(id) | DeviceConnected(id)) => { + break Some(UpliftDeskId::new(id)) } + Some(event) => log::trace!("Unhandled Event: {:?}", event), + None => break None, } - }); + }; - rx + Poll::Ready(next_id) } - pub async fn connect(&self, adapter: &Adapter) -> Result { - UpliftDesk::new(self.0.clone(), adapter).await + fn size_hint(&self) -> (usize, Option) { + // We don't know the lower bound + (0, self.events.size_hint().1) } } -impl From for UpliftDeskId { - fn from(value: Uuid) -> Self { - UpliftDeskId(value.into()) +impl Drop for UpliftDeskIdStream +where + C: Central + Unpin + '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") + } + }); } } #[cfg(feature = "serde")] -mod serde_feture { +mod serde_feature { use super::*; impl From> for UpliftDeskId { @@ -126,6 +147,7 @@ mod sqlx_feature { use sqlx::encode::IsNull; use sqlx::error::BoxDynError; use sqlx::{Database, Decode, Encode, Type}; + use std::result; impl Type for UpliftDeskId where @@ -147,7 +169,7 @@ mod sqlx_feature { { fn decode( value: ::ValueRef<'r>, - ) -> result::Result> { + ) -> result::Result { let raw_value = as Decode>::decode(value)?; Ok(rmp_serde::from_slice(&raw_value)?) @@ -197,26 +219,24 @@ mod sqlx_feature { #[cfg(test)] mod tests { - pub use super::*; - use btleplug::api::Manager as _; - use btleplug::platform::Manager; - #[tokio::test] - async fn test() { - let manager = Manager::new().await.unwrap(); - - let adapters = manager.adapters().await.unwrap(); - let adapter = adapters.into_iter().next().unwrap(); - - let mut rx = UpliftDeskId::scan(&adapter).await; - let mut i = 10; - while let Some(result) = rx.recv().await { - println!("{result:?}"); - i -= 1; - - if i <= 0 { - break; - } - } - } + // #[tokio::test] + // async fn test() { + // let manager = Manager::new().await.unwrap(); + // + // let adapters = manager.adapters().await.unwrap(); + // let adapter = adapters.into_iter().next().unwrap(); + // + // let mut rx = UpliftDeskId::scan(&adapter).await; + // + // let mut i = 10; + // while let Some(result) = rx.recv().await { + // println!("{result:?}"); + // i -= 1; + // + // if i <= 0 { + // break; + // } + // } + // } } diff --git a/crates/uplift_lib/src/lib.rs b/crates/uplift_lib/src/lib.rs index 455cbd1..d5e8530 100644 --- a/crates/uplift_lib/src/lib.rs +++ b/crates/uplift_lib/src/lib.rs @@ -1,272 +1,22 @@ +mod api; mod desk; -mod discovery; -mod error; mod id; +#[cfg(test)] +mod mock_btle; -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; - +pub use crate::api::*; pub use crate::desk::*; -pub use crate::discovery::DeskAdapter; pub use crate::id::*; -use anyhow::{anyhow, Context, Result}; -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, PeripheralId}; -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 ConnectedUpliftDesk { - height: Arc, - raw_height: Arc<(AtomicU8, AtomicU8)>, - data_in_characteristic: Characteristic, - peripheral: Peripheral, - _manager: Manager, -} - -impl ConnectedUpliftDesk { - pub async fn new() -> Result { - log::debug!("Connecting to Bluetooth Manager"); - let manager = Manager::new().await?; - - let adapters = manager.adapters().await?; - let adapter = adapters - .into_iter() - .next() - .ok_or_else(|| anyhow!("Couldn't find an adapter"))?; - - // Grab the first desk - let peripheral = adapter - .scan_for_desks() - .await - .recv() - .await - .expect("Scanner unexpectedly stopped")?; - // let peripheral = adapter.get_desk_peripheral(Uuid::parse_str("0ab28845-04db-6dd1-ddf7-58a1b26ccf31").unwrap()).await?; - - log::debug!("{} - Attempting to connect", peripheral.id()); - - peripheral - .connect() - .await - .context(format!("{} - Connection failed", peripheral.id()))?; - - log::debug!("{} - Connected to peripheral", peripheral.id()); - - // start discovering characteristics on our peripheral - peripheral - .discover_services() - .await - .with_context(|| format!("{} - Discovering Services", peripheral.id()))?; - - 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.id()))?; - - let id = peripheral.id(); - 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}", - id, - 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 = ConnectedUpliftDesk { - 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.id()); - - self.write(&self.data_in_characteristic, &SAVE_SIT_PACKET) - .await - .with_context(|| format!("{} - Saving Sit", self.peripheral.id())) - } - - pub async fn save_stand(&self) -> Result<(), anyhow::Error> { - log::debug!("{} - Save stand", self.peripheral.id()); - - self.write(&self.data_in_characteristic, &SAVE_STAND_PACKET) - .await - .with_context(|| format!("{} - Saving Stand", self.peripheral.id())) - } - - pub async fn sit(&self) -> Result<(), anyhow::Error> { - log::debug!("{} - Sit", self.peripheral.id()); - - self.write(&self.data_in_characteristic, &SIT_PACKET) - .await - .with_context(|| format!("{} - Sitting", self.peripheral.id())) - } - - pub async fn stand(&self) -> Result<(), anyhow::Error> { - log::debug!("{} - Stand", self.peripheral.id()); - - self.write(&self.data_in_characteristic, &STAND_PACKET) - .await - .with_context(|| format!("{} - Standing", self.peripheral.id())) - } - - 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.id()))?; - - // 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.id())) - } -} - -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 { - // TODO https://github.com/justintout/uplift-reconnect/blob/master/lib/ble.dart#L167 - - 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 ConnectedUpliftDesk { - fn drop(&mut self) { - executor::block_on(self.peripheral.disconnect()).unwrap(); - } -} - -fn get_characteristics( - characteristics: BTreeSet, -) -> anyhow::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_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"))?, - )) -} +// // 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..a5afd8b --- /dev/null +++ b/crates/uplift_lib/src/mock_btle.rs @@ -0,0 +1,91 @@ +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::api::CentralEvent; +use btleplug::api::CentralState; +use btleplug::api::ScanFilter; +use btleplug::Result; +use futures::stream::Stream; +use std::collections::BTreeSet; +use std::fmt; +use std::fmt::{Debug, Display, Formatter}; +use std::pin::Pin; + +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 adapter_info(&self) -> Result; + + async fn adapter_state(&self) -> Result; + } + + impl Clone for Central { + fn clone(&self) -> Self; + } +} + +mock! { + pub Peripheral {} + + #[async_trait] + impl api::Peripheral for Peripheral { + fn id(&self) -> PeripheralId; + + fn address(&self) -> BDAddr; + + 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; + } +} From 7025907aef3067ecc947563decb6e54041f04469 Mon Sep 17 00:00:00 2001 From: Dylan Owen Date: Mon, 6 Jul 2026 08:09:31 -0700 Subject: [PATCH 03/12] Full rebuild with core logic abstracted into a library --- .github/workflows/ci.yml | 46 +- .github/workflows/release.yml | 16 +- .run/uplift help.run.xml | 22 + .run/uplift query.run.xml | 22 + Cargo.lock | 1614 ++++++++++++----- Cargo.toml | 24 +- crates/uplift-cli/Cargo.toml | 38 + crates/uplift-cli/Readme.md | 43 + crates/uplift-cli/src/lib.rs | 115 ++ crates/uplift-cli/src/main.rs | 244 +++ crates/uplift-lib/Cargo.toml | 46 + .../src/api/command.rs | 14 +- .../src/api/message.rs | 29 +- crates/uplift-lib/src/api/mod.rs | 84 + crates/uplift-lib/src/desk/mod.rs | 416 +++++ crates/uplift-lib/src/desk/subscription.rs | 265 +++ crates/uplift-lib/src/id.rs | 118 ++ crates/{uplift_lib => uplift-lib}/src/lib.rs | 2 + .../src/mock_btle.rs | 8 +- crates/uplift-lib/src/scanner.rs | 110 ++ crates/uplift_cli/Cargo.toml | 29 - crates/uplift_cli/src/main.rs | 289 --- crates/uplift_lib/Cargo.toml | 46 - crates/uplift_lib/src/api/mod.rs | 47 - crates/uplift_lib/src/desk.rs | 546 ------ crates/uplift_lib/src/id.rs | 242 --- rust-toolchain.toml | 2 +- 27 files changed, 2778 insertions(+), 1699 deletions(-) create mode 100644 .run/uplift help.run.xml create mode 100644 .run/uplift query.run.xml create mode 100755 crates/uplift-cli/Cargo.toml create mode 100644 crates/uplift-cli/Readme.md create mode 100644 crates/uplift-cli/src/lib.rs create mode 100644 crates/uplift-cli/src/main.rs create mode 100755 crates/uplift-lib/Cargo.toml rename crates/{uplift_lib => uplift-lib}/src/api/command.rs (92%) rename crates/{uplift_lib => uplift-lib}/src/api/message.rs (89%) create mode 100644 crates/uplift-lib/src/api/mod.rs create mode 100644 crates/uplift-lib/src/desk/mod.rs create mode 100644 crates/uplift-lib/src/desk/subscription.rs create mode 100644 crates/uplift-lib/src/id.rs rename crates/{uplift_lib => uplift-lib}/src/lib.rs (92%) rename crates/{uplift_lib => uplift-lib}/src/mock_btle.rs (94%) create mode 100644 crates/uplift-lib/src/scanner.rs delete mode 100755 crates/uplift_cli/Cargo.toml delete mode 100644 crates/uplift_cli/src/main.rs delete mode 100755 crates/uplift_lib/Cargo.toml delete mode 100644 crates/uplift_lib/src/api/mod.rs delete mode 100644 crates/uplift_lib/src/desk.rs delete mode 100644 crates/uplift_lib/src/id.rs 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..eb1abb2 --- /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..1e8b700 --- /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 5612639..d9359f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,26 +2,11 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler2" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" - [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] @@ -34,9 +19,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "anstream" -version = "0.6.18" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -49,82 +34,91 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.7" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", - "once_cell", - "windows-sys 0.59.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.4.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] -name = "backtrace" -version = "0.3.74" +name = "base64" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets", -] +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.6.0" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +dependencies = [ + "serde_core", +] [[package]] name = "block-buffer" @@ -135,6 +129,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + [[package]] name = "block2" version = "0.5.1" @@ -146,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", + "bitflags 2.13.0", "bluez-generated", "dbus", "dbus-tokio", @@ -160,34 +162,33 @@ dependencies = [ "log", "serde", "serde-xml-rs", - "thiserror 1.0.69", + "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", + "bitflags 2.13.0", "bluez-async", - "dashmap 6.1.0", + "dashmap", "dbus", "futures", "jni", - "jni-utils", "log", "objc2", "objc2-core-bluetooth", @@ -196,13 +197,20 @@ dependencies = [ "serde", "serde_bytes", "static_assertions", - "thiserror 1.0.69", + "thiserror 2.0.18", "tokio", "tokio-stream", "uuid", "windows", + "windows-future", ] +[[package]] +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" @@ -211,9 +219,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.9.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" [[package]] name = "cesu8" @@ -223,15 +231,72 @@ 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 = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core", +] + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +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.3" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "combine" @@ -252,29 +317,51 @@ 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.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" +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.2.1" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" dependencies = [ "crc-catalog", ] [[package]] name = "crc-catalog" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" [[package]] name = "crossbeam-queue" @@ -302,23 +389,28 @@ dependencies = [ ] [[package]] -name = "dashmap" -version = "5.5.3" +name = "crypto-common" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ - "cfg-if", - "hashbrown 0.14.5", - "lock_api", - "once_cell", - "parking_lot_core", + "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", @@ -330,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]] @@ -352,27 +444,75 @@ 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", - "crypto-common", + "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.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +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" @@ -381,18 +521,18 @@ checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" [[package]] name = "either" -version = "1.13.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" dependencies = [ "serde", ] [[package]] name = "env_filter" -version = "0.1.3" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217" dependencies = [ "log", "regex", @@ -400,60 +540,95 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.6" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0" +checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6" dependencies = [ "anstream", "anstyle", "env_filter", - "humantime", + "jiff", "log", ] [[package]] name = "equivalent" -version = "1.0.1" +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 = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "de48cc4d1c1d97a20fd819def54b890cadde72ed3ad0c614822a0a433361be96" +dependencies = [ + "cfg-if", + "windows-sys 0.61.2", +] [[package]] name = "event-listener" -version = "5.3.1" +version = "5.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +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.1.4" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] [[package]] name = "fragile" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" +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", @@ -466,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", @@ -476,15 +651,15 @@ 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", @@ -504,15 +679,15 @@ dependencies = [ [[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", @@ -521,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", @@ -545,25 +720,30 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] [[package]] name = "generic-array" -version = "0.14.7" +version = "0.14.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" dependencies = [ "typenum", "version_check", ] [[package]] -name = "gimli" -version = "0.31.1" +name = "getrandom" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "rand_core", +] [[package]] name = "hashbrown" @@ -573,153 +753,165 @@ checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "allocator-api2", "equivalent", "foldhash", ] +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + [[package]] name = "hashlink" -version = "0.10.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +checksum = "824e001ac4f3012dd16a264bec811403a67ca9deb6c102fc5049b32c4574b35f" dependencies = [ - "hashbrown 0.15.2", + "hashbrown 0.16.1", ] [[package]] -name = "humantime" -version = "2.1.0" +name = "heck" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] -name = "icu_collections" -version = "1.5.0" +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 = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018" dependencies = [ - "displaydoc", - "yoke", - "zerofrom", - "zerovec", + "hmac", ] [[package]] -name = "icu_locid" -version = "1.5.0" +name = "hmac" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", + "digest 0.11.3", ] [[package]] -name = "icu_locid_transform" -version = "1.5.0" +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 = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", "zerovec", ] [[package]] -name = "icu_locid_transform_data" -version = "1.5.0" +name = "icu_locale_core" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] [[package]] name = "icu_normalizer" -version = "1.5.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", "icu_provider", "smallvec", - "utf16_iter", - "utf8_iter", - "write16", "zerovec", ] [[package]] name = "icu_normalizer_data" -version = "1.5.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "1.5.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ - "displaydoc", "icu_collections", - "icu_locid_transform", + "icu_locale_core", "icu_properties_data", "icu_provider", - "tinystr", + "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "1.5.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "1.5.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", - "icu_locid", - "icu_provider_macros", - "stable_deref_trait", - "tinystr", + "icu_locale_core", "writeable", "yoke", "zerofrom", + "zerotrie", "zerovec", ] -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -728,9 +920,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.0" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", @@ -738,38 +930,69 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.7.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.15.2", + "hashbrown 0.17.1", ] [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itertools" -version = "0.10.5" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" dependencies = [ "either", ] [[package]] -name = "jni" -version = "0.19.0" +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", + "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", + "jni-sys 0.3.1", "log", "thiserror 1.0.69", "walkdir", @@ -777,86 +1000,120 @@ dependencies = [ [[package]] name = "jni-sys" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] [[package]] -name = "jni-utils" -version = "0.1.1" +name = "jni-sys" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "259e9f2c3ead61de911f147000660511f07ab00adeed1d84f5ac4d0386e7a6c4" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" dependencies = [ - "dashmap 5.5.3", - "futures", - "jni", - "log", - "once_cell", - "static_assertions", - "uuid", + "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]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" -version = "0.2.169" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +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 = "06085512b750d640299b79be4bad3d2fa90a9c00b1fd9e1b46364f66f0485c72" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" dependencies = [ "pkg-config", ] +[[package]] +name = "libsqlite3-sys" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f111c8c41e7c61a49cd34e44c7619462967221a6443b0ec299e0ac30cfb9b1" +dependencies = [ + "pkg-config", + "vcpkg", +] + [[package]] name = "litemap" -version = "0.7.4" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +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 = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] -name = "memchr" -version = "2.7.4" +name = "md-5" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" +dependencies = [ + "cfg-if", + "digest 0.11.3", +] [[package]] -name = "miniz_oxide" -version = "0.8.2" +name = "memchr" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" -dependencies = [ - "adler2", -] +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" [[package]] name = "mio" -version = "1.0.3" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" dependencies = [ "libc", "wasi", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -894,6 +1151,47 @@ 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" @@ -925,16 +1223,16 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a644b62ffb826a5277f536cf0f701493de420b13d40e700c452c36567771111" dependencies = [ - "bitflags", + "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" @@ -942,26 +1240,23 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "bitflags", + "bitflags 2.13.0", "block2", "libc", "objc2", ] [[package]] -name = "object" -version = "0.36.5" +name = "once_cell" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" -dependencies = [ - "memchr", -] +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] -name = "once_cell" -version = "1.20.2" +name = "once_cell_polyfill" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "parking" @@ -971,9 +1266,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -981,52 +1276,64 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets", + "windows-link", ] [[package]] -name = "paste" -version = "1.0.15" +name = "percent-encoding" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] -name = "percent-encoding" -version = "2.3.1" +name = "pin-project-lite" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] -name = "pin-project-lite" -version = "0.2.15" +name = "pkg-config" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] -name = "pin-utils" -version = "0.1.0" +name = "portable-atomic" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] -name = "pkg-config" -version = "0.3.31" +name = "portable-atomic-util" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +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.3" +version = "3.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" +checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" dependencies = [ "anstyle", "predicates-core", @@ -1034,15 +1341,15 @@ dependencies = [ [[package]] name = "predicates-core" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" +checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144" [[package]] name = "predicates-tree" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" +checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2" dependencies = [ "predicates-core", "termtree", @@ -1050,36 +1357,59 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.92" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.37" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +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.5.8" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.13.0", ] [[package]] name = "regex" -version = "1.11.1" +version = "1.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" dependencies = [ "aho-corasick", "memchr", @@ -1089,9 +1419,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -1100,37 +1430,34 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "rmp" -version = "0.8.14" +version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4" +checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c" dependencies = [ - "byteorder", "num-traits", - "paste", ] [[package]] name = "rmp-serde" -version = "1.3.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db" +checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155" dependencies = [ - "byteorder", "rmp", "serde", ] [[package]] -name = "rustc-demangle" -version = "0.1.24" +name = "rustversion" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "same-file" @@ -1149,93 +1476,167 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.216" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" +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 1.0.69", - "xml-rs", + "thiserror 2.0.18", + "xml", ] [[package]] name = "serde_bytes" -version = "0.11.15" +version = "0.11.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "387cc504cb06bb40a96c8e04e951fe01854cf6bc921053c954e4a606d9675c6a" +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.216" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" +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.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", ] [[package]] -name = "slab" -version = "0.4.9" +name = "sha2" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ - "autocfg", + "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.13.2" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +dependencies = [ + "serde", +] [[package]] name = "socket2" -version = "0.5.8" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", - "windows-sys 0.52.0", + "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.8.3" +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.8.3" +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", @@ -1244,26 +1645,149 @@ dependencies = [ "futures-intrusive", "futures-io", "futures-util", - "hashbrown 0.15.2", + "hashbrown 0.16.1", "hashlink", "indexmap", "log", "memchr", - "once_cell", "percent-encoding", "serde", - "sha2", + "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", - "thiserror 2.0.9", + "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.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "static_assertions" @@ -1271,11 +1795,28 @@ 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.90" +version = "2.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" dependencies = [ "proc-macro2", "quote", @@ -1284,9 +1825,9 @@ dependencies = [ [[package]] name = "synstructure" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", @@ -1310,11 +1851,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.9" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.9", + "thiserror-impl 2.0.18", ] [[package]] @@ -1330,9 +1871,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.9" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", @@ -1341,34 +1882,48 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.7.6" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "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.42.0" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ - "backtrace", "libc", "mio", "pin-project-lite", "socket2", "tokio-macros", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.4.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", @@ -1377,9 +1932,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" dependencies = [ "futures-core", "pin-project-lite", @@ -1389,9 +1944,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.13" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", @@ -1400,11 +1955,50 @@ 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.41" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", "pin-project-lite", @@ -1414,9 +2008,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.28" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", @@ -1425,45 +2019,72 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.33" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", ] [[package]] name = "typenum" -version = "1.17.0" +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 = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" -version = "1.0.14" +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 = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" [[package]] name = "uom" -version = "0.36.0" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffd36e5350a65d112584053ee91843955826bf9e56ec0d1351214e01f6d7cd9c" +checksum = "a739f83872836c82a4f2527d4e54b37007b3de68cafe7edde95fd695968bf4b9" dependencies = [ + "num-bigint", + "num-complex", + "num-rational", "num-traits", + "serde_core", "typenum", ] [[package]] name = "uplift-cli" -version = "0.0.1" +version = "0.2.0" dependencies = [ "anyhow", "btleplug", + "clap", + "confy", "env_logger", "futures", "log", - "thiserror 2.0.9", + "serde", "tokio", "uom", "uplift-lib", @@ -1492,21 +2113,16 @@ dependencies = [ [[package]] name = "url" -version = "2.5.4" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - [[package]] name = "utf8_iter" version = "1.0.4" @@ -1521,13 +2137,21 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.11.0" +version = "1.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" dependencies = [ - "serde", + "js-sys", + "serde_core", + "wasm-bindgen", ] +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" @@ -1546,68 +2170,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.9" +name = "wasm-bindgen-macro-support" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" dependencies = [ - "windows-sys 0.59.0", + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", ] [[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +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", ] [[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", + "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", @@ -1616,31 +2292,47 @@ 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", "syn", ] +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] + [[package]] name = "windows-result" -version = "0.1.2" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-targets", + "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", + "windows-link", ] [[package]] @@ -1652,6 +2344,15 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -1668,6 +2369,15 @@ dependencies = [ "windows_x86_64_msvc", ] +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -1717,30 +2427,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "write16" -version = "1.0.0" +name = "winnow" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" [[package]] name = "writeable" -version = "0.5.5" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] -name = "xml-rs" -version = "0.8.24" +name = "xml" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea8b391c9a790b496184c29f7f93b9ed5b16abb306c05415b68bcc16e4d06432" +checksum = "636f85e5ca6488e96401b61eb7de54f4e44755c988af0f52cf90230c312a1a89" [[package]] name = "yoke" -version = "0.7.5" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -1748,9 +2463,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.5" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", @@ -1760,18 +2475,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.5" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.5" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", @@ -1779,11 +2494,22 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + [[package]] name = "zerovec" -version = "0.10.4" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "yoke", "zerofrom", @@ -1792,11 +2518,17 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.10.3" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", "syn", ] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index 107f4bf..21c0ffe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,24 @@ resolver = "2" members = [ - "crates/uplift_cli", - "crates/uplift_lib" -] \ No newline at end of file + "crates/*" +] + +[workspace.dependencies] + +# Bluetooth support +btleplug = "0.12" +uuid = "1.8" + +# Async +tokio = "1.49" +tokio-stream = "0.1" +futures = "0.3.30" + +# Units +uom = "0.38" + +serde = "1.0" + +log = "0.4" +env_logger = "0.11" \ 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..bc92699 --- /dev/null +++ b/crates/uplift-cli/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "uplift-cli" +version = "0.2.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" } + +# 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..d684dae --- /dev/null +++ b/crates/uplift-cli/src/lib.rs @@ -0,0 +1,115 @@ +use clap::{Parser, Subcommand}; +use log::debug; +use serde::{Deserialize, Serialize}; +use std::any::type_name; +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}; + +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)] + 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(subcommand)] + save: Option, + }, + /// Stand or use `save` to store the current height + Stand { + #[clap(subcommand)] + save: Option, + }, + /// 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, + /// Listen for height changes + Listen, +} + +#[derive(Subcommand, Debug)] +pub enum SaveCommand { + Save, +} + +#[derive(Serialize, Deserialize)] +pub struct Config { + pub sit_height: Length, + pub stand_height: Length, +} + +impl Default for Config { + fn default() -> Self { + Self { + 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("sit_height", &format_height(self.sit_height)) + .field("stand_height", &format_height(self.stand_height)) + .finish() + } +} + +pub fn load_conf() -> Result { + let conf = confy::load(CONF_NAME, None)?; + + debug!( + "Loaded config @ {:?}: {conf:#?}", + confy::get_configuration_file_path(CONF_NAME, None)? + ); + + Ok(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..39922f7 --- /dev/null +++ b/crates/uplift-cli/src/main.rs @@ -0,0 +1,244 @@ +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::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, UpliftDeskScanner}; + +use uom::si::f32::Length; +use uom::si::length::inch; +use uplift_lib::{UpliftDesk, UpliftDeskId}; +use uuid::Uuid; + +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(adapter.clone(), args.desk.as_deref()).await?; + + desk.connect().await?; + + match args.command { + Commands::Sit { save } => { + if save.is_some() { + 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(Command::GotoSit).await?; + + // let the packet actually send + sleep(PACKET_DELAY).await; + } + } + Commands::Stand { save } => { + if save.is_some() { + desk.command(Command::SaveStand).await?; + + // let the packet actually send + sleep(PACKET_DELAY).await; + + let height = desk.height().await?; + conf.stand_height = height; + save_conf(&conf)?; + + println!("Saved standing height @ {:.1}", format_height(height)); + } else { + desk.command(Command::GotoStand).await?; + + // let the packet actually send + sleep(PACKET_DELAY).await; + } + } + Commands::ForceSit => { + force(Command::GotoSit, conf.sit_height, &mut desk).await?; + } + Commands::ForceStand => { + force(Command::GotoStand, conf.stand_height, &mut desk).await?; + } + Commands::Query => { + let height = desk.height().await?; + let rssi = desk.rssi().await?; + println!("height: {} rssi: {rssi:?}", format_height(height),); + } + Commands::Toggle => { + let height = desk.height().await?; + + let mid = conf.stand_height + conf.sit_height / 2.; + + if height > mid { + desk.command(Command::GotoSit).await?; + } else { + desk.command(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::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")); + } + } + } + } + + Ok(()) +} + +async fn force(command: Command, goal: Length, desk: &mut UpliftDesk) -> Result<(), anyhow::Error> { + 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) + ); + + println!("{}", previous_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; + } + } + } + + desk.command(Command::GotoSit).await?; + + Err(anyhow!( + "Failed to force the desk to the intended height after {attempts} attempts" + )) +} + +async fn choose_desk( + adapter: Adapter, + desk_uuid: Option<&str>, +) -> Result { + let mut scanner = UpliftDeskScanner::new(adapter).await?; + let deadline = Instant::now() + SCAN_TIMEOUT; + + if let Some(desk) = desk_uuid { + let desk_id = + UpliftDeskId::from(Uuid::parse_str(desk).with_context(|| "Invalid UUID for desk")?); + + loop { + match timeout_at(deadline, scanner.next()).await { + Ok(Some(desk)) if desk_id == desk.id() => { + return Ok(desk); + } + Ok(Some(_)) => continue, + _ => { + return Err(anyhow!("Timed out waiting for desk: {desk_id:?}")); + } + } + } + } else { + let mut desks = Vec::new(); + while let Ok(Some(desk)) = timeout_at(deadline, scanner.next()).await { + desks.push(desk) + } + desks.dedup_by_key(|d| d.id()); + + if desks.len() > 1 { + println!("{:?}", desks.iter().map(|d| d.id()).collect::>()); + Ok(desks.into_iter().next().unwrap()) + } else { + desks + .into_iter() + .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..249a77e --- /dev/null +++ b/crates/uplift-lib/Cargo.toml @@ -0,0 +1,46 @@ +[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 = "2021" + +[lib] + +[features] +serde = ["dep:serde", "btleplug/serde", "dep:rmp-serde"] +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 } + +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 similarity index 92% rename from crates/uplift_lib/src/api/command.rs rename to crates/uplift-lib/src/api/command.rs index 8f29bb6..d5fa9ef 100644 --- a/crates/uplift_lib/src/api/command.rs +++ b/crates/uplift-lib/src/api/command.rs @@ -12,10 +12,14 @@ pub enum Command { Up, Down, Stop, + SaveSit, + SaveStand, SaveMemory1, SaveMemory2, SaveMemory3, SaveMemory4, + GotoSit, + GotoStand, GotoMemory1, GotoMemory2, GotoMemory3, @@ -58,7 +62,7 @@ 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(0x2b); +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); @@ -75,12 +79,12 @@ impl Command { Command::Up => Cow::from(&UP), Command::Down => Cow::from(&DOWN), Command::Stop => Cow::from(&STOP), - Command::SaveMemory1 => Cow::from(&SAVE_MEMORY_1), - Command::SaveMemory2 => Cow::from(&SAVE_MEMORY_2), + 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::GotoMemory1 => Cow::from(&GOTO_MEMORY_1), - Command::GotoMemory2 => Cow::from(&GOTO_MEMORY_2), + 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), diff --git a/crates/uplift_lib/src/api/message.rs b/crates/uplift-lib/src/api/message.rs similarity index 89% rename from crates/uplift_lib/src/api/message.rs rename to crates/uplift-lib/src/api/message.rs index c37dee8..61ccfba 100644 --- a/crates/uplift_lib/src/api/message.rs +++ b/crates/uplift-lib/src/api/message.rs @@ -2,8 +2,8 @@ use crate::api::{deserialize_height, PAYLOAD_END}; use nom::branch::alt; use nom::bytes::complete::{tag, take}; use nom::multi::many0; +use nom::IResult; use nom::Parser; -use nom::{IResult, Needed}; use std::fmt::{Debug, Formatter}; type NomResult<'a, O> = Result>>; @@ -12,10 +12,15 @@ 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 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)] @@ -23,6 +28,7 @@ pub enum Message { Height(u16), PhysicalLimits { min: u16, max: u16 }, HeightUnit(HeightUnit), + LimitedStatus, // TODO this has more data but it's unused Unknown { code: u8, data: Vec }, } @@ -60,6 +66,7 @@ fn parse_message_data(initial_i: &[u8]) -> IResult<&[u8], Message> { HEIGHT_CODE => parse_height_message(data)?, HEIGHT_UNIT_CODE => parse_height_unit_message(data)?, PHYSICAL_LIMITS_CODE => parse_physical_limits_message(data)?, + LIMITED_STATUS_CODE => parse_limited_status_message(data)?, _ => Message::Unknown { code: cmd, data: data.into(), @@ -69,7 +76,7 @@ fn parse_message_data(initial_i: &[u8]) -> IResult<&[u8], Message> { Ok((i, message)) } -fn parse_height_message(data: &[u8]) -> NomResult { +fn parse_height_message(data: &[u8]) -> NomResult<'_, Message> { // TODO what does the bonus byte mean? It always seems to be 0x03 let (_, data) = take(2usize)(data)?; @@ -78,7 +85,7 @@ fn parse_height_message(data: &[u8]) -> NomResult { Ok(Message::Height(height)) } -fn parse_height_unit_message(data: &[u8]) -> NomResult { +fn parse_height_unit_message(data: &[u8]) -> NomResult<'_, Message> { let (_, height_unit) = alt(( tag([0x00].as_ref()).map(|_| HeightUnit::Cm), tag([0x01].as_ref()).map(|_| HeightUnit::Inch), @@ -88,15 +95,21 @@ fn parse_height_unit_message(data: &[u8]) -> NomResult { Ok(Message::HeightUnit(height_unit)) } -fn parse_physical_limits_message(data: &[u8]) -> NomResult { +fn parse_physical_limits_message(data: &[u8]) -> NomResult<'_, Message> { let (data, max) = take(2usize)(data)?; - let max = deserialize_height(&max); + let max = deserialize_height(max); let (_, min) = take(2usize)(data)?; - let min = deserialize_height(&min); + let min = deserialize_height(min); Ok(Message::PhysicalLimits { min, max }) } +fn parse_limited_status_message(data: &[u8]) -> NomResult<'_, Message> { + let (_, _status) = take(1usize)(data)?; + + Ok(Message::LimitedStatus) +} + impl Debug for Message { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { @@ -116,6 +129,10 @@ impl Debug for Message { .field(&format_hex(HEIGHT_UNIT_CODE)) .field(unit) .finish(), + Message::LimitedStatus => f + .debug_tuple("LimitedStatus") + .field(&format_hex(LIMITED_STATUS_CODE)) + .finish(), Message::Unknown { code, data } => f .debug_struct("Unknown") .field("code", &format_hex(code)) 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..4aaa13c --- /dev/null +++ b/crates/uplift-lib/src/desk/mod.rs @@ -0,0 +1,416 @@ +mod subscription; + +use crate::api::{Command, EnhancedPeripheral as _, HeightUnit}; +use crate::desk::subscription::DeskSubscription; +use crate::{DeskUuids, UpliftDeskId, DESK_UUIDS}; +use btleplug::api::{Characteristic, Peripheral, WriteType}; +use btleplug::{platform, Error, Result}; +use either::Either; +use std::collections::BTreeSet; +use std::fmt::{Debug, Formatter}; +use std::mem; +use tokio::sync::oneshot; +use tokio_stream::Stream; +use uom::si::f32::Length; + +#[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 height(&mut self) -> Result { + self.get_subscription().await?.get_height().await + } + + pub async fn height_unit(&mut self) -> Result { + Ok(self.get_subscription().await?.get_height_unit()) + } + + 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(()) + } + + 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..8eb3cca --- /dev/null +++ b/crates/uplift-lib/src/desk/subscription.rs @@ -0,0 +1,265 @@ +use crate::api::{Command, EnhancedPeripheral as _, HeightUnit, Message}; +use btleplug::api::{Characteristic, Peripheral, ValueNotification, WriteType}; +use btleplug::platform::PeripheralId; +use btleplug::Error; +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>, + height_unit_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:? F1F11F0100207E Advanced Command: 0x1F/31 data: 00 +/// t:? F1F1FE00FE7E Simple command: 0xFE/-2 +/// t:1100 F1F1FE00FE7E +impl DeskSubscription

{ + pub(super) async fn new( + id: PeripheralId, + data_in_characteristic: Characteristic, + data_out_characteristic: Characteristic, + peripheral: P, + cancel_rx: oneshot::Receiver<()>, + ) -> Self { + let (height_tx, height_rx) = watch::channel(None); + // The app seems to default to inches + let (height_unit_tx, height_unit_rx) = watch::channel(HeightUnit::Inch); + + let receiver = peripheral.notifications().await.unwrap(); + peripheral + .subscribe(&data_out_characteristic) + .await + .unwrap(); + + // 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::FetchHighLowLimit, + Command::FetchHighLowLimit, + Command::FetchHeightValue, + ] { + 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, + height_unit_tx, + cancel_rx, + data_out_characteristic, + peripheral, + ) + .await; + + if let Err(e) = &result { + log::error!("{{{id}}} - Error in our subscription task: {e:?}") + } + + result + } + }); + + Self { + height_rx, + height_unit_rx, + data_in_characteristic, + peripheral, + subscription_live, + } + } + + pub(super) fn get_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(super) fn get_height_unit(&self) -> HeightUnit { + // I don't know how to ask the desk for this info + *self.height_unit_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() + } +} + +async fn subscription_main_loop( + mut receiver: R, + height_tx: watch::Sender>, + height_unit_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{ .. } => {} + Message::HeightUnit(height_unit) => { + height_unit_tx.send(height_unit).map_err(|_| Error::RuntimeError("Couldn't send latest value".to_string()))?; + } + Message::LimitedStatus => {} + 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.clone()), + 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..7918391 --- /dev/null +++ b/crates/uplift-lib/src/id.rs @@ -0,0 +1,118 @@ +use btleplug::platform::PeripheralId; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +#[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()) + } +} + +impl From for UpliftDeskId { + fn from(value: Uuid) -> Self { + UpliftDeskId(value.into()) + } +} + +impl From for PeripheralId { + fn from(val: UpliftDeskId) -> Self { + val.0 + } +} + +#[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(test)] +mod tests { + + // #[tokio::test] + // async fn test() { + // let manager = Manager::new().await.unwrap(); + // + // let adapters = manager.adapters().await.unwrap(); + // let adapter = adapters.into_iter().next().unwrap(); + // + // let mut rx = UpliftDeskId::scan(&adapter).await; + // + // let mut i = 10; + // while let Some(result) = rx.recv().await { + // println!("{result:?}"); + // i -= 1; + // + // if i <= 0 { + // break; + // } + // } + // } +} diff --git a/crates/uplift_lib/src/lib.rs b/crates/uplift-lib/src/lib.rs similarity index 92% rename from crates/uplift_lib/src/lib.rs rename to crates/uplift-lib/src/lib.rs index d5e8530..6af039a 100644 --- a/crates/uplift_lib/src/lib.rs +++ b/crates/uplift-lib/src/lib.rs @@ -3,10 +3,12 @@ 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< diff --git a/crates/uplift_lib/src/mock_btle.rs b/crates/uplift-lib/src/mock_btle.rs similarity index 94% rename from crates/uplift_lib/src/mock_btle.rs rename to crates/uplift-lib/src/mock_btle.rs index a5afd8b..420c3e8 100644 --- a/crates/uplift_lib/src/mock_btle.rs +++ b/crates/uplift-lib/src/mock_btle.rs @@ -13,9 +13,10 @@ use btleplug::Result; use futures::stream::Stream; use std::collections::BTreeSet; use std::fmt; -use std::fmt::{Debug, Display, Formatter}; +use std::fmt::{Debug, Formatter}; use std::pin::Pin; +#[cfg(test)] mock! { pub Central {} @@ -35,6 +36,8 @@ mock! { 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; @@ -45,6 +48,7 @@ mock! { } } +#[cfg(test)] mock! { pub Peripheral {} @@ -54,6 +58,8 @@ mock! { fn address(&self) -> BDAddr; + fn mtu(&self) -> u16; + async fn properties(&self) -> Result>; fn services(&self) -> BTreeSet; diff --git a/crates/uplift-lib/src/scanner.rs b/crates/uplift-lib/src/scanner.rs new file mode 100644 index 0000000..8607357 --- /dev/null +++ b/crates/uplift-lib/src/scanner.rs @@ -0,0 +1,110 @@ +use crate::{UpliftDesk, DESK_UUIDS}; +use btleplug::api::CentralEvent::{DeviceConnected, DeviceDiscovered, DeviceUpdated}; +use btleplug::api::{Central, CentralEvent, ScanFilter}; +use futures::{ready, Stream}; +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; + 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/crates/uplift_cli/Cargo.toml b/crates/uplift_cli/Cargo.toml deleted file mode 100755 index 5cda540..0000000 --- a/crates/uplift_cli/Cargo.toml +++ /dev/null @@ -1,29 +0,0 @@ -[package] -name = "uplift-cli" -version = "0.0.1" -authors = ["Dylan Owen "] -description = "" -readme = "Readme.md" -license = "MPL-2.0" -homepage = "https://github.com/dylanowen/uplift-cli" -repository = "https://github.com/dylanowen/uplift-cli" -edition = "2021" - -[dependencies] -uplift-lib = { path = "../uplift_lib" } -log = "0.4.21" -env_logger = "0.11" - -# async -tokio = { version = "1.37", features = ["rt-multi-thread"] } -futures = "0.3.30" - -# Bluetooth support -btleplug = "0.11.5" -uuid = "1.8" - -uom = "0.36" - -# Error handling -anyhow = "1.0.82" -thiserror = "2.0" diff --git a/crates/uplift_cli/src/main.rs b/crates/uplift_cli/src/main.rs deleted file mode 100644 index 14f1c84..0000000 --- a/crates/uplift_cli/src/main.rs +++ /dev/null @@ -1,289 +0,0 @@ -use btleplug::api::Manager as _; -use btleplug::platform::Manager; -use futures::StreamExt; -use log::LevelFilter; -use std::time::Duration; -use tokio::time::sleep; -use uom::fmt::DisplayStyle; -use uom::si::length::{foot, inch}; -use uplift_lib::Command; -use uplift_lib::UpliftDesk; -use uplift_lib::UpliftDeskId; -use uuid::Uuid; - -#[tokio::main] -async fn main() -> Result<(), anyhow::Error> { - env_logger::builder() - .filter_module("uplift_lib", LevelFilter::Trace) - .init(); - - let manager = Manager::new().await.unwrap(); - - let adapters = manager.adapters().await.unwrap(); - let adapter = adapters.into_iter().next().unwrap(); - - // let id = UpliftDeskId::scan(&adapter) - // .await - // .unwrap() - // .next() - // .await - // .unwrap(); - - let id = UpliftDeskId::from(Uuid::parse_str("0ab28845-04db-6dd1-ddf7-58a1b26ccf31")?); - - let mut desk = UpliftDesk::new(id, &adapter).await.unwrap(); - // let mut desk = UpliftDesk::new(id, &adapter).await.unwrap(); - - let mut height_stream = desk.height_stream().await?; - while let Some(height) = height_stream.next().await { - desk.command(Command::Unknown { - cmd: 0xfe, - data: &[], - }) - .await - .unwrap(); - let rssi = desk.rssi(); - - println!( - "height: {} {} rssi: {rssi:?}", - height - .trunc::() - .into_format_args(foot, DisplayStyle::Abbreviation), - height - .fract::() - .into_format_args(inch, DisplayStyle::Abbreviation) - ) - } - - // let mut interval = interval(Duration::from_millis(500)); - // for _ in 0..10 { - // interval.tick().await; - // println!("name: {:?}", desk.name().await.unwrap()); - // println!("height: {:?}", desk.height().await.unwrap()); - // } - - drop(desk); - - sleep(Duration::from_secs(20)).await; - - Ok(()) -} - -// 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" -// )) -// } diff --git a/crates/uplift_lib/Cargo.toml b/crates/uplift_lib/Cargo.toml deleted file mode 100755 index 2f7b4bc..0000000 --- a/crates/uplift_lib/Cargo.toml +++ /dev/null @@ -1,46 +0,0 @@ -[package] -name = "uplift-lib" -version = "0.0.1" -authors = ["Dylan Owen "] -description = "" -readme = "Readme.md" -license = "MPL-2.0" -homepage = "https://github.com/dylanowen/uplift-cli" -repository = "https://github.com/dylanowen/uplift-cli" -edition = "2021" - -[lib] - -[features] -serde = ["dep:serde", "btleplug/serde"] -sqlx = ["dep:sqlx", "dep:rmp-serde", "serde"] - -[dependencies] -log = "0.4.21" - -# async -tokio = { version = "1.37", features = ["macros", "sync", "time"] } -tokio-stream = "0.1" -futures = "0.3.30" - -# Bluetooth support -btleplug = "0.11.5" -uuid = "1.8" - -nom = "8" - -#autoconver -#uom.version = "0.36" -#uom = { version = "0.36", default-features = false, features = ["autoconvert", "usize", "f32", "si", "std", "serde"] } -uom = { version = "0.36", default-features = false, features = ["f32", "si", "std"] } - -serde = { version = "1.0", features = ["derive"], default-features = false, optional = true } -sqlx = { version = "0.8", path = "../../../sqlx", default-features = false, optional = true } -rmp-serde = { version = "1.3", optional = true } - -either = "1.13" - -[dev-dependencies] -async-trait = "0.1" -mockall = "0.13" -tokio = { version = "1.37", features = ["rt-multi-thread"] } \ No newline at end of file diff --git a/crates/uplift_lib/src/api/mod.rs b/crates/uplift_lib/src/api/mod.rs deleted file mode 100644 index 0a35a26..0000000 --- a/crates/uplift_lib/src/api/mod.rs +++ /dev/null @@ -1,47 +0,0 @@ -mod command; -mod message; - -use btleplug::api::bleuuid; -pub use command::*; -pub use message::*; -use uuid::Uuid; - -pub const DESK_SERVICE_UUID: Uuid = bleuuid::uuid_from_u16(0xff12); - -pub const DESK_DATA_IN_UUID: Uuid = bleuuid::uuid_from_u16(0xff01); -pub const DESK_DATA_OUT_UUID: Uuid = bleuuid::uuid_from_u16(0xff02); -pub const DESK_NAME_UUID: Uuid = bleuuid::uuid_from_u16(0xff06); - -/// 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.rs b/crates/uplift_lib/src/desk.rs deleted file mode 100644 index b575da4..0000000 --- a/crates/uplift_lib/src/desk.rs +++ /dev/null @@ -1,546 +0,0 @@ -use crate::api::{ - Command, EnhancedPeripheral as _, HeightUnit, Message, DESK_DATA_IN_UUID, DESK_DATA_OUT_UUID, - DESK_NAME_UUID, DESK_SERVICE_UUID, -}; -use crate::UpliftDeskId; -use btleplug::api::{ - Central, Characteristic, Peripheral, PeripheralProperties, ValueNotification, WriteType, -}; -use btleplug::platform::PeripheralId; -use btleplug::{platform, Error, Result}; -use either::Either; -use std::collections::BTreeSet; -use std::fmt::{Debug, Formatter}; -use std::future::Future; -use std::mem; -use std::ops::Deref; -use std::time::Duration; -use tokio::sync::{oneshot, watch}; -use tokio::task::JoinHandle; -use tokio::time::interval; -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 UpliftDesk { - properties: PeripheralProperties, - peripheral: P, - data_in_characteristic: Characteristic, - data_out_characteristic: Characteristic, - name_characteristic: Characteristic, - subscription: Either, DeskSubscription

>, - cancel_subscription_tx: oneshot::Sender<()>, -} - -impl UpliftDesk

{ - pub async fn new(id: I, central: &C) -> Result - where - I: Into, - C: Central, - { - let id = id.into(); - log::debug!("{{{id}}} - Connecting to desk"); - - let peripheral = central.peripheral(&id).await?; - - peripheral.connect().await?; - peripheral.discover_services().await?; - - let properties = peripheral.properties().await?.ok_or_else(|| { - log::warn!("{{{id}}} - No properties found for our service"); - Error::DeviceNotFound - })?; - let service = peripheral.services().iter().find(|s| s.uuid == DESK_SERVICE_UUID).cloned().ok_or_else(|| { - log::warn!("{{{id}}} - Attempted to connect to desk that isn't advertising the necessary Service or Characteristics"); - Error::DeviceNotFound - })?; - - let (data_in_characteristic, data_out_characteristic, name_characteristic) = - get_characteristics(service.characteristics)?; - - let (cancel_subscription_tx, cancel_subscription_rx) = oneshot::channel(); - - Ok(Self { - properties, - peripheral, - data_in_characteristic, - data_out_characteristic, - name_characteristic, - subscription: Either::Left(cancel_subscription_rx), - cancel_subscription_tx, - }) - } - - pub fn id(&self) -> UpliftDeskId { - UpliftDeskId(self.peripheral.id()) - } - - pub fn rssi(&self) -> Option { - self.properties.rssi - } - - pub async fn name(&self) -> Result> { - Ok(self - .peripheral - .properties() - .await? - .and_then(|p| p.local_name)) - } - - pub async fn command(&self, command: Command) -> Result<()> { - self.peripheral - .command( - command, - &self.data_in_characteristic, - WriteType::WithoutResponse, - ) - .await - } - - pub async fn height(&mut self) -> Result { - self.get_subscription().await?.get_height().await - } - - pub async fn height_unit(&mut self) -> Result { - Ok(self.get_subscription().await?.get_height_unit()) - } - - pub async fn height_stream(&mut self) -> Result> { - self.get_subscription().await?.height_stream().await - } - - pub fn disconnect(self) -> UpliftDeskId { - // Drop takes care of disconnecting - self.id() - } - - async fn get_subscription(&mut self) -> Result<&DeskSubscription

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

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

{ - fn drop(&mut self) { - let cancel = mem::replace(&mut self.cancel_subscription_tx, oneshot::channel().0); - let _ = cancel.send(()); - - let peripheral = self.peripheral.clone(); - tokio::spawn(async move { - if let Err(e) = peripheral.disconnect().await { - log::warn!("{{{}}} - Error while dropping desk: {e:?}", peripheral.id()); - } - }); - } -} - -struct DeskSubscription { - height_rx: watch::Receiver>, - height_unit_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:? F1F11F0100207E Advanced Command: 0x1F/31 data: 00 -/// t:? F1F1FE00FE7E Simple command: 0xFE/-2 -/// t:1100 F1F1FE00FE7E -impl DeskSubscription

{ - async fn new( - id: PeripheralId, - data_in_characteristic: Characteristic, - data_out_characteristic: Characteristic, - peripheral: P, - cancel_rx: oneshot::Receiver<()>, - ) -> Self { - let (height_tx, height_rx) = watch::channel(None); - // The app seems to default to inches - let (height_unit_tx, height_unit_rx) = watch::channel(HeightUnit::Inch); - - let receiver = peripheral.notifications().await.unwrap(); - peripheral - .subscribe(&data_out_characteristic) - .await - .unwrap(); - - // 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::FetchHighLowLimit, - Command::FetchHighLowLimit, - Command::FetchHeightValue, - ] { - 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, - height_unit_tx, - cancel_rx, - data_out_characteristic, - peripheral, - ) - .await; - - if let Err(e) = &result { - log::error!("{{{id}}} - Error in our subscription task: {e:?}") - } - - result - } - }); - - Self { - height_rx, - height_unit_rx, - data_in_characteristic, - peripheral, - subscription_live, - } - } - - fn get_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().clone(), - )) - } - } - - fn get_height_unit(&self) -> HeightUnit { - // I don't know how to ask the desk for this info - self.height_unit_rx.borrow().clone() - } - - async fn height_stream(&self) -> Result> { - 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().clone())) - }), - ) - } - - 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 - }) - } - - fn is_live(&self) -> bool { - !self.subscription_live.is_finished() - } -} - -async fn subscription_main_loop( - mut receiver: R, - height_tx: watch::Sender>, - height_unit_tx: watch::Sender, - mut cancel_rx: oneshot::Receiver<()>, - data_out_characteristic: Characteristic, - peripheral: P, -) -> 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.len() > 0 { 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{ .. } => {} - Message::HeightUnit(height_unit) => { - height_unit_tx.send(height_unit).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 => { - break - } - } - } - - if let Err(e) = peripheral.unsubscribe(&data_out_characteristic).await { - log::warn!( - "{{{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) -> 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.clone()), - 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. -} - -fn get_characteristics( - 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_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(|| 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, Service}; - use tokio::runtime::Runtime; - use uuid::Uuid; - - #[test] - fn test_new_desk() { - let runtime = Runtime::new().unwrap(); - - runtime.block_on(async { - let central = mock_peripheral(|_| ()); - - let id: PeripheralId = Uuid::default().into(); - let desk = UpliftDesk::new(id, ¢ral).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 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_SERVICE_UUID], - ..Default::default() - })) - }); - peripheral.expect_services().returning(BTreeSet::new); - - let mut drop_peripheral = MockPeripheral::new(); - drop_peripheral - .expect_disconnect() - .once() - .returning(|| Ok(())); - peripheral - .expect_clone() - .return_once(move || drop_peripheral); - - central.expect_peripheral().return_once(|_| Ok(peripheral)); - - let id: PeripheralId = Uuid::default().into(); - assert!(matches!( - UpliftDesk::new(id, ¢ral).await.err().unwrap(), - Error::DeviceNotFound - )); - } - - fn mock_peripheral(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_SERVICE_UUID], - ..Default::default() - })) - }); - peripheral.expect_services().returning(move || { - let mut services = BTreeSet::new(); - services.insert(Service { - uuid: DESK_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_DATA_IN_UUID, DESK_DATA_OUT_UUID, DESK_NAME_UUID] { - characteristics.insert(Characteristic { - uuid, - service_uuid: DESK_SERVICE_UUID, - properties: CharPropFlags::empty(), - descriptors: BTreeSet::new(), - }); - } - - characteristics - } -} diff --git a/crates/uplift_lib/src/id.rs b/crates/uplift_lib/src/id.rs deleted file mode 100644 index 0e7edb6..0000000 --- a/crates/uplift_lib/src/id.rs +++ /dev/null @@ -1,242 +0,0 @@ -use crate::UpliftDesk; -use btleplug::api::CentralEvent::{DeviceConnected, DeviceDiscovered, DeviceUpdated}; -use btleplug::api::{Central, CentralEvent, ScanFilter}; -use btleplug::platform::PeripheralId; -use btleplug::Result; -use futures::{ready, Stream}; -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; -use std::pin::Pin; -use std::task::{Context, Poll}; -use uuid::Uuid; - -use crate::api::DESK_SERVICE_UUID; - -#[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()) - } - - pub async fn scan(central: &C) -> Result> - where - C: Central + Unpin + 'static, - { - UpliftDeskIdStream::new(central).await - } - - pub async fn connect(&self, adapter: &C) -> Result> - where - C: Central, - ::Peripheral: 'static, - { - UpliftDesk::new(self.0.clone(), adapter).await - } -} - -impl From for UpliftDeskId { - fn from(value: Uuid) -> Self { - UpliftDeskId(value.into()) - } -} - -impl From for PeripheralId { - fn from(val: UpliftDeskId) -> Self { - val.0 - } -} - -pub struct UpliftDeskIdStream -where - C: Central + Unpin + 'static, -{ - events: Pin + Send>>, - central: C, -} - -impl UpliftDeskIdStream -where - C: Central + Unpin, -{ - async fn new(central: &C) -> Result { - let central = central.clone(); - let events = central.events().await?; - - central - .start_scan(ScanFilter { - services: vec![DESK_SERVICE_UUID], - }) - .await?; - - log::trace!("Started Scanning"); - - Ok(Self { events, central }) - } -} - -impl Stream for UpliftDeskIdStream -where - C: Central + Unpin, -{ - type Item = UpliftDeskId; - - fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - let next_id = loop { - match ready!(self.events.as_mut().poll_next(cx)) { - Some(DeviceDiscovered(id) | DeviceUpdated(id) | DeviceConnected(id)) => { - break Some(UpliftDeskId::new(id)) - } - Some(event) => log::trace!("Unhandled Event: {:?}", event), - None => break None, - } - }; - - Poll::Ready(next_id) - } - - fn size_hint(&self) -> (usize, Option) { - // We don't know the lower bound - (0, self.events.size_hint().1) - } -} - -impl Drop for UpliftDeskIdStream -where - C: Central + Unpin + '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") - } - }); - } -} - -#[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}; - use std::result; - - 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::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 - // &'q [u8]: Encode<'q, DB>, - // { - // fn encode_by_ref( - // &self, - // buf: &mut ::ArgumentBuffer<'q>, - // ) -> result::Result { - // rmp_serde::to_vec(self)?.encode(buf) - // } - // } - - 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<'q>, - ) -> result::Result { - rmp_serde::to_vec(self)?.encode(buf) - } - } - - // impl<'r, DB: Database> Decode<'r, DB> for UpliftDeskId - // where - // // make sure our DB supports binary - // &'r [u8]: Decode<'r, DB>, - // { - // fn decode( - // value: ::ValueRef<'r>, - // ) -> result::Result> { - // let raw_value = <&[u8] as Decode>::decode(value)?; - // - // Ok(rmp_serde::from_slice(raw_value)?) - // } - // } -} - -#[cfg(test)] -mod tests { - - // #[tokio::test] - // async fn test() { - // let manager = Manager::new().await.unwrap(); - // - // let adapters = manager.adapters().await.unwrap(); - // let adapter = adapters.into_iter().next().unwrap(); - // - // let mut rx = UpliftDeskId::scan(&adapter).await; - // - // let mut i = 10; - // while let Some(result) = rx.recv().await { - // println!("{result:?}"); - // i -= 1; - // - // if i <= 0 { - // break; - // } - // } - // } -} diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 861bb3e..876a7b3 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "1.83" \ No newline at end of file +channel = "1.96.1" \ No newline at end of file From 6ff6545f813745c68bab1fbf0a5e4e84b0ae95da Mon Sep 17 00:00:00 2001 From: Dylan Owen Date: Mon, 6 Jul 2026 08:11:20 -0700 Subject: [PATCH 04/12] version --- crates/uplift-cli/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/uplift-cli/Cargo.toml b/crates/uplift-cli/Cargo.toml index bc92699..6d8c695 100755 --- a/crates/uplift-cli/Cargo.toml +++ b/crates/uplift-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uplift-cli" -version = "0.2.0" +version = "0.5.0" authors = ["Dylan Owen "] description = "CLI for controlling Uplift standing desks via Bluetooth" readme = "Readme.md" From c998101e720da56035b06997c17dbfa668d593c1 Mon Sep 17 00:00:00 2001 From: Dylan Owen Date: Mon, 6 Jul 2026 08:14:16 -0700 Subject: [PATCH 05/12] more --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index d9359f3..1560366 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2075,7 +2075,7 @@ dependencies = [ [[package]] name = "uplift-cli" -version = "0.2.0" +version = "0.5.0" dependencies = [ "anyhow", "btleplug", From 051342d0fa82cd7d587d59519c0624747c553bba Mon Sep 17 00:00:00 2001 From: Dylan Owen Date: Mon, 6 Jul 2026 22:34:40 -0700 Subject: [PATCH 06/12] review --- .run/uplift query.run.xml | 2 +- crates/uplift-cli/Cargo.toml | 2 +- crates/uplift-cli/src/lib.rs | 55 ++++++++------ crates/uplift-cli/src/main.rs | 86 +++++++++++++++------- crates/uplift-lib/Cargo.toml | 2 +- crates/uplift-lib/src/api/command.rs | 2 +- crates/uplift-lib/src/api/message.rs | 6 +- crates/uplift-lib/src/desk/mod.rs | 6 +- crates/uplift-lib/src/desk/subscription.rs | 17 ++--- crates/uplift-lib/src/id.rs | 5 ++ crates/uplift-lib/src/mock_btle.rs | 2 +- crates/uplift-lib/src/scanner.rs | 10 ++- 12 files changed, 125 insertions(+), 70 deletions(-) diff --git a/.run/uplift query.run.xml b/.run/uplift query.run.xml index 1e8b700..fabdb3e 100644 --- a/.run/uplift query.run.xml +++ b/.run/uplift query.run.xml @@ -1,7 +1,7 @@

{ peripheral.clone(), cancel_rx, ) - .await, + .await?, ) } diff --git a/crates/uplift-lib/src/desk/subscription.rs b/crates/uplift-lib/src/desk/subscription.rs index 8eb3cca..cfd72fa 100644 --- a/crates/uplift-lib/src/desk/subscription.rs +++ b/crates/uplift-lib/src/desk/subscription.rs @@ -1,7 +1,7 @@ use crate::api::{Command, EnhancedPeripheral as _, HeightUnit, Message}; +use btleplug::Error; use btleplug::api::{Characteristic, Peripheral, ValueNotification, WriteType}; use btleplug::platform::PeripheralId; -use btleplug::Error; use std::future::Future; use std::ops::Deref; use std::time::Duration; @@ -38,16 +38,13 @@ impl DeskSubscription

{ data_out_characteristic: Characteristic, peripheral: P, cancel_rx: oneshot::Receiver<()>, - ) -> Self { + ) -> btleplug::Result { let (height_tx, height_rx) = watch::channel(None); // The app seems to default to inches let (height_unit_tx, height_unit_rx) = watch::channel(HeightUnit::Inch); - let receiver = peripheral.notifications().await.unwrap(); - peripheral - .subscribe(&data_out_characteristic) - .await - .unwrap(); + let receiver = peripheral.notifications().await?; + peripheral.subscribe(&data_out_characteristic).await?; // spawn the initial commands tokio::spawn({ @@ -94,13 +91,13 @@ impl DeskSubscription

{ } }); - Self { + Ok(Self { height_rx, height_unit_rx, data_in_characteristic, peripheral, subscription_live, - } + }) } pub(super) fn get_height(&self) -> impl Future> + 'static { @@ -240,7 +237,7 @@ where { let maybe_result = rx.borrow_and_update().deref().clone(); match maybe_result { - Some(result) => Ok(result.clone()), + Some(result) => Ok(result), None => { on_none().await?; diff --git a/crates/uplift-lib/src/id.rs b/crates/uplift-lib/src/id.rs index 7918391..183fcc2 100644 --- a/crates/uplift-lib/src/id.rs +++ b/crates/uplift-lib/src/id.rs @@ -1,6 +1,7 @@ use btleplug::platform::PeripheralId; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; +use std::fmt::Debug; use uuid::Uuid; #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -14,6 +15,10 @@ impl UpliftDeskId { { Self(id.into()) } + + pub fn parse(id: &str) -> Result { + Ok(UpliftDeskId::from(Uuid::parse_str(id)?)) + } } impl From for UpliftDeskId { diff --git a/crates/uplift-lib/src/mock_btle.rs b/crates/uplift-lib/src/mock_btle.rs index 420c3e8..e16fb31 100644 --- a/crates/uplift-lib/src/mock_btle.rs +++ b/crates/uplift-lib/src/mock_btle.rs @@ -6,10 +6,10 @@ 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 btleplug::Result; use futures::stream::Stream; use std::collections::BTreeSet; use std::fmt; diff --git a/crates/uplift-lib/src/scanner.rs b/crates/uplift-lib/src/scanner.rs index 8607357..4fb1588 100644 --- a/crates/uplift-lib/src/scanner.rs +++ b/crates/uplift-lib/src/scanner.rs @@ -1,7 +1,8 @@ -use crate::{UpliftDesk, DESK_UUIDS}; +use crate::{DESK_UUIDS, UpliftDesk}; use btleplug::api::CentralEvent::{DeviceConnected, DeviceDiscovered, DeviceUpdated}; -use btleplug::api::{Central, CentralEvent, ScanFilter}; -use futures::{ready, Stream}; +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}; @@ -72,6 +73,9 @@ where // 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)) { From eb44456a9525ff32b909572ef9650855af9a3247 Mon Sep 17 00:00:00 2001 From: Dylan Owen Date: Tue, 7 Jul 2026 07:26:23 -0700 Subject: [PATCH 07/12] review --- crates/uplift-cli/src/main.rs | 2 +- justfile | 25 ++++++++++++------------- rust-toolchain.toml | 3 ++- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/crates/uplift-cli/src/main.rs b/crates/uplift-cli/src/main.rs index d6bd1ca..6d5c21e 100644 --- a/crates/uplift-cli/src/main.rs +++ b/crates/uplift-cli/src/main.rs @@ -270,7 +270,7 @@ async fn choose_desk( 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); + // builder.filter_module("uplift_lib", log::LevelFilter::Trace); if let Some(s) = &args.log_style { builder.parse_write_style(s); 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 index 876a7b3..f0002e2 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,3 @@ [toolchain] -channel = "1.96.1" \ No newline at end of file +channel = "1.96.1" +components = ["clippy", "rustfmt"] \ No newline at end of file From 55847e0eb74169aeb9612a82b66baaba80282f51 Mon Sep 17 00:00:00 2001 From: Dylan Owen Date: Tue, 7 Jul 2026 08:02:40 -0700 Subject: [PATCH 08/12] review --- .run/uplift help.run.xml | 40 +++++++++++----------- Cargo.lock | 1 + crates/uplift-cli/src/main.rs | 6 +++- crates/uplift-lib/Cargo.toml | 3 +- crates/uplift-lib/src/id.rs | 62 ++++++++++++++++++----------------- 5 files changed, 60 insertions(+), 52 deletions(-) diff --git a/.run/uplift help.run.xml b/.run/uplift help.run.xml index eb1abb2..1cfab15 100644 --- a/.run/uplift help.run.xml +++ b/.run/uplift help.run.xml @@ -1,22 +1,22 @@ - - + + \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 1560366..4f163fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2104,6 +2104,7 @@ dependencies = [ "nom", "rmp-serde", "serde", + "serde_json", "sqlx", "tokio", "tokio-stream", diff --git a/crates/uplift-cli/src/main.rs b/crates/uplift-cli/src/main.rs index 6d5c21e..6a320e2 100644 --- a/crates/uplift-cli/src/main.rs +++ b/crates/uplift-cli/src/main.rs @@ -100,7 +100,11 @@ async fn run_command(mut conf: Config, args: Args) -> Result<(), anyhow::Error> Commands::Query => { let height = desk.height().await?; let rssi = desk.rssi().await?; - println!("height: {}\nrssi: {rssi:?}", format_height(height),); + println!( + "desk: {:?}\nheight: {}\nrssi: {rssi:?}", + desk.id(), + format_height(height), + ); } Commands::Toggle => { let height = desk.height().await?; diff --git a/crates/uplift-lib/Cargo.toml b/crates/uplift-lib/Cargo.toml index c23691b..04b9c25 100755 --- a/crates/uplift-lib/Cargo.toml +++ b/crates/uplift-lib/Cargo.toml @@ -11,7 +11,7 @@ edition = "2024" [lib] [features] -serde = ["dep:serde", "btleplug/serde", "dep:rmp-serde"] +serde = ["dep:serde", "btleplug/serde", "dep:rmp-serde", "dep:serde_json"] sqlx = ["dep:sqlx", "dep:rmp-serde", "serde"] [dependencies] @@ -35,6 +35,7 @@ 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" diff --git a/crates/uplift-lib/src/id.rs b/crates/uplift-lib/src/id.rs index 183fcc2..5f65f0e 100644 --- a/crates/uplift-lib/src/id.rs +++ b/crates/uplift-lib/src/id.rs @@ -1,8 +1,7 @@ use btleplug::platform::PeripheralId; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; -use std::fmt::Debug; -use uuid::Uuid; +use std::fmt::{Debug, Display, Formatter}; #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Eq, Hash, Ord, PartialEq, PartialOrd, Clone, Debug)] @@ -16,14 +15,10 @@ impl UpliftDeskId { Self(id.into()) } - pub fn parse(id: &str) -> Result { - Ok(UpliftDeskId::from(Uuid::parse_str(id)?)) - } -} - -impl From for UpliftDeskId { - fn from(value: Uuid) -> Self { - UpliftDeskId(value.into()) + #[cfg(feature = "serde")] + pub fn parse(id: &str) -> Result { + serde_json::from_value::(serde_json::Value::String(id.to_string())) + .map(UpliftDeskId) } } @@ -33,6 +28,17 @@ impl From for PeripheralId { } } +#[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::*; @@ -98,26 +104,22 @@ mod sqlx_feature { } } -#[cfg(test)] +#[cfg(all(test, target_os = "macos", feature = "serde"))] mod tests { + use super::*; - // #[tokio::test] - // async fn test() { - // let manager = Manager::new().await.unwrap(); - // - // let adapters = manager.adapters().await.unwrap(); - // let adapter = adapters.into_iter().next().unwrap(); - // - // let mut rx = UpliftDeskId::scan(&adapter).await; - // - // let mut i = 10; - // while let Some(result) = rx.recv().await { - // println!("{result:?}"); - // i -= 1; - // - // if i <= 0 { - // break; - // } - // } - // } + 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); + } } From edef1500c257191cf07373b3b21803dfaed27b21 Mon Sep 17 00:00:00 2001 From: Dylan Owen Date: Tue, 7 Jul 2026 22:33:50 -0700 Subject: [PATCH 09/12] more messages --- crates/uplift-cli/src/lib.rs | 4 ++ crates/uplift-cli/src/main.rs | 39 +++++++++++++---- crates/uplift-lib/src/api/command.rs | 51 +++++++++++++++++++++- crates/uplift-lib/src/api/message.rs | 51 ++++++++++++++++------ crates/uplift-lib/src/desk/mod.rs | 10 ++++- crates/uplift-lib/src/desk/subscription.rs | 44 ++++++++++++++++--- 6 files changed, 167 insertions(+), 32 deletions(-) diff --git a/crates/uplift-cli/src/lib.rs b/crates/uplift-cli/src/lib.rs index 7ee1921..c85e310 100644 --- a/crates/uplift-cli/src/lib.rs +++ b/crates/uplift-cli/src/lib.rs @@ -55,6 +55,10 @@ pub enum Commands { 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, /// Listen for height changes Listen, /// Add a known desk to prioritize when connecting diff --git a/crates/uplift-cli/src/main.rs b/crates/uplift-cli/src/main.rs index 6a320e2..6030775 100644 --- a/crates/uplift-cli/src/main.rs +++ b/crates/uplift-cli/src/main.rs @@ -53,6 +53,24 @@ async fn run_command(mut conf: Config, args: Args) -> Result<(), anyhow::Error> desk.connect().await?; match args.command { + Commands::Query => { + let height = desk.height().await?; + let limits = desk.physical_limits().await?; + let limit_status = desk.limited_status().await?; + let rssi = desk.rssi().await?; + println!( + "desk: {:?}\n\ + height: {}\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)), + ); + } Commands::Sit { save } => { if save { desk.command(Command::SaveSit).await?; @@ -97,15 +115,6 @@ async fn run_command(mut conf: Config, args: Args) -> Result<(), anyhow::Error> Commands::ForceStand => { force(Command::GotoStand, conf.stand_height, &mut desk).await?; } - Commands::Query => { - let height = desk.height().await?; - let rssi = desk.rssi().await?; - println!( - "desk: {:?}\nheight: {}\nrssi: {rssi:?}", - desk.id(), - format_height(height), - ); - } Commands::Toggle => { let height = desk.height().await?; @@ -131,6 +140,18 @@ async fn run_command(mut conf: Config, args: Args) -> Result<(), anyhow::Error> force(Command::GotoStand, conf.stand_height, &mut desk).await?; } } + Commands::SetUpperLimit => { + desk.command(Command::SaveUpLimit).await?; + + // let the packet actually send + sleep(PACKET_DELAY).await; + } + Commands::SetLowerLimit => { + desk.command(Command::SaveDownLimit).await?; + + // let the packet actually send + sleep(PACKET_DELAY).await; + } Commands::Listen => { let mut heights = desk.height_stream().await?; loop { diff --git a/crates/uplift-lib/src/api/command.rs b/crates/uplift-lib/src/api/command.rs index fda607c..88f7ac9 100644 --- a/crates/uplift-lib/src/api/command.rs +++ b/crates/uplift-lib/src/api/command.rs @@ -26,15 +26,28 @@ pub enum Command { GotoMemory4, FetchHeightValue, FetchHeightRange, - FetchHighLowLimit, GotoHeight { height_mm: u16 }, - SetMemoryHeight { memory_location: u8, height_mm: u16 }, + LockInfo, + SetMemoryClickMode { during_move: bool }, + FetchHighLowLimit, + SaveUpLimit, + SaveDownLimit, + ClearLimit { direction: Direction }, Patch, FetchStandTime, FetchAllTime, + SetMemoryHeight { memory_location: u8, height_mm: u16 }, + HandOperate, Unknown { cmd: u8, data: &'static [u8] }, } +#[derive(Copy, Clone, Debug)] +pub enum Direction { + All, + Up, + Down, +} + pub const UP: [u8; 6] = simple_desk_command(0x01); pub const DOWN: [u8; 6] = simple_desk_command(0x02); /// Save Sit @@ -52,8 +65,32 @@ 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: Direction) -> Vec { + advanced_desk_command( + 0x23, + &[match dir { + Direction::All => 0x0, + Direction::Up => 0x1, + Direction::Down => 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); @@ -73,6 +110,8 @@ pub fn set_memory_height(memory_location: u8, height_mm: u16) -> Vec { advanced_desk_command(0xad, &[memory_location, height_bytes[0], height_bytes[1]]) } +pub const HAND_OPERATE: [u8; 6] = simple_desk_command(0xfe); + impl Command { pub(crate) fn payload(self) -> Cow<'static, [u8]> { match self { @@ -90,7 +129,14 @@ impl Command { 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), @@ -98,6 +144,7 @@ impl Command { memory_location, height_mm, } => Cow::from(set_memory_height(memory_location, height_mm)), + Command::HandOperate => Cow::from(&HAND_OPERATE), Command::Unknown { cmd, data } => Cow::from(advanced_desk_command(cmd, data)), } } diff --git a/crates/uplift-lib/src/api/message.rs b/crates/uplift-lib/src/api/message.rs index 39eb93d..48df0e6 100644 --- a/crates/uplift-lib/src/api/message.rs +++ b/crates/uplift-lib/src/api/message.rs @@ -27,8 +27,8 @@ const HEIGHT_MIN_CODE: u8 = 0x22; pub enum Message { Height(u16), PhysicalLimits { min: u16, max: u16 }, - HeightUnit(HeightUnit), - LimitedStatus, // TODO this has more data but it's unused + HeightUnit { unit: HeightUnit }, + LimitedStatus { status: LimitedDirection }, Unknown { code: u8, data: Vec }, } @@ -38,6 +38,14 @@ pub enum HeightUnit { Inch, } +#[derive(Eq, PartialEq, Clone, Copy, Debug)] +pub enum LimitedDirection { + All, + Up, + Down, + None, +} + impl Message { pub fn parse(raw_message: &[u8]) -> IResult<&[u8], Vec> { many0( @@ -64,8 +72,8 @@ fn parse_message_data(initial_i: &[u8]) -> IResult<&[u8], Message> { let message = match cmd { HEIGHT_CODE => parse_height_message(data)?, - HEIGHT_UNIT_CODE => parse_height_unit_message(data)?, PHYSICAL_LIMITS_CODE => parse_physical_limits_message(data)?, + HEIGHT_UNIT_CODE => parse_height_unit_message(data)?, LIMITED_STATUS_CODE => parse_limited_status_message(data)?, _ => Message::Unknown { code: cmd, @@ -86,13 +94,13 @@ fn parse_height_message(data: &[u8]) -> NomResult<'_, Message> { } fn parse_height_unit_message(data: &[u8]) -> NomResult<'_, Message> { - let (_, height_unit) = alt(( + let (_, unit) = alt(( tag([0x00].as_ref()).map(|_| HeightUnit::Cm), tag([0x01].as_ref()).map(|_| HeightUnit::Inch), )) .parse(data)?; - Ok(Message::HeightUnit(height_unit)) + Ok(Message::HeightUnit { unit }) } fn parse_physical_limits_message(data: &[u8]) -> NomResult<'_, Message> { @@ -105,9 +113,15 @@ fn parse_physical_limits_message(data: &[u8]) -> NomResult<'_, Message> { } fn parse_limited_status_message(data: &[u8]) -> NomResult<'_, Message> { - let (_, _status) = take(1usize)(data)?; + let (_, status) = alt(( + tag([0x00].as_ref()).map(|_| LimitedDirection::None), + tag([0x01].as_ref()).map(|_| LimitedDirection::Up), + tag([0x10].as_ref()).map(|_| LimitedDirection::Down), + tag([0x11].as_ref()).map(|_| LimitedDirection::All), + )) + .parse(data)?; - Ok(Message::LimitedStatus) + Ok(Message::LimitedStatus { status }) } impl Debug for Message { @@ -124,14 +138,15 @@ impl Debug for Message { .field(min) .field(max) .finish(), - Message::HeightUnit(unit) => f + Message::HeightUnit { unit } => f .debug_tuple("HeightUnit") .field(&format_hex(HEIGHT_UNIT_CODE)) .field(unit) .finish(), - Message::LimitedStatus => f - .debug_tuple("LimitedStatus") - .field(&format_hex(LIMITED_STATUS_CODE)) + 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") @@ -173,7 +188,12 @@ mod tests { ]; let (i, result) = Message::parse(&data).unwrap(); assert_eq!(i.len(), 0); - assert_eq!(result, vec![Message::HeightUnit(HeightUnit::Cm)]); + assert_eq!( + result, + vec![Message::HeightUnit { + unit: HeightUnit::Cm + }] + ); let data = [ 0xf2, 0xf2, @@ -185,7 +205,12 @@ mod tests { ]; let (i, result) = Message::parse(&data).unwrap(); assert_eq!(i.len(), 0); - assert_eq!(result, vec![Message::HeightUnit(HeightUnit::Inch)]); + assert_eq!( + result, + vec![Message::HeightUnit { + unit: HeightUnit::Inch + }] + ); } #[test] diff --git a/crates/uplift-lib/src/desk/mod.rs b/crates/uplift-lib/src/desk/mod.rs index f7cea91..4b07639 100644 --- a/crates/uplift-lib/src/desk/mod.rs +++ b/crates/uplift-lib/src/desk/mod.rs @@ -2,7 +2,7 @@ mod subscription; use crate::api::{Command, EnhancedPeripheral as _, HeightUnit}; use crate::desk::subscription::DeskSubscription; -use crate::{DESK_UUIDS, DeskUuids, UpliftDeskId}; +use crate::{DESK_UUIDS, DeskUuids, LimitedDirection, UpliftDeskId}; use btleplug::api::{Characteristic, Peripheral, WriteType}; use btleplug::{Error, Result, platform}; use either::Either; @@ -124,10 +124,18 @@ impl UpliftDesk

{ self.get_subscription().await?.get_height().await } + pub async fn physical_limits(&mut self) -> Result> { + Ok(self.get_subscription().await?.get_physical_limits()) + } + pub async fn height_unit(&mut self) -> Result { Ok(self.get_subscription().await?.get_height_unit()) } + pub async fn limited_status(&mut self) -> Result { + Ok(self.get_subscription().await?.get_limited_status()) + } + pub async fn height_stream(&mut self) -> Result + use

> { self.get_subscription().await?.height_stream().await } diff --git a/crates/uplift-lib/src/desk/subscription.rs b/crates/uplift-lib/src/desk/subscription.rs index cfd72fa..3dc4890 100644 --- a/crates/uplift-lib/src/desk/subscription.rs +++ b/crates/uplift-lib/src/desk/subscription.rs @@ -1,3 +1,4 @@ +use crate::LimitedDirection; use crate::api::{Command, EnhancedPeripheral as _, HeightUnit, Message}; use btleplug::Error; use btleplug::api::{Characteristic, Peripheral, ValueNotification, WriteType}; @@ -16,7 +17,9 @@ use uom::si::length::{centimeter, inch}; pub struct DeskSubscription { height_rx: watch::Receiver>, + physical_limits_rx: watch::Receiver>, height_unit_rx: watch::Receiver, + limited_status_rx: watch::Receiver, data_in_characteristic: Characteristic, peripheral: P, subscription_live: JoinHandle>, @@ -28,9 +31,9 @@ pub struct DeskSubscription { /// t:500 [Command::FetchHighLowLimit] /// t:600 [Command::FetchHeightValue] /// t:700 [Command::FetchHeightRange] -/// t:? F1F11F0100207E Advanced Command: 0x1F/31 data: 00 -/// t:? F1F1FE00FE7E Simple command: 0xFE/-2 -/// t:1100 F1F1FE00FE7E +/// t:? [Command::LockInfo] +/// t:? [Command::HandOperate] +/// t:1100 [Command::HandOperate] impl DeskSubscription

{ pub(super) async fn new( id: PeripheralId, @@ -40,8 +43,10 @@ impl DeskSubscription

{ 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 (limited_status_tx, limited_status_rx) = watch::channel(LimitedDirection::None); let receiver = peripheral.notifications().await?; peripheral.subscribe(&data_out_characteristic).await?; @@ -56,6 +61,7 @@ impl DeskSubscription

{ for cmd in [ Command::FetchHighLowLimit, Command::FetchHighLowLimit, + Command::FetchHeightRange, Command::FetchHeightValue, ] { interval.tick().await; @@ -76,7 +82,9 @@ impl DeskSubscription

{ let result = subscription_main_loop( receiver, height_tx, + physical_limits_tx, height_unit_tx, + limited_status_tx, cancel_rx, data_out_characteristic, peripheral, @@ -93,7 +101,9 @@ impl DeskSubscription

{ Ok(Self { height_rx, + physical_limits_rx, height_unit_rx, + limited_status_rx, data_in_characteristic, peripheral, subscription_live, @@ -111,11 +121,24 @@ impl DeskSubscription

{ } } + pub(super) fn get_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(super) fn get_height_unit(&self) -> HeightUnit { // I don't know how to ask the desk for this info *self.height_unit_rx.borrow() } + pub(super) fn get_limited_status(&self) -> LimitedDirection { + *self.limited_status_rx.borrow() + } + pub(super) async fn height_stream( &self, ) -> btleplug::Result + use

> { @@ -147,10 +170,13 @@ impl DeskSubscription

{ } } +#[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, + limited_status_tx: watch::Sender, mut cancel_rx: oneshot::Receiver<()>, data_out_characteristic: Characteristic, peripheral: P, @@ -178,11 +204,15 @@ where Message::Height(height) => { height_tx.send(Some(height)).map_err(|_| Error::RuntimeError("Couldn't send latest value".to_string()))?; } - Message::PhysicalLimits{ .. } => {} - Message::HeightUnit(height_unit) => { - height_unit_tx.send(height_unit).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::LimitedStatus { status } => { + limited_status_tx.send(status).map_err(|_| Error::RuntimeError("Couldn't send latest value".to_string()))?; } - Message::LimitedStatus => {} Message::Unknown{ .. } => {} } } From 92b519384dc7e3dda111eacca647ea8aacb3e4fb Mon Sep 17 00:00:00 2001 From: Dylan Owen Date: Tue, 7 Jul 2026 22:38:18 -0700 Subject: [PATCH 10/12] more-fixes --- crates/uplift-cli/src/main.rs | 12 ++++++------ crates/uplift-lib/src/api/message.rs | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/uplift-cli/src/main.rs b/crates/uplift-cli/src/main.rs index 6030775..f1578bb 100644 --- a/crates/uplift-cli/src/main.rs +++ b/crates/uplift-cli/src/main.rs @@ -59,12 +59,12 @@ async fn run_command(mut conf: Config, args: Args) -> Result<(), anyhow::Error> let limit_status = desk.limited_status().await?; let rssi = desk.rssi().await?; println!( - "desk: {:?}\n\ - height: {}\n\ - physical min limit: {:?}\n\ - physical max limit: {:?}\n\ - limited status: {limit_status:?}\n\ - rssi: {rssi:?}", + "desk: {}\n\ + height: {}\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)), diff --git a/crates/uplift-lib/src/api/message.rs b/crates/uplift-lib/src/api/message.rs index 48df0e6..f947eab 100644 --- a/crates/uplift-lib/src/api/message.rs +++ b/crates/uplift-lib/src/api/message.rs @@ -85,7 +85,7 @@ fn parse_message_data(initial_i: &[u8]) -> IResult<&[u8], Message> { } fn parse_height_message(data: &[u8]) -> NomResult<'_, Message> { - // TODO what does the bonus byte mean? It always seems to be 0x03 + // TODO what does the bonus byte mean? let (_, data) = take(2usize)(data)?; let height = deserialize_height(data); From 6e4d3af1eddbb7392337b23fe7df99ed813315fe Mon Sep 17 00:00:00 2001 From: Dylan Owen Date: Wed, 8 Jul 2026 22:33:02 -0700 Subject: [PATCH 11/12] more-changes --- crates/uplift-cli/src/lib.rs | 4 + crates/uplift-cli/src/main.rs | 103 ++++++++++++--------- crates/uplift-lib/src/api/command.rs | 22 ++--- crates/uplift-lib/src/api/message.rs | 34 ++++++- crates/uplift-lib/src/desk/mod.rs | 24 ++++- crates/uplift-lib/src/desk/subscription.rs | 29 ++++-- 6 files changed, 142 insertions(+), 74 deletions(-) diff --git a/crates/uplift-cli/src/lib.rs b/crates/uplift-cli/src/lib.rs index c85e310..4e46be3 100644 --- a/crates/uplift-cli/src/lib.rs +++ b/crates/uplift-cli/src/lib.rs @@ -59,6 +59,10 @@ pub enum Commands { 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 diff --git a/crates/uplift-cli/src/main.rs b/crates/uplift-cli/src/main.rs index f1578bb..f6ada00 100644 --- a/crates/uplift-cli/src/main.rs +++ b/crates/uplift-cli/src/main.rs @@ -8,7 +8,7 @@ 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, UpliftDeskScanner}; +use uplift_lib::{Command, LimitDirection, UpliftDeskScanner}; use uom::si::f32::Length; use uom::si::length::inch; @@ -54,22 +54,7 @@ async fn run_command(mut conf: Config, args: Args) -> Result<(), anyhow::Error> match args.command { Commands::Query => { - let height = desk.height().await?; - let limits = desk.physical_limits().await?; - let limit_status = desk.limited_status().await?; - let rssi = desk.rssi().await?; - println!( - "desk: {}\n\ - height: {}\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)), - ); + query(desk).await?; } Commands::Sit { save } => { if save { @@ -84,18 +69,12 @@ async fn run_command(mut conf: Config, args: Args) -> Result<(), anyhow::Error> println!("Saved sitting height @ {:.1}", format_height(height)); } else { - desk.command(Command::GotoSit).await?; - - // let the packet actually send - sleep(PACKET_DELAY).await; + desk.command_retry(Command::GotoSit).await?; } } Commands::Stand { save } => { if save { - desk.command(Command::SaveStand).await?; - - // let the packet actually send - sleep(PACKET_DELAY).await; + desk.command_retry(Command::SaveStand).await?; let height = desk.height().await?; conf.stand_height = height; @@ -103,10 +82,7 @@ async fn run_command(mut conf: Config, args: Args) -> Result<(), anyhow::Error> println!("Saved standing height @ {:.1}", format_height(height)); } else { - desk.command(Command::GotoStand).await?; - - // let the packet actually send - sleep(PACKET_DELAY).await; + desk.command_retry(Command::GotoStand).await?; } } Commands::ForceSit => { @@ -121,9 +97,9 @@ async fn run_command(mut conf: Config, args: Args) -> Result<(), anyhow::Error> let mid = (conf.stand_height + conf.sit_height) / 2.; if height > mid { - desk.command(Command::GotoSit).await?; + desk.command_retry(Command::GotoSit).await?; } else { - desk.command(Command::GotoStand).await?; + desk.command_retry(Command::GotoStand).await?; } // let the packet actually send @@ -141,16 +117,30 @@ async fn run_command(mut conf: Config, args: Args) -> Result<(), anyhow::Error> } } Commands::SetUpperLimit => { - desk.command(Command::SaveUpLimit).await?; + desk.command_retry(Command::SaveUpLimit).await?; - // let the packet actually send - sleep(PACKET_DELAY).await; + query(desk).await?; } Commands::SetLowerLimit => { - desk.command(Command::SaveDownLimit).await?; + desk.command_retry(Command::SaveDownLimit).await?; - // let the packet actually send - sleep(PACKET_DELAY).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?; @@ -175,7 +165,31 @@ async fn run_command(mut conf: Config, args: Args) -> Result<(), anyhow::Error> Ok(()) } -async fn force(command: Command, goal: Length, desk: &mut UpliftDesk) -> Result<(), anyhow::Error> { +async fn query(mut desk: UpliftDesk) -> anyhow::Result<()> { + let subscription = desk.get_subscription().await?; + let height = subscription.height().await?; + let limits = subscription.physical_limits(); + let touch_mode = subscription.touch_mode(); + let limit_status = subscription.limited_status(); + let rssi = desk.rssi().await?; + println!( + "desk: {}\n\ + height: {}\n\ + touch mode: {touch_mode:?}\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.); @@ -240,17 +254,14 @@ async fn choose_desk( } } } else { - // this "works" and is WAAAY faster but could just select the wrong desk - // scanner.next().await.ok_or_else(|| anyhow!("No desk found")) - let mut desks = HashMap::new(); while let Ok(Some(desk)) = timeout_at(deadline, scanner.next()).await { - desks.insert(desk.id(), desk); - } + // TODO this won't support 2 known desks close to each other + if conf.known_desks.contains(&desk.id()) { + return Ok(desk); + } - // if we're choosing across multiple desks prioritize known desks (if there are any) - if desks.len() > 1 && desks.keys().any(|id| conf.known_desks.contains(id)) { - desks.retain(|id, _| conf.known_desks.contains(id)) + desks.insert(desk.id(), desk); } if desks.len() > 1 { diff --git a/crates/uplift-lib/src/api/command.rs b/crates/uplift-lib/src/api/command.rs index 88f7ac9..fe85ee9 100644 --- a/crates/uplift-lib/src/api/command.rs +++ b/crates/uplift-lib/src/api/command.rs @@ -32,20 +32,20 @@ pub enum Command { FetchHighLowLimit, SaveUpLimit, SaveDownLimit, - ClearLimit { direction: Direction }, + ClearLimit { direction: LimitDirection }, Patch, FetchStandTime, FetchAllTime, SetMemoryHeight { memory_location: u8, height_mm: u16 }, - HandOperate, + Handshake, Unknown { cmd: u8, data: &'static [u8] }, } #[derive(Copy, Clone, Debug)] -pub enum Direction { +pub enum LimitDirection { All, - Up, - Down, + Upper, + Lower, } pub const UP: [u8; 6] = simple_desk_command(0x01); @@ -80,13 +80,13 @@ 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: Direction) -> Vec { +pub fn clear_limit(dir: LimitDirection) -> Vec { advanced_desk_command( 0x23, &[match dir { - Direction::All => 0x0, - Direction::Up => 0x1, - Direction::Down => 0x2, + LimitDirection::All => 0x0, + LimitDirection::Upper => 0x1, + LimitDirection::Lower => 0x2, }], ) } @@ -110,7 +110,7 @@ pub fn set_memory_height(memory_location: u8, height_mm: u16) -> Vec { advanced_desk_command(0xad, &[memory_location, height_bytes[0], height_bytes[1]]) } -pub const HAND_OPERATE: [u8; 6] = simple_desk_command(0xfe); +pub const HANDSHAKE: [u8; 6] = simple_desk_command(0xfe); impl Command { pub(crate) fn payload(self) -> Cow<'static, [u8]> { @@ -144,7 +144,7 @@ impl Command { memory_location, height_mm, } => Cow::from(set_memory_height(memory_location, height_mm)), - Command::HandOperate => Cow::from(&HAND_OPERATE), + Command::Handshake => Cow::from(&HANDSHAKE), Command::Unknown { cmd, data } => Cow::from(advanced_desk_command(cmd, data)), } } diff --git a/crates/uplift-lib/src/api/message.rs b/crates/uplift-lib/src/api/message.rs index f947eab..b1302c0 100644 --- a/crates/uplift-lib/src/api/message.rs +++ b/crates/uplift-lib/src/api/message.rs @@ -17,6 +17,7 @@ const ID_CODE: u8 = 0x0f; // not sure what this does but it seems to be related 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; @@ -28,6 +29,7 @@ pub enum Message { Height(u16), PhysicalLimits { min: u16, max: u16 }, HeightUnit { unit: HeightUnit }, + TouchMode { mode: TouchMode }, LimitedStatus { status: LimitedDirection }, Unknown { code: u8, data: Vec }, } @@ -38,11 +40,19 @@ pub enum HeightUnit { 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, - Up, - Down, + Upper, + Lower, None, } @@ -74,6 +84,7 @@ fn parse_message_data(initial_i: &[u8]) -> IResult<&[u8], Message> { 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, @@ -112,11 +123,21 @@ fn parse_physical_limits_message(data: &[u8]) -> NomResult<'_, Message> { 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::Up), - tag([0x10].as_ref()).map(|_| LimitedDirection::Down), + tag([0x01].as_ref()).map(|_| LimitedDirection::Upper), + tag([0x10].as_ref()).map(|_| LimitedDirection::Lower), tag([0x11].as_ref()).map(|_| LimitedDirection::All), )) .parse(data)?; @@ -143,6 +164,11 @@ impl Debug for Message { .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)) diff --git a/crates/uplift-lib/src/desk/mod.rs b/crates/uplift-lib/src/desk/mod.rs index 4b07639..ce7d327 100644 --- a/crates/uplift-lib/src/desk/mod.rs +++ b/crates/uplift-lib/src/desk/mod.rs @@ -9,10 +9,14 @@ 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 { @@ -120,20 +124,30 @@ impl UpliftDesk

{ } } + 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?.get_height().await + self.get_subscription().await?.height().await } pub async fn physical_limits(&mut self) -> Result> { - Ok(self.get_subscription().await?.get_physical_limits()) + Ok(self.get_subscription().await?.physical_limits()) } pub async fn height_unit(&mut self) -> Result { - Ok(self.get_subscription().await?.get_height_unit()) + Ok(self.get_subscription().await?.height_unit()) } pub async fn limited_status(&mut self) -> Result { - Ok(self.get_subscription().await?.get_limited_status()) + Ok(self.get_subscription().await?.limited_status()) } pub async fn height_stream(&mut self) -> Result + use

> { @@ -163,7 +177,7 @@ impl UpliftDesk

{ Ok(()) } - async fn get_subscription(&mut self) -> Result<&DeskSubscription

> { + pub async fn get_subscription(&mut self) -> Result<&DeskSubscription

> { match self { UpliftDesk::Connected { peripheral, diff --git a/crates/uplift-lib/src/desk/subscription.rs b/crates/uplift-lib/src/desk/subscription.rs index 3dc4890..6befd64 100644 --- a/crates/uplift-lib/src/desk/subscription.rs +++ b/crates/uplift-lib/src/desk/subscription.rs @@ -1,5 +1,5 @@ -use crate::LimitedDirection; 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; @@ -19,6 +19,7 @@ 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, @@ -32,8 +33,8 @@ pub struct DeskSubscription { /// t:600 [Command::FetchHeightValue] /// t:700 [Command::FetchHeightRange] /// t:? [Command::LockInfo] -/// t:? [Command::HandOperate] -/// t:1100 [Command::HandOperate] +/// t:? [Command::Handshake] +/// t:1100 [Command::Handshake] impl DeskSubscription

{ pub(super) async fn new( id: PeripheralId, @@ -46,6 +47,7 @@ impl DeskSubscription

{ 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?; @@ -59,10 +61,12 @@ impl DeskSubscription

{ // 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 @@ -84,6 +88,7 @@ impl DeskSubscription

{ height_tx, physical_limits_tx, height_unit_tx, + touch_mode_tx, limited_status_tx, cancel_rx, data_out_characteristic, @@ -103,6 +108,7 @@ impl DeskSubscription

{ height_rx, physical_limits_rx, height_unit_rx, + touch_mode_rx, limited_status_rx, data_in_characteristic, peripheral, @@ -110,7 +116,7 @@ impl DeskSubscription

{ }) } - pub(super) fn get_height(&self) -> impl Future> + 'static { + 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 { @@ -121,7 +127,7 @@ impl DeskSubscription

{ } } - pub(super) fn get_physical_limits(&self) -> Option<(Length, Length)> { + 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? @@ -130,12 +136,15 @@ impl DeskSubscription

{ )) } - pub(super) fn get_height_unit(&self) -> HeightUnit { - // I don't know how to ask the desk for this info + pub fn height_unit(&self) -> HeightUnit { *self.height_unit_rx.borrow() } - pub(super) fn get_limited_status(&self) -> LimitedDirection { + pub fn touch_mode(&self) -> TouchMode { + *self.touch_mode_rx.borrow() + } + + pub fn limited_status(&self) -> LimitedDirection { *self.limited_status_rx.borrow() } @@ -176,6 +185,7 @@ async fn subscription_main_loop( 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, @@ -210,6 +220,9 @@ where 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()))?; } From 3368f4f4a06a786da33ac36a16e863c125a8f03e Mon Sep 17 00:00:00 2001 From: Dylan Owen Date: Thu, 9 Jul 2026 08:10:34 -0700 Subject: [PATCH 12/12] more --- crates/uplift-cli/src/main.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/crates/uplift-cli/src/main.rs b/crates/uplift-cli/src/main.rs index f6ada00..3d33cbf 100644 --- a/crates/uplift-cli/src/main.rs +++ b/crates/uplift-cli/src/main.rs @@ -8,7 +8,7 @@ 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, UpliftDeskScanner}; +use uplift_lib::{Command, LimitDirection, TouchMode, UpliftDeskScanner}; use uom::si::f32::Length; use uom::si::length::inch; @@ -168,14 +168,18 @@ async fn run_command(mut conf: Config, args: Args) -> Result<(), anyhow::Error> async fn query(mut desk: UpliftDesk) -> anyhow::Result<()> { let subscription = desk.get_subscription().await?; let height = subscription.height().await?; - let limits = subscription.physical_limits(); 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:?}\n\ + touch mode: {touch_mode:?}{touch_mode_warning}\n\ physical min limit: {:?}\n\ physical max limit: {:?}\n\ limited status: {limit_status:?}\n\