PR feedback

This commit is contained in:
Marshall Pierce
2023-07-26 13:32:12 -06:00
parent afb21220e2
commit 91971433d2
14 changed files with 163 additions and 151 deletions

View File

@@ -43,18 +43,28 @@ jobs:
inv build.mkdocs inv build.mkdocs
build-rust: build-rust:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
matrix:
python-version: [ "3.8", "3.9", "3.10" ]
fail-fast: false
steps: steps:
- name: Check out from Git - name: Check out from Git
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Set up Python - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4 uses: actions/setup-python@v4
with: with:
python-version: '3.10' python-version: ${{ matrix.python-version }}
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
python -m pip install ".[build,test,development,documentation]" python -m pip install ".[build,test,development,documentation]"
- name: Install Rust toolchain - name: Install Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1 uses: actions-rust-lang/setup-rust-toolchain@v1
with:
components: clippy,rustfmt
- name: Rust Lints
run: cd rust && cargo fmt --check && cargo clippy --all-targets -- --deny warnings
- name: Rust Build
run: cd rust && cargo build --all-targets
- name: Rust Tests - name: Rust Tests
run: cd rust && cargo build --all-targets && cargo test run: cd rust && cargo test

View File

@@ -10,7 +10,7 @@ See the `examples` directory for usage.
# Usage # Usage
Set up a virtualenv for Bumble, or otherwise have an isolated Python environment Set up a virtualenv for Bumble, or otherwise have an isolated Python environment
fur Bumble and its dependencies. for Bumble and its dependencies.
Due to Python being Due to Python being
[picky about how its sys path is set up](https://github.com/PyO3/pyo3/issues/1741, [picky about how its sys path is set up](https://github.com/PyO3/pyo3/issues/1741,

View File

@@ -33,7 +33,7 @@
use bumble::wrapper::{ use bumble::wrapper::{
device::{Device, Peer}, device::{Device, Peer},
profile::BatteryService, profile::BatteryServiceProxy,
transport::Transport, transport::Transport,
PyObjectExt, PyObjectExt,
}; };
@@ -63,12 +63,11 @@ async fn main() -> PyResult<()> {
let conn = device.connect(&cli.target_addr).await?; let conn = device.connect(&cli.target_addr).await?;
let mut peer = Peer::new(conn)?; let mut peer = Peer::new(conn)?;
peer.discover_services().await?; for mut s in peer.discover_services().await? {
for mut s in peer.services()? {
s.discover_characteristics().await?; s.discover_characteristics().await?;
} }
let battery_service = peer let battery_service = peer
.create_service_proxy::<BatteryService>()? .create_service_proxy::<BatteryServiceProxy>()?
.ok_or(anyhow::anyhow!("No battery service found"))?; .ok_or(anyhow::anyhow!("No battery service found"))?;
let mut battery_level_char = battery_service let mut battery_level_char = battery_service

View File

@@ -13,11 +13,13 @@
// limitations under the License. // limitations under the License.
use anyhow::anyhow; use anyhow::anyhow;
use bumble::wrapper::{ use bumble::{
adv::{AdvertisementDataBuilder, CommonDataType}, adv::{AdvertisementDataBuilder, CommonDataType},
device::Device, wrapper::{
logging::{bumble_env_logging_level, py_logging_basic_config}, device::Device,
transport::Transport, logging::{bumble_env_logging_level, py_logging_basic_config},
transport::Transport,
},
}; };
use clap::Parser as _; use clap::Parser as _;
use pyo3::PyResult; use pyo3::PyResult;
@@ -61,7 +63,7 @@ async fn main() -> PyResult<()> {
) )
.map_err(|e| anyhow!(e))?; .map_err(|e| anyhow!(e))?;
device.set_advertisement(adv_data)?; device.set_advertising_data(adv_data)?;
device.power_on().await?; device.power_on().await?;
println!("Advertising..."); println!("Advertising...");

View File

@@ -12,11 +12,16 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
//! Counterpart to the Python example `run_scanner.py` //! Counterpart to the Python example `run_scanner.py`.
//!
//! Device deduplication is done here rather than relying on the controller's filtering to provide
//! for additional features, like the ability to make deduplication time-bounded.
use bumble::wrapper::{ use bumble::{
adv::CommonDataType, core::AdvertisementDataUnit, device::Device, hci::AddressType, adv::CommonDataType,
transport::Transport, wrapper::{
core::AdvertisementDataUnit, device::Device, hci::AddressType, transport::Transport,
},
}; };
use clap::Parser as _; use clap::Parser as _;
use itertools::Itertools; use itertools::Itertools;

View File

@@ -89,7 +89,7 @@ fn main() -> anyhow::Result<()> {
transport_names.push(basic_transport_name.clone()); transport_names.push(basic_transport_name.clone());
} else { } else {
transport_names.push(format!( transport_names.push(format!(
"{}/{}", "{}#{}",
basic_transport_name, basic_transport_name,
device_serials_by_id device_serials_by_id
.get(&device_id) .get(&device_id)
@@ -124,39 +124,39 @@ fn main() -> anyhow::Result<()> {
Style::new().red() Style::new().red()
}; };
println!( println!(
"{:26 }{}", "{:26}{}",
" Bumble Transport Names:".blue(), " Bumble Transport Names:".blue(),
transport_names.iter().map(|n| n.style(style)).join(" or ") transport_names.iter().map(|n| n.style(style)).join(" or ")
) )
} }
println!( println!(
"{:26 }{:03}/{:03}", "{:26}{:03}/{:03}",
" Bus/Device:".green(), " Bus/Device:".green(),
device.bus_number(), device.bus_number(),
device.address() device.address()
); );
println!( println!(
"{:26 }{}", "{:26}{}",
" Class:".green(), " Class:".green(),
class_info.formatted_class_name() class_info.formatted_class_name()
); );
println!( println!(
"{:26 }{}", "{:26}{}",
" Subclass/Protocol:".green(), " Subclass/Protocol:".green(),
class_info.formatted_subclass_protocol() class_info.formatted_subclass_protocol()
); );
if let Some(s) = serial { if let Some(s) = serial {
println!("{:26 }{}", " Serial:".green(), s); println!("{:26}{}", " Serial:".green(), s);
device_serials_by_id device_serials_by_id
.entry(device_id) .entry(device_id)
.or_insert(HashSet::new()) .or_insert(HashSet::new())
.insert(s); .insert(s);
} }
if let Some(m) = mfg { if let Some(m) = mfg {
println!("{:26 }{}", " Manufacturer:".green(), m); println!("{:26}{}", " Manufacturer:".green(), m);
} }
if let Some(p) = product { if let Some(p) = product {
println!("{:26 }{}", " Product:".green(), p); println!("{:26}{}", " Product:".green(), p);
} }
if cli.verbose { if cli.verbose {
@@ -205,15 +205,15 @@ fn print_device_details<T: UsbContext>(
for i in 0..device_desc.num_configurations() { for i in 0..device_desc.num_configurations() {
println!(" Configuration {}", i + 1); println!(" Configuration {}", i + 1);
for interface in device.config_descriptor(i)?.interfaces() { for interface in device.config_descriptor(i)?.interfaces() {
let descriptors: Vec<_> = interface.descriptors().collect(); let interface_descriptors: Vec<_> = interface.descriptors().collect();
for d in &descriptors { for d in &interface_descriptors {
let class_info = let class_info =
ClassInfo::new(d.class_code(), d.sub_class_code(), d.protocol_code()); ClassInfo::new(d.class_code(), d.sub_class_code(), d.protocol_code());
println!( println!(
" Interface: {}{} ({}, {})", " Interface: {}{} ({}, {})",
interface.number(), interface.number(),
if descriptors.len() > 1 { if interface_descriptors.len() > 1 {
format!("/{}", d.setting_number()) format!("/{}", d.setting_number())
} else { } else {
String::new() String::new()

View File

@@ -1,9 +1,7 @@
//! Advertisements //! BLE advertisements.
use crate::wrapper::{ use crate::wrapper::assigned_numbers::{COMPANY_IDS, SERVICE_IDS};
assigned_numbers::{COMPANY_IDS, SERVICE_IDS}, use crate::wrapper::core::{Uuid128, Uuid16, Uuid32};
core::{Uuid128, Uuid16, Uuid32},
};
use itertools::Itertools; use itertools::Itertools;
use nom::{combinator, multi, number}; use nom::{combinator, multi, number};
use std::fmt; use std::fmt;
@@ -151,7 +149,7 @@ impl CommonDataType {
/// Apply type-specific human-oriented formatting to data, if any is applicable /// Apply type-specific human-oriented formatting to data, if any is applicable
pub fn format_data(&self, data: &[u8]) -> Option<String> { pub fn format_data(&self, data: &[u8]) -> Option<String> {
match self { match self {
Self::Flags => Some(Flags::matching(data).map(|f| format!("{:?}", f)).join(", ")), Self::Flags => Some(Flags::matching(data).map(|f| format!("{:?}", f)).join(",")),
Self::CompleteListOf16BitServiceClassUuids Self::CompleteListOf16BitServiceClassUuids
| Self::IncompleteListOf16BitServiceClassUuids | Self::IncompleteListOf16BitServiceClassUuids
| Self::ListOf16BitServiceSolicitationUuids => { | Self::ListOf16BitServiceSolicitationUuids => {
@@ -396,30 +394,12 @@ pub enum Flags {
impl fmt::Debug for Flags { impl fmt::Debug for Flags {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { write!(f, "{}", self.short_name())
Flags::LeLimited => write!(f, "LE Limited"),
Flags::LeDiscoverable => write!(f, "LE General"),
Flags::NoBrEdr => write!(f, "No BR/EDR"),
Flags::BrEdrController => write!(f, "BR/EDR C"),
Flags::BrEdrHost => write!(f, "BR/EDR H"),
}
}
}
impl fmt::Display for Flags {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Flags::LeLimited => write!(f, "LE Limited Discoverable Mode"),
Flags::LeDiscoverable => write!(f, "LE General Discoverable Mode"),
Flags::NoBrEdr => write!(f, "BR/EDR Not Supported"),
Flags::BrEdrController => write!(f, "Simultaneous LE and BR/EDR (Controller)"),
Flags::BrEdrHost => write!(f, "Simultaneous LE and BR/EDR (Host)"),
}
} }
} }
impl Flags { impl Flags {
/// Iterates over the flags that are present in the provided `flags` byte. /// Iterates over the flags that are present in the provided `flags` bytes.
pub fn matching(flags: &[u8]) -> impl Iterator<Item = Self> + '_ { pub fn matching(flags: &[u8]) -> impl Iterator<Item = Self> + '_ {
// The encoding is not clear from the spec: do we look at the first byte? or the last? // The encoding is not clear from the spec: do we look at the first byte? or the last?
// In practice it's only one byte. // In practice it's only one byte.
@@ -437,4 +417,30 @@ impl Flags {
mask & first_byte > 0 mask & first_byte > 0
}) })
} }
/// An abbreviated form of the flag name.
///
/// See [Flags::name] for the full name.
pub fn short_name(&self) -> &'static str {
match self {
Flags::LeLimited => "LE Limited",
Flags::LeDiscoverable => "LE General",
Flags::NoBrEdr => "No BR/EDR",
Flags::BrEdrController => "BR/EDR C",
Flags::BrEdrHost => "BR/EDR H",
}
}
/// The human-readable name of the flag.
///
/// See [Flags::short_name] for a shorter string for use if compactness is important.
pub fn name(&self) -> &'static str {
match self {
Flags::LeLimited => "LE Limited Discoverable Mode",
Flags::LeDiscoverable => "LE General Discoverable Mode",
Flags::NoBrEdr => "BR/EDR Not Supported",
Flags::BrEdrController => "Simultaneous LE and BR/EDR (Controller)",
Flags::BrEdrHost => "Simultaneous LE and BR/EDR (Host)",
}
}
} }

View File

@@ -22,8 +22,10 @@
//! typically do, making it good for prototyping, experimentation, test tools, etc. //! typically do, making it good for prototyping, experimentation, test tools, etc.
//! //!
//! Bumble is primarily written in Python. Rust types that wrap the Python API, which is currently //! Bumble is primarily written in Python. Rust types that wrap the Python API, which is currently
//! all of them, are in the [wrapper] module. //! the bulk of the code, are in the [wrapper] module.
#![deny(missing_docs, unsafe_code)] #![deny(missing_docs, unsafe_code)]
pub mod wrapper; pub mod wrapper;
pub mod adv;

View File

@@ -14,7 +14,7 @@
//! Core types //! Core types
use crate::wrapper::adv::CommonDataTypeCode; use crate::adv::CommonDataTypeCode;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use nom::{bytes, combinator}; use nom::{bytes, combinator};
use pyo3::{intern, PyObject, PyResult, Python}; use pyo3::{intern, PyObject, PyResult, Python};

View File

@@ -14,14 +14,15 @@
//! Devices and connections to them //! Devices and connections to them
use crate::wrapper::{ use crate::{
adv::AdvertisementDataBuilder, adv::AdvertisementDataBuilder,
core::AdvertisingData, wrapper::{
gatt::Service, core::AdvertisingData,
gatt_client::ProfileServiceProxy, gatt_client::{ProfileServiceProxy, ServiceProxy},
hci::Address, hci::Address,
transport::{Sink, Source}, transport::{Sink, Source},
ClosureCallback, ClosureCallback,
},
}; };
use pyo3::types::PyDict; use pyo3::types::PyDict;
use pyo3::{intern, types::PyModule, PyObject, PyResult, Python, ToPyObject}; use pyo3::{intern, types::PyModule, PyObject, PyResult, Python, ToPyObject};
@@ -111,7 +112,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_advertisement(&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.0.setattr(
py, py,
@@ -166,29 +167,34 @@ impl Peer {
} }
/// Populates the peer's cache of services. /// Populates the peer's cache of services.
pub async fn discover_services(&mut self) -> PyResult<()> { ///
/// Returns the discovered services.
pub async fn discover_services(&mut self) -> PyResult<Vec<ServiceProxy>> {
Python::with_gil(|py| { Python::with_gil(|py| {
self.0 self.0
.call_method0(py, intern!(py, "discover_services")) .call_method0(py, intern!(py, "discover_services"))
.and_then(|coroutine| pyo3_asyncio::tokio::into_future(coroutine.as_ref(py))) .and_then(|coroutine| pyo3_asyncio::tokio::into_future(coroutine.as_ref(py)))
})? })?
.await .await
.map(|_| ()) .and_then(|list| {
Python::with_gil(|py| {
list.as_ref(py)
.iter()?
.map(|r| r.map(|h| ServiceProxy(h.to_object(py))))
.collect()
})
})
} }
/// Returns a snapshot of the Services currently in the peer's cache /// Returns a snapshot of the Services currently in the peer's cache
pub fn services(&self) -> PyResult<Vec<Service>> { pub fn services(&self) -> PyResult<Vec<ServiceProxy>> {
Python::with_gil(|py| { Python::with_gil(|py| {
let list = self.0.getattr(py, intern!(py, "services"))?; self.0
.getattr(py, intern!(py, "services"))?
// there's probably a better way to do this
Ok(list
.as_ref(py) .as_ref(py)
.iter()? .iter()?
.collect::<Result<Vec<_>, _>>()? .map(|r| r.map(|h| ServiceProxy(h.to_object(py))))
.into_iter() .collect()
.map(|any| Service(any.to_object(py)))
.collect::<Vec<_>>())
}) })
} }

View File

@@ -1,67 +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.
//! GATT
use crate::wrapper::ClosureCallback;
use pyo3::{intern, types::PyTuple, PyObject, PyResult, Python};
/// A GATT service
pub struct Service(pub(crate) PyObject);
impl Service {
/// Discover the characteristics in this service.
///
/// Populates an internal cache of characteristics in this service.
pub async fn discover_characteristics(&mut self) -> PyResult<()> {
Python::with_gil(|py| {
self.0
.call_method0(py, intern!(py, "discover_characteristics"))
.and_then(|coroutine| pyo3_asyncio::tokio::into_future(coroutine.as_ref(py)))
})?
.await
.map(|_| ())
}
}
/// A GATT characteristic
pub struct Characteristic(pub(crate) PyObject);
impl Characteristic {
/// Subscribe to changes to the characteristic, executing `callback` for each new value
pub async fn subscribe(
&mut self,
callback: impl Fn(Python, &PyTuple) -> PyResult<()> + Send + 'static,
) -> PyResult<()> {
let boxed = ClosureCallback::new(move |py, args, _kwargs| callback(py, args));
Python::with_gil(|py| {
self.0
.call_method1(py, intern!(py, "subscribe"), (boxed,))
.and_then(|obj| pyo3_asyncio::tokio::into_future(obj.as_ref(py)))
})?
.await
.map(|_| ())
}
/// Read the current value of the characteristic
pub async fn read_value(&self) -> PyResult<PyObject> {
Python::with_gil(|py| {
self.0
.call_method0(py, intern!(py, "read_value"))
.and_then(|obj| pyo3_asyncio::tokio::into_future(obj.as_ref(py)))
})?
.await
}
}

View File

@@ -14,7 +14,58 @@
//! GATT client support //! GATT client support
use pyo3::PyObject; use crate::wrapper::ClosureCallback;
use pyo3::types::PyTuple;
use pyo3::{intern, PyObject, PyResult, Python};
/// A GATT service on a remote device
pub struct ServiceProxy(pub(crate) PyObject);
impl ServiceProxy {
/// Discover the characteristics in this service.
///
/// Populates an internal cache of characteristics in this service.
pub async fn discover_characteristics(&mut self) -> PyResult<()> {
Python::with_gil(|py| {
self.0
.call_method0(py, intern!(py, "discover_characteristics"))
.and_then(|coroutine| pyo3_asyncio::tokio::into_future(coroutine.as_ref(py)))
})?
.await
.map(|_| ())
}
}
/// A GATT characteristic on a remote device
pub struct CharacteristicProxy(pub(crate) PyObject);
impl CharacteristicProxy {
/// Subscribe to changes to the characteristic, executing `callback` for each new value
pub async fn subscribe(
&mut self,
callback: impl Fn(Python, &PyTuple) -> PyResult<()> + Send + 'static,
) -> PyResult<()> {
let boxed = ClosureCallback::new(move |py, args, _kwargs| callback(py, args));
Python::with_gil(|py| {
self.0
.call_method1(py, intern!(py, "subscribe"), (boxed,))
.and_then(|obj| pyo3_asyncio::tokio::into_future(obj.as_ref(py)))
})?
.await
.map(|_| ())
}
/// Read the current value of the characteristic
pub async fn read_value(&self) -> PyResult<PyObject> {
Python::with_gil(|py| {
self.0
.call_method0(py, intern!(py, "read_value"))
.and_then(|obj| pyo3_asyncio::tokio::into_future(obj.as_ref(py)))
})?
.await
}
}
/// Equivalent to the Python `ProfileServiceProxy`. /// Equivalent to the Python `ProfileServiceProxy`.
pub trait ProfileServiceProxy { pub trait ProfileServiceProxy {

View File

@@ -28,11 +28,9 @@ use pyo3::{
}; };
pub use pyo3_asyncio; pub use pyo3_asyncio;
pub mod adv;
pub mod assigned_numbers; pub mod assigned_numbers;
pub mod core; pub mod core;
pub mod device; pub mod device;
pub mod gatt;
pub mod gatt_client; pub mod gatt_client;
pub mod hci; pub mod hci;
pub mod logging; pub mod logging;

View File

@@ -14,15 +14,15 @@
//! GATT profiles //! GATT profiles
use crate::wrapper::{gatt::Characteristic, gatt_client::ProfileServiceProxy}; use crate::wrapper::gatt_client::{CharacteristicProxy, ProfileServiceProxy};
use pyo3::{intern, PyObject, PyResult, Python}; use pyo3::{intern, PyObject, PyResult, Python};
/// Exposes the battery GATT service /// Exposes the battery GATT service
pub struct BatteryService(PyObject); pub struct BatteryServiceProxy(PyObject);
impl BatteryService { impl BatteryServiceProxy {
/// Get the battery level, if available /// Get the battery level, if available
pub fn battery_level(&self) -> PyResult<Option<Characteristic>> { pub fn battery_level(&self) -> PyResult<Option<CharacteristicProxy>> {
Python::with_gil(|py| { Python::with_gil(|py| {
self.0 self.0
.getattr(py, intern!(py, "battery_level")) .getattr(py, intern!(py, "battery_level"))
@@ -30,14 +30,14 @@ impl BatteryService {
if level.is_none(py) { if level.is_none(py) {
None None
} else { } else {
Some(Characteristic(level)) Some(CharacteristicProxy(level))
} }
}) })
}) })
} }
} }
impl ProfileServiceProxy for BatteryService { impl ProfileServiceProxy for BatteryServiceProxy {
const PROXY_CLASS_MODULE: &'static str = "bumble.profiles.battery_service"; const PROXY_CLASS_MODULE: &'static str = "bumble.profiles.battery_service";
const PROXY_CLASS_NAME: &'static str = "BatteryServiceProxy"; const PROXY_CLASS_NAME: &'static str = "BatteryServiceProxy";