Port l2cap_bridge sample to Rust

- Added Rust wrappers where relevant
- Edited a couple logs in python l2cap_bridge to be more symmetrical
- Created cli subcommand for running the rustified l2cap bridge
This commit is contained in:
Gabriel White-Vega
2023-08-21 09:54:18 -04:00
parent fd4d1bcca3
commit 5ae668bc70
8 changed files with 913 additions and 10 deletions

View File

@@ -19,16 +19,17 @@ use crate::{
wrapper::{
core::AdvertisingData,
gatt_client::{ProfileServiceProxy, ServiceProxy},
hci::Address,
hci::{Address, HciErrorCode},
host::Host,
l2cap::LeConnectionOrientedChannel,
transport::{Sink, Source},
ClosureCallback, PyObjectExt,
ClosureCallback, PyDictExt, PyObjectExt,
},
};
use pyo3::{
intern,
types::{PyDict, PyModule},
PyObject, PyResult, Python, ToPyObject,
IntoPy, PyObject, PyResult, Python, ToPyObject,
};
use pyo3_asyncio::tokio::into_future;
use std::path;
@@ -87,6 +88,22 @@ impl Device {
.map(Connection)
}
/// Register a callback to be called for each incoming connection.
pub fn on_connection(
&mut self,
callback: impl Fn(Python, Connection) -> PyResult<()> + Send + 'static,
) -> PyResult<()> {
let boxed = ClosureCallback::new(move |py, args, _kwargs| {
callback(py, Connection(args.get_item(0)?.into()))
});
Python::with_gil(|py| {
self.0
.call_method1(py, intern!(py, "add_listener"), ("connection", boxed))
})
.map(|_| ())
}
/// Start scanning
pub async fn start_scanning(&self, filter_duplicates: bool) -> PyResult<()> {
Python::with_gil(|py| {
@@ -161,11 +178,109 @@ impl Device {
.await
.map(|_| ())
}
/// Registers an L2CAP connection oriented channel server. When a client connects to the server,
/// the `server` callback returns a handle to the established channel. When optional arguments
/// are not specified, the Python module specifies the defaults.
pub fn register_l2cap_channel_server(
&self,
psm: u16,
server: impl Fn(Python, LeConnectionOrientedChannel) -> PyResult<()> + Send + 'static,
max_credits: Option<u16>,
mtu: Option<u16>,
mps: Option<u16>,
) -> PyResult<()> {
Python::with_gil(|py| {
let boxed = ClosureCallback::new(move |py, args, _kwargs| {
server(
py,
LeConnectionOrientedChannel::from(args.get_item(0)?.into()),
)
});
let kwargs = PyDict::new(py);
kwargs.set_item("psm", psm)?;
kwargs.set_item("server", boxed.into_py(py))?;
kwargs.set_opt_item("max_credits", max_credits)?;
kwargs.set_opt_item("mtu", mtu)?;
kwargs.set_opt_item("mps", mps)?;
self.0.call_method(
py,
intern!(py, "register_l2cap_channel_server"),
(),
Some(kwargs),
)
})?;
Ok(())
}
}
/// A connection to a remote device.
pub struct Connection(PyObject);
impl Connection {
/// Open an L2CAP channel using this connection. When optional arguments are not specified, the
/// Python module specifies the defaults.
pub async fn open_l2cap_channel(
&self,
psm: u16,
max_credits: Option<u16>,
mtu: Option<u16>,
mps: Option<u16>,
) -> PyResult<LeConnectionOrientedChannel> {
Python::with_gil(|py| {
let kwargs = PyDict::new(py);
kwargs.set_item("psm", psm)?;
kwargs.set_opt_item("max_credits", max_credits)?;
kwargs.set_opt_item("mtu", mtu)?;
kwargs.set_opt_item("mps", mps)?;
self.0
.call_method(py, intern!(py, "open_l2cap_channel"), (), Some(kwargs))
.and_then(|coroutine| pyo3_asyncio::tokio::into_future(coroutine.as_ref(py)))
})?
.await
.map(LeConnectionOrientedChannel::from)
}
/// Disconnect from device with provided reason. When optional arguments are not specified, the
/// Python module specifies the defaults.
pub async fn disconnect(self, reason: Option<HciErrorCode>) -> PyResult<()> {
Python::with_gil(|py| {
let kwargs = PyDict::new(py);
kwargs.set_opt_item("reason", reason)?;
self.0
.call_method(py, intern!(py, "disconnect"), (), Some(kwargs))
.and_then(|coroutine| pyo3_asyncio::tokio::into_future(coroutine.as_ref(py)))
})?
.await
.map(|_| ())
}
/// Register a callback to be called on disconnection.
pub fn on_disconnection(
&mut self,
callback: impl Fn(Python, HciErrorCode) -> PyResult<()> + Send + 'static,
) -> PyResult<()> {
let boxed = ClosureCallback::new(move |py, args, _kwargs| {
callback(py, args.get_item(0)?.extract()?)
});
Python::with_gil(|py| {
self.0
.call_method1(py, intern!(py, "add_listener"), ("disconnection", boxed))
})
.map(|_| ())
}
/// Returns some information about the connection as a [String].
pub fn debug_string(&self) -> PyResult<String> {
Python::with_gil(|py| {
let str_obj = self.0.call_method0(py, intern!(py, "__str__"))?;
str_obj.gil_ref(py).extract()
})
}
}
/// The other end of a connection
pub struct Peer(PyObject);

View File

@@ -15,7 +15,40 @@
//! HCI
use itertools::Itertools as _;
use pyo3::{exceptions::PyException, intern, types::PyModule, PyErr, PyObject, PyResult, Python};
use pyo3::{
exceptions::PyException, intern, types::PyModule, FromPyObject, 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)
}
}
/// Provides helpers for interacting with HCI
pub struct HciConstant;
impl HciConstant {
/// Human-readable error name
pub fn error_name(status: HciErrorCode) -> 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,))?
.extract()
})
}
}
/// A Bluetooth address
pub struct Address(pub(crate) PyObject);

