diff --git a/Cargo.toml b/Cargo.toml index 3591aa4..2bc4bbf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,12 +6,17 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +btleplug = { version = "0.11.3", optional = true } env_logger = "0.11.3" flume = "0.11.0" +futures = "0.3.29" log = "0.4.20" serial2 = { version = "0.2.8", optional = true } snafu = "0.8.2" +tokio = { version = "1.34.0", features = ["full"], optional = true } +uuid = { version = "1.6.0", optional = true } [features] -default = ["uart"] +default = ["uart", "bluetooth"] uart = ["dep:serial2"] +bluetooth = ["dep:btleplug", "dep:tokio", "dep:uuid"] diff --git a/examples/bt_test.rs b/examples/bt_test.rs new file mode 100644 index 0000000..737ce62 --- /dev/null +++ b/examples/bt_test.rs @@ -0,0 +1,38 @@ +use std::time::Duration; + +use btleplug::api::bleuuid::uuid_from_u32; +/// To run this example, you will have to have a BT device with the following requirements: +/// Service: 0x13370042 +/// - READ char: 0x00002a3d +/// - WRITE char: 0x00002a3d +use comm_handler::adapters::bluetooth::{ + uuid_from_u16, BluetoothAdapterConfiguration, ServiceCharacteristic, +}; +use comm_handler::Handler; +use log::trace; + +fn main() { + env_logger::init(); + + let service = uuid_from_u32(0x13370042); + let read = ServiceCharacteristic::new(service, uuid_from_u16(0x2a3d)); + let write = ServiceCharacteristic::new(service, uuid_from_u16(0x2a3d)); + + let config = BluetoothAdapterConfiguration::new("48:51:C5:9C:F3:D7", read, write); + + let handler = Handler::spawn(&config).unwrap(); + + let sender = handler.get_sender(); + let receiver = handler.get_receiver(); + + let original_data = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 0]; + + for _ in 0..5 { + sender.send(original_data.clone()).unwrap(); + + let data = receiver.recv().unwrap(); + trace!("User received data. {data:?}"); + assert_eq!(data, original_data); + std::thread::sleep(Duration::from_millis(100)); + } +} diff --git a/flake.nix b/flake.nix index 5780167..48d841f 100644 --- a/flake.nix +++ b/flake.nix @@ -21,6 +21,10 @@ rust-analyzer pre-commit rustPackages.clippy + + # Cargo clippy dependencies + pkg-config + dbus ]; RUST_SRC_PATH = rustPlatform.rustLibSrc; }; diff --git a/src/adapters/bluetooth.rs b/src/adapters/bluetooth.rs new file mode 100644 index 0000000..6e0ac17 --- /dev/null +++ b/src/adapters/bluetooth.rs @@ -0,0 +1,326 @@ +use crate::{error::Error, traits::Connectable, Result}; +use snafu::Snafu; +use std::{ + sync::{Arc, Mutex}, + time::Duration, +}; + +use uuid::Uuid; + +use log::{debug, error, trace}; + +use crate::traits::{CloneableCommunication, Communication, CommunicationBuilder}; +use btleplug::{ + api::{Central, Characteristic, Manager as _, Peripheral as _, ScanFilter, WriteType}, + platform::{Adapter, Manager, Peripheral}, +}; +use futures::StreamExt; +use tokio::runtime::Runtime; +use tokio::sync::mpsc::channel; + +#[derive(Snafu, Debug)] +pub enum BluetoothError { + #[snafu(display("No adapters present"))] + NoAdaptersPresent, + #[snafu(display("No peripherals found"))] + NoPeripheralsFound, + #[snafu(display("Characteristic not found"))] + CharacteristicNotFound, + #[snafu(display("Read timed out"))] + ReadTimedOut, + + #[snafu(display("Btleplug: {e}"))] + Btleplug { e: btleplug::Error }, +} + +impl From for BluetoothError { + fn from(value: btleplug::Error) -> Self { + BluetoothError::Btleplug { e: value } + } +} + +impl From for BluetoothError { + fn from(_: tokio::time::error::Elapsed) -> Self { + BluetoothError::ReadTimedOut + } +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct ServiceCharacteristic { + service: String, + characteristic: String, +} + +impl ServiceCharacteristic { + pub fn new(service: A, char: B) -> Self { + Self { + service: service.to_string(), + characteristic: char.to_string(), + } + } +} + +#[derive(Debug)] +pub struct BluetoothAdapterConfiguration { + device_mac: String, + read_characteristic: ServiceCharacteristic, + write_characteristic: ServiceCharacteristic, + runtime: Runtime, +} + +impl BluetoothAdapterConfiguration { + pub fn new( + device_mac: T, + read_characteristic: ServiceCharacteristic, + write_characteristic: ServiceCharacteristic, + ) -> Self { + let runtime = Runtime::new().unwrap(); + Self { + device_mac: device_mac.to_string(), + read_characteristic, + write_characteristic, + runtime, + } + } + + fn select_first_adapter(&self) -> Result { + self.runtime.block_on(async { + let manager = Manager::new().await.map_err(BluetoothError::from)?; + let adapters = manager.adapters().await.map_err(BluetoothError::from)?; + debug!("connected_adapters: {:#?}", adapters); + let Some(adapter) = adapters.first() else { + error!("There's no adapters connected!"); + return Err(BluetoothError::NoAdaptersPresent.into()); + }; + debug!("Using the adapter: {:?}", adapter); + Ok(adapter.clone()) + }) + } + + fn find_peripheral(&self, adapter: &Adapter) -> Result { + self.runtime.block_on(async { + let filter = ScanFilter { + services: vec![ + Uuid::parse_str(&self.read_characteristic.service).unwrap(), + Uuid::parse_str(&self.write_characteristic.service).unwrap(), + ], + }; + trace!("Starting scanning"); + adapter + .start_scan(filter) + .await + .map_err(BluetoothError::from)?; + + if tokio::time::timeout(Duration::from_secs(15), async { + loop { + match adapter.peripherals().await { + Ok(a) => { + if a.into_iter() + .inspect(|a| trace!("Found device: {}", a.address())) + .any(|a| a.address().to_string() == self.device_mac) + { + break; + } else { + trace!("Peripheral search returned empty results."); + tokio::time::sleep(Duration::from_secs(1)).await; + } + } + Err(e) => { + error!("adapter.peripherals() failed: {e}"); + } + } + } + }) + .await + .is_err() + { + error!("Failed to find a device you were looking for!"); + return Err(BluetoothError::NoPeripheralsFound.into()); + } + let peripherals = adapter.peripherals().await; + if peripherals.is_err() { + return Err(BluetoothError::NoPeripheralsFound.into()); + } + let peripherals = peripherals.unwrap(); + + if let Some(peripheral) = peripherals + .into_iter() + .find(|a| a.address().to_string() == self.device_mac) + { + Ok(peripheral) + } else { + Err(BluetoothError::NoPeripheralsFound.into()) + } + }) + } + + fn find_characteristic( + &self, + peripheral: &Peripheral, + char: T, + ) -> Result { + let uuid = Uuid::parse_str(&char.to_string()).unwrap(); + self.runtime.block_on(async { + for s in peripheral.services() { + for c in s.characteristics { + if c.uuid == uuid { + return Ok(c); + } + } + } + Err(BluetoothError::CharacteristicNotFound.into()) + }) + } +} + +impl CommunicationBuilder for BluetoothAdapterConfiguration { + fn build(&self) -> Result> { + debug!("Connecting to Bluetooth: {:#?}", self); + trace!("Selecting first adapter"); + let adapter = self.select_first_adapter()?; + trace!("Finding peripheral"); + let peripheral = self.find_peripheral(&adapter)?; + + trace!("Connecting to the peripheral"); + self.runtime + .block_on(peripheral.connect()) + .map_err(BluetoothError::from)?; + + trace!("Discovering services"); + self.runtime + .block_on(peripheral.discover_services()) + .map_err(BluetoothError::from)?; + trace!("Finding services"); + let read_characteristic = + self.find_characteristic(&peripheral, &self.read_characteristic.characteristic)?; + let write_characteristic = + self.find_characteristic(&peripheral, &self.write_characteristic.characteristic)?; + + let runtime = Runtime::new()?; + + let mut notifications = runtime + .block_on(peripheral.notifications()) + .map_err(BluetoothError::from)?; + + let (received_data_sender, received_data_receiver) = channel::>(8); + + runtime.spawn(async move { + while let Some(val) = notifications.next().await { + received_data_sender.send(val.value).await.expect("Failed to send data to the notification receiver!"); + } + }); + + runtime.block_on(peripheral.subscribe(&read_characteristic)) + .map_err(BluetoothError::from)?; + + trace!("Successfully connected to the Bluetooth"); + Ok(Box::new(BluetoothAdapter { + peripheral: Arc::new(peripheral), + write_characteristic, + notification_receiver: Arc::new(Mutex::new(received_data_receiver)), + runtime: Arc::new(runtime), + })) + } +} + +#[derive(Clone)] +pub struct BluetoothAdapter { + peripheral: Arc, + write_characteristic: Characteristic, + runtime: Arc, + notification_receiver: Arc>>>, +} + +impl Communication for BluetoothAdapter { + fn send(&mut self, data: &[u8]) -> Result<()> { + self.error_if_not_connected()?; + debug!("Writing {:?} to BT.", data); + self.runtime + .block_on(self.peripheral.write( + &self.write_characteristic, + data, + WriteType::WithResponse, + )) + .map_err(BluetoothError::from)?; + debug!("Data written to BT."); + Ok(()) + } + + fn recv(&mut self) -> Result>> { + self.error_if_not_connected()?; + let Ok(mut receiver) = self.notification_receiver.lock() else { + return Err(Error::BtlePlug { + e: BluetoothError::ReadTimedOut, + }); + }; + Ok(self + .runtime + .block_on(receiver.recv())) + } +} + +impl BluetoothAdapter { + fn error_if_not_connected(&mut self) -> Result<()> { + if !self.connected() { + return Err(std::io::Error::new(std::io::ErrorKind::Other, "Not connected").into()); + } + Ok(()) + } +} + +impl Connectable for BluetoothAdapter { + fn connect(&mut self) -> Result { + Ok(true) + } + + fn disconnect(&mut self) -> Result { + self.runtime + .block_on(self.peripheral.disconnect()) + .map_err(BluetoothError::from)?; + Ok(true) + } + + fn connected(&mut self) -> bool { + self.runtime + .block_on(self.peripheral.is_connected()) + .unwrap_or(false) + } +} + +impl CloneableCommunication for BluetoothAdapter { + fn boxed_clone(&self) -> Box { + Box::new(self.clone()) + } +} + +pub fn uuid_from_u16(id: u16) -> String { + let bytes = id.to_be_bytes(); + format!( + "0000{:02x}{:02x}-0000-1000-8000-00805f9b34fb", + bytes[0], bytes[1], + ) +} + +#[test] +fn test_uuid_from_u16() { + assert_eq!( + uuid_from_u16(0x2a3d), + "00002a3d-0000-1000-8000-00805f9b34fb" + ); +} + +pub fn uuid_from_u32(id: u32) -> String { + let bytes = id.to_be_bytes(); + format!( + "{:02x}{:02x}{:02x}{:02x}-0000-1000-8000-00805f9b34fb", + bytes[0], bytes[1], bytes[2], bytes[3] + ) +} + +#[test] +fn test_uuid_from_u32() { + assert_eq!( + uuid_from_u32(0x13370042), + "13370042-0000-1000-8000-00805f9b34fb" + ); +} diff --git a/src/adapters/mod.rs b/src/adapters/mod.rs index 81b0a87..e1c68b2 100644 --- a/src/adapters/mod.rs +++ b/src/adapters/mod.rs @@ -1,2 +1,4 @@ +#[cfg(feature = "bluetooth")] +pub mod bluetooth; #[cfg(feature = "uart")] -pub mod uart; \ No newline at end of file +pub mod uart; diff --git a/src/error.rs b/src/error.rs index a1620cb..96e623d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -7,6 +7,12 @@ pub enum Error { #[snafu(display("Flume Error: {e}"))] Flume { e: String }, + + #[cfg(feature = "bluetooth")] + #[snafu(display("Btleplug Error: {e}"))] + BtlePlug { + e: crate::adapters::bluetooth::BluetoothError, + }, } impl From for Error { @@ -22,3 +28,10 @@ impl From> for Error { } } } + +#[cfg(feature = "bluetooth")] +impl From for Error { + fn from(value: crate::adapters::bluetooth::BluetoothError) -> Self { + Error::BtlePlug { e: value } + } +} diff --git a/src/traits/communication.rs b/src/traits/communication.rs index 0706426..5f631ae 100644 --- a/src/traits/communication.rs +++ b/src/traits/communication.rs @@ -9,7 +9,7 @@ pub trait CloneableCommunication { fn boxed_clone(&self) -> Box; } -pub trait Connectable: CommunicationBuilder { +pub trait Connectable { /// Connect to a device /// Here goes the implementation of how to connect to a device fn connect(&mut self) -> Result;