diff --git a/Cargo.toml b/Cargo.toml index 89defaf..82fd8fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "tauri-plugin-blec" license = "MIT OR Apache-2.0" -version = "0.2.0" +version = "0.3.0" authors = ["Manuel Philipp"] description = "BLE-Client plugin for Tauri" edition = "2021" @@ -22,6 +22,7 @@ uuid = "1.11.0" once_cell = "1.20.2" tracing = "0.1.40" futures = { version = "0.3.31", default-features = false } +enumflags2 = { version = "0.7", features = ["serde"] } # [target.'cfg(target_os = "android")'.dependencies] async-trait = "0.1.83" diff --git a/README.md b/README.md index 3ae4663..07bd692 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ let address = ... await connect(address, () => console.log('disconnected')) // send some text to a characteristic const CHARACTERISTIC_UUID = '51FF12BB-3ED8-46E5-B4F9-D64E2FEC021B' -await sendString(CHARACTERISTIC_UUID, 'Test') +await sendString(CHARACTERISTIC_UUID, 'Test', 'withResponse') ``` ## Usage in Backend @@ -75,14 +75,13 @@ This means if you connect from the frontend you can send data from rust without ```rs use uuid::{uuid, Uuid}; +use tauri_plugin_blec::models::WriteType; const CHARACTERISTIC_UUID: Uuid = uuid!("51FF12BB-3ED8-46E5-B4F9-D64E2FEC021B"); const DATA: [u8; 500] = [0; 500]; let handler = tauri_plugin_blec::get_handler().unwrap(); handler - .lock() - .await - .send_data(CHARACTERISTIC_UUID, &DATA) + .send_data(CHARACTERISTIC_UUID, &DATA, WriteType::WithResponse) .await .unwrap(); ``` diff --git a/android/src/main/java/BleClient.kt b/android/src/main/java/BleClient.kt index c496ba9..8737539 100644 --- a/android/src/main/java/BleClient.kt +++ b/android/src/main/java/BleClient.kt @@ -4,20 +4,15 @@ import Peripheral import android.Manifest import android.annotation.SuppressLint import android.app.Activity -import app.tauri.annotation.InvokeArg -import app.tauri.plugin.Invoke - import android.bluetooth.BluetoothAdapter -import android.bluetooth.BluetoothDevice -import android.bluetooth.BluetoothGattCallback import android.bluetooth.BluetoothManager import android.bluetooth.BluetoothProfile import android.bluetooth.le.BluetoothLeScanner import android.bluetooth.le.ScanCallback import android.bluetooth.le.ScanFilter -import android.bluetooth.le.ScanSettings -import android.bluetooth.le.ScanFilter.Builder; +import android.bluetooth.le.ScanFilter.Builder import android.bluetooth.le.ScanResult +import android.bluetooth.le.ScanSettings import android.content.Context.MODE_PRIVATE import android.content.Intent import android.content.SharedPreferences @@ -26,11 +21,15 @@ import android.net.Uri import android.os.Build import android.os.ParcelUuid import android.provider.Settings +import android.util.SparseArray import android.widget.Toast import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat.startActivityForResult import androidx.core.content.ContextCompat.getSystemService +import app.tauri.annotation.InvokeArg import app.tauri.plugin.Channel +import app.tauri.plugin.Invoke +import app.tauri.plugin.JSArray import app.tauri.plugin.JSObject class BleDevice( @@ -38,6 +37,8 @@ class BleDevice( private val name: String, private val rssi: Int, private val connected: Boolean, + private val manufacturerData: SparseArray?, + private val services: List? ){ fun toJsObject():JSObject{ val obj = JSObject() @@ -46,6 +47,33 @@ class BleDevice( obj.put("name",name) obj.put("connected",connected) obj.put("rssi",rssi) + // create Json Array from services + val services = if (services != null) { + val arr = JSArray(); + for (service in services){ + arr.put(service) + } + arr + } else { null } + obj.put("services",services) + // crate object from sparse Array + val manufacturerData = if (manufacturerData != null) { + val subObj = JSObject() + for (i in 0 until manufacturerData.size()) { + val key = manufacturerData.keyAt(i) + // get the object by the key. + val value = manufacturerData.get(key) + val arr = JSArray() + for (element in value){ + // toInt is needed to generate number in Json + // the UByte is serialized as string + arr.put(element.toUByte().toInt()) + } + subObj.put(key.toString(),arr) + } + subObj + } else { null } + obj.put("manufacturerData",manufacturerData) return obj } } @@ -170,7 +198,9 @@ class BleClient(private val activity: Activity, private val plugin: BleClientPlu result.device.address, name, result.rssi, - connected + connected, + result.scanRecord?.manufacturerSpecificData, + result.scanRecord?.serviceUuids ) this@BleClient.plugin.devices[device.address] = Peripheral(this@BleClient.activity, result.device, this@BleClient.plugin) val res = JSObject() diff --git a/android/src/main/java/Peripheral.kt b/android/src/main/java/Peripheral.kt index 86883b6..7c0ed57 100644 --- a/android/src/main/java/Peripheral.kt +++ b/android/src/main/java/Peripheral.kt @@ -119,7 +119,7 @@ class Peripheral(private val activity: Activity, private val device: BluetoothDe value: ByteArray, status: Int ) { - val id = characteristic?.uuid ?: return + val id = characteristic.uuid ?: return val invoke = this@Peripheral.onReadInvoke[id]!! if (status != BluetoothGatt.GATT_SUCCESS){ invoke.reject("Read from characteristic $id failed with status $status") @@ -246,10 +246,6 @@ class Peripheral(private val activity: Activity, private val device: BluetoothDe invoke.resolve(res) } - class Notification( - uuid: UUID, - data: Array - ) fun setNotifyChannel(channel: Channel){ this.notifyChannel = channel; } diff --git a/examples/plugin-blec-example/package.json b/examples/plugin-blec-example/package.json index b5ef307..b315841 100644 --- a/examples/plugin-blec-example/package.json +++ b/examples/plugin-blec-example/package.json @@ -1,27 +1,26 @@ { - "name": "plugin-blec-example", - "private": true, - "version": "0.1.0", - "type": "module", - "scripts": { - "dev": "vite", - "build": "vue-tsc --noEmit && vite build", - "preview": "vite preview", - "tauri": "tauri" - }, - "dependencies": { - "@mnlphlp/plugin-blec": ">=0.3.0", - "@saeris/vue-spinners": "^1.0.8", - "@tauri-apps/api": ">=2.0.0", - "@tauri-apps/plugin-log": "~2", - "@tauri-apps/plugin-shell": ">=2.0.0", - "vue": "^3.3.4" - }, - "devDependencies": { - "@tauri-apps/cli": ">=2.0.0", - "@vitejs/plugin-vue": "^5.0.5", - "typescript": "^5.2.2", - "vite": "^5.3.1", - "vue-tsc": "^2.0.22" - } + "name": "plugin-blec-example", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc --noEmit && vite build", + "preview": "vite preview", + "tauri": "tauri" + }, + "dependencies": { + "@mnlphlp/plugin-blec": "file:../..", + "@tauri-apps/api": ">=2.0.0", + "@tauri-apps/plugin-log": "~2", + "@tauri-apps/plugin-shell": ">=2.0.0", + "vue": "^3.3.4" + }, + "devDependencies": { + "@tauri-apps/cli": ">=2.0.0", + "@vitejs/plugin-vue": "^5.0.5", + "typescript": "~5.6.3", + "vite": "^5.3.1", + "vue-tsc": "^2.1.10" + } } diff --git a/examples/plugin-blec-example/src-tauri/gen/android/app/build.gradle.kts b/examples/plugin-blec-example/src-tauri/gen/android/app/build.gradle.kts index aaca46e..779cf27 100644 --- a/examples/plugin-blec-example/src-tauri/gen/android/app/build.gradle.kts +++ b/examples/plugin-blec-example/src-tauri/gen/android/app/build.gradle.kts @@ -24,6 +24,14 @@ android { versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt() versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0") } + signingConfigs { + create("release") { + keyAlias = "example" + keyPassword = "123456" + storeFile = rootProject.file("example_key.jks") + storePassword = "123456" + } + } buildTypes { getByName("debug") { manifestPlaceholders["usesCleartextTraffic"] = "true" @@ -38,6 +46,7 @@ android { } getByName("release") { isMinifyEnabled = true + signingConfig = signingConfigs.getByName("release") proguardFiles( *fileTree(".") { include("**/*.pro") } .plus(getDefaultProguardFile("proguard-android-optimize.txt")) diff --git a/examples/plugin-blec-example/src-tauri/gen/android/example_key.jks b/examples/plugin-blec-example/src-tauri/gen/android/example_key.jks new file mode 100644 index 0000000..6dbf486 Binary files /dev/null and b/examples/plugin-blec-example/src-tauri/gen/android/example_key.jks differ diff --git a/examples/plugin-blec-example/src-tauri/gen/android/keystore.properties b/examples/plugin-blec-example/src-tauri/gen/android/keystore.properties new file mode 100644 index 0000000..4c1d98b --- /dev/null +++ b/examples/plugin-blec-example/src-tauri/gen/android/keystore.properties @@ -0,0 +1,3 @@ +password=123456 +keyAlias=example +storeFile=./example_key.jks \ No newline at end of file diff --git a/examples/plugin-blec-example/src-tauri/src/lib.rs b/examples/plugin-blec-example/src-tauri/src/lib.rs index 6922dda..b5b9c5b 100644 --- a/examples/plugin-blec-example/src-tauri/src/lib.rs +++ b/examples/plugin-blec-example/src-tauri/src/lib.rs @@ -10,17 +10,14 @@ async fn test() -> bool { let handler = tauri_plugin_blec::get_handler().unwrap(); let start = std::time::Instant::now(); handler - .lock() - .await - .send_data(CHARACTERISTIC_UUID, &DATA) - .await - .unwrap(); - let response = handler - .lock() - .await - .recv_data(CHARACTERISTIC_UUID) + .send_data( + CHARACTERISTIC_UUID, + &DATA, + tauri_plugin_blec::models::WriteType::WithoutResponse, + ) .await .unwrap(); + let response = handler.recv_data(CHARACTERISTIC_UUID).await.unwrap(); let time = start.elapsed(); info!("Time elapsed: {:?}", time); assert_eq!(response, DATA); diff --git a/examples/plugin-blec-example/src/App.vue b/examples/plugin-blec-example/src/App.vue index 536105e..4c0cec8 100644 --- a/examples/plugin-blec-example/src/App.vue +++ b/examples/plugin-blec-example/src/App.vue @@ -5,7 +5,6 @@ import { BleDevice, getConnectionUpdates, startScan, sendString, readString, uns import { onMounted, ref } from 'vue'; import BleDev from './components/BleDev.vue' import { invoke } from '@tauri-apps/api/core' -import { BarLoader } from '@saeris/vue-spinners' const devices = ref([]) @@ -46,6 +45,8 @@ async function test() { console.error(e) } } + +const showServices = ref(false); @@ -200,6 +206,16 @@ button { margin-right: 5px; } +#show-services-label { + margin-top: 5px; + font-size: 1.3em; +} +#show-services { + margin: 5px; + height: 2em; + width: 2em; +} + :root { color: #f6f6f6; background-color: #2f2f2f; diff --git a/examples/plugin-blec-example/src/components/BleDev.vue b/examples/plugin-blec-example/src/components/BleDev.vue index ca23387..7a51bb4 100644 --- a/examples/plugin-blec-example/src/components/BleDev.vue +++ b/examples/plugin-blec-example/src/components/BleDev.vue @@ -1,17 +1,25 @@ diff --git a/guest-js/index.ts b/guest-js/index.ts index de8861d..d0a6d0a 100644 --- a/guest-js/index.ts +++ b/guest-js/index.ts @@ -4,6 +4,8 @@ export type BleDevice = { address: string; name: string; isConnected: boolean; + services: string[]; + manufacturerData: Record; }; /** @@ -74,7 +76,6 @@ export async function connect(address: string, onDisconnect: (() => void) | null }) } catch (e) { console.error(e) - await disconnect() } } @@ -83,10 +84,11 @@ export async function connect(address: string, onDisconnect: (() => void) | null * @param characteristic UUID of the characteristic to write to * @param data Data to write to the characteristic */ -export async function send(characteristic: string, data: Uint8Array) { +export async function send(characteristic: string, data: Uint8Array, writeType: 'withResponse' | 'withoutResponse' = 'withResponse') { await invoke('plugin:blec|send', { characteristic, - data + data, + writeType, }) } @@ -95,10 +97,11 @@ export async function send(characteristic: string, data: Uint8Array) { * @param characteristic UUID of the characteristic to write to * @param data Data to write to the characteristic */ -export async function sendString(characteristic: string, data: string) { +export async function sendString(characteristic: string, data: string, writeType: 'withResponse' | 'withoutResponse' = 'withResponse') { await invoke('plugin:blec|send_string', { characteristic, - data + data, + writeType, }) } diff --git a/package.json b/package.json index dfde8c1..d273cb1 100644 --- a/package.json +++ b/package.json @@ -1,36 +1,36 @@ { - "name": "@mnlphlp/plugin-blec", - "version": "0.3.0", - "author": "Manuel Philipp", - "description": "JS Bindings for BLE-Client plugin for Tauri", - "license": "(MIT OR Apache-2.0)", - "type": "module", + "name": "@mnlphlp/plugin-blec", + "version": "0.3.3", + "author": "Manuel Philipp", + "description": "JS Bindings for BLE-Client plugin for Tauri", + "license": "(MIT OR Apache-2.0)", + "type": "module", + "types": "./dist-js/index.d.ts", + "main": "./dist-js/index.cjs", + "module": "./dist-js/index.js", + "exports": { "types": "./dist-js/index.d.ts", - "main": "./dist-js/index.cjs", - "module": "./dist-js/index.js", - "exports": { - "types": "./dist-js/index.d.ts", - "import": "./dist-js/index.js", - "require": "./dist-js/index.cjs" - }, - "files": [ - "dist-js", - "README.md" - ], - "scripts": { - "build": "rollup -c", - "prepublishOnly": "yarn build", - "pretest": "yarn build" - }, - "dependencies": { - "@tauri-apps/api": ">=2.0.0-beta.6" - }, - "devDependencies": { - "@rollup/plugin-typescript": "^11.1.6", - "rollup": "^4.24.0", - "tslib": "^2.6.2", - "typedoc": "^0.26.11", - "typescript": "^5.3.3" - }, - "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" -} \ No newline at end of file + "import": "./dist-js/index.js", + "require": "./dist-js/index.cjs" + }, + "files": [ + "dist-js", + "README.md" + ], + "scripts": { + "build": "rollup -c", + "prepublishOnly": "yarn build", + "pretest": "yarn build" + }, + "dependencies": { + "@tauri-apps/api": ">=2.0.0-beta.6" + }, + "devDependencies": { + "@rollup/plugin-typescript": "^11.1.6", + "rollup": "^4.24.0", + "tslib": "^2.6.2", + "typedoc": "^0.26.11", + "typescript": "^5.3.3" + }, + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" +} diff --git a/src/android.rs b/src/android.rs index 1e5436f..56f7cc4 100644 --- a/src/android.rs +++ b/src/android.rs @@ -6,9 +6,9 @@ use btleplug::{ }, platform::PeripheralId, }; -use futures::{stream::Once, Stream}; +use futures::Stream; use once_cell::sync::{Lazy, OnceCell}; -use serde::{Deserialize, Serialize}; +use serde::Deserialize; use std::{ collections::{BTreeSet, HashMap}, pin::Pin, @@ -17,7 +17,7 @@ use std::{ use tauri::{ ipc::{Channel, InvokeResponseBody}, plugin::PluginHandle, - AppHandle, Manager as _, Wry, + AppHandle, Wry, }; use tokio::sync::RwLock; use tokio_stream::wrappers::ReceiverStream; @@ -69,6 +69,7 @@ fn on_device_callback(response: InvokeResponseBody) -> std::result::Result<(), t Ok(()) } +#[allow(dependency_on_unit_never_type_fallback)] #[async_trait] impl btleplug::api::Central for Adapter { type Peripheral = Peripheral; @@ -156,6 +157,7 @@ impl Manager { } } +#[allow(dependency_on_unit_never_type_fallback)] #[async_trait] impl btleplug::api::Manager for Manager { type Adapter = Adapter; @@ -166,11 +168,16 @@ impl btleplug::api::Manager for Manager { } #[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct Peripheral { id: PeripheralId, address: BDAddr, name: String, rssi: i16, + #[serde(default)] + manufacturer_data: HashMap>, + #[serde(default)] + services: Vec, } #[derive(serde::Serialize)] #[serde(rename_all = "camelCase")] @@ -190,6 +197,7 @@ struct ReadParams { characteristic: Uuid, } +#[allow(dependency_on_unit_never_type_fallback)] #[async_trait::async_trait] impl btleplug::api::Peripheral for Peripheral { fn id(&self) -> PeripheralId { @@ -205,7 +213,14 @@ impl btleplug::api::Peripheral for Peripheral { address: self.address, local_name: Some(self.name.clone()), rssi: Some(self.rssi), - ..Default::default() + manufacturer_data: self.manufacturer_data.clone(), + services: self.services.clone(), + // TODO: implement the rest + // at the moment not used by the handler or BleDevice struct so we can return default values + address_type: Default::default(), + class: Default::default(), + tx_power_level: Default::default(), + service_data: Default::default(), })) } @@ -262,7 +277,6 @@ impl btleplug::api::Peripheral for Peripheral { characteristics, }); } - info!("services: {services:?}"); services } @@ -420,11 +434,11 @@ impl btleplug::api::Peripheral for Peripheral { Ok(Box::pin(stream)) } - async fn write_descriptor(&self, descriptor: &Descriptor, data: &[u8]) -> Result<()> { + async fn write_descriptor(&self, _descriptor: &Descriptor, _data: &[u8]) -> Result<()> { todo!() } - async fn read_descriptor(&self, descriptor: &Descriptor) -> Result> { + async fn read_descriptor(&self, _descriptor: &Descriptor) -> Result> { todo!() } } diff --git a/src/commands.rs b/src/commands.rs index 3c3bdb3..c35988a 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -6,7 +6,7 @@ use uuid::Uuid; use crate::error::Result; use crate::get_handler; -use crate::models::BleDevice; +use crate::models::{BleDevice, ScanFilter, WriteType}; #[command] pub(crate) async fn scan( @@ -24,7 +24,9 @@ pub(crate) async fn scan( .expect("failed to send device to the front-end"); } }); - handler.lock().await.discover(Some(tx), timeout).await?; + handler + .discover(Some(tx), timeout, ScanFilter::None) + .await?; Ok(()) } @@ -32,7 +34,7 @@ pub(crate) async fn scan( pub(crate) async fn stop_scan(_app: AppHandle) -> Result<()> { tracing::info!("Stopping BLE scan"); let handler = get_handler()?; - handler.lock().await.stop_scan().await?; + handler.stop_scan().await?; Ok(()) } @@ -43,13 +45,15 @@ pub(crate) async fn connect( on_disconnect: Channel<()>, ) -> Result<()> { tracing::info!("Connecting to BLE device: {:?}", address); - let mut handler = get_handler()?.lock().await; + let handler = get_handler()?; let disconnct_handler = move || { on_disconnect .send(()) .expect("failed to send disconnect event to the front-end"); }; - handler.connect(address, Some(disconnct_handler)).await?; + handler + .connect(&address, Some(Box::new(disconnct_handler))) + .await?; Ok(()) } @@ -57,7 +61,7 @@ pub(crate) async fn connect( pub(crate) async fn disconnect(_app: AppHandle) -> Result<()> { tracing::info!("Disconnecting from BLE device"); let handler = get_handler()?; - handler.lock().await.disconnect().await?; + handler.disconnect().await?; Ok(()) } @@ -68,9 +72,9 @@ pub(crate) async fn connection_state( ) -> Result<()> { let handler = get_handler()?; let (tx, mut rx) = tokio::sync::mpsc::channel(1); - handler.lock().await.set_connection_update_channel(tx); + handler.set_connection_update_channel(tx).await; update - .send(handler.lock().await.is_connected()) + .send(handler.is_connected()) .expect("failed to send connection state"); async_runtime::spawn(async move { while let Some(connected) = rx.recv().await { @@ -89,9 +93,9 @@ pub(crate) async fn scanning_state( ) -> Result<()> { let handler = get_handler()?; let (tx, mut rx) = tokio::sync::mpsc::channel(1); - handler.lock().await.set_scanning_update_channel(tx); + handler.set_scanning_update_channel(tx).await; update - .send(handler.lock().await.is_scanning()) + .send(handler.is_scanning().await) .expect("failed to send scanning state"); async_runtime::spawn(async move { while let Some(scanning) = rx.recv().await { @@ -108,21 +112,18 @@ pub(crate) async fn send( _app: AppHandle, characteristic: Uuid, data: Vec, + write_type: WriteType, ) -> Result<()> { info!("Sending data: {data:?}"); let handler = get_handler()?; - handler - .lock() - .await - .send_data(characteristic, &data) - .await?; + handler.send_data(characteristic, &data, write_type).await?; Ok(()) } #[command] pub(crate) async fn recv(_app: AppHandle, characteristic: Uuid) -> Result> { let handler = get_handler()?; - let data = handler.lock().await.recv_data(characteristic).await?; + let data = handler.recv_data(characteristic).await?; Ok(data) } @@ -131,9 +132,10 @@ pub(crate) async fn send_string( app: AppHandle, characteristic: Uuid, data: String, + write_type: WriteType, ) -> Result<()> { let data = data.as_bytes().to_vec(); - send(app, characteristic, data).await + send(app, characteristic, data, write_type).await } #[command] @@ -149,8 +151,6 @@ async fn subscribe_channel(characteristic: Uuid) -> Result( characteristic: Uuid, ) -> Result<()> { let handler = get_handler()?; - handler.lock().await.unsubscribe(characteristic).await?; + handler.unsubscribe(characteristic).await?; Ok(()) } diff --git a/src/error.rs b/src/error.rs index e801282..ba6c195 100644 --- a/src/error.rs +++ b/src/error.rs @@ -28,6 +28,11 @@ pub enum Error { #[error("no bluetooth adapters found")] NoAdapters, + #[error("Unknonwn error during disconnect")] + DisconnectFailed, + + #[error("Unknown error during connect")] + ConnectionFailed, #[cfg(target_os = "android")] #[error(transparent)] PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError), diff --git a/src/handler.rs b/src/handler.rs index 7590170..ffb392e 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -1,24 +1,21 @@ use crate::error::Error; -use crate::models::{fmt_addr, BleDevice}; +use crate::models::{self, fmt_addr, BleDevice, ScanFilter, Service}; use btleplug::api::CentralEvent; -use btleplug::api::{ - Central, Characteristic, Manager as _, Peripheral as _, ScanFilter, WriteType, -}; +use btleplug::api::{Central, Characteristic, Manager as _, Peripheral as _}; +use btleplug::platform::PeripheralId; use futures::{Stream, StreamExt}; use std::collections::HashMap; use std::pin::Pin; use std::sync::Arc; use std::time::Duration; use tauri::async_runtime; -use tokio::sync::{mpsc, Mutex}; +use tokio::sync::{mpsc, watch, Mutex}; use tokio::time::sleep; -use tracing::{debug, info, warn}; +use tracing::{debug, error, info, warn}; use uuid::Uuid; #[cfg(target_os = "android")] use crate::android::{Adapter, Manager, Peripheral}; -#[cfg(target_os = "android")] -use btleplug::api::{Central as _, Manager as _, Peripheral as _}; #[cfg(not(target_os = "android"))] use btleplug::platform::{Adapter, Manager, Peripheral}; @@ -28,19 +25,32 @@ struct Listener { callback: ListenerCallback, } -pub struct Handler { - connected: Option, +struct HandlerState { characs: Vec, - devices: Arc>>, - adapter: Arc, listen_handle: Option>, - notify_listeners: Arc>>, on_disconnect: Option>>, connection_update_channel: Option>, scan_update_channel: Option>, scan_task: Option>, } +impl HandlerState { + fn get_charac(&self, uuid: Uuid) -> Result<&Characteristic, Error> { + let charac = self.characs.iter().find(|c| c.uuid == uuid); + charac.ok_or(Error::CharacNotAvailable(uuid.to_string())) + } +} + +pub struct Handler { + devices: Arc>>, + adapter: Arc, + notify_listeners: Arc>>, + connected_rx: watch::Receiver, + connected_tx: watch::Sender, + state: Mutex, + connected_dev: Mutex>, +} + async fn get_central() -> Result { let manager = Manager::new().await?; let adapters = manager.adapters().await?; @@ -51,28 +61,33 @@ async fn get_central() -> Result { impl Handler { pub(crate) async fn new() -> Result { let central = get_central().await?; + let (connected_tx, connected_rx) = watch::channel(false); Ok(Self { devices: Arc::new(Mutex::new(HashMap::new())), - characs: vec![], - connected: None, adapter: Arc::new(central), - listen_handle: None, notify_listeners: Arc::new(Mutex::new(vec![])), - on_disconnect: None, - connection_update_channel: None, - scan_task: None, - scan_update_channel: None, + connected_rx, + connected_tx, + connected_dev: Mutex::new(None), + state: Mutex::new(HandlerState { + on_disconnect: None, + connection_update_channel: None, + scan_task: None, + scan_update_channel: None, + listen_handle: None, + characs: vec![], + }), }) } /// Returns true if a device is connected pub fn is_connected(&self) -> bool { - self.connected.is_some() + *self.connected_rx.borrow() } /// Returns true if the adapter is scanning - pub fn is_scanning(&self) -> bool { - if let Some(handle) = &self.scan_task { + pub async fn is_scanning(&self) -> bool { + if let Some(handle) = &self.state.lock().await.scan_task { !handle.is_finished() } else { false @@ -87,14 +102,14 @@ impl Handler { /// async_runtime::block_on(async { /// let handler = tauri_plugin_blec::get_handler().unwrap(); /// let (tx, mut rx) = mpsc::channel(1); - /// handler.lock().await.set_scanning_update_channel(tx); + /// handler.set_scanning_update_channel(tx).await; /// while let Some(scanning) = rx.recv().await { /// println!("Scanning: {scanning}"); /// } /// }); /// ``` - pub fn set_scanning_update_channel(&mut self, tx: mpsc::Sender) { - self.scan_update_channel = Some(tx); + pub async fn set_scanning_update_channel(&self, tx: mpsc::Sender) { + self.state.lock().await.scan_update_channel = Some(tx); } /// Takes a sender that will be used to send changes in the connection status @@ -105,18 +120,20 @@ impl Handler { /// async_runtime::block_on(async { /// let handler = tauri_plugin_blec::get_handler().unwrap(); /// let (tx, mut rx) = mpsc::channel(1); - /// handler.lock().await.set_connection_update_channel(tx); + /// handler.set_connection_update_channel(tx).await; /// while let Some(connected) = rx.recv().await { /// println!("Connected: {connected}"); /// } /// }); /// ``` - pub fn set_connection_update_channel(&mut self, tx: mpsc::Sender) { - self.connection_update_channel = Some(tx); + pub async fn set_connection_update_channel(&self, tx: mpsc::Sender) { + self.state.lock().await.connection_update_channel = Some(tx); } /// Connects to the given address - /// If a callback is provided, it will be called when the device is disconnected + /// If a callback is provided, it will be called when the device is disconnected. + /// Because connecting sometimes fails especially on android, this method tries up to 3 times + /// before returning an error /// # Errors /// Returns an error if no devices are found, if the device is already connected, /// if the connection fails, or if the service/characteristics discovery fails @@ -125,63 +142,107 @@ impl Handler { /// use tauri::async_runtime; /// async_runtime::block_on(async { /// let handler = tauri_plugin_blec::get_handler().unwrap(); - /// handler.lock().await.connect("00:00:00:00:00:00".to_string(),Some(|| println!("disconnected"))).await.unwrap(); + /// handler.connect("00:00:00:00:00:00", Some(Box::new(|| println!("disconnected")))).await.unwrap(); /// }); /// ``` pub async fn connect( - &mut self, - address: String, - on_disconnect: Option, + &self, + address: &str, + on_disconnect: Option>, ) -> Result<(), Error> { if self.devices.lock().await.len() == 0 { - self.discover(None, 1000).await?; + self.discover(None, 1000, ScanFilter::None).await?; } + // cancel any running discovery + let _ = self.stop_scan().await; // connect to the given address - self.connect_device(address).await?; + // try up to 3 times before returning an error + let mut connected = Ok(()); + for i in 0..3 { + if let Err(e) = self.connect_device(address).await { + if i < 2 { + warn!("Failed to connect device, retrying in 1s: {e}"); + sleep(Duration::from_secs(1)).await; + continue; + } + connected = Err(e); + } else { + connected = Ok(()); + break; + } + } + if let Err(e) = connected { + *self.connected_dev.lock().await = None; + let _ = self.connected_tx.send(false); + error!("Failed to connect device: {e}"); + return Err(e); + } + let mut state = self.state.lock().await; // set callback to run on disconnect if let Some(cb) = on_disconnect { - self.on_disconnect = Some(Mutex::new(Box::new(cb))); + state.on_disconnect = Some(Mutex::new(cb)); } // discover service/characteristics - self.connect_services().await?; + self.connect_services(&mut state).await?; // start background task for notifications - self.listen_handle = Some(async_runtime::spawn(listen_notify( - self.connected.clone(), + state.listen_handle = Some(async_runtime::spawn(listen_notify( + self.connected_dev.lock().await.clone(), self.notify_listeners.clone(), ))); Ok(()) } - async fn connect_services(&mut self) -> Result<(), Error> { - let device = self.connected.as_ref().ok_or(Error::NoDeviceConnected)?; - device.discover_services().await?; - let services = device.services(); + async fn connect_services(&self, state: &mut HandlerState) -> Result<(), Error> { + let device = self.connected_dev.lock().await; + let device = device.as_ref().ok_or(Error::NoDeviceConnected)?; + let mut services = device.services(); + if services.is_empty() { + device.discover_services().await?; + services = device.services(); + } for s in services { for c in &s.characteristics { - self.characs.push(c.clone()); + state.characs.push(c.clone()); } } Ok(()) } - async fn connect_device(&mut self, address: String) -> Result<(), Error> { + async fn connect_device(&self, address: &str) -> Result<(), Error> { debug!("connecting to {address}",); - if let Some(dev) = self.connected.as_ref() { - if address == fmt_addr(dev.address()) { - return Err(Error::AlreadyConnected); - } - } + let mut connected_rx = self.connected_rx.clone(); let devices = self.devices.lock().await; let device = devices - .get(&address) + .get(address) .ok_or(Error::UnknownPeripheral(address.to_string()))?; - if !device.is_connected().await? { + *self.connected_dev.lock().await = Some(device.clone()); + if device.is_connected().await? { + debug!("Device already connected"); + self.connected_tx + .send(true) + .expect("failed to send connected update"); + } else { + assert!( + !(*connected_rx.borrow_and_update()), + "connected_rx is true without device being connected, this is a bug" + ); debug!("Connecting to device"); device.connect().await?; + debug!("waiting for connection event"); + // wait for the actual connection to be established + connected_rx + .changed() + .await + .expect("failed to wait for connection event"); debug!("Connecting done"); + if !*self.connected_rx.borrow() { + // still not connected + return Err(Error::ConnectionFailed); + } } - self.connected = Some(device.clone()); - if let Some(tx) = &self.connection_update_channel { + + let state = self.state.lock().await; + if let Some(tx) = &state.connection_update_channel { tx.send(true) .await .expect("failed to send connection update"); @@ -190,35 +251,83 @@ impl Handler { } /// Disconnects from the connected device + /// This triggers a disconnect and then waits for the actual disconnect event from the adapter /// # Errors - /// Returns an error if no device is connected or if the disconnect operation fails - pub async fn disconnect(&mut self) -> Result<(), Error> { - debug!("disconnecting"); - if let Some(handle) = self.listen_handle.take() { + /// Returns an error if no device is connected or if the disconnect fails + /// # Panics + /// panics if there is an error with handling the internal disconnect event + pub async fn disconnect(&self) -> Result<(), Error> { + debug!("disconnect triggered by user"); + let mut connected_rx = self.connected_rx.clone(); + { + // Scope is important to not lock device while waiting for disconnect event + let dev = self.connected_dev.lock().await; + if let Some(dev) = dev.as_ref() { + if let Ok(true) = dev.is_connected().await { + assert!( + (*connected_rx.borrow_and_update()), + "connected_rx is false with a device being connected, this is a bug" + ); + dev.disconnect().await?; + } else { + debug!("device is not connected"); + return Err(Error::NoDeviceConnected); + } + } else { + debug!("no device connected"); + return Err(Error::NoDeviceConnected); + } + } + debug!("waiting for disconnect event"); + // the change will be triggered by handle_event -> handle_disconnect which runs in another + // task + connected_rx + .changed() + .await + .expect("failed to wait for disconnect event"); + if *self.connected_rx.borrow() { + // still connected + return Err(Error::DisconnectFailed); + } + Ok(()) + } + + /// Clears internal state, updates connected flag and calls disconnect callback + async fn handle_disconnect(&self, peripheral_id: PeripheralId) -> Result<(), Error> { + let mut dev = self.connected_dev.lock().await; + if !dev.as_ref().is_some_and(|dev| dev.id() == peripheral_id) { + // event not for currently connected device, ignore + return Ok(()); + } + debug!("locking state for disconnect"); + let mut state = self.state.lock().await; + info!("disconnecting"); + *dev = None; + if let Some(handle) = state.listen_handle.take() { handle.abort(); } *self.notify_listeners.lock().await = vec![]; - if let Some(dev) = self.connected.as_mut() { - if let Ok(true) = dev.is_connected().await { - dev.disconnect().await?; - } - self.connected = None; - } - if let Some(on_disconnect) = &self.on_disconnect { + if let Some(on_disconnect) = &state.on_disconnect { let callback = on_disconnect.lock().await; callback(); } - if let Some(tx) = &self.connection_update_channel { + if let Some(tx) = &state.connection_update_channel { tx.send(false).await?; } - self.characs.clear(); + state.characs.clear(); + self.connected_tx + .send(false) + .expect("failed to send connected update"); Ok(()) } - /// Scans for [timeout] milliseconds and periodically sends discovered devices + /// Scans for `timeout` milliseconds and periodically sends discovered devices /// to the given channel. /// A task is spawned to handle the scan and send the devices, so the function /// returns immediately. + /// + /// A Variant of [`ScanFilter`] can be provided to filter the discovered devices + /// /// # Errors /// Returns an error if starting the scan fails /// # Panics @@ -227,48 +336,50 @@ impl Handler { /// ```no_run /// use tauri::async_runtime; /// use tokio::sync::mpsc; + /// use tauri_plugin_blec::models::ScanFilter; + /// /// async_runtime::block_on(async { /// let handler = tauri_plugin_blec::get_handler().unwrap(); /// let (tx, mut rx) = mpsc::channel(1); - /// handler.lock().await.discover(Some(tx),1000).await.unwrap(); + /// handler.discover(Some(tx),1000, ScanFilter::None).await.unwrap(); /// while let Some(devices) = rx.recv().await { /// println!("Discovered {devices:?}"); /// } /// }); /// ``` pub async fn discover( - &mut self, + &self, tx: Option>>, timeout: u64, + filter: ScanFilter, ) -> Result<(), Error> { + let mut state = self.state.lock().await; // stop any ongoing scan - if let Some(handle) = self.scan_task.take() { + if let Some(handle) = state.scan_task.take() { handle.abort(); self.adapter.stop_scan().await?; } // start a new scan self.adapter - .start_scan(ScanFilter { - // services: vec![*SERVICE_UUID], - services: vec![], - }) + .start_scan(btleplug::api::ScanFilter::default()) .await?; - if let Some(tx) = &self.scan_update_channel { + if let Some(tx) = &state.scan_update_channel { tx.send(true).await?; } let mut self_devices = self.devices.clone(); let adapter = self.adapter.clone(); - let scan_update_channel = self.scan_update_channel.clone(); - self.scan_task = Some(tokio::task::spawn(async move { + let scan_update_channel = state.scan_update_channel.clone(); + state.scan_task = Some(tokio::task::spawn(async move { self_devices.lock().await.clear(); let loops = timeout / 200; let mut devices; for _ in 0..loops { sleep(Duration::from_millis(200)).await; - let discovered = adapter + let mut discovered = adapter .peripherals() .await .expect("failed to get peripherals"); + filter_peripherals(&mut discovered, &filter).await; devices = Self::add_devices(&mut self_devices, discovered).await; if !devices.is_empty() { if let Some(tx) = &tx { @@ -286,15 +397,75 @@ impl Handler { Ok(()) } + /// Discover provided services and charecteristics + /// If the device is not connected, a connection is made in order to discover the services and characteristics + /// After the discovery is done, the device is disconnected + /// If the devices was already connected, it will stay connected + /// # Errors + /// Returns an error if the device is not found, if the connection fails, or if the discovery fails + /// # Panics + /// Panics if there is an error with the internal disconnect event + pub async fn discover_services(&self, address: &str) -> Result, Error> { + let mut already_connected = self + .connected_dev + .lock() + .await + .as_ref() + .is_some_and(|dev| address == fmt_addr(dev.address())); + let device = if already_connected { + self.connected_dev + .lock() + .await + .as_ref() + .expect("Connection exists") + .clone() + } else { + let device = self + .devices + .lock() + .await + .get(address) + .ok_or(Error::UnknownPeripheral(address.to_string()))? + .clone(); + if device.is_connected().await? { + already_connected = true; + } else if let Err(e) = self.connect_device(address).await { + *self.connected_dev.lock().await = None; + let _ = self.connected_tx.send(false); + error!("Failed to connect for discovery: {e}"); + return Err(e); + } + device + }; + debug!("discovering services on {address}"); + if device.services().is_empty() { + device.discover_services().await?; + } + let services = device.services().iter().map(Service::from).collect(); + if !already_connected { + let mut connected_rx = self.connected_rx.clone(); + if *connected_rx.borrow_and_update() { + device.disconnect().await?; + debug!("waiting for disconnect event"); + connected_rx + .changed() + .await + .expect("failed to wait for disconnect event"); + } + } + Ok(services) + } + /// Stops scanning for devices /// # Errors /// Returns an error if stopping the scan fails - pub async fn stop_scan(&mut self) -> Result<(), Error> { + pub async fn stop_scan(&self) -> Result<(), Error> { self.adapter.stop_scan().await?; - if let Some(handle) = self.scan_task.take() { + let mut state = self.state.lock().await; + if let Some(handle) = state.scan_task.take() { handle.abort(); } - if let Some(tx) = &self.scan_update_channel { + if let Some(tx) = &state.scan_update_channel { tx.send(false).await?; } Ok(()) @@ -328,17 +499,26 @@ impl Handler { /// ```no_run /// use tauri::async_runtime; /// use uuid::{Uuid,uuid}; + /// use tauri_plugin_blec::models::WriteType; + /// /// const CHARACTERISTIC_UUID: Uuid = uuid!("51FF12BB-3ED8-46E5-B4F9-D64E2FEC021B"); /// async_runtime::block_on(async { /// let handler = tauri_plugin_blec::get_handler().unwrap(); /// let data = [1,2,3,4,5]; - /// let response = handler.lock().await.send_data(CHARACTERISTIC_UUID,&data).await.unwrap(); + /// let response = handler.send_data(CHARACTERISTIC_UUID,&data, WriteType::WithResponse).await.unwrap(); /// }); /// ``` - pub async fn send_data(&mut self, c: Uuid, data: &[u8]) -> Result<(), Error> { - let dev = self.connected.as_ref().ok_or(Error::NoDeviceConnected)?; - let charac = self.get_charac(c)?; - dev.write(charac, data, WriteType::WithoutResponse).await?; + pub async fn send_data( + &self, + c: Uuid, + data: &[u8], + write_type: models::WriteType, + ) -> Result<(), Error> { + let dev = self.connected_dev.lock().await; + let dev = dev.as_ref().ok_or(Error::NoDeviceConnected)?; + let state = self.state.lock().await; + let charac = state.get_charac(c)?; + dev.write(charac, data, write_type.into()).await?; Ok(()) } @@ -354,21 +534,18 @@ impl Handler { /// const CHARACTERISTIC_UUID: Uuid = uuid!("51FF12BB-3ED8-46E5-B4F9-D64E2FEC021B"); /// async_runtime::block_on(async { /// let handler = tauri_plugin_blec::get_handler().unwrap(); - /// let response = handler.lock().await.recv_data(CHARACTERISTIC_UUID).await.unwrap(); + /// let response = handler.recv_data(CHARACTERISTIC_UUID).await.unwrap(); /// }); /// ``` - pub async fn recv_data(&mut self, c: Uuid) -> Result, Error> { - let dev = self.connected.as_ref().ok_or(Error::NoDeviceConnected)?; - let charac = self.get_charac(c)?; + pub async fn recv_data(&self, c: Uuid) -> Result, Error> { + let dev = self.connected_dev.lock().await; + let dev = dev.as_ref().ok_or(Error::NoDeviceConnected)?; + let state = self.state.lock().await; + let charac = state.get_charac(c)?; let data = dev.read(charac).await?; Ok(data) } - fn get_charac(&self, uuid: Uuid) -> Result<&Characteristic, Error> { - let charac = self.characs.iter().find(|c| c.uuid == uuid); - charac.ok_or(Error::CharacNotAvailable(uuid.to_string())) - } - /// Subscribe to notifications from the given characteristic /// The callback will be called whenever a notification is received /// # Errors @@ -381,16 +558,18 @@ impl Handler { /// const CHARACTERISTIC_UUID: Uuid = uuid!("51FF12BB-3ED8-46E5-B4F9-D64E2FEC021B"); /// async_runtime::block_on(async { /// let handler = tauri_plugin_blec::get_handler().unwrap(); - /// let response = handler.lock().await.subscribe(CHARACTERISTIC_UUID,|data| println!("received {data:?}")).await.unwrap(); + /// let response = handler.subscribe(CHARACTERISTIC_UUID,|data| println!("received {data:?}")).await.unwrap(); /// }); /// ``` pub async fn subscribe( - &mut self, + &self, c: Uuid, callback: impl Fn(&[u8]) + Send + Sync + 'static, ) -> Result<(), Error> { - let dev = self.connected.as_ref().ok_or(Error::NoDeviceConnected)?; - let charac = self.get_charac(c)?; + let dev = self.connected_dev.lock().await; + let dev = dev.as_ref().ok_or(Error::NoDeviceConnected)?; + let state = self.state.lock().await; + let charac = state.get_charac(c)?; dev.subscribe(charac).await?; self.notify_listeners.lock().await.push(Listener { uuid: charac.uuid, @@ -404,9 +583,11 @@ impl Handler { /// # Errors /// Returns an error if no device is connected or the characteristic is not available /// or if the unsubscribe operation fails - pub async fn unsubscribe(&mut self, c: Uuid) -> Result<(), Error> { - let dev = self.connected.as_ref().ok_or(Error::NoDeviceConnected)?; - let charac = self.get_charac(c)?; + pub async fn unsubscribe(&self, c: Uuid) -> Result<(), Error> { + let dev = self.connected_dev.lock().await; + let dev = dev.as_ref().ok_or(Error::NoDeviceConnected)?; + let state = self.state.lock().await; + let charac = state.get_charac(c)?; dev.unsubscribe(charac).await?; let mut listeners = self.notify_listeners.lock().await; listeners.retain(|l| l.uuid != charac.uuid); @@ -420,21 +601,94 @@ impl Handler { Ok(events) } - pub(crate) async fn handle_event(&mut self, event: CentralEvent) -> Result<(), Error> { + pub(crate) async fn handle_event(&self, event: CentralEvent) -> Result<(), Error> { match event { - CentralEvent::DeviceDisconnected(_) => self.disconnect().await, - _ => Ok(()), + CentralEvent::DeviceDisconnected(peripheral_id) => { + self.handle_disconnect(peripheral_id).await?; + } + CentralEvent::DeviceConnected(peripheral_id) => { + self.handle_connect(peripheral_id).await; + } + + _event => {} } + Ok(()) } /// Returns the connected device /// # Errors /// Returns an error if no device is connected pub async fn connected_device(&self) -> Result { - let p = self.connected.as_ref().ok_or(Error::NoDeviceConnected)?; + let p = self.connected_dev.lock().await; + let p = p.as_ref().ok_or(Error::NoDeviceConnected)?; let d = BleDevice::from_peripheral(p).await?; Ok(d) } + + #[allow(clippy::redundant_closure_for_method_calls)] + async fn handle_connect(&self, peripheral_id: PeripheralId) { + let connected_device = self.connected_dev.lock().await.as_ref().map(|d| d.id()); + if let Some(connected_device) = connected_device { + if connected_device == peripheral_id { + debug!("connection to {peripheral_id} established"); + self.connected_tx + .send(true) + .expect("failed to send connected update"); + } else { + // event not for currently connected device, ignore + debug!("Unexpected connect event for device {peripheral_id}, connected device is {connected_device}"); + } + } else { + debug!( + "connect event for device {peripheral_id} received without waiting for connection" + ); + } + } +} + +async fn filter_peripherals(discovered: &mut Vec, filter: &ScanFilter) { + if matches!(filter, ScanFilter::None) { + return; + } + let mut remove = vec![]; + for p in discovered.iter().enumerate() { + let Ok(Some(properties)) = p.1.properties().await else { + // can't filter without properties + remove.push(p.0); + continue; + }; + match filter { + ScanFilter::None => unreachable!("Earyl return for no filter"), + ScanFilter::Service(uuid) => { + if !properties.services.iter().any(|s| s == uuid) { + remove.push(p.0); + } + } + ScanFilter::AnyService(uuids) => { + if !properties.services.iter().any(|s| uuids.contains(s)) { + remove.push(p.0); + } + } + ScanFilter::AllServices(uuids) => { + if !uuids.iter().all(|s| properties.services.contains(s)) { + remove.push(p.0); + } + } + ScanFilter::ManufacturerData(key, value) => { + if !properties + .manufacturer_data + .get(key) + .is_some_and(|v| v == value) + { + remove.push(p.0); + } + } + } + } + + for i in remove.iter().rev() { + discovered.swap_remove(*i); + } } async fn listen_notify(dev: Option, listeners: Arc>>) { @@ -444,11 +698,6 @@ async fn listen_notify(dev: Option, listeners: Arc> = OnceCell::new(); +static HANDLER: OnceCell = OnceCell::new(); /// Initializes the plugin. /// # Panics /// Panics if the handler cannot be initialized. pub fn init() -> TauriPlugin { let handler = async_runtime::block_on(Handler::new()).expect("failed to initialize handler"); - let _ = HANDLER.set(Mutex::new(handler)); + let _ = HANDLER.set(handler); #[allow(unused)] Builder::new("blec") @@ -41,25 +40,20 @@ pub fn init() -> TauriPlugin { /// Returns the BLE handler to use blec from rust. /// # Errors /// Returns an error if the handler is not initialized. -pub fn get_handler() -> error::Result<&'static Mutex> { +pub fn get_handler() -> error::Result<&'static Handler> { let handler = HANDLER.get().ok_or(error::Error::HandlerNotInitialized)?; Ok(handler) } async fn handle_events() { - let stream = get_handler() - .expect("failed to get handler") - .lock() - .await + let handler = get_handler().expect("failed to get handler"); + let stream = handler .get_event_stream() .await .expect("failed to get event stream"); stream .for_each(|event| async { - get_handler() - .expect("failed to get handler") - .lock() - .await + handler .handle_event(event) .await .expect("failed to handle event"); diff --git a/src/models.rs b/src/models.rs index 9bef803..c643d33 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1,27 +1,24 @@ +use std::collections::HashMap; + use btleplug::api::BDAddr; +use enumflags2::BitFlags; use serde::{Deserialize, Serialize}; +use uuid::Uuid; use crate::error; -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] -pub struct PingRequest { - pub value: Option, -} - -#[derive(Debug, Clone, Default, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct PingResponse { - pub value: Option, -} - -#[derive(Debug, Clone, Eq, Deserialize, Serialize)] pub struct BleDevice { pub address: String, pub name: String, pub is_connected: bool, + pub manufacturer_data: HashMap>, + pub services: Vec, } +impl Eq for BleDevice {} + impl PartialOrd for BleDevice { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) @@ -41,26 +38,104 @@ impl PartialEq for BleDevice { } impl BleDevice { - pub async fn from_peripheral( + pub(crate) async fn from_peripheral( peripheral: &P, ) -> Result { #[cfg(target_vendor = "apple")] let address = peripheral.id().to_string(); #[cfg(not(target_vendor = "apple"))] let address = peripheral.address().to_string(); + let properties = peripheral.properties().await?.unwrap_or_default(); + let name = properties + .local_name + .unwrap_or_else(|| peripheral.id().to_string()); Ok(Self { address, - name: peripheral - .properties() - .await? - .unwrap_or_default() - .local_name - .ok_or(error::Error::UnknownPeripheral(peripheral.id().to_string()))?, + name, + manufacturer_data: properties.manufacturer_data, + services: properties.services, is_connected: peripheral.is_connected().await?, }) } } +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Service { + pub uuid: Uuid, + pub characteristics: Vec, +} + +impl From<&btleplug::api::Service> for Service { + fn from(service: &btleplug::api::Service) -> Self { + Self { + uuid: service.uuid, + characteristics: service + .characteristics + .iter() + .map(Characteristic::from) + .collect(), + } + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Characteristic { + pub uuid: Uuid, + pub descriptors: Vec, + pub properties: BitFlags, +} + +impl From<&btleplug::api::Characteristic> for Characteristic { + fn from(characteristic: &btleplug::api::Characteristic) -> Self { + Self { + uuid: characteristic.uuid, + descriptors: characteristic.descriptors.iter().map(|d| d.uuid).collect(), + properties: get_flags(characteristic.properties), + } + } +} + +#[derive(Debug, Clone, Copy, Deserialize, Serialize)] +#[enumflags2::bitflags] +#[repr(u8)] +pub enum CharProps { + Broadcast, + Read, + WriteWithoutResponse, + Write, + Notify, + Indicate, + AuthenticatedSignedWrites, + ExtendedProperties, +} + +impl From for CharProps { + fn from(flag: btleplug::api::CharPropFlags) -> Self { + match flag { + btleplug::api::CharPropFlags::BROADCAST => CharProps::Broadcast, + btleplug::api::CharPropFlags::READ => CharProps::Read, + btleplug::api::CharPropFlags::WRITE_WITHOUT_RESPONSE => CharProps::WriteWithoutResponse, + btleplug::api::CharPropFlags::WRITE => CharProps::Write, + btleplug::api::CharPropFlags::NOTIFY => CharProps::Notify, + btleplug::api::CharPropFlags::INDICATE => CharProps::Indicate, + btleplug::api::CharPropFlags::AUTHENTICATED_SIGNED_WRITES => { + CharProps::AuthenticatedSignedWrites + } + btleplug::api::CharPropFlags::EXTENDED_PROPERTIES => CharProps::ExtendedProperties, + _ => unreachable!(), + } + } +} + +fn get_flags(properties: btleplug::api::CharPropFlags) -> BitFlags { + let mut flags = BitFlags::empty(); + for flag in properties.iter() { + flags |= CharProps::from(flag); + } + flags +} + +#[must_use] pub fn fmt_addr(addr: BDAddr) -> String { let a = addr.into_inner(); format!( @@ -68,3 +143,35 @@ pub fn fmt_addr(addr: BDAddr) -> String { a[0], a[1], a[2], a[3], a[4], a[5] ) } + +#[derive(Clone, Copy, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum WriteType { + /// aka request. + WithResponse, + /// aka command. + WithoutResponse, +} + +impl From for btleplug::api::WriteType { + fn from(write_type: WriteType) -> Self { + match write_type { + WriteType::WithResponse => btleplug::api::WriteType::WithResponse, + WriteType::WithoutResponse => btleplug::api::WriteType::WithoutResponse, + } + } +} + +/// Filter for discovering devices. +/// Only devices matching the filter will be returned by the handler::discover method +pub enum ScanFilter { + None, + /// Matches if the device advertises the specified service. + Service(Uuid), + /// Matches if the device advertises any of the specified services. + AnyService(Vec), + /// Matches if the device advertises all of the specified services. + AllServices(Vec), + /// Matches if the device advertises the specified manufacturer data. + ManufacturerData(u16, Vec), +}