UNPKG

@iotize/tap

Version:

IoTize Device client for Javascript

652 lines (641 loc) 25.7 kB
import { bufferToHexString } from '@iotize/common/byte-converter'; import { promiseSerial } from '@iotize/common/promise'; import { QueueComProtocol } from '@iotize/tap/protocol/core'; import { Checksum } from '@iotize/tap/client/impl'; import { createDebugger } from '@iotize/common/debug'; import { CodeError } from '@iotize/common/error'; import { ConnectionState, ComProtocol } from '@iotize/tap/protocol/api'; import { defer } from 'rxjs'; import { filter, share, first, map } from 'rxjs/operators'; const BleConfig = { maxPacketLengthWithoutOffset: 19, services: { upgrade: { service: '9e5d1e47-5c13-43a0-8635-82ad38a1386f', charac: '347f7608-2e2d-47eb-913b-75d4edc4de3b', }, lwm2m: { // // SPP over BLE Service UUID (128-bit) service: '6c7b16c2-2a5b-8c9f-cf42-d31425470e7b', charac: 'cc5c5491-b3be-9287-cb42-f7a6a29a50d5', }, fastLwm2m: { service: '7c7b16c2-2a5b-8c9f-cf42-d31425470e7b', charac: 'dc5c5491-b3be-9287-cb42-f7a6a29a50d5', }, standardClientConfig: { service: '00002902-0000-1000-8000-00805f9b34fb', }, }, }; /** * Created by IoTize on 19/04/2018. * <p> * Glue packet chunk to build the original ble packet * @param {number} bufferLength * @class */ class BLEPacketBuilder { constructor(bufferLength) { this.bufferOffset = 0; this.dataLength = 0; this.data = new Uint8Array(bufferLength); this.reset(); } /** * Append new chunk of data into the builder * @param {Array} dataChunk the data chunk. The first byte must be the position offset of this chunk in the buffer (in bytes) */ append(dataChunk) { if (!dataChunk || dataChunk.length < 2) { return; } this.bufferOffset = (dataChunk[0] & 0xff) - 1; if (this.bufferOffset + dataChunk.length > this.data.length) { throw new Error(// TODO ble error `Buffer size exceeded. Maximum size is ${this.data.length} byte(s).`); } for (let i = 1; i < dataChunk.length; i++) { this.data[this.bufferOffset + i] = dataChunk[i]; } this.dataLength += dataChunk.length - 1; } reset() { this.dataLength = 0; this.bufferOffset = 0; } hasAllChunks() { return this.bufferOffset === -1; } isChecksumValid() { let computedChecksum = this.getComputedChecksum(); let expectedChecksum = this.getExpectedChecksum(); return expectedChecksum === computedChecksum; } getComputedChecksum() { return Checksum.compute(this.data.subarray(0, this.dataLength - 1)) & 0xff; } getExpectedChecksum() { return this.data[this.dataLength - 1]; } /** * @return {Array} the result data without the checksum */ getData() { return this.data.slice(0, this.dataLength - 1); // let result: number[] = (s => { let a = []; while (s-- > 0) a.push(0); return a; })(this.dataLength - 1); // /* arraycopy */((srcPts, srcOff, dstPts, dstOff, size) => { if (srcPts !== dstPts || dstOff >= srcOff + size) { while (--size >= 0) dstPts[dstOff++] = srcPts[srcOff++]; } else { let tmp = srcPts.slice(srcOff, srcOff + size); for (let i = 0; i < size; i++) dstPts[dstOff++] = tmp[i]; } })(this.data, 0, result, 0, result.length); // return result; } /** * @return {Array} the buffer (with the checksum) */ getBuffer() { return this.data; } } /** * Created by IoTize on 19/04/2018. * * Split data into chunks with a size of maxPacketSize + 1 * @param {Array} data * @param {number} maxPacketSize * @class */ class BLEPacketSplitter { /** * * @param data * @param maxPacketSize */ constructor(data, maxPacketSize) { this.lastPacketSize = 0; this.maxPacketSize = 0; this.currentPacketIndex = 0; if (maxPacketSize < 1) { throw new Error('Packet size must be greater than 0'); } this.data = data; this.maxPacketSize = maxPacketSize; this.currentPacketIndex = this.getTotalNumberOfPacket() - 1; this.lastPacketSize = this.getLastPacketSize(); } /** * Create a BLEPacketSplitter instance from data and add the checksum at the end * @param {Array} data body data * @param {number} maxPacketSize packet size * @return {BLEPacketSplitter} the new instance */ static wrapWithChecksum(data, maxPacketSize) { var checkSum = BLEPacketSplitter.computeChecksum(data); var wrappedData = new Uint8Array(data.length + 1); wrappedData.set(data); wrappedData.set([checkSum & 0xff], data.length); return new BLEPacketSplitter(wrappedData, maxPacketSize); } getTotalNumberOfPacket() { return (((this.data.length / this.maxPacketSize) | 0) + (this.data.length % this.maxPacketSize === 0 ? 0 : 1)); } getLastPacketSize() { let result = this.data.length % this.maxPacketSize; if (result === 0) { result = this.maxPacketSize; } return result; } getPackets() { let packets = []; while (this.hasNextPacket()) { packets.push(this.getNextPacket()); } return packets; } getNextPacket() { let packetSize; let offset; if (this.currentPacketIndex > 0) { offset = this.lastPacketSize + (this.currentPacketIndex - 1) * this.maxPacketSize; packetSize = this.maxPacketSize; } else { offset = 0; packetSize = this.lastPacketSize; } let packet = new Uint8Array(packetSize + 1); packet[0] = offset | 0; for (let i = 0; i < packetSize; i++) { packet[i + 1] = this.data[offset + i]; } this.currentPacketIndex--; return packet; } hasNextPacket() { return this.currentPacketIndex >= 0; } static computeChecksum(data) { return Checksum.compute(data) & 0xff; } getTotalSize() { return this.data.length; } } const prefix = '@iotize/tap/protocol/ble'; const debug = createDebugger(prefix); class BleComError extends CodeError { static invalidBleChunkChecksum(packetBuilder) { return InvalidBleFrameChecksum.create(packetBuilder); } static writeSizeAboveMTU(data, mtu) { return new BleComError(BleComError.Code.BleWriteSizeAboveMTU, `Cannot write ${data.length} bytes as it's more than the Maximal Transfert Unit (${mtu})`); } static serviceNotFound(uuid) { return new BleComError(BleComError.Code.BleGattServiceNotFound, `Bluetooth service with id ${uuid} not found`); } static charcacteristicNotFound(uuid) { return new BleComError(BleComError.Code.BleGattCharacteristicNotFound, `Bluetooth characteristic with id ${uuid} not found`); } static bleNotAvailable(msg = 'BLE is not available on your device') { return new BleComError(BleComError.Code.NotAvailable, msg); } static gattServerNotAvailable() { return new BleComError(BleComError.Code.GATTServerNotAvailable, `Generic Attribute Profile (GATT) server is not available`); } static gattServerConnectionFailed() { return new BleComError(BleComError.Code.GATTServerConnectionFailed, `Gatt server connection failed`); } constructor(code, msg) { super(msg, code); } } class InvalidBleFrameChecksum extends BleComError { constructor(packetBuilder) { super(BleComError.Code.InvalidChecksum, `Invalid checksum`); this.packetBuilder = packetBuilder; } static create(packetBuilder) { return new InvalidBleFrameChecksum(packetBuilder); } } (function (BleComError) { let Code; (function (Code) { Code["InvalidChecksum"] = "InvalidChecksum"; Code["BleGattServiceNotFound"] = "BleGattServiceNotFound"; Code["BleGattCharacteristicNotFound"] = "BleGattCharacteristicNotFound"; Code["BleWriteSizeAboveMTU"] = "BleWriteSizeAboveMTU"; Code["GATTServerNotAvailable"] = "BleComErrorGATTServerNotAvailable"; Code["GATTServerConnectionFailed"] = "BleComErrorGATTServerConnectionFailed"; Code["NotAvailable"] = "BleComErrorNotAvailable"; })(Code = BleComError.Code || (BleComError.Code = {})); })(BleComError || (BleComError = {})); var __awaiter$1 = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; /** * @deprecated use UniversalBleProtocolAdpater * Abstract class for BLE communication * * With ble communication, data is split into sub packets. * This class handles creation of packet chunks. * * - You must only implement the function to send one packet chunk writeLwm2mPacketChunk() * - */ class AbstractBleProtocol extends QueueComProtocol { constructor(bleOptions = { mtu: BleConfig.maxPacketLengthWithoutOffset + 1, }) { super(); this.bleOptions = bleOptions; } onNewLwm2mMessage(data) { if (this._readResolve) { debug('onNewLwm2mMessage()', bufferToHexString(data)); this._readResolve(data); } else { console.warn(`No listener for new message... 0x${bufferToHexString(data)}`); this._pendingRead = data; } } onNewLwm2mMessageError(err) { console.error('Error lwm2m response...', err); } onLwm2mDataChunkError(error) { if (this._readReject) { this._readReject(error); } else { console.warn('No listener for read message error...', error); } } getLwm2mResponsePromise() { debug('getLwm2mResponsePromise'); if (this._pendingRead) { let data = this._pendingRead; this._pendingRead = undefined; return Promise.resolve(data); } if (!this._readPromise) { this.createLwm2mResponsePromise(); } return this._readPromise; } createLwm2mResponsePromise() { debug('createLwm2mResponsePromise'); this._readPromise = new Promise((resolve, reject) => { this._readResolve = resolve; this._readReject = reject; }); return this._readPromise; } // send(data: Uint8Array): Observable<Uint8Array> { // debug("BLEProtocol:send() " + bufferToHexString(data)); // return from( // this // .write(data) // .then(() => this.waitForLwm2mResponse()) // .catch((err) => { // console.error("ERROR SEND " + err); // throw err; // }) // ); // } read() { debug('read()...'); if (this.packetBuilder) { if (this.packetBuilder.hasAllChunks()) { let data = this.packetBuilder.getData(); this.packetBuilder = undefined; return Promise.resolve(data); } // TODO manage error console.warn('AbstractBleProtocol::read() There is already a read operation in progress but a new call to ::read has been made.'); } return this.getLwm2mResponsePromise() .then((response) => { this.packetBuilder = undefined; return response; }) .catch((err) => { this.packetBuilder = undefined; throw err; }); } write(data) { return __awaiter$1(this, void 0, void 0, function* () { if (this.packetSplitter != null) { console.warn('BLEProtocol', 'write', 'There are already data being sent but a new call to ::write has been made'); } this.packetSplitter = BLEPacketSplitter.wrapWithChecksum(data, this.bleOptions.mtu - 1); let packets = this.packetSplitter.getPackets(); let promise; this._pendingRead = undefined; this.packetBuilder = new BLEPacketBuilder(AbstractBleProtocol.RECEIVED_BUFFER_LENGTH); if (packets.length > 0) { // this.createLwm2mResponsePromise() // .catch((err) => { // this.onNewLwm2mMessageError(err); // }) promise = promiseSerial(packets, (packet, index) => { return this.writeLwm2mPacketChunk(packet).then((value) => { debug(`Done sending packet ${index + 1}/${packets.length}`); return value; }); }); // packets.forEach((value: Uint8Array, index: number) => { // if (!promise){ // promise = this.writeLwm2mPacketChunk(packets[index]); // } // else{ // promise = promise.then(() => this.writeLwm2mPacketChunk(packets[index])) // } // // promise.then(() => { // // debug("BLEProtocol", "write", `Done sending packet ${index+1}/${packets.length}`); // // }); // }); } else { console.warn('BLEProtocol', 'write', `Nothing to write...`); promise = Promise.resolve(); } return promise .then(() => { this.packetSplitter = undefined; }) .catch((err) => { this.packetSplitter = undefined; throw err; }); }); } /** * * @param data data chunk */ onNewLwm2mPacket(data) { if (!this.packetBuilder) { debug(`Ignoring lwm2m packet 0x${bufferToHexString(data)} as not request has been made yet`); return; } this.packetBuilder.append(data); if (this.packetBuilder.hasAllChunks()) { if (this.packetBuilder.isChecksumValid()) { this.onNewLwm2mMessage(this.packetBuilder.getData()); } else { let error = InvalidBleFrameChecksum.create(this.packetBuilder); this.onLwm2mDataChunkError(error); } } } } // TODO set an option instead ? AbstractBleProtocol.RECEIVED_BUFFER_LENGTH = 255; function sanitizeUUID(input) { return input.replace(/\-/g, ''); } var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; const DEFAULT_BLE_OPTIONS = { maximumBufferLength: 512, waitForWriteAcknowledge: true, preferedComServiceType: 'large-frame', sanitizeUUID: false, }; /** * BLE communication * * With ble communication, data is split into sub packets. * This class handles creation of packet chunks. * * - You must only implement the function to send one packet chunk writeLwm2mPacketChunk() * - */ class UniversalBleProtocolAdapter extends QueueComProtocol { constructor(peripheral, bleOptions = {}) { super(); this.peripheral = peripheral; this._useSplitter = true; this._unexpectedBleDisconnection = this.peripheral.stateChange.pipe(filter((newState) => { const currentProtocolConnectionState = this.getConnectionState(); return (newState === ConnectionState.DISCONNECTED && currentProtocolConnectionState !== ConnectionState.DISCONNECTING && currentProtocolConnectionState !== ConnectionState.DISCONNECTED); }), share()); this.bleOptions = Object.assign(Object.assign({}, DEFAULT_BLE_OPTIONS), bleOptions); this.options.connect.timeout = 8000; this.options.send.timeout = 4000; this.options.disconnect.timeout = 8000; this._unexpectedBleDisconnection.subscribe(() => __awaiter(this, void 0, void 0, function* () { debug('unexpected BLE disconnection detected. Running proper BLE disconnection process'); yield this.disconnect() .toPromise() .catch((err) => { debug(`Proper BLE disconnection process failed with error: ${err.message}`); }); })); } get lwm2mCharc() { if (!this._lwm2mCharc) { this.setConnectionState(ConnectionState.DISCONNECTED); throw ComProtocol.Errors.notConnected({ protocol: this, }); } return this._lwm2mCharc; } sanitizeUUID(uuid) { return this.bleOptions.sanitizeUUID ? sanitizeUUID(uuid) : uuid; } _connect() { return defer(() => __awaiter(this, void 0, void 0, function* () { try { yield this.peripheral.connect(); this._lwm2mCharc = yield this.setupLwm2mCharacteristic(); } catch (err) { try { yield this.peripheral.disconnect(); } catch (err) { debug('Failed to propertly disconnect after connection failed', err.message); } throw err; } })).pipe(share()); } _disconnect() { return defer(() => __awaiter(this, void 0, void 0, function* () { if (this.peripheral) { try { yield this.peripheral.disconnect(); } catch (err) { console.warn(`Failed to properly disconnect from peripheral`, err); } } this._lwm2mCharc = undefined; })).pipe(share()); } setupLwm2mCharacteristic() { return __awaiter(this, void 0, void 0, function* () { const lwm2mServiceUUIDs = [ this.sanitizeUUID(BleConfig.services.lwm2m.service), ]; if (this.bleOptions.preferedComServiceType === 'large-frame') { lwm2mServiceUUIDs.push(this.sanitizeUUID(BleConfig.services.fastLwm2m.service)); } const serviceMap = yield this.peripheral.discoverServices(lwm2mServiceUUIDs); debug('Found services ', Object.keys(serviceMap).join(', '), 'asked for services: ', lwm2mServiceUUIDs.join(', ')); const charac = yield this._selectLwm2mCharacteristic(serviceMap); yield charac.enableNotifications(true); return charac; }); } _selectLwm2mCharacteristic(serviceMap) { return __awaiter(this, void 0, void 0, function* () { const largeFrameServiceUUID = this.sanitizeUUID(BleConfig.services.fastLwm2m.service); const legacyLwm2mServiceUUID = this.sanitizeUUID(BleConfig.services.lwm2m.service); if (this.bleOptions.preferedComServiceType === 'legacy' && serviceMap[legacyLwm2mServiceUUID]) { debug('Force usage of legacy lwm2m characteristic UUID: ' + legacyLwm2mServiceUUID); return this._getLegacyLwm2mCharacteristic(serviceMap[legacyLwm2mServiceUUID]); } else if (serviceMap[largeFrameServiceUUID]) { debug('Found fast lwm2m characteristic UUID: ' + largeFrameServiceUUID); return this._getLargeFrameLwm2mCharacteristic(serviceMap[largeFrameServiceUUID]); } else if (serviceMap[legacyLwm2mServiceUUID]) { debug('Found legacy lwm2m characteristic UUID: ' + legacyLwm2mServiceUUID); return this._getLegacyLwm2mCharacteristic(serviceMap[legacyLwm2mServiceUUID]); } else { debug(`No LwM2M service found. Available services: ${Object.keys(serviceMap).join(', ')}`); throw BleComError.serviceNotFound(legacyLwm2mServiceUUID); } }); } _getLargeFrameLwm2mCharacteristic(service) { return __awaiter(this, void 0, void 0, function* () { this._useSplitter = false; return yield service.getCharacteristic(this.sanitizeUUID(BleConfig.services.fastLwm2m.charac)); }); } _getLegacyLwm2mCharacteristic(service) { return __awaiter(this, void 0, void 0, function* () { this._useSplitter = true; return yield service.getCharacteristic(this.sanitizeUUID(BleConfig.services.lwm2m.charac)); }); } read() { return __awaiter(this, void 0, void 0, function* () { // debug('read()...'); if (!this._readPromise) { this._readPromise = this._createReadPromise(); } return this._readPromise; }); } readUnit() { return __awaiter(this, void 0, void 0, function* () { try { const result = yield this.lwm2mCharc.data .pipe(first(), map((info) => info.data)) .toPromise(); if (!result) { return new Uint8Array(); } return result; } catch (err) { return Promise.reject(err); } }); } write(data) { return __awaiter(this, void 0, void 0, function* () { // debug('write()...'); this._readPromise = this._createReadPromise(); if (this.useSplitter) { const chunks = BLEPacketSplitter.wrapWithChecksum(data, this.chunkSize).getPackets(); for (const chunk of chunks) { yield this.writeUnit(chunk); } } else { return this.writeUnit(data); } }); } get useSplitter() { return this._useSplitter; } get chunkSize() { return BleConfig.maxPacketLengthWithoutOffset; } writeUnit(data) { return __awaiter(this, void 0, void 0, function* () { try { if (this.bleOptions.mtu !== undefined && data.length > this.bleOptions.mtu) { throw BleComError.writeSizeAboveMTU(data, this.bleOptions.mtu); } if (this.bleOptions.waitForWriteAcknowledge) { return this.lwm2mCharc.write(data, true); } else { this.lwm2mCharc.write(data, true).catch((err) => { console.warn(`Write error ignored`, err); }); } } catch (err) { return Promise.reject(err); } }); } _createReadPromise() { return __awaiter(this, void 0, void 0, function* () { // debug(`_createReadPromise()`); if (this.useSplitter) { const packetBuilder = new BLEPacketBuilder(this.bleOptions.maximumBufferLength); while (!packetBuilder.hasAllChunks()) { const chunk = yield this.readUnit(); packetBuilder.append(chunk); } if (packetBuilder.isChecksumValid()) { return packetBuilder.getData(); } else { throw BleComError.invalidBleChunkChecksum(packetBuilder); } } else { return this.readUnit(); } }); } } /** * Generated bundle index. Do not edit. */ export { AbstractBleProtocol, BLEPacketBuilder, BLEPacketSplitter, BleComError, BleConfig, DEFAULT_BLE_OPTIONS, InvalidBleFrameChecksum, UniversalBleProtocolAdapter }; //# sourceMappingURL=iotize-tap-protocol-ble-common.js.map