├── .gitignore ├── index.js ├── package-lock.json ├── package.json └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directories 27 | node_modules 28 | jspm_packages 29 | 30 | # Optional npm cache directory 31 | .npm 32 | 33 | # Optional REPL history 34 | .node_repl_history 35 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var noble = require('noble-mac'); //change to require('noble') for non-macOS 2 | 3 | noble.on('stateChange', function(state) { 4 | if (state === 'poweredOn') { 5 | // Seek for peripherals broadcasting the heart rate service 6 | // This will pick up a Polar H7 and should pick up other ble heart rate bands 7 | // Will use whichever the first one discovered is if more than one are in range 8 | noble.startScanning(["180d"]); 9 | } else { 10 | noble.stopScanning(); 11 | } 12 | }); 13 | 14 | noble.on('discover', function(peripheral) { 15 | // Once peripheral is discovered, stop scanning 16 | noble.stopScanning(); 17 | 18 | // connect to the heart rate sensor 19 | peripheral.connect(function(error){ 20 | // 180d is the bluetooth service for heart rate: 21 | // https://developer.bluetooth.org/gatt/services/Pages/ServiceViewer.aspx?u=org.bluetooth.service.heart_rate.xml 22 | var serviceUUID = ["180d"]; 23 | // 2a37 is the characteristic for heart rate measurement 24 | // https://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicViewer.aspx?u=org.bluetooth.characteristic.heart_rate_measurement.xml 25 | var characteristicUUID = ["2a37"]; 26 | 27 | // use noble's discoverSomeServicesAndCharacteristics 28 | // scoped to the heart rate service and measurement characteristic 29 | peripheral.discoverSomeServicesAndCharacteristics(serviceUUID, characteristicUUID, function(error, services, characteristics){ 30 | characteristics[0].notify(true, function(error){ 31 | characteristics[0].on('data', function(data, isNotification){ 32 | // Upon receiving data (array of bytes), decode the HR (in bpm) and the RRIs included 33 | //console.log('length: ' + data.length) 34 | //console.log('flags: ' + data[0]); //(conv this to binary) see: https://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicViewer.aspx?u=org.bluetooth.characteristic.heart_rate_measurement.xml 35 | //console.log('bpm: ' + data[1]); 36 | var i; RRITxt = ''//'RRI: ' 37 | for (i = 2; i < data.length; i+=2) { //loop over RRI entries (2 indicies per val) 38 | RRITxt += ' ' + (data[i] + 256*data[i+1]); //2nd bit is most significant 39 | } 40 | console.log(RRITxt) 41 | }); 42 | }); 43 | }); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-ble-hr", 3 | "requires": true, 4 | "lockfileVersion": 1, 5 | "dependencies": { 6 | "abbrev": { 7 | "version": "1.1.1", 8 | "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", 9 | "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" 10 | }, 11 | "ansi-regex": { 12 | "version": "2.1.1", 13 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", 14 | "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" 15 | }, 16 | "aproba": { 17 | "version": "1.2.0", 18 | "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", 19 | "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" 20 | }, 21 | "are-we-there-yet": { 22 | "version": "1.1.5", 23 | "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", 24 | "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", 25 | "requires": { 26 | "delegates": "^1.0.0", 27 | "readable-stream": "^2.0.6" 28 | } 29 | }, 30 | "balanced-match": { 31 | "version": "1.0.0", 32 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 33 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" 34 | }, 35 | "bluetooth-hci-socket": { 36 | "version": "0.5.1", 37 | "resolved": "https://registry.npmjs.org/bluetooth-hci-socket/-/bluetooth-hci-socket-0.5.1.tgz", 38 | "integrity": "sha1-774hUk/Bz10/rl1RNl1WHUq77Qs=", 39 | "optional": true, 40 | "requires": { 41 | "debug": "^2.2.0", 42 | "nan": "^2.0.5", 43 | "usb": "^1.1.0" 44 | } 45 | }, 46 | "bplist-parser": { 47 | "version": "0.0.6", 48 | "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.0.6.tgz", 49 | "integrity": "sha1-ONo0cYF9+dRKs4kuJ3B7u9daEbk=", 50 | "optional": true 51 | }, 52 | "brace-expansion": { 53 | "version": "1.1.11", 54 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 55 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 56 | "requires": { 57 | "balanced-match": "^1.0.0", 58 | "concat-map": "0.0.1" 59 | } 60 | }, 61 | "chownr": { 62 | "version": "1.1.1", 63 | "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", 64 | "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==" 65 | }, 66 | "code-point-at": { 67 | "version": "1.1.0", 68 | "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", 69 | "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" 70 | }, 71 | "concat-map": { 72 | "version": "0.0.1", 73 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 74 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" 75 | }, 76 | "console-control-strings": { 77 | "version": "1.1.0", 78 | "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", 79 | "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" 80 | }, 81 | "core-util-is": { 82 | "version": "1.0.2", 83 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 84 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" 85 | }, 86 | "cross-spawn": { 87 | "version": "6.0.5", 88 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", 89 | "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", 90 | "requires": { 91 | "nice-try": "^1.0.4", 92 | "path-key": "^2.0.1", 93 | "semver": "^5.5.0", 94 | "shebang-command": "^1.2.0", 95 | "which": "^1.2.9" 96 | } 97 | }, 98 | "debug": { 99 | "version": "2.2.0", 100 | "resolved": "http://registry.npmjs.org/debug/-/debug-2.2.0.tgz", 101 | "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", 102 | "requires": { 103 | "ms": "0.7.1" 104 | } 105 | }, 106 | "deep-extend": { 107 | "version": "0.6.0", 108 | "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", 109 | "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" 110 | }, 111 | "delegates": { 112 | "version": "1.0.0", 113 | "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", 114 | "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" 115 | }, 116 | "detect-libc": { 117 | "version": "1.0.3", 118 | "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", 119 | "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" 120 | }, 121 | "fs-minipass": { 122 | "version": "1.2.5", 123 | "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz", 124 | "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", 125 | "requires": { 126 | "minipass": "^2.2.1" 127 | } 128 | }, 129 | "fs.realpath": { 130 | "version": "1.0.0", 131 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 132 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" 133 | }, 134 | "gauge": { 135 | "version": "2.7.4", 136 | "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", 137 | "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", 138 | "requires": { 139 | "aproba": "^1.0.3", 140 | "console-control-strings": "^1.0.0", 141 | "has-unicode": "^2.0.0", 142 | "object-assign": "^4.1.0", 143 | "signal-exit": "^3.0.0", 144 | "string-width": "^1.0.1", 145 | "strip-ansi": "^3.0.1", 146 | "wide-align": "^1.1.0" 147 | } 148 | }, 149 | "glob": { 150 | "version": "7.1.3", 151 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", 152 | "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", 153 | "requires": { 154 | "fs.realpath": "^1.0.0", 155 | "inflight": "^1.0.4", 156 | "inherits": "2", 157 | "minimatch": "^3.0.4", 158 | "once": "^1.3.0", 159 | "path-is-absolute": "^1.0.0" 160 | } 161 | }, 162 | "has-unicode": { 163 | "version": "2.0.1", 164 | "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", 165 | "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" 166 | }, 167 | "iconv-lite": { 168 | "version": "0.4.24", 169 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 170 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 171 | "requires": { 172 | "safer-buffer": ">= 2.1.2 < 3" 173 | } 174 | }, 175 | "ignore-walk": { 176 | "version": "3.0.1", 177 | "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", 178 | "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", 179 | "requires": { 180 | "minimatch": "^3.0.4" 181 | } 182 | }, 183 | "inflight": { 184 | "version": "1.0.6", 185 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 186 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 187 | "requires": { 188 | "once": "^1.3.0", 189 | "wrappy": "1" 190 | } 191 | }, 192 | "inherits": { 193 | "version": "2.0.3", 194 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 195 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 196 | }, 197 | "ini": { 198 | "version": "1.3.5", 199 | "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", 200 | "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" 201 | }, 202 | "is-fullwidth-code-point": { 203 | "version": "1.0.0", 204 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", 205 | "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", 206 | "requires": { 207 | "number-is-nan": "^1.0.0" 208 | } 209 | }, 210 | "isarray": { 211 | "version": "1.0.0", 212 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 213 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" 214 | }, 215 | "isexe": { 216 | "version": "2.0.0", 217 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 218 | "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" 219 | }, 220 | "minimatch": { 221 | "version": "3.0.4", 222 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 223 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 224 | "requires": { 225 | "brace-expansion": "^1.1.7" 226 | } 227 | }, 228 | "minimist": { 229 | "version": "0.0.8", 230 | "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", 231 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" 232 | }, 233 | "minipass": { 234 | "version": "2.3.5", 235 | "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz", 236 | "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", 237 | "requires": { 238 | "safe-buffer": "^5.1.2", 239 | "yallist": "^3.0.0" 240 | } 241 | }, 242 | "minizlib": { 243 | "version": "1.1.1", 244 | "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.1.1.tgz", 245 | "integrity": "sha512-TrfjCjk4jLhcJyGMYymBH6oTXcWjYbUAXTHDbtnWHjZC25h0cdajHuPE1zxb4DVmu8crfh+HwH/WMuyLG0nHBg==", 246 | "requires": { 247 | "minipass": "^2.2.1" 248 | } 249 | }, 250 | "mkdirp": { 251 | "version": "0.5.1", 252 | "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", 253 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", 254 | "requires": { 255 | "minimist": "0.0.8" 256 | } 257 | }, 258 | "ms": { 259 | "version": "0.7.1", 260 | "resolved": "http://registry.npmjs.org/ms/-/ms-0.7.1.tgz", 261 | "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=" 262 | }, 263 | "nan": { 264 | "version": "2.11.1", 265 | "resolved": "https://registry.npmjs.org/nan/-/nan-2.11.1.tgz", 266 | "integrity": "sha512-iji6k87OSXa0CcrLl9z+ZiYSuR2o+c0bGuNmXdrhTQTakxytAFsC56SArGYoiHlJlFoHSnvmhpceZJaXkVuOtA==", 267 | "optional": true 268 | }, 269 | "napi-thread-safe-callback": { 270 | "version": "0.0.6", 271 | "resolved": "https://registry.npmjs.org/napi-thread-safe-callback/-/napi-thread-safe-callback-0.0.6.tgz", 272 | "integrity": "sha512-X7uHCOCdY4u0yamDxDrv3jF2NtYc8A1nvPzBQgvpoSX+WB3jAe2cVNsY448V1ucq7Whf9Wdy02HEUoLW5rJKWg==" 273 | }, 274 | "needle": { 275 | "version": "2.2.4", 276 | "resolved": "https://registry.npmjs.org/needle/-/needle-2.2.4.tgz", 277 | "integrity": "sha512-HyoqEb4wr/rsoaIDfTH2aVL9nWtQqba2/HvMv+++m8u0dz808MaagKILxtfeSN7QU7nvbQ79zk3vYOJp9zsNEA==", 278 | "requires": { 279 | "debug": "^2.1.2", 280 | "iconv-lite": "^0.4.4", 281 | "sax": "^1.2.4" 282 | } 283 | }, 284 | "nice-try": { 285 | "version": "1.0.5", 286 | "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", 287 | "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" 288 | }, 289 | "noble": { 290 | "version": "1.9.1", 291 | "resolved": "https://registry.npmjs.org/noble/-/noble-1.9.1.tgz", 292 | "integrity": "sha1-LM0x6tjsktv/bxmkLkILJYvNzdA=", 293 | "requires": { 294 | "bluetooth-hci-socket": "^0.5.1", 295 | "bplist-parser": "0.0.6", 296 | "debug": "~2.2.0", 297 | "xpc-connection": "~0.1.4" 298 | } 299 | }, 300 | "noble-mac": { 301 | "version": "github:Timeular/noble-mac#af4418ef5fb1ec97a662687f6a11b7f7cc59b2b4", 302 | "from": "github:Timeular/noble-mac", 303 | "requires": { 304 | "cross-spawn": "^6.0.5", 305 | "napi-thread-safe-callback": "0.0.6", 306 | "noble": "^1.9.1", 307 | "node-addon-api": "^1.1.0", 308 | "node-pre-gyp": "^0.10.0" 309 | } 310 | }, 311 | "node-addon-api": { 312 | "version": "1.6.0", 313 | "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.6.0.tgz", 314 | "integrity": "sha512-HEUPBHfdH4CLR1Qq4/Ek8GT/qFSvpApjJQmcYdLCL51ADU/Y11kMuFAdIevhNrPh3ylqVGA8k6vI/oi4YUAHbA==" 315 | }, 316 | "node-pre-gyp": { 317 | "version": "0.10.3", 318 | "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz", 319 | "integrity": "sha512-d1xFs+C/IPS8Id0qPTZ4bUT8wWryfR/OzzAFxweG+uLN85oPzyo2Iw6bVlLQ/JOdgNonXLCoRyqDzDWq4iw72A==", 320 | "requires": { 321 | "detect-libc": "^1.0.2", 322 | "mkdirp": "^0.5.1", 323 | "needle": "^2.2.1", 324 | "nopt": "^4.0.1", 325 | "npm-packlist": "^1.1.6", 326 | "npmlog": "^4.0.2", 327 | "rc": "^1.2.7", 328 | "rimraf": "^2.6.1", 329 | "semver": "^5.3.0", 330 | "tar": "^4" 331 | } 332 | }, 333 | "nopt": { 334 | "version": "4.0.1", 335 | "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", 336 | "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", 337 | "requires": { 338 | "abbrev": "1", 339 | "osenv": "^0.1.4" 340 | } 341 | }, 342 | "npm-bundled": { 343 | "version": "1.0.5", 344 | "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.5.tgz", 345 | "integrity": "sha512-m/e6jgWu8/v5niCUKQi9qQl8QdeEduFA96xHDDzFGqly0OOjI7c+60KM/2sppfnUU9JJagf+zs+yGhqSOFj71g==" 346 | }, 347 | "npm-packlist": { 348 | "version": "1.1.12", 349 | "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.1.12.tgz", 350 | "integrity": "sha512-WJKFOVMeAlsU/pjXuqVdzU0WfgtIBCupkEVwn+1Y0ERAbUfWw8R4GjgVbaKnUjRoD2FoQbHOCbOyT5Mbs9Lw4g==", 351 | "requires": { 352 | "ignore-walk": "^3.0.1", 353 | "npm-bundled": "^1.0.1" 354 | } 355 | }, 356 | "npmlog": { 357 | "version": "4.1.2", 358 | "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", 359 | "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", 360 | "requires": { 361 | "are-we-there-yet": "~1.1.2", 362 | "console-control-strings": "~1.1.0", 363 | "gauge": "~2.7.3", 364 | "set-blocking": "~2.0.0" 365 | } 366 | }, 367 | "number-is-nan": { 368 | "version": "1.0.1", 369 | "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", 370 | "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" 371 | }, 372 | "object-assign": { 373 | "version": "4.1.1", 374 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 375 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" 376 | }, 377 | "once": { 378 | "version": "1.4.0", 379 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 380 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 381 | "requires": { 382 | "wrappy": "1" 383 | } 384 | }, 385 | "os-homedir": { 386 | "version": "1.0.2", 387 | "resolved": "http://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", 388 | "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" 389 | }, 390 | "os-tmpdir": { 391 | "version": "1.0.2", 392 | "resolved": "http://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", 393 | "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" 394 | }, 395 | "osenv": { 396 | "version": "0.1.5", 397 | "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", 398 | "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", 399 | "requires": { 400 | "os-homedir": "^1.0.0", 401 | "os-tmpdir": "^1.0.0" 402 | } 403 | }, 404 | "path-is-absolute": { 405 | "version": "1.0.1", 406 | "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 407 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" 408 | }, 409 | "path-key": { 410 | "version": "2.0.1", 411 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", 412 | "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" 413 | }, 414 | "process-nextick-args": { 415 | "version": "2.0.0", 416 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", 417 | "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" 418 | }, 419 | "rc": { 420 | "version": "1.2.8", 421 | "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", 422 | "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", 423 | "requires": { 424 | "deep-extend": "^0.6.0", 425 | "ini": "~1.3.0", 426 | "minimist": "^1.2.0", 427 | "strip-json-comments": "~2.0.1" 428 | }, 429 | "dependencies": { 430 | "minimist": { 431 | "version": "1.2.0", 432 | "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", 433 | "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" 434 | } 435 | } 436 | }, 437 | "readable-stream": { 438 | "version": "2.3.6", 439 | "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", 440 | "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", 441 | "requires": { 442 | "core-util-is": "~1.0.0", 443 | "inherits": "~2.0.3", 444 | "isarray": "~1.0.0", 445 | "process-nextick-args": "~2.0.0", 446 | "safe-buffer": "~5.1.1", 447 | "string_decoder": "~1.1.1", 448 | "util-deprecate": "~1.0.1" 449 | } 450 | }, 451 | "rimraf": { 452 | "version": "2.6.2", 453 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", 454 | "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", 455 | "requires": { 456 | "glob": "^7.0.5" 457 | } 458 | }, 459 | "safe-buffer": { 460 | "version": "5.1.2", 461 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 462 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 463 | }, 464 | "safer-buffer": { 465 | "version": "2.1.2", 466 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 467 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 468 | }, 469 | "sax": { 470 | "version": "1.2.4", 471 | "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", 472 | "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" 473 | }, 474 | "semver": { 475 | "version": "5.6.0", 476 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", 477 | "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==" 478 | }, 479 | "set-blocking": { 480 | "version": "2.0.0", 481 | "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", 482 | "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" 483 | }, 484 | "shebang-command": { 485 | "version": "1.2.0", 486 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", 487 | "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", 488 | "requires": { 489 | "shebang-regex": "^1.0.0" 490 | } 491 | }, 492 | "shebang-regex": { 493 | "version": "1.0.0", 494 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", 495 | "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" 496 | }, 497 | "signal-exit": { 498 | "version": "3.0.2", 499 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", 500 | "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" 501 | }, 502 | "string-width": { 503 | "version": "1.0.2", 504 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", 505 | "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", 506 | "requires": { 507 | "code-point-at": "^1.0.0", 508 | "is-fullwidth-code-point": "^1.0.0", 509 | "strip-ansi": "^3.0.0" 510 | } 511 | }, 512 | "string_decoder": { 513 | "version": "1.1.1", 514 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", 515 | "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", 516 | "requires": { 517 | "safe-buffer": "~5.1.0" 518 | } 519 | }, 520 | "strip-ansi": { 521 | "version": "3.0.1", 522 | "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", 523 | "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", 524 | "requires": { 525 | "ansi-regex": "^2.0.0" 526 | } 527 | }, 528 | "strip-json-comments": { 529 | "version": "2.0.1", 530 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", 531 | "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" 532 | }, 533 | "tar": { 534 | "version": "4.4.8", 535 | "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.8.tgz", 536 | "integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==", 537 | "requires": { 538 | "chownr": "^1.1.1", 539 | "fs-minipass": "^1.2.5", 540 | "minipass": "^2.3.4", 541 | "minizlib": "^1.1.1", 542 | "mkdirp": "^0.5.0", 543 | "safe-buffer": "^5.1.2", 544 | "yallist": "^3.0.2" 545 | } 546 | }, 547 | "usb": { 548 | "version": "1.5.0", 549 | "resolved": "https://registry.npmjs.org/usb/-/usb-1.5.0.tgz", 550 | "integrity": "sha512-/0stiQEmweuO2BKv2avzQQ8ypDUjo4Osz5sSEi+d0F4Rc+ddX1xED3uf4Tkelc1eADlfn0JQZYHP0bI7CNDA0Q==", 551 | "optional": true, 552 | "requires": { 553 | "nan": "^2.8.0", 554 | "node-pre-gyp": "^0.11.0" 555 | }, 556 | "dependencies": { 557 | "node-pre-gyp": { 558 | "version": "0.11.0", 559 | "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz", 560 | "integrity": "sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q==", 561 | "optional": true, 562 | "requires": { 563 | "detect-libc": "^1.0.2", 564 | "mkdirp": "^0.5.1", 565 | "needle": "^2.2.1", 566 | "nopt": "^4.0.1", 567 | "npm-packlist": "^1.1.6", 568 | "npmlog": "^4.0.2", 569 | "rc": "^1.2.7", 570 | "rimraf": "^2.6.1", 571 | "semver": "^5.3.0", 572 | "tar": "^4" 573 | } 574 | } 575 | } 576 | }, 577 | "util-deprecate": { 578 | "version": "1.0.2", 579 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 580 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" 581 | }, 582 | "which": { 583 | "version": "1.3.1", 584 | "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", 585 | "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", 586 | "requires": { 587 | "isexe": "^2.0.0" 588 | } 589 | }, 590 | "wide-align": { 591 | "version": "1.1.3", 592 | "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", 593 | "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", 594 | "requires": { 595 | "string-width": "^1.0.2 || 2" 596 | } 597 | }, 598 | "wrappy": { 599 | "version": "1.0.2", 600 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 601 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" 602 | }, 603 | "xpc-connection": { 604 | "version": "0.1.4", 605 | "resolved": "https://registry.npmjs.org/xpc-connection/-/xpc-connection-0.1.4.tgz", 606 | "integrity": "sha1-3Nf6oq7Gt6bhjMXdrQQvejTHcVY=", 607 | "optional": true, 608 | "requires": { 609 | "nan": "^2.0.5" 610 | } 611 | }, 612 | "yallist": { 613 | "version": "3.0.2", 614 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz", 615 | "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=" 616 | } 617 | } 618 | } 619 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-ble-hr", 3 | "private": true, 4 | "dependencies": { 5 | "noble-mac": "Timeular/noble-mac" 6 | /*modify to: "noble": "^1.7.0" for non-mac*/ 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Receive heart rate and RRI via BLE HR using Node 2 | 3 | This simple example uses [Noble-mac](https://github.com/Timeular/noble-mac) (or with simple documented modification [Noble](https://github.com/sandeepmistry/noble)) to listen to heart rate and beat-to-beat interval (aka RRI or IBI) data transimitted from any device which speaks the [Bluetooth Heart Rate Service](https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.service.heart_rate.xml&u=org.bluetooth.service.heart_rate.xml) protocol. A list of such devices currently includes: 4 | 5 | - Polar H10 6 | - Polar H7 7 | - Wahoo TICKR(X) 8 | - Zephyr HxM (BT4.0 version only) 9 | - 4iiii Viiiiva 10 | 11 | and several others (see Notes below) 12 | 13 | ## Overview 14 | 15 | `index.js` is pretty short and thoroughly commented. The code is very basic: it just finds the first bluetooth peripheral transmitting the bluetooth heart rate service and listens for and parses out the measurement data. 16 | 17 | ## How to use this 18 | 19 | - [Install node](https://nodejs.org/en/download/) 20 | - Clone this repo 21 | - In the root directory, run `npm install` to install noble 22 | - Put on device and run `node index.js` 23 | - After a few seconds, you should see RRI data printed to the terminal 24 | 25 | ## Notes 26 | I searched for a good Python-based solution (in late 2018) and found nothing suitable. This was the best option available and plays nice with Python via the `subprocess` module and pipe of stdout. (see [here](https://stackoverflow.com/a/52940833/695804) for some example code). 27 | 28 | Based on [node-h7-hr](https://github.com/jakelear/node-h7-hr), which pulls HR only and uses the original noble, now dysfunctional on macOS. Took some time to figure out how the bytes were laid out for RRI, but I've documented it. 29 | 30 | After upgrading macOS and Node versions, I had trouble getting things to work anymore. However in the end there was a relatively simple solution, using the [noble-mac](https://github.com/Timeular/noble-mac) package instead of noble, which is more stable on macOS between versions. Those without macOS can make the simple documented changes in `index.js` and `package.json` to use [noble](https://github.com/sandeepmistry/noble) instead. 31 | 32 | If you have trouble with Node versions, unfortunately, this can be a wormwhole. However, I can recommend the [node-reinstall](https://github.com/brock/node-reinstall/) package for both re-installing and installing first time. Also: run commands without `sudo`. 33 | 34 | I have tested with the H7 and the Tickr. This _should_ function with other Bluetooth heart rate sensors that broadcast the Heart Rate (180d) service. This does not include Fitbit or Apple products, which are more locked-down on the data side unfortunately. 35 | 36 | Example lists of devices this should be compatible with (mostly HR straps) can be found at [EliteHRV](https://elitehrv.com/compatible-devices) and [Sweetwater](http://www.sweetwaterhrv.com/healthsensors.shtml) 37 | 38 | ## Resources 39 | 40 | - [Bluetooth Heart Rate Measurement Spec](https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.characteristic.heart_rate_measurement.xml) 41 | - [Polar developer information for H7,H10 etc.](https://developer.polar.com/wiki/H6,_H7_and_H10_Heart_rate_sensors) (including the important information about units for RRI data!) 42 | - If you are interested in accurate R-R interval data (for HRV, for example) be careful which devices you use. Most optical devices (e.g. wrist-worn) provide insufficiently accurate RRI data. ECG is generally a better method for this purpose. See [Marco Altini's great writeup](https://www.hrv4training.com/blog/hardware-for-hrv-what-sensor-should-you-use) for more nuanced analysis and discussion. 43 | 44 | --------------------------------------------------------------------------------