diff --git a/.babelrc b/.babelrc index c31d5a0af4..fd37f72076 100644 --- a/.babelrc +++ b/.babelrc @@ -4,6 +4,7 @@ "stage-0", "react" ], + "retainLines": true, "plugins": ["add-module-exports"], "env": { "production": { @@ -19,7 +20,13 @@ }, "test": { "plugins": [ - ["resolver", { "resolveDirs": [ "app/node_modules" ]}], + [ "module-resolver", { + "root": ["./app/node_modules"], + "alias": { + "node-hid": "./app/node_modules/node-hid", + "serialport": "./app/node_modules/serialport" + } + } ], ["webpack-loaders", { "config": "webpack.config.test.js", "verbose": false }], "babel-plugin-rewire", ["transform-define", { diff --git a/.eslintignore b/.eslintignore index 5c81f84c22..c44e35bdc7 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,3 +1,4 @@ app/dist/ app/main.js node_modules +dist/ \ No newline at end of file diff --git a/.gitignore b/.gitignore index fb430899a8..bbb7eb2362 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,5 @@ coverage _book/ web/ + +\.vscode/ diff --git a/app/reducers/devices.js b/app/reducers/devices.js index 3392982227..e4fc099497 100644 --- a/app/reducers/devices.js +++ b/app/reducers/devices.js @@ -66,6 +66,14 @@ const devices = { source: {type: 'device', driverId: 'AbbottFreeStyleFreedomLite'}, enabled: {mac: false, win: true} }, + abbottfreestylelibre: { + instructions: 'Plug in meter with micro-USB', + key: 'abbottfreestylelibre', + name: 'Abbott FreeStyle Libre', + showDriverLink: {linux: false, mac: false, win: false}, + source: {type: 'device', driverId: 'AbbottFreeStyleLibre'}, + enabled: {linux: true, mac: true, win: true} + }, bayercontournext: { instructions: 'Plug in meter with micro-USB', key: 'bayercontournext', diff --git a/docs/checklists/README.md b/docs/checklists/README.md index e863f5a1fb..d3d208734f 100644 --- a/docs/checklists/README.md +++ b/docs/checklists/README.md @@ -1,6 +1,7 @@ Checklists for the implementation of drivers for reading data from diabetes devices currently supported or in development. - * [Abbott FreeStyle (BGM)](abbottFreeStyleLite.md) + * [Abbott FreeStyle Lite & Freedom Lite (BGM)](abbottFreeStyleLite.md) + * [Abbott FreeStyle Libre (CGM & BGM data)](abbottFreeStyleLibre.md) * [Abbott Precision Xtra (blood glucose & ketone meter)](abbottPrecisionXtra.md) * [Animas Vibe (CGM data)](animasCGM.md) * [Animas Ping and Vibe Insulin Pumps](animasPingAndVibe.md) @@ -11,4 +12,4 @@ Checklists for the implementation of drivers for reading data from diabetes devi * [Insulet OmniPod Insulin Delivery System](insuletOmniPod.md) * [OneTouch VerioIQ (BGM)](oneTouchVerioIQ.md) * [Tandem Insulin Pumps](tandem.md) - * [Tandem G4 (CGM data)](tandemCGM.md) \ No newline at end of file + * [Tandem G4 (CGM data)](tandemCGM.md) diff --git a/docs/checklists/abbottFreeStyleLibre.md b/docs/checklists/abbottFreeStyleLibre.md new file mode 100644 index 0000000000..65da87d8a8 --- /dev/null +++ b/docs/checklists/abbottFreeStyleLibre.md @@ -0,0 +1,147 @@ +## Checklist for CGM Implementation + +(Key: + + - `[x]` available in data protocol/documented in spec and implemented + - `[-]` available in data protocol/documented in spec but *not* yet implemented + - `[?]` unknown whether available in data protocol/documented in spec; *not* yet implemented + - `*[ ]` TODO: needs implementation! + - `[ ]` unavailable in data protocol and/or not documented in spec and not yet implemented) + +### Required if Present + +#### CBG + + - `[x]` cbg values + - `[ ]` units of cbg values (read from device, not hard-coded) + - `[x]` out-of-range values (LO or HI) + - `[x]` out-of-range value thresholds (e.g., often 40 for low and 400 for high on CGMs) + +Device-specific? (Add any device-specific notes/additions here.) + - internal glucose unit is always mg/dL for this device, independent of display unit + - out-of-range thresholds are 41 mg/dL and 499 mg/dL + - out-of-range measurements are reported as values 40 or 500 respectively + +#### Device Events + - `[ ]` calibrations + - `[ ]` calibration value + - `[ ]` units of calibration value (read from device, not hard-coded) + - `[x]` time changes (presence of which is also in the [BtUTC section](#bootstrapping-to-utc) below) + - `[x]` device display time `from` (before change) and `to` (result of change) + - `[x]` agent of change (`automatic` or `manual`) + - `[ ]` timezone + - `[ ]` reason for change (read from device) + +Device-specific? (Add any device-specific notes/additions here.) + - device does not need calibration + +#### Settings + + - `[x]` units preference for BG display + - `[x]` units of data being uploaded (will be mutated to mmol/L storage units if not mmol/L) + - `[x]` transmitter ID + - `[ ]` low alert settings + - `[ ]` enabled + - `[ ]` level/threshold + - `[ ]` snooze threshold + - `[ ]` high alert settings + - `[ ]` enabled + - `[ ]` level/threshold + - `[ ]` snooze threshold + - `[ ]` rate-of-change alerts + - `[ ]` fall rate alert + - `[ ]` enabled + - `[ ]` rate threshold for alerting + - `[ ]` rise rate alert + - `[ ]` enabled + - `[ ]` rate threshold for alerting + - `[ ]` out-of-range alerts + - `[ ]` enabled + - `[ ]` snooze time between alerts + - `[ ]` predictive alerts + - `[ ]` low prediction + - `[ ]` enabled + - `[ ]` time sensitivity (minutes to predicted low for alerting) + - `[ ]` high prediction + - `[ ]` enabled + - `[ ]` time sensitivity (minutes to predicted high for alerting) + - `[ ]` calibration alerts/reminders + - `[ ]` pre-reminder + - `[ ]` overdue alert + +Settings history: + + - `[ ]` device stores all changes to settings OR + - `[x]` device only returns current settings at time of upload + +No Tidepool data model (yet): volume and/or vibrate mode of all alerts (can/should go in `payload`). + +Device-specific? (Add any device-specific notes/additions here.) + +#### "Bootstrapping" to UTC + + - `[x]` index + - `[ ]` UTC timestamp (*Hey, one can dream!*) OR + - `[x]` internal timestamp or persistent log index (across device communication sessions) to order all pump events (regardless of type), independent of device display time OR + - `[ ]` ephemeral log index (does not persist across device communication sessions) to order all pump events (regardless of type), independent of device display time + - `[x]` date & time settings changes + +Device-specific? (Add any device-specific notes/additions here.) + +### No Tidepool Data Model Yet + +> **NB:** You can and should add to this section if there are other data types documented in the device's data protocol specification but not part of Tidepool's data model (yet). + + - `[-]` activity/exercise + - `[-]` food (e.g., Dexcom allows logging carb events) + - `[-]` notes/other events + - `[-]` insulin (rapid acting, long term) + +### Tidepool ingestion API + +Choose one of the following: + + - `[x]` legacy "jellyfish" ingestion API + - `*[ ]` platform ingestion API + +### Known implementation issues/TODOs + +*Use this space to describe device-specific known issues or implementation TODOs **not** contained in the above datatype-specific sections.* + + +## Checklist for Blood Glucose Meter Implementation + +### Required if Present + +- `[x]` smbg values +- `[ ]` units of smbg values (read from device, not hard-coded) +- `[x]` out-of-range values (LO or HI) +- `[x]` out-of-range value thresholds (e.g., often 20 for low and 600 for high on BGMs) +- `[x]` date & time settings changes +- `[x]` blood ketone values +- `[ ]` units of blood ketone values (read from device, not hard-coded) +- `[x]` ketone out-of-range values +- `[x]` ketone out-of-range value thresholds + +Device-specific? (Add any device-specific notes/additions here.) + - internal glucose unit is always mg/dL for this device, independent of display unit + - glucose out-of-range thresholds are 41 mg/dL and 499 mg/dL + - glucose out-of-range measurements are reported as values 40 or 500 respectively + - ketone out-of-range upper threshold is 8.0 mmol/L + +### No Tidepool Data Model Yet + +- `[x]` control (solution) tests (whether marked in UI or auto-detected) - until we have a data model, these should be discarded +- `[-]` device settings, other than date & time (e.g., target blood glucose range) +- `[-]` tag/note (e.g., pre- vs. post-meal) + +### Tidepool ingestion API + +Choose one of the following: + + - `[x]` legacy "jellyfish" ingestion API + - `*[ ]` platform ingestion API + +### Known implementation issues/TODOs + +*Use this space to describe device-specific known issues or implementation TODOs **not** contained in the above datatype-specific sections.* diff --git a/docs/checklisttemplates/CGMChecklist.md b/docs/checklisttemplates/CGMChecklist.md index 07b448b776..aede368eef 100644 --- a/docs/checklisttemplates/CGMChecklist.md +++ b/docs/checklisttemplates/CGMChecklist.md @@ -101,8 +101,4 @@ Choose one of the following: ### Known implementation issues/TODOs -Add any device-specific known issues or implementation TODOs here in checklist format. - -### Known implementation issues/TODOs - *Use this space to describe device-specific known issues or implementation TODOs **not** contained in the above datatype-specific sections.* diff --git a/lib/core/device.js b/lib/core/device.js index 55bc226f06..06b03fb67d 100644 --- a/lib/core/device.js +++ b/lib/core/device.js @@ -37,6 +37,7 @@ var insuletOmniPod = require('../drivers/insulet/insuletDriver'); var oneTouchUltra2 = require('../drivers/onetouch/oneTouchUltra2'); var oneTouchVerioIQ = require('../drivers/onetouch/oneTouchVerioIQ'); var abbottFreeStyleLite = require('../drivers/abbott/abbottFreeStyleLite'); +var abbottFreeStyleLibre = require('../drivers/abbott/abbottFreeStyleLibre'); var bayerContourNext = require('../drivers/bayer/bayerContourNext'); var animasDriver = require('../drivers/animas/animasDriver'); var medtronicDriver = require('../drivers/medtronic/medtronicDriver'); @@ -61,6 +62,7 @@ device._deviceDrivers = { 'OneTouchVerioIQ': oneTouchVerioIQ, 'AbbottFreeStyleLite': abbottFreeStyleLite, 'AbbottFreeStyleFreedomLite': abbottFreeStyleLite, + 'AbbottFreeStyleLibre': abbottFreeStyleLibre, 'BayerContourNext': bayerContourNext, 'BayerContourNextUsb': bayerContourNext, 'BayerContourUsb': bayerContourNext, @@ -77,6 +79,7 @@ device._deviceComms = { 'OneTouchVerioIQ': serialDevice, 'AbbottFreeStyleLite': serialDevice, 'AbbottFreeStyleFreedomLite': serialDevice, + 'AbbottFreeStyleLibre': hidDevice, 'Tandem': serialDevice, 'BayerContourNext': hidDevice, 'BayerContourNextUsb': hidDevice, diff --git a/lib/drivers/abbott/.eslintrc b/lib/drivers/abbott/.eslintrc new file mode 100644 index 0000000000..769cd6b450 --- /dev/null +++ b/lib/drivers/abbott/.eslintrc @@ -0,0 +1,61 @@ +{ + "extends": "airbnb", + "parser": "babel-eslint", + "plugins": ["lodash"], + "parserOptions": { + "ecmaVersion": 6 + }, + "rules": { + "no-plusplus": [ + "error", + { + "allowForLoopAfterthoughts": true + } + ] + }, + "overrides": [ + { + "files": [ + "abbottFreeStyleLite.js", + "abbottPrecisionXtra.js" + ], + "rules": { + "func-names": "warn", + "no-var": "warn", + "vars-on-top": "warn", + "no-unused-vars": "warn", + "object-shorthand": "warn", + "comma-dangle": "warn", + "space-before-function-paren": "warn", + "no-param-reassign": "warn", + "prefer-template": "warn", + "no-useless-escape": "warn", + "keyword-spacing": "warn", + "indent": "warn", + "spaced-comment": "warn", + "eqeqeq": "warn", + "space-infix-ops": "warn", + "prefer-arrow-callback": "warn", + "no-shadow": "warn", + "array-bracket-spacing": "warn", + "no-use-before-define": "warn", + "no-else-return": "warn", + "no-bitwise": "warn", + "consistent-return": "warn", + "no-plusplus": "warn", + "no-continue": "warn", + "no-loop-func": "warn", + "object-curly-spacing": "warn", + "key-spacing": "warn", + "padded-blocks": "warn", + "no-console": "warn", + "no-multi-spaces": "warn", + "no-mixed-operators": "warn", + "max-len": "warn" + } + } + ], + "settings": { + "lodash": 3 + } +} \ No newline at end of file diff --git a/lib/drivers/abbott/abbottFreeStyleLibre.js b/lib/drivers/abbott/abbottFreeStyleLibre.js new file mode 100644 index 0000000000..0d2ecd1f42 --- /dev/null +++ b/lib/drivers/abbott/abbottFreeStyleLibre.js @@ -0,0 +1,207 @@ +/* + * == BSD2 LICENSE == + * Copyright (c) 2017, Tidepool Project + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the associated License, which is identical to the BSD 2-Clause + * License as published by the Open Source Initiative at opensource.org. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the License for more details. + * + * You should have received a copy of the License along with this program; if + * not, you can obtain one from Tidepool Project at tidepool.org. + * == BSD2 LICENSE == + */ + +import { clone, assign } from 'lodash'; +import async from 'async'; +import sundial from 'sundial'; + +import FreeStyleLibreProtocol from './freeStyleLibreProtocol'; +import FreeStyleLibreData from './freeStyleLibreData'; +import { DB_TABLE_ID, CFG_TABLE_ID, DEVICE_MODEL_NAME, COMPRESSION } from './freeStyleLibreConstants'; + +const isBrowser = typeof window !== 'undefined'; +// eslint-disable-next-line no-console +const debug = isBrowser ? require('bows')('FreeStyleLibreDriver') : console.log; + +export default function (config) { + const cfg = clone(config); + const hidDevice = config.deviceComms; + const protocol = new FreeStyleLibreProtocol(cfg); + const dataParser = new FreeStyleLibreData(cfg); + + return { + /* eslint no-param-reassign: + [ "error", { "props": true, "ignorePropertyModificationsFor": ["data"] } ] */ + detect(deviceInfo, cb) { + cb(null, deviceInfo); + }, + + setup(deviceInfo, progress, cb) { + progress(100); + cb(null, { deviceInfo }); + }, + + connect(progress, data, cb) { + hidDevice.connect(data.deviceInfo, FreeStyleLibreProtocol.probe, (err) => { + if (err) { + return cb(err); + } + return protocol.initCommunication(() => { + // ignore results of init as it seems not to be relevant to the following communication + data.disconnect = false; + progress(100); + return cb(null, data); + }); + }); + }, + + getConfigInfo(progress, data, cb) { + progress(0); + + const getterFunctions = [ + (callback) => { protocol.getSerialNumber(callback); }, + (callback) => { protocol.getFirmwareVersion(callback); }, + (callback) => { protocol.getDBRecordNumber(callback); }, + ]; + let counter = 0; + async.series(getterFunctions, (err, result) => { + counter += 1; + progress(100 * (counter / getterFunctions.length)); + + if (err) { + debug('getConfigInfo: ', err); + return cb(err, null); + } + data.connect = true; + result.forEach((element) => { + if (typeof element === 'object') { + debug('getConfigInfo: result object: ', element); + assign(data.deviceInfo, element); + } + return null; + }); + debug('getConfigInfo: data: ', data); + + return cb(null, data); + }); + }, + + fetchData(progress, data, cb) { + progress(0); + + const getterFunctions = [ + (callback) => { protocol.setCompression(COMPRESSION.ENABLED, callback); }, + (callback) => { protocol.getDbSchema(callback); }, + (callback) => { protocol.getCfgSchema(callback); }, + (callback) => { protocol.getDateTime(callback); }, + (callback) => { protocol.getCfgData(CFG_TABLE_ID.METER_FACTORY_CONFIGURATION, callback); }, + (callback) => { protocol.getCfgData(CFG_TABLE_ID.METER_SETTINGS, callback); }, + (callback) => { protocol.getCfgData(CFG_TABLE_ID.USER_PATIENT_CONFIGURATION, callback); }, + (callback) => { protocol.getCfgData(CFG_TABLE_ID.USER_PATIENT_SETTINGS, callback); }, + (callback) => { protocol.getCfgData(CFG_TABLE_ID.INSULIN_SETTINGS, callback); }, + (callback) => { protocol.getCfgData(CFG_TABLE_ID.REMINDER_STRING, callback); }, + (callback) => { protocol.getCfgData(CFG_TABLE_ID.SMART_TAG_NOTES, callback); }, + (callback) => { protocol.getCfgData(CFG_TABLE_ID.STORED_SENSOR_INFORMATION, callback); }, + (callback) => { protocol.getCfgData(CFG_TABLE_ID.REMINDER_DATA, callback); }, + (callback) => { protocol.getDatabase(DB_TABLE_ID.GLUCOSE_RESULT, callback); }, + (callback) => { protocol.getDatabase(DB_TABLE_ID.RAPID_ACTING_INSULIN, callback); }, + (callback) => { protocol.getDatabase(DB_TABLE_ID.HISTORICAL_DATA, callback); }, + (callback) => { protocol.getDatabase(DB_TABLE_ID.EVENT, callback); }, + ]; + + data.aapPackets = []; + let counter = 0; + async.series(getterFunctions, (err, results) => { + counter += 1; + progress(100 * (counter / getterFunctions.length)); + + if (err) { + debug('fetchData: error: ', err); + return cb(err, data); + } + results.forEach((aapPackets) => { + if (typeof aapPackets === 'object') { + data.aapPackets = data.aapPackets.concat(aapPackets); + } + }); + return cb(null, data); + }); + }, + + processData(progress, data, cb) { + debug('processData: num aapPackets:', data.aapPackets.length); + progress(0); + + data.deviceInfo.deviceId = `${data.deviceInfo.driverId}-${data.deviceInfo.serialNumber}`; + cfg.builder.setDefaults({ deviceId: data.deviceInfo.deviceId }); + + data.post_records = + dataParser.processAapPackets(data.aapPackets, data.deviceInfo.dbRecordNumber); + debug('processData: num post records:', data.post_records.length); + + progress(100); + data.processData = true; + return cb(null, data); + }, + + uploadData(progress, data, cb) { + debug('uploadData: num post records:', data.post_records.length); + progress(0); + + const sessionInfo = { + deviceTags: ['bgm', 'cgm'], + deviceManufacturers: ['Abbott'], + deviceModel: DEVICE_MODEL_NAME, + deviceSerialNumber: data.deviceInfo.serialNumber, + deviceId: data.deviceInfo.deviceId, + start: sundial.utcDateString(), + timeProcessing: cfg.tzoUtil.type, + tzName: cfg.timezone, + version: cfg.version, + }; + + cfg.api.upload.toPlatform(data.post_records, sessionInfo, progress, cfg.groupId, + (err, result) => { + progress(100); + + if (err) { + debug(err); + debug(result); + return cb(err, data); + } + data.cleanup = true; + return cb(null, data); + }); + }, + + disconnect(progress, data, cb) { + debug('disconnect'); + hidDevice.removeListeners(); + // Due to an upstream bug in HIDAPI on Windoze, we have to send a command + // to the device to ensure that the listeners are removed before we disconnect + // For more details, see https://github.com/node-hid/node-hid/issues/61 + hidDevice.send(FreeStyleLibreProtocol.buildHidPacket(0x00, ''), () => { + progress(100); + cb(null, data); + }); + }, + + cleanup(progress, data, cb) { + debug('cleanup'); + if (!data.disconnect) { + hidDevice.disconnect(data, () => { + progress(100); + data.cleanup = true; + data.disconnect = true; + cb(null, data); + }); + } else { + progress(100); + } + }, + }; +} diff --git a/lib/drivers/abbott/cli/fslibre.js b/lib/drivers/abbott/cli/fslibre.js new file mode 100755 index 0000000000..a3be050f52 --- /dev/null +++ b/lib/drivers/abbott/cli/fslibre.js @@ -0,0 +1,168 @@ +#!/usr/bin/env babel-node +/* eslint-disable no-console,no-use-before-define */ + +import program from 'commander'; +import fs from 'fs'; +import async from 'async'; + +import hidDevice from '../../../hidDevice'; +import api from '../../../core/api'; +import device from '../../../core/device'; +import config from '../../../../.config'; +import pkg from '../../../../package.json'; +import builder from '../../../objectBuilder'; +import abbottFreeStyleLibre from '../abbottFreeStyleLibre'; + +import stringify from './stringify'; + +// eslint-disable-next-line no-underscore-dangle +global.__DEBUG__ = true; + +const intro = 'FSLibre CLI:'; +let libreDriver; + +program + .version('0.0.1', null) + .option('-u, --username [user]', 'username') + .option('-p, --password [pw]', 'password') + .option('-t, --timezone [tz]', 'named timezone', config.DEFAULT_TIMEZONE) + .option('-f, --file [path]', 'load deviceInfo and aapPackets from JSON file instead of device') + .option('-o, --output [path]', 'save processed data to JSON file instead of uploading') + .parse(process.argv); + +const options = { + api, + timezone: program.timezone, + version: `${pkg.name} ${pkg.version}`, + builder: builder(), +}; + + +if ((program.username && program.password) || program.output) { + if (program.output) { + device.init(options, initCallback); + } else { + login(program.username, program.password, config); + } +} else { + program.help(); +} + +function login(username, password, cfg) { + console.log(intro, 'login:', cfg.API_URL); + api.create({ + apiUrl: cfg.API_URL, + uploadUrl: cfg.UPLOAD_URL, + dataUrl: cfg.DATA_URL, + version: 'uploader node CLI tool - fslibre', + }); + api.init(() => { + api.user.login({ username, password }, loginCallback); + }); +} + +function loginCallback(error, loginData) { + if (error) { + console.log(intro, 'loginCallback: Failed authentication!'); + console.log(error); + process.exit(); + } + console.log(intro, 'loginCallback:', 'Uploading using the timezone', program.timezone); + console.log(intro, 'loginCallback:', 'Uploading for user ', loginData.userid); + + console.log(intro, 'loginCallback:', 'Starting connection to device...'); + options.targetId = loginData.userid; + options.groupId = loginData.userid; + device.init(options, initCallback); +} + +function readDataFromFile() { + console.log(intro, 'Reading JSON data from:', program.file); + return JSON.parse(fs.readFileSync(program.file, 'utf8'), (k, v) => { + if (v !== null && typeof v === 'object' && 'type' in v && + v.type === 'Buffer' && 'data' in v && Array.isArray(v.data)) { + // re-create Buffer objects for data fields of aapPackets + return new Buffer(v.data); + } + return v; + }); +} + +function initCallback() { + if (program.file) { + const data = readDataFromFile(); + + console.log(intro, 'Processing AAP packets, length:', data.aapPackets.length); + libreDriver = abbottFreeStyleLibre(options); + libreDriver.processData(progress => progress, data, processCallback); + } else { + device.detect('AbbottFreeStyleLibre', options, detectCallback); + } +} + +function processCallback(error, data) { + if (error) { + console.log(intro, 'processCallback: Failed:', error); + process.exit(); + } + + console.log(intro, 'Num post records:', data.post_records.length); + + if (program.output) { + writeDataToFile(data, done); + } else { + libreDriver.uploadData(progress => progress, data, uploadCallback); + } +} + +function detectCallback(error, deviceInfo) { + if (deviceInfo !== undefined) { + console.log(intro, 'detectCallback:', 'deviceInfo: ', deviceInfo); + options.deviceInfo = deviceInfo; + if (program.output) { + copyDataFromDeviceToFile(deviceInfo); + } else { + device.upload('AbbottFreeStyleLibre', options, uploadCallback); + } + } else { + console.error(intro, 'detectCallback:', 'Could not find FreeStyle Libre device. Is it connected via USB?'); + console.error(intro, 'detectCallback:', `Error value: ${error}`); + } +} + +function copyDataFromDeviceToFile(deviceInfo) { + options.deviceComms = hidDevice(); + libreDriver = abbottFreeStyleLibre(options); + async.waterfall([ + libreDriver.setup.bind(libreDriver, deviceInfo, () => {}), + libreDriver.connect.bind(libreDriver, () => {}), + libreDriver.getConfigInfo.bind(libreDriver, () => {}), + libreDriver.fetchData.bind(libreDriver, () => {}), + libreDriver.processData.bind(libreDriver, () => {}), + // no call to the upload function here, since we only want to download the data from the device + libreDriver.disconnect.bind(libreDriver, () => {}), + ], (err, resultOptional) => { + const result = resultOptional || {}; + libreDriver.cleanup(() => {}, result, () => { + writeDataToFile(result, done); + }); + }); +} + +function uploadCallback(error) { + if (error) { + console.log(intro, 'uploadCallback:', 'error: ', error); + process.exit(); + } + done(); +} + +function writeDataToFile(data, callback) { + console.log(intro, 'uploadCallback:', 'writing data to file:', program.output); + fs.writeFile(program.output, stringify(data, { indent: 2, maxLevelPretty: 3 }), 'utf8', callback); +} + +function done() { + console.log(intro, 'Done!'); + process.exit(); +} diff --git a/lib/drivers/abbott/cli/stringify.js b/lib/drivers/abbott/cli/stringify.js new file mode 100644 index 0000000000..a70d7e040b --- /dev/null +++ b/lib/drivers/abbott/cli/stringify.js @@ -0,0 +1,81 @@ +// a wrapper around JSON.stringify to allow for more control over the formatting of the JSON + +export default function stringify(obj, optionsOptional) { + const stringOrChar = /("(?:[^\\"]|\\.)*")|[:,]/g; + + function prettify(string) { + return string.replace(stringOrChar, (match, str) => (str ? match : `${match} `)); + } + + function get(opt, name, defaultValue) { + return (name in opt ? opt[name] : defaultValue); + } + + const options = optionsOptional || {}; + const indent = JSON.stringify([1], null, get(options, 'indent', 2)).slice(2, -3); + const maxLength = (indent === '' ? Infinity : get(options, 'maxLength', 80)); + const maxLevelPretty = get(options, 'maxLevelPretty', Infinity); + + return (function _stringify(objectParam, currentIndent, reserved) { + let object = objectParam; + if (object && typeof object.toJSON === 'function') { + object = object.toJSON(); + } + + const string = JSON.stringify(object); + + if (string === undefined) { + return string; + } + + const currentLevel = currentIndent.length / indent.length; + if (currentLevel >= maxLevelPretty) { + return string; + } + + const length = maxLength - currentIndent.length - reserved; + + if (string.length <= length) { + const prettified = prettify(string); + if (prettified.length <= length) { + return prettified; + } + } + + if (typeof object === 'object' && object !== null) { + const nextIndent = currentIndent + indent; + const items = []; + let delimiters; + const comma = (array, index) => (index === array.length - 1 ? 0 : 1); + + if (Array.isArray(object)) { + for (let index = 0; index < object.length; index++) { + items.push( + _stringify(object[index], nextIndent, comma(object, index)) || 'null', + ); + } + delimiters = '[]'; + } else { + Object.keys(object).forEach((key, index, array) => { + const keyPart = `${JSON.stringify(key)}: `; + const value = _stringify(object[key], nextIndent, + keyPart.length + comma(array, index)); + if (value !== undefined) { + items.push(keyPart + value); + } + }); + delimiters = '{}'; + } + + if (items.length > 0) { + return [ + delimiters[0], + indent + items.join(`,\n${nextIndent}`), + delimiters[1], + ].join(`\n${currentIndent}`); + } + } + + return string; + }(obj, '', 0)); +} diff --git a/lib/drivers/abbott/freeStyleLibreConstants.js b/lib/drivers/abbott/freeStyleLibreConstants.js new file mode 100644 index 0000000000..01bcf5c618 --- /dev/null +++ b/lib/drivers/abbott/freeStyleLibreConstants.js @@ -0,0 +1,487 @@ +/* + * == BSD2 LICENSE == + * Copyright (c) 2017, Tidepool Project + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the associated License, which is identical to the BSD 2-Clause + * License as published by the Open Source Initiative at opensource.org. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the License for more details. + * + * You should have received a copy of the License along with this program; if + * not, you can obtain one from Tidepool Project at tidepool.org. + * == BSD2 LICENSE == + */ + +export const DEVICE_MODEL_NAME = 'FreeStyle Libre'; + +export const KETONE_VALUE_FACTOR = 18.0; // according to specs +export const KETONE_HI = 8.0; +export const KETONE_LO = null; // ketone value cannot be low + +export const GLUCOSE_HI = 500; +export const GLUCOSE_LO = 40; + +export const COMMAND = { + INIT_REQUEST_1: 0x04, + INIT_REQUEST_2: 0x05, + INIT_REQUEST_3: 0x15, + INIT_REQUEST_4: 0x01, + BINARY_REQUEST: 0x0a, + BINARY_RESPONSE: 0x0b, + ACK_FROM_DEVICE: 0x0c, + ACK_FROM_HOST: 0x0d, + TEXT_REQUEST: 0x21, + TEXT_RESPONSE: 0x60, +}; + +export const OP_CODE = { + GET_DATABASE: 0x31, + GET_DB_SCHEMA: 0x34, + COMPRESSED_DATABASE: 0x35, + GET_DATE_TIME: 0x41, + GET_CFG_DATA: 0x51, + GET_CFG_SCHEMA: 0x54, + SET_COMPRESSION: 0x60, + FLUSH_BUFFERS: 0x7d, + ERROR: 0x7e, +}; + +export const ERROR = { + OK: 0, + BAD_PARAMS: 1, + WRONG_PARAM_NUM: 2, + CMD_FAILED: 3, + CRC_ERROR: 4, + INVALID_ID: 5, +}; + +export const ERROR_DESCRIPTION = {}; +ERROR_DESCRIPTION[ERROR.OK] = 'OK: No Error.'; +ERROR_DESCRIPTION[ERROR.BAD_PARAMS] = 'Bad parameter values / out of range!'; +ERROR_DESCRIPTION[ERROR.WRONG_PARAM_NUM] = 'Wrong number of parameters!'; +ERROR_DESCRIPTION[ERROR.CMD_FAILED] = 'Command failed!'; +ERROR_DESCRIPTION[ERROR.CRC_ERROR] = 'Wrong checksum!'; +ERROR_DESCRIPTION[ERROR.INVALID_ID] = 'Wrong table ID!'; + +// for CFG and DB fields +export const FIELD_TYPE = { + UNSIGNED_DECIMAL: 0, // uint + UNSIGNED_HEX: 1, // uint (display as hex) + SIGNED_DECIMAL: 2, // int + UTF8: 3, // string + BULK_DATA: 4, // bytes + CRC32: 10, // uint32 + CRC16: 11, // uint16 +}; + +export const DB_TABLE_ID = { + GLUCOSE_RESULT: 0, + RAPID_ACTING_INSULIN: 1, + HISTORICAL_DATA: 6, + EVENT: 7, +}; + +export const DB_RECORD_TYPE = { + COMMON_HEADER: 255, + GLUCOSE_KETONE_SERVING: 0, + GLUCOSE_KETONE_MEAL: 1, + GLUCOSE_KETONE_CARBS: 2, + UNLOGGED_INSULIN: 3, + CONTROL_SOL_TEST: 4, + TIME_CHANGE_RESULT: 5, + INSULIN_CALC: 6, + INSULIN_MANUAL: 7, + HISTORICAL_DATA: 12, + RESULT_RECORD_WRAP: 13, + INSULIN_WRAP: 14, + HISTORICAL_WRAP: 17, + ERROR: 32, + LOW_BATTERY: 33, + DEAD_BATTERY: 34, + TIME_CHANGE: 35, + LOST_TIME: 36, + INSULIN_SETUP_1: 37, + INSULIN_SETUP_2: 38, + INSULIN_SETUP_3: 39, + INSULIN_SETUP_4: 40, + RESTORE_CONFIG: 41, + CLEAR_RESULT_DB: 42, + CLEAR__SCAN_DB: 43, + CLEAR_ACTIVATION_DB: 44, + CLEAR_HISTORICAL_DB: 45, + USER_TIME_CHANGE: 46, + CLEAR_EVENT_DB: 47, + RECOVERY_EVENT: 48, + MICROCONTROLLER_RESET_EVENT: 49, + MASKED_MODE_STATUS_EVENT: 50, + SENSOR_EXPIRED_EVENT: 51, + EVENT_DATABASE_RECORD_NUMBER_WRAP: 52, +}; + +export const DB_WRAP_RECORDS = {}; +DB_WRAP_RECORDS[DB_RECORD_TYPE.RESULT_RECORD_WRAP] = DB_TABLE_ID.GLUCOSE_RESULT; +DB_WRAP_RECORDS[DB_RECORD_TYPE.INSULIN_WRAP] = DB_TABLE_ID.RAPID_ACTING_INSULIN; +DB_WRAP_RECORDS[DB_RECORD_TYPE.HISTORICAL_WRAP] = DB_TABLE_ID.HISTORICAL_DATA; +DB_WRAP_RECORDS[DB_RECORD_TYPE.EVENT_DATABASE_RECORD_NUMBER_WRAP] = DB_TABLE_ID.EVENT; + +export const DB_FIELD_ID = { + RECORD_NUMBER: 0, + TIME_VALID: 7, + TYPE: 8, + READER_TIME: 9, + USER_TIME_OFFSET: 10, + RESULT: 20, + RESULT_STATUS: 61, + TEST_TYPE: 23, + SMART_TAGS: 24, + LONG_ACTING_INSULIN_RECORDED: 25, + RAPID_ACTING_INSULIN: 26, + MEDICATION: 28, + EXERCISE: 29, + TREND: 30, + RAI_RECORD_POINTER: 32, + RAI_DATA_FLAG: 33, + LONG_ACTING_INSULIN: 34, + GLYCEMIC_ALARM: 35, + GRAMS_PER_SERVING: 36, + SERVING_COUNT: 37, + MEAL_TYPE: 38, + GRAMS_OF_CARB: 39, + FOOD_DATA_FLAG: 40, + FOOD_QUICK_TAG: 41, + RESULT_OLD_READER_TIME: 42, + RESULT_OLD_USER_OFFSET: 43, + RESULT_OLD_VALID: 44, + DATA_CRC: 49, + SMART_TAG_0: 50, + SMART_TAG_1: 51, + SMART_TAG_2: 52, + SMART_TAG_3: 53, + SMART_TAG_4: 54, + SMART_TAG_5: 55, + SMART_TAG_CRC: 56, + RESULT_DATA_QUALITY_ERROR: 57, + RESULT_UNLOGGED_INSULIN: 58, + EFFECTIVE_TIME: 59, + WRAP_RECORD_NUMBER: 60, + RAI_RESULT: 70, + SIGNED_OVERRIDE: 71, + SIGNED_CORRECTION: 72, + IOB: 73, + MEAL_INSULIN: 74, + UNLOGGED_INSULIN: 75, + TIME_OFFSET: 76, + INSULIN_CRC: 77, + MANUAL_RAI_RESULT: 78, + INSULIN_RECORD_WARP: 79, + GLUCOSE: 150, + FIRST_FLAG: 151, + TIME_CHANGE: 152, + FOOD_FLAG: 153, + HISTORICAL_RAPID_ACTING_INSULIN: 154, + HISTORICAL_CRC: 155, + HISTORICAL_DATA_QUALITY_ERROR: 156, + LIFE_COUNTER: 157, + HISTORICAL_DATA_WRAP: 158, + ERROR_CODE: 160, + ERROR_DATA: 161, + OLD_READER_TIME: 165, + OLD_USER_OFFSET: 166, + VALID: 167, + RTC_COUNTER: 168, + HIGH_CORRECTION: 203, + LOW_CORRECTION: 204, + CORRECTION_FACTOR: 205, + CALCULATOR_OFF_FLAG: 237, + FIXED_DOSE_BREAKFAST: 206, + FIXED_DOSE_LUNCH: 207, + FIXED_DOSE_DINNER: 208, + MORNING_CARB_INSULIN: 209, + MID_DAY_CARB_INSULIN: 210, + EVENING_CARB_INSULIN: 211, + NIGHT_CARB_INSULIN: 212, + MORNING_UNITS: 213, + MID_DAY_UNITS: 214, + EVENING_UNITS: 215, + NIGH_UNITS: 216, + GRAMS_UNITS: 217, + CARBS: 218, + UNITS_TIME_OF_DAY_BLOCK: 219, + CARB_TIME_OF_DAY_BLOCK: 220, + MORNING_HIGH_CORRECTION: 221, + MORNING_LOW_CORRECTION: 222, + MID_DAY_HIGH_CORRECTION: 223, + MID_DAY_LOW_CORRECTION: 224, + EVENING_HIGH_CORRECTION: 225, + EVENING_LOW_CORRECTION: 226, + NIGHT_HIGH_CORRECTION: 227, + NIGHT_LOW_CORRECTION: 228, + CORRECTION_TIME_OF_DAY_BLOCK: 229, + MORNING_FACTOR: 230, + MID_DAY_FACTOR: 231, + EVENING_FACTOR: 232, + NIGHT_FACTOR: 233, + INSULIN_DURATION: 234, + TARGET_TIME_OF_DAY_BLOCK: 235, + BOB_SYMBOL: 236, + PATIENT_CONFIG: 170, + FACTORY_CONFIG: 171, + CAL_CONFIG_CLEAR: 172, + STRIP_COUNT: 239, + SCAN_COUNTER: 241, + ACTIVATION_COUNT: 242, + RECOVERY_ITEM: 175, + LOW_VOLTAGE: 181, + LOSS_OF_CLOCK: 182, + LOSS_OF_LOCK: 183, + WATCHDOG: 184, + EXTERNAL_PIN_RESET: 185, + POWER_ON_RESET: 186, + JTAG_RESET: 187, + LOCK_UP_ARM: 188, + SOFTWARE_RESET: 189, + MDM_AP_RESET: 190, + EZ_PORT_RESET: 191, + STOP_MODE_RESET: 192, + OLD_MASK_MODE: 193, + NEW_MASK_MODE: 194, + PDU_STATE: 195, + WRITTEN_BY_UI: 196, + EVENT_WRAP: 197, + EVENT_CRC: 253, +}; + +export const CFG_TABLE_ID = { + METER_FACTORY_CONFIGURATION: 1, + METER_SETTINGS: 2, + USER_PATIENT_CONFIGURATION: 3, + USER_PATIENT_SETTINGS: 4, + INSULIN_SETTINGS: 5, + REMINDER_STRING: 6, + SMART_TAG_NOTES: 7, + STORED_SENSOR_INFORMATION: 9, + REMINDER_DATA: 10, +}; + +export const CFG_FIELD_ID = { + SYSTEM_TYPE: 1025, + MARKET_LEVEL: 1026, + MARKET_SUB_LEVEL: 1027, + MARKET_PUCK_LEVEL: 1028, + BRAND_NAME: 1029, + NUMBER_FORMAT: 1056, + UNIT_OF_MEASURE: 1057, + INSULIN_CALC_PRESENT: 1058, + ALLOWABLE_MEAL_UNIT: 1059, + GLYCEMIC_RANGE_HIGH: 1067, + GLYCEMIC_RANGE_LOW: 1068, + TIME_CONVERSION: 1072, + FIRST_TIME_STARTUP_DONE: 1106, + PATIENT_NAME: 1130, + PATIENT_ID: 1131, + TARGET_RANGE_LOW: 1132, + TARGET_RANGE_HIGH: 1133, + LANGUAGE_SETTING: 1134, + TIME_FORMAT: 1135, + INSULIN_DOSE_INCREMENT: 1136, + MASK_MODE_OPTION: 1137, + MASK_MODE_REMINDER: 1138, + MASK_MODE_HOUR: 1139, + MASK_MODE_MIN: 1140, + BEEPER_VOLUME: 1160, + NOTIFICATION_SOUND: 1161, + NOTIFICATION_VIBE: 1162, + BUTTON_SOUND: 1163, + INSULIN_MODE: 1190, + EASY_DONE_FLAG: 1191, + ADVANCE_DONE_FLAG: 1192, + CARB_TYPE: 1193, + CARBS_PER_SERVING: 1194, + CARB_RATIO_FLAG: 1195, + CARB_RATIO_ALL_DAY: 1196, + CARB_RATIO_MORNING: 1197, + CARB_RATIO_MIDDAY: 1198, + CARB_RATIO_EVENING: 1199, + CARB_RATIO_NIGHT: 1200, + SERVING_RATIO_FLAG: 1201, + SERVING_RATIO_ALL_DAY: 1202, + SERVING_RATIO_MORNING: 1203, + SERVING_RATIO_MIDDAY: 1204, + SERVING_RATIO_EVENING: 1205, + SERVING_RATIO_NIGHT: 1206, + IOB_ICON_FLAG: 1207, + INSULIN_DURATION: 1208, + FIXED_DOSE_BREAKFAST: 1209, + FIXED_DOSE_LUNCH: 1210, + FIXED_DOES_DINNER: 1211, + CORRECTION_FACTORS_REQ: 1212, + CORRECTION_TYPE: 1213, + SINGLE_TARGET_RATIO_FLAG: 1214, + SINGLE_TARGET_RATIO_ALL_DAY: 1215, + SINGLE_TARGET_RATIO_MORNING: 1216, + SINGLE_TARGET_RATIO_MIDDAY: 1217, + SINGLE_TARGET_RATIO_EVENING: 1218, + SINGLE_TARGET_RATIO_NIGHT: 1219, + LOW_TARGET_RATIO_FLAG: 1220, + LOW_TARGET_RATIO_ALL_DAY: 1221, + LOW_TARGET_RATIO_MORNING: 1222, + LOW_TARGET_RATIO_MIDDAY: 1223, + LOW_TARGET_RATIO_EVENING: 1224, + LOW_TARGET_RATIO_NIGHT: 1225, + HIGH_TARGET_RATIO_FLAG: 1226, + HIGH_TARGET_RATIO_ALL_DAY: 1227, + HIGH_TARGET_RATIO_MORNING: 1228, + HIGH_TARGET_RATIO_MIDDAY: 1229, + HIGH_TARGET_RATIO_EVENING: 1230, + HIGH_TARGET_RATIO_NIGHT: 1231, + BG_DROP_CORRECTION_RATIO_FLAG: 1232, + BG_DROP_ALL_DAY: 1233, + BG_DROP_MORNING: 1234, + BG_DROP_MIDDAY: 1235, + BG_DROP_EVENING: 1236, + BG_DROP_NIGHT: 1237, + REMINDER_STRING_0: 1260, + REMINDER_STRING_1: 1261, + REMINDER_STRING_2: 1262, + REMINDER_STRING_3: 1263, + REMINDER_STRING_4: 1264, + REMINDER_STRING_5: 1265, + REMINDER_STRING_6: 1266, + REMINDER_STRING_7: 1267, + REMINDER_STRING_8: 1268, + REMINDER_STRING_9: 1269, + REMINDER_STRING_10: 1270, + REMINDER_STRING_11: 1271, + SMART_TAG_0: 1290, + SMART_TAG_1: 1291, + SMART_TAG_2: 1292, + SMART_TAG_3: 1293, + SMART_TAG_4: 1294, + SMART_TAG_5: 1295, + BULK_STORAGE: 1310, + SENSOR_STATE: 1320, + SENSOR_UID: 1321, + PAIRED_FLAG: 1322, + SENSOR_PUCK_INFO: 1323, + SENSOR_START_TIME: 1324, + REMINDER_CUSTOM_STRING_1: 1400, + REMINDER_CUSTOM_STRING_2: 1401, + REMINDER_CUSTOM_STRING_3: 1402, + REMINDER_CUSTOM_STRING_4: 1403, + REMINDER_CUSTOM_STRING_5: 1404, + REMINDER_CUSTOM_STRING_6: 1405, + REMINDER_CUSTOM_STRING_7: 1406, + REMINDER_CUSTOM_STRING_8: 1407, + REMINDER_CUSTOM_STRING_9: 1408, + REMINDER_1_DATA_TYPE: 1415, + REMINDER_1_DATA_STRING_INDEX: 1416, + REMINDER_1_DATA_TIME_HOUR: 1417, + REMINDER_1_DATA_TIME_MINUTE: 1418, + REMINDER_1_DATA_TIME_SECOND: 1419, + REMINDER_1_DATA_STATUS: 1475, + REMINDER_2_DATA_TYPE: 1420, + REMINDER_2_DATA_STRING_INDEX: 1421, + REMINDER_2_DATA_TIME_HOUR: 1422, + REMINDER_2_DATA_TIME_MINUTE: 1423, + REMINDER_2_DATA_TIME_SECOND: 1424, + REMINDER_2_DATA_STATUS: 1476, + REMINDER_3_DATA_TYPE: 1425, + REMINDER_3_DATA_STRING_INDEX: 1426, + REMINDER_3_DATA_TIME_HOUR: 1427, + REMINDER_3_DATA_TIME_MINUTE: 1428, + REMINDER_3_DATA_TIME_SECOND: 1429, + REMINDER_3_DATA_STATUS: 1477, + REMINDER_4_DATA_TYPE: 1430, + REMINDER_4_DATA_STRING_INDEX: 1431, + REMINDER_4_DATA_TIME_HOUR: 1432, + REMINDER_4_DATA_TIME_MINUTE: 1433, + REMINDER_4_DATA_TIME_SECOND: 1434, + REMINDER_4_DATA_STATUS: 1478, + REMINDER_5_DATA_TYPE: 1435, + REMINDER_5_DATA_STRING_INDEX: 1436, + REMINDER_5_DATA_TIME_HOUR: 1437, + REMINDER_5_DATA_TIME_MINUTE: 1438, + REMINDER_5_DATA_TIME_SECOND: 1439, + REMINDER_5_DATA_STATUS: 1479, + REMINDER_6_DATA_TYPE: 1440, + REMINDER_6_DATA_STRING_INDEX: 1441, + REMINDER_6_DATA_TIME_HOUR: 1442, + REMINDER_6_DATA_TIME_MINUTE: 1443, + REMINDER_6_DATA_TIME_SECOND: 1444, + REMINDER_6_DATA_STATUS: 1480, + REMINDER_7_DATA_TYPE: 1445, + REMINDER_7_DATA_STRING_INDEX: 1446, + REMINDER_7_DATA_TIME_HOUR: 1447, + REMINDER_7_DATA_TIME_MINUTE: 1448, + REMINDER_7_DATA_TIME_SECOND: 1449, + REMINDER_7_DATA_STATUS: 1481, + REMINDER_8_DATA_TYPE: 1450, + REMINDER_8_DATA_STRING_INDEX: 1451, + REMINDER_8_DATA_TIME_HOUR: 1452, + REMINDER_8_DATA_TIME_MINUTE: 1453, + REMINDER_8_DATA_TIME_SECOND: 1454, + REMINDER_8_DATA_STATUS: 1482, + REMINDER_9_DATA_TYPE: 1455, + REMINDER_9_DATA_STRING_INDEX: 1456, + REMINDER_9_DATA_TIME_HOUR: 1457, + REMINDER_9_DATA_TIME_MINUTE: 1458, + REMINDER_9_DATA_TIME_SECOND: 1459, + REMINDER_9_DATA_STATUS: 1483, + REMINDER_10_DATA_TYPE: 1460, + REMINDER_10_DATA_STRING_INDEX: 1461, + REMINDER_10_DATA_TIME_HOUR: 1462, + REMINDER_10_DATA_TIME_MINUTE: 1463, + REMINDER_10_DATA_TIME_SECOND: 1464, + REMINDER_10_DATA_STATUS: 1484, + REMINDER_11_DATA_TYPE: 1465, + REMINDER_11_DATA_STRING_INDEX: 1466, + REMINDER_11_DATA_TIME_HOUR: 1467, + REMINDER_11_DATA_TIME_MINUTE: 1468, + REMINDER_11_DATA_TIME_SECOND: 1469, + REMINDER_11_DATA_STATUS: 1485, + REMINDER_12_DATA_TYPE: 1470, + REMINDER_12_DATA_STRING_INDEX: 1471, + REMINDER_12_DATA_TIME_HOUR: 1472, + REMINDER_12_DATA_TIME_MINUTE: 1473, + REMINDER_12_DATA_TIME_SECOND: 1474, + REMINDER_12_DATA_STATUS: 1486, + CONFIG_CRC: 2047, +}; + +export const RESULT_VALUE_TYPE = { + GLUCOSE: 0, + KETONE: 1, + SCAN: 2, +}; + +export const COMPRESSION = { + DISABLED: 0, + ENABLED: 1, +}; + +export const COMPRESSION_TYPE = { + UNCOMPRESSED: 0xfd, + ZERO_COMPRESSED: 0xff, +}; + +export const CRC32_TABLE = [ + 0x00000000, + 0x04c11db7, + 0x09823b6e, + 0x0d4326d9, + 0x130476dc, + 0x17c56b6b, + 0x1a864db2, + 0x1e475005, + 0x2608edb8, + 0x22c9f00f, + 0x2f8ad6d6, + 0x2b4bcb61, + 0x350c9b64, + 0x31cd86d3, + 0x3c8ea00a, + 0x384fbdbd, +]; diff --git a/lib/drivers/abbott/freeStyleLibreData.js b/lib/drivers/abbott/freeStyleLibreData.js new file mode 100644 index 0000000000..dcd7099cf2 --- /dev/null +++ b/lib/drivers/abbott/freeStyleLibreData.js @@ -0,0 +1,466 @@ +/* + * == BSD2 LICENSE == + * Copyright (c) 2017, Tidepool Project + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the associated License, which is identical to the BSD 2-Clause + * License as published by the Open Source Initiative at opensource.org. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the License for more details. + * + * You should have received a copy of the License along with this program; if + * not, you can obtain one from Tidepool Project at tidepool.org. + * == BSD2 LICENSE == + */ + +import _ from 'lodash'; +import sundial from 'sundial'; + +import structJs from '../../struct'; +import TZOUtil from '../../TimezoneOffsetUtil'; +import annotate from '../../eventAnnotations'; + +import { + OP_CODE, + ERROR_DESCRIPTION, + DB_TABLE_ID, + DB_WRAP_RECORDS, + DB_RECORD_TYPE, + CFG_TABLE_ID, + RESULT_VALUE_TYPE, + COMPRESSION_TYPE, + KETONE_VALUE_FACTOR, + KETONE_HI, + KETONE_LO, + GLUCOSE_HI, + GLUCOSE_LO, +} from './freeStyleLibreConstants'; + +const struct = structJs(); + +const isBrowser = typeof window !== 'undefined'; +// eslint-disable-next-line no-console +const debug = isBrowser ? require('bows')('FreeStyleLibreDriver') : console.log; + +const FORMAT = { + ERROR: 'bb', + DATE_TIME: 'bbbbbsb', + RECORD_HEADER: 'sbbin', + HISTORICAL_DATA: 'ssss', + TIME_CHANGE: 'insss', // despite the specs, user time offset is a signed value, same as in header +}; + +const FORMAT_LENGTH = _.mapValues(FORMAT, format => struct.structlen(format)); + +const OP_CODE_PROCESSING_ORDER = [ + OP_CODE.GET_CFG_SCHEMA, // not used for now + OP_CODE.GET_DB_SCHEMA, // not used for now + OP_CODE.GET_DATE_TIME, + OP_CODE.GET_CFG_DATA, + OP_CODE.COMPRESSED_DATABASE, + OP_CODE.GET_DATABASE, +]; + +export default class FreeStyleLibreData { + constructor(cfg) { + this.cfg = cfg; + + this.opCodeHandlers = {}; + this.opCodeHandlers[OP_CODE.GET_DATE_TIME] = this.handleDateTime.bind(this); + this.opCodeHandlers[OP_CODE.GET_DB_SCHEMA] = this.handleDatabaseSchema.bind(this); + this.opCodeHandlers[OP_CODE.COMPRESSED_DATABASE] = this.handleCompressedDatabase.bind(this); + this.opCodeHandlers[OP_CODE.GET_DATABASE] = this.handleDatabase.bind(this); + this.opCodeHandlers[OP_CODE.GET_CFG_SCHEMA] = this.handleConfigSchema.bind(this); + this.opCodeHandlers[OP_CODE.GET_CFG_DATA] = this.handleConfigData.bind(this); + this.opCodeHandlers[OP_CODE.ERROR] = this.constructor.handleError; + } + + processAapPackets(aapPackets, dbRecordNumber) { + this.factoryConfig = {}; + this.deviceDateTime = null; + this.records = []; + this.postRecords = []; + + this.dbRecordNumberNextWrap = {}; + this.numResultRecords = 0; + + // calculate next DB record number wrap, so record numbers can be recovered on truncated DBs + const nextWrap = Math.ceil(dbRecordNumber / 0x10000) * 0x10000; + this.dbRecordNumberNextWrap[DB_TABLE_ID.GLUCOSE_RESULT] = nextWrap; + this.dbRecordNumberNextWrap[DB_TABLE_ID.RAPID_ACTING_INSULIN] = nextWrap; + this.dbRecordNumberNextWrap[DB_TABLE_ID.HISTORICAL_DATA] = nextWrap; + this.dbRecordNumberNextWrap[DB_TABLE_ID.EVENT] = nextWrap; + + // start with newest record number and search backwards when processing the DB records + this.oldestResultRecordNumber = dbRecordNumber; + + // sort AAP packets by their OP code + const aapPacketsByOpCode = {}; + aapPackets.forEach((aapPacket) => { + const opCode = aapPacket.opCode; + if (!(opCode in aapPacketsByOpCode)) { + aapPacketsByOpCode[opCode] = []; + } + aapPacketsByOpCode[opCode].push(aapPacket); + }); + + // process AAP packet in fixed order to make sure data is available when needed + OP_CODE_PROCESSING_ORDER.forEach((opCode) => { + if (opCode in aapPacketsByOpCode) { + aapPacketsByOpCode[opCode].forEach((aapPacket) => { + const handler = this.opCodeHandlers[aapPacket.opCode]; + if (handler) { + handler(aapPacket); + } else { + debug('processAapPackets: no handler found for OP code:', aapPacket.opCode); + } + }); + } + }); + + // the oldest record number in the result DB is used as limit how far back the history records + // are processed but in case the result record DB does not contain any data, process at least + // the last 1000 records this number is lower than the maximal size of the result DB, so we will + // not process further back than any potentially truncated time change records + const oldestValidRecordNumber = + Math.max(0, this.oldestResultRecordNumber - Math.max(0, 1000 - this.numResultRecords)); + + // use only records that are newer than the oldestValidRecordNumber + // older records cannot be properly timestamped due to potentially truncated time change records + this.records = + this.records.filter(elem => elem.headerFields.recordNumber >= oldestValidRecordNumber); + + if (this.records.length === 0) { + debug('processAapPackets: no valid database records found in', + aapPackets.length, 'AAP packets.'); + return []; + } + + // sort records ascending by record number to honor the timeChangeFlag + this.records.sort((a, b) => a.headerFields.recordNumber - b.headerFields.recordNumber); + + // if timeChangeFlag is set, set record number of previous history record lower than the + // previous time change record + // this prevents these records from being bootstrapped with the wrong timezone + let previousTimeChangeRecord = null; + let previousHistoryRecord = null; + this.records.forEach((record) => { + if (record.historyFields) { + if (record.historyFields.timeChangeFlag && previousTimeChangeRecord) { + previousHistoryRecord.headerFields.recordNumber = + previousTimeChangeRecord.headerFields.recordNumber - 1; + } + previousHistoryRecord = record; + } else if (record.timeChangeFields) { + previousTimeChangeRecord = record; + } + }); + + // sort records again ascending by record number to find the most recent one + this.records.sort((a, b) => a.headerFields.recordNumber - b.headerFields.recordNumber); + const timestamp = this.records[this.records.length - 1].jsDate; + const mostRecent = sundial.applyTimezone(timestamp, this.cfg.timezone).toISOString(); + + this.buildTimeChangeRecords(); + this.cfg.tzoUtil = new TZOUtil(this.cfg.timezone, mostRecent, this.postRecords); + + this.buildCBGRecords(); + this.buildMeasurementRecords(); + + return this.postRecords; + } + + static handleError(aapPacket) { + const fields = struct.unpack(aapPacket.data, 0, FORMAT.ERROR, ['opCode', 'errorCode']); + debug('handleError:', ERROR_DESCRIPTION[fields.errorCode], 'for OP code', fields.opCode); + if (aapPacket.data.length > FORMAT_LENGTH.ERROR) { + debug('handleError: extra data:', aapPacket.data.slice(FORMAT_LENGTH.ERROR).toString('hex')); + } + } + + handleDateTime(aapPacket) { + if (aapPacket.dataLength !== FORMAT_LENGTH.DATE_TIME) { + debug('handleDateTime: wrong data length:', aapPacket.dataLength, 'instead of', FORMAT_LENGTH.DATE_TIME); + return; + } + const fields = struct.unpack(aapPacket.data, 0, FORMAT.DATE_TIME, + ['second', 'minute', 'hour', 'day', 'month', 'year', 'valid']); + if (fields.valid !== 1) { + debug('handleDateTime: date not marked as valid:', fields.valid, aapPacket.data.data[0]); + return; + } + this.deviceDateTime = new Date(fields.year, fields.month - 1, fields.day, + fields.hour, fields.minute, fields.second); + debug('handleDateTime: datetime:', this.deviceDateTime); + } + + // eslint-disable-next-line no-unused-vars,class-methods-use-this + handleDatabaseSchema(aapPacket) { + /* + * These are ignored for now, as the schemata are already known from the specs. + * For now they are hardcoded based on the specs for the few record types that are actually + * needed. + * + * The schemata describe the fields in the database records, so that using this information to + * parse the records instead of the hardcoded format strings, would make it possible to + * understand the data even after a potential firmware upgrade that changes the database + * structure. + * (As long as the field IDs stay the same, the fields parsed via these schemata can still be + * evaluated properly.) + * + * Schema description: (example: the record header prefixed to all records) + * + UINT8 RecordHeader_schema[] = + { + // schema descriptor + 48, 0, // [uint16_le] schema table length (including this descriptor) + 1, 0, // [uint16_le] schema table version + 255, // [uint8] schema table/record ID + 6, 0, // [uint16_le] number of data words (16bit) in the record + 5, // [uint8] number of fields in the record + + // field descriptors (8 byte each) + // [uint16_le], [uint16_le], [uint8], [uint8], [uint16_le] + // field ID, word offset, bit offset inside the word, data type, data length in bits + 0,0,0,0,0,1,16,0, + 8,0,1,0,0,0,8,0, + 7,0,1,0,15,0,1,0, + 9,0,2,0,0,0,32,0, + 10,0,4,0,0,2,32,0 + }; + * + */ + } + + getDateTime(readerTime, userTimeOffset) { + const unixTimestamp = this.factoryConfig.timeConversion + readerTime + userTimeOffset; + return new Date(unixTimestamp * 1000); + } + + buildTimeChangeRecords() { + this.records.filter(elem => elem.headerFields.recordType === DB_RECORD_TYPE.TIME_CHANGE_RESULT) + .forEach((record) => { + const oldDateTime = this.getDateTime(record.timeChangeFields.oldReaderTime, + record.timeChangeFields.oldUserTimeOffset); + + const timeChange = this.cfg.builder.makeDeviceEventTimeChange() + .with_change({ + from: sundial.formatDeviceTime(oldDateTime), + to: sundial.formatDeviceTime(record.jsDate), + agent: 'manual', + }) + .with_deviceTime(sundial.formatDeviceTime(record.jsDate)) + .set('index', record.headerFields.recordNumber) + .set('jsDate', record.jsDate); + this.postRecords.push(timeChange); + }); + } + + static addOutOfRangeAnnotation(recordBuilder, low, high, step, type) { + if (low !== null && recordBuilder.value < low + step) { + recordBuilder.with_value(low); + annotate.annotateEvent(recordBuilder, { + code: `${type}/out-of-range`, + value: 'low', + threshold: low + step, + }); + } else if (high !== null && recordBuilder.value > high - step) { + recordBuilder.with_value(high); + annotate.annotateEvent(recordBuilder, { + code: `${type}/out-of-range`, + value: 'high', + threshold: high - step, + }); + } + } + + buildCBGRecords() { + this.records.filter(elem => elem.headerFields.recordType === DB_RECORD_TYPE.HISTORICAL_DATA) + .forEach((record) => { + const cbg = this.cfg.builder.makeCBG() + .with_value(record.historyFields.glucoseValue) + .with_units('mg/dL') // values are always in 'mg/dL', independent of the unitOfMeasure setting + .with_deviceTime(sundial.formatDeviceTime(record.jsDate)) + .set('index', record.headerFields.recordNumber); + + this.constructor.addOutOfRangeAnnotation(cbg, GLUCOSE_LO, GLUCOSE_HI, 1, 'bg'); + + this.cfg.tzoUtil.fillInUTCInfo(cbg, record.jsDate); + this.postRecords.push(cbg.done()); + }); + } + + buildMeasurementRecords() { + this.records.filter(elem => + [DB_RECORD_TYPE.GLUCOSE_KETONE_SERVING, + DB_RECORD_TYPE.GLUCOSE_KETONE_MEAL, + DB_RECORD_TYPE.GLUCOSE_KETONE_CARBS, + ].includes(elem.headerFields.recordType)).forEach((record) => { + let recordBuilder; + + if (record.measurementFields.resultType === RESULT_VALUE_TYPE.GLUCOSE) { + recordBuilder = this.cfg.builder.makeSMBG() + .with_value(record.measurementFields.resultValue) + .with_units('mg/dL'); // values are always in 'mg/dL', independent of the unitOfMeasure setting + + this.constructor.addOutOfRangeAnnotation(recordBuilder, GLUCOSE_LO, GLUCOSE_HI, 1, 'bg'); + } else if (record.measurementFields.resultType === RESULT_VALUE_TYPE.KETONE) { + recordBuilder = this.cfg.builder.makeBloodKetone() + .with_value(record.measurementFields.resultValue / KETONE_VALUE_FACTOR) + .with_units('mmol/L'); + + this.constructor.addOutOfRangeAnnotation(recordBuilder, KETONE_LO, KETONE_HI, 1 / KETONE_VALUE_FACTOR, 'ketone'); + } + + if (recordBuilder) { + recordBuilder = recordBuilder.with_deviceTime(sundial.formatDeviceTime(record.jsDate)) + .set('index', record.headerFields.recordNumber); + this.cfg.tzoUtil.fillInUTCInfo(recordBuilder, record.jsDate); + this.postRecords.push(recordBuilder.done()); + } + }); + } + + handleCompressedDatabase(aapPacket) { + let decompressedBuffer = new Buffer(1); + let compressedOffset = 0; + + // copy table ID + decompressedBuffer[0] = aapPacket.data[compressedOffset]; + compressedOffset += 1; + + while (compressedOffset < aapPacket.dataLength) { + const blockType = aapPacket.data[compressedOffset]; + compressedOffset += 1; + + // parse 24 bit little endian block length + /* eslint-disable no-bitwise */ + let blockLength = aapPacket.data[compressedOffset] + | (aapPacket.data[compressedOffset + 1] << 8) + | (aapPacket.data[compressedOffset + 2] << 16); + /* eslint-enable no-bitwise */ + compressedOffset += 3; + + blockLength *= 4; // convert number of uint32 values to number of uint8 values + + if (blockType === COMPRESSION_TYPE.UNCOMPRESSED) { + decompressedBuffer = Buffer.concat([decompressedBuffer, + aapPacket.data.slice(compressedOffset, compressedOffset + blockLength)]); + compressedOffset += blockLength; + } else if (blockType === COMPRESSION_TYPE.ZERO_COMPRESSED) { + decompressedBuffer = Buffer.concat([decompressedBuffer, Buffer.alloc(blockLength)]); + compressedOffset += blockLength; + } else { + debug('handleCompressedDatabase: failed to decompress!'); + return; + } + } + + // build decompressed AAP packet to process + const decompressedAapPacket = { + packetLength: (aapPacket.packetLength - aapPacket.dataLength) + aapPacket.data.length, + data: decompressedBuffer, + dataLength: aapPacket.data.length, + opCode: OP_CODE.GET_DATABASE, + }; + + this.handleDatabase(decompressedAapPacket); + } + + /* eslint-disable no-bitwise */ + handleDatabase(aapPacket) { + if (aapPacket.dataLength === 0) { + return; + } + + let offset = 0; + const databaseTableId = aapPacket.data[offset]; + offset += 1; + + const headerFields = struct.unpack(aapPacket.data, offset, FORMAT.RECORD_HEADER, + ['recordNumber', 'recordType', 'isTimeValid', 'readerTime', 'userTimeOffset']); + headerFields.isTimeValid = ((headerFields.isTimeValid & 0x80) > 0); + offset += FORMAT_LENGTH.RECORD_HEADER; + + // calculate 32bit record number from 16bit header record number and next wrap around number + headerFields.recordNumber = + this.dbRecordNumberNextWrap[databaseTableId] - (0x10000 - headerFields.recordNumber); + + // find the lowest record number in the results database + if (databaseTableId === DB_TABLE_ID.GLUCOSE_RESULT) { + this.numResultRecords += 1; + this.oldestResultRecordNumber = + Math.min(this.oldestResultRecordNumber, headerFields.recordNumber); + } + + const dateTime = this.getDateTime(headerFields.readerTime, headerFields.userTimeOffset); + + if (headerFields.recordType === DB_RECORD_TYPE.TIME_CHANGE_RESULT) { + const timeChangeFields = struct.unpack(aapPacket.data, offset, FORMAT.TIME_CHANGE, + ['oldReaderTime', 'oldUserTimeOffset', 'valid', 'unused', 'CRC16']); + + // TODO: validate CRC16 + if (timeChangeFields.valid) { + this.records.push({ headerFields, timeChangeFields, jsDate: dateTime }); + } + } else if (headerFields.recordType === DB_RECORD_TYPE.HISTORICAL_DATA) { + const historyFields = struct.unpack(aapPacket.data, offset, FORMAT.HISTORICAL_DATA, + ['glucoseValue', 'lifeCounter', 'dataQualityErrorFlags', 'CRC16']); + historyFields.firstFlag = ((historyFields.glucoseValue & 0x1000) > 0); + historyFields.timeChangeFlag = ((historyFields.glucoseValue & 0x2000) > 0); + historyFields.foodFlag = ((historyFields.glucoseValue & 0x4000) > 0); + historyFields.rapidActingInsulinFlag = ((historyFields.glucoseValue & 0x8000) > 0); + historyFields.glucoseValue &= 0x03ff; + + // TODO: validate CRC16 + if (historyFields.dataQualityErrorFlags === 0) { + // debug('handleDatabase: historyFields:', historyFields, aapPacket.data.toString('hex')); + this.records.push({ headerFields, historyFields, jsDate: dateTime }); + } + } else if ([DB_RECORD_TYPE.GLUCOSE_KETONE_SERVING, + DB_RECORD_TYPE.GLUCOSE_KETONE_MEAL, + DB_RECORD_TYPE.GLUCOSE_KETONE_CARBS, + ].includes(headerFields.recordType)) { + const measurementFields = {}; + const RESULT_VALUE_OFFSET = 0; + struct.unpack(aapPacket.data, offset + RESULT_VALUE_OFFSET, 's', ['resultValue'], measurementFields); + measurementFields.resultType = (measurementFields.resultValue >> 14) & 0x3; + measurementFields.resultValue &= 0x03ff; + + const DATA_QUALITY_ERROR_FLAGS_OFFSET = 10; + struct.unpack(aapPacket.data, offset + DATA_QUALITY_ERROR_FLAGS_OFFSET, 's', ['dataQualityErrorFlags'], measurementFields); + + // TODO: validate CRC16 + if (measurementFields.dataQualityErrorFlags === 0) { + this.records.push({ headerFields, measurementFields, jsDate: dateTime }); + } + } else if (headerFields.recordType in DB_WRAP_RECORDS) { + const DB_RECORD_NUMBER_OFFSET = 0; + // TODO: validate CRC16 + this.dbRecordNumberNextWrap[databaseTableId] = + aapPacket.data.readUInt32LE(offset + DB_RECORD_NUMBER_OFFSET); + } + } + /* eslint-enable no-bitwise */ + + // eslint-disable-next-line no-unused-vars,class-methods-use-this + handleConfigSchema(aapPacket) { + // ignored, since they are currently hardcoded based on the specs + } + + handleConfigData(aapPacket) { + let offset = 0; + const tableId = aapPacket.data[offset]; + offset += 1; + if (tableId === CFG_TABLE_ID.METER_FACTORY_CONFIGURATION) { + const UNIT_OF_MEASURE_OFFSET = 133; + struct.unpack(aapPacket.data, offset + UNIT_OF_MEASURE_OFFSET, 'b', ['unitOfMeasure'], this.factoryConfig); + this.factoryConfig.unitOfMeasure = ['mmol/L', 'mg/dL'][this.factoryConfig.unitOfMeasure]; + + const TIME_CONVERSION_OFFSET = 156; + struct.unpack(aapPacket.data, offset + TIME_CONVERSION_OFFSET, 'i', ['timeConversion'], this.factoryConfig); + } + } +} diff --git a/lib/drivers/abbott/freeStyleLibreProtocol.js b/lib/drivers/abbott/freeStyleLibreProtocol.js new file mode 100644 index 0000000000..8d2c3d56a0 --- /dev/null +++ b/lib/drivers/abbott/freeStyleLibreProtocol.js @@ -0,0 +1,667 @@ +/* + * == BSD2 LICENSE == + * Copyright (c) 2017, Tidepool Project + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the associated License, which is identical to the BSD 2-Clause + * License as published by the Open Source Initiative at opensource.org. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the License for more details. + * + * You should have received a copy of the License along with this program; if + * not, you can obtain one from Tidepool Project at tidepool.org. + * == BSD2 LICENSE == + */ + +/* + * *** FreeStyle Libre communication via USB HID *** + * + * *** HID DATA TRANSFER *** + * - HID reports are used to encapsulate the text and the binary protocol + * - HID report frames always have 64 bytes + * + * HID_FRAME: + * |--------------------------------------------------------------------- + * HID_HEADER HID_DATA + * |- |- |------------------------------------------------ + * COMMAND DATA_LENGTH PAYLOAD_DATA GARBAGE + * |- |- |------------------- |-------------------------- + * 0xnn 0xll (up to 62 bytes) + * + * + * HID_HEADER: + * - COMMAND: 1 byte + * - DATA_LENGTH: 1 byte (excluding HID_HEADER, so valid range is 0-62) + * + * HID_DATA: + * - PAYLOAD_DATA: DATA_LENGTH bytes actual data + * - GARBAGE: (62 - DATA_LENGTH) bytes fill the rest of the frame + * + * COMMAND CODES: + * - from host to device: + * --- 0x01: used in init? + * --- 0x04: used in init? + * --- 0x05: used in init? + * --- 0x0a: BINARY PROTOCOL request data with AAP OP_CODES + * --- 0x0d: ACK received packets + * --- 0x15: used in init? + * --- 0x21: TEXT PROTOCOL command + * --- 0x60: TEXT PROTOCOL command + * + * - from device to host: + * --- 0x06: answer to init 0x05, data in text format containing the device's serial number + * --- 0x0b: BINARY PROTOCOL response to 0x0a + * --- 0x0c: ACK received packets + * --- 0x22: at seemingly random points in the communication? always the same single data byte: 0x05 + * --- 0x34: answer to init 0x04, single data byte with different values? + * --- 0x35: answer to init 0x05, data in text format containing the devices software version + * --- 0x60: TEXT PROTOCOL response + * --- 0x71: answer to init 0x01, single data byte with value 0x01? + * + * + * *** BINARY PROTOCOL *** + * + * - ABMP: ADC Binary 22175 communication Meter Protocol + * --- ATP: ABMP Transport Protocol + * ----- AAP: ABMP Application Protocol + * + * ATP_FRAME: + * + * PAYLOAD_DATA + * |--------------------------------------------------------------- + * ATP_HEADER ATP_DATA + * |- |- |---- |--------------------- |---------- + * SEQ_RX SEQ_TX CRC AAP_FRAME_1 AAP_FRAME_2... + * |--------------------- + * AAP_HEADER AAP_DATA + * |--- |- |--------- + * AAP_LENGTH OP_CODE AAP_DATA + * + * ATP_FRAME: + * - ATP_HEADER + * --- SEQ_RX: 1 byte sequence information: the next expected packet's sequence number + * --- SEQ_TX: 1 byte sequence information: this packet's sequence number + * --- CRC: 32 bit CRC (variation of CRC32-CCITT, using 8 XORs but only a size 16 lookup table) + * - ATP_DATA + * --- can contain one or multiple AAP frames or only parts of one + * + * AAP_FRAME: + * - AAP_HEADER: + * --- AAP_LENGTH: 0 to 3 bytes + * ----- if high bit is set, the lower 7 bits are the length -> max 21 bits for length + * ----- if not, this byte is already the OP_CODE byte + * --- OP_CODE: 1 byte (high bit is 0) + * - AAP_DATA: up to 2MB spread over multiple ATP_DATA fields of consecutive ATP_FRAMES + * + * OP_CODE: (in addition to the ones defined in the specs) + * - 0x7d: make device flush its output buffer + * (sends remaining AAP data, even if not a full ATP frame can be filled) + * + * BINARY COMMAND: + * - exactly one AAP frame (contained in a single ATP frame, inside a single HID report frame) + * + * BINARY RESPONSE: + * - can span multiple AAP frames, possibly spread over multiple ATP and HID frames + * - only completely filled ATP frames are sent + * --- send OP Code 0x7d to force device to send remaining AAP data in partially filled ATP frame + * + * + * *** TEXT PROTOCOL *** + * + * TEXT COMMAND: + * + * PAYLOAD_DATA + * |---------------- + * MESSAGE SEP + * |-------- |-- + * $command? \r\n + * + * TEXT RESPONSE: + * - can span multiple HID frames, ending with STATUS ("CMD OK\r\n" or "CMD Fail!\r\n") + * - lines are separated by SEP: "\r\n" (0x0a 0x0d) + * - MESSAGE is followed by CHECKSUM and STATUS, each in its own line + * + * PAYLOAD_DATA + * |------------------------------------------------------------------------ + * MESSAGE SEP CHECKSUM SEP STATUS SEP + * |-------- |-- |------------- |-- |------ |-- + * message... \r\n "CKSM:[0-9A-F]{8}" \r\n "CMD OK" or "CMD Fail!" \r\n + * + */ + +import async from 'async'; +import structJs from '../../struct'; + +import { DEVICE_MODEL_NAME, OP_CODE, COMMAND, CRC32_TABLE } from './freeStyleLibreConstants'; + +const struct = structJs(); + +const isBrowser = typeof window !== 'undefined'; +// eslint-disable-next-line no-console +const debug = isBrowser ? require('bows')('FreeStyleLibreDriver') : console.log; + +const HID_PACKET_SIZE = 64; +const HID_HEADER_FORMAT = 'bb'; +const HID_HEADER_LENGTH = struct.structlen(HID_HEADER_FORMAT); + +const ATP_HEADER_FORMAT = 'bbi'; +const ATP_HEADER_LENGTH = struct.structlen(ATP_HEADER_FORMAT); + +const ACK_HEADER_FORMAT = 'bb2Z'; +const ACK_HEADER_LENGTH = struct.structlen(ACK_HEADER_FORMAT); + +const READ_TIMEOUT = 5000; // [ms] +const ACK_INTERVAL = 10; // [ms] + +const INIT_ACK_MAGIC_DATA = '\x00\x02'; + +// to know then all AAP responses for a request were received, +// we can look for the AAP data length of the final packet +const FINAL_AAP_DATA_LENGTHS = {}; +// contrary to the specs the db tables are concluded by just 0x31 with length 0, not 0x81 0x31 0xcc +FINAL_AAP_DATA_LENGTHS[OP_CODE.GET_DATABASE] = 0; +FINAL_AAP_DATA_LENGTHS[OP_CODE.GET_DB_SCHEMA] = 0; +FINAL_AAP_DATA_LENGTHS[OP_CODE.GET_DATE_TIME] = 8; +FINAL_AAP_DATA_LENGTHS[OP_CODE.GET_CFG_DATA] = -1; +FINAL_AAP_DATA_LENGTHS[OP_CODE.GET_CFG_SCHEMA] = 0; +FINAL_AAP_DATA_LENGTHS[OP_CODE.SET_COMPRESSION] = 1; + +// +// regular expressions for matching text protocol responses +// +const TEXT_CHECKSUM_FORMAT = 'CKSM:([0-9A-F]{8})\r\n'; +const TEXT_STATUS_FORMAT = 'CMD (OK|Fail!)\r\n'; +const TEXT_STATUS_REGEX = new RegExp(TEXT_STATUS_FORMAT); +// in javascript RegExp "[^]*" has to be used instead of ".*" to match all chars over multiple lines +// the multiline flag /s/ does not exist, so ".*" will never match newlines +const TEXT_RESPONSE_REGEX = new RegExp(`^([^]*)${TEXT_CHECKSUM_FORMAT}${TEXT_STATUS_FORMAT}`); + +const DB_RECORD_NUMBER_REGEX = new RegExp('^DB Record Number = ([0-9]+)$'); + +// after db record responses (e.g. $arresult?, $history?) an additional line is send, +// containing the record count and an additional checksum of just the records: +// COUNT,RECORD_CHECKSUM\r\n +// eslint-disable-next-line no-control-regex +const DB_RECORDS_REGEX = new RegExp('^([^]*\r\n)([0-9]+),([0-9A-F]{8})$'); + + +export default class FreeStyleLibreProtocol { + constructor(config) { + this.config = config; + this.hidDevice = config.deviceComms; + this.sequenceRx = 0; + this.sequenceTx = 0; + this.lastAckRx = 0; + } + + readResponse(responseType, timeout, cb) { + let receiveTimeout = null; + const resetReceiveTimeout = () => { + if (receiveTimeout !== null) { + clearTimeout(receiveTimeout); + } + receiveTimeout = setTimeout(() => { + debug('readResponse: TIMEOUT'); + const e = new Error('Timeout error.'); + e.name = 'TIMEOUT'; + return cb(e, null); + }, timeout); + }; + + let ackInterval = null; + if (responseType === COMMAND.BINARY_RESPONSE) { + ackInterval = setInterval(() => { + if (this.sequenceRx !== this.lastAckRx) { + this.sendAck(); + } + }, ACK_INTERVAL); + } + + const aapPackets = []; + let receiveBuffer = new Buffer(0); + async.doWhilst( + (callback) => { + resetReceiveTimeout(); + this.hidDevice.receive((buffer) => { + if (buffer !== undefined && buffer.length > 0) { + const hidPacket = this.constructor.parseHidPacket(buffer); + + if (hidPacket.responseType === COMMAND.TEXT_RESPONSE) { + // append newly received data to message string + receiveBuffer = Buffer.concat([receiveBuffer, hidPacket.data]); + } else if (hidPacket.responseType === COMMAND.BINARY_RESPONSE) { + const atpPacket = this.constructor.parseAtpFrame(hidPacket.data); + if (atpPacket === null) { + // CRC error: try to get damaged package again + this.sendAck(); + } else if (this.sequenceRx === atpPacket.sequenceTx) { + // next sequence number expected is one higher than the one just received + this.sequenceRx = (this.sequenceRx + 1) % 0x100; + + receiveBuffer = Buffer.concat([receiveBuffer, atpPacket.data]); + } else { + debug('readResponse: Received wrong sequence number', atpPacket.sequenceTx, + ', expected', this.sequenceRx); + // try to get missing package by sending an ACK containing the next expected + // sequence number to the device + this.sendAck(); + } + } else if (hidPacket.responseType === COMMAND.ACK_FROM_DEVICE) { + + // TODO: handle device ack packets + // either throw error on unexpected sequence number + // or actually cache sent packets and resend starting from requested number + + } + } + + // run test to see if we are done receiving + return callback(); + }); + }, + + () => { + if (responseType === COMMAND.TEXT_RESPONSE) { + // if we are waiting for a text response and the buffer contains the status line: done + return !(TEXT_STATUS_REGEX.exec(receiveBuffer.toString())); + } else if (responseType === COMMAND.BINARY_RESPONSE) { + // collect all valid AAP packets from the buffer + let aapPacket; + do { + aapPacket = this.constructor.parseAapFrame(receiveBuffer); + if (aapPacket !== null) { + aapPackets.push(aapPacket); + // remove parsed packet from buffer + receiveBuffer = receiveBuffer.slice(aapPacket.packetLength); + } + } while (aapPacket !== null && receiveBuffer.length > 0); + + if (aapPackets.length === 0) { + return true; + } + + // we can recognise the last AAP packet of a response from its data length + // or from the number of AAP packets + const lastAapPacket = aapPackets[aapPackets.length - 1]; + const finalLength = FINAL_AAP_DATA_LENGTHS[lastAapPacket.opCode]; + // if the finalLength value is negative it describes the index of the final AAP packets + if (finalLength < 0) { + return aapPackets.length < -finalLength; + } + // if it is 0 or higher, it describes the dataLength of the final AAP packet + return lastAapPacket.dataLength !== finalLength; + } else if (responseType === null) { + // no response needed + return false; + } + // continue iterating + return true; + }, + + (err) => { + clearTimeout(receiveTimeout); + // send a final Ack and stop interval timer + if (ackInterval !== null) { + clearInterval(ackInterval); + this.sendAck(); + } + if (err) { + return cb(err, null); + } + if (responseType === COMMAND.BINARY_RESPONSE) { + return cb(null, aapPackets); + } + return cb(null, receiveBuffer.toString()); + }, + ); + } + + static validateTextChecksum(dataString, expectedChecksum) { + /* eslint-disable no-bitwise */ + const calculatedChecksum = dataString.split('') + .reduce((a, b) => a + (b.charCodeAt(0) & 0xff), 0); + /* eslint-enable no-bitwise */ + if (calculatedChecksum !== expectedChecksum) { + debug(`validateTextChecksum: wrong checksum: ${calculatedChecksum} != ${expectedChecksum}`); + } + return calculatedChecksum === expectedChecksum; + } + + static parseHidPacket(buffer) { + const packet = struct.unpack(buffer, 0, HID_HEADER_FORMAT, ['responseType', 'dataLen']); + packet.data = buffer.slice(HID_HEADER_LENGTH, HID_HEADER_LENGTH + packet.dataLen); + return packet; + } + + static buildHidPacket(commandType, data) { + const bytes = new Uint8Array(HID_PACKET_SIZE); + const counter = struct.pack(bytes, 0, HID_HEADER_FORMAT, commandType, data.length); + if (data.length) { + // data can be either a TypedArray or a string + let dataTypeChar = 'B'; + if (typeof (data) === 'string') { + dataTypeChar = 'Z'; + } + struct.pack(bytes, counter, data.length + dataTypeChar, data); + } + return bytes.buffer; + } + + /* eslint-disable no-bitwise */ + static parseAapFrame(buffer) { + const packet = { + dataLength: 0, + }; + + let dataLengthNumBytes = 0; + // the first 0 to 3 bytes describe the aap frame length in their lower 7 bits in little endian + for (let i = 0; i <= 2; i++) { + if (i >= buffer.length) { + return null; + } + const values = struct.unpack(buffer, i, 'b', ['byte']); + // if highest bit is not set, this is already the command byte + if ((values.byte & 0x80) === 0) { + break; + } + // highest bit was set, extract lower 7 bits as length value + let lengthValue = values.byte & 0x7f; + // shift these 7 bits to the left depending on the index i + lengthValue <<= (7 * i); + // combine these bits with the previous length value + packet.dataLength |= lengthValue; + + dataLengthNumBytes += 1; + } + + const opCodeNumBytes = 1; + + if (buffer.length > dataLengthNumBytes) { + // add opCode to packet + struct.unpack(buffer, dataLengthNumBytes, 'b', ['opCode'], packet); + } + + // if there is data missing, return null + packet.packetLength = dataLengthNumBytes + opCodeNumBytes + packet.dataLength; + const numBytesMissing = packet.packetLength - buffer.length; + if (numBytesMissing > 0) { + return null; + } + + if ((packet.opCode & 0x80) !== 0) { + debug(`parseAapFrame: Faulty op code: 0x${packet.opCode.toString(16)}`); + return null; + } + + // add data to packet + packet.data = buffer.slice(dataLengthNumBytes + opCodeNumBytes, packet.packetLength); + + return packet; + } + + static buildAapFrame(opCode, dataArrayOptional) { + let dataArray = dataArrayOptional; + if (dataArray === undefined) { + dataArray = []; + } + const aapDataLengthBytes = []; + let dataLength = dataArray.length; + // as long as there are length bits left + while (dataLength > 0) { + // put the lowest 7 bits in a length byte and set the high bit + const lengthByte = 0x80 | (dataLength & 0x7f); + // append new length byte to length string (little endian ordering) + aapDataLengthBytes.push(lengthByte); + // shift length by the 7 bits just used + dataLength >>= 7; + } + + const packetFormat = `${aapDataLengthBytes.length}Bb${dataArray.length}B`; + const packetLength = struct.structlen(packetFormat); + + const bytes = new Uint8Array(packetLength); + struct.pack(bytes, 0, packetFormat, aapDataLengthBytes, opCode, dataArray); + return bytes; + } + + static parseAtpFrame(buffer) { + const packet = struct.unpack(buffer, 0, ATP_HEADER_FORMAT, ['sequenceRx', 'sequenceTx', 'crc32']); + packet.data = buffer.slice(ATP_HEADER_LENGTH); + const crc32 = FreeStyleLibreProtocol.calcCrc32(packet.data); + if (crc32 !== packet.crc32) { + debug('parseAtpFrame: CRC32 did not match:', crc32.toString(16), '!=', packet.crc32.toString(16)); + return null; + } + return packet; + } + + static calcCrc32(buffer) { + // make zero-padded buffer with length that is multiple of 4 + const paddedBuffer = Buffer.alloc((buffer.length + 3) & 0xfffffffc); + Buffer.from(buffer).copy(paddedBuffer); + + let remainder = 0xffffffff; + for (let index = 0; index < paddedBuffer.length / 4; index++) { + let data = paddedBuffer.readUInt32LE(index * 4); + + data ^= remainder; + + for (let i = 0; i < 8; i++) { + remainder = data >>> 28; + data <<= 4; + data ^= CRC32_TABLE[remainder >>> 0]; // use unsigned remainder as index + } + + remainder = data; + } + return remainder >>> 0; // return unsigned remainder + } + /* eslint-enable no-bitwise */ + + buildAtpFrame(aapFrameArray) { + const atpCrc = this.constructor.calcCrc32(aapFrameArray); + const atpFrameArray = new Uint8Array(ATP_HEADER_LENGTH + aapFrameArray.length); + struct.pack(atpFrameArray, 0, `${ATP_HEADER_FORMAT + aapFrameArray.length}B`, + this.sequenceRx, + this.sequenceTx, + atpCrc, + aapFrameArray); + return atpFrameArray; + } + + buildAckFrame(unknownDataOptional) { + let unknownData = unknownDataOptional; + if (unknownData === undefined) { + unknownData = '\x00\x00'; + } + const ackFrameArray = new Uint8Array(ACK_HEADER_LENGTH); + struct.pack(ackFrameArray, 0, ACK_HEADER_FORMAT, + this.sequenceRx, + this.sequenceTx, + unknownData); + return ackFrameArray; + } + + sendAck(unknownData, cbOptional) { + let cb = cbOptional; + if (cb === undefined) { + cb = () => {}; + } + this.lastAckRx = this.sequenceRx; + const ackFrameArray = this.buildAckFrame(unknownData); + this.hidDevice.send(this.constructor.buildHidPacket(COMMAND.ACK_FROM_HOST, ackFrameArray), cb); + } + + sendCommand(command, responseType, data, cb) { + this.hidDevice.send(this.constructor.buildHidPacket(command, data), () => { + this.readResponse(responseType, READ_TIMEOUT, cb); + }); + } + + parseTextResponse(responseData) { + const match = TEXT_RESPONSE_REGEX.exec(responseData); + if (!match) { + return new Error('Invalid text responseData format.'); + } + const data = match[1]; + const checksum = parseInt(match[2], 16); + const result = match[3]; + + if (result === 'OK') { + if (this.constructor.validateTextChecksum(data, checksum)) { + return data.replace(/\r\n$/, ''); + } + return new Error('Invalid checksum.'); + } + return new Error(`Device responseData was not "OK", but "${result}"`); + } + + requestTextResponse(command, successCallback, errorCallback) { + debug(`requestTextResponse: Sending command: 0x${COMMAND.TEXT_REQUEST.toString(16)}`, + ', data: ', command); + this.sendCommand(COMMAND.TEXT_REQUEST, COMMAND.TEXT_RESPONSE, command, (err, responseData) => { + if (err) { + debug('requestTextResponse: error: ', err); + return errorCallback(err, responseData); + } + + const data = this.parseTextResponse(responseData); + if (data instanceof Error) { + return errorCallback(data, responseData); + } + debug(`requestTextResponse: data: "${data}"`); + return successCallback(data); + }); + } + + requestBinaryResponse(opCode, aapData, cb) { + const atpFrameArray = this.buildAtpFrame(this.constructor.buildAapFrame(opCode, aapData)); + this.sequenceTx = (this.sequenceTx + 1) % 0x100; + debug(`requestBinaryResponse: Sending command: 0x${COMMAND.BINARY_REQUEST.toString(16)}`, + ', data: ', Buffer.from(atpFrameArray).toString('hex')); + this.hidDevice.send( + this.constructor.buildHidPacket(COMMAND.BINARY_REQUEST, atpFrameArray), () => { + const flushData = this.buildAtpFrame(this.constructor.buildAapFrame(OP_CODE.FLUSH_BUFFERS)); + this.sequenceTx = (this.sequenceTx + 1) % 0x100; + this.sendCommand(COMMAND.BINARY_REQUEST, COMMAND.BINARY_RESPONSE, flushData, + (err, aapPackets) => { + if (err) { + debug('requestBinaryResponse: error: ', err); + return cb(err, aapPackets); + } + + debug(`requestBinaryResponse: num aapPackets: ${aapPackets.length}`); + return cb(null, aapPackets); + }); + }); + } + + getDBRecords(command, successCallback, errorCallback) { + debug(`getDBRecords: ${command}`); + this.requestTextResponse(command, (data) => { + const match = DB_RECORDS_REGEX.exec(data); + if (!match) { + return errorCallback(new Error('Invalid response format for database records.'), data); + } + let dbRecords = match[1]; + const dbRecordCount = parseInt(match[2], 10); + const dbRecordChecksum = parseInt(match[3], 16); + + if (!this.constructor.validateTextChecksum(dbRecords, dbRecordChecksum)) { + return errorCallback(new Error('Invalid database record checksum.'), data); + } + + dbRecords = dbRecords.split('\r\n'); + + if (dbRecordCount !== dbRecords.length) { + return errorCallback( + new Error(`Invalid database record count: ${dbRecordCount} != ${dbRecords.length}`), data); + } + + return successCallback(null, dbRecords); + }, errorCallback); + } + + // this is currently unused as it uses part of the text protocol not meant for production + getReaderResultData(cb) { + this.getDBRecords('$arresult?', (data) => { + cb(null, { readerResultData: data }); + }, cb); + } + + // this is currently unused as it uses part of the text protocol not meant for production + getHistoricalScanData(cb) { + this.getDBRecords('$history?', (data) => { + cb(null, { historicalScanData: data }); + }, cb); + } + + getDBRecordNumber(cb) { + const command = '$dbrnum?'; + this.requestTextResponse(command, (data) => { + const match = DB_RECORD_NUMBER_REGEX.exec(data); + if (!match) { + return cb(new Error('Invalid response format for database record number.'), data); + } + const dbRecordNumber = parseInt(match[1], 10); + + return cb(null, { dbRecordNumber }); + }, cb); + } + + setCompression(enableCompression, cb) { + this.requestBinaryResponse(OP_CODE.SET_COMPRESSION, [enableCompression], cb); + } + + getDateTime(cb) { + this.requestBinaryResponse(OP_CODE.GET_DATE_TIME, [], cb); + } + + getDbSchema(cb) { + this.requestBinaryResponse(OP_CODE.GET_DB_SCHEMA, [], cb); + } + + getCfgSchema(cb) { + this.requestBinaryResponse(OP_CODE.GET_CFG_SCHEMA, [], cb); + } + + getCfgData(tableNumber, cb) { + this.requestBinaryResponse(OP_CODE.GET_CFG_DATA, [tableNumber], cb); + } + + getDatabase(tableNumber, cb) { + this.requestBinaryResponse(OP_CODE.GET_DATABASE, [tableNumber], cb); + } + + getFirmwareVersion(cb) { + this.requestTextResponse('$swver?', (data) => { + cb(null, { firmwareVersion: data }); + }, cb); + } + + getSerialNumber(cb) { + this.requestTextResponse('$sn?', (data) => { + cb(null, { serialNumber: data }); + }, cb); + } + + initCommunication(cb) { + const initFunctions = [ + (callback) => { this.sendCommand(COMMAND.INIT_REQUEST_1, null, '', callback); }, + (callback) => { this.sendAck(INIT_ACK_MAGIC_DATA, callback); }, + (callback) => { this.sendCommand(COMMAND.INIT_REQUEST_2, null, '', callback); }, + (callback) => { this.sendCommand(COMMAND.INIT_REQUEST_3, null, '', callback); }, + (callback) => { this.sendCommand(COMMAND.INIT_REQUEST_4, null, '', callback); }, + ]; + async.series(initFunctions, (err, result) => { + cb(err, result); + }); + } + + static probe(cb) { + debug(`probe: not using probe for ${DEVICE_MODEL_NAME}`); + cb(); + } +} diff --git a/lib/drivers/abbott/tools/README b/lib/drivers/abbott/tools/README new file mode 100644 index 0000000000..2a8a50cce5 --- /dev/null +++ b/lib/drivers/abbott/tools/README @@ -0,0 +1,4 @@ +To analyze the USB protocol of the Abbott FreeStyle Libre, install this Lua plugin for Wireshark and sniff the traffic on the appropriate usbmon interface. + +To install this plugin, copy it or make a symlink to it in: +~/.wireshark/plugins diff --git a/lib/drivers/abbott/tools/fslibre_usb_dissector.lua b/lib/drivers/abbott/tools/fslibre_usb_dissector.lua new file mode 100644 index 0000000000..cf36cf8727 --- /dev/null +++ b/lib/drivers/abbott/tools/fslibre_usb_dissector.lua @@ -0,0 +1,278 @@ +-- +-- Wireshark plugin to dissect the USB HID packets of the Abbott FreeStyle Libre +-- + +-- needed for debugging this script +--_G.debug = require("debug") +--require("mobdebug").start() +-- + +local fslibre_usb = Proto("fslibre_usb", "Abbott FreeStyle Libre USB Protocol") + +local fslibre_dump = ProtoField.new("FSLibre Dump", "fslibre_usb.dump", ftypes.BYTES) + +local command = ProtoField.new("Command", "fslibre_usb.command", ftypes.UINT8, nil, base.HEX) +local data_length = ProtoField.new("Data Length", "fslibre_usb.data_length", ftypes.UINT8) + +local text = ProtoField.new("Text", "fslibre_usb.text", ftypes.STRING) + +local atp_frame = ProtoField.new("ATP Frame", "fslibre_usb.atp", ftypes.NONE) +local atp_data = ProtoField.new("ATP Data", "fslibre_usb.atp.data", ftypes.BYTES) +local atp_sequence_rx = ProtoField.new("ATP Seq Rx", "fslibre_usb.atp.sequence_rx", ftypes.UINT8) +local atp_sequence_tx = ProtoField.new("ATP Seq Tx", "fslibre_usb.atp.sequence_tx", ftypes.UINT8) +local atp_crc32 = ProtoField.new("ATP CRC32", "fslibre_usb.atp.crc32", ftypes.UINT32, nil, base.HEX) +local atp_crc32bin = ProtoField.new("ATP CRC32 BIN", "fslibre_usb.atp.crc32bin", ftypes.STRING) +local atp_unknown = ProtoField.new("ATP UNKNOWN", "fslibre_usb.atp.unknown", ftypes.UINT16, nil, base.HEX) + +local aap_frame = ProtoField.new("AAP Frame", "fslibre_usb.aap", ftypes.STRING) +local aap_data_length = ProtoField.new("AAP Data Length", "fslibre_usb.aap.data_length", ftypes.UINT32) +local aap_op_code = ProtoField.new("AAP OP Code", "fslibre_usb.aap.op_code", ftypes.UINT8, nil, base.HEX) +local aap_data = ProtoField.new("AAP Data", "fslibre_usb.aap.data", ftypes.BYTES) +local aap_dump = ProtoField.new("AAP Dump", "fslibre_usb.aap.dump", ftypes.BYTES) + + +fslibre_usb.fields = { + fslibre_dump, + command, + text, + atp_frame, + data_length, + atp_data, + atp_sequence_rx, + atp_sequence_tx, + atp_crc32, + atp_crc32bin, + atp_unknown, + aap_frame, + aap_data_length, + aap_op_code, + aap_data, + aap_dump +} + +local HID_REPORT_LENGTH = 64 +local AAP_OPCODE_LENGTH = 1 + +local pkt_state = {} +local partial_aap_buf = {} + +local function to_bits(num, bits) + -- returns a string of bits, most significant first + bits = bits or math.max(1, select(2, math.frexp(num))) + local t = {} -- table containing the bits + for b = bits, 1, -1 do + t[b] = math.fmod(num, 2) + num = math.floor((num - t[b]) / 2) + end + return table.concat(t) -- convert table to string +end + + +local function dissect_aap(atp_payload_buf, atp_tree) + local aap_data_length_num_bytes = 0 + local aap_data_length_value = 0 + -- the first 0 to 3 bytes describe the aap frame length in their lower 7 bits in little endian + for i = 0, 2 do + -- if we are still parsing the length when we reach the buffer's end, we need at least 1 more byte + if i >= atp_payload_buf:len() then + -- show data as partial aap frame + atp_tree:add(aap_frame, atp_payload_buf:range(0), "[partial]: length not known yet") + return -1 + end + + local byte_value = atp_payload_buf:range(i, 1):uint() + -- if highest bit is not set, this is already the command byte + if not bit32.btest(byte_value, 0x80) then + break + end + -- highest bit was set, extract lower 7 bits as length value + local byte_length_value = bit32.band(byte_value, 0x7f) + -- shift these 7 bits to the left depending on the index i + byte_length_value = bit32.lshift(byte_length_value, 7 * i) + -- combine these bits with the previous length value + aap_data_length_value = bit32.bor(byte_length_value, aap_data_length_value) + aap_data_length_num_bytes = aap_data_length_num_bytes + 1 + end + + local aap_data_offset = aap_data_length_num_bytes + AAP_OPCODE_LENGTH + local aap_data_length_in_this_frame = math.min(atp_payload_buf:len() - aap_data_offset, aap_data_length_value) + + -- if aap packet is longer than current buffer, return number of missing bytes as negative number + if aap_data_length_in_this_frame < aap_data_length_value then + -- show data as partial aap frame, with info about missing bytes and possibly opcode + local op_code_info = "" + if atp_payload_buf:len() > aap_data_length_num_bytes then + local aap_op_code_value = atp_payload_buf:range(aap_data_length_num_bytes, 1):uint() + op_code_info = string.format("[OP Code 0x%x]", aap_op_code_value) + end + local description = string.format("[partial]%s %d of %d bytes", + op_code_info, + aap_data_offset + aap_data_length_in_this_frame, + aap_data_offset + aap_data_length_value) + atp_tree:add(aap_frame, atp_payload_buf:range(0), description) + + return - (aap_data_length_value - aap_data_length_in_this_frame) + end + + -- check that opcode does not have the highest bit set, otherwise cancel parsing due to faulty data + local aap_op_code_value = atp_payload_buf:range(aap_data_length_num_bytes, 1):uint() + if bit32.btest(aap_op_code_value, 0x80) then + return 0 + end + + -- add new aap sub-tree + local aap_tree = atp_tree:add(aap_frame, + atp_payload_buf:range(0, aap_data_length_num_bytes + AAP_OPCODE_LENGTH + aap_data_length_in_this_frame), "") + + -- add hidden field that can be used in custom column to show aap packet data + aap_tree:add(aap_dump, atp_payload_buf:range(0, aap_data_offset + aap_data_length_in_this_frame)):set_hidden() + + -- mark the aap data length bytes at the aap frame start + aap_tree:add(aap_data_length, atp_payload_buf:range(0, aap_data_length_num_bytes), aap_data_length_value) + + -- aap op code is the first byte after the aap length + aap_tree:add(aap_op_code, atp_payload_buf:range(aap_data_length_num_bytes, AAP_OPCODE_LENGTH)) + + if aap_data_length_in_this_frame > 0 then + -- aap data bytes start after the op code + aap_tree:add(aap_data, atp_payload_buf:range(aap_data_offset, aap_data_length_in_this_frame)) + end + + return aap_data_offset + aap_data_length_in_this_frame +end + + +local function reassemble_aap(atp_data_buf, pktinfo, atp_tree) + + local aap_buf + local state = pkt_state[pktinfo.number] + + if state ~= nil then + -- we've already processed this packet + aap_buf = ByteArray.tvb(state.buffer, "AAP Buffer") + state.processed = True + else + -- first time processing this packet + state = {} + + if partial_aap_buf[tostring(pktinfo.src)] ~= nil then + partial_aap_buf[tostring(pktinfo.src)]:append(atp_data_buf(0):bytes()) + aap_buf = ByteArray.tvb(partial_aap_buf[tostring(pktinfo.src)], "AAP Buffer") + else + aap_buf = atp_data_buf + end + end + + if state.processed == nil then + state.buffer = aap_buf(0):bytes() + end + + while aap_buf:len() > 0 do + local aap_packet_length = dissect_aap(aap_buf, atp_tree) + + if aap_packet_length == 0 then + + -- error disecting aap + if state.processed == nil then + pkt_state[pktinfo.number] = state + partial_aap_buf[tostring(pktinfo.src)] = nil + end + return + + elseif aap_packet_length < 0 then + + -- we don't have all the data we need yet + if state.processed == nil then + -- save remaining data + partial_aap_buf[tostring(pktinfo.src)] = aap_buf(0):bytes() + pkt_state[pktinfo.number] = state + end + return + + else + -- consumed one aap packet from aap_buf, remove it and continue parsing + aap_buf = aap_buf:range(aap_packet_length):tvb() + end + end + + if state.processed == nil then + -- emptied aap_buf without any remaining partial aap data + pkt_state[pktinfo.number] = state + partial_aap_buf[tostring(pktinfo.src)] = nil + end +end + + + +function fslibre_usb.dissector(tvbuf, pktinfo, root) + + pktinfo.cols.protocol:set("fslibre_usb") + local pktlen = tvbuf:len() + + if pktlen < HID_REPORT_LENGTH then + -- tell Wireshark that this packet was not for us + return 0 + end + + -- cut off leading data before the actual HID report, which is always 64 bytes + local hid_report_buf = tvbuf:range(pktlen - HID_REPORT_LENGTH, HID_REPORT_LENGTH):tvb() + + pktinfo.cols.protocol = fslibre_usb.name + + local command_value = hid_report_buf:range(0, 1):uint() + local data_length_value = hid_report_buf:range(1, 1):uint() + + local data_offset = 2 + local tree = root:add(fslibre_usb, hid_report_buf:range(0, data_offset + data_length_value)) + + -- add hidden field that can be used in custom column to show whole packet data + tree:add(fslibre_dump, hid_report_buf:range(0, data_offset + data_length_value)):set_hidden() + + -- actually the command is only in the lower 6 bits of the first byte, + -- but the 2 high bits are currently always 0 anyhow + tree:add(command, hid_report_buf:range(0, 1)) + + tree:add(data_length, hid_report_buf:range(1, 1)) + + if data_length_value > 0 then + if command_value == 0x60 or command_value == 0x21 or command_value == 0x06 or command_value == 0x35 then + tree:add(text, hid_report_buf:range(data_offset, data_length_value)) + + else + local atp_tree = tree:add(atp_frame, hid_report_buf:range(data_offset, data_length_value)) + + local atp_buf = hid_report_buf:range(data_offset, data_length_value):tvb() + atp_tree:add(atp_data, atp_buf:range()):set_hidden() + + if data_length_value >= 2 then + atp_tree:add(atp_sequence_rx, atp_buf:range(0, 1)) + atp_tree:add(atp_sequence_tx, atp_buf:range(1, 1)) + + if data_length_value >= 6 then + atp_tree:add(atp_crc32, atp_buf:range(2, 4)) + + local crc32_value = atp_buf:range(2, 4):uint() + atp_tree:add(atp_crc32bin, atp_buf:range(2, 4), to_bits(crc32_value, 32)):set_hidden() + + if data_length_value > 6 then + reassemble_aap(atp_buf:range(6):tvb(), pktinfo, atp_tree) + end + + elseif data_length_value == 4 then + atp_tree:add(atp_unknown, atp_buf:range(2, 2)) + end + end + end + end +end + + +function fslibre_usb.init() + -- needed to track the state of aap reassembly + pkt_state = {} + partial_aap_buf = {} + + -- register this disector for USB vendor:product 1a61:3650 + DissectorTable.get("usb.product"):add(0x1a613650, fslibre_usb) +end + diff --git a/lib/drivers/abbott/tools/lua_debug.sh b/lib/drivers/abbott/tools/lua_debug.sh new file mode 100755 index 0000000000..e7bec69a04 --- /dev/null +++ b/lib/drivers/abbott/tools/lua_debug.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +# This script helps with debugging a Lua Wireshark plugin on Linux (and possibly OSX). +# Before using it, install Zero Brane Studio IDE and start its Debugger Server. +# Open the plugin script to be debugged in the IDE from its install path (e.g. the Wireshark plugin dir). +# Make sure the ZBS variable is set to the install path of the IDE: +export ZBS=/opt/zbstudio + +# In your Lua plugin add these two lines at the very top: +# _G.debug = require("debug") +# require("mobdebug").start() + +# Run this script and it will stop at the above lines in the debugger. + + + +if [[ "$(uname -m)" == "x86_64" ]]; then ARCH="x64"; else ARCH="x86"; fi + +export LUA_PATH="./?.lua;$ZBS/lualibs/?/?.lua;$ZBS/lualibs/?.lua" +export LUA_CPATH="$ZBS/bin/?.so;$ZBS/bin/linux/$ARCH/clibs52/?.so" + +if [ "$#" -ne 1 ]; then + echo "usage: $0 PCAP_FILE" + exit +fi + +# If the plugin is loaded automatically by Wireshark from one of its plugin directories: +tshark -r $1 +#wireshark -r $1 + +# If the plugin is to be manually loaded: +#tshark -X lua_script:fslibre_usb_dissector.lua -r $1 +#wireshark -X lua_script:fslibre_usb_dissector.lua -r $1 diff --git a/lib/struct.js b/lib/struct.js index 7042c07695..1ac5ae07e7 100644 --- a/lib/struct.js +++ b/lib/struct.js @@ -23,6 +23,7 @@ // H -- a 2-byte signed integer in big-endian format // z -- a zero-terminated string of maximum length controlled by the size parameter. // Z -- a string of bytes with the length controlled by the size parameter. +// B -- an array of bytes with the length controlled by the size parameter. // f -- a 4-byte float in little-endian format // F -- a 4-byte float in big-endian format // . -- the appropriate number of bytes is ignored (used for padding) @@ -61,6 +62,9 @@ var _ = require('lodash'); module.exports = function() { var extractString = function(bytes, start, len) { + if (start === undefined) { + start = 0; + } if (!len) { len = bytes.length; } @@ -204,6 +208,13 @@ module.exports = function() { var storeByte = function(v, b, st) { b[st] = v & 0xFF; }; + var storeBytes = function(v, b, st) { + var i=0; + while (i { + if (v !== null && typeof v === 'object' && 'type' in v && + v.type === 'Buffer' && 'data' in v && Array.isArray(v.data)) { + // re-create Buffer objects for data fields of aapPackets + return new Buffer(v.data); + } + return v; + }); +} + +describe('freeStyleLibreData.js', () => { + const cfg = { + builder: builder(), + timezone: 'Europe/Berlin' + }; + + describe('non-static', () => { + let fsLibreData; + + beforeEach(function(){ + fsLibreData = new FreeStyleLibreData(cfg); + }); + + describe('process AAP packets', () => { + + it('should correctly restore 32bit records numbers', () => { + const historicalRecordJson = ` + { + "dataLength": 21, + "opCode": 49, + "packetLength": 23, + "data": {"type":"Buffer","data":[6,164,144,12,128,83,91,16,5,220,205,255,255,75,0,224,31,0,0,69,248]} + } + `; + const data = { + aapPackets: [ + deserialize(factoryConfigJson), + deserialize(historicalRecordJson) + ] + }; + + // potentially problematic 32bit record numbers + const recordNumbers = [0, 1, 0xfffe, 0xffff, 0x10000, 0x10001, 0xffffffff]; + + recordNumbers.forEach(recordNumber => { + // use lower 16bits to replace 16bit record number in historical record + data.aapPackets[1].data[1] = recordNumber & 0xff; + data.aapPackets[1].data[2] = (recordNumber >> 8) & 0xff; + + // use recordNumber + 1 as current DB record number (which will be used for next record written) + const postRecords = fsLibreData.processAapPackets(data.aapPackets, recordNumber + 1); + + // check if valid record was returned and contains correct 32bit record number + expect(postRecords.length).equals(1); + expect(postRecords[0].index).equals(recordNumber); + }); + }); + + }); + + }); + + describe('static', () => { + + describe('validate annotations', () => { + it('should only annotate out-of-range BG values', () => { + const inputData = [ + [GLUCOSE_LO, true], + [GLUCOSE_HI, true], + [GLUCOSE_LO - 1, true], + [GLUCOSE_HI + 1, true], + [GLUCOSE_LO + 1, false], + [GLUCOSE_HI - 1, false], + ]; + inputData.forEach(([value, expectedResult]) => { + const cbg = cfg.builder.makeCBG() + .with_value(value) + .with_units('mg/dL'); + + FreeStyleLibreData.addOutOfRangeAnnotation(cbg, GLUCOSE_LO, GLUCOSE_HI, 1, 'bg'); + + const isAnnotated = annotate.isAnnotated(cbg, 'bg/out-of-range'); + + expect(isAnnotated).equals(expectedResult); + }); + }); + }); + + }); +}); diff --git a/test/lib/abbot/testFreeStyleLibreProtocol.js b/test/lib/abbot/testFreeStyleLibreProtocol.js new file mode 100644 index 0000000000..8d05a72f74 --- /dev/null +++ b/test/lib/abbot/testFreeStyleLibreProtocol.js @@ -0,0 +1,133 @@ +/* + * == BSD2 LICENSE == + * Copyright (c) 2017, Tidepool Project + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the associated License, which is identical to the BSD 2-Clause + * License as published by the Open Source Initiative at opensource.org. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the License for more details. + * + * You should have received a copy of the License along with this program; if + * not, you can obtain one from Tidepool Project at tidepool.org. + * == BSD2 LICENSE == + */ + +/* global beforeEach, describe, it */ + +import {expect} from 'salinity'; + +import FreeStyleLibreProtocol from '../../../lib/drivers/abbott/freeStyleLibreProtocol'; + +describe('freeStyleLibreProtocol.js', () => { + const cfg = {}; + + describe('non-static', () => { + let protocol; + + beforeEach(function(){ + protocol = new FreeStyleLibreProtocol(cfg); + }); + + describe('parse text responses', () => { + + it('should parse and return valid text responses', () => { + const inputData = [ + ['DB Record Number = 226988\r\nCKSM:00000765\r\nCMD OK\r\n', 'DB Record Number = 226988'], + ['2.1.2\r\nCKSM:00000108\r\nCMD OK\r\n', '2.1.2'] + ]; + inputData.forEach(([data, expectedResult]) => { + const result = protocol.parseTextResponse(data); + expect(result).deep.equals(expectedResult); + } + ); + }); + + + it('should not accept invalid text responses', () => { + const inputData = [ + ['DB Record Number = 226988\r\nCKSM:00000765\r\nCMD Fail!\r\n', Error], + ['2.1.2\r\nCKSM:00000111\r\nCMD OK\r\n', Error], + ['2.1.2\r\nCKSM:108\r\nCMD OK\r\n', Error] + ]; + inputData.forEach(([data, expectedResult]) => { + const result = protocol.parseTextResponse(data); + expect(result).instanceof(expectedResult); + } + ); + }); + }); + + }); + + describe('static', () => { + + describe('validate binary protocol checksum', () => { + + it('should produce valid checksums', () => { + // data captured using Wireshark: mapping from AAP packet string to its corresponding ATP CRC32 + const ATP_CRC_LOOKUP = { + '\x34': 0x0032c637, + '\x54': 0xac9700a0, + '\x41': 0xf743b0bb, + '\x7d': 0x167d464f, + '\x81\x51\x01': 0x281fba26, + '\x81\x51\x02': 0x2a764faf, + '\x81\x51\x03': 0x2baee328, + '\x81\x51\x04': 0x2ea5a4bd, + '\x81\x51\x05': 0x2f7d083a, + '\x81\x51\x06': 0x2d14fdb3, + '\x81\x51\x07': 0x2ccc5134, + '\x81\x51\x08': 0x27027299, + '\x81\x51\x09': 0x26dade1e, + '\x81\x51\x0a': 0x24b32b97, + '\x81\x31\x00': 0x48224ccb, + '\x81\x31\x01': 0x49fae04c, + '\x81\x31\x06': 0x4cf1a7d9, + '\x81\x31\x07': 0x4d290b5e, + '\x81\x60\x01': 0xcaf4d6cf + }; + Object.keys(ATP_CRC_LOOKUP).forEach(key => { + const expectedChecksum = ATP_CRC_LOOKUP[key]; + const buffer = new Buffer(key, 'binary'); + const calculatedChecksum = FreeStyleLibreProtocol.calcCrc32(buffer); + expect(calculatedChecksum).equals(expectedChecksum); + } + ); + }); + }); + + describe('validate text protocol checksum', () => { + + it('should accept valid checksums', () => { + const inputData = [ + ['', 0], + ['\x01\x02\x03\x04\x05', 15] + ]; + inputData.forEach(([data, checksum]) => { + const result = FreeStyleLibreProtocol.validateTextChecksum(data, checksum); + expect(result).deep.equals(true); + } + ); + }); + + it('should decline invalid checksums', () => { + const inputData = [ + ['', 10], + ['\x01\x02\x03\x04\x05', 0], + ['', null], + ['', undefined], + ['', ''], + ]; + inputData.forEach(([data, checksum]) => { + const result = FreeStyleLibreProtocol.validateTextChecksum(data, checksum); + expect(result).deep.equals(false); + } + ); + }); + }); + + }); +}); diff --git a/test/lib/testStruct.js b/test/lib/testStruct.js index dc01866b54..603a368dd1 100644 --- a/test/lib/testStruct.js +++ b/test/lib/testStruct.js @@ -130,22 +130,7 @@ describe('struct.js', function(){ expect(theStruct).itself.to.respondTo('copyBytes'); }); }); -// The legal type characters are: -// b -- a 1-byte unsigned value -// y -- a 1-byte signed value -// s -- a 2-byte unsigned short in little-endian format (0x01 0x00 is returned as 1, not 256) -// S -- a 2-byte unsigned short in big-endian format (0x01 0x00 is returned as 256, not 1) -// i -- a 4-byte unsigned integer in little-endian format -// I -- a 4-byte unsigned integer in big-endian format -// n -- a 4-byte signed integer in little-endian format -// N -- a 4-byte signed integer in big-endian format -// h -- a 2-byte signed integer in little-endian format -// H -- a 2-byte signed integer in big-endian format -// z -- a zero-terminated string of maximum length controlled by the size parameter. -// Z -- a string of bytes with the length controlled by the size parameter. -// f -- a 4-byte float in little-endian format -// F -- a 4-byte float in big-endian format -// . -- the appropriate number of bytes is ignored (used for padding) + // The legal type characters are and their meaning are described at the top of the struct.js file. describe('structlen and format parsing', function(){ it('work properly', function(){ expect(theStruct.structlen('b6.Si')).to.equal(13); @@ -196,6 +181,15 @@ describe('struct.js', function(){ var s = String.fromCharCode.apply(null, buf); expect(s).to.equal('\u0055\u0055\u0055\u0055\u00AA\u00AA\u00AA\u00AA\u00FF\u00FF\u00FF\u00FF\u0000\u0000\u0000\u0000'); }); + it('works for B', function(){ + var buf = new Uint8Array(4); + var inputBuf = [1, 2, 3, 4]; + var len = theStruct.pack(buf, 0, '4B', inputBuf); + expect(len).to.equal(4); + var result = String.fromCharCode.apply(null, buf); + var expected = String.fromCharCode.apply(null, inputBuf); + expect(result).to.equal(expected); + }); it('works for z', function(){ var buf = new Uint8Array(8); var len = theStruct.pack(buf, 0, '8z', 'banana'); diff --git a/yarn.lock b/yarn.lock index b96cd73d23..2d2676cc3b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -135,6 +135,10 @@ ansi-escapes@^1.1.0, ansi-escapes@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e" +ansi-escapes@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-2.0.0.tgz#5bae52be424878dd9783e8910e3fc2922e83c81b" + ansi-html@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e" @@ -238,9 +242,9 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" -aria-query@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-0.3.0.tgz#cb8a9984e2862711c83c80ade5b8f5ca0de2b467" +aria-query@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-0.7.0.tgz#4af10a1e61573ddea0cf3b99b51c52c05b424d24" dependencies: ast-types-flow "0.0.7" @@ -274,6 +278,13 @@ array-flatten@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" +array-includes@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.0.3.tgz#184b48f62d92d7452bb31b323165c7f8bd02266d" + dependencies: + define-properties "^1.1.2" + es-abstract "^1.7.0" + array-index@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/array-index/-/array-index-1.0.0.tgz#ec56a749ee103e4e08c790b9c353df16055b97f9" @@ -295,13 +306,6 @@ array-unique@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53" -array.prototype.find@^2.0.1: - version "2.0.4" - resolved "https://registry.yarnpkg.com/array.prototype.find/-/array.prototype.find-2.0.4.tgz#556a5c5362c08648323ddaeb9de9d14bc1864c90" - dependencies: - define-properties "^1.1.2" - es-abstract "^1.7.0" - arrify@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" @@ -431,13 +435,19 @@ axe-core@2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-2.2.0.tgz#00b410b3fc899207d4f2f8e3753cff150d34e4bb" +axobject-query@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-0.1.0.tgz#62f59dbc59c9f9242759ca349960e7a2fe3c36c0" + dependencies: + ast-types-flow "0.0.7" + babar@0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/babar/-/babar-0.0.3.tgz#2f394d4a5918f7e1ae9e5408e9a96f3f935ee1e2" dependencies: colors "~0.6.2" -babel-code-frame@^6.11.0, babel-code-frame@^6.16.0, babel-code-frame@^6.22.0, babel-code-frame@^6.26.0: +babel-code-frame@^6.11.0, babel-code-frame@^6.22.0, babel-code-frame@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" dependencies: @@ -771,19 +781,20 @@ babel-plugin-minify-type-constructors@^0.0.4: dependencies: babel-helper-is-void-0 "^0.0.1" +babel-plugin-module-resolver@2.7.1: + version "2.7.1" + resolved "https://registry.yarnpkg.com/babel-plugin-module-resolver/-/babel-plugin-module-resolver-2.7.1.tgz#18be3c42ddf59f7a456c9e0512cd91394f6e4be1" + dependencies: + find-babel-config "^1.0.1" + glob "^7.1.1" + resolve "^1.2.0" + babel-plugin-react-transform@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/babel-plugin-react-transform/-/babel-plugin-react-transform-2.0.2.tgz#515bbfa996893981142d90b1f9b1635de2995109" dependencies: lodash "^4.6.1" -babel-plugin-resolver@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/babel-plugin-resolver/-/babel-plugin-resolver-1.1.0.tgz#e67c1c1d567fb905434cb587cbdb49fa6e6dd099" - dependencies: - babel-resolver "^1.1.0" - lodash "^4.6.0" - babel-plugin-rewire@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/babel-plugin-rewire/-/babel-plugin-rewire-1.1.0.tgz#a6b966d9d8c06c03d95dcda2eec4e2521519549b" @@ -1463,10 +1474,6 @@ babel-register@^6.16.3, babel-register@^6.24.1, babel-register@^6.26.0, babel-re mkdirp "^0.5.1" source-map-support "^0.4.15" -babel-resolver@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/babel-resolver/-/babel-resolver-1.1.0.tgz#ad705d1a67345840839ec36ab33a0238f307fc41" - babel-runtime@6.18.0: version "6.18.0" resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.18.0.tgz#0f4177ffd98492ef13b9f823e9994a02584c9078" @@ -1590,6 +1597,10 @@ binary@^0.3.0: buffers "~0.1.1" chainsaw "~0.1.0" +bindings@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.2.1.tgz#14ad6113812d2d37d72e67b4cacb4bb726505f11" + bl@^1.0.0: version "1.2.1" resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.1.tgz#cac328f7bee45730d404b692203fcb590e172d5e" @@ -1740,12 +1751,12 @@ browserify-zlib@^0.1.4: dependencies: pako "~0.2.0" -browserslist@1.7.5: - version "1.7.5" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-1.7.5.tgz#eca4713897b51e444283241facf3985de49a9e2b" +browserslist@2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-2.1.4.tgz#cc526af4a1312b7d2e05653e56d0c8ab70c0e053" dependencies: - caniuse-db "^1.0.30000624" - electron-to-chromium "^1.2.3" + caniuse-lite "^1.0.30000670" + electron-to-chromium "^1.3.11" browserslist@^1.3.6, browserslist@^1.4.0, browserslist@^1.5.2, browserslist@^1.7.6: version "1.7.7" @@ -1847,14 +1858,18 @@ caniuse-api@^1.5.2: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-db@1.0.30000626: - version "1.0.30000626" - resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000626.tgz#44363dc86857efaf758fea9faef6a15ed93d8f33" +caniuse-db@1.0.30000671: + version "1.0.30000671" + resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000671.tgz#9f071bbc7b96994638ccbaf47829d58a1577a8ed" -caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000624, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639: +caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639: version "1.0.30000718" resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000718.tgz#86cdd97987302554934c61e106f4e470f16f993c" +caniuse-lite@^1.0.30000670: + version "1.0.30000718" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000718.tgz#0dd24290beb11310b2d80f6b70a823c2a65a6fad" + capture-stack-trace@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz#4a6fa07399c26bba47f0b2496b4d0fb408c5550d" @@ -1915,7 +1930,7 @@ chalk@1.1.3, chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" -chalk@^2.0.1, chalk@^2.1.0: +chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.1.0.tgz#ac5becf14fa21b99c6c92ca7a7d7cfd5b17e743e" dependencies: @@ -2029,7 +2044,7 @@ cli-boxes@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143" -cli-cursor@^1.0.1, cli-cursor@^1.0.2: +cli-cursor@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987" dependencies: @@ -2220,7 +2235,7 @@ concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" -concat-stream@1.6.0, concat-stream@^1.4.6, concat-stream@^1.5.2: +concat-stream@1.6.0, concat-stream@^1.4.6, concat-stream@^1.5.2, concat-stream@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7" dependencies: @@ -2605,13 +2620,13 @@ dateformat@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-2.0.0.tgz#2743e3abb5c3fc2462e527dca445e04e9f4dee17" -debug@2, debug@2.6.8, debug@^2.1.1, debug@^2.1.3, debug@^2.2.0, debug@^2.4.5, debug@^2.5.1, debug@^2.6.1, debug@^2.6.6, debug@^2.6.8: +debug@2, debug@2.6.8, debug@^2.1.1, debug@^2.1.3, debug@^2.2.0, debug@^2.3.2, debug@^2.4.5, debug@^2.5.1, debug@^2.6.1, debug@^2.6.6, debug@^2.6.8: version "2.6.8" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc" dependencies: ms "2.0.0" -debug@2.2.0: +debug@2.2.0, debug@~2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da" dependencies: @@ -2797,7 +2812,7 @@ difflet@1.0.1: deep-is "0.1.x" traverse "0.6.x" -doctrine@1.5.0, doctrine@^1.2.2: +doctrine@1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa" dependencies: @@ -3082,7 +3097,7 @@ electron-rebuild@1.5.7: spawn-rx "^2.0.7" yargs "^3.31.0" -electron-to-chromium@^1.2.3, electron-to-chromium@^1.2.7: +electron-to-chromium@^1.2.7, electron-to-chromium@^1.3.11: version "1.3.18" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.18.tgz#3dcc99da3e6b665f6abbc71c28ad51a2cd731a9c" @@ -3231,7 +3246,7 @@ es5-ext@^0.10.14, es5-ext@^0.10.9, es5-ext@~0.10.14: es6-iterator "2" es6-symbol "~3.1" -es6-iterator@2, es6-iterator@^2.0.1, es6-iterator@~2.0.1: +es6-iterator@2: version "2.0.1" resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.1.tgz#8e319c9f0453bf575d374940a655920e59ca5512" dependencies: @@ -3239,47 +3254,17 @@ es6-iterator@2, es6-iterator@^2.0.1, es6-iterator@~2.0.1: es5-ext "^0.10.14" es6-symbol "^3.1" -es6-map@^0.1.3: - version "0.1.5" - resolved "https://registry.yarnpkg.com/es6-map/-/es6-map-0.1.5.tgz#9136e0503dcc06a301690f0bb14ff4e364e949f0" - dependencies: - d "1" - es5-ext "~0.10.14" - es6-iterator "~2.0.1" - es6-set "~0.1.5" - es6-symbol "~3.1.1" - event-emitter "~0.3.5" - es6-promise@^4.0.5: version "4.1.1" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.1.1.tgz#8811e90915d9a0dba36274f0b242dbda78f9c92a" -es6-set@~0.1.5: - version "0.1.5" - resolved "https://registry.yarnpkg.com/es6-set/-/es6-set-0.1.5.tgz#d2b3ec5d4d800ced818db538d28974db0a73ccb1" - dependencies: - d "1" - es5-ext "~0.10.14" - es6-iterator "~2.0.1" - es6-symbol "3.1.1" - event-emitter "~0.3.5" - -es6-symbol@3.1.1, es6-symbol@^3.0.2, es6-symbol@^3.1, es6-symbol@^3.1.1, es6-symbol@~3.1, es6-symbol@~3.1.1: +es6-symbol@^3.0.2, es6-symbol@^3.1, es6-symbol@~3.1: version "3.1.1" resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.1.tgz#bf00ef4fdab6ba1b46ecb7b629b4c7ed5715cc77" dependencies: d "1" es5-ext "~0.10.14" -es6-weak-map@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.2.tgz#5e3ab32251ffd1538a1f8e5ffa1357772f92d96f" - dependencies: - d "1" - es5-ext "^0.10.14" - es6-iterator "^2.0.1" - es6-symbol "^3.1.1" - escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" @@ -3299,26 +3284,17 @@ escodegen@^1.6.1: optionalDependencies: source-map "~0.2.0" -escope@^3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/escope/-/escope-3.6.0.tgz#e01975e812781a163a6dadfdd80398dc64c889c3" - dependencies: - es6-map "^0.1.3" - es6-weak-map "^2.0.1" - esrecurse "^4.1.0" - estraverse "^4.1.1" - -eslint-config-airbnb-base@^11.1.0: +eslint-config-airbnb-base@^11.3.0: version "11.3.2" resolved "https://registry.yarnpkg.com/eslint-config-airbnb-base/-/eslint-config-airbnb-base-11.3.2.tgz#8703b11abe3c88ac7ec2b745b7fdf52e00ae680a" dependencies: eslint-restricted-globals "^0.1.1" -eslint-config-airbnb@14.1.0: - version "14.1.0" - resolved "https://registry.yarnpkg.com/eslint-config-airbnb/-/eslint-config-airbnb-14.1.0.tgz#355d290040bbf8e00bf8b4b19f4b70cbe7c2317f" +eslint-config-airbnb@15.1.0: + version "15.1.0" + resolved "https://registry.yarnpkg.com/eslint-config-airbnb/-/eslint-config-airbnb-15.1.0.tgz#fd432965a906e30139001ba830f58f73aeddae8e" dependencies: - eslint-config-airbnb-base "^11.1.0" + eslint-config-airbnb-base "^11.3.0" eslint-formatter-pretty@1.1.0: version "1.1.0" @@ -3330,13 +3306,12 @@ eslint-formatter-pretty@1.1.0: plur "^2.1.2" string-width "^2.0.0" -eslint-import-resolver-node@^0.2.0: - version "0.2.3" - resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.2.3.tgz#5add8106e8c928db2cba232bcd9efa846e3da16c" +eslint-import-resolver-node@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.1.tgz#4422574cde66a9a7b099938ee4d508a199e0e3cc" dependencies: - debug "^2.2.0" - object-assign "^4.0.1" - resolve "^1.1.6" + debug "^2.6.8" + resolve "^1.2.0" eslint-import-resolver-webpack@0.8.1: version "0.8.1" @@ -3354,113 +3329,128 @@ eslint-import-resolver-webpack@0.8.1: resolve "^1.2.0" semver "^5.3.0" -eslint-module-utils@^2.0.0: +eslint-module-utils@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.1.1.tgz#abaec824177613b8a95b299639e1b6facf473449" dependencies: debug "^2.6.8" pkg-dir "^1.0.0" -eslint-plugin-compat@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/eslint-plugin-compat/-/eslint-plugin-compat-1.0.2.tgz#914a8fb93a96950140ff902ad2890930e901046c" +eslint-plugin-compat@1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/eslint-plugin-compat/-/eslint-plugin-compat-1.0.4.tgz#76e52038119a5080e2612cc4141d687f4d140398" dependencies: babel-runtime "^6.23.0" - browserslist "1.7.5" - caniuse-db "1.0.30000626" + browserslist "2.1.4" + caniuse-db "1.0.30000671" requireindex "^1.1.0" -eslint-plugin-import@2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.2.0.tgz#72ba306fad305d67c4816348a4699a4229ac8b4e" +eslint-plugin-import@2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.7.0.tgz#21de33380b9efb55f5ef6d2e210ec0e07e7fa69f" dependencies: builtin-modules "^1.1.1" contains-path "^0.1.0" - debug "^2.2.0" + debug "^2.6.8" doctrine "1.5.0" - eslint-import-resolver-node "^0.2.0" - eslint-module-utils "^2.0.0" + eslint-import-resolver-node "^0.3.1" + eslint-module-utils "^2.1.1" has "^1.0.1" lodash.cond "^4.3.0" minimatch "^3.0.3" - pkg-up "^1.0.0" + read-pkg-up "^2.0.0" -eslint-plugin-jsx-a11y@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-4.0.0.tgz#779bb0fe7b08da564a422624911de10061e048ee" +eslint-plugin-jsx-a11y@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-5.1.1.tgz#5c96bb5186ca14e94db1095ff59b3e2bd94069b1" dependencies: - aria-query "^0.3.0" + aria-query "^0.7.0" + array-includes "^3.0.3" ast-types-flow "0.0.7" + axobject-query "^0.1.0" damerau-levenshtein "^1.0.0" emoji-regex "^6.1.0" - jsx-ast-utils "^1.0.0" - object-assign "^4.0.1" + jsx-ast-utils "^1.4.0" + +eslint-plugin-lodash@2.4.3: + version "2.4.3" + resolved "https://registry.yarnpkg.com/eslint-plugin-lodash/-/eslint-plugin-lodash-2.4.3.tgz#8d6f7efebdeaeed0f338cf28405efddca247fc64" + dependencies: + lodash "~4.17.0" -eslint-plugin-mocha@4.9.0: - version "4.9.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-mocha/-/eslint-plugin-mocha-4.9.0.tgz#917a8b499ab8d0c01d69c6e4f81d362ee099b6fd" +eslint-plugin-mocha@4.11.0: + version "4.11.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-mocha/-/eslint-plugin-mocha-4.11.0.tgz#91193a2f55e20a5e35974054a0089d30198ee578" dependencies: - ramda "^0.23.0" + ramda "^0.24.1" eslint-plugin-promise@3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-3.5.0.tgz#78fbb6ffe047201627569e85a6c5373af2a68fca" -eslint-plugin-react@6.10.3: - version "6.10.3" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-6.10.3.tgz#c5435beb06774e12c7db2f6abaddcbf900cd3f78" +eslint-plugin-react@7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.3.0.tgz#ca9368da36f733fbdc05718ae4e91f778f38e344" dependencies: - array.prototype.find "^2.0.1" - doctrine "^1.2.2" + doctrine "^2.0.0" has "^1.0.1" - jsx-ast-utils "^1.3.4" - object.assign "^4.0.4" + jsx-ast-utils "^2.0.0" + prop-types "^15.5.10" eslint-restricted-globals@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/eslint-restricted-globals/-/eslint-restricted-globals-0.1.1.tgz#35f0d5cbc64c2e3ed62e93b4b1a7af05ba7ed4d7" -eslint@3.19.0: - version "3.19.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-3.19.0.tgz#c8fc6201c7f40dd08941b87c085767386a679acc" +eslint-scope@^3.7.1: + version "3.7.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-3.7.1.tgz#3d63c3edfda02e06e01a452ad88caacc7cdcb6e8" dependencies: - babel-code-frame "^6.16.0" - chalk "^1.1.3" - concat-stream "^1.5.2" - debug "^2.1.1" + esrecurse "^4.1.0" + estraverse "^4.1.1" + +eslint@4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.5.0.tgz#bb75d3b8bde97fb5e13efcd539744677feb019c3" + dependencies: + ajv "^5.2.0" + babel-code-frame "^6.22.0" + chalk "^2.1.0" + concat-stream "^1.6.0" + cross-spawn "^5.1.0" + debug "^2.6.8" doctrine "^2.0.0" - escope "^3.6.0" - espree "^3.4.0" + eslint-scope "^3.7.1" + espree "^3.5.0" esquery "^1.0.0" estraverse "^4.2.0" esutils "^2.0.2" file-entry-cache "^2.0.0" - glob "^7.0.3" - globals "^9.14.0" - ignore "^3.2.0" + functional-red-black-tree "^1.0.1" + glob "^7.1.2" + globals "^9.17.0" + ignore "^3.3.3" imurmurhash "^0.1.4" - inquirer "^0.12.0" - is-my-json-valid "^2.10.0" + inquirer "^3.0.6" is-resolvable "^1.0.0" - js-yaml "^3.5.1" - json-stable-stringify "^1.0.0" + js-yaml "^3.9.1" + json-stable-stringify "^1.0.1" levn "^0.3.0" - lodash "^4.0.0" - mkdirp "^0.5.0" + lodash "^4.17.4" + minimatch "^3.0.2" + mkdirp "^0.5.1" natural-compare "^1.4.0" optionator "^0.8.2" - path-is-inside "^1.0.1" - pluralize "^1.2.1" - progress "^1.1.8" - require-uncached "^1.0.2" - shelljs "^0.7.5" - strip-bom "^3.0.0" + path-is-inside "^1.0.2" + pluralize "^4.0.0" + progress "^2.0.0" + require-uncached "^1.0.3" + semver "^5.3.0" + strip-ansi "^4.0.0" strip-json-comments "~2.0.1" - table "^3.7.8" + table "^4.0.1" text-table "~0.2.0" - user-home "^2.0.0" -espree@^3.4.0: +espree@^3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.0.tgz#98358625bdd055861ea27e2867ea729faf463d8d" dependencies: @@ -3504,13 +3494,6 @@ etag@~1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.0.tgz#6f631aef336d6c46362b51764044ce216be3c051" -event-emitter@~0.3.5: - version "0.3.5" - resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39" - dependencies: - d "1" - es5-ext "~0.10.14" - eventemitter3@1.x.x: version "1.2.0" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-1.2.0.tgz#1c86991d816ad1e504750e73874224ecf3bec508" @@ -3640,7 +3623,7 @@ extend@~3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" -external-editor@^2.0.1: +external-editor@^2.0.1, external-editor@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-2.0.4.tgz#1ed9199da9cbfe2ef2f7a31b2fde8b0d12368972" dependencies: @@ -3741,13 +3724,6 @@ fd-slicer@~1.0.1: dependencies: pend "~1.2.0" -figures@^1.3.5: - version "1.7.0" - resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" - dependencies: - escape-string-regexp "^1.0.5" - object-assign "^4.1.0" - figures@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" @@ -3800,6 +3776,13 @@ finalhandler@~1.0.0, finalhandler@~1.0.4: statuses "~1.3.1" unpipe "~1.0.0" +find-babel-config@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/find-babel-config/-/find-babel-config-1.1.0.tgz#acc01043a6749fec34429be6b64f542ebb5d6355" + dependencies: + json5 "^0.5.1" + path-exists "^3.0.0" + find-cache-dir@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-0.1.1.tgz#c8defae57c8a52a8a784f9e31c57c742e993a0b9" @@ -4039,7 +4022,7 @@ fsevents@^1.0.0: nan "^2.3.0" node-pre-gyp "^0.6.36" -fstream-ignore@^1.0.0, fstream-ignore@^1.0.5: +fstream-ignore@^1.0.0, fstream-ignore@^1.0.5, fstream-ignore@~1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/fstream-ignore/-/fstream-ignore-1.0.5.tgz#9c31dae34767018fe1d249b24dada67d092da105" dependencies: @@ -4071,8 +4054,8 @@ fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2, fstream@~1.0.10, fstream@~1.0.8 rimraf "2" function-bind@^1.0.2, function-bind@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.0.tgz#16176714c801798e4e8f2cf7f7529467bb4a5771" + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" function.prototype.name@^1.0.0: version "1.0.3" @@ -4082,6 +4065,10 @@ function.prototype.name@^1.0.0: function-bind "^1.1.0" is-callable "^1.1.3" +functional-red-black-tree@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" + gauge@~1.2.5: version "1.2.7" resolved "https://registry.yarnpkg.com/gauge/-/gauge-1.2.7.tgz#e9cec5483d3d4ee0ef44b60a7d99e4935e136d93" @@ -4243,7 +4230,7 @@ glob@^6.0.4: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@~7.1.1: +glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@~7.1.1: version "7.1.2" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" dependencies: @@ -4288,7 +4275,7 @@ global@^4.3.0: min-document "^2.19.0" process "~0.5.1" -globals@^9.14.0, globals@^9.18.0: +globals@^9.17.0, globals@^9.18.0: version "9.18.0" resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" @@ -4645,7 +4632,7 @@ iferr@^0.1.5, iferr@~0.1.5: version "0.1.5" resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501" -ignore@^3.2.0: +ignore@^3.3.3: version "3.3.4" resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.4.tgz#85ab6d0a9ca8b27b31604c09efe1c14dc21ab872" @@ -4657,6 +4644,10 @@ image-ssim@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/image-ssim/-/image-ssim-0.2.0.tgz#83b42c7a2e6e4b85505477fe6917f5dbc56420e5" +immediate@~3.0.5: + version "3.0.6" + resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" + import-lazy@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43" @@ -4711,22 +4702,23 @@ init-package-json@~1.9.3, init-package-json@~1.9.4: validate-npm-package-license "^3.0.1" validate-npm-package-name "^3.0.0" -inquirer@^0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-0.12.0.tgz#1ef2bfd63504df0bc75785fff8c2c41df12f077e" +inquirer@^3.0.6: + version "3.2.2" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.2.2.tgz#c2aaede1507cc54d826818737742d621bef2e823" dependencies: - ansi-escapes "^1.1.0" - ansi-regex "^2.0.0" - chalk "^1.0.0" - cli-cursor "^1.0.1" + ansi-escapes "^2.0.0" + chalk "^2.0.0" + cli-cursor "^2.1.0" cli-width "^2.0.0" - figures "^1.3.5" + external-editor "^2.0.4" + figures "^2.0.0" lodash "^4.3.0" - readline2 "^1.0.1" - run-async "^0.1.0" - rx-lite "^3.1.2" - string-width "^1.0.1" - strip-ansi "^3.0.0" + mute-stream "0.0.7" + run-async "^2.2.0" + rx-lite "^4.0.8" + rx-lite-aggregates "^4.0.8" + string-width "^2.1.0" + strip-ansi "^4.0.0" through "^2.3.6" inquirer@~3.0.6: @@ -4866,7 +4858,7 @@ is-glob@^2.0.0, is-glob@^2.0.1: dependencies: is-extglob "^1.0.0" -is-my-json-valid@^2.10.0, is-my-json-valid@^2.12.4: +is-my-json-valid@^2.12.4: version "2.16.1" resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.16.1.tgz#5a846777e2c2620d1e69104e5d3a03b1f6088f11" dependencies: @@ -5064,7 +5056,7 @@ js-tokens@^3.0.0, js-tokens@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" -js-yaml@^3.5.1, js-yaml@^3.8.3, js-yaml@^3.8.4: +js-yaml@^3.8.3, js-yaml@^3.8.4, js-yaml@^3.9.1: version "3.9.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.9.1.tgz#08775cebdfdd359209f0d2acd383c8f86a6904a0" dependencies: @@ -5138,7 +5130,7 @@ json-schema@0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" -json-stable-stringify@^1.0.0, json-stable-stringify@^1.0.1: +json-stable-stringify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" dependencies: @@ -5185,10 +5177,16 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" -jsx-ast-utils@^1.0.0, jsx-ast-utils@^1.3.4: +jsx-ast-utils@^1.4.0: version "1.4.1" resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-1.4.1.tgz#3867213e8dd79bf1e8f2300c0cfc1efb182c0df1" +jsx-ast-utils@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.0.0.tgz#ec06a3d60cf307e5e119dac7bad81e89f096f0f8" + dependencies: + array-includes "^3.0.3" + kind-of@^3.0.2: version "3.2.2" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" @@ -5255,6 +5253,12 @@ levn@^0.3.0, levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" +lie@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e" + dependencies: + immediate "~3.0.5" + lighthouse@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/lighthouse/-/lighthouse-2.0.0.tgz#55962239ab858eadf46af03468ae1ca17703fde2" @@ -5654,7 +5658,7 @@ lodash@4.5.1: version "4.5.1" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.5.1.tgz#80e8a074ca5f3893a6b1c10b2a636492d710c316" -lodash@^4.0.0, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.16.6, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.5.1, lodash@^4.6.0, lodash@^4.6.1, lodash@^4.8.0, lodash@~4.17.4: +lodash@^4.0.0, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.16.6, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.5.1, lodash@^4.6.1, lodash@^4.8.0, lodash@~4.17.0, lodash@~4.17.4: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" @@ -5832,14 +5836,18 @@ miller-rabin@^4.0.0: bn.js "^4.0.0" brorand "^1.0.1" -"mime-db@>= 1.29.0 < 2", mime-db@~1.29.0: - version "1.29.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.29.0.tgz#48d26d235589651704ac5916ca06001914266878" +"mime-db@>= 1.29.0 < 2": + version "1.30.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01" mime-db@~1.12.0: version "1.12.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.12.0.tgz#3d0c63180f458eb10d325aaa37d7c58ae312e9d7" +mime-db@~1.29.0: + version "1.29.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.29.0.tgz#48d26d235589651704ac5916ca06001914266878" + mime-types@^2.1.11, mime-types@^2.1.12, mime-types@~2.1.15, mime-types@~2.1.16, mime-types@~2.1.7: version "2.1.16" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.16.tgz#2b858a52e5ecd516db897ac2be87487830698e23" @@ -6001,10 +6009,6 @@ multipipe@^0.1.2: dependencies: duplexer2 "0.0.2" -mute-stream@0.0.5: - version "0.0.5" - resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0" - mute-stream@0.0.7, mute-stream@~0.0.4: version "0.0.7" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" @@ -6017,7 +6021,7 @@ mz@^2.3.1: object-assign "^4.0.1" thenify-all "^1.0.0" -nan@^2.2.0, nan@^2.3.0: +nan@^2.2.0, nan@^2.3.0, nan@^2.4.0: version "2.6.2" resolved "https://registry.yarnpkg.com/nan/-/nan-2.6.2.tgz#e4ff34e6c95fdfb5aecc08de6596f43605a7db45" @@ -6103,6 +6107,13 @@ node-gyp@~3.3.0: tar "^2.0.0" which "1" +node-hid@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/node-hid/-/node-hid-0.5.4.tgz#a7246dfc08d52774147fa264354d5da6eab40253" + dependencies: + nan "^2.4.0" + node-pre-gyp "0.6.31" + node-libs-browser@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-0.7.0.tgz#3e272c0819e308935e26674408d7af0e1491b83b" @@ -6159,7 +6170,21 @@ node-libs-browser@^1.0.0: util "^0.10.3" vm-browserify "0.0.4" -node-pre-gyp@^0.6.36, node-pre-gyp@~0.6.32: +node-pre-gyp@0.6.31: + version "0.6.31" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.31.tgz#d8a00ddaa301a940615dbcc8caad4024d58f6017" + dependencies: + mkdirp "~0.5.1" + nopt "~3.0.6" + npmlog "^4.0.0" + rc "~1.1.6" + request "^2.75.0" + rimraf "~2.5.4" + semver "~5.3.0" + tar "~2.2.1" + tar-pack "~3.3.0" + +node-pre-gyp@^0.6.32, node-pre-gyp@^0.6.36, node-pre-gyp@~0.6.32: version "0.6.36" resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.36.tgz#db604112cb74e0d477554e9b505b17abddfab786" dependencies: @@ -6510,7 +6535,7 @@ npmi@1.0.1: are-we-there-yet "~1.1.2" gauge "~1.2.5" -"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.2: +"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.0, npmlog@^4.0.2: version "4.1.2" resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" dependencies: @@ -6594,7 +6619,7 @@ object-keys@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-0.4.0.tgz#28a6aae7428dd2c3a92f3d95f21335dd204e0336" -object.assign@^4.0.4: +object.assign@^4.0.3, object.assign@^4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.0.4.tgz#b1c9cc044ef1b9fe63606fc141abbb32e14730cc" dependencies: @@ -6844,7 +6869,7 @@ path-is-absolute@^1.0.0, path-is-absolute@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" -path-is-inside@^1.0.1, path-is-inside@~1.0.0, path-is-inside@~1.0.1: +path-is-inside@^1.0.1, path-is-inside@^1.0.2, path-is-inside@~1.0.0, path-is-inside@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" @@ -6931,12 +6956,6 @@ pkg-dir@^1.0.0: dependencies: find-up "^1.0.0" -pkg-up@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-1.0.0.tgz#3e08fb461525c4421624a33b9f7e6d0af5b05a26" - dependencies: - find-up "^1.0.0" - plist@^2.0.1, plist@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/plist/-/plist-2.1.0.tgz#57ccdb7a0821df21831217a3cad54e3e146a1025" @@ -6951,9 +6970,9 @@ plur@^2.1.2: dependencies: irregular-plurals "^1.0.0" -pluralize@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45" +pluralize@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-4.0.0.tgz#59b708c1c0190a2f692f1c7618c446b052fd1762" postcss-calc@^5.2.0: version "5.3.1" @@ -7194,11 +7213,11 @@ postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0 supports-color "^3.2.3" postcss@^6.0.1: - version "6.0.9" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.9.tgz#54819766784a51c65b1ec4d54c2f93765438c35a" + version "6.0.10" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.10.tgz#c311b89734483d87a91a56dc9e53f15f4e6e84e4" dependencies: chalk "^2.1.0" - source-map "^0.5.6" + source-map "^0.5.7" supports-color "^4.2.1" prelude-ls@~1.1.2: @@ -7250,9 +7269,9 @@ progress-stream@^1.1.0: speedometer "~0.1.2" through2 "~0.2.3" -progress@^1.1.8: - version "1.1.8" - resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be" +progress@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.0.tgz#8a1be366bf8fc23db2bd23f10c6fe920b4389d1f" promise@^7.1.1: version "7.3.1" @@ -7272,7 +7291,7 @@ promzard@^0.3.0: dependencies: read "1" -prop-types@^15.0.0, prop-types@^15.5.4, prop-types@^15.5.8: +prop-types@^15.0.0, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.8: version "15.5.10" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.10.tgz#2797dfc3126182e3a95e3dfbb2e893ddd7456154" dependencies: @@ -7375,9 +7394,9 @@ querystringify@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-1.0.0.tgz#6286242112c5b712fa654e526652bf6a13ff05cb" -ramda@^0.23.0: - version "0.23.0" - resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.23.0.tgz#ccd13fff73497a93974e3e86327bfd87bd6e8e2b" +ramda@^0.24.1: + version "0.24.1" + resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.24.1.tgz#c3b7755197f35b8dc3502228262c4c91ddb6b857" randomatic@^1.1.3: version "1.1.7" @@ -7405,6 +7424,15 @@ rc@^1.0.1, rc@^1.1.2, rc@^1.1.6, rc@^1.1.7, rc@^1.2.1: minimist "^1.2.0" strip-json-comments "~2.0.1" +rc@~1.1.6: + version "1.1.7" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.1.7.tgz#c5ea564bb07aff9fd3a5b32e906c1d3a65940fea" + dependencies: + deep-extend "~0.4.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + react-addons-test-utils@15.5.1: version "15.5.1" resolved "https://registry.yarnpkg.com/react-addons-test-utils/-/react-addons-test-utils-15.5.1.tgz#e0d258cda2a122ad0dff69f838260d0c3958f5f7" @@ -7640,7 +7668,7 @@ readable-stream@~2.0.5: string_decoder "~0.10.x" util-deprecate "~1.0.1" -readable-stream@~2.1.5: +readable-stream@~2.1.4, readable-stream@~2.1.5: version "2.1.5" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.1.5.tgz#66fa8b720e1438b364681f2ad1a63c618448c9d0" dependencies: @@ -7670,14 +7698,6 @@ readdirp@^2.0.0: readable-stream "^2.0.2" set-immediate-shim "^1.0.1" -readline2@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/readline2/-/readline2-1.0.1.tgz#41059608ffc154757b715d9989d199ffbf372e35" - dependencies: - code-point-at "^1.0.0" - is-fullwidth-code-point "^1.0.0" - mute-stream "0.0.5" - realize-package-specifier@~3.0.1: version "3.0.3" resolved "https://registry.yarnpkg.com/realize-package-specifier/-/realize-package-specifier-3.0.3.tgz#d0def882952b8de3f67eba5e91199661271f41f4" @@ -7864,7 +7884,7 @@ replace-ext@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-0.0.1.tgz#29bbd92078a739f0bcce2b4ee41e837953522924" -request@2, request@^2.45.0, request@^2.47.0, request@^2.65.0, request@^2.72.0, request@^2.74.0, request@^2.79.0, request@^2.81.0, request@~2.81.0: +request@2, request@^2.45.0, request@^2.47.0, request@^2.65.0, request@^2.72.0, request@^2.74.0, request@^2.75.0, request@^2.79.0, request@^2.81.0, request@~2.81.0: version "2.81.0" resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0" dependencies: @@ -7951,7 +7971,7 @@ require-main-filename@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" -require-uncached@^1.0.2: +require-uncached@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3" dependencies: @@ -8037,7 +8057,7 @@ rimraf@2.2.8: version "2.2.8" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.2.8.tgz#e439be2aaee327321952730f99a8929e4fc50582" -rimraf@~2.5.2, rimraf@~2.5.4: +rimraf@~2.5.1, rimraf@~2.5.2, rimraf@~2.5.4: version "2.5.4" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.5.4.tgz#96800093cbf1a0c86bd95b4625467535c29dfa04" dependencies: @@ -8054,21 +8074,21 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^2.0.0" inherits "^2.0.1" -run-async@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389" - dependencies: - once "^1.3.0" - run-async@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0" dependencies: is-promise "^2.1.0" -rx-lite@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102" +rx-lite-aggregates@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz#753b87a89a11c95467c4ac1626c4efc4e05c67be" + dependencies: + rx-lite "*" + +rx-lite@*, rx-lite@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444" rx@2.3.24: version "2.3.24" @@ -8180,6 +8200,18 @@ send@0.15.4: range-parser "~1.2.0" statuses "~1.3.1" +serialport@4.0.7: + version "4.0.7" + resolved "https://registry.yarnpkg.com/serialport/-/serialport-4.0.7.tgz#421c618a8a612bd40cfa461b4a46154daf2229a5" + dependencies: + bindings "1.2.1" + commander "^2.9.0" + debug "^2.3.2" + lie "^3.1.0" + nan "^2.4.0" + node-pre-gyp "^0.6.32" + object.assign "^4.0.3" + serve-index@^1.7.2: version "1.9.0" resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.0.tgz#d2b280fc560d616ee81b48bf0fa82abed2485ce7" @@ -8265,7 +8297,7 @@ shelljs@0.7.0: interpret "^1.0.0" rechoir "^0.6.2" -shelljs@0.7.8, shelljs@^0.7.5: +shelljs@0.7.8: version "0.7.8" resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.8.tgz#decbcf874b0d1e5fb72e14b164a9683048e9acb3" dependencies: @@ -8399,7 +8431,7 @@ source-map@0.5.6: version "0.5.6" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" -source-map@0.5.x, source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.1, source-map@~0.5.3: +source-map@0.5.x, source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.1, source-map@~0.5.3: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" @@ -8554,7 +8586,7 @@ string-width@^1.0.1, string-width@^1.0.2: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" -string-width@^2.0.0: +string-width@^2.0.0, string-width@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" dependencies: @@ -8708,9 +8740,9 @@ symbol@^0.2.1: version "0.2.3" resolved "https://registry.yarnpkg.com/symbol/-/symbol-0.2.3.tgz#3b9873b8a901e47c6efe21526a3ac372ef28bbc7" -table@^3.7.8: - version "3.8.3" - resolved "https://registry.yarnpkg.com/table/-/table-3.8.3.tgz#2bbc542f0fda9861a755d3947fefd8b3f513855f" +table@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/table/-/table-4.0.1.tgz#a8116c133fac2c61f4a420ab6cdf5c4d61f0e435" dependencies: ajv "^4.7.0" ajv-keywords "^1.0.0" @@ -8740,6 +8772,19 @@ tar-pack@^3.4.0: tar "^2.2.1" uid-number "^0.0.6" +tar-pack@~3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.3.0.tgz#30931816418f55afc4d21775afdd6720cee45dae" + dependencies: + debug "~2.2.0" + fstream "~1.0.10" + fstream-ignore "~1.0.5" + once "~1.3.3" + readable-stream "~2.1.4" + rimraf "~2.5.1" + tar "~2.2.1" + uid-number "~0.0.6" + tar-stream@^1.5.0: version "1.5.4" resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.5.4.tgz#36549cf04ed1aee9b2a30c0143252238daf94016" @@ -9006,7 +9051,7 @@ uglify-to-browserify@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" -uid-number@0.0.6, uid-number@^0.0.6: +uid-number@0.0.6, uid-number@^0.0.6, uid-number@~0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" @@ -9121,7 +9166,7 @@ url@^0.11.0, url@~0.11.0: punycode "1.3.2" querystring "0.2.0" -user-home@2.0.0, user-home@^2.0.0: +user-home@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/user-home/-/user-home-2.0.0.tgz#9c70bfd8169bc1dcbf48604e0f04b8b49cde9e9f" dependencies: