aboutsummaryrefslogtreecommitdiff
path: root/rust
diff options
context:
space:
mode:
authorGabriel White-Vega <gwhitevega@google.com>2023-10-10 11:46:59 -0400
committerGabriel White-Vega <gwhitevega@google.com>2023-10-10 13:35:31 -0400
commit1051648ffb1a1827404a9c8008d4aa486d4c1208 (patch)
treeeb49b08b6d28e72f72f8ad77a35f941a51ab3dec /rust
parenta1b55b94e03e037a0b47d99d7323a01ed3ab3afb (diff)
downloadbumble-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.toml10
-rw-r--r--rust/examples/battery_client.rs9
-rw-r--r--rust/examples/broadcast.rs26
-rw-r--r--rust/examples/scanner.rs12
-rw-r--r--rust/pytests/wrapper.rs77
-rw-r--r--rust/pytests/wrapper/drivers.rs22
-rw-r--r--rust/pytests/wrapper/hci.rs86
-rw-r--r--rust/pytests/wrapper/mod.rs17
-rw-r--r--rust/pytests/wrapper/transport.rs31
-rw-r--r--rust/src/internal/hci/mod.rs12
-rw-r--r--rust/src/internal/hci/tests.rs17
-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.rs23
-rw-r--r--rust/src/wrapper/hci.rs84
-rw-r--r--rust/src/wrapper/mod.rs9
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),
+}