UNPKG

@iotize/device-com-ble.cordova

Version:

Bluetooth Low Energy (BLE) for IoTize modules Plugin

661 lines (651 loc) 26.3 kB
import { ConnectionState } from '@iotize/tap/protocol/api'; import { QueueComProtocol } from '@iotize/tap/protocol/core'; import { defer, throwError, BehaviorSubject, Subject } from 'rxjs'; import { tap, filter, first, catchError, distinctUntilChanged, shareReplay, map, switchMap } from 'rxjs/operators'; import { CodeError } from '@iotize/common/error'; import { createDebugger } from '@iotize/common/debug'; import { BleComError } from '@iotize/tap/protocol/ble/common'; import { hexStringToBuffer as hexStringToBuffer$1, bufferToHexString } from '@iotize/common/byte-converter'; import { safeEnumValue } from '@iotize/common/utility'; class CordovaBLEError extends CodeError { static invalidErrorResult(errObject) { return new CordovaBLEError(`Internal error: ble cordova plugin returned an unexpected error object`, CordovaBLEError.Code.InternalError); } static invalidErrorCode(errObject) { return new CordovaBLEError(`Internal error: ble cordova plugin returned an invalid error code ${errObject.code} with message ${errObject.message}`, CordovaBLEError.Code.InternalError); } static invalidNativeCallResult(result, cause) { return new CordovaBLEError(`Internal error: native call returned an invalid data type. ${cause.message}`, CordovaBLEError.Code.InternalError); } static isValidErrorCode(code) { return code in CordovaBLEError.Code; } static iotizeBLEMissing() { return new CordovaBLEError(`iotizeBLE global variable does not exist. Are you sure app is running inside a Cordova application ?`, CordovaBLEError.Code.InternalError); } } /* istanbul ignore next */ (function (CordovaBLEError) { let Code; (function (Code) { Code["InternalError"] = "CordovaBLEErrorInternalError"; Code["IllegalArgument"] = "CordovaBLEErrorIllegalArgument"; Code["BLENotAvailable"] = "CordovaBLEErrorBLENotAvailable"; Code["InvalidMacAddress"] = "InvalidMacAddressInvalidMacAddress"; Code["ConnectionError"] = "CordovaBLEErrorConnectionError"; Code["RequestError"] = "CordovaBLEErrorRequestError"; Code["LocationServiceDisabled"] = "CordovaBLEErrorLocationServiceDisabled"; Code["DisconnectError"] = "CordovaBLEErrorDisconnectError"; Code["IllegalAction"] = "CordovaBLEErrorIllegalAction"; Code["NotConnectedError"] = "CordovaBLEErrorNotConnectedError"; Code["StatusCodeError"] = "CordovaBLEErrorStatusCodeError"; })(Code = CordovaBLEError.Code || (CordovaBLEError.Code = {})); })(CordovaBLEError || (CordovaBLEError = {})); const debug = createDebugger(`@iotize/device-com-ble.cordova`); function getIoTizeBleCordovaPlugin() { if (typeof iotizeBLE !== "object") { throw CordovaBLEError.iotizeBLEMissing(); } return iotizeBLE; } function getScanRecordsFromBytes(bytes) { //LTV encoding const records = []; for (let i = 0; i < bytes.length; i++) { //first byte is length let size = bytes[i]; if (size == 0) { break; } let type = bytes[i + 1]; records.push({ type, data: bytes.slice(i + 2, i + size + 1) }); i += size; } return records; } var __awaiter$3 = (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()); }); }; class BLEComProtocol extends QueueComProtocol { constructor(deviceId, comProtocolOptions, cordovaInterfaceOverwrite) { super(); this.deviceId = deviceId; this.cordovaInterfaceOverwrite = cordovaInterfaceOverwrite; if (comProtocolOptions) { this.options = comProtocolOptions; } else { this.options.connect.timeout = 60000; } } /** * Lazy reference to iotizeBLE. * We don't want to reference iotizeBLE in constructor as it may be referenced * before cordova plugin is loaded */ get pluginInterface() { return this.cordovaInterfaceOverwrite || getIoTizeBleCordovaPlugin(); } _connect(options) { debug('_connect', options); return this.pluginInterface .connectAndDiscoverTapServices(this.deviceId) .pipe(tap((state) => { this.setConnectionState(state); }), filter((state) => state === ConnectionState.CONNECTED), first()); } _disconnect(options) { debug('_disconnect', options); return defer(() => this.pluginInterface.disConnect(this.deviceId)); } write(data) { return __awaiter$3(this, void 0, void 0, function* () { throw new Error('Method not implemented.'); }); } read() { return __awaiter$3(this, void 0, void 0, function* () { throw new Error('Method not implemented.'); }); } send(data, options) { return defer(() => this.pluginInterface.send(this.deviceId, data)).pipe(catchError((err) => { var _a; if (((_a = err) === null || _a === void 0 ? void 0 : _a.code) === CordovaBLEError.Code.NotConnectedError) { this.setConnectionState(ConnectionState.DISCONNECTED); } return throwError(err); })); } } var __awaiter$2 = (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()); }); }; class CordovaPeripheralAdapter { constructor(deviceId, cordovaPlugin = getIoTizeBleCordovaPlugin()) { this.deviceId = deviceId; this.cordovaPlugin = cordovaPlugin; this._stateChange = new BehaviorSubject(ConnectionState.DISCONNECTED); } get stateChange() { return this._stateChange.asObservable().pipe(distinctUntilChanged()); } get name() { // TODO device name ? return this.id; } get id() { return this.deviceId; } get state() { return ConnectionState[this._stateChange.value]; } discoverServices(serviceUUIDs) { return __awaiter$2(this, void 0, void 0, function* () { this.serviceListCache = yield this.cordovaPlugin.discoverServices(this.deviceId); const result = {}; if (serviceUUIDs) { for (const uuid of serviceUUIDs) { const servicDefinition = this.serviceListCache.find((def) => def.uuid === uuid); if (servicDefinition) { result[uuid] = new CordovaServiceAdapter(servicDefinition, this); } } } else { for (const serviceDefinition of this.serviceListCache) { result[serviceDefinition.uuid] = new CordovaServiceAdapter(serviceDefinition, this); } } return result; }); } connect() { var _a; return __awaiter$2(this, void 0, void 0, function* () { (_a = this.connectionStateSubscription) === null || _a === void 0 ? void 0 : _a.unsubscribe(); const connectObservable = this.cordovaPlugin .connect(this.deviceId) .pipe(shareReplay()); this.connectionStateSubscription = connectObservable.subscribe((state) => { this._stateChange.next(state); }); yield connectObservable .pipe(filter((state) => state === ConnectionState.CONNECTED), first()) .toPromise(); }); } disconnect() { var _a; return __awaiter$2(this, void 0, void 0, function* () { yield this.cordovaPlugin.disConnect(this.deviceId); this._stateChange.next(ConnectionState.DISCONNECTED); (_a = this.connectionStateSubscription) === null || _a === void 0 ? void 0 : _a.unsubscribe(); }); } close() { var _a; return __awaiter$2(this, void 0, void 0, function* () { yield this.cordovaPlugin.close(this.deviceId); this._stateChange.next(ConnectionState.DISCONNECTED); (_a = this.connectionStateSubscription) === null || _a === void 0 ? void 0 : _a.unsubscribe(); }); } getService(uuid) { return __awaiter$2(this, void 0, void 0, function* () { if (!this.serviceListCache) { yield this.discoverServices([uuid]); } const serviceDefinition = this.serviceListCache.find((s) => s.uuid === uuid); if (!serviceDefinition) { throw BleComError.serviceNotFound(uuid); } return new CordovaServiceAdapter(serviceDefinition, this); }); } } class CordovaServiceAdapter { constructor(config, peripheral) { this.config = config; this.peripheral = peripheral; } get uuid() { return this.config.uuid; } getCharacteristic(charcUUID) { var _a; return __awaiter$2(this, void 0, void 0, function* () { const characteristicDescription = (_a = this.config.characteristics) === null || _a === void 0 ? void 0 : _a.find((c) => c.uuid === charcUUID); if (!characteristicDescription) { throw BleComError.charcacteristicNotFound(charcUUID); } return new CordovaCharacteristicAdapter(this, characteristicDescription); }); } getCharacteristics() { return __awaiter$2(this, void 0, void 0, function* () { if (!this.config.characteristics) { return []; } return this.config.characteristics.map((c) => new CordovaCharacteristicAdapter(this, c)); }); } } class CordovaCharacteristicAdapter { constructor(service, config) { this.service = service; this.config = config; } get uuid() { return this.config.uuid; } get properties() { return this.config.properties; } get data() { return this.setupDataStreamIfRequired().pipe(map((data) => ({ data, isNotification: true, }))); } get deviceId() { return this.service.peripheral.deviceId; } get pluginInterface() { return this.service.peripheral.cordovaPlugin; } read() { return __awaiter$2(this, void 0, void 0, function* () { return this.pluginInterface.characteristicReadValue(this.deviceId, this.service.uuid, this.uuid); }); } getDescriptor() { return __awaiter$2(this, void 0, void 0, function* () { throw new Error(`Not implemented yet`); }); } getDescriptors() { return __awaiter$2(this, void 0, void 0, function* () { if (!this.config.descriptors) { return []; } return this.config.descriptors.map((descriptor) => new CordovaDescriptorAdapter(descriptor, this)); }); } write(data, writeWithoutResponse) { return __awaiter$2(this, void 0, void 0, function* () { if (writeWithoutResponse) { yield this.pluginInterface.characteristicWriteWithoutResponse(this.deviceId, this.service.uuid, this.uuid, data); } else { yield this.pluginInterface.characteristicWrite(this.deviceId, this.service.uuid, this.uuid, data); } return data; }); } enableNotifications(enabled) { return __awaiter$2(this, void 0, void 0, function* () { if (enabled) { this.setupDataStreamIfRequired(); yield this.pluginInterface.characteristicStartNotification(this.deviceId, this.service.uuid, this.uuid); } else { yield this.pluginInterface.characteristicStopNotification(this.deviceId, this.service.uuid, this.uuid); } }); } setupDataStreamIfRequired() { if (!this._dataStream) { this._dataStream = this.pluginInterface.characteristicChanged(this.deviceId, this.service.uuid, this.uuid); } return this._dataStream; } } class CordovaDescriptorAdapter { constructor(config, characteristic) { this.config = config; this.characteristic = characteristic; } get uuid() { return this.config.uuid; } readValue() { return __awaiter$2(this, void 0, void 0, function* () { throw new Error(`Reading descriptor value is not implemented yet`); }); } writeValue(data) { return __awaiter$2(this, void 0, void 0, function* () { throw new Error(`Writing descriptor value is not implemented yet`); }); } } // // Copyright 2018 IoTize SAS Inc. Licensed under the MIT license. // // scanner.ts // device-com-ble.cordova BLE Cordova Plugin // 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()); }); }; /** * BLE Tap scanner for cordova apps */ class BLEScanner { /** * * @param cordovaInterfaceOverwrite overwrite cordova interface. Used for testing */ constructor(cordovaInterfaceOverwrite, /** * Request device options used to filter scan results */ requestDeviceOptions) { this.cordovaInterfaceOverwrite = cordovaInterfaceOverwrite; this.requestDeviceOptions = requestDeviceOptions; this._results = new BehaviorSubject([]); this._scanning$ = new BehaviorSubject(false); } /** * Lazy reference to iotizeBLE. * We don't want to reference iotizeBLE in constructor as it may be referenced * before cordova plugin is loaded */ get cordovaInterface() { return this.cordovaInterfaceOverwrite || getIoTizeBleCordovaPlugin(); } get scanning() { return this._scanning$.asObservable(); } get isScanning() { return this._scanning$.value; } /** * Gets the observable on the devices$ Subject * @return */ get results() { return this._results.asObservable(); } /** * Launches the scan for BLE devices * Throws if BLE is not available */ start(options) { var _a; return __awaiter$1(this, void 0, void 0, function* () { (_a = this.scanSubscription) === null || _a === void 0 ? void 0 : _a.unsubscribe(); this.scanSubscription = undefined; this.clearResults(); const isAvailable = yield this.checkAvailable(); if (!isAvailable) { throw BleComError.bleNotAvailable(`BLE is not available. Make sure that BLE is enabled on your device`); } debug("Start Scanning ..."); this._scanning$.next(true); return new Promise((resolve, reject) => { this.scanSubscription = this.cordovaInterface .startScan(this.requestDeviceOptions) .subscribe((result) => { if (result == "Ok") { resolve(); return; } this.addOrRefreshDevice(result); }, (error) => { debug("Start scan failed with error: ", error); this.cordovaInterface .getLastError() .then((lasterror) => { debug("Last BLE error " + lasterror); }) .catch((err) => { debug("Cannot get last BLE error: ", err); }); reject(error); this._scanning$.next(false); }); }); }); } /** * */ stop() { var _a; return __awaiter$1(this, void 0, void 0, function* () { debug("Stop Scanning ..."); try { (_a = this.scanSubscription) === null || _a === void 0 ? void 0 : _a.unsubscribe(); this.scanSubscription = undefined; yield this.cordovaInterface.stopScan(); this._scanning$.next(false); return; } catch (err) { this._scanning$.next(false); throw err; } }); } /** * Returns true if this scanner is available */ checkAvailable() { return this.cordovaInterface.checkAvailable(); } get devices() { return this._results.value; } clearResults() { this._results.next([]); } addOrRefreshDevice(newDevice) { let storedDeviceIndex = this.devices.findIndex((entry) => entry.address == newDevice.address); if (storedDeviceIndex >= 0) { let storedDevice = this.devices[storedDeviceIndex]; if (storedDevice.name != newDevice.name || storedDevice.rssi != newDevice.rssi) { debug(`Updating device at index ${storedDeviceIndex}, name=${storedDevice.name} with rssi=${storedDevice.rssi}`); this.devices[storedDeviceIndex] = newDevice; // this.devices = [...this.devices]; this._results.next(this.devices); } } else { debug(`Adding new device name=${newDevice.name} with rssi=${newDevice.rssi}`); this.devices.push(newDevice); this._results.next(this.devices); } } } 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()); }); }; function hexStringToBuffer(str) { try { return hexStringToBuffer$1(str); } catch (err) { throw CordovaBLEError.invalidNativeCallResult(str, err); } } class IoTizeBleCordovaPlugin { checkAvailable() { return this.execSingleResult("checkAvailable", []); } requestEnableBle() { return this.execSingleResult("enable", []); } startScan(requestDeviceOptions) { return this.execMultipleResult("startScan", [ JSON.stringify(requestDeviceOptions), ]); } stopScan() { return this.execSingleResult("stopScan", []); } connect(deviceId, enableBleIfNot = true) { return this.askBleEnable(enableBleIfNot).pipe(switchMap(() => { return this.execMultipleResult("connect", [deviceId]).pipe(map((state) => { if (!(state in ConnectionState)) { console.warn(`Plugin native code returned an invalid connection state: "${state}".`); } return safeEnumValue(ConnectionState, state); })); })); } askBleEnable(askEnable) { return defer(() => __awaiter(this, void 0, void 0, function* () { if (askEnable) { if (!(yield this.checkAvailable())) { yield this.requestEnableBle(); } } })); } requestMTU(deviceId, mtu) { return this.execSingleResult("requestMTU", [deviceId, mtu]); } connectAndDiscoverTapServices(deviceId, enableBleIfNot = true) { return this.askBleEnable(enableBleIfNot).pipe(switchMap(() => { return this.execMultipleResult("connectAndDiscoverTapServices", [deviceId]).pipe(map((state) => { if (!(state in ConnectionState)) { console.warn(`Plugin native code returned an invalid connection state: "${state}".`); } return safeEnumValue(ConnectionState, state); })); })); } disConnect(deviceId) { return this.execSingleResult("disConnect", [deviceId]); } close(deviceId) { return this.execSingleResult("close", [deviceId]); } isConnected(deviceId) { return this.execSingleResult("isConnected", [deviceId]); } send(deviceId, data) { return __awaiter(this, void 0, void 0, function* () { const hexString = yield this.execSingleResult("sendRequest", [ deviceId, bufferToHexString(data), ]); return hexStringToBuffer(hexString); }); } getLastError() { return this.execSingleResult("getLastError", []); } characteristicStartNotification(deviceId, serviceId, characId) { return this.execSingleResult("characteristicStartNotification", [ deviceId, serviceId, characId, ]); } characteristicChanged(deviceId, serviceId, characId) { return this.execMultipleResult("characteristicChanged", [ deviceId, serviceId, characId, ]).pipe(map((hexString) => { return hexStringToBuffer(hexString); })); } characteristicStopNotification(deviceId, serviceId, characId) { return this.execSingleResult("characteristicStopNotification", [ deviceId, serviceId, characId, ]); } characteristicReadValue(deviceId, serviceId, characId) { return __awaiter(this, void 0, void 0, function* () { const hexString = yield this.execSingleResult("characteristicRead", [deviceId, serviceId, characId]); return hexStringToBuffer(hexString); }); } characteristicWrite(deviceId, serviceId, characId, data) { return this.execSingleResult("characteristicWrite", [ deviceId, serviceId, characId, bufferToHexString(data), ]); } characteristicWriteWithoutResponse(deviceId, serviceId, characId, data) { return this.execSingleResult("characteristicWriteWithoutResponse", [ deviceId, serviceId, characId, bufferToHexString(data), ]); } discoverServices(deviceId) { return __awaiter(this, void 0, void 0, function* () { const services = yield this.execSingleResult("discoverServices", [deviceId]); return services; }); } execSingleResult(methodName, args) { return new Promise((resolve, reject) => { cordova.exec(resolve, (errObject) => { reject(objectErrorToError(errObject)); }, "BLECom", methodName, args); }); } execMultipleResult(methodName, args) { const subject = new Subject(); cordova.exec((data) => { subject.next(data); }, (err) => { subject.error(objectErrorToError(err)); }, "BLECom", methodName, args); return subject.asObservable(); } } function objectErrorToError(errObject) { if (typeof errObject !== "object" || !errObject.code || !errObject.message) { return CordovaBLEError.invalidErrorResult(errObject); } if (!CordovaBLEError.isValidErrorCode(errObject.code)) { return CordovaBLEError.invalidErrorCode(errObject); } return new CordovaBLEError(errObject.message, CordovaBLEError.Code[errObject.code]); } /** * Generated bundle index. Do not edit. */ export { BLEComProtocol, BLEScanner, CordovaCharacteristicAdapter, CordovaPeripheralAdapter, CordovaServiceAdapter, getIoTizeBleCordovaPlugin, getScanRecordsFromBytes, IoTizeBleCordovaPlugin as ɵc }; //# sourceMappingURL=iotize-device-com-ble.cordova.js.map