Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
# 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 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`:

Expand Down
File renamed without changes.
10 changes: 10 additions & 0 deletions config/buttons-radiofruit.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"gpios": {
"buttonUp": 5,
"buttonDown": 6
},
"options": {
"socketPath": "/var/run/pi-buttons.sock",
"reconnectTimeout": 3000
}
}
13 changes: 10 additions & 3 deletions config/display.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
{
"title": " ",
"height": 64,
"displayLines": 8
"radiofruit": {
"title": " ",
"height": 32,
"displayLines": 4
},
"explorerHat": {
"title": " ",
"height": 64,
"displayLines": 8
}
}
66 changes: 47 additions & 19 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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()
Expand Down
14 changes: 7 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
5 changes: 3 additions & 2 deletions scripts/getvoltage.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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'
193 changes: 193 additions & 0 deletions scripts/status-radiofruit.js
Original file line number Diff line number Diff line change
@@ -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
//
Loading