Skip to content

Commit

Permalink
Merge pull request #316 from whitevegagabriel/extended
Browse files Browse the repository at this point in the history
Add support for extended advertising via Rust-only API
  • Loading branch information
barbibulle authored Nov 9, 2023
2 parents 2cd4f84 + 59d7717 commit 46d6242
Show file tree
Hide file tree
Showing 18 changed files with 534 additions and 166 deletions.
8 changes: 5 additions & 3 deletions .github/workflows/python-build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ jobs:
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
- name: Install Python dependencies
run: |
python -m pip install --upgrade pip
python -m pip install ".[build,test,development,documentation]"
Expand All @@ -65,15 +65,17 @@ jobs:
with:
components: clippy,rustfmt
toolchain: ${{ matrix.rust-version }}
- name: Install Rust dependencies
run: cargo install cargo-all-features # allows building/testing combinations of features
- name: Check License Headers
run: cd rust && cargo run --features dev-tools --bin file-header check-all
- name: Rust Build
run: cd rust && cargo build --all-targets && cargo build --all-features --all-targets
run: cd rust && cargo build --all-targets && cargo build-all-features --all-targets
# Lints after build so what clippy needs is already built
- name: Rust Lints
run: cd rust && cargo fmt --check && cargo clippy --all-targets -- --deny warnings && cargo clippy --all-features --all-targets -- --deny warnings
- name: Rust Tests
run: cd rust && cargo test
run: cd rust && cargo test-all-features
# At some point, hook up publishing the binary. For now, just make sure it builds.
# Once we're ready to publish binaries, this should be built with `--release`.
- name: Build Bumble CLI
Expand Down
3 changes: 3 additions & 0 deletions bumble/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -1000,6 +1000,9 @@ def on_hci_le_set_scan_parameters_command(self, command):
'''
See Bluetooth spec Vol 4, Part E - 7.8.10 LE Set Scan Parameters Command
'''
if self.le_scan_enable:
return bytes([HCI_COMMAND_DISALLOWED_ERROR])

self.le_scan_type = command.le_scan_type
self.le_scan_interval = command.le_scan_interval
self.le_scan_window = command.le_scan_window
Expand Down
21 changes: 19 additions & 2 deletions rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ keywords = ["bluetooth", "ble"]
categories = ["api-bindings", "network-programming"]
rust-version = "1.70.0"

# https://github.com/frewsxcv/cargo-all-features#options
[package.metadata.cargo-all-features]
# We are interested in testing subset combinations of this feature, so this is redundant
denylist = ["unstable"]
# To exercise combinations of any of these features, remove from `always_include_features`
always_include_features = ["anyhow", "pyo3-asyncio-attributes", "dev-tools", "bumble-tools"]

[dependencies]
pyo3 = { version = "0.18.3", features = ["macros"] }
pyo3-asyncio = { version = "0.18.0", features = ["tokio-runtime"] }
Expand All @@ -26,6 +33,7 @@ thiserror = "1.0.41"
bytes = "1.5.0"
pdl-derive = "0.2.0"
pdl-runtime = "0.2.0"
futures = "0.3.28"

# Dev tools
file-header = { version = "0.1.2", optional = true }
Expand All @@ -36,7 +44,6 @@ anyhow = { version = "1.0.71", optional = true }
clap = { version = "4.3.3", features = ["derive"], optional = true }
directories = { version = "5.0.1", optional = true }
env_logger = { version = "0.10.0", optional = true }
futures = { version = "0.3.28", optional = true }
log = { version = "0.4.19", optional = true }
owo-colors = { version = "3.5.0", optional = true }
reqwest = { version = "0.11.20", features = ["blocking"], optional = true }
Expand Down Expand Up @@ -74,6 +81,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"
Expand All @@ -85,5 +97,10 @@ anyhow = ["pyo3/anyhow"]
pyo3-asyncio-attributes = ["pyo3-asyncio/attributes"]
dev-tools = ["dep:anyhow", "dep:clap", "dep:file-header", "dep:globset"]
# separate feature for CLI so that dependencies don't spend time building these
bumble-tools = ["dep:clap", "anyhow", "dep:anyhow", "dep:directories", "pyo3-asyncio-attributes", "dep:owo-colors", "dep:reqwest", "dep:rusb", "dep:log", "dep:env_logger", "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"]

# all the unstable features
unstable = ["unstable_extended_adv"]
unstable_extended_adv = []

default = []
9 changes: 3 additions & 6 deletions rust/examples/battery_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
use bumble::wrapper::{
device::{Device, Peer},
hci::{packets::AddressType, Address},
profile::BatteryServiceProxy,
transport::Transport,
PyObjectExt,
Expand All @@ -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?;

Expand Down
26 changes: 21 additions & 5 deletions rust/examples/broadcast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
}
Expand All @@ -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,
Expand Down
12 changes: 5 additions & 7 deletions rust/examples/scanner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
};
Expand All @@ -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
Expand Down
77 changes: 0 additions & 77 deletions rust/pytests/wrapper.rs

This file was deleted.

22 changes: 22 additions & 0 deletions rust/pytests/wrapper/drivers.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
86 changes: 86 additions & 0 deletions rust/pytests/wrapper/hci.rs
Original file line number Diff line number Diff line change
@@ -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)
}
17 changes: 17 additions & 0 deletions rust/pytests/wrapper/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
Loading

0 comments on commit 46d6242

Please sign in to comment.