92
rust/src/wrapper/l2cap.rs Normal file
View File

@@ -0,0 +1,92 @@
// 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.
//! L2CAP
use crate::wrapper::{ClosureCallback, PyObjectExt};
use pyo3::{intern, PyObject, PyResult, Python};
/// L2CAP connection-oriented channel
pub struct LeConnectionOrientedChannel(PyObject);
impl LeConnectionOrientedChannel {
/// Create a LeConnectionOrientedChannel that wraps the provided obj.
pub(crate) fn from(obj: PyObject) -> Self {
Self(obj)
}
/// Queues data to be automatically sent across this channel.
pub fn write(&mut self, data: &[u8]) -> PyResult<()> {
Python::with_gil(|py| self.0.call_method1(py, intern!(py, "write"), (data,))).map(|_| ())
}
/// Wait for queued data to be sent on this channel.
pub async fn drain(&self) -> PyResult<()> {
Python::with_gil(|py| {
self.0
.call_method0(py, intern!(py, "drain"))
.and_then(|coroutine| pyo3_asyncio::tokio::into_future(coroutine.as_ref(py)))
})?
.await
.map(|_| ())
}
/// Register a callback to be called when the channel is closed.
pub fn on_close(
&mut self,
callback: impl Fn(Python) -> PyResult<()> + Send + 'static,
) -> PyResult<()> {
let boxed = ClosureCallback::new(move |py, _args, _kwargs| callback(py));
Python::with_gil(|py| {
self.0
.call_method1(py, intern!(py, "add_listener"), ("close", boxed))
})
.map(|_| ())
}
/// Register a callback to be called when the channel receives data.
pub fn set_sink(
&mut self,
callback: impl Fn(Python, &[u8]) -> PyResult<()> + Send + 'static,
) -> PyResult<()> {
let boxed = ClosureCallback::new(move |py, args, _kwargs| {
callback(py, args.get_item(0)?.extract()?)
});
Python::with_gil(|py| self.0.setattr(py, intern!(py, "sink"), boxed)).map(|_| ())
}
/// Disconnect the l2cap channel.
/// 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 disconnect(self) -> PyResult<()> {
Python::with_gil(|py| {
self.0
.call_method0(py, intern!(py, "disconnect"))
.and_then(|coroutine| pyo3_asyncio::tokio::into_future(coroutine.as_ref(py)))
})?
.await
.map(|_| ())
}
/// Returns some information about the channel as a [String].
pub fn debug_string(&self) -> PyResult<String> {
Python::with_gil(|py| {
let str_obj = self.0.call_method0(py, intern!(py, "__str__"))?;
str_obj.gil_ref(py).extract()
})
}
}

View File

@@ -31,11 +31,11 @@ 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 l2cap;
pub mod logging;
pub mod profile;
pub mod transport;
@@ -71,6 +71,21 @@ impl PyObjectExt for PyObject {
}
}
/// Convenience extensions to [PyDict]
pub trait PyDictExt {
/// Set item in dict only if value is Some, otherwise do nothing.
fn set_opt_item<K: ToPyObject, V: ToPyObject>(&self, key: K, value: Option<V>) -> PyResult<()>;
}
impl PyDictExt for PyDict {
fn set_opt_item<K: ToPyObject, V: ToPyObject>(&self, key: K, value: Option<V>) -> PyResult<()> {
if let Some(value) = value {
self.set_item(key, value)?
}
Ok(())
}
}
/// Wrapper to make Rust closures ([Fn] implementations) callable from Python.
///
/// The Python callable form returns a Python `None`.