UNPKG

fitbit-livedata

Version:

This project aims to getting `livedata` from Fitbit tracker

354 lines (311 loc) 17.1 kB
'use strict';Object.defineProperty(exports, "__esModule", { value: true });exports.Tracker = undefined;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;};}();var _events = require('events');var _events2 = _interopRequireDefault(_events); var _child_process = require('child_process'); var _path = require('path');var _path2 = _interopRequireDefault(_path); var _axios = require('axios');var _axios2 = _interopRequireDefault(_axios); var _debug = require('debug');var _debug2 = _interopRequireDefault(_debug); var _generateBtleCredentials = require('./generateBtleCredentials');var _generateBtleCredentials2 = _interopRequireDefault(_generateBtleCredentials); var _server = require('./gatt/server');var _server2 = _interopRequireDefault(_server);function _interopRequireDefault(obj) {return obj && obj.__esModule ? obj : { default: obj };}function _classCallCheck(instance, Constructor) {if (!(instance instanceof Constructor)) {throw new TypeError("Cannot call a class as a function");}}function _possibleConstructorReturn(self, call) {if (!self) {throw new ReferenceError("this hasn't been initialised - super() hasn't been called");}return call && (typeof call === "object" || typeof call === "function") ? call : self;}function _inherits(subClass, superClass) {if (typeof superClass !== "function" && superClass !== null) {throw new TypeError("Super expression must either be null or a function, not " + typeof superClass);}subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } });if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;} _axios2.default.defaults.baseURL = 'https://android-cdn-api.fitbit.com'; var execAsync = function execAsync(cmd) {return new Promise(function (resolve, reject) { (0, _child_process.exec)(cmd, function (err, stdout, stderr) { if (err) reject(err);else resolve({ stdout: stdout, stderr: stderr }); }); });}; var UUID_CHARACTERISTIC_READ_DATA = 'adabfb01-6e7d-4601-bda2-bffaa68956ba'; var UUID_CHARACTERISTIC_WRITE_DATA = 'adabfb02-6e7d-4601-bda2-bffaa68956ba'; var UUID_CHARACTERISTIC_LIVE_DATA = '558dfa01-4fa8-4105-9f02-4eaa93e62980'; var connectAsync = function connectAsync(peripheral) {return new Promise(function (resolve, reject) { peripheral.connect(function (err) { if (err) reject(err);else resolve(); }); });}; var discoverSomeServicesAndCharacteristicsAsync = function discoverSomeServicesAndCharacteristicsAsync(p, sUUIDs, cUUIDs) {return new Promise(function (res, rej) { p.discoverSomeServicesAndCharacteristics(sUUIDs, cUUIDs, function (err, s, c) { if (err) rej(err);else res({ services: s, characteristics: c }); }); });}; var discoverAllServicesAndCharacteristicsAsync = function discoverAllServicesAndCharacteristicsAsync(p) {return discoverSomeServicesAndCharacteristicsAsync(p, [], []);}; var discoverDescriptorsAsync = function discoverDescriptorsAsync(characteristic) {return new Promise(function (resolve, reject) { characteristic.discoverDescriptors(function (err, descriptor) { if (err) reject(err);else resolve(descriptor); }); });}; var subscribeAsync = function subscribeAsync(characteristic) {return new Promise(function (resolve) { characteristic.subscribe(); resolve(); });}; var unsubscribeAsync = function unsubscribeAsync(characteristic) {return new Promise(function (resolve) { characteristic.unsubscribe(); resolve(); });}; var writeData = function writeData(reqCh, reqData, resCh, cond) {return new Promise(function (resolve) { var onNotify = function onNotify(responseData) { (0, _debug2.default)('fitbit-livedata')('HOST <-- ' + this.uuid + ' \'' + responseData.toString('hex') + '\''); if (cond(responseData)) resolve(responseData);else this.once('data', onNotify.bind(this)); }; resCh.once('data', onNotify.bind(resCh)); reqCh.write(reqData, true, function () { (0, _debug2.default)('fitbit-livedata')('HOST --> ' + reqCh.uuid + ' \'' + reqData.toString('hex') + '\''); }); });}; var arrange4Bytes = function arrange4Bytes(bytes, fr) {return ( bytes[fr + 3] * 16777216 + bytes[fr + 2] * 65536 + bytes[fr + 1] * 256 + bytes[fr]);}; var arrange2Bytes = function arrange2Bytes(bytes, fr) {return ( bytes[fr + 1] * 256 + bytes[fr]);}; var gattServer = new _server2.default();var Tracker = exports.Tracker = function (_EventEmitter) {_inherits(Tracker, _EventEmitter); function Tracker(peripheral, params) {_classCallCheck(this, Tracker);var _this = _possibleConstructorReturn(this, (Tracker.__proto__ || Object.getPrototypeOf(Tracker)).call(this)); _this.peripheral = peripheral; _this.params = params; _this.status = 'disconnected'; _this.peripheral.on('disconnect', function () { _this.emit('disconnected'); });return _this; }_createClass(Tracker, [{ key: 'disconnect', value: function disconnect() {var _this2 = this; return new Promise(function (resolve) { if (_this2.status !== 'disconnected') { _this2.peripheral.removeAllListeners('disconnect'); _this2.peripheral.once('disconnect', function () { resolve(); _this2.status = 'disconnected'; _this2.emit('disconnected'); }); _this2.peripheral.disconnect(); } else { _this2.emit('disconnected'); resolve(); } }); } }, { key: 'connect', value: function connect() {var _this3 = this; return connectAsync(this.peripheral). then(function () { _this3.emit('connecting'); _this3.status = 'connecting'; return discoverAllServicesAndCharacteristicsAsync(_this3.peripheral); }). then(function (data) { (0, _debug2.default)('fitbit-livedata')(data.characteristics.length + ' characteristics are found'); return Promise.all(data.characteristics.map(function (ch) {return discoverDescriptorsAsync(ch);})). then(function () {return data.characteristics;}); }). then(function (chs) { var control = chs.filter(function (ch) {return ch.uuid === UUID_CHARACTERISTIC_READ_DATA.replace(/-/g, '');})[0]; var live = chs.filter(function (ch) {return ch.uuid === UUID_CHARACTERISTIC_LIVE_DATA.replace(/-/g, '');})[0]; var writable = chs.filter(function (ch) {return ch.uuid === UUID_CHARACTERISTIC_WRITE_DATA.replace(/-/g, '');})[0]; return subscribeAsync(control). then(function () { // send message 'OPEN_SESSION' _this3.emit('openingSession'); return writeData( writable, Buffer.from([ 0xc0, 0x0a, 0x0a, 0x00, 0x08, 0x00, 0x10, 0x00, 0x00, 0x00, 0xc8, 0x00, 0x01]), control, function (data) {return data.length === 14;}); }). then(function () { // send message 'Command.AUTH_TRACKER' _this3.emit('authenticating'); var dataSent = [0xc0, 0x50]; var getRandomInt = function getRandomInt(mi, mx) {return Math.floor(Math.random() * (mx - mi + 1)) + mi;};var nonce = _this3.params.auth.btleClientAuthCredentials.nonce; dataSent.push(getRandomInt(0x00, 0xff)); dataSent.push(getRandomInt(0x00, 0xff)); dataSent.push(getRandomInt(0x00, 0xff)); dataSent.push(getRandomInt(0x00, 0xff)); dataSent.push(nonce % 256); dataSent.push(Math.floor(nonce / 256) % 256); dataSent.push(Math.floor(nonce / 65536) % 256); dataSent.push(Math.floor(nonce / 16777216) % 256); return writeData( writable, Buffer.from(dataSent), control, function (data) {return data.length === 14;}); }). then(function (dataRcv) {var authSubKey = _this3.params.auth.btleClientAuthCredentials.authSubKey; var authType = _this3.params.auth.type || ''; var binPath = _path2.default.join(__dirname, '../../bin'); return execAsync('java -cp "' + binPath + '/*" Main ' + dataRcv.toString('hex') + ' ' + authSubKey + ' ' + authType). then(function (res) { var bytes = res.stdout.match(/.{2}/g).map(function (seg) {return parseInt(seg, 16);}); // send message 'Command.SEND_AUTH' _this3.emit('sendAuth'); return writeData( writable, Buffer.from(bytes), control, function (data) {return data.length === 2;}); }); }). then(function () { // send message 'CLOSE_SESSION' _this3.emit('authenticated'); return writeData( writable, Buffer.from([0xc0, 0x01]), control, function (data) {return data.length === 2;}); }). then(function () {return unsubscribeAsync(control);}). then(function () {return subscribeAsync(live);}). then(function () { _this3.emit('connected'); _this3.status = 'connected'; live.on('read', function (data) { // 8cbca859 e70d0000 0c572600 8103 3c00 1400 4d 02 // time steps distanse calories elevation veryActive heartRate heartrate var time = new Date(arrange4Bytes(data, 0) * 1000); var steps = arrange4Bytes(data, 4); var distance = arrange4Bytes(data, 8); var calories = arrange2Bytes(data, 12); var elevation = arrange2Bytes(data, 14) / 10; var veryActive = arrange2Bytes(data, 16); var heartRate = data[18] ? data[18] % 256 : 0; _this3.emit('data', { device: { name: _this3.params.name, address: _this3.params.address, serialNumber: _this3.params.serialNumber }, livedata: { time: time, steps: steps, distance: distance, calories: calories, elevation: elevation, veryActive: veryActive, heartRate: heartRate } }); (0, _debug2.default)('livedata')(' params : ' + _this3.params); (0, _debug2.default)('livedata')(' time : ' + time); (0, _debug2.default)('livedata')(' steps : ' + steps); (0, _debug2.default)('livedata')(' distanse : ' + distance); (0, _debug2.default)('livedata')(' calories : ' + calories); (0, _debug2.default)('livedata')(' elevation : ' + elevation); (0, _debug2.default)('livedata')(' veryActive : ' + veryActive); (0, _debug2.default)('livedata')(' heartRate : ' + heartRate); }); }); }); } }]);return Tracker;}(_events2.default);var FitbitLiveData = function (_EventEmitter2) {_inherits(FitbitLiveData, _EventEmitter2); function FitbitLiveData() {_classCallCheck(this, FitbitLiveData);var _this4 = _possibleConstructorReturn(this, (FitbitLiveData.__proto__ || Object.getPrototypeOf(FitbitLiveData)).call(this)); _this4.trackers = []; _this4.isScanning = false; _this4.noble = null;return _this4; }_createClass(FitbitLiveData, [{ key: 'getTrackers', value: function getTrackers( authinfo) { return new Promise(function (resolve, reject) { (0, _generateBtleCredentials2.default)(authinfo). then(function (trackers) { (0, _debug2.default)('fitbit-livedata')('login succeeded'); resolve(trackers); }). catch(function (err) { (0, _debug2.default)('fitbit-livedata')(err.response.data.errors.map(function (e) {return e.message;}).join('\n')); reject(err); }); }); } }, { key: 'disconnectAllTrackers', value: function disconnectAllTrackers() {var _this5 = this; return this.trackers.reduce( function (prev, curr) {return ( prev.then(function () {return ( new Promise(function (res) { curr.disconnect(). then(function () {return res;}). catch(function () {return res;}); }));}));}, Promise.resolve()). then(function () { _this5.emit('disconnectAll'); if (_this5.noble && _this5.isScanning) { (0, _debug2.default)('fitbit-livedata')('stop scanning.'); _this5.noble.stopScanning(); _this5.isScanning = false; if (process.platform === 'win32') _this5.emit('scanStart'); } }); } }, { key: 'scanTrackers', value: function scanTrackers( trackers) {var _this6 = this; var addresses = this.trackers.map(function (i) {return i.address.toLowerCase();}); trackers.forEach(function (t) { if (addresses.indexOf(t.address.toLowerCase()) < 0) _this6.trackers.push(t); }); if (this.trackers.length === 0) { this.emit('error', 'no available trackers'); return; } Promise.resolve().then(function () {return require('' + (process.platform === 'win32' ? 'noble-uwp' : 'noble'));}).then(function (noble) { if (!_this6.noble) _this6.noble = noble; if (_this6.noble.listenerCount('discover') === 0) { _this6.noble.on('scanStart', function () { _this6.emit('scanStart'); }); _this6.noble.on('scanStop', function () { _this6.emit('scanStop'); }); _this6.noble.on('discover', function (p) { if (p.address === 'unknown') return; var target = _this6.trackers.filter(function (i) {return ( i.address.toLowerCase() === p.address.toLowerCase());}); if (target.length === 1) { (0, _debug2.default)('tracker')('\'' + p.address + '\' is discovered'); var t = new Tracker(p, target[0]); target[0].tracker = t; _this6.emit('discover', t); if (_this6.trackers.filter(function (i) {return !i.tracker;}).length === 0 && _this6.isScanning) { (0, _debug2.default)('fitbit-livedata')('stop scanning.'); _this6.noble.stopScanning(); _this6.isScanning = false; if (process.platform === 'win32') _this6.emit('scanStop'); } } }); } if (gattServer.listenerCount('listen') === 0) { var listen = function listen() { if (_this6.noble.state === 'poweredOn') { if (_this6.trackers.filter(function (t) {return !t.tracker || t.status !== 'connected';}).length > 0 && !_this6.isScanning) { (0, _debug2.default)('fitbit-livedata')('already powered on.'); _this6.noble.startScanning(); _this6.isScanning = true; if (process.platform === 'win32') _this6.emit('scanStart'); } } else { _this6.noble.on('stateChange', function (state) { if (state === 'poweredOn') { if (_this6.trackers.filter(function (t) {return !t.tracker || t.status !== 'connected';}).length > 0 && !_this6.isScanning) { (0, _debug2.default)('fitbit-livedata')('start scanning...'); _this6.noble.startScanning(); _this6.isScanning = true; if (process.platform === 'win32') _this6.emit('scanStart'); } } else { (0, _debug2.default)('fitbit-livedata')('stop scanning.'); _this6.noble.stopScanning(); _this6.isScanning = false; if (process.platform === 'win32') _this6.emit('scanStop'); } }); } }; gattServer.on('listen', listen); gattServer.on('error', function (error) { process.stderr.write(error + '\n'); listen(); }); } gattServer.listen(); }); } }, { key: 'trackerStatus', get: function get() {return this.trackers.map(function (tracker) {var name = tracker.name,address = tracker.address,serialNumber = tracker.serialNumber;return { name: name, address: address, serialNumber: serialNumber, status: tracker.tracker.status };});} }]);return FitbitLiveData;}(_events2.default);exports.default = FitbitLiveData;