fitbit-livedata
Version:
This project aims to getting `livedata` from Fitbit tracker
354 lines (311 loc) • 17.1 kB
JavaScript
;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;