├── .eslintrc.json ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── bower.json ├── dist ├── build.js └── npm │ ├── BluetoothDevice.js │ ├── bluetoothMap.js │ └── errorHandler.js ├── gulpfile.js ├── lib ├── .DS_Store ├── BluetoothDevice.js ├── bluetoothMap.js └── errorHandler.js ├── npm.js ├── package.json └── test ├── browser-integration.html └── lib └── raw-browser-api-tests.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "root": true, 4 | "env": { 5 | "browser": true, 6 | "node": true, 7 | "mocha": true 8 | }, 9 | "plugins": [ 10 | "react" 11 | ], 12 | "parserOptions": { 13 | "ecmaVersion": 6, 14 | "sourceType": "module" 15 | }, 16 | "globals": {} 17 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bower_components 3 | npm-debug.log 4 | .DS_store -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | **/.* 2 | bower.json 3 | bower_components 4 | gulpfile.js 5 | node_modules 6 | npm-debug.log 7 | test -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # web-bluetooth 2 | 3 | [![npm version](https://badge.fury.io/js/web-bluetooth.svg)](https://badge.fury.io/js/web-bluetooth) 4 | ![pull requests welcomed](https://img.shields.io/badge/PRs-welcome-brightgreen.svg) 5 | 6 | ### Library for interacting with Bluetooth 4.0 devices through the browser. 7 | 8 | Connect, read, and write to Bluetooth devices in web applications using only a few lines of Javascript. 9 | 10 | ### Getting Started 11 | 12 | Web-bluetooth (aka Sabertooth) has a few easy ways to quickly get started, each one appealing to your preferences. 13 | 14 | #### Install with npm 15 | 16 | You can install Sabertooth as the npm package 'web-bluetooth' [here](https://npmjs.com/package/web-bluetooth). 17 | 18 | Or run the command-line script below in the project directory in which you would like to use Sabertooth. 19 | ``` 20 | $ npm install web-bluetooth 21 | ``` 22 | The following will load all of the files necessary to run Sabertooth. 23 | 24 | `require('web-bluetooth')` 25 | 26 | ### General Accesibility 27 | 28 | While the Web Bluetooth API is still in development, certain features have been made available. 29 | 30 | > Note: The functionality of Sabertooth is contingent upon the permissions and availabilities of the Web Bluetooth API. The Web Bluetooth API is still in development and many features have not been implemented across browsers. For the current status of the API, please follow this [link](https://webbluetoothcg.github.io/web-bluetooth/). 31 | 32 | | Feature | ChromeOS | Android Mobile | MacOSX | 33 | |:----------|:------------------:|:---------------:|:---------:| 34 | | Device Discovery | ✓ | ✓ | ✓ | 35 | | Device Connecting | ✓ | ✓ | ✓ | 36 | | Device Disconnecting | ✓ | ✓ | ✓ | 37 | | Device Services Read | ✓ | ✓ | 38 | 39 | > Note: To enable the browser to use the Web Bluetooth API (and Sabertooth), experimental flags must be enabled and an https server are required. 40 | 41 | ### GATT Attributes 42 | 43 | A basic understanding of the [Generic Attribute Profile (GATT)](https://developer.bluetooth.org/TechnologyOverview/Pages/GATT.aspx) is helpful when writing applications to interact with Bluetooth devices, but using Sabertooth requires an understanding of two main GATT attributes, GATT services and GATT characteristics. 44 | 45 | Services are collections of characteristics and relationships to other services that encapsulate the behavior of part of a device. For example, the “Battery Service” exposes the Battery Level of a device broadcasting the “Battery Service” service. 46 | 47 | Sabertooth abstracts over the core features of the Web-Bluetooth API, and allows for the use of virtually any GATT service or GATT characteristic, as well as non-GATT services and non-GATT characteristics. 48 | 49 | For the complete list of normative GATT services click [here](https://webbluetoothcg.github.io/web-bluetooth/). As the Web Bluetooth API continues to be developed and as this library matures, full support for more service types will be made available. 50 | 51 | #### Creating a New Bluetooth Device 52 | 53 | ##### `new BluetoothDevice(filters)` 54 | 55 | To begin interacting with a Bluetooth device, create a new instance of BluetoothDevice and save the result to a variable. BluetoothDevice is a constructor that takes in an object filters containing attributes advertised by the Bluetooth device. 56 | 57 | ##### Parameters 58 | `filters` - an object containing at least one valid filter corresponding to attributes advertised by the bluetooth device through which Sabertooth will attempt to request and connect to the device. Below is a schema representing the optional parameters that can be passed into the `BluetoothDevice` constructor to create a new `BluetoothDevice` instance. At least one of the key-value pairs below is needed to request a device and establish an initial connection. 59 | 60 | > Note: Parameters passed into the `filters` object of the `BluetoothDevice` constructor are inclusive; a request and connection to a device can only succeed if the device satisfies all of the provided filters. 61 | 62 | ``` 63 | { 64 | name: 'device_name', 65 | namePrefix: 'devicePrefix', 66 | uuid: 'uuid', 67 | service: 'service' 68 | } 69 | ``` 70 | `name` - the advertised the name of the device; often set by the manufacturer unless modified by the device user 71 | 72 | `namePrefix` - an initial substring of any length that matches an advertised device name 73 | 74 | `uuid` - represents a 128-bit universally unique identifier (uuid). A valid uuid is a string that matches the regexp /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/ 75 | 76 | `services` - an array including at least one service being advertised by the device. 77 | 78 | > Note: Not all services present on a device are advertised. Attempting to access a device by including a filter for a service present on a device but not being advertised by the device will cause the request to the device to fail. 79 | 80 | ##### Returns 81 | A new `BluetoothDevice` instance on which Sabertooth methods can be called. 82 | ##### Example 83 | 84 | ``` 85 | var exampleDevice = new BluetoothDevice({ 86 | namePrefix: 'Surge' 87 | }); 88 | ``` 89 | In the example above, a new BluetoothDevice instance exampleDevice has been created with a valid request filter. The device name is one of the many properties advertised by Bluetooth devices and serves as a possible identifier for establishing initial connections to the device. In this example, the device name will act as the provided filter. 90 | 91 | Below are several other examples of ways in which a new BluetoothDevice instance can be created using different filters or combinations of filters. 92 | ``` 93 | /* Attempts to request a device advertising a name 94 | * beginning with the substring 'Po' 95 | */ 96 | var exampleDevice = new BluetoothDevice({ 97 | namePrefix: 'Po' 98 | }); 99 | ``` 100 | ``` 101 | /* Attempts to request a device advertising the name 102 | * 'Polar H7 Heart Rate Sensor' and ALSO advertising the 103 | * GATT service 'heart_rate' 104 | */ 105 | var exampleDevice = new BluetoothDevice({ 106 | name: 'Polar H7 Heart Rate Sensor' 107 | service: ['heart_rate'] 108 | }); 109 | ``` 110 | --- 111 | #### General Methods 112 | ##### `.connect()` 113 | 114 | Method establishes a persistent connection with a Bluetooth device. 115 | ##### Parameters 116 | None 117 | ##### Returns 118 | A Promise to the device GATT server for the connected device. 119 | ##### Example 120 | ``` 121 | exampleDevice.connect(); 122 | ``` 123 | In the example above, a connection to a previously created BluetoothDevice instance named exampleDevice will be attempted. 124 | 125 | --- 126 | 127 | ##### `.disconnect()` 128 | 129 | Method removes a previous connection with a Bluetooth device. 130 | ##### Parameters 131 | None 132 | ##### Returns 133 | A boolean representing the success of the attempt to disconnect from the device where true represents success. 134 | ##### Example 135 | ``` 136 | exampleDevice.disconnect(); 137 | ``` 138 | --- 139 | ##### `.connected()` 140 | 141 | Method returns the current connection status of the device. 142 | 143 | ##### Parameters 144 | None 145 | ##### Returns 146 | A boolean representing the success of the attempt to disconnect from the device where true is connected and false is disconnected. 147 | 148 | ##### Example 149 | ``` 150 | exampleDevice.connected(); 151 | ``` 152 | In the example above, an attempt will be made to check the connection status of a previously created `BluetoothDevice` instance named `exampleDevice`. 153 | 154 | --- 155 | ##### `.getValue(characteristic)` 156 | 157 | Method attempts to read the value of provided characteristic from a connected `BluetoothDevice` instance. 158 | 159 | 160 | ##### Parameters 161 | `characteristic` - a GATT characteristic or 128-bit uuid string that resolves to a characteristic accessible on the device. 162 | 163 | 164 | ##### Returns 165 | An object containing the [ArrayBuffer](https://tc39.github.io/ecma262/#sec-arraybuffer-constructor) value read from the connected `BluetoothDevice` instance, saved to the key `.rawValue`. For characteristics fully supported by Sabertooth, the return object will also include any parsed values for available descriptors of the requested characteristic as key-vlaue pairs with the descriptor as the key and the parsed value as the value. 166 | 167 | 168 | 169 | ##### Example 170 | ``` 171 | exampleDevice.getValue('battery_level') 172 | .then(value => { 173 | console.log(value.battery_level); 174 | }); 175 | ``` 176 | In the above example, the `.getValue()` method is called on the `BluetoothDevice` instance exampleDevice, which returns an object, in this example referenced as `value`. `value` contains the ArrayBuffer returned from the device stored to the property `rawValue`, and because `'battery_level'` is a fully supported characteristic in Sabertooth, value also contains the parsed integer value for the instance's battery level, stored on the value object as the key `battery_level`. In this example, the parsed integer value representing the device's battery level is being logged to the console. 177 | 178 | --- 179 | ##### `.writeValue(characteristic, value)` 180 | 181 | Method takes a characteristic and value and attempts to write the provided value to the provided characteristic on the device. 182 | 183 | ##### Parameters 184 | `characteristic` - a GATT characteristic or 128-bit uuid string that resolves to a characteristic accessible on the device instance. 185 | 186 | `value` - an ArrayBuffer or DataView 187 | 188 | 189 | ##### Returns 190 | A boolean representing the success of the attempt to write to the provided characteristic where true represents success. 191 | 192 | 193 | 194 | ##### Example 195 | ``` 196 | exampleDevice.writeValue('gap.device_name', 'myFitbit' ) 197 | .then(writeSuccess => { 198 | console.log(writeSuccess); 199 | }); 200 | ``` 201 | In the above example, `.writeValue()` changes the name of the instantiated device to myFitbit. 202 | 203 | --- 204 | ##### `.startNotifications(characteristic, callback)` 205 | 206 | Method takes a characteristic name and a callback function. Provided that the characteristic has a 'notify' property, .startNotifications() will pass the event object broadcasted by the characteristic as the parameter of the callback, and run the callback each time a new event occurs. 207 | 208 | ##### Parameters 209 | `characteristic` - a GATT characteristic or 128-bit uuid string that resolves to a characteristic accessible on the device instance. 210 | 211 | `callback` - a callback triggered as result of a notification from the provided characteristic advertised by the device. The parameter eventObj will automatically be passed into callback for each notification received from the device. 212 | 213 | `eventObj` - an object passed as the sole parameter into the callback provided. `eventObj` contains the ArrayBuffer value notified from the connected `BluetoothDevice` instance, saved to the key `.rawValue`. For characteristics fully supported by Sabertooth, `eventObj` will also include any parsed values for available descriptors of the requested characteristic as key-vlaue pairs with the descriptor as the key and the parsed value as the value. 214 | 215 | 216 | ##### Returns 217 | None. 218 | 219 | ##### Example 220 | ``` 221 | exampleDevice.startNotifications('heart_rate_measurement', eventObj => { 222 | var newHR = eventObj.heart_rate_measurement; 223 | console.log(newHR); 224 | }); 225 | ``` 226 | In the above example, the `.startNotifications()` method is called on the `BluetoothDevice` instance exampleDevice, which attempts to initiate a stream of notifications from the Bluetooth device, where the provided `callback`, in this example an anonymous function with the parameter `eventObj`, will be applied to the `eventObj` returned from each notification from the device. In this example, `eventObj` contains the ArrayBuffer returned from the device notification, stored to the property `rawValue`, and because `'heart_rate_measurement'` is a fully supported characteristic in Sabertooth, `eventObj` also contains the parsed integer value for the device's heart rate measurement, stored on the `eventObj` object as the key `heart_rate_measurement`. In this example, the parsed integer value representing the notification's heart rate measurement is being logged to the console. 227 | 228 | --- 229 | ##### `.stopNotifications(characteristic)` 230 | 231 | This method stops the notifications from the provided characteristic for the `BluetoothDevice` instance. 232 | 233 | 234 | 235 | ##### Parameters 236 | None. 237 | 238 | 239 | ##### Returns 240 | None. 241 | 242 | ##### Example 243 | ``` 244 | exampleDevice.stopNotifications('heart_rate_measurement'); 245 | ``` 246 | In the above example, the `.stopNotifications()` method is called on the `BluetoothDevice` instance `exampleDevice`, which attempts to end a stream of notifications from the Bluetooth device. 247 | 248 | --- 249 | ##### `.addCharacteristic(characteristic, service, properties)` 250 | 251 | This method adds Sabertooth support for the provided characteristic to device instance on which the method was called. 252 | 253 | ##### Parameters 254 | `characteristic` - a GATT characteristic or 128-bit uuid string that resolves to a characteristic accessible on the device instance 255 | 256 | `service` - a GATT service or 128-bit uuid string that resolves to a service accessible on the device instance 257 | 258 | `properties` - an array containing at least one property existing on the characteristic to be added. Currently, Sabertooth supports the properties 'read', 'write', and 'notify'. 259 | 260 | ##### Returns 261 | A boolean representing the success of the attempt to add the characteristic to the device instance where true represents success. 262 | 263 | > Note: For characteristics added to the device instance, Sabertooth cannot parse values read from the device or prepare values to be written to the device. Returned values from device calls to the .readValue() method will return an object containing the raw data returned from the device; device calls to the .writeValue() method will attempt to write the provided value directly to the device in its provided form. 264 | 265 | ##### Example 266 | ``` 267 | exampleDevice.addCharacteristic( 268 | '9a66fd22-0800-9191-11e4-012d1540cb8e', 269 | '9a66fd21-0800-9191-11e4-012d1540cb8e', 270 | ['read','write','notify']); 271 | ``` 272 | In the above example, the `.addCharacteristic()` method is called on the `BluetoothDevice` instance `exampleDevice`, which returns an object, in this example referenced as value, containing the parsed integer value for the instance's battery level, stored on the value object as the key `battery_level`. 273 | 274 | --- 275 | ### Demos 276 | 277 | #### Heart Rate Service 278 | This demo uses the Sabertooth library to connect to a heart rate monitor broadcasting a Heart Rate Service characteristic and reads it's measurement. 279 | 280 | [Visit the GitHub page.](https://github.com/sabertooth-io/demo-heart_rate_service) 281 | 282 | #### Battery Service 283 | This demo uses the Sabertooth library to connect to a device broadcasting a Battery Service characteristic and reads it's level. 284 | 285 | [Visit the GitHub page.](https://github.com/sabertooth-io/demo-battery_service) 286 | 287 | > Notes: 288 | • Requires Android 6.0 Marshmallow, ChromeOS or Chrome for Linux. 289 | • Enable the 'Web Bluetooth' flag. 290 | 291 | ### Authors and Contributors 292 | 293 | Sabertooth is a team of four software developers enthusiastic to be contributing to the open source community. Visit their GitHub pages for more information. 294 | 295 | Aaron Peltz | [GitHub](https://github.com/apeltz/) 296 | 297 | Alex Patch | [GitHub](https://github.com/the-gingerbread-man/) 298 | 299 | Carlos Corral | [GitHub](https://github.com/ccorral/) 300 | 301 | Daniel Lee | [GitHub](https://github.com/dslee393/) 302 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web-bluetooth", 3 | "description": "Library for interacting with Bluetooth 4.0 devices through the browser.", 4 | "main": "dist/build.js", 5 | "moduleType": "amd", 6 | "keywords": [ 7 | "abstraction", 8 | "API", 9 | "ble", 10 | "bluetooth", 11 | "browser", 12 | "client", 13 | "developer", 14 | "developer tool", 15 | "experimental", 16 | "framework", 17 | "internet of things", 18 | "library", 19 | "web-bluetooth" 20 | ], 21 | "authors": [ 22 | "Alex Patch (https://github.com/the-gingerbread-man)", 23 | "Aaron Peltz (https://github.com/apeltz)", 24 | "Carlos Corral (https://github.com/ccorral)", 25 | "Daniel Lee (https://github.com/dslee393)", 26 | "Francois Beaufort (https://github.com/beaufortfrancois)" 27 | ], 28 | "license": "Apache-2.0", 29 | "homepage": "docs.sabertooth.io", 30 | "repository": "github.com/sabertooth-io/web-bluetooth", 31 | "ignore": [ 32 | "**/.*", 33 | "gulpfile.js", 34 | "node_modules", 35 | "npm.js", 36 | "bower_components", 37 | "test", 38 | "tests" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /dist/build.js: -------------------------------------------------------------------------------- 1 | (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 { 66 | if (!bluetooth.gattServiceList.includes(service)) { 67 | console.warn(`${ service } is not a valid service. Please check the service name.`); 68 | } else { 69 | services.push(service); 70 | } 71 | }); 72 | requestParams.filters.push({ services: services }); 73 | } 74 | if (filters.optional_services) { 75 | filters.optional_services.forEach(service => { 76 | if (!bluetooth.gattServiceList.includes(service)) bluetooth.gattServiceList.push(service); 77 | }); 78 | } else { 79 | requestParams.optionalServices = bluetooth.gattServiceList; 80 | } 81 | 82 | return navigator.bluetooth.requestDevice(requestParams).then(device => { 83 | this.apiDevice = device; 84 | return device.gatt.connect(); 85 | }).then(server => { 86 | this.apiServer = server; 87 | return server; 88 | }).catch(err => { 89 | return errorHandler('user_cancelled', err); 90 | }); 91 | } 92 | 93 | /** disconnect - terminates the connection with the device and pauses all data stream subscriptions 94 | * @return {boolean} - success 95 | * 96 | */ 97 | disconnect() { 98 | this.apiServer.connected ? this.apiServer.disconnect() : errorHandler('not_connected'); 99 | return this.apiServer.connected ? errorHandler('issue_disconnecting') : true; 100 | } 101 | 102 | /** getValue - reads the value of a specified characteristic 103 | * 104 | * @param {string} characteristic_name - GATT characteristic name 105 | * @return {promise} - resolves with an object that includes key-value pairs for each of the properties 106 | * successfully read and parsed from the device, as well as the 107 | * raw value object returned by a native readValue request to the 108 | * device characteristic. 109 | */ 110 | getValue(characteristic_name) { 111 | if (!bluetooth.gattCharacteristicsMapping[characteristic_name]) { 112 | return errorHandler('characteristic_error', null, characteristic_name); 113 | } 114 | 115 | const characteristicObj = bluetooth.gattCharacteristicsMapping[characteristic_name]; 116 | 117 | if (!characteristicObj.includedProperties.includes('read')) { 118 | console.warn(`Attempting to access read property of ${ characteristic_name }, 119 | which is not a included as a supported property of the 120 | characteristic. Attempt will resolve with an object including 121 | only a rawValue property with the native API return 122 | for an attempt to readValue() of ${ characteristic_name }.`); 123 | } 124 | 125 | return new Promise((resolve, reject) => { 126 | return resolve(this._returnCharacteristic(characteristic_name)); 127 | }).then(characteristic => { 128 | return characteristic.readValue(); 129 | }).then(value => { 130 | const returnObj = characteristicObj.parseValue ? characteristicObj.parseValue(value) : {}; 131 | returnObj.rawValue = value; 132 | return returnObj; 133 | }).catch(err => { 134 | return errorHandler('read_error', err); 135 | }); 136 | } 137 | 138 | /** writeValue - writes data to a specified characteristic of the device 139 | * 140 | * @param {string} characteristic_name - name of the GATT characteristic 141 | * https://www.bluetooth.com/specifications/assigned-numbers/generic-attribute-profile 142 | * 143 | * @param {string|number} value - value to write to the requested device characteristic 144 | * 145 | * 146 | * @return {boolean} - Result of attempt to write characteristic where true === successfully written 147 | */ 148 | writeValue(characteristic_name, value) { 149 | if (!bluetooth.gattCharacteristicsMapping[characteristic_name]) { 150 | return errorHandler('characteristic_error', null, characteristic_name); 151 | } 152 | 153 | const characteristicObj = bluetooth.gattCharacteristicsMapping[characteristic_name]; 154 | 155 | if (!characteristicObj.includedProperties.includes('write')) { 156 | console.warn(`Attempting to access write property of ${ characteristic_name }, 157 | which is not a included as a supported property of the 158 | characteristic. Attempt will resolve with native API return 159 | for an attempt to writeValue(${ value }) to ${ characteristic_name }.`); 160 | } 161 | 162 | return new Promise((resolve, reject) => { 163 | return resolve(this._returnCharacteristic(characteristic_name)); 164 | }).then(characteristic => { 165 | return characteristic.writeValue(characteristicObj.prepValue ? characteristicObj.prepValue(value) : value); 166 | }).then(changedChar => { 167 | return true; 168 | }).catch(err => { 169 | return errorHandler('write_error', err, characteristic_name); 170 | }); 171 | } 172 | 173 | /** startNotifications - attempts to start notifications for changes to device values and attaches an event listener for each data transmission 174 | * 175 | * @param {string} characteristic_name - GATT characteristic name 176 | * @param {callback} transmissionCallback - callback function to apply to each event while notifications are active 177 | * 178 | * @return 179 | * 180 | */ 181 | startNotifications(characteristic_name, transmissionCallback) { 182 | if (!bluetooth.gattCharacteristicsMapping[characteristic_name]) { 183 | return errorHandler('characteristic_error', null, characteristic_name); 184 | } 185 | 186 | const characteristicObj = bluetooth.gattCharacteristicsMapping[characteristic_name]; 187 | const primary_service_name = characteristicObj.primaryServices[0]; 188 | 189 | if (!characteristicObj.includedProperties.includes('notify')) { 190 | console.warn(`Attempting to access notify property of ${ characteristic_name }, 191 | which is not a included as a supported property of the 192 | characteristic. Attempt will resolve with an object including 193 | only a rawValue property with the native API return 194 | for an attempt to startNotifications() for ${ characteristic_name }.`); 195 | } 196 | 197 | return new Promise((resolve, reject) => { 198 | return resolve(this._returnCharacteristic(characteristic_name)); 199 | }).then(characteristic => { 200 | characteristic.startNotifications().then(() => { 201 | this.cache[primary_service_name][characteristic_name].notifying = true; 202 | return characteristic.addEventListener('characteristicvaluechanged', event => { 203 | const eventObj = characteristicObj.parseValue ? characteristicObj.parseValue(event.target.value) : {}; 204 | eventObj.rawValue = event; 205 | return transmissionCallback(eventObj); 206 | }); 207 | }); 208 | }).catch(err => { 209 | return errorHandler('start_notifications_error', err, characteristic_name); 210 | }); 211 | } 212 | 213 | /** stopNotifications - attempts to stop previously started notifications for a provided characteristic 214 | * 215 | * @param {string} characteristic_name - GATT characteristic name 216 | * 217 | * @return {boolean} success 218 | * 219 | */ 220 | stopNotifications(characteristic_name) { 221 | if (!bluetooth.gattCharacteristicsMapping[characteristic_name]) { 222 | return errorHandler('characteristic_error', null, characteristic_name); 223 | } 224 | 225 | const characteristicObj = bluetooth.gattCharacteristicsMapping[characteristic_name]; 226 | const primary_service_name = characteristicObj.primaryServices[0]; 227 | 228 | if (this.cache[primary_service_name][characteristic_name].notifying) { 229 | return new Promise((resolve, reject) => { 230 | return resolve(this._returnCharacteristic(characteristic_name)); 231 | }).then(characteristic => { 232 | characteristic.stopNotifications().then(() => { 233 | this.cache[primary_service_name][characteristic_name].notifying = false; 234 | return true; 235 | }); 236 | }).catch(err => { 237 | return errorHandler('stop_notifications_error', err, characteristic_name); 238 | }); 239 | } else { 240 | return errorHandler('stop_notifications_not_notifying', null, characteristic_name); 241 | } 242 | } 243 | 244 | /** 245 | * addCharacteristic - adds a new characteristic object to bluetooth.gattCharacteristicsMapping 246 | * 247 | * @param {string} characteristic_name - GATT characteristic name or other characteristic 248 | * @param {string} primary_service_name - GATT primary service name or other parent service of characteristic 249 | * @param {array} propertiesArr - Array of GATT properties as Strings 250 | * 251 | * @return {boolean} - Result of attempt to add characteristic where true === successfully added 252 | */ 253 | addCharacteristic(characteristic_name, primary_service_name, propertiesArr) { 254 | if (bluetooth.gattCharacteristicsMapping[characteristic_name]) { 255 | return errorHandler('add_characteristic_exists_error', null, characteristic_name); 256 | } 257 | 258 | if (!characteristic_name || characteristic_name.constructor !== String || !characteristic_name.length) { 259 | return errorHandler('improper_characteristic_format', null, characteristic_name); 260 | } 261 | 262 | if (!bluetooth.gattCharacteristicsMapping[characteristic_name]) { 263 | if (!primary_service_name || !propertiesArr) { 264 | return errorHandler('new_characteristic_missing_params', null, characteristic_name); 265 | } 266 | if (primary_service_name.constructor !== String || !primary_service_name.length) { 267 | return errorHandler('improper_service_format', null, primary_service_name); 268 | } 269 | if (propertiesArr.constuctor !== Array || !propertiesArr.length) { 270 | return errorHandler('improper_properties_format', null, propertiesArr); 271 | } 272 | 273 | console.warn(`${ characteristic_name } is not yet fully supported.`); 274 | 275 | bluetooth.gattCharacteristicsMapping[characteristic_name] = { 276 | primaryServices: [primary_service_name], 277 | includedProperties: propertiesArr 278 | }; 279 | 280 | return true; 281 | } 282 | } 283 | 284 | /** 285 | * _returnCharacteristic - returns the value of a cached or resolved characteristic or resolved characteristic 286 | * 287 | * @param {string} characteristic_name - GATT characteristic name 288 | * @return {object|false} - the characteristic object, if successfully obtained 289 | */ 290 | _returnCharacteristic(characteristic_name) { 291 | if (!bluetooth.gattCharacteristicsMapping[characteristic_name]) { 292 | return errorHandler('characteristic_error', null, characteristic_name); 293 | } 294 | 295 | const characteristicObj = bluetooth.gattCharacteristicsMapping[characteristic_name]; 296 | const primary_service_name = characteristicObj.primaryServices[0]; 297 | 298 | if (this.cache[primary_service_name] && this.cache[primary_service_name][characteristic_name] && this.cache[primary_service_name][characteristic_name].cachedCharacteristic) { 299 | return this.cache[primary_service_name][characteristic_name].cachedCharacteristic; 300 | } else if (this.cache[primary_service_name] && this.cache[primary_service_name].cachedService) { 301 | this.cache[primary_service_name].cachedService.getCharacteristic(characteristic_name).then(characteristic => { 302 | this.cache[primary_service_name][characteristic_name] = { cachedCharacteristic: characteristic }; 303 | return characteristic; 304 | }).catch(err => { 305 | return errorHandler('_returnCharacteristic_error', err, characteristic_name); 306 | }); 307 | } else { 308 | return this.apiServer.getPrimaryService(primary_service_name).then(service => { 309 | this.cache[primary_service_name] = { 'cachedService': service }; 310 | return service.getCharacteristic(characteristic_name); 311 | }).then(characteristic => { 312 | this.cache[primary_service_name][characteristic_name] = { cachedCharacteristic: characteristic }; 313 | return characteristic; 314 | }).catch(err => { 315 | return errorHandler('_returnCharacteristic_error', err, characteristic_name); 316 | }); 317 | } 318 | } 319 | } 320 | 321 | module.exports = BluetoothDevice; 322 | 323 | },{"./bluetoothMap":2,"./errorHandler":3}],2:[function(require,module,exports){ 324 | const bluetoothMap = { 325 | gattCharacteristicsMapping: { 326 | battery_level: { 327 | primaryServices: ['battery_service'], 328 | includedProperties: ['read', 'notify'], 329 | parseValue: value => { 330 | value = value.buffer ? value : new DataView(value); 331 | let result = {}; 332 | result.battery_level = value.getUint8(0); 333 | return result; 334 | } 335 | }, 336 | blood_pressure_feature: { 337 | primaryServices: ['blood_pressure'], 338 | includedProperties: ['read'] 339 | }, 340 | body_composition_feature: { 341 | primaryServices: ['body_composition'], 342 | includedProperties: ['read'] 343 | }, 344 | bond_management_feature: { 345 | primaryServices: ['bond_management_feature'], 346 | includedProperties: ['read'] 347 | }, 348 | cgm_feature: { 349 | primaryServices: ['continuous_glucose_monitoring'], 350 | includedProperties: ['read'] 351 | }, 352 | cgm_session_run_time: { 353 | primaryServices: ['continuous_glucose_monitoring'], 354 | includedProperties: ['read'] 355 | }, 356 | cgm_session_start_time: { 357 | primaryServices: ['continuous_glucose_monitoring'], 358 | includedProperties: ['read', 'write'] 359 | }, 360 | cgm_status: { 361 | primaryServices: ['continuous_glucose_monitoring'], 362 | includedProperties: ['read'] 363 | }, 364 | csc_feature: { 365 | primaryServices: ['cycling_speed_and_cadence'], 366 | includedProperties: ['read'], 367 | parseValue: value => { 368 | value = value.buffer ? value : new DataView(value); 369 | let flags = value.getUint16(0); 370 | let wheelRevolutionDataSupported = flags & 0x1; 371 | let crankRevolutionDataSupported = flags & 0x2; 372 | let multipleSensDataSupported = flags & 0x3; 373 | let result = {}; 374 | if (wheelRevolutionDataSupported) { 375 | result.wheel_revolution_data_supported = wheelRevolutionDataSupported ? true : false; 376 | } 377 | if (crankRevolutionDataSupported) { 378 | result.crank_revolution_data_supported = crankRevolutionDataSupported ? true : false; 379 | } 380 | if (multipleSensDataSupported) { 381 | result.multiple_sensors_supported = multipleSensDataSupported ? true : false; 382 | } 383 | return result; 384 | } 385 | }, 386 | current_time: { 387 | primaryServices: ['current_time'], 388 | includedProperties: ['read', 'write', 'notify'] 389 | }, 390 | cycling_power_feature: { 391 | primaryServices: ['cycling_power'], 392 | includedProperties: ['read'] 393 | }, 394 | firmware_revision_string: { 395 | primaryServices: ['device_information'], 396 | includedProperties: ['read'] 397 | }, 398 | hardware_revision_string: { 399 | primaryServices: ['device_information'], 400 | includedProperties: ['read'] 401 | }, 402 | ieee_11073_20601_regulatory_certification_data_list: { 403 | primaryServices: ['device_information'], 404 | includedProperties: ['read'] 405 | }, 406 | 'gap.appearance': { 407 | primaryServices: ['generic_access'], 408 | includedProperties: ['read'] 409 | }, 410 | 'gap.device_name': { 411 | primaryServices: ['generic_access'], 412 | includedProperties: ['read', 'write'], 413 | parseValue: value => { 414 | value = value.buffer ? value : new DataView(value); 415 | let result = {}; 416 | result.device_name = ''; 417 | for (var i = 0; i < value.byteLength; i++) { 418 | result.device_name += String.fromCharCode(value.getUint8(i)); 419 | } 420 | return result; 421 | }, 422 | prepValue: value => { 423 | let buffer = new ArrayBuffer(value.length); 424 | let preppedValue = new DataView(buffer); 425 | value.split('').forEach((char, i) => { 426 | preppedValue.setUint8(i, char.charCodeAt(0)); 427 | }); 428 | return preppedValue; 429 | } 430 | }, 431 | 'gap.peripheral_preferred_connection_parameters': { 432 | primaryServices: ['generic_access'], 433 | includedProperties: ['read'] 434 | }, 435 | 'gap.peripheral_privacy_flag': { 436 | primaryServices: ['generic_access'], 437 | includedProperties: ['read'] 438 | }, 439 | glucose_feature: { 440 | primaryServices: ['glucose'], 441 | includedProperties: ['read'], 442 | parseValue: value => { 443 | value = value.buffer ? value : new DataView(value); 444 | let result = {}; 445 | let flags = value.getUint16(0); 446 | result.low_battery_detection_supported = flags & 0x1; 447 | result.sensor_malfunction_detection_supported = flags & 0x2; 448 | result.sensor_sample_size_supported = flags & 0x4; 449 | result.sensor_strip_insertion_error_detection_supported = flags & 0x8; 450 | result.sensor_strip_type_error_detection_supported = flags & 0x10; 451 | result.sensor_result_highLow_detection_supported = flags & 0x20; 452 | result.sensor_temperature_highLow_detection_supported = flags & 0x40; 453 | result.sensor_read_interruption_detection_supported = flags & 0x80; 454 | result.general_device_fault_supported = flags & 0x100; 455 | result.time_fault_supported = flags & 0x200; 456 | result.multiple_bond_supported = flags & 0x400; 457 | return result; 458 | } 459 | }, 460 | http_entity_body: { 461 | primaryServices: ['http_proxy'], 462 | includedProperties: ['read', 'write'] 463 | }, 464 | glucose_measurement: { 465 | primaryServices: ['glucose'], 466 | includedProperties: ['notify'], 467 | parseValue: value => { 468 | value = value.buffer ? value : new DataView(value); 469 | let flags = value.getUint8(0); 470 | let timeOffset = flags & 0x1; 471 | let concentrationTypeSampleLoc = flags & 0x2; 472 | let concentrationUnits = flags & 0x4; 473 | let statusAnnunciation = flags & 0x8; 474 | let contextInformation = flags & 0x10; 475 | let result = {}; 476 | let index = 1; 477 | if (timeOffset) { 478 | result.time_offset = value.getInt16(index, /*little-endian=*/true); 479 | index += 2; 480 | } 481 | if (concentrationTypeSampleLoc) { 482 | if (concentrationUnits) { 483 | result.glucose_concentraiton_molPerL = value.getInt16(index, /*little-endian=*/true); 484 | index += 2; 485 | } else { 486 | result.glucose_concentraiton_kgPerL = value.getInt16(index, /*little-endian=*/true); 487 | index += 2; 488 | } 489 | } 490 | return result; 491 | } 492 | }, 493 | http_headers: { 494 | primaryServices: ['http_proxy'], 495 | includedProperties: ['read', 'write'] 496 | }, 497 | https_security: { 498 | primaryServices: ['http_proxy'], 499 | includedProperties: ['read', 'write'] 500 | }, 501 | intermediate_temperature: { 502 | primaryServices: ['health_thermometer'], 503 | includedProperties: ['read', 'write', 'indicate'] 504 | }, 505 | local_time_information: { 506 | primaryServices: ['current_time'], 507 | includedProperties: ['read', 'write'] 508 | }, 509 | manufacturer_name_string: { 510 | primaryServices: ['device_information'], 511 | includedProperties: ['read'] 512 | }, 513 | model_number_string: { 514 | primaryServices: ['device_information'], 515 | includedProperties: ['read'] 516 | }, 517 | pnp_id: { 518 | primaryServices: ['device_information'], 519 | includedProperties: ['read'] 520 | }, 521 | protocol_mode: { 522 | primaryServices: ['human_interface_device'], 523 | includedProperties: ['read', 'writeWithoutResponse'] 524 | }, 525 | reference_time_information: { 526 | primaryServices: ['current_time'], 527 | includedProperties: ['read'] 528 | }, 529 | supported_new_alert_category: { 530 | primaryServices: ['alert_notification'], 531 | includedProperties: ['read'] 532 | }, 533 | body_sensor_location: { 534 | primaryServices: ['heart_rate'], 535 | includedProperties: ['read'], 536 | parseValue: value => { 537 | value = value.buffer ? value : new DataView(value); 538 | let val = value.getUint8(0); 539 | let result = {}; 540 | switch (val) { 541 | case 0: 542 | result.location = 'Other'; 543 | case 1: 544 | result.location = 'Chest'; 545 | case 2: 546 | result.location = 'Wrist'; 547 | case 3: 548 | result.location = 'Finger'; 549 | case 4: 550 | result.location = 'Hand'; 551 | case 5: 552 | result.location = 'Ear Lobe'; 553 | case 6: 554 | result.location = 'Foot'; 555 | default: 556 | result.location = 'Unknown'; 557 | } 558 | return result; 559 | } 560 | }, 561 | // heart_rate_control_point 562 | heart_rate_control_point: { 563 | primaryServices: ['heart_rate'], 564 | includedProperties: ['write'], 565 | prepValue: value => { 566 | let buffer = new ArrayBuffer(1); 567 | let writeView = new DataView(buffer); 568 | writeView.setUint8(0, value); 569 | return writeView; 570 | } 571 | }, 572 | heart_rate_measurement: { 573 | primaryServices: ['heart_rate'], 574 | includedProperties: ['notify'], 575 | /** 576 | * Parses the event.target.value object and returns object with readable 577 | * key-value pairs for all advertised characteristic values 578 | * 579 | * @param {Object} value Takes event.target.value object from startNotifications method 580 | * 581 | * @return {Object} result Returns readable object with relevant characteristic values 582 | * 583 | */ 584 | parseValue: value => { 585 | value = value.buffer ? value : new DataView(value); 586 | let flags = value.getUint8(0); 587 | let rate16Bits = flags & 0x1; 588 | let contactDetected = flags & 0x2; 589 | let contactSensorPresent = flags & 0x4; 590 | let energyPresent = flags & 0x8; 591 | let rrIntervalPresent = flags & 0x10; 592 | let result = {}; 593 | let index = 1; 594 | if (rate16Bits) { 595 | result.heartRate = value.getUint16(index, /*little-endian=*/true); 596 | index += 2; 597 | } else { 598 | result.heartRate = value.getUint8(index); 599 | index += 1; 600 | } 601 | if (contactSensorPresent) { 602 | result.contactDetected = !!contactDetected; 603 | } 604 | if (energyPresent) { 605 | result.energyExpended = value.getUint16(index, /*little-endian=*/true); 606 | index += 2; 607 | } 608 | if (rrIntervalPresent) { 609 | let rrIntervals = []; 610 | for (; index + 1 < value.byteLength; index += 2) { 611 | rrIntervals.push(value.getUint16(index, /*little-endian=*/true)); 612 | } 613 | result.rrIntervals = rrIntervals; 614 | } 615 | return result; 616 | } 617 | }, 618 | serial_number_string: { 619 | primaryServices: ['device_information'], 620 | includedProperties: ['read'] 621 | }, 622 | software_revision_string: { 623 | primaryServices: ['device_information'], 624 | includedProperties: ['read'] 625 | }, 626 | supported_unread_alert_category: { 627 | primaryServices: ['alert_notification'], 628 | includedProperties: ['read'] 629 | }, 630 | system_id: { 631 | primaryServices: ['device_information'], 632 | includedProperties: ['read'] 633 | }, 634 | temperature_type: { 635 | primaryServices: ['health_thermometer'], 636 | includedProperties: ['read'] 637 | }, 638 | descriptor_value_changed: { 639 | primaryServices: ['environmental_sensing'], 640 | includedProperties: ['indicate', 'writeAux', 'extProp'] 641 | }, 642 | apparent_wind_direction: { 643 | primaryServices: ['environmental_sensing'], 644 | includedProperties: ['read', 'notify', 'writeAux', 'extProp'], 645 | parseValue: value => { 646 | value = value.buffer ? value : new DataView(value); 647 | let result = {}; 648 | result.apparent_wind_direction = value.getUint16(0) * 0.01; 649 | return result; 650 | } 651 | }, 652 | apparent_wind_speed: { 653 | primaryServices: ['environmental_sensing'], 654 | includedProperties: ['read', 'notify', 'writeAux', 'extProp'], 655 | parseValue: value => { 656 | value = value.buffer ? value : new DataView(value); 657 | let result = {}; 658 | result.apparent_wind_speed = value.getUint16(0) * 0.01; 659 | return result; 660 | } 661 | }, 662 | dew_point: { 663 | primaryServices: ['environmental_sensing'], 664 | includedProperties: ['read', 'notify', 'writeAux', 'extProp'], 665 | parseValue: value => { 666 | value = value.buffer ? value : new DataView(value); 667 | let result = {}; 668 | result.dew_point = value.getInt8(0); 669 | return result; 670 | } 671 | }, 672 | elevation: { 673 | primaryServices: ['environmental_sensing'], 674 | includedProperties: ['read', 'notify', 'writeAux', 'extProp'], 675 | parseValue: value => { 676 | value = value.buffer ? value : new DataView(value); 677 | let result = {}; 678 | result.elevation = value.getInt8(0) << 16 | value.getInt8(1) << 8 | value.getInt8(2); 679 | return result; 680 | } 681 | }, 682 | gust_factor: { 683 | primaryServices: ['environmental_sensing'], 684 | includedProperties: ['read', 'notify', 'writeAux', 'extProp'], 685 | parseValue: value => { 686 | value = value.buffer ? value : new DataView(value); 687 | let result = {}; 688 | result.gust_factor = value.getUint8(0) * 0.1; 689 | return result; 690 | } 691 | }, 692 | heat_index: { 693 | primaryServices: ['environmental_sensing'], 694 | includedProperties: ['read', 'notify', 'writeAux', 'extProp'], 695 | parseValue: value => { 696 | value = value.buffer ? value : new DataView(value); 697 | let result = {}; 698 | result.heat_index = value.getInt8(0); 699 | return result; 700 | } 701 | }, 702 | humidity: { 703 | primaryServices: ['environmental_sensing'], 704 | includedProperties: ['read', 'notify', 'writeAux', 'extProp'], 705 | parseValue: value => { 706 | value = value.buffer ? value : new DataView(value); 707 | let result = {}; 708 | 709 | result.humidity = value.getUint16(0) * 0.01; 710 | return result; 711 | } 712 | }, 713 | irradiance: { 714 | primaryServices: ['environmental_sensing'], 715 | includedProperties: ['read', 'notify', 'writeAux', 'extProp'], 716 | parseValue: value => { 717 | value = value.buffer ? value : new DataView(value); 718 | let result = {}; 719 | 720 | result.irradiance = value.getUint16(0) * 0.1; 721 | return result; 722 | } 723 | }, 724 | rainfall: { 725 | primaryServices: ['environmental_sensing'], 726 | includedProperties: ['read', 'notify', 'writeAux', 'extProp'], 727 | parseValue: value => { 728 | value = value.buffer ? value : new DataView(value); 729 | let result = {}; 730 | 731 | result.rainfall = value.getUint16(0) * 0.001; 732 | return result; 733 | } 734 | }, 735 | pressure: { 736 | primaryServices: ['environmental_sensing'], 737 | includedProperties: ['read', 'notify', 'writeAux', 'extProp'], 738 | parseValue: value => { 739 | value = value.buffer ? value : new DataView(value); 740 | let result = {}; 741 | result.pressure = value.getUint32(0) * 0.1; 742 | return result; 743 | } 744 | }, 745 | temperature: { 746 | primaryServices: ['environmental_sensing'], 747 | includedProperties: ['read', 'notify', 'writeAux', 'extProp'], 748 | parseValue: value => { 749 | value = value.buffer ? value : new DataView(value); 750 | let result = {}; 751 | result.temperature = value.getInt16(0) * 0.01; 752 | return result; 753 | } 754 | }, 755 | true_wind_direction: { 756 | primaryServices: ['environmental_sensing'], 757 | includedProperties: ['read', 'notify', 'writeAux', 'extProp'], 758 | parseValue: value => { 759 | value = value.buffer ? value : new DataView(value); 760 | let result = {}; 761 | result.true_wind_direction = value.getUint16(0) * 0.01; 762 | return result; 763 | } 764 | }, 765 | true_wind_speed: { 766 | primaryServices: ['environmental_sensing'], 767 | includedProperties: ['read', 'notify', 'writeAux', 'extProp'], 768 | parseValue: value => { 769 | value = value.buffer ? value : new DataView(value); 770 | let result = {}; 771 | result.true_wind_speed = value.getUint16(0) * 0.01; 772 | return result; 773 | } 774 | }, 775 | uv_index: { 776 | primaryServices: ['environmental_sensing'], 777 | includedProperties: ['read', 'notify', 'writeAux', 'extProp'], 778 | parseValue: value => { 779 | value = value.buffer ? value : new DataView(value); 780 | let result = {}; 781 | result.uv_index = value.getUint8(0); 782 | return result; 783 | } 784 | }, 785 | wind_chill: { 786 | primaryServices: ['environmental_sensing'], 787 | includedProperties: ['read', 'notify', 'writeAux', 'extProp'], 788 | parseValue: value => { 789 | value = value.buffer ? value : new DataView(value); 790 | let result = {}; 791 | result.wind_chill = value.getInt8(0); 792 | return result; 793 | } 794 | }, 795 | barometric_pressure_trend: { 796 | primaryServices: ['environmental_sensing'], 797 | includedProperties: ['read', 'notify', 'writeAux', 'extProp'], 798 | parseValue: value => { 799 | value = value.buffer ? value : new DataView(value); 800 | let val = value.getUint8(0); 801 | let result = {}; 802 | switch (val) { 803 | case 0: 804 | result.barometric_pressure_trend = 'Unknown'; 805 | case 1: 806 | result.barometric_pressure_trend = 'Continuously falling'; 807 | case 2: 808 | result.barometric_pressure_trend = 'Continously rising'; 809 | case 3: 810 | result.barometric_pressure_trend = 'Falling, then steady'; 811 | case 4: 812 | result.barometric_pressure_trend = 'Rising, then steady'; 813 | case 5: 814 | result.barometric_pressure_trend = 'Falling before a lesser rise'; 815 | case 6: 816 | result.barometric_pressure_trend = 'Falling before a greater rise'; 817 | case 7: 818 | result.barometric_pressure_trend = 'Rising before a greater fall'; 819 | case 8: 820 | result.barometric_pressure_trend = 'Rising before a lesser fall'; 821 | case 9: 822 | result.barometric_pressure_trend = 'Steady'; 823 | default: 824 | result.barometric_pressure_trend = 'Could not resolve to trend'; 825 | } 826 | return result; 827 | } 828 | }, 829 | magnetic_declination: { 830 | primaryServices: ['environmental_sensing'], 831 | includedProperties: ['read', 'notify', 'writeAux', 'extProp'], 832 | parseValue: value => { 833 | value = value.buffer ? value : new DataView(value); 834 | let result = {}; 835 | 836 | result.magnetic_declination = value.getUint16(0) * 0.01; 837 | return result; 838 | } 839 | }, 840 | magnetic_flux_density_2D: { 841 | primaryServices: ['environmental_sensing'], 842 | includedProperties: ['read', 'notify', 'writeAux', 'extProp'], 843 | parseValue: value => { 844 | value = value.buffer ? value : new DataView(value); 845 | let result = {}; 846 | //FIXME: need to find out if these values are stored at different byte addresses 847 | // below assumes that values are stored at successive byte addresses 848 | result.magnetic_flux_density_x_axis = value.getInt16(0, /*little-endian=*/true) * 0.0000001; 849 | result.magnetic_flux_density_y_axis = value.getInt16(2, /*little-endian=*/true) * 0.0000001; 850 | return result; 851 | } 852 | }, 853 | magnetic_flux_density_3D: { 854 | primaryServices: ['environmental_sensing'], 855 | includedProperties: ['read', 'notify', 'writeAux', 'extProp'], 856 | parseValue: value => { 857 | value = value.buffer ? value : new DataView(value); 858 | let result = {}; 859 | //FIXME: need to find out if these values are stored at different byte addresses 860 | // below assumes that values are stored at successive byte addresses 861 | result.magnetic_flux_density_x_axis = value.getInt16(0, /*little-endian=*/true) * 0.0000001; 862 | result.magnetic_flux_density_y_axis = value.getInt16(2, /*little-endian=*/true) * 0.0000001; 863 | result.magnetic_flux_density_z_axis = value.getInt16(4, /*little-endian=*/true) * 0.0000001; 864 | return result; 865 | } 866 | }, 867 | tx_power_level: { 868 | primaryServices: ['tx_power'], 869 | includedProperties: ['read'], 870 | parseValue: value => { 871 | value = value.buffer ? value : new DataView(value); 872 | let result = {}; 873 | result.tx_power_level = value.getInt8(0); 874 | return result; 875 | } 876 | }, 877 | weight_scale_feature: { 878 | primaryServices: ['weight_scale'], 879 | includedProperties: ['read'], 880 | parseValue: value => { 881 | value = value.buffer ? value : new DataView(value); 882 | let result = {}; 883 | let flags = value.getInt32(0); 884 | result.time_stamp_supported = flags & 0x1; 885 | result.multiple_sensors_supported = flags & 0x2; 886 | result.BMI_supported = flags & 0x4; 887 | switch (flags & 0x78 >> 3) { 888 | case 0: 889 | result.weight_measurement_resolution = 'Not specified'; 890 | case 1: 891 | result.weight_measurement_resolution = 'Resolution of 0.5 kg or 1 lb'; 892 | case 2: 893 | result.weight_measurement_resolution = 'Resolution of 0.2 kg or 0.5 lb'; 894 | case 3: 895 | result.weight_measurement_resolution = 'Resolution of 0.1 kg or 0.2 lb'; 896 | case 4: 897 | result.weight_measurement_resolution = 'Resolution of 0.05 kg or 0.1 lb'; 898 | case 5: 899 | result.weight_measurement_resolution = 'Resolution of 0.02 kg or 0.05 lb'; 900 | case 6: 901 | result.weight_measurement_resolution = 'Resolution of 0.01 kg or 0.02 lb'; 902 | case 7: 903 | result.weight_measurement_resolution = 'Resolution of 0.005 kg or 0.01 lb'; 904 | default: 905 | result.weight_measurement_resolution = 'Could not resolve'; 906 | } 907 | switch (flags & 0x380 >> 7) { 908 | case 0: 909 | result.height_measurement_resolution = 'Not specified'; 910 | case 1: 911 | result.height_measurement_resolution = 'Resolution of 0.1 meter or 1 inch'; 912 | case 2: 913 | result.height_measurement_resolution = 'Resolution of 0.005 meter or 0.5 inch'; 914 | case 3: 915 | result.height_measurement_resolution = 'Resolution of 0.001 meter or 0.1 inch'; 916 | default: 917 | result.height_measurement_resolution = 'Could not resolve'; 918 | } 919 | // Remaining flags reserved for future use 920 | return result; 921 | } 922 | }, 923 | csc_measurement: { 924 | primaryServices: ['cycling_speed_and_cadence'], 925 | includedProperties: ['notify'], 926 | parseValue: value => { 927 | value = value.buffer ? value : new DataView(value); 928 | let flags = value.getUint8(0); 929 | let wheelRevolution = flags & 0x1; //integer = truthy, 0 = falsy 930 | let crankRevolution = flags & 0x2; 931 | let result = {}; 932 | let index = 1; 933 | if (wheelRevolution) { 934 | result.cumulative_wheel_revolutions = value.getUint32(index, /*little-endian=*/true); 935 | index += 4; 936 | result.last_wheel_event_time_per_1024s = value.getUint16(index, /*little-endian=*/true); 937 | index += 2; 938 | } 939 | if (crankRevolution) { 940 | result.cumulative_crank_revolutions = value.getUint16(index, /*little-endian=*/true); 941 | index += 2; 942 | result.last_crank_event_time_per_1024s = value.getUint16(index, /*little-endian=*/true); 943 | index += 2; 944 | } 945 | return result; 946 | } 947 | }, 948 | sensor_location: { 949 | primaryServices: ['cycling_speed_and_cadence'], 950 | includedProperties: ['read'], 951 | parseValue: value => { 952 | value = value.buffer ? value : new DataView(value); 953 | let val = value.getUint16(0); 954 | let result = {}; 955 | switch (val) { 956 | case 0: 957 | result.location = 'Other'; 958 | case 1: 959 | result.location = 'Top of show'; 960 | case 2: 961 | result.location = 'In shoe'; 962 | case 3: 963 | result.location = 'Hip'; 964 | case 4: 965 | result.location = 'Front Wheel'; 966 | case 5: 967 | result.location = 'Left Crank'; 968 | case 6: 969 | result.location = 'Right Crank'; 970 | case 7: 971 | result.location = 'Left Pedal'; 972 | case 8: 973 | result.location = 'Right Pedal'; 974 | case 9: 975 | result.location = 'Front Hub'; 976 | case 10: 977 | result.location = 'Rear Dropout'; 978 | case 11: 979 | result.location = 'Chainstay'; 980 | case 12: 981 | result.location = 'Rear Wheel'; 982 | case 13: 983 | result.location = 'Rear Hub'; 984 | case 14: 985 | result.location = 'Chest'; 986 | case 15: 987 | result.location = 'Spider'; 988 | case 16: 989 | result.location = 'Chain Ring'; 990 | default: 991 | result.location = 'Unknown'; 992 | } 993 | return result; 994 | } 995 | }, 996 | sc_control_point: { 997 | primaryServices: ['cycling_speed_and_cadence'], 998 | includedProperties: ['write', 'indicate'], 999 | parseValue: value => { 1000 | value = value.buffer ? value : new DataView(value); 1001 | return result; 1002 | } 1003 | }, 1004 | cycling_power_measurement: { 1005 | primaryServices: ['cycling_power'], 1006 | includedProperties: ['notify'], 1007 | parseValue: value => { 1008 | value = value.buffer ? value : new DataView(value); 1009 | let flags = value.getUint16(0); 1010 | let pedal_power_balance_present = flags & 0x1; 1011 | let pedal_power_reference = flags & 0x2; 1012 | let accumulated_torque_present = flags & 0x4; 1013 | let accumulated_torque_source = flags & 0x8; 1014 | let wheel_revolution_data_present = flags & 0x10; 1015 | let crank_revolution_data_present = flags & 0x12; 1016 | let extreme_force_magnitude_present = flags & 0x12; 1017 | let extreme_torque_magnitude_present = flags & 0x12; 1018 | let extreme_angles_present = flags & 0x12; 1019 | let top_dead_spot_angle_present = flags & 0x12; 1020 | let bottom_dead_spot_angle_present = flags & 0x12; 1021 | let accumulated_energy_present = flags & 0x12; 1022 | let offset_compensation_indicator = flags & 0x12; 1023 | let result = {}; 1024 | let index = 1; 1025 | //Watts with resolution of 1 1026 | result.instantaneous_power = value.getInt16(index); 1027 | index += 2; 1028 | if (pedal_power_reference) { 1029 | //Percentage with resolution of 1/2 1030 | result.pedal_power_balance = value.getUint8(index); 1031 | index += 1; 1032 | } 1033 | if (accumulated_torque_present) { 1034 | //Percentage with resolution of 1/2 1035 | result.accumulated_torque = value.getUint16(index); 1036 | index += 2; 1037 | } 1038 | if (wheel_revolution_data_present) { 1039 | result.cumulative_wheel_revolutions = value.Uint32(index); 1040 | index += 4; 1041 | result.last_wheel_event_time_per_2048s = value.Uint16(index); 1042 | index += 2; 1043 | } 1044 | if (crank_revolution_data_present) { 1045 | result.cumulative_crank_revolutions = value.getUint16(index, /*little-endian=*/true); 1046 | index += 2; 1047 | result.last_crank_event_time_per_1024s = value.getUint16(index, /*little-endian=*/true); 1048 | index += 2; 1049 | } 1050 | if (extreme_force_magnitude_present) { 1051 | //Newton meters with resolution of 1 TODO: units? 1052 | result.maximum_force_magnitude = value.getInt16(index); 1053 | index += 2; 1054 | result.minimum_force_magnitude = value.getInt16(index); 1055 | index += 2; 1056 | } 1057 | if (extreme_torque_magnitude_present) { 1058 | //Newton meters with resolution of 1 TODO: units? 1059 | result.maximum_torque_magnitude = value.getInt16(index); 1060 | index += 2; 1061 | result.minimum_torque_magnitude = value.getInt16(index); 1062 | index += 2; 1063 | } 1064 | if (extreme_angles_present) { 1065 | //TODO: UINT12. 1066 | //Newton meters with resolution of 1 TODO: units? 1067 | // result.maximum_angle = value.getInt12(index); 1068 | // index += 2; 1069 | // result.minimum_angle = value.getInt12(index); 1070 | // index += 2; 1071 | } 1072 | if (top_dead_spot_angle_present) { 1073 | //Percentage with resolution of 1/2 1074 | result.top_dead_spot_angle = value.getUint16(index); 1075 | index += 2; 1076 | } 1077 | if (bottom_dead_spot_angle_present) { 1078 | //Percentage with resolution of 1/2 1079 | result.bottom_dead_spot_angle = value.getUint16(index); 1080 | index += 2; 1081 | } 1082 | if (accumulated_energy_present) { 1083 | //kilojoules with resolution of 1 TODO: units? 1084 | result.accumulated_energy = value.getUint16(index); 1085 | index += 2; 1086 | } 1087 | return result; 1088 | } 1089 | } 1090 | }, 1091 | 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'] 1092 | }; 1093 | 1094 | module.exports = bluetoothMap; 1095 | 1096 | },{}],3:[function(require,module,exports){ 1097 | /** errorHandler - Consolodates error message configuration and logic 1098 | * 1099 | * @param {string} errorKey - maps to a detailed error message 1100 | * @param {object} nativeError - the native API error object, if present 1101 | * @param {} alternate - 1102 | * 1103 | */ 1104 | function errorHandler(errorKey, nativeError, alternate) { 1105 | 1106 | const errorMessages = { 1107 | add_characteristic_exists_error: `Characteristic ${ alternate } already exists.`, 1108 | characteristic_error: `Characteristic ${ alternate } not found. Add ${ alternate } to device using addCharacteristic or try another characteristic.`, 1109 | connect_gatt: `Could not connect to GATT. Device might be out of range. Also check to see if filters are vaild.`, 1110 | connect_server: `Could not connect to server on device.`, 1111 | connect_service: `Could not find service.`, 1112 | disconnect_timeout: `Timed out. Could not disconnect.`, 1113 | disconnect_error: `Could not disconnect from device.`, 1114 | improper_characteristic_format: `${ alternate } is not a properly formatted characteristic.`, 1115 | improper_properties_format: `${ alternate } is not a properly formatted properties array.`, 1116 | improper_service_format: `${ alternate } is not a properly formatted service.`, 1117 | issue_disconnecting: `Issue disconnecting with device.`, 1118 | new_characteristic_missing_params: `${ alternate } is not a fully supported characteristic. Please provide an associated primary service and at least one property.`, 1119 | no_device: `No instance of device found.`, 1120 | no_filters: `No filters found on instance of Device. For more information, please visit http://sabertooth.io/#method-newdevice`, 1121 | no_read_property: `No read property on characteristic: ${ alternate }.`, 1122 | no_write_property: `No write property on this characteristic.`, 1123 | not_connected: `Could not disconnect. Device not connected.`, 1124 | parsing_not_supported: `Parsing not supported for characterstic: ${ alternate }.`, 1125 | read_error: `Cannot read value on the characteristic.`, 1126 | _returnCharacteristic_error: `Error accessing characteristic ${ alternate }.`, 1127 | start_notifications_error: `Not able to read stream of data from characteristic: ${ alternate }.`, 1128 | start_notifications_no_notify: `No notify property found on this characteristic: ${ alternate }.`, 1129 | stop_notifications_not_notifying: `Notifications not established for characteristic: ${ alternate } or you have not started notifications.`, 1130 | stop_notifications_error: `Issue stopping notifications for characteristic: ${ alternate } or you have not started notifications.`, 1131 | user_cancelled: `User cancelled the permission request.`, 1132 | uuid_error: `Invalid UUID. For more information on proper formatting of UUIDs, visit https://webbluetoothcg.github.io/web-bluetooth/#uuids`, 1133 | write_error: `Could not change value of characteristic: ${ alternate }.`, 1134 | write_permissions: `${ alternate } characteristic does not have a write property.` 1135 | }; 1136 | 1137 | throw new Error(errorMessages[errorKey]); 1138 | return false; 1139 | } 1140 | 1141 | module.exports = errorHandler; 1142 | 1143 | },{}]},{},[1]); 1144 | -------------------------------------------------------------------------------- /dist/npm/BluetoothDevice.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 4 | 5 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 6 | 7 | var bluetooth = require('./bluetoothMap'); 8 | var errorHandler = require('./errorHandler'); 9 | 10 | /** BluetoothDevice - 11 | * 12 | * @method connect - Establishes a connection with the device 13 | * @method connected - checks apiDevice to see whether device is connected 14 | * @method disconnect - terminates the connection with the device and pauses all data stream subscriptions 15 | * @method getValue - reads the value of a specified characteristic 16 | * @method writeValue - writes data to a specified characteristic of the device 17 | * @method startNotifications - attempts to start notifications for changes to device values and attaches an event listener for each data transmission 18 | * @method stopNotifications - attempts to stop previously started notifications for a provided characteristic 19 | * @method addCharacteristic - adds a new characteristic object to bluetooth.gattCharacteristicsMapping 20 | * @method _returnCharacteristic - _returnCharacteristic - returns the value of a cached or resolved characteristic or resolved characteristic 21 | * 22 | * @param {object} filters - collection of filters for device selectin. All filters are optional, but at least 1 is required. 23 | * .name {string} 24 | * .namePrefix {string} 25 | * .uuid {string} 26 | * .services {array} 27 | * .optionalServices {array} - defaults to all available services, use an empty array to get no optional services 28 | * 29 | * @return {object} Returns a new instance of BluetoothDevice 30 | * 31 | */ 32 | 33 | var BluetoothDevice = function () { 34 | function BluetoothDevice(requestParams) { 35 | _classCallCheck(this, BluetoothDevice); 36 | 37 | this.requestParams = requestParams; 38 | this.apiDevice = null; 39 | this.apiServer = null; 40 | this.cache = {}; 41 | } 42 | 43 | _createClass(BluetoothDevice, [{ 44 | key: 'connected', 45 | value: function connected() { 46 | return this.apiDevice ? this.apiDevice.gatt.connected : errorHandler('no_device'); 47 | } 48 | 49 | /** connect - establishes a connection with the device 50 | * 51 | * NOTE: This method must be triggered by a user gesture to satisfy the native API's permissions 52 | * 53 | * @return {object} - native browser API device server object 54 | */ 55 | 56 | }, { 57 | key: 'connect', 58 | value: function connect() { 59 | var _this = this; 60 | 61 | var filters = this.requestParams; 62 | var requestParams = { filters: [] }; 63 | var uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]/; 64 | 65 | if (!Object.keys(filters).length) { 66 | return errorHandler('no_filters'); 67 | } 68 | if (filters.name) requestParams.filters.push({ name: filters.name }); 69 | if (filters.namePrefix) requestParams.filters.push({ namePrefix: filters.namePrefix }); 70 | if (filters.uuid) { 71 | if (!filters.uuid.match(uuidRegex)) { 72 | errorHandler('uuid_error'); 73 | } else { 74 | requestParams.filters.push({ uuid: filters.uuid }); 75 | } 76 | } 77 | if (filters.services) { 78 | (function () { 79 | var services = []; 80 | filters.services.forEach(function (service) { 81 | if (!bluetooth.gattServiceList.includes(service)) { 82 | console.warn(service + ' is not a valid service. Please check the service name.'); 83 | } else { 84 | services.push(service); 85 | } 86 | }); 87 | requestParams.filters.push({ services: services }); 88 | })(); 89 | } 90 | if (filters.optional_services) { 91 | filters.optional_services.forEach(function (service) { 92 | if (!bluetooth.gattServiceList.includes(service)) bluetooth.gattServiceList.push(service); 93 | }); 94 | } else { 95 | requestParams.optionalServices = bluetooth.gattServiceList; 96 | } 97 | 98 | return navigator.bluetooth.requestDevice(requestParams).then(function (device) { 99 | _this.apiDevice = device; 100 | return device.gatt.connect(); 101 | }).then(function (server) { 102 | _this.apiServer = server; 103 | return server; 104 | }).catch(function (err) { 105 | return errorHandler('user_cancelled', err); 106 | }); 107 | } 108 | 109 | /** disconnect - terminates the connection with the device and pauses all data stream subscriptions 110 | * @return {boolean} - success 111 | * 112 | */ 113 | 114 | }, { 115 | key: 'disconnect', 116 | value: function disconnect() { 117 | this.apiServer.connected ? this.apiServer.disconnect() : errorHandler('not_connected'); 118 | return this.apiServer.connected ? errorHandler('issue_disconnecting') : true; 119 | } 120 | 121 | /** getValue - reads the value of a specified characteristic 122 | * 123 | * @param {string} characteristic_name - GATT characteristic name 124 | * @return {promise} - resolves with an object that includes key-value pairs for each of the properties 125 | * successfully read and parsed from the device, as well as the 126 | * raw value object returned by a native readValue request to the 127 | * device characteristic. 128 | */ 129 | 130 | }, { 131 | key: 'getValue', 132 | value: function getValue(characteristic_name) { 133 | var _this2 = this; 134 | 135 | if (!bluetooth.gattCharacteristicsMapping[characteristic_name]) { 136 | return errorHandler('characteristic_error', null, characteristic_name); 137 | } 138 | 139 | var characteristicObj = bluetooth.gattCharacteristicsMapping[characteristic_name]; 140 | 141 | if (!characteristicObj.includedProperties.includes('read')) { 142 | console.warn('Attempting to access read property of ' + characteristic_name + ',\n which is not a included as a supported property of the\n characteristic. Attempt will resolve with an object including\n only a rawValue property with the native API return\n for an attempt to readValue() of ' + characteristic_name + '.'); 143 | } 144 | 145 | return new Promise(function (resolve, reject) { 146 | return resolve(_this2._returnCharacteristic(characteristic_name)); 147 | }).then(function (characteristic) { 148 | return characteristic.readValue(); 149 | }).then(function (value) { 150 | var returnObj = characteristicObj.parseValue ? characteristicObj.parseValue(value) : {}; 151 | returnObj.rawValue = value; 152 | return returnObj; 153 | }).catch(function (err) { 154 | return errorHandler('read_error', err); 155 | }); 156 | } 157 | 158 | /** writeValue - writes data to a specified characteristic of the device 159 | * 160 | * @param {string} characteristic_name - name of the GATT characteristic 161 | * https://www.bluetooth.com/specifications/assigned-numbers/generic-attribute-profile 162 | * 163 | * @param {string|number} value - value to write to the requested device characteristic 164 | * 165 | * 166 | * @return {boolean} - Result of attempt to write characteristic where true === successfully written 167 | */ 168 | 169 | }, { 170 | key: 'writeValue', 171 | value: function writeValue(characteristic_name, value) { 172 | var _this3 = this; 173 | 174 | if (!bluetooth.gattCharacteristicsMapping[characteristic_name]) { 175 | return errorHandler('characteristic_error', null, characteristic_name); 176 | } 177 | 178 | var characteristicObj = bluetooth.gattCharacteristicsMapping[characteristic_name]; 179 | 180 | if (!characteristicObj.includedProperties.includes('write')) { 181 | console.warn('Attempting to access write property of ' + characteristic_name + ',\n which is not a included as a supported property of the\n characteristic. Attempt will resolve with native API return\n for an attempt to writeValue(' + value + ') to ' + characteristic_name + '.'); 182 | } 183 | 184 | return new Promise(function (resolve, reject) { 185 | return resolve(_this3._returnCharacteristic(characteristic_name)); 186 | }).then(function (characteristic) { 187 | return characteristic.writeValue(characteristicObj.prepValue ? characteristicObj.prepValue(value) : value); 188 | }).then(function (changedChar) { 189 | return true; 190 | }).catch(function (err) { 191 | return errorHandler('write_error', err, characteristic_name); 192 | }); 193 | } 194 | 195 | /** startNotifications - attempts to start notifications for changes to device values and attaches an event listener for each data transmission 196 | * 197 | * @param {string} characteristic_name - GATT characteristic name 198 | * @param {callback} transmissionCallback - callback function to apply to each event while notifications are active 199 | * 200 | * @return 201 | * 202 | */ 203 | 204 | }, { 205 | key: 'startNotifications', 206 | value: function startNotifications(characteristic_name, transmissionCallback) { 207 | var _this4 = this; 208 | 209 | if (!bluetooth.gattCharacteristicsMapping[characteristic_name]) { 210 | return errorHandler('characteristic_error', null, characteristic_name); 211 | } 212 | 213 | var characteristicObj = bluetooth.gattCharacteristicsMapping[characteristic_name]; 214 | var primary_service_name = characteristicObj.primaryServices[0]; 215 | 216 | if (!characteristicObj.includedProperties.includes('notify')) { 217 | console.warn('Attempting to access notify property of ' + characteristic_name + ',\n which is not a included as a supported property of the\n characteristic. Attempt will resolve with an object including\n only a rawValue property with the native API return\n for an attempt to startNotifications() for ' + characteristic_name + '.'); 218 | } 219 | 220 | return new Promise(function (resolve, reject) { 221 | return resolve(_this4._returnCharacteristic(characteristic_name)); 222 | }).then(function (characteristic) { 223 | characteristic.startNotifications().then(function () { 224 | _this4.cache[primary_service_name][characteristic_name].notifying = true; 225 | return characteristic.addEventListener('characteristicvaluechanged', function (event) { 226 | var eventObj = characteristicObj.parseValue ? characteristicObj.parseValue(event.target.value) : {}; 227 | eventObj.rawValue = event; 228 | return transmissionCallback(eventObj); 229 | }); 230 | }); 231 | }).catch(function (err) { 232 | return errorHandler('start_notifications_error', err, characteristic_name); 233 | }); 234 | } 235 | 236 | /** stopNotifications - attempts to stop previously started notifications for a provided characteristic 237 | * 238 | * @param {string} characteristic_name - GATT characteristic name 239 | * 240 | * @return {boolean} success 241 | * 242 | */ 243 | 244 | }, { 245 | key: 'stopNotifications', 246 | value: function stopNotifications(characteristic_name) { 247 | var _this5 = this; 248 | 249 | if (!bluetooth.gattCharacteristicsMapping[characteristic_name]) { 250 | return errorHandler('characteristic_error', null, characteristic_name); 251 | } 252 | 253 | var characteristicObj = bluetooth.gattCharacteristicsMapping[characteristic_name]; 254 | var primary_service_name = characteristicObj.primaryServices[0]; 255 | 256 | if (this.cache[primary_service_name][characteristic_name].notifying) { 257 | return new Promise(function (resolve, reject) { 258 | return resolve(_this5._returnCharacteristic(characteristic_name)); 259 | }).then(function (characteristic) { 260 | characteristic.stopNotifications().then(function () { 261 | _this5.cache[primary_service_name][characteristic_name].notifying = false; 262 | return true; 263 | }); 264 | }).catch(function (err) { 265 | return errorHandler('stop_notifications_error', err, characteristic_name); 266 | }); 267 | } else { 268 | return errorHandler('stop_notifications_not_notifying', null, characteristic_name); 269 | } 270 | } 271 | 272 | /** 273 | * addCharacteristic - adds a new characteristic object to bluetooth.gattCharacteristicsMapping 274 | * 275 | * @param {string} characteristic_name - GATT characteristic name or other characteristic 276 | * @param {string} primary_service_name - GATT primary service name or other parent service of characteristic 277 | * @param {array} propertiesArr - Array of GATT properties as Strings 278 | * 279 | * @return {boolean} - Result of attempt to add characteristic where true === successfully added 280 | */ 281 | 282 | }, { 283 | key: 'addCharacteristic', 284 | value: function addCharacteristic(characteristic_name, primary_service_name, propertiesArr) { 285 | if (bluetooth.gattCharacteristicsMapping[characteristic_name]) { 286 | return errorHandler('add_characteristic_exists_error', null, characteristic_name); 287 | } 288 | 289 | if (!characteristic_name || characteristic_name.constructor !== String || !characteristic_name.length) { 290 | return errorHandler('improper_characteristic_format', null, characteristic_name); 291 | } 292 | 293 | if (!bluetooth.gattCharacteristicsMapping[characteristic_name]) { 294 | if (!primary_service_name || !propertiesArr) { 295 | return errorHandler('new_characteristic_missing_params', null, characteristic_name); 296 | } 297 | if (primary_service_name.constructor !== String || !primary_service_name.length) { 298 | return errorHandler('improper_service_format', null, primary_service_name); 299 | } 300 | if (propertiesArr.constuctor !== Array || !propertiesArr.length) { 301 | return errorHandler('improper_properties_format', null, propertiesArr); 302 | } 303 | 304 | console.warn(characteristic_name + ' is not yet fully supported.'); 305 | 306 | bluetooth.gattCharacteristicsMapping[characteristic_name] = { 307 | primaryServices: [primary_service_name], 308 | includedProperties: propertiesArr 309 | }; 310 | 311 | return true; 312 | } 313 | } 314 | 315 | /** 316 | * _returnCharacteristic - returns the value of a cached or resolved characteristic or resolved characteristic 317 | * 318 | * @param {string} characteristic_name - GATT characteristic name 319 | * @return {object|false} - the characteristic object, if successfully obtained 320 | */ 321 | 322 | }, { 323 | key: '_returnCharacteristic', 324 | value: function _returnCharacteristic(characteristic_name) { 325 | var _this6 = this; 326 | 327 | if (!bluetooth.gattCharacteristicsMapping[characteristic_name]) { 328 | return errorHandler('characteristic_error', null, characteristic_name); 329 | } 330 | 331 | var characteristicObj = bluetooth.gattCharacteristicsMapping[characteristic_name]; 332 | var primary_service_name = characteristicObj.primaryServices[0]; 333 | 334 | if (this.cache[primary_service_name] && this.cache[primary_service_name][characteristic_name] && this.cache[primary_service_name][characteristic_name].cachedCharacteristic) { 335 | return this.cache[primary_service_name][characteristic_name].cachedCharacteristic; 336 | } else if (this.cache[primary_service_name] && this.cache[primary_service_name].cachedService) { 337 | this.cache[primary_service_name].cachedService.getCharacteristic(characteristic_name).then(function (characteristic) { 338 | _this6.cache[primary_service_name][characteristic_name] = { cachedCharacteristic: characteristic }; 339 | return characteristic; 340 | }).catch(function (err) { 341 | return errorHandler('_returnCharacteristic_error', err, characteristic_name); 342 | }); 343 | } else { 344 | return this.apiServer.getPrimaryService(primary_service_name).then(function (service) { 345 | _this6.cache[primary_service_name] = { 'cachedService': service }; 346 | return service.getCharacteristic(characteristic_name); 347 | }).then(function (characteristic) { 348 | _this6.cache[primary_service_name][characteristic_name] = { cachedCharacteristic: characteristic }; 349 | return characteristic; 350 | }).catch(function (err) { 351 | return errorHandler('_returnCharacteristic_error', err, characteristic_name); 352 | }); 353 | } 354 | } 355 | }]); 356 | 357 | return BluetoothDevice; 358 | }(); 359 | 360 | module.exports = BluetoothDevice; -------------------------------------------------------------------------------- /dist/npm/bluetoothMap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var bluetoothMap = { 4 | gattCharacteristicsMapping: { 5 | battery_level: { 6 | primaryServices: ['battery_service'], 7 | includedProperties: ['read', 'notify'], 8 | parseValue: function parseValue(value) { 9 | value = value.buffer ? value : new DataView(value); 10 | var result = {}; 11 | result.battery_level = value.getUint8(0); 12 | return result; 13 | } 14 | }, 15 | blood_pressure_feature: { 16 | primaryServices: ['blood_pressure'], 17 | includedProperties: ['read'] 18 | }, 19 | body_composition_feature: { 20 | primaryServices: ['body_composition'], 21 | includedProperties: ['read'] 22 | }, 23 | bond_management_feature: { 24 | primaryServices: ['bond_management_feature'], 25 | includedProperties: ['read'] 26 | }, 27 | cgm_feature: { 28 | primaryServices: ['continuous_glucose_monitoring'], 29 | includedProperties: ['read'] 30 | }, 31 | cgm_session_run_time: { 32 | primaryServices: ['continuous_glucose_monitoring'], 33 | includedProperties: ['read'] 34 | }, 35 | cgm_session_start_time: { 36 | primaryServices: ['continuous_glucose_monitoring'], 37 | includedProperties: ['read', 'write'] 38 | }, 39 | cgm_status: { 40 | primaryServices: ['continuous_glucose_monitoring'], 41 | includedProperties: ['read'] 42 | }, 43 | csc_feature: { 44 | primaryServices: ['cycling_speed_and_cadence'], 45 | includedProperties: ['read'], 46 | parseValue: function parseValue(value) { 47 | value = value.buffer ? value : new DataView(value); 48 | var flags = value.getUint16(0); 49 | var wheelRevolutionDataSupported = flags & 0x1; 50 | var crankRevolutionDataSupported = flags & 0x2; 51 | var multipleSensDataSupported = flags & 0x3; 52 | var result = {}; 53 | if (wheelRevolutionDataSupported) { 54 | result.wheel_revolution_data_supported = wheelRevolutionDataSupported ? true : false; 55 | } 56 | if (crankRevolutionDataSupported) { 57 | result.crank_revolution_data_supported = crankRevolutionDataSupported ? true : false; 58 | } 59 | if (multipleSensDataSupported) { 60 | result.multiple_sensors_supported = multipleSensDataSupported ? true : false; 61 | } 62 | return result; 63 | } 64 | }, 65 | current_time: { 66 | primaryServices: ['current_time'], 67 | includedProperties: ['read', 'write', 'notify'] 68 | }, 69 | cycling_power_feature: { 70 | primaryServices: ['cycling_power'], 71 | includedProperties: ['read'] 72 | }, 73 | firmware_revision_string: { 74 | primaryServices: ['device_information'], 75 | includedProperties: ['read'] 76 | }, 77 | hardware_revision_string: { 78 | primaryServices: ['device_information'], 79 | includedProperties: ['read'] 80 | }, 81 | ieee_11073_20601_regulatory_certification_data_list: { 82 | primaryServices: ['device_information'], 83 | includedProperties: ['read'] 84 | }, 85 | 'gap.appearance': { 86 | primaryServices: ['generic_access'], 87 | includedProperties: ['read'] 88 | }, 89 | 'gap.device_name': { 90 | primaryServices: ['generic_access'], 91 | includedProperties: ['read', 'write'], 92 | parseValue: function parseValue(value) { 93 | value = value.buffer ? value : new DataView(value); 94 | var result = {}; 95 | result.device_name = ''; 96 | for (var i = 0; i < value.byteLength; i++) { 97 | result.device_name += String.fromCharCode(value.getUint8(i)); 98 | } 99 | return result; 100 | }, 101 | prepValue: function prepValue(value) { 102 | var buffer = new ArrayBuffer(value.length); 103 | var preppedValue = new DataView(buffer); 104 | value.split('').forEach(function (char, i) { 105 | preppedValue.setUint8(i, char.charCodeAt(0)); 106 | }); 107 | return preppedValue; 108 | } 109 | }, 110 | 'gap.peripheral_preferred_connection_parameters': { 111 | primaryServices: ['generic_access'], 112 | includedProperties: ['read'] 113 | }, 114 | 'gap.peripheral_privacy_flag': { 115 | primaryServices: ['generic_access'], 116 | includedProperties: ['read'] 117 | }, 118 | glucose_feature: { 119 | primaryServices: ['glucose'], 120 | includedProperties: ['read'], 121 | parseValue: function parseValue(value) { 122 | value = value.buffer ? value : new DataView(value); 123 | var result = {}; 124 | var flags = value.getUint16(0); 125 | result.low_battery_detection_supported = flags & 0x1; 126 | result.sensor_malfunction_detection_supported = flags & 0x2; 127 | result.sensor_sample_size_supported = flags & 0x4; 128 | result.sensor_strip_insertion_error_detection_supported = flags & 0x8; 129 | result.sensor_strip_type_error_detection_supported = flags & 0x10; 130 | result.sensor_result_highLow_detection_supported = flags & 0x20; 131 | result.sensor_temperature_highLow_detection_supported = flags & 0x40; 132 | result.sensor_read_interruption_detection_supported = flags & 0x80; 133 | result.general_device_fault_supported = flags & 0x100; 134 | result.time_fault_supported = flags & 0x200; 135 | result.multiple_bond_supported = flags & 0x400; 136 | return result; 137 | } 138 | }, 139 | http_entity_body: { 140 | primaryServices: ['http_proxy'], 141 | includedProperties: ['read', 'write'] 142 | }, 143 | glucose_measurement: { 144 | primaryServices: ['glucose'], 145 | includedProperties: ['notify'], 146 | parseValue: function parseValue(value) { 147 | value = value.buffer ? value : new DataView(value); 148 | var flags = value.getUint8(0); 149 | var timeOffset = flags & 0x1; 150 | var concentrationTypeSampleLoc = flags & 0x2; 151 | var concentrationUnits = flags & 0x4; 152 | var statusAnnunciation = flags & 0x8; 153 | var contextInformation = flags & 0x10; 154 | var result = {}; 155 | var index = 1; 156 | if (timeOffset) { 157 | result.time_offset = value.getInt16(index, /*little-endian=*/true); 158 | index += 2; 159 | } 160 | if (concentrationTypeSampleLoc) { 161 | if (concentrationUnits) { 162 | result.glucose_concentraiton_molPerL = value.getInt16(index, /*little-endian=*/true); 163 | index += 2; 164 | } else { 165 | result.glucose_concentraiton_kgPerL = value.getInt16(index, /*little-endian=*/true); 166 | index += 2; 167 | } 168 | } 169 | return result; 170 | } 171 | }, 172 | http_headers: { 173 | primaryServices: ['http_proxy'], 174 | includedProperties: ['read', 'write'] 175 | }, 176 | https_security: { 177 | primaryServices: ['http_proxy'], 178 | includedProperties: ['read', 'write'] 179 | }, 180 | intermediate_temperature: { 181 | primaryServices: ['health_thermometer'], 182 | includedProperties: ['read', 'write', 'indicate'] 183 | }, 184 | local_time_information: { 185 | primaryServices: ['current_time'], 186 | includedProperties: ['read', 'write'] 187 | }, 188 | manufacturer_name_string: { 189 | primaryServices: ['device_information'], 190 | includedProperties: ['read'] 191 | }, 192 | model_number_string: { 193 | primaryServices: ['device_information'], 194 | includedProperties: ['read'] 195 | }, 196 | pnp_id: { 197 | primaryServices: ['device_information'], 198 | includedProperties: ['read'] 199 | }, 200 | protocol_mode: { 201 | primaryServices: ['human_interface_device'], 202 | includedProperties: ['read', 'writeWithoutResponse'] 203 | }, 204 | reference_time_information: { 205 | primaryServices: ['current_time'], 206 | includedProperties: ['read'] 207 | }, 208 | supported_new_alert_category: { 209 | primaryServices: ['alert_notification'], 210 | includedProperties: ['read'] 211 | }, 212 | body_sensor_location: { 213 | primaryServices: ['heart_rate'], 214 | includedProperties: ['read'], 215 | parseValue: function parseValue(value) { 216 | value = value.buffer ? value : new DataView(value); 217 | var val = value.getUint8(0); 218 | var result = {}; 219 | switch (val) { 220 | case 0: 221 | result.location = 'Other'; 222 | case 1: 223 | result.location = 'Chest'; 224 | case 2: 225 | result.location = 'Wrist'; 226 | case 3: 227 | result.location = 'Finger'; 228 | case 4: 229 | result.location = 'Hand'; 230 | case 5: 231 | result.location = 'Ear Lobe'; 232 | case 6: 233 | result.location = 'Foot'; 234 | default: 235 | result.location = 'Unknown'; 236 | } 237 | return result; 238 | } 239 | }, 240 | // heart_rate_control_point 241 | heart_rate_control_point: { 242 | primaryServices: ['heart_rate'], 243 | includedProperties: ['write'], 244 | prepValue: function prepValue(value) { 245 | var buffer = new ArrayBuffer(1); 246 | var writeView = new DataView(buffer); 247 | writeView.setUint8(0, value); 248 | return writeView; 249 | } 250 | }, 251 | heart_rate_measurement: { 252 | primaryServices: ['heart_rate'], 253 | includedProperties: ['notify'], 254 | /** 255 | * Parses the event.target.value object and returns object with readable 256 | * key-value pairs for all advertised characteristic values 257 | * 258 | * @param {Object} value Takes event.target.value object from startNotifications method 259 | * 260 | * @return {Object} result Returns readable object with relevant characteristic values 261 | * 262 | */ 263 | parseValue: function parseValue(value) { 264 | value = value.buffer ? value : new DataView(value); 265 | var flags = value.getUint8(0); 266 | var rate16Bits = flags & 0x1; 267 | var contactDetected = flags & 0x2; 268 | var contactSensorPresent = flags & 0x4; 269 | var energyPresent = flags & 0x8; 270 | var rrIntervalPresent = flags & 0x10; 271 | var result = {}; 272 | var index = 1; 273 | if (rate16Bits) { 274 | result.heartRate = value.getUint16(index, /*little-endian=*/true); 275 | index += 2; 276 | } else { 277 | result.heartRate = value.getUint8(index); 278 | index += 1; 279 | } 280 | if (contactSensorPresent) { 281 | result.contactDetected = !!contactDetected; 282 | } 283 | if (energyPresent) { 284 | result.energyExpended = value.getUint16(index, /*little-endian=*/true); 285 | index += 2; 286 | } 287 | if (rrIntervalPresent) { 288 | var rrIntervals = []; 289 | for (; index + 1 < value.byteLength; index += 2) { 290 | rrIntervals.push(value.getUint16(index, /*little-endian=*/true)); 291 | } 292 | result.rrIntervals = rrIntervals; 293 | } 294 | return result; 295 | } 296 | }, 297 | serial_number_string: { 298 | primaryServices: ['device_information'], 299 | includedProperties: ['read'] 300 | }, 301 | software_revision_string: { 302 | primaryServices: ['device_information'], 303 | includedProperties: ['read'] 304 | }, 305 | supported_unread_alert_category: { 306 | primaryServices: ['alert_notification'], 307 | includedProperties: ['read'] 308 | }, 309 | system_id: { 310 | primaryServices: ['device_information'], 311 | includedProperties: ['read'] 312 | }, 313 | temperature_type: { 314 | primaryServices: ['health_thermometer'], 315 | includedProperties: ['read'] 316 | }, 317 | descriptor_value_changed: { 318 | primaryServices: ['environmental_sensing'], 319 | includedProperties: ['indicate', 'writeAux', 'extProp'] 320 | }, 321 | apparent_wind_direction: { 322 | primaryServices: ['environmental_sensing'], 323 | includedProperties: ['read', 'notify', 'writeAux', 'extProp'], 324 | parseValue: function parseValue(value) { 325 | value = value.buffer ? value : new DataView(value); 326 | var result = {}; 327 | result.apparent_wind_direction = value.getUint16(0) * 0.01; 328 | return result; 329 | } 330 | }, 331 | apparent_wind_speed: { 332 | primaryServices: ['environmental_sensing'], 333 | includedProperties: ['read', 'notify', 'writeAux', 'extProp'], 334 | parseValue: function parseValue(value) { 335 | value = value.buffer ? value : new DataView(value); 336 | var result = {}; 337 | result.apparent_wind_speed = value.getUint16(0) * 0.01; 338 | return result; 339 | } 340 | }, 341 | dew_point: { 342 | primaryServices: ['environmental_sensing'], 343 | includedProperties: ['read', 'notify', 'writeAux', 'extProp'], 344 | parseValue: function parseValue(value) { 345 | value = value.buffer ? value : new DataView(value); 346 | var result = {}; 347 | result.dew_point = value.getInt8(0); 348 | return result; 349 | } 350 | }, 351 | elevation: { 352 | primaryServices: ['environmental_sensing'], 353 | includedProperties: ['read', 'notify', 'writeAux', 'extProp'], 354 | parseValue: function parseValue(value) { 355 | value = value.buffer ? value : new DataView(value); 356 | var result = {}; 357 | result.elevation = value.getInt8(0) << 16 | value.getInt8(1) << 8 | value.getInt8(2); 358 | return result; 359 | } 360 | }, 361 | gust_factor: { 362 | primaryServices: ['environmental_sensing'], 363 | includedProperties: ['read', 'notify', 'writeAux', 'extProp'], 364 | parseValue: function parseValue(value) { 365 | value = value.buffer ? value : new DataView(value); 366 | var result = {}; 367 | result.gust_factor = value.getUint8(0) * 0.1; 368 | return result; 369 | } 370 | }, 371 | heat_index: { 372 | primaryServices: ['environmental_sensing'], 373 | includedProperties: ['read', 'notify', 'writeAux', 'extProp'], 374 | parseValue: function parseValue(value) { 375 | value = value.buffer ? value : new DataView(value); 376 | var result = {}; 377 | result.heat_index = value.getInt8(0); 378 | return result; 379 | } 380 | }, 381 | humidity: { 382 | primaryServices: ['environmental_sensing'], 383 | includedProperties: ['read', 'notify', 'writeAux', 'extProp'], 384 | parseValue: function parseValue(value) { 385 | value = value.buffer ? value : new DataView(value); 386 | var result = {}; 387 | 388 | result.humidity = value.getUint16(0) * 0.01; 389 | return result; 390 | } 391 | }, 392 | irradiance: { 393 | primaryServices: ['environmental_sensing'], 394 | includedProperties: ['read', 'notify', 'writeAux', 'extProp'], 395 | parseValue: function parseValue(value) { 396 | value = value.buffer ? value : new DataView(value); 397 | var result = {}; 398 | 399 | result.irradiance = value.getUint16(0) * 0.1; 400 | return result; 401 | } 402 | }, 403 | rainfall: { 404 | primaryServices: ['environmental_sensing'], 405 | includedProperties: ['read', 'notify', 'writeAux', 'extProp'], 406 | parseValue: function parseValue(value) { 407 | value = value.buffer ? value : new DataView(value); 408 | var result = {}; 409 | 410 | result.rainfall = value.getUint16(0) * 0.001; 411 | return result; 412 | } 413 | }, 414 | pressure: { 415 | primaryServices: ['environmental_sensing'], 416 | includedProperties: ['read', 'notify', 'writeAux', 'extProp'], 417 | parseValue: function parseValue(value) { 418 | value = value.buffer ? value : new DataView(value); 419 | var result = {}; 420 | result.pressure = value.getUint32(0) * 0.1; 421 | return result; 422 | } 423 | }, 424 | temperature: { 425 | primaryServices: ['environmental_sensing'], 426 | includedProperties: ['read', 'notify', 'writeAux', 'extProp'], 427 | parseValue: function parseValue(value) { 428 | value = value.buffer ? value : new DataView(value); 429 | var result = {}; 430 | result.temperature = value.getInt16(0) * 0.01; 431 | return result; 432 | } 433 | }, 434 | true_wind_direction: { 435 | primaryServices: ['environmental_sensing'], 436 | includedProperties: ['read', 'notify', 'writeAux', 'extProp'], 437 | parseValue: function parseValue(value) { 438 | value = value.buffer ? value : new DataView(value); 439 | var result = {}; 440 | result.true_wind_direction = value.getUint16(0) * 0.01; 441 | return result; 442 | } 443 | }, 444 | true_wind_speed: { 445 | primaryServices: ['environmental_sensing'], 446 | includedProperties: ['read', 'notify', 'writeAux', 'extProp'], 447 | parseValue: function parseValue(value) { 448 | value = value.buffer ? value : new DataView(value); 449 | var result = {}; 450 | result.true_wind_speed = value.getUint16(0) * 0.01; 451 | return result; 452 | } 453 | }, 454 | uv_index: { 455 | primaryServices: ['environmental_sensing'], 456 | includedProperties: ['read', 'notify', 'writeAux', 'extProp'], 457 | parseValue: function parseValue(value) { 458 | value = value.buffer ? value : new DataView(value); 459 | var result = {}; 460 | result.uv_index = value.getUint8(0); 461 | return result; 462 | } 463 | }, 464 | wind_chill: { 465 | primaryServices: ['environmental_sensing'], 466 | includedProperties: ['read', 'notify', 'writeAux', 'extProp'], 467 | parseValue: function parseValue(value) { 468 | value = value.buffer ? value : new DataView(value); 469 | var result = {}; 470 | result.wind_chill = value.getInt8(0); 471 | return result; 472 | } 473 | }, 474 | barometric_pressure_trend: { 475 | primaryServices: ['environmental_sensing'], 476 | includedProperties: ['read', 'notify', 'writeAux', 'extProp'], 477 | parseValue: function parseValue(value) { 478 | value = value.buffer ? value : new DataView(value); 479 | var val = value.getUint8(0); 480 | var result = {}; 481 | switch (val) { 482 | case 0: 483 | result.barometric_pressure_trend = 'Unknown'; 484 | case 1: 485 | result.barometric_pressure_trend = 'Continuously falling'; 486 | case 2: 487 | result.barometric_pressure_trend = 'Continously rising'; 488 | case 3: 489 | result.barometric_pressure_trend = 'Falling, then steady'; 490 | case 4: 491 | result.barometric_pressure_trend = 'Rising, then steady'; 492 | case 5: 493 | result.barometric_pressure_trend = 'Falling before a lesser rise'; 494 | case 6: 495 | result.barometric_pressure_trend = 'Falling before a greater rise'; 496 | case 7: 497 | result.barometric_pressure_trend = 'Rising before a greater fall'; 498 | case 8: 499 | result.barometric_pressure_trend = 'Rising before a lesser fall'; 500 | case 9: 501 | result.barometric_pressure_trend = 'Steady'; 502 | default: 503 | result.barometric_pressure_trend = 'Could not resolve to trend'; 504 | } 505 | return result; 506 | } 507 | }, 508 | magnetic_declination: { 509 | primaryServices: ['environmental_sensing'], 510 | includedProperties: ['read', 'notify', 'writeAux', 'extProp'], 511 | parseValue: function parseValue(value) { 512 | value = value.buffer ? value : new DataView(value); 513 | var result = {}; 514 | 515 | result.magnetic_declination = value.getUint16(0) * 0.01; 516 | return result; 517 | } 518 | }, 519 | magnetic_flux_density_2D: { 520 | primaryServices: ['environmental_sensing'], 521 | includedProperties: ['read', 'notify', 'writeAux', 'extProp'], 522 | parseValue: function parseValue(value) { 523 | value = value.buffer ? value : new DataView(value); 524 | var result = {}; 525 | //FIXME: need to find out if these values are stored at different byte addresses 526 | // below assumes that values are stored at successive byte addresses 527 | result.magnetic_flux_density_x_axis = value.getInt16(0, /*little-endian=*/true) * 0.0000001; 528 | result.magnetic_flux_density_y_axis = value.getInt16(2, /*little-endian=*/true) * 0.0000001; 529 | return result; 530 | } 531 | }, 532 | magnetic_flux_density_3D: { 533 | primaryServices: ['environmental_sensing'], 534 | includedProperties: ['read', 'notify', 'writeAux', 'extProp'], 535 | parseValue: function parseValue(value) { 536 | value = value.buffer ? value : new DataView(value); 537 | var result = {}; 538 | //FIXME: need to find out if these values are stored at different byte addresses 539 | // below assumes that values are stored at successive byte addresses 540 | result.magnetic_flux_density_x_axis = value.getInt16(0, /*little-endian=*/true) * 0.0000001; 541 | result.magnetic_flux_density_y_axis = value.getInt16(2, /*little-endian=*/true) * 0.0000001; 542 | result.magnetic_flux_density_z_axis = value.getInt16(4, /*little-endian=*/true) * 0.0000001; 543 | return result; 544 | } 545 | }, 546 | tx_power_level: { 547 | primaryServices: ['tx_power'], 548 | includedProperties: ['read'], 549 | parseValue: function parseValue(value) { 550 | value = value.buffer ? value : new DataView(value); 551 | var result = {}; 552 | result.tx_power_level = value.getInt8(0); 553 | return result; 554 | } 555 | }, 556 | weight_scale_feature: { 557 | primaryServices: ['weight_scale'], 558 | includedProperties: ['read'], 559 | parseValue: function parseValue(value) { 560 | value = value.buffer ? value : new DataView(value); 561 | var result = {}; 562 | var flags = value.getInt32(0); 563 | result.time_stamp_supported = flags & 0x1; 564 | result.multiple_sensors_supported = flags & 0x2; 565 | result.BMI_supported = flags & 0x4; 566 | switch (flags & 0x78 >> 3) { 567 | case 0: 568 | result.weight_measurement_resolution = 'Not specified'; 569 | case 1: 570 | result.weight_measurement_resolution = 'Resolution of 0.5 kg or 1 lb'; 571 | case 2: 572 | result.weight_measurement_resolution = 'Resolution of 0.2 kg or 0.5 lb'; 573 | case 3: 574 | result.weight_measurement_resolution = 'Resolution of 0.1 kg or 0.2 lb'; 575 | case 4: 576 | result.weight_measurement_resolution = 'Resolution of 0.05 kg or 0.1 lb'; 577 | case 5: 578 | result.weight_measurement_resolution = 'Resolution of 0.02 kg or 0.05 lb'; 579 | case 6: 580 | result.weight_measurement_resolution = 'Resolution of 0.01 kg or 0.02 lb'; 581 | case 7: 582 | result.weight_measurement_resolution = 'Resolution of 0.005 kg or 0.01 lb'; 583 | default: 584 | result.weight_measurement_resolution = 'Could not resolve'; 585 | } 586 | switch (flags & 0x380 >> 7) { 587 | case 0: 588 | result.height_measurement_resolution = 'Not specified'; 589 | case 1: 590 | result.height_measurement_resolution = 'Resolution of 0.1 meter or 1 inch'; 591 | case 2: 592 | result.height_measurement_resolution = 'Resolution of 0.005 meter or 0.5 inch'; 593 | case 3: 594 | result.height_measurement_resolution = 'Resolution of 0.001 meter or 0.1 inch'; 595 | default: 596 | result.height_measurement_resolution = 'Could not resolve'; 597 | } 598 | // Remaining flags reserved for future use 599 | return result; 600 | } 601 | }, 602 | csc_measurement: { 603 | primaryServices: ['cycling_speed_and_cadence'], 604 | includedProperties: ['notify'], 605 | parseValue: function parseValue(value) { 606 | value = value.buffer ? value : new DataView(value); 607 | var flags = value.getUint8(0); 608 | var wheelRevolution = flags & 0x1; //integer = truthy, 0 = falsy 609 | var crankRevolution = flags & 0x2; 610 | var result = {}; 611 | var index = 1; 612 | if (wheelRevolution) { 613 | result.cumulative_wheel_revolutions = value.getUint32(index, /*little-endian=*/true); 614 | index += 4; 615 | result.last_wheel_event_time_per_1024s = value.getUint16(index, /*little-endian=*/true); 616 | index += 2; 617 | } 618 | if (crankRevolution) { 619 | result.cumulative_crank_revolutions = value.getUint16(index, /*little-endian=*/true); 620 | index += 2; 621 | result.last_crank_event_time_per_1024s = value.getUint16(index, /*little-endian=*/true); 622 | index += 2; 623 | } 624 | return result; 625 | } 626 | }, 627 | sensor_location: { 628 | primaryServices: ['cycling_speed_and_cadence'], 629 | includedProperties: ['read'], 630 | parseValue: function parseValue(value) { 631 | value = value.buffer ? value : new DataView(value); 632 | var val = value.getUint16(0); 633 | var result = {}; 634 | switch (val) { 635 | case 0: 636 | result.location = 'Other'; 637 | case 1: 638 | result.location = 'Top of show'; 639 | case 2: 640 | result.location = 'In shoe'; 641 | case 3: 642 | result.location = 'Hip'; 643 | case 4: 644 | result.location = 'Front Wheel'; 645 | case 5: 646 | result.location = 'Left Crank'; 647 | case 6: 648 | result.location = 'Right Crank'; 649 | case 7: 650 | result.location = 'Left Pedal'; 651 | case 8: 652 | result.location = 'Right Pedal'; 653 | case 9: 654 | result.location = 'Front Hub'; 655 | case 10: 656 | result.location = 'Rear Dropout'; 657 | case 11: 658 | result.location = 'Chainstay'; 659 | case 12: 660 | result.location = 'Rear Wheel'; 661 | case 13: 662 | result.location = 'Rear Hub'; 663 | case 14: 664 | result.location = 'Chest'; 665 | case 15: 666 | result.location = 'Spider'; 667 | case 16: 668 | result.location = 'Chain Ring'; 669 | default: 670 | result.location = 'Unknown'; 671 | } 672 | return result; 673 | } 674 | }, 675 | sc_control_point: { 676 | primaryServices: ['cycling_speed_and_cadence'], 677 | includedProperties: ['write', 'indicate'], 678 | parseValue: function parseValue(value) { 679 | value = value.buffer ? value : new DataView(value); 680 | return result; 681 | } 682 | }, 683 | cycling_power_measurement: { 684 | primaryServices: ['cycling_power'], 685 | includedProperties: ['notify'], 686 | parseValue: function parseValue(value) { 687 | value = value.buffer ? value : new DataView(value); 688 | var flags = value.getUint16(0); 689 | var pedal_power_balance_present = flags & 0x1; 690 | var pedal_power_reference = flags & 0x2; 691 | var accumulated_torque_present = flags & 0x4; 692 | var accumulated_torque_source = flags & 0x8; 693 | var wheel_revolution_data_present = flags & 0x10; 694 | var crank_revolution_data_present = flags & 0x12; 695 | var extreme_force_magnitude_present = flags & 0x12; 696 | var extreme_torque_magnitude_present = flags & 0x12; 697 | var extreme_angles_present = flags & 0x12; 698 | var top_dead_spot_angle_present = flags & 0x12; 699 | var bottom_dead_spot_angle_present = flags & 0x12; 700 | var accumulated_energy_present = flags & 0x12; 701 | var offset_compensation_indicator = flags & 0x12; 702 | var result = {}; 703 | var index = 1; 704 | //Watts with resolution of 1 705 | result.instantaneous_power = value.getInt16(index); 706 | index += 2; 707 | if (pedal_power_reference) { 708 | //Percentage with resolution of 1/2 709 | result.pedal_power_balance = value.getUint8(index); 710 | index += 1; 711 | } 712 | if (accumulated_torque_present) { 713 | //Percentage with resolution of 1/2 714 | result.accumulated_torque = value.getUint16(index); 715 | index += 2; 716 | } 717 | if (wheel_revolution_data_present) { 718 | result.cumulative_wheel_revolutions = value.Uint32(index); 719 | index += 4; 720 | result.last_wheel_event_time_per_2048s = value.Uint16(index); 721 | index += 2; 722 | } 723 | if (crank_revolution_data_present) { 724 | result.cumulative_crank_revolutions = value.getUint16(index, /*little-endian=*/true); 725 | index += 2; 726 | result.last_crank_event_time_per_1024s = value.getUint16(index, /*little-endian=*/true); 727 | index += 2; 728 | } 729 | if (extreme_force_magnitude_present) { 730 | //Newton meters with resolution of 1 TODO: units? 731 | result.maximum_force_magnitude = value.getInt16(index); 732 | index += 2; 733 | result.minimum_force_magnitude = value.getInt16(index); 734 | index += 2; 735 | } 736 | if (extreme_torque_magnitude_present) { 737 | //Newton meters with resolution of 1 TODO: units? 738 | result.maximum_torque_magnitude = value.getInt16(index); 739 | index += 2; 740 | result.minimum_torque_magnitude = value.getInt16(index); 741 | index += 2; 742 | } 743 | if (extreme_angles_present) { 744 | //TODO: UINT12. 745 | //Newton meters with resolution of 1 TODO: units? 746 | // result.maximum_angle = value.getInt12(index); 747 | // index += 2; 748 | // result.minimum_angle = value.getInt12(index); 749 | // index += 2; 750 | } 751 | if (top_dead_spot_angle_present) { 752 | //Percentage with resolution of 1/2 753 | result.top_dead_spot_angle = value.getUint16(index); 754 | index += 2; 755 | } 756 | if (bottom_dead_spot_angle_present) { 757 | //Percentage with resolution of 1/2 758 | result.bottom_dead_spot_angle = value.getUint16(index); 759 | index += 2; 760 | } 761 | if (accumulated_energy_present) { 762 | //kilojoules with resolution of 1 TODO: units? 763 | result.accumulated_energy = value.getUint16(index); 764 | index += 2; 765 | } 766 | return result; 767 | } 768 | } 769 | }, 770 | 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'] 771 | }; 772 | 773 | module.exports = bluetoothMap; -------------------------------------------------------------------------------- /dist/npm/errorHandler.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** errorHandler - Consolodates error message configuration and logic 4 | * 5 | * @param {string} errorKey - maps to a detailed error message 6 | * @param {object} nativeError - the native API error object, if present 7 | * @param {} alternate - 8 | * 9 | */ 10 | function errorHandler(errorKey, nativeError, alternate) { 11 | 12 | var errorMessages = { 13 | add_characteristic_exists_error: "Characteristic " + alternate + " already exists.", 14 | characteristic_error: "Characteristic " + alternate + " not found. Add " + alternate + " to device using addCharacteristic or try another characteristic.", 15 | connect_gatt: "Could not connect to GATT. Device might be out of range. Also check to see if filters are vaild.", 16 | connect_server: "Could not connect to server on device.", 17 | connect_service: "Could not find service.", 18 | disconnect_timeout: "Timed out. Could not disconnect.", 19 | disconnect_error: "Could not disconnect from device.", 20 | improper_characteristic_format: alternate + " is not a properly formatted characteristic.", 21 | improper_properties_format: alternate + " is not a properly formatted properties array.", 22 | improper_service_format: alternate + " is not a properly formatted service.", 23 | issue_disconnecting: "Issue disconnecting with device.", 24 | new_characteristic_missing_params: alternate + " is not a fully supported characteristic. Please provide an associated primary service and at least one property.", 25 | no_device: "No instance of device found.", 26 | no_filters: "No filters found on instance of Device. For more information, please visit http://sabertooth.io/#method-newdevice", 27 | no_read_property: "No read property on characteristic: " + alternate + ".", 28 | no_write_property: "No write property on this characteristic.", 29 | not_connected: "Could not disconnect. Device not connected.", 30 | parsing_not_supported: "Parsing not supported for characterstic: " + alternate + ".", 31 | read_error: "Cannot read value on the characteristic.", 32 | _returnCharacteristic_error: "Error accessing characteristic " + alternate + ".", 33 | start_notifications_error: "Not able to read stream of data from characteristic: " + alternate + ".", 34 | start_notifications_no_notify: "No notify property found on this characteristic: " + alternate + ".", 35 | stop_notifications_not_notifying: "Notifications not established for characteristic: " + alternate + " or you have not started notifications.", 36 | stop_notifications_error: "Issue stopping notifications for characteristic: " + alternate + " or you have not started notifications.", 37 | user_cancelled: "User cancelled the permission request.", 38 | uuid_error: "Invalid UUID. For more information on proper formatting of UUIDs, visit https://webbluetoothcg.github.io/web-bluetooth/#uuids", 39 | write_error: "Could not change value of characteristic: " + alternate + ".", 40 | write_permissions: alternate + " characteristic does not have a write property." 41 | }; 42 | 43 | throw new Error(errorMessages[errorKey]); 44 | return false; 45 | } 46 | 47 | module.exports = errorHandler; -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const browserify = require('browserify'); 3 | const source = require('vinyl-source-stream'); 4 | const notify = require('gulp-notify'); 5 | 6 | const path = { 7 | SRC: 'lib/BluetoothDevice.js', 8 | NPM_Dest: 'dist/npm', 9 | BOWER_Dest: 'dist/build.js' 10 | } 11 | 12 | const configs = { 13 | npm: { 14 | entries: [`./lib/${file}`], 15 | debug: true, 16 | transform: [ 17 | ['babelify', { presets: ['es2015'] }] 18 | ] 19 | }, 20 | bower: { 21 | entries: 22 | } 23 | } 24 | 25 | function handleErrors() { 26 | notify.onError({ 27 | title : 'Compile Error', 28 | message : '<%= error.message %>' 29 | }).apply(this, arguments); 30 | this.emit('end'); //keeps gulp from hanging on this task 31 | } 32 | 33 | function build(props, src, out) { 34 | 35 | return browserify(props) 36 | .bundle() 37 | .on('error', handleErrors) 38 | .pipe(soucre(src)) 39 | .pipe(source(out)); 40 | } 41 | 42 | gulp.task('default', ['npm', 'bower']); 43 | gulp.task('npm', build.call(this, path.SRC, path.NPM_Dest)); 44 | gulp.task('bower', build.call(this, path.SRC, path.BOWER_Dest)); 45 | -------------------------------------------------------------------------------- /lib/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sabertooth-io/web-bluetooth/ab1ec47fdd13dc992a9093d12443a258b47718db/lib/.DS_Store -------------------------------------------------------------------------------- /lib/BluetoothDevice.js: -------------------------------------------------------------------------------- 1 | const bluetooth = require('./bluetoothMap'); 2 | const errorHandler = require('./errorHandler'); 3 | 4 | /** BluetoothDevice - 5 | * 6 | * @method connect - Establishes a connection with the device 7 | * @method connected - checks apiDevice to see whether device is connected 8 | * @method disconnect - terminates the connection with the device and pauses all data stream subscriptions 9 | * @method getValue - reads the value of a specified characteristic 10 | * @method writeValue - writes data to a specified characteristic of the device 11 | * @method startNotifications - attempts to start notifications for changes to device values and attaches an event listener for each data transmission 12 | * @method stopNotifications - attempts to stop previously started notifications for a provided characteristic 13 | * @method addCharacteristic - adds a new characteristic object to bluetooth.gattCharacteristicsMapping 14 | * @method _returnCharacteristic - _returnCharacteristic - returns the value of a cached or resolved characteristic or resolved characteristic 15 | * 16 | * @param {object} filters - collection of filters for device selectin. All filters are optional, but at least 1 is required. 17 | * .name {string} 18 | * .namePrefix {string} 19 | * .uuid {string} 20 | * .services {array} 21 | * .optionalServices {array} - defaults to all available services, use an empty array to get no optional services 22 | * 23 | * @return {object} Returns a new instance of BluetoothDevice 24 | * 25 | */ 26 | class BluetoothDevice { 27 | 28 | constructor(requestParams) { 29 | this.requestParams = requestParams; 30 | this.apiDevice = null; 31 | this.apiServer = null; 32 | this.cache = {}; 33 | } 34 | 35 | connected() { 36 | return this.apiDevice ? this.apiDevice.gatt.connected : errorHandler('no_device'); 37 | } 38 | 39 | /** connect - establishes a connection with the device 40 | * 41 | * NOTE: This method must be triggered by a user gesture to satisfy the native API's permissions 42 | * 43 | * @return {object} - native browser API device server object 44 | */ 45 | connect() { 46 | const filters = this.requestParams; 47 | const requestParams = { filters: [], }; 48 | const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]/; 49 | 50 | if(!Object.keys(filters).length) { return errorHandler('no_filters'); } 51 | if (filters.name) requestParams.filters.push({ name: filters.name }); 52 | if (filters.namePrefix) requestParams.filters.push({ namePrefix: filters.namePrefix }); 53 | if (filters.uuid) { 54 | if (!filters.uuid.match(uuidRegex)) { 55 | errorHandler('uuid_error'); 56 | } else { 57 | requestParams.filters.push({ uuid: filters.uuid }); 58 | } 59 | } 60 | if (filters.services) { 61 | let services = []; 62 | filters.services.forEach(service => { 63 | if (!bluetooth.gattServiceList.includes(service)) { 64 | console.warn(`${service} is not a valid service. Please check the service name.`); 65 | } else { services.push(service); } 66 | }); 67 | requestParams.filters.push({ services: services }); 68 | } 69 | if (filters.optional_services) { 70 | filters.optional_services.forEach(service => { 71 | if (!bluetooth.gattServiceList.includes(service)) bluetooth.gattServiceList.push(service); 72 | }); 73 | } else { requestParams.optionalServices = bluetooth.gattServiceList; } 74 | 75 | return navigator.bluetooth.requestDevice(requestParams) 76 | .then(device => { 77 | this.apiDevice = device; 78 | return device.gatt.connect(); 79 | }).then(server => { 80 | this.apiServer = server; 81 | return server; 82 | }).catch(err => { 83 | return errorHandler('user_cancelled', err); 84 | }); 85 | } 86 | 87 | /** disconnect - terminates the connection with the device and pauses all data stream subscriptions 88 | * @return {boolean} - success 89 | * 90 | */ 91 | disconnect() { 92 | this.apiServer.connected ? this.apiServer.disconnect() : errorHandler('not_connected'); 93 | return this.apiServer.connected ? errorHandler('issue_disconnecting') : true; 94 | } 95 | 96 | /** getValue - reads the value of a specified characteristic 97 | * 98 | * @param {string} characteristic_name - GATT characteristic name 99 | * @return {promise} - resolves with an object that includes key-value pairs for each of the properties 100 | * successfully read and parsed from the device, as well as the 101 | * raw value object returned by a native readValue request to the 102 | * device characteristic. 103 | */ 104 | getValue(characteristic_name) { 105 | if (!bluetooth.gattCharacteristicsMapping[characteristic_name]) { 106 | return errorHandler('characteristic_error', null, characteristic_name); 107 | } 108 | 109 | const characteristicObj = bluetooth.gattCharacteristicsMapping[characteristic_name]; 110 | 111 | if (!characteristicObj.includedProperties.includes('read')) { 112 | console.warn(`Attempting to access read property of ${characteristic_name}, 113 | which is not a included as a supported property of the 114 | characteristic. Attempt will resolve with an object including 115 | only a rawValue property with the native API return 116 | for an attempt to readValue() of ${characteristic_name}.`); 117 | } 118 | 119 | return new Promise((resolve, reject) => { return resolve(this._returnCharacteristic(characteristic_name)); }) 120 | .then(characteristic => { return characteristic.readValue(); }) 121 | .then(value => { 122 | const returnObj = characteristicObj.parseValue ? characteristicObj.parseValue(value) : {}; 123 | returnObj.rawValue = value; 124 | return returnObj; 125 | }).catch(err => { return errorHandler('read_error', err); }); 126 | } 127 | 128 | /** writeValue - writes data to a specified characteristic of the device 129 | * 130 | * @param {string} characteristic_name - name of the GATT characteristic 131 | * https://www.bluetooth.com/specifications/assigned-numbers/generic-attribute-profile 132 | * 133 | * @param {string|number} value - value to write to the requested device characteristic 134 | * 135 | * 136 | * @return {boolean} - Result of attempt to write characteristic where true === successfully written 137 | */ 138 | writeValue(characteristic_name, value) { 139 | if (!bluetooth.gattCharacteristicsMapping[characteristic_name]) { 140 | return errorHandler('characteristic_error', null, characteristic_name); 141 | } 142 | 143 | const characteristicObj = bluetooth.gattCharacteristicsMapping[characteristic_name]; 144 | 145 | if (!characteristicObj.includedProperties.includes('write')) { 146 | console.warn(`Attempting to access write property of ${characteristic_name}, 147 | which is not a included as a supported property of the 148 | characteristic. Attempt will resolve with native API return 149 | for an attempt to writeValue(${value}) to ${characteristic_name}.`); 150 | } 151 | 152 | return new Promise((resolve, reject) => { return resolve(this._returnCharacteristic(characteristic_name)); }) 153 | .then(characteristic => { return characteristic.writeValue(characteristicObj.prepValue ? characteristicObj.prepValue(value) : value); }) 154 | .then(changedChar => { return true; }) 155 | .catch(err => { return errorHandler('write_error', err, characteristic_name); }); 156 | } 157 | 158 | /** startNotifications - attempts to start notifications for changes to device values and attaches an event listener for each data transmission 159 | * 160 | * @param {string} characteristic_name - GATT characteristic name 161 | * @param {callback} transmissionCallback - callback function to apply to each event while notifications are active 162 | * 163 | * @return 164 | * 165 | */ 166 | startNotifications(characteristic_name, transmissionCallback) { 167 | if (!bluetooth.gattCharacteristicsMapping[characteristic_name]) { return errorHandler('characteristic_error', null, characteristic_name); } 168 | 169 | const characteristicObj = bluetooth.gattCharacteristicsMapping[characteristic_name]; 170 | const primary_service_name = characteristicObj.primaryServices[0]; 171 | 172 | if (!characteristicObj.includedProperties.includes('notify')) { 173 | console.warn(`Attempting to access notify property of ${characteristic_name}, 174 | which is not a included as a supported property of the 175 | characteristic. Attempt will resolve with an object including 176 | only a rawValue property with the native API return 177 | for an attempt to startNotifications() for ${characteristic_name}.`); 178 | } 179 | 180 | return new Promise((resolve, reject) => { return resolve(this._returnCharacteristic(characteristic_name)); }) 181 | .then(characteristic => { 182 | characteristic.startNotifications().then(() => { 183 | this.cache[primary_service_name][characteristic_name].notifying = true; 184 | return characteristic.addEventListener('characteristicvaluechanged', event => { 185 | const eventObj = characteristicObj.parseValue ? characteristicObj.parseValue(event.target.value) : {}; 186 | eventObj.rawValue = event; 187 | return transmissionCallback(eventObj); 188 | }); 189 | }); 190 | }).catch(err => { return errorHandler('start_notifications_error', err, characteristic_name); }); 191 | } 192 | 193 | /** stopNotifications - attempts to stop previously started notifications for a provided characteristic 194 | * 195 | * @param {string} characteristic_name - GATT characteristic name 196 | * 197 | * @return {boolean} success 198 | * 199 | */ 200 | stopNotifications(characteristic_name) { 201 | if (!bluetooth.gattCharacteristicsMapping[characteristic_name]) { 202 | return errorHandler('characteristic_error', null, characteristic_name); 203 | } 204 | 205 | const characteristicObj = bluetooth.gattCharacteristicsMapping[characteristic_name]; 206 | const primary_service_name = characteristicObj.primaryServices[0]; 207 | 208 | if (this.cache[primary_service_name][characteristic_name].notifying) { 209 | return new Promise((resolve, reject) => { return resolve(this._returnCharacteristic(characteristic_name)); }) 210 | .then(characteristic => { 211 | characteristic.stopNotifications().then(() => { 212 | this.cache[primary_service_name][characteristic_name].notifying = false; 213 | return true; 214 | }); 215 | }).catch(err => { 216 | return errorHandler('stop_notifications_error', err, characteristic_name); 217 | }); 218 | } else { 219 | return errorHandler('stop_notifications_not_notifying', null, characteristic_name); 220 | } 221 | } 222 | 223 | /** 224 | * addCharacteristic - adds a new characteristic object to bluetooth.gattCharacteristicsMapping 225 | * 226 | * @param {string} characteristic_name - GATT characteristic name or other characteristic 227 | * @param {string} primary_service_name - GATT primary service name or other parent service of characteristic 228 | * @param {array} propertiesArr - Array of GATT properties as Strings 229 | * 230 | * @return {boolean} - Result of attempt to add characteristic where true === successfully added 231 | */ 232 | addCharacteristic(characteristic_name, primary_service_name, propertiesArr) { 233 | if (bluetooth.gattCharacteristicsMapping[characteristic_name]) { 234 | return errorHandler('add_characteristic_exists_error', null, characteristic_name); 235 | } 236 | 237 | if (!characteristic_name || characteristic_name.constructor !== String || !characteristic_name.length) { 238 | return errorHandler('improper_characteristic_format', null, characteristic_name); 239 | } 240 | 241 | if (!bluetooth.gattCharacteristicsMapping[characteristic_name]) { 242 | if (!primary_service_name || !propertiesArr) { 243 | return errorHandler('new_characteristic_missing_params', null, characteristic_name); 244 | } 245 | if (primary_service_name.constructor !== String || !primary_service_name.length) { 246 | return errorHandler('improper_service_format', null, primary_service_name); 247 | } 248 | if (propertiesArr.constructor !== Array || !propertiesArr.length) { 249 | return errorHandler('improper_properties_format', null, propertiesArr); 250 | } 251 | 252 | console.warn(`${characteristic_name} is not yet fully supported.`); 253 | 254 | bluetooth.gattCharacteristicsMapping[characteristic_name] = { 255 | primaryServices: [primary_service_name], 256 | includedProperties: propertiesArr, 257 | }; 258 | 259 | return true; 260 | } 261 | } 262 | 263 | /** 264 | * _returnCharacteristic - returns the value of a cached or resolved characteristic or resolved characteristic 265 | * 266 | * @param {string} characteristic_name - GATT characteristic name 267 | * @return {object|false} - the characteristic object, if successfully obtained 268 | */ 269 | _returnCharacteristic(characteristic_name) { 270 | if (!bluetooth.gattCharacteristicsMapping[characteristic_name]) { 271 | return errorHandler('characteristic_error', null, characteristic_name); 272 | } 273 | 274 | const characteristicObj = bluetooth.gattCharacteristicsMapping[characteristic_name]; 275 | const primary_service_name = characteristicObj.primaryServices[0]; 276 | 277 | if (this.cache[primary_service_name] && this.cache[primary_service_name][characteristic_name] && this.cache[primary_service_name][characteristic_name].cachedCharacteristic) { 278 | return this.cache[primary_service_name][characteristic_name].cachedCharacteristic; 279 | } else if (this.cache[primary_service_name] && this.cache[primary_service_name].cachedService) { 280 | this.cache[primary_service_name].cachedService.getCharacteristic(characteristic_name) 281 | .then(characteristic => { 282 | this.cache[primary_service_name][characteristic_name] = { cachedCharacteristic: characteristic }; 283 | return characteristic; 284 | }).catch(err => { return errorHandler('_returnCharacteristic_error', err, characteristic_name); }); 285 | } else { 286 | return this.apiServer.getPrimaryService(primary_service_name) 287 | .then(service => { 288 | this.cache[primary_service_name] = { 'cachedService': service }; 289 | return service.getCharacteristic(characteristic_name); 290 | }).then(characteristic => { 291 | this.cache[primary_service_name][characteristic_name] = { cachedCharacteristic: characteristic }; 292 | return characteristic; 293 | }).catch(err => { 294 | return errorHandler('_returnCharacteristic_error', err, characteristic_name); 295 | }); 296 | } 297 | } 298 | } 299 | 300 | module.exports = BluetoothDevice; 301 | -------------------------------------------------------------------------------- /lib/bluetoothMap.js: -------------------------------------------------------------------------------- 1 | const bluetoothMap = { 2 | gattCharacteristicsMapping: { 3 | battery_level: { 4 | primaryServices: ['battery_service'], 5 | includedProperties: ['read', 'notify'], 6 | parseValue: value => { 7 | value = value.buffer ? value : new DataView(value); 8 | let result = {}; 9 | result.battery_level = value.getUint8(0); 10 | return result; 11 | } 12 | }, 13 | blood_pressure_feature: { 14 | primaryServices: ['blood_pressure'], 15 | includedProperties: ['read'] 16 | }, 17 | body_composition_feature: { 18 | primaryServices: ['body_composition'], 19 | includedProperties: ['read'] 20 | }, 21 | bond_management_feature: { 22 | primaryServices: ['bond_management_feature'], 23 | includedProperties: ['read'] 24 | }, 25 | cgm_feature: { 26 | primaryServices: ['continuous_glucose_monitoring'], 27 | includedProperties: ['read'] 28 | }, 29 | cgm_session_run_time: { 30 | primaryServices: ['continuous_glucose_monitoring'], 31 | includedProperties: ['read'] 32 | }, 33 | cgm_session_start_time: { 34 | primaryServices: ['continuous_glucose_monitoring'], 35 | includedProperties: ['read', 'write'] 36 | }, 37 | cgm_status: { 38 | primaryServices: ['continuous_glucose_monitoring'], 39 | includedProperties: ['read'] 40 | }, 41 | csc_feature: { 42 | primaryServices: ['cycling_speed_and_cadence'], 43 | includedProperties: ['read'], 44 | parseValue: value => { 45 | value = value.buffer ? value : new DataView(value); 46 | let flags = value.getUint16(0); 47 | let wheelRevolutionDataSupported = flags & 0x1; 48 | let crankRevolutionDataSupported = flags & 0x2; 49 | let multipleSensDataSupported = flags & 0x3; 50 | let result = {}; 51 | if(wheelRevolutionDataSupported) { 52 | result.wheel_revolution_data_supported = wheelRevolutionDataSupported ? true : false; 53 | } 54 | if(crankRevolutionDataSupported) { 55 | result.crank_revolution_data_supported = crankRevolutionDataSupported ? true : false; 56 | } 57 | if(multipleSensDataSupported) { 58 | result.multiple_sensors_supported = multipleSensDataSupported ? true : false; 59 | } 60 | return result; 61 | } 62 | }, 63 | current_time: { 64 | primaryServices: ['current_time'], 65 | includedProperties: ['read', 'write', 'notify'] 66 | }, 67 | cycling_power_feature: { 68 | primaryServices: ['cycling_power'], 69 | includedProperties: ['read'] 70 | }, 71 | firmware_revision_string: { 72 | primaryServices: ['device_information'], 73 | includedProperties: ['read'] 74 | }, 75 | hardware_revision_string: { 76 | primaryServices: ['device_information'], 77 | includedProperties: ['read'] 78 | }, 79 | ieee_11073_20601_regulatory_certification_data_list: { 80 | primaryServices: ['device_information'], 81 | includedProperties: ['read'] 82 | }, 83 | 'gap.appearance': { 84 | primaryServices: ['generic_access'], 85 | includedProperties: ['read'] 86 | }, 87 | 'gap.device_name': { 88 | primaryServices: ['generic_access'], 89 | includedProperties: ['read', 'write'], 90 | parseValue: value => { 91 | value = value.buffer ? value : new DataView(value); 92 | let result = {}; 93 | result.device_name = ''; 94 | for(var i=0; i { 100 | let buffer = new ArrayBuffer(value.length); 101 | let preppedValue = new DataView(buffer); 102 | value.split('').forEach((char, i)=>{ 103 | preppedValue.setUint8(i, char.charCodeAt(0)); 104 | }) 105 | return preppedValue; 106 | } 107 | }, 108 | 'gap.peripheral_preferred_connection_parameters': { 109 | primaryServices: ['generic_access'], 110 | includedProperties: ['read'] 111 | }, 112 | 'gap.peripheral_privacy_flag': { 113 | primaryServices: ['generic_access'], 114 | includedProperties: ['read'] 115 | }, 116 | glucose_feature: { 117 | primaryServices: ['glucose'], 118 | includedProperties: ['read'], 119 | parseValue: value => { 120 | value = value.buffer ? value : new DataView(value); 121 | let result = {}; 122 | let flags = value.getUint16(0); 123 | result.low_battery_detection_supported = flags & 0x1; 124 | result.sensor_malfunction_detection_supported = flags & 0x2; 125 | result.sensor_sample_size_supported = flags & 0x4; 126 | result.sensor_strip_insertion_error_detection_supported = flags & 0x8; 127 | result.sensor_strip_type_error_detection_supported = flags & 0x10; 128 | result.sensor_result_highLow_detection_supported = flags & 0x20; 129 | result.sensor_temperature_highLow_detection_supported = flags & 0x40; 130 | result.sensor_read_interruption_detection_supported = flags & 0x80; 131 | result.general_device_fault_supported = flags & 0x100; 132 | result.time_fault_supported = flags & 0x200; 133 | result.multiple_bond_supported = flags & 0x400; 134 | return result; 135 | } 136 | }, 137 | http_entity_body: { 138 | primaryServices: ['http_proxy'], 139 | includedProperties: ['read', 'write'] 140 | }, 141 | glucose_measurement: { 142 | primaryServices: ['glucose'], 143 | includedProperties: ['notify'], 144 | parseValue: value => { 145 | value = value.buffer ? value : new DataView(value); 146 | let flags = value.getUint8(0); 147 | let timeOffset = flags & 0x1; 148 | let concentrationTypeSampleLoc = flags & 0x2; 149 | let concentrationUnits = flags & 0x4; 150 | let statusAnnunciation = flags & 0x8; 151 | let contextInformation = flags & 0x10; 152 | let result = {}; 153 | let index = 1; 154 | if (timeOffset) { 155 | result.time_offset = value.getInt16(index, /*little-endian=*/true); 156 | index += 2; 157 | } 158 | if (concentrationTypeSampleLoc){ 159 | if(concentrationUnits){ 160 | result.glucose_concentraiton_molPerL = value.getInt16(index, /*little-endian=*/true ) 161 | index += 2; 162 | } 163 | else { 164 | result.glucose_concentraiton_kgPerL = value.getInt16(index, /*little-endian=*/true ) 165 | index += 2; 166 | } 167 | } 168 | return result; 169 | } 170 | }, 171 | http_headers: { 172 | primaryServices: ['http_proxy'], 173 | includedProperties: ['read', 'write'] 174 | }, 175 | https_security: { 176 | primaryServices: ['http_proxy'], 177 | includedProperties: ['read', 'write'] 178 | }, 179 | intermediate_temperature: { 180 | primaryServices: ['health_thermometer'], 181 | includedProperties: ['read', 'write', 'indicate'] 182 | }, 183 | local_time_information: { 184 | primaryServices: ['current_time'], 185 | includedProperties: ['read', 'write'] 186 | }, 187 | manufacturer_name_string: { 188 | primaryServices: ['device_information'], 189 | includedProperties: ['read'] 190 | }, 191 | model_number_string: { 192 | primaryServices: ['device_information'], 193 | includedProperties: ['read'] 194 | }, 195 | pnp_id: { 196 | primaryServices: ['device_information'], 197 | includedProperties: ['read'] 198 | }, 199 | protocol_mode: { 200 | primaryServices: ['human_interface_device'], 201 | includedProperties: ['read', 'writeWithoutResponse'] 202 | }, 203 | reference_time_information: { 204 | primaryServices: ['current_time'], 205 | includedProperties: ['read'] 206 | }, 207 | supported_new_alert_category: { 208 | primaryServices: ['alert_notification'], 209 | includedProperties: ['read'] 210 | }, 211 | body_sensor_location: { 212 | primaryServices: ['heart_rate'], 213 | includedProperties: ['read'], 214 | parseValue: value => { 215 | value = value.buffer ? value : new DataView(value); 216 | let val = value.getUint8(0); 217 | let result = {}; 218 | switch (val) { 219 | case 0: result.location = 'Other'; 220 | case 1: result.location = 'Chest'; 221 | case 2: result.location = 'Wrist'; 222 | case 3: result.location = 'Finger'; 223 | case 4: result.location = 'Hand'; 224 | case 5: result.location = 'Ear Lobe'; 225 | case 6: result.location = 'Foot'; 226 | default: result.location = 'Unknown'; 227 | } 228 | return result; 229 | } 230 | }, 231 | // heart_rate_control_point 232 | heart_rate_control_point: { 233 | primaryServices: ['heart_rate'], 234 | includedProperties: ['write'], 235 | prepValue: value => { 236 | let buffer = new ArrayBuffer(1); 237 | let writeView = new DataView(buffer); 238 | writeView.setUint8(0,value); 239 | return writeView; 240 | } 241 | }, 242 | heart_rate_measurement: { 243 | primaryServices: ['heart_rate'], 244 | includedProperties: ['notify'], 245 | /** 246 | * Parses the event.target.value object and returns object with readable 247 | * key-value pairs for all advertised characteristic values 248 | * 249 | * @param {Object} value Takes event.target.value object from startNotifications method 250 | * 251 | * @return {Object} result Returns readable object with relevant characteristic values 252 | * 253 | */ 254 | parseValue: value => { 255 | value = value.buffer ? value : new DataView(value); 256 | let flags = value.getUint8(0); 257 | let rate16Bits = flags & 0x1; 258 | let contactDetected = flags & 0x2; 259 | let contactSensorPresent = flags & 0x4; 260 | let energyPresent = flags & 0x8; 261 | let rrIntervalPresent = flags & 0x10; 262 | let result = {}; 263 | let index = 1; 264 | if (rate16Bits) { 265 | result.heartRate = value.getUint16(index, /*little-endian=*/true); 266 | index += 2; 267 | } else { 268 | result.heartRate = value.getUint8(index); 269 | index += 1; 270 | } 271 | if (contactSensorPresent) { 272 | result.contactDetected = !!contactDetected; 273 | } 274 | if (energyPresent) { 275 | result.energyExpended = value.getUint16(index, /*little-endian=*/true); 276 | index += 2; 277 | } 278 | if (rrIntervalPresent) { 279 | let rrIntervals = []; 280 | for (; index + 1 < value.byteLength; index += 2) { 281 | rrIntervals.push(value.getUint16(index, /*little-endian=*/true)); 282 | } 283 | result.rrIntervals = rrIntervals; 284 | } 285 | return result; 286 | } 287 | }, 288 | serial_number_string: { 289 | primaryServices: ['device_information'], 290 | includedProperties: ['read'] 291 | }, 292 | software_revision_string: { 293 | primaryServices: ['device_information'], 294 | includedProperties: ['read'] 295 | }, 296 | supported_unread_alert_category: { 297 | primaryServices: ['alert_notification'], 298 | includedProperties: ['read'] 299 | }, 300 | system_id: { 301 | primaryServices: ['device_information'], 302 | includedProperties: ['read'] 303 | }, 304 | temperature_type: { 305 | primaryServices: ['health_thermometer'], 306 | includedProperties: ['read'] 307 | }, 308 | descriptor_value_changed: { 309 | primaryServices: ['environmental_sensing'], 310 | includedProperties: ['indicate', 'writeAux', 'extProp'], 311 | }, 312 | apparent_wind_direction: { 313 | primaryServices: ['environmental_sensing'], 314 | includedProperties: ['read', 'notify','writeAux', 'extProp'], 315 | parseValue: value => { 316 | value = value.buffer ? value : new DataView(value); 317 | let result = {}; 318 | result.apparent_wind_direction = value.getUint16(0) * 0.01; 319 | return result; 320 | } 321 | }, 322 | apparent_wind_speed: { 323 | primaryServices: ['environmental_sensing'], 324 | includedProperties: ['read', 'notify','writeAux', 'extProp'], 325 | parseValue: value => { 326 | value = value.buffer ? value : new DataView(value); 327 | let result = {}; 328 | result.apparent_wind_speed = value.getUint16(0) * 0.01; 329 | return result; 330 | } 331 | }, 332 | dew_point: { 333 | primaryServices: ['environmental_sensing'], 334 | includedProperties: ['read', 'notify','writeAux', 'extProp'], 335 | parseValue: value => { 336 | value = value.buffer ? value : new DataView(value); 337 | let result = {}; 338 | result.dew_point = value.getInt8(0); 339 | return result; 340 | } 341 | }, 342 | elevation: { 343 | primaryServices: ['environmental_sensing'], 344 | includedProperties: ['read', 'notify','writeAux', 'extProp'], 345 | parseValue: value => { 346 | value = value.buffer ? value : new DataView(value); 347 | let result = {}; 348 | result.elevation = value.getInt8(0) << 16 | value.getInt8(1) << 8 | value.getInt8(2); 349 | return result; 350 | } 351 | }, 352 | gust_factor: { 353 | primaryServices: ['environmental_sensing'], 354 | includedProperties: ['read', 'notify','writeAux', 'extProp'], 355 | parseValue: value => { 356 | value = value.buffer ? value : new DataView(value); 357 | let result = {}; 358 | result.gust_factor = value.getUint8(0) * 0.1; 359 | return result; 360 | } 361 | }, 362 | heat_index: { 363 | primaryServices: ['environmental_sensing'], 364 | includedProperties: ['read', 'notify','writeAux', 'extProp'], 365 | parseValue: value => { 366 | value = value.buffer ? value : new DataView(value); 367 | let result = {}; 368 | result.heat_index = value.getInt8(0); 369 | return result; 370 | } 371 | }, 372 | humidity: { 373 | primaryServices: ['environmental_sensing'], 374 | includedProperties: ['read', 'notify','writeAux', 'extProp'], 375 | parseValue: value => { 376 | value = value.buffer ? value : new DataView(value); 377 | let result = {}; 378 | 379 | result.humidity = value.getUint16(0) * 0.01; 380 | return result; 381 | } 382 | }, 383 | irradiance: { 384 | primaryServices: ['environmental_sensing'], 385 | includedProperties: ['read', 'notify','writeAux', 'extProp'], 386 | parseValue: value => { 387 | value = value.buffer ? value : new DataView(value); 388 | let result = {}; 389 | 390 | result.irradiance = value.getUint16(0) * 0.1; 391 | return result; 392 | } 393 | }, 394 | rainfall: { 395 | primaryServices: ['environmental_sensing'], 396 | includedProperties: ['read', 'notify','writeAux', 'extProp'], 397 | parseValue: value => { 398 | value = value.buffer ? value : new DataView(value); 399 | let result = {}; 400 | 401 | result.rainfall = value.getUint16(0) * 0.001; 402 | return result; 403 | } 404 | }, 405 | pressure: { 406 | primaryServices: ['environmental_sensing'], 407 | includedProperties: ['read', 'notify','writeAux', 'extProp'], 408 | parseValue: value => { 409 | value = value.buffer ? value : new DataView(value); 410 | let result = {}; 411 | result.pressure = value.getUint32(0) * 0.1; 412 | return result; 413 | } 414 | }, 415 | temperature: { 416 | primaryServices: ['environmental_sensing'], 417 | includedProperties: ['read', 'notify','writeAux', 'extProp'], 418 | parseValue: value => { 419 | value = value.buffer ? value : new DataView(value); 420 | let result = {}; 421 | result.temperature = value.getInt16(0) * 0.01; 422 | return result; 423 | } 424 | }, 425 | true_wind_direction: { 426 | primaryServices: ['environmental_sensing'], 427 | includedProperties: ['read', 'notify','writeAux', 'extProp'], 428 | parseValue: value => { 429 | value = value.buffer ? value : new DataView(value); 430 | let result = {}; 431 | result.true_wind_direction = value.getUint16(0) * 0.01; 432 | return result; 433 | } 434 | }, 435 | true_wind_speed: { 436 | primaryServices: ['environmental_sensing'], 437 | includedProperties: ['read', 'notify','writeAux', 'extProp'], 438 | parseValue: value => { 439 | value = value.buffer ? value : new DataView(value); 440 | let result = {}; 441 | result.true_wind_speed = value.getUint16(0) * 0.01; 442 | return result; 443 | } 444 | }, 445 | uv_index: { 446 | primaryServices: ['environmental_sensing'], 447 | includedProperties: ['read', 'notify','writeAux', 'extProp'], 448 | parseValue: value => { 449 | value = value.buffer ? value : new DataView(value); 450 | let result = {}; 451 | result.uv_index = value.getUint8(0); 452 | return result; 453 | } 454 | }, 455 | wind_chill: { 456 | primaryServices: ['environmental_sensing'], 457 | includedProperties: ['read', 'notify','writeAux', 'extProp'], 458 | parseValue: value => { 459 | value = value.buffer ? value : new DataView(value); 460 | let result = {}; 461 | result.wind_chill = value.getInt8(0); 462 | return result; 463 | } 464 | }, 465 | barometric_pressure_trend: { 466 | primaryServices: ['environmental_sensing'], 467 | includedProperties: ['read', 'notify','writeAux', 'extProp'], 468 | parseValue: value => { 469 | value = value.buffer ? value : new DataView(value); 470 | let val = value.getUint8(0); 471 | let result = {}; 472 | switch (val) { 473 | case 0: result.barometric_pressure_trend = 'Unknown'; 474 | case 1: result.barometric_pressure_trend = 'Continuously falling'; 475 | case 2: result.barometric_pressure_trend = 'Continously rising'; 476 | case 3: result.barometric_pressure_trend = 'Falling, then steady'; 477 | case 4: result.barometric_pressure_trend = 'Rising, then steady'; 478 | case 5: result.barometric_pressure_trend = 'Falling before a lesser rise'; 479 | case 6: result.barometric_pressure_trend = 'Falling before a greater rise'; 480 | case 7: result.barometric_pressure_trend = 'Rising before a greater fall'; 481 | case 8: result.barometric_pressure_trend = 'Rising before a lesser fall'; 482 | case 9: result.barometric_pressure_trend = 'Steady'; 483 | default: result.barometric_pressure_trend = 'Could not resolve to trend'; 484 | } 485 | return result; 486 | } 487 | }, 488 | magnetic_declination: { 489 | primaryServices: ['environmental_sensing'], 490 | includedProperties: ['read', 'notify','writeAux', 'extProp'], 491 | parseValue: value => { 492 | value = value.buffer ? value : new DataView(value); 493 | let result = {}; 494 | 495 | result.magnetic_declination = value.getUint16(0) * 0.01; 496 | return result; 497 | } 498 | }, 499 | magnetic_flux_density_2D: { 500 | primaryServices: ['environmental_sensing'], 501 | includedProperties: ['read', 'notify','writeAux', 'extProp'], 502 | parseValue: value => { 503 | value = value.buffer ? value : new DataView(value); 504 | let result = {}; 505 | //FIXME: need to find out if these values are stored at different byte addresses 506 | // below assumes that values are stored at successive byte addresses 507 | result.magnetic_flux_density_x_axis = value.getInt16(0,/*little-endian=*/ true) * 0.0000001; 508 | result.magnetic_flux_density_y_axis = value.getInt16(2,/*little-endian=*/ true) * 0.0000001; 509 | return result; 510 | } 511 | }, 512 | magnetic_flux_density_3D: { 513 | primaryServices: ['environmental_sensing'], 514 | includedProperties: ['read', 'notify','writeAux', 'extProp'], 515 | parseValue: value => { 516 | value = value.buffer ? value : new DataView(value); 517 | let result = {}; 518 | //FIXME: need to find out if these values are stored at different byte addresses 519 | // below assumes that values are stored at successive byte addresses 520 | result.magnetic_flux_density_x_axis = value.getInt16(0,/*little-endian=*/ true) * 0.0000001; 521 | result.magnetic_flux_density_y_axis = value.getInt16(2,/*little-endian=*/ true) * 0.0000001; 522 | result.magnetic_flux_density_z_axis = value.getInt16(4,/*little-endian=*/ true) * 0.0000001; 523 | return result; 524 | } 525 | }, 526 | tx_power_level: { 527 | primaryServices: ['tx_power'], 528 | includedProperties: ['read'], 529 | parseValue: value => { 530 | value = value.buffer ? value : new DataView(value); 531 | let result = {}; 532 | result.tx_power_level = value.getInt8(0); 533 | return result; 534 | } 535 | }, 536 | weight_scale_feature: { 537 | primaryServices: ['weight_scale'], 538 | includedProperties: ['read'], 539 | parseValue: value => { 540 | value = value.buffer ? value : new DataView(value); 541 | let result = {}; 542 | let flags = value.getInt32(0); 543 | result.time_stamp_supported = flags & 0x1; 544 | result.multiple_sensors_supported = flags & 0x2; 545 | result.BMI_supported = flags & 0x4; 546 | switch (flags & 0x78 >> 3) { 547 | case 0: result.weight_measurement_resolution = 'Not specified'; 548 | case 1: result.weight_measurement_resolution = 'Resolution of 0.5 kg or 1 lb'; 549 | case 2: result.weight_measurement_resolution = 'Resolution of 0.2 kg or 0.5 lb'; 550 | case 3: result.weight_measurement_resolution = 'Resolution of 0.1 kg or 0.2 lb'; 551 | case 4: result.weight_measurement_resolution = 'Resolution of 0.05 kg or 0.1 lb'; 552 | case 5: result.weight_measurement_resolution = 'Resolution of 0.02 kg or 0.05 lb'; 553 | case 6: result.weight_measurement_resolution = 'Resolution of 0.01 kg or 0.02 lb'; 554 | case 7: result.weight_measurement_resolution = 'Resolution of 0.005 kg or 0.01 lb'; 555 | default: result.weight_measurement_resolution = 'Could not resolve'; 556 | } 557 | switch (flags & 0x380 >> 7) { 558 | case 0: result.height_measurement_resolution = 'Not specified'; 559 | case 1: result.height_measurement_resolution = 'Resolution of 0.1 meter or 1 inch'; 560 | case 2: result.height_measurement_resolution = 'Resolution of 0.005 meter or 0.5 inch'; 561 | case 3: result.height_measurement_resolution = 'Resolution of 0.001 meter or 0.1 inch'; 562 | default: result.height_measurement_resolution = 'Could not resolve'; 563 | } 564 | // Remaining flags reserved for future use 565 | return result; 566 | } 567 | }, 568 | csc_measurement: { 569 | primaryServices: ['cycling_speed_and_cadence'], 570 | includedProperties: ['notify'], 571 | parseValue: value => { 572 | value = value.buffer ? value : new DataView(value); 573 | let flags = value.getUint8(0); 574 | let wheelRevolution = flags & 0x1; //integer = truthy, 0 = falsy 575 | let crankRevolution = flags & 0x2; 576 | let result = {}; 577 | let index = 1; 578 | if(wheelRevolution) { 579 | result.cumulative_wheel_revolutions = value.getUint32(index,/*little-endian=*/ true); 580 | index += 4; 581 | result.last_wheel_event_time_per_1024s = value.getUint16(index,/*little-endian=*/ true); 582 | index += 2; 583 | } 584 | if(crankRevolution) { 585 | result.cumulative_crank_revolutions = value.getUint16(index,/*little-endian=*/ true); 586 | index += 2; 587 | result.last_crank_event_time_per_1024s = value.getUint16(index,/*little-endian=*/ true); 588 | index += 2; 589 | } 590 | return result; 591 | } 592 | }, 593 | sensor_location: { 594 | primaryServices: ['cycling_speed_and_cadence'], 595 | includedProperties: ['read'], 596 | parseValue: value => { 597 | value = value.buffer ? value : new DataView(value); 598 | let val = value.getUint16(0); 599 | let result = {}; 600 | switch (val) { 601 | case 0: result.location = 'Other'; 602 | case 1: result.location = 'Top of show'; 603 | case 2: result.location = 'In shoe'; 604 | case 3: result.location = 'Hip'; 605 | case 4: result.location = 'Front Wheel'; 606 | case 5: result.location = 'Left Crank'; 607 | case 6: result.location = 'Right Crank'; 608 | case 7: result.location = 'Left Pedal'; 609 | case 8: result.location = 'Right Pedal'; 610 | case 9: result.location = 'Front Hub'; 611 | case 10: result.location = 'Rear Dropout'; 612 | case 11: result.location = 'Chainstay'; 613 | case 12: result.location = 'Rear Wheel'; 614 | case 13: result.location = 'Rear Hub'; 615 | case 14: result.location = 'Chest'; 616 | case 15: result.location = 'Spider'; 617 | case 16: result.location = 'Chain Ring'; 618 | default: result.location = 'Unknown'; 619 | } 620 | return result; 621 | } 622 | }, 623 | sc_control_point: { 624 | primaryServices: ['cycling_speed_and_cadence'], 625 | includedProperties: ['write','indicate'], 626 | parseValue: value => { 627 | value = value.buffer ? value : new DataView(value); 628 | return result; 629 | } 630 | }, 631 | cycling_power_measurement: { 632 | primaryServices: ['cycling_power'], 633 | includedProperties: ['notify'], 634 | parseValue: value => { 635 | value = value.buffer ? value : new DataView(value); 636 | let flags = value.getUint16(0); 637 | let pedal_power_balance_present = flags & 0x1; 638 | let pedal_power_reference = flags & 0x2; 639 | let accumulated_torque_present = flags & 0x4; 640 | let accumulated_torque_source = flags & 0x8; 641 | let wheel_revolution_data_present = flags & 0x10; 642 | let crank_revolution_data_present = flags & 0x12; 643 | let extreme_force_magnitude_present = flags & 0x12; 644 | let extreme_torque_magnitude_present = flags & 0x12; 645 | let extreme_angles_present = flags & 0x12; 646 | let top_dead_spot_angle_present = flags & 0x12; 647 | let bottom_dead_spot_angle_present = flags & 0x12; 648 | let accumulated_energy_present = flags & 0x12; 649 | let offset_compensation_indicator = flags & 0x12; 650 | let result = {}; 651 | let index = 1; 652 | //Watts with resolution of 1 653 | result.instantaneous_power = value.getInt16(index); 654 | index += 2; 655 | if(pedal_power_reference) { 656 | //Percentage with resolution of 1/2 657 | result.pedal_power_balance = value.getUint8(index); 658 | index += 1; 659 | } 660 | if(accumulated_torque_present) { 661 | //Percentage with resolution of 1/2 662 | result.accumulated_torque = value.getUint16(index); 663 | index += 2; 664 | } 665 | if(wheel_revolution_data_present) { 666 | result.cumulative_wheel_revolutions = value.Uint32(index); 667 | index += 4; 668 | result.last_wheel_event_time_per_2048s = value.Uint16(index); 669 | index += 2; 670 | } 671 | if(crank_revolution_data_present) { 672 | result.cumulative_crank_revolutions = value.getUint16(index,/*little-endian=*/ true); 673 | index += 2; 674 | result.last_crank_event_time_per_1024s = value.getUint16(index,/*little-endian=*/ true); 675 | index += 2; 676 | } 677 | if(extreme_force_magnitude_present) { 678 | //Newton meters with resolution of 1 TODO: units? 679 | result.maximum_force_magnitude = value.getInt16(index); 680 | index += 2; 681 | result.minimum_force_magnitude = value.getInt16(index); 682 | index += 2; 683 | } 684 | if(extreme_torque_magnitude_present) { 685 | //Newton meters with resolution of 1 TODO: units? 686 | result.maximum_torque_magnitude = value.getInt16(index); 687 | index += 2; 688 | result.minimum_torque_magnitude = value.getInt16(index); 689 | index += 2; 690 | } 691 | if(extreme_angles_present) { 692 | //TODO: UINT12. 693 | //Newton meters with resolution of 1 TODO: units? 694 | // result.maximum_angle = value.getInt12(index); 695 | // index += 2; 696 | // result.minimum_angle = value.getInt12(index); 697 | // index += 2; 698 | } 699 | if(top_dead_spot_angle_present) { 700 | //Percentage with resolution of 1/2 701 | result.top_dead_spot_angle = value.getUint16(index); 702 | index += 2; 703 | } 704 | if(bottom_dead_spot_angle_present) { 705 | //Percentage with resolution of 1/2 706 | result.bottom_dead_spot_angle = value.getUint16(index); 707 | index += 2; 708 | } 709 | if(accumulated_energy_present) { 710 | //kilojoules with resolution of 1 TODO: units? 711 | result.accumulated_energy = value.getUint16(index); 712 | index += 2; 713 | } 714 | return result; 715 | } 716 | }, 717 | }, 718 | gattServiceList: ['alert_notification', 'automation_io', 'battery_service', 'blood_pressure', 719 | 'body_composition', 'bond_management', 'continuous_glucose_monitoring', 720 | 'current_time', 'cycling_power', 'cycling_speed_and_cadence', 'device_information', 721 | 'environmental_sensing', 'generic_access', 'generic_attribute', 'glucose', 722 | 'health_thermometer', 'heart_rate', 'human_interface_device', 723 | 'immediate_alert', 'indoor_positioning', 'internet_protocol_support', 'link_loss', 724 | 'location_and_navigation', 'next_dst_change', 'phone_alert_status', 725 | 'pulse_oximeter', 'reference_time_update', 'running_speed_and_cadence', 726 | 'scan_parameters', 'tx_power', 'user_data', 'weight_scale' 727 | ], 728 | } 729 | 730 | module.exports = bluetoothMap; 731 | -------------------------------------------------------------------------------- /lib/errorHandler.js: -------------------------------------------------------------------------------- 1 | /** errorHandler - Consolodates error message configuration and logic 2 | * 3 | * @param {string} errorKey - maps to a detailed error message 4 | * @param {object} nativeError - the native API error object, if present 5 | * @param {} alternate - 6 | * 7 | */ 8 | function errorHandler(errorKey, nativeError, alternate) { 9 | 10 | const errorMessages = { 11 | add_characteristic_exists_error: `Characteristic ${alternate} already exists.`, 12 | characteristic_error: `Characteristic ${alternate} not found. Add ${alternate} to device using addCharacteristic or try another characteristic.`, 13 | connect_gatt: `Could not connect to GATT. Device might be out of range. Also check to see if filters are vaild.`, 14 | connect_server: `Could not connect to server on device.`, 15 | connect_service: `Could not find service.`, 16 | disconnect_timeout: `Timed out. Could not disconnect.`, 17 | disconnect_error: `Could not disconnect from device.`, 18 | improper_characteristic_format: `${alternate} is not a properly formatted characteristic.`, 19 | improper_properties_format: `${alternate} is not a properly formatted properties array.`, 20 | improper_service_format: `${alternate} is not a properly formatted service.`, 21 | issue_disconnecting: `Issue disconnecting with device.`, 22 | new_characteristic_missing_params: `${alternate} is not a fully supported characteristic. Please provide an associated primary service and at least one property.`, 23 | no_device: `No instance of device found.`, 24 | no_filters: `No filters found on instance of Device. For more information, please visit http://sabertooth.io/#method-newdevice`, 25 | no_read_property: `No read property on characteristic: ${alternate}.`, 26 | no_write_property: `No write property on this characteristic.`, 27 | not_connected: `Could not disconnect. Device not connected.`, 28 | parsing_not_supported: `Parsing not supported for characterstic: ${alternate}.`, 29 | read_error: `Cannot read value on the characteristic.`, 30 | _returnCharacteristic_error: `Error accessing characteristic ${alternate}.`, 31 | start_notifications_error: `Not able to read stream of data from characteristic: ${alternate}.`, 32 | start_notifications_no_notify: `No notify property found on this characteristic: ${alternate}.`, 33 | stop_notifications_not_notifying: `Notifications not established for characteristic: ${alternate} or you have not started notifications.`, 34 | stop_notifications_error: `Issue stopping notifications for characteristic: ${alternate} or you have not started notifications.`, 35 | user_cancelled: `User cancelled the permission request.`, 36 | uuid_error: `Invalid UUID. For more information on proper formatting of UUIDs, visit https://webbluetoothcg.github.io/web-bluetooth/#uuids`, 37 | write_error: `Could not change value of characteristic: ${alternate}.`, 38 | write_permissions: `${alternate} characteristic does not have a write property.` 39 | } 40 | 41 | throw new Error(errorMessages[errorKey]); 42 | return false; 43 | } 44 | 45 | module.exports = errorHandler; 46 | -------------------------------------------------------------------------------- /npm.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist/npm/BluetoothDevice'); 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web-bluetooth", 3 | "version": "0.1.2", 4 | "description": "Library for interacting with Bluetooth 4.0 devices through the browser.", 5 | "keywords": [ 6 | "abstraction", 7 | "API", 8 | "ble", 9 | "bluetooth", 10 | "browser", 11 | "client", 12 | "developer", 13 | "developer tool", 14 | "experimental", 15 | "framework", 16 | "internet of things", 17 | "library", 18 | "web-bluetooth" 19 | ], 20 | "homepage": "docs.sabertooth.io", 21 | "author": "Alex Patch (https://github.com/the-gingerbread-man)", 22 | "contributors": [ 23 | "Alex Patch (https://github.com/the-gingerbread-man)", 24 | "Aaron Peltz (https://github.com/apeltz)", 25 | "Carlos Corral (https://github.com/ccorral)", 26 | "Daniel Lee (https://github.com/dslee393)", 27 | "Francois Beaufort (https://github.com/beaufortfrancois)" 28 | ], 29 | "maintainers": "Alex Patch (https://github.com/the-gingerbread-man)", 30 | "repository": { 31 | "type": "git", 32 | "url": "git+https://github.com/sabertooth-io/web-bluetooth.git" 33 | }, 34 | "bugs": { 35 | "url": "https://github.com/sabertooth-io/web-bluetooth/issues", 36 | "email": "alex.patch3@gmail.com" 37 | }, 38 | "main": "npm.js", 39 | "scripts": { 40 | "build": "npm run build:npm && npm run build:bower", 41 | "build:npm": "babel lib --presets babel-preset-es2015 --out-dir dist/npm", 42 | "build:bower": "browserify -t [babelify] lib/BluetoothDevice.js -o dist/build.js", 43 | "prepublish": "npm run build", 44 | "test": "open test/browser-integration.html", 45 | "lint": "eslint lib/*.js && eslint lib/**/*.js" 46 | }, 47 | "license": "Apache-2.0", 48 | "devDependencies": { 49 | "babel-cli": "^6.9.0", 50 | "babel-preset-es2015": "^6.9.0", 51 | "babelify": "^7.3.0", 52 | "browserify": "^13.0.1", 53 | "chai": "^3.5.0", 54 | "eslint": "^2.11.1", 55 | "eslint-config-airbnb": "^9.0.1", 56 | "eslint-plugin-import": "^1.8.1", 57 | "eslint-plugin-jsx-a11y": "^1.2.2", 58 | "eslint-plugin-react": "^5.1.1", 59 | "mocha": "^2.4.5", 60 | "sinon": "^1.17.4" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /test/browser-integration.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Sabertooth Browser Tests 6 | 7 | 8 | 9 | 10 |
11 |
12 |
13 | 14 | 15 | 16 | 17 | 20 | 21 |
22 | 23 | 24 | -------------------------------------------------------------------------------- /test/lib/raw-browser-api-tests.js: -------------------------------------------------------------------------------- 1 | const { assert, expect, should } = chai; 2 | let apiDevice; 3 | 4 | describe('Raw API', function() { 5 | it('connects to a device', function() { 6 | expect(typeof apiDevice).to.equal('object'); 7 | }); 8 | }); 9 | 10 | class Button { 11 | constructor(parentId, textContent, onClick) { 12 | this.node = document.createElement('button'); 13 | this.parentNode = document.getElementById(parentId); 14 | this.node.innerHTML = `

${textContent}

`; 15 | this.node.addEventListener('click', onClick); 16 | } 17 | 18 | append() { 19 | this.parentNode.appendChild(this.node); 20 | } 21 | } 22 | 23 | function acquireDeviceRaw(filters) { 24 | navigator.bluetooth.requestDevice(filters) 25 | .then(device => { 26 | apiDevice = device; 27 | mocha.run(); 28 | }) 29 | .catch(err => { 30 | console.error(`ERROR: Unable to acquire device\n ${err}`); 31 | }); 32 | } 33 | 34 | const permissionsButton = new Button('permissions', 'Request Device', () => acquireDeviceRaw({filters: [{ namePrefix: "P" }]})); 35 | permissionsButton.append(); 36 | --------------------------------------------------------------------------------