diff --git a/README.md b/README.md index f2278e5..9c83a41 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,26 @@ # openaps-menu -This is the repository holding the menu-based software code, which you may choose to add to an Explorer HAT or other screen-based rig in order to visualize and enter information into a Pi-based #OpenAPS rig. +This is the repository holding the menu-based software code, which you may choose to add to an Explorer HAT or other screen-based rig (e.g., Adafruit Radiofruit Bonnet) in order to visualize and enter information into a Pi-based #OpenAPS rig. See [here](https://github.com/EnhancedRadioDevices/Explorer-HAT) for more details on the Explorer HAT hardware. +You can set your preferred auto-updating status screen using the following setting in your `~/myopenaps/preferences.json`: + +`"status_screen": "bigbgstatus"` will display the big BG status screen (no graph). + +`"status_screen": "off"` will not auto-update the status screen. + +`"status_screen": "blank"` will wipe the screen during the auto-update, but will wake up when you press a button to let you access menu options. + +By default, the auto-updating status script will invert the display about 50% of the time, to prevent burn-in on the OLED screen. You can turn this off with the following setting in your `~/myopenaps/preferences.json`: + +`"wearOLEDevenly": "off"` + +Or you can have it invert the display from 8pm to 8am with: + +`"wearOLEDevenly": "nightandday"` + + ## Example screen outputs (Note: these are examples. The latest code may yield different menu items and screen displays) ### Status screen: diff --git a/config/buttons-explorerhat.json b/config/buttons-explorerhat.json new file mode 100644 index 0000000..6f1a4f1 --- /dev/null +++ b/config/buttons-explorerhat.json @@ -0,0 +1,10 @@ +{ + "gpios": { + "buttonUp": 17, + "buttonDown": 27 + }, + "options": { + "socketPath": "/var/run/pi-buttons.sock", + "reconnectTimeout": 3000 + } +} diff --git a/config/buttons-radiofruit.json b/config/buttons-radiofruit.json new file mode 100644 index 0000000..c36d2a8 --- /dev/null +++ b/config/buttons-radiofruit.json @@ -0,0 +1,10 @@ +{ + "gpios": { + "buttonUp": 5, + "buttonDown": 6 + }, + "options": { + "socketPath": "/var/run/pi-buttons.sock", + "reconnectTimeout": 3000 + } +} diff --git a/config/buttons.json b/config/buttons.json deleted file mode 100644 index c7b9e38..0000000 --- a/config/buttons.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "pins": { - "buttonUp": 11, - "buttonDown": 13 - }, - "options": { - "pressed": 200, - "clicked": 400 - } -} diff --git a/config/display.json b/config/display.json index ec77703..427d0c0 100644 --- a/config/display.json +++ b/config/display.json @@ -1,5 +1,12 @@ { - "title": " ", - "height": 64, - "displayLines": 8 + "radiofruit": { + "title": " ", + "height": 32, + "displayLines": 4 + }, + "explorerHat": { + "title": " ", + "height": 64, + "displayLines": 8 + } } diff --git a/config/menus/menu.json b/config/menus/menu.json index a0a1d67..e345388 100644 --- a/config/menus/menu.json +++ b/config/menus/menu.json @@ -4,8 +4,11 @@ "menu": [ { "label": "Status Graph", - "command": "node scripts/status.js", - "emit": "nothing" + "emit": "showgraphstatus" + }, + { + "label": "Big BG Status", + "emit": "showbigBGstatus" }, { "label": "Set Temp Target", @@ -54,8 +57,7 @@ }, { "label": "Unicorn Logo", - "command": "node scripts/unicorn.js", - "emit": "nothing" + "emit": "showlogo" } ] }, @@ -141,6 +143,16 @@ "label": "Cancel Reboot", "command": "shutdown -c; echo Reboot canceled", "emit": "showoutput" + }, + { + "label": "Reboot NOW", + "command": "node scripts/display_image.js ./static/black.png; shutdown -r now;", + "emit": "nothing" + }, + { + "label": "Shutdown", + "command": "node scripts/display_image.js ./static/black.png; shutdown -P now", + "emit": "nothing" } ] } diff --git a/index.js b/index.js index 51b62ed..9d9e726 100644 --- a/index.js +++ b/index.js @@ -10,21 +10,46 @@ const i2c = require('i2c-bus'); const path = require('path'); const pngparse = require('pngparse'); const extend = require('extend'); +var fs = require('fs'); var i2cBus = i2c.openSync(1); -// setup the display -var displayConfig = require('./config/display.json'); +var openapsDir = "/root/myopenaps"; //if you're using a nonstandard OpenAPS directory, set that here. NOT RECOMMENDED. + +try { + var preferences = JSON.parse(fs.readFileSync(openapsDir+"/preferences.json")); +} catch (e) { + console.error("Could not load preferences.json", e); +} + +// setup the display, depending on its size (Eadiofruit is 128x32 and Explorer HAT is 128x64) +if (preferences.hardwaretype && preferences.hardwaretype == "radiofruit") { + var displayConfig = require('./config/display.json').radiofruit; +} else { + var displayConfig = require('./config/display.json').explorerHat; +} + displayConfig.i2cBus = i2cBus; -var display = require('./lib/display/ssd1306')(displayConfig); -// display the logo -pngparse.parseFile('./static/unicorn.png', function(err, image) { - if(err) - throw err - display.clear(); - display.oled.drawBitmap(image.data); -}); +try { + var display = require('./lib/display/ssd1306')(displayConfig); + if (preferences.hardwaretype && preferences.hardwaretype == "radiofruit") { + displayImage('./static/unicorn_128x32.png'); + } else { + displayImage('./static/unicorn_128x64.png'); + } +} catch (e) { + console.warn("Could not setup display:", e); +} + +function displayImage(pathToImage) { + pngparse.parseFile(pathToImage, function(err, image) { + if(err) + throw err + display.clear(); + display.oled.drawBitmap(image.data); + }); +} // setup battery voltage monitor var voltageConfig = require('./config/voltage.json') @@ -44,11 +69,36 @@ socketServer .on('warning', (warn) => { console.log('socket-server warning: ', warn.reason) }) +.on('displaystatus', function () { + if (display) { + if (preferences.status_screen && preferences.status_screen == "bigbgstatus") { + bigBGStatus(display, openapsDir); + } else if (preferences.status_screen && preferences.status_screen == "off") { + //don't auto-update the screen if it's turned off + } else if (preferences.status_screen && preferences.status_screen == "blank") { + display.clear(true); + } else if (preferences.hardwaretype && preferences.hardwaretype == "radiofruit") { + radiofruitStatus(display, openapsDir); //radiofruit text status script + } else { + graphStatus(display, openapsDir); //default to graph status + } + } +}) - +// load up graphical status scripts +const graphStatus = require('./scripts/status.js'); +const bigBGStatus = require('./scripts/big_bg_status.js'); +const radiofruitStatus = require('./scripts/status-radiofruit.js'); +// if you want to add your own status display script, it will be easiest to replace one of the above! // setup the menus -var buttonsConfig = require('./config/buttons.json'); + +if (preferences.hardwaretype && preferences.hardwaretype == "radiofruit") { + var buttonsConfig = require('./config/buttons-radiofruit.json'); +} else { + var buttonsConfig = require('./config/buttons-explorerhat.json'); +} + var menuConfig = { menuFile: process.cwd() + path.sep + './config/menus/menu.json', // file path for the menu definition onChange: showMenu, // method to call when menu changes @@ -64,6 +114,22 @@ var hidMenu = require('./lib/hid-menu/hid-menu')(buttonsConfig, menuConfig); hidMenu .on('nothing', function () { }) +.on('showgraphstatus', function () { + graphStatus(display, openapsDir); +}) +.on('showbigBGstatus', function () { + bigBGStatus(display, openapsDir); +}) +.on('showRadiofruitStatus', function () { + radiofruitStatus(display, openapsDir); +}) +.on('showlogo', function () { + if (preferences.hardwaretype && preferences.hardwaretype == "radiofruit") { + displayImage('./static/unicorn_128x32.png'); + } else { + displayImage('./static/unicorn_128x64.png'); + } +}) .on('showvoltage', function () { voltage() .then(function (v) { @@ -85,16 +151,18 @@ hidMenu // display the current menu on the display function showMenu(menu) { - display.clear(); - var text = ''; + if (display) { + display.clear(); + var text = ''; - var p = menu.getParentSelect(); - text += p ? '[' + p.label + ']\n' : ''; - var c = menu.getCurrentSelect(); - menu.getActiveMenu().forEach(function (m) { - text += (m.selected ? '>' : ' ') + m.label + '\n'; - }); + var p = menu.getParentSelect(); + text += p ? '[' + p.label + ']\n' : ''; + var c = menu.getCurrentSelect(); + menu.getActiveMenu().forEach(function (m) { + text += (m.selected ? '>' : ' ') + m.label + '\n'; + }); -// console.log(text); - display.write(text); + // console.log(text); + display.write(text); + } } diff --git a/lib/hid-menu/hid-menu.js b/lib/hid-menu/hid-menu.js index cfb427a..fec9b85 100644 --- a/lib/hid-menu/hid-menu.js +++ b/lib/hid-menu/hid-menu.js @@ -8,38 +8,36 @@ const Menube = require('menube'); function createHIDMenu(configButtons, configMenus) { - if (!configButtons.pins || !configButtons.pins.buttonUp || !configButtons.pins.buttonDown) { + if (!configButtons.gpios || !configButtons.gpios.buttonUp || !configButtons.gpios.buttonDown) { throw new Error('Incomplete pins definition in configuration.'); } - var pins = configButtons.pins; + var gpios = configButtons.gpios; var buttonOptions = configButtons.options || {}; var onChange = configMenus.onChange; var menu = Menube(configMenus.menuFile, configMenus.menuSettings); var displayDirty = false; - // var buttons = require('rpi-gpio-buttons')([pins.buttonUp, pins.buttonDown], buttonOptions); - var piButtons = require('../pi-buttons'); + var piButtons = require('node-pi-buttons')(configButtons.options); menu.on('menu_changed', function () { displayDirty = false; // the parent will redraw the display }); -// buttons piButtons - .on('clicked', function (pin) { + .on('clicked', function (gpio, data) { if (displayDirty) { // fake menu changed to force redraw menu.emit('menu_changed'); displayDirty = false; } else { - switch(pin) { - case pins.buttonUp: + switch(parseInt(gpio, 10)) { + case gpios.buttonUp: if (!displayDirty) { menu.menuUp(); } break; - case pins.buttonDown: + case gpios.buttonDown: if (!displayDirty) { menu.menuDown(); } @@ -47,31 +45,34 @@ function createHIDMenu(configButtons, configMenus) { } } }) - .on('double_clicked', function (pin) { + .on('double_clicked', function (gpio, data) { if (displayDirty) { // fake menu changed to force redraw menu.emit('menu_changed'); displayDirty = false; } else { - switch (pin) { - case pins.buttonUp: + switch (parseInt(gpio, 10)) { + case gpios.buttonUp: menu.menuBack(); break; - case pins.buttonDown: + case gpios.buttonDown: displayDirty = true; // activate may write something to the display menu.activateSelect(); break; } } }) - .on('released', function (pin) { + .on('released', function (gpio, data) { if (displayDirty) { // fake menu changed to force redraw menu.emit('menu_changed'); displayDirty = false; } + }) + .on('error', function (data) { + console.log('ERROR: ', data.error); }); return menu; diff --git a/lib/pi-buttons/a.out b/lib/pi-buttons/a.out deleted file mode 100755 index b8e6cf4..0000000 Binary files a/lib/pi-buttons/a.out and /dev/null differ diff --git a/lib/pi-buttons/build.sh b/lib/pi-buttons/build.sh deleted file mode 100755 index cbe167a..0000000 --- a/lib/pi-buttons/build.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -# TODO convert to Makefile - -gcc buttons.c -lpthread -lrt diff --git a/lib/pi-buttons/buttons.c b/lib/pi-buttons/buttons.c deleted file mode 100644 index 701d365..0000000 --- a/lib/pi-buttons/buttons.c +++ /dev/null @@ -1,393 +0,0 @@ -// configure INPUT - -// poll INPUT - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -#include "buttons.h" - - -int main(int argc, char *argv[]) { - int l, gpioCount; - buttonDefinition * buttons[MAX_BUTTONS]; - pthread_t buttonThread; - pthread_t socketThread; - int clients[MAX_CLIENTS]; - - - // TODO these need to come from a config file or command line args - const char * gpios[] = { - "17", - "27" - }; - gpioCount = 2; - - // init client file descriptors - for(l = 0; l < MAX_CLIENTS; l++) { - clients[l] = -1; - } - pthread_create(&socketThread, NULL, &socketServer, &clients); - - // init buttons - for(l = 0; l < gpioCount; l++) { - buttons[l] = (buttonDefinition *)malloc(sizeof(buttonDefinition)); - buttons[l]->gpio = gpios[l]; - pthread_mutex_init(&buttons[l]->lockControl, NULL); - pthread_barrier_init(&buttons[l]->barrierControl, NULL, 2); - buttons[l]->clients = clients; - pthread_create(&buttons[l]->parent, NULL, &buttonParent, buttons[l]); - } - - // TODO can all threads be monitored simultaneously and restart if one fails? - for(l = 0; l < gpioCount; l++) { - pthread_join(buttons[l]->parent, NULL); - } - -} - - -void * buttonParent(void * args) { - buttonDefinition * button; - button = (buttonDefinition *)args; - char buff[30]; - struct pollfd pollfdStruct; - int pollStatus; - uint8_t c; - - sprintf(buff, "/sys/class/gpio/gpio%s/value", button->gpio); - if ((button->fd = open(buff, O_RDWR)) < 0) { - printf("Failed to open gpio%d.\n", button->gpio); - exit(1); - } - - // configure polling structure - pollfdStruct.fd = button->fd; - pollfdStruct.events = POLLPRI | POLLERR; - - // configure button structure - button->state = STATE_INIT; - button->debounceState = INACTIVE; - button->value = RELEASED; - - // clear out any waiting gpio values - pollStatus = poll(&pollfdStruct, 1, 10); // 10 millisecond wait for input - if (pollStatus > 0) { - if (pollfdStruct.revents & POLLPRI) { - lseek (pollfdStruct.fd, 0, SEEK_SET) ; // Rewind - (void)read (pollfdStruct.fd, &c, 1) ; // Read & clear - } - } - - // reset button state - button->state = STATE_IDLE; - pthread_mutex_lock(&button->lockControl); - pthread_create(&button->child, NULL, &buttonChild, button); - - for(;;) { - pollStatus = poll(&pollfdStruct, 1, -1) ; - if (pollStatus > 0) { - if (pollfdStruct.revents & POLLPRI) { - lseek (pollfdStruct.fd, 0, SEEK_SET) ; // Rewind - (void)read (pollfdStruct.fd, &c, 1) ; // Read & clear - - button->lastValue = c; - - if (button->debounceState == INACTIVE) { - // when not in a debounce state then signal child about button change - pthread_mutex_unlock(&button->lockControl); // signal child button event has started - pthread_barrier_wait(&button->barrierControl); // wait on begin sychronization - pthread_mutex_lock(&button->lockControl); // regain lock for next event - pthread_barrier_wait(&button->barrierControl); // signal child synchronized - } - else { - // TODO do we need to do anything if in debounce? probably not. - } - } - - } - else { - // something wrong with poll status - - } - } -} - - -void * buttonChild(void * args) { - buttonDefinition * button; - button = (buttonDefinition *)args; - int lockStatus, startDebounce = 0; - long lockTimeout = TIMEOUT_FALSE; - char eventMsg[EVENT_MSG_MAX_LENGTH]; - - // main loop - for(;;) { - // wait for next event, unlock or timeout - if (lockTimeout) { - lockStatus = pthread_mutex_timedlock(&button->lockControl, &button->conditionTime); - } - else { - lockStatus = pthread_mutex_lock(&button->lockControl); // wait for parent to signal button event started - } - - lockTimeout = TIMEOUT_FALSE; - if (lockStatus != ETIMEDOUT && button->debounceState == INACTIVE) { - // not a timeout and not in debounce state, start debounce of button value - button->debounceState = ACTIVE; - button->value = button->lastValue; - clock_gettime(CLOCK_REALTIME, &button->lastTime); - setConditionNS(&button->lastTime, &button->conditionTime, DEBOUNCE_NS); - lockTimeout = TIMEOUT_TRUE; - emitFormattedMessage(eventMsg, EVENT_STRING[button_changed], button); - } - else if (lockStatus == ETIMEDOUT && button->debounceState == ACTIVE) { - button->debounceState = INACTIVE; - // timed out while in the debounce state, perform state transition - switch(button->state) { - case STATE_IDLE: - if (button->value == PRESSED && button->lastValue == PRESSED) { - // button pressed and held - button->state = STATE_PRESSED; - emitFormattedMessage(eventMsg, EVENT_STRING[button_press], button); - setConditionNS(&button->lastTime, &button->conditionTime, PRESSED_NS); - lockTimeout = TIMEOUT_TRUE; - } - else if (button->value == PRESSED) { - // button pressed and released within debounce - emitFormattedMessage(eventMsg, EVENT_STRING[button_press], button); - emitFormattedMessage(eventMsg, EVENT_STRING[button_release], button); - button->state = STATE_CLICKED; - setConditionNS(&button->lastTime, &button->conditionTime, CLICKED_NS); - lockTimeout = TIMEOUT_TRUE; - } - break; - - case STATE_PRESSED: - if (button->lastValue == RELEASED) { - // button released - emitFormattedMessage(eventMsg, EVENT_STRING[button_release], button); - button->state = STATE_CLICKED; - setConditionNS(&button->lastTime, &button->conditionTime, CLICKED_NS); - lockTimeout = TIMEOUT_TRUE; - } - else if (button->value == RELEASED && button->lastValue == PRESSED) { - // button released and pressed within debounce - emitFormattedMessage(eventMsg, EVENT_STRING[button_release], button); - emitFormattedMessage(eventMsg, EVENT_STRING[button_press], button); - button->state = STATE_CLICKED_PRESSED; - setConditionNS(&button->lastTime, &button->conditionTime, PRESSED_NS); - lockTimeout = TIMEOUT_TRUE; - } - else { - // unknown, reset state - button->state = STATE_IDLE; - } - break; - - case STATE_CLICKED: - if (button->lastValue == PRESSED) { - // after clicked button pressed and held - button->state = STATE_CLICKED_PRESSED; - emitFormattedMessage(eventMsg, EVENT_STRING[button_press], button); - setConditionNS(&button->lastTime, &button->conditionTime, PRESSED_NS); - lockTimeout = TIMEOUT_TRUE; - } - else if (button->value == PRESSED && button->lastValue == RELEASED) { - // after clicked button pressed and released within debounce - emitFormattedMessage(eventMsg, EVENT_STRING[button_press], button); - emitFormattedMessage(eventMsg, EVENT_STRING[button_release], button); - button->state = STATE_DOUBLE_CLICKED; - setConditionNS(&button->lastTime, &button->conditionTime, CLICKED_NS); - lockTimeout = TIMEOUT_TRUE; - } - else { - // unknown, reset state - button->state = STATE_IDLE; - } - break; - - case STATE_CLICKED_PRESSED: - if (button->lastValue == RELEASED) { - emitFormattedMessage(eventMsg, EVENT_STRING[button_release], button); - button->state = STATE_DOUBLE_CLICKED; - emitState(eventMsg, button); - button->state = STATE_IDLE; - } - else { - emitState(eventMsg, button); - button->state = STATE_RELEASE_WAIT; - } - break; - - case STATE_DOUBLE_CLICKED: - case STATE_RELEASE_WAIT: - emitState(eventMsg, button); - button->state = STATE_IDLE; - break; - } - } - else { - // emit state and transition state - emitState(eventMsg, button); - switch(button->state) { - case STATE_PRESSED: - case STATE_CLICKED_PRESSED: - button->state = STATE_RELEASE_WAIT; - break; - - default: - button->state = STATE_IDLE; - break; - } - } - - // if child has lock then perform handshake with parent - if (!lockStatus) { - pthread_mutex_unlock(&button->lockControl); // release for synchronization - pthread_barrier_wait(&button->barrierControl); // begin synchronization - pthread_barrier_wait(&button->barrierControl); // wait for synchronized - } - } -} - - - -void * socketServer(void * args) { - int * clients; - clients = (int *)args; - int l, fd, socket = openSocket(); - - while (1) { - // wait for connection - if ( (fd = accept(socket, NULL, NULL)) == -1) { - fprintf(stderr, "Error accepting incoming connection.\n"); - continue; - } - - for(l = 0; l < MAX_CLIENTS; l++) { - if (clients[l] == -1) { - clients[l] = fd; - break; - } - } - - if (l == MAX_CLIENTS) { - send(fd, ERROR_MAX_CLIENTS, strlen(ERROR_MAX_CLIENTS), MSG_NOSIGNAL); - } - else { - // add connection to empty slot -printf("Connect %d\n", fd); - } - } -} - - -// emit event for the current button state -void emitState(char * buffer, buttonDefinition * button) { - switch (button->state) { - case STATE_PRESSED: - // emit event and transition to release wait - emitFormattedMessage(buffer, EVENT_STRING[pressed], button); - break; - - case STATE_CLICKED: - // emit event and transition to idle - emitFormattedMessage(buffer, EVENT_STRING[clicked], button); - break; - - case STATE_CLICKED_PRESSED: - // emit event and transition to release wait - emitFormattedMessage(buffer, EVENT_STRING[clicked_pressed], button); - break; - - case STATE_DOUBLE_CLICKED: - // emit event and transition to idle - emitFormattedMessage(buffer, EVENT_STRING[double_clicked], button); - break; - - case STATE_RELEASE_WAIT: - // emit event and transition to idle - emitFormattedMessage(buffer, EVENT_STRING[released], button); - break; - } -} - - -void emitFormattedMessage(char * buffer, const char * eventString, buttonDefinition * button) { - if (strlen(EVENT_MSG_FORMAT) + strlen(eventString) + strlen(button->gpio) >= EVENT_MSG_MAX_LENGTH) { - fprintf(stderr, "Emit message too large for buffer."); - } - else { - sprintf(buffer, EVENT_MSG_FORMAT, eventString, button->gpio, button->lastTime.tv_sec, button->lastTime.tv_nsec); - emitMessage(buffer, button->clients); - } -} - - -void emitMessage(const char * msg, int * clients) { - int l, wl; - for(l = 0; l < MAX_CLIENTS; l++) { - if (clients[l] != -1) { - wl = send(clients[l], msg, strlen(msg), MSG_NOSIGNAL); - if (wl == -1) { - // failure, remove client - close(clients[l]); - clients[l] = -1; - } - } - } -} - - -int openSocket() { - struct sockaddr_un addr; - int fd; - - if ( (fd = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) { - fprintf(stderr, "Error opening event socket."); - exit(-1); - } - - memset(&addr, 0, sizeof(addr)); - addr.sun_family = AF_UNIX; - strncpy(addr.sun_path, EVENT_SOCKET_PATH, sizeof(addr.sun_path)-1); - unlink(EVENT_SOCKET_PATH); - - if (bind(fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) { - fprintf(stderr, "Event socket bind error."); - exit(-1); - } - - if (listen(fd, 5) == -1) { - fprintf(stderr, "Event socket listen error."); - exit(-1); - } - - return fd; -} - - -void setConditionNS(struct timespec * currentTime, struct timespec * targetTime, uint32_t ns) { - targetTime->tv_sec = currentTime->tv_sec; - targetTime->tv_nsec = currentTime->tv_nsec; - if (1000000000 - targetTime->tv_nsec < ns) { - targetTime->tv_sec += 1; - targetTime->tv_nsec = ns - (targetTime->tv_nsec - 1000000000); - } - else { - targetTime->tv_nsec = targetTime->tv_nsec + ns; - } -} diff --git a/lib/pi-buttons/buttons.h b/lib/pi-buttons/buttons.h deleted file mode 100644 index 4949758..0000000 --- a/lib/pi-buttons/buttons.h +++ /dev/null @@ -1,108 +0,0 @@ -#define EVENT_SOCKET_PATH "./buttonevents" -#define MAX_BUTTONS 10 -#define MAX_CLIENTS 2 -#define ERROR_MAX_CLIENTS "error {\"error\": \"Maximum client connections exceeded.\"}" - -enum ButtonState { - STATE_INIT, - STATE_IDLE, - STATE_PRESSED, - STATE_CLICKED, - STATE_CLICKED_PRESSED, - STATE_DOUBLE_CLICKED, - STATE_RELEASE_WAIT -}; - -enum DebounceState { - INACTIVE, - ACTIVE -}; - -enum LockTimeoutState { - TIMEOUT_FALSE, - TIMEOUT_TRUE -}; - -enum ButtonValue { - PRESSED = 48, - RELEASED -}; - -// I.E. 'button_changed {"gpio": "17", "time": 012345678900123456789}' -#define EVENT_MSG_MAX_LENGTH 128 -static const char * EVENT_MSG_FORMAT = "%s {\"gpio\": \"%s\", \"time\": {\"tv_sec\": %ld, \"tv_nsec\": %ld}}\n"; - -// define events -#define FOREACH_EVENT(EVENT) \ - EVENT(button_changed) \ - EVENT(button_press) \ - EVENT(button_release) \ - EVENT(pressed) \ - EVENT(clicked) \ - EVENT(clicked_pressed) \ - EVENT(double_clicked) \ - EVENT(released) \ - -#define GENERATE_ENUM(ENUM) ENUM, -#define GENERATE_STRING(STRING) #STRING, - -enum EVENT_ENUM { - FOREACH_EVENT(GENERATE_ENUM) -}; - -static const char *EVENT_STRING[] = { - FOREACH_EVENT(GENERATE_STRING) -}; - - -#define SEC_NSEC 1000000000 -#define DEBOUNCE_MS 30 -#define DEBOUNCE_NS 20000000 -#define PRESSED_MS 200 -#define PRESSED_NS 200000000 -#define CLICKED_MS 200 -#define CLICKED_NS 200000000 - -typedef struct { - pthread_mutex_t lockControl; - pthread_barrier_t barrierControl; - struct timespec lastTime; - struct timespec conditionTime; - const char * gpio; - int fd; // file descriptor for button input - enum ButtonState state; - int debounceState; - uint8_t value; - uint8_t lastValue; - int * clients; - pthread_t parent; - pthread_t child; - long debounce_ns; -} buttonDefinition; - -typedef struct { - char ** gpios; - int gpioCount; - int * clients; -} pollerThreadArgs; - -typedef struct { - int index; - int fd; // file descriptor for button input - enum ButtonState state; - int debouncing; - uint8_t value; - uint8_t lastValue; - int * clients; -} gpioButton; - -void * buttonPoller(void * args); -void * buttonDebounce(void * args); -void * socketServer(void * args); -int openSocket(); -void emitMessage(const char * msg, int * clients); -void * buttonParent(void * args); -void * buttonChild(void * args); -void emitState(char * buffer, buttonDefinition * button); -void emitFormattedMessage(char * buffer, const char * eventString, buttonDefinition * button); -void setConditionNS(struct timespec * currentTime, struct timespec * targetTime, uint32_t ns); diff --git a/lib/pi-buttons/index.js b/lib/pi-buttons/index.js deleted file mode 100644 index 7f92464..0000000 --- a/lib/pi-buttons/index.js +++ /dev/null @@ -1,28 +0,0 @@ -'use strict'; - -const net = require('net'); -const events = require('events'); -const emitter = new events.EventEmitter(); -const pins = { [17]: 11, [27]: 13 }; - -var client = net.createConnection(__dirname + "/buttonevents"); - -client.on("connect", function() { - -}); - -client.on("data", function(data) { - let packets = data.toString().split(/\r?\n/); - packets.forEach(packet => { - var parts = /^([^{]+)\s({.*})/.exec(packet); - if (parts) { - try { - var d = JSON.parse(parts[2]); - emitter.emit(parts[1], pins[d.gpio], d); - } - catch (e) {} - } - }); -}); - -module.exports = emitter; diff --git a/lib/pi-buttons/setup.sh b/lib/pi-buttons/setup.sh deleted file mode 100755 index 14cf0a6..0000000 --- a/lib/pi-buttons/setup.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/sh - -echo 17 > /sys/class/gpio/export -echo 22 > /sys/class/gpio/export -echo 27 > /sys/class/gpio/export - -sleep 3 - -echo "in" > /sys/class/gpio/gpio17/direction -echo "both" > /sys/class/gpio/gpio17/edge - -echo "in" > /sys/class/gpio/gpio27/direction -echo "both" > /sys/class/gpio/gpio27/edge - -echo "out" > /sys/class/gpio/gpio22/direction diff --git a/lib/socket-server/socket-server.js b/lib/socket-server/socket-server.js index dfffc5d..1a9307b 100644 --- a/lib/socket-server/socket-server.js +++ b/lib/socket-server/socket-server.js @@ -91,6 +91,11 @@ module.exports = function (config) { try { switch (cmd.command) { + case 'status': + emitter.emit('displaystatus'); + socketServer.clientWrite(client, 200, 'Success', 'HAT Display Updated'); + break; + case 'read_voltage': require('./commands/read_voltage')(config) .then((response) => { diff --git a/openaps-menu.sh b/openaps-menu.sh index ea3f3f0..3cfeb21 100755 --- a/openaps-menu.sh +++ b/openaps-menu.sh @@ -1,3 +1,2 @@ #!/bin/bash - -(cd lib/pi-buttons/ && ./setup.sh && ./a.out) & node index.js +node index.js diff --git a/package.json b/package.json index 8994aa2..13759ff 100644 --- a/package.json +++ b/package.json @@ -9,13 +9,13 @@ "author": "", "license": "MIT", "dependencies": { - "extend": "^3.0.1", - "i2c-bus": "^1.2.2", - "menube": "^1.0.3", - "oled-font-5x7": "^1.0.0", - "oled-i2c-bus": "git+https://github.com/bnielsen1965/oled-i2c-bus.git", + "extend": "^3.0.2", + "i2c-bus": "^4.0.10", + "menube": "^1.0.4", + "oled-font-5x7": "^1.0.3", + "oled-i2c-bus": "^1.0.11", "pngparse": "^2.0.1", - "rpi-gpio": "^0.9.1", - "rpi-gpio-buttons": "^1.0.2" + "node-pi-buttons": "^1.0.1", + "rpi-gpio": "^2.1.3" } } diff --git a/scripts/big_bg_status.js b/scripts/big_bg_status.js new file mode 100644 index 0000000..e4bb30f --- /dev/null +++ b/scripts/big_bg_status.js @@ -0,0 +1,166 @@ +var fs = require('fs'); +var font = require('oled-font-5x7'); + +// Rounds value to 'digits' decimal places +function round(value, digits) +{ + if (! digits) { digits = 0; } + var scale = Math.pow(10, digits); + return Math.round(value * scale) / scale; +} + +function convert_bg(value, profile) +{ + if (profile != null && profile.out_units == "mmol/L") + { + return round(value / 18, 1).toFixed(1); + } + else + { + return Math.round(value); + } +} + +function stripLeadingZero(value) +{ + var re = /^(-)?0+(?=[\.\d])/; + return value.toString().replace( re, '$1'); +} + +module.exports = bigbgstatus; + +// +//Start of status display function +// + +function bigbgstatus(display, openapsDir) { + +display.oled.clearDisplay(true); //clear the buffer + +//Parse all the .json files we need +try { + var profile = JSON.parse(fs.readFileSync(openapsDir+"/settings/profile.json")); +} catch (e) { + // Note: profile.json is optional as it's only needed for mmol conversion for now. Print an error, but not return + console.error("Status screen display error: could not parse profile.json: ", e); +} +try { + var batterylevel = JSON.parse(fs.readFileSync(openapsDir+"/monitor/edison-battery.json")); +} catch (e) { + console.error("Status screen display error: could not parse edison-battery.json: ", e); +} +try { + var suggested = JSON.parse(fs.readFileSync(openapsDir+"/enact/suggested.json")); +} catch (e) { + console.error("Status screen display error: could not parse suggested.json: ", e); +} +try { + var bg = JSON.parse(fs.readFileSync(openapsDir+"/monitor/glucose.json")); +} catch (e) { + console.error("Status screen display error: could not parse glucose.json: ", e); +} +try { + var iob = JSON.parse(fs.readFileSync(openapsDir+"/monitor/iob.json")); +} catch (e) { + console.error("Status screen display error: could not parse iob.json: ", e); +} +try { + var cob = JSON.parse(fs.readFileSync(openapsDir+"/monitor/meal.json")); +} catch (e) { + console.error("Status screen display error: could not parse meal.json: ", e); +} +try { + var stats = fs.statSync("/tmp/pump_loop_success"); +} catch (e) { + console.error("Status screen display error: could not find pump_loop_success"); +} + +if(batterylevel) { + //Process and display battery gauge + display.oled.drawLine(116, 57, 127, 57, 1, false); //top + display.oled.drawLine(116, 63, 127, 63, 1, false); //bottom + display.oled.drawLine(116, 57, 116, 63, 1, false); //left + display.oled.drawLine(127, 57, 127, 63, 1, false); //right + display.oled.drawLine(115, 59, 115, 61, 1, false); //iconify + var batt = Math.round(batterylevel.battery / 10); + display.oled.fillRect(127-batt, 58, batt, 5, 1, false); //fill battery gauge +} + +//calculate timeago for BG +if(bg && profile) { + var startDate = new Date(bg[0].date); + var endDate = new Date(); + var minutes = Math.round(( (endDate.getTime() - startDate.getTime()) / 1000) / 60); + if (bg[0].delta) { + var delta = Math.round(bg[0].delta); + } else if (bg[1] && bg[0].date - bg[1].date > 200000 ) { + var delta = Math.round(bg[0].glucose - bg[1].glucose); + } else if (bg[2] && bg[0].date - bg[2].date > 200000 ) { + var delta = Math.round(bg[0].glucose - bg[2].glucose); + } else if (bg[3] && bg[0].date - bg[3].date > 200000 ) { + var delta = Math.round(bg[0].glucose - bg[3].glucose); + } else { + var delta = 0; + } + + //display BG number, add plus sign if delta is positive + display.oled.setCursor(0,0); + if (delta >= 0) { + display.oled.writeString(font, 3, ""+convert_bg(bg[0].glucose, profile), 1, false, 0, false); + display.oled.writeString(font, 1, "+"+stripLeadingZero(convert_bg(delta, profile)), 1, false, 0, false); + display.oled.writeString(font, 2, " "+minutes+"m", 1, false, 0, false); + } else { + display.oled.writeString(font, 3, ""+convert_bg(bg[0].glucose, profile), 1, false, 0, false); + display.oled.writeString(font, 1, ""+stripLeadingZero(convert_bg(delta, profile)), 1, false, 0, false); + display.oled.writeString(font, 2, " "+minutes+"m", 1, false, 0, false); + } +} + +//calculate timeago for last successful loop +if(stats) { + var date = new Date(stats.mtime); + var hour = date.getHours(); + hour = (hour < 10 ? "0" : "") + hour; + var min = date.getMinutes(); + min = (min < 10 ? "0" : "") + min; + + //display last loop time + display.oled.setCursor(0,57); + display.oled.writeString(font, 1, "Last loop at: "+hour+":"+min, 1, false, 0, false); +} + +//parse and render COB/IOB +if(iob && cob) { + display.oled.setCursor(0,23); + display.oled.writeString(font, 1, "IOB:", 1, false, 0, false); + display.oled.writeString(font, 2, " "+iob[0].iob+'U', 1, false, 0, false); + display.oled.setCursor(0,39); + display.oled.writeString(font, 1, "COB:", 1, false, 0, false); + display.oled.writeString(font, 2, " "+cob.mealCOB+'g', 1, false, 0, false); +} + +display.oled.dimDisplay(true); //dim the display +display.oled.update(); // write buffer to the screen + +fs.readFile(openapsDir+"/preferences.json", function (err, data) { + if (err) throw err; + preferences = JSON.parse(data); + if (preferences.wearOLEDevenly && preferences.wearOLEDevenly.includes("off")) { + display.oled.invertDisplay(false); + } + else if (preferences.wearOLEDevenly && preferences.wearOLEDevenly.includes("nightandday") && (hour >= 20 || hour <= 8)) { + display.oled.invertDisplay(false); + } + else if (preferences.wearOLEDevenly && preferences.wearOLEDevenly.includes("nightandday") && (hour <= 20 && hour >= 8)) { + display.oled.invertDisplay(true); + } + else { + display.oled.invertDisplay((endDate % 2 == 1)); + } +}); + + // +}//End of status display function + // + + diff --git a/scripts/getip.sh b/scripts/getip.sh index 132510e..32eaf3b 100755 --- a/scripts/getip.sh +++ b/scripts/getip.sh @@ -1,5 +1,6 @@ #!/bin/bash -IP=$(ip -f inet -o addr show wlan0|cut -d\ -f 7 | cut -d/ -f 1) -echo -e "Current IP Address:\n$IP\n" -echo -e "To connect: ssh\npi@$IP\n" +wlan0IP=$(ip -f inet -o addr show wlan0|cut -d\ -f 7 | cut -d/ -f 1) +bnep0IP=$(ip -f inet -o addr show bnep0|cut -d\ -f 7 | cut -d/ -f 1) +echo -e "Current WiFi IP:\n$wlan0IP\n" +echo -e "Current BT IP:\n$bnep0IP\n" diff --git a/scripts/getvoltage.sh b/scripts/getvoltage.sh index 266f413..f8e16ad 100755 --- a/scripts/getvoltage.sh +++ b/scripts/getvoltage.sh @@ -3,5 +3,7 @@ command -v socat >/dev/null 2>&1 || { echo >&2 "I require socat but it's not installed. Aborting."; exit 1; } RESPONSE=`echo '{"command":"read_voltage"}' | socat -,ignoreeof ~/src/openaps-menu/socket-server.sock | sed -n 's/.*"response":\([^}]*\)}/\1/p'` -[[ $RESPONSE = *[![:space:]]* ]] && echo $RESPONSE +[[ $RESPONSE == "{}" ]] && unset RESPONSE +[[ $RESPONSE = *[![:space:]]* ]] && echo $RESPONSE || echo '{"batteryVoltage":3340,"battery":99}' +# the OR at the end of the above line uploads a fake voltage (3340) and percentage (99), to work around a problem with nighscout crashing when receiving a null value #./getvoltage.sh | sed -n 's/.*"response":\([^}]*\)}/\1/p' diff --git a/scripts/status-radiofruit.js b/scripts/status-radiofruit.js new file mode 100644 index 0000000..73188ec --- /dev/null +++ b/scripts/status-radiofruit.js @@ -0,0 +1,193 @@ +var fs = require('fs'); +var font = require('oled-font-5x7'); + +// Rounds value to 'digits' decimal places +function round(value, digits) +{ + if (! digits) { digits = 0; } + var scale = Math.pow(10, digits); + return Math.round(value * scale) / scale; +} + +function convert_bg(value, profile) +{ + if (profile != null && profile.out_units == "mmol/L") + { + return round(value / 18, 1).toFixed(1); + } + else + { + return Math.round(value); + } +} + +function stripLeadingZero(value) +{ + var re = /^(-)?0+(?=[\.\d])/; + return value.toString().replace( re, '$1'); +} + +module.exports = radiofruitStatus; + +// +//Start of status display function +// + +function radiofruitStatus(display, openapsDir) { + +display.oled.clearDisplay(true); //clear display buffer + +//Parse all the .json files we need +try { + var profile = JSON.parse(fs.readFileSync(openapsDir+"/settings/profile.json")); +} catch (e) { + console.error("Status screen display error: could not parse profile.json: ", e); +} +try { + var status = JSON.parse(fs.readFileSync(openapsDir+"/monitor/status.json")); +} catch (e) { + console.error("Status screen display error: could not parse status.json: ", e); +} +try { + var suggested = JSON.parse(fs.readFileSync(openapsDir+"/enact/suggested.json")); +} catch (e) { + console.error("Status screen display error: could not parse suggested.json: ", e); +} +try { + var bg = JSON.parse(fs.readFileSync(openapsDir+"/monitor/glucose.json")); +} catch (e) { + console.error("Status screen display error: could not parse glucose.json: ", e); +} +try { + var temp = JSON.parse(fs.readFileSync(openapsDir+"/monitor/last_temp_basal.json")); + var statusStats = fs.statSync(openapsDir+"/monitor/last_temp_basal.json"); +} catch (e) { + console.error("Status screen display error: could not parse last_temp_basal.json: ", e); +} +try { + var iob = JSON.parse(fs.readFileSync(openapsDir+"/monitor/iob.json")); +} catch (e) { + console.error("Status screen display error: could not parse iob.json: ", e); +} +try { + var cob = JSON.parse(fs.readFileSync(openapsDir+"/monitor/meal.json")); +} catch (e) { + console.error("Status screen display error: could not parse meal.json: ", e); +} +try { + var pumpbattery = JSON.parse(fs.readFileSync(openapsDir+"/monitor/battery.json")); +} catch (e) { + console.error("Status screen display error: could not parse battery.json: ", e); +} + +//display warning messages +if (status && suggested && pumpbattery) { + var notLoopingReason = suggested.reason; + display.oled.setCursor(0,16); + if (pumpbattery.voltage <= 1.25) { + display.oled.writeString(font, 1, "LOW PUMP BATT.", 1, false, 0, false); + yOffset = 3; + } + else if (status.suspended == true) { + display.oled.writeString(font, 1, "PUMP SUSPENDED", 1, false, 0, false); + yOffset = 3; + } + else if (status.bolusing == true) { + display.oled.writeString(font, 1, "PUMP BOLUSING", 1, false, 0, false); + yOffset = 3; + } + else if (notLoopingReason.includes("CGM is calibrating")) { + display.oled.writeString(font, 1, "CGM calib./???/noisy", 1, false, 0, false); + yOffset = 3; + } + else if (notLoopingReason.includes("CGM data is unchanged")) { + display.oled.writeString(font, 1, "CGM data unchanged", 1, false, 0, false); + yOffset = 3; + } + else if (notLoopingReason.includes("BG data is too old")) { + display.oled.writeString(font, 1, "BG data too old", 1, false, 0, false); + yOffset = 3; + } + else if (notLoopingReason.includes("currenttemp rate")) { + display.oled.writeString(font, 1, "Temp. mismatch", 1, false, 0, false); + yOffset = 3; + } + else if (suggested.carbsReq) { + display.oled.writeString(font, 1, "Carbs Requiredd: "+suggested.carbsReq+'g', 1, false, 0, false); + yOffset = 3; + } +//add more on-screen warnings/messages, maybe some special ones for xdrip-js users? +} + +//calculate timeago for BG +var startDate = new Date(bg[0].date); +var endDate = new Date(); +var minutes = Math.round(( (endDate.getTime() - startDate.getTime()) / 1000) / 60); +if (bg[0].delta) { + var delta = Math.round(bg[0].delta); +} else if (bg[1] && bg[0].date - bg[1].date > 200000 ) { + var delta = Math.round(bg[0].glucose - bg[1].glucose); +} else if (bg[2] && bg[0].date - bg[2].date > 200000 ) { + var delta = Math.round(bg[0].glucose - bg[2].glucose); +} else if (bg[3] && bg[0].date - bg[3].date > 200000 ) { + var delta = Math.round(bg[0].glucose - bg[3].glucose); +} else { + var delta = 0; +} +//display BG number and timeago, add plus sign if delta is positive +display.oled.setCursor(0,24); +if (delta >= 0) { + display.oled.writeString(font, 1, "BG:"+convert_bg(bg[0].glucose, profile)+"+"+stripLeadingZero(convert_bg(delta, profile))+" "+minutes+"m", 1, false, 0, false); +} else { + display.oled.writeString(font, 1, "BG:"+convert_bg(bg[0].glucose, profile)+""+stripLeadingZero(convert_bg(delta, profile))+" "+minutes+"m", 1, false, 0, false); +} + +//display current temp basal and how long ago it was set, on the first line of the screen +if (statusStats && temp) { + startDate = new Date(statusStats.mtime); + endDate = new Date(); + var minutesAgo = Math.round(( (endDate.getTime() - startDate.getTime()) / 1000) / 60); + //display current temp basal + display.oled.setCursor(0,0); + var tempRate = Math.round(temp.rate*10)/10; + display.oled.writeString(font, 1, "TB: "+temp.duration+'m '+tempRate+'U/h '+'('+minutesAgo+'m ago)', 1, false, 0, false); +} + +//display current COB and IOB, on the second line of the screen +if (iob && cob) { + display.oled.setCursor(0,8); + display.oled.writeString(font, 1, "COB: "+cob.mealCOB+"g IOB: "+iob[0].iob+'U', 1, false, 0, false); +} + +//render clock +var clockDate = new Date(); +var clockHour = clockDate.getHours(); +clockHour = (clockHour < 10 ? "0" : "") + clockHour; +var clockMin = clockDate.getMinutes(); +clockMin = (clockMin < 10 ? "0" : "") + clockMin; +display.oled.setCursor(97, 24); +display.oled.writeString(font, 1, clockHour+":"+clockMin, 1, false, 0, false); + +display.oled.dimDisplay(true); //dim the display +display.oled.update(); //write buffer to the screen + +fs.readFile(openapsDir+"/preferences.json", function (err, data) { + if (err) throw err; + preferences = JSON.parse(data); + if (preferences.wearOLEDevenly && preferences.wearOLEDevenly.includes("off")) { + display.oled.invertDisplay(false); + } + else if (preferences.wearOLEDevenly && preferences.wearOLEDevenly.includes("nightandday") && (clockHour >= 20 || clockHour <= 8)) { + display.oled.invertDisplay(false); + } + else if (preferences.wearOLEDevenly && preferences.wearOLEDevenly.includes("nightandday") && (clockHour <= 20 && clockHour >= 8)) { + display.oled.invertDisplay(true); + } + else { + display.oled.invertDisplay((endDate % 2 == 1)); + } +}); + + // +}//End of status display function + // diff --git a/scripts/status.js b/scripts/status.js index 462ed68..b280e05 100644 --- a/scripts/status.js +++ b/scripts/status.js @@ -1,13 +1,5 @@ - -'use strict'; - -const i2c = require('i2c-bus'); -const path = require('path'); -const extend = require('extend'); -var os = require('os'); var fs = require('fs'); var font = require('oled-font-5x7'); -var i2cBus = i2c.openSync(1); // Rounds value to 'digits' decimal places function round(value, digits) @@ -35,160 +27,242 @@ function stripLeadingZero(value) return value.toString().replace( re, '$1'); } -// setup the display -var displayConfig = require('/root/src/openaps-menu/config/display.json'); -displayConfig.i2cBus = i2cBus; -var display = require('/root/src/openaps-menu/lib/display/ssd1306')(displayConfig); +module.exports = graphicalStatus; + +// +//Start of status display function +// + +function graphicalStatus(display, openapsDir) { + +display.oled.clearDisplay(true); //clear display buffer //Parse all the .json files we need try { - var profile = JSON.parse(fs.readFileSync("/root/myopenaps/settings/profile.json")); + var profile = JSON.parse(fs.readFileSync(openapsDir+"/settings/profile.json")); } catch (e) { - // Note: profile.json is optional as it's only needed for mmol conversion for now. Print an error, but not return - console.error("Could not parse profile.json: ", e); + console.error("Status screen display error: could not parse profile.json: ", e); } try { - var batterylevel = JSON.parse(fs.readFileSync("/root/myopenaps/monitor/edison-battery.json")); + var batterylevel = JSON.parse(fs.readFileSync(openapsDir+"/monitor/edison-battery.json")); } catch (e) { - console.error("Could not parse edison-battery.json: ", e); + console.error("Status screen display error: could not parse edison-battery.json: ", e); } - -if(batterylevel) { - //Process and display battery gauge - display.oled.drawLine(115, 57, 127, 57, 1); //top - display.oled.drawLine(115, 63, 127, 63, 1); //bottom - display.oled.drawLine(115, 57, 115, 63, 1); //left - display.oled.drawLine(127, 57, 127, 63, 1); //right - display.oled.drawLine(114, 59, 114, 61, 1); //iconify - var batt = Math.round(127 - (batterylevel.battery / 10)); - display.oled.fillRect(batt, 58, 126, 62, 1); //fill battery gauge -} - -//Create and render clock -function displayClock() { - var date = new Date(); - var hour = date.getHours(); - hour = (hour < 10 ? "0" : "") + hour; - var min = date.getMinutes(); - min = (min < 10 ? "0" : "") + min; - display.oled.setCursor(83, 57); - display.oled.writeString(font, 1, hour+":"+min, 1, true); -} - -displayClock(); - -//bg graph -display.oled.drawLine(5, 51, 5, 21, 1); -display.oled.drawLine(5, 51, 127, 51, 1); -//targets high and low -display.oled.drawLine(2, 30, 5, 30, 1); -display.oled.drawLine(2, 40, 5, 40, 1); - try { - var suggested = JSON.parse(fs.readFileSync("/root/myopenaps/enact/suggested.json")); + var status = JSON.parse(fs.readFileSync(openapsDir+"/monitor/status.json")); } catch (e) { - return console.error("Could not parse suggested.json: ", e); + console.error("Status screen display error: could not parse status.json: ", e); } try { - var bg = JSON.parse(fs.readFileSync("/root/myopenaps/monitor/glucose.json")); + var suggested = JSON.parse(fs.readFileSync(openapsDir+"/enact/suggested.json")); } catch (e) { - return console.error("Could not parse glucose.json: ", e); -} -//render BG graph -var numBGs = (suggested.predBGs != undefined) ? (72) : (120); //fill the whole graph with BGs if there are no predictions -var date = new Date(); -var zerotime = date.getTime() - ((numBGs * 5) * 600); -var zero_x = numBGs + 5; -for (var i = 0; i < numBGs; i++) { - if (bg[i] != null) { - var x = 2 + zero_x + Math.round(((((bg[i].date - zerotime)/1000)/60)/5)); - var y = Math.round( 21 - ( ( bg[i].glucose - 250 ) / 8 ) ); - //left and right boundaries - if ( x < 5 ) x = 5; + console.error("Status screen display error: could not parse suggested.json: ", e); +} +try { + var bg = JSON.parse(fs.readFileSync(openapsDir+"/monitor/glucose.json")); +} catch (e) { + console.error("Status screen display error: could not parse glucose.json: ", e); +} +try { + var temp = JSON.parse(fs.readFileSync(openapsDir+"/monitor/last_temp_basal.json")); + var statusStats = fs.statSync(openapsDir+"/monitor/last_temp_basal.json"); +} catch (e) { + console.error("Status screen display error: could not parse last_temp_basal.json: ", e); +} +try { + var iob = JSON.parse(fs.readFileSync(openapsDir+"/monitor/iob.json")); +} catch (e) { + console.error("Status screen display error: could not parse iob.json: ", e); +} +try { + var cob = JSON.parse(fs.readFileSync(openapsDir+"/monitor/meal.json")); +} catch (e) { + console.error("Status screen display error: could not parse meal.json: ", e); +} +try { + var pumpbattery = JSON.parse(fs.readFileSync(openapsDir+"/monitor/battery.json")); +} catch (e) { + console.error("Status screen display error: could not parse battery.json: ", e); +} + +//Process and display battery gauge +if(batterylevel) { + display.oled.drawLine(116, 57, 127, 57, 1, false); //top + display.oled.drawLine(116, 63, 127, 63, 1, false); //bottom + display.oled.drawLine(116, 57, 116, 63, 1, false); //left + display.oled.drawLine(127, 57, 127, 63, 1, false); //right + display.oled.drawLine(115, 59, 115, 61, 1, false); //make it look like a battery + var batt = Math.round(batterylevel.battery / 10); + display.oled.fillRect(127-batt, 58, batt, 5, 1, false); //fill battery gauge +} + +//display warning messages, and move the graph to make room for the message +var yOffset = 0; //offset for graph, if we need to move it +if (status && suggested && pumpbattery) { + var notLoopingReason = suggested.reason; + display.oled.setCursor(0,16); + if (pumpbattery.voltage <= 1.25) { + display.oled.writeString(font, 1, "LOW PUMP BATT.", 1, false, 0, false); + yOffset = 3; + } + else if (status.suspended == true) { + display.oled.writeString(font, 1, "PUMP SUSPENDED", 1, false, 0, false); + yOffset = 3; + } + else if (status.bolusing == true) { + display.oled.writeString(font, 1, "PUMP BOLUSING", 1, false, 0, false); + yOffset = 3; + } + else if (notLoopingReason.includes("CGM is calibrating")) { + display.oled.writeString(font, 1, "CGM calib./???/noisy", 1, false, 0, false); + yOffset = 3; + } + else if (notLoopingReason.includes("CGM data is unchanged")) { + display.oled.writeString(font, 1, "CGM data unchanged", 1, false, 0, false); + yOffset = 3; + } + else if (notLoopingReason.includes("BG data is too old")) { + display.oled.writeString(font, 1, "BG data too old", 1, false, 0, false); + yOffset = 3; + } + else if (notLoopingReason.includes("currenttemp rate")) { + display.oled.writeString(font, 1, "Temp. mismatch", 1, false, 0, false); + yOffset = 3; + } + else if (suggested.carbsReq) { + display.oled.writeString(font, 1, "Carbs Required: "+suggested.carbsReq+'g', 1, false, 0, false); + yOffset = 3; + } +//add more on-screen warnings/messages, maybe some special ones for xdrip-js users? +} + +//display current target(s) +if (profile) { + var targetLow = Math.round( (21+yOffset) - ( ( profile.bg_targets.targets[0].low - 250 ) / 8 ) ); + var targetHigh = Math.round( (21+yOffset) - ( ( profile.bg_targets.targets[0].high - 250 ) / 8 ) ); + display.oled.drawLine(2, targetHigh, 5, targetHigh, 1, false); + display.oled.drawLine(2, targetLow, 5, targetLow, 1, false); +} + +if (bg) { + //render BG graph + var numBGs = ((suggested != undefined) && (suggested.predBGs != undefined)) ? (72) : (120); //fill the whole graph with BGs if there are no predictions var date = new Date(); + var date = new Date(); + var zerotime = date.getTime() - ((numBGs * 5) * 600); + var zero_x = numBGs + 5; + for (var i = 0; i < numBGs; i++) { + if (bg[i] != null) { + var x = zero_x + Math.round(((((bg[i].date - zerotime)/1000)/60)/5)); + var y = Math.round( (21+yOffset) - ( ( bg[i].glucose - 250 ) / 8 ) ); + //left and right boundaries + if ( x < 5 ) x = 5; + if ( x > 127 ) x = 127; + //upper and lower boundaries + if ( y < (21+yOffset) ) y = (21+yOffset); + if ( y > (51+yOffset) ) y = (51+yOffset); + display.oled.drawPixel([x, y, 1, false]); + // if we have multiple data points within 3m, look further back to fill in the graph + if ( bg[i-1] && bg[i-1].date - bg[i].date < 200000 ) { + numBGs++; + } + } + } + + //calculate timeago for BG + var startDate = new Date(bg[0].date); + var endDate = new Date(); + var minutes = Math.round(( (endDate.getTime() - startDate.getTime()) / 1000) / 60); + if (bg[0].delta) { + var delta = Math.round(bg[0].delta); + } else if (bg[1] && bg[0].date - bg[1].date > 200000 ) { + var delta = Math.round(bg[0].glucose - bg[1].glucose); + } else if (bg[2] && bg[0].date - bg[2].date > 200000 ) { + var delta = Math.round(bg[0].glucose - bg[2].glucose); + } else if (bg[3] && bg[0].date - bg[3].date > 200000 ) { + var delta = Math.round(bg[0].glucose - bg[3].glucose); + } else { + var delta = 0; + } + //display BG number and timeago, add plus sign if delta is positive + display.oled.setCursor(0,57); + if (delta >= 0) { + display.oled.writeString(font, 1, "BG:"+convert_bg(bg[0].glucose, profile)+"+"+stripLeadingZero(convert_bg(delta, profile))+" "+minutes+"m", 1, false, 0, false); + } else { + display.oled.writeString(font, 1, "BG:"+convert_bg(bg[0].glucose, profile)+""+stripLeadingZero(convert_bg(delta, profile))+" "+minutes+"m", 1, false, 0, false); + } +} + +//render predictions on the graph, but only if we have them +if (bg && suggested && suggested.predBGs != undefined) { + //render line between actual BG and predicted + x = zero_x + 1; + display.oled.drawLine(x, 51+yOffset, x, 21+yOffset, 1, false); + //render predictions + var predictions = [suggested.predBGs.IOB, suggested.predBGs.ZT, suggested.predBGs.UAM, suggested.predBGs.COB]; + for (i = 0; i <= 48; i++) { + x++; + for(var n = 0; n <=3 && (predictions[n] != undefined); n++) { + y = Math.round( (21+yOffset) - ( (predictions[n][i] - 250 ) / 8) ); + //right boundary if ( x > 127 ) x = 127; //upper and lower boundaries - if ( y < 21 ) y = 21; - if ( y > 51 ) y = 51; - display.oled.drawPixel([x, y, 1]); - // if we have multiple data points within 3m, look further back to fill in the graph - if ( bg[i-1] && bg[i-1].date - bg[i].date < 200000 ) { - numBGs++; + if ( y < (21+yOffset) ) y = (21+yOffset); + if ( y > (51+yOffset) ) y = (51+yOffset); + display.oled.drawPixel([x, y, 1, false]); } } } -//render predictions, only if we have them -if (suggested.predBGs != undefined) { - //render line between actual BG and predicted - x = zero_x + 3; - display.oled.drawLine(x, 51, x, 21, 1); - //render predictions - var predictions = [suggested.predBGs.IOB, suggested.predBGs.ZT, suggested.predBGs.UAM, suggested.predBGs.COB]; - for (i = 0; i <= 48; i++) { - x++; - for(var n = 0; n <=3 && (predictions[n] != undefined); n++) { - y = Math.round( 21 - ( (predictions[n][i] - 250 ) / 8) ); - //right boundary - if ( x > 127 ) x = 127; - //upper and lower boundaries - if ( y < 21 ) y = 21; - if ( y > 51 ) y = 51; - display.oled.drawPixel([x, y, 1]); - } - } -} -//calculate timeago for BG -var startDate = new Date(bg[0].date); -var endDate = new Date(); -var minutes = Math.round(( (endDate.getTime() - startDate.getTime()) / 1000) / 60); -if (bg[0].delta) { - var delta = Math.round(bg[0].delta); -} else if (bg[1] && bg[0].date - bg[1].date > 200000 ) { - var delta = Math.round(bg[0].glucose - bg[1].glucose); -} else if (bg[2] && bg[0].date - bg[2].date > 200000 ) { - var delta = Math.round(bg[0].glucose - bg[2].glucose); -} else if (bg[3] && bg[0].date - bg[3].date > 200000 ) { - var delta = Math.round(bg[0].glucose - bg[3].glucose); -} else { - var delta = 0; -} - -//display BG number, add plus sign if delta is positive -display.oled.setCursor(0,57); -if (delta >= 0) { - display.oled.writeString(font, 1, "BG:"+convert_bg(bg[0].glucose, profile)+"+"+stripLeadingZero(convert_bg(delta, profile))+" "+minutes+"m", 1, true); -} else { - display.oled.writeString(font, 1, "BG:"+convert_bg(bg[0].glucose, profile)+""+stripLeadingZero(convert_bg(delta, profile))+" "+minutes+"m", 1, true); +//display current temp basal and how long ago it was set, on the first line of the screen +if (statusStats && temp) { + startDate = new Date(statusStats.mtime); + endDate = new Date(); + var minutesAgo = Math.round(( (endDate.getTime() - startDate.getTime()) / 1000) / 60); + //display current temp basal + display.oled.setCursor(0,0); + var tempRate = Math.round(temp.rate*10)/10; + display.oled.writeString(font, 1, "TB: "+temp.duration+'m '+tempRate+'U/h '+'('+minutesAgo+'m ago)', 1, false, 0, false); } -try { - var temp = JSON.parse(fs.readFileSync("/root/myopenaps/monitor/last_temp_basal.json")); -} catch (e) { - return console.error("Could not parse last_temp_basal.json: ", e); +//display current COB and IOB, on the second line of the screen +if (iob && cob) { + display.oled.setCursor(0,8); + display.oled.writeString(font, 1, "COB: "+cob.mealCOB+"g IOB: "+iob[0].iob+'U', 1, false, 0, false); } -//calculate timeago for status -var stats = fs.statSync("/root/myopenaps/monitor/last_temp_basal.json"); -startDate = new Date(stats.mtime); -endDate = new Date(); -minutes = Math.round(( (endDate.getTime() - startDate.getTime()) / 1000) / 60); +//display bg graph axes +display.oled.drawLine(5, 51+yOffset, 5, 21+yOffset, 1, false); +display.oled.drawLine(5, 51+yOffset, 127, 51+yOffset, 1, false); -//render current temp basal -display.oled.setCursor(0,0); -var tempRate = Math.round(temp.rate*10)/10; -display.oled.writeString(font, 1, "TB: "+temp.duration+'m '+tempRate+'U/h '+'('+minutes+'m ago)', 1); +//render clock +var clockDate = new Date(); +var clockHour = clockDate.getHours(); +clockHour = (clockHour < 10 ? "0" : "") + clockHour; +var clockMin = clockDate.getMinutes(); +clockMin = (clockMin < 10 ? "0" : "") + clockMin; +display.oled.setCursor(83, 57); +display.oled.writeString(font, 1, clockHour+":"+clockMin, 1, false, 0, false); -try { - var iob = JSON.parse(fs.readFileSync("/root/myopenaps/monitor/iob.json")); -} catch (e) { - return console.error("Could not parse iob.json: ", e); -} +display.oled.dimDisplay(true); //dim the display +display.oled.update(); //write buffer to the screen -try { - var cob = JSON.parse(fs.readFileSync("/root/myopenaps/monitor/meal.json")); -} catch (e) { - return console.error("Could not parse meal.json: ", e); -} -//parse and render COB/IOB -display.oled.setCursor(0,8); -display.oled.writeString(font, 1, "COB: "+cob.mealCOB+"g IOB: "+iob[0].iob+'U', 1, true); +fs.readFile(openapsDir+"/preferences.json", function (err, data) { + if (err) throw err; + preferences = JSON.parse(data); + if (preferences.wearOLEDevenly && preferences.wearOLEDevenly.includes("off")) { + display.oled.invertDisplay(false); + } + else if (preferences.wearOLEDevenly && preferences.wearOLEDevenly.includes("nightandday") && (clockHour >= 20 || clockHour <= 8)) { + display.oled.invertDisplay(false); + } + else if (preferences.wearOLEDevenly && preferences.wearOLEDevenly.includes("nightandday") && (clockHour <= 20 && clockHour >= 8)) { + display.oled.invertDisplay(true); + } + else { + display.oled.invertDisplay((endDate % 2 == 1)); + } +}); + + // +}//End of status display function + // diff --git a/scripts/status.sh b/scripts/status.sh new file mode 100755 index 0000000..3115173 --- /dev/null +++ b/scripts/status.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +command -v socat >/dev/null 2>&1 || { echo >&2 "I require socat but it's not installed. Aborting."; exit 1; } + +RESPONSE=`echo '{"command":"status"}' | socat -,ignoreeof ~/src/openaps-menu/socket-server.sock | sed -n 's/.*"response":\([^}]*\)}/\1/p'` +echo $RESPONSE + +#./getvoltage.sh | sed -n 's/.*"response":\([^}]*\)}/\1/p' diff --git a/scripts/unicorn.js b/scripts/unicorn.js deleted file mode 100644 index 68aed35..0000000 --- a/scripts/unicorn.js +++ /dev/null @@ -1,25 +0,0 @@ - -'use strict'; - - -const i2c = require('i2c-bus'); -const path = require('path'); -const pngparse = require('pngparse'); -const extend = require('extend'); - -var i2cBus = i2c.openSync(1); - -// setup the display -var displayConfig = require('../config/display.json'); -displayConfig.i2cBus = i2cBus; -var display = require('../lib/display/ssd1306')(displayConfig); - -// display the logo -pngparse.parseFile('./static/unicorn.png', function(err, image) { - if(err) - throw err - display.oled.drawBitmap(image.data); -}); - -//dim the display -display.oled.dimDisplay(true); diff --git a/static/black.png b/static/black.png new file mode 100644 index 0000000..2188e0f Binary files /dev/null and b/static/black.png differ diff --git a/static/unicorn_128x32.png b/static/unicorn_128x32.png new file mode 100644 index 0000000..d492fdf Binary files /dev/null and b/static/unicorn_128x32.png differ diff --git a/static/unicorn_128x64.png b/static/unicorn_128x64.png new file mode 100644 index 0000000..3981ca7 Binary files /dev/null and b/static/unicorn_128x64.png differ