@iotize/device-com-ble.cordova
Version:
Bluetooth Low Energy (BLE) for IoTize modules Plugin
661 lines (651 loc) • 26.3 kB
JavaScript
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(`/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