Skip to content

Commit

Permalink
Add support for Loop/OpenAPS predictions
Browse files Browse the repository at this point in the history
  • Loading branch information
mddub committed Oct 19, 2016
1 parent 1d65975 commit 42c77ff
Show file tree
Hide file tree
Showing 16 changed files with 303 additions and 45 deletions.
52 changes: 52 additions & 0 deletions config/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,58 @@ <h3>PUMP DATA:</h3>
</div>
</div>

<div class="item-container">
<h3>PREDICTED BGS:</h3>
<div class="item-container-footer">
Predicted BG values can be shown from Loop or OpenAPS. If you enable predictions, you should also adjust the point size under the "Graph" tab so that more readings can be shown.
</div>
</div>

<div class="item-container">
<div class="item-container-content">
<label class="item">
<input name="predictEnabled" type="checkbox" class="item-checkbox">
Show predicted BGs
</label>
<label class="item prediction-setting">
Prediction source
<select name="predictSource" dir="rtl" class="item-select">
<option value="loop" class="item-select-option">Loop</option>
<option value="openaps" class="item-select-option">OpenAPS</option>
</select>
</label>
<label class="item prediction-setting">
Maximum length
<select name="predictMaxLength" class="item-select">
<option value="6" class="item-select-option">30 minutes</option>
<option value="12" class="item-select-option">1 hour</option>
<option value="18" class="item-select-option">1.5 hours</option>
<option value="24" class="item-select-option">2 hours</option>
<option value="30" class="item-select-option">2.5 hours</option>
<option value="36" class="item-select-option">3 hours</option>
<option value="42" class="item-select-option">3.5 hours</option>
<option value="48" class="item-select-option">4 hours</option>
<option value="54" class="item-select-option">4.5 hours</option>
<option value="60" class="item-select-option">5 hours</option>
</select>
</label>
<div class="color-platforms-only">
<label class="item prediction-setting">
Color in range
<input type="text" class="item-color item-color-normal" name="predictColorDefault" value="#000000">
</label>
<label class="item prediction-setting">
Color above range
<input type="text" class="item-color item-color-normal" name="predictColorHigh" value="#000000">
</label>
<label class="item prediction-setting">
Color below range
<input type="text" class="item-color item-color-normal" name="predictColorLow" value="#000000">
</label>
</div>
</div>
</div>

<div class="item-container">
<h3>UPDATE SETTINGS:</h3>
</div>
Expand Down
12 changes: 12 additions & 0 deletions config/js/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,10 @@
$('[name=bolusTicks]').prop('checked', !!current['bolusTicks']);
$('[name=basalGraph]').prop('checked', !!current['basalGraph']);

$('[name=predictEnabled]').prop('checked', !!current['predictEnabled']);
$('[name=predictSource]').val(current['predictSource']);
$('[name=predictMaxLength]').val(current['predictMaxLength']);

$('[name=updateEveryMinute]').val(current['updateEveryMinute'] ? 'true' : 'false');

$('[name=layout][value=' + current.layout + ']').addClass('active');
Expand Down Expand Up @@ -656,6 +660,9 @@
batteryAsNumber: $('[name=batteryAsNumber][value=number]').hasClass('active'),
bolusTicks: $('[name=bolusTicks]').is(':checked'),
basalGraph: $('[name=basalGraph]').is(':checked'),
predictEnabled: $('[name=predictEnabled]').is(':checked'),
predictSource: $('[name=predictSource]').val(),
predictMaxLength: tryParseInt($('[name=predictMaxLength]').val()),
updateEveryMinute: $('[name=updateEveryMinute]').val() === 'true',
layout: $('[name=layout].active').attr('value'),
advancedLayout: $('[name=advancedLayout]').is(':checked'),
Expand Down Expand Up @@ -739,6 +746,11 @@
});
$('#basalGraph').trigger('change');

$('[name=predictEnabled]').on('change', function(evt) {
$('.prediction-setting').toggle($(evt.currentTarget).is(':checked'));
});
$('[name=predictEnabled]').trigger('change');

$('[name=pointStyle]').on('click', onPointStyleClick);

$('[name=pointShape]').on('click', setTimeout.bind(this, onPointSettingsChange, 0));
Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@
"statusText": 7,
"graphExtra": 8,
"statusRecency": 9,
"prediction1": 10,
"prediction2": 11,
"prediction3": 12,
"predictionRecency": 13,

