From b96fd5a7ed3f5e4b63832062cc7a52790a72d2d5 Mon Sep 17 00:00:00 2001 From: Marshall Pierce <575695+marshallpierce@users.noreply.github.com> Date: Mon, 25 Mar 2024 17:08:33 -0600 Subject: [PATCH 1/2] GATT server support for Rust With a whole lot of support to get there: - Device::add_service(), with some complexity around how to represent the mutable Python `Service` being used under the covers - AttributePermission, CharacteristicProperty, and other supporting GATT-related stuff - Ability to handle the different data types used by various Python `Characteristic` subclasses - Support for running tests in the context of a background `rootcanal` process - `TryToPy` and `TryFromPy` traits to help make the lifecycle more clear for structs that have a Python equivalent, but are also meaningful standalone - Add example with a minimal battery service - Rearrange known service IDs to make it possible to refer to a service UUID in a meaningful way - Make Address hold its own state to avoid propagating PyResult so much, and adopt the new TryToPy and TryFromPy traits - `AdvertisingDataValue` trait so users don't have to remember as many ADU encoding details at a call site - Sprinkle in more wrap_python_async to work on Python 3.8 and 3.9 --- bumble/device.py | 9 +- bumble/gatt_server.py | 2 +- rust/Cargo.lock | 1440 ++++++++++++----- rust/Cargo.toml | 47 +- rust/README.md | 2 +- rust/examples/battery_client.rs | 109 -- rust/examples/battery_service.rs | 188 +++ rust/examples/broadcast.rs | 12 +- rust/examples/scanner.rs | 72 +- rust/pytests/assigned_numbers.rs | 2 +- rust/pytests/pytests.rs | 4 +- rust/pytests/rootcanal.rs | 295 ++++ rust/pytests/wrapper/att.rs | 81 + rust/pytests/wrapper/drivers.rs | 2 +- rust/pytests/wrapper/gatt.rs | 258 +++ rust/pytests/wrapper/hci.rs | 33 +- rust/pytests/wrapper/mod.rs | 23 +- rust/pytests/wrapper/transport.rs | 2 +- rust/src/cli/firmware/mod.rs | 2 +- rust/src/cli/firmware/rtk.rs | 2 +- rust/src/cli/l2cap/client_bridge.rs | 12 +- rust/src/cli/l2cap/mod.rs | 5 +- rust/src/cli/l2cap/server_bridge.rs | 2 +- rust/src/cli/mod.rs | 2 +- rust/src/cli/usb/mod.rs | 2 +- rust/src/{ => internal}/adv.rs | 42 +- rust/src/internal/att/mod.rs | 220 +++ rust/src/internal/core.rs | 195 +++ rust/src/internal/drivers/mod.rs | 2 +- rust/src/internal/drivers/rtk.rs | 2 +- rust/src/internal/gatt/mod.rs | 157 ++ rust/src/internal/gatt/server.rs | 104 ++ rust/src/internal/hci/mod.rs | 2 +- rust/src/internal/hci/packets.pdl | 4 +- rust/src/internal/hci/tests.rs | 2 +- rust/src/internal/mod.rs | 9 +- rust/src/lib.rs | 5 +- rust/src/main.rs | 2 +- .../wrapper/assigned_numbers/company_ids.rs | 2 +- rust/src/wrapper/assigned_numbers/mod.rs | 4 +- rust/src/wrapper/assigned_numbers/services.rs | 343 +++- rust/src/wrapper/att.rs | 62 + rust/src/wrapper/common.rs | 2 +- rust/src/wrapper/controller.rs | 8 +- rust/src/wrapper/core.rs | 200 +-- rust/src/wrapper/device/mod.rs | 346 +++- rust/src/wrapper/device/tests.rs | 2 +- rust/src/wrapper/drivers/mod.rs | 2 +- rust/src/wrapper/drivers/rtk.rs | 2 +- rust/src/wrapper/gatt/client.rs | 226 +++ rust/src/wrapper/gatt/mod.rs | 32 + rust/src/wrapper/gatt/profile.rs | 103 ++ rust/src/wrapper/gatt/server.rs | 136 ++ rust/src/wrapper/gatt_client.rs | 79 - rust/src/wrapper/hci.rs | 189 ++- rust/src/wrapper/host.rs | 2 +- rust/src/wrapper/l2cap.rs | 2 +- rust/src/wrapper/link.rs | 2 +- rust/src/wrapper/logging.rs | 2 +- rust/src/wrapper/mod.rs | 24 +- rust/src/wrapper/profile.rs | 44 - rust/src/wrapper/transport.rs | 2 +- rust/tools/file_header.rs | 2 +- rust/tools/gen_assigned_numbers.rs | 4 +- 64 files changed, 4043 insertions(+), 1133 deletions(-) delete mode 100644 rust/examples/battery_client.rs create mode 100644 rust/examples/battery_service.rs create mode 100644 rust/pytests/rootcanal.rs create mode 100644 rust/pytests/wrapper/att.rs create mode 100644 rust/pytests/wrapper/gatt.rs rename rust/src/{ => internal}/adv.rs (93%) create mode 100644 rust/src/internal/att/mod.rs create mode 100644 rust/src/internal/core.rs create mode 100644 rust/src/internal/gatt/mod.rs create mode 100644 rust/src/internal/gatt/server.rs create mode 100644 rust/src/wrapper/att.rs create mode 100644 rust/src/wrapper/gatt/client.rs create mode 100644 rust/src/wrapper/gatt/mod.rs create mode 100644 rust/src/wrapper/gatt/profile.rs create mode 100644 rust/src/wrapper/gatt/server.rs delete mode 100644 rust/src/wrapper/gatt_client.rs delete mode 100644 rust/src/wrapper/profile.rs diff --git a/bumble/device.py b/bumble/device.py index 10ce28ab..c8e71a7e 100644 --- a/bumble/device.py +++ b/bumble/device.py @@ -44,7 +44,7 @@ from pyee import EventEmitter from .colors import color -from .att import ATT_CID, ATT_DEFAULT_MTU, ATT_PDU +from .att import ATT_CID, ATT_DEFAULT_MTU, ATT_PDU, Attribute from .gatt import Characteristic, Descriptor, Service from .hci import ( HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_192_TYPE, @@ -3531,7 +3531,12 @@ def add_default_services(self, generic_access_service=True): async def notify_subscriber(self, connection, attribute, value=None, force=False): await self.gatt_server.notify_subscriber(connection, attribute, value, force) - async def notify_subscribers(self, attribute, value=None, force=False): + async def notify_subscribers( + self, + attribute: Attribute, + value: Optional[bytes] = None, + force: bool = False, + ) -> None: await self.gatt_server.notify_subscribers(attribute, value, force) async def indicate_subscriber(self, connection, attribute, value=None, force=False): diff --git a/bumble/gatt_server.py b/bumble/gatt_server.py index be2b88ed..5afdd999 100644 --- a/bumble/gatt_server.py +++ b/bumble/gatt_server.py @@ -491,7 +491,7 @@ async def notify_subscribers( attribute: Attribute, value: Optional[bytes] = None, force: bool = False, - ): + ) -> None: return await self.notify_or_indicate_subscribers(False, attribute, value, force) async def indicate_subscribers( diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 5305f9f6..a76388c7 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -17,20 +17,46 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + [[package]] name = "aho-corasick" -version = "1.0.5" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c378d78423fdad8089616f827526ee33c19f2fddbd5de1629152c9593ba4783" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" -version = "0.5.0" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c" +checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" dependencies = [ "anstyle", "anstyle-parse", @@ -42,43 +68,43 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.2" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15c4c2c83f81532e5845a733998b6971faca23490340a418e9b72a3ec9de12ea" +checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" [[package]] name = "anstyle-parse" -version = "0.2.1" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.0" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" dependencies = [ - "windows-sys", + "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "2.1.0" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" dependencies = [ "anstyle", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] name = "anyhow" -version = "1.0.75" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" [[package]] name = "argh" @@ -99,7 +125,7 @@ dependencies = [ "argh_shared", "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.55", ] [[package]] @@ -111,6 +137,17 @@ dependencies = [ "serde", ] +[[package]] +name = "async-trait" +version = "0.1.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507401cad91ec6a857ed5513a2073c82a9b9048762b885bb98655b306964681" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", +] + [[package]] name = "atty" version = "0.2.14" @@ -130,9 +167,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "backtrace" -version = "0.3.69" +version = "0.3.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" dependencies = [ "addr2line", "cc", @@ -145,9 +182,15 @@ dependencies = [ [[package]] name = "base64" -version = "0.21.3" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "414dcefbc63d77c526a76b3afcf6fbb9b5e2791c19c3aa2297733208750c6e53" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "bitflags" @@ -157,9 +200,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" [[package]] name = "block-buffer" @@ -172,9 +215,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.6.2" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c2f7349907b712260e64b0afe2f84692af14a454be26187d9df565c7f69266a" +checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706" dependencies = [ "memchr", "serde", @@ -185,8 +228,11 @@ name = "bumble" version = "0.2.0" dependencies = [ "anyhow", + "async-trait", "bytes", - "clap 4.4.1", + "cfg-if", + "chrono", + "clap 4.5.3", "directories", "env_logger", "file-header", @@ -211,26 +257,57 @@ dependencies = [ "tempfile", "thiserror", "tokio", + "tokio-util", + "uuid", + "zip", ] [[package]] name = "bumpalo" -version = "3.13.0" +version = "3.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" +checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" [[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.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" + +[[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.11+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +dependencies = [ + "cc", + "libc", + "pkg-config", +] [[package]] name = "cc" -version = "1.0.83" +version = "1.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" dependencies = [ + "jobserver", "libc", ] @@ -240,6 +317,36 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + +[[package]] +name = "chrono" +version = "0.4.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets 0.52.4", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clap" version = "3.2.25" @@ -249,45 +356,44 @@ dependencies = [ "atty", "bitflags 1.3.2", "clap_lex 0.2.4", - "indexmap", - "strsim", + "indexmap 1.9.3", + "strsim 0.10.0", "termcolor", "textwrap", ] [[package]] name = "clap" -version = "4.4.1" +version = "4.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c8d502cbaec4595d2e7d5f61e318f05417bd2b66fdc3809498f0d3fdf0bea27" +checksum = "949626d00e063efc93b6dca932419ceb5432f99769911c0b995f7e884c778813" dependencies = [ "clap_builder", "clap_derive", - "once_cell", ] [[package]] name = "clap_builder" -version = "4.4.1" +version = "4.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5891c7bc0edb3e1c2204fc5e94009affabeb1821c9e5fdc3959536c5c0bb984d" +checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" dependencies = [ "anstream", "anstyle", - "clap_lex 0.5.1", - "strsim", + "clap_lex 0.7.0", + "strsim 0.11.0", ] [[package]] name = "clap_derive" -version = "4.4.0" +version = "4.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9fd1a5729c4548118d7d70ff234a44868d00489a4b6597b0b020918a0e91a1a" +checksum = "90239a040c80f5e14809ca132ddc4176ab33d5e17e49691793296e3fcb34d72f" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.55", ] [[package]] @@ -301,9 +407,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.5.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" +checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" [[package]] name = "codespan-reporting" @@ -321,11 +427,17 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + [[package]] name = "core-foundation" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", @@ -333,26 +445,34 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "cpufeatures" -version = "0.2.9" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] [[package]] -name = "crossbeam" -version = "0.8.2" +name = "crc32fast" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2801af0d36612ae591caa9568261fddce32ce6e08a7275ea334a06a4ad021a2c" +checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" dependencies = [ "cfg-if", +] + +[[package]] +name = "crossbeam" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" +dependencies = [ "crossbeam-channel", "crossbeam-deque", "crossbeam-epoch", @@ -362,56 +482,46 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.8" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95" dependencies = [ - "cfg-if", "crossbeam-utils", ] [[package]] name = "crossbeam-deque" -version = "0.8.3" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" dependencies = [ - "cfg-if", "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" -version = "0.9.15" +version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ - "autocfg", - "cfg-if", "crossbeam-utils", - "memoffset 0.9.0", - "scopeguard", ] [[package]] name = "crossbeam-queue" -version = "0.3.8" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" +checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" dependencies = [ - "cfg-if", "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.16" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" -dependencies = [ - "cfg-if", -] +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" [[package]] name = "crypto-common" @@ -423,6 +533,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + [[package]] name = "digest" version = "0.10.7" @@ -431,6 +550,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -451,14 +571,14 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] name = "either" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" [[package]] name = "encoding_rs" @@ -470,44 +590,49 @@ dependencies = [ ] [[package]] -name = "env_logger" -version = "0.10.0" +name = "env_filter" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" +checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea" dependencies = [ - "humantime", - "is-terminal", "log", "regex", - "termcolor", ] [[package]] -name = "errno" -version = "0.3.3" +name = "env_logger" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd" +checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9" dependencies = [ - "errno-dragonfly", - "libc", - "windows-sys", + "anstream", + "anstyle", + "env_filter", + "humantime", + "log", ] [[package]] -name = "errno-dragonfly" -version = "0.1.2" +name = "equivalent" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ - "cc", "libc", + "windows-sys 0.52.0", ] [[package]] name = "fastrand" -version = "2.0.0" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" +checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" [[package]] name = "file-header" @@ -522,6 +647,16 @@ dependencies = [ "walkdir", ] +[[package]] +name = "flate2" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" @@ -545,18 +680,18 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] [[package]] name = "futures" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" dependencies = [ "futures-channel", "futures-core", @@ -569,9 +704,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", "futures-sink", @@ -579,15 +714,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-executor" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" dependencies = [ "futures-core", "futures-task", @@ -596,38 +731,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-macro" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.55", ] [[package]] name = "futures-sink" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-util" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-channel", "futures-core", @@ -653,9 +788,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.10" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" dependencies = [ "cfg-if", "libc", @@ -664,28 +799,28 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "globset" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "759c97c1e17c55525b57192c06a267cda0ac5210b222d6b82189a2338fa1c13d" +checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" dependencies = [ "aho-corasick", "bstr", - "fnv", "log", - "regex", + "regex-automata", + "regex-syntax", ] [[package]] name = "h2" -version = "0.3.21" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833" +checksum = "51ee2dd2e4f378392eeff5d51618cd9a63166a2513846bbc55f21cfacd9199d4" dependencies = [ "bytes", "fnv", @@ -693,7 +828,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap", + "indexmap 2.2.6", "slab", "tokio", "tokio-util", @@ -706,12 +841,24 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" + [[package]] name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -723,9 +870,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.3.2" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "hex" @@ -733,11 +880,20 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "http" -version = "0.2.9" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" dependencies = [ "bytes", "fnv", @@ -746,12 +902,24 @@ dependencies = [ [[package]] name = "http-body" -version = "0.4.5" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" dependencies = [ "bytes", "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", "pin-project-lite", ] @@ -761,12 +929,6 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - [[package]] name = "humantime" version = "2.1.0" @@ -775,46 +937,105 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.27" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +checksum = "186548d73ac615b32a73aafe38fb4f56c0d340e110e5a200bcadbaf2e199263a" dependencies = [ "bytes", "futures-channel", - "futures-core", "futures-util", "h2", "http", "http-body", "httparse", - "httpdate", "itoa", "pin-project-lite", - "socket2 0.4.9", + "smallvec", "tokio", - "tower-service", - "tracing", "want", ] +[[package]] +name = "hyper-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c" +dependencies = [ + "futures-util", + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + [[package]] name = "hyper-tls" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", + "http-body-util", "hyper", + "hyper-util", "native-tls", "tokio", "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "socket2", + "tokio", + "tower", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", ] [[package]] name = "idna" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ "unicode-bidi", "unicode-normalization", @@ -827,58 +1048,75 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +dependencies = [ + "equivalent", + "hashbrown 0.14.3", ] [[package]] name = "indoc" -version = "1.0.9" +version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306" +checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" [[package]] -name = "inventory" -version = "0.3.12" +name = "inout" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1be380c410bf0595e94992a648ea89db4dd3f3354ba54af206fd2a68cf5ac8e" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] [[package]] -name = "ipnet" -version = "2.8.0" +name = "inventory" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" +checksum = "f958d3d68f4167080a18141e10381e7634563984a537f2a49a30fd8e53ac5767" [[package]] -name = "is-terminal" -version = "0.4.9" +name = "ipnet" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" -dependencies = [ - "hermit-abi 0.3.2", - "rustix", - "windows-sys", -] +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" [[package]] name = "itertools" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" dependencies = [ "either", ] [[package]] name = "itoa" -version = "1.0.9" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" + +[[package]] +name = "jobserver" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6" +dependencies = [ + "libc", +] [[package]] name = "js-sys" -version = "0.3.64" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] @@ -891,9 +1129,20 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.147" +version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" + +[[package]] +name = "libredox" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +dependencies = [ + "bitflags 2.5.0", + "libc", + "redox_syscall", +] [[package]] name = "libusb1-sys" @@ -909,9 +1158,9 @@ dependencies = [ [[package]] name = "license" -version = "3.1.1" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66615d42e949152327c402e03cd29dab8bff91ce470381ac2ca6d380d8d9946" +checksum = "778718185117620a06e95d2b1e57d50166b1d6bfad93c8abfc1b3344c863ad8c" dependencies = [ "reword", "serde", @@ -920,15 +1169,15 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.5" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "lock_api" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" dependencies = [ "autocfg", "scopeguard", @@ -936,33 +1185,15 @@ dependencies = [ [[package]] name = "log" -version = "0.4.20" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "memchr" -version = "2.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5486aed0026218e61b8a01d5fbd5a0a134649abb71a0e53b7bc088529dced86e" - -[[package]] -name = "memoffset" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" -dependencies = [ - "autocfg", -] - -[[package]] -name = "memoffset" -version = "0.8.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1" -dependencies = [ - "autocfg", -] +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" [[package]] name = "memoffset" @@ -987,22 +1218,22 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" dependencies = [ "adler", ] [[package]] name = "mio" -version = "0.8.8" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "wasi", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1025,15 +1256,14 @@ dependencies = [ [[package]] name = "nix" -version = "0.26.4" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.5.0", "cfg-if", + "cfg_aliases", "libc", - "memoffset 0.7.1", - "pin-utils", ] [[package]] @@ -1046,38 +1276,53 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-traits" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi 0.3.2", + "hermit-abi 0.3.9", "libc", ] [[package]] name = "object" -version = "0.32.0" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ac5bbd07aea88c60a577a1ce218075ffd59208b2d7ca97adf9bfc5aeb21ebe" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "openssl" -version = "0.10.60" +version = "0.10.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79a4c6c3a2b158f7f8f2a2fc5a969fa3a068df6fc9dbb4a43845436e3af7c800" +checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.5.0", "cfg-if", "foreign-types", "libc", @@ -1094,7 +1339,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.55", ] [[package]] @@ -1105,9 +1350,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.96" +version = "0.9.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3812c071ba60da8b5677cc12bcb1d42989a65553772897a7e0355545a819838f" +checksum = "dda2b0f344e78efc2facf7d195d098df0dd72151b26ab98da807afc26c198dff" dependencies = [ "cc", "libc", @@ -1123,15 +1368,15 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "os_str_bytes" -version = "6.5.1" +version = "6.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d5d9eb14b174ee9aa2ef96dc2b94637a2d4b6e7cb873c7e171f0c20c6cf3eac" +checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" [[package]] name = "owo-colors" -version = "3.5.0" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" +checksum = "caff54706df99d2a78a5a4e3455ff45448d81ef1bb63c22cd14052ca0e993a3f" [[package]] name = "parking_lot" @@ -1139,32 +1384,55 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ - "lock_api", - "parking_lot_core", + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.48.5", +] + +[[package]] +name = "password-hash" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" +dependencies = [ + "base64ct", + "rand_core", + "subtle", ] [[package]] -name = "parking_lot_core" -version = "0.9.8" +name = "pbkdf2" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" dependencies = [ - "cfg-if", - "libc", - "redox_syscall 0.3.5", - "smallvec", - "windows-targets", + "digest", + "hmac", + "password-hash", + "sha2", ] [[package]] name = "pdl-compiler" -version = "0.2.0" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee66995739fb9ddd9155767990a54aadd226ee32408a94f99f94883ff445ceba" +checksum = "d277c9e6a1869e95522f9bd532bc225f85c03434b48cd914524235910f9ccdbe" dependencies = [ "argh", "codespan-reporting", - "heck", + "heck 0.4.1", "pest", "pest_derive", "prettyplease", @@ -1172,28 +1440,28 @@ dependencies = [ "quote", "serde", "serde_json", - "syn 2.0.29", + "syn 2.0.55", ] [[package]] name = "pdl-derive" -version = "0.2.0" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "113e4a1215c407466b36d2c2f6a6318819d6b22ccdd3acb7bb35e27a68806034" +checksum = "8510f4df2b1e42ab2f4da0d2714b1975c5e59de69bb568cb29e9844863a2f6b3" dependencies = [ "codespan-reporting", "pdl-compiler", "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.55", "termcolor", ] [[package]] name = "pdl-runtime" -version = "0.2.0" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d36c2f9799613babe78eb5cd9a353d527daaba6c3d1f39a1175657a35790732" +checksum = "ce05e0a116b0250bb41732e2858eada706aebdf311d7f832898c9256bb442abe" dependencies = [ "bytes", "thiserror", @@ -1201,15 +1469,15 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.7.4" +version = "2.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c022f1e7b65d6a24c0dbbd5fb344c66881bc01f3e5ae74a1c8100f2f985d98a4" +checksum = "56f8023d0fb78c8e03784ea1c7f3fa36e68a723138990b8d5a47d916b651e7a8" dependencies = [ "memchr", "thiserror", @@ -1218,9 +1486,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.7.4" +version = "2.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35513f630d46400a977c4cb58f78e1bfbe01434316e60c37d27b9ad6139c66d8" +checksum = "b0d24f72393fd16ab6ac5738bc33cdb6a9aa73f8b902e8fe29cf4e67d7dd1026" dependencies = [ "pest", "pest_generator", @@ -1228,28 +1496,48 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.4" +version = "2.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc9fc1b9e7057baba189b5c626e2d6f40681ae5b6eb064dc7c7834101ec8123a" +checksum = "fdc17e2a6c7d0a492f0158d7a4bd66cc17280308bbaff78d5bef566dca35ab80" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.55", ] [[package]] name = "pest_meta" -version = "2.7.4" +version = "2.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1df74e9e7ec4053ceb980e7c0c8bd3594e977fde1af91daba9c928e8e8c6708d" +checksum = "934cd7631c050f4674352a6e835d5f6711ffbfb9345c2fc0107155ac495ae293" dependencies = [ "once_cell", "pest", "sha2", ] +[[package]] +name = "pin-project" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", +] + [[package]] name = "pin-project-lite" version = "0.2.13" @@ -1264,9 +1552,21 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.27" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "portable-atomic" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" + +[[package]] +name = "powerfmt" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" @@ -1276,35 +1576,36 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "prettyplease" -version = "0.2.12" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c64d9ba0963cdcea2e1b2230fbae2bab30eb25a174be395c41e764bfb65dd62" +checksum = "8d3928fb5db768cb86f891ff014f0144589297e3c6a1aba6ed7cecfdace270c7" dependencies = [ "proc-macro2", - "syn 2.0.29", + "syn 2.0.55", ] [[package]] name = "proc-macro2" -version = "1.0.66" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" dependencies = [ "unicode-ident", ] [[package]] name = "pyo3" -version = "0.18.3" +version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3b1ac5b3731ba34fdaa9785f8d74d17448cd18f30cf19e0c7e7b1fdb5272109" +checksum = "53bdbb96d49157e65d45cc287af5f32ffadd5f4761438b527b055fb0d4bb8233" dependencies = [ "anyhow", "cfg-if", "indoc", "libc", - "memoffset 0.8.0", + "memoffset", "parking_lot", + "portable-atomic", "pyo3-build-config", "pyo3-ffi", "pyo3-macros", @@ -1313,9 +1614,9 @@ dependencies = [ [[package]] name = "pyo3-asyncio" -version = "0.18.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3564762e37035cfc486228e10b0528460fa026d681b5763873c693aa0d5c260" +checksum = "6ea6b68e93db3622f3bb3bf363246cf948ed5375afe7abff98ccbdd50b184995" dependencies = [ "clap 3.2.25", "futures", @@ -1329,9 +1630,9 @@ dependencies = [ [[package]] name = "pyo3-asyncio-macros" -version = "0.18.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be72d4cd43a27530306bd0d20d3932182fbdd072c6b98d3638bc37efb9d559dd" +checksum = "56c467178e1da6252c95c29ecf898b133f742e9181dca5def15dc24e19d45a39" dependencies = [ "proc-macro2", "quote", @@ -1340,9 +1641,9 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.18.3" +version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cb946f5ac61bb61a5014924910d936ebd2b23b705f7a4a3c40b05c720b079a3" +checksum = "deaa5745de3f5231ce10517a1f5dd97d53e5a2fd77aa6b5842292085831d48d7" dependencies = [ "once_cell", "target-lexicon", @@ -1350,9 +1651,9 @@ dependencies = [ [[package]] name = "pyo3-ffi" -version = "0.18.3" +version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd4d7c5337821916ea2a1d21d1092e8443cf34879e53a0ac653fbb98f44ff65c" +checksum = "62b42531d03e08d4ef1f6e85a2ed422eb678b8cd62b762e53891c05faf0d4afa" dependencies = [ "libc", "pyo3-build-config", @@ -1360,32 +1661,34 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.18.3" +version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d39c55dab3fc5a4b25bbd1ac10a2da452c4aca13bb450f22818a002e29648d" +checksum = "7305c720fa01b8055ec95e484a6eca7a83c841267f0dd5280f0c8b8551d2c158" dependencies = [ "proc-macro2", "pyo3-macros-backend", "quote", - "syn 1.0.109", + "syn 2.0.55", ] [[package]] name = "pyo3-macros-backend" -version = "0.18.3" +version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97daff08a4c48320587b5224cc98d609e3c27b6d437315bd40b605c98eeb5918" +checksum = "7c7e9b68bb9c3149c5b0cade5d07f953d6d125eb4337723c4ccdb665f1f96185" dependencies = [ + "heck 0.4.1", "proc-macro2", + "pyo3-build-config", "quote", - "syn 1.0.109", + "syn 2.0.55", ] [[package]] name = "quote" -version = "1.0.33" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -1422,38 +1725,29 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_syscall" -version = "0.3.5" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" dependencies = [ "bitflags 1.3.2", ] [[package]] name = "redox_users" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" dependencies = [ "getrandom", - "redox_syscall 0.2.16", + "libredox", "thiserror", ] [[package]] name = "regex" -version = "1.9.4" +version = "1.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12de2eff854e5fa4b1295edd650e227e9d8fb0c9e90b12e7f36d6a6811791a29" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" dependencies = [ "aho-corasick", "memchr", @@ -1463,9 +1757,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.7" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49530408a136e16e5b486e883fbb6ba058e8e4e8ae6621a77b048b314336e629" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" dependencies = [ "aho-corasick", "memchr", @@ -1474,26 +1768,30 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.7.5" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "reqwest" -version = "0.11.20" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1" +checksum = "2d66674f2b6fb864665eea7a3c1ac4e3dfacd2fda83cf6f935a612e01b0e3338" dependencies = [ "base64", "bytes", "encoding_rs", + "futures-channel", "futures-core", "futures-util", "h2", "http", "http-body", + "http-body-util", "hyper", + "hyper-rustls", "hyper-tls", + "hyper-util", "ipnet", "js-sys", "log", @@ -1502,16 +1800,23 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", + "rustls", + "rustls-pemfile", + "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", + "sync_wrapper", + "system-configuration", "tokio", "tokio-native-tls", + "tokio-rustls", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", + "webpki-roots", "winreg", ] @@ -1524,6 +1829,21 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rusb" version = "0.9.3" @@ -1542,15 +1862,55 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustix" -version = "0.38.10" +version = "0.38.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed6248e1caa625eb708e266e06159f135e8c26f2bb7ceb72dc4b2766d0340964" +checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.5.0", "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99008d7ad0bbbea527ec27bddbc0e432c5b87d8175178cee68d2eec9c4a1813c" +dependencies = [ + "log", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64", +] + +[[package]] +name = "rustls-pki-types" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "868e20fada228fefaf6b652e00cc73623d54f8171e7352c18bb281571f2d92da" + +[[package]] +name = "rustls-webpki" +version = "0.102.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", ] [[package]] @@ -1561,9 +1921,9 @@ checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" [[package]] name = "ryu" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" [[package]] name = "same-file" @@ -1576,11 +1936,11 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" dependencies = [ - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -1614,29 +1974,29 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.188" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.188" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.55", ] [[package]] name = "serde_json" -version = "1.0.105" +version = "1.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" +checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" dependencies = [ "itoa", "ryu", @@ -1655,6 +2015,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha2" version = "0.10.8" @@ -1686,29 +2057,25 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.11.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" -version = "0.4.9" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" dependencies = [ "libc", - "winapi", + "windows-sys 0.52.0", ] [[package]] -name = "socket2" -version = "0.5.3" +name = "spin" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" -dependencies = [ - "libc", - "windows-sys", -] +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" [[package]] name = "strsim" @@ -1716,25 +2083,37 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "strsim" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" + [[package]] name = "strum" -version = "0.25.0" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" +checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" [[package]] name = "strum_macros" -version = "0.25.2" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8d03b598d3d0fff69bf533ee3ef19b8eeb342729596df84bcc7e1f96ec4059" +checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro2", "quote", "rustversion", - "syn 2.0.29", + "syn 2.0.55", ] +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + [[package]] name = "syn" version = "1.0.109" @@ -1748,69 +2127,114 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.29" +version = "2.0.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a" +checksum = "002a1b3dbf967edfafc32655d0f377ab0bb7b994aa1d32c8cc7e9b8bf3ebb8f0" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "target-lexicon" -version = "0.12.11" +version = "0.12.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d0e916b1148c8e263850e1ebcbd046f333e0683c724876bb0da63ea4373dc8a" +checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f" [[package]] name = "tempfile" -version = "3.8.0" +version = "3.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", "fastrand", - "redox_syscall 0.3.5", "rustix", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] name = "termcolor" -version = "1.2.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" dependencies = [ "winapi-util", ] [[package]] name = "textwrap" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" +checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" [[package]] name = "thiserror" -version = "1.0.47" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97a802ec30afc17eee47b2855fc72e0c4cd62be9b4efe6591edde0ec5bd68d8f" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.47" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bb623b56e39ab7dcd4b1b98bb6c8f8d907ed255b18de254088016b27a8ee19b" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.55", ] +[[package]] +name = "time" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + [[package]] name = "tinyvec" version = "1.6.0" @@ -1828,9 +2252,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.32.0" +version = "1.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" +checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" dependencies = [ "backtrace", "bytes", @@ -1840,20 +2264,20 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.3", + "socket2", "tokio-macros", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] name = "tokio-macros" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.55", ] [[package]] @@ -1866,11 +2290,22 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" +dependencies = [ + "rustls", + "rustls-pki-types", + "tokio", +] + [[package]] name = "tokio-util" -version = "0.7.8" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" dependencies = [ "bytes", "futures-core", @@ -1880,6 +2315,28 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + [[package]] name = "tower-service" version = "0.3.2" @@ -1888,29 +2345,29 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.37" +version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ - "cfg-if", + "log", "pin-project-lite", "tracing-core", ] [[package]] name = "tracing-core" -version = "0.1.31" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", ] [[package]] name = "try-lock" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typenum" @@ -1926,30 +2383,30 @@ checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" [[package]] name = "unicode-bidi" -version = "0.3.13" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" [[package]] name = "unicode-ident" -version = "1.0.11" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" dependencies = [ "tinyvec", ] [[package]] name = "unicode-segmentation" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" [[package]] name = "unicode-width" @@ -1959,15 +2416,21 @@ checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" [[package]] name = "unindent" -version = "0.1.11" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1766d682d402817b5ac4490b3c3002d91dfa0d22812f341609f97b08757359c" +checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", "idna", @@ -1980,6 +2443,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "uuid" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" + [[package]] name = "vcpkg" version = "0.2.15" @@ -1994,9 +2463,9 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[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", @@ -2019,9 +2488,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.87" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -2029,24 +2498,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.87" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.55", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.37" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" +checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" dependencies = [ "cfg-if", "js-sys", @@ -2056,9 +2525,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.87" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2066,33 +2535,42 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.87" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.55", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.87" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "web-sys" -version = "0.3.64" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" dependencies = [ "js-sys", "wasm-bindgen", ] +[[package]] +name = "webpki-roots" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3de34ae270483955a94f4b21bdaaeb83d508bb84a01435f393818edb0012009" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "winapi" version = "0.3.9" @@ -2111,9 +2589,9 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" dependencies = [ "winapi", ] @@ -2124,13 +2602,31 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.4", +] + [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.4", ] [[package]] @@ -2139,13 +2635,28 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +dependencies = [ + "windows_aarch64_gnullvm 0.52.4", + "windows_aarch64_msvc 0.52.4", + "windows_i686_gnu 0.52.4", + "windows_i686_msvc 0.52.4", + "windows_x86_64_gnu 0.52.4", + "windows_x86_64_gnullvm 0.52.4", + "windows_x86_64_msvc 0.52.4", ] [[package]] @@ -2154,42 +2665,84 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" + [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" + [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" + [[package]] name = "winreg" version = "0.50.0" @@ -2197,5 +2750,60 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ "cfg-if", - "windows-sys", + "windows-sys 0.48.0", +] + +[[package]] +name = "zeroize" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" + +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "aes", + "byteorder", + "bzip2", + "constant_time_eq", + "crc32fast", + "crossbeam-utils", + "flate2", + "hmac", + "pbkdf2", + "sha1", + "time", + "zstd", +] + +[[package]] +name = "zstd" +version = "0.11.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "5.0.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.9+zstd.1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e16efa8a874a0481a574084d34cc26fdb3b99627480f785888deb6386506656" +dependencies = [ + "cc", + "pkg-config", ] diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 73c9ac35..bf16ce62 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -17,51 +17,57 @@ rust-version = "1.76.0" # We are interested in testing subset combinations of this feature, so this is redundant denylist = ["unstable"] # To exercise combinations of any of these features, remove from `always_include_features` -always_include_features = ["anyhow", "pyo3-asyncio-attributes", "dev-tools", "bumble-tools"] +always_include_features = ["pyo3-asyncio-attributes", "dev-tools", "bumble-tools"] [dependencies] -pyo3 = { version = "0.18.3", features = ["macros"] } -pyo3-asyncio = { version = "0.18.0", features = ["tokio-runtime"] } +pyo3 = { version = "0.20.2", features = ["macros", "anyhow"] } +pyo3-asyncio = { version = "0.20.0", features = ["tokio-runtime"] } tokio = { version = "1.28.2", features = ["macros", "signal"] } nom = "7.1.3" -strum = "0.25.0" -strum_macros = "0.25.0" +strum = "0.26.1" +strum_macros = "0.26.1" hex = "0.4.3" -itertools = "0.11.0" +itertools = "0.12.1" lazy_static = "1.4.0" thiserror = "1.0.41" bytes = "1.5.0" pdl-derive = "0.2.0" pdl-runtime = "0.2.0" futures = "0.3.28" +async-trait = "0.1.77" +uuid = "1.8.0" # Dev tools file-header = { version = "0.1.2", optional = true } globset = { version = "0.4.13", optional = true } # CLI -anyhow = { version = "1.0.71", optional = true } +anyhow = "1.0.71" clap = { version = "4.3.3", features = ["derive"], optional = true } directories = { version = "5.0.1", optional = true } -env_logger = { version = "0.10.0", optional = true } -log = { version = "0.4.19", optional = true } -owo-colors = { version = "3.5.0", optional = true } -reqwest = { version = "0.11.20", features = ["blocking"], optional = true } +env_logger = { version = "0.11.2", optional = true } +log = { version = "0.4.19" } +owo-colors = { version = "4.0.0", optional = true } +reqwest = { version = "0.12.2", features = ["blocking"], optional = true } rusb = { version = "0.9.2", optional = true } [dev-dependencies] tokio = { version = "1.28.2", features = ["full"] } +tokio-util = "0.7.10" tempfile = "3.6.0" -nix = "0.26.2" -anyhow = "1.0.71" -pyo3 = { version = "0.18.3", features = ["macros", "anyhow"] } -pyo3-asyncio = { version = "0.18.0", features = ["tokio-runtime", "attributes", "testing"] } +nix = { version = "0.28.0", features = ["fs"] } +pyo3-asyncio = { version = "0.20.0", features = ["tokio-runtime", "attributes", "testing"] } rusb = "0.9.2" rand = "0.8.5" clap = { version = "4.3.3", features = ["derive"] } -owo-colors = "3.5.0" +owo-colors = "4.0.0" log = "0.4.19" -env_logger = "0.10.0" +env_logger = "0.11.2" +chrono = "0.4.34" +directories = "5.0.1" +reqwest = { version = "0.12.2", features = ["rustls-tls"] } +zip = "0.6.6" +cfg-if = "1.0.0" [package.metadata.docs.rs] rustdoc-args = ["--generate-link-to-definition"] @@ -93,11 +99,10 @@ path = "pytests/pytests.rs" harness = false [features] -anyhow = ["pyo3/anyhow"] pyo3-asyncio-attributes = ["pyo3-asyncio/attributes"] -dev-tools = ["dep:anyhow", "dep:clap", "dep:file-header", "dep:globset"] -# separate feature for CLI so that dependencies don't spend time building these -bumble-tools = ["dep:clap", "anyhow", "dep:anyhow", "dep:directories", "pyo3-asyncio-attributes", "dep:owo-colors", "dep:reqwest", "dep:rusb", "dep:log", "dep:env_logger"] +dev-tools = ["dep:clap", "dep:file-header", "dep:globset"] +# separate feature for CLI so that reverse dependencies don't spend time building these +bumble-tools = ["dep:clap", "dep:directories", "pyo3-asyncio-attributes", "dep:owo-colors", "dep:reqwest", "dep:rusb", "dep:env_logger"] # all the unstable features unstable = ["unstable_extended_adv"] diff --git a/rust/README.md b/rust/README.md index e08ef252..324a8b6c 100644 --- a/rust/README.md +++ b/rust/README.md @@ -14,7 +14,7 @@ Set up a virtualenv for Bumble, or otherwise have an isolated Python environment for Bumble and its dependencies. Due to Python being -[picky about how its sys path is set up](https://github.com/PyO3/pyo3/issues/1741, +[picky about how its sys path is set up](https://github.com/PyO3/pyo3/issues/1741), it's necessary to explicitly point to the virtualenv's `site-packages`. Use suitable virtualenv paths as appropriate for your OS, as seen here running the `battery_client` example: diff --git a/rust/examples/battery_client.rs b/rust/examples/battery_client.rs deleted file mode 100644 index 613d9e8b..00000000 --- a/rust/examples/battery_client.rs +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright 2023 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Counterpart to the Python example `battery_server.py`. -//! -//! Start an Android emulator from Android Studio, or otherwise have netsim running. -//! -//! Run the server from the project root: -//! ``` -//! PYTHONPATH=. python examples/battery_server.py \ -//! examples/device1.json android-netsim -//! ``` -//! -//! Then run this example from the `rust` directory: -//! -//! ``` -//! PYTHONPATH=..:/path/to/virtualenv/site-packages/ \ -//! cargo run --example battery_client -- \ -//! --transport android-netsim \ -//! --target-addr F0:F1:F2:F3:F4:F5 -//! ``` - -use bumble::wrapper::{ - device::{Device, Peer}, - hci::{packets::AddressType, Address}, - profile::BatteryServiceProxy, - transport::Transport, - PyObjectExt, -}; -use clap::Parser as _; -use log::info; -use owo_colors::OwoColorize; -use pyo3::prelude::*; - -#[pyo3_asyncio::tokio::main] -async fn main() -> PyResult<()> { - env_logger::builder() - .filter_level(log::LevelFilter::Info) - .init(); - - let cli = Cli::parse(); - - let transport = Transport::open(cli.transport).await?; - - let address = Address::new("F0:F1:F2:F3:F4:F5", AddressType::RandomDeviceAddress)?; - let device = Device::with_hci("Bumble", address, transport.source()?, transport.sink()?)?; - - device.power_on().await?; - - let conn = device.connect(&cli.target_addr).await?; - let mut peer = Peer::new(conn)?; - for mut s in peer.discover_services().await? { - s.discover_characteristics().await?; - } - let battery_service = peer - .create_service_proxy::()? - .ok_or(anyhow::anyhow!("No battery service found"))?; - - let mut battery_level_char = battery_service - .battery_level()? - .ok_or(anyhow::anyhow!("No battery level characteristic"))?; - info!( - "{} {}", - "Initial Battery Level:".green(), - battery_level_char - .read_value() - .await? - .extract_with_gil::()? - ); - battery_level_char - .subscribe(|_py, args| { - info!( - "{} {:?}", - "Battery level update:".green(), - args.get_item(0)?.extract::()?, - ); - Ok(()) - }) - .await?; - - // wait until user kills the process - tokio::signal::ctrl_c().await?; - Ok(()) -} - -#[derive(clap::Parser)] -#[command(author, version, about, long_about = None)] -struct Cli { - /// Bumble transport spec. - /// - /// - #[arg(long)] - transport: String, - - /// Address to connect to - #[arg(long)] - target_addr: String, -} diff --git a/rust/examples/battery_service.rs b/rust/examples/battery_service.rs new file mode 100644 index 00000000..9c801ce9 --- /dev/null +++ b/rust/examples/battery_service.rs @@ -0,0 +1,188 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Counterpart to the Python examples `battery_client` and `battery_server.py`. +//! +//! Start an Android emulator from Android Studio, or otherwise have netsim running. +//! +//! Run the server from the project root: +//! ``` +//! PYTHONPATH=. python examples/battery_server.py \ +//! examples/device1.json android-netsim +//! ``` +//! +//! Then run this example from the `rust` directory: +//! +//! ``` +//! PYTHONPATH=..:/path/to/virtualenv/site-packages/ \ +//! cargo run --example battery_client -- \ +//! --transport android-netsim \ +//! client \ +//! --target-addr F0:F1:F2:F3:F4:F5 +//! ``` + +use anyhow::anyhow; +use async_trait::async_trait; +use bumble::wrapper::{ + assigned_numbers::services, + att::{AttributePermission, AttributeRead, NoOpWrite}, + core::{AdvertisementDataBuilder, CommonDataType, Uuid16}, + device::{Connection, Device, Peer}, + gatt::{ + profile::proxy::BatteryServiceProxy, + server::{Characteristic, CharacteristicValueHandler, Service}, + CharacteristicProperty, + }, + hci::{packets::AddressType, Address}, + transport::Transport, +}; +use clap::Parser as _; +use log::info; +use owo_colors::OwoColorize; +use pyo3::prelude::*; +use rand::Rng; +use std::time::Duration; + +#[pyo3_asyncio::tokio::main] +async fn main() -> PyResult<()> { + env_logger::builder() + .filter_level(log::LevelFilter::Info) + .init(); + + let cli = Cli::parse(); + + let transport = Transport::open(cli.transport).await?; + let address = Address::from_be_hex("F0:F1:F2:F3:F4:F5", AddressType::RandomDeviceAddress) + .map_err(|e| anyhow!(e))?; + let device = + Device::with_hci("Bumble", address, transport.source()?, transport.sink()?).await?; + device.power_on().await?; + + match cli.subcommand { + Subcommand::Client { target_addr } => { + let target = Address::from_be_hex(&target_addr, AddressType::RandomDeviceAddress) + .map_err(|e| anyhow!("{:?}", e))?; + run_client(device, target).await + } + Subcommand::Server => run_server(device).await, + }?; + + // wait until user kills the process + tokio::signal::ctrl_c().await?; + Ok(()) +} + +async fn run_client(device: Device, target_addr: Address) -> anyhow::Result<()> { + info!("Connecting"); + let conn = device.connect(&target_addr).await?; + let mut peer = Peer::new(conn).await?; + info!("Discovering"); + peer.discover_services().await?; + peer.discover_characteristics().await?; + + let battery_service = peer + .create_service_proxy::()? + .ok_or(anyhow::anyhow!("No battery service found"))?; + + info!("Getting characteristic"); + let mut battery_level_char = battery_service + .battery_level()? + .ok_or(anyhow::anyhow!("No battery level characteristic"))?; + info!("Reading"); + info!( + "{} {}", + "Initial Battery Level:".green(), + battery_level_char.read_value().await? + ); + info!("Subscribing"); + battery_level_char + .subscribe(|value| { + info!("{} {:?}", "Battery level update:".green(), value); + }) + .await?; + + Ok(()) +} + +async fn run_server(mut device: Device) -> anyhow::Result<()> { + let uuid = services::BATTERY.uuid(); + let battery_level_uuid = Uuid16::from(0x2A19).into(); + let battery_level = Characteristic::new( + battery_level_uuid, + CharacteristicProperty::Read | CharacteristicProperty::Notify, + AttributePermission::Readable.into(), + CharacteristicValueHandler::new(Box::new(BatteryRead), Box::new(NoOpWrite)), + ); + let service = Service::new(uuid.into(), vec![battery_level]); + let service_handle = device.add_service(&service)?; + + let mut builder = AdvertisementDataBuilder::new(); + builder.append(CommonDataType::CompleteLocalName, "Bumble Battery")?; + builder.append( + CommonDataType::IncompleteListOf16BitServiceClassUuids, + &uuid, + )?; + builder.append( + CommonDataType::Appearance, + // computer (0x02) - laptop (0x03) + [0x02, 0x03].as_slice(), + )?; + device.set_advertising_data(builder)?; + device.power_on().await?; + device.start_advertising(true).await?; + + let char_handle = service_handle + .characteristic_handle(battery_level_uuid) + .expect("Battery level should be present"); + + loop { + tokio::time::sleep(Duration::from_secs(3)).await; + device.notify_subscribers(char_handle).await?; + } +} + +struct BatteryRead; + +#[async_trait] +impl AttributeRead for BatteryRead { + async fn read(&self, conn: Connection) -> anyhow::Result> { + info!("Client at {:?} reading battery level", conn.peer_address()?); + Ok(vec![rand::thread_rng().gen_range(0..=100)]) + } +} + +#[derive(clap::Parser)] +#[command(author, version, about, long_about = None)] +struct Cli { + /// Bumble transport spec. + /// + /// + #[arg(long)] + transport: String, + + #[clap(subcommand)] + subcommand: Subcommand, +} + +#[derive(clap::Subcommand, Debug, Clone)] +enum Subcommand { + /// Battery service GATT client + Client { + /// Address to connect to + #[arg(long)] + target_addr: String, + }, + /// Battery service GATT server + Server, +} diff --git a/rust/examples/broadcast.rs b/rust/examples/broadcast.rs index affe21e4..9e093a18 100644 --- a/rust/examples/broadcast.rs +++ b/rust/examples/broadcast.rs @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -44,22 +44,20 @@ async fn main() -> PyResult<()> { &cli.device_config, transport.source()?, transport.sink()?, - )?; + ) + .await?; let mut adv_data = AdvertisementDataBuilder::new(); adv_data - .append( - CommonDataType::CompleteLocalName, - "Bumble from Rust".as_bytes(), - ) + .append(CommonDataType::CompleteLocalName, "Bumble from Rust") .map_err(|e| anyhow!(e))?; // Randomized TX power adv_data .append( CommonDataType::TxPowerLevel, - &[rand::thread_rng().gen_range(-100_i8..=20) as u8], + [rand::thread_rng().gen_range(-100_i8..=20) as u8].as_slice(), ) .map_err(|e| anyhow!(e))?; diff --git a/rust/examples/scanner.rs b/rust/examples/scanner.rs index 0880e257..125e5686 100644 --- a/rust/examples/scanner.rs +++ b/rust/examples/scanner.rs @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -17,17 +17,16 @@ //! Device deduplication is done here rather than relying on the controller's filtering to provide //! for additional features, like the ability to make deduplication time-bounded. -use bumble::{ - adv::CommonDataType, - wrapper::{ - core::AdvertisementDataUnit, - device::Device, - hci::{packets::AddressType, Address}, - transport::Transport, - }, +use anyhow::anyhow; +use bumble::wrapper::{ + core::{AdvertisementDataUnit, CommonDataType}, + device::Device, + hci::{packets::AddressType, Address}, + transport::Transport, }; use clap::Parser as _; use itertools::Itertools; +use owo_colors::colors::css; use owo_colors::{OwoColorize, Style}; use pyo3::PyResult; use std::{ @@ -46,37 +45,37 @@ async fn main() -> PyResult<()> { let transport = Transport::open(cli.transport).await?; - let address = Address::new("F0:F1:F2:F3:F4:F5", AddressType::RandomDeviceAddress)?; - let mut device = Device::with_hci("Bumble", address, transport.source()?, transport.sink()?)?; + let address = Address::from_be_hex("F0:F1:F2:F3:F4:F5", AddressType::RandomDeviceAddress) + .map_err(|e| anyhow!(e))?; + let mut device = + Device::with_hci("Bumble", address, transport.source()?, transport.sink()?).await?; // in practice, devices can send multiple advertisements from the same address, so we keep // track of a timestamp for each set of data let seen_advertisements = Arc::new(Mutex::new(collections::HashMap::< - Vec, + Address, collections::HashMap, time::Instant>, >::new())); let seen_adv_clone = seen_advertisements.clone(); device.on_advertisement(move |_py, adv| { - let rssi = adv.rssi()?; - let data_units = adv.data()?.data_units()?; - let addr = adv.address()?; + let rssi = adv.rssi(); + let data_units = adv.data().data_units(); + let addr = adv.address(); let show_adv = if cli.filter_duplicates { - let addr_bytes = addr.as_le_bytes()?; - let mut seen_adv_cache = seen_adv_clone.lock().unwrap(); let expiry_duration = time::Duration::from_secs(cli.dedup_expiry_secs); - let advs_from_addr = seen_adv_cache.entry(addr_bytes).or_default(); + let advs_from_addr = seen_adv_cache.entry(addr).or_default(); // we expect cache hits to be the norm, so we do a separate lookup to avoid cloning // on every lookup with entry() - let show = if let Some(prev) = advs_from_addr.get_mut(&data_units) { + let show = if let Some(prev) = advs_from_addr.get_mut(data_units) { let expired = prev.elapsed() > expiry_duration; *prev = time::Instant::now(); expired } else { - advs_from_addr.insert(data_units.clone(), time::Instant::now()); + advs_from_addr.insert(data_units.to_vec(), time::Instant::now()); true }; @@ -92,21 +91,21 @@ async fn main() -> PyResult<()> { return Ok(()); } - let addr_style = if adv.is_connectable()? { + let addr_style = if adv.is_connectable() { Style::new().yellow() } else { Style::new().red() }; - let (type_style, qualifier) = match adv.address()?.address_type()? { + let (type_style, qualifier) = match adv.address().address_type() { AddressType::PublicIdentityAddress | AddressType::PublicDeviceAddress => { (Style::new().cyan(), "") } _ => { - if addr.is_static()? { - (Style::new().green(), "(static)") - } else if addr.is_resolvable()? { - (Style::new().magenta(), "(resolvable)") + if addr.is_static() { + (Style::new().green(), "(static) ") + } else if addr.is_resolvable() { + (Style::new().magenta(), "(resolvable) ") } else { (Style::new().default_color(), "") } @@ -114,16 +113,19 @@ async fn main() -> PyResult<()> { }; println!( - ">>> {} [{:?}] {qualifier}:\n RSSI: {}", - addr.as_hex()?.style(addr_style), - addr.address_type()?.style(type_style), + ">>> {} [{:?}] {qualifier}{}:\n RSSI: {}", + addr.as_be_hex().style(addr_style), + addr.address_type().style(type_style), + chrono::Local::now() + .format("%Y-%m-%d %H:%M:%S%.3f") + .fg::(), rssi, ); - data_units.into_iter().for_each(|(code, data)| { - let matching = CommonDataType::for_type_code(code).collect::>(); + data_units.iter().for_each(|(code, data)| { + let matching = CommonDataType::for_type_code(*code).collect::>(); let code_str = if matching.is_empty() { - format!("0x{}", hex::encode_upper([code.into()])) + format!("0x{}", hex::encode_upper([u8::from(*code)])) } else { matching .iter() @@ -137,16 +139,16 @@ async fn main() -> PyResult<()> { let data_str = matching .iter() .filter_map(|t| { - t.format_data(&data).map(|formatted| { + t.format_data(data).map(|formatted| { format!( "{} {}", formatted, - format!("(raw: 0x{})", hex::encode_upper(&data)).dimmed() + format!("(raw: 0x{})", hex::encode_upper(data)).dimmed() ) }) }) .next() - .unwrap_or_else(|| format!("0x{}", hex::encode_upper(&data))); + .unwrap_or_else(|| format!("0x{}", hex::encode_upper(data))); println!(" [{}]: {}", code_str, data_str) }); diff --git a/rust/pytests/assigned_numbers.rs b/rust/pytests/assigned_numbers.rs index 7f8f1d15..76235249 100644 --- a/rust/pytests/assigned_numbers.rs +++ b/rust/pytests/assigned_numbers.rs @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/rust/pytests/pytests.rs b/rust/pytests/pytests.rs index 4a30e8de..5033336f 100644 --- a/rust/pytests/pytests.rs +++ b/rust/pytests/pytests.rs @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -19,3 +19,5 @@ async fn main() -> pyo3::PyResult<()> { mod assigned_numbers; mod wrapper; + +pub(crate) mod rootcanal; diff --git a/rust/pytests/rootcanal.rs b/rust/pytests/rootcanal.rs new file mode 100644 index 00000000..6ccfba8d --- /dev/null +++ b/rust/pytests/rootcanal.rs @@ -0,0 +1,295 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use anyhow::anyhow; +use cfg_if::cfg_if; +use log::{debug, info, warn}; +use rand::Rng; +use std::{env, fs, future::Future, io::Read as _, path}; +use tokio::{ + io::{self, AsyncBufReadExt as _}, + select, + sync::mpsc, + task, +}; +use tokio_util::sync::CancellationToken; + +/// Run the provided closure with rootcanal running in the background. +/// +/// Port info for the rootcanal instance is passed to the closure; those ports must be used rather +/// than the default rootcanal ports so that multiple tests may run concurrently without clashing. +/// +/// Sets up `env_logger`, if it wasn't already configured, so that debug logging with rootcanal +/// output can be enabled with `RUST_LOG=debug` env var. +/// +/// If the correct rootcanal binary isn't already cached, it will be downloaded from GitHub +/// Releases if a binary is available for the current OS & architecture. +/// +/// This is a stop-gap until rootcanal's build is improved to the point that a rootcanal-sys crate, +/// and using rootcanal as a library rather than a separate process, becomes feasible. +pub(crate) async fn run_with_rootcanal(closure: F) -> anyhow::Result<()> +where + O: Future>, + F: Fn(RootcanalPorts) -> O, +{ + env_logger::try_init().unwrap_or_else(|_e| debug!("logger already initialized; skipping")); + + let bin = find_rootcanal().await?; + + // loop until available ports are found + loop { + // random non-privileged ports + let hci_port: u16 = rand::thread_rng().gen_range(1_025..65_535); + let rc_ports = RootcanalPorts { + hci_port, + link_ble_port: hci_port + 1, + link_port: hci_port + 2, + test_port: hci_port + 3, + }; + + debug!("Trying to launch rootcanal with {:?}", rc_ports); + + let mut child = tokio::process::Command::new(bin.as_os_str()) + .args([ + "-hci_port", + &rc_ports.hci_port.to_string(), + "-link_ble_port", + &rc_ports.link_ble_port.to_string(), + "-link_port", + &rc_ports.link_port.to_string(), + "-test_port", + &rc_ports.test_port.to_string(), + ]) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .stdin(std::process::Stdio::null()) + // clean up the process if the test panics (e.g. assert failure) + .kill_on_drop(true) + .spawn()?; + + let (tx, mut rx) = mpsc::unbounded_channel(); + let stdout_task = spawn_output_copier(child.stdout.take().unwrap(), tx.clone()); + let stderr_task = spawn_output_copier(child.stderr.take().unwrap(), tx); + + // Rootcanal doesn't exit or write to stderr if it can't bind to ports, so we + // search through stdout to look for `Error binding...` that would show up before + // `initialize: Finished`. + + let started_successfully = loop { + match rx.recv().await { + None => { + warn!("Rootcanal output copiers aborted early"); + break false; + } + Some(line) => { + if line.contains("Error binding test channel listener socket to port") { + debug!("Rootcanal failed to bind ports {:?}: {line}", rc_ports); + break false; + } + + if line.contains("initialize: Finished") { + // Didn't see an error, so rootcanal is usable + break true; + } + } + } + }; + + // Print further rootcanal output as it arrives so that it will interleave with + // test printlns, etc + let cancellation_token = CancellationToken::new(); + let printer_task = spawn_printer(rx, cancellation_token.clone()); + + let test_res = if started_successfully { + // run test task + Some(closure(rc_ports).await) + } else { + None + }; + + child + .start_kill() + .unwrap_or_else(|e| warn!("Could not kill rootcanal: {:?}", e)); + cancellation_token.cancel(); + stdout_task + .await + .unwrap_or_else(|e| warn!("stdout task failed: {:?}", e)); + stderr_task + .await + .unwrap_or_else(|e| warn!("stderr task failed: {:?}", e)); + printer_task + .await + .unwrap_or_else(|e| warn!("print task failed: {:?}", e)); + + match child.wait().await { + Ok(exit) => debug!("exit status: {exit}"), + Err(e) => warn!("Error while waiting for child: {:?}", e), + } + + if let Some(res) = test_res { + break res; + } + } +} + +#[derive(Debug)] +pub(crate) struct RootcanalPorts { + /// HCI TCP server port + pub(crate) hci_port: u16, + /// LE link TCP server port + pub(crate) link_ble_port: u16, + /// Link TCP server port + pub(crate) link_port: u16, + /// Test TCP port + pub(crate) test_port: u16, +} + +/// Spawn a task that reads lines from `read` and writes them to `tx` +fn spawn_output_copier( + read: R, + tx: mpsc::UnboundedSender, +) -> task::JoinHandle<()> { + tokio::spawn(async move { + let reader = io::BufReader::new(read); + let mut lines = reader.lines(); + + loop { + let res = lines.next_line().await; + match res { + Ok(None) => { + // no more lines + return; + } + Ok(Some(l)) => { + if tx.send(l).is_err() { + // rx closed + return; + }; + } + Err(e) => { + warn!("Could not read rootcanal output: {:?}", e) + } + } + } + }) +} + +/// Spawn a task to print output from rootcanal as it happens +fn spawn_printer( + mut rx: mpsc::UnboundedReceiver, + cancellation_token: CancellationToken, +) -> task::JoinHandle<()> { + tokio::spawn(async move { + loop { + select! { + _ = cancellation_token.cancelled() => { + // signal copier tasks to close + rx.close(); + // print any buffered lines + while let Some(line) = rx.recv().await { + debug!("{line}") + } + + break; + } + opt = rx.recv() => { + match opt { + None => break, + Some(line) => debug!("{line}"), + } + } + } + } + }) +} + +/// Returns the path to a rootcanal binary, downloading it from GitHub if necessary. +async fn find_rootcanal() -> anyhow::Result { + let version = "1.10.0"; + // from https://github.com/google/rootcanal/releases + let (url, zip_bin_path) = match (env::consts::OS, env::consts::ARCH) { + ("linux", "x86_64") => ("https://github.com/google/rootcanal/releases/download/v1.10.0/rootcanal-1.10.0-linux-x86_64.zip", "rootcanal-linux-x86_64/bin/rootcanal"), + ("macos", "x86_64") => ("https://github.com/google/rootcanal/releases/download/v1.10.0/rootcanal-1.10.0-macos-x86_64.zip", "rootcanal-macos-x86_64/bin/rootcanal"), + _ => { + return Err(anyhow!("No Rootcanal binary available for {} {}, sorry", env::consts::OS, env::consts::ARCH)); + } + }; + let rootcanal_dir = directories::ProjectDirs::from("com", "google", "bumble") + .map(|pd| { + pd.data_local_dir() + .join("test-tools") + .join("rootcanal") + .join(version) + .join("bin") + }) + .ok_or_else(|| anyhow!("Couldn't resolve rootcanal dir"))?; + fs::create_dir_all(&rootcanal_dir)?; + + with_dir_lock(&rootcanal_dir.join(".lockdir"), async { + let rootcanal_bin = rootcanal_dir.join("rootcanal"); + if rootcanal_bin.exists() { + return Ok(rootcanal_bin); + } + + info!("Downloading rootcanal {version} to {:?}", rootcanal_bin); + + let resp_body = reqwest::get(url).await?.bytes().await?; + let mut archive = zip::ZipArchive::new(std::io::Cursor::new(&resp_body))?; + let mut zip_entry = archive.by_name(zip_bin_path)?; + let mut buf = Vec::new(); + zip_entry.read_to_end(&mut buf)?; + fs::write(&rootcanal_bin, buf)?; + + cfg_if! { + if #[cfg(unix)] { + use std::os::unix::fs::PermissionsExt; + fs::set_permissions(&rootcanal_bin, fs::Permissions::from_mode(0o744))?; + } + } + + Ok(rootcanal_bin) + }) + .await +} + +/// Execute `closure` with a simple directory lock held. +/// +/// Assumes that directory creation is atomic, which it is on most OS's. +async fn with_dir_lock( + dir: &path::Path, + closure: impl Future>, +) -> anyhow::Result { + // wait until we can create the dir + loop { + match fs::create_dir(dir) { + Ok(_) => break, + Err(e) => { + if e.kind() == io::ErrorKind::AlreadyExists { + tokio::time::sleep(std::time::Duration::from_millis(50)).await; + } else { + warn!( + "Unexpected error creating lockdir at {}: {:?}", + dir.to_string_lossy(), + e + ); + return Err(e.into()); + } + } + } + } + let res = closure.await; + + let _ = fs::remove_dir(dir).map_err(|e| warn!("Could not remove lockdir: {:?}", e)); + res +} diff --git a/rust/pytests/wrapper/att.rs b/rust/pytests/wrapper/att.rs new file mode 100644 index 00000000..8088ae52 --- /dev/null +++ b/rust/pytests/wrapper/att.rs @@ -0,0 +1,81 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::wrapper::eval_bumble; +use bumble::wrapper::{ + att::{AttributePermission, AttributeUuid}, + core::{TryToPy, Uuid16}, + PyDictExt, +}; +use pyo3::{types::PyDict, PyResult, Python}; + +#[pyo3_asyncio::tokio::test] +fn attribute_permissions_to_py() -> PyResult<()> { + let perms = AttributePermission::Readable | AttributePermission::Writeable; + + Python::with_gil(|py| { + let locals = PyDict::from_pairs(py, &[("perms", perms.try_to_py(py)?)])?; + + let and_readable = eval_bumble( + py, + "perms & bumble.att.Attribute.Permissions.READABLE", + locals, + )? + .extract::()?; + assert_eq!(0x01, and_readable); + + // authz isn't in the set, so should get 0 + let and_read_authz = eval_bumble( + py, + "perms & bumble.att.Attribute.Permissions.READ_REQUIRES_AUTHORIZATION", + locals, + )? + .extract::()?; + assert_eq!(0, and_read_authz); + Ok(()) + }) +} + +#[pyo3_asyncio::tokio::test] +fn attribute_uuid16_to_py() -> PyResult<()> { + let battery_service = AttributeUuid::Uuid16(Uuid16::from_be_bytes([0x18, 0x0F])); + + Python::with_gil(|py| { + let locals = PyDict::from_pairs(py, &[("uuid", battery_service.try_to_py(py)?)])?; + + let uuid_str = eval_bumble(py, "uuid.to_hex_str()", locals)?.extract::()?; + assert_eq!("180F".to_string(), uuid_str); + + let eq_built_in_battery = + eval_bumble(py, "uuid == bumble.gatt.GATT_BATTERY_SERVICE", locals)? + .extract::()?; + assert!(eq_built_in_battery); + + Ok(()) + }) +} + +#[pyo3_asyncio::tokio::test] +fn attribute_uuid128_to_py() -> PyResult<()> { + let expanded_uuid = AttributeUuid::Uuid128(Uuid16::from_be_bytes([0xAA, 0xBB]).into()); + + Python::with_gil(|py| { + let locals = PyDict::from_pairs(py, &[("uuid", expanded_uuid.try_to_py(py)?)])?; + + let uuid_str = eval_bumble(py, "uuid.to_hex_str()", locals)?.extract::()?; + assert_eq!("0000AABB00001000800000805F9B34FB".to_string(), uuid_str); + + Ok(()) + }) +} diff --git a/rust/pytests/wrapper/drivers.rs b/rust/pytests/wrapper/drivers.rs index d2517eb1..4b812bc1 100644 --- a/rust/pytests/wrapper/drivers.rs +++ b/rust/pytests/wrapper/drivers.rs @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/rust/pytests/wrapper/gatt.rs b/rust/pytests/wrapper/gatt.rs new file mode 100644 index 00000000..83e48135 --- /dev/null +++ b/rust/pytests/wrapper/gatt.rs @@ -0,0 +1,258 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::rootcanal::run_with_rootcanal; +use crate::wrapper::eval_bumble; +use anyhow::anyhow; +use bumble::wrapper::gatt::client::CharacteristicProxy; +use bumble::{ + adv::{AdvertisementDataBuilder, CommonDataType}, + wrapper::{ + att::{AttributePermission, MutexRead, NoOpWrite}, + core::{TryToPy, Uuid128}, + device::{Device, Peer, ServiceHandle}, + gatt::{ + server::{Characteristic, CharacteristicValueHandler, Service}, + CharacteristicProperty, + }, + hci::{packets::AddressType, Address}, + transport::Transport, + PyDictExt, + }, +}; +use pyo3::{types::PyDict, PyResult, Python}; +use std::sync::Arc; + +#[pyo3_asyncio::tokio::test] +fn characteristic_properties_to_py() -> PyResult<()> { + let props = CharacteristicProperty::Read | CharacteristicProperty::Write; + + Python::with_gil(|py| { + let locals = PyDict::from_pairs(py, &[("props", props.try_to_py(py)?)])?; + + let and_read = eval_bumble( + py, + "props & bumble.gatt.Characteristic.Properties.READ", + locals, + )? + .extract::()?; + assert_eq!(0x02, and_read); + + // notify not in the set, should get 0 + let and_notify = eval_bumble( + py, + "props & bumble.gatt.Characteristic.Properties.NOTIFY", + locals, + )? + .extract::()?; + assert_eq!(0, and_notify); + Ok(()) + }) +} + +#[pyo3_asyncio::tokio::test] +async fn rootcanal_gatt_client_connects_to_service_and_reads() -> PyResult<()> { + run_with_rootcanal(|rc_ports| async move { + let transport_spec = format!("tcp-client:127.0.0.1:{}", rc_ports.hci_port); + + // random (type 4) UUIDs + let service_uuid = Uuid128::parse_str("074e27b7-cd51-4678-b74a-09fdc8f363fc").unwrap(); + let characteristic_uuid = + Uuid128::parse_str("5666aaef-cd71-47d5-9ad3-c81a72c1521f").unwrap(); + + let initial_characteristic_value = vec![1, 2, 3, 4]; + let (server_address, _server_transport, _server_device, read_handler, _service_handle) = + start_server( + &transport_spec, + service_uuid, + characteristic_uuid, + initial_characteristic_value.clone(), + ) + .await?; + + let (_client_transport, discovered_characteristic) = connect_and_discover( + transport_spec, + service_uuid, + characteristic_uuid, + &server_address, + ) + .await?; + + // read the value + assert_eq!( + initial_characteristic_value, + discovered_characteristic.read_value().await? + ); + + // update the server for the second read + read_handler.set(&[5, 6, 7, 8]); + + // read again + assert_eq!( + vec![5, 6, 7, 8], + discovered_characteristic.read_value().await? + ); + + Ok(()) + }) + .await + .map_err(|e| e.into()) +} + +#[pyo3_asyncio::tokio::test] +async fn rootcanal_gatt_client_connects_to_service_and_is_notified() -> PyResult<()> { + run_with_rootcanal(|rc_ports| async move { + let transport_spec = format!("tcp-client:127.0.0.1:{}", rc_ports.hci_port); + + // random (type 4) UUIDs + let service_uuid = Uuid128::parse_str("074e27b7-cd51-4678-b74a-09fdc8f363fc").unwrap(); + let characteristic_uuid = + Uuid128::parse_str("5666aaef-cd71-47d5-9ad3-c81a72c1521f").unwrap(); + + let initial_characteristic_value = vec![1, 2, 3, 4]; + let (server_address, _server_transport, mut server_device, _read_handler, service_handle) = + start_server( + &transport_spec, + service_uuid, + characteristic_uuid, + initial_characteristic_value.clone(), + ) + .await?; + + let (_client_transport, mut discovered_characteristic) = connect_and_discover( + transport_spec, + service_uuid, + characteristic_uuid, + &server_address, + ) + .await?; + + let char_handle = service_handle + .characteristic_handle(characteristic_uuid) + .ok_or(anyhow!("Characteristic not found"))?; + + // using a broadcast channel so we don't have to deal with a blocking or async send + let (tx, mut rx) = tokio::sync::broadcast::channel(1); + + // notify the client + discovered_characteristic + .subscribe(move |value| { + let clone = tx.clone(); + let _ = clone.send(value); + }) + .await?; + + server_device.notify_subscribers(char_handle).await?; + + let notify_data = rx.recv().await?; + assert_eq!(initial_characteristic_value, notify_data); + + Ok(()) + }) + .await + .map_err(|e| e.into()) +} + +/// Start a GATT server with the specified characteristic +/// +/// Returns transport to keep it alive for the duration of the test +async fn start_server( + transport_spec: &str, + service_uuid: Uuid128, + characteristic_uuid: Uuid128, + initial_characteristic_value: Vec, +) -> anyhow::Result<(Address, Transport, Device, Arc, ServiceHandle)> { + // start server + let server_transport = Transport::open(transport_spec).await?; + let server_address = + Address::from_be_hex("F0:F1:F2:F3:F4:F5", AddressType::RandomDeviceAddress) + .map_err(|e| anyhow!(e))?; + let mut server_device = Device::with_hci( + "Bumble", + server_address, + server_transport.source()?, + server_transport.sink()?, + ) + .await?; + server_device.power_on().await?; + + // add service + let read_handler = Arc::new(MutexRead::new(initial_characteristic_value)); + let characteristic = Characteristic::new( + characteristic_uuid.into(), + CharacteristicProperty::Read | CharacteristicProperty::Notify, + AttributePermission::Readable.into(), + CharacteristicValueHandler::new(Box::new(read_handler.clone()), Box::new(NoOpWrite)), + ); + let service = Service::new(service_uuid.into(), vec![characteristic]); + let service_handle = server_device.add_service(&service)?; + + // broadcast adv for service + let mut builder = AdvertisementDataBuilder::new(); + builder.append(CommonDataType::CompleteLocalName, "Bumble Test")?; + builder.append( + CommonDataType::IncompleteListOf128BitServiceClassUuids, + &service_uuid, + )?; + server_device.set_advertising_data(builder)?; + server_device.start_advertising(true).await?; + Ok(( + server_address, + server_transport, + server_device, + read_handler, + service_handle, + )) +} + +/// Connect to a GATT server and find the specified characteristic +/// +/// Returns transport to keep it alive for the duration of the test +async fn connect_and_discover( + transport_spec: String, + service_uuid: Uuid128, + characteristic_uuid: Uuid128, + server_address: &Address, +) -> anyhow::Result<(Transport, CharacteristicProxy>)> { + let client_transport = Transport::open(transport_spec).await?; + let client_address = + Address::from_be_hex("F0:F1:F2:F3:F4:F6", AddressType::RandomDeviceAddress) + .map_err(|e| anyhow!(e))?; + let client_device = Device::with_hci( + "Bumble", + client_address, + client_transport.source()?, + client_transport.sink()?, + ) + .await?; + client_device.power_on().await?; + let conn = client_device.connect(server_address).await?; + let mut peer = Peer::new(conn).await?; + + peer.discover_services().await?; + peer.discover_characteristics().await?; + + let discovered_service = peer + .services_by_uuid(service_uuid)? + .into_iter() + .next() + .ok_or(anyhow!("Service not found"))?; + + let discovered_characteristic = discovered_service + .characteristics_by_uuid(characteristic_uuid)? + .into_iter() + .next() + .ok_or(anyhow!("Characteristic not found"))?; + Ok((client_transport, discovered_characteristic)) +} diff --git a/rust/pytests/wrapper/hci.rs b/rust/pytests/wrapper/hci.rs index c4ce20d0..e56babaa 100644 --- a/rust/pytests/wrapper/hci.rs +++ b/rust/pytests/wrapper/hci.rs @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,8 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. +use anyhow::anyhow; use bumble::wrapper::{ controller::Controller, + core::{TryFromPy, TryToPy}, device::Device, hci::{ packets::{ @@ -28,12 +30,13 @@ use bumble::wrapper::{ }; use pyo3::{ exceptions::PyException, - {PyErr, PyResult}, + Python, {PyErr, PyResult}, }; #[pyo3_asyncio::tokio::test] async fn test_hci_roundtrip_success_and_failure() -> PyResult<()> { - let address = Address::new("F0:F1:F2:F3:F4:F5", AddressType::RandomDeviceAddress)?; + let address = Address::from_be_hex("F0:F1:F2:F3:F4:F5", AddressType::RandomDeviceAddress) + .map_err(|e| anyhow!(e))?; let device = create_local_device(address).await?; device.power_on().await?; @@ -78,9 +81,29 @@ async fn test_hci_roundtrip_success_and_failure() -> PyResult<()> { Ok(()) } +#[pyo3_asyncio::tokio::test] +async fn address_roundtrip() -> PyResult<()> { + for addr_type in [ + AddressType::PublicDeviceAddress, + AddressType::RandomDeviceAddress, + AddressType::PublicIdentityAddress, + AddressType::RandomIdentityAddress, + ] { + let addr = Address::from_be_hex("F0:F1:F2:F3:F4:F5", addr_type).map_err(|e| anyhow!(e))?; + + Python::with_gil(|py| { + assert_eq!(addr, Address::try_from_py(py, addr.try_to_py(py)?)?); + + Ok::<(), PyErr>(()) + })?; + } + + Ok(()) +} + async fn create_local_device(address: Address) -> PyResult { let link = Link::new_local_link()?; - let controller = Controller::new("C1", None, None, Some(link), Some(address.clone())).await?; + let controller = Controller::new("C1", None, None, Some(link), Some(address)).await?; let host = Host::new(controller.clone().into(), controller.into()).await?; - Device::new(None, Some(address), None, Some(host), None) + Device::new(None, Some(address), None, Some(host), None).await } diff --git a/rust/pytests/wrapper/mod.rs b/rust/pytests/wrapper/mod.rs index 3bc9127b..66ba6411 100644 --- a/rust/pytests/wrapper/mod.rs +++ b/rust/pytests/wrapper/mod.rs @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,6 +12,27 @@ // See the License for the specific language governing permissions and // limitations under the License. +use bumble::wrapper::PyDictExt; +use pyo3::{intern, prelude::PyModule, types::PyDict, PyAny, PyResult, Python}; + mod drivers; mod hci; mod transport; + +mod att; +mod gatt; + +/// Eval the provided snippet. +/// +/// The `bumble` module will be available as a global. +fn eval_bumble<'py>(py: Python<'py>, snippet: &str, locals: &'py PyDict) -> PyResult<&'py PyAny> { + let globals = PyDict::from_pairs( + py, + &[("bumble", PyModule::import(py, intern!(py, "bumble"))?)], + )?; + // import modules we might use so python doesn't mysteriously fail to access submodules + // when accessing values that live directly in a module, not in a class in a module + let _ = PyModule::import(py, intern!(py, "bumble.gatt"))?; + + py.eval(snippet, Some(globals), Some(locals)) +} diff --git a/rust/pytests/wrapper/transport.rs b/rust/pytests/wrapper/transport.rs index 333005bd..2eee3f17 100644 --- a/rust/pytests/wrapper/transport.rs +++ b/rust/pytests/wrapper/transport.rs @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/rust/src/cli/firmware/mod.rs b/rust/src/cli/firmware/mod.rs index 1fa14176..8af5ab74 100644 --- a/rust/src/cli/firmware/mod.rs +++ b/rust/src/cli/firmware/mod.rs @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/rust/src/cli/firmware/rtk.rs b/rust/src/cli/firmware/rtk.rs index 10285649..303a6423 100644 --- a/rust/src/cli/firmware/rtk.rs +++ b/rust/src/cli/firmware/rtk.rs @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/rust/src/cli/l2cap/client_bridge.rs b/rust/src/cli/l2cap/client_bridge.rs index 31bc0213..7d895123 100644 --- a/rust/src/cli/l2cap/client_bridge.rs +++ b/rust/src/cli/l2cap/client_bridge.rs @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -23,6 +23,9 @@ use crate::cli::l2cap::{ inject_py_event_loop, proxy_l2cap_rx_to_tcp_tx, proxy_tcp_rx_to_l2cap_tx, BridgeData, }; +use anyhow::anyhow; +use bumble::wrapper::hci::packets::AddressType; +use bumble::wrapper::hci::Address; use bumble::wrapper::{ device::{Connection, Device}, hci::HciConstant, @@ -52,7 +55,12 @@ pub async fn start(args: &Args, device: &mut Device) -> PyResult<()> { "{}", format!("### Connecting to {}...", args.bluetooth_address).yellow() ); - let mut ble_connection = device.connect(&args.bluetooth_address).await?; + let mut ble_connection = device + .connect( + &Address::from_be_hex(&args.bluetooth_address, AddressType::RandomDeviceAddress) + .map_err(|e| anyhow!("{:?}", e))?, + ) + .await?; ble_connection.on_disconnection(|_py, reason| { let disconnection_info = match HciConstant::error_name(reason) { Ok(info_string) => info_string, diff --git a/rust/src/cli/l2cap/mod.rs b/rust/src/cli/l2cap/mod.rs index 3f86b24d..84d60693 100644 --- a/rust/src/cli/l2cap/mod.rs +++ b/rust/src/cli/l2cap/mod.rs @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -43,7 +43,8 @@ pub(crate) async fn run( println!("<<< connected"); let mut device = - Device::from_config_file_with_hci(&device_config, transport.source()?, transport.sink()?)?; + Device::from_config_file_with_hci(&device_config, transport.source()?, transport.sink()?) + .await?; device.power_on().await?; diff --git a/rust/src/cli/l2cap/server_bridge.rs b/rust/src/cli/l2cap/server_bridge.rs index 3f8041a4..7a13d08a 100644 --- a/rust/src/cli/l2cap/server_bridge.rs +++ b/rust/src/cli/l2cap/server_bridge.rs @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/rust/src/cli/mod.rs b/rust/src/cli/mod.rs index e58f88c7..7f2c3c44 100644 --- a/rust/src/cli/mod.rs +++ b/rust/src/cli/mod.rs @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/rust/src/cli/usb/mod.rs b/rust/src/cli/usb/mod.rs index 5cab29a3..c334773c 100644 --- a/rust/src/cli/usb/mod.rs +++ b/rust/src/cli/usb/mod.rs @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/rust/src/adv.rs b/rust/src/internal/adv.rs similarity index 93% rename from rust/src/adv.rs rename to rust/src/internal/adv.rs index 6f84cc5b..c50a5e8b 100644 --- a/rust/src/adv.rs +++ b/rust/src/internal/adv.rs @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -174,7 +174,9 @@ impl CommonDataType { .map(|uuid| { SERVICE_IDS .get(&uuid) - .map(|name| format!("{:?} ({name})", uuid)) + .map(|service_info| { + format!("{:?} ({})", uuid, service_info.name()) + }) .unwrap_or_else(|| format!("{:?}", uuid)) }) .join(", ") @@ -201,7 +203,7 @@ impl CommonDataType { "service={:?}, data={}", SERVICE_IDS .get(&uuid) - .map(|name| format!("{:?} ({name})", uuid)) + .map(|service_info| format!("{:?} ({})", uuid, service_info.name())) .unwrap_or_else(|| format!("{:?}", uuid)), hex::encode_upper(rem) ) @@ -364,20 +366,26 @@ impl AdvertisementDataBuilder { /// Append advertising data to the builder. /// /// Returns an error if the data cannot be appended. - pub fn append( + pub fn append( &mut self, type_code: impl Into, - data: &[u8], + value: &T, ) -> Result<(), AdvertisementDataBuilderError> { + // write to an intermediate buffer rather than ask the value its length, since that could + // be implemented incorrectly + let mut encode_buf = Vec::new(); + value.write_to(&mut encode_buf); + self.encoded_data.push( - data.len() + encode_buf + .len() .try_into() .ok() .and_then(|len: u8| len.checked_add(1)) .ok_or(AdvertisementDataBuilderError::DataTooLong)?, ); self.encoded_data.push(type_code.into().0); - self.encoded_data.extend_from_slice(data); + self.encoded_data.extend_from_slice(&encode_buf); Ok(()) } @@ -387,6 +395,26 @@ impl AdvertisementDataBuilder { } } +/// Encode values per the spec for advertising data +pub trait AdvertisementDataValue { + /// Write this value's data to the buffer + fn write_to(&self, buf: &mut impl bytes::BufMut); +} + +impl AdvertisementDataValue for [u8] { + fn write_to(&self, buf: &mut impl bytes::BufMut) { + // write slices as-is + buf.put(self) + } +} + +impl AdvertisementDataValue for str { + fn write_to(&self, buf: &mut impl bytes::BufMut) { + // write strings as UTF-8 + buf.put(self.as_bytes()) + } +} + /// Errors that can occur when building advertisement data with [AdvertisementDataBuilder]. #[derive(Debug, PartialEq, Eq, thiserror::Error)] pub enum AdvertisementDataBuilderError { diff --git a/rust/src/internal/att/mod.rs b/rust/src/internal/att/mod.rs new file mode 100644 index 00000000..327b7032 --- /dev/null +++ b/rust/src/internal/att/mod.rs @@ -0,0 +1,220 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Core types for the ATT layer + +use crate::internal::core::{Uuid128, Uuid16}; +use crate::wrapper::device::Connection; +use async_trait::async_trait; +use std::ops::Deref; +use std::{collections, ops, sync}; + +/// One or more [AttributePermission]. +/// +/// # Examples +/// ``` +/// use bumble::wrapper::att::{AttributePermission, AttributePermissions}; +/// +/// let perms: AttributePermissions = +/// AttributePermission::Readable | AttributePermission::Writeable; +/// ``` +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AttributePermissions { + permissions: collections::HashSet, +} + +impl AttributePermissions { + /// Returns an iterator over the [AttributePermission] set in this instance. + pub fn iter(&self) -> impl Iterator + '_ { + self.permissions.iter().copied() + } + + /// Returns true iff `permission` is set. + pub fn has_permission(&self, permission: AttributePermission) -> bool { + self.permissions.contains(&permission) + } +} + +impl From for AttributePermissions { + fn from(value: AttributePermission) -> Self { + Self { + permissions: collections::HashSet::from([value]), + } + } +} + +impl ops::BitOr<&Self> for AttributePermissions { + type Output = Self; + + fn bitor(mut self, rhs: &Self) -> Self::Output { + self.permissions.extend(rhs.permissions.iter()); + self + } +} + +impl ops::BitOr for AttributePermissions { + type Output = Self; + + fn bitor(mut self, rhs: AttributePermission) -> Self::Output { + self.permissions.insert(rhs); + self + } +} + +impl ops::BitOrAssign<&Self> for AttributePermissions { + fn bitor_assign(&mut self, rhs: &Self) { + self.permissions.extend(rhs.permissions.iter()); + } +} + +impl ops::BitOrAssign for AttributePermissions { + fn bitor_assign(&mut self, rhs: AttributePermission) { + self.permissions.insert(rhs); + } +} + +/// An individual attribute permission +#[allow(missing_docs)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum AttributePermission { + Readable, + Writeable, + ReadRequiresEncryption, + WriteRequiresEncryption, + ReadRequiresAuthn, + WriteRequiresAuthn, + ReadRequiresAuthz, + WriteRequiresAuthz, +} + +impl ops::BitOr for AttributePermission { + type Output = AttributePermissions; + + fn bitor(self, rhs: Self) -> Self::Output { + AttributePermissions { + permissions: collections::HashSet::from([self, rhs]), + } + } +} + +/// A UUID that defines a particular attribute's type. +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +pub enum AttributeUuid { + /// 16-bit UUID, limited to types in the BLE spec + Uuid16(Uuid16), + /// 128-bit UUID, for custom entities + Uuid128(Uuid128), +} + +impl From for AttributeUuid { + fn from(value: Uuid16) -> Self { + Self::Uuid16(value) + } +} + +impl From for AttributeUuid { + fn from(value: Uuid128) -> Self { + Self::Uuid128(value) + } +} + +/// Callback invoked when a GATT client reads an attribute value +#[async_trait] +pub trait AttributeRead: Send + Sync { + /// Produce data to send to the client at `conn`. + /// + /// Takes `&self`, not `&mut self`, to allow concurrent invocations. + async fn read(&self, conn: Connection) -> anyhow::Result>; +} + +/// Callback invoked when a GATT client writes an attribute value +#[async_trait] +pub trait AttributeWrite: Send + Sync { + /// Accept data provided by the client at `conn`. + /// + /// Takes `&self`, not `&mut self`, to allow concurrent invocations. + async fn write(&self, data: Vec, conn: Connection) -> anyhow::Result<()>; +} + +/// An [AttributeWrite] impl that does nothing when invoked. +pub struct NoOpWrite; + +#[async_trait] +impl AttributeWrite for NoOpWrite { + async fn write(&self, _data: Vec, _conn: Connection) -> anyhow::Result<()> { + // no op + Ok(()) + } +} + +// For user convenience, impl read/write for Arc'd handlers +#[async_trait] +impl AttributeRead for sync::Arc { + async fn read(&self, conn: Connection) -> anyhow::Result> { + self.deref().read(conn).await + } +} + +#[async_trait] +impl AttributeWrite for sync::Arc { + async fn write(&self, data: Vec, conn: Connection) -> anyhow::Result<()> { + self.deref().write(data, conn).await + } +} + +/// A simple [AttributeRead] that just holds bytes wrapped in a mutex for convenient changing +/// on the fly +pub struct MutexRead { + data: sync::Mutex>, +} + +impl MutexRead { + /// Create a new MutexRead with the provided `data` + pub fn new(data: Vec) -> Self { + Self { + data: sync::Mutex::new(data), + } + } + + /// Set a new value to be used on the next read + pub fn set(&self, data: &[u8]) { + let mut guard = self.data.lock().unwrap(); + guard.clear(); + guard.extend_from_slice(data); + } +} + +#[async_trait] +impl AttributeRead for MutexRead { + async fn read(&self, _conn: Connection) -> anyhow::Result> { + Ok(self.data.lock().unwrap().clone()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn or_into_perms_works() { + let perms = AttributePermission::Readable | AttributePermission::Writeable; + + let expected = collections::HashSet::from([ + AttributePermission::Readable, + AttributePermission::Writeable, + ]); + + assert_eq!(expected, perms.iter().collect()); + } +} diff --git a/rust/src/internal/core.rs b/rust/src/internal/core.rs new file mode 100644 index 00000000..65c4b5c0 --- /dev/null +++ b/rust/src/internal/core.rs @@ -0,0 +1,195 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::internal::adv::AdvertisementDataValue; +use lazy_static::lazy_static; +use nom::combinator; +use std::fmt; + +lazy_static! { + static ref BASE_UUID: [u8; 16] = hex::decode("0000000000001000800000805F9B34FB") + .unwrap() + .try_into() + .unwrap(); +} + +/// 16-bit UUID +#[derive(PartialEq, Eq, Hash, Clone, Copy)] +pub struct Uuid16 { + /// Big-endian bytes + be_bytes: [u8; 2], +} + +impl Uuid16 { + /// Construct a UUID from little-endian bytes + pub fn from_le_bytes(mut le_bytes: [u8; 2]) -> Self { + le_bytes.reverse(); + Self::from_be_bytes(le_bytes) + } + + /// Construct a UUID from big-endian bytes + pub const fn from_be_bytes(be_bytes: [u8; 2]) -> Self { + Self { be_bytes } + } + + /// The UUID in big-endian bytes form + pub fn as_be_bytes(&self) -> [u8; 2] { + self.be_bytes + } + + /// The UUID in little-endian bytes form + pub fn as_le_bytes(&self) -> [u8; 2] { + let mut uuid = self.be_bytes; + uuid.reverse(); + uuid + } + + pub(crate) fn parse_le(input: &[u8]) -> nom::IResult<&[u8], Self> { + combinator::map_res(nom::bytes::complete::take(2_usize), |b: &[u8]| { + b.try_into().map(|mut uuid: [u8; 2]| { + uuid.reverse(); + Self { be_bytes: uuid } + }) + })(input) + } +} + +impl From for Uuid16 { + fn from(value: u16) -> Self { + Self { + be_bytes: value.to_be_bytes(), + } + } +} + +impl fmt::Debug for Uuid16 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "UUID-16:{}", hex::encode_upper(self.be_bytes)) + } +} + +impl AdvertisementDataValue for Uuid16 { + fn write_to(&self, buf: &mut impl bytes::BufMut) { + buf.put(self.as_le_bytes().as_slice()) + } +} + +/// 32-bit UUID +#[derive(PartialEq, Eq, Hash)] +pub struct Uuid32 { + /// Big-endian bytes + be_bytes: [u8; 4], +} + +impl Uuid32 { + /// The UUID in big-endian bytes form + pub fn as_be_bytes(&self) -> [u8; 4] { + self.be_bytes + } + + pub(crate) fn parse(input: &[u8]) -> nom::IResult<&[u8], Self> { + combinator::map_res(nom::bytes::complete::take(4_usize), |b: &[u8]| { + b.try_into().map(|mut uuid: [u8; 4]| { + uuid.reverse(); + Self { be_bytes: uuid } + }) + })(input) + } +} + +impl fmt::Debug for Uuid32 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "UUID-32:{}", hex::encode_upper(self.be_bytes)) + } +} + +impl From for Uuid32 { + fn from(value: Uuid16) -> Self { + let mut uuid = [0; 4]; + uuid[2..].copy_from_slice(&value.be_bytes); + Self { be_bytes: uuid } + } +} + +/// 128-bit UUID +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +pub struct Uuid128 { + /// Big-endian bytes + be_bytes: [u8; 16], +} + +impl Uuid128 { + /// The UUID in big-endian bytes form + pub fn as_be_bytes(&self) -> [u8; 16] { + self.be_bytes + } + /// The UUID in little-endian bytes form + pub fn as_le_bytes(&self) -> [u8; 16] { + let mut bytes = self.be_bytes; + bytes.reverse(); + bytes + } + + pub(crate) fn parse_le(input: &[u8]) -> nom::IResult<&[u8], Self> { + combinator::map_res(nom::bytes::complete::take(16_usize), |b: &[u8]| { + b.try_into().map(|mut uuid: [u8; 16]| { + uuid.reverse(); + Self { be_bytes: uuid } + }) + })(input) + } + + /// Parse the normal dash-separated form of a UUID, returning None if the input is invalid + pub fn parse_str(input: &str) -> Option { + uuid::Uuid::parse_str(input).ok().map(|u| Self { + be_bytes: u.into_bytes(), + }) + } +} + +impl fmt::Debug for Uuid128 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}-{}-{}-{}-{}", + hex::encode_upper(&self.be_bytes[..4]), + hex::encode_upper(&self.be_bytes[4..6]), + hex::encode_upper(&self.be_bytes[6..8]), + hex::encode_upper(&self.be_bytes[8..10]), + hex::encode_upper(&self.be_bytes[10..]) + ) + } +} + +impl From for Uuid128 { + fn from(value: Uuid16) -> Self { + let mut uuid = *BASE_UUID; + uuid[2..4].copy_from_slice(&value.be_bytes); + Self { be_bytes: uuid } + } +} + +impl From for Uuid128 { + fn from(value: Uuid32) -> Self { + let mut uuid = *BASE_UUID; + uuid[..4].copy_from_slice(&value.be_bytes); + Self { be_bytes: uuid } + } +} + +impl AdvertisementDataValue for Uuid128 { + fn write_to(&self, buf: &mut impl bytes::BufMut) { + buf.put(self.as_le_bytes().as_slice()) + } +} diff --git a/rust/src/internal/drivers/mod.rs b/rust/src/internal/drivers/mod.rs index 5e72c59d..f3783047 100644 --- a/rust/src/internal/drivers/mod.rs +++ b/rust/src/internal/drivers/mod.rs @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/rust/src/internal/drivers/rtk.rs b/rust/src/internal/drivers/rtk.rs index 2d4e685b..637f0146 100644 --- a/rust/src/internal/drivers/rtk.rs +++ b/rust/src/internal/drivers/rtk.rs @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/rust/src/internal/gatt/mod.rs b/rust/src/internal/gatt/mod.rs new file mode 100644 index 00000000..7209422d --- /dev/null +++ b/rust/src/internal/gatt/mod.rs @@ -0,0 +1,157 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Core GATT types + +use std::ops; +use strum::IntoEnumIterator; + +pub(crate) mod server; + +/// Combined properties for a GATT characteristic. +/// See [CharacteristicProperty] for individual properties. +/// +/// # Examples +/// +/// Creating a [CharacteristicProperties] by OR-ing together individual properties: +/// ``` +/// use bumble::wrapper::gatt::CharacteristicProperty; +/// +/// let mut properties = CharacteristicProperty::Read | CharacteristicProperty::Write; +/// assert!(properties.has_property(CharacteristicProperty::Read)); +/// properties |= CharacteristicProperty::Notify; +/// assert!(properties.has_property(CharacteristicProperty::Notify)); +/// ``` +/// +/// Creating a [CharacteristicProperties] directly from an individual property: +/// ``` +/// use bumble::wrapper::gatt::{CharacteristicProperties, CharacteristicProperty}; +/// +/// let properties: CharacteristicProperties = CharacteristicProperty::Broadcast.into(); +/// ``` +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CharacteristicProperties { + /// Bit vector of properties `OR`d together. + /// + /// See [CharacteristicProperty::as_bit_mask]. + pub(crate) bits: u8, +} + +impl CharacteristicProperties { + /// Returns an iterator over the individual properties set in this instance. + pub fn iter(&self) -> impl Iterator + '_ { + CharacteristicProperty::iter().filter(|c| self.has_property(*c)) + } + + /// Returns true iff the specified property is set. + pub fn has_property(&self, p: CharacteristicProperty) -> bool { + self.bits & p.as_bit_mask() > 0 + } +} + +impl From for CharacteristicProperties { + fn from(value: CharacteristicProperty) -> Self { + Self { + bits: value.as_bit_mask(), + } + } +} + +impl ops::BitOr<&Self> for CharacteristicProperties { + type Output = Self; + + fn bitor(self, rhs: &Self) -> Self::Output { + Self { + bits: self.bits | rhs.bits, + } + } +} + +impl ops::BitOr for CharacteristicProperties { + type Output = Self; + + fn bitor(self, rhs: CharacteristicProperty) -> Self::Output { + Self { + bits: self.bits | rhs.as_bit_mask(), + } + } +} + +impl ops::BitOrAssign<&Self> for CharacteristicProperties { + fn bitor_assign(&mut self, rhs: &Self) { + self.bits |= rhs.bits + } +} + +impl ops::BitOrAssign for CharacteristicProperties { + fn bitor_assign(&mut self, rhs: CharacteristicProperty) { + self.bits |= rhs.as_bit_mask() + } +} + +/// Individual properties defining what operations are permitted for a GATT characteristic value. +/// Combined into [CharacteristicProperties]. +#[allow(missing_docs)] +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, strum_macros::EnumIter)] +pub enum CharacteristicProperty { + Broadcast, + Read, + WriteWithoutResponse, + Write, + Notify, + Indicate, + AuthnSignedWrites, + ExtendedProps, +} + +impl CharacteristicProperty { + /// Returns the assigned bit for the property. + fn as_bit_mask(&self) -> u8 { + // Per 3.3.1.1 + match self { + CharacteristicProperty::Broadcast => 0x01, + CharacteristicProperty::Read => 0x02, + CharacteristicProperty::WriteWithoutResponse => 0x04, + CharacteristicProperty::Write => 0x08, + CharacteristicProperty::Notify => 0x10, + CharacteristicProperty::Indicate => 0x20, + CharacteristicProperty::AuthnSignedWrites => 0x40, + CharacteristicProperty::ExtendedProps => 0x80, + } + } +} + +impl ops::BitOr for CharacteristicProperty { + type Output = CharacteristicProperties; + + fn bitor(self, rhs: Self) -> Self::Output { + CharacteristicProperties::from(self) | rhs + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections; + + #[test] + fn or_into_properties_works() { + let props = CharacteristicProperty::Indicate | CharacteristicProperty::Read; + let expected = collections::HashSet::from([ + CharacteristicProperty::Indicate, + CharacteristicProperty::Read, + ]); + assert_eq!(expected, props.iter().collect()) + } +} diff --git a/rust/src/internal/gatt/server.rs b/rust/src/internal/gatt/server.rs new file mode 100644 index 00000000..9d38c030 --- /dev/null +++ b/rust/src/internal/gatt/server.rs @@ -0,0 +1,104 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Support for making GATT servers + +use crate::internal::att::{AttributePermissions, AttributeRead, AttributeUuid, AttributeWrite}; +use crate::internal::gatt::CharacteristicProperties; +use std::sync; + +/// The UUID and characteristics for a GATT service +pub struct Service { + pub(crate) uuid: AttributeUuid, + pub(crate) characteristics: Vec, +} + +impl Service { + /// Build a new Service with the provided uuid and characteristics. + pub fn new(uuid: AttributeUuid, characteristics: Vec) -> Self { + Self { + uuid, + characteristics, + } + } + + /// Returns the service's UUID + pub fn uuid(&self) -> &AttributeUuid { + &self.uuid + } + + /// Returns an iterator over the service's characteristics + pub fn iter_characteristics(&self) -> impl Iterator { + self.characteristics.iter() + } +} + +/// A GATT characteristic hosted in a service +pub struct Characteristic { + pub(crate) uuid: AttributeUuid, + pub(crate) properties: CharacteristicProperties, + pub(crate) permissions: AttributePermissions, + pub(crate) value: CharacteristicValueHandler, +} + +impl Characteristic { + /// Create a new Characteristic. + /// `properties` apply at the GATT layer. + /// `permissions` apply to the underlying attribute holding the Characteristic Declaration at + /// the ATT layer. + pub fn new( + uuid: AttributeUuid, + properties: CharacteristicProperties, + permissions: AttributePermissions, + value: CharacteristicValueHandler, + ) -> Self { + Self { + uuid, + properties, + permissions, + value, + } + } + + /// Returns the UUID of the characteristic + pub fn uuid(&self) -> AttributeUuid { + self.uuid + } + + /// Returns the GATT characteristic properties + pub fn properties(&self) -> &CharacteristicProperties { + &self.properties + } + + /// Returns the ATT permissions + pub fn permissions(&self) -> &AttributePermissions { + &self.permissions + } +} + +/// Wraps logic executed when a value is read from or written to. +pub struct CharacteristicValueHandler { + pub(crate) read: sync::Arc>, + pub(crate) write: sync::Arc>, +} + +impl CharacteristicValueHandler { + /// Create a new value with the provided read and write callbacks. + pub fn new(read: Box, write: Box) -> Self { + Self { + read: sync::Arc::new(read), + write: sync::Arc::new(write), + } + } +} diff --git a/rust/src/internal/hci/mod.rs b/rust/src/internal/hci/mod.rs index 7830e318..1c76ac14 100644 --- a/rust/src/internal/hci/mod.rs +++ b/rust/src/internal/hci/mod.rs @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/rust/src/internal/hci/packets.pdl b/rust/src/internal/hci/packets.pdl index 694d37cf..2bd8559b 100644 --- a/rust/src/internal/hci/packets.pdl +++ b/rust/src/internal/hci/packets.pdl @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -6250,4 +6250,4 @@ packet MsftLeMonitorDeviceEventPayload : MsftEventPayload (msft_event_code = MSF test MsftLeMonitorDeviceEventPayload { "\x02\x01\x00\x01\x02\x03\x04\x05\x10\x00", "\x02\x02\xf0\xf1\xf2\xf3\xf4\xf5\xaa\x02", -} \ No newline at end of file +} diff --git a/rust/src/internal/hci/tests.rs b/rust/src/internal/hci/tests.rs index ff9e72b0..c888b2e7 100644 --- a/rust/src/internal/hci/tests.rs +++ b/rust/src/internal/hci/tests.rs @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/rust/src/internal/mod.rs b/rust/src/internal/mod.rs index 9514ed6f..bde68004 100644 --- a/rust/src/internal/mod.rs +++ b/rust/src/internal/mod.rs @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -19,3 +19,10 @@ pub(crate) mod drivers; pub(crate) mod hci; + +pub(crate) mod att; +pub(crate) mod gatt; + +pub(crate) mod core; + +pub mod adv; diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 2bcb3980..8e355656 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -28,6 +28,5 @@ pub mod wrapper; -pub mod adv; - +pub use internal::adv; pub(crate) mod internal; diff --git a/rust/src/main.rs b/rust/src/main.rs index c21f4c85..dd50628d 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/rust/src/wrapper/assigned_numbers/company_ids.rs b/rust/src/wrapper/assigned_numbers/company_ids.rs index 1c558b1e..cef31b21 100644 --- a/rust/src/wrapper/assigned_numbers/company_ids.rs +++ b/rust/src/wrapper/assigned_numbers/company_ids.rs @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/rust/src/wrapper/assigned_numbers/mod.rs b/rust/src/wrapper/assigned_numbers/mod.rs index 25847183..09bca12e 100644 --- a/rust/src/wrapper/assigned_numbers/mod.rs +++ b/rust/src/wrapper/assigned_numbers/mod.rs @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -15,7 +15,7 @@ //! Assigned numbers from the Bluetooth spec. mod company_ids; -mod services; +pub mod services; pub use company_ids::COMPANY_IDS; pub use services::SERVICE_IDS; diff --git a/rust/src/wrapper/assigned_numbers/services.rs b/rust/src/wrapper/assigned_numbers/services.rs index da1c9920..a2041f70 100644 --- a/rust/src/wrapper/assigned_numbers/services.rs +++ b/rust/src/wrapper/assigned_numbers/services.rs @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,69 +14,298 @@ //! Assigned service IDs +#![allow(missing_docs)] + use crate::wrapper::core::Uuid16; use lazy_static::lazy_static; use std::collections; lazy_static! { /// Assigned service IDs - pub static ref SERVICE_IDS: collections::HashMap = [ - (0x1800_u16, "Generic Access"), - (0x1801, "Generic Attribute"), - (0x1802, "Immediate Alert"), - (0x1803, "Link Loss"), - (0x1804, "TX Power"), - (0x1805, "Current Time"), - (0x1806, "Reference Time Update"), - (0x1807, "Next DST Change"), - (0x1808, "Glucose"), - (0x1809, "Health Thermometer"), - (0x180A, "Device Information"), - (0x180D, "Heart Rate"), - (0x180E, "Phone Alert Status"), - (0x180F, "Battery"), - (0x1810, "Blood Pressure"), - (0x1811, "Alert Notification"), - (0x1812, "Human Interface Device"), - (0x1813, "Scan Parameters"), - (0x1814, "Running Speed and Cadence"), - (0x1815, "Automation IO"), - (0x1816, "Cycling Speed and Cadence"), - (0x1818, "Cycling Power"), - (0x1819, "Location and Navigation"), - (0x181A, "Environmental Sensing"), - (0x181B, "Body Composition"), - (0x181C, "User Data"), - (0x181D, "Weight Scale"), - (0x181E, "Bond Management"), - (0x181F, "Continuous Glucose Monitoring"), - (0x1820, "Internet Protocol Support"), - (0x1821, "Indoor Positioning"), - (0x1822, "Pulse Oximeter"), - (0x1823, "HTTP Proxy"), - (0x1824, "Transport Discovery"), - (0x1825, "Object Transfer"), - (0x1826, "Fitness Machine"), - (0x1827, "Mesh Provisioning"), - (0x1828, "Mesh Proxy"), - (0x1829, "Reconnection Configuration"), - (0x183A, "Insulin Delivery"), - (0x183B, "Binary Sensor"), - (0x183C, "Emergency Configuration"), - (0x183E, "Physical Activity Monitor"), - (0x1843, "Audio Input Control"), - (0x1844, "Volume Control"), - (0x1845, "Volume Offset Control"), - (0x1846, "Coordinated Set Identification Service"), - (0x1847, "Device Time"), - (0x1848, "Media Control Service"), - (0x1849, "Generic Media Control Service"), - (0x184A, "Constant Tone Extension"), - (0x184B, "Telephone Bearer Service"), - (0x184C, "Generic Telephone Bearer Service"), - (0x184D, "Microphone Control"), + pub static ref SERVICE_IDS: collections::HashMap = [ + GENERIC_ACCESS, + GENERIC_ATTRIBUTE, + IMMEDIATE_ALERT, + LINK_LOSS, + TX_POWER, + CURRENT_TIME, + REFERENCE_TIME_UPDATE, + NEXT_DST_CHANGE, + GLUCOSE, + HEALTH_THERMOMETER, + DEVICE_INFORMATION, + HEART_RATE, + PHONE_ALERT_STATUS, + BATTERY, + BLOOD_PRESSURE, + ALERT_NOTIFICATION, + HUMAN_INTERFACE_DEVICE, + SCAN_PARAMETERS, + RUNNING_SPEED_AND_CADENCE, + AUTOMATION_IO, + CYCLING_SPEED_AND_CADENCE, + CYCLING_POWER, + LOCATION_AND_NAVIGATION, + ENVIRONMENTAL_SENSING, + BODY_COMPOSITION, + USER_DATA, + WEIGHT_SCALE, + BOND_MANAGEMENT, + CONTINUOUS_GLUCOSE_MONITORING, + INTERNET_PROTOCOL_SUPPORT, + INDOOR_POSITIONING, + PULSE_OXIMETER, + HTTP_PROXY, + TRANSPORT_DISCOVERY, + OBJECT_TRANSFER, + FITNESS_MACHINE, + MESH_PROVISIONING, + MESH_PROXY, + RECONNECTION_CONFIGURATION, + INSULIN_DELIVERY, + BINARY_SENSOR, + EMERGENCY_CONFIGURATION, + PHYSICAL_ACTIVITY_MONITOR, + AUDIO_INPUT_CONTROL, + VOLUME_CONTROL, + VOLUME_OFFSET_CONTROL, + COORDINATED_SET_IDENTIFICATION, + DEVICE_TIME, + MEDIA_CONTROL, + GENERIC_MEDIA_CONTROL, + CONSTANT_TONE_EXTENSION, + TELEPHONE_BEARER, + GENERIC_TELEPHONE_BEARER, + MICROPHONE_CONTROL, ] .into_iter() - .map(|(num, name)| (Uuid16::from_le_bytes(num.to_le_bytes()), name)) + .map(|n| (n.uuid, n)) .collect(); } + +pub const GENERIC_ACCESS: NamedUuid = NamedUuid::new( + Uuid16::from_be_bytes(0x1800_u16.to_be_bytes()), + "Generic Access", +); +pub const GENERIC_ATTRIBUTE: NamedUuid = NamedUuid::new( + Uuid16::from_be_bytes(0x1801_u16.to_be_bytes()), + "Generic Attribute", +); +pub const IMMEDIATE_ALERT: NamedUuid = NamedUuid::new( + Uuid16::from_be_bytes(0x1802_u16.to_be_bytes()), + "Immediate Alert", +); +pub const LINK_LOSS: NamedUuid = + NamedUuid::new(Uuid16::from_be_bytes(0x1803_u16.to_be_bytes()), "Link Loss"); +pub const TX_POWER: NamedUuid = + NamedUuid::new(Uuid16::from_be_bytes(0x1804_u16.to_be_bytes()), "TX Power"); +pub const CURRENT_TIME: NamedUuid = NamedUuid::new( + Uuid16::from_be_bytes(0x1805_u16.to_be_bytes()), + "Current Time", +); +pub const REFERENCE_TIME_UPDATE: NamedUuid = NamedUuid::new( + Uuid16::from_be_bytes(0x1806_u16.to_be_bytes()), + "Reference Time Update", +); +pub const NEXT_DST_CHANGE: NamedUuid = NamedUuid::new( + Uuid16::from_be_bytes(0x1807_u16.to_be_bytes()), + "Next DST Change", +); +pub const GLUCOSE: NamedUuid = + NamedUuid::new(Uuid16::from_be_bytes(0x1808_u16.to_be_bytes()), "Glucose"); +pub const HEALTH_THERMOMETER: NamedUuid = NamedUuid::new( + Uuid16::from_be_bytes(0x1809_u16.to_be_bytes()), + "Health Thermometer", +); +pub const DEVICE_INFORMATION: NamedUuid = NamedUuid::new( + Uuid16::from_be_bytes(0x180A_u16.to_be_bytes()), + "Device Information", +); +pub const HEART_RATE: NamedUuid = NamedUuid::new( + Uuid16::from_be_bytes(0x180D_u16.to_be_bytes()), + "Heart Rate", +); +pub const PHONE_ALERT_STATUS: NamedUuid = NamedUuid::new( + Uuid16::from_be_bytes(0x180E_u16.to_be_bytes()), + "Phone Alert Status", +); +pub const BATTERY: NamedUuid = + NamedUuid::new(Uuid16::from_be_bytes(0x180F_u16.to_be_bytes()), "Battery"); +pub const BLOOD_PRESSURE: NamedUuid = NamedUuid::new( + Uuid16::from_be_bytes(0x1810_u16.to_be_bytes()), + "Blood Pressure", +); +pub const ALERT_NOTIFICATION: NamedUuid = NamedUuid::new( + Uuid16::from_be_bytes(0x1811_u16.to_be_bytes()), + "Alert Notification", +); +pub const HUMAN_INTERFACE_DEVICE: NamedUuid = NamedUuid::new( + Uuid16::from_be_bytes(0x1812_u16.to_be_bytes()), + "Human Interface Device", +); +pub const SCAN_PARAMETERS: NamedUuid = NamedUuid::new( + Uuid16::from_be_bytes(0x1813_u16.to_be_bytes()), + "Scan Parameters", +); +pub const RUNNING_SPEED_AND_CADENCE: NamedUuid = NamedUuid::new( + Uuid16::from_be_bytes(0x1814_u16.to_be_bytes()), + "Running Speed and Cadence", +); +pub const AUTOMATION_IO: NamedUuid = NamedUuid::new( + Uuid16::from_be_bytes(0x1815_u16.to_be_bytes()), + "Automation IO", +); +pub const CYCLING_SPEED_AND_CADENCE: NamedUuid = NamedUuid::new( + Uuid16::from_be_bytes(0x1816_u16.to_be_bytes()), + "Cycling Speed and Cadence", +); +pub const CYCLING_POWER: NamedUuid = NamedUuid::new( + Uuid16::from_be_bytes(0x1818_u16.to_be_bytes()), + "Cycling Power", +); +pub const LOCATION_AND_NAVIGATION: NamedUuid = NamedUuid::new( + Uuid16::from_be_bytes(0x1819_u16.to_be_bytes()), + "Location and Navigation", +); +pub const ENVIRONMENTAL_SENSING: NamedUuid = NamedUuid::new( + Uuid16::from_be_bytes(0x181A_u16.to_be_bytes()), + "Environmental Sensing", +); +pub const BODY_COMPOSITION: NamedUuid = NamedUuid::new( + Uuid16::from_be_bytes(0x181B_u16.to_be_bytes()), + "Body Composition", +); +pub const USER_DATA: NamedUuid = + NamedUuid::new(Uuid16::from_be_bytes(0x181C_u16.to_be_bytes()), "User Data"); +pub const WEIGHT_SCALE: NamedUuid = NamedUuid::new( + Uuid16::from_be_bytes(0x181D_u16.to_be_bytes()), + "Weight Scale", +); +pub const BOND_MANAGEMENT: NamedUuid = NamedUuid::new( + Uuid16::from_be_bytes(0x181E_u16.to_be_bytes()), + "Bond Management", +); +pub const CONTINUOUS_GLUCOSE_MONITORING: NamedUuid = NamedUuid::new( + Uuid16::from_be_bytes(0x181F_u16.to_be_bytes()), + "Continuous Glucose Monitoring", +); +pub const INTERNET_PROTOCOL_SUPPORT: NamedUuid = NamedUuid::new( + Uuid16::from_be_bytes(0x1820_u16.to_be_bytes()), + "Internet Protocol Support", +); +pub const INDOOR_POSITIONING: NamedUuid = NamedUuid::new( + Uuid16::from_be_bytes(0x1821_u16.to_be_bytes()), + "Indoor Positioning", +); +pub const PULSE_OXIMETER: NamedUuid = NamedUuid::new( + Uuid16::from_be_bytes(0x1822_u16.to_be_bytes()), + "Pulse Oximeter", +); +pub const HTTP_PROXY: NamedUuid = NamedUuid::new( + Uuid16::from_be_bytes(0x1823_u16.to_be_bytes()), + "HTTP_Proxy", +); +pub const TRANSPORT_DISCOVERY: NamedUuid = NamedUuid::new( + Uuid16::from_be_bytes(0x1824_u16.to_be_bytes()), + "Transport Discovery", +); +pub const OBJECT_TRANSFER: NamedUuid = NamedUuid::new( + Uuid16::from_be_bytes(0x1825_u16.to_be_bytes()), + "Object Transfer", +); +pub const FITNESS_MACHINE: NamedUuid = NamedUuid::new( + Uuid16::from_be_bytes(0x1826_u16.to_be_bytes()), + "Fitness Machine", +); +pub const MESH_PROVISIONING: NamedUuid = NamedUuid::new( + Uuid16::from_be_bytes(0x1827_u16.to_be_bytes()), + "Mesh Provisioning", +); +pub const MESH_PROXY: NamedUuid = NamedUuid::new( + Uuid16::from_be_bytes(0x1828_u16.to_be_bytes()), + "Mesh Proxy", +); +pub const RECONNECTION_CONFIGURATION: NamedUuid = NamedUuid::new( + Uuid16::from_be_bytes(0x1829_u16.to_be_bytes()), + "Reconnection Configuration", +); +pub const INSULIN_DELIVERY: NamedUuid = NamedUuid::new( + Uuid16::from_be_bytes(0x183A_u16.to_be_bytes()), + "Insulin Delivery", +); +pub const BINARY_SENSOR: NamedUuid = NamedUuid::new( + Uuid16::from_be_bytes(0x183B_u16.to_be_bytes()), + "Binary Sensor", +); +pub const EMERGENCY_CONFIGURATION: NamedUuid = NamedUuid::new( + Uuid16::from_be_bytes(0x183C_u16.to_be_bytes()), + "Emergency Configuration", +); +pub const PHYSICAL_ACTIVITY_MONITOR: NamedUuid = NamedUuid::new( + Uuid16::from_be_bytes(0x183E_u16.to_be_bytes()), + "Physical Activity Monitor", +); +pub const AUDIO_INPUT_CONTROL: NamedUuid = NamedUuid::new( + Uuid16::from_be_bytes(0x1843_u16.to_be_bytes()), + "Audio Input Control", +); +pub const VOLUME_CONTROL: NamedUuid = NamedUuid::new( + Uuid16::from_be_bytes(0x1844_u16.to_be_bytes()), + "Volume Control", +); +pub const VOLUME_OFFSET_CONTROL: NamedUuid = NamedUuid::new( + Uuid16::from_be_bytes(0x1845_u16.to_be_bytes()), + "Volume Offset Control", +); +pub const COORDINATED_SET_IDENTIFICATION: NamedUuid = NamedUuid::new( + Uuid16::from_be_bytes(0x1846_u16.to_be_bytes()), + "Coordinated Set Identification Service", +); +pub const DEVICE_TIME: NamedUuid = NamedUuid::new( + Uuid16::from_be_bytes(0x1847_u16.to_be_bytes()), + "Device Time", +); +pub const MEDIA_CONTROL: NamedUuid = NamedUuid::new( + Uuid16::from_be_bytes(0x1848_u16.to_be_bytes()), + "Media Control Service", +); +pub const GENERIC_MEDIA_CONTROL: NamedUuid = NamedUuid::new( + Uuid16::from_be_bytes(0x1849_u16.to_be_bytes()), + "Generic Media Control Service", +); +pub const CONSTANT_TONE_EXTENSION: NamedUuid = NamedUuid::new( + Uuid16::from_be_bytes(0x184A_u16.to_be_bytes()), + "Constant Tone Extension", +); +pub const TELEPHONE_BEARER: NamedUuid = NamedUuid::new( + Uuid16::from_be_bytes(0x184B_u16.to_be_bytes()), + "Telephone Bearer Service", +); +pub const GENERIC_TELEPHONE_BEARER: NamedUuid = NamedUuid::new( + Uuid16::from_be_bytes(0x184C_u16.to_be_bytes()), + "Generic Telephone Bearer Service", +); +pub const MICROPHONE_CONTROL: NamedUuid = NamedUuid::new( + Uuid16::from_be_bytes(0x184D_u16.to_be_bytes()), + "Microphone Control", +); + +/// Basic info about a service defined in the BT spec. +#[derive(Debug, Clone)] +pub struct NamedUuid { + uuid: Uuid16, + name: &'static str, +} + +impl NamedUuid { + const fn new(uuid: Uuid16, name: &'static str) -> Self { + Self { uuid, name } + } + + pub fn uuid(&self) -> Uuid16 { + self.uuid + } + pub fn name(&self) -> &'static str { + self.name + } +} diff --git a/rust/src/wrapper/att.rs b/rust/src/wrapper/att.rs new file mode 100644 index 00000000..7d34adb3 --- /dev/null +++ b/rust/src/wrapper/att.rs @@ -0,0 +1,62 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Support for the ATT layer. + +pub use crate::internal::att::{ + AttributePermission, AttributePermissions, AttributeRead, AttributeUuid, AttributeWrite, + MutexRead, NoOpWrite, +}; +use crate::wrapper::core::TryToPy; +use pyo3::prelude::PyModule; +use pyo3::types::PyBytes; +use pyo3::{intern, PyAny, PyResult, Python}; + +impl TryToPy for AttributeUuid { + fn try_to_py<'py>(&self, py: Python<'py>) -> PyResult<&'py PyAny> { + let le_bytes = match self { + AttributeUuid::Uuid16(u) => u.as_le_bytes().to_vec(), + AttributeUuid::Uuid128(u) => u.as_le_bytes().to_vec(), + }; + + PyModule::import(py, intern!(py, "bumble.core"))? + .getattr(intern!(py, "UUID"))? + .getattr(intern!(py, "from_bytes"))? + .call1((PyBytes::new(py, &le_bytes),)) + } +} + +impl TryToPy for AttributePermissions { + fn try_to_py<'py>(&self, py: Python<'py>) -> PyResult<&'py PyAny> { + let all_perm_bits = self.iter().fold(0_u8, |accum, perm| { + accum + | match perm { + // This mapping is what Bumble uses internally; unclear if it's standardized + AttributePermission::Readable => 0x01, + AttributePermission::Writeable => 0x02, + AttributePermission::ReadRequiresEncryption => 0x04, + AttributePermission::WriteRequiresEncryption => 0x08, + AttributePermission::ReadRequiresAuthn => 0x10, + AttributePermission::WriteRequiresAuthn => 0x20, + AttributePermission::ReadRequiresAuthz => 0x40, + AttributePermission::WriteRequiresAuthz => 0x80, + } + }); + + PyModule::import(py, intern!(py, "bumble.att"))? + .getattr(intern!(py, "Attribute"))? + .getattr(intern!(py, "Permissions"))? + .call1((all_perm_bits,)) + } +} diff --git a/rust/src/wrapper/common.rs b/rust/src/wrapper/common.rs index 4947560b..2c27f3dd 100644 --- a/rust/src/wrapper/common.rs +++ b/rust/src/wrapper/common.rs @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/rust/src/wrapper/controller.rs b/rust/src/wrapper/controller.rs index cec10dbf..4a24f387 100644 --- a/rust/src/wrapper/controller.rs +++ b/rust/src/wrapper/controller.rs @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -15,6 +15,7 @@ //! Controller components use crate::wrapper::{ common::{TransportSink, TransportSource}, + core::TryToPy, hci::Address, link::Link, wrap_python_async, PyDictExt, @@ -52,7 +53,10 @@ impl Controller { kwargs.set_opt_item("host_source", host_source)?; kwargs.set_opt_item("host_sink", host_sink)?; kwargs.set_opt_item("link", link)?; - kwargs.set_opt_item("public_address", public_address)?; + kwargs.set_opt_item( + "public_address", + public_address.map(|a| a.try_to_py(py)).transpose()?, + )?; // Controller constructor (`__init__`) is not (and can't be) marked async, but calls // `get_running_loop`, and thus needs wrapped in an async function. diff --git a/rust/src/wrapper/core.rs b/rust/src/wrapper/core.rs index bb171d1d..bb752992 100644 --- a/rust/src/wrapper/core.rs +++ b/rust/src/wrapper/core.rs @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,183 +14,57 @@ //! Core types -use crate::adv::CommonDataTypeCode; -use lazy_static::lazy_static; -use nom::{bytes, combinator}; -use pyo3::{intern, PyObject, PyResult, Python}; -use std::fmt; - -lazy_static! { - static ref BASE_UUID: [u8; 16] = hex::decode("0000000000001000800000805F9B34FB") - .unwrap() - .try_into() - .unwrap(); -} +pub use crate::internal::{ + adv::{AdvertisementDataBuilder, CommonDataType, CommonDataTypeCode, Flags}, + core::{Uuid128, Uuid16, Uuid32}, +}; +use pyo3::{intern, PyAny, PyResult, Python}; /// A type code and data pair from an advertisement pub type AdvertisementDataUnit = (CommonDataTypeCode, Vec); /// Contents of an advertisement -pub struct AdvertisingData(pub(crate) PyObject); +pub struct AdvertisingData { + data_units: Vec, +} impl AdvertisingData { /// Data units in the advertisement contents - pub fn data_units(&self) -> PyResult> { - Python::with_gil(|py| { - let list = self.0.getattr(py, intern!(py, "ad_structures"))?; - - list.as_ref(py) - .iter()? - .collect::, _>>()? - .into_iter() - .map(|tuple| { - let type_code = tuple - .call_method1(intern!(py, "__getitem__"), (0,))? - .extract::()? - .into(); - let data = tuple - .call_method1(intern!(py, "__getitem__"), (1,))? - .extract::>()?; - Ok((type_code, data)) - }) - .collect::, _>>() - }) - } -} - -/// 16-bit UUID -#[derive(PartialEq, Eq, Hash, Clone, Copy)] -pub struct Uuid16 { - /// Big-endian bytes - uuid: [u8; 2], -} - -impl Uuid16 { - /// Construct a UUID from little-endian bytes - pub fn from_le_bytes(mut bytes: [u8; 2]) -> Self { - bytes.reverse(); - Self::from_be_bytes(bytes) - } - - /// Construct a UUID from big-endian bytes - pub fn from_be_bytes(bytes: [u8; 2]) -> Self { - Self { uuid: bytes } - } - - /// The UUID in big-endian bytes form - pub fn as_be_bytes(&self) -> [u8; 2] { - self.uuid - } - - /// The UUID in little-endian bytes form - pub fn as_le_bytes(&self) -> [u8; 2] { - let mut uuid = self.uuid; - uuid.reverse(); - uuid - } - - pub(crate) fn parse_le(input: &[u8]) -> nom::IResult<&[u8], Self> { - combinator::map_res(bytes::complete::take(2_usize), |b: &[u8]| { - b.try_into().map(|mut uuid: [u8; 2]| { - uuid.reverse(); - Self { uuid } - }) - })(input) - } -} - -impl fmt::Debug for Uuid16 { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "UUID-16:{}", hex::encode_upper(self.uuid)) - } -} - -/// 32-bit UUID -#[derive(PartialEq, Eq, Hash)] -pub struct Uuid32 { - /// Big-endian bytes - uuid: [u8; 4], -} - -impl Uuid32 { - /// The UUID in big-endian bytes form - pub fn as_bytes(&self) -> [u8; 4] { - self.uuid - } - - pub(crate) fn parse(input: &[u8]) -> nom::IResult<&[u8], Self> { - combinator::map_res(bytes::complete::take(4_usize), |b: &[u8]| { - b.try_into().map(|mut uuid: [u8; 4]| { - uuid.reverse(); - Self { uuid } - }) - })(input) - } -} - -impl fmt::Debug for Uuid32 { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "UUID-32:{}", hex::encode_upper(self.uuid)) - } -} - -impl From for Uuid32 { - fn from(value: Uuid16) -> Self { - let mut uuid = [0; 4]; - uuid[2..].copy_from_slice(&value.uuid); - Self { uuid } + pub fn data_units(&self) -> &[AdvertisementDataUnit] { + &self.data_units } } -/// 128-bit UUID -#[derive(PartialEq, Eq, Hash)] -pub struct Uuid128 { - /// Big-endian bytes - uuid: [u8; 16], -} - -impl Uuid128 { - /// The UUID in big-endian bytes form - pub fn as_bytes(&self) -> [u8; 16] { - self.uuid - } - - pub(crate) fn parse_le(input: &[u8]) -> nom::IResult<&[u8], Self> { - combinator::map_res(bytes::complete::take(16_usize), |b: &[u8]| { - b.try_into().map(|mut uuid: [u8; 16]| { - uuid.reverse(); - Self { uuid } +impl TryFromPy for AdvertisingData { + fn try_from_py<'py>(py: Python<'py>, obj: &'py PyAny) -> PyResult { + let list = obj.getattr(intern!(py, "ad_structures"))?; + + list.iter()? + .collect::, _>>()? + .into_iter() + .map(|tuple| { + let type_code = tuple + .call_method1(intern!(py, "__getitem__"), (0,))? + .extract::()? + .into(); + let data = tuple + .call_method1(intern!(py, "__getitem__"), (1,))? + .extract::>()?; + Ok((type_code, data)) }) - })(input) + .collect::, _>>() + .map(|data_units| Self { data_units }) } } -impl fmt::Debug for Uuid128 { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{}-{}-{}-{}-{}", - hex::encode_upper(&self.uuid[..4]), - hex::encode_upper(&self.uuid[4..6]), - hex::encode_upper(&self.uuid[6..8]), - hex::encode_upper(&self.uuid[8..10]), - hex::encode_upper(&self.uuid[10..]) - ) - } +/// Fallibly create a [PyAny] with a provided GIL token. +pub trait TryToPy { + /// Build a Python representation of the data in `self` using the provided GIL token `py`. + fn try_to_py<'py>(&self, py: Python<'py>) -> PyResult<&'py PyAny>; } -impl From for Uuid128 { - fn from(value: Uuid16) -> Self { - let mut uuid = *BASE_UUID; - uuid[2..4].copy_from_slice(&value.uuid); - Self { uuid } - } -} - -impl From for Uuid128 { - fn from(value: Uuid32) -> Self { - let mut uuid = *BASE_UUID; - uuid[..4].copy_from_slice(&value.uuid); - Self { uuid } - } +/// Fallibly extract a Rust type from a [PyAny] with a provided GIL token. +pub trait TryFromPy: Sized { + /// Build a Rust representation of the Python object behind the [PyAny]. + fn try_from_py<'py>(py: Python<'py>, obj: &'py PyAny) -> PyResult; } diff --git a/rust/src/wrapper/device/mod.rs b/rust/src/wrapper/device/mod.rs index 82a274a0..3689f724 100644 --- a/rust/src/wrapper/device/mod.rs +++ b/rust/src/wrapper/device/mod.rs @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,40 +14,43 @@ //! Devices and connections to them +use crate::wrapper::gatt::profile::proxy::ProfileServiceProxy; +#[cfg(doc)] +use crate::wrapper::gatt::server::Characteristic; #[cfg(feature = "unstable_extended_adv")] -use crate::wrapper::{ - hci::packets::{ +use crate::wrapper::hci::{ + packets::{ self, AdvertisingEventProperties, AdvertisingFilterPolicy, Enable, EnabledSet, FragmentPreference, LeSetAdvertisingSetRandomAddressBuilder, LeSetExtendedAdvertisingDataBuilder, LeSetExtendedAdvertisingEnableBuilder, LeSetExtendedAdvertisingParametersBuilder, Operation, OwnAddressType, PeerAddressType, PrimaryPhyType, SecondaryPhyType, }, - ConversionError, + AddressConversionError, }; -use crate::{ - adv::AdvertisementDataBuilder, - wrapper::{ - core::AdvertisingData, - gatt_client::{ProfileServiceProxy, ServiceProxy}, - hci::{ - packets::{Command, ErrorCode, Event}, - Address, HciCommand, WithPacketType, - }, - host::Host, - l2cap::LeConnectionOrientedChannel, - transport::{Sink, Source}, - ClosureCallback, PyDictExt, PyObjectExt, +use crate::wrapper::{ + att::AttributeUuid, + core::{AdvertisementDataBuilder, AdvertisingData, TryFromPy, TryToPy}, + gatt::{client::ServiceProxy, server::Service}, + hci::{ + packets::{Command, ErrorCode, Event}, + Address, HciCommand, WithPacketType, }, + host::Host, + l2cap::LeConnectionOrientedChannel, + transport::{Sink, Source}, + wrap_python_async, ClosureCallback, PyDictExt, PyObjectExt, }; +#[cfg(feature = "unstable_extended_adv")] +use anyhow::anyhow; use pyo3::{ exceptions::PyException, intern, - types::{PyDict, PyModule}, - IntoPy, PyErr, PyObject, PyResult, Python, ToPyObject, + types::{PyDict, PyList, PyModule}, + IntoPy, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject, }; use pyo3_asyncio::tokio::into_future; -use std::path; +use std::{collections, path}; #[cfg(test)] mod tests; @@ -92,6 +95,7 @@ enum AdvertisingStatus { /// A device that can send/receive HCI frames. pub struct Device { + /// Python `Device` obj: PyObject, advertising_status: AdvertisingStatus, } @@ -102,7 +106,7 @@ impl Device { /// Creates a Device. When optional arguments are not specified, the Python object specifies the /// defaults. - pub fn new( + pub async fn new( name: Option<&str>, address: Option
, config: Option, @@ -112,51 +116,73 @@ impl Device { Python::with_gil(|py| { let kwargs = PyDict::new(py); kwargs.set_opt_item("name", name)?; - kwargs.set_opt_item("address", address)?; + kwargs.set_opt_item("address", address.map(|a| a.try_to_py(py)).transpose()?)?; kwargs.set_opt_item("config", config)?; kwargs.set_opt_item("host", host)?; kwargs.set_opt_item("generic_access_service", generic_access_service)?; - PyModule::import(py, intern!(py, "bumble.device"))? - .getattr(intern!(py, "Device"))? + let device_ctor = PyModule::import(py, intern!(py, "bumble.device"))? + .getattr(intern!(py, "Device"))?; + + // Needed for Python 3.8-3.9, in which the Semaphore object, when constructed, calls + // `get_event_loop`. + wrap_python_async(py, device_ctor)? .call((), Some(kwargs)) - .map(|any| Self { - obj: any.into(), - advertising_status: AdvertisingStatus::NotAdvertising, - }) + .and_then(into_future) + })? + .await + .map(|obj| Self { + obj, + advertising_status: AdvertisingStatus::NotAdvertising, }) } /// Create a Device per the provided file configured to communicate with a controller through an HCI source/sink - pub fn from_config_file_with_hci( + pub async fn from_config_file_with_hci( device_config: &path::Path, source: Source, sink: Sink, ) -> PyResult { Python::with_gil(|py| { - PyModule::import(py, intern!(py, "bumble.device"))? + let device_ctor = PyModule::import(py, intern!(py, "bumble.device"))? .getattr(intern!(py, "Device"))? - .call_method1( - intern!(py, "from_config_file_with_hci"), - (device_config, source.0, sink.0), - ) - .map(|any| Self { - obj: any.into(), - advertising_status: AdvertisingStatus::NotAdvertising, - }) + .getattr(intern!(py, "from_config_file_with_hci"))?; + + // Needed for Python 3.8-3.9, in which the Semaphore object, when constructed, calls + // `get_event_loop`. + wrap_python_async(py, device_ctor)? + .call((device_config, source.0, sink.0), None) + .and_then(into_future) + })? + .await + .map(|obj| Self { + obj, + advertising_status: AdvertisingStatus::NotAdvertising, }) } /// Create a Device configured to communicate with a controller through an HCI source/sink - pub fn with_hci(name: &str, address: Address, source: Source, sink: Sink) -> PyResult { + pub async fn with_hci( + name: &str, + address: Address, + source: Source, + sink: Sink, + ) -> PyResult { Python::with_gil(|py| { - PyModule::import(py, intern!(py, "bumble.device"))? + let device_ctor = PyModule::import(py, intern!(py, "bumble.device"))? .getattr(intern!(py, "Device"))? - .call_method1(intern!(py, "with_hci"), (name, address.0, source.0, sink.0)) - .map(|any| Self { - obj: any.into(), - advertising_status: AdvertisingStatus::NotAdvertising, - }) + .getattr(intern!(py, "with_hci"))?; + + // Needed for Python 3.8-3.9, in which the Semaphore object, when constructed, calls + // `get_event_loop`. + wrap_python_async(py, device_ctor)? + .call((name, address.try_to_py(py)?, source.0, sink.0), None) + .and_then(into_future) + })? + .await + .map(|obj| Self { + obj, + advertising_status: AdvertisingStatus::NotAdvertising, }) } @@ -199,10 +225,10 @@ impl Device { } /// Connect to a peer - pub async fn connect(&self, peer_addr: &str) -> PyResult { + pub async fn connect(&self, peer_addr: &Address) -> PyResult { Python::with_gil(|py| { self.obj - .call_method1(py, intern!(py, "connect"), (peer_addr,)) + .call_method1(py, intern!(py, "connect"), (peer_addr.try_to_py(py)?,)) .and_then(|coroutine| into_future(coroutine.as_ref(py))) })? .await @@ -244,7 +270,7 @@ impl Device { callback: impl Fn(Python, Advertisement) -> PyResult<()> + Send + 'static, ) -> PyResult<()> { let boxed = ClosureCallback::new(move |py, args, _kwargs| { - callback(py, Advertisement(args.get_item(0)?.into())) + callback(py, Advertisement::try_from_py(py, args.get_item(0)?)?) }); Python::with_gil(|py| { @@ -352,11 +378,10 @@ impl Device { .await?; // set random address - let random_address: packets::Address = - self.random_address()?.try_into().map_err(|e| match e { - ConversionError::Python(pyerr) => pyerr, - ConversionError::Native(e) => PyErr::new::(format!("{e:?}")), - })?; + let random_address: packets::Address = self + .random_address()? + .try_into() + .map_err(|e: AddressConversionError| anyhow!(e))?; let random_address_cmd = LeSetAdvertisingSetRandomAddressBuilder { advertising_handle: Self::ADVERTISING_HANDLE_EXTENDED, random_address, @@ -467,13 +492,65 @@ impl Device { Python::with_gil(|py| { self.obj .getattr(py, intern!(py, "random_address")) - .map(Address) + .and_then(|obj| Address::try_from_py(py, obj.as_ref(py))) }) } + + /// Add a GATT service to the device + pub fn add_service(&mut self, service: &Service) -> PyResult { + Python::with_gil(|py| { + let (char_handles, py_chars): ( + collections::HashMap, + Vec<_>, + ) = service + .characteristics + .iter() + .map(|characteristic| { + characteristic.try_to_py(py).map(|py_characteristic| { + ( + ( + characteristic.uuid, + CharacteristicHandle { + uuid: characteristic.uuid, + obj: py_characteristic.into(), + }, + ), + py_characteristic, + ) + }) + }) + .collect::, _>>()? + .into_iter() + .unzip(); + + let py_service = PyModule::import(py, intern!(py, "bumble.gatt"))? + .getattr(intern!(py, "Service"))? + .call1((service.uuid.try_to_py(py)?, PyList::new(py, py_chars)))?; + + self.obj + .call_method1(py, intern!(py, "add_service"), (py_service,)) + .map(|_| ServiceHandle { + uuid: service.uuid, + char_handles, + }) + }) + } + + /// Notify subscribers to the characteristic + pub async fn notify_subscribers(&mut self, char_handle: &CharacteristicHandle) -> PyResult<()> { + Python::with_gil(|py| { + self.obj + .call_method1(py, intern!(py, "notify_subscribers"), (&char_handle.obj,)) + .and_then(|coroutine| into_future(coroutine.as_ref(py))) + })? + .await + .map(|_| ()) + } } /// A connection to a remote device. -pub struct Connection(PyObject); +// Wraps a `bumble.device.Connection` +pub struct Connection(pub(crate) PyObject); impl Connection { /// Open an L2CAP channel using this connection. When optional arguments are not specified, the @@ -536,6 +613,15 @@ impl Connection { str_obj.gil_ref(py).extract() }) } + + /// Returns the address of the peer on the other end of the connection. + pub fn peer_address(&self) -> PyResult
{ + Python::with_gil(|py| { + self.0 + .getattr(py, intern!(py, "peer_address")) + .and_then(|obj| Address::try_from_py(py, obj.as_ref(py))) + }) + } } /// The other end of a connection @@ -543,13 +629,19 @@ pub struct Peer(PyObject); impl Peer { /// Wrap a [Connection] in a Peer - pub fn new(conn: Connection) -> PyResult { + pub async fn new(conn: Connection) -> PyResult { Python::with_gil(|py| { - PyModule::import(py, intern!(py, "bumble.device"))? - .getattr(intern!(py, "Peer"))? - .call1((conn.0,)) - .map(|obj| Self(obj.into())) - }) + let peer_ctor = + PyModule::import(py, intern!(py, "bumble.device"))?.getattr(intern!(py, "Peer"))?; + + // Needed for Python 3.8-3.9, in which the Semaphore object, when constructed, calls + // `get_event_loop`. + wrap_python_async(py, peer_ctor)? + .call((conn.0,), None) + .and_then(into_future) + })? + .await + .map(Self) } /// Populates the peer's cache of services. @@ -572,6 +664,19 @@ impl Peer { }) } + /// Populate the peer's cache of characteristics for cached services. + /// + /// Should be called after [Peer::discover_services] has cached the available services. + pub async fn discover_characteristics(&mut self) -> PyResult<()> { + Python::with_gil(|py| { + self.0 + .call_method0(py, intern!(py, "discover_characteristics")) + .and_then(|coroutine| into_future(coroutine.as_ref(py))) + })? + .await + .map(|_| ()) + } + /// Returns a snapshot of the Services currently in the peer's cache pub fn services(&self) -> PyResult> { Python::with_gil(|py| { @@ -584,6 +689,24 @@ impl Peer { }) } + /// Returns the services matching the provided UUID. + /// + /// The service cache must have already been populated, e.g. via [Peer::discover_services]. + pub fn services_by_uuid(&self, uuid: impl Into) -> PyResult> { + Python::with_gil(|py| { + self.0 + .call_method1( + py, + intern!(py, "get_services_by_uuid"), + (uuid.into().try_to_py(py)?,), + )? + .as_ref(py) + .iter()? + .map(|r| r.map(|h| ServiceProxy(h.to_object(py)))) + .collect() + }) + } + /// Build a [ProfileServiceProxy] for the specified type. /// [Peer::discover_services] or some other means of populating the Peer's service cache must be /// called first, or the required service won't be found. @@ -599,31 +722,49 @@ impl Peer { } /// A BLE advertisement -pub struct Advertisement(PyObject); +pub struct Advertisement { + address: Address, + connectable: bool, + rssi: i8, + data: AdvertisingData, +} impl Advertisement { /// Address that sent the advertisement - pub fn address(&self) -> PyResult
{ - Python::with_gil(|py| self.0.getattr(py, intern!(py, "address")).map(Address)) + pub fn address(&self) -> Address { + self.address } /// Returns true if the advertisement is connectable - pub fn is_connectable(&self) -> PyResult { - Python::with_gil(|py| { - self.0 - .getattr(py, intern!(py, "is_connectable"))? - .extract::(py) - }) + pub fn is_connectable(&self) -> bool { + self.connectable } /// RSSI of the advertisement - pub fn rssi(&self) -> PyResult { - Python::with_gil(|py| self.0.getattr(py, intern!(py, "rssi"))?.extract::(py)) + pub fn rssi(&self) -> i8 { + self.rssi } /// Data in the advertisement - pub fn data(&self) -> PyResult { - Python::with_gil(|py| self.0.getattr(py, intern!(py, "data")).map(AdvertisingData)) + pub fn data(&self) -> &AdvertisingData { + &self.data + } +} + +impl TryFromPy for Advertisement { + fn try_from_py<'py>(py: Python<'py>, obj: &'py PyAny) -> PyResult { + Ok(Self { + address: obj + .getattr(intern!(py, "address")) + .and_then(|obj| Address::try_from_py(py, obj))?, + connectable: obj + .getattr(intern!(py, "is_connectable"))? + .extract::()?, + rssi: obj.getattr(intern!(py, "rssi"))?.extract::()?, + data: obj + .getattr(intern!(py, "data")) + .and_then(|obj| AdvertisingData::try_from_py(py, obj))?, + }) } } @@ -636,3 +777,60 @@ impl Advertisement { fn default_ignored_peer_address() -> packets::Address { packets::Address::try_from(0x0000_0000_0000_u64).unwrap() } + +/// The result of adding a [Service] to a [Device]'s GATT server. +/// +/// See [Device::add_service] +/// +/// # Background +/// +/// Under the hood this is the same Python `Service` type used as an argument to +/// `Device.add_service`, but that relies on `Service` inheriting `Attribute` so that +/// the attribute protocol's handle can be written during `add_service` directly into +/// the `Service` and `Characteristic` objects. The presence of ATT-internal mutable bookkeeping +/// in the higher level `Service` and `Characteristic` doesn't seem very Rust-idiomatic, +/// so the Python `Service` and `Characteristic` produced by [Service] and +/// [Characteristic] during [Device::add_service] are instead exposed +/// as [ServiceHandle] and [CharacteristicHandle], distinct from the higher level +/// [Service] and [Characteristic]. +pub struct ServiceHandle { + uuid: AttributeUuid, + char_handles: collections::HashMap, + // no need for the `Service` PyObject yet, but could certainly be added if needed +} + +impl ServiceHandle { + /// Returns the UUID for the service + pub fn uuid(&self) -> AttributeUuid { + self.uuid + } + + /// Returns the characteristic handle for the characteristic UUID, if it exists. + pub fn characteristic_handle( + &self, + uuid: impl Into, + ) -> Option<&CharacteristicHandle> { + self.char_handles.get(&uuid.into()) + } + + /// Returns an iterator over the characteristic handlesin the service. + pub fn characteristic_handles(&self) -> impl Iterator { + self.char_handles.values() + } +} + +/// A handle for the result of adding a [Characteristic] as part of a [Service]. +/// +/// See [Device::add_service] and [ServiceHandle]. +pub struct CharacteristicHandle { + uuid: AttributeUuid, + /// Python `Characteristic` + obj: PyObject, +} + +impl CharacteristicHandle { + /// Returns the UUID for the characteristc + pub fn uuid(&self) -> AttributeUuid { + self.uuid + } +} diff --git a/rust/src/wrapper/device/tests.rs b/rust/src/wrapper/device/tests.rs index 648b919b..87a13b83 100644 --- a/rust/src/wrapper/device/tests.rs +++ b/rust/src/wrapper/device/tests.rs @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/rust/src/wrapper/drivers/mod.rs b/rust/src/wrapper/drivers/mod.rs index ff38ac18..163b327f 100644 --- a/rust/src/wrapper/drivers/mod.rs +++ b/rust/src/wrapper/drivers/mod.rs @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/rust/src/wrapper/drivers/rtk.rs b/rust/src/wrapper/drivers/rtk.rs index 1f629d1d..05710f63 100644 --- a/rust/src/wrapper/drivers/rtk.rs +++ b/rust/src/wrapper/drivers/rtk.rs @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/rust/src/wrapper/gatt/client.rs b/rust/src/wrapper/gatt/client.rs new file mode 100644 index 00000000..7ef66457 --- /dev/null +++ b/rust/src/wrapper/gatt/client.rs @@ -0,0 +1,226 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! GATT client support + +use crate::wrapper::att::AttributeUuid; +use crate::wrapper::core::TryToPy; +#[cfg(doc)] +use crate::wrapper::device::Peer; +use crate::wrapper::ClosureCallback; +use anyhow::anyhow; +use log::warn; +use pyo3::types::PyInt; +#[cfg(doc)] +use pyo3::PyAny; +use pyo3::{intern, types::PyBytes, PyObject, PyResult, PyTryFrom, Python, ToPyObject}; +use std::marker; + +/// A GATT service on a remote device +pub struct ServiceProxy(pub(crate) PyObject); + +impl ServiceProxy { + /// Discover the characteristics in this service. + /// + /// Populates an internal cache of characteristics in this service. + pub async fn discover_characteristics(&mut self) -> PyResult<()> { + Python::with_gil(|py| { + self.0 + .call_method0(py, intern!(py, "discover_characteristics")) + .and_then(|coroutine| pyo3_asyncio::tokio::into_future(coroutine.as_ref(py))) + })? + .await + .map(|_| ()) + } + + /// Return the characteristics matching the provided UUID. + pub fn characteristics_by_uuid( + &self, + uuid: impl Into, + ) -> PyResult>>> { + Python::with_gil(|py| { + self.0 + .call_method1( + py, + intern!(py, "get_characteristics_by_uuid"), + (uuid.into().try_to_py(py)?,), + )? + .as_ref(py) + .iter()? + .map(|r| r.map(|h| CharacteristicProxy::new(h.to_object(py)))) + .collect() + }) + } +} + +/// A GATT characteristic on a remote device that converts values from the Python side with `V`. +/// +/// `V` must be selected appropriately for the underlying Python representation of the +/// characteristic value. See the [CharacteristicBaseValue] docs. +pub struct CharacteristicProxy { + obj: PyObject, + value_marker: marker::PhantomData, +} + +impl CharacteristicProxy { + /// Create a new proxy around a Python `Characteristic` + pub(crate) fn new(obj: PyObject) -> Self { + Self { + obj, + value_marker: Default::default(), + } + } + + /// Read the current value of the characteristic + pub async fn read_value(&self) -> PyResult { + Python::with_gil(|py| { + self.obj + .call_method0(py, intern!(py, "read_value")) + .and_then(|obj| pyo3_asyncio::tokio::into_future(obj.as_ref(py))) + })? + .await + .and_then(|obj| { + Python::with_gil(|py| { + obj.downcast::(py) + .map_err(|e| e.into()) + .and_then(V::from_python) + }) + }) + } + + /// Subscribe to changes to the characteristic, executing `callback` for each new value + pub async fn subscribe(&mut self, callback: impl Fn(V) + Send + 'static) -> PyResult<()> { + let boxed = ClosureCallback::new(move |_py, args, _kwargs| { + args.get_item(0) + .and_then(|obj| obj.downcast::().map_err(|e| e.into())) + .and_then(V::from_python) + .map(&callback) + }); + + Python::with_gil(|py| { + self.obj + .call_method1(py, intern!(py, "subscribe"), (boxed,)) + .and_then(|obj| pyo3_asyncio::tokio::into_future(obj.as_ref(py))) + })? + .await + .map(|_| ()) + } +} + +/// Abstracts over the different ways that Python Bumble represents characteristic values. +/// +/// If a characteristic is accessed directly from a [Peer], the underlying representation is a +/// Python `bytes`, and Rust `Vec` is suitable via [PyBytes]. +/// +/// If it's accessed via a `CharacteristicAdapter`, however (e.g. via a `ProfileServiceProxy`), +/// the Python representation depends on the adapter used. A Python `int`, for instance, would +/// require a Rust numeric type like `u8` via [PyInt]. +pub trait CharacteristicBaseValue: Sized + Send { + /// Py03 type to `downcast()` [PyAny] to + type PythonType: for<'a> PyTryFrom<'a>; + + /// Extract `Self` from the downcasted type + fn from_python(py_obj: &Self::PythonType) -> PyResult; +} + +impl CharacteristicBaseValue for Vec { + type PythonType = PyBytes; + + fn from_python(py_obj: &Self::PythonType) -> PyResult { + Ok(py_obj.as_bytes().to_vec()) + } +} + +impl CharacteristicBaseValue for u8 { + type PythonType = PyInt; + + fn from_python(py_obj: &Self::PythonType) -> PyResult { + py_obj.extract() + } +} + +/// A wrapper around a [CharacteristicProxy] for converting to the characteristic value to a +/// more convenient type. +pub struct CharacteristicAdapter +where + V: CharacteristicBaseValue, + A: CharacteristicAdaptedValue, +{ + proxy: CharacteristicProxy, + adapted_marker: marker::PhantomData, +} + +impl CharacteristicAdapter +where + V: CharacteristicBaseValue, + A: CharacteristicAdaptedValue, +{ + /// Create a new adapter wrapping `proxy` + pub fn new(proxy: CharacteristicProxy) -> Self { + Self { + proxy, + adapted_marker: Default::default(), + } + } + + /// Read the value from the characteristic, deserialized into `A`. + pub async fn read_value(&self) -> PyResult { + self.proxy + .read_value() + .await + .and_then(|value| A::deserialize(value).map_err(|e| anyhow!(e).into())) + } + + /// Subscribe to notifications, deserializing each value to `C` before invoking the callback. + /// + /// If deserialization fails, the error will be logged at `warn`. + pub async fn subscribe(&mut self, callback: impl Fn(A) + Send + 'static) -> PyResult<()> { + self.proxy + .subscribe(move |base_value| match A::deserialize(base_value) { + Ok(v) => callback(v), + Err(e) => { + warn!("Could not deserialize value: {}", e) + } + }) + .await + } +} + +/// Provides characteristic value conversions for a specific higher level type. +/// +/// `V` defines the [CharacteristicBaseValue] on top of which the conversion can be applied. +pub trait CharacteristicAdaptedValue: Sized + Send { + /// Deserialize [Self] from a characteristic value's bytes + fn deserialize(base_value: V) -> Result; + /// Serialize [Self] into bytes to write into a characteristic + fn serialize(value: Self) -> V; +} + +/// Error used when [CharacteristicAdaptedValue] fails +#[derive(thiserror::Error, Debug)] +#[error("Value deserialization failed: {0}")] +pub struct CharacteristicValueDeserializationError(anyhow::Error); + +impl CharacteristicAdaptedValue> for u8 { + fn deserialize(base_value: Vec) -> Result { + base_value + .first() + .copied() + .ok_or_else(|| CharacteristicValueDeserializationError(anyhow!("Empty value"))) + } + + fn serialize(value: Self) -> Vec { + vec![value] + } +} diff --git a/rust/src/wrapper/gatt/mod.rs b/rust/src/wrapper/gatt/mod.rs new file mode 100644 index 00000000..828c32a4 --- /dev/null +++ b/rust/src/wrapper/gatt/mod.rs @@ -0,0 +1,32 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! GATT support + +pub use crate::internal::gatt::{CharacteristicProperties, CharacteristicProperty}; +use crate::wrapper::core::TryToPy; +use pyo3::{intern, prelude::PyModule, PyAny, PyResult, Python}; + +pub mod client; +pub mod profile; +pub mod server; + +impl TryToPy for CharacteristicProperties { + fn try_to_py<'py>(&self, py: Python<'py>) -> PyResult<&'py PyAny> { + PyModule::import(py, intern!(py, "bumble.gatt"))? + .getattr(intern!(py, "Characteristic"))? + .getattr(intern!(py, "Properties"))? + .call1((self.bits,)) + } +} diff --git a/rust/src/wrapper/gatt/profile.rs b/rust/src/wrapper/gatt/profile.rs new file mode 100644 index 00000000..a30e686b --- /dev/null +++ b/rust/src/wrapper/gatt/profile.rs @@ -0,0 +1,103 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! GATT profiles. + +use crate::internal::core::Uuid16; +use crate::wrapper::assigned_numbers::services; +use crate::wrapper::device::Peer; +use crate::wrapper::gatt::client::CharacteristicAdapter; +use anyhow::anyhow; +use pyo3::PyResult; + +/// Client for the Battery GATT service. +/// +/// Compare with [proxy::BatteryServiceProxy], which is a wrapper around a Python proxy. +pub struct BatteryClient { + battery_level: CharacteristicAdapter, u8>, +} + +impl BatteryClient { + /// Construct a client for the peer, if the relevant service and characteristic are present. + /// + /// Service and characteristic discovery must already have been done for the peer. + pub fn new(peer: &Peer) -> PyResult { + let service = peer + .services_by_uuid(services::BATTERY.uuid())? + .into_iter() + .next() + .ok_or(anyhow!("Battery service not found"))?; + + let battery_level = service + .characteristics_by_uuid(Uuid16::from(0x2A19_u16))? + .into_iter() + .next() + .ok_or(anyhow!("Battery level characteristic not found")) + .map(CharacteristicAdapter::, u8>::new)?; + + Ok(Self { battery_level }) + } + + /// Characteristic for the battery level. + pub fn battery_level(&self) -> &CharacteristicAdapter, u8> { + &self.battery_level + } +} +pub mod proxy { + //! Support for using the Python class `ProfileServiceProxy` and its subclasses. + //! + //! If there is already a `ProfileServiceProxy` in Python Bumble that you wish to use, implement + //! [ProfileServiceProxy] accordingly. + + use crate::wrapper::gatt::client::CharacteristicProxy; + use crate::wrapper::PyObjectExt; + use pyo3::{intern, PyObject, PyResult, Python}; + + /// Trait to represent Rust wrappers around Python `ProfileServiceProxy` subclasses. + /// + /// Used with [crate::wrapper::device::Peer::create_service_proxy]. + pub trait ProfileServiceProxy { + /// The module containing the proxy class + const PROXY_CLASS_MODULE: &'static str; + /// The module class name + const PROXY_CLASS_NAME: &'static str; + + /// Wrap a PyObject in the Rust wrapper type + fn wrap(obj: PyObject) -> Self; + } + + /// Wrapper around the Python `BatteryServiceProxy` class. + pub struct BatteryServiceProxy(PyObject); + + impl BatteryServiceProxy { + /// Get the battery level, if available + pub fn battery_level(&self) -> PyResult>> { + Python::with_gil(|py| { + self.0 + .getattr(py, intern!(py, "battery_level")) + // uses `PackedCharacteristicAdapter` to expose a Python `int` + .map(|level| level.into_option(CharacteristicProxy::::new)) + }) + } + } + + impl ProfileServiceProxy for BatteryServiceProxy { + const PROXY_CLASS_MODULE: &'static str = "bumble.profiles.battery_service"; + const PROXY_CLASS_NAME: &'static str = "BatteryServiceProxy"; + + fn wrap(obj: PyObject) -> Self { + Self(obj) + } + } +} diff --git a/rust/src/wrapper/gatt/server.rs b/rust/src/wrapper/gatt/server.rs new file mode 100644 index 00000000..e1cb3c7d --- /dev/null +++ b/rust/src/wrapper/gatt/server.rs @@ -0,0 +1,136 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! GATT services + +use crate::internal::att::AttributeWrite; +pub use crate::internal::gatt::server::{Characteristic, CharacteristicValueHandler, Service}; +use crate::wrapper::att::AttributeRead; +use crate::wrapper::core::TryToPy; +use crate::wrapper::device::Connection; +use pyo3::exceptions::PyException; +use pyo3::prelude::PyModule; +use pyo3::types::{PyBytes, PyDict, PyTuple}; +use pyo3::{intern, pyclass, pymethods, Py, PyAny, PyErr, PyObject, PyResult, Python}; +use std::sync; + +impl TryToPy for Characteristic { + fn try_to_py<'py>(&self, py: Python<'py>) -> PyResult<&'py PyAny> { + PyModule::import(py, intern!(py, "bumble.gatt"))? + .getattr(intern!(py, "Characteristic"))? + .call1(( + self.uuid.try_to_py(py)?, + self.properties.try_to_py(py)?, + self.permissions.try_to_py(py)?, + self.value.try_to_py(py)?, + )) + } +} + +impl TryToPy for CharacteristicValueHandler { + fn try_to_py<'py>(&self, py: Python<'py>) -> PyResult<&'py PyAny> { + PyModule::import(py, intern!(py, "bumble.gatt"))? + .getattr(intern!(py, "CharacteristicValue"))? + .call1(( + AttributeReadWrapper { + read: self.read.clone(), + }, + AttributeWriteWrapper { + write: self.write.clone(), + }, + )) + } +} + +/// Python callable used to wrap the attribute read callback +#[pyclass(module = "bumble.rust")] +struct AttributeReadWrapper { + read: sync::Arc>, +} + +#[pymethods] +impl AttributeReadWrapper { + #[allow(unused)] + // Without `signature`, python tries to squeeze the first arg (Connection) into PyTuple + // and of course fails. + #[pyo3(signature = (* args, * * kwargs))] + fn __call__( + &self, + py: Python<'_>, + args: &PyTuple, + kwargs: Option<&PyDict>, + ) -> PyResult { + let callback = self.read.clone(); + let conn = args + .iter() + .next() + .ok_or_else(|| PyErr::new::("Could not get connection")) + .map(|any| Connection(any.into()))?; + + pyo3_asyncio::tokio::future_into_py(py, async move { + callback + .read(conn) + .await + // acquire GIL later, after waiting, then wrap in Py to decouple from that short GIL + .map(|v| Python::with_gil(|py2| Py::::from(PyBytes::new(py2, &v)))) + .map_err(|e| e.into()) + }) + // Between the two GILs and Futures, the lifetimes are confusing, but the compiler agrees + // that decoupling the &PyAny that holds the future from py's lifetime here is ok + .map(|py_any| py_any.into()) + } +} + +/// Like [AttributeReadWrapper] but for attribute writes +#[pyclass(module = "bumble.rust")] +struct AttributeWriteWrapper { + write: sync::Arc>, +} + +#[pymethods] +impl AttributeWriteWrapper { + #[allow(unused)] + #[pyo3(signature = (* args, * * kwargs))] + fn __call__( + &self, + py: Python<'_>, + args: &PyTuple, + kwargs: Option<&PyDict>, + ) -> PyResult { + let bytes = args + .iter() + .nth(1) + .ok_or(PyErr::new::( + "No bytes provided to write callback", + ))? + .downcast::()? + .as_bytes() + .to_vec(); + let conn = args + .iter() + .next() + .ok_or_else(|| PyErr::new::("Could not get connection")) + .map(|any| Connection(any.into()))?; + + let callback = self.write.clone(); + pyo3_asyncio::tokio::future_into_py(py, async move { + callback + .write(bytes, conn) + .await + .map(|_| Python::with_gil(|py2| py2.None())) + .map_err(|_| PyErr::new::("Attribute read failed")) + }) + .map(|py_any| py_any.into()) + } +} diff --git a/rust/src/wrapper/gatt_client.rs b/rust/src/wrapper/gatt_client.rs deleted file mode 100644 index aff1cb21..00000000 --- a/rust/src/wrapper/gatt_client.rs +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright 2023 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! GATT client support - -use crate::wrapper::ClosureCallback; -use pyo3::types::PyTuple; -use pyo3::{intern, PyObject, PyResult, Python}; - -/// A GATT service on a remote device -pub struct ServiceProxy(pub(crate) PyObject); - -impl ServiceProxy { - /// Discover the characteristics in this service. - /// - /// Populates an internal cache of characteristics in this service. - pub async fn discover_characteristics(&mut self) -> PyResult<()> { - Python::with_gil(|py| { - self.0 - .call_method0(py, intern!(py, "discover_characteristics")) - .and_then(|coroutine| pyo3_asyncio::tokio::into_future(coroutine.as_ref(py))) - })? - .await - .map(|_| ()) - } -} - -/// A GATT characteristic on a remote device -pub struct CharacteristicProxy(pub(crate) PyObject); - -impl CharacteristicProxy { - /// Subscribe to changes to the characteristic, executing `callback` for each new value - pub async fn subscribe( - &mut self, - callback: impl Fn(Python, &PyTuple) -> PyResult<()> + Send + 'static, - ) -> PyResult<()> { - let boxed = ClosureCallback::new(move |py, args, _kwargs| callback(py, args)); - - Python::with_gil(|py| { - self.0 - .call_method1(py, intern!(py, "subscribe"), (boxed,)) - .and_then(|obj| pyo3_asyncio::tokio::into_future(obj.as_ref(py))) - })? - .await - .map(|_| ()) - } - - /// Read the current value of the characteristic - pub async fn read_value(&self) -> PyResult { - Python::with_gil(|py| { - self.0 - .call_method0(py, intern!(py, "read_value")) - .and_then(|obj| pyo3_asyncio::tokio::into_future(obj.as_ref(py))) - })? - .await - } -} - -/// Equivalent to the Python `ProfileServiceProxy`. -pub trait ProfileServiceProxy { - /// The module containing the proxy class - const PROXY_CLASS_MODULE: &'static str; - /// The module class name - const PROXY_CLASS_NAME: &'static str; - - /// Wrap a PyObject in the Rust wrapper type - fn wrap(obj: PyObject) -> Self; -} diff --git a/rust/src/wrapper/hci.rs b/rust/src/wrapper/hci.rs index 52480c81..0b9f3b69 100644 --- a/rust/src/wrapper/hci.rs +++ b/rust/src/wrapper/hci.rs @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,16 +14,16 @@ //! HCI +use std::fmt; // re-export here, and internal usages of these imports should refer to this mod, not the internal // mod pub(crate) use crate::internal::hci::WithPacketType; pub use crate::internal::hci::{packets, Error, Packet}; -use crate::wrapper::{ - hci::packets::{AddressType, Command, ErrorCode}, - ConversionError, -}; +use crate::wrapper::core::{TryFromPy, TryToPy}; +use crate::wrapper::hci::packets::{AddressType, Command, ErrorCode}; use itertools::Itertools as _; +use pyo3::types::PyBytes; use pyo3::{ exceptions::PyException, intern, types::PyModule, FromPyObject, IntoPy, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject, @@ -73,100 +73,159 @@ impl IntoPy for HciCommand { } /// A Bluetooth address -#[derive(Clone)] -pub struct Address(pub(crate) PyObject); +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +pub struct Address { + /// Little-endian bytes + le_bytes: [u8; 6], + address_type: AddressType, +} impl Address { - /// Creates a new [Address] object. - pub fn new(address: &str, address_type: AddressType) -> PyResult { - Python::with_gil(|py| { - PyModule::import(py, intern!(py, "bumble.device"))? - .getattr(intern!(py, "Address"))? - .call1((address, address_type)) - .map(|any| Self(any.into())) + /// Creates a new address with the provided little-endian bytes. + pub fn from_le_bytes(le_bytes: [u8; 6], address_type: AddressType) -> Self { + Self { + le_bytes, + address_type, + } + } + + /// Creates a new address with the provided big endian hex (with or without `:` separators). + /// + /// # Examples + /// + /// ``` + /// use bumble::{wrapper::{hci::{Address, packets::AddressType}}}; + /// let hex = "F0:F1:F2:F3:F4:F5"; + /// assert_eq!( + /// hex, + /// Address::from_be_hex(hex, AddressType::PublicDeviceAddress).unwrap().as_be_hex() + /// ); + /// ``` + pub fn from_be_hex( + address: &str, + address_type: AddressType, + ) -> Result { + let filtered: String = address.chars().filter(|c| *c != ':').collect(); + let mut bytes: [u8; 6] = hex::decode(filtered) + .map_err(|_| InvalidAddressHex { address })? + .try_into() + .map_err(|_| InvalidAddressHex { address })?; + bytes.reverse(); + + Ok(Self { + le_bytes: bytes, + address_type, }) } /// The type of address - pub fn address_type(&self) -> PyResult { - Python::with_gil(|py| { - self.0 - .getattr(py, intern!(py, "address_type"))? - .extract::(py)? - .try_into() - .map_err(|addr_type| { - PyErr::new::(format!( - "Failed to convert {addr_type} to AddressType" - )) - }) - }) + pub fn address_type(&self) -> AddressType { + self.address_type } /// True if the address is static - pub fn is_static(&self) -> PyResult { - Python::with_gil(|py| { - self.0 - .getattr(py, intern!(py, "is_static"))? - .extract::(py) - }) + pub fn is_static(&self) -> bool { + !self.is_public() && self.le_bytes[5] >> 6 == 3 + } + + /// True if the address type is [AddressType::PublicIdentityAddress] or + /// [AddressType::PublicDeviceAddress] + pub fn is_public(&self) -> bool { + matches!( + self.address_type, + AddressType::PublicDeviceAddress | AddressType::PublicIdentityAddress + ) } /// True if the address is resolvable - pub fn is_resolvable(&self) -> PyResult { - Python::with_gil(|py| { - self.0 - .getattr(py, intern!(py, "is_resolvable"))? - .extract::(py) - }) + pub fn is_resolvable(&self) -> bool { + matches!( + self.address_type, + AddressType::PublicIdentityAddress | AddressType::RandomIdentityAddress + ) } /// Address bytes in _little-endian_ format - pub fn as_le_bytes(&self) -> PyResult> { - Python::with_gil(|py| { - self.0 - .call_method0(py, intern!(py, "to_bytes"))? - .extract::>(py) - }) + pub fn as_le_bytes(&self) -> [u8; 6] { + self.le_bytes } /// Address bytes as big-endian colon-separated hex - pub fn as_hex(&self) -> PyResult { - self.as_le_bytes().map(|bytes| { - bytes - .into_iter() - .rev() - .map(|byte| hex::encode_upper([byte])) - .join(":") - }) + pub fn as_be_hex(&self) -> String { + self.le_bytes + .into_iter() + .rev() + .map(|byte| hex::encode_upper([byte])) + .join(":") } } -impl ToPyObject for Address { - fn to_object(&self, _py: Python<'_>) -> PyObject { - self.0.clone() +// show a more readable form than default Debug for a byte array +impl fmt::Debug for Address { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "Address {{ address: {}, type: {:?} }}", + self.as_be_hex(), + self.address_type + ) } } -/// An error meaning that the u64 value did not represent a valid BT address. -#[derive(Debug)] -pub struct InvalidAddress(#[allow(unused)] u64); +impl TryToPy for Address { + fn try_to_py<'py>(&self, py: Python<'py>) -> PyResult<&'py PyAny> { + PyModule::import(py, intern!(py, "bumble.device"))? + .getattr(intern!(py, "Address"))? + .call1((PyBytes::new(py, &self.le_bytes), self.address_type)) + } +} + +impl TryFromPy for Address { + fn try_from_py<'py>(py: Python<'py>, obj: &'py PyAny) -> PyResult { + let address_type = obj + .getattr(intern!(py, "address_type"))? + .extract::()? + .try_into() + .map_err(|addr_type| { + PyErr::new::(format!( + "Failed to convert {addr_type} to AddressType" + )) + })?; + + let address = obj.call_method0(intern!(py, "to_bytes"))?.extract()?; + + Ok(Self { + le_bytes: address, + address_type, + }) + } +} + +/// Error type for [Address::from_be_hex]. +#[derive(Debug, thiserror::Error)] +#[error("Invalid address hex: {address}")] +pub struct InvalidAddressHex<'a> { + address: &'a str, +} + +/// An error meaning that the internal u64 value used to convert to [packets::Address] did not +/// represent a valid BT address. +#[derive(Debug, thiserror::Error)] +#[error("Invalid address u64: {0}")] +pub struct AddressConversionError(#[allow(unused)] u64); impl TryInto for Address { - type Error = ConversionError; + type Error = AddressConversionError; fn try_into(self) -> Result { - let addr_le_bytes = self.as_le_bytes().map_err(ConversionError::Python)?; - // packets::Address only supports converting from a u64 (TODO: update if/when it supports converting from [u8; 6] -- https://github.com/google/pdl/issues/75) // So first we take the python `Address` little-endian bytes (6 bytes), copy them into a // [u8; 8] in little-endian format, and finally convert it into a u64. let mut buf = [0_u8; 8]; - buf[0..6].copy_from_slice(&addr_le_bytes); + buf[0..6].copy_from_slice(&self.as_le_bytes()); let address_u64 = u64::from_le_bytes(buf); - packets::Address::try_from(address_u64) - .map_err(InvalidAddress) - .map_err(ConversionError::Native) + packets::Address::try_from(address_u64).map_err(AddressConversionError) } } diff --git a/rust/src/wrapper/host.rs b/rust/src/wrapper/host.rs index 82956648..f70e2cf1 100644 --- a/rust/src/wrapper/host.rs +++ b/rust/src/wrapper/host.rs @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/rust/src/wrapper/l2cap.rs b/rust/src/wrapper/l2cap.rs index 06fbc52f..da866513 100644 --- a/rust/src/wrapper/l2cap.rs +++ b/rust/src/wrapper/l2cap.rs @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/rust/src/wrapper/link.rs b/rust/src/wrapper/link.rs index 7169ef52..bf7abcb7 100644 --- a/rust/src/wrapper/link.rs +++ b/rust/src/wrapper/link.rs @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/rust/src/wrapper/logging.rs b/rust/src/wrapper/logging.rs index bd932cb7..6518e66a 100644 --- a/rust/src/wrapper/logging.rs +++ b/rust/src/wrapper/logging.rs @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/rust/src/wrapper/mod.rs b/rust/src/wrapper/mod.rs index afe437da..b4a1d74b 100644 --- a/rust/src/wrapper/mod.rs +++ b/rust/src/wrapper/mod.rs @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -36,13 +36,14 @@ pub mod controller; pub mod core; pub mod device; pub mod drivers; -pub mod gatt_client; + +pub mod att; +pub mod gatt; pub mod hci; pub mod host; pub mod l2cap; pub mod link; pub mod logging; -pub mod profile; pub mod transport; /// Convenience extensions to [PyObject] @@ -78,11 +79,23 @@ impl PyObjectExt for PyObject { /// Convenience extensions to [PyDict] pub trait PyDictExt { + /// Build a dict from name/value pairs. + fn from_pairs<'py>(py: Python<'py>, pairs: &[(&str, impl ToPyObject)]) -> PyResult<&'py Self>; /// Set item in dict only if value is Some, otherwise do nothing. fn set_opt_item(&self, key: K, value: Option) -> PyResult<()>; } impl PyDictExt for PyDict { + fn from_pairs<'py>(py: Python<'py>, pairs: &[(&str, impl ToPyObject)]) -> PyResult<&'py Self> { + let dict = PyDict::new(py); + + for (name, value) in pairs { + dict.set_item(name, value)?; + } + + Ok(dict) + } + fn set_opt_item(&self, key: K, value: Option) -> PyResult<()> { if let Some(value) = value { self.set_item(key, value)? @@ -94,10 +107,11 @@ impl PyDictExt for PyDict { /// Wrapper to make Rust closures ([Fn] implementations) callable from Python. /// /// The Python callable form returns a Python `None`. -#[pyclass(name = "SubscribeCallback")] +#[pyclass(module = "bumble.rust")] pub(crate) struct ClosureCallback { // can't use generics in a pyclass, so have to box #[allow(clippy::type_complexity)] + // TODO should this require Sync? callback: Box) -> PyResult<()> + Send + 'static>, } @@ -114,7 +128,7 @@ impl ClosureCallback { #[pymethods] impl ClosureCallback { - #[pyo3(signature = (*args, **kwargs))] + #[pyo3(signature = (* args, * * kwargs))] fn __call__( &self, py: Python<'_>, diff --git a/rust/src/wrapper/profile.rs b/rust/src/wrapper/profile.rs deleted file mode 100644 index fc473ff9..00000000 --- a/rust/src/wrapper/profile.rs +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright 2023 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! GATT profiles - -use crate::wrapper::{ - gatt_client::{CharacteristicProxy, ProfileServiceProxy}, - PyObjectExt, -}; -use pyo3::{intern, PyObject, PyResult, Python}; - -/// Exposes the battery GATT service -pub struct BatteryServiceProxy(PyObject); - -impl BatteryServiceProxy { - /// Get the battery level, if available - pub fn battery_level(&self) -> PyResult> { - Python::with_gil(|py| { - self.0 - .getattr(py, intern!(py, "battery_level")) - .map(|level| level.into_option(CharacteristicProxy)) - }) - } -} - -impl ProfileServiceProxy for BatteryServiceProxy { - const PROXY_CLASS_MODULE: &'static str = "bumble.profiles.battery_service"; - const PROXY_CLASS_NAME: &'static str = "BatteryServiceProxy"; - - fn wrap(obj: PyObject) -> Self { - Self(obj) - } -} diff --git a/rust/src/wrapper/transport.rs b/rust/src/wrapper/transport.rs index 8c626872..bd4d24b5 100644 --- a/rust/src/wrapper/transport.rs +++ b/rust/src/wrapper/transport.rs @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/rust/tools/file_header.rs b/rust/tools/file_header.rs index fb3286d7..2af00e83 100644 --- a/rust/tools/file_header.rs +++ b/rust/tools/file_header.rs @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/rust/tools/gen_assigned_numbers.rs b/rust/tools/gen_assigned_numbers.rs index b2c525e1..b12a25e2 100644 --- a/rust/tools/gen_assigned_numbers.rs +++ b/rust/tools/gen_assigned_numbers.rs @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -82,7 +82,7 @@ fn load_company_ids() -> PyResult> { }) } -const LICENSE_HEADER: &str = r#"// Copyright 2023 Google LLC +const LICENSE_HEADER: &str = r#"// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. From 19dc37d59b5ac1356cc4f5491b367ba76e5c05f7 Mon Sep 17 00:00:00 2001 From: Marshall Pierce Date: Thu, 4 Apr 2024 15:53:59 -0600 Subject: [PATCH 2/2] Address PR feedback --- bumble/transport/tcp_server.py | 1 + rust/CHANGELOG.md | 7 ++ rust/examples/battery_service.rs | 42 +++++++++-- rust/pytests/rootcanal.rs | 5 ++ rust/src/cli/firmware/rtk.rs | 6 +- rust/src/internal/hci/mod.rs | 108 +++++++++++++++++++++++++++- rust/src/lib.rs | 7 ++ rust/src/wrapper/hci.rs | 118 +------------------------------ rust/src/wrapper/mod.rs | 3 +- 9 files changed, 171 insertions(+), 126 deletions(-) diff --git a/bumble/transport/tcp_server.py b/bumble/transport/tcp_server.py index 8991ead0..0a648fd3 100644 --- a/bumble/transport/tcp_server.py +++ b/bumble/transport/tcp_server.py @@ -30,6 +30,7 @@ # ----------------------------------------------------------------------------- + # A pass-through function to ease mock testing. async def _create_server(*args, **kw_args): await asyncio.get_running_loop().create_server(*args, **kw_args) diff --git a/rust/CHANGELOG.md b/rust/CHANGELOG.md index ce575a25..d1379856 100644 --- a/rust/CHANGELOG.md +++ b/rust/CHANGELOG.md @@ -1,3 +1,10 @@ +# Next + +- GATT server support +- Tests w/ rootcanal +- Battery service example +- Address is now pure Rust that's convertible to its Python equivalent rather than just holding a PyObject + # 0.2.0 - Code-gen company ID table diff --git a/rust/examples/battery_service.rs b/rust/examples/battery_service.rs index 9c801ce9..84ce87c5 100644 --- a/rust/examples/battery_service.rs +++ b/rust/examples/battery_service.rs @@ -31,6 +31,20 @@ //! client \ //! --target-addr F0:F1:F2:F3:F4:F5 //! ``` +//! +//! Any combo will work, e.g. a Rust server and Python client: +//! +//! ``` +//! PYTHONPATH=..:/path/to/virtualenv/site-packages/ \ +//! cargo run --example battery_service -- \ +//! --transport android-netsim \ +//! server +//! ``` +//! +//! ``` +//! PYTHONPATH=. python examples/battery_client.py \ +//! android-netsim F0:F1:F2:F3:F4:F5 +//! ``` use anyhow::anyhow; use async_trait::async_trait; @@ -53,6 +67,8 @@ use owo_colors::OwoColorize; use pyo3::prelude::*; use rand::Rng; use std::time::Duration; +use tokio::select; +use tokio_util::sync::CancellationToken; #[pyo3_asyncio::tokio::main] async fn main() -> PyResult<()> { @@ -116,7 +132,7 @@ async fn run_client(device: Device, target_addr: Address) -> anyhow::Result<()> } async fn run_server(mut device: Device) -> anyhow::Result<()> { - let uuid = services::BATTERY.uuid(); + let battery_service_uuid = services::BATTERY.uuid(); let battery_level_uuid = Uuid16::from(0x2A19).into(); let battery_level = Characteristic::new( battery_level_uuid, @@ -124,14 +140,14 @@ async fn run_server(mut device: Device) -> anyhow::Result<()> { AttributePermission::Readable.into(), CharacteristicValueHandler::new(Box::new(BatteryRead), Box::new(NoOpWrite)), ); - let service = Service::new(uuid.into(), vec![battery_level]); + let service = Service::new(battery_service_uuid.into(), vec![battery_level]); let service_handle = device.add_service(&service)?; let mut builder = AdvertisementDataBuilder::new(); builder.append(CommonDataType::CompleteLocalName, "Bumble Battery")?; builder.append( CommonDataType::IncompleteListOf16BitServiceClassUuids, - &uuid, + &battery_service_uuid, )?; builder.append( CommonDataType::Appearance, @@ -146,10 +162,28 @@ async fn run_server(mut device: Device) -> anyhow::Result<()> { .characteristic_handle(battery_level_uuid) .expect("Battery level should be present"); + let cancellation_token = CancellationToken::new(); + + let ct = cancellation_token.clone(); + tokio::spawn(async move { + let _ = tokio::signal::ctrl_c().await; + println!("Ctrl-C caught"); + ct.cancel(); + }); + loop { - tokio::time::sleep(Duration::from_secs(3)).await; + select! { + _ = tokio::time::sleep(Duration::from_secs(3)) => {} + _ = cancellation_token.cancelled() => { break } + } + device.notify_subscribers(char_handle).await?; } + + println!("Stopping advertising"); + device.stop_advertising().await?; + + Ok(()) } struct BatteryRead; diff --git a/rust/pytests/rootcanal.rs b/rust/pytests/rootcanal.rs index 6ccfba8d..46bcfe2f 100644 --- a/rust/pytests/rootcanal.rs +++ b/rust/pytests/rootcanal.rs @@ -271,11 +271,16 @@ async fn with_dir_lock( closure: impl Future>, ) -> anyhow::Result { // wait until we can create the dir + let mut printed_contention_msg = false; loop { match fs::create_dir(dir) { Ok(_) => break, Err(e) => { if e.kind() == io::ErrorKind::AlreadyExists { + if !printed_contention_msg { + printed_contention_msg = true; + eprintln!("Dir lock contention; sleeping"); + } tokio::time::sleep(std::time::Duration::from_millis(50)).await; } else { warn!( diff --git a/rust/src/cli/firmware/rtk.rs b/rust/src/cli/firmware/rtk.rs index 303a6423..d002b94c 100644 --- a/rust/src/cli/firmware/rtk.rs +++ b/rust/src/cli/firmware/rtk.rs @@ -16,6 +16,7 @@ use crate::{Download, Source}; use anyhow::anyhow; +use bumble::project_dir; use bumble::wrapper::{ drivers::rtk::{Driver, DriverInfo, Firmware}, host::{DriverFactory, Host}, @@ -28,10 +29,7 @@ use std::{fs, path}; pub(crate) async fn download(dl: Download) -> PyResult<()> { let data_dir = dl .output_dir - .or_else(|| { - directories::ProjectDirs::from("com", "google", "bumble") - .map(|pd| pd.data_local_dir().join("firmware").join("realtek")) - }) + .or_else(|| project_dir().map(|pd| pd.data_local_dir().join("firmware").join("realtek"))) .unwrap_or_else(|| { eprintln!("Could not determine standard data directory"); path::PathBuf::from(".") diff --git a/rust/src/internal/hci/mod.rs b/rust/src/internal/hci/mod.rs index 1c76ac14..b9269c9f 100644 --- a/rust/src/internal/hci/mod.rs +++ b/rust/src/internal/hci/mod.rs @@ -12,9 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. +use itertools::Itertools; pub use pdl_runtime::{Error, Packet}; +use std::fmt; -use crate::internal::hci::packets::{Acl, Command, Event, Sco}; +use crate::internal::hci::packets::{Acl, AddressType, Command, Event, Sco}; use pdl_derive::pdl; #[allow(missing_docs, warnings, clippy::all)] @@ -23,6 +25,110 @@ pub mod packets {} #[cfg(test)] mod tests; +/// A Bluetooth address +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +pub struct Address { + /// Little-endian bytes + le_bytes: [u8; 6], + address_type: AddressType, +} + +impl Address { + /// Creates a new address with the provided little-endian bytes. + pub fn from_le_bytes(le_bytes: [u8; 6], address_type: AddressType) -> Self { + Self { + le_bytes, + address_type, + } + } + + /// Creates a new address with the provided big endian hex (with or without `:` separators). + /// + /// # Examples + /// + /// ``` + /// use bumble::{wrapper::{hci::{Address, packets::AddressType}}}; + /// let hex = "F0:F1:F2:F3:F4:F5"; + /// assert_eq!( + /// hex, + /// Address::from_be_hex(hex, AddressType::PublicDeviceAddress).unwrap().as_be_hex() + /// ); + /// ``` + pub fn from_be_hex( + address: &str, + address_type: AddressType, + ) -> Result { + let filtered: String = address.chars().filter(|c| *c != ':').collect(); + let mut bytes: [u8; 6] = hex::decode(filtered) + .map_err(|_| InvalidAddressHex { address })? + .try_into() + .map_err(|_| InvalidAddressHex { address })?; + bytes.reverse(); + + Ok(Self { + le_bytes: bytes, + address_type, + }) + } + + /// The type of address + pub fn address_type(&self) -> AddressType { + self.address_type + } + + /// True if the address is static + pub fn is_static(&self) -> bool { + !self.is_public() && self.le_bytes[5] >> 6 == 3 + } + + /// True if the address type is [AddressType::PublicIdentityAddress] or + /// [AddressType::PublicDeviceAddress] + pub fn is_public(&self) -> bool { + matches!( + self.address_type, + AddressType::PublicDeviceAddress | AddressType::PublicIdentityAddress + ) + } + + /// True if the address is resolvable + pub fn is_resolvable(&self) -> bool { + self.address_type == AddressType::RandomDeviceAddress && self.le_bytes[5] >> 6 == 1 + } + + /// Address bytes in _little-endian_ format + pub fn as_le_bytes(&self) -> [u8; 6] { + self.le_bytes + } + + /// Address bytes as big-endian colon-separated hex + pub fn as_be_hex(&self) -> String { + self.le_bytes + .into_iter() + .rev() + .map(|byte| hex::encode_upper([byte])) + .join(":") + } +} + +// show a more readable form than default Debug for a byte array +impl fmt::Debug for Address { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "Address {{ address: {}, type: {:?} }}", + self.as_be_hex(), + self.address_type + ) + } +} + +/// Error type for [Address::from_be_hex]. +#[derive(Debug, thiserror::Error)] +#[error("Invalid address hex: {address}")] +pub struct InvalidAddressHex<'a> { + address: &'a str, +} + /// HCI Packet type, prepended to the packet. /// Rootcanal's PDL declaration excludes this from ser/deser and instead is implemented in code. /// To maintain the ability to easily use future versions of their packet PDL, packet type is diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 8e355656..8e24dddb 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -30,3 +30,10 @@ pub mod wrapper; pub use internal::adv; pub(crate) mod internal; + +/// Directory for Bumble local storage for the current user according to the OS's conventions, +/// if a convention is known for the current OS +#[cfg(any(feature = "bumble-tools", test))] +pub fn project_dir() -> Option { + directories::ProjectDirs::from("com", "google", "bumble") +} diff --git a/rust/src/wrapper/hci.rs b/rust/src/wrapper/hci.rs index 0b9f3b69..d15579de 100644 --- a/rust/src/wrapper/hci.rs +++ b/rust/src/wrapper/hci.rs @@ -14,15 +14,13 @@ //! HCI -use std::fmt; // re-export here, and internal usages of these imports should refer to this mod, not the internal // mod pub(crate) use crate::internal::hci::WithPacketType; -pub use crate::internal::hci::{packets, Error, Packet}; +pub use crate::internal::hci::{packets, Address, Error, InvalidAddressHex, Packet}; use crate::wrapper::core::{TryFromPy, TryToPy}; use crate::wrapper::hci::packets::{AddressType, Command, ErrorCode}; -use itertools::Itertools as _; use pyo3::types::PyBytes; use pyo3::{ exceptions::PyException, intern, types::PyModule, FromPyObject, IntoPy, PyAny, PyErr, PyObject, @@ -72,111 +70,11 @@ impl IntoPy for HciCommand { } } -/// A Bluetooth address -#[derive(Clone, Copy, PartialEq, Eq, Hash)] -pub struct Address { - /// Little-endian bytes - le_bytes: [u8; 6], - address_type: AddressType, -} - -impl Address { - /// Creates a new address with the provided little-endian bytes. - pub fn from_le_bytes(le_bytes: [u8; 6], address_type: AddressType) -> Self { - Self { - le_bytes, - address_type, - } - } - - /// Creates a new address with the provided big endian hex (with or without `:` separators). - /// - /// # Examples - /// - /// ``` - /// use bumble::{wrapper::{hci::{Address, packets::AddressType}}}; - /// let hex = "F0:F1:F2:F3:F4:F5"; - /// assert_eq!( - /// hex, - /// Address::from_be_hex(hex, AddressType::PublicDeviceAddress).unwrap().as_be_hex() - /// ); - /// ``` - pub fn from_be_hex( - address: &str, - address_type: AddressType, - ) -> Result { - let filtered: String = address.chars().filter(|c| *c != ':').collect(); - let mut bytes: [u8; 6] = hex::decode(filtered) - .map_err(|_| InvalidAddressHex { address })? - .try_into() - .map_err(|_| InvalidAddressHex { address })?; - bytes.reverse(); - - Ok(Self { - le_bytes: bytes, - address_type, - }) - } - - /// The type of address - pub fn address_type(&self) -> AddressType { - self.address_type - } - - /// True if the address is static - pub fn is_static(&self) -> bool { - !self.is_public() && self.le_bytes[5] >> 6 == 3 - } - - /// True if the address type is [AddressType::PublicIdentityAddress] or - /// [AddressType::PublicDeviceAddress] - pub fn is_public(&self) -> bool { - matches!( - self.address_type, - AddressType::PublicDeviceAddress | AddressType::PublicIdentityAddress - ) - } - - /// True if the address is resolvable - pub fn is_resolvable(&self) -> bool { - matches!( - self.address_type, - AddressType::PublicIdentityAddress | AddressType::RandomIdentityAddress - ) - } - - /// Address bytes in _little-endian_ format - pub fn as_le_bytes(&self) -> [u8; 6] { - self.le_bytes - } - - /// Address bytes as big-endian colon-separated hex - pub fn as_be_hex(&self) -> String { - self.le_bytes - .into_iter() - .rev() - .map(|byte| hex::encode_upper([byte])) - .join(":") - } -} - -// show a more readable form than default Debug for a byte array -impl fmt::Debug for Address { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "Address {{ address: {}, type: {:?} }}", - self.as_be_hex(), - self.address_type - ) - } -} - impl TryToPy for Address { fn try_to_py<'py>(&self, py: Python<'py>) -> PyResult<&'py PyAny> { PyModule::import(py, intern!(py, "bumble.device"))? .getattr(intern!(py, "Address"))? - .call1((PyBytes::new(py, &self.le_bytes), self.address_type)) + .call1((PyBytes::new(py, &self.as_le_bytes()), self.address_type())) } } @@ -194,20 +92,10 @@ impl TryFromPy for Address { let address = obj.call_method0(intern!(py, "to_bytes"))?.extract()?; - Ok(Self { - le_bytes: address, - address_type, - }) + Ok(Self::from_le_bytes(address, address_type)) } } -/// Error type for [Address::from_be_hex]. -#[derive(Debug, thiserror::Error)] -#[error("Invalid address hex: {address}")] -pub struct InvalidAddressHex<'a> { - address: &'a str, -} - /// An error meaning that the internal u64 value used to convert to [packets::Address] did not /// represent a valid BT address. #[derive(Debug, thiserror::Error)] diff --git a/rust/src/wrapper/mod.rs b/rust/src/wrapper/mod.rs index b4a1d74b..07ace34e 100644 --- a/rust/src/wrapper/mod.rs +++ b/rust/src/wrapper/mod.rs @@ -31,13 +31,12 @@ use pyo3::{ }; pub mod assigned_numbers; +pub mod att; pub mod common; pub mod controller; pub mod core; pub mod device; pub mod drivers; - -pub mod att; pub mod gatt; pub mod hci; pub mod host;