diff --git a/README.md b/README.md index 675d22c..9c83a41 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 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. @@ -8,8 +8,9 @@ You can set your preferred auto-updating status screen using the following setti `"status_screen": "bigbgstatus"` will display the big BG status screen (no graph). -`"status_screen": "off"` will turn the auto-updating screen off. +`"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`: diff --git a/config/buttons.json b/config/buttons-explorerhat.json similarity index 100% rename from config/buttons.json rename to config/buttons-explorerhat.json 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/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/index.js b/index.js index 7d653b3..9d9e726 100644 --- a/index.js +++ b/index.js @@ -16,17 +16,41 @@ var i2cBus = i2c.openSync(1); var openapsDir = "/root/myopenaps"; //if you're using a nonstandard OpenAPS directory, set that here. NOT RECOMMENDED. -// setup the display -var displayConfig = require('./config/display.json'); +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; try { var display = require('./lib/display/ssd1306')(displayConfig); - displayImage('./static/unicorn.png'); //display logo + 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') voltageConfig.i2cBus = i2cBus @@ -47,37 +71,34 @@ socketServer }) .on('displaystatus', function () { if (display) { - var preferences; - fs.readFile(openapsDir+'/preferences.json', function (err, data) { - if (err) throw err; - preferences = JSON.parse(data); 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 } - }); } }) -function displayImage(pathToImage) { - pngparse.parseFile(pathToImage, function(err, image) { - if(err) - throw err - display.clear(); - display.oled.drawBitmap(image.data); - }); -} - // 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 @@ -99,8 +120,15 @@ hidMenu .on('showbigBGstatus', function () { bigBGStatus(display, openapsDir); }) +.on('showRadiofruitStatus', function () { + radiofruitStatus(display, openapsDir); +}) .on('showlogo', function () { - displayImage('./static/unicorn.png'); + if (preferences.hardwaretype && preferences.hardwaretype == "radiofruit") { + displayImage('./static/unicorn_128x32.png'); + } else { + displayImage('./static/unicorn_128x64.png'); + } }) .on('showvoltage', function () { voltage() diff --git a/package.json b/package.json index 3fb697e..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", - "node-pi-buttons": "git+https://github.com/bnielsen1965/node-pi-buttons.git", - "rpi-gpio": "^0.9.1" + "node-pi-buttons": "^1.0.1", + "rpi-gpio": "^2.1.3" } } diff --git a/scripts/getvoltage.sh b/scripts/getvoltage.sh index 9c656ea..f8e16ad 100755 --- a/scripts/getvoltage.sh +++ b/scripts/getvoltage.sh @@ -2,7 +2,8 @@ command -v socat >/dev/null 2>&1 || { echo >&2 "I require socat but it's not installed. Aborting."; exit 1; } -[[ $RESPONSE == "{}" ]] && unset RESPONSE 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 4db9fe2..b280e05 100644 --- a/scripts/status.js +++ b/scripts/status.js @@ -79,6 +79,11 @@ try { } 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) { @@ -91,12 +96,16 @@ if(batterylevel) { display.oled.fillRect(127-batt, 58, batt, 5, 1, false); //fill battery gauge } -//display reason for not looping, and move the graph to make room for the message +//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) { +if (status && suggested && pumpbattery) { var notLoopingReason = suggested.reason; display.oled.setCursor(0,16); - if (status.suspended == true) { + 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; } @@ -116,6 +125,14 @@ if (status && suggested) { 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? } @@ -129,7 +146,7 @@ if (profile) { if (bg) { //render BG graph - var numBGs = (suggested.predBGs != undefined) ? (72) : (120); //fill the whole graph with BGs if there are no predictions + 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; 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