diff --git a/lib/Javascript/src/BluetoothRemoteGATTCharacteristic.ts b/lib/Javascript/src/BluetoothRemoteGATTCharacteristic.ts index e55a4c15..09127fea 100644 --- a/lib/Javascript/src/BluetoothRemoteGATTCharacteristic.ts +++ b/lib/Javascript/src/BluetoothRemoteGATTCharacteristic.ts @@ -6,6 +6,7 @@ import { bluetoothRequest } from "./WebKit"; import { BluetoothUUID } from "./BluetoothUUID"; import { copyOf } from "./Data"; import { EmptyObject } from "./EmptyObject"; +import { isView } from "./Data"; import { store } from "./Store"; type DiscoverDescriptorsRequest = { @@ -171,5 +172,3 @@ export class BluetoothRemoteGATTCharacteristic extends EventTarget { return this._writeValue(value, false) } } - -const isView = (source: ArrayBuffer | ArrayBufferView): source is ArrayBufferView => (source as ArrayBufferView).buffer !== undefined; diff --git a/lib/Javascript/src/BluetoothRemoteGATTDescriptor.ts b/lib/Javascript/src/BluetoothRemoteGATTDescriptor.ts index aab8f010..8fa87f99 100644 --- a/lib/Javascript/src/BluetoothRemoteGATTDescriptor.ts +++ b/lib/Javascript/src/BluetoothRemoteGATTDescriptor.ts @@ -1,7 +1,10 @@ +import { arrayBufferToBase64 } from "./Data"; import { base64ToDataView } from "./Data"; import { BluetoothRemoteGATTCharacteristic } from "./BluetoothRemoteGATTCharacteristic"; import { bluetoothRequest } from "./WebKit"; import { copyOf } from "./Data"; +import { EmptyObject } from "./EmptyObject"; +import { isView } from "./Data"; type ReadDescriptorRequest = { device: string; @@ -11,6 +14,15 @@ type ReadDescriptorRequest = { descriptor: string; } +type WriteDescriptorRequest = { + device: string; + service: string; + characteristic: string; + instance: number; + descriptor: string, + value: string; +} + // https://developer.mozilla.org/en-US/docs/Web/API/BluetoothRemoteGATTDescriptor export class BluetoothRemoteGATTDescriptor { // https://developer.mozilla.org/en-US/docs/Web/API/BluetoothRemoteGATTDescriptor/value @@ -38,4 +50,20 @@ export class BluetoothRemoteGATTDescriptor { this.value = base64ToDataView(response) return copyOf(this.value) } + + writeValue = async (value: ArrayBuffer | ArrayBufferView): Promise => { + const arrayBuffer = isView(value) ? value.buffer : value; + const base64 = arrayBufferToBase64(arrayBuffer) + await bluetoothRequest( + 'writeDescriptor', + { + device: this.characteristic.service.device.id, + service: this.characteristic.service.uuid, + characteristic: this.characteristic.uuid, + instance: this.characteristic.instance, + descriptor: this.uuid, + value: base64 + } + ) + } } diff --git a/lib/Javascript/src/Data.ts b/lib/Javascript/src/Data.ts index a7585666..869f7404 100644 --- a/lib/Javascript/src/Data.ts +++ b/lib/Javascript/src/Data.ts @@ -22,3 +22,5 @@ export const base64ToDataView = (base64: string): DataView => { export function copyOf(data: DataView): DataView { return new DataView(data.buffer.slice(0)) } + +export const isView = (source: ArrayBuffer | ArrayBufferView): source is ArrayBufferView => (source as ArrayBufferView).buffer !== undefined; diff --git a/lib/Sources/BluetoothAction/Message+Action.swift b/lib/Sources/BluetoothAction/Message+Action.swift index 0de44da4..3e9c2ccc 100644 --- a/lib/Sources/BluetoothAction/Message+Action.swift +++ b/lib/Sources/BluetoothAction/Message+Action.swift @@ -29,6 +29,8 @@ extension Message { return WriteCharacteristic.create(from: self) case .readDescriptor: return ReadDescriptor.create(from: self) + case .writeDescriptor: + return WriteDescriptor.create(from: self) case .startNotifications: return StartNotifications.create(from: self) case .stopNotifications: diff --git a/lib/Sources/BluetoothAction/WriteDescriptor.swift b/lib/Sources/BluetoothAction/WriteDescriptor.swift new file mode 100644 index 00000000..ee6a7096 --- /dev/null +++ b/lib/Sources/BluetoothAction/WriteDescriptor.swift @@ -0,0 +1,68 @@ +import Bluetooth +import BluetoothClient +import BluetoothMessage +import Foundation +import JsMessage + +struct WriteDescriptorRequest: JsMessageDecodable { + let peripheralId: UUID + let serviceUuid: UUID + let characteristicUuid: UUID + let characteristicInstance: UInt32 + let descriptorUuid: UUID + let value: Data + + static func decode(from data: [String: JsType]?) -> Self? { + guard let device = data?["device"]?.string.flatMap(UUID.init(uuidString:)) else { + return nil + } + guard let service = data?["service"]?.string.flatMap(UUID.init(uuidString:)) else { + return nil + } + guard let characteristic = data?["characteristic"]?.string.flatMap(UUID.init(uuidString:)) else { + return nil + } + guard let instance = data?["instance"]?.number?.uint32Value else { + return nil + } + guard let descriptor = data?["descriptor"]?.string.flatMap(UUID.init(uuidString:)) else { + return nil + } + guard let value = data?["value"]?.data else { + return nil + } + return .init( + peripheralId: device, + serviceUuid: service, + characteristicUuid: characteristic, + characteristicInstance: instance, + descriptorUuid: descriptor, + value: value + ) + } +} + +// Response is unused on JavaScript side but needed to have `ReadDescriptor` conform to `BluetoothAction`. +struct DescriptorResponse: JsMessageEncodable { + func toJsMessage() -> JsMessage.JsMessageResponse { + .body([:]) + } +} + +struct WriteDescriptor: BluetoothAction { + let requiresReadyState: Bool = true + let request: WriteDescriptorRequest + + func execute(state: BluetoothState, client: BluetoothClient) async throws -> DescriptorResponse { + let peripheral = try await state.getConnectedPeripheral(request.peripheralId) + let characteristic = try await state.getCharacteristic( + peripheralId: request.peripheralId, + serviceId: request.serviceUuid, + characteristicId: request.characteristicUuid, + instance: request.characteristicInstance + ) + let descriptor = try characteristic.getDescriptor(request.descriptorUuid) + _ = try await client.descriptorWrite(peripheral, characteristic: characteristic, descriptor: descriptor, value: request.value) + return DescriptorResponse() + } +} diff --git a/lib/Sources/BluetoothClient/BluetoothClient.swift b/lib/Sources/BluetoothClient/BluetoothClient.swift index 207ddf43..5d642811 100644 --- a/lib/Sources/BluetoothClient/BluetoothClient.swift +++ b/lib/Sources/BluetoothClient/BluetoothClient.swift @@ -23,4 +23,5 @@ public protocol BluetoothClient: Sendable { func characteristicRead(_ peripheral: Peripheral, characteristic: Characteristic) async throws -> CharacteristicChangedEvent func characteristicWrite(_ peripheral: Peripheral, characteristic: Characteristic, value: Data, withResponse: Bool) async throws -> CharacteristicEvent func descriptorRead(_ peripheral: Peripheral, characteristic: Characteristic, descriptor: Descriptor) async throws -> DescriptorChangedEvent + func descriptorWrite(_ peripheral: Peripheral, characteristic: Characteristic, descriptor: Descriptor, value: Data) async throws -> DescriptorEvent } diff --git a/lib/Sources/BluetoothClient/Events/EventName.swift b/lib/Sources/BluetoothClient/Events/EventName.swift index f908fe4a..bf148f86 100644 --- a/lib/Sources/BluetoothClient/Events/EventName.swift +++ b/lib/Sources/BluetoothClient/Events/EventName.swift @@ -12,4 +12,5 @@ public enum EventName: Sendable { case canSendWriteWithoutResponse case characteristicValue case descriptorValue + case descriptorWrite } diff --git a/lib/Sources/BluetoothClient/Mocks/MockClient.swift b/lib/Sources/BluetoothClient/Mocks/MockClient.swift index 9918ce3f..f0824de8 100644 --- a/lib/Sources/BluetoothClient/Mocks/MockClient.swift +++ b/lib/Sources/BluetoothClient/Mocks/MockClient.swift @@ -22,6 +22,7 @@ public struct MockBluetoothClient: BluetoothClient { public var onCharacteristicRead: @Sendable (_ peripheral: Peripheral, _ characteristic: Characteristic) async throws -> CharacteristicChangedEvent public var onCharacteristicWrite: @Sendable (_ peripheral: Peripheral, _ characteristic: Characteristic, _ value: Data, _ withResponse: Bool) async throws -> CharacteristicEvent public var onDescriptorRead: @Sendable (_ peripheral: Peripheral, _ characteristic: Characteristic, _ descriptor: Descriptor) async throws -> DescriptorChangedEvent + public var onDescriptorWrite: @Sendable (_ peripheral: Peripheral, _ characteristic: Characteristic, _ descriptor: Descriptor, _ value: Data) async throws -> DescriptorEvent public init() { let (stream, continuation) = AsyncStream.makeStream() @@ -43,6 +44,7 @@ public struct MockBluetoothClient: BluetoothClient { self.onCharacteristicRead = { _, _ in fatalError("Not implemented") } self.onCharacteristicWrite = { _, _, _, _ in fatalError("Not implemented") } self.onDescriptorRead = { _, _, _ in fatalError("Not implemented") } + self.onDescriptorWrite = { _, _, _, _ in fatalError("Not implemented") } } public func enable() async { @@ -108,4 +110,8 @@ public struct MockBluetoothClient: BluetoothClient { public func descriptorRead(_ peripheral: Peripheral, characteristic: Characteristic, descriptor: Descriptor) async throws -> DescriptorChangedEvent { try await onDescriptorRead(peripheral, characteristic, descriptor) } + + public func descriptorWrite(_ peripheral: Peripheral, characteristic: Characteristic, descriptor: Descriptor, value: Data) async throws -> DescriptorEvent { + try await onDescriptorWrite(peripheral, characteristic, descriptor, value) + } } diff --git a/lib/Sources/BluetoothMessage/Message.swift b/lib/Sources/BluetoothMessage/Message.swift index 66fef7e1..4d153433 100644 --- a/lib/Sources/BluetoothMessage/Message.swift +++ b/lib/Sources/BluetoothMessage/Message.swift @@ -31,6 +31,7 @@ public struct Message { // GATT Descriptor case readDescriptor + case writeDescriptor } public let action: Action diff --git a/lib/Sources/BluetoothNative/Coordinator.swift b/lib/Sources/BluetoothNative/Coordinator.swift index c9183c3a..92b6c779 100644 --- a/lib/Sources/BluetoothNative/Coordinator.swift +++ b/lib/Sources/BluetoothNative/Coordinator.swift @@ -145,6 +145,14 @@ class Coordinator: @unchecked Sendable { } } + func writeDescriptor(peripheral: Peripheral, descriptor: Descriptor, value: Data) { + queue.async { + guard let nativePeripheral = peripheral.rawValue else { return } + guard let nativeDescriptor = descriptor.rawValue else { return } + nativePeripheral.writeValue(value, for: nativeDescriptor) + } + } + func setNotify(peripheral: Peripheral, characteristic: Characteristic, value: Bool) { queue.async { guard let nativePeripheral = peripheral.rawValue else { return } diff --git a/lib/Sources/BluetoothNative/NativeClient.swift b/lib/Sources/BluetoothNative/NativeClient.swift index b5f514b0..95fe7f5f 100644 --- a/lib/Sources/BluetoothNative/NativeClient.swift +++ b/lib/Sources/BluetoothNative/NativeClient.swift @@ -118,4 +118,10 @@ struct NativeBluetoothClient: BluetoothClient { coordinator.readDescriptor(peripheral: peripheral, descriptor: descriptor) } } + + func descriptorWrite(_ peripheral: Peripheral, characteristic: Characteristic, descriptor: Descriptor, value: Data) async throws -> DescriptorEvent { + return try await server.awaitEvent(key: .descriptor(.descriptorWrite, peripheralId: peripheral.id, characteristicId: characteristic.uuid, instance: characteristic.instance, descriptorId: descriptor.uuid)) { + coordinator.writeDescriptor(peripheral: peripheral, descriptor: descriptor, value: value) + } + } } diff --git a/lib/Sources/WebView/Resources/Generated/BluetoothPolyfill.js b/lib/Sources/WebView/Resources/Generated/BluetoothPolyfill.js index db038db5..5e3aec40 100644 --- a/lib/Sources/WebView/Resources/Generated/BluetoothPolyfill.js +++ b/lib/Sources/WebView/Resources/Generated/BluetoothPolyfill.js @@ -33,6 +33,7 @@ const base64ToDataView = (base64) => { function copyOf(data) { return new DataView(data.buffer.slice(0)); } +const isView = (source) => source.buffer !== undefined; class BluetoothRemoteGATTDescriptor { constructor(characteristic, uuid) { @@ -49,6 +50,18 @@ class BluetoothRemoteGATTDescriptor { this.value = base64ToDataView(response); return copyOf(this.value); }; + this.writeValue = async (value) => { + const arrayBuffer = isView(value) ? value.buffer : value; + const base64 = arrayBufferToBase64(arrayBuffer); + await bluetoothRequest('writeDescriptor', { + device: this.characteristic.service.device.id, + service: this.characteristic.service.uuid, + characteristic: this.characteristic.uuid, + instance: this.characteristic.instance, + descriptor: this.uuid, + value: base64 + }); + }; this.value = null; } } @@ -410,10 +423,10 @@ const keyForCharacteristic = (characteristic) => { }; class Store { constructor() { - _Store_devices.set(this, undefined); + _Store_devices.set(this, void 0); this.getDevice = (uuid) => { var _a; - return (_a = __classPrivateFieldGet(this, _Store_devices, "f").get(uuid)) === null || _a === undefined ? undefined : _a.device; + return (_a = __classPrivateFieldGet(this, _Store_devices, "f").get(uuid)) === null || _a === void 0 ? void 0 : _a.device; }; this.addDevice = (device) => { __classPrivateFieldGet(this, _Store_devices, "f").get(device.id); @@ -421,7 +434,7 @@ class Store { }; this.getService = (deviceUuid, serviceUuid) => { var _a, _b; - return (_b = (_a = __classPrivateFieldGet(this, _Store_devices, "f").get(deviceUuid)) === null || _a === undefined ? undefined : _a.services.get(serviceUuid)) === null || _b === undefined ? undefined : _b.service; + return (_b = (_a = __classPrivateFieldGet(this, _Store_devices, "f").get(deviceUuid)) === null || _a === void 0 ? void 0 : _a.services.get(serviceUuid)) === null || _b === void 0 ? void 0 : _b.service; }; this.addService = (service) => { const deviceRecord = __classPrivateFieldGet(this, _Store_devices, "f").get(service.device.id); @@ -432,11 +445,11 @@ class Store { }; this.getCharacteristic = (service, uuid, instance) => { var _a, _b, _c; - return (_c = (_b = (_a = __classPrivateFieldGet(this, _Store_devices, "f").get(service.device.id)) === null || _a === undefined ? undefined : _a.services.get(service.uuid)) === null || _b === undefined ? undefined : _b.characteristics.get(characteristicKey(uuid, instance))) === null || _c === undefined ? undefined : _c.characteristic; + return (_c = (_b = (_a = __classPrivateFieldGet(this, _Store_devices, "f").get(service.device.id)) === null || _a === void 0 ? void 0 : _a.services.get(service.uuid)) === null || _b === void 0 ? void 0 : _b.characteristics.get(characteristicKey(uuid, instance))) === null || _c === void 0 ? void 0 : _c.characteristic; }; this.addCharacteristic = (service, characteristic) => { var _a; - const serviceRecord = (_a = __classPrivateFieldGet(this, _Store_devices, "f").get(service.device.id)) === null || _a === undefined ? undefined : _a.services.get(service.uuid); + const serviceRecord = (_a = __classPrivateFieldGet(this, _Store_devices, "f").get(service.device.id)) === null || _a === void 0 ? void 0 : _a.services.get(service.uuid); if (!serviceRecord) { throw new ReferenceError(`Service ${service.uuid} not found`); } @@ -446,7 +459,7 @@ class Store { var _a; for (const deviceRecord of __classPrivateFieldGet(this, _Store_devices, "f").values()) { for (const serviceRecord of deviceRecord.services.values()) { - const characteristic = (_a = serviceRecord.characteristics.get(key)) === null || _a === undefined ? undefined : _a.characteristic; + const characteristic = (_a = serviceRecord.characteristics.get(key)) === null || _a === void 0 ? void 0 : _a.characteristic; if (characteristic) { return characteristic; } @@ -465,12 +478,12 @@ class Store { this.getDescriptor = (characteristic, uuid) => { var _a, _b, _c; return (_c = (_b = (_a = __classPrivateFieldGet(this, _Store_devices, "f") - .get(characteristic.service.device.id)) === null || _a === undefined ? undefined : _a.services.get(characteristic.service.uuid)) === null || _b === undefined ? undefined : _b.characteristics.get(characteristicKey(characteristic.uuid, characteristic.instance))) === null || _c === undefined ? undefined : _c.descriptors.get(uuid); + .get(characteristic.service.device.id)) === null || _a === void 0 ? void 0 : _a.services.get(characteristic.service.uuid)) === null || _b === void 0 ? void 0 : _b.characteristics.get(characteristicKey(characteristic.uuid, characteristic.instance))) === null || _c === void 0 ? void 0 : _c.descriptors.get(uuid); }; this.addDescriptor = (characteristic, descriptor) => { var _a, _b; const characteristicRecord = (_b = (_a = __classPrivateFieldGet(this, _Store_devices, "f") - .get(characteristic.service.device.id)) === null || _a === undefined ? undefined : _a.services.get(characteristic.service.uuid)) === null || _b === undefined ? undefined : _b.characteristics.get(keyForCharacteristic(characteristic)); + .get(characteristic.service.device.id)) === null || _a === void 0 ? void 0 : _a.services.get(characteristic.service.uuid)) === null || _b === void 0 ? void 0 : _b.characteristics.get(keyForCharacteristic(characteristic)); if (!characteristicRecord) { throw new ReferenceError(`Characteristic ${characteristic.uuid} not found`); } @@ -567,7 +580,6 @@ class BluetoothRemoteGATTCharacteristic extends EventTarget { this.value = null; } } -const isView = (source) => source.buffer !== undefined; const getOrCreateCharacteristic = (service, uuid, properties, instance) => { const existingCharacteristic = store.getCharacteristic(service, uuid, instance); @@ -689,7 +701,7 @@ class Bluetooth extends EventTarget { class ValueEvent extends Event { constructor(type, eventInitDict) { super(type, eventInitDict); - this.value = eventInitDict === null || eventInitDict === undefined ? undefined : eventInitDict.value; + this.value = eventInitDict === null || eventInitDict === void 0 ? void 0 : eventInitDict.value; } }