diff options
author | Gabriel White-Vega <gwhitevega@google.com> | 2023-10-10 11:46:59 -0400 |
---|---|---|
committer | Gabriel White-Vega <gwhitevega@google.com> | 2023-10-10 13:35:31 -0400 |
commit | 1051648ffb1a1827404a9c8008d4aa486d4c1208 (patch) | |
tree | eb49b08b6d28e72f72f8ad77a35f941a51ab3dec /rust | |
parent | a1b55b94e03e037a0b47d99d7323a01ed3ab3afb (diff) | |
download | bumble-1051648ffb1a1827404a9c8008d4aa486d4c1208.tar.gz |
Add support for extended advertising via Rust-only API
* Extended functionality is gated on an "unstable" feature
* Designed for very simple use and minimal interferance with existing legacy implementation
* Intended to be temporary, until bumble can integrate extended advertising into its core functionality
* Dropped `HciCommandWrapper` in favor of using bumble's `HCI_Command.from_bytes` for converting from PDL into bumble implementation
* Refactored Address and Device constructors to better match what the python constructors expect
Diffstat (limited to 'rust')
-rw-r--r-- | rust/Cargo.toml | 10 | ||||
-rw-r--r-- | rust/examples/battery_client.rs | 9 | ||||
-rw-r--r-- | rust/examples/broadcast.rs | 26 | ||||
-rw-r--r-- | rust/examples/scanner.rs | 12 | ||||
-rw-r--r-- | rust/pytests/wrapper.rs | 77 | ||||
-rw-r--r-- | rust/pytests/wrapper/drivers.rs | 22 | ||||
-rw-r--r-- | rust/pytests/wrapper/hci.rs | 86 | ||||
-rw-r--r-- | rust/pytests/wrapper/mod.rs | 17 | ||||
-rw-r--r-- | rust/pytests/wrapper/transport.rs | 31 | ||||
-rw-r--r-- | rust/src/internal/hci/mod.rs | 12 | ||||
-rw-r--r-- | rust/src/internal/hci/tests.rs | 17 | ||||
-rw-r--r-- | rust/src/wrapper/device/mod.rs (renamed from rust/src/wrapper/device.rs) | 224 | ||||
-rw-r--r-- | rust/src/wrapper/device/tests.rs | 23 | ||||
-rw-r--r-- | rust/src/wrapper/hci.rs | 84 | ||||
-rw-r--r-- | rust/src/wrapper/mod.rs | 9 |
15 files changed, 501 insertions, 158 deletions
diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 35a0f4c..a3c63ba 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -74,6 +74,11 @@ name = "bumble" path = "src/main.rs" required-features = ["bumble-tools"] +[[example]] +name = "broadcast" +path = "examples/broadcast.rs" +required-features = ["unstable_extended_adv"] + # test entry point that uses pyo3_asyncio's test harness [[test]] name = "pytests" @@ -86,4 +91,9 @@ 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", "dep:futures"] + +# all the unstable features +unstable = ["unstable_extended_adv"] +unstable_extended_adv = [] + default = [] diff --git a/rust/examples/battery_client.rs b/rust/examples/battery_client.rs index 007ccb6..613d9e8 100644 --- a/rust/examples/battery_client.rs +++ b/rust/examples/battery_client.rs @@ -33,6 +33,7 @@ use bumble::wrapper::{ device::{Device, Peer}, + hci::{packets::AddressType, Address}, profile::BatteryServiceProxy, transport::Transport, PyObjectExt, @@ -52,12 +53,8 @@ async fn main() -> PyResult<()> { let transport = Transport::open(cli.transport).await?; - let device = Device::with_hci( - "Bumble", - "F0:F1:F2:F3:F4:F5", - transport.source()?, - transport.sink()?, - )?; + 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?; diff --git a/rust/examples/broadcast.rs b/rust/examples/broadcast.rs index f87b644..affe21e 100644 --- a/rust/examples/broadcast.rs +++ b/rust/examples/broadcast.rs @@ -63,17 +63,28 @@ async fn main() -> PyResult<()> { ) .map_err(|e| anyhow!(e))?; - device.set_advertising_data(adv_data)?; device.power_on().await?; - println!("Advertising..."); - device.start_advertising(true).await?; + if cli.extended { + println!("Starting extended advertisement..."); + device.start_advertising_extended(adv_data).await?; + } else { + device.set_advertising_data(adv_data)?; + + println!("Starting legacy advertisement..."); + device.start_advertising(true).await?; + } // wait until user kills the process tokio::signal::ctrl_c().await?; - println!("Stopping..."); - device.stop_advertising().await?; + if cli.extended { + println!("Stopping extended advertisement..."); + device.stop_advertising_extended().await?; + } else { + println!("Stopping legacy advertisement..."); + device.stop_advertising().await?; + } Ok(()) } @@ -86,12 +97,17 @@ struct Cli { /// See, for instance, `examples/device1.json` in the Python project. #[arg(long)] device_config: path::PathBuf, + /// Bumble transport spec. /// /// <https://google.github.io/bumble/transports/index.html> #[arg(long)] transport: String, + /// Whether to perform an extended (BT 5.0) advertisement + #[arg(long)] + extended: bool, + /// Log HCI commands #[arg(long)] log_hci: bool, diff --git a/rust/examples/scanner.rs b/rust/examples/scanner.rs index ec931b5..21292d6 100644 --- a/rust/examples/scanner.rs +++ b/rust/examples/scanner.rs @@ -20,7 +20,9 @@ use bumble::{ adv::CommonDataType, wrapper::{ - core::AdvertisementDataUnit, device::Device, hci::packets::AddressType, + core::AdvertisementDataUnit, + device::Device, + hci::{packets::AddressType, Address}, transport::Transport, }, }; @@ -44,12 +46,8 @@ async fn main() -> PyResult<()> { let transport = Transport::open(cli.transport).await?; - let mut device = Device::with_hci( - "Bumble", - "F0:F1:F2:F3:F4:F5", - transport.source()?, - transport.sink()?, - )?; + let address = Address::new("F0:F1:F2:F3:F4:F5", AddressType::RandomDeviceAddress)?; + let mut device = Device::with_hci("Bumble", address, transport.source()?, transport.sink()?)?; // in practice, devices can send multiple advertisements from the same address, so we keep // track of a timestamp for each set of data diff --git a/rust/pytests/wrapper.rs b/rust/pytests/wrapper.rs deleted file mode 100644 index 9fd65e7..0000000 --- a/rust/pytests/wrapper.rs +++ /dev/null @@ -1,77 +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. - -use bumble::wrapper::{ - controller::Controller, - device::Device, - drivers::rtk::DriverInfo, - hci::{ - packets::{ - AddressType, ErrorCode, ReadLocalVersionInformationBuilder, - ReadLocalVersionInformationComplete, - }, - Address, Error, - }, - host::Host, - link::Link, - transport::Transport, -}; -use nix::sys::stat::Mode; -use pyo3::{ - exceptions::PyException, - {PyErr, PyResult}, -}; - -#[pyo3_asyncio::tokio::test] -async fn fifo_transport_can_open() -> PyResult<()> { - let dir = tempfile::tempdir().unwrap(); - let mut fifo = dir.path().to_path_buf(); - fifo.push("bumble-transport-fifo"); - nix::unistd::mkfifo(&fifo, Mode::S_IRWXU).unwrap(); - - let mut t = Transport::open(format!("file:{}", fifo.to_str().unwrap())).await?; - - t.close().await?; - - Ok(()) -} - -#[pyo3_asyncio::tokio::test] -async fn realtek_driver_info_all_drivers() -> PyResult<()> { - assert_eq!(12, DriverInfo::all_drivers()?.len()); - Ok(()) -} - -#[pyo3_asyncio::tokio::test] -async fn hci_command_wrapper_has_correct_methods() -> PyResult<()> { - let address = Address::new("F0:F1:F2:F3:F4:F5", &AddressType::RandomDeviceAddress)?; - let link = Link::new_local_link()?; - let controller = Controller::new("C1", None, None, Some(link), Some(address.clone())).await?; - let host = Host::new(controller.clone().into(), controller.into()).await?; - let device = Device::new(None, Some(address), None, Some(host), None)?; - - device.power_on().await?; - - // Send some simple command. A successful response means [HciCommandWrapper] has the minimum - // required interface for the Python code to think its an [HCI_Command] object. - let command = ReadLocalVersionInformationBuilder {}; - let event: ReadLocalVersionInformationComplete = device - .send_command(&command.into(), true) - .await? - .try_into() - .map_err(|e: Error| PyErr::new::<PyException, _>(e.to_string()))?; - - assert_eq!(ErrorCode::Success, event.get_status()); - Ok(()) -} diff --git a/rust/pytests/wrapper/drivers.rs b/rust/pytests/wrapper/drivers.rs new file mode 100644 index 0000000..d2517eb --- /dev/null +++ b/rust/pytests/wrapper/drivers.rs @@ -0,0 +1,22 @@ +// 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. + +use bumble::wrapper::drivers::rtk::DriverInfo; +use pyo3::PyResult; + +#[pyo3_asyncio::tokio::test] +async fn realtek_driver_info_all_drivers() -> PyResult<()> { + assert_eq!(12, DriverInfo::all_drivers()?.len()); + Ok(()) +} diff --git a/rust/pytests/wrapper/hci.rs b/rust/pytests/wrapper/hci.rs new file mode 100644 index 0000000..c4ce20d --- /dev/null +++ b/rust/pytests/wrapper/hci.rs @@ -0,0 +1,86 @@ +// 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. + +use bumble::wrapper::{ + controller::Controller, + device::Device, + hci::{ + packets::{ + AddressType, Enable, ErrorCode, LeScanType, LeScanningFilterPolicy, + LeSetScanEnableBuilder, LeSetScanEnableComplete, LeSetScanParametersBuilder, + LeSetScanParametersComplete, OwnAddressType, + }, + Address, Error, + }, + host::Host, + link::Link, +}; +use pyo3::{ + exceptions::PyException, + {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 device = create_local_device(address).await?; + + device.power_on().await?; + + // BLE Spec Core v5.3 + // 7.8.9 LE Set Scan Parameters command + // ... + // The Host shall not issue this command when scanning is enabled in the + // Controller; if it is the Command Disallowed error code shall be used. + // ... + + let command = LeSetScanEnableBuilder { + filter_duplicates: Enable::Disabled, + // will cause failure later + le_scan_enable: Enable::Enabled, + }; + + let event: LeSetScanEnableComplete = device + .send_command(command.into(), false) + .await? + .try_into() + .map_err(|e: Error| PyErr::new::<PyException, _>(e.to_string()))?; + + assert_eq!(ErrorCode::Success, event.get_status()); + + let command = LeSetScanParametersBuilder { + le_scan_type: LeScanType::Passive, + le_scan_interval: 0, + le_scan_window: 0, + own_address_type: OwnAddressType::RandomDeviceAddress, + scanning_filter_policy: LeScanningFilterPolicy::AcceptAll, + }; + + let event: LeSetScanParametersComplete = device + .send_command(command.into(), false) + .await? + .try_into() + .map_err(|e: Error| PyErr::new::<PyException, _>(e.to_string()))?; + + assert_eq!(ErrorCode::CommandDisallowed, event.get_status()); + + Ok(()) +} + +async fn create_local_device(address: Address) -> PyResult<Device> { + let link = Link::new_local_link()?; + let controller = Controller::new("C1", None, None, Some(link), Some(address.clone())).await?; + let host = Host::new(controller.clone().into(), controller.into()).await?; + Device::new(None, Some(address), None, Some(host), None) +} diff --git a/rust/pytests/wrapper/mod.rs b/rust/pytests/wrapper/mod.rs new file mode 100644 index 0000000..3bc9127 --- /dev/null +++ b/rust/pytests/wrapper/mod.rs @@ -0,0 +1,17 @@ +// 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. + +mod drivers; +mod hci; +mod transport; diff --git a/rust/pytests/wrapper/transport.rs b/rust/pytests/wrapper/transport.rs new file mode 100644 index 0000000..333005b --- /dev/null +++ b/rust/pytests/wrapper/transport.rs @@ -0,0 +1,31 @@ +// 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. + +use bumble::wrapper::transport::Transport; +use nix::sys::stat::Mode; +use pyo3::PyResult; + +#[pyo3_asyncio::tokio::test] +async fn fifo_transport_can_open() -> PyResult<()> { + let dir = tempfile::tempdir().unwrap(); + let mut fifo = dir.path().to_path_buf(); + fifo.push("bumble-transport-fifo"); + nix::unistd::mkfifo(&fifo, Mode::S_IRWXU).unwrap(); + + let mut t = Transport::open(format!("file:{}", fifo.to_str().unwrap())).await?; + + t.close().await?; + + Ok(()) +} diff --git a/rust/src/internal/hci/mod.rs b/rust/src/internal/hci/mod.rs index 232c49f..7830e31 100644 --- a/rust/src/internal/hci/mod.rs +++ b/rust/src/internal/hci/mod.rs @@ -94,7 +94,7 @@ impl From<Error> for PacketTypeParseError { impl WithPacketType<Self> for Command { fn to_vec_with_packet_type(self) -> Vec<u8> { - prepend_packet_type(PacketType::Command, self.to_vec()) + prepend_packet_type(PacketType::Command, self) } fn parse_with_packet_type(bytes: &[u8]) -> Result<Self, PacketTypeParseError> { @@ -104,7 +104,7 @@ impl WithPacketType<Self> for Command { impl WithPacketType<Self> for Acl { fn to_vec_with_packet_type(self) -> Vec<u8> { - prepend_packet_type(PacketType::Acl, self.to_vec()) + prepend_packet_type(PacketType::Acl, self) } fn parse_with_packet_type(bytes: &[u8]) -> Result<Self, PacketTypeParseError> { @@ -114,7 +114,7 @@ impl WithPacketType<Self> for Acl { impl WithPacketType<Self> for Sco { fn to_vec_with_packet_type(self) -> Vec<u8> { - prepend_packet_type(PacketType::Sco, self.to_vec()) + prepend_packet_type(PacketType::Sco, self) } fn parse_with_packet_type(bytes: &[u8]) -> Result<Self, PacketTypeParseError> { @@ -124,7 +124,7 @@ impl WithPacketType<Self> for Sco { impl WithPacketType<Self> for Event { fn to_vec_with_packet_type(self) -> Vec<u8> { - prepend_packet_type(PacketType::Event, self.to_vec()) + prepend_packet_type(PacketType::Event, self) } fn parse_with_packet_type(bytes: &[u8]) -> Result<Self, PacketTypeParseError> { @@ -132,7 +132,9 @@ impl WithPacketType<Self> for Event { } } -fn prepend_packet_type(packet_type: PacketType, mut packet_bytes: Vec<u8>) -> Vec<u8> { +fn prepend_packet_type<T: Packet>(packet_type: PacketType, packet: T) -> Vec<u8> { + // TODO: refactor if `pdl` crate adds API for writing into buffer (github.com/google/pdl/issues/74) + let mut packet_bytes = packet.to_vec(); packet_bytes.insert(0, packet_type.into()); packet_bytes } diff --git a/rust/src/internal/hci/tests.rs b/rust/src/internal/hci/tests.rs index 7962c88..ff9e72b 100644 --- a/rust/src/internal/hci/tests.rs +++ b/rust/src/internal/hci/tests.rs @@ -22,9 +22,8 @@ use bytes::Bytes; #[test] fn prepends_packet_type() { let packet_type = PacketType::Event; - let packet_bytes = vec![0x00, 0x00, 0x00, 0x00]; - let actual = prepend_packet_type(packet_type, packet_bytes); - assert_eq!(vec![0x04, 0x00, 0x00, 0x00, 0x00], actual); + let actual = prepend_packet_type(packet_type, FakePacket { bytes: vec![0xFF] }); + assert_eq!(vec![0x04, 0xFF], actual); } #[test] @@ -75,11 +74,15 @@ fn test_packet_roundtrip_with_type() { } #[derive(Debug, PartialEq)] -struct FakePacket; +struct FakePacket { + bytes: Vec<u8>, +} impl FakePacket { - fn parse(_bytes: &[u8]) -> Result<Self, Error> { - Ok(Self) + fn parse(bytes: &[u8]) -> Result<Self, Error> { + Ok(Self { + bytes: bytes.to_vec(), + }) } } @@ -89,6 +92,6 @@ impl Packet for FakePacket { } fn to_vec(self) -> Vec<u8> { - Vec::new() + self.bytes } } diff --git a/rust/src/wrapper/device.rs b/rust/src/wrapper/device/mod.rs index 6bf958a..e040a89 100644 --- a/rust/src/wrapper/device.rs +++ b/rust/src/wrapper/device/mod.rs @@ -14,7 +14,17 @@ //! Devices and connections to them -use crate::internal::hci::WithPacketType; +#[cfg(feature = "unstable_extended_adv")] +use crate::wrapper::{ + hci::packets::{ + self, AdvertisingEventProperties, AdvertisingFilterPolicy, Enable, EnabledSet, + FragmentPreference, LeSetAdvertisingSetRandomAddressBuilder, + LeSetExtendedAdvertisingDataBuilder, LeSetExtendedAdvertisingEnableBuilder, + LeSetExtendedAdvertisingParametersBuilder, Operation, OwnAddressType, PeerAddressType, + PrimaryPhyType, SecondaryPhyType, + }, + ConversionError, +}; use crate::{ adv::AdvertisementDataBuilder, wrapper::{ @@ -22,7 +32,7 @@ use crate::{ gatt_client::{ProfileServiceProxy, ServiceProxy}, hci::{ packets::{Command, ErrorCode, Event}, - Address, HciCommandWrapper, + Address, HciCommand, WithPacketType, }, host::Host, l2cap::LeConnectionOrientedChannel, @@ -39,6 +49,9 @@ use pyo3::{ use pyo3_asyncio::tokio::into_future; use std::path; +#[cfg(test)] +mod tests; + /// Represents the various properties of some device pub struct DeviceConfiguration(PyObject); @@ -69,9 +82,19 @@ impl ToPyObject for DeviceConfiguration { } } +/// Used for tracking what advertising state a device might be in +#[derive(PartialEq)] +enum AdvertisingStatus { + AdvertisingLegacy, + AdvertisingExtended, + NotAdvertising, +} + /// A device that can send/receive HCI frames. -#[derive(Clone)] -pub struct Device(PyObject); +pub struct Device { + obj: PyObject, + advertising_status: AdvertisingStatus, +} impl Device { /// Creates a Device. When optional arguments are not specified, the Python object specifies the @@ -94,7 +117,10 @@ impl Device { PyModule::import(py, intern!(py, "bumble.device"))? .getattr(intern!(py, "Device"))? .call((), Some(kwargs)) - .map(|any| Self(any.into())) + .map(|any| Self { + obj: any.into(), + advertising_status: AdvertisingStatus::NotAdvertising, + }) }) } @@ -111,28 +137,35 @@ impl Device { intern!(py, "from_config_file_with_hci"), (device_config, source.0, sink.0), ) - .map(|any| Self(any.into())) + .map(|any| Self { + obj: any.into(), + 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: &str, source: Source, sink: Sink) -> PyResult<Self> { + pub fn with_hci(name: &str, address: Address, source: Source, sink: Sink) -> PyResult<Self> { Python::with_gil(|py| { PyModule::import(py, intern!(py, "bumble.device"))? .getattr(intern!(py, "Device"))? - .call_method1(intern!(py, "with_hci"), (name, address, source.0, sink.0)) - .map(|any| Self(any.into())) + .call_method1(intern!(py, "with_hci"), (name, address.0, source.0, sink.0)) + .map(|any| Self { + obj: any.into(), + advertising_status: AdvertisingStatus::NotAdvertising, + }) }) } /// Sends an HCI command on this Device, returning the command's event result. - pub async fn send_command(&self, command: &Command, check_result: bool) -> PyResult<Event> { + pub async fn send_command(&self, command: Command, check_result: bool) -> PyResult<Event> { + let bumble_hci_command = HciCommand::try_from(command)?; Python::with_gil(|py| { - self.0 + self.obj .call_method1( py, intern!(py, "send_command"), - (HciCommandWrapper(command.clone()), check_result), + (bumble_hci_command, check_result), ) .and_then(|coroutine| into_future(coroutine.as_ref(py))) })? @@ -151,7 +184,7 @@ impl Device { /// Turn the device on pub async fn power_on(&self) -> PyResult<()> { Python::with_gil(|py| { - self.0 + self.obj .call_method0(py, intern!(py, "power_on")) .and_then(|coroutine| into_future(coroutine.as_ref(py))) })? @@ -162,7 +195,7 @@ impl Device { /// Connect to a peer pub async fn connect(&self, peer_addr: &str) -> PyResult<Connection> { Python::with_gil(|py| { - self.0 + self.obj .call_method1(py, intern!(py, "connect"), (peer_addr,)) .and_then(|coroutine| into_future(coroutine.as_ref(py))) })? @@ -180,7 +213,7 @@ impl Device { }); Python::with_gil(|py| { - self.0 + self.obj .call_method1(py, intern!(py, "add_listener"), ("connection", boxed)) }) .map(|_| ()) @@ -191,7 +224,7 @@ impl Device { Python::with_gil(|py| { let kwargs = PyDict::new(py); kwargs.set_item("filter_duplicates", filter_duplicates)?; - self.0 + self.obj .call_method(py, intern!(py, "start_scanning"), (), Some(kwargs)) .and_then(|coroutine| into_future(coroutine.as_ref(py))) })? @@ -209,7 +242,7 @@ impl Device { }); Python::with_gil(|py| { - self.0 + self.obj .call_method1(py, intern!(py, "add_listener"), ("advertisement", boxed)) }) .map(|_| ()) @@ -218,7 +251,7 @@ impl Device { /// Set the advertisement data to be used when [Device::start_advertising] is called. pub fn set_advertising_data(&mut self, adv_data: AdvertisementDataBuilder) -> PyResult<()> { Python::with_gil(|py| { - self.0.setattr( + self.obj.setattr( py, intern!(py, "advertising_data"), adv_data.into_bytes().as_slice(), @@ -230,7 +263,7 @@ impl Device { /// Returns the host used by the device, if any pub fn host(&mut self) -> PyResult<Option<Host>> { Python::with_gil(|py| { - self.0 + self.obj .getattr(py, intern!(py, "host")) .map(|obj| obj.into_option(Host::from)) }) @@ -238,27 +271,151 @@ impl Device { /// Start advertising the data set with [Device.set_advertisement]. pub async fn start_advertising(&mut self, auto_restart: bool) -> PyResult<()> { + if self.advertising_status == AdvertisingStatus::AdvertisingExtended { + return Err(PyErr::new::<PyException, _>("Already advertising in extended mode. Stop the existing extended advertisement to start a legacy advertisement.")); + } + Python::with_gil(|py| { let kwargs = PyDict::new(py); kwargs.set_item("auto_restart", auto_restart)?; - self.0 + self.obj .call_method(py, intern!(py, "start_advertising"), (), Some(kwargs)) .and_then(|coroutine| into_future(coroutine.as_ref(py))) })? .await - .map(|_| ()) + .map(|_| ())?; + + self.advertising_status = AdvertisingStatus::AdvertisingLegacy; + Ok(()) + } + + /// Start advertising the data set in extended mode, replacing any existing extended adv. The + /// advertisement will be non-connectable. + /// + /// Fails if the device is already advertising in legacy mode. + #[cfg(feature = "unstable_extended_adv")] + pub async fn start_advertising_extended( + &mut self, + adv_data: AdvertisementDataBuilder, + ) -> PyResult<()> { + // TODO: add tests when local controller object supports extended advertisement commands (github.com/google/bumble/pull/238) + match self.advertising_status { + AdvertisingStatus::AdvertisingLegacy => return Err(PyErr::new::<PyException, _>("Already advertising in legacy mode. Stop the existing legacy advertisement to start an extended advertisement.")), + // Stop the current extended advertisement before advertising with new data. + // We could just issue an LeSetExtendedAdvertisingData command, but this approach + // allows better future flexibility if `start_advertising_extended` were to change. + AdvertisingStatus::AdvertisingExtended => self.stop_advertising_extended().await?, + _ => {} + } + + // if you change this, don't forget to change the same handle in `stop_advertising_extended` + let advertising_handle = 0x00; + + // set extended params + let properties = AdvertisingEventProperties { + connectable: 0, + scannable: 0, + directed: 0, + high_duty_cycle: 0, + legacy: 0, + anonymous: 0, + tx_power: 0, + }; + let extended_advertising_params_cmd = LeSetExtendedAdvertisingParametersBuilder { + advertising_event_properties: properties, + advertising_filter_policy: AdvertisingFilterPolicy::AllDevices, + advertising_handle, + advertising_sid: 0, + advertising_tx_power: 0, + own_address_type: OwnAddressType::RandomDeviceAddress, + peer_address: default_ignored_peer_address(), + peer_address_type: PeerAddressType::PublicDeviceOrIdentityAddress, + primary_advertising_channel_map: 7, + primary_advertising_interval_max: 200, + primary_advertising_interval_min: 100, + primary_advertising_phy: PrimaryPhyType::Le1m, + scan_request_notification_enable: Enable::Disabled, + secondary_advertising_max_skip: 0, + secondary_advertising_phy: SecondaryPhyType::Le1m, + }; + self.send_command(extended_advertising_params_cmd.into(), true) + .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::<PyException, _>(format!("{e:?}")), + })?; + let random_address_cmd = LeSetAdvertisingSetRandomAddressBuilder { + advertising_handle, + random_address, + }; + self.send_command(random_address_cmd.into(), true).await?; + + // set adv data + let advertising_data_cmd = LeSetExtendedAdvertisingDataBuilder { + advertising_data: adv_data.into_bytes(), + advertising_handle, + fragment_preference: FragmentPreference::ControllerMayFragment, + operation: Operation::CompleteAdvertisement, + }; + self.send_command(advertising_data_cmd.into(), true).await?; + + // enable adv + let extended_advertising_enable_cmd = LeSetExtendedAdvertisingEnableBuilder { + enable: Enable::Enabled, + enabled_sets: vec![EnabledSet { + advertising_handle, + duration: 0, + max_extended_advertising_events: 0, + }], + }; + self.send_command(extended_advertising_enable_cmd.into(), true) + .await?; + + self.advertising_status = AdvertisingStatus::AdvertisingExtended; + Ok(()) } /// Stop advertising. pub async fn stop_advertising(&mut self) -> PyResult<()> { Python::with_gil(|py| { - self.0 + self.obj .call_method0(py, intern!(py, "stop_advertising")) .and_then(|coroutine| into_future(coroutine.as_ref(py))) })? .await - .map(|_| ()) + .map(|_| ())?; + + if self.advertising_status == AdvertisingStatus::AdvertisingLegacy { + self.advertising_status = AdvertisingStatus::NotAdvertising; + } + Ok(()) + } + + /// Stop advertising extended. + #[cfg(feature = "unstable_extended_adv")] + pub async fn stop_advertising_extended(&mut self) -> PyResult<()> { + if AdvertisingStatus::AdvertisingExtended != self.advertising_status { + return Ok(()); + } + + // disable adv + let extended_advertising_enable_cmd = LeSetExtendedAdvertisingEnableBuilder { + enable: Enable::Disabled, + enabled_sets: vec![EnabledSet { + advertising_handle: 0x00, + duration: 0, + max_extended_advertising_events: 0, + }], + }; + self.send_command(extended_advertising_enable_cmd.into(), true) + .await?; + + self.advertising_status = AdvertisingStatus::NotAdvertising; + Ok(()) } /// Registers an L2CAP connection oriented channel server. When a client connects to the server, @@ -286,7 +443,7 @@ impl Device { kwargs.set_opt_item("max_credits", max_credits)?; kwargs.set_opt_item("mtu", mtu)?; kwargs.set_opt_item("mps", mps)?; - self.0.call_method( + self.obj.call_method( py, intern!(py, "register_l2cap_channel_server"), (), @@ -295,6 +452,15 @@ impl Device { })?; Ok(()) } + + /// Gets the Device's `random_address` property + pub fn random_address(&self) -> PyResult<Address> { + Python::with_gil(|py| { + self.obj + .getattr(py, intern!(py, "random_address")) + .map(Address) + }) + } } /// A connection to a remote device. @@ -451,3 +617,13 @@ impl Advertisement { Python::with_gil(|py| self.0.getattr(py, intern!(py, "data")).map(AdvertisingData)) } } + +/// Use this address when sending an HCI command that requires providing a peer address, but the +/// command is such that the peer address will be ignored. +/// +/// Internal to bumble, this address might mean "any", but a packets::Address typically gets sent +/// directly to a controller, so we don't have to worry about it. +#[cfg(feature = "unstable_extended_adv")] +fn default_ignored_peer_address() -> packets::Address { + packets::Address::try_from(0x0000_0000_0000_u64).unwrap() +} diff --git a/rust/src/wrapper/device/tests.rs b/rust/src/wrapper/device/tests.rs new file mode 100644 index 0000000..648b919 --- /dev/null +++ b/rust/src/wrapper/device/tests.rs @@ -0,0 +1,23 @@ +// 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. + +#[cfg(feature = "unstable_extended_adv")] +use crate::wrapper::device::default_ignored_peer_address; + +#[test] +#[cfg(feature = "unstable_extended_adv")] +fn default_peer_address_does_not_panic() { + let result = std::panic::catch_unwind(default_ignored_peer_address); + assert!(result.is_ok()) +} diff --git a/rust/src/wrapper/hci.rs b/rust/src/wrapper/hci.rs index b029a65..db09f16 100644 --- a/rust/src/wrapper/hci.rs +++ b/rust/src/wrapper/hci.rs @@ -14,18 +14,19 @@ //! HCI +// 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::{ - internal::hci::WithPacketType, - wrapper::hci::packets::{AddressType, Command, ErrorCode}, +use crate::wrapper::{ + hci::packets::{AddressType, Command, ErrorCode}, + ConversionError, }; use itertools::Itertools as _; use pyo3::{ - exceptions::PyException, - intern, pyclass, pymethods, - types::{PyBytes, PyModule}, - FromPyObject, IntoPy, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject, + exceptions::PyException, intern, types::PyModule, FromPyObject, IntoPy, PyAny, PyErr, PyObject, + PyResult, Python, ToPyObject, }; /// Provides helpers for interacting with HCI @@ -43,17 +44,45 @@ impl HciConstant { } } +/// Bumble's representation of an HCI command. +pub(crate) struct HciCommand(pub(crate) PyObject); + +impl HciCommand { + fn from_bytes(bytes: &[u8]) -> PyResult<Self> { + Python::with_gil(|py| { + PyModule::import(py, intern!(py, "bumble.hci"))? + .getattr(intern!(py, "HCI_Command"))? + .call_method1(intern!(py, "from_bytes"), (bytes,)) + .map(|obj| Self(obj.to_object(py))) + }) + } +} + +impl TryFrom<Command> for HciCommand { + type Error = PyErr; + + fn try_from(value: Command) -> Result<Self, Self::Error> { + HciCommand::from_bytes(&value.to_vec_with_packet_type()) + } +} + +impl IntoPy<PyObject> for HciCommand { + fn into_py(self, _py: Python<'_>) -> PyObject { + self.0 + } +} + /// A Bluetooth address #[derive(Clone)] pub struct Address(pub(crate) PyObject); impl Address { - /// Creates a new [Address] object - pub fn new(address: &str, address_type: &AddressType) -> PyResult<Self> { + /// Creates a new [Address] object. + pub fn new(address: &str, address_type: AddressType) -> PyResult<Self> { Python::with_gil(|py| { PyModule::import(py, intern!(py, "bumble.device"))? .getattr(intern!(py, "Address"))? - .call1((address, address_type.to_object(py))) + .call1((address, address_type)) .map(|any| Self(any.into())) }) } @@ -118,27 +147,28 @@ impl ToPyObject for Address { } } -/// Implements minimum necessary interface to be treated as bumble's [HCI_Command]. -/// While pyo3's macros do not support generics, this could probably be refactored to allow multiple -/// implementations of the HCI_Command methods in the future, if needed. -#[pyclass] -pub(crate) struct HciCommandWrapper(pub(crate) Command); - -#[pymethods] -impl HciCommandWrapper { - fn __bytes__(&self, py: Python) -> PyResult<PyObject> { - let bytes = PyBytes::new(py, &self.0.clone().to_vec_with_packet_type()); - Ok(bytes.into_py(py)) - } +/// An error meaning that the u64 value did not represent a valid BT address. +#[derive(Debug)] +pub struct InvalidAddress(u64); - #[getter] - fn op_code(&self) -> u16 { - self.0.get_op_code().into() +impl TryInto<packets::Address> for Address { + type Error = ConversionError<InvalidAddress>; + + fn try_into(self) -> Result<packets::Address, Self::Error> { + let addr_le_bytes = self.as_le_bytes().map_err(ConversionError::Python)?; + + let mut buf = [0_u8; 8]; + buf[0..6].copy_from_slice(&addr_le_bytes); + let address_u64 = u64::from_le_bytes(buf); + + packets::Address::try_from(address_u64) + .map_err(InvalidAddress) + .map_err(ConversionError::Native) } } -impl ToPyObject for AddressType { - fn to_object(&self, py: Python<'_>) -> PyObject { +impl IntoPy<PyObject> for AddressType { + fn into_py(self, py: Python<'_>) -> PyObject { u8::from(self).to_object(py) } } diff --git a/rust/src/wrapper/mod.rs b/rust/src/wrapper/mod.rs index 27b86d9..afe437d 100644 --- a/rust/src/wrapper/mod.rs +++ b/rust/src/wrapper/mod.rs @@ -132,3 +132,12 @@ pub(crate) fn wrap_python_async<'a>(py: Python<'a>, function: &'a PyAny) -> PyRe .getattr(intern!(py, "wrap_async"))? .call1((function,)) } + +/// Represents the two major kinds of errors that can occur when converting between Rust and Python. +pub enum ConversionError<T> { + /// Occurs across the Python/native boundary. + Python(PyErr), + /// Occurs within the native ecosystem, such as when performing more transformations before + /// finally converting to the native type. + Native(T), +} |