mirror of https://github.com/espruino/BangleApps
Added vernier respiration app
parent
f6184f0fd1
commit
d9441b3a40
15
apps.json
15
apps.json
|
@ -3977,5 +3977,20 @@
|
|||
{"name":"ffcniftya.app.js","url":"app.js"},
|
||||
{"name":"ffcniftya.img","url":"app-icon.js","evaluate":true}
|
||||
]
|
||||
},
|
||||
{ "id": "vernierrespirate",
|
||||
"name": "Vernier Go Direct Respiration Belt",
|
||||
"shortName":"Respiration Belt",
|
||||
"version":"0.01",
|
||||
"description": "Connects to a Go Direct Respiration Belt and shows respiration rate",
|
||||
"icon": "app.png",
|
||||
"tags": "health,bluetooth",
|
||||
"supports" : ["BANGLEJS","BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
"storage": [
|
||||
{"name":"vernierrespirate.app.js","url":"app.js"},
|
||||
{"name":"vernierrespirate.img","url":"app-icon.js","evaluate":true}
|
||||
],
|
||||
"data": [{"name":"vernierrespirate.json"}]
|
||||
}
|
||||
]
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
0.01: New App!
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEw4UA///9nou30h/qJf8Ah/wBasK0ALhHcMBqALBgtABYsVqgLBAYILFqtUF4MVqoKEgoLFqALGEYQLFAwILEGAlV6tUlWoitXGAgLC1WqBYsBq+VqwLBAYPVMIUFBYN61Uq1oLBHgQLC1WohWqrwLDiteEIOggQDB2pICivqA4IFBlWq1YLCq/V9WkAoMa1YHBKQVf9XUAoMX1f1KgVVEYIpDEYILBLwIuBC4YFBMAMBLAILFLQILBrdV12UBYMW3VV8tAgt9q+2BYee6t9qEFuoLHroLBqte6wvDy+1ZoILBvdWSoeV9oLD+tfBYYFBBYTEBrq5CgN1aQNQgNVAALdEAAISBAYL1EfgISCAgIKDDAQSEAH4AQ"))
|
|
@ -0,0 +1,256 @@
|
|||
|
||||
// get settings
|
||||
var settings = require("Storage").readJSON("vernierrespirate.json",1)||{};
|
||||
settings.vibrateBPM = settings.vibrateBPM||27;
|
||||
// settings.vibrate; // undefined / "calculated" / "vernier"
|
||||
|
||||
function saveSettings() {
|
||||
require("Storage").writeJSON("vernierrespirate.json", settings);
|
||||
}
|
||||
|
||||
|
||||
g.clear();
|
||||
var graphHeight = g.getHeight()-100;
|
||||
var last = {
|
||||
time : Date.now(),
|
||||
x : 0,
|
||||
y : 24,
|
||||
};
|
||||
var avrValue;
|
||||
var aboveAvr = false;
|
||||
var lastBreath;
|
||||
var lastBreaths = [];
|
||||
var vibrateInterval;
|
||||
|
||||
function onMsg(txt) {
|
||||
print(txt);
|
||||
E.showMessage(txt);
|
||||
}
|
||||
|
||||
function setVibrate(isOn) {
|
||||
var wasOn = vibrateInterval!==undefined;
|
||||
if (isOn == wasOn) return;
|
||||
|
||||
if (isOn) {
|
||||
vibrateInterval = setInterval(function() {
|
||||
Bangle.buzz();
|
||||
}, 1000);
|
||||
} else {
|
||||
clearInterval(vibrateInterval);
|
||||
vibrateInterval = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function onBreath() {
|
||||
var t = Date.now();
|
||||
if (lastBreath!==undefined) {
|
||||
// time between breaths
|
||||
var value = 60000 / (t-lastBreath);
|
||||
// average of last 3
|
||||
while (lastBreaths.length>=3) lastBreaths.shift(); // keep length small
|
||||
lastBreaths.push(value);
|
||||
value = E.sum(lastBreaths) / lastBreaths.length;
|
||||
// draw value
|
||||
g.reset();
|
||||
g.clearRect(0,g.getHeight()-100,g.getWidth(),g.getHeight()-50);
|
||||
g.setFont("6x8").setFontAlign(0,0);
|
||||
g.drawString("Calculated measurement", g.getWidth()/2, g.getHeight()-95);
|
||||
g.setFont("Vector",40).setFontAlign(0,0);
|
||||
g.drawString(value.toFixed(2), g.getWidth()/2, g.getHeight()-70);
|
||||
// set vibration IF we're doing it from our calculations
|
||||
if (settings.vibrate == "calculated")
|
||||
setVibrate(value > settings.vibrateBPM);
|
||||
}
|
||||
lastBreath = t;
|
||||
}
|
||||
|
||||
function onData(n, value) {
|
||||
g.reset();
|
||||
if (n==2) {
|
||||
function scale(v) {
|
||||
return Math.max(graphHeight - (1+v*4),24);
|
||||
}
|
||||
if (avrValue==undefined) avrValue=value;
|
||||
avrValue = avrValue*0.95 + value*0.05;
|
||||
if (avrValue < 1) avrValue = 1;
|
||||
if (value > avrValue) {
|
||||
if (!aboveAvr) onBreath();
|
||||
aboveAvr = true;
|
||||
} else aboveAvr = false;
|
||||
|
||||
var t = Date.now();
|
||||
var x = Math.round((t - last.time) / 100) // 10 per second
|
||||
if (last.x>=g.getWidth()) {
|
||||
x = 0;
|
||||
last.x = 0;
|
||||
last.time = t;
|
||||
g.clearRect(0,24,g.getWidth(),graphHeight);
|
||||
}
|
||||
var y = scale(value);
|
||||
g.setPixel(x, scale(avrValue), "#f00");
|
||||
g.drawLine(last.x, last.y, x, y);
|
||||
last.x = x;
|
||||
last.y = y;
|
||||
}
|
||||
if (n==4) {
|
||||
g.clearRect(0,g.getHeight()-50,g.getWidth(),g.getHeight());
|
||||
g.setFont("6x8").setFontAlign(0,0);
|
||||
g.drawString("GoDirect measurement", g.getWidth()/2, g.getHeight()-45);
|
||||
g.setFont("Vector",40).setFontAlign(0,0);
|
||||
g.drawString(value.toFixed(2), g.getWidth()/2, g.getHeight()-20);
|
||||
// set vibration IF we're doing it from our calculations
|
||||
if (settings.vibrate == "vernier")
|
||||
setVibrate(value > settings.vibrateBPM);
|
||||
}
|
||||
Bangle.setLCDPower(1); // ensure LCD is on
|
||||
}
|
||||
|
||||
function connect() {
|
||||
var gatt, service, rx, tx;
|
||||
var rollingCounter = 0xFF;
|
||||
|
||||
// any button to exit
|
||||
Bangle.setUI("updown", function() {
|
||||
setVibrate(false);
|
||||
Bangle.buzz();
|
||||
try {
|
||||
if (gatt) gatt.disconnect();
|
||||
} catch (e) {
|
||||
}
|
||||
setTimeout(mainMenu, 1000);
|
||||
});
|
||||
|
||||
function sendCommand(subCommand) {
|
||||
const command = new Uint8Array(4 + subCommand.length);
|
||||
command.set(new Uint8Array(subCommand), 4);
|
||||
// Populate the packet header bytes
|
||||
command[0] = 0x58; // header
|
||||
command[1] = command.length;
|
||||
command[2] = --rollingCounter;
|
||||
command[3] = E.sum(command) & 0xFF; // checksum
|
||||
return tx.writeValue(command);
|
||||
}
|
||||
function firstSetBit(v) {
|
||||
return v & -v;
|
||||
}
|
||||
function handleResponse(dv) {
|
||||
//print(dv.buffer);
|
||||
var resType = dv.getUint8(0);
|
||||
if (resType==0x20) {
|
||||
// [32, 25, 207, 216, 6, 6, 0, 2, 252, 128, 138, 7, 191, 0, 0, 192, 127, 128, 49, 8, 191, 0, 0, 192, 127])
|
||||
// 6 = data type = real
|
||||
// 6,0 = bit mask for sensors
|
||||
// 2 = value count
|
||||
if (dv.getUint8(4)!=6) return; //throw "Not float32 data";
|
||||
var sensorIds = dv.getUint16(5, true);
|
||||
// var count = dv.getUint8(7); doesn't seem right
|
||||
var offs = 9;
|
||||
while (sensorIds) {
|
||||
var value = dv.getFloat32(offs, true);
|
||||
var s = firstSetBit(sensorIds);
|
||||
if (isFinite(value)) onData(s,value);
|
||||
//else print(s,value);
|
||||
sensorIds &= ~s;
|
||||
offs += 4;
|
||||
}
|
||||
} else {
|
||||
var cmd = dv.getUint8(4); // cmd
|
||||
//print("CMD",dv.buffer);
|
||||
}
|
||||
}
|
||||
|
||||
onMsg("Searching...");
|
||||
NRF.requestDevice({ filters: [{ namePrefix: 'GDX-RB' }] }).then(function(device) {
|
||||
device.on("gattserverdisconnected", function() {
|
||||
onMsg("Device disconnected");
|
||||
});
|
||||
onMsg("Found. Connecting...");
|
||||
return device.gatt.connect({minInterval:20, maxInterval:20});
|
||||
}).then(function(g) {
|
||||
gatt = g;
|
||||
return gatt.getPrimaryService("d91714ef-28b9-4f91-ba16-f0d9a604f112");
|
||||
}).then(function(s) {
|
||||
service = s;
|
||||
return service.getCharacteristic("f4bf14a6-c7d5-4b6d-8aa8-df1a7c83adcb");
|
||||
}).then(function(c) {
|
||||
tx = c;
|
||||
return service.getCharacteristic("b41e6675-a329-40e0-aa01-44d2f444babe");
|
||||
}).then(function(c) {
|
||||
rx = c;
|
||||
rx.on('characteristicvaluechanged', function(event) {
|
||||
//print("EVT",event.target.value.buffer);
|
||||
handleResponse(event.target.value);
|
||||
});
|
||||
return rx.startNotifications();
|
||||
}).then(function() {
|
||||
onMsg("Init");
|
||||
sendCommand([ // init
|
||||
0x1a, 0xa5, 0x4a, 0x06,
|
||||
0x49, 0x07, 0x48, 0x08,
|
||||
0x47, 0x09, 0x46, 0x0a,
|
||||
0x45, 0x0b, 0x44, 0x0c,
|
||||
0x43, 0x0d, 0x42, 0x0e,
|
||||
0x41,
|
||||
]);
|
||||
/*setTimeout(function() {
|
||||
print("Set measurement period");
|
||||
var us = 100000; // period in us
|
||||
sendCommand([0x1b, 0xff, 0x00,
|
||||
us & 255,
|
||||
(us >> 8) & 255,
|
||||
(us >> 16) & 255,
|
||||
(us >> 24) & 255,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00]);
|
||||
}, 100);*/
|
||||
|
||||
/* setTimeout(function() {
|
||||
print("Get sensor info");
|
||||
sendCommand([0x51, 0]); // get sensor IDs
|
||||
// returns [152, 10, 1, 39, 81, 253, 54, 0, 0, 0]
|
||||
// 54 is the bit mask of available channels
|
||||
//sendCommand([106, 16]); // get sensor info
|
||||
}, 2000);*/
|
||||
|
||||
setTimeout(function() {
|
||||
onMsg("Start measurements");
|
||||
//https://github.com/VernierST/godirect-js/blob/main/src/Device.js#L588
|
||||
var channels = 6; // data channels 4 and 2
|
||||
sendCommand([ // start measurements
|
||||
0x18, 0xff, 0x01, channels,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00
|
||||
]);
|
||||
}, 500);
|
||||
}).catch(function() {
|
||||
onMsg("Connect Fail");
|
||||
});
|
||||
}
|
||||
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
|
||||
function mainMenu() {
|
||||
var vibText = ["No","Calculated","Vernier"];
|
||||
var vibValue = ["","calculated","vernier"];
|
||||
E.showMenu({"":{title:"Respiration Belt"},
|
||||
"< Back" : () => { saveSettings(); load(); },
|
||||
"Connect" : () => { saveSettings(); E.showMenu(); connect(); },
|
||||
"Vib" : {
|
||||
value : Math.max(vibValue.indexOf(settings.vibrate),0),
|
||||
format : v => vibText[v],
|
||||
min:0,max:2,
|
||||
onchange : v => { settings.vibrate=vibValue[v]; }
|
||||
},
|
||||
"BPM" : {
|
||||
value : settings.vibrateBPM,
|
||||
min:10,max:50,
|
||||
onchange : v => { settings.vibrateBPM=v; }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
mainMenu();
|
Binary file not shown.
After Width: | Height: | Size: 1.9 KiB |
Loading…
Reference in New Issue