web-bluetooth
Version:
Library for interacting with Bluetooth 4.0 devices through the browser.
773 lines (767 loc) • 26.2 kB
JavaScript
'use strict';
var bluetoothMap = {
gattCharacteristicsMapping: {
battery_level: {
primaryServices: ['battery_service'],
includedProperties: ['read', 'notify'],
parseValue: function parseValue(value) {
value = value.buffer ? value : new DataView(value);
var 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: function parseValue(value) {
value = value.buffer ? value : new DataView(value);
var flags = value.getUint16(0);
var wheelRevolutionDataSupported = flags & 0x1;
var crankRevolutionDataSupported = flags & 0x2;
var multipleSensDataSupported = flags & 0x3;
var 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: function parseValue(value) {
value = value.buffer ? value : new DataView(value);
var result = {};
result.device_name = '';
for (var i = 0; i < value.byteLength; i++) {
result.device_name += String.fromCharCode(value.getUint8(i));
}
return result;
},
prepValue: function prepValue(value) {
var buffer = new ArrayBuffer(value.length);
var preppedValue = new DataView(buffer);
value.split('').forEach(function (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: function parseValue(value) {
value = value.buffer ? value : new DataView(value);
var result = {};
var 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: function parseValue(value) {
value = value.buffer ? value : new DataView(value);
var flags = value.getUint8(0);
var timeOffset = flags & 0x1;
var concentrationTypeSampleLoc = flags & 0x2;
var concentrationUnits = flags & 0x4;
var statusAnnunciation = flags & 0x8;
var contextInformation = flags & 0x10;
var result = {};
var 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: function parseValue(value) {
value = value.buffer ? value : new DataView(value);
var val = value.getUint8(0);
var 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: function prepValue(value) {
var buffer = new ArrayBuffer(1);
var 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: function parseValue(value) {
value = value.buffer ? value : new DataView(value);
var flags = value.getUint8(0);
var rate16Bits = flags & 0x1;
var contactDetected = flags & 0x2;
var contactSensorPresent = flags & 0x4;
var energyPresent = flags & 0x8;
var rrIntervalPresent = flags & 0x10;
var result = {};
var 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) {
var 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: function parseValue(value) {
value = value.buffer ? value : new DataView(value);
var result = {};
result.apparent_wind_direction = value.getUint16(0) * 0.01;
return result;
}
},
apparent_wind_speed: {
primaryServices: ['environmental_sensing'],
includedProperties: ['read', 'notify', 'writeAux', 'extProp'],
parseValue: function parseValue(value) {
value = value.buffer ? value : new DataView(value);
var result = {};
result.apparent_wind_speed = value.getUint16(0) * 0.01;
return result;
}
},
dew_point: {
primaryServices: ['environmental_sensing'],
includedProperties: ['read', 'notify', 'writeAux', 'extProp'],
parseValue: function parseValue(value) {
value = value.buffer ? value : new DataView(value);
var result = {};
result.dew_point = value.getInt8(0);
return result;
}
},
elevation: {
primaryServices: ['environmental_sensing'],
includedProperties: ['read', 'notify', 'writeAux', 'extProp'],
parseValue: function parseValue(value) {
value = value.buffer ? value : new DataView(value);
var 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: function parseValue(value) {
value = value.buffer ? value : new DataView(value);
var result = {};
result.gust_factor = value.getUint8(0) * 0.1;
return result;
}
},
heat_index: {
primaryServices: ['environmental_sensing'],
includedProperties: ['read', 'notify', 'writeAux', 'extProp'],
parseValue: function parseValue(value) {
value = value.buffer ? value : new DataView(value);
var result = {};
result.heat_index = value.getInt8(0);
return result;
}
},
humidity: {
primaryServices: ['environmental_sensing'],
includedProperties: ['read', 'notify', 'writeAux', 'extProp'],
parseValue: function parseValue(value) {
value = value.buffer ? value : new DataView(value);
var result = {};
result.humidity = value.getUint16(0) * 0.01;
return result;
}
},
irradiance: {
primaryServices: ['environmental_sensing'],
includedProperties: ['read', 'notify', 'writeAux', 'extProp'],
parseValue: function parseValue(value) {
value = value.buffer ? value : new DataView(value);
var result = {};
result.irradiance = value.getUint16(0) * 0.1;
return result;
}
},
rainfall: {
primaryServices: ['environmental_sensing'],
includedProperties: ['read', 'notify', 'writeAux', 'extProp'],
parseValue: function parseValue(value) {
value = value.buffer ? value : new DataView(value);
var result = {};
result.rainfall = value.getUint16(0) * 0.001;
return result;
}
},
pressure: {
primaryServices: ['environmental_sensing'],
includedProperties: ['read', 'notify', 'writeAux', 'extProp'],
parseValue: function parseValue(value) {
value = value.buffer ? value : new DataView(value);
var result = {};
result.pressure = value.getUint32(0) * 0.1;
return result;
}
},
temperature: {
primaryServices: ['environmental_sensing'],
includedProperties: ['read', 'notify', 'writeAux', 'extProp'],
parseValue: function parseValue(value) {
value = value.buffer ? value : new DataView(value);
var result = {};
result.temperature = value.getInt16(0) * 0.01;
return result;
}
},
true_wind_direction: {
primaryServices: ['environmental_sensing'],
includedProperties: ['read', 'notify', 'writeAux', 'extProp'],
parseValue: function parseValue(value) {
value = value.buffer ? value : new DataView(value);
var result = {};
result.true_wind_direction = value.getUint16(0) * 0.01;
return result;
}
},
true_wind_speed: {
primaryServices: ['environmental_sensing'],
includedProperties: ['read', 'notify', 'writeAux', 'extProp'],
parseValue: function parseValue(value) {
value = value.buffer ? value : new DataView(value);
var result = {};
result.true_wind_speed = value.getUint16(0) * 0.01;
return result;
}
},
uv_index: {
primaryServices: ['environmental_sensing'],
includedProperties: ['read', 'notify', 'writeAux', 'extProp'],
parseValue: function parseValue(value) {
value = value.buffer ? value : new DataView(value);
var result = {};
result.uv_index = value.getUint8(0);
return result;
}
},
wind_chill: {
primaryServices: ['environmental_sensing'],
includedProperties: ['read', 'notify', 'writeAux', 'extProp'],
parseValue: function parseValue(value) {
value = value.buffer ? value : new DataView(value);
var result = {};
result.wind_chill = value.getInt8(0);
return result;
}
},
barometric_pressure_trend: {
primaryServices: ['environmental_sensing'],
includedProperties: ['read', 'notify', 'writeAux', 'extProp'],
parseValue: function parseValue(value) {
value = value.buffer ? value : new DataView(value);
var val = value.getUint8(0);
var 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: function parseValue(value) {
value = value.buffer ? value : new DataView(value);
var result = {};
result.magnetic_declination = value.getUint16(0) * 0.01;
return result;
}
},
magnetic_flux_density_2D: {
primaryServices: ['environmental_sensing'],
includedProperties: ['read', 'notify', 'writeAux', 'extProp'],
parseValue: function parseValue(value) {
value = value.buffer ? value : new DataView(value);
var 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: function parseValue(value) {
value = value.buffer ? value : new DataView(value);
var 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: function parseValue(value) {
value = value.buffer ? value : new DataView(value);
var result = {};
result.tx_power_level = value.getInt8(0);
return result;
}
},
weight_scale_feature: {
primaryServices: ['weight_scale'],
includedProperties: ['read'],
parseValue: function parseValue(value) {
value = value.buffer ? value : new DataView(value);
var result = {};
var 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: function parseValue(value) {
value = value.buffer ? value : new DataView(value);
var flags = value.getUint8(0);
var wheelRevolution = flags & 0x1; //integer = truthy, 0 = falsy
var crankRevolution = flags & 0x2;
var result = {};
var 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: function parseValue(value) {
value = value.buffer ? value : new DataView(value);
var val = value.getUint16(0);
var 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: function parseValue(value) {
value = value.buffer ? value : new DataView(value);
return result;
}
},
cycling_power_measurement: {
primaryServices: ['cycling_power'],
includedProperties: ['notify'],
parseValue: function parseValue(value) {
value = value.buffer ? value : new DataView(value);
var flags = value.getUint16(0);
var pedal_power_balance_present = flags & 0x1;
var pedal_power_reference = flags & 0x2;
var accumulated_torque_present = flags & 0x4;
var accumulated_torque_source = flags & 0x8;
var wheel_revolution_data_present = flags & 0x10;
var crank_revolution_data_present = flags & 0x12;
var extreme_force_magnitude_present = flags & 0x12;
var extreme_torque_magnitude_present = flags & 0x12;
var extreme_angles_present = flags & 0x12;
var top_dead_spot_angle_present = flags & 0x12;
var bottom_dead_spot_angle_present = flags & 0x12;
var accumulated_energy_present = flags & 0x12;
var offset_compensation_indicator = flags & 0x12;
var result = {};
var 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;