Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions lib/Javascript/src/BluetoothRemoteGATTCharacteristic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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;
28 changes: 28 additions & 0 deletions lib/Javascript/src/BluetoothRemoteGATTDescriptor.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -38,4 +50,20 @@ export class BluetoothRemoteGATTDescriptor {
this.value = base64ToDataView(response)
return copyOf(this.value)
}

writeValue = async (value: ArrayBuffer | ArrayBufferView): Promise<void> => {
const arrayBuffer = isView(value) ? value.buffer : value;
const base64 = arrayBufferToBase64(arrayBuffer)
await bluetoothRequest<WriteDescriptorRequest, EmptyObject>(
'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
}
)
}
}
2 changes: 2 additions & 0 deletions lib/Javascript/src/Data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
2 changes: 2 additions & 0 deletions lib/Sources/BluetoothAction/Message+Action.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
68 changes: 68 additions & 0 deletions lib/Sources/BluetoothAction/WriteDescriptor.swift
Original file line number Diff line number Diff line change
@@ -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()
}
}
1 change: 1 addition & 0 deletions lib/Sources/BluetoothClient/BluetoothClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
1 change: 1 addition & 0 deletions lib/Sources/BluetoothClient/Events/EventName.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ public enum EventName: Sendable {
case canSendWriteWithoutResponse
case characteristicValue
case descriptorValue
case descriptorWrite
}
6 changes: 6 additions & 0 deletions lib/Sources/BluetoothClient/Mocks/MockClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<any BluetoothEvent>.makeStream()
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
}
}
1 change: 1 addition & 0 deletions lib/Sources/BluetoothMessage/Message.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public struct Message {

// GATT Descriptor
case readDescriptor
case writeDescriptor
}

public let action: Action
Expand Down
8 changes: 8 additions & 0 deletions lib/Sources/BluetoothNative/Coordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
6 changes: 6 additions & 0 deletions lib/Sources/BluetoothNative/NativeClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
32 changes: 22 additions & 10 deletions lib/Sources/WebView/Resources/Generated/BluetoothPolyfill.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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;
}
}
Expand Down Expand Up @@ -410,18 +423,18 @@ 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);
__classPrivateFieldGet(this, _Store_devices, "f").set(device.id, { uuid: device.id, device, services: new Map() });
};
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);
Expand All @@ -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`);
}
Expand All @@ -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;
}
Expand All @@ -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`);
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
}

Expand Down
Loading