🔀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>
pull/632/head
OmegaRogue 2021-01-13 14:47:50 +01:00 committed by GitHub
parent 6f26bf47b5
commit e9c7e2ede7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 1866 additions and 18 deletions

108
apps.json
View File

@ -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}
]
}
]

3
apps/HRV/ChangeLog Normal file
View File

@ -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

18
apps/HRV/README.md Normal file
View File

@ -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 dont look right, try repositioning the watch and try again - you can use the HR monitor app to confirm fitting.

1
apps/HRV/app-icon.js Normal file
View File

@ -0,0 +1 @@
E.toArrayBuffer(atob("MDABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAYAA8AAAA8AA8ABgB8AA8ADwD4AA8AH4HwAA8AP8PgAA8Af+fAAA8A//+AAA8B+P8AAA8D4H4AAA8HwDwAAA8PgBgAAA8PAAAAAA8GAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA/////+AA//////AAf/////AAP////+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"))

303
apps/HRV/app.js Normal file
View File

@ -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++;
}
});

BIN
apps/HRV/hrv.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 749 B

View File

@ -1,4 +1,6 @@
0.01: New Widget!
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.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()

View File

@ -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};
})();

View File

@ -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>

View File

@ -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);

View File

@ -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 };

12
apps/breath/README.md Normal file
View File

@ -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

1
apps/breath/app-icon.js Normal file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwxH+AH4A/AH4AFqoAFF94zoF5QxkF5gxiF/t5F92k5wvt5AvtvXOAAIvqvJeBF9i9BAAYvoFwovnvQuGF816XYYwiFw4tIF8qMHF86NJF8iOLGDqOMztV0YvOxIABF5qIB0l6RxFPp1VMBgtCGB4eDvTtH0dPL4gwHFQYwPXBjBTGBwvVYTIv/FyAviFpYuOF6bsOF8wqFFpxffACIuuF7llL99lGKKPeGKAvgGBzufF6XHAAPNFzIvUAAZqEFqC/SF44AkF/4A/AH4A/ADIA="))

BIN
apps/breath/app-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

222
apps/breath/app.js Normal file
View File

@ -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;
}
});

View File

@ -0,0 +1 @@
{"period":2,"exhale_pause":1,"inhale_pause":1,"colour":["#00ff7f","green"],"vibrate":"forward","ex_in_ratio":"5:6"}

4
apps/dtlaunch/ChangeLog Normal file
View File

@ -0,0 +1,4 @@
0.01: Initial version
0.02: Multiple pages
0.03: cycle thru pages

16
apps/dtlaunch/README.md Normal file
View File

@ -0,0 +1,16 @@
# Desktop style App Launcher
![](screenshot.jpg)
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

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwhC/AH4ATxAAQC+2N7vd7AX/C/6/7a/4X/a/4X/C/4X/C/4Xfl3iC6vu9wXtI653WAH4A/ABg"))

73
apps/dtlaunch/app.js Normal file
View File

@ -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);

BIN
apps/dtlaunch/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

@ -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 youve 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.

View File

@ -0,0 +1 @@
E.toArrayBuffer(atob("MDABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAYAA8AAAA8AA8ABgB8AA8ADwD4AA8AH4HwAA8AP8PgAA8Af+fAAA8A//+AAA8B+P8AAA8D4H4AAA8HwDwAAA8PgBgAAA8PAAAAAA8GAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA/////+AA//////AAf/////AAP////+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"))

Binary file not shown.

After

Width:  |  Height:  |  Size: 821 B

149
apps/edisonsball/app.js Normal file
View File

@ -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();
}
});

View File

@ -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

1
apps/hardalarm/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: Add a number to match to turn off alarm

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwkE5gA/AF9cBZXFQYIOGBIUMC5PATgQJFqAIBgovMBwISDAYQ5HGBAAGFxQ/FgMzmcgJ5BIKgAXFIxYuBgMxCIMjmcQgECiBHLFwITBFYIvBiBLBmQwLqECCYMziICBmIeBD4IwKFwQAIGAJGJRQUhgIbDAocQJBHAgYlCOQIrDAoUwhhGIiZZBX4gFEAgIXICIMwX4gFFC45eDF5RgI4pZHAo0gC43AXoaPJC4J4GRwQAMSA4XfmKuBC6kQRYQXMO4YACXYYADO46nDIwcCiBIFU47XDBwcTkBQFa4/MH4sAkYxBBAoWGC4I/DmQ1BdwJPEC5CQEmAECGQKOKMA0gCYRgELxBICCYUyFQZgCJgIWI5lQYIxjCGYMFC5PFLAiKDFwRGJGATCFLoYuKGAYYGiAuMDAkiCoMiCx6SCAAq7IF48F4tQCoMFqAXP4AQF4B1MSAZXFMwIXPA41VC5wA/ADAA=="))

112
apps/hardalarm/app.js Normal file
View File

@ -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();

BIN
apps/hardalarm/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

25
apps/hardalarm/boot.js Normal file
View File

@ -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);
}
}
})();

127
apps/hardalarm/hardalarm.js Normal file
View File

@ -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);
}

11
apps/hardalarm/widget.js Normal file
View File

@ -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);
}};
})()

13
apps/hrrawexp/README.md Normal file
View File

@ -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

View File

@ -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=="))

BIN
apps/hrrawexp/app-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 826 B

97
apps/hrrawexp/app.js Normal file
View File

@ -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");
}
});

View File

@ -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>

1
apps/lazyclock/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: Launch app

26
apps/lazyclock/README.md Normal file
View File

@ -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

View File

@ -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();

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwhC/AH4A1iIXXDCwXYF1kBIzEQXlfdC9sNC6oWB6BFVFy5dWIqoXV93u8ArThwXB9wLHhGIAAeAFwwwJCwgABC54uFC5XoHQoXQnBTFCwwkFlwXClAjFFhI7CwYWB8YOFRpYMCkfjFwQPDLpYMHABQwFC6IwETQ4TIGAwXOCQIQDIyIRFGARyRGAQXQRIwXCDoTGGXJAKBeQwA/AH4A6A"))

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -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

View File

@ -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}}})();

View File

@ -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"});
}

View File

@ -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