@ermitsrl/bluetooth-le
Version:
Capacitor plugin for Bluetooth Low Energy
331 lines • 14.6 kB
JavaScript
import { WebPlugin } from '@capacitor/core';
import { hexStringToDataView, mapToObject, webUUIDToString } from './conversion';
import { runWithTimeout } from './timeout';
export class BluetoothLeWeb extends WebPlugin {
constructor() {
super(...arguments);
this.deviceMap = new Map();
this.discoveredDevices = new Map();
this.scan = null;
this.DEFAULT_CONNECTION_TIMEOUT = 10000;
this.onAdvertisementReceivedCallback = this.onAdvertisementReceived.bind(this);
this.onDisconnectedCallback = this.onDisconnected.bind(this);
this.onCharacteristicValueChangedCallback = this.onCharacteristicValueChanged.bind(this);
}
async initialize() {
if (typeof navigator === 'undefined' || !navigator.bluetooth) {
throw this.unavailable('Web Bluetooth API not available in this browser.');
}
const isAvailable = await navigator.bluetooth.getAvailability();
if (!isAvailable) {
throw this.unavailable('No Bluetooth radio available.');
}
}
async isEnabled() {
// not available on web
return { value: true };
}
async enable() {
throw this.unavailable('enable is not available on web.');
}
async disable() {
throw this.unavailable('disable is not available on web.');
}
async startEnabledNotifications() {
// not available on web
}
async stopEnabledNotifications() {
// not available on web
}
async isLocationEnabled() {
throw this.unavailable('isLocationEnabled is not available on web.');
}
async openLocationSettings() {
throw this.unavailable('openLocationSettings is not available on web.');
}
async openBluetoothSettings() {
throw this.unavailable('openBluetoothSettings is not available on web.');
}
async openAppSettings() {
throw this.unavailable('openAppSettings is not available on web.');
}
async setDisplayStrings() {
// not available on web
}
async requestDevice(options) {
const filters = this.getFilters(options);
const device = await navigator.bluetooth.requestDevice({
filters: filters.length ? filters : undefined,
optionalServices: options === null || options === void 0 ? void 0 : options.optionalServices,
acceptAllDevices: filters.length === 0,
});
this.deviceMap.set(device.id, device);
const bleDevice = this.getBleDevice(device);
return bleDevice;
}
async requestLEScan(options) {
this.requestBleDeviceOptions = options;
const filters = this.getFilters(options);
await this.stopLEScan();
this.discoveredDevices = new Map();
navigator.bluetooth.removeEventListener('advertisementreceived', this.onAdvertisementReceivedCallback);
navigator.bluetooth.addEventListener('advertisementreceived', this.onAdvertisementReceivedCallback);
this.scan = await navigator.bluetooth.requestLEScan({
filters: filters.length ? filters : undefined,
acceptAllAdvertisements: filters.length === 0,
keepRepeatedDevices: options === null || options === void 0 ? void 0 : options.allowDuplicates,
});
}
onAdvertisementReceived(event) {
var _a, _b;
const deviceId = event.device.id;
this.deviceMap.set(deviceId, event.device);
const isNew = !this.discoveredDevices.has(deviceId);
if (isNew || ((_a = this.requestBleDeviceOptions) === null || _a === void 0 ? void 0 : _a.allowDuplicates)) {
this.discoveredDevices.set(deviceId, true);
const device = this.getBleDevice(event.device);
const result = {
device,
localName: device.name,
rssi: event.rssi,
txPower: event.txPower,
manufacturerData: mapToObject(event.manufacturerData),
serviceData: mapToObject(event.serviceData),
uuids: (_b = event.uuids) === null || _b === void 0 ? void 0 : _b.map(webUUIDToString),
};
this.notifyListeners('onScanResult', result);
}
}
async stopLEScan() {
var _a;
if ((_a = this.scan) === null || _a === void 0 ? void 0 : _a.active) {
this.scan.stop();
}
this.scan = null;
}
async getDevices(_options) {
const devices = await navigator.bluetooth.getDevices();
const bleDevices = devices.map((device) => {
this.deviceMap.set(device.id, device);
const bleDevice = this.getBleDevice(device);
return bleDevice;
});
return { devices: bleDevices };
}
async getConnectedDevices(_options) {
const devices = await navigator.bluetooth.getDevices();
const bleDevices = devices
.filter((device) => {
var _a;
return (_a = device.gatt) === null || _a === void 0 ? void 0 : _a.connected;
})
.map((device) => {
this.deviceMap.set(device.id, device);
const bleDevice = this.getBleDevice(device);
return bleDevice;
});
return { devices: bleDevices };
}
async scanNetworks(_options) {
throw this.unavailable('scanNetworks is not available on web.');
}
async provision(_options) {
throw this.unavailable('provision is not available on web.');
}
async connect(options) {
var _a, _b;
const device = this.getDeviceFromMap(options.deviceId);
device.removeEventListener('gattserverdisconnected', this.onDisconnectedCallback);
device.addEventListener('gattserverdisconnected', this.onDisconnectedCallback);
const timeoutError = Symbol();
if (device.gatt === undefined) {
throw new Error('No gatt server available.');
}
try {
const timeout = (_a = options.timeout) !== null && _a !== void 0 ? _a : this.DEFAULT_CONNECTION_TIMEOUT;
await runWithTimeout(device.gatt.connect(), timeout, timeoutError);
}
catch (error) {
// cancel pending connect call, does not work yet in chromium because of a bug:
// https://bugs.chromium.org/p/chromium/issues/detail?id=684073
await ((_b = device.gatt) === null || _b === void 0 ? void 0 : _b.disconnect());
if (error === timeoutError) {
throw new Error('Connection timeout');
}
else {
throw error;
}
}
}
onDisconnected(event) {
const deviceId = event.target.id;
const key = `disconnected|${deviceId}`;
this.notifyListeners(key, null);
}
async createBond(_options) {
throw this.unavailable('createBond is not available on web.');
}
async isBonded(_options) {
throw this.unavailable('isBonded is not available on web.');
}
async setPin(_options) {
throw this.unavailable('setPin is not available on web.');
}
async disconnect(options) {
var _a;
(_a = this.getDeviceFromMap(options.deviceId).gatt) === null || _a === void 0 ? void 0 : _a.disconnect();
}
async getServices(options) {
var _a, _b;
const services = (_b = (await ((_a = this.getDeviceFromMap(options.deviceId).gatt) === null || _a === void 0 ? void 0 : _a.getPrimaryServices()))) !== null && _b !== void 0 ? _b : [];
const bleServices = [];
for (const service of services) {
const characteristics = await service.getCharacteristics();
const bleCharacteristics = [];
for (const characteristic of characteristics) {
bleCharacteristics.push({
uuid: characteristic.uuid,
properties: this.getProperties(characteristic),
descriptors: await this.getDescriptors(characteristic),
});
}
bleServices.push({ uuid: service.uuid, characteristics: bleCharacteristics });
}
return { services: bleServices };
}
async getDescriptors(characteristic) {
try {
const descriptors = await characteristic.getDescriptors();
return descriptors.map((descriptor) => ({
uuid: descriptor.uuid,
}));
}
catch (_a) {
return [];
}
}
getProperties(characteristic) {
return {
broadcast: characteristic.properties.broadcast,
read: characteristic.properties.read,
writeWithoutResponse: characteristic.properties.writeWithoutResponse,
write: characteristic.properties.write,
notify: characteristic.properties.notify,
indicate: characteristic.properties.indicate,
authenticatedSignedWrites: characteristic.properties.authenticatedSignedWrites,
reliableWrite: characteristic.properties.reliableWrite,
writableAuxiliaries: characteristic.properties.writableAuxiliaries,
};
}
async getCharacteristic(options) {
var _a;
const service = await ((_a = this.getDeviceFromMap(options.deviceId).gatt) === null || _a === void 0 ? void 0 : _a.getPrimaryService(options === null || options === void 0 ? void 0 : options.service));
return service === null || service === void 0 ? void 0 : service.getCharacteristic(options === null || options === void 0 ? void 0 : options.characteristic);
}
async getDescriptor(options) {
const characteristic = await this.getCharacteristic(options);
return characteristic === null || characteristic === void 0 ? void 0 : characteristic.getDescriptor(options === null || options === void 0 ? void 0 : options.descriptor);
}
async discoverServices(_options) {
throw this.unavailable('discoverServices is not available on web.');
}
async readRssi(_options) {
throw this.unavailable('readRssi is not available on web.');
}
async read(options) {
const characteristic = await this.getCharacteristic(options);
const value = await (characteristic === null || characteristic === void 0 ? void 0 : characteristic.readValue());
return { value };
}
async write(options) {
const characteristic = await this.getCharacteristic(options);
let dataView;
if (typeof options.value === 'string') {
dataView = hexStringToDataView(options.value);
}
else {
dataView = options.value;
}
await (characteristic === null || characteristic === void 0 ? void 0 : characteristic.writeValueWithResponse(dataView));
}
async writeWithoutResponse(options) {
const characteristic = await this.getCharacteristic(options);
let dataView;
if (typeof options.value === 'string') {
dataView = hexStringToDataView(options.value);
}
else {
dataView = options.value;
}
await (characteristic === null || characteristic === void 0 ? void 0 : characteristic.writeValueWithoutResponse(dataView));
}
async readDescriptor(options) {
const descriptor = await this.getDescriptor(options);
const value = await (descriptor === null || descriptor === void 0 ? void 0 : descriptor.readValue());
return { value };
}
async writeDescriptor(options) {
const descriptor = await this.getDescriptor(options);
let dataView;
if (typeof options.value === 'string') {
dataView = hexStringToDataView(options.value);
}
else {
dataView = options.value;
}
await (descriptor === null || descriptor === void 0 ? void 0 : descriptor.writeValue(dataView));
}
async startNotifications(options) {
const characteristic = await this.getCharacteristic(options);
characteristic === null || characteristic === void 0 ? void 0 : characteristic.removeEventListener('characteristicvaluechanged', this.onCharacteristicValueChangedCallback);
characteristic === null || characteristic === void 0 ? void 0 : characteristic.addEventListener('characteristicvaluechanged', this.onCharacteristicValueChangedCallback);
await (characteristic === null || characteristic === void 0 ? void 0 : characteristic.startNotifications());
}
onCharacteristicValueChanged(event) {
var _a, _b;
const characteristic = event.target;
const key = `notification|${(_a = characteristic.service) === null || _a === void 0 ? void 0 : _a.device.id}|${(_b = characteristic.service) === null || _b === void 0 ? void 0 : _b.uuid}|${characteristic.uuid}`;
this.notifyListeners(key, {
value: characteristic.value,
});
}
async stopNotifications(options) {
const characteristic = await this.getCharacteristic(options);
await (characteristic === null || characteristic === void 0 ? void 0 : characteristic.stopNotifications());
}
getFilters(options) {
var _a;
const filters = [];
for (const service of (_a = options === null || options === void 0 ? void 0 : options.services) !== null && _a !== void 0 ? _a : []) {
filters.push({
services: [service],
name: options === null || options === void 0 ? void 0 : options.name,
namePrefix: options === null || options === void 0 ? void 0 : options.namePrefix,
});
}
if (((options === null || options === void 0 ? void 0 : options.name) || (options === null || options === void 0 ? void 0 : options.namePrefix)) && filters.length === 0) {
filters.push({
name: options.name,
namePrefix: options.namePrefix,
});
}
return filters;
}
getDeviceFromMap(deviceId) {
const device = this.deviceMap.get(deviceId);
if (device === undefined) {
throw new Error('Device not found. Call "requestDevice", "requestLEScan" or "getDevices" first.');
}
return device;
}
getBleDevice(device) {
var _a;
const bleDevice = {
deviceId: device.id,
// use undefined instead of null if name is not available
name: (_a = device.name) !== null && _a !== void 0 ? _a : undefined,
};
return bleDevice;
}
}
//# sourceMappingURL=web.js.map