UNPKG

web-bluetooth

Version:

Library for interacting with Bluetooth 4.0 devices through the browser.

1,144 lines (1,100 loc) 43.3 kB
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ 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; },{"./bluetoothMap":2,"./errorHandler":3}],2:[function(require,module,exports){ const bluetoothMap = { gattCharacteristicsMapping: { battery_level: { primaryServices: ['battery_service'], includedProperties: ['read', 'notify'], parseValue: value => { value = value.buffer ? value : new DataView(value); let result = {}; result.battery_level = value.getUint8(0); return result; } }, blood_pressure_feature: { primaryServices: ['blood_pressure'], includedProperties: ['read'] }, body_composition_feature: { primaryServices: ['body_composition'], includedProperties: ['read'] }, bond_management_feature: { primaryServices: ['bond_management_feature'], includedProperties: ['read'] }, cgm_feature: { primaryServices: ['continuous_glucose_monitoring'], includedProperties: ['read'] }, cgm_session_run_time: { primaryServices: ['continuous_glucose_monitoring'], includedProperties: ['read'] }, cgm_session_start_time: { primaryServices: ['continuous_glucose_monitoring'], includedProperties: ['read', 'write'] }, cgm_status: { primaryServices: ['continuous_glucose_monitoring'], includedProperties: ['read'] }, csc_feature: { primaryServices: ['cycling_speed_and_cadence'], includedProperties: ['read'], parseValue: value => { value = value.buffer ? value : new DataView(value); let flags = value.getUint16(0); let wheelRevolutionDataSupported = flags & 0x1; let crankRevolutionDataSupported = flags & 0x2; let multipleSensDataSupported = flags & 0x3; let result = {}; if (wheelRevolutionDataSupported) { result.wheel_revolution_data_supported = wheelRevolutionDataSupported ? true : false; } if (crankRevolutionDataSupported) { result.crank_revolution_data_supported = crankRevolutionDataSupported ? true : false; } if (multipleSensDataSupported) { result.multiple_sensors_supported = multipleSensDataSupported ? true : false; } return result; } }, current_time: { primaryServices: ['current_time'], includedProperties: ['read', 'write', 'notify'] }, cycling_power_feature: { primaryServices: ['cycling_power'], includedProperties: ['read'] }, firmware_revision_string: { primaryServices: ['device_information'], includedProperties: ['read'] }, hardware_revision_string: { primaryServices: ['device_information'], includedProperties: ['read'] }, ieee_11073_20601_regulatory_certification_data_list: { primaryServices: ['device_information'], includedProperties: ['read'] }, 'gap.appearance': { primaryServices: ['generic_access'], includedProperties: ['read'] }, 'gap.device_name': { primaryServices: ['generic_access'], includedProperties: ['read', 'write'], parseValue: value => { value = value.buffer ? value : new DataView(value); let result = {}; result.device_name = ''; for (var i = 0; i < value.byteLength; i++) { result.device_name += String.fromCharCode(value.getUint8(i)); } return result; }, prepValue: value => { let buffer = new ArrayBuffer(value.length); let preppedValue = new DataView(buffer); value.split('').forEach((char, i) => { preppedValue.setUint8(i, char.charCodeAt(0)); }); return preppedValue; } }, 'gap.peripheral_preferred_connection_parameters': { primaryServices: ['generic_access'], includedProperties: ['read'] }, 'gap.peripheral_privacy_flag': { primaryServices: ['generic_access'], includedProperties: ['read'] }, glucose_feature: { primaryServices: ['glucose'], includedProperties: ['read'], parseValue: value => { value = value.buffer ? value : new DataView(value); let result = {}; let flags = value.getUint16(0); result.low_battery_detection_supported = flags & 0x1; result.sensor_malfunction_detection_supported = flags & 0x2; result.sensor_sample_size_supported = flags & 0x4; result.sensor_strip_insertion_error_detection_supported = flags & 0x8; result.sensor_strip_type_error_detection_supported = flags & 0x10; result.sensor_result_highLow_detection_supported = flags & 0x20; result.sensor_temperature_highLow_detection_supported = flags & 0x40; result.sensor_read_interruption_detection_supported = flags & 0x80; result.general_device_fault_supported = flags & 0x100; result.time_fault_supported = flags & 0x200; result.multiple_bond_supported = flags & 0x400; return result; } }, http_entity_body: { primaryServices: ['http_proxy'], includedProperties: ['read', 'write'] }, glucose_measurement: { primaryServices: ['glucose'], includedProperties: ['notify'], parseValue: value => { value = value.buffer ? value : new DataView(value); let flags = value.getUint8(0); let timeOffset = flags & 0x1; let concentrationTypeSampleLoc = flags & 0x2; let concentrationUnits = flags & 0x4; let statusAnnunciation = flags & 0x8; let contextInformation = flags & 0x10; let result = {}; let index = 1; if (timeOffset) { result.time_offset = value.getInt16(index, /*little-endian=*/true); index += 2; } if (concentrationTypeSampleLoc) { if (concentrationUnits) { result.glucose_concentraiton_molPerL = value.getInt16(index, /*little-endian=*/true); index += 2; } else { result.glucose_concentraiton_kgPerL = value.getInt16(index, /*little-endian=*/true); index += 2; } } return result; } }, http_headers: { primaryServices: ['http_proxy'], includedProperties: ['read', 'write'] }, https_security: { primaryServices: ['http_proxy'], includedProperties: ['read', 'write'] }, intermediate_temperature: { primaryServices: ['health_thermometer'], includedProperties: ['read', 'write', 'indicate'] }, local_time_information: { primaryServices: ['current_time'], includedProperties: ['read', 'write'] }, manufacturer_name_string: { primaryServices: ['device_information'], includedProperties: ['read'] }, model_number_string: { primaryServices: ['device_information'], includedProperties: ['read'] }, pnp_id: { primaryServices: ['device_information'], includedProperties: ['read'] }, protocol_mode: { primaryServices: ['human_interface_device'], includedProperties: ['read', 'writeWithoutResponse'] }, reference_time_information: { primaryServices: ['current_time'], includedProperties: ['read'] }, supported_new_alert_category: { primaryServices: ['alert_notification'], includedProperties: ['read'] }, body_sensor_location: { primaryServices: ['heart_rate'], includedProperties: ['read'], parseValue: value => { value = value.buffer ? value : new DataView(value); let val = value.getUint8(0); let result = {}; switch (val) { case 0: result.location = 'Other'; case 1: result.location = 'Chest'; case 2: result.location = 'Wrist'; case 3: result.location = 'Finger'; case 4: result.location = 'Hand'; case 5: result.location = 'Ear Lobe'; case 6: result.location = 'Foot'; default: result.location = 'Unknown'; } return result; } }, // heart_rate_control_point heart_rate_control_point: { primaryServices: ['heart_rate'], includedProperties: ['write'], prepValue: value => { let buffer = new ArrayBuffer(1); let writeView = new DataView(buffer); writeView.setUint8(0, value); return writeView; } }, heart_rate_measurement: { primaryServices: ['heart_rate'], includedProperties: ['notify'], /** * Parses the event.target.value object and returns object with readable * key-value pairs for all advertised characteristic values * * @param {Object} value Takes event.target.value object from startNotifications method * * @return {Object} result Returns readable object with relevant characteristic values * */ parseValue: value => { value = value.buffer ? value : new DataView(value); let flags = value.getUint8(0); let rate16Bits = flags & 0x1; let contactDetected = flags & 0x2; let contactSensorPresent = flags & 0x4; let energyPresent = flags & 0x8; let rrIntervalPresent = flags & 0x10; let result = {}; let index = 1; if (rate16Bits) { result.heartRate = value.getUint16(index, /*little-endian=*/true); index += 2; } else { result.heartRate = value.getUint8(index); index += 1; } if (contactSensorPresent) { result.contactDetected = !!contactDetected; } if (energyPresent) { result.energyExpended = value.getUint16(index, /*little-endian=*/true); index += 2; } if (rrIntervalPresent) { let rrIntervals = []; for (; index + 1 < value.byteLength; index += 2) { rrIntervals.push(value.getUint16(index, /*little-endian=*/true)); } result.rrIntervals = rrIntervals; } return result; } }, serial_number_string: { primaryServices: ['device_information'], includedProperties: ['read'] }, software_revision_string: { primaryServices: ['device_information'], includedProperties: ['read'] }, supported_unread_alert_category: { primaryServices: ['alert_notification'], includedProperties: ['read'] }, system_id: { primaryServices: ['device_information'], includedProperties: ['read'] }, temperature_type: { primaryServices: ['health_thermometer'], includedProperties: ['read'] }, descriptor_value_changed: { primaryServices: ['environmental_sensing'], includedProperties: ['indicate', 'writeAux', 'extProp'] }, apparent_wind_direction: { primaryServices: ['environmental_sensing'], includedProperties: ['read', 'notify', 'writeAux', 'extProp'], parseValue: value => { value = value.buffer ? value : new DataView(value); let result = {}; result.apparent_wind_direction = value.getUint16(0) * 0.01; return result; } }, apparent_wind_speed: { primaryServices: ['environmental_sensing'], includedProperties: ['read', 'notify', 'writeAux', 'extProp'], parseValue: value => { value = value.buffer ? value : new DataView(value); let result = {}; result.apparent_wind_speed = value.getUint16(0) * 0.01; return result; } }, dew_point: { primaryServices: ['environmental_sensing'], includedProperties: ['read', 'notify', 'writeAux', 'extProp'], parseValue: value => { value = value.buffer ? value : new DataView(value); let result = {}; result.dew_point = value.getInt8(0); return result; } }, elevation: { primaryServices: ['environmental_sensing'], includedProperties: ['read', 'notify', 'writeAux', 'extProp'], parseValue: value => { value = value.buffer ? value : new DataView(value); let result = {}; result.elevation = value.getInt8(0) << 16 | value.getInt8(1) << 8 | value.getInt8(2); return result; } }, gust_factor: { primaryServices: ['environmental_sensing'], includedProperties: ['read', 'notify', 'writeAux', 'extProp'], parseValue: value => { value = value.buffer ? value : new DataView(value); let result = {}; result.gust_factor = value.getUint8(0) * 0.1; return result; } }, heat_index: { primaryServices: ['environmental_sensing'], includedProperties: ['read', 'notify', 'writeAux', 'extProp'], parseValue: value => { value = value.buffer ? value : new DataView(value); let result = {}; result.heat_index = value.getInt8(0); return result; } }, humidity: { primaryServices: ['environmental_sensing'], includedProperties: ['read', 'notify', 'writeAux', 'extProp'], parseValue: value => { value = value.buffer ? value : new DataView(value); let result = {}; result.humidity = value.getUint16(0) * 0.01; return result; } }, irradiance: { primaryServices: ['environmental_sensing'], includedProperties: ['read', 'notify', 'writeAux', 'extProp'], parseValue: value => { value = value.buffer ? value : new DataView(value); let result = {}; result.irradiance = value.getUint16(0) * 0.1; return result; } }, rainfall: { primaryServices: ['environmental_sensing'], includedProperties: ['read', 'notify', 'writeAux', 'extProp'], parseValue: value => { value = value.buffer ? value : new DataView(value); let result = {}; result.rainfall = value.getUint16(0) * 0.001; return result; } }, pressure: { primaryServices: ['environmental_sensing'], includedProperties: ['read', 'notify', 'writeAux', 'extProp'], parseValue: value => { value = value.buffer ? value : new DataView(value); let result = {}; result.pressure = value.getUint32(0) * 0.1; return result; } }, temperature: { primaryServices: ['environmental_sensing'], includedProperties: ['read', 'notify', 'writeAux', 'extProp'], parseValue: value => { value = value.buffer ? value : new DataView(value); let result = {}; result.temperature = value.getInt16(0) * 0.01; return result; } }, true_wind_direction: { primaryServices: ['environmental_sensing'], includedProperties: ['read', 'notify', 'writeAux', 'extProp'], parseValue: value => { value = value.buffer ? value : new DataView(value); let result = {}; result.true_wind_direction = value.getUint16(0) * 0.01; return result; } }, true_wind_speed: { primaryServices: ['environmental_sensing'], includedProperties: ['read', 'notify', 'writeAux', 'extProp'], parseValue: value => { value = value.buffer ? value : new DataView(value); let result = {}; result.true_wind_speed = value.getUint16(0) * 0.01; return result; } }, uv_index: { primaryServices: ['environmental_sensing'], includedProperties: ['read', 'notify', 'writeAux', 'extProp'], parseValue: value => { value = value.buffer ? value : new DataView(value); let result = {}; result.uv_index = value.getUint8(0); return result; } }, wind_chill: { primaryServices: ['environmental_sensing'], includedProperties: ['read', 'notify', 'writeAux', 'extProp'], parseValue: value => { value = value.buffer ? value : new DataView(value); let result = {}; result.wind_chill = value.getInt8(0); return result; } }, barometric_pressure_trend: { primaryServices: ['environmental_sensing'], includedProperties: ['read', 'notify', 'writeAux', 'extProp'], parseValue: value => { value = value.buffer ? value : new DataView(value); let val = value.getUint8(0); let result = {}; switch (val) { case 0: result.barometric_pressure_trend = 'Unknown'; case 1: result.barometric_pressure_trend = 'Continuously falling'; case 2: result.barometric_pressure_trend = 'Continously rising'; case 3: result.barometric_pressure_trend = 'Falling, then steady'; case 4: result.barometric_pressure_trend = 'Rising, then steady'; case 5: result.barometric_pressure_trend = 'Falling before a lesser rise'; case 6: result.barometric_pressure_trend = 'Falling before a greater rise'; case 7: result.barometric_pressure_trend = 'Rising before a greater fall'; case 8: result.barometric_pressure_trend = 'Rising before a lesser fall'; case 9: result.barometric_pressure_trend = 'Steady'; default: result.barometric_pressure_trend = 'Could not resolve to trend'; } return result; } }, magnetic_declination: { primaryServices: ['environmental_sensing'], includedProperties: ['read', 'notify', 'writeAux', 'extProp'], parseValue: value => { value = value.buffer ? value : new DataView(value); let result = {}; result.magnetic_declination = value.getUint16(0) * 0.01; return result; } }, magnetic_flux_density_2D: { primaryServices: ['environmental_sensing'], includedProperties: ['read', 'notify', 'writeAux', 'extProp'], parseValue: value => { value = value.buffer ? value : new DataView(value); let result = {}; //FIXME: need to find out if these values are stored at different byte addresses // below assumes that values are stored at successive byte addresses result.magnetic_flux_density_x_axis = value.getInt16(0, /*little-endian=*/true) * 0.0000001; result.magnetic_flux_density_y_axis = value.getInt16(2, /*little-endian=*/true) * 0.0000001; return result; } }, magnetic_flux_density_3D: { primaryServices: ['environmental_sensing'], includedProperties: ['read', 'notify', 'writeAux', 'extProp'], parseValue: value => { value = value.buffer ? value : new DataView(value); let result = {}; //FIXME: need to find out if these values are stored at different byte addresses // below assumes that values are stored at successive byte addresses result.magnetic_flux_density_x_axis = value.getInt16(0, /*little-endian=*/true) * 0.0000001; result.magnetic_flux_density_y_axis = value.getInt16(2, /*little-endian=*/true) * 0.0000001; result.magnetic_flux_density_z_axis = value.getInt16(4, /*little-endian=*/true) * 0.0000001; return result; } }, tx_power_level: { primaryServices: ['tx_power'], includedProperties: ['read'], parseValue: value => { value = value.buffer ? value : new DataView(value); let result = {}; result.tx_power_level = value.getInt8(0); return result; } }, weight_scale_feature: { primaryServices: ['weight_scale'], includedProperties: ['read'], parseValue: value => { value = value.buffer ? value : new DataView(value); let result = {}; let flags = value.getInt32(0); result.time_stamp_supported = flags & 0x1; result.multiple_sensors_supported = flags & 0x2; result.BMI_supported = flags & 0x4; switch (flags & 0x78 >> 3) { case 0: result.weight_measurement_resolution = 'Not specified'; case 1: result.weight_measurement_resolution = 'Resolution of 0.5 kg or 1 lb'; case 2: result.weight_measurement_resolution = 'Resolution of 0.2 kg or 0.5 lb'; case 3: result.weight_measurement_resolution = 'Resolution of 0.1 kg or 0.2 lb'; case 4: result.weight_measurement_resolution = 'Resolution of 0.05 kg or 0.1 lb'; case 5: result.weight_measurement_resolution = 'Resolution of 0.02 kg or 0.05 lb'; case 6: result.weight_measurement_resolution = 'Resolution of 0.01 kg or 0.02 lb'; case 7: result.weight_measurement_resolution = 'Resolution of 0.005 kg or 0.01 lb'; default: result.weight_measurement_resolution = 'Could not resolve'; } switch (flags & 0x380 >> 7) { case 0: result.height_measurement_resolution = 'Not specified'; case 1: result.height_measurement_resolution = 'Resolution of 0.1 meter or 1 inch'; case 2: result.height_measurement_resolution = 'Resolution of 0.005 meter or 0.5 inch'; case 3: result.height_measurement_resolution = 'Resolution of 0.001 meter or 0.1 inch'; default: result.height_measurement_resolution = 'Could not resolve'; } // Remaining flags reserved for future use return result; } }, csc_measurement: { primaryServices: ['cycling_speed_and_cadence'], includedProperties: ['notify'], parseValue: value => { value = value.buffer ? value : new DataView(value); let flags = value.getUint8(0); let wheelRevolution = flags & 0x1; //integer = truthy, 0 = falsy let crankRevolution = flags & 0x2; let result = {}; let index = 1; if (wheelRevolution) { result.cumulative_wheel_revolutions = value.getUint32(index, /*little-endian=*/true); index += 4; result.last_wheel_event_time_per_1024s = value.getUint16(index, /*little-endian=*/true); index += 2; } if (crankRevolution) { result.cumulative_crank_revolutions = value.getUint16(index, /*little-endian=*/true); index += 2; result.last_crank_event_time_per_1024s = value.getUint16(index, /*little-endian=*/true); index += 2; } return result; } }, sensor_location: { primaryServices: ['cycling_speed_and_cadence'], includedProperties: ['read'], parseValue: value => { value = value.buffer ? value : new DataView(value); let val = value.getUint16(0); let result = {}; switch (val) { case 0: result.location = 'Other'; case 1: result.location = 'Top of show'; case 2: result.location = 'In shoe'; case 3: result.location = 'Hip'; case 4: result.location = 'Front Wheel'; case 5: result.location = 'Left Crank'; case 6: result.location = 'Right Crank'; case 7: result.location = 'Left Pedal'; case 8: result.location = 'Right Pedal'; case 9: result.location = 'Front Hub'; case 10: result.location = 'Rear Dropout'; case 11: result.location = 'Chainstay'; case 12: result.location = 'Rear Wheel'; case 13: result.location = 'Rear Hub'; case 14: result.location = 'Chest'; case 15: result.location = 'Spider'; case 16: result.location = 'Chain Ring'; default: result.location = 'Unknown'; } return result; } }, sc_control_point: { primaryServices: ['cycling_speed_and_cadence'], includedProperties: ['write', 'indicate'], parseValue: value => { value = value.buffer ? value : new DataView(value); return result; } }, cycling_power_measurement: { primaryServices: ['cycling_power'], includedProperties: ['notify'], parseValue: value => { value = value.buffer ? value : new DataView(value); let flags = value.getUint16(0); let pedal_power_balance_present = flags & 0x1; let pedal_power_reference = flags & 0x2; let accumulated_torque_present = flags & 0x4; let accumulated_torque_source = flags & 0x8; let wheel_revolution_data_present = flags & 0x10; let crank_revolution_data_present = flags & 0x12; let extreme_force_magnitude_present = flags & 0x12; let extreme_torque_magnitude_present = flags & 0x12; let extreme_angles_present = flags & 0x12; let top_dead_spot_angle_present = flags & 0x12; let bottom_dead_spot_angle_present = flags & 0x12; let accumulated_energy_present = flags & 0x12; let offset_compensation_indicator = flags & 0x12; let result = {}; let index = 1; //Watts with resolution of 1 result.instantaneous_power = value.getInt16(index); index += 2; if (pedal_power_reference) { //Percentage with resolution of 1/2 result.pedal_power_balance = value.getUint8(index); index += 1; } if (accumulated_torque_present) { //Percentage with resolution of 1/2 result.accumulated_torque = value.getUint16(index); index += 2; } if (wheel_revolution_data_present) { result.cumulative_wheel_revolutions = value.Uint32(index); index += 4; result.last_wheel_event_time_per_2048s = value.Uint16(index); index += 2; } if (crank_revolution_data_present) { result.cumulative_crank_revolutions = value.getUint16(index, /*little-endian=*/true); index += 2; result.last_crank_event_time_per_1024s = value.getUint16(index, /*little-endian=*/true); index += 2; } if (extreme_force_magnitude_present) { //Newton meters with resolution of 1 TODO: units? result.maximum_force_magnitude = value.getInt16(index); index += 2; result.minimum_force_magnitude = value.getInt16(index); index += 2; } if (extreme_torque_magnitude_present) { //Newton meters with resolution of 1 TODO: units? result.maximum_torque_magnitude = value.getInt16(index); index += 2; result.minimum_torque_magnitude = value.getInt16(index); index += 2; } if (extreme_angles_present) { //TODO: UINT12. //Newton meters with resolution of 1 TODO: units? // result.maximum_angle = value.getInt12(index); // index += 2; // result.minimum_angle = value.getInt12(index); // index += 2; } if (top_dead_spot_angle_present) { //Percentage with resolution of 1/2 result.top_dead_spot_angle = value.getUint16(index); index += 2; } if (bottom_dead_spot_angle_present) { //Percentage with resolution of 1/2 result.bottom_dead_spot_angle = value.getUint16(index); index += 2; } if (accumulated_energy_present) { //kilojoules with resolution of 1 TODO: units? result.accumulated_energy = value.getUint16(index); index += 2; } return result; } } }, gattServiceList: ['alert_notification', 'automation_io', 'battery_service', 'blood_pressure', 'body_composition', 'bond_management', 'continuous_glucose_monitoring', 'current_time', 'cycling_power', 'cycling_speed_and_cadence', 'device_information', 'environmental_sensing', 'generic_access', 'generic_attribute', 'glucose', 'health_thermometer', 'heart_rate', 'human_interface_device', 'immediate_alert', 'indoor_positioning', 'internet_protocol_support', 'link_loss', 'location_and_navigation', 'next_dst_change', 'phone_alert_status', 'pulse_oximeter', 'reference_time_update', 'running_speed_and_cadence', 'scan_parameters', 'tx_power', 'user_data', 'weight_scale'] }; module.exports = bluetoothMap; },{}],3:[function(require,module,exports){ /** errorHandler - Consolodates error message configuration and logic * * @param {string} errorKey - maps to a detailed error message * @param {object} nativeError - the native API error object, if present * @param {} alternate - * */ function errorHandler(errorKey, nativeError, alternate) { const errorMessages = { add_characteristic_exists_error: `Characteristic ${ alternate } already exists.`, characteristic_error: `Characteristic ${ alternate } not found. Add ${ alternate } to device using addCharacteristic or try another characteristic.`, connect_gatt: `Could not connect to GATT. Device might be out of range. Also check to see if filters are vaild.`, connect_server: `Could not connect to server on device.`, connect_service: `Could not find service.`, disconnect_timeout: `Timed out. Could not disconnect.`, disconnect_error: `Could not disconnect from device.`, improper_characteristic_format: `${ alternate } is not a properly formatted characteristic.`, improper_properties_format: `${ alternate } is not a properly formatted properties array.`, improper_service_format: `${ alternate } is not a properly formatted service.`, issue_disconnecting: `Issue disconnecting with device.`, new_characteristic_missing_params: `${ alternate } is not a fully supported characteristic. Please provide an associated primary service and at least one property.`, no_device: `No instance of device found.`, no_filters: `No filters found on instance of Device. For more information, please visit http://sabertooth.io/#method-newdevice`, no_read_property: `No read property on characteristic: ${ alternate }.`, no_write_property: `No write property on this characteristic.`, not_connected: `Could not disconnect. Device not connected.`, parsing_not_supported: `Parsing not supported for characterstic: ${ alternate }.`, read_error: `Cannot read value on the characteristic.`, _returnCharacteristic_error: `Error accessing characteristic ${ alternate }.`, start_notifications_error: `Not able to read stream of data from characteristic: ${ alternate }.`, start_notifications_no_notify: `No notify property found on this characteristic: ${ alternate }.`, stop_notifications_not_notifying: `Notifications not established for characteristic: ${ alternate } or you have not started notifications.`, stop_notifications_error: `Issue stopping notifications for characteristic: ${ alternate } or you have not started notifications.`, user_cancelled: `User cancelled the permission request.`, uuid_error: `Invalid UUID. For more information on proper formatting of UUIDs, visit https://webbluetoothcg.github.io/web-bluetooth/#uuids`, write_error: `Could not change value of characteristic: ${ alternate }.`, write_permissions: `${ alternate } characteristic does not have a write property.` }; throw new Error(errorMessages[errorKey]); return false; } module.exports = errorHandler; },{}]},{},[1]);