mirror of
https://github.com/google/bumble.git
synced 2026-07-14 14:10:50 +00:00
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
This commit is contained in:
+10
-6
@@ -1000,12 +1000,16 @@ class Controller:
|
|||||||
'''
|
'''
|
||||||
See Bluetooth spec Vol 4, Part E - 7.8.10 LE Set Scan Parameters Command
|
See Bluetooth spec Vol 4, Part E - 7.8.10 LE Set Scan Parameters Command
|
||||||
'''
|
'''
|
||||||
self.le_scan_type = command.le_scan_type
|
ret = HCI_SUCCESS
|
||||||
self.le_scan_interval = command.le_scan_interval
|
if not self.le_scan_enable:
|
||||||
self.le_scan_window = command.le_scan_window
|
self.le_scan_type = command.le_scan_type
|
||||||
self.le_scan_own_address_type = command.own_address_type
|
self.le_scan_interval = command.le_scan_interval
|
||||||
self.le_scanning_filter_policy = command.scanning_filter_policy
|
self.le_scan_window = command.le_scan_window
|
||||||
return bytes([HCI_SUCCESS])
|
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):
|
def on_hci_le_set_scan_enable_command(self, command):
|
||||||
'''
|
'''
|
||||||
|
|||||||
@@ -74,6 +74,11 @@ name = "bumble"
|
|||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
required-features = ["bumble-tools"]
|
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 entry point that uses pyo3_asyncio's test harness
|
||||||
[[test]]
|
[[test]]
|
||||||
name = "pytests"
|
name = "pytests"
|
||||||
@@ -86,4 +91,9 @@ pyo3-asyncio-attributes = ["pyo3-asyncio/attributes"]
|
|||||||
dev-tools = ["dep:anyhow", "dep:clap", "dep:file-header", "dep:globset"]
|
dev-tools = ["dep:anyhow", "dep:clap", "dep:file-header", "dep:globset"]
|
||||||
# separate feature for CLI so that dependencies don't spend time building these
|
# 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"]
|
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 = []
|
default = []
|
||||||
|
|||||||
@@ -33,6 +33,7 @@
|
|||||||
|
|
||||||
use bumble::wrapper::{
|
use bumble::wrapper::{
|
||||||
device::{Device, Peer},
|
device::{Device, Peer},
|
||||||
|
hci::{packets::AddressType, Address},
|
||||||
profile::BatteryServiceProxy,
|
profile::BatteryServiceProxy,
|
||||||
transport::Transport,
|
transport::Transport,
|
||||||
PyObjectExt,
|
PyObjectExt,
|
||||||
@@ -52,12 +53,8 @@ async fn main() -> PyResult<()> {
|
|||||||
|
|
||||||
let transport = Transport::open(cli.transport).await?;
|
let transport = Transport::open(cli.transport).await?;
|
||||||
|
|
||||||
let device = Device::with_hci(
|
let address = Address::new("F0:F1:F2:F3:F4:F5", AddressType::RandomDeviceAddress)?;
|
||||||
"Bumble",
|
let device = Device::with_hci("Bumble", address, transport.source()?, transport.sink()?)?;
|
||||||
"F0:F1:F2:F3:F4:F5",
|
|
||||||
transport.source()?,
|
|
||||||
transport.sink()?,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
device.power_on().await?;
|
device.power_on().await?;
|
||||||
|
|
||||||
|
|||||||
@@ -63,17 +63,28 @@ async fn main() -> PyResult<()> {
|
|||||||
)
|
)
|
||||||
.map_err(|e| anyhow!(e))?;
|
.map_err(|e| anyhow!(e))?;
|
||||||
|
|
||||||
device.set_advertising_data(adv_data)?;
|
|
||||||
device.power_on().await?;
|
device.power_on().await?;
|
||||||
|
|
||||||
println!("Advertising...");
|
if cli.extended {
|
||||||
device.start_advertising(true).await?;
|
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
|
// wait until user kills the process
|
||||||
tokio::signal::ctrl_c().await?;
|
tokio::signal::ctrl_c().await?;
|
||||||
|
|
||||||
println!("Stopping...");
|
if cli.extended {
|
||||||
device.stop_advertising().await?;
|
println!("Stopping extended advertisement...");
|
||||||
|
device.stop_advertising_extended().await?;
|
||||||
|
} else {
|
||||||
|
println!("Stopping legacy advertisement...");
|
||||||
|
device.stop_advertising().await?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -86,12 +97,17 @@ struct Cli {
|
|||||||
/// See, for instance, `examples/device1.json` in the Python project.
|
/// See, for instance, `examples/device1.json` in the Python project.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
device_config: path::PathBuf,
|
device_config: path::PathBuf,
|
||||||
|
|
||||||
/// Bumble transport spec.
|
/// Bumble transport spec.
|
||||||
///
|
///
|
||||||
/// <https://google.github.io/bumble/transports/index.html>
|
/// <https://google.github.io/bumble/transports/index.html>
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
transport: String,
|
transport: String,
|
||||||
|
|
||||||
|
/// Whether to perform an extended (BT 5.0) advertisement
|
||||||
|
#[arg(long)]
|
||||||
|
extended: bool,
|
||||||
|
|
||||||
/// Log HCI commands
|
/// Log HCI commands
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
log_hci: bool,
|
log_hci: bool,
|
||||||
|
|||||||
@@ -20,7 +20,9 @@
|
|||||||
use bumble::{
|
use bumble::{
|
||||||
adv::CommonDataType,
|
adv::CommonDataType,
|
||||||
wrapper::{
|
wrapper::{
|
||||||
core::AdvertisementDataUnit, device::Device, hci::packets::AddressType,
|
core::AdvertisementDataUnit,
|
||||||
|
device::Device,
|
||||||
|
hci::{packets::AddressType, Address},
|
||||||
transport::Transport,
|
transport::Transport,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -44,12 +46,8 @@ async fn main() -> PyResult<()> {
|
|||||||
|
|
||||||
let transport = Transport::open(cli.transport).await?;
|
let transport = Transport::open(cli.transport).await?;
|
||||||
|
|
||||||
let mut device = Device::with_hci(
|
let address = Address::new("F0:F1:F2:F3:F4:F5", AddressType::RandomDeviceAddress)?;
|
||||||
"Bumble",
|
let mut device = Device::with_hci("Bumble", address, transport.source()?, transport.sink()?)?;
|
||||||
"F0:F1:F2:F3:F4:F5",
|
|
||||||
transport.source()?,
|
|
||||||
transport.sink()?,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// in practice, devices can send multiple advertisements from the same address, so we keep
|
// in practice, devices can send multiple advertisements from the same address, so we keep
|
||||||
// track of a timestamp for each set of data
|
// track of a timestamp for each set of data
|
||||||
|
|||||||
@@ -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(())
|
|
||||||
}
|
|
||||||
@@ -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(())
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -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(())
|
||||||
|
}
|
||||||
@@ -94,7 +94,7 @@ impl From<Error> for PacketTypeParseError {
|
|||||||
|
|
||||||
impl WithPacketType<Self> for Command {
|
impl WithPacketType<Self> for Command {
|
||||||
fn to_vec_with_packet_type(self) -> Vec<u8> {
|
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> {
|
fn parse_with_packet_type(bytes: &[u8]) -> Result<Self, PacketTypeParseError> {
|
||||||
@@ -104,7 +104,7 @@ impl WithPacketType<Self> for Command {
|
|||||||
|
|
||||||
impl WithPacketType<Self> for Acl {
|
impl WithPacketType<Self> for Acl {
|
||||||
fn to_vec_with_packet_type(self) -> Vec<u8> {
|
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> {
|
fn parse_with_packet_type(bytes: &[u8]) -> Result<Self, PacketTypeParseError> {
|
||||||
@@ -114,7 +114,7 @@ impl WithPacketType<Self> for Acl {
|
|||||||
|
|
||||||
impl WithPacketType<Self> for Sco {
|
impl WithPacketType<Self> for Sco {
|
||||||
fn to_vec_with_packet_type(self) -> Vec<u8> {
|
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> {
|
fn parse_with_packet_type(bytes: &[u8]) -> Result<Self, PacketTypeParseError> {
|
||||||
@@ -124,7 +124,7 @@ impl WithPacketType<Self> for Sco {
|
|||||||
|
|
||||||
impl WithPacketType<Self> for Event {
|
impl WithPacketType<Self> for Event {
|
||||||
fn to_vec_with_packet_type(self) -> Vec<u8> {
|
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> {
|
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.insert(0, packet_type.into());
|
||||||
packet_bytes
|
packet_bytes
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,9 +22,8 @@ use bytes::Bytes;
|
|||||||
#[test]
|
#[test]
|
||||||
fn prepends_packet_type() {
|
fn prepends_packet_type() {
|
||||||
let packet_type = PacketType::Event;
|
let packet_type = PacketType::Event;
|
||||||
let packet_bytes = vec![0x00, 0x00, 0x00, 0x00];
|
let actual = prepend_packet_type(packet_type, FakePacket { bytes: vec![0xFF] });
|
||||||
let actual = prepend_packet_type(packet_type, packet_bytes);
|
assert_eq!(vec![0x04, 0xFF], actual);
|
||||||
assert_eq!(vec![0x04, 0x00, 0x00, 0x00, 0x00], actual);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -75,11 +74,15 @@ fn test_packet_roundtrip_with_type() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
struct FakePacket;
|
struct FakePacket {
|
||||||
|
bytes: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
impl FakePacket {
|
impl FakePacket {
|
||||||
fn parse(_bytes: &[u8]) -> Result<Self, Error> {
|
fn parse(bytes: &[u8]) -> Result<Self, Error> {
|
||||||
Ok(Self)
|
Ok(Self {
|
||||||
|
bytes: bytes.to_vec(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,6 +92,6 @@ impl Packet for FakePacket {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn to_vec(self) -> Vec<u8> {
|
fn to_vec(self) -> Vec<u8> {
|
||||||
Vec::new()
|
self.bytes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,17 @@
|
|||||||
|
|
||||||
//! Devices and connections to them
|
//! 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::{
|
use crate::{
|
||||||
adv::AdvertisementDataBuilder,
|
adv::AdvertisementDataBuilder,
|
||||||
wrapper::{
|
wrapper::{
|
||||||
@@ -22,7 +32,7 @@ use crate::{
|
|||||||
gatt_client::{ProfileServiceProxy, ServiceProxy},
|
gatt_client::{ProfileServiceProxy, ServiceProxy},
|
||||||
hci::{
|
hci::{
|
||||||
packets::{Command, ErrorCode, Event},
|
packets::{Command, ErrorCode, Event},
|
||||||
Address, HciCommandWrapper,
|
Address, HciCommand, WithPacketType,
|
||||||
},
|
},
|
||||||
host::Host,
|
host::Host,
|
||||||
l2cap::LeConnectionOrientedChannel,
|
l2cap::LeConnectionOrientedChannel,
|
||||||
@@ -39,6 +49,9 @@ use pyo3::{
|
|||||||
use pyo3_asyncio::tokio::into_future;
|
use pyo3_asyncio::tokio::into_future;
|
||||||
use std::path;
|
use std::path;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
|
|
||||||
/// Represents the various properties of some device
|
/// Represents the various properties of some device
|
||||||
pub struct DeviceConfiguration(PyObject);
|
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.
|
/// A device that can send/receive HCI frames.
|
||||||
#[derive(Clone)]
|
pub struct Device {
|
||||||
pub struct Device(PyObject);
|
obj: PyObject,
|
||||||
|
advertising_status: AdvertisingStatus,
|
||||||
|
}
|
||||||
|
|
||||||
impl Device {
|
impl Device {
|
||||||
/// Creates a Device. When optional arguments are not specified, the Python object specifies the
|
/// 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"))?
|
PyModule::import(py, intern!(py, "bumble.device"))?
|
||||||
.getattr(intern!(py, "Device"))?
|
.getattr(intern!(py, "Device"))?
|
||||||
.call((), Some(kwargs))
|
.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"),
|
intern!(py, "from_config_file_with_hci"),
|
||||||
(device_config, source.0, sink.0),
|
(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
|
/// 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| {
|
Python::with_gil(|py| {
|
||||||
PyModule::import(py, intern!(py, "bumble.device"))?
|
PyModule::import(py, intern!(py, "bumble.device"))?
|
||||||
.getattr(intern!(py, "Device"))?
|
.getattr(intern!(py, "Device"))?
|
||||||
.call_method1(intern!(py, "with_hci"), (name, address, source.0, sink.0))
|
.call_method1(intern!(py, "with_hci"), (name, address.0, source.0, sink.0))
|
||||||
.map(|any| Self(any.into()))
|
.map(|any| Self {
|
||||||
|
obj: any.into(),
|
||||||
|
advertising_status: AdvertisingStatus::NotAdvertising,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sends an HCI command on this Device, returning the command's event result.
|
/// 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| {
|
Python::with_gil(|py| {
|
||||||
self.0
|
self.obj
|
||||||
.call_method1(
|
.call_method1(
|
||||||
py,
|
py,
|
||||||
intern!(py, "send_command"),
|
intern!(py, "send_command"),
|
||||||
(HciCommandWrapper(command.clone()), check_result),
|
(bumble_hci_command, check_result),
|
||||||
)
|
)
|
||||||
.and_then(|coroutine| into_future(coroutine.as_ref(py)))
|
.and_then(|coroutine| into_future(coroutine.as_ref(py)))
|
||||||
})?
|
})?
|
||||||
@@ -151,7 +184,7 @@ impl Device {
|
|||||||
/// Turn the device on
|
/// Turn the device on
|
||||||
pub async fn power_on(&self) -> PyResult<()> {
|
pub async fn power_on(&self) -> PyResult<()> {
|
||||||
Python::with_gil(|py| {
|
Python::with_gil(|py| {
|
||||||
self.0
|
self.obj
|
||||||
.call_method0(py, intern!(py, "power_on"))
|
.call_method0(py, intern!(py, "power_on"))
|
||||||
.and_then(|coroutine| into_future(coroutine.as_ref(py)))
|
.and_then(|coroutine| into_future(coroutine.as_ref(py)))
|
||||||
})?
|
})?
|
||||||
@@ -162,7 +195,7 @@ impl Device {
|
|||||||
/// Connect to a peer
|
/// Connect to a peer
|
||||||
pub async fn connect(&self, peer_addr: &str) -> PyResult<Connection> {
|
pub async fn connect(&self, peer_addr: &str) -> PyResult<Connection> {
|
||||||
Python::with_gil(|py| {
|
Python::with_gil(|py| {
|
||||||
self.0
|
self.obj
|
||||||
.call_method1(py, intern!(py, "connect"), (peer_addr,))
|
.call_method1(py, intern!(py, "connect"), (peer_addr,))
|
||||||
.and_then(|coroutine| into_future(coroutine.as_ref(py)))
|
.and_then(|coroutine| into_future(coroutine.as_ref(py)))
|
||||||
})?
|
})?
|
||||||
@@ -180,7 +213,7 @@ impl Device {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Python::with_gil(|py| {
|
Python::with_gil(|py| {
|
||||||
self.0
|
self.obj
|
||||||
.call_method1(py, intern!(py, "add_listener"), ("connection", boxed))
|
.call_method1(py, intern!(py, "add_listener"), ("connection", boxed))
|
||||||
})
|
})
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
@@ -191,7 +224,7 @@ impl Device {
|
|||||||
Python::with_gil(|py| {
|
Python::with_gil(|py| {
|
||||||
let kwargs = PyDict::new(py);
|
let kwargs = PyDict::new(py);
|
||||||
kwargs.set_item("filter_duplicates", filter_duplicates)?;
|
kwargs.set_item("filter_duplicates", filter_duplicates)?;
|
||||||
self.0
|
self.obj
|
||||||
.call_method(py, intern!(py, "start_scanning"), (), Some(kwargs))
|
.call_method(py, intern!(py, "start_scanning"), (), Some(kwargs))
|
||||||
.and_then(|coroutine| into_future(coroutine.as_ref(py)))
|
.and_then(|coroutine| into_future(coroutine.as_ref(py)))
|
||||||
})?
|
})?
|
||||||
@@ -209,7 +242,7 @@ impl Device {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Python::with_gil(|py| {
|
Python::with_gil(|py| {
|
||||||
self.0
|
self.obj
|
||||||
.call_method1(py, intern!(py, "add_listener"), ("advertisement", boxed))
|
.call_method1(py, intern!(py, "add_listener"), ("advertisement", boxed))
|
||||||
})
|
})
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
@@ -218,7 +251,7 @@ impl Device {
|
|||||||
/// Set the advertisement data to be used when [Device::start_advertising] is called.
|
/// Set the advertisement data to be used when [Device::start_advertising] is called.
|
||||||
pub fn set_advertising_data(&mut self, adv_data: AdvertisementDataBuilder) -> PyResult<()> {
|
pub fn set_advertising_data(&mut self, adv_data: AdvertisementDataBuilder) -> PyResult<()> {
|
||||||
Python::with_gil(|py| {
|
Python::with_gil(|py| {
|
||||||
self.0.setattr(
|
self.obj.setattr(
|
||||||
py,
|
py,
|
||||||
intern!(py, "advertising_data"),
|
intern!(py, "advertising_data"),
|
||||||
adv_data.into_bytes().as_slice(),
|
adv_data.into_bytes().as_slice(),
|
||||||
@@ -230,7 +263,7 @@ impl Device {
|
|||||||
/// Returns the host used by the device, if any
|
/// Returns the host used by the device, if any
|
||||||
pub fn host(&mut self) -> PyResult<Option<Host>> {
|
pub fn host(&mut self) -> PyResult<Option<Host>> {
|
||||||
Python::with_gil(|py| {
|
Python::with_gil(|py| {
|
||||||
self.0
|
self.obj
|
||||||
.getattr(py, intern!(py, "host"))
|
.getattr(py, intern!(py, "host"))
|
||||||
.map(|obj| obj.into_option(Host::from))
|
.map(|obj| obj.into_option(Host::from))
|
||||||
})
|
})
|
||||||
@@ -238,27 +271,151 @@ impl Device {
|
|||||||
|
|
||||||
/// Start advertising the data set with [Device.set_advertisement].
|
/// Start advertising the data set with [Device.set_advertisement].
|
||||||
pub async fn start_advertising(&mut self, auto_restart: bool) -> PyResult<()> {
|
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| {
|
Python::with_gil(|py| {
|
||||||
let kwargs = PyDict::new(py);
|
let kwargs = PyDict::new(py);
|
||||||
kwargs.set_item("auto_restart", auto_restart)?;
|
kwargs.set_item("auto_restart", auto_restart)?;
|
||||||
|
|
||||||
self.0
|
self.obj
|
||||||
.call_method(py, intern!(py, "start_advertising"), (), Some(kwargs))
|
.call_method(py, intern!(py, "start_advertising"), (), Some(kwargs))
|
||||||
.and_then(|coroutine| into_future(coroutine.as_ref(py)))
|
.and_then(|coroutine| into_future(coroutine.as_ref(py)))
|
||||||
})?
|
})?
|
||||||
.await
|
.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.
|
/// Stop advertising.
|
||||||
pub async fn stop_advertising(&mut self) -> PyResult<()> {
|
pub async fn stop_advertising(&mut self) -> PyResult<()> {
|
||||||
Python::with_gil(|py| {
|
Python::with_gil(|py| {
|
||||||
self.0
|
self.obj
|
||||||
.call_method0(py, intern!(py, "stop_advertising"))
|
.call_method0(py, intern!(py, "stop_advertising"))
|
||||||
.and_then(|coroutine| into_future(coroutine.as_ref(py)))
|
.and_then(|coroutine| into_future(coroutine.as_ref(py)))
|
||||||
})?
|
})?
|
||||||
.await
|
.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,
|
/// 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("max_credits", max_credits)?;
|
||||||
kwargs.set_opt_item("mtu", mtu)?;
|
kwargs.set_opt_item("mtu", mtu)?;
|
||||||
kwargs.set_opt_item("mps", mps)?;
|
kwargs.set_opt_item("mps", mps)?;
|
||||||
self.0.call_method(
|
self.obj.call_method(
|
||||||
py,
|
py,
|
||||||
intern!(py, "register_l2cap_channel_server"),
|
intern!(py, "register_l2cap_channel_server"),
|
||||||
(),
|
(),
|
||||||
@@ -295,6 +452,15 @@ impl Device {
|
|||||||
})?;
|
})?;
|
||||||
Ok(())
|
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.
|
/// 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))
|
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()
|
||||||
|
}
|
||||||
@@ -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())
|
||||||
|
}
|
||||||
+56
-26
@@ -14,18 +14,19 @@
|
|||||||
|
|
||||||
//! HCI
|
//! 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};
|
pub use crate::internal::hci::{packets, Error, Packet};
|
||||||
|
|
||||||
use crate::{
|
use crate::wrapper::{
|
||||||
internal::hci::WithPacketType,
|
hci::packets::{AddressType, Command, ErrorCode},
|
||||||
wrapper::hci::packets::{AddressType, Command, ErrorCode},
|
ConversionError,
|
||||||
};
|
};
|
||||||
use itertools::Itertools as _;
|
use itertools::Itertools as _;
|
||||||
use pyo3::{
|
use pyo3::{
|
||||||
exceptions::PyException,
|
exceptions::PyException, intern, types::PyModule, FromPyObject, IntoPy, PyAny, PyErr, PyObject,
|
||||||
intern, pyclass, pymethods,
|
PyResult, Python, ToPyObject,
|
||||||
types::{PyBytes, PyModule},
|
|
||||||
FromPyObject, IntoPy, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Provides helpers for interacting with HCI
|
/// 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
|
/// A Bluetooth address
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Address(pub(crate) PyObject);
|
pub struct Address(pub(crate) PyObject);
|
||||||
|
|
||||||
impl Address {
|
impl Address {
|
||||||
/// Creates a new [Address] object
|
/// Creates a new [Address] object.
|
||||||
pub fn new(address: &str, address_type: &AddressType) -> PyResult<Self> {
|
pub fn new(address: &str, address_type: AddressType) -> PyResult<Self> {
|
||||||
Python::with_gil(|py| {
|
Python::with_gil(|py| {
|
||||||
PyModule::import(py, intern!(py, "bumble.device"))?
|
PyModule::import(py, intern!(py, "bumble.device"))?
|
||||||
.getattr(intern!(py, "Address"))?
|
.getattr(intern!(py, "Address"))?
|
||||||
.call1((address, address_type.to_object(py)))
|
.call1((address, address_type))
|
||||||
.map(|any| Self(any.into()))
|
.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].
|
/// An error meaning that the u64 value did not represent a valid BT address.
|
||||||
/// While pyo3's macros do not support generics, this could probably be refactored to allow multiple
|
#[derive(Debug)]
|
||||||
/// implementations of the HCI_Command methods in the future, if needed.
|
pub struct InvalidAddress(u64);
|
||||||
#[pyclass]
|
|
||||||
pub(crate) struct HciCommandWrapper(pub(crate) Command);
|
|
||||||
|
|
||||||
#[pymethods]
|
impl TryInto<packets::Address> for Address {
|
||||||
impl HciCommandWrapper {
|
type Error = ConversionError<InvalidAddress>;
|
||||||
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))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[getter]
|
fn try_into(self) -> Result<packets::Address, Self::Error> {
|
||||||
fn op_code(&self) -> u16 {
|
let addr_le_bytes = self.as_le_bytes().map_err(ConversionError::Python)?;
|
||||||
self.0.get_op_code().into()
|
|
||||||
|
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 {
|
impl IntoPy<PyObject> for AddressType {
|
||||||
fn to_object(&self, py: Python<'_>) -> PyObject {
|
fn into_py(self, py: Python<'_>) -> PyObject {
|
||||||
u8::from(self).to_object(py)
|
u8::from(self).to_object(py)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,3 +132,12 @@ pub(crate) fn wrap_python_async<'a>(py: Python<'a>, function: &'a PyAny) -> PyRe
|
|||||||
.getattr(intern!(py, "wrap_async"))?
|
.getattr(intern!(py, "wrap_async"))?
|
||||||
.call1((function,))
|
.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),
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user