From 5c60d1a98103a9b2af069e045b04b1c790e13292 Mon Sep 17 00:00:00 2001 From: Abe Zukor Date: Mon, 10 Jun 2024 17:40:40 -0700 Subject: [PATCH 1/3] AsyncReadWrite L2Cap. Add Linux and Apple L2Cap Channels. --- Cargo.toml | 14 +- README.md | 2 + examples/l2cap_client.rs | 64 ++++++ src/android/device.rs | 12 +- src/android/l2cap_channel.rs | 331 +++++++++++++---------------- src/android/mod.rs | 4 +- src/bluer.rs | 4 +- src/bluer/device.rs | 18 +- src/bluer/l2cap_channel.rs | 118 +++++++--- src/corebluetooth.rs | 4 +- src/corebluetooth/delegates.rs | 4 +- src/corebluetooth/device.rs | 45 +++- src/corebluetooth/l2cap_channel.rs | 145 ++++++++++--- src/corebluetooth/types.rs | 120 ++++++++++- src/device.rs | 13 +- src/l2cap_channel.rs | 119 ++--------- src/lib.rs | 13 +- src/windows/device.rs | 2 +- 18 files changed, 648 insertions(+), 384 deletions(-) create mode 100644 examples/l2cap_client.rs diff --git a/Cargo.toml b/Cargo.toml index bc85ef7..017c5ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ targets = [ [features] serde = ["dep:serde", "uuid/serde", "bluer/serde"] +l2cap = ["dep:tokio", "bluer/l2cap"] [dependencies] async-trait = "0.1.57" @@ -30,7 +31,12 @@ serde = { version = "1.0.143", optional = true, features = ["derive"] } tracing = { version = "0.1.36", default-features = false } [dev-dependencies] -tokio = { version = "1.20.1", features = ["macros", "rt-multi-thread", "time"] } +tokio = { version = "1.20.1", features = [ + "macros", + "rt-multi-thread", + "time", + "io-util", +] } tracing-subscriber = { version = "0.3.15", features = ["env-filter"] } [target.'cfg(not(target_os = "linux"))'.dependencies] @@ -57,13 +63,19 @@ tokio = { version = "1.20.1", features = ["rt-multi-thread"] } [target.'cfg(target_os = "android")'.dependencies] java-spaghetti = "0.2.0" async-channel = "2.2.0" +tokio = { version = "1.20.1", features = ["rt", "io-util"], optional = true } [target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] async-broadcast = "0.5.1" objc = "0.2.7" objc_id = "0.1.1" objc-foundation = "0.1.1" +tokio = { version = "1.20.1", features = ["net"], optional = true } [[example]] name = "scan" doc-scrape-examples = true + +[[example]] +name = "l2cap_client" +required-features = ["l2cap"] diff --git a/README.md b/README.md index e7281a1..8e63c02 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,7 @@ below. | [`Device::pair_with_agent`][Device::pair_with_agent] | ✨ | ✅ | ✅ | | [`Device::unpair`][Device::unpair] | ❌ | ✅ | ✅ | | [`Device::rssi`][Device::rssi] | ✅ | ❌ | ❌ | +| [`Device::open_l2cap_channel`][Device::open_l2cap_channel] | ⌛️ | ❌ | ⌛️ | | [`Service::uuid`][Service::uuid] | ✅ | ✅ | ⌛️ | | [`Service::is_primary`][Service::is_primary] | ✅ | ❌ | ✅ | | [`Characteristic::uuid`][Characteristic::uuid] | ✅ | ✅ | ⌛️ | @@ -149,6 +150,7 @@ Refer to the [API documentation] for more details. [Device::unpair]: https://docs.rs/bluest/latest/bluest/struct.Device.html#method.unpair [Device::discover_services]: https://docs.rs/bluest/latest/bluest/struct.Device.html#method.discover_services [Device::rssi]: https://docs.rs/bluest/latest/bluest/struct.Device.html#method.rssi +[Device::open_l2cap_channel]: https://docs.rs/bluest/latest/bluest/struct.Device.html#method.open_l2cap_channel [Service::uuid]: https://docs.rs/bluest/latest/bluest/struct.Service.html#method.uuid [Service::is_primary]: https://docs.rs/bluest/latest/bluest/struct.Service.html#method.is_primary [Service::discover_characteristics]: https://docs.rs/bluest/latest/bluest/struct.Service.html#method.discover_characteristics diff --git a/examples/l2cap_client.rs b/examples/l2cap_client.rs new file mode 100644 index 0000000..7f309fd --- /dev/null +++ b/examples/l2cap_client.rs @@ -0,0 +1,64 @@ +// This is designed to be used in conjunction with the l2cap_server example from bluer. https://github.com/bluez/bluer/blob/869dab889140e3be5a0f1791c40825730893c8b6/bluer/examples/l2cap_server.rs + +use std::error::Error; + +use bluest::{Adapter, Uuid as BluestUUID}; +use futures_lite::StreamExt; +use tokio::io::AsyncReadExt; +use tracing::info; +use tracing::metadata::LevelFilter; + +#[cfg(target_os = "linux")] +const SERVICE_UUID: BluestUUID = bluer::Uuid::from_u128(0xFEED0000F00D); + +#[cfg(not(target_os = "linux"))] +use uuid::Uuid; +#[cfg(not(target_os = "linux"))] +const SERVICE_UUID: BluestUUID = Uuid::from_u128(0xFEED0000F00D); + +const PSM: u16 = 0x80 + 5; + +const HELLO_MSG: &[u8] = b"Hello from l2cap_server!"; + +#[tokio::main] +async fn main() -> Result<(), Box> { + use tracing_subscriber::prelude::*; + use tracing_subscriber::{fmt, EnvFilter}; + + tracing_subscriber::registry() + .with(fmt::layer()) + .with( + EnvFilter::builder() + .with_default_directive(LevelFilter::INFO.into()) + .from_env_lossy(), + ) + .init(); + + let adapter = Adapter::default().await.unwrap(); + adapter.wait_available().await?; + + info!("looking for device"); + let device = adapter + .discover_devices(&[SERVICE_UUID]) + .await? + .next() + .await + .ok_or("Failed to discover device")??; + info!( + "found device: {} ({:?})", + device.name().as_deref().unwrap_or("(unknown)"), + device.id() + ); + + adapter.connect_device(&device).await.unwrap(); + + let mut channel = device.open_l2cap_channel(PSM, true).await.unwrap(); + + info!("Reading from channel."); + let mut hello_buf = [0u8; HELLO_MSG.len()]; + channel.read_exact(&mut hello_buf).await.unwrap(); + + info!("Got {} from channel", std::str::from_utf8(&hello_buf).unwrap()); + assert_eq!(hello_buf, HELLO_MSG); + Ok(()) +} diff --git a/src/android/device.rs b/src/android/device.rs index 7d468e4..6fe73c8 100644 --- a/src/android/device.rs +++ b/src/android/device.rs @@ -4,13 +4,14 @@ use java_spaghetti::Global; use uuid::Uuid; use super::bindings::android::bluetooth::BluetoothDevice; -use super::l2cap_channel::{L2capChannelReader, L2capChannelWriter}; use crate::pairing::PairingAgent; use crate::{DeviceId, Result, Service, ServicesChanged}; #[derive(Clone)] pub struct DeviceImpl { pub(super) id: DeviceId, + + #[allow(unused)] pub(super) device: Global, } @@ -97,12 +98,9 @@ impl DeviceImpl { todo!() } - 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) + #[cfg(feature = "l2cap")] + pub async fn open_l2cap_channel(&self, psm: u16, secure: bool) -> Result { + super::l2cap_channel::Channel::new(self.device.clone(), psm, secure) } } diff --git a/src/android/l2cap_channel.rs b/src/android/l2cap_channel.rs index 6a2c723..0fe82ff 100644 --- a/src/android/l2cap_channel.rs +++ b/src/android/l2cap_channel.rs @@ -1,128 +1,152 @@ -use std::sync::Arc; -use std::{fmt, slice, thread}; +use std::{ + fmt, + io::Result, + pin::Pin, + slice, + task::{Context, Poll}, + thread, +}; -use async_channel::{Receiver, Sender, TryRecvError, TrySendError}; use java_spaghetti::{ByteArray, Global, Local, PrimitiveArray}; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, DuplexStream, ReadBuf}; +use tokio::runtime::Handle; use tracing::{debug, warn}; use super::bindings::android::bluetooth::{BluetoothDevice, BluetoothSocket}; +use super::bindings::java::io::{InputStream, OutputStream}; use super::OptionExt; -use crate::error::ErrorKind; -use crate::{Error, Result}; - -pub fn open_l2cap_channel( - device: Global, - psm: u16, - secure: bool, -) -> std::prelude::v1::Result<(L2capChannelReader, L2capChannelWriter), crate::Error> { - device.vm().with_env(|env| { - let device = device.as_local(env); - - let channel = if secure { - device.createL2capChannel(psm as _)?.non_null()? - } else { - device.createInsecureL2capChannel(psm as _)?.non_null()? - }; - - channel.connect()?; - - // The L2capCloser closes the l2cap channel when dropped. - // We put it in an Arc held by both the reader and writer, so it gets dropped - // when - let closer = Arc::new(L2capCloser { - channel: channel.as_global(), - }); - let (read_sender, read_receiver) = async_channel::bounded::>(16); - let (write_sender, write_receiver) = async_channel::bounded::>(16); - let input_stream = channel.getInputStream()?.non_null()?.as_global(); - let output_stream = channel.getOutputStream()?.non_null()?.as_global(); - - // Unfortunately, Android's API for L2CAP channels is only blocking. Only way to deal with it - // is to launch two background threads with blocking loops for reading and writing, which communicate - // with the async Rust world via async channels. - // - // The loops stop when either Android returns an error (for example if the channel is closed), or the - // async channel gets closed because the user dropped the reader or writer structs. - thread::spawn(move || { - debug!("l2cap read thread running!"); - - input_stream.vm().with_env(|env| { - let stream = input_stream.as_local(env); - let arr: Local = ByteArray::new(env, 1024); - - loop { - match stream.read_byte_array(&arr) { - Ok(n) if n < 0 => { - warn!("failed to read from l2cap channel: {}", n); - break; - } - Err(e) => { - warn!("failed to read from l2cap channel: {:?}", e); +const BUFFER_CAPACITY: usize = 4096; +pub struct Channel { + stream: Pin>, + channel: Global, +} + +impl Channel { + pub fn new(device: Global, psm: u16, secure: bool) -> crate::Result { + let rt = tokio::runtime::Handle::current(); + + device.vm().with_env(|env| { + let device = device.as_local(env); + + let channel = if secure { + device.createL2capChannel(psm as _)?.non_null()? + } else { + device.createInsecureL2capChannel(psm as _)?.non_null()? + }; + + channel.connect()?; + + let global_channel = channel.as_global(); + + let (native_in_stream, native_out_stream) = tokio::io::duplex(BUFFER_CAPACITY); + + let (read_out, write_out) = tokio::io::split(native_out_stream); + + let input_stream = channel.getInputStream()?.non_null()?.as_global(); + let output_stream = channel.getOutputStream()?.non_null()?.as_global(); + + // Unfortunately, Android's API for L2CAP channels is only blocking. Only way to deal with it + // is to launch two background threads with blocking loops for reading and writing, which communicate + // with the async Rust world via the stream channels. + let read_rt = rt.clone(); + thread::spawn(move || Self::read_thread(input_stream, write_out, Box::pin(read_rt))); + + let transmit_size = usize::try_from(channel.getMaxTransmitPacketSize()?).unwrap(); + thread::spawn(move || Self::write_thread(output_stream, read_out, Box::pin(rt), transmit_size)); + + Ok(Self { + stream: Box::pin(native_in_stream), + channel: global_channel, + }) + }) + } + + // + // The loops stop when either Android returns an error (for example if the channel is closed), or the + // async channel gets closed because the user dropped the reader or writer structs. + fn read_thread( + input_stream: Global, + mut write_output: impl AsyncWrite + Unpin, + mut rt: Pin>, + ) { + debug!("l2cap read thread running!"); + + input_stream.vm().with_env(|env| { + let stream = input_stream.as_local(env); + let arr: Local = ByteArray::new(env, 1024); + + loop { + match stream.read_byte_array(&arr) { + Ok(n) if n < 0 => { + warn!("failed to read from l2cap channel: {}", n); + break; + } + Err(e) => { + warn!("failed to read from l2cap channel: {:?}", e); + break; + } + Ok(n) => { + let n = n as usize; + let mut buf = vec![0u8; n]; + arr.get_region(0, u8toi8_mut(&mut buf)); + + if let Err(e) = rt.as_mut().block_on(write_output.write_all(&mut buf)) { + warn!("failed to enqueue received l2cap packet: {:?}", e); break; } - Ok(n) => { - let n = n as usize; - let mut buf = vec![0u8; n]; - arr.get_region(0, u8toi8_mut(&mut buf)); - if let Err(e) = read_sender.send_blocking(buf) { - warn!("failed to enqueue received l2cap packet: {:?}", e); - break; - } - } } } - }); - - debug!("l2cap read thread exiting!"); + } }); - thread::spawn(move || { - debug!("l2cap write thread running!"); + debug!("l2cap read thread exiting!"); + } + + // + // The loops stop when either Android returns an error (for example if the channel is closed), or the + // async channel gets closed because the user dropped the reader or writer structs. - output_stream.vm().with_env(|env| { - let stream = output_stream.as_local(env); + fn write_thread( + output_stream: Global, + mut read_output: impl AsyncRead + Unpin, + rt: Pin>, + transmit_size: usize, + ) { + debug!("l2cap write thread running!"); - loop { - match write_receiver.recv_blocking() { - Err(e) => { - warn!("failed to dequeue l2cap packet to send: {:?}", e); + output_stream.vm().with_env(|env| { + let stream = output_stream.as_local(env); + + let mut buf = vec![0u8; transmit_size]; + + loop { + match rt.block_on(read_output.read(&mut buf)) { + Err(e) => { + warn!("failed to dequeue l2cap packet to send: {:?}", e); + break; + } + Ok(0) => { + debug!("End of stream reached"); + break; + } + Ok(packet_size) => { + let b = ByteArray::new_from(env, u8toi8(&buf[..packet_size])); + if let Err(e) = stream.write_byte_array(b) { + warn!("failed to write to l2cap channel: {:?}", e); break; - } - Ok(packet) => { - let b = ByteArray::new_from(env, u8toi8(&packet)); - if let Err(e) = stream.write_byte_array(b) { - warn!("failed to write to l2cap channel: {:?}", e); - break; - }; - } + }; } } - }); - - debug!("l2cap write thread exiting!"); + } }); - Ok(( - L2capChannelReader { - closer: closer.clone(), - stream: read_receiver, - }, - L2capChannelWriter { - closer, - stream: write_sender, - }, - )) - }) -} - -/// Utility struct to close the channel on drop. -pub(super) struct L2capCloser { - channel: Global, + debug!("l2cap write thread exiting!"); + } } -impl L2capCloser { - fn close(&self) { +impl Drop for Channel { + fn drop(&mut self) { self.channel.vm().with_env(|env| { let channel = self.channel.as_local(env); match channel.close() { @@ -133,100 +157,35 @@ impl L2capCloser { } } -impl Drop for L2capCloser { - fn drop(&mut self) { - self.close() +impl AsyncRead for Channel { + fn poll_read(mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut ReadBuf<'_>) -> Poll> { + self.stream.as_mut().poll_read(cx, buf) } } -pub struct L2capChannelReader { - stream: Receiver>, - closer: Arc, -} - -impl L2capChannelReader { - #[inline] - pub async fn read(&mut self, buf: &mut [u8]) -> Result { - let packet = self - .stream - .recv() - .await - .map_err(|_| Error::new(ErrorKind::ConnectionFailed, None, "channel is closed".to_string()))?; - - if packet.len() > buf.len() { - return Err(Error::new( - ErrorKind::InvalidParameter, - None, - "Buffer is too small".to_string(), - )); - } - - buf[..packet.len()].copy_from_slice(&packet); - - Ok(packet.len()) +impl AsyncWrite for Channel { + fn poll_write(mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll> { + self.stream.as_mut().poll_write(cx, buf) } - #[inline] - pub fn try_read(&mut self, buf: &mut [u8]) -> Result { - let packet = self.stream.try_recv().map_err(|e| match e { - TryRecvError::Empty => Error::new(ErrorKind::NotReady, None, "no received packet in queue".to_string()), - TryRecvError::Closed => Error::new(ErrorKind::ConnectionFailed, None, "channel is closed".to_string()), - })?; - - if packet.len() > buf.len() { - return Err(Error::new( - ErrorKind::InvalidParameter, - None, - "Buffer is too small".to_string(), - )); - } - - buf[..packet.len()].copy_from_slice(&packet); - - Ok(packet.len()) - } - - pub async fn close(&mut self) -> Result<()> { - self.closer.close(); - Ok(()) - } -} - -impl fmt::Debug for L2capChannelReader { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("L2capChannelReader") - } -} - -pub struct L2capChannelWriter { - stream: Sender>, - closer: Arc, -} - -impl L2capChannelWriter { - pub async fn write(&mut self, packet: &[u8]) -> Result<()> { - self.stream - .send(packet.to_vec()) - .await - .map_err(|_| Error::new(ErrorKind::ConnectionFailed, None, "channel is closed".to_string())) - } - - pub fn try_write(&mut self, packet: &[u8]) -> Result<()> { - self.stream.try_send(packet.to_vec()).map_err(|e| match e { - TrySendError::Closed(_) => Error::new(ErrorKind::ConnectionFailed, None, "channel is closed".to_string()), - TrySendError::Full(_) => Error::new(ErrorKind::NotReady, None, "No buffer space for write".to_string()), - }) + fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.stream.as_mut().poll_flush(cx) } - pub async fn close(&mut self) -> Result<()> { - self.closer.close(); - Ok(()) + fn poll_shutdown( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + self.stream.as_mut().poll_shutdown(cx) } } -impl fmt::Debug for L2capChannelWriter { +impl std::fmt::Debug for Channel { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("L2capChannelWriter") + f.debug_struct("Channel") + .field("stream", &self.stream) + .field("channel", &"Android Bluetooth Channel") + .finish() } } diff --git a/src/android/mod.rs b/src/android/mod.rs index f79a685..d042a3c 100644 --- a/src/android/mod.rs +++ b/src/android/mod.rs @@ -7,9 +7,11 @@ pub mod adapter; pub mod characteristic; pub mod descriptor; pub mod device; -pub mod l2cap_channel; pub mod service; +#[cfg(feature = "l2cap")] +pub mod l2cap_channel; + pub(crate) mod bindings; /// A platform-specific device identifier. diff --git a/src/bluer.rs b/src/bluer.rs index a290b9b..cf663b0 100644 --- a/src/bluer.rs +++ b/src/bluer.rs @@ -2,9 +2,11 @@ pub mod adapter; pub mod characteristic; pub mod descriptor; pub mod device; -pub mod l2cap_channel; pub mod service; +#[cfg(feature = "l2cap")] +pub mod l2cap_channel; + mod error; /// A platform-specific device identifier. diff --git a/src/bluer/device.rs b/src/bluer/device.rs index 9fc5c43..8a1354f 100644 --- a/src/bluer/device.rs +++ b/src/bluer/device.rs @@ -3,7 +3,6 @@ use std::sync::Arc; use futures_core::Stream; use futures_lite::StreamExt; -use super::l2cap_channel::{L2capChannelReader, L2capChannelWriter}; use super::DeviceId; use crate::device::ServicesChanged; use crate::error::ErrorKind; @@ -292,12 +291,17 @@ impl DeviceImpl { } } - pub async fn open_l2cap_channel( - &self, - _psm: u16, - _secure: bool, - ) -> std::prelude::v1::Result<(L2capChannelReader, L2capChannelWriter), crate::Error> { - Err(ErrorKind::NotSupported.into()) + #[cfg(feature = "l2cap")] + pub async fn open_l2cap_channel(&self, psm: u16, secure: bool) -> Result { + let address_type = self.inner.address_type().await.map_err(|err| { + crate::Error::new( + crate::error::ErrorKind::Internal, + Some(Box::new(err)), + "Could not get address".to_owned(), + ) + })?; + let sa = bluer::l2cap::SocketAddr::new(self.inner.address(), address_type, psm); + super::l2cap_channel::Channel::new(sa, secure).await } } diff --git a/src/bluer/l2cap_channel.rs b/src/bluer/l2cap_channel.rs index ed0e4e9..b314441 100644 --- a/src/bluer/l2cap_channel.rs +++ b/src/bluer/l2cap_channel.rs @@ -1,52 +1,108 @@ -use std::fmt; +use std::{ + io::Result, + pin::Pin, + task::{Context, Poll}, + time::Duration, +}; -use crate::Result; +use bluer::l2cap::{SocketAddr, Stream}; +use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; +use tracing::{debug, trace}; -pub struct L2capChannelReader { - _private: (), -} +use crate::error::ErrorKind; -impl L2capChannelReader { - #[inline] - pub async fn read(&mut self, _buf: &mut [u8]) -> Result { - todo!() - } +const SECURE_CHANNEL_KEY_SIZE: u8 = 16; - pub fn try_read(&mut self, _buf: &mut [u8]) -> Result { - todo!() - } +#[derive(Debug)] +pub struct Channel { + stream: Pin>, +} - pub async fn close(&mut self) -> Result<()> { - todo!() - } +enum ChannelCreationError { + StreamCreationError(std::io::Error), + SetSecurityError(std::io::Error), + ConnectionError(std::io::Error), + ConnectionTimeout(tokio::time::error::Elapsed), } -impl fmt::Debug for L2capChannelReader { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("L2capChannelReader") +impl Channel { + pub async fn new(sa: SocketAddr, secure: bool) -> crate::Result { + let stream = Stream::connect(sa) + .await + .map_err(ChannelCreationError::ConnectionError)?; + + if secure { + stream + .as_ref() + .set_security(bluer::l2cap::Security { + level: bluer::l2cap::SecurityLevel::High, + key_size: SECURE_CHANNEL_KEY_SIZE, + }) + .map_err(ChannelCreationError::SetSecurityError)?; + } + + trace!(name: "Bluetooth Stream", + "Local address: {:?}\n Remote address: {:?}\n Send MTU: {:?}\n Recv MTU: {:?}\n Security: {:?}\n Flow control: {:?}", + stream.as_ref().local_addr(), + stream.peer_addr(), + stream.as_ref().send_mtu(), + stream.as_ref().recv_mtu(), + stream.as_ref().security(), + stream.as_ref().flow_control(), + ); + + Ok(Self { + stream: Box::pin(stream), + }) } } -pub struct L2capChannelWriter { - _private: (), +impl AsyncRead for Channel { + fn poll_read(mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut ReadBuf<'_>) -> Poll> { + self.stream.as_mut().poll_read(cx, buf) + } } -impl L2capChannelWriter { - pub async fn write(&mut self, _packet: &[u8]) -> Result<()> { - todo!() +impl AsyncWrite for Channel { + fn poll_write(mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll> { + self.stream.as_mut().poll_write(cx, buf) } - pub fn try_write(&mut self, _packet: &[u8]) -> Result<()> { - todo!() + fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.stream.as_mut().poll_flush(cx) } - pub async fn close(&mut self) -> Result<()> { - todo!() + fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.stream.as_mut().poll_shutdown(cx) } } -impl fmt::Debug for L2capChannelWriter { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("L2capChannelWriter") +impl From for crate::Error { + fn from(value: ChannelCreationError) -> Self { + let kind = match &value { + ChannelCreationError::StreamCreationError(_) | ChannelCreationError::SetSecurityError(_) => { + ErrorKind::Internal + } + ChannelCreationError::ConnectionError(_) => ErrorKind::ConnectionFailed, + ChannelCreationError::ConnectionTimeout(_) => ErrorKind::Timeout, + }; + let message = match &value { + ChannelCreationError::StreamCreationError(_) => "Error creating a new l2cap stream.", + ChannelCreationError::SetSecurityError(_) => "Error setting connection security level.", + ChannelCreationError::ConnectionError(_) => "Error connecting to l2cap stream.", + ChannelCreationError::ConnectionTimeout(_) => { + "Timeout occured before stream parameters could be determined." + } + }; + crate::Error::new( + kind, + match value { + ChannelCreationError::StreamCreationError(io) + | ChannelCreationError::SetSecurityError(io) + | ChannelCreationError::ConnectionError(io) => Some(Box::new(io)), + ChannelCreationError::ConnectionTimeout(elapsed) => Some(Box::new(elapsed)), + }, + message.to_owned(), + ) } } diff --git a/src/corebluetooth.rs b/src/corebluetooth.rs index 5fbd2ab..0be6176 100644 --- a/src/corebluetooth.rs +++ b/src/corebluetooth.rs @@ -5,9 +5,11 @@ pub mod characteristic; pub mod descriptor; pub mod device; pub mod error; -pub mod l2cap_channel; pub mod service; +#[cfg(feature = "l2cap")] +pub mod l2cap_channel; + mod delegates; mod types; diff --git a/src/corebluetooth/delegates.rs b/src/corebluetooth/delegates.rs index 76a898c..47e26cb 100644 --- a/src/corebluetooth/delegates.rs +++ b/src/corebluetooth/delegates.rs @@ -116,7 +116,7 @@ pub enum PeripheralEvent { invalidated_services: Vec>, }, L2CAPChannelOpened { - channel: ShareId, + channel: Option>, error: Option>, }, } @@ -537,7 +537,7 @@ impl PeripheralDelegate { delegate_method!(did_read_rssi(peripheral, rssi: i16, error: Option)); delegate_method!(did_update_name(peripheral)); delegate_method!(did_modify_services(peripheral, invalidated_services: Vec)); - delegate_method!(did_open_l2cap_channel(peripheral, channel: Object, error: Option)); + delegate_method!(did_open_l2cap_channel(peripheral, channel: Option, error: Option)); fn class() -> &'static Class { static DELEGATE_CLASS_INIT: Once = Once::new(); diff --git a/src/corebluetooth/device.rs b/src/corebluetooth/device.rs index 8044678..6cc0270 100644 --- a/src/corebluetooth/device.rs +++ b/src/corebluetooth/device.rs @@ -6,13 +6,15 @@ use objc_foundation::{INSArray, INSFastEnumeration, INSString, NSArray}; use objc_id::ShareId; use super::delegates::{PeripheralDelegate, PeripheralEvent}; -use super::l2cap_channel::{L2capChannelReader, L2capChannelWriter}; use super::types::{CBPeripheral, CBPeripheralState, CBService, CBUUID}; use crate::device::ServicesChanged; use crate::error::ErrorKind; use crate::pairing::PairingAgent; use crate::{Device, DeviceId, Error, Result, Service, Uuid}; +#[cfg(feature = "l2cap")] +use super::l2cap_channel::Channel; + /// A Bluetooth LE device #[derive(Clone)] pub struct DeviceImpl { @@ -215,12 +217,41 @@ impl DeviceImpl { } } - pub async fn open_l2cap_channel( - &self, - _psm: u16, - _secure: bool, - ) -> std::prelude::v1::Result<(L2capChannelReader, L2capChannelWriter), crate::Error> { - Err(ErrorKind::NotSupported.into()) + #[cfg(feature = "l2cap")] + pub async fn open_l2cap_channel(&self, psm: u16, secure: bool) -> Result { + if secure { + return Err(Error::new( + ErrorKind::NotSupported, + None, + "Corebluetooth does not support secure sockets".to_owned(), + )); + } + if !self.is_connected().await { + return Err(ErrorKind::NotConnected.into()); + } + + let mut receiver = self.delegate.sender().new_receiver(); + self.peripheral.open_l2_cap_channel(psm); + + loop { + match receiver.recv().await.map_err(Error::from_recv_error)? { + PeripheralEvent::L2CAPChannelOpened { + channel: Some(chan), + error: None, + } => return Channel::new(chan), + PeripheralEvent::L2CAPChannelOpened { + channel: None, + error: ns_error, + } => { + return Err(Error::new( + ErrorKind::ConnectionFailed, + None, + format!("Failed to Open L2Cap Connection with error {:?}", ns_error), + )) + } + _ => (), + } + } } } diff --git a/src/corebluetooth/l2cap_channel.rs b/src/corebluetooth/l2cap_channel.rs index ed0e4e9..3ed821d 100644 --- a/src/corebluetooth/l2cap_channel.rs +++ b/src/corebluetooth/l2cap_channel.rs @@ -1,52 +1,133 @@ -use std::fmt; +use std::{ + io::Result, + os::fd::{FromRawFd, RawFd}, + pin::Pin, + task::{Context, Poll}, +}; -use crate::Result; +use objc_foundation::INSData; +use objc_id::{Id, Shared}; +use tokio::{ + io::{AsyncRead, AsyncWrite, ReadBuf}, + net::UnixStream, +}; -pub struct L2capChannelReader { - _private: (), -} +use super::types::{kCFStreamPropertySocketNativeHandle, CBL2CAPChannel, CFStream}; +use crate::{error::ErrorKind, Error}; -impl L2capChannelReader { - #[inline] - pub async fn read(&mut self, _buf: &mut [u8]) -> Result { - todo!() - } +// This implementation is based upon the fact that that CBL2CAPChannel::outputStream -> an NS Output Stream; (https://developer.apple.com/documentation/foundation/outputstream) +// NS Output stream is toll free bridged to CFWriteStream (https://developer.apple.com/documentation/corefoundation/cfwritestream) +// CFWriteStream is a subclass of CFStream (https://developer.apple.com/documentation/corefoundation/cfstream?language=objc) +// CF Stream has properties (https://developer.apple.com/documentation/corefoundation/cfstream/stream_properties?language=objc) +// One of them is kCFStreamPropertySocketNativeHandle https://developer.apple.com/documentation/corefoundation/kcfstreampropertysocketnativehandle?language=objc +// kCFStreamPropertySocketNativeHandle is of type CFSocketNativeHandle https://developer.apple.com/documentation/corefoundation/cfsocketnativehandle?language=objc +// CFSocketNativeHandle is a property of CFSocket https://developer.apple.com/documentation/corefoundation/cfsocket?language=objc +// CF Socket is defined to be a bsd socket +// BSD Sockets are Unix Sockets on mac os - pub fn try_read(&mut self, _buf: &mut [u8]) -> Result { - todo!() - } +#[derive(Debug)] +pub struct Channel { + _channel: Id, + stream: Pin>, +} - pub async fn close(&mut self) -> Result<()> { - todo!() - } +enum ChannelCreationError { + FileDescriptorPropertyNotValid, + InputFileDescriptorBytesWrongSize, + OutputFileDescriptorBytesWrongSize, + FileDescriptorsNotIdentical, + SetNonBlockingModeFailed(std::io::Error), + TokioStreamCreation(std::io::Error), } -impl fmt::Debug for L2capChannelReader { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("L2capChannelReader") +impl Channel { + pub fn new(channel: Id) -> crate::Result { + let input_stream = channel.input_stream(); + let output_stream = channel.output_stream(); + + let in_stream_prop = input_stream.property(&unsafe { kCFStreamPropertySocketNativeHandle }); + let out_stream_prop = output_stream.property(&unsafe { kCFStreamPropertySocketNativeHandle }); + + let (Some(in_data), Some(out_data)) = (in_stream_prop, out_stream_prop) else { + return Err(ChannelCreationError::FileDescriptorPropertyNotValid.into()); + }; + let in_bytes = in_data + .bytes() + .try_into() + .map_err(|_| ChannelCreationError::InputFileDescriptorBytesWrongSize)?; + let in_fd = RawFd::from_ne_bytes(in_bytes); + + let out_bytes = out_data + .bytes() + .try_into() + .map_err(|_| ChannelCreationError::OutputFileDescriptorBytesWrongSize)?; + let out_fd = RawFd::from_ne_bytes(out_bytes); + + if in_fd != out_fd { + return Err(ChannelCreationError::FileDescriptorsNotIdentical.into()); + }; + + let stream = unsafe { std::os::unix::net::UnixStream::from_raw_fd(in_fd) }; + stream + .set_nonblocking(true) + .map_err(ChannelCreationError::SetNonBlockingModeFailed)?; + + let tokio_stream = UnixStream::try_from(stream).map_err(ChannelCreationError::TokioStreamCreation)?; + + let stream = Box::pin(tokio_stream); + + Ok(Self { + _channel: channel, + stream, + }) } } - -pub struct L2capChannelWriter { - _private: (), +impl AsyncRead for Channel { + fn poll_read(mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut ReadBuf<'_>) -> Poll> { + self.stream.as_mut().poll_read(cx, buf) + } } -impl L2capChannelWriter { - pub async fn write(&mut self, _packet: &[u8]) -> Result<()> { - todo!() +impl AsyncWrite for Channel { + fn poll_write(mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll> { + self.stream.as_mut().poll_write(cx, buf) } - pub fn try_write(&mut self, _packet: &[u8]) -> Result<()> { - todo!() + fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.stream.as_mut().poll_flush(cx) } - pub async fn close(&mut self) -> Result<()> { - todo!() + fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.stream.as_mut().poll_shutdown(cx) } } -impl fmt::Debug for L2capChannelWriter { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("L2capChannelWriter") +impl From for Error { + fn from(value: ChannelCreationError) -> Self { + let message = match &value { + ChannelCreationError::FileDescriptorPropertyNotValid => "File descriptor property not valid.", + ChannelCreationError::InputFileDescriptorBytesWrongSize => { + "Input file descriptor bytes are an invalid size." + } + ChannelCreationError::OutputFileDescriptorBytesWrongSize => { + "Output file descriptor bytes are an invalid size." + } + ChannelCreationError::FileDescriptorsNotIdentical => "Input and output file descriptors are not identical.", + ChannelCreationError::SetNonBlockingModeFailed(_) => "Could not get convert socket to async.", + ChannelCreationError::TokioStreamCreation(_) => "Failed to create tokio unix socket.", + }; + + Error::new( + ErrorKind::Internal, + match value { + ChannelCreationError::FileDescriptorPropertyNotValid + | ChannelCreationError::InputFileDescriptorBytesWrongSize + | ChannelCreationError::OutputFileDescriptorBytesWrongSize + | ChannelCreationError::FileDescriptorsNotIdentical => None, + ChannelCreationError::SetNonBlockingModeFailed(src) + | ChannelCreationError::TokioStreamCreation(src) => Some(Box::new(src)), + }, + message.to_owned(), + ) } } diff --git a/src/corebluetooth/types.rs b/src/corebluetooth/types.rs index 8a178ab..f7ebedd 100644 --- a/src/corebluetooth/types.rs +++ b/src/corebluetooth/types.rs @@ -4,16 +4,17 @@ use std::collections::HashMap; use std::ffi::c_ulong; +use std::num::TryFromIntError; use std::os::raw::{c_char, c_void}; use objc::rc::autoreleasepool; use objc::runtime::{Object, BOOL, NO}; -use objc::{msg_send, sel, sel_impl}; +use objc::{msg_send, sel, sel_impl, Message}; use objc_foundation::{ object_struct, INSData, INSDictionary, INSFastEnumeration, INSObject, INSString, NSArray, NSData, NSDictionary, NSObject, NSString, }; -use objc_id::{Id, ShareId}; +use objc_id::{Id, Owned, ShareId, Shared}; use super::delegates::{CentralDelegate, PeripheralDelegate}; use crate::btuuid::BluetoothUuidExt; @@ -144,6 +145,20 @@ impl CBATTError { pub const INSUFFICIENT_RESOURCES: CBATTError = CBATTError(17); } +#[non_exhaustive] +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum NSStreamStatus { + #[default] + NotOpen = 0, + Opening = 1, + Open = 2, + Reading = 3, + Writing = 4, + AtEnd = 5, + Closed = 6, + Error = 7, +} + impl AdvertisementData { pub(super) fn from_nsdictionary(adv_data: &ShareId>) -> Self { let is_connectable = adv_data @@ -228,6 +243,10 @@ extern "C" { // CBConnectionEventMatchingOption static CBConnectionEventMatchingOptionPeripheralUUIDs: id; static CBConnectionEventMatchingOptionServiceUUIDs: id; + + //For Handling L2Cap Streams + #[cfg(feature = "l2cap")] + pub(super) static kCFStreamPropertySocketNativeHandle: id; } pub const QOS_CLASS_USER_INTERACTIVE: isize = 0x21; @@ -270,6 +289,9 @@ object_struct!(CBCharacteristic); object_struct!(CBDescriptor); object_struct!(CBL2CAPChannel); +object_struct!(NSInputStream); +object_struct!(NSOutputStream); + impl NSError { pub fn code(&self) -> NSInteger { unsafe { msg_send![self, code] } @@ -492,6 +514,10 @@ impl CBPeripheral { pub fn read_rssi(&self) { unsafe { msg_send![self, readRSSI] } } + + pub fn open_l2_cap_channel(&self, psm: u16) { + unsafe { msg_send![self, openL2CAPChannel: psm] } + } } impl CBService { @@ -562,3 +588,93 @@ impl CBDescriptor { autoreleasepool(move || unsafe { option_from_ptr(msg_send![self, value]) }) } } + +impl CBL2CAPChannel { + pub fn input_stream(&self) -> ShareId { + autoreleasepool(move || unsafe { ShareId::from_ptr(msg_send![self, inputStream]) }) + } + pub fn output_stream(&self) -> ShareId { + autoreleasepool(move || unsafe { ShareId::from_ptr(msg_send![self, outputStream]) }) + } +} + +/// Trait for Objects that inherit from [NSStream](https://developer.apple.com/documentation/foundation/nsstream) +/// +/// # Safety +/// Only implement for objective C objects that inherit from NSStream. +pub(super) unsafe trait NSStream: Sized + Message { + fn open(&self) { + unsafe { msg_send![self, open] } + } + + fn close(&self) { + unsafe { msg_send![self, close] } + } + + fn stream_error(&self) -> Option> { + unsafe { option_from_ptr(msg_send![self, streamError]) } + } + + fn stream_status(&self) -> Result { + let status: NSUInteger = unsafe { msg_send![self, streamStatus] }; + NSStreamStatus::try_from(status) + } +} + +pub(super) unsafe trait CFStream: Sized + Message { + fn property(&self, key: &id) -> Option<&NSData> { + let key = unsafe { extern_nsstring(*key) }; + let obj_ptr: *const NSObject = unsafe { msg_send![self, propertyForKey: key] }; + if obj_ptr.is_null() { + println!("Object Pointer Null"); + return None; + } + let class = NSData::class(); + let is_ns_data: BOOL = unsafe { msg_send![self, isKindOfClass:class] }; + Some(unsafe { &*(obj_ptr as *const Object as *const NSData) }) + } +} +unsafe impl CFStream for S {} + +impl TryFrom for NSStreamStatus { + type Error = &'static str; + + fn try_from(value: NSUInteger) -> Result>::Error> { + Ok(match value { + 0 => Self::NotOpen, + 1 => Self::Opening, + 2 => Self::Open, + 3 => Self::Reading, + 4 => Self::Writing, + 5 => Self::AtEnd, + 6 => Self::Closed, + 7 => Self::Error, + _ => return Err("Invalid Stream Status"), + }) + } +} + +impl NSInputStream { + pub fn has_bytes_available(&self) -> bool { + let b: BOOL = unsafe { msg_send![self, hasBytesAvailable] }; + b != NO + } + + pub fn read(&self, buffer: &mut [u8]) -> isize { + unsafe { msg_send![self, read:buffer.as_mut_ptr() maxLength:buffer.len()] } + } +} + +unsafe impl NSStream for NSInputStream {} + +impl NSOutputStream { + pub fn has_space_available(&self) -> bool { + let b: BOOL = unsafe { msg_send![self, hasSpaceAvailable] }; + b != NO + } + pub fn write(&self, buffer: &[u8]) -> isize { + unsafe { msg_send![self, write: buffer.as_ptr() maxLength:buffer.len()] } + } +} + +unsafe impl NSStream for NSOutputStream {} diff --git a/src/device.rs b/src/device.rs index 511c8ea..fa62b13 100644 --- a/src/device.rs +++ b/src/device.rs @@ -4,10 +4,12 @@ use futures_core::Stream; use futures_lite::StreamExt; use crate::error::ErrorKind; -use crate::l2cap_channel::L2capChannel; use crate::pairing::PairingAgent; use crate::{sys, DeviceId, Error, Result, Service, Uuid}; +#[cfg(feature = "l2cap")] +use crate::l2cap_channel::L2capChannel; + /// A Bluetooth LE device #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Device(pub(crate) sys::device::DeviceImpl); @@ -158,11 +160,14 @@ impl Device { /// /// # Platform specific /// - /// Returns [`NotSupported`][crate::error::ErrorKind::NotSupported] on iOS/MacOS, Windows and Linux. + /// Returns [`NotSupported`][crate::error::ErrorKind::NotSupported] on Windows. + #[cfg(feature = "l2cap")] #[inline] 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: Box::pin(channel), + }) } } diff --git a/src/l2cap_channel.rs b/src/l2cap_channel.rs index d8fdb96..4f54192 100644 --- a/src/l2cap_channel.rs +++ b/src/l2cap_channel.rs @@ -1,114 +1,35 @@ -use crate::{sys, Result}; +use std::{ + io::Result, + pin::Pin, + task::{Context, Poll}, +}; -/// 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, -} +use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; -/// Reader half of a L2CAP Connection-oriented Channel (CoC) -#[derive(Debug)] -pub struct L2capChannelReader { - reader: sys::l2cap_channel::L2capChannelReader, -} +use crate::sys; -/// Writerhalf of a L2CAP Connection-oriented Channel (CoC) +/// A Bluetooth LE L2CAP Connection-oriented Channel (CoC) #[derive(Debug)] -pub struct L2capChannelWriter { - writer: sys::l2cap_channel::L2capChannelWriter, -} - -impl L2capChannel { - /// Read a packet from the L2CAP channel. - /// - /// The packet is written to the start of `buf`, and the packet length is returned. - #[inline] - pub async fn read(&mut self, buf: &mut [u8]) -> Result { - self.reader.read(buf).await - } - - /// Write a packet to the L2CAP channel. - #[inline] - pub async fn write(&mut self, packet: &[u8]) -> Result<()> { - self.writer.write(packet).await - } - - /// 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; - (L2capChannelReader { reader }, L2capChannelWriter { writer }) - } +pub struct L2capChannel { + pub(crate) channel: Pin>, } -impl L2capChannelReader { - /// Read a packet from the L2CAP channel. - /// - /// The packet is written to the start of `buf`, and the packet length is returned. - #[inline] - pub async fn read(&mut self, buf: &mut [u8]) -> Result { - self.reader.read(buf).await - } - - /// Try reading a packet from the L2CAP channel. - /// - /// The packet is written to the start of `buf`, and the packet length is returned. - /// - /// If no packet is immediately available for reading, this returns an error with kind `NotReady`. - #[inline] - pub fn try_read(&mut self, buf: &mut [u8]) -> Result { - self.reader.try_read(buf) - } - - /// 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 L2capChannel { + fn poll_read(mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut ReadBuf<'_>) -> Poll> { + self.channel.as_mut().poll_read(cx, buf) } } -impl L2capChannelWriter { - /// Write a packet to the L2CAP channel. - /// - /// If the buffer is full, this will wait until there's buffer space for the packet. - #[inline] - pub async fn write(&mut self, packet: &[u8]) -> Result<()> { - self.writer.write(packet).await +impl AsyncWrite for L2capChannel { + fn poll_write(mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll> { + self.channel.as_mut().poll_write(cx, buf) } - /// Try writing a packet to the L2CAP channel. - /// - /// If there's no buffer space, this returns an error with kind `NotReady`. - #[inline] - pub fn try_write(&mut self, packet: &[u8]) -> Result<()> { - self.writer.try_write(packet) + fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.channel.as_mut().poll_flush(cx) } - /// 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 + fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.channel.as_mut().poll_shutdown(cx) } } diff --git a/src/lib.rs b/src/lib.rs index 3d64ab8..c8c303e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -119,7 +119,6 @@ mod characteristic; mod descriptor; mod device; pub mod error; -mod l2cap_channel; pub mod pairing; mod service; mod util; @@ -143,7 +142,6 @@ pub use characteristic::Characteristic; pub use descriptor::Descriptor; pub use device::{Device, ServicesChanged}; pub use error::Error; -pub use l2cap_channel::{L2capChannel, L2capChannelReader, L2capChannelWriter}; pub use service::Service; pub use sys::DeviceId; #[cfg(not(target_os = "linux"))] @@ -158,6 +156,17 @@ use crate::corebluetooth as sys; #[cfg(target_os = "windows")] use crate::windows as sys; +#[cfg(all( + feature = "l2cap", + any(target_os = "android", target_os = "linux", target_os = "macos", target_os = "ios") +))] +mod l2cap_channel; +#[cfg(all( + feature = "l2cap", + any(target_os = "android", target_os = "linux", target_os = "macos", target_os = "ios") +))] +pub use l2cap_channel::L2capChannel; + /// Convenience alias for a result with [`Error`] pub type Result = core::result::Result; diff --git a/src/windows/device.rs b/src/windows/device.rs index f2d190f..19e0903 100644 --- a/src/windows/device.rs +++ b/src/windows/device.rs @@ -277,7 +277,7 @@ impl DeviceImpl { &self, _psm: u16, _secure: bool, - ) -> std::prelude::v1::Result<(L2capChannelReader, L2capChannelWriter), crate::Error> { + ) -> Result<(L2capChannelReader, L2capChannelWriter)> { Err(ErrorKind::NotSupported.into()) } } From cf6ff2993858c2584835da611dd4dd147f1516f6 Mon Sep 17 00:00:00 2001 From: Abraham Zukor Date: Thu, 13 Jun 2024 17:45:35 -0700 Subject: [PATCH 2/3] clippy --- src/bluer/l2cap_channel.rs | 21 +++++---------------- src/corebluetooth/types.rs | 2 ++ 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/src/bluer/l2cap_channel.rs b/src/bluer/l2cap_channel.rs index b314441..3e03d07 100644 --- a/src/bluer/l2cap_channel.rs +++ b/src/bluer/l2cap_channel.rs @@ -2,12 +2,11 @@ use std::{ io::Result, pin::Pin, task::{Context, Poll}, - time::Duration, }; use bluer::l2cap::{SocketAddr, Stream}; use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; -use tracing::{debug, trace}; +use tracing::trace; use crate::error::ErrorKind; @@ -19,10 +18,8 @@ pub struct Channel { } enum ChannelCreationError { - StreamCreationError(std::io::Error), SetSecurityError(std::io::Error), ConnectionError(std::io::Error), - ConnectionTimeout(tokio::time::error::Elapsed), } impl Channel { @@ -80,27 +77,19 @@ impl AsyncWrite for Channel { impl From for crate::Error { fn from(value: ChannelCreationError) -> Self { let kind = match &value { - ChannelCreationError::StreamCreationError(_) | ChannelCreationError::SetSecurityError(_) => { - ErrorKind::Internal - } + ChannelCreationError::SetSecurityError(_) => ErrorKind::Internal, ChannelCreationError::ConnectionError(_) => ErrorKind::ConnectionFailed, - ChannelCreationError::ConnectionTimeout(_) => ErrorKind::Timeout, }; let message = match &value { - ChannelCreationError::StreamCreationError(_) => "Error creating a new l2cap stream.", ChannelCreationError::SetSecurityError(_) => "Error setting connection security level.", ChannelCreationError::ConnectionError(_) => "Error connecting to l2cap stream.", - ChannelCreationError::ConnectionTimeout(_) => { - "Timeout occured before stream parameters could be determined." - } }; crate::Error::new( kind, match value { - ChannelCreationError::StreamCreationError(io) - | ChannelCreationError::SetSecurityError(io) - | ChannelCreationError::ConnectionError(io) => Some(Box::new(io)), - ChannelCreationError::ConnectionTimeout(elapsed) => Some(Box::new(elapsed)), + ChannelCreationError::SetSecurityError(io) | ChannelCreationError::ConnectionError(io) => { + Some(Box::new(io)) + } }, message.to_owned(), ) diff --git a/src/corebluetooth/types.rs b/src/corebluetooth/types.rs index f7ebedd..d79e055 100644 --- a/src/corebluetooth/types.rs +++ b/src/corebluetooth/types.rs @@ -621,6 +621,8 @@ pub(super) unsafe trait NSStream: Sized + Message { } } +/// # Safety +/// Only implement for objective C object that inherit from CFStream (https://developer.apple.com/documentation/corefoundation/cfstream) pub(super) unsafe trait CFStream: Sized + Message { fn property(&self, key: &id) -> Option<&NSData> { let key = unsafe { extern_nsstring(*key) }; From c3729ff96e1eaff8f432bbb249ec1bc91feae153 Mon Sep 17 00:00:00 2001 From: Abe Zukor Date: Sat, 15 Jun 2024 23:33:04 -0700 Subject: [PATCH 3/3] Nightly Format and Windows compile fix --- src/android/l2cap_channel.rs | 12 +++----- src/bluer/l2cap_channel.rs | 8 ++--- src/corebluetooth/device.rs | 5 ++- src/corebluetooth/l2cap_channel.rs | 19 +++++------- src/device.rs | 5 ++- src/l2cap_channel.rs | 8 ++--- src/windows/device.rs | 1 + src/windows/l2cap_channel.rs | 49 +++++++----------------------- 8 files changed, 34 insertions(+), 73 deletions(-) diff --git a/src/android/l2cap_channel.rs b/src/android/l2cap_channel.rs index 0fe82ff..7a1c73c 100644 --- a/src/android/l2cap_channel.rs +++ b/src/android/l2cap_channel.rs @@ -1,11 +1,7 @@ -use std::{ - fmt, - io::Result, - pin::Pin, - slice, - task::{Context, Poll}, - thread, -}; +use std::io::Result; +use std::pin::Pin; +use std::task::{Context, Poll}; +use std::{fmt, slice, thread}; use java_spaghetti::{ByteArray, Global, Local, PrimitiveArray}; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, DuplexStream, ReadBuf}; diff --git a/src/bluer/l2cap_channel.rs b/src/bluer/l2cap_channel.rs index 3e03d07..58858c6 100644 --- a/src/bluer/l2cap_channel.rs +++ b/src/bluer/l2cap_channel.rs @@ -1,8 +1,6 @@ -use std::{ - io::Result, - pin::Pin, - task::{Context, Poll}, -}; +use std::io::Result; +use std::pin::Pin; +use std::task::{Context, Poll}; use bluer::l2cap::{SocketAddr, Stream}; use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; diff --git a/src/corebluetooth/device.rs b/src/corebluetooth/device.rs index 6cc0270..c15255d 100644 --- a/src/corebluetooth/device.rs +++ b/src/corebluetooth/device.rs @@ -6,15 +6,14 @@ use objc_foundation::{INSArray, INSFastEnumeration, INSString, NSArray}; use objc_id::ShareId; use super::delegates::{PeripheralDelegate, PeripheralEvent}; +#[cfg(feature = "l2cap")] +use super::l2cap_channel::Channel; use super::types::{CBPeripheral, CBPeripheralState, CBService, CBUUID}; use crate::device::ServicesChanged; use crate::error::ErrorKind; use crate::pairing::PairingAgent; use crate::{Device, DeviceId, Error, Result, Service, Uuid}; -#[cfg(feature = "l2cap")] -use super::l2cap_channel::Channel; - /// A Bluetooth LE device #[derive(Clone)] pub struct DeviceImpl { diff --git a/src/corebluetooth/l2cap_channel.rs b/src/corebluetooth/l2cap_channel.rs index 3ed821d..1e930c1 100644 --- a/src/corebluetooth/l2cap_channel.rs +++ b/src/corebluetooth/l2cap_channel.rs @@ -1,19 +1,16 @@ -use std::{ - io::Result, - os::fd::{FromRawFd, RawFd}, - pin::Pin, - task::{Context, Poll}, -}; +use std::io::Result; +use std::os::fd::{FromRawFd, RawFd}; +use std::pin::Pin; +use std::task::{Context, Poll}; use objc_foundation::INSData; use objc_id::{Id, Shared}; -use tokio::{ - io::{AsyncRead, AsyncWrite, ReadBuf}, - net::UnixStream, -}; +use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; +use tokio::net::UnixStream; use super::types::{kCFStreamPropertySocketNativeHandle, CBL2CAPChannel, CFStream}; -use crate::{error::ErrorKind, Error}; +use crate::error::ErrorKind; +use crate::Error; // This implementation is based upon the fact that that CBL2CAPChannel::outputStream -> an NS Output Stream; (https://developer.apple.com/documentation/foundation/outputstream) // NS Output stream is toll free bridged to CFWriteStream (https://developer.apple.com/documentation/corefoundation/cfwritestream) diff --git a/src/device.rs b/src/device.rs index fa62b13..31d6c9f 100644 --- a/src/device.rs +++ b/src/device.rs @@ -4,11 +4,10 @@ use futures_core::Stream; use futures_lite::StreamExt; use crate::error::ErrorKind; -use crate::pairing::PairingAgent; -use crate::{sys, DeviceId, Error, Result, Service, Uuid}; - #[cfg(feature = "l2cap")] use crate::l2cap_channel::L2capChannel; +use crate::pairing::PairingAgent; +use crate::{sys, DeviceId, Error, Result, Service, Uuid}; /// A Bluetooth LE device #[derive(Debug, Clone, PartialEq, Eq, Hash)] diff --git a/src/l2cap_channel.rs b/src/l2cap_channel.rs index 4f54192..661e459 100644 --- a/src/l2cap_channel.rs +++ b/src/l2cap_channel.rs @@ -1,8 +1,6 @@ -use std::{ - io::Result, - pin::Pin, - task::{Context, Poll}, -}; +use std::io::Result; +use std::pin::Pin; +use std::task::{Context, Poll}; use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; diff --git a/src/windows/device.rs b/src/windows/device.rs index 19e0903..0dffaa4 100644 --- a/src/windows/device.rs +++ b/src/windows/device.rs @@ -273,6 +273,7 @@ impl DeviceImpl { Err(ErrorKind::NotSupported.into()) } + #[cfg(feature = "l2cap")] pub async fn open_l2cap_channel( &self, _psm: u16, diff --git a/src/windows/l2cap_channel.rs b/src/windows/l2cap_channel.rs index ed0e4e9..9cb7022 100644 --- a/src/windows/l2cap_channel.rs +++ b/src/windows/l2cap_channel.rs @@ -2,51 +2,24 @@ use std::fmt; use crate::Result; -pub struct L2capChannelReader { - _private: (), -} - -impl L2capChannelReader { - #[inline] - pub async fn read(&mut self, _buf: &mut [u8]) -> Result { - todo!() - } +pub struct Channel {} - pub fn try_read(&mut self, _buf: &mut [u8]) -> Result { - todo!() - } - - pub async fn close(&mut self) -> Result<()> { - todo!() +impl AsyncRead for Channel { + fn poll_read(mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut ReadBuf<'_>) -> Poll> { + unimplemented!() } } -impl fmt::Debug for L2capChannelReader { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("L2capChannelReader") +impl AsyncWrite for Channel { + fn poll_write(mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll> { + unimplemented!() } -} -pub struct L2capChannelWriter { - _private: (), -} - -impl L2capChannelWriter { - pub async fn write(&mut self, _packet: &[u8]) -> Result<()> { - todo!() - } - - pub fn try_write(&mut self, _packet: &[u8]) -> Result<()> { - todo!() + fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + unimplemented!() } - pub async fn close(&mut self) -> Result<()> { - todo!() - } -} - -impl fmt::Debug for L2capChannelWriter { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("L2capChannelWriter") + fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + unimplemented!() } }