mirror of
https://github.com/bckelley/cgm-remote-monitor.git
synced 2026-08-24 03:14:12 -05:00
Revert "Revert to c1b2988eb0 (Merge branch 'hotfix/0.3.5')"
This reverts commit 1cc9c6127c.
This commit is contained in:
@@ -8,5 +8,8 @@ my.env
|
||||
*.env
|
||||
static/bower_components/
|
||||
.*.sw?
|
||||
.DS_Store
|
||||
|
||||
.vagrant
|
||||
/iisnode
|
||||
/web.config
|
||||
@@ -5,6 +5,8 @@ cgm-remote-monitor (a.k.a. NightScout)
|
||||
[](https://david-dm.org/nightscout/cgm-remote-monitor)
|
||||
[](https://gitter.im/nightscout/public)
|
||||
|
||||
[](https://heroku.com/deploy)
|
||||
|
||||
This acts as a web-based CGM (Continuous Glucose Montinor) to allow
|
||||
multiple caregivers to remotely view a patients glucose data in
|
||||
realtime. The server reads a MongoDB which is intended to be data
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "CGM Remote Monitor",
|
||||
"repository": "https://github.com/nightscout/cgm-remote-monitor",
|
||||
"env": {
|
||||
"MONGO_COLLECTION": {
|
||||
"description": "The mongo collection to connect to.",
|
||||
"value": "nightscout"
|
||||
}
|
||||
},
|
||||
"addons": [
|
||||
"mongolab"
|
||||
]
|
||||
}
|
||||
Executable
+11
@@ -0,0 +1,11 @@
|
||||
#!/bin/sh
|
||||
|
||||
curl -H "Content-Type: application/json" -XPOST 'http://localhost:1337/api/v1/treatments/' -d '{
|
||||
"enteredBy": "Dad",
|
||||
"eventType":"Site Change",
|
||||
"glucoseValue": 322,
|
||||
"glucoseType": "sensor",
|
||||
"carbsGiven": 0,
|
||||
"insulinGiven": 1.25,
|
||||
"notes": "Argh..."
|
||||
}'
|
||||
@@ -2,6 +2,8 @@
|
||||
"name": "nightscout",
|
||||
"version": "0.3.5",
|
||||
"dependencies": {
|
||||
"angularjs": "1.3.0-beta.19",
|
||||
"bootstrap": "~3.2.0",
|
||||
"d3": "3.4.3",
|
||||
"jquery": "2.1.0",
|
||||
"jQuery-Storage-API": "~1.7.2",
|
||||
|
||||
@@ -23,9 +23,10 @@ function config ( ) {
|
||||
env.name = software.name;
|
||||
env.DISPLAY_UNITS = process.env.DISPLAY_UNITS || 'mg/dl';
|
||||
env.PORT = process.env.PORT || 1337;
|
||||
env.mongo = process.env.MONGO_CONNECTION || process.env.CUSTOMCONNSTR_mongo;
|
||||
env.mongo_collection = process.env.CUSTOMCONNSTR_mongo_collection || 'entries';
|
||||
env.mongo = process.env.MONGO_CONNECTION || process.env.CUSTOMCONNSTR_mongo || process.env.MONGOLAB_URI;
|
||||
env.mongo_collection = process.env.CUSTOMCONNSTR_mongo_collection || process.env.MONGO_COLLECTION || 'entries';
|
||||
env.settings_collection = process.env.CUSTOMCONNSTR_mongo_settings_collection || 'settings';
|
||||
env.treatments_collection = process.env.CUSTOMCONNSTR_mongo_treatments_collection || 'treatments';
|
||||
var shasum = crypto.createHash('sha1');
|
||||
var useSecret = (process.env.API_SECRET && process.env.API_SECRET.length > 0);
|
||||
env.api_secret = null;
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
function create (env, entries, settings) {
|
||||
function create (env, entries, settings, treatments) {
|
||||
var express = require('express'),
|
||||
app = express( )
|
||||
;
|
||||
@@ -29,6 +29,7 @@ function create (env, entries, settings) {
|
||||
// Entries and settings
|
||||
app.use('/', require('./entries/')(app, wares, entries));
|
||||
app.use('/', require('./settings/')(app, wares, settings));
|
||||
app.use('/', require('./treatments/')(app, wares, treatments));
|
||||
|
||||
// Status
|
||||
app.use('/', require('./status')(app, wares));
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
'use strict';
|
||||
|
||||
var consts = require('../../constants');
|
||||
|
||||
function configure (app, wares, treatments) {
|
||||
var express = require('express'),
|
||||
api = express.Router( );
|
||||
|
||||
// invoke common middleware
|
||||
api.use(wares.sendJSONStatus);
|
||||
// text body types get handled as raw buffer stream
|
||||
api.use(wares.bodyParser.raw( ));
|
||||
// json body types get handled as parsed json
|
||||
api.use(wares.bodyParser.json( ));
|
||||
// also support url-encoded content-type
|
||||
api.use(wares.bodyParser.urlencoded({ extended: true }));
|
||||
|
||||
// List settings available
|
||||
api.get('/treatments/', function(req, res) {
|
||||
treatments.list(function (err, profiles) {
|
||||
return res.json(profiles);
|
||||
});
|
||||
});
|
||||
|
||||
function config_authed (app, api, wares, treatments) {
|
||||
|
||||
api.post('/treatments/', /*TODO: auth disabled for quick UI testing... wares.verifyAuthorization, */ function(req, res) {
|
||||
var treatment = req.body;
|
||||
treatments.create(treatment, function (err, created) {
|
||||
if (err)
|
||||
res.sendJSONStatus(res, consts.HTTP_INTERNAL_ERROR, 'Mongo Error', err);
|
||||
else
|
||||
res.json(created);
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
if (app.enabled('api') || true /*TODO: auth disabled for quick UI testing...*/) {
|
||||
config_authed(app, api, wares, treatments);
|
||||
}
|
||||
|
||||
return api;
|
||||
}
|
||||
|
||||
module.exports = configure;
|
||||
|
||||
+9
-3
@@ -92,11 +92,17 @@ function entries (name, storage) {
|
||||
with_collection(function(err, collection) {
|
||||
if (err) { fn(err); return; }
|
||||
// potentially a batch insert
|
||||
collection.insert(docs, function (err, created) {
|
||||
// execute the callback
|
||||
fn(err, created, docs);
|
||||
var firstErr = null,
|
||||
totalCreated = 0;
|
||||
|
||||
docs.forEach(function(doc) {
|
||||
collection.update(doc, doc, {upsert: true}, function (err, created) {
|
||||
firstErr = firstErr || err;
|
||||
totalCreated += created;
|
||||
});
|
||||
});
|
||||
fn(firstErr, totalCreated, docs);
|
||||
});
|
||||
}
|
||||
|
||||
function getEntry(fn, id) {
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
'use strict';
|
||||
|
||||
function configure (collection, storage) {
|
||||
|
||||
function create (obj, fn) {
|
||||
obj.created_at = (new Date( )).toISOString( );
|
||||
api( ).insert(obj, function (err, doc) {
|
||||
fn(null, doc);
|
||||
});
|
||||
}
|
||||
|
||||
function list (fn) {
|
||||
return api( ).find({ }).sort({created_at: -1}).toArray(fn);
|
||||
}
|
||||
|
||||
function api ( ) {
|
||||
return storage.pool.db.collection(collection);
|
||||
}
|
||||
|
||||
api.list = list;
|
||||
api.create = create;
|
||||
return api;
|
||||
}
|
||||
module.exports = configure;
|
||||
+36
-10
@@ -1,5 +1,5 @@
|
||||
|
||||
function websocket (env, server, entries) {
|
||||
function websocket (env, server, entries, treatments) {
|
||||
"use strict";
|
||||
// CONSTANTS
|
||||
var ONE_HOUR = 3600000,
|
||||
@@ -24,8 +24,10 @@ var dir2Char = {
|
||||
var io;
|
||||
var watchers = 0;
|
||||
var now = new Date().getTime();
|
||||
var cgmData = [];
|
||||
var patientData = [];
|
||||
|
||||
var cgmData = [],
|
||||
treatmentData = [],
|
||||
patientData = [];
|
||||
|
||||
function start ( ) {
|
||||
io = require('socket.io').listen(server);
|
||||
@@ -101,6 +103,7 @@ function update() {
|
||||
now = Date.now();
|
||||
|
||||
cgmData = [];
|
||||
treatmentData = [];
|
||||
var earliest_data = now - TWO_DAYS;
|
||||
var q = { find: {"date": {"$gte": earliest_data}} };
|
||||
entries.list(q, function (err, results) {
|
||||
@@ -114,9 +117,16 @@ function update() {
|
||||
cgmData.push(obj);
|
||||
}
|
||||
});
|
||||
treatments.list(function (err, results) {
|
||||
treatmentData = results.map(function(treatment) {
|
||||
var timestamp = new Date(treatment.timestamp || treatment.created_at);
|
||||
treatment.x = timestamp.getTime();
|
||||
return treatment;
|
||||
});
|
||||
// all done, do loadData
|
||||
loadData( );
|
||||
});
|
||||
});
|
||||
|
||||
return update;
|
||||
}
|
||||
@@ -133,16 +143,21 @@ function emitAlarm(alarmType) {
|
||||
function loadData() {
|
||||
|
||||
console.log('running loadData');
|
||||
var treatment = [];
|
||||
var mbg = [];
|
||||
|
||||
var actual = [];
|
||||
var actual = [],
|
||||
actualCurrent,
|
||||
treatment = [],
|
||||
errorCode;
|
||||
|
||||
if (cgmData) {
|
||||
actual = cgmData.slice();
|
||||
actual.sort(function(a, b) {
|
||||
return a.x - b.x;
|
||||
});
|
||||
|
||||
actualCurrent = actual.length > 0 ? actual[actual.length - 1].y : null;
|
||||
|
||||
// sgv less than or equal to 10 means error code
|
||||
// or warm up period code, so ignore
|
||||
actual = actual.filter(function (a) {
|
||||
@@ -150,6 +165,15 @@ function loadData() {
|
||||
})
|
||||
}
|
||||
|
||||
if (treatmentData) {
|
||||
treatment = treatmentData.slice();
|
||||
treatment.sort(function(a, b) {
|
||||
return a.x - b.x;
|
||||
});
|
||||
}
|
||||
|
||||
if (actualCurrent && actualCurrent < 39) errorCode = actualCurrent;
|
||||
|
||||
var actualLength = actual.length - 1;
|
||||
|
||||
if (actualLength > 1) {
|
||||
@@ -181,8 +205,8 @@ function loadData() {
|
||||
//TODO: need to consider when data being sent has less than the 2 day minimum
|
||||
|
||||
// consolidate and send the data to the client
|
||||
var shouldEmit = is_different(actual, predicted, mbg, treatment);
|
||||
patientData = [actual, predicted, mbg, treatment];
|
||||
var shouldEmit = is_different(actual, predicted, mbg, treatment, errorCode);
|
||||
patientData = [actual, predicted, mbg, treatment, errorCode];
|
||||
console.log('patientData', patientData.length);
|
||||
if (shouldEmit) {
|
||||
emitData( );
|
||||
@@ -195,16 +219,16 @@ function loadData() {
|
||||
avgLoss += 1 / size * Math.pow(log10(predicted[j].y / 120), 2);
|
||||
}
|
||||
|
||||
//console.log(alarms['urgent_alarm'].threshold);
|
||||
//console.log(alarms['alarm'].threshold);
|
||||
if (avgLoss > alarms['urgent_alarm'].threshold) {
|
||||
emitAlarm('urgent_alarm');
|
||||
} else if (avgLoss > alarms['alarm'].threshold) {
|
||||
emitAlarm('alarm');
|
||||
} else if (errorCode) {
|
||||
emitAlarm('urgent_alarm');
|
||||
}
|
||||
}
|
||||
}
|
||||
function is_different (actual, predicted, mbg, treatment) {
|
||||
function is_different (actual, predicted, mbg, treatment, errorCode) {
|
||||
if (patientData && patientData.length < 3) {
|
||||
return true;
|
||||
}
|
||||
@@ -213,12 +237,14 @@ function loadData() {
|
||||
, predicted: patientData[1].slice(-1).pop( )
|
||||
, mbg: patientData[2].slice(-1).pop( )
|
||||
, treatment: patientData[3].slice(-1).pop( )
|
||||
, errorCode: patientData.length >= 5 ? patientData[4] : 0
|
||||
};
|
||||
var last = {
|
||||
actual: actual.slice(-1).pop( )
|
||||
, predicted: predicted.slice(-1).pop( )
|
||||
, mbg: mbg.slice(-1).pop( )
|
||||
, treatment: treatment.slice(-1).pop( )
|
||||
, errorCode: errorCode
|
||||
};
|
||||
|
||||
// textual diff of objects
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
"express": "^4.6.1",
|
||||
"express-extension-to-accept": "0.0.2",
|
||||
"mongodb": "^1.4.7",
|
||||
"moment": "2.8.1",
|
||||
"sgvdata": "0.0.2",
|
||||
"socket.io": "^0.9.17"
|
||||
},
|
||||
|
||||
@@ -31,7 +31,8 @@ var express = require('express');
|
||||
///////////////////////////////////////////////////
|
||||
var entries = require('./lib/entries')(env.mongo_collection, store);
|
||||
var settings = require('./lib/settings')(env.settings_collection, store);
|
||||
var api = require('./lib/api/')(env, entries, settings);
|
||||
var treatments = require('./lib/treatments')(env.treatments_collection, store);
|
||||
var api = require('./lib/api/')(env, entries, settings, treatments);
|
||||
var pebble = require('./lib/pebble');
|
||||
///////////////////////////////////////////////////
|
||||
|
||||
@@ -39,7 +40,6 @@ var pebble = require('./lib/pebble');
|
||||
// setup http server
|
||||
///////////////////////////////////////////////////
|
||||
var PORT = env.PORT;
|
||||
var THIRTY_DAYS = 2592000;
|
||||
|
||||
var app = express();
|
||||
var appInfo = software.name + ' ' + software.version;
|
||||
@@ -57,7 +57,8 @@ app.get('/pebble', pebble(entries));
|
||||
//app.get('/package.json', software);
|
||||
|
||||
// define static server
|
||||
var staticFiles = express.static(env.static_files, {maxAge: THIRTY_DAYS * 1000});
|
||||
//TODO: JC - changed cache to 1 hour from 30d ays to bypass cache hell until we have a real solution
|
||||
var staticFiles = express.static(env.static_files, {maxAge: 60 * 60 * 1000});
|
||||
|
||||
// serve the static content
|
||||
app.use(staticFiles);
|
||||
@@ -76,7 +77,7 @@ store(function ready ( ) {
|
||||
// setup socket io for data and message transmission
|
||||
///////////////////////////////////////////////////
|
||||
var websocket = require('./lib/websocket');
|
||||
var io = websocket(env, server, entries);
|
||||
var io = websocket(env, server, entries, treatments);
|
||||
});
|
||||
|
||||
///////////////////////////////////////////////////
|
||||
|
||||
+27
-2
@@ -17,6 +17,31 @@
|
||||
#drawer i {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
#treatmentDrawer {
|
||||
background-color: #666;
|
||||
border-left: 1px solid #999;
|
||||
box-shadow: inset 4px 4px 5px 0px rgba(50, 50, 50, 0.75);
|
||||
color: #eee;
|
||||
display: none;
|
||||
font-size: 16px;
|
||||
height: calc(100% - 45px);
|
||||
overflow-y: auto;
|
||||
position: absolute;
|
||||
margin-top: 45px;
|
||||
right: -200px;
|
||||
width: 300px;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
#treatmentDrawer i {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
#treatmentDrawer a {
|
||||
color: white;
|
||||
}
|
||||
|
||||
#about {
|
||||
margin-top: 1em;
|
||||
}
|
||||
@@ -98,7 +123,7 @@ h1, legend,
|
||||
padding: 0;
|
||||
float: right;
|
||||
height: 44px;
|
||||
width: 180px;
|
||||
width: 190px;
|
||||
opacity: 0.75;
|
||||
vertical-align: middle;
|
||||
}
|
||||
@@ -118,7 +143,7 @@ h1, legend,
|
||||
#buttonbar a {
|
||||
float: left;
|
||||
text-decoration: none;
|
||||
width: 44px;
|
||||
width: 34px;
|
||||
}
|
||||
#buttonbar i {
|
||||
padding-left: 12px;
|
||||
|
||||
+1
-1
@@ -136,7 +136,7 @@ body {
|
||||
|
||||
#bgButton,
|
||||
#silenceBtn {
|
||||
z-index: 999;
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
#bgButton {
|
||||
|
||||
@@ -60,6 +60,12 @@
|
||||
"code": 59392,
|
||||
"src": "mfglabs"
|
||||
},
|
||||
{
|
||||
"uid": "55e2ff85b1c459c383f46da6e96014b0",
|
||||
"css": "plus",
|
||||
"code": 59403,
|
||||
"src": "elusive"
|
||||
},
|
||||
{
|
||||
"uid": "b59b3b618699b467541f631edd5a02ed",
|
||||
"css": "volume",
|
||||
|
||||
Vendored
+1
@@ -10,3 +10,4 @@
|
||||
.icon-battery-100:before { content: '\e808'; } /* '' */
|
||||
.icon-cancel-circled:before { content: '\e809'; } /* '' */
|
||||
.icon-volume:before { content: '\e80a'; } /* '' */
|
||||
.icon-plus:before { content: '\e80b'; } /* '' */
|
||||
+7
-6
File diff suppressed because one or more lines are too long
+1
@@ -10,3 +10,4 @@
|
||||
.icon-battery-100 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-cancel-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-volume { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-plus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
Vendored
+1
@@ -21,3 +21,4 @@
|
||||
.icon-battery-100 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-cancel-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-volume { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-plus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
Vendored
+7
-6
@@ -1,10 +1,10 @@
|
||||
@font-face {
|
||||
font-family: 'fontello';
|
||||
src: url('../font/fontello.eot?77167544');
|
||||
src: url('../font/fontello.eot?77167544#iefix') format('embedded-opentype'),
|
||||
url('../font/fontello.woff?77167544') format('woff'),
|
||||
url('../font/fontello.ttf?77167544') format('truetype'),
|
||||
url('../font/fontello.svg?77167544#fontello') format('svg');
|
||||
src: url('../font/fontello.eot?48374311');
|
||||
src: url('../font/fontello.eot?48374311#iefix') format('embedded-opentype'),
|
||||
url('../font/fontello.woff?48374311') format('woff'),
|
||||
url('../font/fontello.ttf?48374311') format('truetype'),
|
||||
url('../font/fontello.svg?48374311#fontello') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
@media screen and (-webkit-min-device-pixel-ratio:0) {
|
||||
@font-face {
|
||||
font-family: 'fontello';
|
||||
src: url('../font/fontello.svg?77167544#fontello') format('svg');
|
||||
src: url('../font/fontello.svg?48374311#fontello') format('svg');
|
||||
}
|
||||
}
|
||||
*/
|
||||
@@ -61,3 +61,4 @@
|
||||
.icon-battery-100:before { content: '\e808'; } /* '' */
|
||||
.icon-cancel-circled:before { content: '\e809'; } /* '' */
|
||||
.icon-volume:before { content: '\e80a'; } /* '' */
|
||||
.icon-plus:before { content: '\e80b'; } /* '' */
|
||||
@@ -270,6 +270,7 @@ body {
|
||||
<div title="Code: 0xe808" class="the-icons span3"><i class="icon-battery-100"></i> <span class="i-name">icon-battery-100</span><span class="i-code">0xe808</span></div>
|
||||
<div title="Code: 0xe809" class="the-icons span3"><i class="icon-cancel-circled"></i> <span class="i-name">icon-cancel-circled</span><span class="i-code">0xe809</span></div>
|
||||
<div title="Code: 0xe80a" class="the-icons span3"><i class="icon-volume"></i> <span class="i-name">icon-volume</span><span class="i-code">0xe80a</span></div>
|
||||
<div title="Code: 0xe80b" class="the-icons span3"><i class="icon-plus"></i> <span class="i-name">icon-plus</span><span class="i-code">0xe80b</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container footer">Generated by <a href="http://fontello.com">fontello.com</a></div>
|
||||
|
||||
Binary file not shown.
@@ -17,6 +17,7 @@
|
||||
<glyph glyph-name="battery-100" unicode="" d="m365 194q-21 0-37 15t-15 37l0 209q0 21 15 36t37 15 36-15 15-36l0-209q0-21-15-37t-36-15z m-156 0q-21 0-37 15t-16 37l0 209q0 21 16 36t37 15 36-15 15-36l0-209q0-21-15-37t-36-15z m469 0q-22 0-37 15t-16 37l0 209q0 21 16 36t37 15 36-15 15-36l0-209q0-21-15-37t-36-15z m-157 0q-21 0-37 15t-15 37l0 209q0 21 15 36t37 15 37-15 15-36l0-209q0-21-15-37t-37-15z m364 312q44 0 74-30t31-73l0-104q0-44-31-75t-74-30q0-65-46-111t-110-45l-573 0q-65 0-110 45t-46 111l0 312q0 65 46 111t110 46l573 0q65 0 110-46t46-111z m-104-312l0 312q0 22-15 37t-37 16l-573 0q-21 0-37-16t-15-37l0-312q0-21 15-36t37-15l573 0q21 0 37 15t15 36z" horiz-adv-x="990" />
|
||||
<glyph glyph-name="cancel-circled" unicode="" d="m420 770q174 0 297-123t123-297-123-297-297-123-297 123-123 297 123 297 297 123z m86-420l154 154-86 86-154-152-152 152-88-86 154-154-154-152 88-86 152 152 154-152 86 86z" horiz-adv-x="840" />
|
||||
<glyph glyph-name="volume" unicode="" d="m0 142l0 416 236 0 354 289 0-994-354 289-236 0z m652 35q73 74 73 176t-73 178l71 74q105-106 107-254 0-145-107-246z m118-119q123 119 123 295t-123 299l76 74q154-154 154-372t-154-372z" horiz-adv-x="1000" />
|
||||
<glyph glyph-name="plus" unicode="" d="m0 209l0 282 359 0 0 359 282 0 0-359 359 0 0-282-359 0 0-359-282 0 0 359-359 0z" horiz-adv-x="1000" />
|
||||
</font>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.3 KiB |
Binary file not shown.
Binary file not shown.
Executable
BIN
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
+60
-1
@@ -1,7 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="viewport" content="width=device-width, maximum-scale=1, initial-scale=1, user-scalable=0" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<link rel="apple-touch-icon" href="/images/logomobile.png">
|
||||
<title>NightScout</title>
|
||||
<link href="/images/round1.png" rel="icon" id="favicon" type="image/png" />
|
||||
<link rel="stylesheet" type="text/css" href="/css/main.css?v=0.3.3" />
|
||||
@@ -16,6 +18,7 @@
|
||||
<a id="testAlarms" class="tip" original-title="Alarm Test / Smartphone Enable" href="#"><i class="icon-volume"></i></a>
|
||||
<a id="hideToolbar" class="tip" original-title="Hides the toolbar" href="#"><i class="icon-angle-double-up"></i></a>
|
||||
<a id="drawerToggle" class="tip" original-title="Settings" href="#"><i class="icon-cog"></i></a>
|
||||
<a id="treatmentDrawerToggle" class="tip" original-title="Treatments" href="#"><i class="icon-plus"></i></a>
|
||||
</div>
|
||||
<h1 class="customTitle">Nightscout</h1>
|
||||
</div>
|
||||
@@ -68,6 +71,11 @@
|
||||
<dd><input type="radio" name="units-browser" id="mgdl-browser" value="mg/dl" checked /><label for="mgdl-browser">mg/dL</label><br />
|
||||
<dd><input type="radio" name="units-browser" id="mmol-browser" value="mmol" /><label for="mmol-browser">mmol/L</label>
|
||||
</dl>
|
||||
<dl class="toggle">
|
||||
<dt>Enable Alarms <a class="tip" original-title="When enabled the an alarm will sound."><i class="icon-help-circled"></i></a></dt>
|
||||
<dd><input type="checkbox" name="alarmhigh-browser" id="alarmhigh-browser" /><label for="alarmhigh-browser">High Alarm</label></dd>
|
||||
<dd><input type="checkbox" name="alarmlow-browser" id="alarmlow-browser" /><label for="alarmlow-browser">Low Alarm</label></dd>
|
||||
</dl>
|
||||
<dl class="toggle">
|
||||
<dt>Night Mode <a class="tip" original-title="When enabled the page will be dimmed from 10pm - 6am."><i class="icon-help-circled"></i></a></dt>
|
||||
<dd><input type="checkbox" name="nightmode-browser" id="nightmode-browser" /><label for="nightmode-browser">Enable</label></dd>
|
||||
@@ -76,6 +84,11 @@
|
||||
<dt>Custom Title</dt>
|
||||
<dd><input type="text" id="customTitle" value="Nightscout" /></dd>
|
||||
</dl>
|
||||
<dl class="radio">
|
||||
<dt>Theme</dt>
|
||||
<dd><input type="radio" name="theme-browser" id="theme-default-browser" value="default" checked /><label for="theme-default-browser">Default</label><br />
|
||||
<dd><input type="radio" name="theme-browser" id="theme-colors-browser" value="colors" /><label for="theme-colors-browser">Colors</label>
|
||||
</dl>
|
||||
</fieldset>
|
||||
|
||||
<input type="submit" id="save" value="Save" />
|
||||
@@ -102,6 +115,52 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="treatmentDrawer">
|
||||
<form id="treatment-form" onsubmit="return treatmentSubmit();">
|
||||
<fieldset class="treatmentData">
|
||||
<legend>Log a Treatment</legend>
|
||||
<dl>
|
||||
<dt>Entered By:</dt>
|
||||
<dd><input type="text" id="enteredBy" value="" /></dd>
|
||||
</dl>
|
||||
<label for="eventType">Event Type:</label>
|
||||
<select id="eventType">
|
||||
<option value="BG Check">Blood Glucose Check</option>
|
||||
<option value="Snack Bolus">Snack Bolus</option>
|
||||
<option value="Meal Bolus">Meal Bolus</option>
|
||||
<option value="Correction Bolus">Correction Bolus</option>
|
||||
<option value="Food Correction">Food Correction</option>
|
||||
<option value="Question">Note or Question to Parents</option>
|
||||
</select>
|
||||
|
||||
<fieldset>
|
||||
<legend>Glucose Reading</legend>
|
||||
<input type="number" step="any" id="glucoseValue" />
|
||||
<label><br>Measurement Method:<br></label>
|
||||
<label for="meter">
|
||||
<input type="radio" name="glucoseType" id="meter" value="Finger"/>
|
||||
<span>Meter</span>
|
||||
</label>
|
||||
<label for="sensor">
|
||||
<input type="radio" name="glucoseType" id="sensor" value="Sensor"/>
|
||||
<span>Sensor<br></span>
|
||||
</label>
|
||||
</fieldset>
|
||||
<label for="carbsGiven">Carbs Given (mass in grams):<br></label>
|
||||
<input type="number" step="any" min="0" id="carbsGiven" />
|
||||
<br>
|
||||
<label for="insulinGiven"><br>Insulin Given (units):<br></label>
|
||||
<input type="number" step="any" min="0" id="insulinGiven" />
|
||||
<br>
|
||||
<!-- Label and textarea -->
|
||||
<label for="notes"><br>Additional Notes, Comments:<br></label>
|
||||
<textarea id="notes"></textarea><br>
|
||||
<button type="submit">Submit Form</button>
|
||||
</fieldset>
|
||||
<a href="/treatments.html" target="treatments">View all treatments</a>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="audio alarms">
|
||||
<audio src="/audio/alarm.mp3" preload="auto" loop="true" class="alarm mp3" type="audio/mp3"></audio>
|
||||
<audio src="/audio/alarm2.mp3" preload="auto" loop="true" class="urgent alarm2 mp3" type="audio/mp3"></audio>
|
||||
|
||||
+172
-53
@@ -1,8 +1,8 @@
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
var retrospectivePredictor = true,
|
||||
latestSGV,
|
||||
var latestSGV,
|
||||
errorCode,
|
||||
treatments,
|
||||
padding = { top: 20, right: 10, bottom: 30, left: 10 },
|
||||
opacity = {current: 1, DAY: 1, NIGHT: 0.5},
|
||||
@@ -23,7 +23,9 @@
|
||||
clip,
|
||||
TWENTY_FIVE_MINS_IN_MS = 1500000,
|
||||
THIRTY_MINS_IN_MS = 1800000,
|
||||
FORTY_MINS_IN_MS = 2400000,
|
||||
FORTY_TWO_MINS_IN_MS = 2520000,
|
||||
SIXTY_MINS_IN_MS = 3600000,
|
||||
FOCUS_DATA_RANGE_MS = 12600000, // 3.5 hours of actual data
|
||||
FORMAT_TIME = '%I:%M%', //alternate format '%H:%M'
|
||||
audio = document.getElementById('audio'),
|
||||
@@ -41,6 +43,14 @@
|
||||
var tickValues = [2.0, 3.0, 4.0, 6.0, 10.0, 15.0, 22.0];
|
||||
}
|
||||
|
||||
//TODO: get these from the config
|
||||
var targetTop = 180,
|
||||
targetBottom = 80;
|
||||
|
||||
var futureOpacity = d3.scale.linear( )
|
||||
.domain([TWENTY_FIVE_MINS_IN_MS, SIXTY_MINS_IN_MS])
|
||||
.range([0.8, 0.1]);
|
||||
|
||||
// create svg and g to contain the chart contents
|
||||
var charts = d3.select('#chartContainer').append('svg')
|
||||
.append('g')
|
||||
@@ -135,7 +145,7 @@
|
||||
// get the desired opacity for context chart based on the brush extent
|
||||
function highlightBrushPoints(data) {
|
||||
if (data.date.getTime() >= brush.extent()[0].getTime() && data.date.getTime() <= brush.extent()[1].getTime()) {
|
||||
return 1;
|
||||
return futureOpacity(data.date - latestSGV.x);
|
||||
} else {
|
||||
return 0.5;
|
||||
}
|
||||
@@ -211,15 +221,15 @@
|
||||
}
|
||||
|
||||
var element = document.getElementById('bgButton').hidden == '';
|
||||
|
||||
var nowDate = new Date(brushExtent[1] - THIRTY_MINS_IN_MS);
|
||||
|
||||
// predict for retrospective data
|
||||
if (retrospectivePredictor && brushExtent[1].getTime() - THIRTY_MINS_IN_MS < now && element != true) {
|
||||
if (brushExtent[1].getTime() - THIRTY_MINS_IN_MS < now && element != true) {
|
||||
// filter data for -12 and +5 minutes from reference time for retrospective focus data prediction
|
||||
var nowData = data.filter(function(d) {
|
||||
return d.date.getTime() >= brushExtent[1].getTime() - FORTY_TWO_MINS_IN_MS &&
|
||||
d.date.getTime() <= brushExtent[1].getTime() - TWENTY_FIVE_MINS_IN_MS
|
||||
d.date.getTime() <= brushExtent[1].getTime() - TWENTY_FIVE_MINS_IN_MS &&
|
||||
d.color != 'none';
|
||||
});
|
||||
if (nowData.length > 1) {
|
||||
var prediction = predictAR(nowData);
|
||||
@@ -238,18 +248,69 @@
|
||||
$('#currentTime')
|
||||
.text(formatTime(new Date(brushExtent[1] - THIRTY_MINS_IN_MS)))
|
||||
.css('text-decoration','line-through');
|
||||
} else if (retrospectivePredictor) {
|
||||
|
||||
$('#lastEntry').text("RETRO").removeClass('current');
|
||||
|
||||
$('.container #noButton .currentBG').css({color: 'grey'});
|
||||
$('.container #noButton .currentDirection').css({color: 'grey'});
|
||||
|
||||
} else {
|
||||
// if the brush comes back into the current time range then it should reset to the current time and sg
|
||||
var nowData = data.filter(function(d) {
|
||||
return d.color != 'none';
|
||||
});
|
||||
nowData = [nowData[nowData.length - 2], nowData[nowData.length - 1]];
|
||||
var prediction = predictAR(nowData);
|
||||
focusData = focusData.concat(prediction);
|
||||
var dateTime = new Date(now);
|
||||
nowDate = dateTime;
|
||||
$('#currentTime')
|
||||
.text(formatTime(dateTime))
|
||||
.css('text-decoration', '');
|
||||
|
||||
if (errorCode) {
|
||||
var errorDisplay;
|
||||
|
||||
switch (parseInt(errorCode)) {
|
||||
case 0: errorDisplay = '??0'; break; //None
|
||||
case 1: errorDisplay = '?SN'; break; //SENSOR_NOT_ACTIVE
|
||||
case 2: errorDisplay = '??2'; break; //MINIMAL_DEVIATION
|
||||
case 3: errorDisplay = '?NA'; break; //NO_ANTENNA
|
||||
case 5: errorDisplay = '?NC'; break; //SENSOR_NOT_CALIBRATED
|
||||
case 6: errorDisplay = '?CD'; break; //COUNTS_DEVIATION
|
||||
case 7: errorDisplay = '??7'; break; //?
|
||||
case 8: errorDisplay = '??8'; break; //?
|
||||
case 9: errorDisplay = '⌛'; break; //ABSOLUTE_DEVIATION
|
||||
case 10: errorDisplay = '???'; break; //POWER_DEVIATION
|
||||
case 12: errorDisplay = '?RF'; break; //BAD_RF
|
||||
default: errorDisplay = '?' + parseInt(errorCode) + '?'; break;
|
||||
}
|
||||
|
||||
$('#lastEntry').text("CGM ERROR").removeClass('current').addClass("urgent");
|
||||
|
||||
$('.container .currentBG').html(errorDisplay)
|
||||
.css('text-decoration', '');
|
||||
$('.container .currentDirection').html('✖');
|
||||
|
||||
var color = sgvToColor(errorCode);
|
||||
$('.container #noButton .currentBG').css({color: color});
|
||||
$('.container #noButton .currentDirection').css({color: color});
|
||||
|
||||
} else {
|
||||
|
||||
var secsSinceLast = (Date.now() - new Date(latestSGV.x).getTime()) / 1000;
|
||||
$('#lastEntry').text(timeAgo(secsSinceLast)).toggleClass('current', secsSinceLast < 10 * 60);
|
||||
|
||||
$('.container .currentBG')
|
||||
.text(scaleBg(latestSGV.y))
|
||||
.css('text-decoration', '');
|
||||
$('.container .currentDirection')
|
||||
.html(latestSGV.direction);
|
||||
|
||||
var color = sgvToColor(latestSGV.y);
|
||||
$('.container #noButton .currentBG').css({color: color});
|
||||
$('.container #noButton .currentDirection').css({color: color});
|
||||
}
|
||||
}
|
||||
|
||||
xScale.domain(brush.extent());
|
||||
@@ -271,6 +332,7 @@
|
||||
.attr('cx', function (d) { return xScale(d.date); })
|
||||
.attr('cy', function (d) { return yScale(d.sgv); })
|
||||
.attr('fill', function (d) { return d.color; })
|
||||
.attr('opacity', function (d) { return futureOpacity(d.date - latestSGV.x); })
|
||||
.attr('r', 3);
|
||||
|
||||
focusCircles.exit()
|
||||
@@ -556,18 +618,18 @@
|
||||
.transition()
|
||||
.duration(UPDATE_TRANS_MS)
|
||||
.attr('x1', xScale2(dataRange[0]))
|
||||
.attr('y1', yScale2(scaleBg(180)))
|
||||
.attr('y1', yScale2(scaleBg(targetTop)))
|
||||
.attr('x2', xScale2(dataRange[1]))
|
||||
.attr('y2', yScale2(scaleBg(180)));
|
||||
.attr('y2', yScale2(scaleBg(targetTop)));
|
||||
|
||||
// transition low line to correct location
|
||||
context.select('.low-line')
|
||||
.transition()
|
||||
.duration(UPDATE_TRANS_MS)
|
||||
.attr('x1', xScale2(dataRange[0]))
|
||||
.attr('y1', yScale2(scaleBg(80)))
|
||||
.attr('y1', yScale2(scaleBg(targetBottom)))
|
||||
.attr('x2', xScale2(dataRange[1]))
|
||||
.attr('y2', yScale2(scaleBg(80)));
|
||||
.attr('y2', yScale2(scaleBg(targetBottom)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -653,7 +715,6 @@
|
||||
socket.on('now', function (d) {
|
||||
now = d;
|
||||
var dateTime = new Date(now);
|
||||
// lixgbg old: $('#currentTime').text(d3.time.format('%I:%M%p')(dateTime));
|
||||
$('#currentTime').text(formatTime(dateTime));
|
||||
|
||||
// Dim the screen by reducing the opacity when at nighttime
|
||||
@@ -668,41 +729,36 @@
|
||||
|
||||
socket.on('sgv', function (d) {
|
||||
if (d.length > 1) {
|
||||
errorCode = d.length >= 5 ? d[4] : undefined;
|
||||
|
||||
// change the next line so that it uses the prediction if the signal gets lost (max 1/2 hr)
|
||||
if (d[0].length) {
|
||||
var current = d[0][d[0].length - 1];
|
||||
latestSGV = current;
|
||||
var secsSinceLast = (Date.now() - new Date(current.x).getTime()) / 1000;
|
||||
var currentBG = current.y;
|
||||
latestSGV = d[0][d[0].length - 1];
|
||||
|
||||
//TODO: currently these are filtered on the server
|
||||
//TODO: use icons for these magic values
|
||||
switch (current.y) {
|
||||
case 0: currentBG = '??0'; break; //None
|
||||
case 1: currentBG = '?SN'; break; //SENSOR_NOT_ACTIVE
|
||||
case 2: currentBG = '??2'; break; //MINIMAL_DEVIATION
|
||||
case 3: currentBG = '?NA'; break; //NO_ANTENNA
|
||||
case 5: currentBG = '?NC'; break; //SENSOR_NOT_CALIBRATED
|
||||
case 6: currentBG = '?CD'; break; //COUNTS_DEVIATION
|
||||
case 7: currentBG = '??7'; break; //?
|
||||
case 8: currentBG = '??8'; break; //?
|
||||
case 9: currentBG = '?AD'; break; //ABSOLUTE_DEVIATION
|
||||
case 10: currentBG = '?PD'; break; //POWER_DEVIATION
|
||||
case 12: currentBG = '?RF'; break; //BAD_RF
|
||||
default:
|
||||
currentBG = scaleBg(currentBG);
|
||||
break;
|
||||
//TODO: alarmHigh/alarmLow probably shouldn't be here
|
||||
if (browserSettings.alarmHigh) {
|
||||
$('.container .current').toggleClass('high', latestSGV.y > 180);
|
||||
}
|
||||
|
||||
$('#lastEntry').text(timeAgo(secsSinceLast)).toggleClass('current', secsSinceLast < 10 * 60);
|
||||
$('.container .currentBG').text(currentBG);
|
||||
$('.container .currentDirection').html(current.direction);
|
||||
$('.container .current').toggleClass('high', current.y > 180).toggleClass('low', current.y < 70)
|
||||
if (browserSettings.alarmLow) {
|
||||
$('.container .current').toggleClass('low', latestSGV.y < 70);
|
||||
}
|
||||
data = d[0].map(function (obj) { return { date: new Date(obj.x), sgv: scaleBg(obj.y), direction: obj.direction, color: 'grey'} });
|
||||
data = data.concat(d[1].map(function (obj) { return { date: new Date(obj.x), sgv: scaleBg(obj.y), color: 'blue'} }));
|
||||
}
|
||||
data = d[0].map(function (obj) {
|
||||
return { date: new Date(obj.x), sgv: scaleBg(obj.y), direction: obj.direction, color: sgvToColor(obj.y)}
|
||||
});
|
||||
// TODO: This is a kludge to advance the time as data becomes stale by making old predictor clear (using color = 'none')
|
||||
// This shouldn't have to be sent and can be fixed by using xScale.domain([x0,x1]) function with
|
||||
// 2 days before now as x0 and 30 minutes from now for x1 for context plot, but this will be
|
||||
// required to happen when "now" event is sent from websocket.js every minute. When fixed,
|
||||
// remove all "color != 'none'" code
|
||||
data = data.concat(d[1].map(function (obj) { return { date: new Date(obj.x), sgv: scaleBg(obj.y), color: 'none'} }));
|
||||
data = data.concat(d[2].map(function (obj) { return { date: new Date(obj.x), sgv: scaleBg(obj.y), color: 'red'} }));
|
||||
|
||||
data.forEach(function (d) {
|
||||
if (d.sgv < 39)
|
||||
d.color = "transparent";
|
||||
})
|
||||
|
||||
treatments = d[3];
|
||||
if (!isInitialData) {
|
||||
isInitialData = true;
|
||||
@@ -713,6 +769,22 @@
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function sgvToColor(sgv) {
|
||||
var color = 'grey';
|
||||
|
||||
if (browserSettings.theme == "colors") {
|
||||
if (sgv > targetTop) {
|
||||
color = 'yellow';
|
||||
} else if (sgv >= targetBottom && sgv <= targetTop) {
|
||||
color = '#4cff00';
|
||||
} else if (sgv < targetBottom) {
|
||||
color = 'red';
|
||||
}
|
||||
}
|
||||
|
||||
return color;
|
||||
}
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
@@ -722,16 +794,20 @@
|
||||
console.log('Client connected to server.')
|
||||
});
|
||||
socket.on('alarm', function () {
|
||||
if (browserSettings.alarmHigh) {
|
||||
console.log("Alarm raised!");
|
||||
currentAlarmType = 'alarm';
|
||||
generateAlarm(alarmSound);
|
||||
}
|
||||
brushInProgress = false;
|
||||
updateChart(false);
|
||||
});
|
||||
socket.on('urgent_alarm', function () {
|
||||
if (browserSettings.alarmLow) {
|
||||
console.log("Urgent alarm raised!");
|
||||
currentAlarmType = 'urgent_alarm';
|
||||
generateAlarm(urgentAlarmSound);
|
||||
}
|
||||
brushInProgress = false;
|
||||
updateChart(false);
|
||||
});
|
||||
@@ -851,6 +927,18 @@
|
||||
//draw a compact visualization of a treatment (carbs, insulin)
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
function drawTreatment(treatment, scale, showValues) {
|
||||
|
||||
if (!treatment.CR) {
|
||||
//plot a simple treatment point
|
||||
console.info("plotting treatment", treatment);
|
||||
var treatmentDots = focus.selectAll('treatment-dot')
|
||||
.data(treatment)
|
||||
.enter()
|
||||
.append('g')
|
||||
.attr('transform', 'translate(' + xScale(treatment.x) + ', ' + yScale(scaleBg(300)) + ')');
|
||||
|
||||
//TODO: some d3 magic to get the treatments to display and show a tooltip
|
||||
} else {
|
||||
var carbs = treatment.carbs;
|
||||
var insulin = treatment.insulin;
|
||||
var CR = treatment.CR;
|
||||
@@ -874,10 +962,18 @@
|
||||
if (insulin > 0) arc_data[5].element = Math.round(insulin * 10) / 10 + ' U';
|
||||
|
||||
var arc = d3.svg.arc()
|
||||
.innerRadius(function (d) { return 5 * d.inner; })
|
||||
.outerRadius(function (d) { return 5 * d.outer; })
|
||||
.endAngle(function (d) { return d.start; })
|
||||
.startAngle(function (d) { return d.end; });
|
||||
.innerRadius(function (d) {
|
||||
return 5 * d.inner;
|
||||
})
|
||||
.outerRadius(function (d) {
|
||||
return 5 * d.outer;
|
||||
})
|
||||
.endAngle(function (d) {
|
||||
return d.start;
|
||||
})
|
||||
.startAngle(function (d) {
|
||||
return d.end;
|
||||
});
|
||||
|
||||
var treatmentDots = focus.selectAll('treatment-dot')
|
||||
.data(arc_data)
|
||||
@@ -887,8 +983,12 @@
|
||||
|
||||
var arcs = treatmentDots.append('path')
|
||||
.attr('class', 'path')
|
||||
.attr('fill', function (d, i) { return d.color; })
|
||||
.attr('id', function (d, i) { return 's' + i; })
|
||||
.attr('fill', function (d, i) {
|
||||
return d.color;
|
||||
})
|
||||
.attr('id', function (d, i) {
|
||||
return 's' + i;
|
||||
})
|
||||
.attr('d', arc);
|
||||
|
||||
|
||||
@@ -908,7 +1008,10 @@
|
||||
d.innerRadius = d.outerRadius * 2.1;
|
||||
return 'translate(' + arc.centroid(d) + ')';
|
||||
})
|
||||
.text(function (d) { return d.element; })
|
||||
.text(function (d) {
|
||||
return d.element;
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -922,6 +1025,8 @@
|
||||
var BG_REF = 140;
|
||||
var BG_MIN = 36;
|
||||
var BG_MAX = 400;
|
||||
// these are the one sigma limits for the first 13 prediction interval uncertainties (65 minutes)
|
||||
var CONE = [0.020, 0.041, 0.061, 0.081, 0.099, 0.116, 0.132, 0.146, 0.159, 0.171, 0.182, 0.192, 0.201];
|
||||
if (actual.length < 2) {
|
||||
var y = [Math.log(actual[0].sgv / BG_REF), Math.log(actual[0].sgv / BG_REF)];
|
||||
} else {
|
||||
@@ -932,17 +1037,31 @@
|
||||
y = [Math.log(actual[0].sgv / BG_REF), Math.log(actual[0].sgv / BG_REF)];
|
||||
}
|
||||
}
|
||||
var n = 20;
|
||||
var AR = [-0.723, 1.716];
|
||||
var dt = actual[1].date.getTime();
|
||||
for (var i = 0; i <= n; i++) {
|
||||
var predictedColor = 'blue';
|
||||
if (browserSettings.theme == "colors") {
|
||||
predictedColor = 'cyan';
|
||||
}
|
||||
for (var i = 0; i < CONE.length; i++) {
|
||||
y = [y[1], AR[0] * y[0] + AR[1] * y[1]];
|
||||
dt = dt + FIVE_MINUTES;
|
||||
predicted[i] = {
|
||||
date: new Date(dt+3000),
|
||||
sgv: Math.max(BG_MIN, Math.min(BG_MAX, Math.round(BG_REF * Math.exp(y[1])))),
|
||||
color: 'blue'
|
||||
// Add 2000 ms so not same point as SG
|
||||
predicted[i * 2] = {
|
||||
date: new Date(dt + 2000),
|
||||
sgv: Math.max(BG_MIN, Math.min(BG_MAX, Math.round(BG_REF * Math.exp((y[1] - 2 * CONE[i]))))),
|
||||
color: predictedColor
|
||||
};
|
||||
// Add 4000 ms so not same point as SG
|
||||
predicted[i * 2 + 1] = {
|
||||
date: new Date(dt + 4000),
|
||||
sgv: Math.max(BG_MIN, Math.min(BG_MAX, Math.round(BG_REF * Math.exp((y[1] + 2 * CONE[i]))))),
|
||||
color: predictedColor
|
||||
};
|
||||
predicted.forEach(function (d) {
|
||||
if (d.sgv < BG_MIN)
|
||||
d.color = "transparent";
|
||||
})
|
||||
}
|
||||
return predicted;
|
||||
}
|
||||
|
||||
+115
-7
@@ -1,9 +1,13 @@
|
||||
var drawerIsOpen = false;
|
||||
var treatmentDrawerIsOpen = false;
|
||||
var browserStorage = $.localStorage;
|
||||
var defaultSettings = {
|
||||
"units": "mg/dl",
|
||||
"nightMode": false
|
||||
}
|
||||
"alarmHigh": true,
|
||||
"alarmLow": true,
|
||||
"nightMode": false,
|
||||
"theme": "default"
|
||||
};
|
||||
|
||||
var app = {};
|
||||
$.ajax("/api/v1/status.json", {
|
||||
@@ -26,29 +30,42 @@ $.ajax("/api/v1/status.json", {
|
||||
function getBrowserSettings(storage) {
|
||||
var json = {};
|
||||
try {
|
||||
json = {
|
||||
var json = {
|
||||
"units": storage.get("units"),
|
||||
"alarmHigh": storage.get("alarmHigh"),
|
||||
"alarmLow": storage.get("alarmLow"),
|
||||
"nightMode": storage.get("nightMode"),
|
||||
"customTitle": storage.get("customTitle")
|
||||
"customTitle": storage.get("customTitle"),
|
||||
"theme": storage.get("theme")
|
||||
};
|
||||
|
||||
// Default browser units to server units if undefined.
|
||||
json.units = setDefault(json.units, serverSettings.units);
|
||||
//console.log("browserSettings.units: " + json.units);
|
||||
if (json.units == "mmol") {
|
||||
$("#mmol-browser").prop("checked", true);
|
||||
} else {
|
||||
$("#mgdl-browser").prop("checked", true);
|
||||
}
|
||||
|
||||
json.alarmHigh = setDefault(json.alarmHigh, defaultSettings.alarmHigh);
|
||||
$("#alarmhigh-browser").prop("checked", json.alarmHigh);
|
||||
json.alarmLow = setDefault(json.alarmLow, defaultSettings.alarmLow);
|
||||
$("#alarmlow-browser").prop("checked", json.alarmLow);
|
||||
|
||||
json.nightMode = setDefault(json.nightMode, defaultSettings.nightMode);
|
||||
$("#nightmode-browser").prop("checked", json.nightMode);
|
||||
|
||||
if (json.customTitle) {
|
||||
$("h1.customTitle").html(json.customTitle);
|
||||
$("h1.customTitle").text(json.customTitle);
|
||||
$("input#customTitle").prop("value", json.customTitle);
|
||||
document.title = "Nightscout: " + json.customTitle;
|
||||
}
|
||||
|
||||
if (json.theme == "colors") {
|
||||
$("#theme-colors-browser").prop("checked", true);
|
||||
} else {
|
||||
$("#theme-default-browser").prop("checked", true);
|
||||
}
|
||||
}
|
||||
catch(err) {
|
||||
showLocalstorageError();
|
||||
@@ -84,12 +101,23 @@ function jsonIsNotEmpty(json) {
|
||||
}
|
||||
function storeInBrowser(json, storage) {
|
||||
if (json.units) storage.set("units", json.units);
|
||||
if (json.alarmHigh == true) {
|
||||
storage.set("alarmHigh", true)
|
||||
} else {
|
||||
storage.set("alarmHigh", false)
|
||||
}
|
||||
if (json.alarmLow == true) {
|
||||
storage.set("alarmLow", true)
|
||||
} else {
|
||||
storage.set("alarmLow", false)
|
||||
}
|
||||
if (json.nightMode == true) {
|
||||
storage.set("nightMode", true)
|
||||
} else {
|
||||
storage.set("nightMode", false)
|
||||
}
|
||||
if (json.customTitle) storage.set("customTitle", json.customTitle);
|
||||
if (json.theme) storage.set("theme", json.theme);
|
||||
event.preventDefault();
|
||||
}
|
||||
function storeOnServer(json) {
|
||||
@@ -132,6 +160,7 @@ function closeDrawer(callback) {
|
||||
});
|
||||
drawerIsOpen = false;
|
||||
}
|
||||
|
||||
function openDrawer() {
|
||||
drawerIsOpen = true;
|
||||
$("#container").animate({marginLeft: "-200px"}, 300);
|
||||
@@ -140,6 +169,30 @@ function openDrawer() {
|
||||
$("#drawer").animate({right: "0"}, 300);
|
||||
}
|
||||
|
||||
function closeTreatmentDrawer(callback) {
|
||||
$("#container").animate({marginLeft: "0px"}, 400, callback);
|
||||
$("#chartContainer").animate({marginLeft: "0px"}, 400);
|
||||
$("#treatmentDrawer").animate({right: "-300px"}, 400, function() {
|
||||
$("#treatmentDrawer").css("display", "none");
|
||||
});
|
||||
treatmentDrawerIsOpen = false;
|
||||
}
|
||||
function openTreatmentDrawer() {
|
||||
treatmentDrawerIsOpen = true;
|
||||
$("#container").animate({marginLeft: "-300px"}, 400);
|
||||
$("#chartContainer").animate({marginLeft: "-300px"}, 400);
|
||||
$("#treatmentDrawer").css("display", "block");
|
||||
$("#treatmentDrawer").animate({right: "0"}, 400);
|
||||
|
||||
$('#enteredBy').val(browserStorage.get("enteredBy") || '');
|
||||
$('#eventType').val('BG Check');
|
||||
$('#glucoseValue').val('');
|
||||
$('#meter').prop('checked', true)
|
||||
$('#carbsGiven').val('');
|
||||
$('#insulinGiven').val('');
|
||||
$('#notes').val('');
|
||||
}
|
||||
|
||||
|
||||
function closeNotification() {
|
||||
var notify = $("#notification");
|
||||
@@ -195,6 +248,33 @@ function stretchStatusForToolbar(toolbarState){
|
||||
}
|
||||
}
|
||||
|
||||
function treatmentSubmit(event) {
|
||||
|
||||
var data = new Object();
|
||||
data.enteredBy = document.getElementById("enteredBy").value;
|
||||
data.eventType = document.getElementById("eventType").value;
|
||||
data.glucose = document.getElementById("glucoseValue").value;
|
||||
data.glucoseType = $('#treatment-form input[name=glucoseType]:checked').val();
|
||||
data.carbs = document.getElementById("carbsGiven").value;
|
||||
data.insulin = document.getElementById("insulinGiven").value;
|
||||
data.notes = document.getElementById("notes").value;
|
||||
|
||||
var dataJson = JSON.stringify(data, null, " ");
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", "/api/v1/treatments/", true);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
xhr.send(dataJson);
|
||||
|
||||
browserStorage.set("enteredBy", data.enteredBy);
|
||||
|
||||
closeTreatmentDrawer();
|
||||
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var querystring = getQueryParms();
|
||||
// var serverSettings = getServerSettings();
|
||||
@@ -222,6 +302,12 @@ Dropdown.prototype.open = function (e) {
|
||||
|
||||
|
||||
$("#drawerToggle").click(function(event) {
|
||||
//close other drawers
|
||||
if(treatmentDrawerIsOpen) {
|
||||
closeTreatmentDrawer();
|
||||
treatmentDrawerIsOpen = false;
|
||||
}
|
||||
|
||||
if(drawerIsOpen) {
|
||||
closeDrawer();
|
||||
drawerIsOpen = false;
|
||||
@@ -232,6 +318,25 @@ $("#drawerToggle").click(function(event) {
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
$("#treatmentDrawerToggle").click(function(event) {
|
||||
//close other drawers
|
||||
if(drawerIsOpen) {
|
||||
closeDrawer();
|
||||
drawerIsOpen = false;
|
||||
}
|
||||
|
||||
if(treatmentDrawerIsOpen) {
|
||||
closeTreatmentDrawer();
|
||||
treatmentDrawerIsOpen = false;
|
||||
} else {
|
||||
openTreatmentDrawer();
|
||||
treatmentDrawerIsOpen = true;
|
||||
}
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
$("#treatmentDrawer button").click(treatmentSubmit);
|
||||
|
||||
$("#notification").click(function(event) {
|
||||
closeNotification();
|
||||
event.preventDefault();
|
||||
@@ -255,8 +360,11 @@ $("#showToolbar").find("a").click(function(event) {
|
||||
$("input#save").click(function() {
|
||||
storeInBrowser({
|
||||
"units": $("input:radio[name=units-browser]:checked").val(),
|
||||
"alarmHigh": $("#alarmhigh-browser").prop("checked"),
|
||||
"alarmLow": $("#alarmlow-browser").prop("checked"),
|
||||
"nightMode": $("#nightmode-browser").prop("checked"),
|
||||
"customTitle": $("input#customTitle").prop("value")
|
||||
"customTitle": $("input#customTitle").prop("value"),
|
||||
"theme": $("input:radio[name=theme-browser]:checked").val()
|
||||
}, browserStorage);
|
||||
|
||||
storeOnServer({
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Nightscout: Treatments</title>
|
||||
<link href="/images/round1.png" rel="icon" id="favicon" type="image/png" />
|
||||
<link rel="stylesheet" href="/bower_components/bootstrap/dist/css/bootstrap.min.css">
|
||||
<style type="text/css">
|
||||
table tr td {
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
<script src="/bower_components/angularjs/angular.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
'use strict';
|
||||
|
||||
var app = angular.module('ns-treatments', []);
|
||||
|
||||
app.controller('TreatmentsController', function ($scope, $http, $timeout, visibility) {
|
||||
var pageIsHidden = false;
|
||||
|
||||
function update() {
|
||||
console.info("update called");
|
||||
if (pageIsHidden) {
|
||||
console.info('Not updating, since page is hidden');
|
||||
} else {
|
||||
console.info('Updating, since page is visible');
|
||||
$http.get('/api/v1/treatments').success(function(treatments) {
|
||||
console.info("got treatments", treatments);
|
||||
$scope.treatments = treatments;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$scope.$on('visibilityChanged', function(event, isHidden) {
|
||||
console.info('changed pageIsHidden to ' + isHidden);
|
||||
pageIsHidden = isHidden;
|
||||
if (!pageIsHidden) update();
|
||||
});
|
||||
|
||||
function startUpdateCycle() {
|
||||
update();
|
||||
$timeout(startUpdateCycle, 20 * 1000);
|
||||
}
|
||||
|
||||
startUpdateCycle();
|
||||
|
||||
$scope.glucoseDisplay = function(treatment) {
|
||||
if (treatment.glucose)
|
||||
return treatment.glucose + (treatment.glucoseType ? ' (' + treatment.glucoseType + ')' : '');
|
||||
else
|
||||
return '';
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
app.service('visibility', function visibility($rootScope) {
|
||||
function visibilityChanged() {
|
||||
$rootScope.$broadcast('visibilityChanged', !!(document.hidden || document.webkitHidden || document.mozHidden || document.msHidden));
|
||||
}
|
||||
|
||||
document.addEventListener('webkitvisibilitychange', visibilityChanged);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body ng-app="ns-treatments">
|
||||
<div class="container" ng-controller="TreatmentsController">
|
||||
<h3>Nightscout: Treatments</h3>
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Time</td>
|
||||
<td>Event Type</td>
|
||||
<td>BG</td>
|
||||
<td>Insulin</td>
|
||||
<td>Carbs</td>
|
||||
<td>Entered By</td>
|
||||
<td>Notes</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="treatment in treatments">
|
||||
<td>{{treatment.created_at | date:'short'}}</td>
|
||||
<td>{{treatment.eventType}}</td>
|
||||
<td>{{glucoseDisplay(treatment)}}</td>
|
||||
<td>{{treatment.insulin | number: 2}}</td>
|
||||
<td>{{treatment.carbs}}</td>
|
||||
<td>{{treatment.enteredBy}}</td>
|
||||
<td>{{treatment.notes}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
db.treatments.find().forEach(
|
||||
function (elem) {
|
||||
db.treatments.update(
|
||||
{
|
||||
_id: elem._id
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
glucose: elem.glucoseValue,
|
||||
insulin: elem.insulinGiven,
|
||||
carbs: elem.carbsGiven
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,125 @@
|
||||
'use strict';
|
||||
///////////////////////////////////////////////////
|
||||
// This script is intended to be run as a cron job
|
||||
// every n-minutes or whatever the equiv is on windows
|
||||
//
|
||||
// Author: John A. [euclidjda](https://github.com/euclidjda)
|
||||
// Source: https://gist.github.com/euclidjda/4ae207a89921f21382a9
|
||||
///////////////////////////////////////////////////
|
||||
|
||||
///////////////////////////////////////////////////
|
||||
// DB Connection setup and utils
|
||||
///////////////////////////////////////////////////
|
||||
|
||||
var mongodb = require('mongodb');
|
||||
var software = require('./package.json');
|
||||
var env = require('./env')( );
|
||||
|
||||
main();
|
||||
|
||||
function main( ) {
|
||||
|
||||
var MongoClient = mongodb.MongoClient;
|
||||
|
||||
MongoClient.connect(env.mongo, function connected (err, db) {
|
||||
|
||||
console.log("Connected to mongo, ERROR: %j", err);
|
||||
if (err) { throw err; }
|
||||
populate_collection( db );
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function populate_collection( db ) {
|
||||
|
||||
//console.log( 'mongo = ' + env.mongo );
|
||||
//console.log( 'collection = ' + env.mongo_collection );
|
||||
|
||||
var cgm_collection = db.collection( env.mongo_collection );
|
||||
|
||||
var new_cgm_record = get_cgm_record();
|
||||
|
||||
cgm_collection.insert( new_cgm_record, function(err,created) {
|
||||
|
||||
// TODO: Error checking
|
||||
process.exit( 0 );
|
||||
|
||||
} );
|
||||
|
||||
|
||||
}
|
||||
|
||||
function get_cgm_record( ) {
|
||||
|
||||
var dateobj = new Date();
|
||||
var datemil = dateobj.getTime();
|
||||
var datesec = datemil / 1000;
|
||||
var datestr = getDateString( dateobj );
|
||||
|
||||
// We put the time in a range from -1 to +1 for every thiry minute period
|
||||
var range = (datesec % 1800)/900 - 1.0;
|
||||
|
||||
// The we push through a COS function and scale between 40 and 400 (so it is like a bg level)
|
||||
var sgv = Math.floor(360*(Math.cos( 10.0 * range / 3.14 ) / 2 + 0.5)) + 40;
|
||||
var dir = range > 0.0 ? "FortyFiveDown" : "FortyFiveUp";
|
||||
|
||||
console.log( 'Writing Record: ');
|
||||
console.log( 'sgv = ' + sgv );
|
||||
console.log( 'date = ' + datemil );
|
||||
console.log( 'dir = ' + dir );
|
||||
console.log( 'str = ' + datestr );
|
||||
|
||||
var mondo_db = null;
|
||||
var doc = { 'device' :'dexcom' ,
|
||||
'date' : datemil ,
|
||||
'sgv' : sgv ,
|
||||
'direction' : dir ,
|
||||
'dateString' : datestr };
|
||||
|
||||
|
||||
return doc;
|
||||
}
|
||||
|
||||
function getDateString( d ) {
|
||||
|
||||
// How I wish js had strftime. This would be one line of code!
|
||||
|
||||
var month = d.getMonth();
|
||||
var day = d.getDay();
|
||||
var year = d.getFullYear();
|
||||
|
||||
if (month < 10 ) month = '0'+month;
|
||||
if (day < 10 ) day = '0'+day;
|
||||
|
||||
var hour = d.getHours();
|
||||
var min = d.getMinutes();
|
||||
var sec = d.getSeconds();
|
||||
|
||||
var ampm = 'PM';
|
||||
|
||||
if (hour < 12)
|
||||
{
|
||||
ampm = "AM";
|
||||
}
|
||||
else
|
||||
{
|
||||
ampm = "PM";
|
||||
}
|
||||
|
||||
if (hour == 0)
|
||||
{
|
||||
hour = 12;
|
||||
}
|
||||
if (hour > 12)
|
||||
{
|
||||
hour = hour - 12;
|
||||
}
|
||||
|
||||
if (hour < 10) hour = '0' + hour;
|
||||
if (min < 10) min = '0' + min;
|
||||
if (sec < 10) sec = '0' + sec;
|
||||
|
||||
return month + '/' + day + '/' + year + ' ' + hour + ':' + min + ':' + sec + ' ' + ampm;
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user