hap-controller
Version:
Library to implement a HAP (HomeKit) controller
940 lines (939 loc) • 74.1 kB
JavaScript
"use strict";
/**
* Controller class for interacting with a HAP device over GATT.
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const Characteristic = __importStar(require("../../model/characteristic"));
const events_1 = require("events");
const gatt_connection_1 = __importDefault(require("./gatt-connection"));
const GattConstants = __importStar(require("./gatt-constants"));
const gatt_protocol_1 = __importDefault(require("./gatt-protocol"));
const GattUtils = __importStar(require("./gatt-utils"));
const pairing_protocol_1 = __importStar(require("../../protocol/pairing-protocol"));
const Service = __importStar(require("../../model/service"));
const tlv_1 = require("../../model/tlv");
const ip_discovery_1 = require("../ip/ip-discovery");
const debug_1 = __importDefault(require("debug"));
const queue_1 = require("../../utils/queue");
const debug = (0, debug_1.default)('hap-controller:gatt-client');
class GattClient extends events_1.EventEmitter {
/**
* Initialize the GattClient object.
*
* @param {string} deviceId - ID of the device
* @param {NoblePeripheral} peripheral - Peripheral object from noble
* @param {PairingData?} pairingData - existing pairing data
*/
constructor(deviceId, peripheral, pairingData) {
super();
this.subscribedCharacteristics = [];
this.deviceId = deviceId;
this.peripheral = peripheral;
this.pairingProtocol = new pairing_protocol_1.default(pairingData);
this.gattProtocol = new gatt_protocol_1.default();
this.tid = Math.floor(Math.random() * 254);
this.queue = new queue_1.OpQueue();
this.pairingQueue = new queue_1.OpQueue();
}
/**
* Queue an operation for the client.
*
* @param {function} op - Function to add to the queue
* @returns {Promise} Promise which resolves when the function is called.
*/
_queueOperation(op) {
return this.queue.queue(op);
}
/**
* Queue an operation for the pairing.
*
* @param {function} op - Function to add to the queue
* @returns {Promise} Promise which resolves when the function is called.
*/
_queuePairingOperation(op) {
return this.pairingQueue.queue(op);
}
/**
* Get the next transaction ID.
*
* @returns {number} Transaction ID.
*/
getNextTransactionId() {
this.tid++;
if (this.tid > 255) {
this.tid = 0;
}
return this.tid;
}
/**
* Get the data (keys) that needs to be stored long-term.
*
* @returns {PairingData} Object containing the keys that should be stored.
*/
getLongTermData() {
return this.pairingProtocol.getLongTermData();
}
/**
* Verify the provided PIN
*
* @param pin {string} PIN
*/
verifyPin(pin) {
this.pairingProtocol.verifyPin(pin);
}
/**
* Run the identify routine on a device.
*
* @returns {Promise} Promise which resolves if identify succeeded.
*/
identify() {
const serviceUuid = GattUtils.uuidToNobleUuid(Service.uuidFromService('public.hap.service.accessory-information'));
const characteristicUuid = GattUtils.uuidToNobleUuid(Characteristic.uuidFromCharacteristic('public.hap.characteristic.identify'));
return this._queueOperation(async () => {
const connection = new gatt_connection_1.default(this.peripheral);
try {
await connection.connect();
const { characteristics } = await new GattUtils.Watcher(this.peripheral, this.peripheral.discoverSomeServicesAndCharacteristicsAsync([serviceUuid], [characteristicUuid])).getPromise();
const characteristic = characteristics.find((c) => {
return c.uuid === characteristicUuid;
});
if (!characteristic) {
throw new Error('Identify characteristic not found');
}
const iid = await this._readInstanceId(characteristic);
const data = new Map();
data.set(GattConstants.Types['HAP-Param-Value'], Buffer.from([1]));
const pdu = this.gattProtocol.buildCharacteristicWriteRequest(this.getNextTransactionId(), iid, data);
const pdus = await connection.writeCharacteristic(characteristic, [pdu]);
if (pdus.length === 0) {
throw new Error('No response from identify routine');
}
const status = pdus[0].readUInt8(2);
if (status !== 0) {
throw new Error(`Identify returned error status: ${status}`);
}
// eslint-disable-next-line @typescript-eslint/no-empty-function
await connection.disconnect().catch(() => { });
}
catch (err) {
// eslint-disable-next-line @typescript-eslint/no-empty-function
await connection.disconnect().catch(() => { });
throw err;
}
});
}
/**
* Read the instance ID descriptor for a characteristic. The peripheral must
* already be connected.
*
* @param {Object} characteristic - The characteristic to read from
* @returns {Promise} Promise which resolves to the IID.
*/
async _readInstanceId(characteristic) {
const characteristicInstanceIdUuid = GattUtils.uuidToNobleUuid(GattConstants.CharacteristicInstanceIdUuid);
const characteristicInstanceIdShortUuid = GattUtils.uuidToNobleUuid(GattConstants.CharacteristicInstanceIdShortUuid);
const descriptors = await new GattUtils.Watcher(this.peripheral, characteristic.discoverDescriptorsAsync()).getPromise();
const descriptor = descriptors.find((d) => {
return d.uuid === characteristicInstanceIdUuid || d.uuid === characteristicInstanceIdShortUuid;
});
if (!descriptor) {
throw new Error('Could not find IID');
}
const data = await new GattUtils.Watcher(this.peripheral, descriptor.readValueAsync()).getPromise();
return data.readUInt16LE(0);
}
async getPairingMethod() {
const serviceUuid = GattUtils.uuidToNobleUuid(Service.uuidFromService('public.hap.service.pairing'));
const featureCharacteristicUuid = GattUtils.uuidToNobleUuid(Characteristic.uuidFromCharacteristic('public.hap.characteristic.pairing.features'));
return this._queueOperation(async () => {
const connection = new gatt_connection_1.default(this.peripheral);
try {
await connection.connect();
const { characteristics } = await new GattUtils.Watcher(this.peripheral, this.peripheral.discoverSomeServicesAndCharacteristicsAsync([serviceUuid], [featureCharacteristicUuid])).getPromise();
const characteristic = characteristics.find((c) => {
return c.uuid === featureCharacteristicUuid;
});
if (!characteristic) {
throw new Error('pairing.features characteristic not found');
}
const iid = await this._readInstanceId(characteristic);
const pdu = this.gattProtocol.buildCharacteristicReadRequest(this.getNextTransactionId(), iid);
const pdus = await connection.writeCharacteristic(characteristic, [pdu]);
// eslint-disable-next-line @typescript-eslint/no-empty-function
await connection.disconnect().catch(() => { });
if (pdus.length !== 0) {
const response = pdus[0];
const pairFeatures = response.readUInt8();
const pairMethod = pairFeatures & ip_discovery_1.DiscoveryPairingFeatureFlags.SupportsAppleAuthenticationCoprocessor
? pairing_protocol_1.PairMethods.PairSetupWithAuth
: pairing_protocol_1.PairMethods.PairSetup;
return pairMethod;
}
else {
throw new Error('Could not read the Pairing Feature information');
}
}
catch (err) {
// eslint-disable-next-line @typescript-eslint/no-empty-function
await connection.disconnect().catch(() => { });
throw err;
}
});
}
/**
* Begins the pairing process. For devices with random pins, this
* will cause it to show the pin on the screen.
*
* @param {PairMethods} [pairMethod] - Method to use for pairing, default is PairSetupWithAuth
* @param {PairingTypeFlags} [pairFlags] - Flags to use for Pairing for PairSetup
* * No provided flags is equivalent to providing
* kPairingFlag_Transient and kPairingFlag_Split and in this case a new
* code is generated randomly by the device (if supported) or the
* pre-defined code is used
* * If only the flag kPairingFlag_Split is provided the code which
* was created on the device from last transient+split call is reused
* and needs to be provided in finishPairing by user of this library
* * If only the flag kPairingFlag_Transient is provided the session
* security is enabled but no final pairing is done
* @returns {Promise} Promise which resolves to opaque
* pairing data when complete.
*/
async startPairing(pairMethod = pairing_protocol_1.PairMethods.PairSetupWithAuth, pairFlags = 0) {
const serviceUuid = GattUtils.uuidToNobleUuid(Service.uuidFromService('public.hap.service.pairing'));
const characteristicUuid = GattUtils.uuidToNobleUuid(Characteristic.uuidFromCharacteristic('public.hap.characteristic.pairing.pair-setup'));
return this._queueOperation(async () => {
const connection = (this._pairingConnection = new gatt_connection_1.default(this.peripheral));
await connection.connect();
const { characteristics } = await new GattUtils.Watcher(this.peripheral, this.peripheral.discoverSomeServicesAndCharacteristicsAsync([serviceUuid], [characteristicUuid])).getPromise();
const characteristic = characteristics.find((c) => {
return c.uuid === characteristicUuid;
});
if (!characteristic) {
throw new Error('pair-setup characteristic not found');
}
const iid = await this._readInstanceId(characteristic);
const packet = await this.pairingProtocol.buildPairSetupM1(pairMethod, pairFlags);
const data = new Map();
data.set(GattConstants.Types['HAP-Param-Value'], packet);
data.set(GattConstants.Types['HAP-Param-Return-Response'], Buffer.from([1]));
let pdu = this.gattProtocol.buildCharacteristicWriteRequest(this.getNextTransactionId(), iid, data);
let pdus = await connection.writeCharacteristic(characteristic, [pdu]);
if (pdus.length === 0) {
throw new Error('M1: No response');
}
let response = pdus[0];
let status = response.readUInt8(2);
if (status !== 0) {
throw new Error(`M1: Got error status: ${status}`);
}
if (response.length < 5) {
pdu = this.gattProtocol.buildCharacteristicReadRequest(this.getNextTransactionId(), iid);
pdus = await connection.writeCharacteristic(characteristic, [pdu]);
if (pdus.length === 0) {
throw new Error('M2: No response');
}
response = pdus[0];
status = response.readUInt8(2);
if (status !== 0) {
throw new Error(`M2: Got error status: ${status}`);
}
}
const body = (0, tlv_1.decodeBuffer)(response.slice(5, response.length));
if (!body.has(GattConstants.Types['HAP-Param-Value'])) {
throw new Error('M2: HAP-Param-Value missing');
}
const tlv = await this.pairingProtocol.parsePairSetupM2(body.get(GattConstants.Types['HAP-Param-Value']));
return { tlv, iid, characteristic };
});
}
/**
* Finishes a pairing process that began with startPairing()
*
* @param {PairingData} pairingData - The pairing data returned from startPairing()
* @param {string} pin - The pairing PIN, needs to be formatted as XXX-XX-XXX
* @returns {Promise} Promise which resolves when pairing is complete.
*/
async finishPairing(pairingData, pin) {
const { tlv, iid, characteristic } = pairingData;
this.verifyPin(pin);
if (!this._pairingConnection) {
throw new Error('Must call startPairing() first');
}
return this._queueOperation(async () => {
const connection = this._pairingConnection;
const protocol = this.pairingProtocol;
try {
const m3 = await protocol.buildPairSetupM3(tlv, pin);
const m3Data = new Map();
m3Data.set(GattConstants.Types['HAP-Param-Value'], m3);
m3Data.set(GattConstants.Types['HAP-Param-Return-Response'], Buffer.from([1]));
const m3Pdu = this.gattProtocol.buildCharacteristicWriteRequest(this.getNextTransactionId(), iid, m3Data);
let pdus = await connection.writeCharacteristic(characteristic, [m3Pdu]);
if (pdus.length === 0) {
throw new Error('M3: No response');
}
let response = pdus[0];
let status = response.readUInt8(2);
if (status !== 0) {
throw new Error(`M3: Got error status: ${status}`);
}
if (response.length < 5) {
const pdu = this.gattProtocol.buildCharacteristicReadRequest(this.getNextTransactionId(), iid);
pdus = await connection.writeCharacteristic(characteristic, [pdu]);
if (pdus.length === 0) {
throw new Error('M4: No response');
}
response = pdus[0];
status = response.readUInt8(2);
if (status !== 0) {
throw new Error(`M4: Got error status: ${status}`);
}
}
const buffers = [pdus[0].slice(5, pdus[0].length)];
pdus.slice(1).map((p) => buffers.push(p.slice(2, p.length)));
const m4Body = (0, tlv_1.decodeBuffer)(Buffer.concat(buffers));
if (!m4Body.has(GattConstants.Types['HAP-Param-Value'])) {
throw new Error('M4: HAP-Param-Value missing');
}
await this.pairingProtocol.parsePairSetupM4(m4Body.get(GattConstants.Types['HAP-Param-Value']));
if (!this.pairingProtocol.isTransientOnlyPairSetup()) {
// According to specs for a transient pairSetup process no M5/6 is done, which should end in a
// "non pairing" result and we miss AccessoryId and AccessoryLTPK, but the current session is
// authenticated
const m5 = await this.pairingProtocol.buildPairSetupM5();
const m5Data = new Map();
m5Data.set(GattConstants.Types['HAP-Param-Value'], m5);
m5Data.set(GattConstants.Types['HAP-Param-Return-Response'], Buffer.from([1]));
const m5Pdu = this.gattProtocol.buildCharacteristicWriteRequest(this.getNextTransactionId(), iid, m5Data);
pdus = await connection.writeCharacteristic(characteristic, [m5Pdu]);
if (pdus.length === 0) {
throw new Error('M5: No response');
}
response = pdus[0];
status = response.readUInt8(2);
if (status !== 0) {
throw new Error(`M5: Got error status: ${status}`);
}
if (response.length < 5) {
const pdu = this.gattProtocol.buildCharacteristicReadRequest(this.getNextTransactionId(), iid);
pdus = await connection.writeCharacteristic(characteristic, [pdu]);
if (pdus.length === 0) {
throw new Error('M6: No response');
}
response = pdus[0];
status = response.readUInt8(2);
if (status !== 0) {
throw new Error(`M6: Got error status: ${status}`);
}
}
const m6Body = (0, tlv_1.decodeBuffer)(response.slice(5, response.length));
if (!m6Body.has(GattConstants.Types['HAP-Param-Value'])) {
throw new Error('M6: HAP-Param-Value missing');
}
await this.pairingProtocol.parsePairSetupM6(m6Body.get(GattConstants.Types['HAP-Param-Value']));
}
// eslint-disable-next-line @typescript-eslint/no-empty-function
await connection.disconnect().catch(() => { });
}
catch (err) {
// eslint-disable-next-line @typescript-eslint/no-empty-function
await connection.disconnect().catch(() => { });
throw err;
}
});
}
/**
* Attempt to pair with a device.
*
* @param {string} pin - The pairing PIN, needs to be formatted as XXX-XX-XXX
* @param {PairMethods} [pairMethod] - Method to use for pairing, default is PairSetupWithAuth
* @param {PairingTypeFlags} [pairFlags] - Flags to use for Pairing for PairSetup
* @returns {Promise} Promise which resolves when pairing is complete.
*/
async pairSetup(pin, pairMethod = pairing_protocol_1.PairMethods.PairSetupWithAuth, pairFlags = 0) {
return await this.finishPairing(await this.startPairing(pairMethod, pairFlags), pin);
}
/**
* Method used internally to generate session keys for a connection.
*
* @private
* @param {GattConnection} connection - Existing GattConnection object
* @returns {Promise} Promise which resolves when the pairing has been verified.
*/
async _pairVerify(connection) {
return this._queuePairingOperation(async () => {
debug('Start Pair-Verify process ...');
const serviceUuid = GattUtils.uuidToNobleUuid(Service.uuidFromService('public.hap.service.pairing'));
const characteristicUuid = GattUtils.uuidToNobleUuid(Characteristic.uuidFromCharacteristic('public.hap.characteristic.pairing.pair-verify'));
const { characteristics } = await new GattUtils.Watcher(this.peripheral, this.peripheral.discoverSomeServicesAndCharacteristicsAsync([serviceUuid], [characteristicUuid])).getPromise();
const characteristic = characteristics.find((c) => {
return c.uuid === characteristicUuid;
});
if (!characteristic) {
throw new Error('pair-verify characteristic not found');
}
const iid = await this._readInstanceId(characteristic);
if (this.pairingProtocol.canResume()) {
const m1 = await this.pairingProtocol.buildPairResumeM1();
const m1Data = new Map();
m1Data.set(GattConstants.Types['HAP-Param-Value'], m1);
m1Data.set(GattConstants.Types['HAP-Param-Return-Response'], Buffer.from([1]));
let pdu = this.gattProtocol.buildCharacteristicWriteRequest(this.getNextTransactionId(), iid, m1Data);
let pdus = await connection.writeCharacteristic(characteristic, [pdu]);
if (pdus.length === 0) {
throw new Error('M1: No response');
}
let response = pdus[0];
let status = response.readUInt8(2);
if (status !== 0) {
throw new Error(`M1: Got error status: ${status}`);
}
if (response.length < 5) {
const pdu = this.gattProtocol.buildCharacteristicReadRequest(this.getNextTransactionId(), iid);
pdus = await connection.writeCharacteristic(characteristic, [pdu]);
if (pdus.length === 0) {
throw new Error('M2: No response');
}
const response = pdus[0];
const status = response.readUInt8(2);
if (status !== 0) {
throw new Error(`M2: Got error status: ${status}`);
}
}
const m2Body = (0, tlv_1.decodeBuffer)(response.slice(5, response.length));
if (!m2Body.has(GattConstants.Types['HAP-Param-Value'])) {
throw new Error('M2: HAP-Param-Value missing');
}
try {
await this.pairingProtocol.parsePairResumeM2(m2Body.get(GattConstants.Types['HAP-Param-Value']));
}
catch (_) {
await this.pairingProtocol.parsePairVerifyM2(m2Body.get(GattConstants.Types['HAP-Param-Value']));
const m3 = await this.pairingProtocol.buildPairVerifyM3();
const m3Data = new Map();
m3Data.set(GattConstants.Types['HAP-Param-Value'], m3);
m3Data.set(GattConstants.Types['HAP-Param-Return-Response'], Buffer.from([1]));
pdu = this.gattProtocol.buildCharacteristicWriteRequest(this.getNextTransactionId(), iid, m3Data);
pdus = await connection.writeCharacteristic(characteristic, [pdu]);
if (pdus.length === 0) {
throw new Error('M3: No response');
}
response = pdus[0];
status = response.readUInt8(2);
if (status !== 0) {
throw new Error(`M3: Got error status: ${status}`);
}
if (response.length < 5) {
pdu = this.gattProtocol.buildCharacteristicReadRequest(this.getNextTransactionId(), iid);
pdus = await connection.writeCharacteristic(characteristic, [pdu]);
if (pdus.length === 0) {
throw new Error('M4: No response');
}
response = pdus[0];
status = response.readUInt8(2);
if (status !== 0) {
throw new Error(`M4: Got error status: ${status}`);
}
}
const m4Body = (0, tlv_1.decodeBuffer)(response.slice(5, response.length));
if (!m4Body.has(GattConstants.Types['HAP-Param-Value'])) {
throw new Error('M4: HAP-Param-Value missing');
}
await this.pairingProtocol.parsePairVerifyM4(m4Body.get(GattConstants.Types['HAP-Param-Value']));
}
}
else {
const m1 = await this.pairingProtocol.buildPairVerifyM1();
const m1Data = new Map();
m1Data.set(GattConstants.Types['HAP-Param-Value'], m1);
m1Data.set(GattConstants.Types['HAP-Param-Return-Response'], Buffer.from([1]));
let pdu = this.gattProtocol.buildCharacteristicWriteRequest(this.getNextTransactionId(), iid, m1Data);
let pdus = await connection.writeCharacteristic(characteristic, [pdu]);
if (pdus.length === 0) {
throw new Error('M1: No response');
}
let response = pdus[0];
let status = response.readUInt8(2);
if (status !== 0) {
throw new Error(`M1: Got error status: ${status}`);
}
if (response.length < 5) {
pdu = this.gattProtocol.buildCharacteristicReadRequest(this.getNextTransactionId(), iid);
pdus = await connection.writeCharacteristic(characteristic, [pdu]);
if (pdus.length === 0) {
throw new Error('M2: No response');
}
response = pdus[0];
status = response.readUInt8(2);
if (status !== 0) {
throw new Error(`M2: Got error status: ${status}`);
}
}
const m2Body = (0, tlv_1.decodeBuffer)(response.slice(5, response.length));
if (!m2Body.has(GattConstants.Types['HAP-Param-Value'])) {
throw new Error('M2: HAP-Param-Value missing');
}
await this.pairingProtocol.parsePairVerifyM2(m2Body.get(GattConstants.Types['HAP-Param-Value']));
const m3 = await this.pairingProtocol.buildPairVerifyM3();
const m3Data = new Map();
m3Data.set(GattConstants.Types['HAP-Param-Value'], m3);
m3Data.set(GattConstants.Types['HAP-Param-Return-Response'], Buffer.from([1]));
pdu = this.gattProtocol.buildCharacteristicWriteRequest(this.getNextTransactionId(), iid, m3Data);
pdus = await connection.writeCharacteristic(characteristic, [pdu]);
if (pdus.length === 0) {
throw new Error('M3: No response');
}
response = pdus[0];
status = response.readUInt8(2);
if (status !== 0) {
throw new Error(`M3: Got error status: ${status}`);
}
if (response.length < 5) {
pdu = this.gattProtocol.buildCharacteristicReadRequest(this.getNextTransactionId(), iid);
pdus = await connection.writeCharacteristic(characteristic, [pdu]);
if (pdus.length === 0) {
throw new Error('M4: No response');
}
response = pdus[0];
status = response.readUInt8(2);
if (status !== 0) {
throw new Error(`M4: Got error status: ${status}`);
}
}
const m4Body = (0, tlv_1.decodeBuffer)(response.slice(5, response.length));
if (!m4Body.has(GattConstants.Types['HAP-Param-Value'])) {
throw new Error('M4: HAP-Param-Value missing');
}
await this.pairingProtocol.parsePairVerifyM4(m4Body.get(GattConstants.Types['HAP-Param-Value']));
}
const keys = await this.pairingProtocol.getSessionKeys();
connection.setSessionKeys(keys);
debug('Finished Pair-Verify process ...');
});
}
/**
* Unpair the controller from a device.
*
* @param {string|Buffer} identifier - Identifier of the controller to remove
* @returns {Promise} Promise which resolves when the process completes.
*/
removePairing(identifier) {
const serviceUuid = GattUtils.uuidToNobleUuid(Service.uuidFromService('public.hap.service.pairing'));
const characteristicUuid = GattUtils.uuidToNobleUuid(Characteristic.uuidFromCharacteristic('public.hap.characteristic.pairing.pairings'));
return this._queueOperation(async () => {
const connection = new gatt_connection_1.default(this.peripheral);
try {
if (typeof identifier === 'string') {
identifier = pairing_protocol_1.default.bufferFromHex(identifier);
}
await connection.connect();
await this._pairVerify(connection);
const { characteristics } = await new GattUtils.Watcher(this.peripheral, this.peripheral.discoverSomeServicesAndCharacteristicsAsync([serviceUuid], [characteristicUuid])).getPromise();
const characteristic = characteristics.find((c) => {
return c.uuid === characteristicUuid;
});
if (!characteristic) {
throw new Error('pairings characteristic not found');
}
const iid = await this._readInstanceId(characteristic);
await this._pairVerify(connection);
const packet = await this.pairingProtocol.buildRemovePairingM1(identifier);
const data = new Map();
data.set(GattConstants.Types['HAP-Param-Value'], packet);
data.set(GattConstants.Types['HAP-Param-Return-Response'], Buffer.from([1]));
const pdu = this.gattProtocol.buildCharacteristicWriteRequest(this.getNextTransactionId(), iid, data);
let pdus = await connection.writeCharacteristic(characteristic, [pdu]);
if (pdus.length === 0) {
throw new Error('M1: No response');
}
let response = pdus[0];
let status = response.readUInt8(2);
if (status !== 0) {
throw new Error(`M1: Got error status: ${status}`);
}
if (response.length < 5) {
const pdu = this.gattProtocol.buildCharacteristicReadRequest(this.getNextTransactionId(), iid);
pdus = await connection.writeCharacteristic(characteristic, [pdu]);
if (pdus.length === 0) {
throw new Error('M2: No response');
}
response = pdus[0];
status = response.readUInt8(2);
if (status !== 0) {
throw new Error(`M2: Got error status: ${status}`);
}
}
const body = (0, tlv_1.decodeBuffer)(response.slice(5, response.length));
if (!body.has(GattConstants.Types['HAP-Param-Value'])) {
throw new Error('M2: HAP-Param-Value missing');
}
await this.pairingProtocol.parseRemovePairingM2(body.get(GattConstants.Types['HAP-Param-Value']));
// eslint-disable-next-line @typescript-eslint/no-empty-function
await connection.disconnect().catch(() => { });
}
catch (err) {
// eslint-disable-next-line @typescript-eslint/no-empty-function
await connection.disconnect().catch(() => { });
throw err;
}
});
}
/**
* Add a pairing to a device.
*
* @param {string} identifier - Identifier of new controller
* @param {Buffer} ltpk - Long-term public key of the new controller
* @param {boolean} isAdmin - Whether or not the new controller is an admin
* @returns {Promise} Promise which resolves when the process is complete.
*/
addPairing(identifier, ltpk, isAdmin) {
const serviceUuid = GattUtils.uuidToNobleUuid(Service.uuidFromService('public.hap.service.pairing'));
const characteristicUuid = GattUtils.uuidToNobleUuid(Characteristic.uuidFromCharacteristic('public.hap.characteristic.pairing.pairings'));
return this._queueOperation(async () => {
const connection = new gatt_connection_1.default(this.peripheral);
try {
await connection.connect();
const { characteristics } = await new GattUtils.Watcher(this.peripheral, this.peripheral.discoverSomeServicesAndCharacteristicsAsync([serviceUuid], [characteristicUuid])).getPromise();
const characteristic = characteristics.find((c) => {
return c.uuid === characteristicUuid;
});
if (!characteristic) {
throw new Error('pairings characteristic not found');
}
const iid = await this._readInstanceId(characteristic);
await this._pairVerify(connection);
const packet = await this.pairingProtocol.buildAddPairingM1(identifier, ltpk, isAdmin);
const data = new Map();
data.set(GattConstants.Types['HAP-Param-Value'], packet);
data.set(GattConstants.Types['HAP-Param-Return-Response'], Buffer.from([1]));
let pdu = this.gattProtocol.buildCharacteristicWriteRequest(this.getNextTransactionId(), iid, data);
let pdus = await connection.writeCharacteristic(characteristic, [pdu]);
if (pdus.length === 0) {
throw new Error('M1: No response');
}
let response = pdus[0];
let status = response.readUInt8(2);
if (status !== 0) {
throw new Error(`M1: Got error status: ${status}`);
}
if (response.length < 5) {
pdu = this.gattProtocol.buildCharacteristicReadRequest(this.getNextTransactionId(), iid);
pdus = await connection.writeCharacteristic(characteristic, [pdu]);
if (pdus.length === 0) {
throw new Error('M2: No response');
}
response = pdus[0];
status = response.readUInt8(2);
if (status !== 0) {
throw new Error(`M2: Got error status: ${status}`);
}
}
const body = (0, tlv_1.decodeBuffer)(response.slice(5, response.length));
if (!body.has(GattConstants.Types['HAP-Param-Value'])) {
throw new Error('M2: HAP-Param-Value missing');
}
await this.pairingProtocol.parseAddPairingM2(body.get(GattConstants.Types['HAP-Param-Value']));
// eslint-disable-next-line @typescript-eslint/no-empty-function
await connection.disconnect().catch(() => { });
}
catch (err) {
// eslint-disable-next-line @typescript-eslint/no-empty-function
await connection.disconnect();
throw err;
}
});
}
/**
* List the pairings on a device.
*
* @returns {Promise} Promise which resolves to the final TLV when the process
* is complete.
*/
listPairings() {
const serviceUuid = GattUtils.uuidToNobleUuid(Service.uuidFromService('public.hap.service.pairing'));
const characteristicUuid = GattUtils.uuidToNobleUuid(Characteristic.uuidFromCharacteristic('public.hap.characteristic.pairing.pairings'));
return this._queueOperation(async () => {
const connection = new gatt_connection_1.default(this.peripheral);
try {
await connection.connect();
const { characteristics } = await new GattUtils.Watcher(this.peripheral, this.peripheral.discoverSomeServicesAndCharacteristicsAsync([serviceUuid], [characteristicUuid])).getPromise();
const characteristic = characteristics.find((c) => {
return c.uuid === characteristicUuid;
});
if (!characteristic) {
throw new Error('pairings characteristic not found');
}
const iid = await this._readInstanceId(characteristic);
await this._pairVerify(connection);
const packet = await this.pairingProtocol.buildListPairingsM1();
const data = new Map();
data.set(GattConstants.Types['HAP-Param-Value'], packet);
data.set(GattConstants.Types['HAP-Param-Return-Response'], Buffer.from([1]));
let pdu = this.gattProtocol.buildCharacteristicWriteRequest(this.getNextTransactionId(), iid, data);
let pdus = await connection.writeCharacteristic(characteristic, [pdu]);
if (pdus.length === 0) {
throw new Error('M1: No response');
}
let response = pdus[0];
let status = response.readUInt8(2);
if (status !== 0) {
throw new Error(`M1: Got error status: ${status}`);
}
if (response.length < 5) {
pdu = this.gattProtocol.buildCharacteristicReadRequest(this.getNextTransactionId(), iid);
pdus = await connection.writeCharacteristic(characteristic, [pdu]);
if (pdus.length === 0) {
throw new Error('M2: No response');
}
response = pdus[0];
status = response.readUInt8(2);
if (status !== 0) {
throw new Error(`M2: Got error status: ${status}`);
}
}
const body = (0, tlv_1.decodeBuffer)(response.slice(5, response.length));
if (!body.has(GattConstants.Types['HAP-Param-Value'])) {
throw new Error('M2: HAP-Param-Value missing');
}
const tlv = await this.pairingProtocol.parseListPairingsM2(body.get(GattConstants.Types['HAP-Param-Value']));
// eslint-disable-next-line @typescript-eslint/no-empty-function
await connection.disconnect().catch(() => { });
return tlv;
}
catch (err) {
// eslint-disable-next-line @typescript-eslint/no-empty-function
await connection.disconnect().catch(() => { });
throw err;
}
});
}
/**
* Get the accessory attribute database from a device.
*
* @returns {Promise} Promise which resolves to the JSON document.
*/
getAccessories() {
const pairingUuid = GattUtils.uuidToNobleUuid(Service.uuidFromService('public.hap.service.pairing'));
const protocolInformationUuid = GattUtils.uuidToNobleUuid(Service.uuidFromService('public.hap.service.protocol.information.service'));
const serviceInstanceIdUuid = GattUtils.uuidToNobleUuid(GattConstants.ServiceInstanceIdUuid);
const serviceSignatureUuid = GattUtils.uuidToNobleUuid(GattConstants.ServiceSignatureUuid);
return this._queueOperation(async () => {
const database = {
accessories: [
{
aid: 1,
services: [],
},
],
};
const connection = new gatt_connection_1.default(this.peripheral);
try {
await connection.connect();
const { services, characteristics: allCharacteristics } = await new GattUtils.Watcher(this.peripheral, this.peripheral.discoverAllServicesAndCharacteristicsAsync()).getPromise();
// Get the Service IIDs
let queue = new queue_1.OpQueue();
let lastOp = Promise.resolve();
for (const service of services) {
if (service.uuid === pairingUuid || service.uuid === protocolInformationUuid) {
continue;
}
const characteristic = service.characteristics.find((c) => {
return c.uuid === serviceInstanceIdUuid;
});
if (!characteristic) {
continue;
}
lastOp = queue.queue(async () => {
const data = await new GattUtils.Watcher(this.peripheral, characteristic.readAsync()).getPromise();
database.accessories[0].services.push({
iid: data.readUInt16LE(0),
type: GattUtils.nobleUuidToUuid(service.uuid),
characteristics: service.characteristics
.filter((c) => {
return c.uuid !== serviceInstanceIdUuid && c.uuid !== serviceSignatureUuid;
})
.map((c) => {
return {
type: GattUtils.nobleUuidToUuid(c.uuid),
perms: [],
format: 'data',
};
}),
});
});
}
await lastOp;
queue = new queue_1.OpQueue();
lastOp = Promise.resolve();
const characteristics = [];
for (const characteristic of allCharacteristics) {
lastOp = queue.queue(async () => {
try {
const iid = await this._readInstanceId(characteristic);
const serviceType = GattUtils.nobleUuidToUuid(characteristic._serviceUuid);
const characteristicType = GattUtils.nobleUuidToUuid(characteristic.uuid);
for (const service of database.accessories[0].services) {
if (service.type === serviceType) {
for (const characteristic of service.characteristics) {
if (characteristic.type === characteristicType) {
characteristic.iid = iid;
break;
}
}
break;
}
}
characteristics.push({ characteristic, iid });
}
catch (_) {
// Ignore errors here, as not all characteristics will have IIDs
}
});
}
await lastOp;
queue = new queue_1.OpQueue();
lastOp = Promise.resolve();
for (const c of characteristics) {
const serviceUuid = GattUtils.nobleUuidToUuid(c.characteristic._serviceUuid);
const characteristicUuid = GattUtils.nobleUuidToUuid(c.characteristic.uuid);
if (characteristicUuid === GattConstants.ServiceSignatureUuid) {
const service = database.accessories[0].services.find((s) => {
return s.type === serviceUuid;
});
if (!service) {
continue;
}
const pdu = this.gattProtocol.buildServiceSignatureReadRequest(this.getNextTransactionId(), service.iid);
lastOp = queue.queue(async () => {
const pdus = await connection.writeCharacteristic(c.characteristic, [pdu]);
if (pdus.length === 0) {
return;
}
const response = pdus[0];
const body = (0, tlv_1.decodeBuffer)(response.slice(5, response.length));
const props = body.get(GattConstants.Types['HAP-Param-HAP-Service-Properties']);
if (props && props.length) {
switch (props.readUInt16LE(0)) {
case 1:
service.primary = true;
break;
case 2:
service.hidden = true;
break;
}
}
const linked = body.get(GattConstants.Types['HAP-Param-HAP-Linked-Services']);
if (linked && linked.length) {
service.linked = [];
for (let idx = 0; idx < linked.length; idx += 2) {
service.linked.push(linked.readUInt16LE(idx));
}
}
});
}
}
await lastOp;
await this._pairVerify(connection);
const toFetch = [];
for (const c of characteristics) {
const serviceUuid = GattUtils.nobleUuidToUuid(c.characteristic._serviceUuid);
const characteristicUuid = GattUtils.nobleUuidToUuid(c.characteristic.uuid);
if (characteristicUuid !== GattConstants.ServiceSignatureUuid &&
c.characteristic._serviceUuid !==
protocolInformationUuid &&
c.characteristic._serviceUuid !== pairingUuid) {
toFetch.push({
serviceUuid,
characteristicUuid,
iid: c.iid,
});
}
}
const list = await this.getCharacteristics(toFetch, {
meta: true,
perms: true,
ev: true,
type: true,
extra: true,
}, connection);
for (const entry of list.characteristics) {
if (!entry) {
continue;
}
const service = database.accessories[0].services.find((s) => {
return s.type === entry.serviceUuid;
});
if (!service) {
continue;
}
const characteristic = service.characteristics.find((c) => {
return c.iid === entry.iid;
});
if (!characteristic) {
continue;
}
delete entry.serviceUuid;
Object.assign(characteristic, entry);
}
// eslint-disable-next-line @typescript-eslint/no-empty-function
await connection.disconnect().catch(() => { });
return database;
}
catch (err) {
// eslint-disable-next-line @typescript-eslint/no-empty-function
await connection.disconnect().catch(() => { });
throw err;
}
});
}
/**
* Read a set of characteristics.
*
* @param {Object[]} characteristics - Characteristics to get, as a list of
* objects: {characteristicUuid, serviceUuid, iid, format}
* @param {Object?} options - Options dictating what metadata to fetch
* @param {Object?} connection - Existing GattConnection object, must already
* be paired and verified
* @returns {Promise} Promise which resolves to the JSON document.
*/
getCharacteristics(characteristics, options = {}, connection = null) {
const skipQueue = connection !== null;
const fn = async () => {
options = Object.assign({
meta: false,
perms: false,
type: false,
ev: false,
extra: false,
}, options);
const cList = [];
let needToClose = false;
try {
if (!connection) {
needToClose = true;