"mmol": 1,
"topOfGraph": 2,
Expand Down
28 changes: 26 additions & 2 deletions src/app_messages.c
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ bool get_byte_array(DictionaryIterator *data, uint8_t *dest, uint8_t key, size_t
if (required) {
return fail_missing_required_value(key);
} else {
memcpy(dest, fallback, ARRAY_LENGTH(fallback));
memcpy(dest, fallback, max_length * sizeof(uint8_t));
return pass_default_value(key, "byte array");
}
}
Expand Down Expand Up @@ -113,6 +113,15 @@ bool get_cstring(DictionaryIterator *data, char *dest, uint8_t key, size_t max_l
}
}

static void get_prediction(DictionaryIterator *data, uint8_t *dest, uint8_t key, uint8_t *max_length) {
Tuple *t = dict_find(data, key);
if (t && t->type == TUPLE_BYTE_ARRAY) {
uint8_t length = t->length < PREDICTION_MAX_LENGTH ? t->length : PREDICTION_MAX_LENGTH;
memcpy(dest, t->value->data, length);
*max_length = length > *max_length ? length : *max_length;
}
}

bool validate_data_message(DictionaryIterator *data, DataMessage *out) {
/*
* Validation is not necessary for messages from the PebbleKit JS half of
Expand All @@ -125,7 +134,12 @@ bool validate_data_message(DictionaryIterator *data, DataMessage *out) {

out->received_at = time(NULL);

return true
// If the series are of different lengths, pad the shorter ones with zeroes
memcpy(out->prediction_1, zeroes, PREDICTION_MAX_LENGTH * sizeof(uint8_t));
memcpy(out->prediction_2, zeroes, PREDICTION_MAX_LENGTH * sizeof(uint8_t));
memcpy(out->prediction_3, zeroes, PREDICTION_MAX_LENGTH * sizeof(uint8_t));

bool success = true
&& get_int32(data, &out->recency, MESSAGE_KEY_recency, false, 0)
&& get_byte_array(data, out->sgvs, MESSAGE_KEY_sgvs, GRAPH_MAX_SGV_COUNT, true, NULL)
&& get_byte_array_length(data, &out->sgv_count, GRAPH_MAX_SGV_COUNT, MESSAGE_KEY_sgvs)
Expand All @@ -135,6 +149,16 @@ bool validate_data_message(DictionaryIterator *data, DataMessage *out) {
&& get_cstring(data, out->status_text, MESSAGE_KEY_statusText, STATUS_BAR_MAX_LENGTH, false, "")
&& get_int32(data, &out->status_recency, MESSAGE_KEY_statusRecency, false, -1)
&& get_byte_array(data, (uint8_t*)out->graph_extra, MESSAGE_KEY_graphExtra, GRAPH_MAX_SGV_COUNT, false, zeroes);

out->prediction_length = 0;
get_prediction(data, out->prediction_1, MESSAGE_KEY_prediction1, &out->prediction_length);
get_prediction(data, out->prediction_2, MESSAGE_KEY_prediction2, &out->prediction_length);
get_prediction(data, out->prediction_3, MESSAGE_KEY_prediction3, &out->prediction_length);
if (out->prediction_length > 0) {
success = success && get_int32(data, &out->prediction_recency, MESSAGE_KEY_predictionRecency, false, 0);
}

return success;
}

static DataMessage *_last_data_message = NULL;
Expand Down
6 changes: 6 additions & 0 deletions src/app_messages.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

#define GRAPH_MAX_SGV_COUNT 144
#define STATUS_BAR_MAX_LENGTH 256
#define PREDICTION_MAX_LENGTH 60
#define NO_DELTA_VALUE 65536

typedef union GraphExtra {
Expand All @@ -26,6 +27,11 @@ typedef struct __attribute__((__packed__)) DataMessage {
char status_text[STATUS_BAR_MAX_LENGTH];
int32_t status_recency;
GraphExtra graph_extra[GRAPH_MAX_SGV_COUNT];
uint8_t prediction_length;
uint8_t prediction_1[PREDICTION_MAX_LENGTH];
uint8_t prediction_2[PREDICTION_MAX_LENGTH];
uint8_t prediction_3[PREDICTION_MAX_LENGTH];
int32_t prediction_recency;
} DataMessage;

bool get_int32(DictionaryIterator *data, int32_t *dest, uint8_t key, bool required, int32_t fallback);
Expand Down
7 changes: 5 additions & 2 deletions src/comm.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
#include <pebble.h>
#include "app_messages.h"

// Can be up to ~620 when status bar text is 255 characters and point width is 1px
#define CONTENT_SIZE 768
// This can theoretically be maxed out to 984 bytes by combining:
// - status bar text of 255 characters
// - point width of 1px (144 points + 144 "graph extra")
// - 3 prediction series of length 60
#define CONTENT_SIZE 1024

// There are many failure modes...
#define INITIAL_TIMEOUT_HALVED 2500
Expand Down
114 changes: 86 additions & 28 deletions src/graph_element.c
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include "staleness.h"

#define BOLUS_TICK_HEIGHT 7
#define MAX_PREDICTION_AGE_TO_SHOW_SECONDS (20*60)
#define NO_BG 32767

static GPoint center_of_point(int16_t x, int16_t y) {
Expand Down Expand Up @@ -40,7 +41,7 @@ static int16_t bg_to_y(int16_t height, int16_t bg, Preferences *prefs) {
return (float)height - (float)(bg - graph_min) / (float)(graph_max - graph_min) * (float)height + 0.5f;
}

static int16_t index_to_x(uint8_t i, uint8_t graph_width, uint8_t padding) {
static int16_t index_to_x(int16_t i, uint8_t graph_width, uint8_t padding) {
return graph_width - (get_prefs()->point_width + get_prefs()->point_margin) * (1 + i + padding) + get_prefs()->point_margin - get_prefs()->point_right_margin;
}

Expand All @@ -58,6 +59,7 @@ static int16_t bg_to_y_for_point(uint8_t height, int16_t bg, Preferences *prefs)
}
}

#ifdef PBL_COLOR
static GColor color_for_bg(int16_t bg, Preferences *prefs) {
if (bg > prefs->top_of_range) {
return prefs->colors[COLOR_KEY_POINT_HIGH];
Expand All @@ -68,6 +70,17 @@ static GColor color_for_bg(int16_t bg, Preferences *prefs) {
}
}

static GColor color_for_predicted_bg(int16_t bg, Preferences *prefs) {
if (bg > prefs->top_of_range) {
return get_prefs()->colors[COLOR_KEY_PREDICT_HIGH];
} else if (bg < prefs->bottom_of_range) {
return get_prefs()->colors[COLOR_KEY_PREDICT_LOW];
} else {
return get_prefs()->colors[COLOR_KEY_PREDICT_DEFAULT];
}
}
#endif

static void fill_rect_gray(GContext *ctx, GRect bounds, GColor previous_color) {
graphics_context_set_fill_color(ctx, GColorLightGray);
graphics_fill_rect(ctx, bounds, 0, GCornerNone);
Expand All @@ -79,8 +92,7 @@ static uint8_t sgv_graph_height(int16_t available_height) {
}

static void graph_update_proc(Layer *layer, GContext *ctx) {
uint8_t i;
int16_t x, y;
int16_t i, x, y;
GSize layer_size = layer_get_bounds(layer).size;
uint8_t graph_width = layer_size.w;
uint8_t graph_height = sgv_graph_height(layer_size.h);
Expand All @@ -89,7 +101,6 @@ static void graph_update_proc(Layer *layer, GContext *ctx) {
GColor color = ((GraphData*)layer_get_data(layer))->color;
graphics_context_set_stroke_color(ctx, color);
graphics_context_set_fill_color(ctx, color);
uint8_t padding = graph_staleness_padding();

// Target range bounds
// Draw bounds symmetrically, on the inside of the range
Expand Down Expand Up @@ -117,6 +128,25 @@ static void graph_update_proc(Layer *layer, GContext *ctx) {
return;
}

uint8_t sgv_padding = sgv_graph_padding();

// Prediction preprocessing
uint8_t prediction_skip = 0;
uint8_t prediction_padding = 0;
if (data->prediction_length > 0) {
// Show prediction points starting 2.5 minutes after the most recent SGV
time_t future_boundary = data->received_at - data->recency + 5 * 60 * sgv_padding + 150;
time_t prediction_start_time = data->received_at - data->prediction_recency;
if (prediction_start_time < future_boundary) {
prediction_skip = (future_boundary - prediction_start_time + 299) / 300;
} else {
prediction_padding = (prediction_start_time - future_boundary) / 300;
}
}

uint8_t padding = data->prediction_length - prediction_skip + prediction_padding + sgv_padding;
int16_t prediction_line_x = index_to_x(-1, graph_width, padding - sgv_padding) - prefs->point_margin - 1;

// Line and point preprocessing
static GPoint to_plot[GRAPH_MAX_SGV_COUNT];
int16_t bg;
Expand All @@ -136,6 +166,42 @@ static void graph_update_proc(Layer *layer, GContext *ctx) {
}
uint8_t plot_count = i;

// Basals
if (prefs->basal_graph) {
graphics_draw_line(ctx, GPoint(0, graph_height), GPoint(graph_width, graph_height));
for(i = 0; i < data->sgv_count; i++) {
uint8_t basal = data->graph_extra[i].basal;
x = index_to_x(i, graph_width, padding);
y = layer_size.h - basal;
uint8_t width = prefs->point_width + prefs->point_margin;
if (prefs->point_margin < 0 && i == 0) {
// if points overlap and this is the rightmost point, extend its basal to the right edge
width -= prefs->point_margin;
}
if (i == data->sgv_count - 1 && x >= 0) {
// if this is the last point to draw, extend its basal data to the left edge
width += x;
x = 0;
}
graphics_draw_line(ctx, GPoint(x, y), GPoint(x + width - 1, y));
if (basal > 1) {
fill_rect_gray(ctx, GRect(x, y + 1, width, basal - 1), color);
}
}
if (sgv_padding > 0) {
x = index_to_x(padding - 1, graph_width, 0);
graphics_fill_rect(ctx, GRect(x, graph_height, prediction_line_x - x, prefs->basal_height), 0, GCornerNone);
}
}

// Vertical line dividing history from prediction
if (data->prediction_length > 0) {
graphics_context_set_stroke_color(ctx, color);
for (y = 1; y < layer_size.h; y += 2) {
graphics_draw_pixel(ctx, GPoint(prediction_line_x, y));
}
}

// Line
if (prefs->plot_line) {
graphics_context_set_stroke_width(ctx, prefs->plot_line_width);
Expand Down Expand Up @@ -169,6 +235,22 @@ static void graph_update_proc(Layer *layer, GContext *ctx) {
}
}

// Prediction
if (data->prediction_length > 0 && data->received_at - data->prediction_recency >= time(NULL) - MAX_PREDICTION_AGE_TO_SHOW_SECONDS) {
uint8_t* series[3] = {data->prediction_1, data->prediction_2, data->prediction_3};
for(uint8_t si = 0; si < 3; si++) {
for(i = prediction_skip; i < data->prediction_length; i++) {
bg = series[si][i] * 2;
if (bg == 0) {
continue;
}
x = index_to_x(-i + prediction_skip - prediction_padding - 1, graph_width, padding - sgv_padding);
y = bg_to_y_for_point(graph_height, bg, prefs);
plot_point(x, y, COLOR_FALLBACK(color_for_predicted_bg(bg, prefs), GColorLightGray), ctx);
}
}
}

graphics_context_set_fill_color(ctx, color);
graphics_context_set_stroke_color(ctx, color);

Expand All @@ -179,30 +261,6 @@ static void graph_update_proc(Layer *layer, GContext *ctx) {
plot_tick(x, graph_height, ctx, prefs->point_width);
}
}

// Basals
if (prefs->basal_graph) {
graphics_draw_line(ctx, GPoint(0, graph_height), GPoint(graph_width, graph_height));
for(i = 0; i < data->sgv_count; i++) {
uint8_t basal = data->graph_extra[i].basal;
x = index_to_x(i, graph_width, padding);
y = layer_size.h - basal;
uint8_t width = prefs->point_width + prefs->point_margin;
if (i == data->sgv_count - 1 && x >= 0) {
// if this is the last point to draw, extend its basal data to the left edge
width += x;
x = 0;
}
graphics_draw_line(ctx, GPoint(x, y), GPoint(x + width - 1, y));
if (basal > 1) {
fill_rect_gray(ctx, GRect(x, y + 1, width, basal - 1), color);
}
}
if (padding > 0) {
x = index_to_x(padding - 1, graph_width, 0);
graphics_fill_rect(ctx, GRect(x, graph_height, graph_width - x, prefs->basal_height), 0, GCornerNone);
}
}
}

static void recency_size_changed(GSize size, void *context) {
Expand Down
Loading

0 comments on commit 42c77ff

Please sign in to comment.