Merge branch 'espruino:master' into master

pull/1703/head
Ronin0000 2022-04-19 07:29:35 -07:00 committed by GitHub
commit fbbb441bdd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
136 changed files with 2752 additions and 280 deletions

View File

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

2
apps/2047pp/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.01: New app!
0.02: Better support for watch themes

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwYda7dtwAQNmwRB2wQMgO2CIXACJcNCIfYCJYOCCgQRNJQYRM2ADBgwR/CKprRWAKPQWZ0DCIjXLjYREGpYODAQVgCBB3Btj+EAoQAGO4IdCgImDCAwLCAoo4IF4J3DCIPDCIQ4FO4VtwARCAoIRGRgQCBa4IRCKAQRERgOwIIIRDAoOACIoIBwwRHLIqMCFgIRCGQQRIWAYRLYQoREWwTmHO4IRCFgLXHPoi/CbogAFEAIRCWwTpKEwZBCHwK5BCJZEBCJZcCGQTLDCJK/BAQIRKMoaSDOIYAFeQYRMcYRWBXIUAWYPACIq8DagfACJQLCCIYsBU4QRF7B9CAogRGI4QLCAoprIMoZKER5C/DAoShMAo4AGfAQFIACQ="))

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,4 +2,5 @@
0.02: Use build in function for steps and other improvements.
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.05: Included icons for information.
0.06: Design and usability improvements.

View File

@ -1,13 +1,13 @@
# Black & White clock
# BW Clock
![](screenshot.png)
## 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>

View File

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

View File

@ -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"],

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
apps/bwclk/screenshot_3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -18,7 +18,7 @@
E.showMenu({
'': { 'title': 'BlackWhite Clock' },
'': { 'title': 'BW Clock' },
'< Back': back,
'Fullscreen': {
value: settings.fullscreen,

View File

@ -1 +1,2 @@
0.01: New App!
0.02: Support Bangle.js 2

209
apps/choozi/appb2.js Normal file
View File

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

View File

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

View File

@ -244,7 +244,7 @@ function run(){
Bangle.setLCDMode();
g.clear();
g.flip();
E.showMessage("Loading...");
E.showMessage(/*LANG*/"Loading...");
load(app.src);
}

View File

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

View File

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

View File

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

1
apps/golfview/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: New App! Very limited course support.

21
apps/golfview/LICENSE Normal file
View File

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

33
apps/golfview/README.md Normal file
View File

@ -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.
![hole3](screenshot.png)
## 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)

154
apps/golfview/custom.html Normal file
View File

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

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("lEowcF23btoCRpMkyVKBxHayQOCpNbBw/UBwkrBw21BYQCCkoOIy/JCIetI4+X+QgEBxNIBxFqBxFWBxAsEBxHpBYPbsgOFqQOEy3btgOLDoIOrrYOKMoQOKOgYsKBx1KBwaGBWYgOBkVJBwKGBYQwOBiVJlIdCkmVBxdZfwwOGF4IONkoLCB2J3BBxkgQwQOKWYlJlYOUtQORtskzQOHtoOE7QOLAQdbBw21BydKBYgCD6gODBYwCNA"))

212
apps/golfview/golfview.js Normal file
View File

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

BIN
apps/golfview/golfview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

63
apps/golfview/maptools.js Normal file
View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

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

View File

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

View File

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

View File

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

1
apps/heartzone/ChangeLog Normal file
View File

@ -0,0 +1 @@
1.0: Initial release.

35
apps/heartzone/README.md Normal file
View File

@ -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:
![Start screen](screenshots/start.png)
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:
![Screen while we are monitoring](screenshots/running.png)
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):
![Screen while we are paused](screenshots/paused.png)
When you're done, simply press the side button to exit the app.
## Creator
[Uberi](https://github.com/Uberi)

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwkBiIA/AHqFCAxQWJ5gABCIQGGABEQB4QABgIGGC5MMCAnAAwwuOABAwIC64/FABBIIC68ADBnAVJEP+AXLBoJ2H/4XN/54GBAIXOGAouBBAMAABQXBGAoHCAB4wDFwQARGAYvWL7CPDbBXAR46/DiAXJgK/Id4URGBHABobwHEAIwIBQQuHAAcYGA3AwIUKC4eAC4sIC5+IGAnAxAXQkAXDgQXRkQwC4EiC6QwCgQXTl0M4HiC6nghwXV93uC9MRC44WOGAIXFFx4ABC4oWQiMSC4chC6MRC4YWSiMeC4PhC6cRC4IWUGAIuVAH4AVA="))

87
apps/heartzone/app.js Normal file
View File

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

BIN
apps/heartzone/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

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

View File

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

View File

@ -1 +1,2 @@
0.01: New App!
0.02: Now keeps user input trace intact by changing how the screen is updated.

View File

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

View File

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

View File

@ -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: "" },

View File

@ -20,6 +20,6 @@ const avg = [];
setInterval(function() {
drawTemperature();
}, 2000);
E.showMessage("Loading...");
E.showMessage(/*LANG*/"Loading...");
Bangle.loadWidgets();
Bangle.drawWidgets();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwhC/AFXdAAQVVDKQWHDB0NC5PQCyoYMCxZJKFxgwKCxowJC6xGOJBALE6YwRBQnf+YXPIwvf/4YKJAgXHDBQXNDBIXO/89C5YKFC4gYIC54YHC6AYGC6IYFC9JHWO6ynLa64XJ+YWGC5wWIC5oWJC4p4F74WKOwgXG6YWKC4xIFABRGFYI4uPC7JIOIw4wPCxAwNFxIYMCxZJLCxgYJCxwZGCqIA/AC4="))

15
apps/messagesmusic/app.js Normal file
View File

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

BIN
apps/messagesmusic/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

1
apps/mosaic/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: First release

14
apps/mosaic/README.md Normal file
View File

@ -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).
![](mosaic-scr1.png)
![](mosaic-scr2.png)
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/)

