mirror of
https://github.com/google/bumble.git
synced 2026-05-04 03:18:01 +00:00
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:
@@ -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);
|
||||
|
||||
|
||||
@@ -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
92
rust/src/wrapper/l2cap.rs
Normal 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()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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`.
|
||||
|
||||
Reference in New Issue
Block a user