diff --git a/README.md b/README.md index fce1471..1753ba4 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,8 @@ Below is an index of all the methods available. - [`read(...)`](#read) - [`write(...)`](#write) - [`writeWithoutResponse(...)`](#writewithoutresponse) +- [`readDescriptor(...)`](#readdescriptor) +- [`writeDescriptor(...)`](#writedescriptor) - [`startNotifications(...)`](#startnotifications) - [`stopNotifications(...)`](#stopnotifications) - [Interfaces](#interfaces) @@ -299,6 +301,8 @@ _Note_: web support depends on the browser, see [implementation status](https:// | [`readRssi(...)`](#readrssi) | ✅ | ✅ | ❌ | | [`read(...)`](#read) | ✅ | ✅ | ✅ | | [`write(...)`](#write) | ✅ | ✅ | ✅ | +| [`readDescriptor(...)`](#read) | ✅ | ✅ | ✅ | +| [`writeDescriptor(...)`](#write) | ✅ | ✅ | ✅ | | [`writeWithoutResponse(...)`](#writewithoutresponse) | ✅ | ✅ | ✅ | | [`startNotifications(...)`](#startnotifications) | ✅ | ✅ | ✅ | | [`stopNotifications(...)`](#stopnotifications) | ✅ | ✅ | ✅ | @@ -678,6 +682,43 @@ Write a value to a characteristic without waiting for a response. --- +### readDescriptor(...) + +```typescript +readDescriptor(deviceId: string, service: string, characteristic: string, descriptor: string) => Promise +``` + +Read the value of a descriptor. + +| Param | Type | Description | +| -------------------- | ------------------- | -------------------------------------------------------------------------------------------------------------- | +| **`deviceId`** | string | The ID of the device to use (obtained from [requestDevice](#requestDevice) or [requestLEScan](#requestLEScan)) | +| **`service`** | string | UUID of the service (see [UUID format](#uuid-format)) | +| **`characteristic`** | string | UUID of the characteristic (see [UUID format](#uuid-format)) | +| **`descriptor`** | string | UUID of the descriptor (see [UUID format](#uuid-format)) | + +**Returns:** Promise<DataView> + +--- + +### writeDescriptor(...) + +```typescript +writeDescriptor(deviceId: string, service: string, characteristic: string, descriptor: string, value: DataView) => Promise +``` + +Write a value to a descriptor. + +| Param | Type | Description | +| -------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **`deviceId`** | string | The ID of the device to use (obtained from [requestDevice](#requestDevice) or [requestLEScan](#requestLEScan)) | +| **`service`** | string | UUID of the service (see [UUID format](#uuid-format)) | +| **`characteristic`** | string | UUID of the characteristic (see [UUID format](#uuid-format)) | +| **`descriptor`** | string | UUID of the descriptor (see [UUID format](#uuid-format)) | +| **`value`** | DataView | The value to write as a DataView. To create a DataView from an array of numbers, there is a helper function, e.g. numbersToDataView([1, 0]) | + +--- + ### startNotifications(...) ```typescript @@ -805,10 +846,11 @@ buffer as needed. #### BleCharacteristic -| Prop | Type | -| ---------------- | ----------------------------------------------------------------------------------- | -| **`uuid`** | string | -| **`properties`** | BleCharacteristicProperties | +| Prop | Type | +| ----------------- | ----------------------------------------------------------------------------------- | +| **`uuid`** | string | +| **`properties`** | BleCharacteristicProperties | +| **`descriptors`** | BleDescriptor[] | #### BleCharacteristicProperties @@ -827,6 +869,12 @@ buffer as needed. | **`notifyEncryptionRequired`** | boolean | | **`indicateEncryptionRequired`** | boolean | +#### BleDescriptor + +| Prop | Type | +| ---------- | ------------------- | +| **`uuid`** | string | + ### Enums #### ScanMode diff --git a/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/BluetoothLe.kt b/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/BluetoothLe.kt index 29c6e95..f717ae2 100644 --- a/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/BluetoothLe.kt +++ b/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/BluetoothLe.kt @@ -407,6 +407,13 @@ class BluetoothLe : Plugin() { val bleCharacteristic = JSObject() bleCharacteristic.put("uuid", characteristic.uuid) bleCharacteristic.put("properties", getProperties(characteristic)) + var bleDescriptors = JSArray() + characteristic.descriptors.forEach { descriptor -> + val bleDescriptor = JSObject() + bleDescriptor.put("uuid", descriptor.uuid) + bleDescriptors.put(bleDescriptor) + } + bleCharacteristic.put("descriptors", bleDescriptors) bleCharacteristics.put(bleCharacteristic) } val bleService = JSObject() @@ -531,6 +538,48 @@ class BluetoothLe : Plugin() { } } + @PluginMethod + fun readDescriptor(call: PluginCall) { + val device = getDevice(call) ?: return + val descriptor = getDescriptor(call) ?: return + device.readDescriptor(descriptor.first, descriptor.second, descriptor.third) { response -> + run { + if (response.success) { + val ret = JSObject() + ret.put("value", response.value) + call.resolve(ret) + } else { + call.reject(response.value) + } + } + } + } + + @PluginMethod + fun writeDescriptor(call: PluginCall) { + val device = getDevice(call) ?: return + val descriptor = getDescriptor(call) ?: return + val value = call.getString("value", null) + if (value == null) { + call.reject("Value required.") + return + } + device.writeDescriptor( + descriptor.first, + descriptor.second, + descriptor.third, + value + ) { response -> + run { + if (response.success) { + call.resolve() + } else { + call.reject(response.value) + } + } + } + } + @PluginMethod fun startNotifications(call: PluginCall) { val device = getDevice(call) ?: return @@ -784,4 +833,21 @@ class BluetoothLe : Plugin() { } return Pair(serviceUUID, characteristicUUID) } + + private fun getDescriptor(call: PluginCall): Triple? { + val characteristic = getCharacteristic(call) ?: return null + val descriptorString = call.getString("descriptor", null) + val descriptorUUID: UUID? + try { + descriptorUUID = UUID.fromString(descriptorString) + } catch (e: IllegalAccessException) { + call.reject("Invalid descriptor UUID.") + return null + } + if (descriptorUUID == null) { + call.reject("Descriptor UUID required.") + return null + } + return Triple(characteristic.first, characteristic.second, descriptorUUID) + } } diff --git a/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/Device.kt b/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/Device.kt index 8d8d5dd..97bf8ea 100644 --- a/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/Device.kt +++ b/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/Device.kt @@ -39,7 +39,6 @@ class Device( private var bluetoothGatt: BluetoothGatt? = null private var callbackMap = HashMap Unit)>() private var timeoutMap = HashMap() - private var setNotificationsKey = "" private var bondStateReceiver: BroadcastReceiver? = null private val gattCallback: BluetoothGattCallback = object : BluetoothGattCallback() { @@ -144,19 +143,40 @@ class Device( } } + override fun onDescriptorRead( + gatt: BluetoothGatt, + descriptor: BluetoothGattDescriptor, + status: Int + ) { + super.onDescriptorRead(gatt, descriptor, status) + val key = + "readDescriptor|${descriptor.characteristic.service.uuid}|${descriptor.characteristic.uuid}|${descriptor.uuid}" + if (status == BluetoothGatt.GATT_SUCCESS) { + val data = descriptor.value + if (data != null && data.isNotEmpty()) { + val value = bytesToString(data) + resolve(key, value) + } else { + reject(key, "No data received while reading descriptor.") + } + } else { + reject(key, "Reading descriptor failed.") + } + } + override fun onDescriptorWrite( gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int ) { super.onDescriptorWrite(gatt, descriptor, status) + val key = + "writeDescriptor|${descriptor.characteristic.service.uuid}|${descriptor.characteristic.uuid}|${descriptor.uuid}" if (status == BluetoothGatt.GATT_SUCCESS) { - resolve(setNotificationsKey, "Setting notification succeeded.") + resolve(key, "Descriptor successfully written.") } else { - reject(setNotificationsKey, "Setting notification failed.") + reject(key, "Writing descriptor failed.") } - setNotificationsKey = "" - } } @@ -343,8 +363,7 @@ class Device( notifyCallback: ((CallbackResponse) -> Unit)?, callback: (CallbackResponse) -> Unit, ) { - val key = "setNotifications|$serviceUUID|$characteristicUUID" - setNotificationsKey = key + val key = "writeDescriptor|$serviceUUID|$characteristicUUID|$CLIENT_CHARACTERISTIC_CONFIG" val notifyKey = "notification|$serviceUUID|$characteristicUUID" callbackMap[key] = callback if (notifyCallback != null) { @@ -387,6 +406,67 @@ class Device( // wait for onDescriptorWrite } + fun readDescriptor( + serviceUUID: UUID, + characteristicUUID: UUID, + descriptorUUID: UUID, + callback: (CallbackResponse) -> Unit + ) { + val key = "readDescriptor|$serviceUUID|$characteristicUUID|$descriptorUUID" + callbackMap[key] = callback + val service = bluetoothGatt?.getService(serviceUUID) + val characteristic = service?.getCharacteristic(characteristicUUID) + if (characteristic == null) { + reject(key, "Characteristic not found.") + return + } + val descriptor = characteristic?.getDescriptor(descriptorUUID) + if (descriptor == null) { + reject(key, "Descriptor not found.") + return + } + val result = bluetoothGatt?.readDescriptor(descriptor) + if (result != true) { + reject(key, "Reading descriptor failed.") + return + } + setTimeout(key, "Read descriptor timeout.") + } + + fun writeDescriptor( + serviceUUID: UUID, + characteristicUUID: UUID, + descriptorUUID: UUID, + value: String, + callback: (CallbackResponse) -> Unit + ) { + val key = "writeDescriptor|$serviceUUID|$characteristicUUID|$descriptorUUID" + callbackMap[key] = callback + val service = bluetoothGatt?.getService(serviceUUID) + val characteristic = service?.getCharacteristic(characteristicUUID) + if (characteristic == null) { + reject(key, "Characteristic not found.") + return + } + val descriptor = characteristic?.getDescriptor(descriptorUUID) + if (descriptor == null) { + reject(key, "Descriptor not found.") + return + } + if (value == "") { + reject(key, "Invalid data.") + return + } + val bytes = stringToBytes(value) + descriptor.value = bytes + val result = bluetoothGatt?.writeDescriptor(descriptor) + if (result != true) { + reject(key, "Writing characteristic failed.") + return + } + setTimeout(key, "Write timeout.") + } + private fun resolve(key: String, value: String) { if (callbackMap.containsKey(key)) { Log.d(TAG, "resolve: $key $value") diff --git a/ios/Plugin/Conversion.swift b/ios/Plugin/Conversion.swift index ecf7a40..4930da3 100644 --- a/ios/Plugin/Conversion.swift +++ b/ios/Plugin/Conversion.swift @@ -1,6 +1,19 @@ import Foundation import CoreBluetooth +func descriptorValueToString(_ value: Any) -> String { + if let str = value as? String { + return str + } + if let data = value as? Data { + return dataToString(data) + } + if let uuid = value as? CBUUID { + return uuid.uuidString + } + return "" +} + func dataToString(_ data: Data) -> String { var valueString = "" for byte in data { diff --git a/ios/Plugin/Device.swift b/ios/Plugin/Device.swift index ae1abaa..c18fb3a 100644 --- a/ios/Plugin/Device.swift +++ b/ios/Plugin/Device.swift @@ -9,6 +9,8 @@ class Device: NSObject, CBPeripheralDelegate { private var timeoutMap = [String: DispatchWorkItem]() private var servicesCount = 0 private var servicesDiscovered = 0 + private var characteristicsCount = 0 + private var characteristicsDiscovered = 0 init(_ peripheral: CBPeripheral) { super.init() @@ -46,7 +48,9 @@ class Device: NSObject, CBPeripheralDelegate { } self.servicesCount = peripheral.services?.count ?? 0 self.servicesDiscovered = 0 - for service in peripheral.services! { + self.characteristicsCount = 0 + self.characteristicsDiscovered = 0 + for service in peripheral.services ?? [] { peripheral.discoverCharacteristics(nil, for: service) } } @@ -54,7 +58,19 @@ class Device: NSObject, CBPeripheralDelegate { func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { self.servicesDiscovered += 1 print("didDiscoverCharacteristicsFor", self.servicesDiscovered, self.servicesCount) - if self.servicesDiscovered >= self.servicesCount { + self.characteristicsCount += service.characteristics?.count ?? 0 + for characteristic in service.characteristics ?? [] { + peripheral.discoverDescriptors(for: characteristic) + } + // if the last service does not have characteristics, resolve the connect call now + if self.servicesDiscovered >= self.servicesCount && self.characteristicsDiscovered >= self.characteristicsCount { + self.resolve("connect", "Connection successful.") + } + } + + func peripheral(_ peripheral: CBPeripheral, didDiscoverDescriptorsFor characteristic: CBCharacteristic, error: Error?) { + self.characteristicsDiscovered += 1 + if self.servicesDiscovered >= self.servicesCount && self.characteristicsDiscovered >= self.characteristicsCount { self.resolve("connect", "Connection successful.") } } @@ -93,6 +109,18 @@ class Device: NSObject, CBPeripheralDelegate { return nil } + private func getDescriptor(_ serviceUUID: CBUUID, _ characteristicUUID: CBUUID, _ descriptorUUID: CBUUID) -> CBDescriptor? { + guard let characteristic = self.getCharacteristic(serviceUUID, characteristicUUID) else { + return nil + } + for descriptor in characteristic.descriptors ?? [] { + if descriptor.uuid == descriptorUUID { + return descriptor + } + } + return nil + } + func read(_ serviceUUID: CBUUID, _ characteristicUUID: CBUUID, _ callback: @escaping Callback) { let key = "read|\(serviceUUID.uuidString)|\(characteristicUUID.uuidString)" self.callbackMap[key] = callback @@ -127,6 +155,32 @@ class Device: NSObject, CBPeripheralDelegate { } } + func readDescriptor(_ serviceUUID: CBUUID, _ characteristicUUID: CBUUID, _ descriptorUUID: CBUUID, _ callback: @escaping Callback) { + let key = "readDescriptor|\(serviceUUID.uuidString)|\(characteristicUUID.uuidString)|\(descriptorUUID.uuidString)" + self.callbackMap[key] = callback + guard let descriptor = self.getDescriptor(serviceUUID, characteristicUUID, descriptorUUID) else { + self.reject(key, "Descriptor not found.") + return + } + print("Reading descriptor value") + self.peripheral.readValue(for: descriptor) + self.setTimeout(key, "Read descriptor timeout.") + } + + func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor descriptor: CBDescriptor, error: Error?) { + let key = self.getKey("readDescriptor", descriptor) + if error != nil { + self.reject(key, error!.localizedDescription) + return + } + if descriptor.value == nil { + self.reject(key, "Descriptor contains no value.") + return + } + let valueString = descriptorValueToString(descriptor.value!) + self.resolve(key, valueString) + } + func write(_ serviceUUID: CBUUID, _ characteristicUUID: CBUUID, _ value: String, _ writeType: CBCharacteristicWriteType, _ callback: @escaping Callback) { let key = "write|\(serviceUUID.uuidString)|\(characteristicUUID.uuidString)" self.callbackMap[key] = callback @@ -156,6 +210,31 @@ class Device: NSObject, CBPeripheralDelegate { self.resolve(key, "Successfully written value.") } + func writeDescriptor(_ serviceUUID: CBUUID, _ characteristicUUID: CBUUID, _ descriptorUUID: CBUUID, _ value: String, _ callback: @escaping Callback) { + let key = "writeDescriptor|\(serviceUUID.uuidString)|\(characteristicUUID.uuidString)|\(descriptorUUID.uuidString)" + self.callbackMap[key] = callback + guard let descriptor = self.getDescriptor(serviceUUID, characteristicUUID, descriptorUUID) else { + self.reject(key, "Descriptor not found.") + return + } + if value == "" { + self.reject(key, "Invalid data.") + return + } + let data: Data = stringToData(value) + self.peripheral.writeValue(data, for: descriptor) + self.setTimeout(key, "Write descriptor timeout.") + } + + func peripheral(_ peripheral: CBPeripheral, didWriteValueFor descriptor: CBDescriptor, error: Error?) { + let key = self.getKey("writeDescriptor", descriptor) + if error != nil { + self.reject(key, error!.localizedDescription) + return + } + self.resolve(key, "Successfully written descriptor value.") + } + func setNotifications( _ serviceUUID: CBUUID, _ characteristicUUID: CBUUID, @@ -187,18 +266,29 @@ class Device: NSObject, CBPeripheralDelegate { self.resolve(key, "Successfully set notifications.") } - private func getKey(_ prefix: String, _ characteristic: CBCharacteristic) -> String { + private func getKey(_ prefix: String, _ characteristic: CBCharacteristic?) -> String { let serviceUUIDString: String - let service: CBService? = characteristic.service + let service: CBService? = characteristic?.service if service != nil { serviceUUIDString = cbuuidToStringUppercase(service!.uuid) } else { serviceUUIDString = "UNKNOWN-SERVICE" } - let characteristicUUIDString = cbuuidToStringUppercase(characteristic.uuid) + let characteristicUUIDString: String + if characteristic != nil { + characteristicUUIDString = cbuuidToStringUppercase(characteristic!.uuid) + } else { + characteristicUUIDString = "UNKNOWN-CHARACTERISTIC" + } return "\(prefix)|\(serviceUUIDString)|\(characteristicUUIDString)" } + private func getKey(_ prefix: String, _ descriptor: CBDescriptor) -> String { + let baseKey = self.getKey(prefix, descriptor.characteristic) + let characteristicUUIDString = cbuuidToStringUppercase(descriptor.uuid) + return "\(baseKey)|\(characteristicUUIDString)" + } + private func resolve(_ key: String, _ value: String) { let callback = self.callbackMap[key] if callback != nil { diff --git a/ios/Plugin/Plugin.m b/ios/Plugin/Plugin.m index fac95fd..309afaa 100644 --- a/ios/Plugin/Plugin.m +++ b/ios/Plugin/Plugin.m @@ -29,6 +29,8 @@ CAP_PLUGIN_METHOD(read, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(write, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(writeWithoutResponse, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(readDescriptor, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(writeDescriptor, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(startNotifications, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(stopNotifications, CAPPluginReturnPromise); ) diff --git a/ios/Plugin/Plugin.swift b/ios/Plugin/Plugin.swift index 0ef6353..7ab0c8a 100644 --- a/ios/Plugin/Plugin.swift +++ b/ios/Plugin/Plugin.swift @@ -2,11 +2,13 @@ import Foundation import Capacitor import CoreBluetooth +//swiftlint:disable type_body_length @objc(BluetoothLe) public class BluetoothLe: CAPPlugin { typealias BleDevice = [String: Any] typealias BleService = [String: Any] typealias BleCharacteristic = [String: Any] + typealias BleDescriptor = [String: Any] private var deviceManager: DeviceManager? private var deviceMap = [String: Device]() private var displayStrings = [String: String]() @@ -247,9 +249,16 @@ public class BluetoothLe: CAPPlugin { for service in services { var bleCharacteristics = [BleCharacteristic]() for characteristic in service.characteristics ?? [] { + var bleDescriptors = [BleDescriptor]() + for descriptor in characteristic.descriptors ?? [] { + bleDescriptors.append([ + "uuid": cbuuidToString(descriptor.uuid) + ]) + } bleCharacteristics.append([ "uuid": cbuuidToString(characteristic.uuid), - "properties": getProperties(characteristic) + "properties": getProperties(characteristic), + "descriptors": bleDescriptors ]) } bleServices.append([ @@ -340,6 +349,38 @@ public class BluetoothLe: CAPPlugin { }) } + @objc func readDescriptor(_ call: CAPPluginCall) { + guard self.getDeviceManager(call) != nil else { return } + guard let device = self.getDevice(call) else { return } + guard let descriptor = self.getDescriptor(call) else { return } + device.readDescriptor(descriptor.0, descriptor.1, descriptor.2, {(success, value) -> Void in + if success { + call.resolve([ + "value": value + ]) + } else { + call.reject(value) + } + }) + } + + @objc func writeDescriptor(_ call: CAPPluginCall) { + guard self.getDeviceManager(call) != nil else { return } + guard let device = self.getDevice(call) else { return } + guard let descriptor = self.getDescriptor(call) else { return } + guard let value = call.getString("value") else { + call.reject("value must be provided") + return + } + device.writeDescriptor(descriptor.0, descriptor.1, descriptor.2, value, {(success, value) -> Void in + if success { + call.resolve() + } else { + call.reject(value) + } + }) + } + @objc func startNotifications(_ call: CAPPluginCall) { guard self.getDeviceManager(call) != nil else { return } guard let device = self.getDevice(call) else { return } @@ -435,6 +476,19 @@ public class BluetoothLe: CAPPlugin { return (serviceUUID, characteristicUUID) } + private func getDescriptor(_ call: CAPPluginCall) -> (CBUUID, CBUUID, CBUUID)? { + guard let characteristic = getCharacteristic(call) else { + return nil + } + guard let descriptor = call.getString("descriptor") else { + call.reject("Descriptor UUID required.") + return nil + } + let descriptorUUID = CBUUID(string: descriptor) + + return (characteristic.0, characteristic.1, descriptorUUID) + } + private func getBleDevice(_ device: Device) -> BleDevice { var bleDevice = [ "deviceId": device.getId() diff --git a/src/bleClient.ts b/src/bleClient.ts index a02a3ee..717e18f 100644 --- a/src/bleClient.ts +++ b/src/bleClient.ts @@ -192,6 +192,31 @@ export interface BleClientInterface { */ writeWithoutResponse(deviceId: string, service: string, characteristic: string, value: DataView): Promise; + /** + * Read the value of a descriptor. + * @param deviceId The ID of the device to use (obtained from [requestDevice](#requestDevice) or [requestLEScan](#requestLEScan)) + * @param service UUID of the service (see [UUID format](#uuid-format)) + * @param characteristic UUID of the characteristic (see [UUID format](#uuid-format)) + * @param descriptor UUID of the descriptor (see [UUID format](#uuid-format)) + */ + readDescriptor(deviceId: string, service: string, characteristic: string, descriptor: string): Promise; + + /** + * Write a value to a descriptor. + * @param deviceId The ID of the device to use (obtained from [requestDevice](#requestDevice) or [requestLEScan](#requestLEScan)) + * @param service UUID of the service (see [UUID format](#uuid-format)) + * @param characteristic UUID of the characteristic (see [UUID format](#uuid-format)) + * @param descriptor UUID of the descriptor (see [UUID format](#uuid-format)) + * @param value The value to write as a DataView. To create a DataView from an array of numbers, there is a helper function, e.g. numbersToDataView([1, 0]) + */ + writeDescriptor( + deviceId: string, + service: string, + characteristic: string, + descriptor: string, + value: DataView + ): Promise; + /** * Start listening to changes of the value of a characteristic. For an example, see [usage](#usage). * @param deviceId The ID of the device to use (obtained from [requestDevice](#requestDevice) or [requestLEScan](#requestLEScan)) @@ -475,6 +500,56 @@ class BleClientClass implements BleClientInterface { }); } + async readDescriptor( + deviceId: string, + service: string, + characteristic: string, + descriptor: string + ): Promise { + service = validateUUID(service); + characteristic = validateUUID(characteristic); + descriptor = validateUUID(descriptor); + const value = await this.queue(async () => { + const result = await BluetoothLe.readDescriptor({ + deviceId, + service, + characteristic, + descriptor, + }); + return this.convertValue(result.value); + }); + return value; + } + + async writeDescriptor( + deviceId: string, + service: string, + characteristic: string, + descriptor: string, + value: DataView + ): Promise { + service = validateUUID(service); + characteristic = validateUUID(characteristic); + descriptor = validateUUID(descriptor); + return this.queue(async () => { + if (!value?.buffer) { + throw new Error('Invalid data.'); + } + let writeValue: DataView | string = value; + if (Capacitor.getPlatform() !== 'web') { + // on native we can only write strings + writeValue = dataViewToHexString(value); + } + await BluetoothLe.writeDescriptor({ + deviceId, + service, + characteristic, + descriptor, + value: writeValue, + }); + }); + } + async startNotifications( deviceId: string, service: string, diff --git a/src/definitions.ts b/src/definitions.ts index af4fc8c..af4b5c1 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -88,9 +88,14 @@ export interface BleService { readonly characteristics: BleCharacteristic[]; } +export interface BleDescriptor { + readonly uuid: string; +} + export interface BleCharacteristic { readonly uuid: string; readonly properties: BleCharacteristicProperties; + readonly descriptors: BleDescriptor[]; } export interface BleCharacteristicProperties { @@ -118,6 +123,13 @@ export interface ReadOptions { characteristic: string; } +export interface ReadDescriptorOptions { + deviceId: string; + service: string; + characteristic: string; + descriptor: string; +} + export type Data = DataView | string; export interface WriteOptions { @@ -131,6 +143,18 @@ export interface WriteOptions { value: Data; } +export interface WriteDescriptorOptions { + deviceId: string; + service: string; + characteristic: string; + descriptor: string; + /** + * android, ios: string + * web: DataView + */ + value: Data; +} + export interface BooleanResult { value: boolean; } @@ -228,6 +252,8 @@ export interface BluetoothLePlugin { read(options: ReadOptions): Promise; write(options: WriteOptions): Promise; writeWithoutResponse(options: WriteOptions): Promise; + readDescriptor(options: ReadDescriptorOptions): Promise; + writeDescriptor(options: WriteDescriptorOptions): Promise; startNotifications(options: ReadOptions): Promise; stopNotifications(options: ReadOptions): Promise; } diff --git a/src/web.ts b/src/web.ts index fa730cd..0d27e8d 100644 --- a/src/web.ts +++ b/src/web.ts @@ -1,6 +1,6 @@ import { WebPlugin } from '@capacitor/core'; -import type { BleCharacteristic, BleCharacteristicProperties, BleService } from '.'; +import type { BleCharacteristic, BleCharacteristicProperties, BleDescriptor, BleService } from '.'; import { hexStringToDataView, mapToObject, webUUIDToString } from './conversion'; import type { BleDevice, @@ -11,12 +11,14 @@ import type { GetConnectedDevicesOptions, GetDevicesOptions, GetDevicesResult, + ReadDescriptorOptions, ReadOptions, ReadResult, ReadRssiResult, RequestBleDeviceOptions, ScanResultInternal, WriteOptions, + WriteDescriptorOptions, } from './definitions'; import { runWithTimeout } from './timeout'; @@ -202,17 +204,30 @@ export class BluetoothLeWeb extends WebPlugin implements BluetoothLePlugin { const bleServices: BleService[] = []; for (const service of services) { const characteristics = await service.getCharacteristics(); - const bleCharacteristics: BleCharacteristic[] = characteristics.map((characteristic) => { - return { + const bleCharacteristics: BleCharacteristic[] = []; + for (const characteristic of characteristics) { + bleCharacteristics.push({ uuid: characteristic.uuid, properties: this.getProperties(characteristic), - }; - }); + descriptors: await this.getDescriptors(characteristic), + }); + } bleServices.push({ uuid: service.uuid, characteristics: bleCharacteristics }); } return { services: bleServices }; } + private async getDescriptors(characteristic: BluetoothRemoteGATTCharacteristic): Promise { + try { + const descriptors = await characteristic.getDescriptors(); + return descriptors.map((descriptor) => ({ + uuid: descriptor.uuid, + })); + } catch { + return []; + } + } + private getProperties(characteristic: BluetoothRemoteGATTCharacteristic): BleCharacteristicProperties { return { broadcast: characteristic.properties.broadcast, @@ -234,6 +249,13 @@ export class BluetoothLeWeb extends WebPlugin implements BluetoothLePlugin { return service?.getCharacteristic(options?.characteristic); } + private async getDescriptor( + options: ReadDescriptorOptions | WriteDescriptorOptions + ): Promise { + const characteristic = await this.getCharacteristic(options); + return characteristic?.getDescriptor(options?.descriptor); + } + async readRssi(_options: DeviceIdOptions): Promise { throw this.unavailable('readRssi is not available on web.'); } @@ -266,6 +288,23 @@ export class BluetoothLeWeb extends WebPlugin implements BluetoothLePlugin { await characteristic?.writeValueWithoutResponse(dataView); } + async readDescriptor(options: ReadDescriptorOptions): Promise { + const descriptor = await this.getDescriptor(options); + const value = await descriptor?.readValue(); + return { value }; + } + + async writeDescriptor(options: WriteDescriptorOptions): Promise { + const descriptor = await this.getDescriptor(options); + let dataView: DataView; + if (typeof options.value === 'string') { + dataView = hexStringToDataView(options.value); + } else { + dataView = options.value; + } + await descriptor?.writeValue(dataView); + } + async startNotifications(options: ReadOptions): Promise { const characteristic = await this.getCharacteristic(options); characteristic?.removeEventListener('characteristicvaluechanged', this.onCharacteristicValueChanged);