diff --git a/Cargo.toml b/Cargo.toml index 4f5898e3..ef95899d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,27 @@ libc = "0.2.119" [target.'cfg(target_os = "windows")'.dependencies] windows = { version = "0.33.0", features = ["Devices_Bluetooth", "Devices_Bluetooth_GenericAttributeProfile", "Devices_Bluetooth_Advertisement", "Devices_Radios", "Foundation_Collections", "Foundation", "Storage_Streams"] } +[target.'cfg(target_arch = "wasm32")'.dependencies] +js-sys = "0.3.56" +wasm-bindgen = "0.2.79" +wasm-bindgen-futures = "0.4.29" + +[target.'cfg(target_arch = "wasm32")'.dependencies.web-sys] +version = "0.3.56" +features = [ + 'Bluetooth', + 'BluetoothCharacteristicProperties', + 'BluetoothDevice', + 'BluetoothLeScanFilterInit', + 'BluetoothRemoteGattCharacteristic', + 'BluetoothRemoteGattServer', + 'BluetoothRemoteGattService', + 'Event', + 'Navigator', + 'RequestDeviceOptions', + 'Window', +] + [dev-dependencies] rand = "0.8.5" pretty_env_logger = "0.4.0" diff --git a/src/lib.rs b/src/lib.rs index 2d438d2d..7ea2a79a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -105,6 +105,8 @@ mod corebluetooth; pub mod platform; #[cfg(feature = "serde")] pub mod serde; +#[cfg(target_arch = "wasm32")] +mod wasm; #[cfg(target_os = "windows")] mod winrtble; @@ -132,6 +134,9 @@ pub enum Error { #[error("Invalid Bluetooth address: {0}")] InvalidBDAddr(#[from] ParseBDAddrError), + #[error("JavaScript {:?}", _0)] + JavaScript(String), + #[error("{}", _0)] Other(Box), } diff --git a/src/platform.rs b/src/platform.rs index 4cc543e1..5cd99d52 100644 --- a/src/platform.rs +++ b/src/platform.rs @@ -9,6 +9,10 @@ pub use crate::bluez::{ pub use crate::corebluetooth::{ adapter::Adapter, manager::Manager, peripheral::Peripheral, peripheral::PeripheralId, }; +#[cfg(target_arch = "wasm32")] +pub use crate::wasm::{ + adapter::Adapter, manager::Manager, peripheral::Peripheral, peripheral::PeripheralId, +}; #[cfg(target_os = "windows")] pub use crate::winrtble::{ adapter::Adapter, manager::Manager, peripheral::Peripheral, peripheral::PeripheralId, diff --git a/src/wasm/adapter.rs b/src/wasm/adapter.rs new file mode 100644 index 00000000..2fa99916 --- /dev/null +++ b/src/wasm/adapter.rs @@ -0,0 +1,151 @@ +use super::peripheral::{Peripheral, PeripheralId}; +use super::utils::wrap_promise; +use crate::api::{BDAddr, Central, CentralEvent, Peripheral as _, ScanFilter}; +use crate::common::adapter_manager::AdapterManager; +use crate::{Error, Result}; +use async_trait::async_trait; +use futures::channel::oneshot; +use futures::stream::Stream; +use js_sys::Array; +use std::pin::Pin; +use std::sync::Arc; +use wasm_bindgen::prelude::*; +use wasm_bindgen_futures::spawn_local; +use web_sys::{BluetoothDevice, BluetoothLeScanFilterInit, RequestDeviceOptions}; + +macro_rules! spawn_local { + ($a:expr) => {{ + let (sender, receiver) = oneshot::channel(); + spawn_local(async move { + let _ = sender.send($a); + }); + receiver.await.unwrap() + }}; +} + +/// Implementation of [api::Central](crate::api::Central). +#[derive(Clone, Debug)] +pub struct Adapter { + manager: Arc>, +} + +fn bluetooth() -> Option { + web_sys::window().unwrap().navigator().bluetooth() +} + +#[async_trait] +trait AddPeripheralAndEmit { + async fn add_inital_periperals(&self) -> Vec; + fn add_device(&self, device: JsValue) -> Option; +} + +#[async_trait] +impl AddPeripheralAndEmit for Arc> { + async fn add_inital_periperals(&self) -> Vec { + if !self.peripherals().is_empty() { + return vec![]; + } + + let self_clone = self.clone(); + spawn_local!({ + wrap_promise::(bluetooth().unwrap().get_devices()) + .await + .map_or(vec![], |devices| { + devices + .iter() + .map(|device| self_clone.add_device(device).unwrap()) + .collect() + }) + }) + } + + fn add_device(&self, device: JsValue) -> Option { + let p = Peripheral::new(Arc::downgrade(self), BluetoothDevice::from(device)); + let id = p.id(); + if self.peripheral(&id).is_none() { + self.add_peripheral(p); + Some(id) + } else { + None + } + } +} + +impl Adapter { + pub(crate) fn try_new() -> Option { + if let Some(_) = bluetooth() { + Some(Self { + manager: Arc::new(AdapterManager::default()), + }) + } else { + None + } + } +} + +#[async_trait] +impl Central for Adapter { + type Peripheral = Peripheral; + + async fn events(&self) -> Result + Send>>> { + Ok(self.manager.event_stream()) + } + + async fn start_scan(&self, filter: ScanFilter) -> Result<()> { + let manager = self.manager.clone(); + spawn_local!({ + for id in manager.add_inital_periperals().await { + manager.emit(CentralEvent::DeviceDiscovered(id)); + } + + let mut options = RequestDeviceOptions::new(); + let optional_services = Array::new(); + let filters = Array::new(); + + for uuid in filter.services.iter() { + let mut filter = BluetoothLeScanFilterInit::new(); + let filter_services = Array::new(); + filter_services.push(&uuid.to_string().into()); + filter.services(&filter_services.into()); + filters.push(&filter.into()); + optional_services.push(&uuid.to_string().into()); + } + + options.filters(&filters.into()); + options.optional_services(&optional_services.into()); + + wrap_promise(bluetooth().unwrap().request_device(&options)) + .await + .map(|device| { + if let Some(id) = manager.add_device(device) { + manager.emit(CentralEvent::DeviceDiscovered(id)); + } + () + }) + }) + } + + async fn stop_scan(&self) -> Result<()> { + Ok(()) + } + + async fn peripherals(&self) -> Result> { + self.manager.add_inital_periperals().await; + Ok(self.manager.peripherals()) + } + + async fn peripheral(&self, id: &PeripheralId) -> Result { + self.manager.add_inital_periperals().await; + self.manager.peripheral(id).ok_or(Error::DeviceNotFound) + } + + async fn add_peripheral(&self, _address: BDAddr) -> Result { + Err(Error::NotSupported( + "Can't add a Peripheral from a BDAddr".to_string(), + )) + } + + async fn adapter_info(&self) -> Result { + Ok("WebBluetooth".to_string()) + } +} diff --git a/src/wasm/manager.rs b/src/wasm/manager.rs new file mode 100644 index 00000000..66472fa8 --- /dev/null +++ b/src/wasm/manager.rs @@ -0,0 +1,26 @@ +use super::adapter::Adapter; +use crate::{api, Result}; +use async_trait::async_trait; + +/// Implementation of [api::Manager](crate::api::Manager). +#[derive(Clone, Debug)] +pub struct Manager {} + +impl Manager { + pub async fn new() -> Result { + Ok(Self {}) + } +} + +#[async_trait] +impl api::Manager for Manager { + type Adapter = Adapter; + + async fn adapters(&self) -> Result> { + if let Some(adapter) = Adapter::try_new() { + Ok(vec![adapter]) + } else { + Ok(vec![]) + } + } +} diff --git a/src/wasm/mod.rs b/src/wasm/mod.rs new file mode 100644 index 00000000..6666abb3 --- /dev/null +++ b/src/wasm/mod.rs @@ -0,0 +1,4 @@ +pub mod adapter; +pub mod manager; +pub mod peripheral; +mod utils; diff --git a/src/wasm/peripheral.rs b/src/wasm/peripheral.rs new file mode 100644 index 00000000..405a35ca --- /dev/null +++ b/src/wasm/peripheral.rs @@ -0,0 +1,398 @@ +use super::utils::{uuid_from_string, wrap_promise}; +use crate::api::{ + self, BDAddr, CentralEvent, CharPropFlags, Characteristic, PeripheralProperties, Service, + ValueNotification, WriteType, +}; +use crate::common::{ + adapter_manager::AdapterManager, util::notifications_stream_from_broadcast_receiver, +}; +use crate::{Error, Result}; +use async_trait::async_trait; +use futures::channel::{mpsc, oneshot}; +use futures::stream::{Stream, StreamExt}; +use js_sys::{Array, DataView, Uint8Array}; +use std::collections::{BTreeSet, HashMap}; +use std::fmt::{self, Debug, Formatter}; +use std::pin::Pin; +use std::sync::{Arc, Mutex, Weak}; +use tokio::sync::broadcast; +use uuid::Uuid; +use wasm_bindgen::prelude::*; +use wasm_bindgen::JsCast; +use wasm_bindgen_futures::spawn_local; +use web_sys::{ + BluetoothCharacteristicProperties, BluetoothDevice, BluetoothRemoteGattCharacteristic, + BluetoothRemoteGattServer, BluetoothRemoteGattService, Event, +}; + +macro_rules! send_cmd { + ($self:ident, $cmd:ident$(, $opt:expr)*) => {{ + let (sender, receiver) = oneshot::channel(); + let _ = $self.shared.sender.unbounded_send(PeripheralSharedCmd::$cmd(sender, $($opt),*)); + receiver.await.unwrap() + }}; +} + +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct PeripheralId(String); + +/// Implementation of [api::Peripheral](crate::api::Peripheral). +#[derive(Clone)] +pub struct Peripheral { + shared: Arc, +} + +enum PeripheralSharedCmd { + IsConnected(oneshot::Sender>), + Connect(oneshot::Sender>), + Disconnect(oneshot::Sender>), + DiscoverServices(oneshot::Sender>>), + Read(oneshot::Sender>>, Uuid), + Write(oneshot::Sender>, Uuid, Vec, WriteType), + Subscribe(oneshot::Sender>, Uuid), + Unsubscribe(oneshot::Sender>, Uuid), +} + +struct Shared { + id: String, + name: Option, + services: Mutex>, + sender: mpsc::UnboundedSender, + notifications_channel: broadcast::Sender, +} + +struct SharedExecuter { + manager: Weak>, + device: BluetoothDevice, + characteristics: HashMap, + ongattserverdisconnected: Closure, + oncharacteristicvaluechanged: Closure, +} + +impl SharedExecuter { + fn gatt(&self) -> BluetoothRemoteGattServer { + self.device.gatt().unwrap() + } + + async fn is_connected(&self) -> Result { + Ok(self.gatt().connected()) + } + + async fn connect(&self) -> Result<()> { + if self.gatt().connected() { + return Ok(()); + } + + wrap_promise::(self.gatt().connect()) + .await + .map(|gatt| { + if let Some(manager) = self.manager.upgrade() { + manager.emit(CentralEvent::DeviceConnected(gatt.device().id().into())); + } + () + }) + } + + async fn disconnect(&self) -> Result<()> { + Ok(self.gatt().disconnect()) + } + + async fn discover_services(&mut self) -> Result> { + self.characteristics.clear(); + let services = wrap_promise::(self.gatt().get_primary_services()).await?; + let mut ret = BTreeSet::new(); + for service in services.iter() { + let mut characteristics = BTreeSet::new(); + let service = BluetoothRemoteGattService::from(service); + let service_uuid = uuid_from_string(service.uuid()); + + if let Ok(chars) = wrap_promise::(service.get_characteristics()).await { + for ch in chars.iter() { + let ch = BluetoothRemoteGattCharacteristic::from(ch); + let uuid = uuid_from_string(ch.uuid()); + characteristics.insert(Characteristic { + uuid, + service_uuid, + properties: ch.properties().into(), + }); + self.characteristics.insert(uuid, ch); + } + } + + ret.insert(Service { + uuid: service_uuid, + primary: service.is_primary(), + characteristics, + }); + } + Ok(ret) + } + + fn get_characteristic(&self, uuid: Uuid) -> Result<&BluetoothRemoteGattCharacteristic> { + self.characteristics.get(&uuid).map_or( + Err(Error::NotSupported("Characteristic not found".into())), + |characteristic| Ok(characteristic), + ) + } + + async fn write(&self, uuid: Uuid, mut data: Vec, write_type: WriteType) -> Result<()> { + let characteristic = self.get_characteristic(uuid)?; + wrap_promise::(match write_type { + WriteType::WithResponse => { + characteristic.write_value_with_response_with_u8_array(&mut data) + } + WriteType::WithoutResponse => { + characteristic.write_value_without_response_with_u8_array(&mut data) + } + }) + .await + .map(|_| ()) + } + + async fn read(&self, uuid: Uuid) -> Result> { + let characteristic = self.get_characteristic(uuid)?; + wrap_promise::(characteristic.read_value()) + .await + .map(|value| Uint8Array::new(&value.buffer()).to_vec()) + } + + async fn subscribe(&self, uuid: Uuid) -> Result<()> { + let characteristic = self.get_characteristic(uuid)?; + characteristic.set_oncharacteristicvaluechanged(Some( + self.oncharacteristicvaluechanged.as_ref().unchecked_ref(), + )); + wrap_promise::(characteristic.start_notifications()) + .await + .map(|_| ()) + } + + async fn unsubscribe(&self, uuid: Uuid) -> Result<()> { + let characteristic = self.get_characteristic(uuid)?; + characteristic.set_oncharacteristicvaluechanged(None); + wrap_promise::(characteristic.stop_notifications()) + .await + .map(|_| ()) + } + + fn new( + manager: Weak>, + device: BluetoothDevice, + notifications_sender: broadcast::Sender, + ) -> Self { + let manager_clone = manager.clone(); + let ongattserverdisconnected = Closure::wrap(Box::new(move |e: Event| { + let device = BluetoothDevice::from(JsValue::from(e.target().unwrap())); + if let Some(manager_upgrade) = manager_clone.upgrade() { + manager_upgrade.emit(CentralEvent::DeviceDisconnected(device.id().into())); + } + }) as Box); + + let oncharacteristicvaluechanged = Closure::wrap(Box::new(move |e: Event| { + let characteristic = + BluetoothRemoteGattCharacteristic::from(JsValue::from(e.target().unwrap())); + let notification = ValueNotification { + uuid: uuid_from_string(characteristic.uuid()), + value: characteristic + .value() + .map_or(vec![], |value| Uint8Array::new(&value.buffer()).to_vec()), + }; + // Note: we ignore send errors here which may happen while there are no + // receivers... + let _ = notifications_sender.send(notification); + }) as Box); + + SharedExecuter { + manager, + device, + characteristics: HashMap::new(), + ongattserverdisconnected, + oncharacteristicvaluechanged, + } + } + + async fn run(&mut self, mut receiver: mpsc::UnboundedReceiver) { + self.device.set_ongattserverdisconnected(Some( + self.ongattserverdisconnected.as_ref().unchecked_ref(), + )); + + while let Some(x) = receiver.next().await { + match x { + PeripheralSharedCmd::IsConnected(result) => { + let _ = result.send(self.is_connected().await); + } + PeripheralSharedCmd::Connect(result) => { + let _ = result.send(self.connect().await); + } + PeripheralSharedCmd::Disconnect(result) => { + let _ = result.send(self.disconnect().await); + } + PeripheralSharedCmd::DiscoverServices(result) => { + let _ = result.send(self.discover_services().await); + } + PeripheralSharedCmd::Read(result, characteristic) => { + let _ = result.send(self.read(characteristic).await); + } + PeripheralSharedCmd::Write(result, characteristic, data, write_type) => { + let _ = result.send(self.write(characteristic, data, write_type).await); + } + PeripheralSharedCmd::Subscribe(result, characteristic) => { + let _ = result.send(self.subscribe(characteristic).await); + } + PeripheralSharedCmd::Unsubscribe(result, characteristic) => { + let _ = result.send(self.unsubscribe(characteristic).await); + } + } + } + } +} + +impl Shared { + fn new(manager: Weak>, device: BluetoothDevice) -> Self { + let id = device.id().clone(); + let name = device.name().clone(); + let services = Mutex::new(BTreeSet::::new()); + + let (notifications_channel, _) = broadcast::channel(16); + let mut shared_executer = + SharedExecuter::new(manager.clone(), device, notifications_channel.clone()); + + let (sender, receiver) = mpsc::unbounded(); + spawn_local(async move { + shared_executer.run(receiver).await; + }); + + Self { + id, + name, + services, + sender, + notifications_channel, + } + } +} + +impl Peripheral { + pub(crate) fn new(manager: Weak>, device: BluetoothDevice) -> Self { + Peripheral { + shared: Arc::new(Shared::new(manager, device)), + } + } +} + +#[async_trait] +impl api::Peripheral for Peripheral { + fn id(&self) -> PeripheralId { + self.shared.id.clone().into() + } + + fn address(&self) -> BDAddr { + BDAddr::default() + } + + async fn properties(&self) -> Result> { + Ok(Some(PeripheralProperties { + address: BDAddr::default(), + address_type: None, + local_name: self.shared.name.clone(), + tx_power_level: None, + rssi: None, + manufacturer_data: HashMap::new(), + service_data: HashMap::new(), + services: Vec::new(), + })) + } + + fn services(&self) -> BTreeSet { + self.shared.services.lock().unwrap().clone() + } + + async fn is_connected(&self) -> Result { + send_cmd!(self, IsConnected) + } + + async fn connect(&self) -> Result<()> { + send_cmd!(self, Connect) + } + + async fn disconnect(&self) -> Result<()> { + send_cmd!(self, Disconnect) + } + + async fn discover_services(&self) -> Result<()> { + send_cmd!(self, DiscoverServices).map(|services| { + *self.shared.services.lock().unwrap() = services; + () + }) + } + + async fn write( + &self, + characteristic: &Characteristic, + data: &[u8], + write_type: WriteType, + ) -> Result<()> { + send_cmd!(self, Write, characteristic.uuid, data.to_vec(), write_type) + } + + async fn read(&self, characteristic: &Characteristic) -> Result> { + send_cmd!(self, Read, characteristic.uuid) + } + + async fn subscribe(&self, characteristic: &Characteristic) -> Result<()> { + send_cmd!(self, Subscribe, characteristic.uuid) + } + + async fn unsubscribe(&self, characteristic: &Characteristic) -> Result<()> { + send_cmd!(self, Unsubscribe, characteristic.uuid) + } + + async fn notifications(&self) -> Result + Send>>> { + let receiver = self.shared.notifications_channel.subscribe(); + Ok(notifications_stream_from_broadcast_receiver(receiver)) + } +} + +impl From for CharPropFlags { + fn from(flags: BluetoothCharacteristicProperties) -> Self { + let mut result = CharPropFlags::default(); + if flags.broadcast() { + result.insert(CharPropFlags::BROADCAST); + } + if flags.read() { + result.insert(CharPropFlags::READ); + } + if flags.write_without_response() { + result.insert(CharPropFlags::WRITE_WITHOUT_RESPONSE); + } + if flags.write() { + result.insert(CharPropFlags::WRITE); + } + if flags.notify() { + result.insert(CharPropFlags::NOTIFY); + } + if flags.indicate() { + result.insert(CharPropFlags::INDICATE); + } + if flags.authenticated_signed_writes() { + result.insert(CharPropFlags::AUTHENTICATED_SIGNED_WRITES); + } + result + } +} + +impl From for PeripheralId { + fn from(id: String) -> Self { + PeripheralId(id) + } +} + +impl Debug for Peripheral { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + let shared = &self.shared; + + f.debug_struct("Peripheral") + .field("id", &shared.id) + .field("name", &shared.name) + .finish() + } +} diff --git a/src/wasm/utils.rs b/src/wasm/utils.rs new file mode 100644 index 00000000..218aa74a --- /dev/null +++ b/src/wasm/utils.rs @@ -0,0 +1,16 @@ +use crate::{Error, Result}; +use js_sys::{Error as JsError, Promise}; +use uuid::Uuid; +use wasm_bindgen::prelude::*; +use wasm_bindgen_futures::JsFuture; + +pub async fn wrap_promise>(promise: Promise) -> Result { + match JsFuture::from(promise).await { + Ok(value) => Ok(T::from(value)), + Err(err) => Err(Error::JavaScript(JsError::from(err).message().into())), + } +} + +pub fn uuid_from_string(uuid: String) -> Uuid { + Uuid::parse_str(&uuid).unwrap() +}