forked from auracaster/bumble_mirror
Rust tools for working with Realtek firmware
Further adventures in porting tools to Rust to flesh out the supported API. These tools didn't feel like `example`s, so I made a top level `bumble` CLI tool that hosts them all as subcommands. I also moved the usb probe not-really-an-`example` into it as well. I'm open to suggestions on how best to organize the subcommands to make them intuitive to explore with `--help`, and how to leave room for other future tools. I also adopted the per-OS project data dir for a default firmware location so that users can download once and then use those .bin files from anywhere without having to sprinkle .bin files in project directories or reaching inside the python package dir hierarchy.
This commit is contained in:
@@ -20,12 +20,17 @@ use crate::{
|
||||
core::AdvertisingData,
|
||||
gatt_client::{ProfileServiceProxy, ServiceProxy},
|
||||
hci::Address,
|
||||
host::Host,
|
||||
transport::{Sink, Source},
|
||||
ClosureCallback,
|
||||
ClosureCallback, PyObjectExt,
|
||||
},
|
||||
};
|
||||
use pyo3::types::PyDict;
|
||||
use pyo3::{intern, types::PyModule, PyObject, PyResult, Python, ToPyObject};
|
||||
use pyo3::{
|
||||
intern,
|
||||
types::{PyDict, PyModule},
|
||||
PyObject, PyResult, Python, ToPyObject,
|
||||
};
|
||||
use pyo3_asyncio::tokio::into_future;
|
||||
use std::path;
|
||||
|
||||
/// A device that can send/receive HCI frames.
|
||||
@@ -65,7 +70,7 @@ impl Device {
|
||||
Python::with_gil(|py| {
|
||||
self.0
|
||||
.call_method0(py, intern!(py, "power_on"))
|
||||
.and_then(|coroutine| pyo3_asyncio::tokio::into_future(coroutine.as_ref(py)))
|
||||
.and_then(|coroutine| into_future(coroutine.as_ref(py)))
|
||||
})?
|
||||
.await
|
||||
.map(|_| ())
|
||||
@@ -76,7 +81,7 @@ impl Device {
|
||||
Python::with_gil(|py| {
|
||||
self.0
|
||||
.call_method1(py, intern!(py, "connect"), (peer_addr,))
|
||||
.and_then(|coroutine| pyo3_asyncio::tokio::into_future(coroutine.as_ref(py)))
|
||||
.and_then(|coroutine| into_future(coroutine.as_ref(py)))
|
||||
})?
|
||||
.await
|
||||
.map(Connection)
|
||||
@@ -89,7 +94,7 @@ impl Device {
|
||||
kwargs.set_item("filter_duplicates", filter_duplicates)?;
|
||||
self.0
|
||||
.call_method(py, intern!(py, "start_scanning"), (), Some(kwargs))
|
||||
.and_then(|coroutine| pyo3_asyncio::tokio::into_future(coroutine.as_ref(py)))
|
||||
.and_then(|coroutine| into_future(coroutine.as_ref(py)))
|
||||
})?
|
||||
.await
|
||||
.map(|_| ())
|
||||
@@ -123,6 +128,15 @@ impl Device {
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
/// Returns the host used by the device, if any
|
||||
pub fn host(&mut self) -> PyResult<Option<Host>> {
|
||||
Python::with_gil(|py| {
|
||||
self.0
|
||||
.getattr(py, intern!(py, "host"))
|
||||
.map(|obj| obj.into_option(Host::from))
|
||||
})
|
||||
}
|
||||
|
||||
/// Start advertising the data set with [Device.set_advertisement].
|
||||
pub async fn start_advertising(&mut self, auto_restart: bool) -> PyResult<()> {
|
||||
Python::with_gil(|py| {
|
||||
@@ -131,7 +145,7 @@ impl Device {
|
||||
|
||||
self.0
|
||||
.call_method(py, intern!(py, "start_advertising"), (), Some(kwargs))
|
||||
.and_then(|coroutine| pyo3_asyncio::tokio::into_future(coroutine.as_ref(py)))
|
||||
.and_then(|coroutine| into_future(coroutine.as_ref(py)))
|
||||
})?
|
||||
.await
|
||||
.map(|_| ())
|
||||
@@ -142,7 +156,7 @@ impl Device {
|
||||
Python::with_gil(|py| {
|
||||
self.0
|
||||
.call_method0(py, intern!(py, "stop_advertising"))
|
||||
.and_then(|coroutine| pyo3_asyncio::tokio::into_future(coroutine.as_ref(py)))
|
||||
.and_then(|coroutine| into_future(coroutine.as_ref(py)))
|
||||
})?
|
||||
.await
|
||||
.map(|_| ())
|
||||
@@ -173,7 +187,7 @@ impl Peer {
|
||||
Python::with_gil(|py| {
|
||||
self.0
|
||||
.call_method0(py, intern!(py, "discover_services"))
|
||||
.and_then(|coroutine| pyo3_asyncio::tokio::into_future(coroutine.as_ref(py)))
|
||||
.and_then(|coroutine| into_future(coroutine.as_ref(py)))
|
||||
})?
|
||||
.await
|
||||
.and_then(|list| {
|
||||
@@ -207,13 +221,7 @@ impl Peer {
|
||||
let class = module.getattr(P::PROXY_CLASS_NAME)?;
|
||||
self.0
|
||||
.call_method1(py, intern!(py, "create_service_proxy"), (class,))
|
||||
.map(|obj| {
|
||||
if obj.is_none(py) {
|
||||
None
|
||||
} else {
|
||||
Some(P::wrap(obj))
|
||||
}
|
||||
})
|
||||
.map(|obj| obj.into_option(P::wrap))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
17
rust/src/wrapper/drivers/mod.rs
Normal file
17
rust/src/wrapper/drivers/mod.rs
Normal file
@@ -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.
|
||||
|
||||
//! Device drivers
|
||||
|
||||
pub mod rtk;
|
||||
141
rust/src/wrapper/drivers/rtk.rs
Normal file
141
rust/src/wrapper/drivers/rtk.rs
Normal file
@@ -0,0 +1,141 @@
|
||||
// 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.
|
||||
|
||||
//! Drivers for Realtek controllers
|
||||
|
||||
use crate::wrapper::{host::Host, PyObjectExt};
|
||||
use pyo3::{intern, types::PyModule, PyObject, PyResult, Python, ToPyObject};
|
||||
use pyo3_asyncio::tokio::into_future;
|
||||
|
||||
pub use crate::internal::drivers::rtk::{Firmware, Patch};
|
||||
|
||||
/// Driver for a Realtek controller
|
||||
pub struct Driver(PyObject);
|
||||
|
||||
impl Driver {
|
||||
/// Locate the driver for the provided host.
|
||||
pub async fn for_host(host: &Host, force: bool) -> PyResult<Option<Self>> {
|
||||
Python::with_gil(|py| {
|
||||
PyModule::import(py, intern!(py, "bumble.drivers.rtk"))?
|
||||
.getattr(intern!(py, "Driver"))?
|
||||
.call_method1(intern!(py, "for_host"), (&host.obj, force))
|
||||
.and_then(into_future)
|
||||
})?
|
||||
.await
|
||||
.map(|obj| obj.into_option(Self))
|
||||
}
|
||||
|
||||
/// Check if the host has a known driver.
|
||||
pub async fn check(host: &Host) -> PyResult<bool> {
|
||||
Python::with_gil(|py| {
|
||||
PyModule::import(py, intern!(py, "bumble.drivers.rtk"))?
|
||||
.getattr(intern!(py, "Driver"))?
|
||||
.call_method1(intern!(py, "check"), (&host.obj,))
|
||||
.and_then(|obj| obj.extract::<bool>())
|
||||
})
|
||||
}
|
||||
|
||||
/// Find the [DriverInfo] for the host, if one matches
|
||||
pub async fn driver_info_for_host(host: &Host) -> PyResult<Option<DriverInfo>> {
|
||||
Python::with_gil(|py| {
|
||||
PyModule::import(py, intern!(py, "bumble.drivers.rtk"))?
|
||||
.getattr(intern!(py, "Driver"))?
|
||||
.call_method1(intern!(py, "driver_info_for_host"), (&host.obj,))
|
||||
.and_then(into_future)
|
||||
})?
|
||||
.await
|
||||
.map(|obj| obj.into_option(DriverInfo))
|
||||
}
|
||||
|
||||
/// Send a command to the device to drop firmware
|
||||
pub async fn drop_firmware(host: &mut Host) -> PyResult<()> {
|
||||
Python::with_gil(|py| {
|
||||
PyModule::import(py, intern!(py, "bumble.drivers.rtk"))?
|
||||
.getattr(intern!(py, "Driver"))?
|
||||
.call_method1(intern!(py, "drop_firmware"), (&host.obj,))
|
||||
.and_then(into_future)
|
||||
})?
|
||||
.await
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
/// Load firmware onto the device.
|
||||
pub async fn download_firmware(&mut self) -> PyResult<()> {
|
||||
Python::with_gil(|py| {
|
||||
self.0
|
||||
.call_method0(py, intern!(py, "download_firmware"))
|
||||
.and_then(|coroutine| into_future(coroutine.as_ref(py)))
|
||||
})?
|
||||
.await
|
||||
.map(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
/// Metadata about a known driver & applicable device
|
||||
pub struct DriverInfo(PyObject);
|
||||
|
||||
impl DriverInfo {
|
||||
/// Returns a list of all drivers that Bumble knows how to handle.
|
||||
pub fn all_drivers() -> PyResult<Vec<DriverInfo>> {
|
||||
Python::with_gil(|py| {
|
||||
PyModule::import(py, intern!(py, "bumble.drivers.rtk"))?
|
||||
.getattr(intern!(py, "Driver"))?
|
||||
.getattr(intern!(py, "DRIVER_INFOS"))?
|
||||
.iter()?
|
||||
.map(|r| r.map(|h| DriverInfo(h.to_object(py))))
|
||||
.collect::<PyResult<Vec<_>>>()
|
||||
})
|
||||
}
|
||||
|
||||
/// The firmware file name to load from the filesystem, e.g. `foo_fw.bin`.
|
||||
pub fn firmware_name(&self) -> PyResult<String> {
|
||||
Python::with_gil(|py| {
|
||||
self.0
|
||||
.getattr(py, intern!(py, "fw_name"))?
|
||||
.as_ref(py)
|
||||
.extract::<String>()
|
||||
})
|
||||
}
|
||||
|
||||
/// The config file name, if any, to load from the filesystem, e.g. `foo_config.bin`.
|
||||
pub fn config_name(&self) -> PyResult<Option<String>> {
|
||||
Python::with_gil(|py| {
|
||||
let obj = self.0.getattr(py, intern!(py, "config_name"))?;
|
||||
let handle = obj.as_ref(py);
|
||||
|
||||
if handle.is_none() {
|
||||
Ok(None)
|
||||
} else {
|
||||
handle
|
||||
.extract::<String>()
|
||||
.map(|s| if s.is_empty() { None } else { Some(s) })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether or not config is required.
|
||||
pub fn config_needed(&self) -> PyResult<bool> {
|
||||
Python::with_gil(|py| {
|
||||
self.0
|
||||
.getattr(py, intern!(py, "config_needed"))?
|
||||
.as_ref(py)
|
||||
.extract::<bool>()
|
||||
})
|
||||
}
|
||||
|
||||
/// ROM id
|
||||
pub fn rom(&self) -> PyResult<u32> {
|
||||
Python::with_gil(|py| self.0.getattr(py, intern!(py, "rom"))?.as_ref(py).extract())
|
||||
}
|
||||
}
|
||||
71
rust/src/wrapper/host.rs
Normal file
71
rust/src/wrapper/host.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
// 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.
|
||||
|
||||
//! Host-side types
|
||||
|
||||
use crate::wrapper::transport::{Sink, Source};
|
||||
use pyo3::{intern, prelude::PyModule, types::PyDict, PyObject, PyResult, Python};
|
||||
|
||||
/// Host HCI commands
|
||||
pub struct Host {
|
||||
pub(crate) obj: PyObject,
|
||||
}
|
||||
|
||||
impl Host {
|
||||
/// Create a Host that wraps the provided obj
|
||||
pub(crate) fn from(obj: PyObject) -> Self {
|
||||
Self { obj }
|
||||
}
|
||||
|
||||
/// Create a new Host
|
||||
pub fn new(source: Source, sink: Sink) -> PyResult<Self> {
|
||||
Python::with_gil(|py| {
|
||||
PyModule::import(py, intern!(py, "bumble.host"))?
|
||||
.getattr(intern!(py, "Host"))?
|
||||
.call((source.0, sink.0), None)
|
||||
.map(|any| Self { obj: any.into() })
|
||||
})
|
||||
}
|
||||
|
||||
/// Send a reset command and perform other reset tasks.
|
||||
pub async fn reset(&mut self, driver_factory: DriverFactory) -> PyResult<()> {
|
||||
Python::with_gil(|py| {
|
||||
let kwargs = match driver_factory {
|
||||
DriverFactory::None => {
|
||||
let kw = PyDict::new(py);
|
||||
kw.set_item("driver_factory", py.None())?;
|
||||
Some(kw)
|
||||
}
|
||||
DriverFactory::Auto => {
|
||||
// leave the default in place
|
||||
None
|
||||
}
|
||||
};
|
||||
self.obj
|
||||
.call_method(py, intern!(py, "reset"), (), kwargs)
|
||||
.and_then(|coroutine| pyo3_asyncio::tokio::into_future(coroutine.as_ref(py)))
|
||||
})?
|
||||
.await
|
||||
.map(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
/// Driver factory to use when initializing a host
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum DriverFactory {
|
||||
/// Do not load drivers
|
||||
None,
|
||||
/// Load appropriate driver, if any is found
|
||||
Auto,
|
||||
}
|
||||
@@ -31,14 +31,17 @@ pub use pyo3_asyncio;
|
||||
pub mod assigned_numbers;
|
||||
pub mod core;
|
||||
pub mod device;
|
||||
|
||||
pub mod drivers;
|
||||
pub mod gatt_client;
|
||||
pub mod hci;
|
||||
pub mod host;
|
||||
pub mod logging;
|
||||
pub mod profile;
|
||||
pub mod transport;
|
||||
|
||||
/// Convenience extensions to [PyObject]
|
||||
pub trait PyObjectExt {
|
||||
pub trait PyObjectExt: Sized {
|
||||
/// Get a GIL-bound reference
|
||||
fn gil_ref<'py>(&'py self, py: Python<'py>) -> &'py PyAny;
|
||||
|
||||
@@ -49,6 +52,17 @@ pub trait PyObjectExt {
|
||||
{
|
||||
Python::with_gil(|py| self.gil_ref(py).extract::<T>())
|
||||
}
|
||||
|
||||
/// If the Python object is a Python `None`, return a Rust `None`, otherwise `Some` with the mapped type
|
||||
fn into_option<T>(self, map_obj: impl Fn(Self) -> T) -> Option<T> {
|
||||
Python::with_gil(|py| {
|
||||
if self.gil_ref(py).is_none() {
|
||||
None
|
||||
} else {
|
||||
Some(map_obj(self))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl PyObjectExt for PyObject {
|
||||
|
||||
@@ -14,7 +14,10 @@
|
||||
|
||||
//! GATT profiles
|
||||
|
||||
use crate::wrapper::gatt_client::{CharacteristicProxy, ProfileServiceProxy};
|
||||
use crate::wrapper::{
|
||||
gatt_client::{CharacteristicProxy, ProfileServiceProxy},
|
||||
PyObjectExt,
|
||||
};
|
||||
use pyo3::{intern, PyObject, PyResult, Python};
|
||||
|
||||
/// Exposes the battery GATT service
|
||||
@@ -26,13 +29,7 @@ impl BatteryServiceProxy {
|
||||
Python::with_gil(|py| {
|
||||
self.0
|
||||
.getattr(py, intern!(py, "battery_level"))
|
||||
.map(|level| {
|
||||
if level.is_none(py) {
|
||||
None
|
||||
} else {
|
||||
Some(CharacteristicProxy(level))
|
||||
}
|
||||
})
|
||||
.map(|level| level.into_option(CharacteristicProxy))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user