diff --git a/bumble/controller.py b/bumble/controller.py index 9b2960a..baa0746 100644 --- a/bumble/controller.py +++ b/bumble/controller.py @@ -1000,12 +1000,16 @@ class Controller: ''' See Bluetooth spec Vol 4, Part E - 7.8.10 LE Set Scan Parameters Command ''' - self.le_scan_type = command.le_scan_type - self.le_scan_interval = command.le_scan_interval - self.le_scan_window = command.le_scan_window - self.le_scan_own_address_type = command.own_address_type - self.le_scanning_filter_policy = command.scanning_filter_policy - return bytes([HCI_SUCCESS]) + ret = HCI_SUCCESS + if not self.le_scan_enable: + self.le_scan_type = command.le_scan_type + self.le_scan_interval = command.le_scan_interval + self.le_scan_window = command.le_scan_window + self.le_scan_own_address_type = command.own_address_type + self.le_scanning_filter_policy = command.scanning_filter_policy + else: + ret = HCI_COMMAND_DISALLOWED_ERROR + return bytes([ret]) def on_hci_le_set_scan_enable_command(self, command): ''' 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. /// /// #[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::(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::(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::(e.to_string()))?; + + assert_eq!(ErrorCode::CommandDisallowed, event.get_status()); + + 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 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 for PacketTypeParseError { impl WithPacketType for Command { fn to_vec_with_packet_type(self) -> Vec { - prepend_packet_type(PacketType::Command, self.to_vec()) + prepend_packet_type(PacketType::Command, self) } fn parse_with_packet_type(bytes: &[u8]) -> Result { @@ -104,7 +104,7 @@ impl WithPacketType for Command { impl WithPacketType for Acl { fn to_vec_with_packet_type(self) -> Vec { - prepend_packet_type(PacketType::Acl, self.to_vec()) + prepend_packet_type(PacketType::Acl, self) } fn parse_with_packet_type(bytes: &[u8]) -> Result { @@ -114,7 +114,7 @@ impl WithPacketType for Acl { impl WithPacketType for Sco { fn to_vec_with_packet_type(self) -> Vec { - prepend_packet_type(PacketType::Sco, self.to_vec()) + prepend_packet_type(PacketType::Sco, self) } fn parse_with_packet_type(bytes: &[u8]) -> Result { @@ -124,7 +124,7 @@ impl WithPacketType for Sco { impl WithPacketType for Event { fn to_vec_with_packet_type(self) -> Vec { - prepend_packet_type(PacketType::Event, self.to_vec()) + prepend_packet_type(PacketType::Event, self) } fn parse_with_packet_type(bytes: &[u8]) -> Result { @@ -132,7 +132,9 @@ impl WithPacketType for Event { } } -fn prepend_packet_type(packet_type: PacketType, mut packet_bytes: Vec) -> Vec { +fn prepend_packet_type(packet_type: PacketType, packet: T) -> Vec { + // 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, +} impl FakePacket { - fn parse(_bytes: &[u8]) -> Result { - Ok(Self) + fn parse(bytes: &[u8]) -> Result { + Ok(Self { + bytes: bytes.to_vec(), + }) } } @@ -89,6 +92,6 @@ impl Packet for FakePacket { } fn to_vec(self) -> Vec { - Vec::new() + self.bytes } } diff --git a/rust/src/wrapper/device.rs b/rust/src/wrapper/device/mod.rs similarity index 64% rename from rust/src/wrapper/device.rs rename to 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 { + pub fn with_hci(name: &str, address: Address, source: Source, sink: Sink) -> PyResult { 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 { + pub async fn send_command(&self, command: Command, check_result: bool) -> PyResult { + 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 { 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> { 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::("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::("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::(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
{ + 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 { + 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 for HciCommand { + type Error = PyErr; + + fn try_from(value: Command) -> Result { + HciCommand::from_bytes(&value.to_vec_with_packet_type()) + } +} + +impl IntoPy 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 { + /// 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.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); +/// An error meaning that the u64 value did not represent a valid BT address. +#[derive(Debug)] +pub struct InvalidAddress(u64); -#[pymethods] -impl HciCommandWrapper { - fn __bytes__(&self, py: Python) -> PyResult { - let bytes = PyBytes::new(py, &self.0.clone().to_vec_with_packet_type()); - Ok(bytes.into_py(py)) - } +impl TryInto for Address { + type Error = ConversionError; - #[getter] - fn op_code(&self) -> u16 { - self.0.get_op_code().into() + fn try_into(self) -> Result { + 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 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 { + /// 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), +}