New medical info app

Initial implementation to enable custom text in the medical alert widget

Possible future updates could include better editing UI instead of
raw json, more information eg. medications, ability to set info
from other apps, etc.

Signed-off-by: James Taylor <jt-git@nti.me.uk>
pull/2193/head
James Taylor 2022-10-24 12:22:35 +01:00
parent 5c110a89fb
commit 5fa96ee5f7
14 changed files with 303 additions and 12 deletions

View File

@ -0,0 +1 @@
0.01: Initial Medical Information application!

View File

@ -0,0 +1,27 @@
# Medical Information
This app displays basic medical information, and provides a common way to set up the `medicalinfo.json` file, which other apps can use if required.
## Medical information JSON file
When the app is loaded from the app loader, a file named `medicalinfo.json` is loaded along with the javascript etc.
The file has the following contents:
```
{
"bloodType": "",
"height": "",
"weight": "",
"medicalAlert": [ "" ]
}
```
## Medical information editor
Clicking on the download icon of `Medical Information` in the app loader invokes the editor.
The editor downloads and displays the current `medicalinfo.json` file, which can then be edited.
The edited `medicalinfo.json` file is uploaded to the Bangle by clicking the `Upload` button.
## Creator
James Taylor ([jt-nti](https://github.com/jt-nti))

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwg+7kUiCykCC4MgFykgDIIXUAQgAMiMRiREBC4YABkILBCxEBC4pHCC4kQFxIXEAAgXCGBERif/+QXHl//mIXJj//+YXHn//+IXL/8yCwsjBIIXNABIX/C63d7oDB+czmaPPC7hHR/oWBAAPfC65HRC7qnXX/4XDABAXkIIQAFI5wXXL/5f/L/5fvC9sTC5cxC5IAOC48BCxsQC44wOCxAArA"))

61
apps/medicalinfo/app.js Normal file
View File

@ -0,0 +1,61 @@
const medicalinfo = require('medicalinfo').load();
// const medicalinfo = {
// bloodType: "O+",
// height: "166cm",
// weight: "73kg"
// };
function hasAlert(info) {
return (Array.isArray(info.medicalAlert)) && (info.medicalAlert[0]);
}
// No space for widgets!
// TODO: no padlock widget visible so prevent screen locking?
g.clear();
const bodyFont = g.getFonts().includes("12x20") ? "12x20" : "6x8:2";
g.setFont(bodyFont);
const title = hasAlert(medicalinfo) ? "MEDICAL ALERT" : "Medical Information";
var lines = [];
lines = g.wrapString(title, g.getWidth() - 10);
var titleCnt = lines.length;
if (titleCnt) lines.push(""); // add blank line after title
if (hasAlert(medicalinfo)) {
medicalinfo.medicalAlert.forEach(function (details) {
lines = lines.concat(g.wrapString(details, g.getWidth() - 10));
});
lines.push(""); // add blank line after medical alert
}
if (medicalinfo.bloodType) {
lines = lines.concat(g.wrapString("Blood group: " + medicalinfo.bloodType, g.getWidth() - 10));
}
if (medicalinfo.height) {
lines = lines.concat(g.wrapString("Height: " + medicalinfo.height, g.getWidth() - 10));
}
if (medicalinfo.weight) {
lines = lines.concat(g.wrapString("Weight: " + medicalinfo.weight, g.getWidth() - 10));
}
lines.push("");
// TODO: display instructions for updating medical info if there is none!
E.showScroller({
h: g.getFontHeight(), // height of each menu item in pixels
c: lines.length, // number of menu items
// a function to draw a menu item
draw: function (idx, r) {
// FIXME: in 2v13 onwards, clearRect(r) will work fine. There's a bug in 2v12
g.setBgColor(idx < titleCnt ? g.theme.bg2 : g.theme.bg).
setColor(idx < titleCnt ? g.theme.fg2 : g.theme.fg).
clearRect(r.x, r.y, r.x + r.w, r.y + r.h);
g.setFont(bodyFont).drawString(lines[idx], r.x, r.y);
}
});
// Show launcher when button pressed
setWatch(() => load(), process.env.HWVERSION === 2 ? BTN : BTN3, { repeat: false, edge: "falling" });

BIN
apps/medicalinfo/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 713 B

View File

@ -0,0 +1,135 @@
<html>
<head>
<link rel="stylesheet" href="../../css/spectre.min.css" />
<style type="text/css">
.alert {
padding: 20px;
background-color: #f44336; /* Red */
color: white;
margin-bottom: 15px;
}
</style>
</head>
<body>
<div id="info"></div>
<button id="btnReload" class="btn btn-primary">Reload from watch</button>
<button id="btnUpload" class="btn btn-primary">Upload to watch</button>
<button id="btnDownload" class="btn btn-primary">Download</button>
<pre id="medicalinfo" contenteditable></pre>
<script src="../../core/lib/interface.js"></script>
<script>
const medicalInfoFile = "medicalinfo.json";
function errorFormat() {
var date = new Date();
var error =
'<p class="alert">' +
date.toUTCString() +
" : Wrong format, it should be JSON" +
"</p>";
return error;
}
function getEditableContent() {
return document.getElementById("medicalinfo").innerHTML.replace(/<[^>]*>/g, '');;
}
function isJsonString(str) {
try {
JSON.parse(str);
} catch (e) {
console.log(str)
console.log(e)
return false;
}
return true;
}
function uploadFile(fileid, contents) {
Puck.write(
`\x10(function() {
require("Storage").write("${fileid}",'${contents}');
Bluetooth.print("OK");
})()\n`,
(ret) => {
console.log("uploadFile", ret);
}
);
}
/* Load settings JSON file from the watch.
*/
function loadMedicalInfo() {
document.getElementById("info").innerHTML = "";
Util.showModal("Loading...");
Puck.eval(`require('Storage').readJSON("${medicalInfoFile}")`, (data) => {
document.getElementById("medicalinfo").innerHTML = JSON.stringify(
data,
null,
2
);
Util.hideModal();
});
}
/* Save settings as a JSON file on the watch.
*/
function uploadMedicalInfo() {
document.getElementById("info").innerHTML = "";
Util.showModal("Uploading...");
let medicalInfoJson = getEditableContent();
if (isJsonString(medicalInfoJson)) {
let shortMedicalInfoJson = JSON.stringify(JSON.parse(medicalInfoJson));
uploadFile(medicalInfoFile, shortMedicalInfoJson);
} else {
document.getElementById("info").innerHTML = errorFormat();
}
Util.hideModal();
}
function downloadMedicalInfo() {
document.getElementById("info").innerHTML = "";
Util.showModal("Downloading...");
let medicalInfoJson = getEditableContent();
if (isJsonString(medicalInfoJson)) {
var a = document.createElement("a"),
file = new Blob([medicalInfoJson], { type: "application/json" });
var url = URL.createObjectURL(file);
a.href = url;
a.download = medicalInfoFile;
document.body.appendChild(a);
a.click();
setTimeout(function () {
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}, 0);
} else {
document.getElementById("info").innerHTML = errorFormat();
}
Util.hideModal();
}
document
.getElementById("btnUpload")
.addEventListener("click", function () {
uploadMedicalInfo();
});
document
.getElementById("btnDownload")
.addEventListener("click", function () {
downloadMedicalInfo();
});
document
.getElementById("btnReload")
.addEventListener("click", function () {
loadMedicalInfo();
});
function onInit() {
loadMedicalInfo();
}
</script>
</body>
</html>

21
apps/medicalinfo/lib.js Normal file
View File

@ -0,0 +1,21 @@
const storage = require('Storage');
exports.load = function () {
const medicalinfo = storage.readJSON('medicalinfo.json') || {
bloodType: "",
height: "",
weight: "",
medicalAlert: [""]
};
// Don't return anything unexpected
const expectedMedicalinfo = [
"bloodType",
"height",
"weight",
"medicalAlert"
].filter(key => key in medicalinfo)
.reduce((obj, key) => (obj[key] = medicalinfo[key], obj), {});
return expectedMedicalinfo;
};

View File

@ -0,0 +1,6 @@
{
"bloodType": "",
"height": "",
"weight": "",
"medicalAlert": [ "" ]
}

View File

@ -0,0 +1,20 @@
{ "id": "medicalinfo",
"name": "Medical Information",
"version":"0.01",
"description": "Provides 'medicalinfo.json' used by various health apps, as well as a way to edit it from the App Loader",
"icon": "app.png",
"tags": "health,medical",
"type": "app",
"supports" : ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
"screenshots": [{"url":"screenshot_light.png"}],
"interface": "interface.html",
"storage": [
{"name":"medicalinfo.app.js","url":"app.js"},
{"name":"medicalinfo.img","url":"app-icon.js","evaluate":true},
{"name":"medicalinfo","url":"lib.js"}
],
"data": [
{"name":"medicalinfo.json","url":"medicalinfo.json"}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -1 +1,2 @@
0.01: Initial Medical Alert Widget! 0.01: Initial Medical Alert Widget!
0.02: Use Medical Information app for medical alert text, and to display details

View File

@ -11,12 +11,12 @@ Implemented:
- Basic medical alert logo and message - Basic medical alert logo and message
- Only display bottom widget on clocks - Only display bottom widget on clocks
- High contrast colours depending on theme - High contrast colours depending on theme
- Configure medical alert text (using Medical Information app)
- Show details when touched (using Medical Information app)
Future: Future:
- Configure when to show bottom widget (always/never/clocks) - Configure when to show bottom widget (always/never/clocks)
- Configure medical alert text
- Show details when touched
## Creator ## Creator

View File

@ -1,10 +1,11 @@
{ "id": "widmeda", { "id": "widmeda",
"name": "Medical Alert Widget", "name": "Medical Alert Widget",
"shortName":"Medical Alert", "shortName":"Medical Alert",
"version":"0.01", "version":"0.02",
"description": "Display a medical alert in the bottom widget section.", "description": "Display a medical alert in the bottom widget section.",
"icon": "widget.png", "icon": "widget.png",
"type": "widget", "type": "widget",
"dependencies" : { "medicalinfo":"app" },
"tags": "health,medical,tools,widget", "tags": "health,medical,tools,widget",
"supports" : ["BANGLEJS2"], "supports" : ["BANGLEJS2"],
"readme": "README.md", "readme": "README.md",

View File

@ -1,30 +1,47 @@
(() => { (() => {
function getAlertText() {
const medicalinfo = require("medicalinfo").load();
const alertText = ((Array.isArray(medicalinfo.medicalAlert)) && (medicalinfo.medicalAlert[0])) ? medicalinfo.medicalAlert[0] : "";
return (g.wrapString(alertText, g.getWidth()).length === 1) ? alertText : "MEDICAL ALERT";
}
// Top right star of life logo // Top right star of life logo
WIDGETS["widmedatr"]={ WIDGETS["widmedatr"] = {
area: "tr", area: "tr",
width: 24, width: 24,
draw: function() { draw: function () {
g.reset(); g.reset();
g.setColor("#f00"); g.setColor("#f00");
g.drawImage(atob("FhYBAAAAA/AAD8AAPwAc/OD/P8P8/x/z/n+/+P5/wP58A/nwP5/x/v/n/P+P8/w/z/Bz84APwAA/AAD8AAAAAA=="), this.x + 1, this.y + 1); g.drawImage(atob("FhYBAAAAA/AAD8AAPwAc/OD/P8P8/x/z/n+/+P5/wP58A/nwP5/x/v/n/P+P8/w/z/Bz84APwAA/AAD8AAAAAA=="), this.x + 1, this.y + 1);
} }
}; };
// Bottom medical alert message // Bottom medical alert text
WIDGETS["widmedabl"]={ WIDGETS["widmedabl"] = {
area: "bl", area: "bl",
width: Bangle.CLOCK?Bangle.appRect.w:0, width: Bangle.CLOCK ? Bangle.appRect.w : 0,
draw: function() { draw: function () {
// Only show the widget on clocks // Only show the widget on clocks
if (!Bangle.CLOCK) return; if (!Bangle.CLOCK) return;
g.reset(); g.reset();
g.setBgColor(g.theme.dark ? "#fff" : "#f00"); g.setBgColor(g.theme.dark ? "#fff" : "#f00");
g.setColor(g.theme.dark ? "#f00" : "#fff"); g.setColor(g.theme.dark ? "#f00" : "#fff");
g.setFont("Vector",18); g.setFont("Vector", 16);
g.setFontAlign(0,0); g.setFontAlign(0, 0);
g.clearRect(this.x, this.y, this.x + this.width - 1, this.y + 23); g.clearRect(this.x, this.y, this.x + this.width - 1, this.y + 23);
g.drawString("MEDICAL ALERT", this.width / 2, this.y + ( 23 / 2 ));
const alertText = getAlertText();
g.drawString(alertText, this.width / 2, this.y + (23 / 2));
} }
}; };
Bangle.on("touch", (_, c) => {
const bl = WIDGETS.widmedabl;
const tr = WIDGETS.widmedatr;
if ((bl && c.x >= bl.x && c.x < bl.x + bl.width && c.y >= bl.y && c.y <= bl.y + 24)
|| (tr && c.x >= tr.x && c.x < tr.x + tr.width && c.y >= tr.y && c.y <= tr.y + 24)) {
load("medicalinfo.app.js");
}
});
})(); })();