19
apps/mosaic/metadata.json Normal file
View File

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

BIN
apps/mosaic/mosaic-scr1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

BIN
apps/mosaic/mosaic-scr2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

103
apps/mosaic/mosaic.app.js Normal file
View File

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

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwcAtu27dt3MkyVJkgHC23UA4WSCP4R/CP4RFBAfSA4VJA4QCFCP4R/CP4RJ7oaMCP4R/CP4RFbge9BoYID3IQCkgR/CP4R/CIoA=="))

BIN
apps/mosaic/mosaic.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 649 B

View File

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

Binary file not shown.

View File

@ -0,0 +1,2 @@
0.01: New App!
0.02: Support Dark Theme.

View File

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

File diff suppressed because one or more lines are too long

View File

@ -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": [

1
apps/noteify/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: Initial version

20
apps/noteify/README.md Normal file
View File

@ -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
![](menu.png)
![](note.png)
![](timer-alert.png)

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

@ -0,0 +1 @@
E.toArrayBuffer(atob("MDCEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAO7u7u7u7u7u7u7u7u7u7u7u7gAAAAAAAO7u7u7u7u7u7u7u7u7u7u7u7gAAAAAAAO7u7u7u7u7u7u7u7u7u7u7u7gAAAAAAAO7u7u7u7u7u7u7u7u7u7u7u7gAAAAAAAO7u7u7u7u7u7u7u7u7u7u7u7gAAAAAAAO7u7u7u7u7u7u7u7u7u7u7u7gAAAAAAAO7u7u7u7u7u7u7u7u7u7u7u7gAAAAAAAO7u7u7u7u7u7u7u7u7u7u7u7gAAAAAAAO7u3d3d3d3d3d3d3d3d3d3u7gAAAAAAAO7u3d3d3d3d3d3d3d3d3d3u7gAAAAAAAO7u7u7u7u7u7u7u7u7u7u7u7gAAAAAAAO7u7u7u7u7u7u7u7u7u7u7u7gAAAAAAAO7u7u7u7u7u7u7u7u7u7u7u7gAAAAAAAO7u3d3d3d3d3d3d3d3d3d3u7gAAAAAAAO7u3d3d3d3d3d3d3d3d3d3u7gAAAAAAAO7u7u7u7u7u7u7u7u7u7u7u7gAAAAAAAO7u7u7u7u7u7u7u7u7u7u7u7gAAAAAAAO7u7u7u7u7u7u7u7u7u7u7u7gAAAAAAAO7u3d3d3d3d3d3u7u7u7u7u7gAAAAAAAO7u3d3d3d3d3d3u7u7u7u7u7gAAAAAAAO7u7u7u7u7u7u7u7u7u7u7u7gAAAAAAAO7u7u7u7u7u7u7u7u7u7u7u7gAAAAAAAO7u7u7u7u7u7u7u7u7u7u7u7gAAAAAAAO7u7u7u7u7u7u7u7u7u7u7u7gAAAAAAAO7u7u7u7u7u7u7u7u7u7u7u7gAAAAAAAO7u7u7u7u7u7u7u7u7u7u7u7gAAAAAAAO7u7u7u7u7u7u7u7u7/////7gAAAAAAAO7u7u7u7u7u7u7u7u7////+4AAAAAAAAO7u7u7u7u7u7u7u7u7////uAAAAAAAAAO7u7u7u7u7u7u7u7u7///7gAAAAAAAAAO7u7u7u7u7u7u7u7u7//+4AAAAAAAAAAO7u7u7u7u7u7u7u7u7//uAAAAAAAAAAAO7u7u7u7u7u7u7u7u7/7gAAAAAAAAAAAO7u7u7u7u7u7u7u7u7+4AAAAAAAAAAAAO7u7u7u7u7u7u7u7u7uAAAAAAAAAAAAAO7u7u7u7u7u7u7u7u7gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="))

304
apps/noteify/app.js Normal file
View File

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

BIN
apps/noteify/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 B

BIN
apps/noteify/menu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

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

BIN
apps/noteify/note.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

8
apps/noteify/widget.js Normal file
View File

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

1
apps/openwind/ChangeLog Normal file
View File

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

22
apps/openwind/README.md Normal file
View File

@ -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)
![](openwind_screenshot.png)

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AH4A/AH4A/AH4AzhMJF94wtF+QwsF/4APnAACF54wZFoYxNF7guHGBQv0GCwuJGBIvFACov/AD4vvd6Yv/GCoumGIwtpAH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AHoA=="))

113
apps/openwind/app.js Normal file
View File

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

BIN
apps/openwind/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 497 B

View File

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

Some files were not shown because too many files have changed in this diff Show More