From 89f0aa2a83bd40a53ee43c290ce9fd3043baf8c4 Mon Sep 17 00:00:00 2001 From: Richard Hopton Date: Tue, 28 Mar 2023 23:27:50 -0700 Subject: [PATCH] feat: Add methods for all BLE interactions --- lib/connection.js | 166 +++++++++++++++++++++++++++++++++++++++++++ lib/protoc/api.proto | 6 +- lib/protoc/api_pb.js | 36 +++++----- 3 files changed, 187 insertions(+), 21 deletions(-) diff --git a/lib/connection.js b/lib/connection.js index fccecc4..62c0e01 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -11,6 +11,15 @@ const isBase64 = (payload) => new RegExp(base64Regex, 'gi').test(payload); const base64Decode = (message) => isBase64(message) ? Buffer.from(message, 'base64').toString('ascii') : message; +const uuidRegex = new RegExp( + /([a-f0-9]{8})([a-f0-9]{4})([a-f0-9]{4})([a-f0-9]{4})([a-f0-9]{12})/ +); +const uuidDecode = (segments) => + segments + .map((segment) => BigInt(segment).toString(16).padStart(16, '0')) + .join('') + .replace(uuidRegex, '$1-$2-$3-$4-$5'); + const mapMessageByType = (type, obj) => { switch (type) { case 'SubscribeLogsResponse': { @@ -18,6 +27,36 @@ const mapMessageByType = (type, obj) => { const message = base64Decode(obj.message); return { ...obj, message }; } + case 'BluetoothLEAdvertisementResponse': { + // decode name to string + const name = base64Decode(obj.name); + return { ...obj, name }; + } + case 'BluetoothGATTGetServicesResponse': { + // decode uuidList to uuid string + const { servicesList, ...rest } = obj; + return { + ...rest, + servicesList: servicesList.map( + ({ uuidList, characteristicsList, ...rest }) => ({ + uuid: uuidDecode(uuidList), + ...rest, + characteristicsList: characteristicsList.map( + ({ uuidList, descriptorsList, ...rest }) => ({ + uuid: uuidDecode(uuidList), + ...rest, + descriptorsList: descriptorsList.map( + ({ uuidList, ...rest }) => ({ + uuid: uuidDecode(uuidList), + ...rest, + }) + ), + }) + ), + }) + ), + }; + } default: return obj; } @@ -329,6 +368,133 @@ class EsphomeNativeApiConnection extends EventEmitter { switchCommandService(data) { Entities.Switch.commandService(this, data); } + subscribeBluetoothAdvertisementService() { + if (!this.connected) throw new Error(`Not connected`); + if (!this.authorized) throw new Error(`Not authorized`); + this.sendMessage(new pb.SubscribeBluetoothLEAdvertisementsRequest()); + } + unsubscribeBluetoothAdvertisementService() { + if (!this.connected) throw new Error(`Not connected`); + if (!this.authorized) throw new Error(`Not authorized`); + this.sendMessage(new pb.UnsubscribeBluetoothLEAdvertisementsRequest()); + } + async connectBluetoothDeviceService(address) { + if (!this.connected) throw new Error(`Not connected`); + if (!this.authorized) throw new Error(`Not authorized`); + return await this.sendMessageAwaitResponse( + new pb.BluetoothDeviceRequest([address]), + 'BluetoothDeviceConnectionResponse', + 10 + ); + } + async disconnectBluetoothDeviceService(address) { + if (!this.connected) throw new Error(`Not connected`); + if (!this.authorized) throw new Error(`Not authorized`); + return await this.sendMessageAwaitResponse( + new pb.BluetoothDeviceRequest([ + address, + pb.BluetoothDeviceRequestType + .BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT, + ]), + 'BluetoothDeviceConnectionResponse' + ); + } + async listBluetoothGattServicesService(address) { + if (!this.connected) throw new Error(`Not connected`); + if (!this.authorized) throw new Error(`Not authorized`); + const message = new pb.BluetoothGATTGetServicesRequest([address]); + + const servicesList = []; + const onMessage = (message) => { + if (message.address === address) + servicesList.push(...message.servicesList); + }; + this.on('message.BluetoothGATTGetServicesResponse', onMessage); + await this.sendMessageAwaitResponse( + message, + 'BluetoothGATTGetServicesDoneResponse' + ).then( + () => { + this.off('message.BluetoothGATTGetServicesResponse', onMessage); + }, + (e) => { + this.off('message.BluetoothGATTGetServicesResponse', onMessage); + throw e; + } + ); + return { address, servicesList }; + } + + async readBluetoothGATTCharacteristicService(address, handle) { + if (!this.connected) throw new Error(`Not connected`); + if (!this.authorized) throw new Error(`Not authorized`); + return await this.sendMessageAwaitResponse( + new pb.BluetoothGATTReadRequest([address, handle]), + 'BluetoothGATTReadResponse' + ); + } + + async writeBluetoothGATTCharacteristicService( + address, + handle, + value, + response = false + ) { + if (!this.connected) throw new Error(`Not connected`); + if (!this.authorized) throw new Error(`Not authorized`); + + const message = new pb.BluetoothGATTWriteRequest([ + address, + handle, + response, + value, + ]); + if (!response) return this.sendMessage(message); + + return await this.sendMessageAwaitResponse( + message, + 'BluetoothGATTWriteResponse' + ); + } + + async notifyBluetoothGATTCharacteristicService(address, handle) { + if (!this.connected) throw new Error(`Not connected`); + if (!this.authorized) throw new Error(`Not authorized`); + return await this.sendMessageAwaitResponse( + new pb.BluetoothGATTNotifyRequest([address, handle, true]), + 'BluetoothGATTNotifyResponse' + ); + } + + async readBluetoothGATTDescriptorService(address, handle) { + if (!this.connected) throw new Error(`Not connected`); + if (!this.authorized) throw new Error(`Not authorized`); + return await this.sendMessageAwaitResponse( + new pb.BluetoothGATTReadDescriptorRequest([address, handle]), + 'BluetoothGATTReadResponse' + ); + } + + async writeBluetoothGATTDescriptorService( + address, + handle, + value, + waitForResponse = false + ) { + if (!this.connected) throw new Error(`Not connected`); + if (!this.authorized) throw new Error(`Not authorized`); + const message = new pb.BluetoothGATTWriteDescriptorRequest([ + address, + handle, + value, + ]); + if (!waitForResponse) return this.sendMessage(message); + + return await this.sendMessageAwaitResponse( + message, + 'BluetoothGATTWriteResponse' + ); + } } module.exports = EsphomeNativeApiConnection; diff --git a/lib/protoc/api.proto b/lib/protoc/api.proto index 85ac51c..94b39a5 100644 --- a/lib/protoc/api.proto +++ b/lib/protoc/api.proto @@ -1218,19 +1218,19 @@ message BluetoothGATTGetServicesRequest { } message BluetoothGATTDescriptor { - repeated uint64 uuid = 1; + repeated uint64 uuid = 1 [jstype=JS_STRING]; uint32 handle = 2; } message BluetoothGATTCharacteristic { - repeated uint64 uuid = 1; + repeated uint64 uuid = 1 [jstype=JS_STRING]; uint32 handle = 2; uint32 properties = 3; repeated BluetoothGATTDescriptor descriptors = 4; } message BluetoothGATTService { - repeated uint64 uuid = 1; + repeated uint64 uuid = 1 [jstype=JS_STRING]; uint32 handle = 2; repeated BluetoothGATTCharacteristic characteristics = 3; } diff --git a/lib/protoc/api_pb.js b/lib/protoc/api_pb.js index b72b9bb..c93d634 100644 --- a/lib/protoc/api_pb.js +++ b/lib/protoc/api_pb.js @@ -22207,7 +22207,7 @@ proto.BluetoothGATTDescriptor.deserializeBinaryFromReader = function(msg, reader var field = reader.getFieldNumber(); switch (field) { case 1: - var values = /** @type {!Array} */ (reader.isDelimited() ? reader.readPackedUint64() : [reader.readUint64()]); + var values = /** @type {!Array} */ (reader.isDelimited() ? reader.readPackedUint64String() : [reader.readUint64String()]); for (var i = 0; i < values.length; i++) { msg.addUuid(values[i]); } @@ -22247,7 +22247,7 @@ proto.BluetoothGATTDescriptor.serializeBinaryToWriter = function(message, writer var f = undefined; f = message.getUuidList(); if (f.length > 0) { - writer.writePackedUint64( + writer.writePackedUint64String( 1, f ); @@ -22264,15 +22264,15 @@ proto.BluetoothGATTDescriptor.serializeBinaryToWriter = function(message, writer /** * repeated uint64 uuid = 1; - * @return {!Array} + * @return {!Array} */ proto.BluetoothGATTDescriptor.prototype.getUuidList = function() { - return /** @type {!Array} */ (jspb.Message.getRepeatedField(this, 1)); + return /** @type {!Array} */ (jspb.Message.getRepeatedField(this, 1)); }; /** - * @param {!Array} value + * @param {!Array} value * @return {!proto.BluetoothGATTDescriptor} returns this */ proto.BluetoothGATTDescriptor.prototype.setUuidList = function(value) { @@ -22281,7 +22281,7 @@ proto.BluetoothGATTDescriptor.prototype.setUuidList = function(value) { /** - * @param {number} value + * @param {string} value * @param {number=} opt_index * @return {!proto.BluetoothGATTDescriptor} returns this */ @@ -22398,7 +22398,7 @@ proto.BluetoothGATTCharacteristic.deserializeBinaryFromReader = function(msg, re var field = reader.getFieldNumber(); switch (field) { case 1: - var values = /** @type {!Array} */ (reader.isDelimited() ? reader.readPackedUint64() : [reader.readUint64()]); + var values = /** @type {!Array} */ (reader.isDelimited() ? reader.readPackedUint64String() : [reader.readUint64String()]); for (var i = 0; i < values.length; i++) { msg.addUuid(values[i]); } @@ -22447,7 +22447,7 @@ proto.BluetoothGATTCharacteristic.serializeBinaryToWriter = function(message, wr var f = undefined; f = message.getUuidList(); if (f.length > 0) { - writer.writePackedUint64( + writer.writePackedUint64String( 1, f ); @@ -22479,15 +22479,15 @@ proto.BluetoothGATTCharacteristic.serializeBinaryToWriter = function(message, wr /** * repeated uint64 uuid = 1; - * @return {!Array} + * @return {!Array} */ proto.BluetoothGATTCharacteristic.prototype.getUuidList = function() { - return /** @type {!Array} */ (jspb.Message.getRepeatedField(this, 1)); + return /** @type {!Array} */ (jspb.Message.getRepeatedField(this, 1)); }; /** - * @param {!Array} value + * @param {!Array} value * @return {!proto.BluetoothGATTCharacteristic} returns this */ proto.BluetoothGATTCharacteristic.prototype.setUuidList = function(value) { @@ -22496,7 +22496,7 @@ proto.BluetoothGATTCharacteristic.prototype.setUuidList = function(value) { /** - * @param {number} value + * @param {string} value * @param {number=} opt_index * @return {!proto.BluetoothGATTCharacteristic} returns this */ @@ -22668,7 +22668,7 @@ proto.BluetoothGATTService.deserializeBinaryFromReader = function(msg, reader) { var field = reader.getFieldNumber(); switch (field) { case 1: - var values = /** @type {!Array} */ (reader.isDelimited() ? reader.readPackedUint64() : [reader.readUint64()]); + var values = /** @type {!Array} */ (reader.isDelimited() ? reader.readPackedUint64String() : [reader.readUint64String()]); for (var i = 0; i < values.length; i++) { msg.addUuid(values[i]); } @@ -22713,7 +22713,7 @@ proto.BluetoothGATTService.serializeBinaryToWriter = function(message, writer) { var f = undefined; f = message.getUuidList(); if (f.length > 0) { - writer.writePackedUint64( + writer.writePackedUint64String( 1, f ); @@ -22738,15 +22738,15 @@ proto.BluetoothGATTService.serializeBinaryToWriter = function(message, writer) { /** * repeated uint64 uuid = 1; - * @return {!Array} + * @return {!Array} */ proto.BluetoothGATTService.prototype.getUuidList = function() { - return /** @type {!Array} */ (jspb.Message.getRepeatedField(this, 1)); + return /** @type {!Array} */ (jspb.Message.getRepeatedField(this, 1)); }; /** - * @param {!Array} value + * @param {!Array} value * @return {!proto.BluetoothGATTService} returns this */ proto.BluetoothGATTService.prototype.setUuidList = function(value) { @@ -22755,7 +22755,7 @@ proto.BluetoothGATTService.prototype.setUuidList = function(value) { /** - * @param {number} value + * @param {string} value * @param {number=} opt_index * @return {!proto.BluetoothGATTService} returns this */