Ability to send HCI commands from Rust

* Autogenerate packet code in Rust from PDL (packet file copied from rootcanal)
* Implement parsing of packets that have a type header
* Expose Python APIs for sending HCI commands
* Expose Python APIs for instantiating a local controller
This commit is contained in:
Gabriel White-Vega
2023-09-14 14:28:22 -04:00
parent c12dee4e76
commit 7e331c2944
16 changed files with 7041 additions and 72 deletions

View File

@@ -194,6 +194,21 @@ class Controller:
self.terminated = asyncio.get_running_loop().create_future()
@classmethod
async def create(
cls,
name,
host_source=None,
host_sink: Optional[TransportSink] = None,
link=None,
public_address: Optional[Union[bytes, str, Address]] = None,
):
'''
Rust's pyo3_asyncio needs the constructor to be async in order to properly
inject a running loop for creating the `terminated` future.
'''
return Controller(name, host_source, host_sink, link, public_address)
@property
def host(self):
return self.hci_sink

218
rust/Cargo.lock generated
View File

@@ -80,6 +80,37 @@ version = "1.0.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
[[package]]
name = "argh"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7af5ba06967ff7214ce4c7419c7d185be7ecd6cc4965a8f6e1d8ce0398aad219"
dependencies = [
"argh_derive",
"argh_shared",
]
[[package]]
name = "argh_derive"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56df0aeedf6b7a2fc67d06db35b09684c3e8da0c95f8f27685cb17e08413d87a"
dependencies = [
"argh_shared",
"proc-macro2",
"quote",
"syn 2.0.29",
]
[[package]]
name = "argh_shared"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5693f39141bda5760ecc4111ab08da40565d1771038c4a0250f03457ec707531"
dependencies = [
"serde",
]
[[package]]
name = "atty"
version = "0.2.14"
@@ -130,6 +161,15 @@ version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635"
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "bstr"
version = "1.6.2"
@@ -145,6 +185,7 @@ name = "bumble"
version = "0.1.0"
dependencies = [
"anyhow",
"bytes",
"clap 4.4.1",
"directories",
"env_logger",
@@ -158,6 +199,7 @@ dependencies = [
"nix",
"nom",
"owo-colors",
"pdl-derive",
"pyo3",
"pyo3-asyncio",
"rand",
@@ -178,9 +220,9 @@ checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1"
[[package]]
name = "bytes"
version = "1.4.0"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
[[package]]
name = "cc"
@@ -262,6 +304,16 @@ version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961"
[[package]]
name = "codespan-reporting"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
dependencies = [
"termcolor",
"unicode-width",
]
[[package]]
name = "colorchoice"
version = "1.0.0"
@@ -284,6 +336,15 @@ version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
[[package]]
name = "cpufeatures"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1"
dependencies = [
"libc",
]
[[package]]
name = "crossbeam"
version = "0.8.2"
@@ -351,6 +412,26 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "crypto-common"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]]
name = "directories"
version = "5.0.1"
@@ -559,6 +640,16 @@ dependencies = [
"slab",
]
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "getrandom"
version = "0.2.10"
@@ -1064,12 +1155,90 @@ dependencies = [
"windows-targets",
]
[[package]]
name = "pdl-compiler"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f99a9ac3d6a426c6fc0b85903a16d77bf148e20ddf755361f76e230d1b6d72cf"
dependencies = [
"argh",
"codespan-reporting",
"heck",
"pest",
"pest_derive",
"prettyplease",
"proc-macro2",
"quote",
"serde",
"serde_json",
"syn 2.0.29",
]
[[package]]
name = "pdl-derive"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ad882bdd3b2584b42180b7a216bfccc7c05e714de2789ee25ea622c85064ef7"
dependencies = [
"codespan-reporting",
"pdl-compiler",
"proc-macro2",
"quote",
"syn 2.0.29",
"termcolor",
]
[[package]]
name = "percent-encoding"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
[[package]]
name = "pest"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c022f1e7b65d6a24c0dbbd5fb344c66881bc01f3e5ae74a1c8100f2f985d98a4"
dependencies = [
"memchr",
"thiserror",
"ucd-trie",
]
[[package]]
name = "pest_derive"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35513f630d46400a977c4cb58f78e1bfbe01434316e60c37d27b9ad6139c66d8"
dependencies = [
"pest",
"pest_generator",
]
[[package]]
name = "pest_generator"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc9fc1b9e7057baba189b5c626e2d6f40681ae5b6eb064dc7c7834101ec8123a"
dependencies = [
"pest",
"pest_meta",
"proc-macro2",
"quote",
"syn 2.0.29",
]
[[package]]
name = "pest_meta"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1df74e9e7ec4053ceb980e7c0c8bd3594e977fde1af91daba9c928e8e8c6708d"
dependencies = [
"once_cell",
"pest",
"sha2",
]
[[package]]
name = "pin-project-lite"
version = "0.2.13"
@@ -1094,6 +1263,16 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "prettyplease"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c64d9ba0963cdcea2e1b2230fbae2bab30eb25a174be395c41e764bfb65dd62"
dependencies = [
"proc-macro2",
"syn 2.0.29",
]
[[package]]
name = "proc-macro2"
version = "1.0.66"
@@ -1465,6 +1644,17 @@ dependencies = [
"serde",
]
[[package]]
name = "sha2"
version = "0.10.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.1"
@@ -1711,6 +1901,18 @@ version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
[[package]]
name = "typenum"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]]
name = "ucd-trie"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9"
[[package]]
name = "unicode-bidi"
version = "0.3.13"
@@ -1738,6 +1940,12 @@ version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
[[package]]
name = "unicode-width"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85"
[[package]]
name = "unindent"
version = "0.1.11"
@@ -1767,6 +1975,12 @@ version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version_check"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "walkdir"
version = "2.4.0"

