🔀Merge branch master from upstream (#23)
* Create HRV.js * Add files via upload * Create hrv-icon.js * Delete hrv-icon.js * Create HRV-icon.js * Update apps.json * Update apps.json * Update apps.json * Rename HRV.js to app.js * Rename HRV-icon.js to app-icon.js * Update apps.json * Update apps.json * Update apps.json * Update apps.json * add hardalarm * fix app description * fix same name for storage in apps.json * Add dtlaunch * Update app.js updated with declutter and additional options * Create ChangeLog * Update app.js * Create README.md * Update README.md * Update app.js * Update app.js * Update README.md * Update app.js updated so that now it takes readings every 3 minutes in continous mode and also the sample range has been extended to 30 seconds rather than just 20 * tweak * Create app.js * Add files via upload * Delete icon.png * Add files via upload * Update apps.json * Delete app-icon.png * Create app-icon.js * Update app-icon.js * Update app-icon.js * Update app-icon.js * Update app-icon.js * Add files via upload * Update app-icon.js * Update app.js * Create README.md * Update apps.json * Update README.md * Update apps.json * Update apps.json * Update app-icon.js * Update apps.json * Update app-icon.js * Update app-icon.js * Update apps.json * Update app-icon.js * Update apps.json * Update app-icon.js * Update apps.json * Update apps.json * Update README.md * Update app.js * Update README.md * Update app.js * Improve readibility to README.md * bump version * activepedom 0.05: Fix default step/distance display if it hasn't been set up first * Add files via upload * Update apps.json * Update apps.json * Update apps.json * Update app-icon.js * Update app-icon.js * Update app-icon.js * Delete app-icon.png * Add files via upload * Delete app-icon.png * Add files via upload * Update app-icon.js * Delete app-icon.png * Add files via upload * Update apps.json * Update README.md * Update apps.json * Update app-icon.js * Update app-icon.js * Update apps.json * Create hrmexp.js * Delete app.js * Update apps.json * Update apps.json * Update app-icon.js * Update app-icon.js * Update apps.json * Update app-icon.js * Update hrmexp.js * Update hrmexp.js * Update apps.json * Create README.md * Add files via upload * Create app.js * Create app-icon.js * Update apps.json * Update apps.json * Update apps.json * Update apps.json * Update apps.json * remove remove * Update README.md * Update README.md * Update README.md * Added Interface file to allow downloading (and deletion!) of BangleRun data files * added WIDGETS.activepedom.getSteps() * Update ChangeLog 0.06: Added WIDGETS.activepedom.getSteps() * bump versions * Create interface.html * Update apps.json * Update README.md * Add files via upload * Update and rename breather_settings.txt to settings.js * Update apps.json * Update app.js * Update app.js * Add files via upload * Update README.md * Delete readme_gif.gif * Update README.md * Update apps.json * Update README.md * Update README.md * Update README.md * Update README.md * Update app.js * Update app.js * Update apps.json * Rename settings.js to settings.json * feat(app): Add "Lazy Clock" app * update multiclock * update dtlaunch app * minor tweak for faster writes (best to end with a newline anyway) Co-authored-by: Gordon Williams <gw@pur3.co.uk> Co-authored-by: Ben Jabituya <74158243+jabituyaben@users.noreply.github.com> Co-authored-by: jamespsteinberg@gmail.com <jamespsteinberg@gmail.com> Co-authored-by: jeffmer <jeffmer@users.noreply.github.com> Co-authored-by: krichtof <geek@robiweb.net> Co-authored-by: hughbarney <hughbarney@gmail.com> Co-authored-by: Olly Cross <olly@ollyollyolly.com> Co-authored-by: Jeff Magee <jeffreymagee@gmail.com>master
108
apps.json
|
@ -1341,7 +1341,7 @@
|
|||
"name": "Active Pedometer",
|
||||
"shortName":"Active Pedometer",
|
||||
"icon": "app.png",
|
||||
"version":"0.04",
|
||||
"version":"0.06",
|
||||
"description": "Pedometer that filters out arm movement and displays a step goal progress. Steps are saved to a daily file and can be viewed as graph.",
|
||||
"tags": "outdoors,widget",
|
||||
"readme": "README.md",
|
||||
|
@ -1554,7 +1554,8 @@
|
|||
"shortName": "BangleRun",
|
||||
"icon": "banglerun.png",
|
||||
"version": "0.05",
|
||||
"description": "An app for running sessions.",
|
||||
"interface": "interface.html",
|
||||
"description": "An app for running sessions. Displays info and logs your run for later viewing.",
|
||||
"tags": "run,running,fitness,outdoors",
|
||||
"allow_emulator": false,
|
||||
"storage": [
|
||||
|
@ -2138,12 +2139,12 @@
|
|||
{ "id": "multiclock",
|
||||
"name": "Multi Clock",
|
||||
"icon": "multiclock.png",
|
||||
"version":"0.07",
|
||||
"version":"0.08",
|
||||
"description": "Clock with multiple faces - Big, Analogue, Digital, Text, Time-Date.\n Switch between faces with BTN1 & BTN3",
|
||||
"readme": "README.md",
|
||||
"tags": "clock",
|
||||
"type":"clock",
|
||||
"allow_emulator":false,
|
||||
"allow_emulator":true,
|
||||
"storage": [
|
||||
{"name":"multiclock.app.js","url":"clock.js"},
|
||||
{"name":"big.face.js","url":"big.js"},
|
||||
|
@ -2478,5 +2479,104 @@
|
|||
{"name":"gmeter.app.js","url":"app.js"},
|
||||
{"name":"gmeter.img","url":"app-icon.js","evaluate":true}
|
||||
]
|
||||
},
|
||||
{ "id": "dtlaunch",
|
||||
"name": "Desktop Launcher",
|
||||
"icon": "icon.png",
|
||||
"version":"0.03",
|
||||
"description": "Desktop style App Launcher with six apps per page - fast access if you have lots of apps installed.",
|
||||
"readme": "README.md",
|
||||
"tags": "tool,system,launcher",
|
||||
"type":"launch",
|
||||
"storage": [
|
||||
{"name":"dtlaunch.app.js","url":"app.js"},
|
||||
{"name":"dtlaunch.img","url":"app-icon.js","evaluate":true}
|
||||
]
|
||||
},
|
||||
{ "id": "HRV",
|
||||
"name": "Heart Rate Variability monitor",
|
||||
"shortName":"HRV monitor",
|
||||
"icon": "hrv.png",
|
||||
"version":"0.03",
|
||||
"description": "Heart Rate Variability monitor, see Readme for more info",
|
||||
"tags": "",
|
||||
"readme": "README.md",
|
||||
"storage": [
|
||||
{"name":"HRV.app.js","url":"app.js"},
|
||||
{"name":"HRV.img","url":"app-icon.js","evaluate":true}
|
||||
]
|
||||
},
|
||||
{ "id": "hardalarm",
|
||||
"name": "Hard Alarm",
|
||||
"shortName":"HardAlarm",
|
||||
"icon": "app.png",
|
||||
"version":"0.01",
|
||||
"description": "Make sure you wake up! Count to the right number to turn off the alarm",
|
||||
"tags": "tool,alarm,widget",
|
||||
"storage": [
|
||||
{"name":"hardalarm.app.js","url":"app.js"},
|
||||
{"name":"hardalarm.boot.js","url":"boot.js"},
|
||||
{"name":"hardalarm.js","url":"hardalarm.js"},
|
||||
{"name":"hardalarm.img","url":"app-icon.js","evaluate":true},
|
||||
{"name":"hardalarm.wid.js","url":"widget.js"}
|
||||
],
|
||||
"data": [
|
||||
{"name":"hardalarm.json"}
|
||||
]
|
||||
},
|
||||
{ "id": "edisonsball",
|
||||
"name": "Edison's Ball",
|
||||
"shortName":"Edison's Ball",
|
||||
"icon": "app-icon.png",
|
||||
"version":"0.01",
|
||||
"description": "Hypnagogia/Micro-Sleep alarm for experimental use in exploring sleep transition and combating drowsiness",
|
||||
"tags": "",
|
||||
"readme": "README.md",
|
||||
"storage": [
|
||||
{"name":"edisonsball.app.js","url":"app.js"},
|
||||
{"name":"edisonsball.img","url":"app-icon.js","evaluate":true}
|
||||
]
|
||||
},
|
||||
{ "id": "hrrawexp",
|
||||
"name": "HRM Data Exporter",
|
||||
"shortName":"HRM Data Exporter",
|
||||
"icon": "app-icon.png",
|
||||
"version":"0.01",
|
||||
"description": "export raw hrm signal data to a csv file",
|
||||
"tags": "",
|
||||
"readme": "README.md",
|
||||
"interface": "interface.html",
|
||||
"storage": [
|
||||
{"name":"hrrawexp.app.js","url":"app.js"},
|
||||
{"name":"hrrawexp.img","url":"app-icon.js","evaluate":true}
|
||||
]
|
||||
},
|
||||
{ "id": "breath",
|
||||
"name": "Breathing App",
|
||||
"shortName":"Breathing App",
|
||||
"icon": "app-icon.png",
|
||||
"version":"0.01",
|
||||
"description": "app to aid relaxation and train breath syncronicity using haptics and visualisation, also displays HR",
|
||||
"tags": "tools,health",
|
||||
"readme": "README.md",
|
||||
"storage": [
|
||||
{"name":"breath.app.js","url":"app.js"},
|
||||
{"name":"breath.settings.json","url":"settings.json"},
|
||||
{"name":"breath.img","url":"app-icon.js","evaluate":true}
|
||||
]
|
||||
},
|
||||
{ "id": "lazyclock",
|
||||
"name": "Lazy Clock",
|
||||
"icon": "lazyclock.png",
|
||||
"version":"0.01",
|
||||
"readme": "README.md",
|
||||
"description": "Tells the time, roughly",
|
||||
"tags": "clock",
|
||||
"type":"clock",
|
||||
"allow_emulator":true,
|
||||
"storage": [
|
||||
{"name":"lazyclock.app.js","url":"lazyclock-app.js"},
|
||||
{"name":"lazyclock.img","url":"lazyclock-icon.js","evaluate":true}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
0.01: New App!
|
||||
0.02: Added options to either run as a one-off reading, or a continuous mode to log data until the watch is reset
|
||||
0.03: Add RMSSD recording
|
|
@ -0,0 +1,18 @@
|
|||
Monitor Heart Rate Variability using the Bangle.JS
|
||||
===================================================
|
||||
|
||||
One-time mode:
|
||||
-------------
|
||||
|
||||
This will take a HRV measurement over a single approx 30 second period. It will also provide you with a HR reading based on the post-processing of the signal.
|
||||
|
||||
HRV metrics displayed are currently RMSSD (Root Mean Square of the Successive Differences) and also SDNN (standard deviation of NN intervals).
|
||||
|
||||
Continuous mode:
|
||||
----------------
|
||||
|
||||
This will continually take measurements over 30 second periods every 3 and half minutes and log them to a CSV file on the Bangle until the watch is reset; this file can then be reviewed in Excel or other apps. The log file is reset each time you restart and select this mode to save on storage. The log file is just 1 line per each 3 minute cycle showing: timestamp, HR, SDNN, RMSSD, sample count, Temp (uncalibrated CPU temp), and movement based on the accelerometer. The additional metrics aside from the HRM data are useful in analysing sleep.
|
||||
|
||||
Note that in both modes, if the watch seems unresponsive, it's processing data and if you continue to hold the reset button it will eventually restart.
|
||||
|
||||
If your sample count is less than around 5 samples and/or the readings don’t look right, try repositioning the watch and try again - you can use the HR monitor app to confirm fitting.
|
|
@ -0,0 +1 @@
|
|||
E.toArrayBuffer(atob("MDABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAYAA8AAAA8AA8ABgB8AA8ADwD4AA8AH4HwAA8AP8PgAA8Af+fAAA8A//+AAA8B+P8AAA8D4H4AAA8HwDwAAA8PgBgAAA8PAAAAAA8GAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA/////+AA//////AAf/////AAP////+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"))
|
|
@ -0,0 +1,303 @@
|
|||
var option = null;
|
||||
|
||||
//debugging or analysis files
|
||||
var logfile = require("Storage").open("HRV_log.csv", "w");
|
||||
|
||||
logfile = require("Storage").open("HRV_log.csv", "a");
|
||||
|
||||
var csv = [
|
||||
"time",
|
||||
"sample count",
|
||||
"HR",
|
||||
"SDNN",
|
||||
"RMSSD",
|
||||
"Temp",
|
||||
"movement"
|
||||
];
|
||||
logfile.write(csv.join(",")+"\n");
|
||||
|
||||
var debugging = true;
|
||||
|
||||
var first_signals = 0; // ignore the first several signals
|
||||
var heartrate = [];
|
||||
var BPM_array = [];
|
||||
var raw_HR_array = new Float32Array(1536);
|
||||
var alternate_array = new Float32Array(3072);
|
||||
var pulse_array = [];
|
||||
var pulsecount = 0;
|
||||
var cutoff_threshold = 0.5;
|
||||
var sample_frequency = 51.6;
|
||||
var gap_threshold = 0.15;
|
||||
var hr_min = 40;
|
||||
var hr_max = 160;
|
||||
var movement = 0;
|
||||
|
||||
function storeMyData(data, file_type) {
|
||||
log = raw_HR_array;
|
||||
// shift elements backwards - note the 4, because a Float32 is 4 bytes
|
||||
log.set(new Float32Array(log.buffer, 4 /*bytes*/));
|
||||
// add ad final element
|
||||
log[log.length - 1] = data;
|
||||
}
|
||||
|
||||
function average(samples) {
|
||||
var sum = 0;
|
||||
for (var i = 0; i < samples.length; i++) {
|
||||
sum += parseFloat(samples[i]);
|
||||
}
|
||||
var avg = sum / samples.length;
|
||||
return avg;
|
||||
}
|
||||
|
||||
function StandardDeviation (array) {
|
||||
const n = array.length;
|
||||
const mean = array.reduce((a, b) => a + b) / n;
|
||||
return Math.sqrt(array.map(x => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / n);
|
||||
}
|
||||
|
||||
function turn_off() {
|
||||
Bangle.setHRMPower(0);
|
||||
|
||||
var accel = setInterval(function () {
|
||||
movement = movement + Bangle.getAccel().diff;
|
||||
}, 1000);
|
||||
|
||||
g.clear();
|
||||
g.drawString("processing 1/5", 120, 120);
|
||||
|
||||
rolling_average(raw_HR_array,5);
|
||||
g.clear();
|
||||
g.drawString("processing 2/5", 120, 120);
|
||||
|
||||
upscale();
|
||||
g.clear();
|
||||
g.drawString("processing 3/5", 120, 120);
|
||||
|
||||
rolling_average(alternate_array,5);
|
||||
g.clear();
|
||||
g.drawString("processing 4/5", 120, 120);
|
||||
|
||||
apply_cutoff();
|
||||
find_peaks();
|
||||
|
||||
g.clear();
|
||||
g.drawString("processing 5/5", 120, 120);
|
||||
|
||||
calculate_HRV();
|
||||
}
|
||||
|
||||
function bernstein(A, B, C, D, E, t) {
|
||||
s = 1 - t;
|
||||
x = (A * Math.pow(s, 4)) + (B * 4 * Math.pow(s, 3) * t) + (C * 6 * s * s * t * t)
|
||||
+ (D * 4 * s * Math.pow(t, 3)) + (E * Math.pow(t, 4));
|
||||
return x;
|
||||
}
|
||||
|
||||
function upscale() {
|
||||
var index = 0;
|
||||
for (let i = raw_HR_array.length - 1; i > 5; i -= 5) {
|
||||
p0 = raw_HR_array[i];
|
||||
p1 = raw_HR_array[i - 1];
|
||||
p2 = raw_HR_array[i - 2];
|
||||
p3 = raw_HR_array[i - 3];
|
||||
p4 = raw_HR_array[i - 4];
|
||||
for (let T = 0; T < 100; T += 10) {
|
||||
x = T / 100;
|
||||
D = bernstein(p0, p1, p2, p3, p4, x);
|
||||
alternate_array[index] = D;
|
||||
index++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function rolling_average(values, count) {
|
||||
var temp_array = [];
|
||||
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
temp_array = [];
|
||||
for (let x = 0; x < count; x++)
|
||||
temp_array.push(values[i + x]);
|
||||
|
||||
values[i] = average(temp_array);
|
||||
}
|
||||
}
|
||||
|
||||
function apply_cutoff() {
|
||||
var x;
|
||||
for (let i = 0; i < alternate_array.length; i++) {
|
||||
x = alternate_array[i];
|
||||
if (x < cutoff_threshold)
|
||||
x = cutoff_threshold;
|
||||
alternate_array[i] = x;
|
||||
}
|
||||
}
|
||||
|
||||
function find_peaks() {
|
||||
var previous;
|
||||
var previous_slope = 0;
|
||||
var slope;
|
||||
var gap_size = 0;
|
||||
var temp_array = [];
|
||||
|
||||
for (let i = 0; i < alternate_array.length; i++) {
|
||||
if (previous == null)
|
||||
previous = alternate_array[i];
|
||||
slope = alternate_array[i] - previous;
|
||||
if (slope * previous_slope < 0) {
|
||||
if (gap_size > 30) {
|
||||
pulse_array.push(gap_size);
|
||||
gap_size = 0;
|
||||
}
|
||||
}
|
||||
else {
|
||||
gap_size++;
|
||||
}
|
||||
previous_slope = slope;
|
||||
previous = alternate_array[i];
|
||||
}
|
||||
}
|
||||
|
||||
function RMSSD(samples){
|
||||
var sum = 0;
|
||||
var square = 0;
|
||||
var data = [];
|
||||
var value = 0;
|
||||
|
||||
for (let i = 0; i < samples.length-1; i++) {
|
||||
value = Math.abs(samples[i]-samples[i+1])*((1 / (sample_frequency * 2)) * 1000);
|
||||
data.push(value);
|
||||
}
|
||||
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
square = data[i] * data[i];
|
||||
Math.round(square);
|
||||
sum += square;
|
||||
}
|
||||
|
||||
var meansquare = sum/data.length;
|
||||
var RMS = Math.sqrt(meansquare);
|
||||
RMS = parseInt(RMS);
|
||||
return RMS;
|
||||
}
|
||||
|
||||
function calculate_HRV() {
|
||||
var gap_average = average(pulse_array);
|
||||
var temp_array = [];
|
||||
var gap_max = (1 + gap_threshold) * gap_average;
|
||||
var gap_min = (1 - gap_threshold) * gap_average;
|
||||
for (let i = 0; i < pulse_array.length; i++) {
|
||||
if (pulse_array[i] > gap_min && pulse_array[i] < gap_max)
|
||||
temp_array.push(pulse_array[i]);
|
||||
}
|
||||
gap_average = average(temp_array);
|
||||
var calculatedHR = (sample_frequency*60)/(gap_average/2);
|
||||
if(option == 0)
|
||||
g.flip();
|
||||
g.clear();
|
||||
//var display_stdv = StandardDeviation(pulse_array).toFixed(1);
|
||||
var SDNN = (StandardDeviation(temp_array) * (1 / (sample_frequency * 2) * 1000)).toFixed(0);
|
||||
var RMS_SD = RMSSD(temp_array);
|
||||
g.drawString("SDNN:" + SDNN
|
||||
+"\nRMSSD:" + RMS_SD
|
||||
+ "\nHR:" + calculatedHR.toFixed(0)
|
||||
+"\nSample Count:" + temp_array.length, 120, 120);
|
||||
|
||||
if(option == 0){
|
||||
Bangle.buzz(500,1);
|
||||
clearInterval(routine);
|
||||
}
|
||||
|
||||
else{
|
||||
var csv = [
|
||||
0|getTime(),
|
||||
temp_array.length,
|
||||
calculatedHR.toFixed(0),
|
||||
SDNN,
|
||||
RMS_SD,
|
||||
E.getTemperature(),
|
||||
movement.toFixed(5)
|
||||
];
|
||||
logfile.write(csv.join(",")+"\n");
|
||||
|
||||
movement = 0;
|
||||
// for (let i = 0; i < raw_HR_array.length; i++) {
|
||||
// raw_HR_array[i] = null;
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
||||
function btn1Pressed() {
|
||||
if(option === null){
|
||||
clearInterval(accel);
|
||||
g.clear();
|
||||
g.drawString("one-off assessment", 120, 120);
|
||||
option = 0;
|
||||
Bangle.setHRMPower(1);
|
||||
}
|
||||
}
|
||||
|
||||
function btn3Pressed() {
|
||||
if(option === null){
|
||||
logfile.write(""); //reset HRV log
|
||||
clearInterval(accel);
|
||||
g.clear();
|
||||
g.drawString("continuous mode", 120, 120);
|
||||
option = 1;
|
||||
Bangle.setHRMPower(1);
|
||||
}
|
||||
}
|
||||
|
||||
var routine = setInterval(function () {
|
||||
clearInterval(accel);
|
||||
first_signals = 0; // ignore the first several signals
|
||||
pulsecount = 0;
|
||||
BPM_array = [];
|
||||
heartrate = [];
|
||||
pulse_array = [];
|
||||
Bangle.setHRMPower(1);
|
||||
}, 180000);
|
||||
|
||||
var accel = setInterval(function () {
|
||||
movement = movement + Bangle.getAccel().diff;
|
||||
}, 1000);
|
||||
|
||||
g.clear();
|
||||
g.setColor("#00ff7f");
|
||||
g.setFont("6x8", 2);
|
||||
g.setFontAlign(-1,1);
|
||||
g.drawString("continuous", 120, 210);
|
||||
g.setFontAlign(-1,1);
|
||||
g.drawString("one-time", 140, 50);
|
||||
|
||||
g.setColor("#ffffff");
|
||||
g.setFontAlign(0, 0); // center font
|
||||
g.drawString("check app README", 120, 120);
|
||||
g.drawString("for more info", 120, 140);
|
||||
|
||||
setWatch(btn1Pressed, BTN1, {repeat:true});
|
||||
setWatch(btn3Pressed, BTN3, {repeat:true});
|
||||
|
||||
Bangle.on('HRM', function (hrm) {
|
||||
if(option == 0)
|
||||
g.flip();
|
||||
if (first_signals < 3) {
|
||||
g.clear();
|
||||
g.drawString("setting up...\nremain still " + first_signals * 20 + "%", 120, 120);
|
||||
first_signals++;
|
||||
}
|
||||
else {
|
||||
BPM_array = hrm.raw;
|
||||
if(hrm.bpm > hr_min && hrm.bpm < hr_max)
|
||||
heartrate.push(hrm.bpm);
|
||||
if (pulsecount < 7) {
|
||||
for (let i = 0; i < 256; i++) {
|
||||
storeMyData(BPM_array[i], 0);
|
||||
}
|
||||
g.clear();
|
||||
g.drawString("logging: " + ((pulsecount/6)*100).toFixed(0) + "%", 120, 120);
|
||||
}
|
||||
if(pulsecount == 6)
|
||||
turn_off();
|
||||
pulsecount++;
|
||||
}
|
||||
});
|
After Width: | Height: | Size: 749 B |
|
@ -2,3 +2,5 @@
|
|||
0.02: Distance calculation and display
|
||||
0.03: Data logging and display
|
||||
0.04: Steps are set to 0 in log on new day
|
||||
0.05: Fix default step/distance display if it hasn't been set up first
|
||||
0.06: Added WIDGETS.activepedom.getSteps()
|
||||
|
|
|
@ -68,6 +68,8 @@
|
|||
'stepSensitivity' : 80,
|
||||
'stepGoal' : 10000,
|
||||
'stepLength' : 75,
|
||||
'lineOne' : "Distance",
|
||||
'lineTwo' : "Steps",
|
||||
};
|
||||
if (!settings) { loadSettings(); }
|
||||
return (key in settings) ? settings[key] : DEFAULTS[key];
|
||||
|
@ -160,7 +162,6 @@
|
|||
if (active == 1) g.setColor(0x07E0); //green
|
||||
else g.setColor(0xFFFF); //white
|
||||
g.setFont("6x8", 2);
|
||||
|
||||
if (setting('lineOne') == 'Steps') {
|
||||
g.drawString(kFormatterSteps(stepsCounted),this.x+1,this.y); //first line, big number, steps
|
||||
}
|
||||
|
@ -227,6 +228,6 @@
|
|||
|
||||
setStepSensitivity(setting('stepSensitivity')); //set step sensitivity (80 is standard, 400 is muss less sensitive)
|
||||
timerStoreData = setInterval(storeData, storeDataInterval); //store data regularly
|
||||
//Add widget
|
||||
WIDGETS["activepedom"]={area:"tl",width:width,draw:draw};
|
||||
//Add widget, use: WIDGETS.activepedom.getSteps() inside another App to return todays step count
|
||||
WIDGETS["activepedom"]={area:"tl",width:width,draw:draw, getSteps:()=>stepsCounted};
|
||||
})();
|
|
@ -0,0 +1,216 @@
|
|||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="../../css/spectre.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="tracks"></div>
|
||||
|
||||
<script src="../../core/lib/interface.js"></script>
|
||||
<script>
|
||||
/* TODO: Calculate cadence from step count */
|
||||
var domTracks = document.getElementById("tracks");
|
||||
|
||||
function saveKML(track,title) {
|
||||
var kml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<kml xmlns="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2">
|
||||
<Document>
|
||||
<Schema id="schema">
|
||||
<gx:SimpleArrayField name="heartrate" type="int">
|
||||
<displayName>Heart Rate</displayName>
|
||||
</gx:SimpleArrayField>
|
||||
<gx:SimpleArrayField name="steps" type="int">
|
||||
<displayName>Step Count</displayName>
|
||||
</gx:SimpleArrayField>
|
||||
<gx:SimpleArrayField name="distance" type="float">
|
||||
<displayName>Distance</displayName>
|
||||
</gx:SimpleArrayField>
|
||||
<gx:SimpleArrayField name="cadence" type="int">
|
||||
<displayName>Cadence</displayName>
|
||||
</gx:SimpleArrayField>
|
||||
</Schema>
|
||||
<Folder>
|
||||
<name>Tracks</name>
|
||||
<Placemark>
|
||||
<name>${title}</name>
|
||||
<gx:Track>
|
||||
${track.map(pt=>` <when>${pt.date.toISOString()}</when>\n`).join("")}
|
||||
${track.map(pt=>` <gx:coord>${pt.lon} ${pt.lat} ${pt.alt}</gx:coord>\n`).join("")}
|
||||
<ExtendedData>
|
||||
<SchemaData schemaUrl="#schema">
|
||||
<gx:SimpleArrayData name="heartrate">
|
||||
${track.map(pt=>` <gx:value>${pt.heartrate}</gx:value>\n`).join("")}
|
||||
</gx:SimpleArrayData>
|
||||
<gx:SimpleArrayData name="steps">
|
||||
${track.map(pt=>` <gx:value>${pt.steps}</gx:value>\n`).join("")}
|
||||
</gx:SimpleArrayData>
|
||||
<gx:SimpleArrayData name="distance">
|
||||
${track.map(pt=>` <gx:value>${pt.distance}</gx:value>\n`).join("")}
|
||||
</gx:SimpleArrayData>
|
||||
</SchemaData>
|
||||
</ExtendedData>
|
||||
</gx:Track>
|
||||
</Placemark>
|
||||
</Folder>
|
||||
</Document>
|
||||
</kml>`;
|
||||
var a = document.createElement("a"),
|
||||
file = new Blob([kml], {type: "application/vnd.google-earth.kml+xml"});
|
||||
var url = URL.createObjectURL(file);
|
||||
a.href = url;
|
||||
a.download = title+".kml";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
setTimeout(function() {
|
||||
document.body.removeChild(a);
|
||||
window.URL.revokeObjectURL(url);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function saveGPX(track, title) {
|
||||
var gpx = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<gpx creator="Bangle.js" version="1.1" xmlns="http://www.topografix.com/GPX/1/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v1 http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v1 http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v1 http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v1 http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v1 http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd" xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1" xmlns:gpxx="http://www.garmin.com/xmlschemas/GpxExtensions/v3">
|
||||
<metadata>
|
||||
<time>${track[0].date.toISOString()}</time>
|
||||
</metadata>
|
||||
<trk>
|
||||
<name>${title}</name>
|
||||
<trkseg>`;
|
||||
track.forEach(pt=>{
|
||||
gpx += `
|
||||
<trkpt lat="${pt.lat}" lon="${pt.lon}">
|
||||
<ele>${pt.alt}</ele>
|
||||
<time>${pt.date.toISOString()}</time>
|
||||
<extensions>
|
||||
<gpxtpx:TrackPointExtension>
|
||||
<gpxtpx:hr>${pt.heartrate}</gpxtpx:hr>
|
||||
<gpxtpx:distance>${pt.distance}</gpxtpx:distance>
|
||||
${/* <gpxtpx:cad>65</gpxtpx:cad> */""}
|
||||
</gpxtpx:TrackPointExtension>
|
||||
</extensions>
|
||||
</trkpt>`;
|
||||
});
|
||||
gpx += `
|
||||
</trkseg>
|
||||
</trk>
|
||||
</gpx>`;
|
||||
var a = document.createElement("a"),
|
||||
file = new Blob([gpx], {type: "application/gpx+xml"});
|
||||
var url = URL.createObjectURL(file);
|
||||
a.href = url;
|
||||
a.download = title+".gpx";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
setTimeout(function() {
|
||||
document.body.removeChild(a);
|
||||
window.URL.revokeObjectURL(url);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function trackLineToObject(l, hasFileName) {
|
||||
// "timestamp,latitude,longitude,altitude,duration,distance,heartrate,steps\n"
|
||||
var t = l.trim().split(",");
|
||||
var n = hasFileName ? 1 : 0;
|
||||
var o = {
|
||||
date : new Date(parseInt(t[n+0])),
|
||||
lat : parseFloat(t[n+1]),
|
||||
lon : parseFloat(t[n+2]),
|
||||
alt : parseFloat(t[n+3]),
|
||||
duration : parseFloat(t[n+4]),
|
||||
distance : parseFloat(t[n+5]),
|
||||
heartrate : parseInt(t[n+6]),
|
||||
steps : parseInt(t[n+7]),
|
||||
};
|
||||
if (hasFileName)
|
||||
o.filename = t[0];
|
||||
return o;
|
||||
}
|
||||
|
||||
function downloadTrack(trackid, callback) {
|
||||
Util.showModal("Downloading Track...");
|
||||
Util.readStorageFile(trackid, data=>{
|
||||
Util.hideModal();
|
||||
var trackLines = data.trim().split("\n");
|
||||
trackLines.shift(); // remove first line, which is column header
|
||||
// should be:
|
||||
// "timestamp,latitude,longitude,altitude,duration,distance,heartrate,steps\n"
|
||||
var track = trackLines.map(l=>trackLineToObject(l,false));
|
||||
callback(track);
|
||||
});
|
||||
}
|
||||
function getTrackList() {
|
||||
Util.showModal("Loading Tracks...");
|
||||
domTracks.innerHTML = "";
|
||||
Puck.eval(`require("Storage").list(/banglerun_.*\\1/).map(fn=>{fn=fn.slice(0,-1);var f=require("Storage").open(fn,"r");f.readLine();return fn+","+f.readLine()})`,trackLines=>{
|
||||
var html = `<div class="container">
|
||||
<div class="columns">\n`;
|
||||
trackLines.forEach(l => {
|
||||
var track = trackLineToObject(l, true /*has filename*/);
|
||||
html += `
|
||||
<div class="column col-12">
|
||||
<div class="card-header">
|
||||
<div class="card-title h5">Track ${track.filename}</div>
|
||||
<div class="card-subtitle text-gray">${track.date.toString().substr(0,24)}</div>
|
||||
</div>
|
||||
<div class="card-image">
|
||||
<iframe
|
||||
width="100%"
|
||||
height="250"
|
||||
frameborder="0" style="border:0"
|
||||
src="https://www.google.com/maps/embed/v1/place?key=AIzaSyBxTcwrrVOh2piz7EmIs1Xn4FsRxJWeVH4&q=${track.lat},${track.lon}&zoom=10" allowfullscreen>
|
||||
</iframe>
|
||||
</div>
|
||||
<div class="card-body"></div>
|
||||
<div class="card-footer">
|
||||
<button class="btn btn-primary" trackid="${track.filename}" task="downloadkml">Download KML</button>
|
||||
<button class="btn btn-primary" trackid="${track.filename}" task="downloadgpx">Download GPX</button>
|
||||
<button class="btn btn-default" trackid="${track.filename}" task="delete">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
if (trackLines.length==0) {
|
||||
html += `
|
||||
<div class="column col-12">
|
||||
<div class="card-header">
|
||||
<div class="card-title h5">No tracks</div>
|
||||
<div class="card-subtitle text-gray">No GPS tracks found</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
html += `
|
||||
</div>
|
||||
</div>`;
|
||||
domTracks.innerHTML = html;
|
||||
Util.hideModal();
|
||||
var buttons = domTracks.querySelectorAll("button");
|
||||
for (var i=0;i<buttons.length;i++) {
|
||||
buttons[i].addEventListener("click",event => {
|
||||
var button = event.currentTarget;
|
||||
var trackid = button.getAttribute("trackid");
|
||||
var task = button.getAttribute("task");
|
||||
if (task=="delete") {
|
||||
Util.showModal("Deleting Track...");
|
||||
Util.eraseStorageFile(trackid,()=>{
|
||||
Util.hideModal();
|
||||
getTrackList();
|
||||
});
|
||||
}
|
||||
if (task=="downloadkml") {
|
||||
downloadTrack(trackid, track => saveKML(track, `Bangle.js Track ${trackid}`));
|
||||
}
|
||||
if (task=="downloadgpx") {
|
||||
downloadTrack(trackid, track => saveGPX(track, `Bangle.js Track ${trackid}`));
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function onInit() {
|
||||
getTrackList();
|
||||
}
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -24,6 +24,10 @@ function parseNmea(state: AppState, nmea: string): void {
|
|||
const tokens = nmea.split(',');
|
||||
const sentence = tokens[0].substr(3, 3);
|
||||
|
||||
// FIXME: Bangle.js reports HDOP from GGA - can this be used instead
|
||||
// of manually parsing all of the raw GPS data, which can cause FIFO_FULL
|
||||
// errors?
|
||||
|
||||
switch (sentence) {
|
||||
case 'GGA':
|
||||
state.lat = parseCoordinate(tokens[2]) * (tokens[3] === 'N' ? 1 : -1);
|
||||
|
|
|
@ -17,11 +17,10 @@ function initLog(state: AppState): void {
|
|||
'distance',
|
||||
'heartrate',
|
||||
'steps',
|
||||
].join(','));
|
||||
].join(',') + '\n');
|
||||
}
|
||||
|
||||
function updateLog(state: AppState): void {
|
||||
state.file.write('\n');
|
||||
state.file.write([
|
||||
Date.now().toFixed(0),
|
||||
state.lat.toFixed(6),
|
||||
|
@ -31,7 +30,7 @@ function updateLog(state: AppState): void {
|
|||
state.distance.toFixed(2),
|
||||
state.hr.toFixed(0),
|
||||
state.steps.toFixed(0),
|
||||
].join(','));
|
||||
].join(',') + '\n');
|
||||
}
|
||||
|
||||
export { initLog, updateLog };
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
Breathing App
|
||||
=============
|
||||
This app attempts to aid relaxation and train breath syncronicity by providing a visualisation of a circle that expands and contracts to guide breathing rate. The app also modulates the vibration motor so you don't neccessarily have to look at the screen. Your HR is displayed in the lower left and there are a few parameters you can change to tailor what works best for you. The menu is quie self-explanatory, the 'ex_in_ratio' just allows an option to make the exhale speed slightly faster if you prefer that - durations can be further altered by increasing the pause times.
|
||||
|
||||
Resonance frequency breathing is a way of breathing (slow relaxed diaphragmatic breathing at around 3-7 breaths per minute) that has a regulating effect on the autonomic nervous system and other key body systems such as the circulatory system. This has many benefits, supported by numerous studies e.g.:
|
||||
|
||||
Increases pulmonary function
|
||||
Lowers blood pressure
|
||||
Improves baroreflex gain
|
||||
Improves heart rate variability
|
||||
Increases the ability to handle stress
|
||||
Clinical improvements in asthma
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwxH+AH4A/AH4AFqoAFF94zoF5QxkF5gxiF/t5F92k5wvt5AvtvXOAAIvqvJeBF9i9BAAYvoFwovnvQuGF816XYYwiFw4tIF8qMHF86NJF8iOLGDqOMztV0YvOxIABF5qIB0l6RxFPp1VMBgtCGB4eDvTtH0dPL4gwHFQYwPXBjBTGBwvVYTIv/FyAviFpYuOF6bsOF8wqFFpxffACIuuF7llL99lGKKPeGKAvgGBzufF6XHAAPNFzIvUAAZqEFqC/SF44AkF/4A/AH4A/ADIA="))
|
After Width: | Height: | Size: 1.3 KiB |
|
@ -0,0 +1,222 @@
|
|||
var angle = -90;
|
||||
var origin = angle * (Math.PI / 180);
|
||||
var max_radius = 70;
|
||||
var direction = 1;
|
||||
var display_HR = "--";
|
||||
var first_signal = true;
|
||||
var interval;
|
||||
var timeout;
|
||||
var settings;
|
||||
var status = 0;
|
||||
|
||||
var colours = {
|
||||
green: ["#00ff7f", "green"],
|
||||
white: ["#ffffff", "white"],
|
||||
blue: ["#00abff", "blue"],
|
||||
red: ["#ff3329", "red"],
|
||||
yellow: ["#fdff00", "yellow"]
|
||||
};
|
||||
|
||||
var settings_file = require("Storage").open("breath.settings.json", "r");
|
||||
|
||||
var test = settings_file.read(settings_file.getLength());
|
||||
|
||||
if(test!== undefined)
|
||||
settings = JSON.parse(test);
|
||||
|
||||
if (settings === undefined) {
|
||||
settings = {
|
||||
period: 2,
|
||||
exhale_pause: 1,
|
||||
inhale_pause: 1,
|
||||
colour: colours.green,
|
||||
vibrate: "forward",
|
||||
ex_in_ratio: "1:1"
|
||||
};
|
||||
}
|
||||
|
||||
var selection = ["speed", "exhale pause", "inhale pause", "colour", "vibrate",
|
||||
"ex_in_ratio", "in_progress", "paused"];
|
||||
|
||||
var colours = {
|
||||
green: ["#00ff7f", "green"],
|
||||
white: ["#ffffff", "white"],
|
||||
blue: ["#00abff", "blue"],
|
||||
red: ["#ff3329", "red"],
|
||||
yellow: ["#fdff00", "yellow"]
|
||||
};
|
||||
|
||||
g.setFont("6x8", 2);
|
||||
|
||||
function circle() {
|
||||
|
||||
g.clear();
|
||||
adjusted_radius = max_radius * Math.abs(origin);
|
||||
g.drawCircle(120, 120, adjusted_radius);
|
||||
radius = Math.abs(Math.sin(origin));
|
||||
angle += 2;
|
||||
origin = angle * (Math.PI / 180);
|
||||
if (angle >= 0 && angle < 90) {
|
||||
if (angle == 2) {
|
||||
clearInterval();
|
||||
g.setFontAlign(-1, -1);
|
||||
g.drawString("<<", 220, 40);
|
||||
status = 7;
|
||||
timeout = setTimeout(function () {
|
||||
interval = restart_interval();
|
||||
}, settings.exhale_pause * 1000);
|
||||
}
|
||||
direction = 0;
|
||||
}
|
||||
else {
|
||||
if (angle == 90)
|
||||
angle = -90;
|
||||
if (angle == -90) {
|
||||
clearInterval();
|
||||
g.setFontAlign(-1, -1);
|
||||
g.drawString("<<", 220, 40);
|
||||
status = 7;
|
||||
timeout = setTimeout(function () {
|
||||
interval = restart_interval();
|
||||
}, settings.inhale_pause * 1000);
|
||||
}
|
||||
direction = 1;
|
||||
}
|
||||
g.drawString(display_HR, 20, 200);
|
||||
|
||||
g.flip();
|
||||
|
||||
if (settings.vibrate == "forward")
|
||||
Bangle.buzz(50, Math.abs(origin)/1.5);
|
||||
else if (settings.vibrate == "backward")
|
||||
Bangle.buzz(50, (1.6 - (Math.abs(origin))));
|
||||
}
|
||||
|
||||
function restart_interval() {
|
||||
status = 6;
|
||||
var calc = 5 - settings.period;
|
||||
calc *= 15;
|
||||
calc += 120;
|
||||
if(direction == 1 && settings.ex_in_ratio == "5:6"){
|
||||
calc -= calc*0.2;
|
||||
}
|
||||
interval = setInterval(circle, calc);
|
||||
}
|
||||
|
||||
function update_menu() {
|
||||
g.clear();
|
||||
g.setColor(settings.colour[0]);
|
||||
g.setFontAlign(-1, -1);
|
||||
g.drawString("+/-", 200, 200);
|
||||
g.drawString("<>", 220, 40);
|
||||
g.drawString("GO", 210, 120);
|
||||
g.setFontAlign(-1, -1);
|
||||
var cursor = 60;
|
||||
|
||||
while (cursor < 180) {
|
||||
var key = Object.keys(settings)[(cursor - 60) / 20];
|
||||
var value = settings[key];
|
||||
|
||||
if (status == ((cursor - 60) / 20)) {
|
||||
g.setColor(colours.white[0]);
|
||||
}
|
||||
else
|
||||
g.setColor(settings.colour[0]);
|
||||
|
||||
var display_txt = selection[(cursor - 60) / 20] + ": " + value;
|
||||
|
||||
if(((cursor - 60) / 20) == 3)
|
||||
display_txt = selection[(cursor - 60) / 20] + ": " + value[1];
|
||||
|
||||
g.drawString(display_txt, 10, cursor);
|
||||
cursor += 20;
|
||||
}
|
||||
}
|
||||
|
||||
function btn1Pressed() {
|
||||
if (status < 6) {
|
||||
status += 1;
|
||||
if (status == 6)
|
||||
status = 0;
|
||||
|
||||
update_menu();
|
||||
}
|
||||
else if (status == 7) {
|
||||
clearTimeout(timeout);
|
||||
clearInterval();
|
||||
status = 0;
|
||||
update_menu();
|
||||
}
|
||||
}
|
||||
|
||||
function btn2Pressed() {
|
||||
if (status < 6) {
|
||||
settings_file = require("Storage").open("breath.settings.json", "w");
|
||||
settings_file.write(JSON.stringify(settings));
|
||||
Bangle.setHRMPower(1);
|
||||
g.setColor(settings.colour[0]);
|
||||
restart_interval();
|
||||
}
|
||||
}
|
||||
|
||||
function btn3Pressed() {
|
||||
if (status < 6) {
|
||||
if (status == 0) {
|
||||
settings.period += 1;
|
||||
if (settings.period > 6)
|
||||
settings.period = 1;
|
||||
}
|
||||
else if (status == 1) {
|
||||
settings.exhale_pause += 1;
|
||||
if (settings.exhale_pause > 4)
|
||||
settings.exhale_pause = 1;
|
||||
}
|
||||
else if (status == 2) {
|
||||
settings.inhale_pause += 1;
|
||||
if (settings.inhale_pause > 4)
|
||||
settings.inhale_pause = 1;
|
||||
}
|
||||
else if (status == 3) {
|
||||
if (settings.colour[0] == colours.green[0]) {
|
||||
settings.colour = colours.blue;
|
||||
}
|
||||
else if (settings.colour[0] == colours.blue[0])
|
||||
settings.colour = colours.red;
|
||||
else if (settings.colour[0] == colours.red[0])
|
||||
settings.colour = colours.yellow;
|
||||
else if (settings.colour[0] == colours.yellow[0])
|
||||
settings.colour = colours.green;
|
||||
}
|
||||
else if (status == 4) {
|
||||
if (settings.vibrate == "forward")
|
||||
settings.vibrate = "backward";
|
||||
else if (settings.vibrate == "backward")
|
||||
settings.vibrate = "off";
|
||||
else if (settings.vibrate == "off")
|
||||
settings.vibrate = "forward";
|
||||
}
|
||||
else if(status == 5){
|
||||
if(settings.ex_in_ratio == "1:1")
|
||||
settings.ex_in_ratio = "5:6";
|
||||
else
|
||||
settings.ex_in_ratio = "1:1";
|
||||
}
|
||||
update_menu();
|
||||
}
|
||||
}
|
||||
|
||||
update_menu();
|
||||
|
||||
setWatch(btn1Pressed, BTN1, { repeat: true });
|
||||
setWatch(btn2Pressed, BTN2, { repeat: true });
|
||||
setWatch(btn3Pressed, BTN3, { repeat: true });
|
||||
|
||||
Bangle.on('HRM', function (hrm) {
|
||||
if (first_signal)
|
||||
first_signal = false;
|
||||
else{
|
||||
var signal = hrm.bpm;
|
||||
if(signal > 50 && signal < 180)
|
||||
display_HR = signal;
|
||||
}
|
||||
});
|
|
@ -0,0 +1 @@
|
|||
{"period":2,"exhale_pause":1,"inhale_pause":1,"colour":["#00ff7f","green"],"vibrate":"forward","ex_in_ratio":"5:6"}
|
|
@ -0,0 +1,4 @@
|
|||
0.01: Initial version
|
||||
0.02: Multiple pages
|
||||
0.03: cycle thru pages
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# Desktop style App Launcher
|
||||
|
||||

|
||||
|
||||
In the picture above, the Settings app is selected.
|
||||
## Controls
|
||||
|
||||
**BTN1** - move backward through app icons on a page
|
||||
|
||||
**BTN2** - run the selected app
|
||||
|
||||
**BTN3** - move forward through app icons
|
||||
|
||||
**Swipe Left** - move to next page of app icons
|
||||
|
||||
**Swipe Right** - move to previous page of app icons
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwhC/AH4ATxAAQC+2N7vd7AX/C/6/7a/4X/a/4X/C/4X/C/4Xfl3iC6vu9wXtI653WAH4A/ABg"))
|
|
@ -0,0 +1,73 @@
|
|||
/* Desktop launcher
|
||||
*
|
||||
*/
|
||||
|
||||
var s = require("Storage");
|
||||
var apps = s.list(/\.info$/).map(app=>{var a=s.readJSON(app,1);return a&&{name:a.name,type:a.type,icon:a.icon,sortorder:a.sortorder,src:a.src};}).filter(app=>app && (app.type=="app" || app.type=="clock" || !app.type));
|
||||
apps.sort((a,b)=>{
|
||||
var n=(0|a.sortorder)-(0|b.sortorder);
|
||||
if (n) return n; // do sortorder first
|
||||
if (a.name<b.name) return -1;
|
||||
if (a.name>b.name) return 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
var Napps = apps.length;
|
||||
var Npages = Math.ceil(Napps/6);
|
||||
var maxPage = Npages-1;
|
||||
var selected = 0;
|
||||
var oldselected = -1;
|
||||
var page = 0;
|
||||
|
||||
function draw_icon(p,n,selected) {
|
||||
var x = (n%3)*80;
|
||||
var y = n>2?130:40;
|
||||
(selected?g.setColor(0.3,0.3,0.3):g.setColor(0,0,0)).fillRect(x,y,x+79,y+89);
|
||||
g.drawImage(s.read(apps[p*6+n].icon),x+10,y+10,{scale:1.25});
|
||||
g.setColor(-1).setFontAlign(0,-1,0).setFont("6x8",1);
|
||||
var txt = apps[p*6+n].name.split(" ");
|
||||
for (var i = 0; i < txt.length; i++) {
|
||||
txt[i] = txt[i].trim();
|
||||
g.drawString(txt[i],x+40,y+70+i*8);
|
||||
}
|
||||
}
|
||||
|
||||
function drawPage(p){
|
||||
g.setColor(0,0,0).fillRect(0,0,239,239);
|
||||
g.setFont("6x8",2).setFontAlign(0,-1,0).setColor(1,1,1).drawString("Bangle ("+(p+1)+"/"+Npages+")",120,12);
|
||||
for (var i=0;i<6;i++) {
|
||||
if (!apps[p*6+i]) return i;
|
||||
draw_icon(p,i,selected==i);
|
||||
}
|
||||
}
|
||||
|
||||
Bangle.on("swipe",(dir)=>{
|
||||
selected = 0;
|
||||
oldselected=-1;
|
||||
if (dir<0){
|
||||
++page; if (page>maxPage) page=0;
|
||||
drawPage(page);
|
||||
} else {
|
||||
--page; if (page<0) page=maxPage;
|
||||
drawPage(page);
|
||||
}
|
||||
});
|
||||
|
||||
function nextapp(d){
|
||||
oldselected = selected;
|
||||
selected+=d;
|
||||
selected = selected<0?5:selected>5?0:selected;
|
||||
selected = (page*6+selected)>=Napps?0:selected;
|
||||
draw_icon(page,selected,true);
|
||||
if (oldselected>=0) draw_icon(page,oldselected,false);
|
||||
}
|
||||
|
||||
function doselect(){
|
||||
load(apps[page*6+selected].src);
|
||||
}
|
||||
|
||||
setWatch(nextapp.bind(null,-1), BTN1, {repeat:true,edge:"falling"});
|
||||
setWatch(doselect, BTN2, {repeat:true,edge:"falling"});
|
||||
setWatch(nextapp.bind(null,1), BTN3, {repeat:true,edge:"falling"});
|
||||
|
||||
drawPage(0);
|
After Width: | Height: | Size: 305 B |
After Width: | Height: | Size: 49 KiB |
|
@ -0,0 +1,5 @@
|
|||
The application is based on a technique that Thomas Edison used to prevent falling asleep using a steel ball. Essentially the app starts with a display that shows the current HR value that the watch alarm is set to and this can be adjusted with buttons 1 and 3. This HR settng should be the approximate value you want the alarm to trigger and so you should ideally know both what your HR is currently and what your heartrate normally is during sleep. For your current HR according to the watch, you can simply use the HR monitor available in the Espruino app loader, and then from that you can choose a lower value as the target for the alarm and adjust as required.
|
||||
|
||||
When you press the middle button on the side, the HR monitor starts, the alarm will trigger when your heart rate average drops to the limit you’ve set and has a certain level of steadiness that is determined by a assessing the variance over several readings - the sensitivity of this variance can be adjusted in a variable in the app's code under 'ADVANCED SETTINGS' if needed. The code also has a basic logging function which shows, in a CSV file, when you started the HR tracker and when the alarm was triggered.
|
||||
|
||||
When the alarm triggers, the app resets and you can adjust the HR setting again to a lower value and/or restart.
|
|
@ -0,0 +1 @@
|
|||
E.toArrayBuffer(atob("MDABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAYAA8AAAA8AA8ABgB8AA8ADwD4AA8AH4HwAA8AP8PgAA8Af+fAAA8A//+AAA8B+P8AAA8D4H4AAA8HwDwAAA8PgBgAAA8PAAAAAA8GAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA/////+AA//////AAf/////AAP////+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"))
|
After Width: | Height: | Size: 821 B |
|
@ -0,0 +1,149 @@
|
|||
//ADVANCED SETTINGS
|
||||
var lower_limit_BPM = 49;
|
||||
var upper_limit_BPM = 140;
|
||||
var deviation_threshold = 3;
|
||||
|
||||
var target_heartrate = 70;
|
||||
var heartrate_set;
|
||||
|
||||
var currentBPM;
|
||||
var lastBPM;
|
||||
var firstBPM = true; // first reading since sensor turned on
|
||||
var HR_samples = [];
|
||||
var trigger_count = 0;
|
||||
var file = require("Storage").open("steel_log.csv","a");
|
||||
var launchtime;
|
||||
var alarm_length;
|
||||
|
||||
function btn1Pressed() {
|
||||
if(!heartrate_set){
|
||||
target_heartrate++;
|
||||
update_target_HR();
|
||||
}
|
||||
}
|
||||
|
||||
function btn2Pressed() {
|
||||
heartrate_set = true;
|
||||
Bangle.setHRMPower(1);
|
||||
launchtime = 0|getTime();
|
||||
g.clear();
|
||||
g.setFont("6x8",2);
|
||||
g.drawString("tracking HR...", 120,120);
|
||||
g.setFont("6x8",3);
|
||||
}
|
||||
|
||||
function update_target_HR(){
|
||||
|
||||
g.clear();
|
||||
g.setColor("#00ff7f");
|
||||
g.setFont("6x8", 4);
|
||||
g.setFontAlign(0,0); // center font
|
||||
|
||||
g.drawString(target_heartrate, 120,120);
|
||||
g.setFont("6x8", 2);
|
||||
g.setFontAlign(-1,-1);
|
||||
g.drawString("-", 220, 200);
|
||||
g.drawString("+", 220, 40);
|
||||
g.drawString("GO", 210, 120);
|
||||
|
||||
g.setColor("#ffffff");
|
||||
g.setFontAlign(0,0); // center font
|
||||
g.drawString("target HR", 120,90);
|
||||
|
||||
g.setFont("6x8", 1);
|
||||
g.drawString("if unsure, start with 7-10%\n less than waking average and\n adjust as required", 120,170);
|
||||
|
||||
g.setFont("6x8",3);
|
||||
g.flip();
|
||||
}
|
||||
|
||||
function btn3Pressed(){
|
||||
if(!heartrate_set){
|
||||
target_heartrate--;
|
||||
update_target_HR();
|
||||
}
|
||||
}
|
||||
|
||||
function alarm(){
|
||||
if(alarm_length > 0){
|
||||
//Bangle.beep(500,4000);
|
||||
Bangle.buzz(500,1);
|
||||
alarm_length--;
|
||||
}
|
||||
else{
|
||||
clearInterval(alarm);
|
||||
if(trigger_count > 1)
|
||||
Bangle.setHRMPower(0);
|
||||
}
|
||||
}
|
||||
|
||||
function average(nums) {
|
||||
return nums.reduce((a, b) => (a + b)) / nums.length;
|
||||
}
|
||||
|
||||
function getStandardDeviation (array) {
|
||||
const n = array.length;
|
||||
const mean = array.reduce((a, b) => a + b) / n;
|
||||
return Math.sqrt(array.map(x => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / n);
|
||||
}
|
||||
|
||||
function checkHR() {
|
||||
var bpm = currentBPM, isCurrent = true;
|
||||
if (bpm===undefined) {
|
||||
bpm = lastBPM;
|
||||
isCurrent = false;
|
||||
}
|
||||
if (bpm===undefined || bpm < lower_limit_BPM || bpm > upper_limit_BPM)
|
||||
bpm = "--";
|
||||
if (bpm != "--"){
|
||||
HR_samples.push(bpm);
|
||||
// Terminal.println(bpm);
|
||||
}
|
||||
|
||||
if(HR_samples.length == 5){
|
||||
g.clear();
|
||||
average_HR = average(HR_samples).toFixed(0);
|
||||
stdev_HR = getStandardDeviation (HR_samples).toFixed(1);
|
||||
|
||||
g.drawString("HR: " + average_HR, 120,100);
|
||||
g.drawString("STDEV: " + stdev_HR, 120,160);
|
||||
HR_samples = [];
|
||||
if(average_HR < target_heartrate && stdev_HR < deviation_threshold){
|
||||
|
||||
Bangle.setHRMPower(0);
|
||||
alarm_length = 4;
|
||||
setInterval(alarm, 2000);
|
||||
|
||||
trigger_count++;
|
||||
var csv = [
|
||||
0|getTime(),
|
||||
launchtime,
|
||||
average_HR,
|
||||
stdev_HR
|
||||
];
|
||||
file.write(csv.join(",")+"\n");
|
||||
|
||||
heartrate_set = false;
|
||||
update_target_HR();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
update_target_HR();
|
||||
|
||||
setWatch(btn1Pressed, BTN1, {repeat:true});
|
||||
setWatch(btn2Pressed, BTN2, {repeat:true});
|
||||
setWatch(btn3Pressed, BTN3, {repeat:true});
|
||||
|
||||
Bangle.on('HRM',function(hrm) {
|
||||
|
||||
if(trigger_count < 2){
|
||||
if (firstBPM)
|
||||
firstBPM=false; // ignore the first one as it's usually rubbish
|
||||
else {
|
||||
currentBPM = hrm.bpm;
|
||||
lastBPM = currentBPM;
|
||||
}
|
||||
checkHR();
|
||||
}
|
||||
});
|
|
@ -17,4 +17,4 @@
|
|||
Ensure default time period is 10
|
||||
0.14: Now use the openstmap lib for map plotting
|
||||
0.15: Add plotTrack method to allow current track to be plotted on a map (#395)
|
||||
Add gpsrec app to Settings menu
|
||||
0.16: Add gpsrec app to Settings menu
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
0.01: Add a number to match to turn off alarm
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwkE5gA/AF9cBZXFQYIOGBIUMC5PATgQJFqAIBgovMBwISDAYQ5HGBAAGFxQ/FgMzmcgJ5BIKgAXFIxYuBgMxCIMjmcQgECiBHLFwITBFYIvBiBLBmQwLqECCYMziICBmIeBD4IwKFwQAIGAJGJRQUhgIbDAocQJBHAgYlCOQIrDAoUwhhGIiZZBX4gFEAgIXICIMwX4gFFC45eDF5RgI4pZHAo0gC43AXoaPJC4J4GRwQAMSA4XfmKuBC6kQRYQXMO4YACXYYADO46nDIwcCiBIFU47XDBwcTkBQFa4/MH4sAkYxBBAoWGC4I/DmQ1BdwJPEC5CQEmAECGQKOKMA0gCYRgELxBICCYUyFQZgCJgIWI5lQYIxjCGYMFC5PFLAiKDFwRGJGATCFLoYuKGAYYGiAuMDAkiCoMiCx6SCAAq7IF48F4tQCoMFqAXP4AQF4B1MSAZXFMwIXPA41VC5wA/ADAA=="))
|
|
@ -0,0 +1,112 @@
|
|||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
|
||||
var alarms = require("Storage").readJSON("hardalarm.json",1)||[];
|
||||
/*alarms = [
|
||||
{ on : true,
|
||||
hr : 6.5, // hours + minutes/60
|
||||
msg : "Eat chocolate",
|
||||
last : 0, // last day of the month we alarmed on - so we don't alarm twice in one day!
|
||||
rp : true, // repeat
|
||||
as : false, // auto snooze
|
||||
}
|
||||
];*/
|
||||
|
||||
function formatTime(t) {
|
||||
var hrs = 0|t;
|
||||
var mins = Math.round((t-hrs)*60);
|
||||
return hrs+":"+("0"+mins).substr(-2);
|
||||
}
|
||||
|
||||
function getCurrentHr() {
|
||||
var time = new Date();
|
||||
return time.getHours()+(time.getMinutes()/60)+(time.getSeconds()/3600);
|
||||
}
|
||||
|
||||
function showMainMenu() {
|
||||
const menu = {
|
||||
'': { 'title': 'Alarms' },
|
||||
'New Alarm': ()=>editAlarm(-1)
|
||||
};
|
||||
alarms.forEach((alarm,idx)=>{
|
||||
txt = (alarm.on?"on ":"off ")+formatTime(alarm.hr);
|
||||
if (alarm.rp) txt += " (repeat)";
|
||||
menu[txt] = function() {
|
||||
editAlarm(idx);
|
||||
};
|
||||
});
|
||||
menu['< Back'] = ()=>{load();};
|
||||
return E.showMenu(menu);
|
||||
}
|
||||
|
||||
function editAlarm(alarmIndex) {
|
||||
var newAlarm = alarmIndex<0;
|
||||
var hrs = 12;
|
||||
var mins = 0;
|
||||
var en = true;
|
||||
var repeat = true;
|
||||
var as = false;
|
||||
if (!newAlarm) {
|
||||
var a = alarms[alarmIndex];
|
||||
hrs = 0|a.hr;
|
||||
mins = Math.round((a.hr-hrs)*60);
|
||||
en = a.on;
|
||||
repeat = a.rp;
|
||||
as = a.as;
|
||||
}
|
||||
const menu = {
|
||||
'': { 'title': 'Alarms' },
|
||||
'Hours': {
|
||||
value: hrs,
|
||||
onchange: function(v){if (v<0)v=23;if (v>23)v=0;hrs=v;this.value=v;} // no arrow fn -> preserve 'this'
|
||||
},
|
||||
'Minutes': {
|
||||
value: mins,
|
||||
onchange: function(v){if (v<0)v=59;if (v>59)v=0;mins=v;this.value=v;} // no arrow fn -> preserve 'this'
|
||||
},
|
||||
'Enabled': {
|
||||
value: en,
|
||||
format: v=>v?"On":"Off",
|
||||
onchange: v=>en=v
|
||||
},
|
||||
'Repeat': {
|
||||
value: en,
|
||||
format: v=>v?"Yes":"No",
|
||||
onchange: v=>repeat=v
|
||||
},
|
||||
'Auto snooze': {
|
||||
value: as,
|
||||
format: v=>v?"Yes":"No",
|
||||
onchange: v=>as=v
|
||||
}
|
||||
};
|
||||
function getAlarm() {
|
||||
var hr = hrs+(mins/60);
|
||||
var day = 0;
|
||||
// If alarm is for tomorrow not today (eg, in the past), set day
|
||||
if (hr < getCurrentHr())
|
||||
day = (new Date()).getDate();
|
||||
// Save alarm
|
||||
return {
|
||||
on : en, hr : hr,
|
||||
last : day, rp : repeat, as: as
|
||||
};
|
||||
}
|
||||
menu["> Save"] = function() {
|
||||
if (newAlarm) alarms.push(getAlarm());
|
||||
else alarms[alarmIndex] = getAlarm();
|
||||
require("Storage").write("hardalarm.json",JSON.stringify(alarms));
|
||||
showMainMenu();
|
||||
};
|
||||
if (!newAlarm) {
|
||||
menu["> Delete"] = function() {
|
||||
alarms.splice(alarmIndex,1);
|
||||
require("Storage").write("hardalarm.json",JSON.stringify(alarms));
|
||||
showMainMenu();
|
||||
};
|
||||
}
|
||||
menu['< Back'] = showMainMenu;
|
||||
return E.showMenu(menu);
|
||||
}
|
||||
|
||||
showMainMenu();
|
After Width: | Height: | Size: 1.9 KiB |
|
@ -0,0 +1,25 @@
|
|||
// check for alarms
|
||||
(function() {
|
||||
var alarms = require('Storage').readJSON('hardalarm.json',1)||[];
|
||||
var time = new Date();
|
||||
var active = alarms.filter(a=>a.on);
|
||||
if (active.length) {
|
||||
active = active.sort((a,b)=>(a.hr-b.hr)+(a.last-b.last)*24);
|
||||
var hr = time.getHours()+(time.getMinutes()/60)+(time.getSeconds()/3600);
|
||||
if (!require('Storage').read("hardalarm.js")) {
|
||||
console.log("No alarm app!");
|
||||
require('Storage').write('hardalarm.json',"[]");
|
||||
} else {
|
||||
var t = 3600000*(active[0].hr-hr);
|
||||
if (active[0].last == time.getDate() || t < 0) t += 86400000;
|
||||
if (t<1000) t=1000;
|
||||
/* execute alarm at the correct time. We avoid execing immediately
|
||||
since this code will get called AGAIN when hardalarm.js is loaded. alarm.js
|
||||
will then clearInterval() to get rid of this call so it can proceed
|
||||
normally. */
|
||||
setTimeout(function() {
|
||||
load("hardalarm.js");
|
||||
},t);
|
||||
}
|
||||
}
|
||||
})();
|
|
@ -0,0 +1,127 @@
|
|||
// Chances are boot0.js got run already and scheduled *another*
|
||||
// 'load(hardalarm.js)' - so let's remove it first!
|
||||
clearInterval();
|
||||
|
||||
function formatTime(t) {
|
||||
var hrs = 0|t;
|
||||
var mins = Math.round((t-hrs)*60);
|
||||
return hrs+":"+("0"+mins).substr(-2);
|
||||
}
|
||||
|
||||
function getCurrentHr() {
|
||||
var time = new Date();
|
||||
return time.getHours()+(time.getMinutes()/60)+(time.getSeconds()/3600);
|
||||
}
|
||||
|
||||
function getRandomInt(max) {
|
||||
return Math.floor(Math.random() * Math.floor(max));
|
||||
}
|
||||
|
||||
function getRandomFromRange(lowerRangeMin, lowerRangeMax, higherRangeMin, higherRangeMax) {
|
||||
var lowerRange = lowerRangeMax - lowerRangeMin;
|
||||
var higherRange = higherRangeMax - higherRangeMin;
|
||||
var fullRange = lowerRange + higherRange;
|
||||
var randomNum = getRandomInt(fullRange);
|
||||
if(randomNum <= (lowerRangeMax - lowerRangeMin)) {
|
||||
return randomNum + lowerRangeMin;
|
||||
} else {
|
||||
return randomNum + (higherRangeMin - lowerRangeMax);
|
||||
}
|
||||
}
|
||||
|
||||
function showNumberPicker(currentGuess, randomNum) {
|
||||
if(currentGuess == randomNum) {
|
||||
E.showMessage("" + currentGuess + "\n PRESS ENTER", "Get to " + randomNum);
|
||||
} else {
|
||||
E.showMessage("" + currentGuess, "Get to " + randomNum);
|
||||
}
|
||||
}
|
||||
|
||||
function showPrompt(msg, buzzCount, alarm) {
|
||||
E.showPrompt(msg,{
|
||||
title:"STAY AWAKE!",
|
||||
buttons : {"Sleep":0,"Stop":1} // default is sleep so it'll come back in 10 mins
|
||||
}).then(function(choice) {
|
||||
buzzCount = 0;
|
||||
if (choice==0) {
|
||||
if(alarm.ohr===undefined) alarm.ohr = alarm.hr;
|
||||
alarm.hr += 10/60; // 10 minutes
|
||||
require("Storage").write("hardalarm.json",JSON.stringify(alarms));
|
||||
load();
|
||||
} else if(choice==1) {
|
||||
alarm.last = (new Date()).getDate();
|
||||
if (alarm.ohr!==undefined) {
|
||||
alarm.hr = alarm.ohr;
|
||||
delete alarm.ohr;
|
||||
}
|
||||
if (!alarm.rp) alarm.on = false;
|
||||
require("Storage").write("hardalarm.json",JSON.stringify(alarms));
|
||||
load();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showAlarm(alarm) {
|
||||
var msg = formatTime(alarm.hr);
|
||||
var buzzCount = 20;
|
||||
if (alarm.msg)
|
||||
msg += "\n"+alarm.msg;
|
||||
var okClicked = false;
|
||||
var currentGuess = 10;
|
||||
var randomNum = getRandomFromRange(0, 7, 13, 20);
|
||||
showNumberPicker(currentGuess, randomNum)
|
||||
setWatch(o => {
|
||||
if(!okClicked && currentGuess < 20) {
|
||||
currentGuess = currentGuess + 1;
|
||||
showNumberPicker(currentGuess, randomNum);
|
||||
}
|
||||
}, BTN1, {repeat: true, edge: 'rising'});
|
||||
|
||||
setWatch(o => {
|
||||
if(currentGuess == randomNum) {
|
||||
okClicked = true;
|
||||
showPrompt(msg, buzzCount, alarm);
|
||||
}
|
||||
}, BTN2, {repeat: true, edge: 'rising'});
|
||||
|
||||
setWatch(o => {
|
||||
if(!okClicked && currentGuess > 0) {
|
||||
currentGuess = currentGuess - 1;
|
||||
showNumberPicker(currentGuess, randomNum);
|
||||
}
|
||||
}, BTN3, {repeat: true, edge: 'rising'});
|
||||
|
||||
function buzz() {
|
||||
Bangle.buzz(500).then(()=>{
|
||||
setTimeout(()=>{
|
||||
Bangle.buzz(500).then(function() {
|
||||
setTimeout(()=>{
|
||||
Bangle.buzz(2000).then(function() {
|
||||
if (buzzCount--)
|
||||
setTimeout(buzz, 2000);
|
||||
else if(alarm.as) { // auto-snooze
|
||||
buzzCount = 20;
|
||||
setTimeout(buzz, 600000); // 10 minutes
|
||||
}
|
||||
});
|
||||
},100);
|
||||
});
|
||||
},100);
|
||||
});
|
||||
}
|
||||
buzz();
|
||||
}
|
||||
|
||||
// Check for alarms
|
||||
var day = (new Date()).getDate();
|
||||
var hr = getCurrentHr()+10000; // get current time - 10s in future to ensure we alarm if we've started the app a tad early
|
||||
var alarms = require("Storage").readJSON("hardalarm.json",1)||[];
|
||||
var active = alarms.filter(a=>a.on&&(a.hr<hr)&&(a.last!=day));
|
||||
if (active.length) {
|
||||
// if there's an alarm, show it
|
||||
active = active.sort((a,b)=>a.hr-b.hr);
|
||||
showAlarm(active[0]);
|
||||
} else {
|
||||
// otherwise just go back to default app
|
||||
setTimeout(load, 100);
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
(() => {
|
||||
var alarms = require('Storage').readJSON('hardalarm.json',1)||[];
|
||||
alarms = alarms.filter(alarm=>alarm.on);
|
||||
if (!alarms.length) return; // no alarms, no widget!
|
||||
delete alarms;
|
||||
// add the widget
|
||||
WIDGETS["alarm"]={area:"tl",width:24,draw:function() {
|
||||
g.setColor(-1);
|
||||
g.drawImage(atob("GBgBAAAAAAAAABgADhhwDDwwGP8YGf+YMf+MM//MM//MA//AA//AA//AA//AA//AA//AB//gD//wD//wAAAAADwAABgAAAAAAAAA"),this.x,this.y);
|
||||
}};
|
||||
})()
|
|
@ -0,0 +1,13 @@
|
|||
Extract hrm raw signal data to CSV file
|
||||
=======================================
|
||||
|
||||
Simple app that will run the heart rate monitor for a defined period of time you set at the start.
|
||||
|
||||
-The app creates a csv file (it's actually just 1 column) and you can download this via My Apps in the App Loader.
|
||||
|
||||
-The max time value is 60 minutes.
|
||||
|
||||
-The first item holds the data/time when the readings were taken and the file is reset each time the app is run.
|
||||
|
||||
-The hrm sensor is sampled @50Hz and this app does not do any processing on it other than clip overly high/extreme values, the array is written as-is. There is an example Python script that can process this signal, smooth it and also extract a myriad of heart rate variability metrics using the hrvanalysis library:
|
||||
https://github.com/jabituyaben/BangleJS-HRM-Signal-Processing
|
|
@ -0,0 +1 @@
|
|||
E.toArrayBuffer(atob("MDCBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfwAP4AAD/8A//AAH//D//gAP//n//gAf/////AA/////+AA/////8MA/////4cB/////w+B/////B+B////+D+B////8H+B////4P+B////w/+B/+P/h/+B/+H/D/+A/+D+H/8A//h8P/8A//w4f/8Af/4A//4Af/8B//4AP/+D//wAP//H//wAH/////gAD/////AAB////+AAA////+AAAf///8AAAP///wAAAH///gAAAD///AAAAA//+AAAAAf/4AAAAAH/gAAAAAB+AAAAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="))
|
After Width: | Height: | Size: 826 B |
|
@ -0,0 +1,97 @@
|
|||
var counter = 1;
|
||||
var logging_started;
|
||||
var interval;
|
||||
var value;
|
||||
|
||||
var file = require("Storage").open("hrm_log.csv", "w");
|
||||
file.write("");
|
||||
|
||||
file = require("Storage").open("hrm_log.csv", "a");
|
||||
|
||||
function update_timer() {
|
||||
g.clear();
|
||||
g.setColor("#00ff7f");
|
||||
g.setFont("6x8", 4);
|
||||
g.setFontAlign(0, 0); // center font
|
||||
|
||||
g.drawString(counter, 120, 120);
|
||||
g.setFont("6x8", 2);
|
||||
g.setFontAlign(-1, -1);
|
||||
g.drawString("-", 220, 200);
|
||||
g.drawString("+", 220, 40);
|
||||
g.drawString("GO", 210, 120);
|
||||
|
||||
g.setColor("#ffffff");
|
||||
g.setFontAlign(0, 0); // center font
|
||||
g.drawString("Timer (minutes)", 120, 90);
|
||||
|
||||
g.setFont("6x8", 4); // bitmap font, 8x magnified
|
||||
|
||||
if (!logging_started)
|
||||
g.flip();
|
||||
}
|
||||
|
||||
function btn1Pressed() {
|
||||
if (!logging_started) {
|
||||
if (counter < 60)
|
||||
counter += 1;
|
||||
else
|
||||
counter = 1;
|
||||
update_timer();
|
||||
}
|
||||
}
|
||||
|
||||
function btn3Pressed() {
|
||||
if (!logging_started) {
|
||||
if (counter > 1)
|
||||
counter -= 1;
|
||||
else
|
||||
counter = 60;
|
||||
update_timer();
|
||||
}
|
||||
}
|
||||
|
||||
function btn2Pressed() {
|
||||
launchtime = 0 | getTime();
|
||||
file.write(launchtime + "," + "\n");
|
||||
logging_started = true;
|
||||
counter = counter * 60;
|
||||
interval = setInterval(countDown, 1000);
|
||||
Bangle.setHRMPower(1);
|
||||
}
|
||||
|
||||
function fmtMSS(e) {
|
||||
var m = Math.floor(e % 3600 / 60).toString().padStart(2, '0'),
|
||||
s = Math.floor(e % 60).toString().padStart(2, '0');
|
||||
return m + ':' + s;
|
||||
}
|
||||
|
||||
function countDown() {
|
||||
g.clear();
|
||||
counter--;
|
||||
if (counter == 0) {
|
||||
Bangle.setHRMPower(0);
|
||||
clearInterval(interval);
|
||||
g.drawString("Finished", g.getWidth() / 2, g.getHeight() / 2);
|
||||
Bangle.buzz(500, 1);
|
||||
}
|
||||
else
|
||||
g.drawString(fmtMSS(counter), g.getWidth() / 2, g.getHeight() / 2);
|
||||
}
|
||||
|
||||
update_timer();
|
||||
|
||||
setWatch(btn1Pressed, BTN1, { repeat: true });
|
||||
setWatch(btn2Pressed, BTN2, { repeat: true });
|
||||
setWatch(btn3Pressed, BTN3, { repeat: true });
|
||||
|
||||
Bangle.on('HRM', function (hrm) {
|
||||
for (let i = 0; i < hrm.raw.length; i++) {
|
||||
value = hrm.raw[i];
|
||||
if (value < -2)
|
||||
value = -2;
|
||||
if (value > 6)
|
||||
value = 6;
|
||||
file.write(value + "," + "\n");
|
||||
}
|
||||
});
|
|
@ -0,0 +1,54 @@
|
|||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="../../css/spectre.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="data"></div>
|
||||
<button class="btn btn-default" id="btnSave">Save</button>
|
||||
<button class="btn btn-default" id="btnDelete">Delete</button>
|
||||
|
||||
<script src="../../core/lib/interface.js"></script>
|
||||
<script>
|
||||
var dataElement = document.getElementById("data");
|
||||
var csvData = "";
|
||||
|
||||
function getData() {
|
||||
// show loading window
|
||||
Util.showModal("Loading...");
|
||||
// get the data
|
||||
dataElement.innerHTML = "";
|
||||
Util.readStorageFile(`hrm_log.csv`,data=>{
|
||||
csvData = data.trim();
|
||||
// remove window
|
||||
Util.hideModal();
|
||||
// If no data, report it and exit
|
||||
if (data.length==0) {
|
||||
dataElement.innerHTML = "<b>No data found</b>";
|
||||
return;
|
||||
}
|
||||
else{
|
||||
dataElement.innerHTML = "<b>data file found</b>";
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// You can call a utility function to save the data
|
||||
document.getElementById("btnSave").addEventListener("click", function() {
|
||||
Util.saveCSV("HRM_data", csvData);
|
||||
});
|
||||
// Or you can also delete the file
|
||||
document.getElementById("btnDelete").addEventListener("click", function() {
|
||||
Util.showModal("Deleting...");
|
||||
Util.eraseStorageFile("hrm_log.csv", function() {
|
||||
Util.hideModal();
|
||||
getData();
|
||||
});
|
||||
});
|
||||
// Called when app starts
|
||||
function onInit() {
|
||||
getData();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1 @@
|
|||
0.01: Launch app
|
|
@ -0,0 +1,26 @@
|
|||
# Lazy clock
|
||||
|
||||
This clock gives you the time (roughly).
|
||||
|
||||
* 11:05 becomes 'About eleven'
|
||||
* 15:34 becomes 'Just gone half three'
|
||||
* 04:12 becomes 'Around quarter past four'
|
||||
|
||||
Phrases have a 10 min 'resolution':
|
||||
* 10:00 - 10:09: past the hour,
|
||||
* 10:10 - 10:19: about quarter past
|
||||
* 10:20 - 10:29: nearly half past
|
||||
* 10:30 - 10:39: just gone half past
|
||||
* 10:40 - 10:49: about quarter to
|
||||
* 10:50 - 10:59: almost the hour
|
||||
|
||||
Various phrases and combinations for each time chunk are provided.
|
||||
|
||||
## Usage
|
||||
|
||||
* Press BTN-1 to see the actual time and date
|
||||
* Press BTN-3 to cycle through lazy descriptions
|
||||
|
||||
## Attributions
|
||||
|
||||
Icon from https://icons8.com
|
|
@ -0,0 +1,236 @@
|
|||
let secondInterval;
|
||||
let showRealTime = false;
|
||||
|
||||
const utils = {
|
||||
random: function(items) {
|
||||
return items[~~(items.length * Math.random())];
|
||||
},
|
||||
|
||||
oneIn: function(chance) {
|
||||
return Math.floor(Math.random() * Math.floor(chance + 1)) === chance;
|
||||
},
|
||||
|
||||
hours2Word: function(hours, minutes) {
|
||||
const numbers = [
|
||||
'twelve',
|
||||
'one',
|
||||
'two',
|
||||
'three',
|
||||
'four',
|
||||
'five',
|
||||
'six',
|
||||
'seven',
|
||||
'eight',
|
||||
'nine',
|
||||
'ten',
|
||||
'eleven',
|
||||
'twelve',
|
||||
];
|
||||
|
||||
let adjustedHours = hours;
|
||||
|
||||
if (minutes > 40) {
|
||||
adjustedHours += 1;
|
||||
}
|
||||
|
||||
if (adjustedHours > 12) {
|
||||
adjustedHours -= 12;
|
||||
}
|
||||
|
||||
return numbers[adjustedHours];
|
||||
},
|
||||
|
||||
print: function(str) {
|
||||
let fontSize = 4;
|
||||
const width = g.getWidth();
|
||||
const height = g.getHeight() - 48;
|
||||
const lines = str.split(`\n`).length;
|
||||
let totalHeight;
|
||||
|
||||
do {
|
||||
g.setFont("6x8", fontSize);
|
||||
totalHeight = g.getFontHeight() * lines;
|
||||
if (fontSize === 1 || (g.stringWidth(str) < width && totalHeight < height)) {
|
||||
break;
|
||||
}
|
||||
fontSize--;
|
||||
|
||||
} while (true);
|
||||
|
||||
const x = width / 2;
|
||||
|
||||
const y = (g.getHeight() / 2) - (g.getFontHeight() * ((lines - 1) / 2));
|
||||
g.drawString(str, x, y < 25 ? 24 : y);
|
||||
}
|
||||
};
|
||||
|
||||
const words = {
|
||||
approx: ['\'Bout', 'About', 'Around', `Summat\nlike`, 'Near', 'Close to'],
|
||||
approach: ['Nearly', `Coming\nup to`, 'Approaching', `A touch\nbefore`],
|
||||
past: [`A shade\nafter`, `A whisker\nafter`, 'Just gone'],
|
||||
quarter: ['Quarter', `Fifteen\nminutes`],
|
||||
half: ['Half', 'Half past'],
|
||||
exactly: ['exactly', 'on the dot', 'o\' clock'],
|
||||
ish: ['-ish', `\n(ish)`]
|
||||
};
|
||||
|
||||
function switchMode() {
|
||||
showRealTime = !showRealTime;
|
||||
refreshTime();
|
||||
}
|
||||
|
||||
function drawRealTime(date) {
|
||||
const pad = (number) => `0${number}`.substr(-2);
|
||||
const hours = pad(date.getHours());
|
||||
const minutes = pad(date.getMinutes());
|
||||
|
||||
g.setFontAlign(0,1);
|
||||
g.setFont("6x8", 8);
|
||||
g.drawString(`${hours}:${minutes}`, g.getWidth() / 2, g.getHeight() / 2);
|
||||
|
||||
g.setFont("6x8", 3);
|
||||
g.setFontAlign(0, -1);
|
||||
g.drawString(date.toISOString().split('T')[0], g.getWidth() / 2, g.getHeight() / 2);
|
||||
|
||||
}
|
||||
|
||||
function drawDumbTime(time) {
|
||||
const hours = time.getHours();
|
||||
const minutes = time.getMinutes();
|
||||
|
||||
function formatTime(hours, minutes) {
|
||||
const makeApprox = (str, template) => {
|
||||
let _template = template || 'approx';
|
||||
if (utils.oneIn(2)) {
|
||||
_template = 'approx';
|
||||
|
||||
if (utils.oneIn(words.approx.length)) {
|
||||
const ish = utils.random(words.ish);
|
||||
return `${str}${ish}`;
|
||||
}
|
||||
}
|
||||
|
||||
const approx = `${utils.random(words[_template])} `;
|
||||
|
||||
return `${approx}\n${str.toLowerCase()}`;
|
||||
};
|
||||
|
||||
const formatters = {
|
||||
'onTheHour': (hoursAsWord) => {
|
||||
const exactly = utils.random(words.exactly);
|
||||
|
||||
return `${hoursAsWord}\n${exactly}`;
|
||||
},
|
||||
'nearTheHour': (hoursAsWord) => {
|
||||
const template = (minutes < 10) ? 'past' : 'approach';
|
||||
|
||||
return makeApprox(hoursAsWord, template);
|
||||
},
|
||||
'nearQuarter': (hoursAsWord, minutes) => {
|
||||
const direction = (minutes > 30) ? 'to' : 'past';
|
||||
const quarter = utils.random(words.quarter);
|
||||
|
||||
const formatted = `${quarter} ${direction}\n${hoursAsWord}`;
|
||||
|
||||
return (minutes === 15 || minutes === 45) ? formatted : makeApprox(formatted);
|
||||
},
|
||||
'nearHalf': (hoursAsWord, minutes) => {
|
||||
const half = utils.random(words.half);
|
||||
|
||||
const formatted = `${half}\n${hoursAsWord}`;
|
||||
|
||||
const template = (minutes > 30) ? 'past' : 'approach';
|
||||
return (minutes === 30) ? formatted : makeApprox(formatted, template);
|
||||
},
|
||||
};
|
||||
|
||||
function getFormatter(hours, minutes) {
|
||||
if (minutes === 0) {
|
||||
return formatters.onTheHour;
|
||||
} else if (minutes > 50 || minutes < 10) {
|
||||
return formatters.nearTheHour;
|
||||
} else if (minutes > 40|| minutes < 20) {
|
||||
return formatters.nearQuarter;
|
||||
} else {
|
||||
return formatters.nearHalf;
|
||||
}
|
||||
}
|
||||
|
||||
const hoursAsWord = utils.hours2Word(hours, minutes);
|
||||
|
||||
const formatter = getFormatter(hours, minutes);
|
||||
|
||||
return formatter(hoursAsWord, minutes);
|
||||
}
|
||||
|
||||
utils.print(formatTime(hours, minutes));
|
||||
}
|
||||
|
||||
function cancelTimeout() {
|
||||
if (secondInterval) {
|
||||
clearTimeout(secondInterval);
|
||||
}
|
||||
|
||||
secondInterval = undefined;
|
||||
}
|
||||
|
||||
function refreshTime() {
|
||||
cancelTimeout();
|
||||
|
||||
g.clearRect(0, 24, g.getWidth(), g.getHeight()-24);
|
||||
g.reset();
|
||||
g.setFontAlign(0,0);
|
||||
|
||||
const time = new Date();
|
||||
|
||||
const method = showRealTime ? drawRealTime : drawDumbTime;
|
||||
|
||||
method(time);
|
||||
|
||||
const secondsTillRefresh = 60 - time.getSeconds();
|
||||
|
||||
secondInterval = setTimeout(refreshTime, secondsTillRefresh * 1000);
|
||||
}
|
||||
|
||||
|
||||
function startClock() {
|
||||
const secondsToRefresh = refreshTime();
|
||||
}
|
||||
|
||||
function addEvents() {
|
||||
Bangle.on('lcdPower', (on) => {
|
||||
cancelTimeout();
|
||||
if (on) {
|
||||
startClock();
|
||||
}
|
||||
});
|
||||
|
||||
setWatch(switchMode, BTN1, {
|
||||
repeat: true,
|
||||
edge: "falling"
|
||||
});
|
||||
|
||||
setWatch(Bangle.showLauncher, BTN2, {
|
||||
repeat: false,
|
||||
edge: "falling"
|
||||
});
|
||||
|
||||
|
||||
setWatch(refreshTime, BTN3, {
|
||||
repeat: true,
|
||||
edge: "falling"
|
||||
});
|
||||
}
|
||||
|
||||
function init() {
|
||||
g.clear();
|
||||
|
||||
startClock();
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
|
||||
addEvents();
|
||||
}
|
||||
|
||||
|
||||
init();
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwhC/AH4A1iIXXDCwXYF1kBIzEQXlfdC9sNC6oWB6BFVFy5dWIqoXV93u8ArThwXB9wLHhGIAAeAFwwwJCwgABC54uFC5XoHQoXQnBTFCwwkFlwXClAjFFhI7CwYWB8YOFRpYMCkfjFwQPDLpYMHABQwFC6IwETQ4TIGAwXOCQIQDIyIRFGARyRGAQXQRIwXCDoTGGXJAKBeQwA/AH4A6A"))
|
After Width: | Height: | Size: 1.2 KiB |
|
@ -5,6 +5,8 @@
|
|||
0.05: Add README
|
||||
0.06: Add txt clock
|
||||
0.07: Add Time Date clock and fix font sizes
|
||||
0.08: Add pinned clock face
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
(function(){return function(){function e(a,d,b,c){a*=k;g.fillPoly([120+Math.sin(a)*d,134-Math.cos(a)*d,120+Math.sin(a+h)*c,134-Math.cos(a+h)*c,120+Math.sin(a)*b,134-Math.cos(a)*b,120+Math.sin(a-h)*c,134-Math.cos(a-h)*c])}function l(){g.setColor(0,0,0);e(360*f.getSeconds()/60,-5,90,3);0===f.getSeconds()&&(e(360*(d.getHours()+d.getMinutes()/60)/12,-16,60,7),e(360*d.getMinutes()/60,-16,86,7),d=new Date);g.setColor(1,1,1);e(360*(d.getHours()+d.getMinutes()/60)/12,-16,60,7);e(360*d.getMinutes()/60,-16,
|
||||
86,7);g.setColor(0,1,1);f=new Date;e(360*f.getSeconds()/60,-5,90,3);g.setColor(0,0,0);g.fillCircle(120,134,2)}var h=Math.PI/2,k=Math.PI/180,d,f;return{init:function(){f=d=new Date;g.setColor(1,1,1);for(var a=0;60>a;a++){var e=360*a/60,b=e*k,c=120+100*Math.sin(b);b=134-100*Math.cos(b);0==e%90?(g.setColor(0,1,1),g.fillRect(c-6,b-6,c+6,b+6)):0==e%30?(g.setColor(0,1,1),g.fillRect(c-4,b-4,c+4,b+4)):(g.setColor(1,1,1),g.fillRect(c-1,b-1,c+1,b+1))}l()},tick:l}}})();
|
|
@ -1,6 +1,8 @@
|
|||
var FACES = [];
|
||||
var iface = 0;
|
||||
require("Storage").list(/\.face\.js$/).forEach(face=>FACES.push(eval(require("Storage").read(face))));
|
||||
var STOR = require("Storage");
|
||||
STOR.list(/\.face\.js$/).forEach(face=>FACES.push(eval(require("Storage").read(face))));
|
||||
var lastface = STOR.readJSON("multiclock.json")||{pinned:0};
|
||||
var iface = lastface.pinned;
|
||||
var face = FACES[iface]();
|
||||
var intervalRefSec;
|
||||
|
||||
|
@ -25,7 +27,14 @@ function setButtons(){
|
|||
face = FACES[iface]();
|
||||
startdraw();
|
||||
}
|
||||
setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"});
|
||||
function finish(){
|
||||
if (lastface.pinned!=iface){
|
||||
lastface.pinned=iface;
|
||||
STOR.write("multiclock.json",lastface);
|
||||
}
|
||||
Bangle.showLauncher();
|
||||
}
|
||||
setWatch(finish, BTN2, {repeat:false,edge:"falling"});
|
||||
setWatch(newFace.bind(null,1), BTN1, {repeat:true,edge:"rising"});
|
||||
setWatch(newFace.bind(null,-1), BTN3, {repeat:true,edge:"rising"});
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ This app allows you to remotely control devices (or anything else you like!) wit
|
|||
First, you'll need a device that supports BLE.
|
||||
|
||||
Install EspruinoHub following the directions at [https://github.com/espruino/EspruinoHub](https://github.com/espruino/EspruinoHub)
|
||||
|
||||
Install [Node-RED](https://nodered.org/docs/getting-started)
|
||||
|
||||
## Example Node-RED flow
|
||||
|
|