diff --git a/lib/pebble.js b/lib/pebble.js index d918de564..fac7b3cfb 100644 --- a/lib/pebble.js +++ b/lib/pebble.js @@ -1,3 +1,5 @@ +'use strict'; + var DIRECTIONS = { NONE: 0 , DoubleUp: 1 @@ -12,6 +14,7 @@ var DIRECTIONS = { }; var iob = require("./iob")(); +var async = require('async'); function directionToTrend (direction) { var trend = 8; @@ -22,97 +25,125 @@ function directionToTrend (direction) { } function pebble (req, res) { - var ONE_DAY = 24 * 60 * 60 * 1000; - var useMetricBg = (req.query.units === "mmol"); - var uploaderBattery; - var treatmentResults; - var profileResult; + var ONE_DAY = 24 * 60 * 60 * 1000 + , uploaderBattery + , treatmentResults + , profileResult + , sgvData = [ ] + , calData = [ ]; function scaleBg(bg) { - if (useMetricBg) { + if (req.mmol) { return (Math.round((bg / 18) * 10) / 10).toFixed(1); - } else + } else { return bg; + } } - function get_latest (err, results) { + function sendData () { var now = Date.now(); - var sgvData = [ ]; - var calData = [ ]; - - results.forEach(function(element, index, array) { - if (element) { - var obj = {}; - if (element.sgv) { - var next = null; - var sgvs = results.filter(function(d) { - return !!d.sgv; - }); - if (index + 1 < sgvs.length) { - next = sgvs[index + 1]; - } - obj.sgv = scaleBg(element.sgv).toString(); - obj.bgdelta = (next ? (scaleBg(element.sgv) - scaleBg(next.sgv) ) : 0); - if (useMetricBg) { - obj.bgdelta = obj.bgdelta.toFixed(1); - } - if ('direction' in element) { - obj.trend = directionToTrend(element.direction); - obj.direction = element.direction; - } - obj.datetime = element.date; - if (req.rawbg) { - obj.filtered = element.filtered; - obj.unfiltered = element.unfiltered; - obj.noise = element.noise; - obj.rssi = element.rssi; - } - // obj.date = element.date.toString( ); - sgvData.push(obj); - } else if (req.rawbg && element.type == 'cal') { - calData.push(element); - } - } - }); - var count = parseInt(req.query.count) || 1; - - var bgs = sgvData.slice(0, count); //for compatibility we're keeping battery and iob here, but they would be better somewhere else - bgs[0].battery = uploaderBattery ? "" + uploaderBattery : undefined; - if (req.iob) { - bgs[0].iob = iob.calcTotal(treatmentResults.slice(0, 20), profileResult, new Date(now)).display; + if (sgvData.length > 0) { + sgvData[0].battery = uploaderBattery ? "" + uploaderBattery : undefined; + if (req.iob) { + sgvData[0].iob = iob.calcTotal(treatmentResults.slice(0, 20), profileResult, new Date(now)).display; + } } - var result = { status: [ {now:now}], bgs: bgs, cals: calData.slice(0, count) }; + var result = { status: [ {now: now} ], bgs: sgvData, cals: calData }; res.setHeader('content-type', 'application/json'); res.write(JSON.stringify(result)); res.end( ); - // collection.db.close(); } - req.devicestatus.last(function(err, value) { - if (!err && value) { - uploaderBattery = value.uploaderBattery; - } else { - console.error("req.devicestatus.tail", err); - } - var earliest_data = Date.now() - ONE_DAY; - loadTreatments(req, earliest_data, function (err, trs) { - treatmentResults = trs; - loadProfile(req, function (err, profileResults) { - profileResults.forEach(function(profile) { - if (profile) { - if (profile.dia) { - profileResult = profile; + var earliest_data = Date.now() - ONE_DAY; + + async.parallel({ + devicestatus: function (callback) { + req.devicestatus.last(function (err, value) { + if (!err && value) { + uploaderBattery = value.uploaderBattery; + } else { + console.error("req.devicestatus.tail", err); } - } + callback(); }); - var q = { find: {"date": {"$gte": earliest_data}} }; - req.entries.list(q, get_latest); - }); - }); - }); + } + , treatments: function(callback) { + loadTreatments(req, earliest_data, function (err, trs) { + treatmentResults = trs; + callback(); + }); + } + , profile: function(callback) { + loadProfile(req, function (err, profileResults) { + profileResults.forEach(function (profile) { + if (profile) { + if (profile.dia) { + profileResult = profile; + } + } + }); + callback(); + }); + } + , cal: function(callback) { + if (req.rawbg) { + var cq = { count: req.count, find: {type: 'cal'} }; + req.entries.list(cq, function (err, results) { + results.forEach(function (element) { + if (element) { + calData.push({ + slope: Math.round(element.slope) + , intercept: Math.round(element.intercept) + , scale: Math.round(element.scale) + }); + } + }); + callback(); + }); + } else { + callback(); + } + } + , entries: function(callback) { + var q = { count: req.count, find: { "sgv": { $exists: true }} }; + + req.entries.list(q, function(err, results) { + results.forEach(function(element, index) { + if (element) { + var obj = {}; + var next = null; + var sgvs = results.filter(function(d) { + return !!d.sgv; + }); + if (index + 1 < sgvs.length) { + next = sgvs[index + 1]; + } + obj.sgv = scaleBg(element.sgv).toString(); + obj.bgdelta = (next ? (scaleBg(element.sgv) - scaleBg(next.sgv) ) : 0); + if (req.mmol) { + obj.bgdelta = obj.bgdelta.toFixed(1); + } + if ('direction' in element) { + obj.trend = directionToTrend(element.direction); + obj.direction = element.direction; + } + obj.datetime = element.date; + if (req.rawbg) { + obj.filtered = element.filtered; + obj.unfiltered = element.unfiltered; + obj.noise = element.noise; + } + sgvData.push(obj); + } + }); + callback(); + }); + } + }, sendData); + } function loadTreatments(req, earliest_data, fn) { @@ -140,6 +171,9 @@ function configure (entries, treatments, profile, devicestatus, env) { req.devicestatus = devicestatus; req.rawbg = env.enable && env.enable.indexOf('rawbg') > -1; req.iob = env.enable && env.enable.indexOf('iob') > -1; + req.mmol = (req.query.units || env.DISPLAY_UNITS) === 'mmol'; + req.count = parseInt(req.query.count) || 1; + next( ); } return [middle, pebble]; diff --git a/lib/websocket.js b/lib/websocket.js index 6936ef173..161d239f8 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -1,3 +1,4 @@ +var async = require('async'); function websocket (env, server, entries, treatments, profiles) { "use strict"; @@ -33,7 +34,12 @@ var dir2Char = { patientData = []; function start ( ) { - io = require('socket.io').listen(server); + io = require('socket.io').listen(server, { + //these only effect the socket.io.js file that is sent to the client, but better than nothing + 'browser client minification': true, + 'browser client etag': true, + 'browser client gzip': true + }); } // get data from database and setup to update every minute function kickstart (fn) { @@ -149,62 +155,80 @@ function update() { mbgData = []; profileData = []; var earliest_data = now - TWO_DAYS; - var q = { find: {"date": {"$gte": earliest_data}} }; - entries.list(q, function (err, results) { - results.forEach(function(element, index, array) { - if (element) { - if (element.mbg) { - var obj = {}; - obj.y = element.mbg; - obj.x = element.date; - obj.d = element.dateString; - obj.device = element.device; - mbgData.push(obj); - } else if (element.sgv) { - var obj = {}; - obj.y = element.sgv; - obj.x = element.date; - obj.d = element.dateString; - obj.device = element.device; - obj.direction = directionToChar(element.direction); - obj.filtered = element.filtered; - obj.unfiltered = element.unfiltered; - obj.noise = element.noise; - obj.rssi = element.rssi; - cgmData.push(obj); - } else if (element.slope) { - var obj = {}; - obj.x = element.date; - obj.d = element.dateString; - obj.scale = element.scale; - obj.intercept = element.intercept; - obj.slope = element.slope; - calData.push(obj); - } - } - }); - var tq = { find: {"created_at": {"$gte": new Date(earliest_data).toISOString()}} }; - treatments.list(tq, function (err, results) { - treatmentData = results.map(function(treatment) { - var timestamp = new Date(treatment.timestamp || treatment.created_at); - treatment.x = timestamp.getTime(); - return treatment; - }); - - profiles.list(function (err, results) { - // There should be only one document in the profile collection with a DIA. If there are multiple, use the last one. - results.forEach(function(element, index, array) { - if (element) { - if (element.dia) { - profileData[0] = element; + + async.parallel({ + entries: function(callback) { + var q = { find: {"date": {"$gte": earliest_data}} }; + entries.list(q, function (err, results) { + results.forEach(function (element) { + if (element) { + if (element.mbg) { + mbgData.push({ + y: element.mbg + , x: element.date + , d: element.dateString + , device: element.device + }); + } else if (element.sgv) { + cgmData.push({ + y: element.sgv + , x: element.date + , d: element.dateString + , device: element.device + , direction: directionToChar(element.direction) + , filtered: element.filtered + , unfiltered: element.unfiltered + , noise: element.noise + , rssi: element.rssi + }); + } } - } - }); - // all done, do loadData - loadData( ); - }); - }); - }); + }); + callback(); + }) + } + , cal: function(callback) { + var cq = { count: 1, find: {"type": "cal"} }; + entries.list(cq, function (err, results) { + results.forEach(function (element) { + if (element) { + calData.push({ + x: element.date + , d: element.dateString + , scale: element.scale + , intercept: element.intercept + , slope: element.slope + }); + } + }); + callback(); + }); + } + , treatments: function(callback) { + var tq = { find: {"created_at": {"$gte": new Date(earliest_data).toISOString()}} }; + treatments.list(tq, function (err, results) { + treatmentData = results.map(function (treatment) { + var timestamp = new Date(treatment.timestamp || treatment.created_at); + treatment.x = timestamp.getTime(); + return treatment; + }); + callback(); + }); + } + , profile: function(callback) { + profiles.list(function (err, results) { + // There should be only one document in the profile collection with a DIA. If there are multiple, use the last one. + results.forEach(function(element, index, array) { + if (element) { + if (element.dia) { + profileData[0] = element; + } + } + }); + callback(); + }); + } + }, loadData); return update; } diff --git a/package.json b/package.json index b7ac8480f..92b50a630 100644 --- a/package.json +++ b/package.json @@ -32,9 +32,11 @@ "node": "0.10.x" }, "dependencies": { + "async": "^0.9.0", "body-parser": "^1.4.3", "bower": "^1.3.8", "browserify-express": "^0.1.4", + "compression": "^1.4.2", "errorhandler": "^1.1.1", "event-stream": "~3.1.5", "express": "^4.6.1", diff --git a/server.js b/server.js index 2f56c7597..e427926d6 100644 --- a/server.js +++ b/server.js @@ -43,6 +43,7 @@ var store = require('./lib/storage')(env, function() { var express = require('express'); +var compression = require('compression'); /////////////////////////////////////////////////// // api and json object variables @@ -66,9 +67,14 @@ var appInfo = software.name + ' ' + software.version; app.set('title', appInfo); app.enable('trust proxy'); // Allows req.secure test on heroku https connections. -//if (env.api_secret) { -// console.log("API_SECRET", env.api_secret); -//} +app.use(compression({filter: shouldCompress})); + +function shouldCompress(req, res) { + //TODO: return false here if we find a condition where we don't want to compress + // fallback to standard filter function + return compression.filter(req, res); +} + app.use('/api/v1', api); // pebble data diff --git a/static/js/client.js b/static/js/client.js index 4db9acee5..2d5e06c5e 100644 --- a/static/js/client.js +++ b/static/js/client.js @@ -307,7 +307,8 @@ var app = {}, browserSettings = {}, browserStorage = $.localStorage; var nowDate = new Date(brushExtent[1] - THIRTY_MINS_IN_MS); - var currentBG = $('.bgStatus .currentBG') + var bgButton = $('.bgButton') + , currentBG = $('.bgStatus .currentBG') , currentDirection = $('.bgStatus .currentDirection') , currentDetails = $('.bgStatus .currentDetails') , lastEntry = $('#lastEntry'); @@ -324,6 +325,11 @@ var app = {}, browserSettings = {}, browserStorage = $.localStorage; currentBG.text(scaleBg(value)); } + bgButton.removeClass('urgent warning inrange'); + if (!inRetroMode()) { + bgButton.addClass(sgvToColoredRange(value)); + } + currentBG.toggleClass('error-code', value < 39); currentBG.toggleClass('bg-limit', value == 39 || value > 400); } @@ -370,12 +376,6 @@ var app = {}, browserSettings = {}, browserStorage = $.localStorage; } } - if (inRetroMode()) { - $('.bgButton').removeClass('urgent warning inrange'); - } else { - $('.bgButton').addClass(sgvToColoredRange(latestSGV.y)); - } - // predict for retrospective data // by changing lookback from 1 to 2, we modify the AR algorithm to determine its initial slope from 10m // of data instead of 5, which eliminates the incorrect and misleading predictions generated when @@ -463,8 +463,16 @@ var app = {}, browserSettings = {}, browserStorage = $.localStorage; }; function prepareFocusCircles(sel) { + var badData = []; sel.attr('cx', function (d) { return xScale(d.date); }) - .attr('cy', function (d) { return yScale(d.sgv); }) + .attr('cy', function (d) { + if (isNaN(d.sgv)) { + badData.push(d); + return yScale(scaleBg(450)); + } else { + return yScale(d.sgv); + } + }) .attr('fill', function (d) { return d.color; }) .attr('opacity', function (d) { return futureOpacity(d.date.getTime() - latestSGV.x); }) .attr('stroke-width', function (d) { if (d.type == 'mbg') return 2; else return 0; }) @@ -474,6 +482,10 @@ var app = {}, browserSettings = {}, browserStorage = $.localStorage; }) .attr('r', function (d) { return dotRadius(d.type); }); + if (badData.length > 0) { + console.warn("Bad Data: isNaN(sgv)", badData); + } + return sel; } @@ -918,14 +930,26 @@ var app = {}, browserSettings = {}, browserStorage = $.localStorage; .data(data); function prepareContextCircles(sel) { + var badData = []; sel.attr('cx', function (d) { return xScale2(d.date); }) - .attr('cy', function (d) { return yScale2(d.sgv); }) + .attr('cy', function (d) { + if (isNaN(d.sgv)) { + badData.push(d); + return yScale2(scaleBg(450)); + } else { + return yScale2(d.sgv); + } + }) .attr('fill', function (d) { return d.color; }) .style('opacity', function (d) { return highlightBrushPoints(d) }) .attr('stroke-width', function (d) {if (d.type == 'mbg') return 2; else return 0; }) .attr('stroke', function (d) { return 'white'; }) .attr('r', function(d) { if (d.type == 'mbg') return 4; else return 2;}); + if (badData.length > 0) { + console.warn("Bad Data: isNaN(sgv)", badData); + } + return sel; } @@ -1093,6 +1117,11 @@ var app = {}, browserSettings = {}, browserStorage = $.localStorage; R2 = Math.sqrt(Math.max(carbs, insulin * CR)) / scale, R3 = R2 + 8 / scale; + if (isNaN(R1) || isNaN(R3) || isNaN(R3)) { + console.warn("Bad Data: Found isNaN value in treatment", treatment); + return; + } + var arc_data = [ { 'element': '', 'color': 'white', 'start': -1.5708, 'end': 1.5708, 'inner': 0, 'outer': R1 }, { 'element': '', 'color': 'transparent', 'start': -1.5708, 'end': 1.5708, 'inner': R2, 'outer': R3 }, diff --git a/tests/pebble.test.js b/tests/pebble.test.js index d6704ed79..e9dd57d1e 100644 --- a/tests/pebble.test.js +++ b/tests/pebble.test.js @@ -4,8 +4,8 @@ var should = require('should'); //Mock entries var entries = { - list: function(q, callback) { - var results = [ + list: function(opts, callback) { + var sgvs = [ { device: 'dexcom', date: 1422727301000, dateString: 'Sat Jan 31 10:01:41 PST 2015', @@ -18,14 +18,6 @@ var entries = { noise: 1 }, { device: 'dexcom', - date: 1422647711000, - dateString: 'Fri Jan 30 11:55:11 PST 2015', - slope: 895.8571693029189, - intercept: 34281.06876195567, - scale: 1, - type: 'cal' - }, - { device: 'dexcom', date: 1422727001000, dateString: 'Sat Jan 31 09:56:41 PST 2015', sgv: 84, @@ -70,7 +62,25 @@ var entries = { noise: 1 } ]; - callback(null, results); + + var cals = [ + { device: 'dexcom', + date: 1422647711000, + dateString: 'Fri Jan 30 11:55:11 PST 2015', + slope: 895.8571693029189, + intercept: 34281.06876195567, + scale: 1, + type: 'cal' + } + ]; + + var count = (opts && opts.count) || 1; + + if (opts && opts.find && opts.find.sgv) { + callback(null, sgvs.slice(0, count)); + } else if (opts && opts.find && opts.find.type == 'cal') { + callback(null, cals.slice(0, count)); + } } }; @@ -164,13 +174,12 @@ describe('Pebble Endpoint with Raw', function ( ) { bg.filtered.should.equal(113984); bg.unfiltered.should.equal(111920); bg.noise.should.equal(1); - bg.rssi.should.equal(179); bg.battery.should.equal('100'); res.body.cals.length.should.equal(1); var cal = res.body.cals[0]; - cal.slope.should.equal(895.8571693029189); - cal.intercept.should.equal(34281.06876195567); + cal.slope.should.equal(896); + cal.intercept.should.equal(34281); cal.scale.should.equal(1); done( ); });