web-bluetooth
Version:
Library for interacting with Bluetooth 4.0 devices through the browser.
301 lines (268 loc) • 14.2 kB
JavaScript
const bluetooth = require('./bluetoothMap');
const errorHandler = require('./errorHandler');
/** BluetoothDevice -
*
* @method connect - Establishes a connection with the device
* @method connected - checks apiDevice to see whether device is connected
* @method disconnect - terminates the connection with the device and pauses all data stream subscriptions
* @method getValue - reads the value of a specified characteristic
* @method writeValue - writes data to a specified characteristic of the device
* @method startNotifications - attempts to start notifications for changes to device values and attaches an event listener for each data transmission
* @method stopNotifications - attempts to stop previously started notifications for a provided characteristic
* @method addCharacteristic - adds a new characteristic object to bluetooth.gattCharacteristicsMapping
* @method _returnCharacteristic - _returnCharacteristic - returns the value of a cached or resolved characteristic or resolved characteristic
*
* @param {object} filters - collection of filters for device selectin. All filters are optional, but at least 1 is required.
* .name {string}
* .namePrefix {string}
* .uuid {string}
* .services {array}
* .optionalServices {array} - defaults to all available services, use an empty array to get no optional services
*
* @return {object} Returns a new instance of BluetoothDevice
*
*/
class BluetoothDevice {
constructor(requestParams) {
this.requestParams = requestParams;
this.apiDevice = null;
this.apiServer = null;
this.cache = {};
}
connected() {
return this.apiDevice ? this.apiDevice.gatt.connected : errorHandler('no_device');
}
/** connect - establishes a connection with the device
*
* NOTE: This method must be triggered by a user gesture to satisfy the native API's permissions
*
* @return {object} - native browser API device server object
*/
connect() {
const filters = this.requestParams;
const requestParams = { filters: [], };
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]/;
if(!Object.keys(filters).length) { return errorHandler('no_filters'); }
if (filters.name) requestParams.filters.push({ name: filters.name });
if (filters.namePrefix) requestParams.filters.push({ namePrefix: filters.namePrefix });
if (filters.uuid) {
if (!filters.uuid.match(uuidRegex)) {
errorHandler('uuid_error');
} else {
requestParams.filters.push({ uuid: filters.uuid });
}
}
if (filters.services) {
let services = [];
filters.services.forEach(service => {
if (!bluetooth.gattServiceList.includes(service)) {
console.warn(`${service} is not a valid service. Please check the service name.`);
} else { services.push(service); }
});
requestParams.filters.push({ services: services });
}
if (filters.optional_services) {
filters.optional_services.forEach(service => {
if (!bluetooth.gattServiceList.includes(service)) bluetooth.gattServiceList.push(service);
});
} else { requestParams.optionalServices = bluetooth.gattServiceList; }
return navigator.bluetooth.requestDevice(requestParams)
.then(device => {
this.apiDevice = device;
return device.gatt.connect();
}).then(server => {
this.apiServer = server;
return server;
}).catch(err => {
return errorHandler('user_cancelled', err);
});
}
/** disconnect - terminates the connection with the device and pauses all data stream subscriptions
* @return {boolean} - success
*
*/
disconnect() {
this.apiServer.connected ? this.apiServer.disconnect() : errorHandler('not_connected');
return this.apiServer.connected ? errorHandler('issue_disconnecting') : true;
}
/** getValue - reads the value of a specified characteristic
*
* @param {string} characteristic_name - GATT characteristic name
* @return {promise} - resolves with an object that includes key-value pairs for each of the properties
* successfully read and parsed from the device, as well as the
* raw value object returned by a native readValue request to the
* device characteristic.
*/
getValue(characteristic_name) {
if (!bluetooth.gattCharacteristicsMapping[characteristic_name]) {
return errorHandler('characteristic_error', null, characteristic_name);
}
const characteristicObj = bluetooth.gattCharacteristicsMapping[characteristic_name];
if (!characteristicObj.includedProperties.includes('read')) {
console.warn(`Attempting to access read property of ${characteristic_name},
which is not a included as a supported property of the
characteristic. Attempt will resolve with an object including
only a rawValue property with the native API return
for an attempt to readValue() of ${characteristic_name}.`);
}
return new Promise((resolve, reject) => { return resolve(this._returnCharacteristic(characteristic_name)); })
.then(characteristic => { return characteristic.readValue(); })
.then(value => {
const returnObj = characteristicObj.parseValue ? characteristicObj.parseValue(value) : {};
returnObj.rawValue = value;
return returnObj;
}).catch(err => { return errorHandler('read_error', err); });
}
/** writeValue - writes data to a specified characteristic of the device
*
* @param {string} characteristic_name - name of the GATT characteristic
* https://www.bluetooth.com/specifications/assigned-numbers/generic-attribute-profile
*
* @param {string|number} value - value to write to the requested device characteristic
*
*
* @return {boolean} - Result of attempt to write characteristic where true === successfully written
*/
writeValue(characteristic_name, value) {
if (!bluetooth.gattCharacteristicsMapping[characteristic_name]) {
return errorHandler('characteristic_error', null, characteristic_name);
}
const characteristicObj = bluetooth.gattCharacteristicsMapping[characteristic_name];
if (!characteristicObj.includedProperties.includes('write')) {
console.warn(`Attempting to access write property of ${characteristic_name},
which is not a included as a supported property of the
characteristic. Attempt will resolve with native API return
for an attempt to writeValue(${value}) to ${characteristic_name}.`);
}
return new Promise((resolve, reject) => { return resolve(this._returnCharacteristic(characteristic_name)); })
.then(characteristic => { return characteristic.writeValue(characteristicObj.prepValue ? characteristicObj.prepValue(value) : value); })
.then(changedChar => { return true; })
.catch(err => { return errorHandler('write_error', err, characteristic_name); });
}
/** startNotifications - attempts to start notifications for changes to device values and attaches an event listener for each data transmission
*
* @param {string} characteristic_name - GATT characteristic name
* @param {callback} transmissionCallback - callback function to apply to each event while notifications are active
*
* @return
*
*/
startNotifications(characteristic_name, transmissionCallback) {
if (!bluetooth.gattCharacteristicsMapping[characteristic_name]) { return errorHandler('characteristic_error', null, characteristic_name); }
const characteristicObj = bluetooth.gattCharacteristicsMapping[characteristic_name];
const primary_service_name = characteristicObj.primaryServices[0];
if (!characteristicObj.includedProperties.includes('notify')) {
console.warn(`Attempting to access notify property of ${characteristic_name},
which is not a included as a supported property of the
characteristic. Attempt will resolve with an object including
only a rawValue property with the native API return
for an attempt to startNotifications() for ${characteristic_name}.`);
}
return new Promise((resolve, reject) => { return resolve(this._returnCharacteristic(characteristic_name)); })
.then(characteristic => {
characteristic.startNotifications().then(() => {
this.cache[primary_service_name][characteristic_name].notifying = true;
return characteristic.addEventListener('characteristicvaluechanged', event => {
const eventObj = characteristicObj.parseValue ? characteristicObj.parseValue(event.target.value) : {};
eventObj.rawValue = event;
return transmissionCallback(eventObj);
});
});
}).catch(err => { return errorHandler('start_notifications_error', err, characteristic_name); });
}
/** stopNotifications - attempts to stop previously started notifications for a provided characteristic
*
* @param {string} characteristic_name - GATT characteristic name
*
* @return {boolean} success
*
*/
stopNotifications(characteristic_name) {
if (!bluetooth.gattCharacteristicsMapping[characteristic_name]) {
return errorHandler('characteristic_error', null, characteristic_name);
}
const characteristicObj = bluetooth.gattCharacteristicsMapping[characteristic_name];
const primary_service_name = characteristicObj.primaryServices[0];
if (this.cache[primary_service_name][characteristic_name].notifying) {
return new Promise((resolve, reject) => { return resolve(this._returnCharacteristic(characteristic_name)); })
.then(characteristic => {
characteristic.stopNotifications().then(() => {
this.cache[primary_service_name][characteristic_name].notifying = false;
return true;
});
}).catch(err => {
return errorHandler('stop_notifications_error', err, characteristic_name);
});
} else {
return errorHandler('stop_notifications_not_notifying', null, characteristic_name);
}
}
/**
* addCharacteristic - adds a new characteristic object to bluetooth.gattCharacteristicsMapping
*
* @param {string} characteristic_name - GATT characteristic name or other characteristic
* @param {string} primary_service_name - GATT primary service name or other parent service of characteristic
* @param {array} propertiesArr - Array of GATT properties as Strings
*
* @return {boolean} - Result of attempt to add characteristic where true === successfully added
*/
addCharacteristic(characteristic_name, primary_service_name, propertiesArr) {
if (bluetooth.gattCharacteristicsMapping[characteristic_name]) {
return errorHandler('add_characteristic_exists_error', null, characteristic_name);
}
if (!characteristic_name || characteristic_name.constructor !== String || !characteristic_name.length) {
return errorHandler('improper_characteristic_format', null, characteristic_name);
}
if (!bluetooth.gattCharacteristicsMapping[characteristic_name]) {
if (!primary_service_name || !propertiesArr) {
return errorHandler('new_characteristic_missing_params', null, characteristic_name);
}
if (primary_service_name.constructor !== String || !primary_service_name.length) {
return errorHandler('improper_service_format', null, primary_service_name);
}
if (propertiesArr.constructor !== Array || !propertiesArr.length) {
return errorHandler('improper_properties_format', null, propertiesArr);
}
console.warn(`${characteristic_name} is not yet fully supported.`);
bluetooth.gattCharacteristicsMapping[characteristic_name] = {
primaryServices: [primary_service_name],
includedProperties: propertiesArr,
};
return true;
}
}
/**
* _returnCharacteristic - returns the value of a cached or resolved characteristic or resolved characteristic
*
* @param {string} characteristic_name - GATT characteristic name
* @return {object|false} - the characteristic object, if successfully obtained
*/
_returnCharacteristic(characteristic_name) {
if (!bluetooth.gattCharacteristicsMapping[characteristic_name]) {
return errorHandler('characteristic_error', null, characteristic_name);
}
const characteristicObj = bluetooth.gattCharacteristicsMapping[characteristic_name];
const primary_service_name = characteristicObj.primaryServices[0];
if (this.cache[primary_service_name] && this.cache[primary_service_name][characteristic_name] && this.cache[primary_service_name][characteristic_name].cachedCharacteristic) {
return this.cache[primary_service_name][characteristic_name].cachedCharacteristic;
} else if (this.cache[primary_service_name] && this.cache[primary_service_name].cachedService) {
this.cache[primary_service_name].cachedService.getCharacteristic(characteristic_name)
.then(characteristic => {
this.cache[primary_service_name][characteristic_name] = { cachedCharacteristic: characteristic };
return characteristic;
}).catch(err => { return errorHandler('_returnCharacteristic_error', err, characteristic_name); });
} else {
return this.apiServer.getPrimaryService(primary_service_name)
.then(service => {
this.cache[primary_service_name] = { 'cachedService': service };
return service.getCharacteristic(characteristic_name);
}).then(characteristic => {
this.cache[primary_service_name][characteristic_name] = { cachedCharacteristic: characteristic };
return characteristic;
}).catch(err => {
return errorHandler('_returnCharacteristic_error', err, characteristic_name);
});
}
}
}
module.exports = BluetoothDevice;