Merge branch 'master' of github.com:espruino/BangleApps into pushups

master
frederic wagner 2024-01-22 09:32:00 +01:00
commit f0c7d311c7
47 changed files with 763 additions and 78 deletions

View File

@ -21,7 +21,6 @@
'< Back': back,
'Full Screen': {
value: settings.fullscreen,
format: () => (settings.fullscreen ? 'Yes' : 'No'),
onchange: () => {
settings.fullscreen = !settings.fullscreen;
save();

View File

@ -11,7 +11,6 @@
'< Back': back,
'Buzz': {
value: "buzz" in settings ? settings.buzz : false,
format: () => (settings.buzz ? 'Yes' : 'No'),
onchange: () => {
settings.buzz = !settings.buzz;
save('buzz', settings.buzz);

View File

@ -1,2 +1,3 @@
0.01: Added app
0.02: Removed unneeded squares
0.03: Added settings with fullscreen option

View File

@ -1,3 +1,7 @@
var settings = Object.assign({
fullscreen: false,
}, require('Storage').readJSON("binaryclk.json", true) || {});
function draw() {
var dt = new Date();
var h = dt.getHours(), m = dt.getMinutes();
@ -11,10 +15,14 @@ function draw() {
g.clearRect(Bangle.appRect);
let i = 0;
var gap = 8;
var mgn = 20;
if (settings.fullscreen) {
gap = 12;
mgn = 0;
}
const sq = 29;
const gap = 8;
const mgn = 20;
const pos = sq + gap;
var pos = sq + gap;
for (let r = 3; r >= 0; r--) {
for (let c = 0; c < 4; c++) {
@ -26,14 +34,15 @@ function draw() {
}
i++;
}
g.clearRect(mgn/2 + gap, mgn + gap, mgn/2 + gap + sq, mgn + 2 * gap + 2 * sq);
g.clearRect(mgn/2 + 3 * gap + 2 * sq, mgn + gap, mgn/2 + 3 * gap + 3 * sq, mgn + gap + sq);
g.clearRect(mgn/2 + gap, mgn + gap, mgn/2 + gap + sq, mgn + 2 * gap + 2 * sq);
g.clearRect(mgn/2 + 3 * gap + 2 * sq, mgn + gap, mgn/2 + 3 * gap + 3 * sq, mgn + gap + sq);
}
g.clear();
draw();
var secondInterval = setInterval(draw, 60000);
Bangle.setUI("clock");
Bangle.loadWidgets();
Bangle.drawWidgets();
if (!settings.fullscreen) {
Bangle.loadWidgets();
Bangle.drawWidgets();
}

View File

@ -1,7 +1,7 @@
{
"id": "binaryclk",
"name": "Bin Clock",
"version": "0.02",
"version": "0.03",
"description": "Clock face to show binary time in 24 hr format",
"icon": "app-icon.png",
"screenshots": [{"url":"screenshot.png"}],
@ -11,6 +11,8 @@
"allow_emulator": true,
"storage": [
{"name":"binaryclk.app.js","url":"app.js"},
{"name":"binaryclk.settings.js","url":"settings.js"},
{"name":"binaryclk.img","url":"app-icon.js","evaluate":true}
]
],
"data": [{"name":"binaryclk.json"}]
}

View File

@ -0,0 +1,22 @@
(function(back) {
var FILE = "binaryclk.json";
var settings = Object.assign({
fullscreen: false,
}, require('Storage').readJSON(FILE, true) || {});
function writeSettings() {
require('Storage').writeJSON(FILE, settings);
}
E.showMenu({
"" : { "title" : "Bin Clock" },
"< Back" : () => back(),
'Fullscreen': {
value: settings.fullscreen,
onchange: v => {
settings.fullscreen = v;
writeSettings();
}
},
});
})

View File

@ -32,7 +32,6 @@
},
'Show Lock': {
value: settings.showLock,
format: () => (settings.showLock ? 'Yes' : 'No'),
onchange: () => {
settings.showLock = !settings.showLock;
save();
@ -40,7 +39,6 @@
},
'Hide Colon': {
value: settings.hideColon,
format: () => (settings.hideColon ? 'Yes' : 'No'),
onchange: () => {
settings.hideColon = !settings.hideColon;
save();

View File

@ -32,7 +32,6 @@
},
'Show Lock': {
value: settings.showLock,
format: () => (settings.showLock ? 'Yes' : 'No'),
onchange: () => {
settings.showLock = !settings.showLock;
save();
@ -40,7 +39,6 @@
},
'Hide Colon': {
value: settings.hideColon,
format: () => (settings.hideColon ? 'Yes' : 'No'),
onchange: () => {
settings.hideColon = !settings.hideColon;
save();

View File

@ -17,3 +17,4 @@
0.15: Edit holidays on device in settings
0.16: Add menu to fast open settings to edit holidays
Display Widgets in menus
0.17: Load holidays before events so the latter is not overpainted

View File

@ -43,24 +43,24 @@ const dowLbls = function() {
}();
const loadEvents = () => {
// add holidays & other events
events = (require("Storage").readJSON("calendar.days.json",1) || []).map(d => {
const date = new Date(d.date);
const o = {date: date, msg: d.name, type: d.type};
if (d.repeat) {
o.repeat = d.repeat;
}
return o;
});
// all alarms that run on a specific date
events = (require("Storage").readJSON("sched.json",1) || []).filter(a => a.on && a.date).map(a => {
events = events.concat((require("Storage").readJSON("sched.json",1) || []).filter(a => a.on && a.date).map(a => {
const date = new Date(a.date);
const time = timeutils.decodeTime(a.t);
date.setHours(time.h);
date.setMinutes(time.m);
date.setSeconds(time.s);
return {date: date, msg: a.msg, type: "e"};
});
// add holidays & other events
(require("Storage").readJSON("calendar.days.json",1) || []).forEach(d => {
const date = new Date(d.date);
const o = {date: date, msg: d.name, type: d.type};
if (d.repeat) {
o.repeat = d.repeat;
}
events.push(o);
});
}));
};
const loadSettings = () => {
@ -280,14 +280,12 @@ const showMenu = function() {
setUI();
},
/*LANG*/"Exit": () => load(),
/*LANG*/"Settings": () => {
const appSettings = eval(require('Storage').read('calendar.settings.js'));
appSettings(() => {
/*LANG*/"Settings": () =>
eval(require('Storage').read('calendar.settings.js'))(() => {
loadSettings();
loadEvents();
showMenu();
});
},
}),
};
if (require("Storage").read("alarm.app.js")) {
menu[/*LANG*/"Launch Alarms"] = () => {

View File

@ -1,7 +1,7 @@
{
"id": "calendar",
"name": "Calendar",
"version": "0.16",
"version": "0.17",
"description": "Monthly calendar, displays holidays uploaded from the web interface and scheduled events.",
"icon": "calendar.png",
"screenshots": [{"url":"screenshot_calendar.png"}],

View File

@ -30,7 +30,6 @@
},
/*LANG*/'show widgets': {
value: !!settings.showWidgets,
format: () => (settings.showWidgets ? 'Yes' : 'No'),
onchange: x => save('showWidgets', x),
},
/*LANG*/'update interval': {
@ -45,7 +44,6 @@
},
/*LANG*/'show big weather': {
value: !!settings.showBigWeather,
format: () => (settings.showBigWeather ? 'Yes' : 'No'),
onchange: x => save('showBigWeather', x),
},
/*LANG*/'colorize icons': ()=>showCircleMenus()
@ -87,8 +85,7 @@
const colorizeIconKey = circleName + "colorizeIcon";
menu[/*LANG*/'circle ' + circleId] = {
value: settings[colorizeIconKey] || false,
format: () => (settings[colorizeIconKey]? /*LANG*/'Yes': /*LANG*/'No'),
onchange: x => save(colorizeIconKey, x),
onchange: x => save(colorizeIconKey, x),
};
}
E.showMenu(menu);

View File

@ -9,7 +9,6 @@
'': { 'title': 'CLI complete clk' },
'Show battery': {
value: "battery" in settings ? settings.battery : false,
format: () => (settings.battery ? 'Yes' : 'No'),
onchange: () => {
settings.battery = !settings.battery;
save('battery', settings.battery);
@ -27,7 +26,6 @@
},
'Show weather': {
value: "weather" in settings ? settings.weather : false,
format: () => (settings.weather ? 'Yes' : 'No'),
onchange: () => {
settings.weather = !settings.weather;
save('weather', settings.weather);
@ -35,7 +33,6 @@
},
'Show steps': {
value: "steps" in settings ? settings.steps : false,
format: () => (settings.steps ? 'Yes' : 'No'),
onchange: () => {
settings.steps = !settings.steps;
save('steps', settings.steps);
@ -43,7 +40,6 @@
},
'Show heartrate': {
value: "heartrate" in settings ? settings.heartrate : false,
format: () => (settings.heartrate ? 'Yes' : 'No'),
onchange: () => {
settings.heartrate = !settings.heartrate;
save('heartrate', settings.heartrate);

View File

@ -8,3 +8,4 @@
0.08: Catch and discard swipe events on fw2v19 and up (as well as some cutting
edge 2v18 ones), allowing compatability with the Back Swipe app.
0.09: Fix colors settings, where color was stored as string instead of the expected int.
0.10: Fix touch region for letters

View File

@ -107,7 +107,7 @@ exports.input = function(options) {
"ram";
// ABCDEFGHIJKLMNOPQRSTUVWXYZ
// Choose character by draging along red rectangle at bottom of screen
if (event.y >= ( (R.y+R.h) - 12 )) {
if (event.y >= ( (R.y+R.h) - 26 )) {
// Translate x-position to character
if (event.x < ABCPADDING) { abcHL = 0; }
else if (event.x >= 176-ABCPADDING) { abcHL = 25; }
@ -139,7 +139,7 @@ exports.input = function(options) {
// 12345678901234567890
// Choose number or puctuation by draging on green rectangle
else if ((event.y < ( (R.y+R.h) - 12 )) && (event.y > ( (R.y+R.h) - 52 ))) {
else if ((event.y < ( (R.y+R.h) - 26 )) && (event.y > ( (R.y+R.h) - 52 ))) {
// Translate x-position to character
if (event.x < NUMPADDING) { numHL = 0; }
else if (event.x > 176-NUMPADDING) { numHL = NUM.length-1; }

View File

@ -1,6 +1,6 @@
{ "id": "dragboard",
"name": "Dragboard",
"version":"0.09",
"version":"0.10",
"description": "A library for text input via swiping keyboard",
"icon": "app.png",
"type":"textinput",

View File

@ -1 +1,2 @@
0.01: attempt to import
0.02: Make it possible for Fastload Utils to fastload into this app.

View File

@ -1,5 +1,7 @@
// App Forge
"Bangle.loadWidgets()"; // Facilitates fastloading to this app via Fastload Utils, while still not loading widgets on standard `load` calls.
st = require('Storage');
l = /^a\..*\.js$/;

View File

@ -1,6 +1,6 @@
{ "id": "forge",
"name": "App Forge",
"version":"0.01",
"version":"0.02",
"description": "Easy way to run development versions of your apps",
"icon": "app.png",
"readme": "README.md",

View File

@ -23,7 +23,6 @@
'< Back': back,
'Show Widgets': {
value: settings.showWidgets,
format: () => (settings.showWidgets ? 'Yes' : 'No'),
onchange: () => {
settings.showWidgets = !settings.showWidgets;
save();

View File

@ -431,7 +431,7 @@ function handleUpload() {
storage:[
{name:"RAM", content:hexJS},
]
});
}, { noFinish: true });
}
document.getElementById('fileLoader').addEventListener('change', handleFileSelect, false);

View File

@ -43,7 +43,6 @@
/*LANG*/"Step Goal Notification": {
value: "stepGoalNotification" in settings ? settings.stepGoalNotification : false,
format: () => (settings.stepGoalNotification ? 'Yes' : 'No'),
onchange: () => {
settings.stepGoalNotification = !settings.stepGoalNotification;
setSettings();

View File

@ -76,7 +76,6 @@ var bg_code = [
},
'Full Screen': {
value: settings.fullscreen,
format: () => (settings.fullscreen ? 'Yes' : 'No'),
onchange: () => {
settings.fullscreen = !settings.fullscreen;
save();
@ -120,7 +119,6 @@ var bg_code = [
},
'Disable alarm functionality': {
value: settings.disableAlarms,
format: () => (settings.disableAlarms ? 'Yes' : 'No'),
onchange: () => {
settings.disableAlarms = !settings.disableAlarms;
save();
@ -128,7 +126,6 @@ var bg_code = [
},
'Disable data pages functionality': {
value: settings.disableData,
format: () => (settings.disableData ? 'Yes' : 'No'),
onchange: () => {
settings.disableData = !settings.disableData;
save();
@ -136,7 +133,6 @@ var bg_code = [
},
'Random colors on open': {
value: settings.randomColors,
format: () => (settings.randomColors ? 'Yes' : 'No'),
onchange: () => {
settings.randomColors = !settings.randomColors;
save();

View File

@ -27,13 +27,12 @@
}
var font_options = ["Limelight","GochiHand","Grenadier","Monoton"];
E.showMenu({
'': { 'title': 'Limelight Clock' },
'< Back': back,
'Full Screen': {
value: s.fullscreen,
format: () => (s.fullscreen ? 'Yes' : 'No'),
onchange: () => {
s.fullscreen = !s.fullscreen;
save();
@ -50,7 +49,6 @@
},
'Vector Font': {
value: s.vector,
format: () => (s.vector ? 'Yes' : 'No'),
onchange: () => {
s.vector = !s.vector;
save();
@ -68,7 +66,6 @@
},
'Second Hand': {
value: s.secondhand,
format: () => (s.secondhand ? 'Yes' : 'No'),
onchange: () => {
s.secondhand = !s.secondhand;
save();

View File

@ -0,0 +1 @@
0.1: init app

21
apps/line_clock/LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Paul Spenke
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

11
apps/line_clock/README.md Normal file
View File

@ -0,0 +1,11 @@
# Line Clock
This app displays a simple, different looking, analog clock. It considers the
currently configured "theme" (and may therefore look different than shown in
the screenshot on your watch depending on which theme you prefer).
![](app-screenshot.png)
## License
[MIT License](LICENSE)

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwgYMJh/4AgUD+AeKgIRDj/+n41O/4RQABcfIJYAEKZgAkL4U/8ARNBwIRP/+AGx6YBPSH/4ASPh/A/hfDAAZAHg/8gP/LguSoARHEwIRFiVJkDCFjgRHgEJkg4CcwQjIAAMEHAUDCoIRB46kIHAkH//xLIw4I8eAnCNKHAYAO/xxEABg4ByASPHAkBKAbUE/5xGhP//wRFv4RDOIYIB//ACQr1FHAIRJAA0TCAP/ZwIALgYRJVowRCj/4BIkBLIgABgRHC/KqFaI4RC5MkJBlPR4UECJizJJwoAKCKImVQAwAJv0HL5S6CbwIjLCKMAn4RDh0/LMKMhWaYAKA="))

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

287
apps/line_clock/app.js Normal file
View File

@ -0,0 +1,287 @@
const handWidth = 6;
const hourRadius = 4;
const hourWidth = 8;
const hourLength = 40;
const hourSLength = 20;
const radius = 220;
const lineOffset = 115;
const hourOffset = 32;
const numberOffset = 85;
const numberSize = 22;
const storage = require('Storage');
const SETTINGS_FILE = "line_clock.setting.json";
let initialSettings = {
showLock: true,
showMinute: true,
};
let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || initialSettings;
for (const key in saved_settings) {
initialSettings[key] = saved_settings[key];
}
let gWidth = g.getWidth(), gCenterX = gWidth/2;
let gHeight = g.getHeight(), gCenterY = gHeight/2;
let currentTime = new Date();
let currentHour = currentTime.getHours();
let currentMinute = currentTime.getMinutes();
let drawTimeout;
function imgLock() {
return {
width : 16, height : 16, bpp : 1,
transparent : 0,
buffer : E.toArrayBuffer(atob("A8AH4A5wDDAYGBgYP/w//D/8Pnw+fD58Pnw//D/8P/w="))
};
}
/**
* Retrieves the angle of the hour hand for the current time.
*
* @returns {number} The angle of the hour hand in degrees.
*/
function getHourHandAngle() {
let hourHandAngle = 30 * currentHour;
hourHandAngle += 0.5 * currentMinute;
return hourHandAngle;
}
let hourAngle = getHourHandAngle();
/**
* Converts degrees to radians.
*
* @param {number} degrees - The degrees to be converted to radians.
* @return {number} - The equivalent value in radians.
*/
function degreesToRadians(degrees) {
return degrees * (Math.PI / 180);
}
/**
* Rotates an array of points around a given angle and radius.
*
* @param {Array} points - The array of points to be rotated.
* @param {number} angle - The angle in degrees to rotate the points.
* @param {number} rad - The radius to offset the rotation.
* @returns {Array} - The array of rotated points.
*/
function rotatePoints(points, angle, rad) {
const ang = degreesToRadians(angle);
const hAng = degreesToRadians(hourAngle);
const rotatedPoints = [];
points.map(function(point) {
return {
x: point.x * Math.cos(ang) - point.y * Math.sin(ang),
y: point.x * Math.sin(ang) + point.y * Math.cos(ang)
};
}).forEach(function(point) {
rotatedPoints.push(point.x + gCenterX - (rad * Math.sin(hAng)));
rotatedPoints.push(point.y + gCenterY + (rad * Math.cos(hAng)));
});
return rotatedPoints;
}
/**
* Draws a hand on the canvas.
*
* @function drawHand
*
* @returns {void}
*/
function drawHand() {
g.setColor(0xF800);
const halfWidth = handWidth / 2;
const points = [{
x: -halfWidth,
y: -gHeight
}, {
x: halfWidth,
y: -gHeight
}, {
x: halfWidth,
y: gHeight
}, {
x: -halfWidth,
y: gHeight
}];
g.fillPolyAA(rotatePoints(points, hourAngle, 0));
}
/**
* Retrieves the hour coordinates for a given small flag.
* @param {boolean} small - Determines if the flag is small.
* @returns {Array} - An array of hour coordinates.
*/
function getHourCoordinates(small) {
const dist = small ? (hourSLength - hourLength) : 0;
const halfWidth = hourWidth / 2;
const gh = gHeight + lineOffset;
return [{
x: -halfWidth,
y: -gh - dist
}, {
x: halfWidth,
y: -gh - dist
}, {
x: halfWidth,
y: -gh + hourLength
}, {
x: -halfWidth,
y: -gh + hourLength
}];
}
/**
* Assign the given time to the hour dot on the clock face.
*
* @param {number} a - The time value to assign to the hour dot.
* @return {void}
*/
function hourDot(a) {
const h = gHeight + lineOffset;
const rotatedPoints = rotatePoints(
[{
x: 0,
y: -h + hourLength - (hourRadius / 2)
}], a, radius
);
g.fillCircle(rotatedPoints[0], rotatedPoints[1], hourRadius);
}
/**
* Convert an hour into a number and display it on the clock face.
*
* @param {number} a - The hour to be converted (between 0 and 360 degrees).
*/
function hourNumber(a) {
const h = gHeight + lineOffset;
const rotatedPoints = rotatePoints(
[{
x: 0,
y: -h + hourLength + hourOffset
}], a, radius
);
g.drawString(String(a / 30), rotatedPoints[0], rotatedPoints[1]);
}
/**
* Draws a number on the display.
*
* @param {number} n - The number to be drawn.
* @return {void}
*/
function drawNumber(n) {
const h = gHeight + lineOffset;
const halfWidth = handWidth / 2;
const rotatedPoints = rotatePoints(
[{
x: 0,
y: -h + hourLength + numberOffset
}], hourAngle, radius
);
g.setColor(0xF800);
g.fillCircle(rotatedPoints[0], rotatedPoints[1], numberSize+ halfWidth);
g.setColor(g.theme.bg);
g.fillCircle(rotatedPoints[0], rotatedPoints[1], numberSize - halfWidth);
g.setColor(g.theme.fg);
g.setFont("Vector:"+numberSize);
g.drawString(String(n), rotatedPoints[0], rotatedPoints[1]);
}
const hourPoints = getHourCoordinates(false);
const hourSPoints = getHourCoordinates(true);
/**
* Draws an hour on a clock face.
*
* @param {number} h - The hour to be drawn on the clock face.
* @return {undefined}
*/
function drawHour(h) {
if (h === 0) { h= 12; }
if (h === 13) { h= 1; }
g.setColor(g.theme.fg);
g.setFont("Vector:32");
const a = h * 30;
g.fillPolyAA(rotatePoints(hourPoints, a, radius));
g.fillPolyAA(rotatePoints(hourSPoints, a + 15, radius));
hourNumber(a);
hourDot(a + 5);
hourDot(a + 10);
hourDot(a + 20);
hourDot(a + 25);
}
function queueDraw() {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = setTimeout(function() {
drawTimeout = undefined;
draw();
}, 60000 - (Date.now() % 60000));
}
function lockListenerBw() {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
draw();
}
Bangle.on('lock', lockListenerBw);
Bangle.setUI({
mode : "clock",
// TODO implement https://www.espruino.com/Bangle.js+Fast+Load
// remove : function() {
// Bangle.removeListener('lock', lockListenerBw);
// if (drawTimeout) clearTimeout(drawTimeout);
// drawTimeout = undefined;
// }
});
/**
* Draws a clock on the canvas using the current time.
*
* @return {undefined}
*/
function draw() {
queueDraw();
currentTime = new Date();
currentHour = currentTime.getHours();
if (currentHour > 12) {
currentHour -= 12;
}
currentMinute = currentTime.getMinutes();
hourAngle = getHourHandAngle();
g.clear();
g.setFontAlign(0, 0);
g.setColor(g.theme.bg);
g.fillRect(0, 0, gWidth, gHeight);
if(initialSettings.showLock && Bangle.isLocked()){
g.setColor(g.theme.fg);
g.drawImage(imgLock(), gWidth-16, 2);
}
drawHour(currentHour);
drawHour(currentHour-1);
drawHour(currentHour+1);
drawHand();
if(initialSettings.showMinute){
drawNumber(currentMinute);
}
}
draw();

View File

@ -0,0 +1,19 @@
{ "id": "line_clock",
"name": "Line Clock",
"shortName":"Line Clock",
"version":"0.1",
"description": "a readable analog clock",
"icon": "app-icon.png",
"type": "clock",
"tags": "clock",
"supports" : ["BANGLEJS2"],
"allow_emulator": true,
"screenshots": [{"url":"app-screenshot.png"}],
"readme": "README.md",
"storage": [
{"name":"line_clock.app.js","url":"app.js"},
{"name":"line_clock.img","url":"app-icon.js","evaluate":true},
{"name":"line_clock.settings.js","url":"settings.js"}
],
"data":[{"name":"line_clock.setting.json"}]
}

View File

@ -0,0 +1,37 @@
(function(back) {
const SETTINGS_FILE = "line_clock.setting.json";
// initialize with default settings...
const storage = require('Storage')
let settings = {
showLock: true,
showMinute: true,
};
let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings;
for (const key in saved_settings) {
settings[key] = saved_settings[key]
}
function save() {
storage.write(SETTINGS_FILE, settings)
}
E.showMenu({
'': { 'title': 'Line Clock' },
'< Back': back,
'Show Lock': {
value: settings.showLock,
onchange: () => {
settings.showLock = !settings.showLock;
save();
},
},
'Show Minute': {
value: settings.showMinute,
onchange: () => {
settings.showMinute = !settings.showMinute;
save();
},
}
});
})

View File

@ -32,7 +32,6 @@
},
'Show Lock': {
value: settings.showLock,
format: () => (settings.showLock ? 'Yes' : 'No'),
onchange: () => {
settings.showLock = !settings.showLock;
save();
@ -40,7 +39,6 @@
},
'Hide Colon': {
value: settings.hideColon,
format: () => (settings.hideColon ? 'Yes' : 'No'),
onchange: () => {
settings.hideColon = !settings.hideColon;
save();

View File

@ -25,7 +25,6 @@
'< Back': back,
'Show Widgets': {
value: settings.showWidgets,
format: () => (settings.showWidgets ? 'Yes' : 'No'),
onchange: () => {
settings.showWidgets = !settings.showWidgets;
save();

View File

@ -23,7 +23,7 @@
var color_options = ['Green','Orange','Cyan','Purple','Red','Blue'];
var bg_code = ['#0f0','#ff0','#0ff','#f0f','#f00','#00f'];
var theme_options = ['System', 'Light', 'Dark'];
E.showMenu({
'': { 'title': 'Pebble Clock' },
/*LANG*/'< Back': back,
@ -48,7 +48,6 @@
},
/*LANG*/'Show Lock': {
value: settings.showlock,
format: () => (settings.showlock ? /*LANG*/'Yes' : /*LANG*/'No'),
onchange: () => {
settings.showlock = !settings.showlock;
save();

View File

@ -21,42 +21,34 @@
},
/*LANG*/'Red': {
value: !!settings.colorRed,
format: () => (settings.colorRed ? 'Yes' : 'No'),
onchange: x => save('colorRed', x),
},
/*LANG*/'Green': {
value: !!settings.colorGreen,
format: () => (settings.colorGreen ? 'Yes' : 'No'),
onchange: x => save('colorGreen', x),
},
/*LANG*/'Blue': {
value: !!settings.colorBlue,
format: () => (settings.colorBlue ? 'Yes' : 'No'),
onchange: x => save('colorBlue', x),
},
/*LANG*/'Magenta': {
value: !!settings.colorMagenta,
format: () => (settings.colorMagenta ? 'Yes' : 'No'),
onchange: x => save('colorMagenta', x),
},
/*LANG*/'Cyan': {
value: !!settings.colorCyan,
format: () => (settings.colorCyan ? 'Yes' : 'No'),
onchange: x => save('colorCyan', x),
},
/*LANG*/'Yellow': {
value: !!settings.colorYellow,
format: () => (settings.colorYellow ? 'Yes' : 'No'),
onchange: x => save('colorYellow', x),
},
/*LANG*/'Black': {
value: !!settings.colorBlack,
format: () => (settings.colorBlack ? 'Yes' : 'No'),
onchange: x => save('colorBlack', x),
},
/*LANG*/'White': {
value: !!settings.colorWhite,
format: () => (settings.colorWhite ? 'Yes' : 'No'),
onchange: x => save('colorWhite', x),
}
};

View File

@ -0,0 +1 @@
0.01: New App

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AFW7AAgThDBQeNCaYaSDg4TTFygcFCaYuWDgYTTFyHB5AmRGCARK4XJF7bXR5PQF9vQ6/CF8COL6/XYDgwERxYvBYEKOM67AhLQwEDFwTAGYywvIXIIhCAgPQSILAE4RmWF5AnBMAQECA4gJDMCovDGAfBQ4PR4K+C4RiCZAr3UFwgvDJ4IAB5IrEYAgJBHwaoF6HQNRIvFEQItDXYaTEYAQ+EAogADZZIvFGAYTBAgIBBFQg0CRwQREF5wuGeQpNDQYSUDAYYyFR5guIF4jyCdQ3CMYY+DNwTtRMBSJCAwgyBNAI+HFygwEQoJ4EQwQpEHwwuVGAhPFGwIABFIY+GFywwDdoTAFe4ZgCFzjDFSAy4NFyowIF4PIF0gwHXAKYGFz4wHXBgubGAxeOFzT1KFsowQF0AwNF0QxKFsoxIEj/XGBgQDCJYrODQICHEgQDGAQoSECwhaNCoYJDAwgoHBxBfVJgpfJGIgOHL5haGPgoBDTxIBHAH4AkA=="))

287
apps/stressless/app.js Normal file
View File

@ -0,0 +1,287 @@
var option = null;
//debugging or analysis files
//var logfile = require("Storage").open("HRV_log.csv", "w");
var logfile = require("Storage").open("HRV_logs.csv", "a");
var csv = [
"time",
"sample count",
"HR",
"SDNN",
"RMSSD",
"Temp",
"movement"
];
logfile.write(csv.join(",")+"\n");
var debugging = true;
var samples = 0; // how many samples have we connected?
var collectData = false; // are we currently collecting data?
var BPM_array = [];
var raw_HR_array = new Float32Array(1536);
var alternate_array = new Float32Array(3072);
var pulse_array = [];
var cutoff_threshold = 0.5;
var sample_frequency = 51.6;
var gap_threshold = 0.15;
var movement = 0;
var px = g.getWidth()/2;
var py = g.getHeight()/2;
var accel; // interval for acceleration logging
function storeMyData(data, file_type) { "ram"
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) {
return E.sum(samples) / samples.length; // faster builtin
/* 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 = E.sum(array) / n; //array.reduce((a, b) => a + b) / n;
//return Math.sqrt(array.map(x => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / n);
return Math.sqrt(E.variance(array, mean));
}
function turn_off() {
Bangle.setHRMPower(0);
g.clear();
g.drawString("processing 1/5", px, py);
rolling_average(raw_HR_array,5);
g.clear();
g.drawString("processing 2/5", px, py);
upscale();
g.clear();
g.drawString("processing 3/5", px, py);
rolling_average(alternate_array,5);
g.clear();
g.drawString("processing 4/5", px, py);
apply_cutoff();
find_peaks();
g.clear();
g.drawString("processing 5/5", px, py);
calculate_HRV();
}
function bernstein(A, B, C, D, E, t) { "ram"
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() { "ram"
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) { "ram"
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() { "ram"
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() { "ram"
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){ "ram"
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)
Bangle.setLCDPower(1);
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, px, py);
Bangle.setLCDPower(1);
if(option == 0) { // single run
Bangle.buzz(500,1);
option = null;
drawButtons();
} else {
var csv = [
0|getTime(),
temp_array.length,
calculatedHR.toFixed(0),
SDNN,
RMS_SD,
E.getTemperature(),
movement.toFixed(5)
];
logfile.write(csv.join(",")+"\n");
turn_on();
}
}
function btn3Pressed() {
if(option === null){
logfile.write(""); //reset HRV log
g.clear();
g.drawString("continuous mode", px, py);
option = 1;
turn_on();
}
}
function turn_on() {
BPM_array = [];
pulse_array = [];
samples = 0;
if (accel) clearInterval(accel);
movement = 0;
accel = setInterval(function () {
movement = movement + Bangle.getAccel().diff;
}, 1000);
Bangle.setHRMPower(1);
collectData = true;
}
function drawButtons() {
g.setColor("#00ff7f");
g.setFont("6x8", 2);
g.setFontAlign(-1,1);
g.drawString("start recording HRV", 120, 210);
g.setColor("#ffffff");
g.setFontAlign(0, 0);
}
g.clear();
drawButtons();
g.setFont("6x8", 2);
g.setColor("#ffffff");
g.setFontAlign(0, 0); // center font
setWatch(btn3Pressed, BTN3, {repeat:true});
Bangle.on('HRM-raw', function (e) {
if (!collectData) return;
storeMyData(e.raw, 0);
if (!(samples & 7)) {
Bangle.setLCDPower(1);
g.clearRect(0, py-10, g.getWidth(), py+22);
if (samples < 100)
g.drawString("setting up...\nremain still " + samples + "%", px, py, true);
else
g.drawString("logging: " + (samples*100/raw_HR_array.length).toFixed(0) + "%", px, py, true);
}
if (samples > raw_HR_array.length) {
collectData = false;
turn_off();
}
samples++;
});

View File

@ -0,0 +1,13 @@
{ "id": "stressless",
"name": "Stressless",
"shortName":"Stressless",
"icon": "stressless.png",
"version":"0.01",
"description": "This is a heart activity tracker for PIIS stressless project",
"tags": "health",
"supports": ["BANGLEJS"],
"storage": [
{"name":"stressless.app.js","url":"app.js"},
{"name":"stressless.img","url":"app-icon.js","evaluate":true}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -21,7 +21,6 @@
},
'Hide Widget': {
value: "hide" in settings ? settings.hide : false,
format: () => (settings.hide ? 'Yes' : 'No'),
onchange: () => {
settings.hide = !settings.hide
save('hide', settings.hide);

View File

@ -43,7 +43,6 @@
},
'Hide Widget': {
value: s.hide,
format: () => (s.hide ? 'Yes' : 'No'),
onchange: () => {
s.hide = !s.hide;
save();

View File

@ -37,7 +37,6 @@
},
'Show Progress': {
value: s.progress,
format: () => (s.progress ? 'Yes' : 'No'),
onchange: () => {
s.progress = !s.progress
save();
@ -45,7 +44,6 @@
},
'Large Digits': {
value: s.large,
format: () => (s.large ? 'Yes' : 'No'),
onchange: () => {
s.large = !s.large
save();
@ -53,7 +51,6 @@
},
'Hide Widget': {
value: s.hide,
format: () => (s.hide ? 'Yes' : 'No'),
onchange: () => {
s.hide = !s.hide
save();

View File

@ -256,9 +256,17 @@ apps.forEach((app,appIdx) => {
if (a>=0 && b>=0 && a<b)
WARN(`Clock ${app.id} file calls loadWidgets before setUI (clock widget/etc won't be aware a clock app is running)`, {file:appDirRelative+file.url, line : fileContents.substr(0,a).split("\n").length});
}
// if settings, suggest adding to datafiles
if (/\.settings?\.js$/.test(file.name) && (!app.data || app.data.every(d => !d.name || !d.name.endsWith(".json")))) {
WARN(`App ${app.id} has a setting file but no corresponding data entry (add \`"data":[{"name":"${app.id}.settings.json"}]\`)`, {file:appDirRelative+file.url});
// if settings
if (/\.settings?\.js$/.test(file.name)) {
// suggest adding to datafiles
if (!app.data || app.data.every(d => !d.name || !d.name.endsWith(".json"))) {
WARN(`App ${app.id} has a setting file but no corresponding data entry (add \`"data":[{"name":"${app.id}.settings.json"}]\`)`, {file:appDirRelative+file.url});
}
// check for manual boolean formatter
const m = fileContents.match(/format: *\(\) *=>.*["'](yes|on)["']/i);
if (m) {
WARN(`Settings for ${app.id} has a boolean formatter - this is handled automatically, the line can be removed`, {file:appDirRelative+file.url, line: fileContents.substr(0, m.index).split("\n").length});
}
}
}
for (const key in file) {

2
core

@ -1 +1 @@
Subproject commit c97b7851f50cfff4e898c2264a337a17085ce463
Subproject commit e6a65a8cb20a730f75bbbab549c602300e69e8c4