@iotize/tap
Version:
IoTize Device client for Javascript
652 lines (641 loc) • 25.7 kB
JavaScript
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