Merge branch 'espruino:master' into master
|
@ -17,7 +17,7 @@ class TwoK {
|
|||
bh = Math.floor(h/4);
|
||||
bw = Math.floor(w/4);
|
||||
g.clearRect(0, 0, g.getWidth()-1, yo).setFontAlign(0, 0, 0);
|
||||
g.setFont("Vector", 16).setColor("#fff").drawString("Score:"+this.score.toString(), g.getWidth()/2, 8);
|
||||
g.setFont("Vector", 16).setColor(g.theme.fg).drawString("Score:"+this.score.toString(), g.getWidth()/2, 8);
|
||||
this.drawBRect(xo-3, yo-3, xo+w+2, yo+h+2, 4, "#a88", "#caa", false);
|
||||
for (y=0; y<4; ++y)
|
||||
for (x=0; x<4; ++x) {
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
0.01: New app!
|
||||
0.02: Better support for watch themes
|
|
@ -2,7 +2,7 @@
|
|||
"name": "2047pp",
|
||||
"shortName":"2047pp",
|
||||
"icon": "app.png",
|
||||
"version":"0.01",
|
||||
"version":"0.02",
|
||||
"description": "Bangle version of a tile shifting game",
|
||||
"supports" : ["BANGLEJS","BANGLEJS2"],
|
||||
"allow_emulator": true,
|
||||
|
|
|
@ -29,7 +29,7 @@ function showMenu() {
|
|||
}
|
||||
|
||||
function viewLog(n) {
|
||||
E.showMessage("Loading...");
|
||||
E.showMessage(/*LANG*/"Loading...");
|
||||
var f = require("Storage").open(getFileName(n), "r");
|
||||
var records = 0, l = "", ll="";
|
||||
while ((l=f.readLine())!==undefined) {records++;ll=l;}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
0.01: New App!
|
|
@ -0,0 +1,13 @@
|
|||
# Activity reminder
|
||||
|
||||
A reminder to take short walks for the ones with a sedentary lifestyle.
|
||||
The alert will popup only if you didn't take your short walk yet
|
||||
|
||||
Differents settings can be personnalized:
|
||||
- Enable : Enable/Disable the app
|
||||
- Start hour: Hour to start the reminder
|
||||
- End hour: Hour to end the reminder
|
||||
- Max innactivity: Maximum innactivity time to allow before the alert. From 15 min to 60 min
|
||||
- Dismiss delay: Delay added before the next alert if the alert is dismissed. From 5 to 15 min
|
||||
- Min steps: Minimal amount of steps to count as an activity
|
||||
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwYda7dtwAQNmwRB2wQMgO2CIXACJcNCIfYCJYOCCgQRNJQYRM2ADBgwR/CKprRWAKPQWZ0DCIjXLjYREGpYODAQVgCBB3Btj+EAoQAGO4IdCgImDCAwLCAoo4IF4J3DCIPDCIQ4FO4VtwARCAoIRGRgQCBa4IRCKAQRERgOwIIIRDAoOACIoIBwwRHLIqMCFgIRCGQQRIWAYRLYQoREWwTmHO4IRCFgLXHPoi/CbogAFEAIRCWwTpKEwZBCHwK5BCJZEBCJZcCGQTLDCJK/BAQIRKMoaSDOIYAFeQYRMcYRWBXIUAWYPACIq8DagfACJQLCCIYsBU4QRF7B9CAogRGI4QLCAoprIMoZKER5C/DAoShMAo4AGfAQFIACQ="))
|
|
@ -0,0 +1,37 @@
|
|||
function drawAlert(){
|
||||
E.showPrompt("Innactivity detected",{
|
||||
title:"Activity reminder",
|
||||
buttons : {"Ok": true,"Dismiss": false}
|
||||
}).then(function(v) {
|
||||
if(v == true){
|
||||
stepsArray = stepsArray.slice(0, activityreminder.maxInnactivityMin - 3);
|
||||
require("activityreminder").saveStepsArray(stepsArray);
|
||||
}
|
||||
if(v == false){
|
||||
stepsArray = stepsArray.slice(0, activityreminder.maxInnactivityMin - activityreminder.dismissDelayMin);
|
||||
require("activityreminder").saveStepsArray(stepsArray);
|
||||
}
|
||||
load();
|
||||
});
|
||||
|
||||
Bangle.buzz(400);
|
||||
setTimeout(load, 10000);
|
||||
}
|
||||
|
||||
function run(){
|
||||
if(stepsArray.length == activityreminder.maxInnactivityMin){
|
||||
if (stepsArray[0] - stepsArray[stepsArray.length-1] < activityreminder.minSteps){
|
||||
drawAlert();
|
||||
}
|
||||
}else{
|
||||
eval(require("Storage").read("activityreminder.settings.js"))(()=>load());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
g.clear();
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
activityreminder = require("activityreminder").loadSettings();
|
||||
stepsArray = require("activityreminder").loadStepsArray();
|
||||
run();
|
After Width: | Height: | Size: 10 KiB |
|
@ -0,0 +1,29 @@
|
|||
function run(){
|
||||
var now = new Date();
|
||||
var h = now.getHours();
|
||||
if(h >= activityreminder.startHour && h < activityreminder.endHour){
|
||||
var health = Bangle.getHealthStatus("day");
|
||||
stepsArray.unshift(health.steps);
|
||||
stepsArray = stepsArray.slice(0, activityreminder.maxInnactivityMin);
|
||||
require("activityreminder").saveStepsArray(stepsArray);
|
||||
}
|
||||
else{
|
||||
if(stepsArray != []){
|
||||
stepsArray = [];
|
||||
require("activityreminder").saveStepsArray(stepsArray);
|
||||
}
|
||||
}
|
||||
if(stepsArray.length >= activityreminder.maxInnactivityMin){
|
||||
if (stepsArray[0] - stepsArray[stepsArray.length-1] < activityreminder.minSteps){
|
||||
load('activityreminder.app.js');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
activityreminder = require("activityreminder").loadSettings();
|
||||
if(activityreminder.enabled) {
|
||||
stepsArray = require("activityreminder").loadStepsArray();
|
||||
setInterval(run, 60000);
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
exports.loadSettings = function() {
|
||||
return Object.assign({
|
||||
enabled: true,
|
||||
startHour: 9,
|
||||
endHour: 20,
|
||||
maxInnactivityMin: 30,
|
||||
dismissDelayMin: 15,
|
||||
minSteps: 50
|
||||
}, require("Storage").readJSON("ar.settings.json", true) || {});
|
||||
};
|
||||
|
||||
exports.writeSettings = function(settings){
|
||||
require("Storage").writeJSON("ar.settings.json", settings);
|
||||
};
|
||||
|
||||
exports.saveStepsArray = function(stepsArray) {
|
||||
require("Storage").writeJSON("ar.stepsarray.json", stepsArray);
|
||||
};
|
||||
|
||||
exports.loadStepsArray = function(){
|
||||
return require("Storage").readJSON("ar.stepsarray.json") || [];
|
||||
};
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"id": "activityreminder",
|
||||
"name": "Activity Reminder",
|
||||
"shortName":"Activity Reminder",
|
||||
"description": "A reminder to take short walks for the ones with a sedentary lifestyle",
|
||||
"version":"0.01",
|
||||
"icon": "app.png",
|
||||
"type": "app",
|
||||
"tags": "tool,activity",
|
||||
"supports": ["BANGLEJS", "BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
"storage": [
|
||||
{"name": "activityreminder.app.js", "url":"app.js"},
|
||||
{"name": "activityreminder.boot.js", "url": "boot.js"},
|
||||
{"name": "activityreminder.settings.js", "url": "settings.js"},
|
||||
{"name": "activityreminder", "url": "lib.js"},
|
||||
{"name": "activityreminder.img", "url": "app-icon.js", "evaluate": true}
|
||||
],
|
||||
"data": [
|
||||
{"name": "ar.settings.json", "name": "ar.stepsarray.json"}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
(function(back) {
|
||||
// Load settings
|
||||
var settings = require("activityreminder").loadSettings();
|
||||
|
||||
// Show the menu
|
||||
E.showMenu({
|
||||
"" : { "title" : "Activity Reminder" },
|
||||
"< Back" : () => back(),
|
||||
'Enable': {
|
||||
value: !!settings.enabled,
|
||||
format: v => v?"Yes":"No",
|
||||
onchange: v => {
|
||||
settings.enabled = v;
|
||||
require("activityreminder").writeSettings(settings);
|
||||
}
|
||||
},
|
||||
'Start hour': {
|
||||
value: 9|settings.startHour,
|
||||
min: 0, max: 24,
|
||||
onchange: v => {
|
||||
settings.startHour = v;
|
||||
require("activityreminder").writeSettings(settings);
|
||||
}
|
||||
},
|
||||
'End hour': {
|
||||
value: 20|settings.endHour,
|
||||
min: 0, max: 24,
|
||||
onchange: v => {
|
||||
settings.endHour = v;
|
||||
require("activityreminder").writeSettings(settings);
|
||||
}
|
||||
},
|
||||
'Max innactivity': {
|
||||
value: 30|settings.maxInnactivityMin,
|
||||
min: 15, max: 60,
|
||||
onchange: v => {
|
||||
settings.maxInnactivityMin = v;
|
||||
require("activityreminder").writeSettings(settings);
|
||||
}
|
||||
},
|
||||
'Dismiss delay': {
|
||||
value: 10|settings.dismissDelayMin,
|
||||
min: 5, max: 15,
|
||||
onchange: v => {
|
||||
settings.dismissDelayMin = v;
|
||||
require("activityreminder").writeSettings(settings);
|
||||
}
|
||||
},
|
||||
'Min steps': {
|
||||
value: 50|settings.minSteps,
|
||||
min: 10, max: 500,
|
||||
onchange: v => {
|
||||
settings.minSteps = v;
|
||||
require("activityreminder").writeSettings(settings);
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
|
@ -1,7 +1,7 @@
|
|||
/* This rewrites boot0.js based on current settings. If settings changed then it
|
||||
recalculates, but this avoids us doing a whole bunch of reconfiguration most
|
||||
of the time. */
|
||||
E.showMessage("Updating boot0...");
|
||||
E.showMessage(/*LANG*/"Updating boot0...");
|
||||
var s = require('Storage').readJSON('setting.json',1)||{};
|
||||
var BANGLEJS2 = process.env.HWVERSION==2; // Is Bangle.js 2
|
||||
var boot = "", bootPost = "";
|
||||
|
@ -209,7 +209,7 @@ delete bootPost;
|
|||
delete bootFiles;
|
||||
delete fileSize;
|
||||
delete fileOffset;
|
||||
E.showMessage("Reloading...");
|
||||
E.showMessage(/*LANG*/"Reloading...");
|
||||
eval(require('Storage').read('.boot0'));
|
||||
// .bootcde should be run automatically after if required, since
|
||||
// we normally get called automatically from '.boot0'
|
||||
|
|
|
@ -3,3 +3,4 @@
|
|||
0.03: Adapt colors based on the theme of the user.
|
||||
0.04: Steps can be hidden now such that the time is even larger.
|
||||
0.05: Included icons for information.
|
||||
0.06: Design and usability improvements.
|
|
@ -1,13 +1,13 @@
|
|||
# Black & White clock
|
||||
# BW Clock
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
- Fullscreen on/off
|
||||
- The design is adapted to the theme of your bangle.
|
||||
- Tab left/right of screen to show steps, temperature etc.
|
||||
- Enable / disable lock icon in the settings.
|
||||
- If the "sched" app is installed tab top / bottom of the screen to set the timer.
|
||||
- The design is adapted to the theme of your bangle.
|
||||
|
||||
## Thanks to
|
||||
<a href="https://www.flaticon.com/free-icons/" title="Icons">Icons created by Flaticon</a>
|
||||
|
|
|
@ -99,21 +99,36 @@ var imgCharging = {
|
|||
buffer : require("heatshrink").decompress(atob("//+v///k///4AQPwBANgBoMxBoMb/P+h/w/kH8H4gfB+EBwfggHH4EAt4CBn4CBj4CBh4FCCIO/8EB//Agf/wEH/8Gh//x////fAQIA="))
|
||||
};
|
||||
|
||||
var imgWatch = {
|
||||
width : 24, height : 24, bpp : 1,
|
||||
transparent : 1,
|
||||
buffer : require("heatshrink").decompress(atob("/8B//+ARANB/l4//5/1/+f/n/n5+fAQnf9/P44CC8/n7/n+YOB/+fDQQgCEwQsCHBBEC"))
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
* Draw timeout
|
||||
* INFO ENTRIES
|
||||
*/
|
||||
// timeout used to update every minute
|
||||
var drawTimeout;
|
||||
var infoArray = [
|
||||
function(){ return [ null, null, "left" ] },
|
||||
function(){ return [ "Bangle", imgWatch, "right" ] },
|
||||
function(){ return [ E.getBattery() + "%", imgBattery, "left" ] },
|
||||
function(){ return [ getSteps(), imgSteps, "left" ] },
|
||||
function(){ return [ Math.round(Bangle.getHealthStatus("last").bpm) + " bpm", imgBpm, "left"] },
|
||||
function(){ return [ getWeather().temp, imgTemperature, "left" ] },
|
||||
function(){ return [ getWeather().wind, imgWind, "left" ] },
|
||||
];
|
||||
const NUM_INFO=infoArray.length;
|
||||
|
||||
// schedule a draw for the next minute
|
||||
function queueDraw() {
|
||||
if (drawTimeout) clearTimeout(drawTimeout);
|
||||
drawTimeout = setTimeout(function() {
|
||||
drawTimeout = undefined;
|
||||
draw();
|
||||
}, 60000 - (Date.now() % 60000));
|
||||
|
||||
function getInfoEntry(){
|
||||
if(isAlarmEnabled()){
|
||||
return [getAlarmMinutes() + " min.", imgTimer, "left"]
|
||||
} else if(Bangle.isCharging()){
|
||||
return [E.getBattery() + "%", imgCharging, "left"]
|
||||
} else{
|
||||
return infoArray[settings.showInfo]();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -121,19 +136,21 @@ function queueDraw() {
|
|||
* Helper
|
||||
*/
|
||||
function getSteps() {
|
||||
var steps = 0;
|
||||
try{
|
||||
if (WIDGETS.wpedom !== undefined) {
|
||||
return WIDGETS.wpedom.getSteps();
|
||||
steps = WIDGETS.wpedom.getSteps();
|
||||
} else if (WIDGETS.activepedom !== undefined) {
|
||||
return WIDGETS.activepedom.getSteps();
|
||||
steps = WIDGETS.activepedom.getSteps();
|
||||
} else {
|
||||
return Bangle.getHealthStatus("day").steps;
|
||||
steps = Bangle.getHealthStatus("day").steps;
|
||||
}
|
||||
} catch(ex) {
|
||||
// In case we failed, we can only show 0 steps.
|
||||
}
|
||||
|
||||
return 0;
|
||||
steps = Math.round(steps/100) / 10; // This ensures that we do not show e.g. 15.0k and 15k instead
|
||||
return steps + "k";
|
||||
}
|
||||
|
||||
|
||||
|
@ -225,111 +242,109 @@ function decreaseAlarm(){
|
|||
|
||||
|
||||
/*
|
||||
* D R A W
|
||||
* DRAW functions
|
||||
*/
|
||||
|
||||
function draw() {
|
||||
// queue draw in one minute
|
||||
// Queue draw again
|
||||
queueDraw();
|
||||
|
||||
// Set info
|
||||
var showInfo = settings.showInfo;
|
||||
if(isAlarmEnabled()){
|
||||
showInfo = 100;
|
||||
}
|
||||
|
||||
if(Bangle.isCharging()){
|
||||
showInfo = 101;
|
||||
}
|
||||
// Draw clock
|
||||
drawDate();
|
||||
drawTime();
|
||||
drawLock();
|
||||
drawWidgets();
|
||||
}
|
||||
|
||||
|
||||
function drawDate(){
|
||||
// Draw background
|
||||
var y = H/5*2 + (settings.fullscreen ? 0 : 8);
|
||||
g.reset().clearRect(0,0,W,W);
|
||||
|
||||
// Draw date
|
||||
y -= settings.fullscreen ? 8 : 0;
|
||||
var date = new Date();
|
||||
var dateStr = date.getDate();
|
||||
dateStr = ("0" + dateStr).substr(-2);
|
||||
g.setMediumFont(); // Needed to compute the width correctly
|
||||
var dateW = g.stringWidth(dateStr);
|
||||
|
||||
g.setSmallFont();
|
||||
var dayStr = locale.dow(date, true);
|
||||
var monthStr = locale.month(date, 1);
|
||||
var dayW = Math.max(g.stringWidth(dayStr), g.stringWidth(monthStr));
|
||||
var fullDateW = dateW + 10 + dayW;
|
||||
|
||||
g.setFontAlign(-1,1);
|
||||
g.setMediumFont();
|
||||
g.setColor(g.theme.fg);
|
||||
g.drawString(dateStr, W/2 - fullDateW / 2, y+5);
|
||||
|
||||
g.setSmallFont();
|
||||
g.drawString(monthStr, W/2 - fullDateW/2 + 10 + dateW, y+3);
|
||||
g.drawString(dayStr, W/2 - fullDateW/2 + 10 + dateW, y-23);
|
||||
}
|
||||
|
||||
|
||||
function drawTime(){
|
||||
// Draw background
|
||||
var yOffset = settings.fullscreen ? 0 : 10;
|
||||
var y = H/5*2 + yOffset;
|
||||
g.reset().clearRect(0,0,W,W);
|
||||
var y = H/5*2 + (settings.fullscreen ? 0 : 8);
|
||||
g.setColor(g.theme.fg);
|
||||
g.fillRect(0,y,W,H);
|
||||
|
||||
// Draw date
|
||||
y -= settings.fullscreen ? 5 : 0;
|
||||
var date = new Date();
|
||||
g.setColor(g.theme.fg);
|
||||
g.setFontAlign(1,1);
|
||||
g.setMediumFont();
|
||||
var dateStr = date.getDate();
|
||||
dateStr = ("0" + dateStr).substr(-2);
|
||||
g.drawString(dateStr, W/2-1, y+4);
|
||||
|
||||
g.setSmallFont();
|
||||
g.setFontAlign(-1,1);
|
||||
g.drawString(locale.dow(date, true), W/2 + 10, y-23);
|
||||
g.drawString(locale.month(date, 1), W/2 + 10, y+1);
|
||||
|
||||
// Draw time
|
||||
g.setColor(g.theme.bg);
|
||||
g.setFontAlign(0,-1);
|
||||
var timeStr = locale.time(date,1);
|
||||
y += settings.fullscreen ? 20 : 10;
|
||||
y += settings.fullscreen ? 14 : 10;
|
||||
|
||||
if(showInfo == 0){
|
||||
y += 8;
|
||||
var infoEntry = getInfoEntry();
|
||||
var infoStr = infoEntry[0];
|
||||
var infoImg = infoEntry[1];
|
||||
var printImgLeft = infoEntry[2] == "left";
|
||||
|
||||
// Show large or small time depending on info entry
|
||||
if(infoStr == null){
|
||||
y += 10;
|
||||
g.setLargeFont();
|
||||
} else {
|
||||
g.setMediumFont();
|
||||
}
|
||||
|
||||
g.drawString(timeStr, W/2, y);
|
||||
|
||||
// Draw info or timer
|
||||
y += H/5*2-5;
|
||||
g.setFontAlign(0,0);
|
||||
if(showInfo > 0){
|
||||
g.setSmallFont();
|
||||
|
||||
var infoStr = "";
|
||||
var infoImg;
|
||||
if(showInfo == 100){
|
||||
infoStr = getAlarmMinutes() + " min.";
|
||||
infoImg = imgTimer;
|
||||
} else if(showInfo == 101){
|
||||
infoStr = E.getBattery() + "%";
|
||||
infoImg = imgCharging;
|
||||
} else if (showInfo == 1){
|
||||
infoStr = E.getBattery() + "%";
|
||||
infoImg = imgBattery;
|
||||
} else if (showInfo == 2){
|
||||
infoStr = getSteps()
|
||||
infoStr = Math.round(infoStr/100) / 10; // This ensures that we do not show e.g. 15.0k and 15k instead
|
||||
infoStr = infoStr + "k";
|
||||
infoImg = imgSteps;
|
||||
} else if (showInfo == 3){
|
||||
infoStr = Math.round(Bangle.getHealthStatus("day").bpm) + " bpm";
|
||||
infoImg = imgBpm;
|
||||
} else if (showInfo == 4){
|
||||
var weather = getWeather();
|
||||
infoStr = weather.temp;
|
||||
infoImg = imgTemperature;
|
||||
} else if (showInfo == 5){
|
||||
var weather = getWeather();
|
||||
infoStr = weather.wind;
|
||||
infoImg = imgWind;
|
||||
}
|
||||
|
||||
var imgWidth = 0;
|
||||
if(infoImg !== undefined){
|
||||
imgWidth = infoImg.width;
|
||||
var strWidth = g.stringWidth(infoStr);
|
||||
g.drawImage(infoImg, W/2 - strWidth/2 - infoImg.width/2 - 5, y - infoImg.height/2);
|
||||
}
|
||||
g.drawString(infoStr, W/2 + imgWidth/2, y+3);
|
||||
// Draw info if set
|
||||
if(infoStr == null){
|
||||
return;
|
||||
}
|
||||
|
||||
// Draw lock
|
||||
y += H/5*2-5;
|
||||
g.setFontAlign(0,0);
|
||||
g.setSmallFont();
|
||||
var imgWidth = 0;
|
||||
if(infoImg !== undefined){
|
||||
imgWidth = infoImg.width;
|
||||
var strWidth = g.stringWidth(infoStr);
|
||||
g.drawImage(
|
||||
infoImg,
|
||||
W/2 + (printImgLeft ? -strWidth/2-2 : strWidth/2+2) - infoImg.width/2,
|
||||
y - infoImg.height/2
|
||||
);
|
||||
}
|
||||
g.drawString(infoStr, printImgLeft ? W/2 + imgWidth/2 + 2 : W/2 - imgWidth/2 - 2, y+3);
|
||||
}
|
||||
|
||||
|
||||
function drawLock(){
|
||||
if(settings.showLock && Bangle.isLocked()){
|
||||
g.setColor(g.theme.fg);
|
||||
g.drawImage(imgLock, W-16, 2);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw widgets if not fullscreen
|
||||
|
||||
function drawWidgets(){
|
||||
if(settings.fullscreen){
|
||||
for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";}
|
||||
} else {
|
||||
|
@ -337,6 +352,27 @@ function draw() {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* Draw timeout
|
||||
*/
|
||||
// timeout used to update every minute
|
||||
var drawTimeout;
|
||||
|
||||
// schedule a draw for the next minute
|
||||
function queueDraw() {
|
||||
if (drawTimeout) clearTimeout(drawTimeout);
|
||||
drawTimeout = setTimeout(function() {
|
||||
drawTimeout = undefined;
|
||||
draw();
|
||||
}, 60000 - (Date.now() % 60000));
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Load clock, widgets and listen for events
|
||||
*/
|
||||
Bangle.loadWidgets();
|
||||
|
||||
// Clear the screen once, at startup and set the correct theme.
|
||||
|
@ -381,32 +417,34 @@ Bangle.on('touch', function(btn, e){
|
|||
if(is_upper){
|
||||
Bangle.buzz(40, 0.6);
|
||||
increaseAlarm();
|
||||
draw();
|
||||
drawTime();
|
||||
}
|
||||
|
||||
if(is_lower){
|
||||
Bangle.buzz(40, 0.6);
|
||||
decreaseAlarm();
|
||||
draw();
|
||||
drawTime();
|
||||
}
|
||||
|
||||
var maxInfo = 6;
|
||||
if(is_right){
|
||||
Bangle.buzz(40, 0.6);
|
||||
settings.showInfo = (settings.showInfo+1) % maxInfo;
|
||||
storage.write(SETTINGS_FILE, settings);
|
||||
draw();
|
||||
settings.showInfo = (settings.showInfo+1) % NUM_INFO;
|
||||
drawTime();
|
||||
}
|
||||
|
||||
if(is_left){
|
||||
Bangle.buzz(40, 0.6);
|
||||
settings.showInfo = settings.showInfo-1;
|
||||
settings.showInfo = settings.showInfo < 0 ? maxInfo-1 : settings.showInfo;
|
||||
storage.write(SETTINGS_FILE, settings);
|
||||
draw();
|
||||
settings.showInfo = settings.showInfo < 0 ? NUM_INFO-1 : settings.showInfo;
|
||||
drawTime();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
E.on("kill", function(){
|
||||
storage.write(SETTINGS_FILE, settings);
|
||||
});
|
||||
|
||||
|
||||
// Show launcher when middle button pressed
|
||||
Bangle.setUI("clock");
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"id": "bwclk",
|
||||
"name": "BlackWhite Clock",
|
||||
"version": "0.05",
|
||||
"description": "Black and white clock.",
|
||||
"name": "BW Clock",
|
||||
"version": "0.06",
|
||||
"description": "BW Clock.",
|
||||
"readme": "README.md",
|
||||
"icon": "app.png",
|
||||
"screenshots": [{"url":"screenshot.png"}, {"url":"screenshot_2.png"}],
|
||||
"screenshots": [{"url":"screenshot.png"}, {"url":"screenshot_2.png"}, {"url":"screenshot_3.png"}],
|
||||
"type": "clock",
|
||||
"tags": "clock",
|
||||
"supports": ["BANGLEJS2"],
|
||||
|
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 3.2 KiB |
|
@ -18,7 +18,7 @@
|
|||
|
||||
|
||||
E.showMenu({
|
||||
'': { 'title': 'BlackWhite Clock' },
|
||||
'': { 'title': 'BW Clock' },
|
||||
'< Back': back,
|
||||
'Fullscreen': {
|
||||
value: settings.fullscreen,
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
0.01: New App!
|
||||
0.02: Support Bangle.js 2
|
||||
|
|
|
@ -0,0 +1,209 @@
|
|||
//g.setTheme({fg : 0xFFFF, fg2 : 0xFFFF,bg2 : 0x0007,fgH : 0xFFFF,bgH : 0x02F7,dark : true});
|
||||
|
||||
|
||||
/* Choozi - Choose people or things at random using Bangle.js.
|
||||
* Inspired by the "Chwazi" Android app
|
||||
*
|
||||
* James Stanley 2021
|
||||
*/
|
||||
|
||||
var colours = ['#ff0000', '#ff8080', '#00ff00', '#80ff80', '#0000ff', '#8080ff', '#ffff00', '#00ffff', '#ff00ff', '#ff8000', '#ff0080', '#8000ff', '#0080ff'];
|
||||
|
||||
var stepAngle = 0.18; // radians - resolution of polygon
|
||||
var gapAngle = 0.035; // radians - gap between segments
|
||||
var perimMin = 80; // px - min. radius of perimeter
|
||||
var perimMax = 87; // px - max. radius of perimeter
|
||||
|
||||
var segmentMax = 70; // px - max radius of filled-in segment
|
||||
var segmentStep = 5; // px - step size of segment fill animation
|
||||
var circleStep = 4; // px - step size of circle fill animation
|
||||
|
||||
// rolling ball animation:
|
||||
var maxSpeed = 0.08; // rad/sec
|
||||
var minSpeed = 0.001; // rad/sec
|
||||
var animStartSteps = 300; // how many steps before it can start slowing?
|
||||
var accel = 0.0002; // rad/sec/sec - acc-/deceleration rate
|
||||
var ballSize = 3; // px - ball radius
|
||||
var ballTrack = 75; // px - radius of ball path
|
||||
|
||||
var centreX = 88; // px - centre of screen
|
||||
var centreY = 88; // px - centre of screen
|
||||
|
||||
var fontSize = 50; // px
|
||||
|
||||
var radians = 2*Math.PI; // radians per circle
|
||||
|
||||
var defaultN = 3; // default value for N
|
||||
var minN = 2;
|
||||
var maxN = colours.length;
|
||||
var N;
|
||||
var arclen;
|
||||
|
||||
// https://www.frankmitchell.org/2015/01/fisher-yates/
|
||||
function shuffle (array) {
|
||||
var i = 0
|
||||
, j = 0
|
||||
, temp = null;
|
||||
|
||||
for (i = array.length - 1; i > 0; i -= 1) {
|
||||
j = Math.floor(Math.random() * (i + 1));
|
||||
temp = array[i];
|
||||
array[i] = array[j];
|
||||
array[j] = temp;
|
||||
}
|
||||
}
|
||||
|
||||
// draw an arc between radii minR and maxR, and between
|
||||
// angles minAngle and maxAngle
|
||||
function arc(minR, maxR, minAngle, maxAngle) {
|
||||
var step = stepAngle;
|
||||
var angle = minAngle;
|
||||
var inside = [];
|
||||
var outside = [];
|
||||
var c, s;
|
||||
while (angle < maxAngle) {
|
||||
c = Math.cos(angle);
|
||||
s = Math.sin(angle);
|
||||
inside.push(centreX+c*minR); // x
|
||||
inside.push(centreY+s*minR); // y
|
||||
// outside coordinates are built up in reverse order
|
||||
outside.unshift(centreY+s*maxR); // y
|
||||
outside.unshift(centreX+c*maxR); // x
|
||||
angle += step;
|
||||
}
|
||||
c = Math.cos(maxAngle);
|
||||
s = Math.sin(maxAngle);
|
||||
inside.push(centreX+c*minR);
|
||||
inside.push(centreY+s*minR);
|
||||
outside.unshift(centreY+s*maxR);
|
||||
outside.unshift(centreX+c*maxR);
|
||||
|
||||
var vertices = inside.concat(outside);
|
||||
g.fillPoly(vertices, true);
|
||||
}
|
||||
|
||||
// draw the arc segments around the perimeter
|
||||
function drawPerimeter() {
|
||||
g.clear();
|
||||
for (var i = 0; i < N; i++) {
|
||||
g.setColor(colours[i%colours.length]);
|
||||
var minAngle = (i/N)*radians;
|
||||
arc(perimMin,perimMax,minAngle,minAngle+arclen);
|
||||
}
|
||||
}
|
||||
|
||||
// animate a ball rolling around and settling at "target" radians
|
||||
function animateChoice(target) {
|
||||
var angle = 0;
|
||||
var speed = 0;
|
||||
var oldx = -10;
|
||||
var oldy = -10;
|
||||
var decelFromAngle = -1;
|
||||
var allowDecel = false;
|
||||
for (var i = 0; true; i++) {
|
||||
angle = angle + speed;
|
||||
if (angle > radians) angle -= radians;
|
||||
if (i < animStartSteps || (speed < maxSpeed && !allowDecel)) {
|
||||
speed = speed + accel;
|
||||
if (speed > maxSpeed) {
|
||||
speed = maxSpeed;
|
||||
/* when we reach max speed, we know how long it takes
|
||||
* to accelerate, and therefore how long to decelerate, so
|
||||
* we can work out what angle to start decelerating from */
|
||||
if (decelFromAngle < 0) {
|
||||
decelFromAngle = target-angle;
|
||||
while (decelFromAngle < 0) decelFromAngle += radians;
|
||||
while (decelFromAngle > radians) decelFromAngle -= radians;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!allowDecel && (angle < decelFromAngle) && (angle+speed >= decelFromAngle)) allowDecel = true;
|
||||
if (allowDecel) speed = speed - accel;
|
||||
if (speed < minSpeed) speed = minSpeed;
|
||||
if (speed == minSpeed && angle < target && angle+speed >= target) return;
|
||||
}
|
||||
|
||||
var r = i/2;
|
||||
if (r > ballTrack) r = ballTrack;
|
||||
var x = centreX+Math.cos(angle)*r;
|
||||
var y = centreY+Math.sin(angle)*r;
|
||||
g.setColor('#000000');
|
||||
g.fillCircle(oldx,oldy,ballSize+1);
|
||||
g.setColor('#ffffff');
|
||||
g.fillCircle(x, y, ballSize);
|
||||
oldx=x;
|
||||
oldy=y;
|
||||
}
|
||||
}
|
||||
|
||||
// choose a winning segment and animate its selection
|
||||
function choose() {
|
||||
var chosen = Math.floor(Math.random()*N);
|
||||
var minAngle = (chosen/N)*radians;
|
||||
var maxAngle = minAngle + arclen;
|
||||
animateChoice((minAngle+maxAngle)/2);
|
||||
g.setColor(colours[chosen%colours.length]);
|
||||
for (var i = segmentMax-segmentStep; i >= 0; i -= segmentStep)
|
||||
arc(i, perimMax, minAngle, maxAngle);
|
||||
arc(0, perimMax, minAngle, maxAngle);
|
||||
for (var r = 1; r < segmentMax; r += circleStep)
|
||||
g.fillCircle(centreX,centreY,r);
|
||||
g.fillCircle(centreX,centreY,segmentMax);
|
||||
}
|
||||
|
||||
// draw the current value of N in the middle of the screen, with
|
||||
// up/down arrows
|
||||
function drawN() {
|
||||
g.setColor('#000000');
|
||||
g.setFont("Vector",fontSize);
|
||||
g.drawString(N,centreX-g.stringWidth(N)/2+4,centreY-fontSize/2);
|
||||
if (N < maxN)
|
||||
g.fillPoly([centreX-6,centreY-fontSize/2-7, centreX+6,centreY-fontSize/2-7, centreX, centreY-fontSize/2-14]);
|
||||
if (N > minN)
|
||||
g.fillPoly([centreX-6,centreY+fontSize/2+5, centreX+6,centreY+fontSize/2+5, centreX, centreY+fontSize/2+12]);
|
||||
}
|
||||
|
||||
// update number of segments, with min/max limit, "arclen" update,
|
||||
// and screen reset
|
||||
function setN(n) {
|
||||
N = n;
|
||||
if (N < minN) N = minN;
|
||||
if (N > maxN) N = maxN;
|
||||
arclen = radians/N - gapAngle;
|
||||
drawPerimeter();
|
||||
}
|
||||
|
||||
// save N to choozi.txt
|
||||
function writeN() {
|
||||
var file = require("Storage").open("choozi.txt","w");
|
||||
file.write(N);
|
||||
}
|
||||
|
||||
// load N from choozi.txt
|
||||
function readN() {
|
||||
var file = require("Storage").open("choozi.txt","r");
|
||||
var n = file.readLine();
|
||||
if (n !== undefined) setN(parseInt(n));
|
||||
else setN(defaultN);
|
||||
}
|
||||
|
||||
shuffle(colours); // is this really best?
|
||||
Bangle.setLCDTimeout(0); // keep screen on
|
||||
readN();
|
||||
drawN();
|
||||
|
||||
setWatch(() => {
|
||||
writeN();
|
||||
drawPerimeter();
|
||||
choose();
|
||||
}, BTN1, {repeat:true});
|
||||
|
||||
Bangle.on('touch', function(zone,e) {
|
||||
if(e.x>+88){
|
||||
setN(N-1);
|
||||
drawN();
|
||||
}else{
|
||||
setN(N+1);
|
||||
drawN();
|
||||
}
|
||||
});
|
|
@ -1,16 +1,17 @@
|
|||
{
|
||||
"id": "choozi",
|
||||
"name": "Choozi",
|
||||
"version": "0.01",
|
||||
"version": "0.02",
|
||||
"description": "Choose people or things at random using Bangle.js.",
|
||||
"icon": "app.png",
|
||||
"tags": "tool",
|
||||
"supports": ["BANGLEJS"],
|
||||
"supports": ["BANGLEJS","BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
"allow_emulator": true,
|
||||
"screenshots": [{"url":"bangle1-choozi-screenshot1.png"},{"url":"bangle1-choozi-screenshot2.png"}],
|
||||
"storage": [
|
||||
{"name":"choozi.app.js","url":"app.js"},
|
||||
{"name":"choozi.app.js","url":"app.js","supports": ["BANGLEJS"]},
|
||||
{"name":"choozi.app.js","url":"appb2.js","supports": ["BANGLEJS2"]},
|
||||
{"name":"choozi.img","url":"app-icon.js","evaluate":true}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -244,7 +244,7 @@ function run(){
|
|||
Bangle.setLCDMode();
|
||||
g.clear();
|
||||
g.flip();
|
||||
E.showMessage("Loading...");
|
||||
E.showMessage(/*LANG*/"Loading...");
|
||||
load(app.src);
|
||||
}
|
||||
|
||||
|
|
|
@ -9,3 +9,4 @@
|
|||
0.09: fix the trasparent widget bar if there are no widgets for Bangle 2
|
||||
0.10: added "one click exit" setting for Bangle 2
|
||||
0.11: Fix bangle.js 1 white icons not displaying
|
||||
0.12: On Bangle 2 change to swiping up/down to move between pages as to match page indicator. Swiping from left to right now loads the clock.
|
||||
|
|
|
@ -85,18 +85,25 @@ function drawPage(p){
|
|||
g.flip();
|
||||
}
|
||||
|
||||
Bangle.on("swipe",(dir)=>{
|
||||
Bangle.on("swipe",(dirLeftRight, dirUpDown)=>{
|
||||
selected = 0;
|
||||
oldselected=-1;
|
||||
if (dir<0){
|
||||
if (dirUpDown==-1){
|
||||
++page; if (page>maxPage) page=0;
|
||||
drawPage(page);
|
||||
} else {
|
||||
} else if (dirUpDown==1){
|
||||
--page; if (page<0) page=maxPage;
|
||||
drawPage(page);
|
||||
}
|
||||
if (dirLeftRight==1) showClock();
|
||||
});
|
||||
|
||||
function showClock(){
|
||||
var app = require("Storage").readJSON('setting.json', 1).clock;
|
||||
if (app) load(app);
|
||||
else E.showMessage("clock\nnot found");
|
||||
}
|
||||
|
||||
function isTouched(p,n){
|
||||
if (n<0 || n>3) return false;
|
||||
var x1 = (n%2)*72+XOFF; var y1 = n>1?72+YOFF:YOFF;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "dtlaunch",
|
||||
"name": "Desktop Launcher",
|
||||
"version": "0.11",
|
||||
"version": "0.12",
|
||||
"description": "Desktop style App Launcher with six (four for Bangle 2) apps per page - fast access if you have lots of apps installed.",
|
||||
"screenshots": [{"url":"shot1.png"},{"url":"shot2.png"},{"url":"shot3.png"}],
|
||||
"icon": "icon.png",
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
0.01: New App! Very limited course support.
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2022 Jason Dekarske
|
||||
|
||||
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.
|
|
@ -0,0 +1,33 @@
|
|||
# Golf View
|
||||
|
||||
This app leverages open source map data to give you a birds eye view of your golf game! See a preview of any hole as well as your realtime distance to the green and position on the hole.
|
||||
|
||||

|
||||
|
||||
## Usage
|
||||
|
||||
Swipe left and right to select your hole. Use the GPS assist app to get a faster GPS fix.
|
||||
|
||||
## Contributions
|
||||
|
||||
The performance of this app depends on the accuracy and consistency of user-submitted maps.
|
||||
|
||||
- See official mapping guidelines [here](https://wiki.openstreetmap.org/wiki/Tag:leisure%3Dgolf_course).
|
||||
- All holes and features must be within the target course's area.
|
||||
- Supported features are greens, fairways, tees, bunkers, water hazards and holes.
|
||||
- All features for a given hole should have the "ref" tag with the hole number as value. Shared features should list ref values separated by ';'. [example](https://www.openstreetmap.org/way/36896320).
|
||||
- here must be 18 holes and they must have the following tags: handicap, par, ref, dist.
|
||||
- For any mapping assistance or issues, please file in the <a href="https://github.com/espruino/BangleApps/issues/new?assignees=&labels=bug&template=bangle-bug-report-custom-form.yaml&title=[golfview]+Short+description+of+bug">official repo</a>.
|
||||
|
||||
[Example Course](https://www.openstreetmap.org/way/25447898)
|
||||
## Controls
|
||||
|
||||
Swipe to change holes and tap to see a green closeup.
|
||||
|
||||
## Requests/Creator
|
||||
|
||||
[Jason Dekarske](https://github.com/jdekarske)
|
||||
|
||||
## Attribution
|
||||
|
||||
[© OpenStreetMap contributors](https://www.openstreetmap.org/copyright)
|
|
@ -0,0 +1,154 @@
|
|||
<html>
|
||||
|
||||
<head>
|
||||
<link rel="stylesheet" href="../../css/spectre.min.css">
|
||||
<script src="https://cdn.jsdelivr.net/gh/mourner/simplify-js@1.2.4/simplify.min.js"></script>
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"
|
||||
integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div>
|
||||
<input type="text" placeholder="Course ID" id="course_id">
|
||||
<button type="button" onclick="courseSearch();">Search</button>
|
||||
<p id="status"></p>
|
||||
<div>
|
||||
<button id="upload" class="btn btn-primary" disabled="true">Upload to Device</button>
|
||||
<button id="download" class="btn btn-primary" disabled="true">Download Course</button>
|
||||
</div>
|
||||
<p>A course needs a few things to be parsed correctly by this tool.</p>
|
||||
<ul>
|
||||
<li>See official mapping guidelines <a
|
||||
href="https://wiki.openstreetmap.org/wiki/Tag:leisure%3Dgolf_course">here</a>.</li>
|
||||
<li>All holes and features must be within the target course's area.</li>
|
||||
<li>Supported features are greens, fairways, tees, bunkers, water hazards and holes.</li>
|
||||
<li>All features for a given hole should have the "ref" tag with the hole number as value. Shared features should
|
||||
list ref values separated by ';'. <a href="https://www.openstreetmap.org/way/36896320">example</a>.</li>
|
||||
<li>There must be 18 holes and they must have the following tags: handicap, par, ref, dist</li>
|
||||
<li>For any mapping assistance or issues, please file in the <a
|
||||
href="https://github.com/espruino/BangleApps/issues/new?assignees=&labels=bug&template=bangle-bug-report-custom-form.yaml&title=[golfview]+Short+description+of+bug">official
|
||||
repo</a></li>
|
||||
</ul>
|
||||
<a href="https://www.openstreetmap.org/way/25447898">Example Course</a>
|
||||
<a href="https://www.openstreetmap.org/copyright">© OpenStreetMap contributors</p>
|
||||
</div>
|
||||
|
||||
<script src="../../core/lib/customize.js"></script>
|
||||
<script src="./maptools.js"></script>
|
||||
|
||||
<script>
|
||||
const url = "https://overpass-api.de/api/interpreter";
|
||||
let query = `[out:json][timeout:5];way(25447898);map_to_area ->.golfcourse;way["golf"="hole"](area.golfcourse)->.holes;(relation["golf"="fairway"](area.golfcourse);way["golf"~"^(green|tee|water_hazard|bunker|fairway)"](area.golfcourse);)->.features;.holes out geom;.features out geom;`;
|
||||
let course_input = null;
|
||||
|
||||
function courseSearch() {
|
||||
let inputVal = document.getElementById("course_id").value;
|
||||
query = `[out:json][timeout:5];way(${inputVal});map_to_area ->.golfcourse;way["golf"="hole"](area.golfcourse)->.holes;(relation["golf"="fairway"](area.golfcourse);way["golf"~"^(green|tee|water_hazard|bunker|fairway)"](area.golfcourse);)->.features;.holes out geom;.features out geom;`;
|
||||
doQuery();
|
||||
}
|
||||
|
||||
function processFeatures(course_verbose) {
|
||||
let course_processed = {
|
||||
holes: {}
|
||||
};
|
||||
for (let i = 0; i < course_verbose.length; i++) {
|
||||
const element = course_verbose[i];
|
||||
|
||||
if (element.tags.golf === "hole") {
|
||||
// if we find a high-level hole feature
|
||||
// todo check if hole exists
|
||||
let current_hole = parseInt(element.tags.ref); //subsequent way features should be applied to the current hole
|
||||
let tees = []
|
||||
Object.keys(element.tags).forEach((key) => {
|
||||
if (key.includes("dist")) {
|
||||
tees.push(Math.round(element.tags[key]));
|
||||
}
|
||||
})
|
||||
var hole = {
|
||||
hole_number: current_hole,
|
||||
handicap: parseInt(element.tags.handicap),
|
||||
par: parseInt(element.tags.par),
|
||||
nodesXY: preprocessCoords(element.geometry, element.geometry[0]),
|
||||
tees: tees.sort(),
|
||||
way: element.geometry,
|
||||
features: [],
|
||||
angle: 0,
|
||||
}
|
||||
|
||||
hole.angle = angle(hole.nodesXY[0], hole.nodesXY[hole.nodesXY.length - 1])
|
||||
course_processed.holes[current_hole.toString()] = hole;
|
||||
}
|
||||
|
||||
else {
|
||||
if (!("ref" in element.tags)) continue;
|
||||
if (element.type === "relation") {
|
||||
for (member of element.members) {
|
||||
if (member.role === "outer") break; // only use the outer because it is overwritten anyway
|
||||
}
|
||||
Object.assign(element, { "geometry": member.geometry });
|
||||
}
|
||||
// if we find a feature add it to the corresponding hole
|
||||
let active_holes = element.tags.ref.split(";"); // a feature can be on more than one hole
|
||||
for (feature_hole of active_holes) {
|
||||
let new_feature = {
|
||||
nodesXY: preprocessCoords(element.geometry, course_processed.holes[feature_hole].way[0]),
|
||||
type: element.tags.golf,
|
||||
id: element.id,
|
||||
}
|
||||
course_processed.holes[feature_hole].features.push(new_feature);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return course_processed;
|
||||
}
|
||||
|
||||
function preprocessCoords(coord_array, origin) {
|
||||
let many_points = arraytoXY(coord_array, origin);
|
||||
let less_points = simplify(many_points, 2, true); // from simplify-js
|
||||
|
||||
// convert to int to save some memory
|
||||
less_points = less_points.map(function (pt) {
|
||||
return { x: Math.round(pt.x), y: Math.round(pt.y) }
|
||||
});
|
||||
|
||||
return less_points;
|
||||
}
|
||||
|
||||
var courses = [];
|
||||
var course_name = "Davis";
|
||||
$("#upload").click(function () {
|
||||
sendCustomizedApp({
|
||||
storage: courses,
|
||||
});
|
||||
});
|
||||
|
||||
$("#download").click(function () {
|
||||
downloadObjectAsJSON(courses[0].content, "golfcourse-" + course_name);
|
||||
});
|
||||
|
||||
// download info from the course
|
||||
function doQuery() {
|
||||
$.post(url, query, function (result) {
|
||||
if (result.elements.length === 0) {
|
||||
$('#status').text("Course not found!");
|
||||
return;
|
||||
}
|
||||
course_input = result;
|
||||
console.log(course_input);
|
||||
out = processFeatures(course_input.elements);
|
||||
console.log(out);
|
||||
courses.push({
|
||||
name: "golfcourse-" + course_name + ".json",
|
||||
content: JSON.stringify(out),
|
||||
});
|
||||
$('#status').text("Course retrieved!");
|
||||
$('#upload').attr("disabled", false);
|
||||
$('#download').attr("disabled", false);
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("lEowcF23btoCRpMkyVKBxHayQOCpNbBw/UBwkrBw21BYQCCkoOIy/JCIetI4+X+QgEBxNIBxFqBxFWBxAsEBxHpBYPbsgOFqQOEy3btgOLDoIOrrYOKMoQOKOgYsKBx1KBwaGBWYgOBkVJBwKGBYQwOBiVJlIdCkmVBxdZfwwOGF4IONkoLCB2J3BBxkgQwQOKWYlJlYOUtQORtskzQOHtoOE7QOLAQdbBw21BydKBYgCD6gODBYwCNA"))
|
|
@ -0,0 +1,212 @@
|
|||
// maptools.js
|
||||
const EARTHRADIUS = 6371000; //km
|
||||
|
||||
function radians(a) {
|
||||
return a * Math.PI / 180;
|
||||
}
|
||||
|
||||
function degrees(a) {
|
||||
let d = a * 180 / Math.PI;
|
||||
return (d + 360) % 360;
|
||||
}
|
||||
|
||||
function toXY(a, origin) {
|
||||
let pt = {
|
||||
x: 0,
|
||||
y: 0
|
||||
};
|
||||
|
||||
pt.x = EARTHRADIUS * radians(a.lon - origin.lon) * Math.cos(radians((a.lat + origin.lat) / 2));
|
||||
pt.y = EARTHRADIUS * radians(origin.lat - a.lat);
|
||||
return pt;
|
||||
}
|
||||
|
||||
function arraytoXY(array, origin) {
|
||||
let out = [];
|
||||
for (var j in array) {
|
||||
let newpt = toXY(array[j], origin);
|
||||
out.push(newpt);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function angle(a, b) {
|
||||
let x = b.x - a.x;
|
||||
let y = b.y - a.y;
|
||||
return Math.atan2(-y, x);
|
||||
}
|
||||
|
||||
function rotateVec(a, theta) {
|
||||
let pt = {
|
||||
x: 0,
|
||||
y: 0
|
||||
};
|
||||
c = Math.cos(theta);
|
||||
s = Math.sin(theta);
|
||||
pt.x = c * a.x - s * a.y;
|
||||
pt.y = s * a.x + c * a.y;
|
||||
return pt;
|
||||
}
|
||||
|
||||
function distance(a, b) {
|
||||
return Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2));
|
||||
}
|
||||
|
||||
|
||||
// golfview.js
|
||||
let course = require("Storage").readJSON("golfcourse-Davis.json").holes;//TODO use the course ID
|
||||
let current_hole = 1;
|
||||
let hole = course[current_hole.toString()];
|
||||
let user_position = {
|
||||
fix: false,
|
||||
lat: 0,
|
||||
lon: 0,
|
||||
x: 0,
|
||||
y: 0,
|
||||
to_hole: 0,
|
||||
last_time: getTime(),
|
||||
transform: {},
|
||||
};
|
||||
|
||||
function drawUser() {
|
||||
if(!user_position.fix) return;
|
||||
let new_pos = g.transformVertices([user_position.x,user_position.y],user_position.transform);
|
||||
g.setColor(g.theme.fg);
|
||||
g.drawCircle(new_pos[0],new_pos[1],8);
|
||||
}
|
||||
|
||||
function drawHole(l) {
|
||||
|
||||
//console.log(l);
|
||||
let hole_straight_distance = distance(
|
||||
hole.nodesXY[0],
|
||||
hole.nodesXY[hole.nodesXY.length - 1]
|
||||
);
|
||||
|
||||
let scale = 0.9 * l.h / hole_straight_distance;
|
||||
|
||||
let transform = {
|
||||
x: l.x + l.w / 2, // center in the box
|
||||
y: l.h * 0.95, // pad it just a bit TODO use the extent of the teeboxes/green
|
||||
scale: scale, // scale factor (default 1)
|
||||
rotate: hole.angle - Math.PI / 2.0, // angle in radians (default 0)
|
||||
};
|
||||
|
||||
user_position.transform = transform;
|
||||
|
||||
// draw the fairways first
|
||||
hole.features.sort((a, b) => {
|
||||
if (a.type === "fairway") {
|
||||
return -1;
|
||||
}
|
||||
});
|
||||
|
||||
for (var feature of hole.features) {
|
||||
//console.log(Object.keys(feature));
|
||||
if (feature.type === "fairway") {
|
||||
g.setColor(1, 0, 1); // magenta
|
||||
} else if (feature.type === "tee") {
|
||||
g.setColor(1, 0, 0); // red
|
||||
} else if (feature.type === "green") {
|
||||
g.setColor(0, 1, 0); // green
|
||||
} else if (feature.type === "bunker") {
|
||||
g.setColor(1, 1, 0); // yellow
|
||||
} else if (feature.type === "water_hazard") {
|
||||
g.setColor(0, 0, 1); // blue
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
var nodelist = [];
|
||||
feature.nodesXY.forEach(function (node) {
|
||||
nodelist.push(node.x);
|
||||
nodelist.push(node.y);
|
||||
});
|
||||
newnodelist = g.transformVertices(nodelist, transform);
|
||||
|
||||
g.fillPoly(newnodelist, true);
|
||||
//console.log(feature.type);
|
||||
//console.log(newnodelist);
|
||||
|
||||
drawUser();
|
||||
}
|
||||
|
||||
var waynodelist = [];
|
||||
hole.nodesXY.forEach(function (node) {
|
||||
waynodelist.push(node.x);
|
||||
waynodelist.push(node.y);
|
||||
});
|
||||
|
||||
newnodelist = g.transformVertices(waynodelist, transform);
|
||||
g.setColor(0, 1, 1); // cyan
|
||||
g.drawPoly(newnodelist);
|
||||
}
|
||||
|
||||
function setHole(current_hole) {
|
||||
layout.hole.label = "HOLE " + current_hole;
|
||||
layout.par.label = "PAR " + course[current_hole.toString()].par;
|
||||
layout.hcp.label = "HCP " + course[current_hole.toString()].handicap;
|
||||
layout.postyardage.label = course[current_hole.toString()].tees[course[current_hole.toString()].tees.length - 1]; //TODO only use longest hole for now
|
||||
|
||||
g.clear();
|
||||
layout.render();
|
||||
}
|
||||
|
||||
function updateDistanceToHole() {
|
||||
let xy = toXY({ "lat": user_position.lat, "lon": user_position.lon }, hole.way[0]);
|
||||
user_position.x = xy.x;
|
||||
user_position.y = xy.y;
|
||||
user_position.last_time = getTime();
|
||||
let new_distance = Math.round(distance(xy, hole.nodesXY[hole.nodesXY.length - 1]) * 1.093613); //TODO meters later
|
||||
//console.log(new_distance);
|
||||
layout.measyardage.label = (new_distance < 999) ? new_distance : "---";
|
||||
|
||||
g.clear();
|
||||
layout.render();
|
||||
}
|
||||
|
||||
Bangle.on('swipe', function (direction) {
|
||||
if (direction > 0) {
|
||||
current_hole--;
|
||||
} else {
|
||||
current_hole++;
|
||||
}
|
||||
|
||||
if (current_hole > 18) { current_hole = 1; }
|
||||
if (current_hole < 1) { current_hole = 18; }
|
||||
hole = course[current_hole.toString()];
|
||||
|
||||
setHole(current_hole);
|
||||
});
|
||||
|
||||
Bangle.on('GPS', (fix) => {
|
||||
if (isNaN(fix.lat)) return;
|
||||
//console.log(fix.hdop * 5); //precision
|
||||
user_position.fix = true;
|
||||
user_position.lat = fix.lat;
|
||||
user_position.lon = fix.lon;
|
||||
updateDistanceToHole();
|
||||
drawUser();
|
||||
});
|
||||
|
||||
// The layout, referencing the custom renderer
|
||||
var Layout = require("Layout");
|
||||
var layout = new Layout({
|
||||
type: "h", c: [
|
||||
{
|
||||
type: "v", c: [
|
||||
{ type: "txt", font: "10%", id: "hole", label: "HOLE 18" },
|
||||
{ type: "txt", font: "10%", id: "par", label: "PAR 4" },
|
||||
{ type: "txt", font: "10%", id: "hcp", label: "HCP 18" },
|
||||
{ type: "txt", font: "35%", id: "postyardage", label: "---" },
|
||||
{ type: "txt", font: "20%", id: "measyardage", label: "---" },
|
||||
]
|
||||
},
|
||||
{ type: "custom", render: drawHole, id: "graph", bgCol: g.theme.bg, fillx: 1, filly: 1 }
|
||||
],
|
||||
lazy: true
|
||||
});
|
||||
|
||||
Bangle.setGPSPower(1);
|
||||
setHole(current_hole);
|
||||
//layout.debug();
|
After Width: | Height: | Size: 9.8 KiB |
|
@ -0,0 +1,63 @@
|
|||
const EARTHRADIUS = 6371000; //km
|
||||
|
||||
function radians(a) {
|
||||
return a * Math.PI / 180;
|
||||
}
|
||||
|
||||
function degrees(a) {
|
||||
let d = a * 180 / Math.PI;
|
||||
return (d + 360) % 360;
|
||||
}
|
||||
|
||||
function toXY(a, origin) {
|
||||
let pt = {
|
||||
x: 0,
|
||||
y: 0
|
||||
};
|
||||
|
||||
pt.x = EARTHRADIUS * radians(a.lon - origin.lon) * Math.cos(radians((a.lat + origin.lat) / 2));
|
||||
pt.y = EARTHRADIUS * radians(origin.lat - a.lat);
|
||||
return pt;
|
||||
}
|
||||
|
||||
function arraytoXY(array, origin) {
|
||||
let out = [];
|
||||
for (var j in array) {
|
||||
let newpt = toXY(array[j], origin);
|
||||
out.push(newpt);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function angle(a, b) {
|
||||
let x = b.x - a.x;
|
||||
let y = b.y - a.y;
|
||||
return Math.atan2(-y, x);
|
||||
}
|
||||
|
||||
function rotateVec(a, theta) {
|
||||
let pt = {
|
||||
x: 0,
|
||||
y: 0
|
||||
};
|
||||
c = Math.cos(theta);
|
||||
s = Math.sin(theta);
|
||||
pt.x = c * a.x - s * a.y;
|
||||
pt.y = s * a.x + c * a.y;
|
||||
return pt;
|
||||
}
|
||||
|
||||
function distance(a,b) {
|
||||
return Math.sqrt(Math.pow(a.x-b.x,2) + Math.pow(a.y-b.y,2))
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/questions/19721439/download-json-object-as-a-file-from-browser
|
||||
function downloadObjectAsJSON(exportObj, exportName) {
|
||||
var dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(exportObj); // must be stringified!!
|
||||
var downloadAnchorNode = document.createElement('a');
|
||||
downloadAnchorNode.setAttribute("href", dataStr);
|
||||
downloadAnchorNode.setAttribute("download", exportName + ".json");
|
||||
document.body.appendChild(downloadAnchorNode); // required for firefox
|
||||
downloadAnchorNode.click();
|
||||
downloadAnchorNode.remove();
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
{ "id": "golfview",
|
||||
"name": "Golf View",
|
||||
"version":"0.01",
|
||||
"description": "This app will provide you with on course data to support your golf game!",
|
||||
"icon": "golfview.png",
|
||||
"tags": "outdoors, gps",
|
||||
"allow_emulator": true,
|
||||
"supports" : ["BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
"custom": "custom.html",
|
||||
"storage": [
|
||||
{"name":"golfview.app.js","url":"golfview.js"},
|
||||
{"name":"golfview.img","url":"golfview-icon.js","evaluate":true}
|
||||
]
|
||||
}
|
After Width: | Height: | Size: 3.3 KiB |
|
@ -5,7 +5,7 @@ function satelliteImage() {
|
|||
var Layout = require("Layout");
|
||||
var layout;
|
||||
//Bangle.setGPSPower(1, "app");
|
||||
E.showMessage("Loading..."); // avoid showing rubbish on screen
|
||||
E.showMessage(/*LANG*/"Loading..."); // avoid showing rubbish on screen
|
||||
|
||||
var lastFix = {
|
||||
fix: -1,
|
||||
|
|
|
@ -126,7 +126,7 @@ function asTime(v){
|
|||
|
||||
function viewTrack(n, info) {
|
||||
if (!info) {
|
||||
E.showMessage("Loading...","GPS Track "+n);
|
||||
E.showMessage(/*LANG*/"Loading...","GPS Track "+n);
|
||||
info = getTrackInfo(n);
|
||||
}
|
||||
const menu = {
|
||||
|
|
|
@ -10,7 +10,7 @@ var Layout = require("Layout");
|
|||
Bangle.setGPSPower(1, "app");
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
E.showMessage("Loading..."); // avoid showing rubbish on screen
|
||||
E.showMessage(/*LANG*/"Loading..."); // avoid showing rubbish on screen
|
||||
|
||||
function setGPSTime() {
|
||||
if (fix.time!==undefined) {
|
||||
|
|
|
@ -70,7 +70,7 @@ function menuHRM() {
|
|||
|
||||
|
||||
function stepsPerHour() {
|
||||
E.showMessage("Loading...");
|
||||
E.showMessage(/*LANG*/"Loading...");
|
||||
var data = new Uint16Array(24);
|
||||
require("health").readDay(new Date(), h=>data[h.hr]+=h.steps);
|
||||
g.clear(1);
|
||||
|
@ -81,7 +81,7 @@ function stepsPerHour() {
|
|||
}
|
||||
|
||||
function stepsPerDay() {
|
||||
E.showMessage("Loading...");
|
||||
E.showMessage(/*LANG*/"Loading...");
|
||||
var data = new Uint16Array(31);
|
||||
require("health").readDailySummaries(new Date(), h=>data[h.day]+=h.steps);
|
||||
g.clear(1);
|
||||
|
@ -92,7 +92,7 @@ function stepsPerDay() {
|
|||
}
|
||||
|
||||
function hrmPerHour() {
|
||||
E.showMessage("Loading...");
|
||||
E.showMessage(/*LANG*/"Loading...");
|
||||
var data = new Uint16Array(24);
|
||||
var cnt = new Uint8Array(23);
|
||||
require("health").readDay(new Date(), h=>{
|
||||
|
@ -108,7 +108,7 @@ function hrmPerHour() {
|
|||
}
|
||||
|
||||
function hrmPerDay() {
|
||||
E.showMessage("Loading...");
|
||||
E.showMessage(/*LANG*/"Loading...");
|
||||
var data = new Uint16Array(31);
|
||||
var cnt = new Uint8Array(31);
|
||||
require("health").readDailySummaries(new Date(), h=>{
|
||||
|
@ -124,7 +124,7 @@ function hrmPerDay() {
|
|||
}
|
||||
|
||||
function movementPerHour() {
|
||||
E.showMessage("Loading...");
|
||||
E.showMessage(/*LANG*/"Loading...");
|
||||
var data = new Uint16Array(24);
|
||||
require("health").readDay(new Date(), h=>data[h.hr]+=h.movement);
|
||||
g.clear(1);
|
||||
|
@ -135,7 +135,7 @@ function movementPerHour() {
|
|||
}
|
||||
|
||||
function movementPerDay() {
|
||||
E.showMessage("Loading...");
|
||||
E.showMessage(/*LANG*/"Loading...");
|
||||
var data = new Uint16Array(31);
|
||||
require("health").readDailySummaries(new Date(), h=>data[h.day]+=h.movement);
|
||||
g.clear(1);
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
1.0: Initial release.
|
|
@ -0,0 +1,35 @@
|
|||
# HeartZone
|
||||
|
||||
HeartZone continuously monitors your heart rate. If your heart rate is outside of your configured limits, you get a configurable buzz.
|
||||
|
||||
Inspired by [Workout HRM](https://github.com/espruino/BangleApps/tree/master/apps/wohrm), but I wanted the following features:
|
||||
|
||||
* Larger text, more contrast, and color-coding for better readability while exercising.
|
||||
* Configurable buzz interval, instead of at every heart rate reading (which was too distracting).
|
||||
* Pause for a rest and resume afterwards without having to restart the heart rate sensor (which takes several seconds each time to stabilize).
|
||||
* Configure the minimum heart rate confidence threshold (bad readings cause buzzes that have to be ignored).
|
||||
|
||||
However, compared to Workout HRM, HeartZone doesn't support:
|
||||
|
||||
* In-app configuration of the heart rate thresholds - you can only do it in the Settings app.
|
||||
* Bangle.js 1 - this only supports Bangle.js 2.
|
||||
|
||||
## Usage
|
||||
|
||||
When you first start the app, it will begin displaying your heart rate after a few seconds. Until the heart rate confidence is above your configured minimum confidence, the background will be colored red:
|
||||
|
||||

|
||||
|
||||
After the heart rate confidence is at an acceptable level, the background will be colored white, and you will receive buzzes on your wrist while your heart rate is out of the configured range. By default, the BPM-too-low buzz is 200ms, while the BPM-too-high buzz is 1000ms:
|
||||
|
||||

|
||||
|
||||
If you're taking a break, swipe down to turn off the buzzes while continuing to measure and display your heart rate (swipe up again to end your break):
|
||||
|
||||

|
||||
|
||||
When you're done, simply press the side button to exit the app.
|
||||
|
||||
## Creator
|
||||
|
||||
[Uberi](https://github.com/Uberi)
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwkBiIA/AHqFCAxQWJ5gABCIQGGABEQB4QABgIGGC5MMCAnAAwwuOABAwIC64/FABBIIC68ADBnAVJEP+AXLBoJ2H/4XN/54GBAIXOGAouBBAMAABQXBGAoHCAB4wDFwQARGAYvWL7CPDbBXAR46/DiAXJgK/Id4URGBHABobwHEAIwIBQQuHAAcYGA3AwIUKC4eAC4sIC5+IGAnAxAXQkAXDgQXRkQwC4EiC6QwCgQXTl0M4HiC6nghwXV93uC9MRC44WOGAIXFFx4ABC4oWQiMSC4chC6MRC4YWSiMeC4PhC6cRC4IWUGAIuVAH4AVA="))
|
|
@ -0,0 +1,87 @@
|
|||
// clear screen and draw widgets
|
||||
g.clear();
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
|
||||
var statusRect = {x: Bangle.appRect.x, y: Bangle.appRect.y, w: Bangle.appRect.w, h: 32};
|
||||
var settingsRect = {x: Bangle.appRect.x, y: Bangle.appRect.y2 - 16, w: Bangle.appRect.w, h: 16};
|
||||
var hrmRect = {x: Bangle.appRect.x, y: statusRect.y + statusRect.h, w: Bangle.appRect.w, h: Bangle.appRect.h - statusRect.h - settingsRect.h};
|
||||
|
||||
var isPaused = false;
|
||||
var settings = Object.assign({
|
||||
minBpm: 120,
|
||||
maxBpm: 160,
|
||||
minConfidence: 60,
|
||||
minBuzzIntervalSeconds: 5,
|
||||
tooLowBuzzDurationMillis: 200,
|
||||
tooHighBuzzDurationMillis: 1000,
|
||||
}, require('Storage').readJSON("heartzone.settings.json", true) || {});
|
||||
|
||||
// draw current settings at the bottom
|
||||
g.setFont6x15(1).setFontAlign(0, -1, 0);
|
||||
g.drawString(settings.minBpm + "<BPM<" + settings.maxBpm + ", >=" + settings.minConfidence + "% conf.", settingsRect.x + (settingsRect.w / 2), settingsRect.y + 4);
|
||||
|
||||
function drawStatus(status) { // draw status bar at the top
|
||||
g.setBgColor(g.theme.bg).setColor(g.theme.fg);
|
||||
g.clearRect(statusRect);
|
||||
|
||||
g.setFontVector(statusRect.h - 4).setFontAlign(0, -1, 0);
|
||||
g.drawString(status, statusRect.x + (statusRect.w / 2), statusRect.y + 2);
|
||||
}
|
||||
|
||||
function drawHRM(hrmInfo) { // draw HRM info display
|
||||
g.setBgColor(hrmInfo.confidence > settings.minConfidence ? '#fff' : '#f00').setColor(hrmInfo.confidence > settings.minConfidence ? '#000' : '#fff');
|
||||
g.setFontAlign(-1, -1, 0);
|
||||
g.clearRect(hrmRect);
|
||||
|
||||
var px = hrmRect.x + 10, py = hrmRect.y + 10;
|
||||
g.setFontVector((hrmRect.h / 2) - 20);
|
||||
g.drawString(hrmInfo.bpm, px, py);
|
||||
g.setFontVector(16);
|
||||
g.drawString('BPM', px + g.stringWidth(hrmInfo.bpm.toString()) + 32, py);
|
||||
py += hrmRect.h / 2;
|
||||
|
||||
g.setFontVector((hrmRect.h / 2) - 20);
|
||||
g.drawString(hrmInfo.confidence, px, py);
|
||||
g.setFontVector(16);
|
||||
g.drawString('% conf.', px + g.stringWidth(hrmInfo.confidence.toString()) + 32, py);
|
||||
}
|
||||
|
||||
drawHRM({bpm: '?', confidence: '?'});
|
||||
drawStatus('RUNNING');
|
||||
|
||||
var lastBuzz = getTime();
|
||||
Bangle.on('HRM', function(hrmInfo) {
|
||||
if (!isPaused) {
|
||||
var currentTime;
|
||||
if (hrmInfo.confidence > settings.minConfidence) {
|
||||
if (hrmInfo.bpm < settings.minBpm) {
|
||||
currentTime = getTime();
|
||||
if (currentTime - lastBuzz > settings.minBuzzIntervalSeconds) {
|
||||
lastBuzz = currentTime;
|
||||
Bangle.buzz(settings.tooLowBuzzDurationMillis);
|
||||
}
|
||||
} else if (hrmInfo.bpm > settings.maxBpm) {
|
||||
currentTime = getTime();
|
||||
if (currentTime - lastBuzz > minBuzzIntervalSeconds) {
|
||||
lastBuzz = currentTime;
|
||||
Bangle.buzz(settings.tooHighBuzzDurationMillis);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
drawHRM(hrmInfo);
|
||||
});
|
||||
|
||||
Bangle.setUI('updown', function(action) {
|
||||
if (action == -1) { // up
|
||||
isPaused = false;
|
||||
drawStatus("RUNNING");
|
||||
} else if (action == 1) { // down
|
||||
isPaused = true;
|
||||
drawStatus("PAUSED");
|
||||
}
|
||||
});
|
||||
setWatch(function() { Bangle.setHRMPower(false, "heartzone"); load(); }, BTN1);
|
||||
|
||||
Bangle.setHRMPower(true, "heartzone");
|
After Width: | Height: | Size: 4.8 KiB |
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"id": "heartzone",
|
||||
"name": "HeartZone",
|
||||
"version": "1.0",
|
||||
"description": "Exercise app for keeping your heart rate in the aerobic zone. Buzzes the watch at configurable intervals when your heart rate is outside of configured limits.",
|
||||
"readme":"README.md",
|
||||
"screenshots": [
|
||||
{"url": "screenshots/start.png"},
|
||||
{"url": "screenshots/running.png"},
|
||||
{"url": "screenshots/paused.png"}
|
||||
],
|
||||
"icon": "icon.png",
|
||||
"tags": "health",
|
||||
"supports": ["BANGLEJS2"],
|
||||
"storage": [
|
||||
{"name":"heartzone.app.js","url":"app.js"},
|
||||
{"name":"heartzone.settings.js","url":"settings.js"},
|
||||
{"name":"heartzone.img","url":"app-icon.js","evaluate":true}
|
||||
],
|
||||
"data": [{"name":"heartzone.settings.json"}]
|
||||
}
|
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 3.3 KiB |
|
@ -0,0 +1,27 @@
|
|||
(function(back) {
|
||||
var FILE = "heartzone.settings.json";
|
||||
var settings = Object.assign({
|
||||
minBpm: 120,
|
||||
maxBpm: 160,
|
||||
minConfidence: 60,
|
||||
minBuzzIntervalSeconds: 5,
|
||||
tooLowBuzzDurationMillis: 200,
|
||||
tooHighBuzzDurationMillis: 1000,
|
||||
}, require('Storage').readJSON(FILE, true) || {});
|
||||
|
||||
function writeSettings() {
|
||||
require('Storage').writeJSON(FILE, settings);
|
||||
}
|
||||
|
||||
// Show the menu
|
||||
E.showMenu({
|
||||
"" : { "title" : "HeartZone" },
|
||||
"< Save & Return" : () => { writeSettings(); back(); },
|
||||
'Min BPM': {value: 0 | settings.minBpm, min: 80, max: 200, step: 10, onchange: v => { settings.minBpm = v; }},
|
||||
'Max BPM': {value: 0 | settings.maxBpm, min: 80, max: 200, step: 10, onchange: v => { settings.maxBpm = v; }},
|
||||
'Min % conf.': {value: 0 | settings.minConfidence, min: 30, max: 100, step: 5, onchange: v => { settings.minConfidence = v; }},
|
||||
'Min buzz int. (sec)': {value: 0 | settings.minBuzzIntervalSeconds, min: 1, max: 30, onchange: v => { settings.minBuzzIntervalSeconds = v; }},
|
||||
'BPM too low buzz (ms)': {value: 0 | settings.tooLowBuzzDurationMillis, min: 0, max: 3000, step: 100, onchange: v => { settings.tooLowBuzzDurationMillis = v; }},
|
||||
'BPM too high buzz (ms)': {value: 0 | settings.tooHighBuzzDurationMillis, min: 0, max: 3000, step: 100, onchange: v => { settings.tooHighBuzzDurationMillis = v; }},
|
||||
});
|
||||
})
|
|
@ -105,8 +105,10 @@ E.on('notify',msg=>{
|
|||
"io.robbie.HomeAssistant": "Home Assistant",
|
||||
"net.weks.prowl": "Prowl",
|
||||
"net.whatsapp.WhatsApp": "WhatsApp",
|
||||
"net.superblock.Pushover": "Pushover",
|
||||
"nl.ah.Appie": "Albert Heijn",
|
||||
"nl.postnl.TrackNTrace": "PostNL",
|
||||
"org.whispersystems.signal": "Signal",
|
||||
"ph.telegra.Telegraph": "Telegram",
|
||||
"tv.twitch": "Twitch",
|
||||
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
0.01: New App!
|
||||
0.02: Now keeps user input trace intact by changing how the screen is updated.
|
||||
|
|
|
@ -45,11 +45,39 @@ exports.getStrokes( (id,s) => Bangle.strokes[id] = Unistroke.new(s) );
|
|||
|
||||
var flashToggle = false;
|
||||
const R = Bangle.appRect;
|
||||
var Rx1;
|
||||
var Rx2;
|
||||
var Ry1;
|
||||
var Ry2;
|
||||
|
||||
function findMarker(strArr) {
|
||||
if (strArr.length == 0) {
|
||||
Rx1 = 4;
|
||||
Rx2 = 6*4;
|
||||
Ry1 = 8*4;
|
||||
Ry2 = 8*4 + 3;
|
||||
} else if (strArr.length <= 4) {
|
||||
Rx1 = (strArr[strArr.length-1].length)%7*6*4 + 4 ;
|
||||
Rx2 = (strArr[strArr.length-1].length)%7*6*4 + 6*4;
|
||||
Ry1 = (strArr.length)*(8*4) + Math.floor((strArr[strArr.length-1].length)/7)*(8*4);
|
||||
Ry2 = (strArr.length)*(8*4) + Math.floor((strArr[strArr.length-1].length)/7)*(8*4) + 3;
|
||||
} else {
|
||||
Rx1 = (strArr[strArr.length-1].length)%7*6*4 + 4 ;
|
||||
Rx2 = (strArr[strArr.length-1].length)%7*6*4 + 6*4;
|
||||
Ry1 = (4)*(8*4) + Math.floor((strArr[strArr.length-1].length)/7)*(8*4);
|
||||
Ry2 = (4)*(8*4) + Math.floor((strArr[strArr.length-1].length)/7)*(8*4) + 3;
|
||||
}
|
||||
//print(Rx1,Rx2,Ry1, Ry2);
|
||||
return {x:Rx1,y:Ry1,x2:Rx2,y2:Ry2};
|
||||
}
|
||||
|
||||
function draw(noclear) {
|
||||
g.reset();
|
||||
if (!noclear) g.clearRect(R);
|
||||
var l = g.setFont("6x8:4").wrapString(text+(flashToggle?"_":" "), R.w-8);
|
||||
var l = g.setFont("6x8:4").wrapString(text+' ', R.w-8);
|
||||
if (!l) l = [];
|
||||
//print(text+':');
|
||||
//print(l);
|
||||
if (!noclear) (flashToggle?(g.fillRect(findMarker(l))):(g.clearRect(findMarker(l))));
|
||||
if (l.length>4) l=l.slice(-4);
|
||||
g.drawString(l.join("\n"),R.x+4,R.y+4);
|
||||
}
|
||||
|
@ -80,6 +108,7 @@ exports.getStrokes( (id,s) => Bangle.strokes[id] = Unistroke.new(s) );
|
|||
var ch = o.stroke;
|
||||
if (ch=="\b") text = text.slice(0,-1);
|
||||
else text += ch;
|
||||
g.clearRect(R);
|
||||
}
|
||||
flashToggle = true;
|
||||
draw();
|
||||
|
@ -87,7 +116,7 @@ exports.getStrokes( (id,s) => Bangle.strokes[id] = Unistroke.new(s) );
|
|||
Bangle.on('stroke',strokeHandler);
|
||||
g.reset().clearRect(R);
|
||||
show();
|
||||
draw(true);
|
||||
draw(false);
|
||||
var flashInterval;
|
||||
|
||||
return new Promise((resolve,reject) => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{ "id": "kbswipe",
|
||||
"name": "Swipe keyboard",
|
||||
"version":"0.01",
|
||||
"version":"0.02",
|
||||
"description": "A library for text input via PalmOS style swipe gestures (beta!)",
|
||||
"icon": "app.png",
|
||||
"type":"textinput",
|
||||
|
|
|
@ -583,6 +583,24 @@ var locales = {
|
|||
abday: "ne,po,út,st,čt,pá,so",
|
||||
day: "neděle,pondělí,úterý,středa,čtvrtek,pátek,sobota",
|
||||
trans: { yes: "ano", Yes: "Ano", no: "ne", No: "Ne", ok: "ok", on: "zap", off: "vyp" }
|
||||
},
|
||||
"hr_HR": {
|
||||
lang: "hr_HR",
|
||||
decimal_point: ",",
|
||||
thousands_sep: ".",
|
||||
currency_symbol: "€",
|
||||
int_curr_symbol: "EUR",
|
||||
speed: "km/h",
|
||||
distance: { 0: "m", 1: "km" },
|
||||
temperature: "°C",
|
||||
ampm: { 0: "dop.", 1: "pop." },
|
||||
timePattern: { 0: "%HH:%MM:%SS", 1: "%HH:%MM" },
|
||||
datePattern: { 0: "%-d. %b %Y", 1: "%-d.%-m.%Y" }, // "3. jan. 2020" // "3.1.2020"(short)
|
||||
abmonth: "sij.,velj.,ožu.,tra.,svi,lip.,srp.,kol.,ruj.,lis.,stu.,pro.",
|
||||
month: "siječanj,veljača,ožujak,travanj,svibanj,lipanj,srpanj,kolovoz,rujan,listopad,studeni,prosinac",
|
||||
abday: "ned.,pon.,uto.,sri.,čet.,pet.,sub.",
|
||||
day: "nedjelja,ponedjeljak,utorak,srijeda,četvrtak,petak,subota",
|
||||
trans: { yes: "da", Yes: "Da", no: "ne", No: "Ne", ok: "ok", on: "Uklj.", off: "Isklj.", "< Back": "< Natrag" }
|
||||
},
|
||||
"sl_SI": {
|
||||
lang: "sl_SI",
|
||||
|
@ -662,7 +680,7 @@ var locales = {
|
|||
thousands_sep: " ",
|
||||
currency_symbol: "kr",
|
||||
int_curr_symbol: "NOK",
|
||||
speed: "kmh",
|
||||
speed: "kmt",
|
||||
distance: { 0: "m", 1: "km" },
|
||||
temperature: "°C",
|
||||
ampm: { 0: "", 1: "" },
|
||||
|
|
|
@ -20,6 +20,6 @@ const avg = [];
|
|||
setInterval(function() {
|
||||
drawTemperature();
|
||||
}, 2000);
|
||||
E.showMessage("Loading...");
|
||||
E.showMessage(/*LANG*/"Loading...");
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
|
|
|
@ -44,3 +44,4 @@
|
|||
0.29: Fix message list overwrites on Bangle.js 1 (fix #1642)
|
||||
0.30: Add new Icons (Youtube, Twitch, MS TODO, Teams, Snapchat, Signal, Post & DHL, Nina, Lieferando, Kalender, Discord, Corona Warn, Bibel)
|
||||
0.31: Option to disable icon flashing
|
||||
0.32: Added an option to allow quiet mode to override message auto-open
|
||||
|
|
|
@ -56,9 +56,16 @@ exports.pushMessage = function(event) {
|
|||
}
|
||||
// otherwise load messages/show widget
|
||||
var loadMessages = Bangle.CLOCK || event.important;
|
||||
// first, buzz
|
||||
var quiet = (require('Storage').readJSON('setting.json',1)||{}).quiet;
|
||||
var unlockWatch = (require('Storage').readJSON('messages.settings.json',1)||{}).unlockWatch;
|
||||
var appSettings = require('Storage').readJSON('messages.settings.json',1)||{};
|
||||
var unlockWatch = appSettings.unlockWatch;
|
||||
var quietNoAutOpn = appSettings.quietNoAutOpn;
|
||||
delete appSettings;
|
||||
// don't auto-open messages in quiet mode if quietNoAutOpn is true
|
||||
if(quiet && quietNoAutOpn) {
|
||||
loadMessages = false;
|
||||
}
|
||||
// first, buzz
|
||||
if (!quiet && loadMessages && global.WIDGETS && WIDGETS.messages){
|
||||
WIDGETS.messages.buzz();
|
||||
if(unlockWatch != false){
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "messages",
|
||||
"name": "Messages",
|
||||
"version": "0.31",
|
||||
"version": "0.32",
|
||||
"description": "App to display notifications from iOS and Gadgetbridge/Android",
|
||||
"icon": "app.png",
|
||||
"type": "app",
|
||||
|
|
|
@ -53,6 +53,11 @@
|
|||
format: v => v?/*LANG*/'Yes':/*LANG*/'No',
|
||||
onchange: v => updateSetting("flash", v)
|
||||
},
|
||||
/*LANG*/'Quiet mode disables auto-open': {
|
||||
value: !!settings().quietNoAutOpn,
|
||||
format: v => v?/*LANG*/'Yes':/*LANG*/'No',
|
||||
onchange: v => updateSetting("quietNoAutOpn", v)
|
||||
},
|
||||
};
|
||||
E.showMenu(mainmenu);
|
||||
})
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
0.01: New App!
|
|
@ -0,0 +1,15 @@
|
|||
Hacky app that uses Messages app and it's library to push a message that triggers the music controls. It's nearly not an app, and yet it moves.
|
||||
|
||||
This app require Messages setting 'Auto-open Music' to be 'Yes'. If it isn't, the app will change it to 'Yes' and let it stay that way.
|
||||
|
||||
Making the music controls accessible this way lets one start a music stream on the phone in some situations even though the message app didn't receive a music message from gadgetbridge to begin with. (I think.)
|
||||
|
||||
It is suggested to use Messages Music along side the app Quick Launch.
|
||||
|
||||
Messages Music v0.01 has been verified to work with Messages v0.31 on Bangle.js 2 fw2v13.
|
||||
|
||||
Music Messages should work with forks of the original Messages app. At least as long as functions pushMessage() in the library and showMusicMessage() in app.js hasn't been changed too much.
|
||||
|
||||
Messages app is created by Gordon Williams with contributions from [Jeroen Peters](https://github.com/jeroenpeters1986).
|
||||
|
||||
The icon used for this app is from [https://icons8.com](https://icons8.com).
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwhC/AFXdAAQVVDKQWHDB0NC5PQCyoYMCxZJKFxgwKCxowJC6xGOJBALE6YwRBQnf+YXPIwvf/4YKJAgXHDBQXNDBIXO/89C5YKFC4gYIC54YHC6AYGC6IYFC9JHWO6ynLa64XJ+YWGC5wWIC5oWJC4p4F74WKOwgXG6YWKC4xIFABRGFYI4uPC7JIOIw4wPCxAwNFxIYMCxZJLCxgYJCxwZGCqIA/AC4="))
|
|
@ -0,0 +1,15 @@
|
|||
let showMusic = () => {
|
||||
Bangle.CLOCK = 1; // To pass condition in messages library
|
||||
require('messages').pushMessage({"t":"add","artist":" ","album":" ","track":" ","dur":0,"c":-1,"n":-1,"id":"music","title":"Music","state":"play","new":true});
|
||||
Bangle.CLOCK = undefined;
|
||||
};
|
||||
|
||||
var settings = require('Storage').readJSON('messages.settings.json', true) || {}; //read settings if they exist else set to empty dict
|
||||
if (!settings.openMusic) {
|
||||
settings.openMusic = true; // This app/hack works as intended only if this setting is true
|
||||
require('Storage').writeJSON('messages.settings.json', settings);
|
||||
E.showMessage("First run:\n\nMessages setting\n\n 'Auto-Open Music'\n\n set to 'Yes'");
|
||||
setTimeout(()=>{showMusic();}, 5000);
|
||||
} else {
|
||||
showMusic();
|
||||
}
|
After Width: | Height: | Size: 1.2 KiB |
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"id": "messagesmusic",
|
||||
"name":"Messages Music",
|
||||
"version":"0.01",
|
||||
"description": "Uses Messages library to push a music message which in turn displays Messages app music controls",
|
||||
"icon":"app.png",
|
||||
"type": "app",
|
||||
"tags":"tool,music",
|
||||
"screenshots": [{"url":"screenshot.png"}],
|
||||
"supports": ["BANGLEJS","BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
"storage": [
|
||||
{"name":"messagesmusic.app.js","url":"app.js"},
|
||||
{"name":"messagesmusic.img","url":"app-icon.js","evaluate":true}
|
||||
],
|
||||
"dependencies": {"messages":"app"}
|
||||
|
||||
}
|
After Width: | Height: | Size: 2.3 KiB |
|
@ -0,0 +1 @@
|
|||
0.01: First release
|
|
@ -0,0 +1,14 @@
|
|||
# Mosaic Clock
|
||||
|
||||
A fabulously colourful clock!
|
||||
|
||||
* Clearly shows the time on a colourful background that changes every minute.
|
||||
* Dark and Light theme compatible, with a setting to override the digit colour scheme.
|
||||
* Show or hide widgets with a setting (default shows widgets).
|
||||
|
||||

|
||||

|
||||
|
||||
This clock is inspired by the mosaic watchface for pebble: https://apps.rebble.io/en_US/application/55386bcd2aead62b16000028
|
||||
|
||||
Written by: [Sir Indy](https://github.com/sir-indy) For support and discussion please post in the [Bangle JS Forum](http://forum.espruino.com/microcosms/1424/)
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"id":"mosaic",
|
||||
"name":"Mosaic Clock",
|
||||
"shortName": "Mosaic Clock",
|
||||
"version": "0.01",
|
||||
"description": "A fabulously colourful clock",
|
||||
"readme": "README.md",
|
||||
"icon":"mosaic.png",
|
||||
"screenshots": [{"url":"mosaic-scr1.png"},{"url":"mosaic-scr2.png"}],
|
||||
"type": "clock",
|
||||
"tags": "clock",
|
||||
"supports": ["BANGLEJS", "BANGLEJS2"],
|
||||
"allow_emulator": true,
|
||||
"storage": [
|
||||
{"name":"mosaic.app.js","url":"mosaic.app.js"},
|
||||
{"name":"mosaic.settings.js","url":"mosaic.settings.js"},
|
||||
{"name":"mosaic.img","url":"mosaic.icon.js","evaluate":true}
|
||||
]
|
||||
}
|
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 28 KiB |
|
@ -0,0 +1,103 @@
|
|||
Array.prototype.sample = function(){
|
||||
return this[Math.floor(Math.random()*this.length)];
|
||||
};
|
||||
|
||||
const SETTINGS_FILE = "mosaic.settings.json";
|
||||
let settings;
|
||||
let theme;
|
||||
let timeout = 60;
|
||||
let drawTimeout;
|
||||
let colours = [
|
||||
'#f00', '#00f', '#0f0', '#ff0', '#f0f', '#0ff',
|
||||
'#8f0', '#f08', '#f80', '#80f', '#0f8', '#08f',
|
||||
];
|
||||
let digits = [
|
||||
E.toArrayBuffer(atob("BQcB/Gtax+A=")),
|
||||
E.toArrayBuffer(atob("BQeCAX9c1zXNc1zX9A==")),
|
||||
E.toArrayBuffer(atob("BQcB/Hsbx+A=")),
|
||||
E.toArrayBuffer(atob("BQcB/Hsex+A=")),
|
||||
E.toArrayBuffer(atob("BQeCAf/zPM8D/Nc1/A==")),
|
||||
E.toArrayBuffer(atob("BQcB/G8ex+A=")),
|
||||
E.toArrayBuffer(atob("BQcB/G8ax+A=")),
|
||||
E.toArrayBuffer(atob("BQeCAf/wP81zXNc1/A==")),
|
||||
E.toArrayBuffer(atob("BQcB/Gsax+A=")),
|
||||
E.toArrayBuffer(atob("BQcB/Gsex+A="))
|
||||
];
|
||||
|
||||
function loadSettings() {
|
||||
settings = require("Storage").readJSON(SETTINGS_FILE,1)|| {'showWidgets': false, 'theme':'System'};
|
||||
}
|
||||
|
||||
function loadThemeColors() {
|
||||
theme = {fg: g.theme.fg, bg: g.theme.bg};
|
||||
if (settings.theme === "Dark") {
|
||||
theme.fg = g.toColor(1,1,1);
|
||||
theme.bg = g.toColor(0,0,0);
|
||||
}
|
||||
else if (settings.theme === "Light") {
|
||||
theme.fg = g.toColor(0,0,0);
|
||||
theme.bg = g.toColor(1,1,1);
|
||||
}
|
||||
}
|
||||
|
||||
function queueDraw(seconds) {
|
||||
let millisecs = seconds * 1000;
|
||||
if (drawTimeout) clearTimeout(drawTimeout);
|
||||
drawTimeout = setTimeout(function() {
|
||||
drawTimeout = undefined;
|
||||
draw();
|
||||
}, millisecs - (Date.now() % millisecs));
|
||||
}
|
||||
|
||||
function draw() {
|
||||
// draw colourful grid
|
||||
for (let i_x = 0; i_x < num_squares_w; i_x++) {
|
||||
for (let i_y = 0; i_y < num_squares_h; i_y++) {
|
||||
g.setColor(colours.sample()).fillRect(
|
||||
o_w+i_x*s, o_h+i_y*s, o_w+i_x*s+s, o_h+i_y*s+s
|
||||
);
|
||||
}
|
||||
}
|
||||
let t = new Date();
|
||||
g.setBgColor(theme.fg);
|
||||
g.setColor(theme.bg);
|
||||
g.drawImage(digits[Math.floor(t.getHours()/10)], (mid_x-5)*s+o_w, (mid_y-7)*s+o_h, {scale:s});
|
||||
g.drawImage(digits[t.getHours() % 10], (mid_x+1)*s+o_w, (mid_y-7)*s+o_h, {scale:s});
|
||||
g.drawImage(digits[Math.floor(t.getMinutes()/10)], (mid_x-5)*s+o_w, (mid_y+1)*s+o_h, {scale:s});
|
||||
g.drawImage(digits[t.getMinutes() % 10], (mid_x+1)*s+o_w, (mid_y+1)*s+o_h, {scale:s});
|
||||
|
||||
queueDraw(timeout);
|
||||
}
|
||||
|
||||
g.clear();
|
||||
loadSettings();
|
||||
loadThemeColors();
|
||||
|
||||
offset_widgets = settings.showWidgets ? 24 : 0;
|
||||
let available_height = g.getHeight() - offset_widgets;
|
||||
|
||||
// Calculate grid size and offsets
|
||||
let s = Math.floor(available_height/17);
|
||||
let num_squares_w = Math.round(g.getWidth()/s) - 1;
|
||||
let num_squares_h = Math.round(available_height/s) - 1;
|
||||
let o_w = Math.floor((g.getWidth() - num_squares_w * s)/2);
|
||||
let o_h = Math.floor((g.getHeight() - num_squares_h * s+offset_widgets)/2);
|
||||
let mid_x = Math.floor(num_squares_w/2);
|
||||
let mid_y = Math.floor((num_squares_h-1)/2);
|
||||
|
||||
draw();
|
||||
|
||||
Bangle.on('lcdPower',on=>{
|
||||
if (on) {
|
||||
draw(); // draw immediately, queue redraw
|
||||
} else { // stop draw timer
|
||||
if (drawTimeout) clearTimeout(drawTimeout);
|
||||
drawTimeout = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
Bangle.setUI('clock');
|
||||
if (settings.showWidgets) {
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwcAtu27dt3MkyVJkgHC23UA4WSCP4R/CP4RFBAfSA4VJA4QCFCP4R/CP4RJ7oaMCP4R/CP4RFbge9BoYID3IQCkgR/CP4R/CIoA=="))
|
After Width: | Height: | Size: 649 B |
|
@ -0,0 +1,44 @@
|
|||
(function(back) {
|
||||
const SETTINGS_FILE = "mosaic.settings.json";
|
||||
|
||||
// initialize with default settings...
|
||||
let s = {'showWidgets': false, 'theme':'System'}
|
||||
|
||||
// ...and overwrite them with any saved values
|
||||
// This way saved values are preserved if a new version adds more settings
|
||||
const storage = require('Storage')
|
||||
let settings = storage.readJSON(SETTINGS_FILE, 1) || s;
|
||||
const saved = settings || {}
|
||||
for (const key in saved) {
|
||||
s[key] = saved[key]
|
||||
}
|
||||
|
||||
function save() {
|
||||
settings = s
|
||||
storage.write(SETTINGS_FILE, settings)
|
||||
}
|
||||
|
||||
var theme_options = ['System', 'Light', 'Dark'];
|
||||
|
||||
E.showMenu({
|
||||
'': { 'title': 'Mosaic Clock' },
|
||||
'< Back': back,
|
||||
'Show Widgets': {
|
||||
value: settings.showWidgets,
|
||||
format: () => (settings.showWidgets ? 'Yes' : 'No'),
|
||||
onchange: () => {
|
||||
settings.showWidgets = !settings.showWidgets;
|
||||
save();
|
||||
}
|
||||
},
|
||||
'Theme': {
|
||||
value: 0 | theme_options.indexOf(s.theme),
|
||||
min: 0, max: theme_options.length - 1,
|
||||
format: v => theme_options[v],
|
||||
onchange: v => {
|
||||
s.theme = theme_options[v];
|
||||
save();
|
||||
}
|
||||
},
|
||||
});
|
||||
})
|
|
@ -0,0 +1,2 @@
|
|||
0.01: New App!
|
||||
0.02: Support Dark Theme.
|
|
@ -1,5 +1,5 @@
|
|||
## Magic the Gathering Watch Face
|
||||
Magic the Gathering themed watch face. Embrace the inner wizzard. Dispay any of the different types of mana on your watch. Which color are you devoted to today?
|
||||
|
||||
It supports both light and dark mode.
|
||||
### Touch Enabled
|
||||
Simply touch the screen on the sides to switch the mana colors
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"id": "mtgwatchface",
|
||||
"name": "MTG Watchface",
|
||||
"shortName": "Magic the Gathering Watch Face",
|
||||
"version": "1v03",
|
||||
"version": "0.02",
|
||||
"description": "Magic the Gathering themed watch face. Embrace the inner wizzard. Dispay any of the different types of mana on your watch. Which color are you devoted to today? ",
|
||||
"icon": "icon.png",
|
||||
"screenshots": [
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
0.01: Initial version
|
|
@ -0,0 +1,20 @@
|
|||
# WARNING
|
||||
|
||||
This app uses the [Scheduler library](https://banglejs.com/apps/?id=sched) and requires a keyboard such as [Swipe keyboard](https://banglejs.com/apps/?id=kbswipe).
|
||||
|
||||
## Usage
|
||||
|
||||
* Select "New note" and use the onscreen keyboard to type.
|
||||
* Hit back button to exit back to the main menu. New notes are added to the main menu. If you don't type anything and you hit the back button, no new note will be saved.
|
||||
* Selecting a note from the main menu will allow you to edit, delete, or change the position of the note (1 being the top of the list).
|
||||
* By selecting "set as alarm" or "set as timer", you can also use this note as a custom message for alerts from alarms and timers. Once you hit save, the alarm or timer is set.
|
||||
* Any alarms or timers you set will appear under "edit alarms/timers." If the alarm/timer is set to a note, the note will appear on the top of the menu. If an alarm/timer is set without a custom message, it will simply say Alarm or Timer on the top of the menu.
|
||||
* On the alarm/timer alert, only the first 30 characters of the note will appear - any more and you run the risk of pushing the sleep/ok buttons off-screen.
|
||||
|
||||
## Images
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
|
@ -0,0 +1 @@
|
|||
E.toArrayBuffer(atob("MDCEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAO7u7u7u7u7u7u7u7u7u7u7u7gAAAAAAAO7u7u7u7u7u7u7u7u7u7u7u7gAAAAAAAO7u7u7u7u7u7u7u7u7u7u7u7gAAAAAAAO7u7u7u7u7u7u7u7u7u7u7u7gAAAAAAAO7u7u7u7u7u7u7u7u7u7u7u7gAAAAAAAO7u7u7u7u7u7u7u7u7u7u7u7gAAAAAAAO7u7u7u7u7u7u7u7u7u7u7u7gAAAAAAAO7u7u7u7u7u7u7u7u7u7u7u7gAAAAAAAO7u3d3d3d3d3d3d3d3d3d3u7gAAAAAAAO7u3d3d3d3d3d3d3d3d3d3u7gAAAAAAAO7u7u7u7u7u7u7u7u7u7u7u7gAAAAAAAO7u7u7u7u7u7u7u7u7u7u7u7gAAAAAAAO7u7u7u7u7u7u7u7u7u7u7u7gAAAAAAAO7u3d3d3d3d3d3d3d3d3d3u7gAAAAAAAO7u3d3d3d3d3d3d3d3d3d3u7gAAAAAAAO7u7u7u7u7u7u7u7u7u7u7u7gAAAAAAAO7u7u7u7u7u7u7u7u7u7u7u7gAAAAAAAO7u7u7u7u7u7u7u7u7u7u7u7gAAAAAAAO7u3d3d3d3d3d3u7u7u7u7u7gAAAAAAAO7u3d3d3d3d3d3u7u7u7u7u7gAAAAAAAO7u7u7u7u7u7u7u7u7u7u7u7gAAAAAAAO7u7u7u7u7u7u7u7u7u7u7u7gAAAAAAAO7u7u7u7u7u7u7u7u7u7u7u7gAAAAAAAO7u7u7u7u7u7u7u7u7u7u7u7gAAAAAAAO7u7u7u7u7u7u7u7u7u7u7u7gAAAAAAAO7u7u7u7u7u7u7u7u7u7u7u7gAAAAAAAO7u7u7u7u7u7u7u7u7/////7gAAAAAAAO7u7u7u7u7u7u7u7u7////+4AAAAAAAAO7u7u7u7u7u7u7u7u7////uAAAAAAAAAO7u7u7u7u7u7u7u7u7///7gAAAAAAAAAO7u7u7u7u7u7u7u7u7//+4AAAAAAAAAAO7u7u7u7u7u7u7u7u7//uAAAAAAAAAAAO7u7u7u7u7u7u7u7u7/7gAAAAAAAAAAAO7u7u7u7u7u7u7u7u7+4AAAAAAAAAAAAO7u7u7u7u7u7u7u7u7uAAAAAAAAAAAAAO7u7u7u7u7u7u7u7u7gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="))
|
|
@ -0,0 +1,304 @@
|
|||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
|
||||
var notes = require("Storage").readJSON("noteify.json", true) || [];
|
||||
var alarms = require("sched").getAlarms();
|
||||
msg = "";
|
||||
|
||||
function startNote(idx) {
|
||||
idx == undefined ? note = "" : note = notes[idx].note;
|
||||
require("textinput").input({text:note}).then(result => {
|
||||
if (result != "") {
|
||||
idx == undefined ? notes.push({"note" : result}) : notes[idx].note = result;
|
||||
require("Storage").write("noteify.json",JSON.stringify(notes));
|
||||
}
|
||||
showMainMenu();
|
||||
});
|
||||
}
|
||||
|
||||
function viewNote(idx) {
|
||||
var textY = 30;
|
||||
var textBound = g.stringMetrics(g.setColor(g.theme.fg).setFont("6x8:2").setFontAlign(-1, -1).drawString(g.wrapString(notes[idx].note, g.getWidth()).join("\n"), 0, textY)).height;
|
||||
Bangle.setUI({mode:"custom", drag:e=>{
|
||||
textY += e.dy;
|
||||
g.setClipRect(0, 30, g.getWidth(), g.getHeight());
|
||||
if (textY > 30) textY = 30;
|
||||
if (textY < textBound) textY = textBound;
|
||||
g.clearRect(0, 30, g.getWidth(), g.getHeight()).setColor(g.theme.fg).setFont("6x8:2").setFontAlign(-1, -1).drawString(g.wrapString(notes[idx].note, g.getWidth()).join("\n"), 0, textY);
|
||||
},back:()=>{
|
||||
Bangle.setUI();
|
||||
showEditMenu(idx);
|
||||
}});
|
||||
|
||||
}
|
||||
|
||||
function showMainMenu() {
|
||||
var mainMenu = {
|
||||
"" : { "title" : "Noteify" },
|
||||
"< Back" : function() { load(); },
|
||||
"New note" : function() {
|
||||
E.showMenu();
|
||||
startNote();
|
||||
},
|
||||
"Edit alarms/timers" : function() { showAlarmMenu(); },
|
||||
};
|
||||
|
||||
notes.forEach((a, idx) => {
|
||||
mainMenu[notes[idx].note.length > 12 ? notes[idx].note.substring(0, 12)+"..." : notes[idx].note] = function () { showEditMenu(idx);};
|
||||
});
|
||||
msg = "";
|
||||
E.showMenu(mainMenu);
|
||||
}
|
||||
|
||||
function showEditMenu(idx) {
|
||||
var moveNote = notes[idx].note;
|
||||
var editMenu = {
|
||||
"" : { "title" : notes[idx].note.length > 12 ? notes[idx].note.replace(/\n/g, " ").substring(0, 12)+"..." : notes[idx].note.replace(/\n/g, " ") },
|
||||
"View note" : function() {
|
||||
E.showMenu();
|
||||
viewNote(idx);
|
||||
},
|
||||
"Edit note" : function() {
|
||||
E.showMenu();
|
||||
startNote(idx);
|
||||
},
|
||||
"Delete note" : function() {
|
||||
notes.splice(idx,1);
|
||||
require("Storage").write("noteify.json",JSON.stringify(notes));
|
||||
showMainMenu();
|
||||
},
|
||||
"Set as alarm" : function() {
|
||||
//limit alarm msg to 30 chars
|
||||
msg = moveNote.substring(0, 30);
|
||||
editAlarm(-1);
|
||||
},
|
||||
"Set as timer" : function () {
|
||||
msg = moveNote.substring(0, 30);
|
||||
editTimer(-1);
|
||||
},
|
||||
"Change position" : {
|
||||
value : idx+1,
|
||||
min : 1,
|
||||
max : notes.length,
|
||||
wrap : true,
|
||||
onchange : function(v) {
|
||||
//save changes from change position
|
||||
if (v-1 != idx) {
|
||||
notes.splice(v-1, 0, notes.splice(idx, 1)[0]);
|
||||
require("Storage").write("noteify.json",JSON.stringify(notes));
|
||||
}
|
||||
},
|
||||
},
|
||||
"< Back" : function() {
|
||||
showMainMenu();
|
||||
},
|
||||
};
|
||||
E.showMenu(editMenu);
|
||||
}
|
||||
|
||||
function decodeTime(t) {
|
||||
t = 0|t; // sanitise
|
||||
var hrs = 0|(t/3600000);
|
||||
return { hrs : hrs, mins : Math.round((t-hrs*3600000)/60000) };
|
||||
}
|
||||
|
||||
// time in { hrs, mins } -> ms
|
||||
function encodeTime(o) {
|
||||
return o.hrs*3600000 + o.mins*60000;
|
||||
}
|
||||
|
||||
function formatTime(t) {
|
||||
var o = decodeTime(t);
|
||||
return o.hrs+":"+("0"+o.mins).substr(-2);
|
||||
}
|
||||
|
||||
function getCurrentTime() {
|
||||
var time = new Date();
|
||||
return (
|
||||
time.getHours() * 3600000 +
|
||||
time.getMinutes() * 60000 +
|
||||
time.getSeconds() * 1000
|
||||
);
|
||||
}
|
||||
|
||||
function saveAndReload() {
|
||||
require("sched").setAlarms(alarms);
|
||||
require("sched").reload();
|
||||
}
|
||||
|
||||
function showAlarmMenu() {
|
||||
const menu = {
|
||||
'': { 'title': 'Alarm/Timer' },
|
||||
'< Back' : ()=>{showMainMenu();},
|
||||
'New Alarm': ()=>editAlarm(-1),
|
||||
'New Timer': ()=>editTimer(-1)
|
||||
};
|
||||
alarms.forEach((alarm,idx)=>{
|
||||
var type,txt; // a leading space is currently required (JS error in Espruino 2v12)
|
||||
if (alarm.timer) {
|
||||
type = /*LANG*/"Timer";
|
||||
txt = " "+formatTime(alarm.timer);
|
||||
} else {
|
||||
type = /*LANG*/"Alarm";
|
||||
txt = " "+formatTime(alarm.t);
|
||||
}
|
||||
if (alarm.rp) txt += "\0"+atob("FBaBAAABgAAcAAHn//////wAHsABzAAYwAAMAADAAAAAAwAAMAADGAAzgAN4AD//////54AAOAABgAA=");
|
||||
// rename duplicate alarms
|
||||
if (menu[type+txt]) {
|
||||
var n = 2;
|
||||
while (menu[type+" "+n+txt]) n++;
|
||||
txt = type+" "+n+txt;
|
||||
} else txt = type+txt;
|
||||
// add to menu
|
||||
menu[txt] = {
|
||||
value : "\0"+atob(alarm.on?"EhKBAH//v/////////////5//x//j//H+eP+Mf/A//h//z//////////3//g":"EhKBAH//v//8AA8AA8AA8AA8AA8AA8AA8AA8AA8AA8AA8AA8AA8AA///3//g"),
|
||||
onchange : function() {
|
||||
if (alarm.timer) editTimer(idx, alarm);
|
||||
else editAlarm(idx, alarm);
|
||||
}
|
||||
};
|
||||
});
|
||||
if (WIDGETS["alarm"]) WIDGETS["alarm"].reload();
|
||||
return E.showMenu(menu);
|
||||
}
|
||||
|
||||
function editDOW(dow, onchange) {
|
||||
const menu = {
|
||||
'': { 'title': 'Days of Week' },
|
||||
'< Back' : () => onchange(dow)
|
||||
};
|
||||
for (var i = 0; i < 7; i++) (i => {
|
||||
var dayOfWeek = require("locale").dow({ getDay: () => i });
|
||||
menu[dayOfWeek] = {
|
||||
value: !!(dow&(1<<i)),
|
||||
format: v => v ? "Yes" : "No",
|
||||
onchange: v => v ? dow |= 1<<i : dow &= ~(1<<i),
|
||||
};
|
||||
})(i);
|
||||
E.showMenu(menu);
|
||||
}
|
||||
|
||||
function editAlarm(alarmIndex, alarm) {
|
||||
var newAlarm = alarmIndex<0;
|
||||
var a = {
|
||||
t : 12*3600000, // 12 o clock default
|
||||
on : true,
|
||||
rp : true,
|
||||
as : false,
|
||||
dow : 0b1111111,
|
||||
last : 0,
|
||||
vibrate : ".."
|
||||
};
|
||||
if (msg != "") a["msg"] = msg;
|
||||
if (!newAlarm) Object.assign(a, alarms[alarmIndex]);
|
||||
if (alarm) Object.assign(a,alarm);
|
||||
var t = decodeTime(a.t);
|
||||
|
||||
var alarmTitle = (a.msg == undefined) ? 'Alarm' : (a.msg.length > 12) ? a.msg.replace(/\n/g, " ").substring(0, 12)+"..." : msg.replace(/\n/g, " ").substring(0, 12)+"...";
|
||||
|
||||
const menu = {
|
||||
'': { 'title': alarmTitle },
|
||||
'< Back' : () => showAlarmMenu(),
|
||||
'Days': {
|
||||
value: "SMTWTFS".split("").map((d,n)=>a.dow&(1<<n)?d:".").join(""),
|
||||
onchange: () => editDOW(a.dow, d=>{a.dow=d;editAlarm(alarmIndex,a)})
|
||||
},
|
||||
'Hours': {
|
||||
value: t.hrs, min : 0, max : 23, wrap : true,
|
||||
onchange: v => t.hrs=v
|
||||
},
|
||||
'Minutes': {
|
||||
value: t.mins, min : 0, max : 59, wrap : true,
|
||||
onchange: v => t.mins=v
|
||||
},
|
||||
'Enabled': {
|
||||
value: a.on,
|
||||
format: v=>v?"On":"Off",
|
||||
onchange: v=>a.on=v
|
||||
},
|
||||
'Repeat': {
|
||||
value: a.rp,
|
||||
format: v=>v?"Yes":"No",
|
||||
onchange: v=>a.rp=v
|
||||
},
|
||||
'Vibrate': require("buzz_menu").pattern(a.vibrate, v => a.vibrate=v ),
|
||||
'Auto snooze': {
|
||||
value: a.as,
|
||||
format: v=>v?"Yes":"No",
|
||||
onchange: v=>a.as=v
|
||||
}
|
||||
};
|
||||
menu["Save"] = function() {
|
||||
a.t = encodeTime(t);
|
||||
a.last = (a.t < getCurrentTime()) ? (new Date()).getDate() : 0;
|
||||
a.last = 0;
|
||||
if (newAlarm) alarms.push(a);
|
||||
else alarms[alarmIndex] = a;
|
||||
saveAndReload();
|
||||
showMainMenu();
|
||||
};
|
||||
if (!newAlarm) {
|
||||
menu["Delete"] = function() {
|
||||
alarms.splice(alarmIndex,1);
|
||||
saveAndReload();
|
||||
showMainMenu();
|
||||
};
|
||||
}
|
||||
return E.showMenu(menu);
|
||||
}
|
||||
|
||||
function editTimer(alarmIndex, alarm) {
|
||||
var newAlarm = alarmIndex<0;
|
||||
var a = {
|
||||
timer : 5*60*1000, // 5 minutes
|
||||
on : true,
|
||||
rp : false,
|
||||
as : false,
|
||||
dow : 0b1111111,
|
||||
last : 0,
|
||||
vibrate : ".."
|
||||
};
|
||||
if (msg != "") a["msg"] = msg;
|
||||
if (!newAlarm) Object.assign(a, alarms[alarmIndex]);
|
||||
if (alarm) Object.assign(a,alarm);
|
||||
var t = decodeTime(a.timer);
|
||||
|
||||
var timerTitle = (a.msg == undefined) ? 'Timer' : (a.msg.length > 12) ? a.msg.replace(/\n/g, " ").substring(0, 12)+"..." : msg.replace(/\n/g, " ").substring(0, 12)+"...";
|
||||
|
||||
const menu = {
|
||||
'': { 'title': timerTitle },
|
||||
'< Back' : () => showMainMenu(),
|
||||
'Hours': {
|
||||
value: t.hrs, min : 0, max : 23, wrap : true,
|
||||
onchange: v => t.hrs=v
|
||||
},
|
||||
'Minutes': {
|
||||
value: t.mins, min : 0, max : 59, wrap : true,
|
||||
onchange: v => t.mins=v
|
||||
},
|
||||
'Enabled': {
|
||||
value: a.on,
|
||||
format: v=>v?"On":"Off",
|
||||
onchange: v=>a.on=v
|
||||
},
|
||||
'Vibrate': require("buzz_menu").pattern(a.vibrate, v => a.vibrate=v ),
|
||||
};
|
||||
menu["Save"] = function() {
|
||||
a.timer = encodeTime(t);
|
||||
a.t = getCurrentTime() + a.timer;
|
||||
if (newAlarm) alarms.push(a);
|
||||
else alarms[alarmIndex] = a;
|
||||
saveAndReload();
|
||||
showMainMenu();
|
||||
};
|
||||
if (!newAlarm) {
|
||||
menu["Delete"] = function() {
|
||||
alarms.splice(alarmIndex,1);
|
||||
saveAndReload();
|
||||
showMainMenu();
|
||||
};
|
||||
}
|
||||
return E.showMenu(menu);
|
||||
}
|
||||
|
||||
showMainMenu();
|
After Width: | Height: | Size: 284 B |
After Width: | Height: | Size: 3.4 KiB |
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"id": "noteify",
|
||||
"name": "Noteify",
|
||||
"version": "0.01",
|
||||
"description": "Write notes using an onscreen keyboard and use them as custom messages for alarms or timers.",
|
||||
"icon": "app.png",
|
||||
"tags": "tool,alarm",
|
||||
"supports": ["BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
"storage": [
|
||||
{"name":"noteify.app.js","url":"app.js"},
|
||||
{"name":"noteify.img","url":"app-icon.js","evaluate":true},
|
||||
{"name":"noteify.wid.js","url":"widget.js"}
|
||||
],
|
||||
"data": [{"name":"noteify.json"}],
|
||||
"dependencies": {"scheduler":"type","textinput":"type"},
|
||||
"screenshots": [
|
||||
{"url": "menu.png"},
|
||||
{"url": "note.png"},
|
||||
{"url": "timer-alert.png"}
|
||||
]
|
||||
}
|
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 2.9 KiB |
|
@ -0,0 +1,8 @@
|
|||
WIDGETS["alarm"]={area:"tl",width:0,draw:function() {
|
||||
if (this.width) g.reset().drawImage(atob("GBgBAAAAAAAAABgADhhwDDwwGP8YGf+YMf+MM//MM//MA//AA//AA//AA//AA//AA//AB//gD//wD//wAAAAADwAABgAAAAAAAAA"),this.x,this.y);
|
||||
},reload:function() {
|
||||
// don't include library here as we're trying to use as little RAM as possible
|
||||
WIDGETS["alarm"].width = (require('Storage').readJSON('sched.json',1)||[]).some(alarm=>alarm.on&&(alarm.hidden!==false)) ? 24 : 0;
|
||||
}
|
||||
};
|
||||
WIDGETS["alarm"].reload();
|
|
@ -0,0 +1 @@
|
|||
0.01: New App!
|
|
@ -0,0 +1,22 @@
|
|||
# OpenWind
|
||||
|
||||
Receive and display data from a wireless [OpenWind](https://www.openwind.de/) sailing wind instrument on the Bangle.
|
||||
|
||||
## Usage
|
||||
|
||||
Upon startup, the app will attempt to automatically connect to the wind instrument. This typically only takes a few seconds.
|
||||
|
||||
## Features
|
||||
|
||||
The app displays the apparent wind direction (via a green dot) and speed (green numbers, in knots) relative to the mounting direction of the wind vane.
|
||||
If "True wind" is enabled in settings and a GPS fix is available, the true wind speed and direction (relative to the mounting direction of the vane) is
|
||||
additionally displayed in red. In this mode, the speed over ground in knots is also shown at the bottom left of the screen.
|
||||
|
||||
## Controls
|
||||
|
||||
There are no controls in the main app, but there are two settings in the settings app that can be changed:
|
||||
|
||||
* True wind: enables or disables true wind calculations; enabling this will turn on GPS inside the app
|
||||
* Mounting angle: mounting relative to the boat of the wind instrument (in degrees)
|
||||
|
||||

|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AH4A/AH4A/AH4AzhMJF94wtF+QwsF/4APnAACF54wZFoYxNF7guHGBQv0GCwuJGBIvFACov/AD4vvd6Yv/GCoumGIwtpAH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AHoA=="))
|
|
@ -0,0 +1,113 @@
|
|||
OW_CHAR_UUID = '0000cc91-0000-1000-8000-00805f9b34fb';
|
||||
require("Font7x11Numeric7Seg").add(Graphics);
|
||||
gatt = {};
|
||||
cx = g.getWidth()/2;
|
||||
cy = 24+(g.getHeight()-24)/2;
|
||||
w = (g.getWidth()-24)/2;
|
||||
|
||||
gps_course = { spd: 0 };
|
||||
|
||||
var settings = require("Storage").readJSON('openwindsettings.json', 1) || {};
|
||||
|
||||
i = 0;
|
||||
hullpoly = [];
|
||||
for (y=-1; y<=1; y+=0.1) {
|
||||
hullpoly[i++] = cx - (y<0 ? 1+y*0.15 : (Math.sqrt(1-0.7*y*y)-Math.sqrt(0.3))/(1-Math.sqrt(0.3)))*w*0.3;
|
||||
hullpoly[i++] = cy - y*w*0.7;
|
||||
}
|
||||
for (y=1; y>=-1; y-=0.1) {
|
||||
hullpoly[i++] = cx + (y<0 ? 1+y*0.15 : (Math.sqrt(1-0.7*y*y)-Math.sqrt(0.3))/(1-Math.sqrt(0.3)))*w*0.3;
|
||||
hullpoly[i++] = cy - y*w*0.7;
|
||||
}
|
||||
|
||||
function wind_updated(ev) {
|
||||
if (ev.target.uuid == "0xcc91") {
|
||||
awa = settings.mount_angle-ev.target.value.getInt16(1, true)*0.1;
|
||||
aws = ev.target.value.getInt16(3, true)*0.01;
|
||||
// console.log(awa, aws);
|
||||
if (gps_course.spd > 0) {
|
||||
wv = { // wind vector (in fixed reference frame)
|
||||
lon: Math.sin(Math.PI*(gps_course.course+awa)/180)*aws,
|
||||
lat: Math.cos(Math.PI*(gps_course.course+awa)/180)*aws
|
||||
};
|
||||
twv = { lon: wv.lon+gps_course.lon, lat: wv.lat+gps_course.lat };
|
||||
tws = Math.sqrt(Math.pow(twv.lon,2)+Math.pow(twv.lat, 2));
|
||||
twa = Math.atan2(twv.lat, twv.lon)*180/Math.PI-gps_course.course;
|
||||
if (twa<0) twa += 360;
|
||||
if (twa>360) twa -=360;
|
||||
}
|
||||
else {
|
||||
tws = -1;
|
||||
twa = 0;
|
||||
}
|
||||
draw_compass(awa,aws,twa,tws);
|
||||
}
|
||||
}
|
||||
|
||||
function draw_compass(awa, aws, twa, tws) {
|
||||
g.clearRect(0, 24, g.getWidth()-1, g.getHeight()-1);
|
||||
fh = w*0.15;
|
||||
g.setColor(0, 0, 1).fillPoly(hullpoly);
|
||||
g.setFontVector(fh).setColor(g.theme.fg);
|
||||
g.setFontAlign(0, 0, 0).drawString("0", cx, 24+fh/2);
|
||||
g.setFontAlign(0, 0, 1).drawString("90", g.getWidth()-12-fh, cy);
|
||||
g.setFontAlign(0, 0, 2).drawString("180", cx, g.getHeight()-fh/2);
|
||||
g.setFontAlign(0, 0, 3).drawString("270", 12+fh/2, cy);
|
||||
for (i=0; i<4; ++i) {
|
||||
a = i*Math.PI/2+Math.PI/4;
|
||||
g.drawLineAA(cx+Math.cos(a)*w*0.85, cy+Math.sin(a)*w*0.85, cx+Math.cos(a)*w*0.99, cy+Math.sin(a)*w*0.99);
|
||||
}
|
||||
g.setColor(0, 1, 0).fillCircle(cx+Math.sin(Math.PI*awa/180)*w*0.9, cy+Math.cos(Math.PI*awa/180)*w*0.9, w*0.1);
|
||||
if (tws>0) g.setColor(1, 0, 0).fillCircle(cx+Math.sin(Math.PI*twa/180)*w*0.9, cy+Math.cos(Math.PI*twa/180)*w*0.9, w*0.1);
|
||||
g.setColor(0, 1, 0).setFont("7x11Numeric7Seg",w*0.06);
|
||||
g.setFontAlign(0, 0, 0).drawString(aws.toFixed(1), cx, cy-0.32*w);
|
||||
if (tws>0) g.setColor(1, 0, 0).drawString(tws.toFixed(1), cx, cy+0.32*w);
|
||||
if (settings.truewind && typeof gps_course.spd!=='undefined') {
|
||||
spd = gps_course.spd/1.852;
|
||||
g.setColor(g.theme.fg).setFont("7x11Numeric7Seg", w*0.03).setFontAlign(-1, 1, 0).drawString(spd.toFixed(1), 1, g.getHeight()-1);
|
||||
}
|
||||
}
|
||||
|
||||
function parseDevice(d) {
|
||||
device = d;
|
||||
console.log("Found device");
|
||||
device.gatt.connect().then(function(ga) {
|
||||
console.log("Connected");
|
||||
gatt = ga;
|
||||
return ga.getPrimaryService("cc90");
|
||||
}).then(function(s) {
|
||||
return s.getCharacteristic("cc91");
|
||||
}).then(function(c) {
|
||||
c.on('characteristicvaluechanged', (event)=>wind_updated(event));
|
||||
return c.startNotifications();
|
||||
}).then(function() {
|
||||
console.log("Done!");
|
||||
}).catch(function(e) {
|
||||
console.log("ERROR"+e);
|
||||
});}
|
||||
|
||||
function connection_setup() {
|
||||
NRF.setScan();
|
||||
NRF.setScan(parseDevice, { filters: [{services:["cc90"]}], timeout: 2000});
|
||||
console.log("Scanning for OW sensor");
|
||||
}
|
||||
|
||||
if (settings.truewind) {
|
||||
Bangle.on('GPS',function(fix) {
|
||||
if (fix.fix && fix.satellites>3 && fix.speed>2) { // only uses fixes w/ more than 3 sats and speed > 2kph
|
||||
gps_course =
|
||||
{ lon: Math.sin(Math.PI*fix.course/180)*fix.speed/1.852,
|
||||
lat: Math.cos(Math.PI*fix.course/180)*fix.speed/1.852,
|
||||
spd: fix.speed,
|
||||
course: fix.course
|
||||
};
|
||||
}
|
||||
else gps_course.spd = -1;
|
||||
});
|
||||
Bangle.setGPSPower(1, "app");
|
||||
}
|
||||
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
draw_compass(0, 0, 0, 0);
|
||||
connection_setup();
|
After Width: | Height: | Size: 497 B |
|
@ -0,0 +1,15 @@
|
|||
{ "id": "openwind",
|
||||
"name": "OpenWind",
|
||||
"shortName":"OpenWind",
|
||||
"version":"0.01",
|
||||
"description": "OpenWind",
|
||||
"icon": "openwind.png",
|
||||
"readme": "README.md",
|
||||
"tags": "ble,outdoors,gps,sailing",
|
||||
"supports" : ["BANGLEJS", "BANGLEJS2"],
|
||||
"storage": [
|
||||
{"name":"openwind.app.js","url":"app.js"},
|
||||
{"name":"openwind.img","url":"app-icon.js","evaluate":true},
|
||||
{"name":"openwind.settings.js", "url":"settings.js"}
|
||||
]
|
||||
}
|