View File

@@ -23,6 +23,8 @@ hex = "0.4.3"
itertools = "0.11.0"
lazy_static = "1.4.0"
thiserror = "1.0.41"
bytes = "1.5.0"
pdl-derive = "0.1.7"
# Dev tools
file-header = { version = "0.1.2", optional = true }

View File

@@ -20,7 +20,8 @@
use bumble::{
adv::CommonDataType,
wrapper::{
core::AdvertisementDataUnit, device::Device, hci::AddressType, transport::Transport,
core::AdvertisementDataUnit, device::Device, hci::packets::AddressType,
transport::Transport,
},
};
use clap::Parser as _;
@@ -102,7 +103,9 @@ async fn main() -> PyResult<()> {
};
let (type_style, qualifier) = match adv.address()?.address_type()? {
AddressType::PublicIdentity | AddressType::PublicDevice => (Style::new().cyan(), ""),
AddressType::PublicIdentityAddress | AddressType::PublicDeviceAddress => {
(Style::new().cyan(), "")
}
_ => {
if addr.is_static()? {
(Style::new().green(), "(static)")

View File

@@ -12,9 +12,24 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use bumble::wrapper::{drivers::rtk::DriverInfo, transport::Transport};
use bumble::wrapper::{
controller::Controller,
device::Device,
drivers::rtk::DriverInfo,
hci::packets::{
AddressType, Error, ErrorCode, ReadLocalVersionInformationBuilder,
ReadLocalVersionInformationComplete,
},
hci::Address,
host::Host,
link::Link,
transport::Transport,
};
use nix::sys::stat::Mode;
use pyo3::PyResult;
use pyo3::{
exceptions::PyException,
{PyErr, PyResult},
};
#[pyo3_asyncio::tokio::test]
async fn fifo_transport_can_open() -> PyResult<()> {
@@ -35,3 +50,26 @@ 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())?;
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(())
}

View File

@@ -0,0 +1,19 @@
// 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 pdl_derive::pdl;
#[allow(missing_docs, warnings, clippy::all)]
#[pdl("src/internal/hci/packets.pdl")]
pub mod packets {}

File diff suppressed because it is too large Load Diff

View File

@@ -18,3 +18,4 @@
//! to discover.
pub(crate) mod drivers;
pub(crate) mod hci;

View File

@@ -0,0 +1,34 @@
// 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.
//! Shared resources found under bumble's common.py
use pyo3::{PyObject, Python, ToPyObject};
/// Represents the sink for some transport mechanism
pub struct TransportSink(pub(crate) PyObject);
impl ToPyObject for TransportSink {
fn to_object(&self, _py: Python<'_>) -> PyObject {
self.0.clone()
}
}
/// Represents the source for some transport mechanism
pub struct TransportSource(pub(crate) PyObject);
impl ToPyObject for TransportSource {
fn to_object(&self, _py: Python<'_>) -> PyObject {
self.0.clone()
}
}

View File

@@ -0,0 +1,62 @@
// 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.
//! Controller components
use crate::wrapper::{
common::{TransportSink, TransportSource},
hci::Address,
link::Link,
PyDictExt,
};
use pyo3::{
intern,
types::{PyDict, PyModule},
PyObject, PyResult, Python,
};
use pyo3_asyncio::tokio::into_future;
/// A controller that can send and receive HCI frames via some link
#[derive(Clone)]
pub struct Controller(pub(crate) PyObject);
impl Controller {
/// Creates a new [Controller] object. When optional arguments are not specified, the Python
/// module specifies the defaults. Must be called from a thread with a Python event loop, which
/// should be true on `tokio::main` and `async_std::main`.
///
/// For more info, see https://awestlake87.github.io/pyo3-asyncio/master/doc/pyo3_asyncio/#event-loop-references-and-contextvars.
pub async fn new(
name: &str,
host_source: Option<TransportSource>,
host_sink: Option<TransportSink>,
link: Option<Link>,
public_address: Option<Address>,
) -> PyResult<Self> {
Python::with_gil(|py| {
let kwargs = PyDict::new(py);
kwargs.set_item("name", name)?;
kwargs.set_opt_item("host_source", host_source)?;
kwargs.set_opt_item("host_sink", host_sink)?;
kwargs.set_opt_item("link", link)?;
kwargs.set_opt_item("public_address", public_address)?;
PyModule::import(py, intern!(py, "bumble.controller"))?
.getattr(intern!(py, "Controller"))?
.call_method("create", (), Some(kwargs))
.and_then(into_future)
})?
.await
.map(Self)
}
}

View File

@@ -19,7 +19,10 @@ use crate::{
wrapper::{
core::AdvertisingData,
gatt_client::{ProfileServiceProxy, ServiceProxy},
hci::{Address, HciErrorCode},
hci::{
packets::{Command, ErrorCode, Event},
Address, HciCommandWrapper, WithPacketType,
},
host::Host,
l2cap::LeConnectionOrientedChannel,
transport::{Sink, Source},
@@ -27,18 +30,73 @@ use crate::{
},
};
use pyo3::{
exceptions::PyException,
intern,
types::{PyDict, PyModule},
IntoPy, PyObject, PyResult, Python, ToPyObject,
IntoPy, PyErr, PyObject, PyResult, Python, ToPyObject,
};
use pyo3_asyncio::tokio::into_future;
use std::path;
/// Represents the various properties of some device
pub struct DeviceConfiguration(PyObject);
impl DeviceConfiguration {
/// Creates a new configuration, letting the internal Python object set all the defaults
pub fn new() -> PyResult<DeviceConfiguration> {
Python::with_gil(|py| {
PyModule::import(py, intern!(py, "bumble.device"))?
.getattr(intern!(py, "DeviceConfiguration"))?
.call0()
.map(|any| Self(any.into()))
})
}
/// Creates a new configuration from the specified file
pub fn load_from_file(&mut self, device_config: &path::Path) -> PyResult<()> {
Python::with_gil(|py| {
self.0
.call_method1(py, intern!(py, "load_from_file"), (device_config,))
})
.map(|_| ())
}
}
impl ToPyObject for DeviceConfiguration {
fn to_object(&self, _py: Python<'_>) -> PyObject {
self.0.clone()
}
}
/// A device that can send/receive HCI frames.
#[derive(Clone)]
pub struct Device(PyObject);
impl Device {
/// Creates a Device. When optional arguments are not specified, the Python object specifies the
/// defaults.
pub fn new(
name: Option<&str>,
address: Option<Address>,
config: Option<DeviceConfiguration>,
host: Option<Host>,
generic_access_service: Option<bool>,
) -> PyResult<Self> {
Python::with_gil(|py| {
let kwargs = PyDict::new(py);
kwargs.set_opt_item("name", name)?;
kwargs.set_opt_item("address", address)?;
kwargs.set_opt_item("config", config)?;
kwargs.set_opt_item("host", host)?;
kwargs.set_opt_item("generic_access_service", generic_access_service)?;
PyModule::import(py, intern!(py, "bumble.device"))?
.getattr(intern!(py, "Device"))?
.call((), Some(kwargs))
.map(|any| Self(any.into()))
})
}
/// Create a Device per the provided file configured to communicate with a controller through an HCI source/sink
pub fn from_config_file_with_hci(
device_config: &path::Path,
@@ -66,6 +124,29 @@ impl Device {
})
}
/// 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> {
Python::with_gil(|py| {
self.0
.call_method1(
py,
intern!(py, "send_command"),
(HciCommandWrapper(command.clone()), check_result),
)
.and_then(|coroutine| into_future(coroutine.as_ref(py)))
})?
.await
.and_then(|event| {
Python::with_gil(|py| {
let py_bytes = event.call_method0(py, intern!(py, "__bytes__"))?;
let bytes: &[u8] = py_bytes.extract(py)?;
let event = Event::parse_with_packet_type(bytes)
.map_err(|e| PyErr::new::<PyException, _>(e.to_string()))?;
Ok(event)
})
})
}
/// Turn the device on
pub async fn power_on(&self) -> PyResult<()> {
Python::with_gil(|py| {
@@ -244,7 +325,7 @@ impl Connection {
/// Disconnect from device with provided reason. When optional arguments are not specified, the
/// Python module specifies the defaults.
pub async fn disconnect(&mut self, reason: Option<HciErrorCode>) -> PyResult<()> {
pub async fn disconnect(&mut self, reason: Option<ErrorCode>) -> PyResult<()> {
Python::with_gil(|py| {
let kwargs = PyDict::new(py);
kwargs.set_opt_item("reason", reason)?;
@@ -259,7 +340,7 @@ impl Connection {
/// Register a callback to be called on disconnection.
pub fn on_disconnection(
&mut self,
callback: impl Fn(Python, HciErrorCode) -> PyResult<()> + Send + 'static,
callback: impl Fn(Python, ErrorCode) -> PyResult<()> + Send + 'static,
) -> PyResult<()> {
let boxed = ClosureCallback::new(move |py, args, _kwargs| {
callback(py, args.get_item(0)?.extract()?)

View File

@@ -14,84 +14,62 @@
//! HCI
pub use crate::internal::hci::packets;
use crate::wrapper::hci::packets::{
Acl, AddressType, Command, Error, ErrorCode, Event, Packet, Sco,
};
use itertools::Itertools as _;
use pyo3::{
exceptions::PyException, intern, types::PyModule, FromPyObject, PyAny, PyErr, PyObject,
PyResult, Python, ToPyObject,
exceptions::PyException,
intern, pyclass, pymethods,
types::{PyBytes, PyModule},
FromPyObject, IntoPy, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject,
};
/// HCI error code.
pub struct HciErrorCode(u8);
impl<'source> FromPyObject<'source> for HciErrorCode {
fn extract(ob: &'source PyAny) -> PyResult<Self> {
Ok(HciErrorCode(ob.extract()?))
}
}
impl ToPyObject for HciErrorCode {
fn to_object(&self, py: Python<'_>) -> PyObject {
self.0.to_object(py)
}
}
use std::fmt::{Display, Formatter};
/// Provides helpers for interacting with HCI
pub struct HciConstant;
impl HciConstant {
/// Human-readable error name
pub fn error_name(status: HciErrorCode) -> PyResult<String> {
pub fn error_name(status: ErrorCode) -> PyResult<String> {
Python::with_gil(|py| {
PyModule::import(py, intern!(py, "bumble.hci"))?
.getattr(intern!(py, "HCI_Constant"))?
.call_method1(intern!(py, "error_name"), (status.0,))?
.call_method1(intern!(py, "error_name"), (status.to_object(py),))?
.extract()
})
}
}
/// 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> {
Python::with_gil(|py| {
PyModule::import(py, intern!(py, "bumble.device"))?
.getattr(intern!(py, "Address"))?
.call1((address, address_type.to_object(py)))
.map(|any| Self(any.into()))
})
}
/// The type of address
pub fn address_type(&self) -> PyResult<AddressType> {
Python::with_gil(|py| {
let addr_type = self
.0
self.0
.getattr(py, intern!(py, "address_type"))?
.extract::<u32>(py)?;
let module = PyModule::import(py, intern!(py, "bumble.hci"))?;
let klass = module.getattr(intern!(py, "Address"))?;
if addr_type
== klass
.getattr(intern!(py, "PUBLIC_DEVICE_ADDRESS"))?
.extract::<u32>()?
{
Ok(AddressType::PublicDevice)
} else if addr_type
== klass
.getattr(intern!(py, "RANDOM_DEVICE_ADDRESS"))?
.extract::<u32>()?
{
Ok(AddressType::RandomDevice)
} else if addr_type
== klass
.getattr(intern!(py, "PUBLIC_IDENTITY_ADDRESS"))?
.extract::<u32>()?
{
Ok(AddressType::PublicIdentity)
} else if addr_type
== klass
.getattr(intern!(py, "RANDOM_IDENTITY_ADDRESS"))?
.extract::<u32>()?
{
Ok(AddressType::RandomIdentity)
} else {
Err(PyErr::new::<PyException, _>("Invalid address type"))
}
.extract::<u8>(py)?
.try_into()
.map_err(|addr_type| {
PyErr::new::<PyException, _>(format!(
"Failed to convert {addr_type} to AddressType"
))
})
})
}
@@ -134,12 +112,221 @@ impl Address {
}
}
/// BT address types
#[allow(missing_docs)]
#[derive(PartialEq, Eq, Debug)]
pub enum AddressType {
PublicDevice,
RandomDevice,
PublicIdentity,
RandomIdentity,
impl ToPyObject for Address {
fn to_object(&self, _py: Python<'_>) -> PyObject {
self.0.clone()
}
}
/// 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))
}
#[getter]
fn op_code(&self) -> u16 {
self.0.get_op_code().into()
}
}
/// HCI Packet type, prepended to the packet.
/// Rootcanal's PDL declaration excludes this from ser/deser and instead is implemented in code.
/// To maintain the ability to easily use future versions of their packet PDL, packet type is
/// implemented here.
#[derive(Debug)]
pub(crate) enum PacketType {
Command = 0x01,
Acl = 0x02,
Sco = 0x03,
Event = 0x04,
}
impl From<PacketType> for u8 {
fn from(packet_type: PacketType) -> Self {
match packet_type {
PacketType::Command => 0x01,
PacketType::Acl => 0x02,
PacketType::Sco => 0x03,
PacketType::Event => 0x04,
}
}
}
impl TryFrom<u8> for PacketType {
type Error = PacketTypeParseError;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0x01 => Ok(PacketType::Command),
0x02 => Ok(PacketType::Acl),
0x03 => Ok(PacketType::Sco),
0x04 => Ok(PacketType::Event),
_ => Err(PacketTypeParseError::NonexistentPacketType(value)),
}
}
}
/// Allows for smoother interoperability between a [Packet] and a bytes representation of it that
/// includes its type as a header
pub(crate) trait WithPacketType<T: Packet> {
/// Converts the [Packet] into bytes, prefixed with its type
fn to_vec_with_packet_type(self) -> Vec<u8>;
/// Parses a [Packet] out of bytes that are prefixed with the packet's type
fn parse_with_packet_type(bytes: &[u8]) -> Result<T, PacketTypeParseError>;
}
/// Errors that may arise when parsing a packet that is prefixed with its type
pub(crate) enum PacketTypeParseError {
EmptySlice,
NoPacketBytes,
PacketTypeMismatch {
expected: PacketType,
actual: PacketType,
},
NonexistentPacketType(u8),
PacketParse(packets::Error),
}
impl Display for PacketTypeParseError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
PacketTypeParseError::EmptySlice => write!(f, "The slice being parsed was empty"),
PacketTypeParseError::NoPacketBytes => write!(
f,
"There were no bytes left after parsing the packet type header"
),
PacketTypeParseError::PacketTypeMismatch { expected, actual } => {
write!(f, "Expected type: {expected:?}, but got: {actual:?}")
}
PacketTypeParseError::NonexistentPacketType(packet_byte) => {
write!(f, "Packet type ({packet_byte:X}) does not exist")
}
PacketTypeParseError::PacketParse(e) => f.write_str(&e.to_string()),
}
}
}
impl From<packets::Error> for PacketTypeParseError {
fn from(value: Error) -> Self {
Self::PacketParse(value)
}
}
impl WithPacketType<Self> for Command {
fn to_vec_with_packet_type(self) -> Vec<u8> {
let mut bytes = Vec::<u8>::new();
bytes.push(PacketType::Command.into());
bytes.append(&mut self.to_vec());
bytes
}
fn parse_with_packet_type(bytes: &[u8]) -> Result<Self, PacketTypeParseError> {
let first_byte = bytes.first().ok_or(PacketTypeParseError::EmptySlice)?;
match PacketType::try_from(*first_byte)? {
PacketType::Command => {
let packet_bytes = bytes.get(1..).ok_or(PacketTypeParseError::NoPacketBytes)?;
Ok(Command::parse(packet_bytes)?)
}
packet_type => Err(PacketTypeParseError::PacketTypeMismatch {
expected: PacketType::Command,
actual: packet_type,
}),
}
}
}
impl WithPacketType<Self> for Acl {
fn to_vec_with_packet_type(self) -> Vec<u8> {
let mut bytes = Vec::<u8>::new();
bytes.push(PacketType::Acl.into());
bytes.append(&mut self.to_vec());
bytes
}
fn parse_with_packet_type(bytes: &[u8]) -> Result<Self, PacketTypeParseError> {
let first_byte = bytes.first().ok_or(PacketTypeParseError::EmptySlice)?;
match PacketType::try_from(*first_byte)? {
PacketType::Acl => {
let packet_bytes = bytes.get(1..).ok_or(PacketTypeParseError::NoPacketBytes)?;
Ok(Acl::parse(packet_bytes)?)
}
packet_type => Err(PacketTypeParseError::PacketTypeMismatch {
expected: PacketType::Acl,
actual: packet_type,
}),
}
}
}
impl WithPacketType<Self> for Sco {
fn to_vec_with_packet_type(self) -> Vec<u8> {
let mut bytes = Vec::<u8>::new();
bytes.push(PacketType::Sco.into());
bytes.append(&mut self.to_vec());
bytes
}
fn parse_with_packet_type(bytes: &[u8]) -> Result<Self, PacketTypeParseError> {
let first_byte = bytes.first().ok_or(PacketTypeParseError::EmptySlice)?;
match PacketType::try_from(*first_byte)? {
PacketType::Sco => {
let packet_bytes = bytes.get(1..).ok_or(PacketTypeParseError::NoPacketBytes)?;
Ok(Sco::parse(packet_bytes)?)
}
packet_type => Err(PacketTypeParseError::PacketTypeMismatch {
expected: PacketType::Sco,
actual: packet_type,
}),
}
}
}
impl WithPacketType<Self> for Event {
fn to_vec_with_packet_type(self) -> Vec<u8> {
let mut bytes = Vec::<u8>::new();
bytes.push(PacketType::Event.into());
bytes.append(&mut self.to_vec());
bytes
}
fn parse_with_packet_type(bytes: &[u8]) -> Result<Self, PacketTypeParseError> {
let first_byte = bytes.first().ok_or(PacketTypeParseError::EmptySlice)?;
match PacketType::try_from(*first_byte)? {
PacketType::Event => {
let packet_bytes = bytes.get(1..).ok_or(PacketTypeParseError::NoPacketBytes)?;
Ok(Event::parse(packet_bytes)?)
}
packet_type => Err(PacketTypeParseError::PacketTypeMismatch {
expected: PacketType::Event,
actual: packet_type,
}),
}
}
}
impl ToPyObject for AddressType {
fn to_object(&self, py: Python<'_>) -> PyObject {
u8::from(self).to_object(py)
}
}
impl<'source> FromPyObject<'source> for ErrorCode {
fn extract(ob: &'source PyAny) -> PyResult<Self> {
ob.extract()
}
}
impl ToPyObject for ErrorCode {
fn to_object(&self, py: Python<'_>) -> PyObject {
u8::from(self).to_object(py)
}
}

View File

@@ -15,7 +15,7 @@
//! Host-side types
use crate::wrapper::transport::{Sink, Source};
use pyo3::{intern, prelude::PyModule, types::PyDict, PyObject, PyResult, Python};
use pyo3::{intern, prelude::PyModule, types::PyDict, PyObject, PyResult, Python, ToPyObject};
/// Host HCI commands
pub struct Host {
@@ -61,6 +61,12 @@ impl Host {
}
}
impl ToPyObject for Host {
fn to_object(&self, _py: Python<'_>) -> PyObject {
self.obj.clone()
}
}
/// Driver factory to use when initializing a host
#[derive(Debug, Clone)]
pub enum DriverFactory {

38
rust/src/wrapper/link.rs Normal file
View File

@@ -0,0 +1,38 @@
// 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.
//! Link components
use pyo3::{intern, types::PyModule, PyObject, PyResult, Python, ToPyObject};
/// Link bus for controllers to communicate with each other
#[derive(Clone)]
pub struct Link(pub(crate) PyObject);
impl Link {
/// Creates a [Link] object that transports messages locally
pub fn new_local_link() -> PyResult<Self> {
Python::with_gil(|py| {
PyModule::import(py, intern!(py, "bumble.link"))?
.getattr(intern!(py, "LocalLink"))?
.call0()
.map(|any| Self(any.into()))
})
}
}
impl ToPyObject for Link {
fn to_object(&self, _py: Python<'_>) -> PyObject {
self.0.clone()
}
}

View File

@@ -29,6 +29,8 @@ use pyo3::{
pub use pyo3_asyncio;
pub mod assigned_numbers;
pub mod common;
pub mod controller;
pub mod core;
pub mod device;
pub mod drivers;
@@ -36,6 +38,7 @@ pub mod gatt_client;
pub mod hci;
pub mod host;
pub mod l2cap;
pub mod link;
pub mod logging;
pub mod profile;
pub mod transport;

View File

@@ -14,6 +14,7 @@
//! HCI packet transport
use crate::wrapper::controller::Controller;
use pyo3::{intern, types::PyModule, PyObject, PyResult, Python};
/// A source/sink pair for HCI packet I/O.
@@ -67,6 +68,18 @@ impl Drop for Transport {
#[derive(Clone)]
pub struct Source(pub(crate) PyObject);
impl From<Controller> for Source {
fn from(value: Controller) -> Self {
Self(value.0)
}
}
/// The sink side of a [Transport].
#[derive(Clone)]
pub struct Sink(pub(crate) PyObject);
impl From<Controller> for Sink {
fn from(value: Controller) -> Self {
Self(value.0)
}
}