diff --git a/Cargo.toml b/Cargo.toml index 608ed3d..8d27c88 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] @@ -57,7 +57,8 @@ 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"] } [target.'cfg(target_os = "android")'.dependencies] 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 769e1aa..c883da5 100644 --- a/src/android/l2cap_channel.rs +++ b/src/android/l2cap_channel.rs @@ -11,8 +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::Result; +use crate::l2cap_channel::{derive_async_read, derive_async_write, PIPE_CAPACITY}; pub fn open_l2cap_channel( device: Global, @@ -116,11 +115,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 +149,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,34 +178,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); - let ret = stream.poll_write(cx, buf); - ret - } - - 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 c1e6087..26093b9 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,16 @@ 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(super::l2cap_channel::L2capChannel(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..4989414 100644 --- a/src/bluer/l2cap_channel.rs +++ b/src/bluer/l2cap_channel.rs @@ -1,51 +1,49 @@ #![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 bluer::l2cap::Stream; use futures_lite::io::{AsyncRead, AsyncWrite}; -#[derive(Debug)] -pub struct L2capChannelReader { - _private: (), -} +use crate::l2cap_channel::{derive_async_read, derive_async_write}; -impl L2capChannelReader { - pub async fn close(&mut self) -> crate::Result<()> { - todo!() - } -} +pub struct L2capChannel(pub(super) Compat); -impl AsyncRead for L2capChannelReader { - fn poll_read( - self: Pin<&mut Self>, - _cx: &mut Context<'_>, - _buf: &mut [u8], - ) -> std::task::Poll> { - todo!() +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 }) } } -#[derive(Debug)] -pub struct L2capChannelWriter { - _private: (), + +derive_async_read!(L2capChannel, 0); +derive_async_write!(L2capChannel, 0); + +pub struct L2capChannelReader { + pub(crate) reader: Compat, } -impl L2capChannelWriter { - pub async fn close(&mut self) -> crate::Result<()> { - todo!() +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) } } -impl AsyncWrite for L2capChannelWriter { - fn poll_write(self: Pin<&mut Self>, _cx: &mut Context<'_>, _buf: &[u8]) -> std::task::Poll> { - todo!() - } +pub struct L2capChannelWriter { + pub(crate) writer: Compat, +} - fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> std::task::Poll> { - todo!() - } +derive_async_write!(L2capChannelWriter, writer); - fn poll_close(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> std::task::Poll> { - todo!() +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/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..1f763f4 100644 --- a/src/corebluetooth/l2cap_channel.rs +++ b/src/corebluetooth/l2cap_channel.rs @@ -16,8 +16,7 @@ use objc2_foundation::{ use tracing::{debug, trace, warn}; use super::dispatch::Dispatched; -use crate::l2cap_channel::PIPE_CAPACITY; -use crate::Result; +use crate::l2cap_channel::{derive_async_read, derive_async_write, PIPE_CAPACITY}; /// Utility struct to close the channel on drop. pub(super) struct L2capCloser { @@ -43,10 +42,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 +83,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 +138,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 989e7d7..0ae440c 100644 --- a/src/device.rs +++ b/src/device.rs @@ -164,8 +164,8 @@ 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 }) + 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 fe0481c..3ae1ac9 100644 --- a/src/l2cap_channel.rs +++ b/src/l2cap_channel.rs @@ -1,123 +1,90 @@ -#![cfg(feature = "l2cap")] use std::pin; 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 -/// 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, +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) + } + } + }; } -/// Reader half of a L2CAP Connection-oriented Channel (CoC) -#[derive(Debug)] -pub struct L2capChannelReader { - reader: sys::l2cap_channel::L2capChannelReader, +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) + } + } + }; } -/// Writerhalf of a L2CAP Connection-oriented Channel (CoC) -#[derive(Debug)] -pub struct L2capChannelWriter { - writer: sys::l2cap_channel::L2capChannelWriter, -} +pub(crate) use {derive_async_read, derive_async_write}; -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 - } +/// A Bluetooth LE L2CAP Connection-oriented Channel (CoC) +pub struct L2capChannel(pub(super) sys::l2cap_channel::L2capChannel); - /// Split the channel into read and write halves. - #[inline] +impl L2capChannel { + /// Splits the channel into a read half and a write half pub fn split(self) -> (L2capChannelReader, L2capChannelWriter) { - let Self { reader, writer } = self; + let (reader, writer) = self.0.split(); (L2capChannelReader { reader }, L2capChannelWriter { writer }) } } -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) - } -} - -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 - } -} +derive_async_read!(L2capChannel, 0); +derive_async_write!(L2capChannel, 0); -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) - } +/// Reader half of a L2CAP Connection-oriented Channel (CoC) +#[derive(Debug)] +pub struct L2capChannelReader { + reader: sys::l2cap_channel::L2capChannelReader, } -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 - } +/// Writerhalf of a L2CAP Connection-oriented Channel (CoC) +#[derive(Debug)] +pub struct L2capChannelWriter { + writer: sys::l2cap_channel::L2capChannelWriter, } -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: pin::Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - let writer = pin::pin!(&mut self.writer); - writer.poll_flush(cx) - } +derive_async_read!(L2capChannelReader, reader); - 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); diff --git a/src/lib.rs b/src/lib.rs index 9176059..15a5203 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,10 @@ mod characteristic; mod descriptor; mod device; pub mod error; + +#[cfg(feature = "l2cap")] mod l2cap_channel; + pub mod pairing; mod service; mod util;