From 901ca350902da90d4f9aac57eeeadfdf486e5961 Mon Sep 17 00:00:00 2001 From: Abraham Zukor Date: Mon, 4 Aug 2025 11:17:32 -0700 Subject: [PATCH 1/7] l2cap: Fix android Clippy Lint --- src/android/l2cap_channel.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/android/l2cap_channel.rs b/src/android/l2cap_channel.rs index 769e1aa..1f1682f 100644 --- a/src/android/l2cap_channel.rs +++ b/src/android/l2cap_channel.rs @@ -190,8 +190,7 @@ impl L2capChannelWriter { impl AsyncWrite for L2capChannelWriter { fn poll_write(mut self: pin::Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll> { let stream = pin::pin!(&mut self.stream); - let ret = stream.poll_write(cx, buf); - ret + stream.poll_write(cx, buf) } fn poll_flush(mut self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll> { From 35899dacbfa9241210859163c66586b4abd9eb07 Mon Sep 17 00:00:00 2001 From: Abraham Zukor Date: Mon, 4 Aug 2025 11:18:06 -0700 Subject: [PATCH 2/7] l2cap: Linux Support --- Cargo.toml | 3 +- src/bluer/device.rs | 16 ++- src/bluer/error.rs | 31 ++++++ src/bluer/l2cap_channel.rs | 50 +++++---- src/device.rs | 11 +- src/l2cap_channel.rs | 204 ++++++++++++++++++++++++++++--------- 6 files changed, 242 insertions(+), 73 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 608ed3d..13da051 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ targets = [ [features] unstable = [] -l2cap = ["dep:piper", "futures-lite/std", "futures-lite/alloc"] +l2cap = ["dep:piper", "futures-lite/std", "futures-lite/alloc", "bluer/l2cap", "async-compat"] serde = ["dep:serde", "uuid/serde", "bluer/serde"] [dependencies] @@ -58,6 +58,7 @@ windows = { version = "0.48.0", features = [ [target.'cfg(target_os = "linux")'.dependencies] bluer = { version = "0.16.1", features = ["bluetoothd"] } +async-compat = { version = "0.2", optional = true } tokio = { version = "1.20.1", features = ["rt-multi-thread"] } [target.'cfg(target_os = "android")'.dependencies] diff --git a/src/bluer/device.rs b/src/bluer/device.rs index c1e6087..6ec8aad 100644 --- a/src/bluer/device.rs +++ b/src/bluer/device.rs @@ -3,8 +3,6 @@ use std::sync::Arc; use futures_core::Stream; use futures_lite::StreamExt; -#[cfg(feature = "l2cap")] -use super::l2cap_channel::{L2capChannelReader, L2capChannelWriter}; use super::DeviceId; use crate::device::ServicesChanged; use crate::error::ErrorKind; @@ -296,10 +294,18 @@ impl DeviceImpl { #[cfg(feature = "l2cap")] pub async fn open_l2cap_channel( &self, - _psm: u16, + psm: u16, _secure: bool, - ) -> std::prelude::v1::Result<(L2capChannelReader, L2capChannelWriter), crate::Error> { - Err(ErrorKind::NotSupported.into()) + ) -> Result { + use async_compat::Compat; + use bluer::l2cap::{SocketAddr, Stream as L2CapStream}; + use bluer::AddressType; + + let target_sa = SocketAddr::new(self.inner.address(), AddressType::LePublic, psm); + let stream = L2CapStream::connect(target_sa).await?; + Ok(crate::l2cap_channel::L2capChannel { + stream: Compat::new(stream), + }) } } diff --git a/src/bluer/error.rs b/src/bluer/error.rs index 0ccaffb..e6611e8 100644 --- a/src/bluer/error.rs +++ b/src/bluer/error.rs @@ -24,3 +24,34 @@ fn kind_from_bluer(err: &bluer::Error) -> ErrorKind { _ => ErrorKind::Other, } } + +#[cfg(feature = "l2cap")] +impl From for crate::Error { + fn from(err: std::io::Error) -> Self { + crate::Error::new(kind_from_io(&err.kind()), Some(Box::new(err)), String::new()) + } +} + +#[cfg(feature = "l2cap")] +fn kind_from_io(err: &std::io::ErrorKind) -> ErrorKind { + use std::io::ErrorKind as StdErrorKind; + + match err { + StdErrorKind::NotFound => ErrorKind::NotFound, + StdErrorKind::PermissionDenied => ErrorKind::NotAuthorized, + StdErrorKind::ConnectionRefused + | StdErrorKind::ConnectionReset + | StdErrorKind::HostUnreachable + | StdErrorKind::NetworkUnreachable + | StdErrorKind::ConnectionAborted => ErrorKind::ConnectionFailed, + StdErrorKind::NotConnected => ErrorKind::NotConnected, + StdErrorKind::AddrNotAvailable | StdErrorKind::NetworkDown | StdErrorKind::ResourceBusy => { + ErrorKind::AdapterUnavailable + } + StdErrorKind::TimedOut => ErrorKind::Timeout, + StdErrorKind::Unsupported => ErrorKind::NotSupported, + StdErrorKind::Other => ErrorKind::Other, + // None of the other errors have semantic meaning for us + _ => ErrorKind::Internal, + } +} diff --git a/src/bluer/l2cap_channel.rs b/src/bluer/l2cap_channel.rs index eff1325..8323829 100644 --- a/src/bluer/l2cap_channel.rs +++ b/src/bluer/l2cap_channel.rs @@ -1,13 +1,15 @@ #![cfg(feature = "l2cap")] -use std::pin::Pin; -use std::task::Context; +use std::fmt::Debug; +use std::pin; +use std::task::{Context, Poll}; +use async_compat::Compat; +use bluer::l2cap::stream::{OwnedReadHalf, OwnedWriteHalf}; use futures_lite::io::{AsyncRead, AsyncWrite}; -#[derive(Debug)] pub struct L2capChannelReader { - _private: (), + pub(crate) reader: Compat, } impl L2capChannelReader { @@ -17,17 +19,20 @@ impl L2capChannelReader { } impl AsyncRead for L2capChannelReader { - fn poll_read( - self: Pin<&mut Self>, - _cx: &mut Context<'_>, - _buf: &mut [u8], - ) -> std::task::Poll> { - todo!() + fn poll_read(mut self: pin::Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut [u8]) -> Poll> { + let reader = pin::pin!(&mut self.reader); + reader.poll_read(cx, buf) + } +} + +impl Debug for L2capChannelReader { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Debug::fmt(self.reader.get_ref(), f) } } -#[derive(Debug)] + pub struct L2capChannelWriter { - _private: (), + pub(crate) writer: Compat, } impl L2capChannelWriter { @@ -37,15 +42,24 @@ impl L2capChannelWriter { } impl AsyncWrite for L2capChannelWriter { - fn poll_write(self: Pin<&mut Self>, _cx: &mut Context<'_>, _buf: &[u8]) -> std::task::Poll> { - todo!() + fn poll_write(mut self: pin::Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll> { + let writer = pin::pin!(&mut self.writer); + writer.poll_write(cx, buf) } - fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> std::task::Poll> { - todo!() + fn poll_flush(mut self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll> { + let writer = pin::pin!(&mut self.writer); + writer.poll_flush(cx) } - fn poll_close(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> std::task::Poll> { - todo!() + fn poll_close(mut self: pin::Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let writer = pin::pin!(&mut self.writer); + writer.poll_close(cx) + } +} + +impl Debug for L2capChannelWriter { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Debug::fmt(self.writer.get_ref(), f) } } diff --git a/src/device.rs b/src/device.rs index 989e7d7..a0bb7cf 100644 --- a/src/device.rs +++ b/src/device.rs @@ -164,8 +164,15 @@ impl Device { #[inline] #[cfg(feature = "l2cap")] pub async fn open_l2cap_channel(&self, psm: u16, secure: bool) -> Result { - let (reader, writer) = self.0.open_l2cap_channel(psm, secure).await?; - Ok(L2capChannel { reader, writer }) + #[cfg(not(target_os = "linux"))] + { + let (reader, writer) = self.0.open_l2cap_channel(psm, secure).await?; + Ok(L2capChannel { reader, writer }) + } + #[cfg(target_os = "linux")] + { + self.0.open_l2cap_channel(psm, secure).await + } } } diff --git a/src/l2cap_channel.rs b/src/l2cap_channel.rs index fe0481c..426d4a6 100644 --- a/src/l2cap_channel.rs +++ b/src/l2cap_channel.rs @@ -9,67 +9,177 @@ use crate::{sys, Result}; #[allow(unused)] pub(crate) const PIPE_CAPACITY: usize = 0x100000; // 1Mb -/// A Bluetooth LE L2CAP Connection-oriented Channel (CoC) -#[derive(Debug)] -pub struct L2capChannel { - pub(crate) reader: sys::l2cap_channel::L2capChannelReader, - pub(crate) writer: sys::l2cap_channel::L2capChannelWriter, -} - -/// Reader half of a L2CAP Connection-oriented Channel (CoC) -#[derive(Debug)] -pub struct L2capChannelReader { - reader: sys::l2cap_channel::L2capChannelReader, -} +#[cfg(not(target_os = "linux"))] +mod channel { + use std::pin; + use std::task::{Context, Poll}; + + use futures_lite::io::{AsyncRead, AsyncWrite}; + + use crate::{ + sys::l2cap_channel::{L2capChannelReader, L2capChannelWriter}, + Result, + }; + + /// A Bluetooth LE L2CAP Connection-oriented Channel (CoC) + #[derive(Debug)] + pub struct L2capChannel { + pub(crate) reader: L2capChannelReader, + pub(crate) writer: L2capChannelWriter, + } -/// Writerhalf of a L2CAP Connection-oriented Channel (CoC) -#[derive(Debug)] -pub struct L2capChannelWriter { - writer: sys::l2cap_channel::L2capChannelWriter, -} + impl L2capChannel { + /// Close the L2CAP channel. + /// + /// This closes the entire channel, in both directions (reading and writing). + /// + /// The channel is automatically closed when `L2capChannel` is dropped, so + /// you don't need to call this explicitly. + #[inline] + pub async fn close(&mut self) -> Result<()> { + self.writer.close().await + } + + /// Split the channel into read and write halves. + #[inline] + pub fn split(self) -> (L2capChannelReader, L2capChannelWriter) { + let Self { reader, writer } = self; + (reader, writer) + } + } -impl L2capChannel { - /// Close the L2CAP channel. - /// - /// This closes the entire channel, in both directions (reading and writing). - /// - /// The channel is automatically closed when `L2capChannel` is dropped, so - /// you don't need to call this explicitly. - #[inline] - pub async fn close(&mut self) -> Result<()> { - self.writer.close().await + impl AsyncRead for L2capChannel { + fn poll_read( + mut self: pin::Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut [u8], + ) -> Poll> { + let reader = pin::pin!(&mut self.reader); + reader.poll_read(cx, buf) + } } - /// Split the channel into read and write halves. - #[inline] - pub fn split(self) -> (L2capChannelReader, L2capChannelWriter) { - let Self { reader, writer } = self; - (L2capChannelReader { reader }, L2capChannelWriter { writer }) + impl AsyncWrite for L2capChannel { + fn poll_write(mut self: pin::Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll> { + let writer = pin::pin!(&mut self.writer); + writer.poll_write(cx, buf) + } + + fn poll_flush(mut self: pin::Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let writer = pin::pin!(&mut self.writer); + writer.poll_flush(cx) + } + + fn poll_close(mut self: pin::Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let writer = pin::pin!(&mut self.writer); + writer.poll_close(cx) + } } } -impl AsyncRead for L2capChannel { - fn poll_read(mut self: pin::Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut [u8]) -> Poll> { - let reader = pin::pin!(&mut self.reader); - reader.poll_read(cx, buf) +#[cfg(target_os = "linux")] +mod channel { + use std::fmt::Debug; + use std::pin; + use std::task::{Context, Poll}; + + use async_compat::Compat; + use bluer::l2cap::Stream; + use futures_lite::io::{AsyncRead, AsyncWrite}; + use tokio::io::AsyncWriteExt; + + use crate::{ + sys::l2cap_channel::{L2capChannelReader, L2capChannelWriter}, + Result, + }; + + /// A Bluetooth LE L2CAP Connection-oriented Channel (CoC) + pub struct L2capChannel { + pub(crate) stream: Compat, } -} -impl AsyncWrite for L2capChannel { - fn poll_write(mut self: pin::Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll> { - let writer = pin::pin!(&mut self.writer); - writer.poll_write(cx, buf) + impl L2capChannel { + /// Close the L2CAP channel. + /// + /// This closes the entire channel, in both directions (reading and writing). + /// + /// The channel is automatically closed when `L2capChannel` is dropped, so + /// you don't need to call this explicitly. + #[inline] + pub async fn close(&mut self) -> Result<()> { + self.stream.get_mut().shutdown().await?; + Ok(()) + } + + /// Split the channel into read and write halves. + #[inline] + pub fn split(self) -> (L2capChannelReader, L2capChannelWriter) { + let (reader, writer) = self.stream.into_inner().into_split(); + ( + L2capChannelReader { + reader: Compat::new(reader), + }, + L2capChannelWriter { + writer: Compat::new(writer), + }, + ) + } + + /// Gets the Unerlying Stream type wich may support platform-specific additional functionality. + /// + /// Linux Only + pub fn into_inner(self) -> Stream { + self.stream.into_inner() + } } - fn poll_flush(mut self: pin::Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - let writer = pin::pin!(&mut self.writer); - writer.poll_flush(cx) + impl AsyncRead for L2capChannel { + fn poll_read( + mut self: pin::Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut [u8], + ) -> Poll> { + let stream = pin::pin!(&mut self.stream); + stream.poll_read(cx, buf) + } } - fn poll_close(mut self: pin::Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - let writer = pin::pin!(&mut self.writer); - writer.poll_close(cx) + impl AsyncWrite for L2capChannel { + fn poll_write(mut self: pin::Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll> { + let stream = pin::pin!(&mut self.stream); + stream.poll_write(cx, buf) + } + + fn poll_flush(mut self: pin::Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let stream = pin::pin!(&mut self.stream); + stream.poll_flush(cx) + } + + fn poll_close(mut self: pin::Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let stream = pin::pin!(&mut self.stream); + stream.poll_close(cx) + } } + + impl Debug for L2capChannel { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Debug::fmt(self.stream.get_ref(), f) + } + } +} + +pub use channel::L2capChannel; + +/// Reader half of a L2CAP Connection-oriented Channel (CoC) +#[derive(Debug)] +pub struct L2capChannelReader { + reader: sys::l2cap_channel::L2capChannelReader, +} + +/// Writerhalf of a L2CAP Connection-oriented Channel (CoC) +#[derive(Debug)] +pub struct L2capChannelWriter { + writer: sys::l2cap_channel::L2capChannelWriter, } impl L2capChannelReader { From 2baec8fbf2b5c5f8cc1c5a0714ddf1182e101533 Mon Sep 17 00:00:00 2001 From: Abe Zukor Date: Sat, 16 Aug 2025 23:53:13 -0700 Subject: [PATCH 3/7] l2cap: Platform specific `L2capChannel` This also modifies the public api to rely on `AsyncWrite::poll_close` and `Drop` for closing the l2cap channels instead of having bespoke functions for it --- src/android/device.rs | 12 +- src/android/l2cap_channel.rs | 59 +++---- src/bluer/device.rs | 6 +- src/bluer/l2cap_channel.rs | 50 ++---- src/corebluetooth/device.rs | 8 +- src/corebluetooth/l2cap_channel.rs | 39 ++--- src/device.rs | 11 +- src/l2cap_channel.rs | 260 +++++++---------------------- 8 files changed, 124 insertions(+), 321 deletions(-) diff --git a/src/android/device.rs b/src/android/device.rs index 42b43ef..9ae392c 100644 --- a/src/android/device.rs +++ b/src/android/device.rs @@ -4,8 +4,6 @@ use java_spaghetti::Global; use uuid::Uuid; use super::bindings::android::bluetooth::BluetoothDevice; -#[cfg(feature = "l2cap")] -use super::l2cap_channel::{L2capChannelReader, L2capChannelWriter}; use crate::pairing::PairingAgent; use crate::{DeviceId, Result, Service, ServicesChanged}; @@ -100,12 +98,10 @@ impl DeviceImpl { } #[cfg(feature = "l2cap")] - pub async fn open_l2cap_channel( - &self, - psm: u16, - secure: bool, - ) -> std::prelude::v1::Result<(L2capChannelReader, L2capChannelWriter), crate::Error> { - super::l2cap_channel::open_l2cap_channel(self.device.clone(), psm, secure) + pub async fn open_l2cap_channel(&self, psm: u16, secure: bool) -> Result { + let (reader, writer) = super::l2cap_channel::open_l2cap_channel(self.device.clone(), psm, secure)?; + + Ok(super::l2cap_channel::L2capChannel { reader, writer }) } } diff --git a/src/android/l2cap_channel.rs b/src/android/l2cap_channel.rs index 1f1682f..37b8e1e 100644 --- a/src/android/l2cap_channel.rs +++ b/src/android/l2cap_channel.rs @@ -12,7 +12,7 @@ use tracing::{debug, trace, warn}; use super::bindings::android::bluetooth::{BluetoothDevice, BluetoothSocket}; use super::OptionExt; use crate::l2cap_channel::PIPE_CAPACITY; -use crate::Result; +use crate::{derive_async_read, derive_async_write}; pub fn open_l2cap_channel( device: Global, @@ -116,11 +116,11 @@ pub fn open_l2cap_channel( Ok(( L2capChannelReader { - closer: closer.clone(), + _closer: closer.clone(), stream: read_receiver, }, L2capChannelWriter { - closer, + _closer: closer, stream: write_sender, }, )) @@ -150,25 +150,27 @@ impl Drop for L2capCloser { } } -pub struct L2capChannelReader { - stream: piper::Reader, - closer: Arc, +pub struct L2capChannel { + pub(super) reader: L2capChannelReader, + pub(super) writer: L2capChannelWriter, } -impl L2capChannelReader { - pub async fn close(&mut self) -> Result<()> { - self.closer.close(); - Ok(()) +impl L2capChannel { + pub fn split(self) -> (L2capChannelReader, L2capChannelWriter) { + (self.reader, self.writer) } } -impl AsyncRead for L2capChannelReader { - fn poll_read(mut self: pin::Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut [u8]) -> Poll> { - let stream = pin::pin!(&mut self.stream); - stream.poll_read(cx, buf) - } +derive_async_read!(L2capChannel, reader); +derive_async_write!(L2capChannel, writer); + +pub struct L2capChannelReader { + stream: piper::Reader, + _closer: Arc, } +derive_async_read!(L2capChannelReader, stream); + impl fmt::Debug for L2capChannelReader { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("L2capChannelReader") @@ -177,33 +179,10 @@ impl fmt::Debug for L2capChannelReader { pub struct L2capChannelWriter { stream: piper::Writer, - closer: Arc, -} - -impl L2capChannelWriter { - pub async fn close(&mut self) -> Result<()> { - self.closer.close(); - Ok(()) - } + _closer: Arc, } -impl AsyncWrite for L2capChannelWriter { - fn poll_write(mut self: pin::Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll> { - let stream = pin::pin!(&mut self.stream); - stream.poll_write(cx, buf) - } - - fn poll_flush(mut self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll> { - let stream = pin::pin!(&mut self.stream); - stream.poll_flush(cx) - } - - fn poll_close(mut self: pin::Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - self.closer.close(); - let stream = pin::pin!(&mut self.stream); - stream.poll_close(cx) - } -} +derive_async_write!(L2capChannelWriter, stream); impl fmt::Debug for L2capChannelWriter { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { diff --git a/src/bluer/device.rs b/src/bluer/device.rs index 6ec8aad..26093b9 100644 --- a/src/bluer/device.rs +++ b/src/bluer/device.rs @@ -296,16 +296,14 @@ impl DeviceImpl { &self, psm: u16, _secure: bool, - ) -> Result { + ) -> Result { use async_compat::Compat; use bluer::l2cap::{SocketAddr, Stream as L2CapStream}; use bluer::AddressType; let target_sa = SocketAddr::new(self.inner.address(), AddressType::LePublic, psm); let stream = L2CapStream::connect(target_sa).await?; - Ok(crate::l2cap_channel::L2capChannel { - stream: Compat::new(stream), - }) + Ok(super::l2cap_channel::L2capChannel(Compat::new(stream))) } } diff --git a/src/bluer/l2cap_channel.rs b/src/bluer/l2cap_channel.rs index 8323829..e4a34b7 100644 --- a/src/bluer/l2cap_channel.rs +++ b/src/bluer/l2cap_channel.rs @@ -6,25 +6,30 @@ use std::task::{Context, Poll}; use async_compat::Compat; use bluer::l2cap::stream::{OwnedReadHalf, OwnedWriteHalf}; +use bluer::l2cap::Stream; use futures_lite::io::{AsyncRead, AsyncWrite}; -pub struct L2capChannelReader { - pub(crate) reader: Compat, -} +use crate::{derive_async_read, derive_async_write}; -impl L2capChannelReader { - pub async fn close(&mut self) -> crate::Result<()> { - todo!() +pub struct L2capChannel(pub(super) Compat); + +impl L2capChannel { + pub fn split(self) -> (L2capChannelReader, L2capChannelWriter) { + let (reader, writer) = self.0.into_inner().into_split(); + let (reader, writer) = (Compat::new(reader), Compat::new(writer)); + (L2capChannelReader { reader }, L2capChannelWriter { writer }) } } -impl AsyncRead for L2capChannelReader { - fn poll_read(mut self: pin::Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut [u8]) -> Poll> { - let reader = pin::pin!(&mut self.reader); - reader.poll_read(cx, buf) - } +derive_async_read!(L2capChannel, 0); +derive_async_write!(L2capChannel, 0); + +pub struct L2capChannelReader { + pub(crate) reader: Compat, } +derive_async_read!(L2capChannelReader, reader); + impl Debug for L2capChannelReader { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { Debug::fmt(self.reader.get_ref(), f) @@ -35,28 +40,7 @@ pub struct L2capChannelWriter { pub(crate) writer: Compat, } -impl L2capChannelWriter { - pub async fn close(&mut self) -> crate::Result<()> { - todo!() - } -} - -impl AsyncWrite for L2capChannelWriter { - fn poll_write(mut self: pin::Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll> { - let writer = pin::pin!(&mut self.writer); - writer.poll_write(cx, buf) - } - - fn poll_flush(mut self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll> { - let writer = pin::pin!(&mut self.writer); - writer.poll_flush(cx) - } - - fn poll_close(mut self: pin::Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - let writer = pin::pin!(&mut self.writer); - writer.poll_close(cx) - } -} +derive_async_write!(L2capChannelWriter, writer); impl Debug for L2capChannelWriter { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { diff --git a/src/corebluetooth/device.rs b/src/corebluetooth/device.rs index d7e9f27..7335d6b 100644 --- a/src/corebluetooth/device.rs +++ b/src/corebluetooth/device.rs @@ -227,11 +227,7 @@ impl DeviceImpl { /// Open L2CAP channel given PSM #[cfg(feature = "l2cap")] - pub async fn open_l2cap_channel( - &self, - psm: u16, - _secure: bool, - ) -> Result<(L2capChannelReader, L2capChannelWriter)> { + pub async fn open_l2cap_channel(&self, psm: u16, _secure: bool) -> Result { use tracing::{debug, info}; let mut receiver = self.delegate.sender().new_receiver(); @@ -267,7 +263,7 @@ impl DeviceImpl { let reader = L2capChannelReader::new(l2capchannel.clone()); let writer = L2capChannelWriter::new(l2capchannel); - Ok((reader, writer)) + Ok(super::l2cap_channel::L2capChannel { reader, writer }) } } diff --git a/src/corebluetooth/l2cap_channel.rs b/src/corebluetooth/l2cap_channel.rs index ac6b79c..48f9d14 100644 --- a/src/corebluetooth/l2cap_channel.rs +++ b/src/corebluetooth/l2cap_channel.rs @@ -17,7 +17,7 @@ use tracing::{debug, trace, warn}; use super::dispatch::Dispatched; use crate::l2cap_channel::PIPE_CAPACITY; -use crate::Result; +use crate::{derive_async_read, derive_async_write}; /// Utility struct to close the channel on drop. pub(super) struct L2capCloser { @@ -43,10 +43,24 @@ impl Drop for L2capCloser { } } +pub struct L2capChannel { + pub(super) reader: L2capChannelReader, + pub(super) writer: L2capChannelWriter, +} + +impl L2capChannel { + pub fn split(self) -> (L2capChannelReader, L2capChannelWriter) { + (self.reader, self.writer) + } +} + +derive_async_read!(L2capChannel, reader); +derive_async_write!(L2capChannel, writer); + /// The reader side of an L2CAP channel. pub struct L2capChannelReader { stream: piper::Reader, - closer: Arc, + _closer: Arc, _delegate: Retained, } @@ -70,23 +84,12 @@ impl L2capChannelReader { Self { stream: read_rx, _delegate: delegate, - closer, + _closer: closer, } } - - /// Closes the L2CAP channel reader. - pub async fn close(&mut self) -> Result<()> { - self.closer.close(); - Ok(()) - } } -impl AsyncRead for L2capChannelReader { - fn poll_read(mut self: pin::Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut [u8]) -> Poll> { - let stream = pin::pin!(&mut self.stream); - stream.poll_read(cx, buf) - } -} +derive_async_read!(L2capChannelReader, stream); impl fmt::Debug for L2capChannelReader { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -136,12 +139,6 @@ impl L2capChannelWriter { center.postNotificationName_object(&name, None); } } - - /// Closes the L2CAP channel writer. - pub async fn close(&mut self) -> Result<()> { - self.closer.close(); - Ok(()) - } } impl AsyncWrite for L2capChannelWriter { diff --git a/src/device.rs b/src/device.rs index a0bb7cf..0ae440c 100644 --- a/src/device.rs +++ b/src/device.rs @@ -164,15 +164,8 @@ impl Device { #[inline] #[cfg(feature = "l2cap")] pub async fn open_l2cap_channel(&self, psm: u16, secure: bool) -> Result { - #[cfg(not(target_os = "linux"))] - { - let (reader, writer) = self.0.open_l2cap_channel(psm, secure).await?; - Ok(L2capChannel { reader, writer }) - } - #[cfg(target_os = "linux")] - { - self.0.open_l2cap_channel(psm, secure).await - } + let channel = self.0.open_l2cap_channel(psm, secure).await?; + Ok(L2capChannel(channel)) } } diff --git a/src/l2cap_channel.rs b/src/l2cap_channel.rs index 426d4a6..abb0dae 100644 --- a/src/l2cap_channel.rs +++ b/src/l2cap_channel.rs @@ -4,171 +4,77 @@ use std::task::{Context, Poll}; use futures_lite::io::{AsyncRead, AsyncWrite}; -use crate::{sys, Result}; +use crate::sys; #[allow(unused)] pub(crate) const PIPE_CAPACITY: usize = 0x100000; // 1Mb -#[cfg(not(target_os = "linux"))] -mod channel { - use std::pin; - use std::task::{Context, Poll}; - - use futures_lite::io::{AsyncRead, AsyncWrite}; - - use crate::{ - sys::l2cap_channel::{L2capChannelReader, L2capChannelWriter}, - Result, - }; - - /// A Bluetooth LE L2CAP Connection-oriented Channel (CoC) - #[derive(Debug)] - pub struct L2capChannel { - pub(crate) reader: L2capChannelReader, - pub(crate) writer: L2capChannelWriter, - } - - impl L2capChannel { - /// Close the L2CAP channel. - /// - /// This closes the entire channel, in both directions (reading and writing). - /// - /// The channel is automatically closed when `L2capChannel` is dropped, so - /// you don't need to call this explicitly. - #[inline] - pub async fn close(&mut self) -> Result<()> { - self.writer.close().await - } - - /// Split the channel into read and write halves. - #[inline] - pub fn split(self) -> (L2capChannelReader, L2capChannelWriter) { - let Self { reader, writer } = self; - (reader, writer) +#[macro_export] +#[doc(hidden)] +macro_rules! derive_async_read { + ($type:ty, $field:tt) => { + impl AsyncRead for $type { + fn poll_read( + mut self: pin::Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut [u8], + ) -> Poll> { + let reader = pin::pin!(&mut self.$field); + reader.poll_read(cx, buf) + } } - } - - impl AsyncRead for L2capChannel { - fn poll_read( - mut self: pin::Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &mut [u8], - ) -> Poll> { - let reader = pin::pin!(&mut self.reader); - reader.poll_read(cx, buf) - } - } - - impl AsyncWrite for L2capChannel { - fn poll_write(mut self: pin::Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll> { - let writer = pin::pin!(&mut self.writer); - writer.poll_write(cx, buf) - } - - fn poll_flush(mut self: pin::Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - let writer = pin::pin!(&mut self.writer); - writer.poll_flush(cx) - } - - fn poll_close(mut self: pin::Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - let writer = pin::pin!(&mut self.writer); - writer.poll_close(cx) - } - } -} - -#[cfg(target_os = "linux")] -mod channel { - use std::fmt::Debug; - use std::pin; - use std::task::{Context, Poll}; - - use async_compat::Compat; - use bluer::l2cap::Stream; - use futures_lite::io::{AsyncRead, AsyncWrite}; - use tokio::io::AsyncWriteExt; - - use crate::{ - sys::l2cap_channel::{L2capChannelReader, L2capChannelWriter}, - Result, }; +} - /// A Bluetooth LE L2CAP Connection-oriented Channel (CoC) - pub struct L2capChannel { - pub(crate) stream: Compat, - } - - impl L2capChannel { - /// Close the L2CAP channel. - /// - /// This closes the entire channel, in both directions (reading and writing). - /// - /// The channel is automatically closed when `L2capChannel` is dropped, so - /// you don't need to call this explicitly. - #[inline] - pub async fn close(&mut self) -> Result<()> { - self.stream.get_mut().shutdown().await?; - Ok(()) - } - - /// Split the channel into read and write halves. - #[inline] - pub fn split(self) -> (L2capChannelReader, L2capChannelWriter) { - let (reader, writer) = self.stream.into_inner().into_split(); - ( - L2capChannelReader { - reader: Compat::new(reader), - }, - L2capChannelWriter { - writer: Compat::new(writer), - }, - ) - } - - /// Gets the Unerlying Stream type wich may support platform-specific additional functionality. - /// - /// Linux Only - pub fn into_inner(self) -> Stream { - self.stream.into_inner() - } - } - - impl AsyncRead for L2capChannel { - fn poll_read( - mut self: pin::Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &mut [u8], - ) -> Poll> { - let stream = pin::pin!(&mut self.stream); - stream.poll_read(cx, buf) - } - } - - impl AsyncWrite for L2capChannel { - fn poll_write(mut self: pin::Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll> { - let stream = pin::pin!(&mut self.stream); - stream.poll_write(cx, buf) - } - - fn poll_flush(mut self: pin::Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - let stream = pin::pin!(&mut self.stream); - stream.poll_flush(cx) +#[macro_export] +#[doc(hidden)] +macro_rules! derive_async_write { + ($type:ty, $field:tt) => { + impl AsyncWrite for $type { + fn poll_write( + mut self: pin::Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + let writer = pin::pin!(&mut self.$field); + writer.poll_write(cx, buf) + } + + fn poll_flush(mut self: pin::Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let writer = pin::pin!(&mut self.$field); + writer.poll_flush(cx) + } + + fn poll_close(mut self: pin::Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let writer = pin::pin!(&mut self.$field); + writer.poll_close(cx) + } + + fn poll_write_vectored( + mut self: pin::Pin<&mut Self>, + cx: &mut Context<'_>, + bufs: &[std::io::IoSlice<'_>], + ) -> Poll> { + let writer = pin::pin!(&mut self.$field); + writer.poll_write_vectored(cx, bufs) + } } + }; +} - fn poll_close(mut self: pin::Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - let stream = pin::pin!(&mut self.stream); - stream.poll_close(cx) - } - } +/// A Bluetooth LE L2CAP Connection-oriented Channel (CoC) +pub struct L2capChannel(pub(super) sys::l2cap_channel::L2capChannel); - impl Debug for L2capChannel { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - Debug::fmt(self.stream.get_ref(), f) - } +impl L2capChannel { + /// Splits the channel into a read half and a write half + pub fn split(self) -> (L2capChannelReader, L2capChannelWriter) { + let (reader, writer) = self.0.split(); + (L2capChannelReader { reader }, L2capChannelWriter { writer }) } } -pub use channel::L2capChannel; +derive_async_read!(L2capChannel, 0); +derive_async_write!(L2capChannel, 0); /// Reader half of a L2CAP Connection-oriented Channel (CoC) #[derive(Debug)] @@ -182,52 +88,6 @@ pub struct L2capChannelWriter { writer: sys::l2cap_channel::L2capChannelWriter, } -impl L2capChannelReader { - /// Close the L2CAP channel. - /// - /// This closes the entire channel, not just the read half. - /// - /// The channel is automatically closed when both the `L2capChannelWriter` - /// and `L2capChannelReader` are dropped, so you don't need to call this explicitly. - #[inline] - pub async fn close(&mut self) -> Result<()> { - self.reader.close().await - } -} - -impl AsyncRead for L2capChannelReader { - fn poll_read(mut self: pin::Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut [u8]) -> Poll> { - let reader = pin::pin!(&mut self.reader); - reader.poll_read(cx, buf) - } -} - -impl L2capChannelWriter { - /// Close the L2CAP channel. - /// - /// This closes the entire channel, not just the write half. - /// - /// The channel is automatically closed when both the `L2capChannelWriter` - /// and `L2capChannelReader` are dropped, so you don't need to call this explicitly. - #[inline] - pub async fn close(&mut self) -> Result<()> { - self.writer.close().await - } -} - -impl AsyncWrite for L2capChannelWriter { - fn poll_write(mut self: pin::Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll> { - let writer = pin::pin!(&mut self.writer); - writer.poll_write(cx, buf) - } +derive_async_read!(L2capChannelReader, reader); - fn poll_flush(mut self: pin::Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - let writer = pin::pin!(&mut self.writer); - writer.poll_flush(cx) - } - - fn poll_close(mut self: pin::Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - let writer = pin::pin!(&mut self.writer); - writer.poll_close(cx) - } -} +derive_async_write!(L2capChannelWriter, writer); From 7a02581711ad14b7771b10b54f9ced68b6ee0fe4 Mon Sep 17 00:00:00 2001 From: Abe Zukor Date: Sat, 23 Aug 2025 00:39:54 -0700 Subject: [PATCH 4/7] Done export `derive_async_read` and `derive_async_write` --- src/android/l2cap_channel.rs | 1 - src/bluer/l2cap_channel.rs | 2 -- src/corebluetooth/l2cap_channel.rs | 1 - src/l2cap_channel.rs | 4 ---- src/lib.rs | 10 +++++++--- 5 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/android/l2cap_channel.rs b/src/android/l2cap_channel.rs index 37b8e1e..d351ac9 100644 --- a/src/android/l2cap_channel.rs +++ b/src/android/l2cap_channel.rs @@ -12,7 +12,6 @@ use tracing::{debug, trace, warn}; use super::bindings::android::bluetooth::{BluetoothDevice, BluetoothSocket}; use super::OptionExt; use crate::l2cap_channel::PIPE_CAPACITY; -use crate::{derive_async_read, derive_async_write}; pub fn open_l2cap_channel( device: Global, diff --git a/src/bluer/l2cap_channel.rs b/src/bluer/l2cap_channel.rs index e4a34b7..a9ec4c3 100644 --- a/src/bluer/l2cap_channel.rs +++ b/src/bluer/l2cap_channel.rs @@ -9,8 +9,6 @@ use bluer::l2cap::stream::{OwnedReadHalf, OwnedWriteHalf}; use bluer::l2cap::Stream; use futures_lite::io::{AsyncRead, AsyncWrite}; -use crate::{derive_async_read, derive_async_write}; - pub struct L2capChannel(pub(super) Compat); impl L2capChannel { diff --git a/src/corebluetooth/l2cap_channel.rs b/src/corebluetooth/l2cap_channel.rs index 48f9d14..b253261 100644 --- a/src/corebluetooth/l2cap_channel.rs +++ b/src/corebluetooth/l2cap_channel.rs @@ -17,7 +17,6 @@ use tracing::{debug, trace, warn}; use super::dispatch::Dispatched; use crate::l2cap_channel::PIPE_CAPACITY; -use crate::{derive_async_read, derive_async_write}; /// Utility struct to close the channel on drop. pub(super) struct L2capCloser { diff --git a/src/l2cap_channel.rs b/src/l2cap_channel.rs index abb0dae..f1140a3 100644 --- a/src/l2cap_channel.rs +++ b/src/l2cap_channel.rs @@ -9,8 +9,6 @@ use crate::sys; #[allow(unused)] pub(crate) const PIPE_CAPACITY: usize = 0x100000; // 1Mb -#[macro_export] -#[doc(hidden)] macro_rules! derive_async_read { ($type:ty, $field:tt) => { impl AsyncRead for $type { @@ -26,8 +24,6 @@ macro_rules! derive_async_read { }; } -#[macro_export] -#[doc(hidden)] macro_rules! derive_async_write { ($type:ty, $field:tt) => { impl AsyncWrite for $type { diff --git a/src/lib.rs b/src/lib.rs index 9176059..3a278d1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -91,9 +91,9 @@ //!| [`Characteristic::max_write_len`][Characteristic::max_write_len] | ✅ | ✅ | ⌛️ | //!| [`Descriptor::uuid`][Descriptor::uuid] | ✅ | ✅ | ⌛️ | //! -//! ✅ = supported -//! ✨ = managed automatically by the OS, this method is a no-op -//! ⌛️ = the underlying API is async so this method uses Tokio's `block_in_place` API internally +//! ✅ = supported +//! ✨ = managed automatically by the OS, this method is a no-op +//! ⌛️ = the underlying API is async so this method uses Tokio's `block_in_place` API internally //! ❌ = returns a [`NotSupported`][error::ErrorKind::NotSupported] error //! //! Also, the errors returned by APIs in a given situation may not be consistent from platform to platform. For example, @@ -119,7 +119,11 @@ mod characteristic; mod descriptor; mod device; pub mod error; + +#[cfg(feature = "l2cap")] +#[macro_use] mod l2cap_channel; + pub mod pairing; mod service; mod util; From fc220a9fd61b6422daccece6a2f69d97d8df8e8f Mon Sep 17 00:00:00 2001 From: Abe Zukor Date: Sat, 30 Aug 2025 21:21:37 -0700 Subject: [PATCH 5/7] `pub(crate) use {derive_async_read, derive_async_write};` instead of `#[macro_use]` --- src/android/l2cap_channel.rs | 2 +- src/bluer/l2cap_channel.rs | 2 ++ src/corebluetooth/l2cap_channel.rs | 2 +- src/l2cap_channel.rs | 2 ++ src/lib.rs | 1 - 5 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/android/l2cap_channel.rs b/src/android/l2cap_channel.rs index d351ac9..c883da5 100644 --- a/src/android/l2cap_channel.rs +++ b/src/android/l2cap_channel.rs @@ -11,7 +11,7 @@ use tracing::{debug, trace, warn}; use super::bindings::android::bluetooth::{BluetoothDevice, BluetoothSocket}; use super::OptionExt; -use crate::l2cap_channel::PIPE_CAPACITY; +use crate::l2cap_channel::{derive_async_read, derive_async_write, PIPE_CAPACITY}; pub fn open_l2cap_channel( device: Global, diff --git a/src/bluer/l2cap_channel.rs b/src/bluer/l2cap_channel.rs index a9ec4c3..4989414 100644 --- a/src/bluer/l2cap_channel.rs +++ b/src/bluer/l2cap_channel.rs @@ -9,6 +9,8 @@ use bluer::l2cap::stream::{OwnedReadHalf, OwnedWriteHalf}; use bluer::l2cap::Stream; use futures_lite::io::{AsyncRead, AsyncWrite}; +use crate::l2cap_channel::{derive_async_read, derive_async_write}; + pub struct L2capChannel(pub(super) Compat); impl L2capChannel { diff --git a/src/corebluetooth/l2cap_channel.rs b/src/corebluetooth/l2cap_channel.rs index b253261..1f763f4 100644 --- a/src/corebluetooth/l2cap_channel.rs +++ b/src/corebluetooth/l2cap_channel.rs @@ -16,7 +16,7 @@ use objc2_foundation::{ use tracing::{debug, trace, warn}; use super::dispatch::Dispatched; -use crate::l2cap_channel::PIPE_CAPACITY; +use crate::l2cap_channel::{derive_async_read, derive_async_write, PIPE_CAPACITY}; /// Utility struct to close the channel on drop. pub(super) struct L2capCloser { diff --git a/src/l2cap_channel.rs b/src/l2cap_channel.rs index f1140a3..3ee7c38 100644 --- a/src/l2cap_channel.rs +++ b/src/l2cap_channel.rs @@ -58,6 +58,8 @@ macro_rules! derive_async_write { }; } +pub(crate) use {derive_async_read, derive_async_write}; + /// A Bluetooth LE L2CAP Connection-oriented Channel (CoC) pub struct L2capChannel(pub(super) sys::l2cap_channel::L2capChannel); diff --git a/src/lib.rs b/src/lib.rs index 3a278d1..15a5203 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -121,7 +121,6 @@ mod device; pub mod error; #[cfg(feature = "l2cap")] -#[macro_use] mod l2cap_channel; pub mod pairing; From cadbacd47a59c75437ed9da5d7748e5c41f96c83 Mon Sep 17 00:00:00 2001 From: Abe Zukor Date: Sat, 30 Aug 2025 21:28:45 -0700 Subject: [PATCH 6/7] Upgrade bluer - Clears future incompatability error ``` warning: the following packages contain code that will be rejected by a future version of Rust: bluer v0.16.1 note: to see what the problems were, use the option `--future-incompat-report`, or run `cargo report future-incompatibilities --id 1` ``` --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 13da051..8d27c88 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,7 +57,7 @@ windows = { version = "0.48.0", features = [ ] } [target.'cfg(target_os = "linux")'.dependencies] -bluer = { version = "0.16.1", features = ["bluetoothd"] } +bluer = { version = "0.17.4", features = ["bluetoothd"] } async-compat = { version = "0.2", optional = true } tokio = { version = "1.20.1", features = ["rt-multi-thread"] } From 8fff7fbfb0baace71570cf2014644e53a857eb3f Mon Sep 17 00:00:00 2001 From: Abe Zukor Date: Sat, 30 Aug 2025 21:31:55 -0700 Subject: [PATCH 7/7] clippy: Fix duplicated `#[cfg(target_os = "linux")]` --- src/l2cap_channel.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/l2cap_channel.rs b/src/l2cap_channel.rs index 3ee7c38..3ae1ac9 100644 --- a/src/l2cap_channel.rs +++ b/src/l2cap_channel.rs @@ -1,4 +1,3 @@ -#![cfg(feature = "l2cap")] use std::pin; use std::task::{Context, Poll};