1
0
Fork 0

Merge branch 'espruino:master' into master

master
xxDUxx 2022-05-30 19:41:13 +02:00 committed by GitHub
commit 67d590713f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
345 changed files with 8762 additions and 1719 deletions

1
.gitignore vendored
View File

@ -11,3 +11,4 @@ tests/Layout/bin/tmp.*
tests/Layout/testresult.bmp
apps.local.json
_site
.jekyll-cache

View File

@ -226,10 +226,8 @@ and which gives information about the app for the Launcher.
"name":"Short Name", // for Bangle.js menu
"icon":"*myappid", // for Bangle.js menu
"src":"-myappid", // source file
"type":"widget/clock/app/bootloader", // optional, default "app"
// if this is 'widget' then it's not displayed in the menu
// if it's 'clock' then it'll be loaded by default at boot time
// if this is 'bootloader' then it's code that is run at boot time, but is not in a menu
"type":"widget/clock/app/bootloader/...", // optional, default "app"
// see 'type' in 'metadata.json format' below for more options/info
"version":"1.23",
// added by BangleApps loader on upload based on metadata.json
"files:"file1,file2,file3",
@ -252,17 +250,24 @@ and which gives information about the app for the Launcher.
"version": "0v01", // the version of this app
"description": "...", // long description (can contain markdown)
"icon": "icon.png", // icon in apps/
"screenshots" : [ { url:"screenshot.png" } ], // optional screenshot for app
"screenshots" : [ { "url":"screenshot.png" } ], // optional screenshot for app
"type":"...", // optional(if app) -
// 'app' - an application
// 'clock' - a clock - required for clocks to automatically start
// 'widget' - a widget
// 'launch' - replacement launcher app
// 'bootloader' - code that runs at startup only
// 'bootloader' - an app that at startup (app.boot.js) but doesn't have a launcher entry for 'app.js'
// 'settings' - apps that appear in Settings->Apps (with appname.settings.js) but that have no 'app.js'
// 'RAM' - code that runs and doesn't upload anything to storage
// 'launch' - replacement 'Launcher'
// 'textinput' - provides a 'textinput' library that allows text to be input on the Bangle
// 'scheduler' - provides 'sched' library and boot code for scheduling alarms/timers
// (currently only 'sched' app)
// 'notify' - provides 'notify' library for showing notifications
// 'locale' - provides 'locale' library for language-specific date/distance/etc
// (a version of 'locale' is included in the firmware)
"tags": "", // comma separated tag list for searching
"supports": ["BANGLEJS2"], // List of device IDs supported, either BANGLEJS or BANGLEJS2
"dependencies" : { "notify":"type" } // optional, app 'types' we depend on
"dependencies" : { "notify":"type" } // optional, app 'types' we depend on (see "type" above)
"dependencies" : { "messages":"app" } // optional, depend on a specific app ID
// for instance this will use notify/notifyfs is they exist, or will pull in 'notify'
"readme": "README.md", // if supplied, a link to a markdown-style text file

View File

@ -1 +1 @@
theme: jekyll-theme-minimal
theme: jekyll-theme-slate

352
android.html Normal file
View File

@ -0,0 +1,352 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=0.8,maximum-scale=0.8, minimum-scale=0.8, shrink-to-fit=no">
<link rel="stylesheet" href="css/spectre.min.css">
<link rel="stylesheet" href="css/spectre-exp.min.css">
<link rel="stylesheet" href="css/spectre-icons.min.css">
<link rel="stylesheet" href="css/pwa.css">
<link rel="stylesheet" href="css/main.css">
<link rel="apple-touch-icon" sizes="180x180" href="img/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="img/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="img/favicon-16x16.png">
<link rel="manifest" href="site.webmanifest">
<link rel="mask-icon" href="img/safari-pinned-tab.svg" color="#5755d9">
<meta name="apple-mobile-web-app-title" content="BangleApps">
<meta name="application-name" content="BangleApps">
<meta name="msapplication-TileColor" content="#5755d9">
<meta name="theme-color" content="#5755d9">
<title>Bangle.js App Loader</title>
</head>
<body>
<!--<button id="test">Test</button>
<div id="status"></div>-->
<header class="navbar-primary navbar">
<section class="navbar-section" >
<a href="https://banglejs.com" target="_blank" class="navbar-brand mr-2" ><img src="img/banglejs-logo-sml.png" alt="Bangle.js">
<div>App Loader</div></a>
<!-- <a href="#" class="btn btn-link">...</a> -->
</section>
<section class="navbar-section">
<button class="btn" id="connectmydevice">Connect</button>
</section>
<!--<section class="navbar-section">
<div class="input-group input-inline">
<input class="form-input" type="text" placeholder="search">
<button class="btn btn-primary input-group-btn">Search</button>
</div>
</section>-->
</header>
<div class="container" style="padding-top:4px">
<p id="requireHTTPS" class="hidden">
<b>STOP!</b> This page <b>must</b> be served over HTTPS. Please <a>reload this page via HTTPS</a>.
</p>
</div>
<ul class="tab tab-block" id="tab-navigate">
<li class="tab-item active" id="tab-librarycontainer">
<a href="javascript:showTab('librarycontainer')">Library</a>
</li>
<li class="tab-item" id="tab-myappscontainer">
<a href="javascript:showTab('myappscontainer')">My Apps</a>
</li>
<li class="tab-item" id="tab-morecontainer">
<a href="javascript:showTab('morecontainer')">More...</a>
</li>
</ul>
<div class="container" id="toastcontainer">
</div>
<div class="container apploader-tab" id="librarycontainer">
<div class="dropdown-container">
<div class="dropdown devicetype-nav">
<a href="#" class="btn btn-link dropdown-toggle" tabindex="0">
<span>All apps</span><i class="icon icon-caret"></i>
</a>
<!-- menu component -->
<ul class="menu">
<li class="menu-item"><a>All apps</a></li>
<li class="menu-item"><a dt="BANGLEJS">Bangle.js 1</a></li>
<li class="menu-item"><a dt="BANGLEJS2">Bangle.js 2</a></li>
</ul>
</div>
<div class="filter-nav">
<label class="chip active" filterid="">Default</label>
<label class="chip" filterid="clock">Clocks</label>
<label class="chip" filterid="game">Games</label>
<label class="chip" filterid="tool">Tools</label>
<label class="chip" filterid="widget">Widgets</label>
<label class="chip" filterid="bluetooth">Bluetooth</label>
<label class="chip" filterid="outdoors">Outdoors</label>
<label class="chip" filterid="favourites">Favourites</label>
</div>
<div class="sort-nav hidden">
<span>Sort by:</span>
<label class="chip active" sortid="">None</label>
<label class="chip" sortid="created">New</label>
<label class="chip" sortid="modified">Updated</label>
</div>
</div>
<div class="panel" style="clear:both">
<div class="panel-header">
<div class="input-group" id="searchform">
<input class="form-input" type="text" placeholder="Keywords...">
<button class="btn btn-primary input-group-btn">Search</button>
</div>
</div>
<div class="panel-body columns"><!-- apps go here --></div>
</div>
</div>
<div class="container apploader-tab" id="myappscontainer" style="display:none">
<div class="panel">
<div class="panel-header" style="text-align:right">
<button class="btn refresh">Refresh...</button>
<button class="btn btn-primary updateapps hidden">Update X apps</button>
</div>
<div class="panel-body columns"><!-- apps go here --></div>
</div>
</div>
<div class="container apploader-tab" id="morecontainer" style="display:none">
<div class="hero bg-gray">
<div class="hero-body">
<a href="https://banglejs.com" target="_blank"><img src="img/banglejs-logo-mid.png" alt="Bangle.js"></a>
<h2>App Loader</h2>
<p>A tool for uploading and removing apps from <a href="https://banglejs.com" target="_blank">Bangle.js Smart Watches</a></p>
</div>
</div>
<div class="container" style="padding-top: 8px;">
<p><b>Can't connect?</b> Check out the <a href="https://www.espruino.com/Troubleshooting+Bangle.js" target="_blank">Bangle.js Troubleshooting page</a>
<p id="apploaderlinks"></p>
<p>Check out <a href="https://github.com/espruino/BangleApps" target="_blank">the Source on GitHub</a>, or
find out <a href="https://www.espruino.com/Bangle.js+App+Loader" target="_blank">how to add your own app</a></p>
<p>Using <a href="https://espruino.com/" target="_blank">Espruino</a>, Icons from <a href="https://icons8.com/" target="_blank">icons8.com</a></p>
<h3>Utilities</h3>
<p><button class="btn" id="settime">Set Bangle.js Time</button>
<button class="btn" id="removeall" data-tooltip="Delete everything from your Bangle, leaving it blank">Remove all Apps</button>
<button class="btn" id="reinstallall" data-tooltip="Remove and re-install every app, leaving all other data intact">Reinstall apps</button>
<button class="btn" id="installdefault">Install default apps</button>
<button class="btn" id="installfavourite" data-tooltip="Delete everything, install apps you've marked as favourites">Install favourite apps</button></p>
<p><button class="btn tooltip tooltip-right" id="downloadallapps" data-tooltip="Download all Bangle.js files to a ZIP file">Backup</button>
<button class="btn tooltip tooltip-right" id="uploadallapps" data-tooltip="Restore Bangle.js from a ZIP file">Restore</button></p>
<h3>Settings</h3>
<div class="form-group">
<label class="form-switch">
<input type="checkbox" id="settings-pretokenise">
<i class="form-icon"></i> Pretokenise apps before upload (smaller, faster apps)
</label>
<label class="form-switch">
<input type="checkbox" id="settings-settime">
<i class="form-icon"></i> Always update time when we connect
</label>
<div class="form-group">
<select class="form-select form-inline" id="settings-lang" style="width: 10em">
<option value="">None (English)</option>
</select>&nbsp;&nbsp;<span>Translations (<a href="https://github.com/espruino/BangleApps/issues/1311" target="_blank">BETA - more info</a>). Any apps that are uploaded to Bangle.js after changing this will have any text automatically translated.</span>
</div>
<button class="btn" id="defaultsettings">Default settings</button>
</div>
<div id="more-deviceinfo" style="display:none">
<h3>Device info</h3>
<div id="more-deviceinfo-content"></div>
</div>
</div>
</div>
<footer class="floating hidden">
<!-- Install button, hidden by default -->
<div id="installContainer" class="hidden">
<button id="butInstall" type="button">
Install
</button>
</div>
</footer>
<script src="https://www.puck-js.com/puck.js"></script>
<script src="core/lib/marked.min.js"></script>
<script src="core/lib/espruinotools.js"></script>
<script src="core/lib/heatshrink.js"></script>
<script src="core/js/utils.js"></script>
<script src="loader.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js"></script> <!-- for backup.js -->
<script src="backup.js"></script>
<script src="core/js/ui.js"></script>
<script src="core/js/comms.js"></script>
<script src="core/js/appinfo.js"></script>
<script src="core/js/index.js"></script>
<script src="core/js/pwa.js" defer></script>
<script>
/*Android = {
bangleTx : function(data) {
console.log("TX : "+JSON.stringify(data));
}
};*/
/*document.getElementById("test").addEventListener("click", function() {
console.log("Pressed");
Android.bangleTx("LED1.toggle();\n");
});*/
if (typeof Android!=="undefined") {
console.log("Running in Android, overwrite Puck library");
var isBusy = false;
var queue = [];
var connection = {
cb : function(data) {},
write : function(data, writecb) {
Android.bangleTx(data);
Puck.writeProgress(data.length, data.length);
if (writecb) setTimeout(writecb,10);
},
close : function() {},
received : "",
hadData : false
}
function bangleRx(data) {
// document.getElementById("status").innerText = "RX:"+data;
connection.received += data;
connection.hadData = true;
if (connection.cb) connection.cb(data);
}
function log(level, s) {
if (Puck.log) Puck.log(level, s);
}
function handleQueue() {
if (!queue.length) return;
var q = queue.shift();
log(3,"Executing "+JSON.stringify(q)+" from queue");
if (q.type == "write") Puck.write(q.data, q.callback, q.callbackNewline);
else log(1,"Unknown queue item "+JSON.stringify(q));
}
/* convenience function... Write data, call the callback with data:
callbackNewline = false => if no new data received for ~0.2 sec
callbackNewline = true => after a newline */
function write(data, callback, callbackNewline) {
let result;
/// If there wasn't a callback function, then promisify
if (typeof callback !== 'function') {
callbackNewline = callback;
result = new Promise((resolve, reject) => callback = (value, err) => {
if (err) reject(err);
else resolve(value);
});
}
if (isBusy) {
log(3, "Busy - adding Puck.write to queue");
queue.push({type:"write", data:data, callback:callback, callbackNewline:callbackNewline});
return result;
}
var cbTimeout;
function onWritten() {
if (callbackNewline) {
connection.cb = function(d) {
var newLineIdx = connection.received.indexOf("\n");
if (newLineIdx>=0) {
var l = connection.received.substr(0,newLineIdx);
connection.received = connection.received.substr(newLineIdx+1);
connection.cb = undefined;
if (cbTimeout) clearTimeout(cbTimeout);
cbTimeout = undefined;
if (callback)
callback(l);
isBusy = false;
handleQueue();
}
};
}
// wait for any received data if we have a callback...
var maxTime = 300; // 30 sec - Max time we wait in total, even if getting data
var dataWaitTime = callbackNewline ? 100/*10 sec if waiting for newline*/ : 3/*300ms*/;
var maxDataTime = dataWaitTime; // max time we wait after having received data
cbTimeout = setTimeout(function timeout() {
cbTimeout = undefined;
if (maxTime) maxTime--;
if (maxDataTime) maxDataTime--;
if (connection.hadData) maxDataTime=dataWaitTime;
if (maxDataTime && maxTime) {
cbTimeout = setTimeout(timeout, 100);
} else {
connection.cb = undefined;
if (callback)
callback(connection.received);
isBusy = false;
handleQueue();
connection.received = "";
}
connection.hadData = false;
}, 100);
}
if (!connection.txInProgress) connection.received = "";
isBusy = true;
connection.write(data, onWritten);
return result
}
// ----------------------------------------------------------
Puck = {
/// Are we writing debug information? 0 is no, 1 is some, 2 is more, 3 is all.
debug : Puck.debug,
/// Should we use flow control? Default is true
flowControl : true,
/// Used internally to write log information - you can replace this with your own function
log : function(level, s) { if (level <= this.debug) console.log("<BLE> "+s)},
/// Called with the current send progress or undefined when done - you can replace this with your own function
writeProgress : Puck.writeProgress,
connect : function(callback) {
setTimeout(callback, 10);
},
write : write,
eval : function(expr, cb) {
const response = write('\x10Bluetooth.println(JSON.stringify(' + expr + '))\n', true)
.then(function (d) {
try {
return JSON.parse(d);
} catch (e) {
log(1, "Unable to decode " + JSON.stringify(d) + ", got " + e.toString());
return Promise.reject(d);
}
});
if (cb) {
return void response.then(cb, (err) => cb(null, err));
} else {
return response;
}
},
isConnected : function() { return true; },
getConnection : function() { return connection; },
close : function() {
if (connection)
connection.close();
},
};
// no need for header
document.getElementsByTagName("header")[0].style="display:none";
// force connection attempt automatically
setTimeout(function() {
getInstalledApps(true).catch(err => {
showToast("Device connection failed, "+err,"error");
if ("object"==typeof err) console.log(err.stack);
});
}, 500);
}
</script>
</body>
</html>

View File

@ -1,2 +1,5 @@
0.01: New App!
0.02: Fix the settings bug and some tweaking
0.03: Do not alarm while charging
0.04: Obey system quiet mode
0.05: Battery optimisation, add the pause option, bug fixes

View File

@ -1,13 +1,14 @@
# 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
The alert will popup only if you didn't take your short walk yet.
Different settings can be personalized:
- Enable : Enable/Disable the app
- Start hour: Hour to start the reminder
- End hour: Hour to end the reminder
- Max inactivity: Maximum inactivity time to allow before the alert. From 15 to 60 min
- Dismiss delay: Delay added before the next alert if the alert is dismissed. From 5 to 15 min
- Max inactivity: Maximum inactivity time to allow before the alert. From 15 to 120 min
- Dismiss delay: Delay added before the next alert if the alert is dismissed. From 5 to 60 min
- Pause delay: Same as Dismiss delay but longer (usefull for meetings and such). From 30 to 240 min
- Min steps: Minimal amount of steps to count as an activity

View File

@ -1,37 +1,42 @@
function drawAlert() {
E.showPrompt("Inactivity detected", {
title: "Activity reminder",
buttons : {"Ok": true,"Dismiss": false}
buttons: { "Ok": 1, "Dismiss": 2, "Pause": 3 }
}).then(function (v) {
if(v == true){
stepsArray = stepsArray.slice(0, activityreminder.maxInnactivityMin - 3);
require("activityreminder").saveStepsArray(stepsArray);
if (v == 1) {
activityreminder_data.okDate = new Date();
}
if(v == false){
stepsArray = stepsArray.slice(0, activityreminder.maxInnactivityMin - activityreminder.dismissDelayMin);
require("activityreminder").saveStepsArray(stepsArray);
if (v == 2) {
activityreminder_data.dismissDate = new Date();
}
if (v == 3) {
activityreminder_data.pauseDate = new Date();
}
activityreminder.saveData(activityreminder_data);
load();
});
// Obey system quiet mode:
if (!(storage.readJSON('setting.json', 1) || {}).quiet) {
Bangle.buzz(400);
}
setTimeout(load, 20000);
}
function run() {
if(stepsArray.length == activityreminder.maxInnactivityMin){
if (stepsArray[0] - stepsArray[stepsArray.length-1] < activityreminder.minSteps){
if (activityreminder.mustAlert(activityreminder_data, activityreminder_settings)) {
drawAlert();
}
} else {
eval(require("Storage").read("activityreminder.settings.js"))(()=>load());
eval(storage.read("activityreminder.settings.js"))(() => load());
}
}
const activityreminder = require("activityreminder");
const storage = require("Storage");
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
activityreminder = require("activityreminder").loadSettings();
stepsArray = require("activityreminder").loadStepsArray();
const activityreminder_settings = activityreminder.loadSettings();
const activityreminder_data = activityreminder.loadData();
run();

View File

@ -1,29 +1,45 @@
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);
if (isNotWorn()) return;
let now = new Date();
let h = now.getHours();
let health = Bangle.getHealthStatus("day");
if (h >= activityreminder_settings.startHour && h < activityreminder_settings.endHour) {
if (health.steps - activityreminder_data.stepsOnDate >= activityreminder_settings.minSteps // more steps made than needed
|| health.steps < activityreminder_data.stepsOnDate) { // new day or reboot of the watch
activityreminder_data.stepsOnDate = health.steps;
activityreminder_data.stepsDate = now;
activityreminder.saveData(activityreminder_data);
/* todo in a futur release
add settimer to trigger like 10 secs after the stepsDate + minSteps
cancel all other timers of this app
*/
}
else{
if(stepsArray != []){
stepsArray = [];
require("activityreminder").saveStepsArray(stepsArray);
}
}
if(stepsArray.length >= activityreminder.maxInnactivityMin){
if (stepsArray[0] - stepsArray[stepsArray.length-1] < activityreminder.minSteps){
if(activityreminder.mustAlert(activityreminder_data, activityreminder_settings)){
load('activityreminder.app.js');
}
}
}
function isNotWorn() {
// todo in a futur release check temperature and mouvement in a futur release
return Bangle.isCharging();
}
activityreminder = require("activityreminder").loadSettings();
if(activityreminder.enabled) {
stepsArray = require("activityreminder").loadStepsArray();
const activityreminder = require("activityreminder");
const activityreminder_settings = activityreminder.loadSettings();
if (activityreminder_settings.enabled) {
const activityreminder_data = activityreminder.loadData();
if(activityreminder_data.firstLoad){
activityreminder_data.firstLoad =false;
activityreminder.saveData(activityreminder_data);
}
setInterval(run, 60000);
/* todo in a futur release
increase setInterval time to something that is still sensible (5 mins ?)
add settimer to trigger like 10 secs after the stepsDate + minSteps
cancel all other timers of this app
*/
}

View File

@ -1,3 +1,5 @@
const storage = require("Storage");
exports.loadSettings = function () {
return Object.assign({
enabled: true,
@ -5,18 +7,51 @@ exports.loadSettings = function() {
endHour: 20,
maxInnactivityMin: 30,
dismissDelayMin: 15,
pauseDelayMin: 120,
minSteps: 50
}, require("Storage").readJSON("activityreminder.s.json", true) || {});
}, storage.readJSON("activityreminder.s.json", true) || {});
};
exports.writeSettings = function (settings) {
require("Storage").writeJSON("activityreminder.s.json", settings);
storage.writeJSON("activityreminder.s.json", settings);
};
exports.saveStepsArray = function(stepsArray) {
require("Storage").writeJSON("activityreminder.sa.json", stepsArray);
exports.saveData = function (data) {
storage.writeJSON("activityreminder.data.json", data);
};
exports.loadStepsArray = function(){
return require("Storage").readJSON("activityreminder.sa.json") || [];
exports.loadData = function () {
let health = Bangle.getHealthStatus("day");
const data = Object.assign({
firstLoad: true,
stepsDate: new Date(),
stepsOnDate: health.steps,
okDate: new Date(1970),
dismissDate: new Date(1970),
pauseDate: new Date(1970),
},
storage.readJSON("activityreminder.data.json") || {});
if(typeof(data.stepsDate) == "string")
data.stepsDate = new Date(data.stepsDate);
if(typeof(data.okDate) == "string")
data.okDate = new Date(data.okDate);
if(typeof(data.dismissDate) == "string")
data.dismissDate = new Date(data.dismissDate);
if(typeof(data.pauseDate) == "string")
data.pauseDate = new Date(data.pauseDate);
return data;
};
exports.mustAlert = function(activityreminder_data, activityreminder_settings) {
let now = new Date();
if ((now - activityreminder_data.stepsDate) / 60000 > activityreminder_settings.maxInnactivityMin) { // inactivity detected
if ((now - activityreminder_data.okDate) / 60000 > 3 && // last alert anwsered with ok was more than 3 min ago
(now - activityreminder_data.dismissDate) / 60000 > activityreminder_settings.dismissDelayMin && // last alert was more than dismissDelayMin ago
(now - activityreminder_data.pauseDate) / 60000 > activityreminder_settings.pauseDelayMin) { // last alert was more than pauseDelayMin ago
return true;
}
}
return false;
}

View File

@ -3,7 +3,7 @@
"name": "Activity Reminder",
"shortName":"Activity Reminder",
"description": "A reminder to take short walks for the ones with a sedentary lifestyle",
"version":"0.02",
"version":"0.05",
"icon": "app.png",
"type": "app",
"tags": "tool,activity",
@ -18,6 +18,6 @@
],
"data": [
{"name": "activityreminder.s.json"},
{"name": "activityreminder.sa.json"}
{"name": "activityreminder.data.json"}
]
}

View File

@ -1,6 +1,7 @@
(function (back) {
// Load settings
var settings = require("activityreminder").loadSettings();
const activityreminder = require("activityreminder");
const settings = activityreminder.loadSettings();
// Show the menu
E.showMenu({
@ -11,7 +12,7 @@
format: v => v ? "Yes" : "No",
onchange: v => {
settings.enabled = v;
require("activityreminder").writeSettings(settings);
activityreminder.writeSettings(settings);
}
},
'Start hour': {
@ -19,7 +20,7 @@
min: 0, max: 24,
onchange: v => {
settings.startHour = v;
require("activityreminder").writeSettings(settings);
activityreminder.writeSettings(settings);
}
},
'End hour': {
@ -27,7 +28,7 @@
min: 0, max: 24,
onchange: v => {
settings.endHour = v;
require("activityreminder").writeSettings(settings);
activityreminder.writeSettings(settings);
}
},
'Max inactivity': {
@ -35,7 +36,7 @@
min: 15, max: 120,
onchange: v => {
settings.maxInnactivityMin = v;
require("activityreminder").writeSettings(settings);
activityreminder.writeSettings(settings);
},
format: x => {
return x + " min";
@ -43,10 +44,21 @@
},
'Dismiss delay': {
value: settings.dismissDelayMin,
min: 5, max: 15,
min: 5, max: 60,
onchange: v => {
settings.dismissDelayMin = v;
require("activityreminder").writeSettings(settings);
activityreminder.writeSettings(settings);
},
format: x => {
return x + " min";
}
},
'Pause delay': {
value: settings.pauseDelayMin,
min: 30, max: 240,
onchange: v => {
settings.pauseDelayMin = v;
activityreminder.writeSettings(settings);
},
format: x => {
return x + " min";
@ -57,7 +69,7 @@
min: 10, max: 500,
onchange: v => {
settings.minSteps = v;
require("activityreminder").writeSettings(settings);
activityreminder.writeSettings(settings);
}
}
});

View File

@ -24,3 +24,8 @@
0.23: Fix regression with Days of Week (#1735)
0.24: Automatically save the alarm/timer when the user returns to the main menu using the back arrow
Add "Enable All", "Disable All" and "Remove All" actions
0.25: Fix redrawing selected Alarm/Timer entry inside edit submenu
0.26: Add support for Monday as first day of the week (#1780)
0.27: New UI!
0.28: Fix bug with alarms not firing when configured to fire only once
0.29: Fix wrong 'dow' handling in new timer if first day of week is Monday

View File

@ -1,7 +1,31 @@
Alarms & Timers
===============
# Alarms & Timers
This app allows you to add/modify any alarms and timers.
It uses the [`sched` library](https://github.com/espruino/BangleApps/blob/master/apps/sched)
to handle the alarm scheduling in an efficient way that can work alongside other apps.
It uses the [`sched` library](https://github.com/espruino/BangleApps/blob/master/apps/sched) to handle the alarm scheduling in an efficient way that can work alongside other apps.
## Menu overview
- `New...`
- `New Alarm` &rarr; Configure a new alarm
- `Repeat` &rarr; Select when the alarm will fire. You can select a predefined option (_Once_, _Every Day_, _Workdays_ or _Weekends_ or you can configure the days freely)
- `New Timer` &rarr; Configure a new timer
- `Advanced`
- `Scheduler settings` &rarr; Open the [Scheduler](https://github.com/espruino/BangleApps/tree/master/apps/sched) settings page, see its [README](https://github.com/espruino/BangleApps/blob/master/apps/sched/README.md) for details
- `Enable All` &rarr; Enable _all_ disabled alarms & timers
- `Disable All` &rarr; Disable _all_ enabled alarms & timers
- `Delete All` &rarr; Delete _all_ alarms & timers
## Creator
- [Gordon Williams](https://github.com/gfwilliams)
## Main Contributors
- [Alessandro Cocco](https://github.com/alessandrococco) - New UI, full rewrite, new features
- [Sabin Iacob](https://github.com/m0n5t3r) - Auto snooze support
- [storm64](https://github.com/storm64) - Fix redrawing in submenus
## Attributions
All icons used in this app are from [icons8](https://icons8.com).

View File

@ -1,240 +1,358 @@
Bangle.loadWidgets();
Bangle.drawWidgets();
// 0 = Sunday (default), 1 = Monday
const firstDayOfWeek = (require("Storage").readJSON("setting.json", true) || {}).firstDayOfWeek || 0;
const WORKDAYS = 62
const WEEKEND = firstDayOfWeek ? 192 : 65;
const EVERY_DAY = firstDayOfWeek ? 254 : 127;
const iconAlarmOn = "\0" + atob("GBiBAAAAAAAAAAYAYA4AcBx+ODn/nAP/wAf/4A/n8A/n8B/n+B/n+B/n+B/n+B/h+B/4+A/+8A//8Af/4AP/wAH/gAB+AAAAAAAAAA==");
const iconAlarmOff = "\0" + (g.theme.dark
? atob("GBjBAP////8AAAAAAAAGAGAOAHAcfjg5/5wD/8AH/+AP5/AP5/Af5/gf5/gf5wAf5gAf4Hgf+f4P+bYP8wMH84cD84cB8wMAebYAAf4AAHg=")
: atob("GBjBAP//AAAAAAAAAAAGAGAOAHAcfjg5/5wD/8AH/+AP5/AP5/Af5/gf5/gf5wAf5gAf4Hgf+f4P+bYP8wMH84cD84cB8wMAebYAAf4AAHg="));
const iconTimerOn = "\0" + (g.theme.dark
? atob("GBjBAP////8AAAAAAAAAAAAH/+AH/+ABgYABgYABgYAA/wAA/wAAfgAAPAAAPAAAfgAA5wAAwwABgYABgYABgYAH/+AH/+AAAAAAAAAAAAA=")
: atob("GBjBAP//AAAAAAAAAAAAAAAH/+AH/+ABgYABgYABgYAA/wAA/wAAfgAAPAAAPAAAfgAA5wAAwwABgYABgYABgYAH/+AH/+AAAAAAAAAAAAA="));
const iconTimerOff = "\0" + (g.theme.dark
? atob("GBjBAP////8AAAAAAAAAAAAH/+AH/+ABgYABgYABgYAA/wAA/wAAfgAAPAAAPAAAfgAA5HgAwf4BgbYBgwMBg4cH84cH8wMAAbYAAf4AAHg=")
: atob("GBjBAP//AAAAAAAAAAAAAAAH/+AH/+ABgYABgYABgYAA/wAA/wAAfgAAPAAAPAAAfgAA5HgAwf4BgbYBgwMBg4cH84cH8wMAAbYAAf4AAHg="));
// An array of alarm objects (see sched/README.md)
let alarms = require("sched").getAlarms();
var alarms = require("sched").getAlarms();
function getCurrentTime() {
let time = new Date();
return (
time.getHours() * 3600000 +
time.getMinutes() * 60000 +
time.getSeconds() * 1000
);
function handleFirstDayOfWeek(dow) {
if (firstDayOfWeek == 1) {
if ((dow & 1) == 1) {
// In the scheduler API Sunday is 1.
// Here the week starts on Monday and Sunday is ON so
// when I read the dow I need to move Sunday to 128...
dow += 127;
} else if ((dow & 128) == 128) {
// ... and then when I write the dow I need to move Sunday back to 1.
dow -= 127;
}
}
return dow;
}
function saveAndReload() {
require("sched").setAlarms(alarms);
require("sched").reload();
}
// Check the first day of week and update the dow field accordingly (alarms only!)
alarms.filter(e => e.timer === undefined).forEach(a => a.dow = handleFirstDayOfWeek(a.dow));
function showMainMenu() {
// Timer img "\0"+atob("DhKBAP////MDDAwwMGGBzgPwB4AeAPwHOBhgwMMzDez////w")
// Alarm img "\0"+atob("FBSBAABgA4YcMPDGP8Zn/mx/48//PP/zD/8A//AP/wD/8A//AP/wH/+D//w//8AAAADwAAYA")
const menu = {
'': { 'title': /*LANG*/'Alarms&Timers' },
/*LANG*/'< Back' : ()=>{load();},
/*LANG*/'New Alarm': ()=>editAlarm(-1),
/*LANG*/'New Timer': ()=>editTimer(-1)
"": { "title": /*LANG*/"Alarms & Timers" },
"< Back": () => load(),
/*LANG*/"New...": () => showNewMenu()
};
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 = " "+require("sched").formatTime(alarm.timer);
} else {
type = /*LANG*/"Alarm";
txt = " "+require("sched").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);
}
alarms.forEach((e, index) => {
var label = e.timer
? require("time_utils").formatDuration(e.timer)
: require("time_utils").formatTime(e.t) + (e.rp ? ` ${decodeDOW(e)}` : "");
menu[label] = {
value: e.on ? (e.timer ? iconTimerOn : iconAlarmOn) : (e.timer ? iconTimerOff : iconAlarmOff),
onchange: () => setTimeout(e.timer ? showEditTimerMenu : showEditAlarmMenu, 10, e, index)
};
});
if (alarms.some(e => !e.on)) {
menu[/*LANG*/"Enable All"] = () => enableAll(true);
}
if (alarms.some(e => e.on)) {
menu[/*LANG*/"Disable All"] = () => enableAll(false);
}
if (alarms.length > 0) {
menu[/*LANG*/"Delete All"] = () => deleteAll();
}
menu[/*LANG*/"Advanced"] = () => showAdvancedMenu();
if (WIDGETS["alarm"]) WIDGETS["alarm"].reload();
return E.showMenu(menu);
}
function editDOW(dow, onchange) {
const menu = {
'': { 'title': /*LANG*/'Days of Week' },
/*LANG*/'< Back' : () => onchange(dow)
};
for (let i = 0; i < 7; i++) (i => {
let dayOfWeek = require("locale").dow({ getDay: () => i });
menu[dayOfWeek] = {
value: !!(dow&(1<<i)),
format: v => v ? /*LANG*/"Yes" : /*LANG*/"No",
onchange: v => v ? dow |= 1<<i : dow &= ~(1<<i),
};
})(i);
E.showMenu(menu);
}
function editAlarm(alarmIndex, alarm) {
let newAlarm = alarmIndex < 0;
let a = require("sched").newDefaultAlarm();
if (!newAlarm) Object.assign(a, alarms[alarmIndex]);
if (alarm) Object.assign(a,alarm);
let t = require("sched").decodeTime(a.t);
const menu = {
'': { 'title': /*LANG*/'Alarm' },
/*LANG*/'< Back': () => {
saveAlarm(newAlarm, alarmIndex, a, t);
showMainMenu();
},
/*LANG*/'Hours': {
value: t.hrs, min : 0, max : 23, wrap : true,
onchange: v => t.hrs=v
},
/*LANG*/'Minutes': {
value: t.mins, min : 0, max : 59, wrap : true,
onchange: v => t.mins=v
},
/*LANG*/'Enabled': {
value: a.on,
format: v => v ? /*LANG*/"On" : /*LANG*/"Off",
onchange: v=>a.on=v
},
/*LANG*/'Repeat': {
value: a.rp,
format: v => v ? /*LANG*/"Yes" : /*LANG*/"No",
onchange: v => a.rp = v
},
/*LANG*/'Days': {
value: "SMTWTFS".split("").map((d,n)=>a.dow&(1<<n)?d:".").join(""),
onchange: () => editDOW(a.dow, d => {
a.dow = d;
a.t = require("sched").encodeTime(t);
editAlarm(alarmIndex, a);
})
},
/*LANG*/'Vibrate': require("buzz_menu").pattern(a.vibrate, v => a.vibrate=v ),
/*LANG*/'Auto Snooze': {
value: a.as,
format: v => v ? /*LANG*/"Yes" : /*LANG*/"No",
onchange: v => a.as = v
}
};
menu[/*LANG*/"Cancel"] = () => showMainMenu();
if (!newAlarm) {
menu[/*LANG*/"Delete"] = function () {
alarms.splice(alarmIndex, 1);
saveAndReload();
showMainMenu();
};
}
return E.showMenu(menu);
}
function saveAlarm(newAlarm, alarmIndex, a, t) {
a.t = require("sched").encodeTime(t);
a.last = (a.t < getCurrentTime()) ? (new Date()).getDate() : 0;
if (newAlarm) {
alarms.push(a);
} else {
alarms[alarmIndex] = a;
}
saveAndReload();
}
function editTimer(alarmIndex, alarm) {
let newAlarm = alarmIndex < 0;
let a = require("sched").newDefaultTimer();
if (!newAlarm) Object.assign(a, alarms[alarmIndex]);
if (alarm) Object.assign(a,alarm);
let t = require("sched").decodeTime(a.timer);
const menu = {
'': { 'title': /*LANG*/'Timer' },
/*LANG*/'< Back': () => {
saveTimer(newAlarm, alarmIndex, a, t);
showMainMenu();
},
/*LANG*/'Hours': {
value: t.hrs, min : 0, max : 23, wrap : true,
onchange: v => t.hrs=v
},
/*LANG*/'Minutes': {
value: t.mins, min : 0, max : 59, wrap : true,
onchange: v => t.mins=v
},
/*LANG*/'Enabled': {
value: a.on,
format: v => v ? /*LANG*/"On" : /*LANG*/"Off",
onchange: v => a.on = v
},
/*LANG*/'Vibrate': require("buzz_menu").pattern(a.vibrate, v => a.vibrate=v ),
};
menu[/*LANG*/"Cancel"] = () => showMainMenu();
if (!newAlarm) {
menu[/*LANG*/"Delete"] = function() {
alarms.splice(alarmIndex,1);
saveAndReload();
showMainMenu();
};
}
return E.showMenu(menu);
}
function saveTimer(newAlarm, alarmIndex, a, t) {
a.timer = require("sched").encodeTime(t);
a.t = getCurrentTime() + a.timer;
a.last = 0;
if (newAlarm) {
alarms.push(a);
} else {
alarms[alarmIndex] = a;
}
saveAndReload();
}
function enableAll(on) {
E.showPrompt(/*LANG*/"Are you sure?", {
title: on ? /*LANG*/"Enable All" : /*LANG*/"Disable All"
}).then((confirm) => {
if (confirm) {
alarms.forEach(alarm => alarm.on = on);
saveAndReload();
}
showMainMenu();
function showNewMenu() {
E.showMenu({
"": { "title": /*LANG*/"New..." },
"< Back": () => showMainMenu(),
/*LANG*/"Alarm": () => showEditAlarmMenu(undefined, undefined),
/*LANG*/"Timer": () => showEditTimerMenu(undefined, undefined)
});
}
function showEditAlarmMenu(selectedAlarm, alarmIndex) {
var isNew = alarmIndex === undefined;
var alarm = require("sched").newDefaultAlarm();
alarm.dow = handleFirstDayOfWeek(alarm.dow);
if (selectedAlarm) {
Object.assign(alarm, selectedAlarm);
}
var time = require("time_utils").decodeTime(alarm.t);
const menu = {
"": { "title": isNew ? /*LANG*/"New Alarm" : /*LANG*/"Edit Alarm" },
"< Back": () => {
saveAlarm(alarm, alarmIndex, time);
showMainMenu();
},
/*LANG*/"Hour": {
value: time.h,
format: v => ("0" + v).substr(-2),
min: 0,
max: 23,
wrap: true,
onchange: v => time.h = v
},
/*LANG*/"Minute": {
value: time.m,
format: v => ("0" + v).substr(-2),
min: 0,
max: 59,
wrap: true,
onchange: v => time.m = v
},
/*LANG*/"Enabled": {
value: alarm.on,
onchange: v => alarm.on = v
},
/*LANG*/"Repeat": {
value: decodeDOW(alarm),
onchange: () => setTimeout(showEditRepeatMenu, 100, alarm.rp, alarm.dow, (repeat, dow) => {
alarm.rp = repeat;
alarm.dow = dow;
alarm.t = require("time_utils").encodeTime(time);
setTimeout(showEditAlarmMenu, 10, alarm, alarmIndex);
})
},
/*LANG*/"Vibrate": require("buzz_menu").pattern(alarm.vibrate, v => alarm.vibrate = v),
/*LANG*/"Auto Snooze": {
value: alarm.as,
onchange: v => alarm.as = v
},
/*LANG*/"Cancel": () => showMainMenu()
};
if (!isNew) {
menu[/*LANG*/"Delete"] = () => {
E.showPrompt(/*LANG*/"Are you sure?", { title: /*LANG*/"Delete Alarm" }).then((confirm) => {
if (confirm) {
alarms.splice(alarmIndex, 1);
saveAndReload();
showMainMenu();
} else {
alarm.t = require("time_utils").encodeTime(time);
setTimeout(showEditAlarmMenu, 10, alarm, alarmIndex);
}
});
};
}
E.showMenu(menu);
}
function saveAlarm(alarm, alarmIndex, time) {
alarm.t = require("time_utils").encodeTime(time);
alarm.last = alarm.t < require("time_utils").getCurrentTimeMillis() ? new Date().getDate() : 0;
if (alarmIndex === undefined) {
alarms.push(alarm);
} else {
alarms[alarmIndex] = alarm;
}
saveAndReload();
}
function saveAndReload() {
// Before saving revert the dow to the standard format (alarms only!)
alarms.filter(e => e.timer === undefined).forEach(a => a.dow = handleFirstDayOfWeek(a.dow));
require("sched").setAlarms(alarms);
require("sched").reload();
// Fix after save
alarms.filter(e => e.timer === undefined).forEach(a => a.dow = handleFirstDayOfWeek(a.dow));
}
function decodeDOW(alarm) {
return alarm.rp
? require("date_utils")
.dows(firstDayOfWeek, 2)
.map((day, index) => alarm.dow & (1 << (index + firstDayOfWeek)) ? day : "_")
.join("")
.toLowerCase()
: "Once"
}
function showEditRepeatMenu(repeat, dow, dowChangeCallback) {
var originalRepeat = repeat;
var originalDow = dow;
var isCustom = repeat && dow != WORKDAYS && dow != WEEKEND && dow != EVERY_DAY;
const menu = {
"": { "title": /*LANG*/"Repeat Alarm" },
"< Back": () => dowChangeCallback(repeat, dow),
/*LANG*/"Once": {
// The alarm will fire once. Internally it will be saved
// as "fire every days" BUT the repeat flag is false so
// we avoid messing up with the scheduler.
value: !repeat,
onchange: () => dowChangeCallback(false, EVERY_DAY)
},
/*LANG*/"Workdays": {
value: repeat && dow == WORKDAYS,
onchange: () => dowChangeCallback(true, WORKDAYS)
},
/*LANG*/"Weekends": {
value: repeat && dow == WEEKEND,
onchange: () => dowChangeCallback(true, WEEKEND)
},
/*LANG*/"Every Day": {
value: repeat && dow == EVERY_DAY,
onchange: () => dowChangeCallback(true, EVERY_DAY)
},
/*LANG*/"Custom": {
value: isCustom ? decodeDOW({ rp: true, dow: dow }) : false,
onchange: () => setTimeout(showCustomDaysMenu, 10, isCustom ? dow : EVERY_DAY, dowChangeCallback, originalRepeat, originalDow)
}
};
E.showMenu(menu);
}
function showCustomDaysMenu(dow, dowChangeCallback, originalRepeat, originalDow) {
const menu = {
"": { "title": /*LANG*/"Custom Days" },
"< Back": () => {
// If the user unchecks all the days then we assume repeat = once
// and we force the dow to every day.
var repeat = dow > 0;
dowChangeCallback(repeat, repeat ? dow : EVERY_DAY)
}
};
require("date_utils").dows(firstDayOfWeek).forEach((day, i) => {
menu[day] = {
value: !!(dow & (1 << (i + firstDayOfWeek))),
onchange: v => v ? (dow |= 1 << (i + firstDayOfWeek)) : (dow &= ~(1 << (i + firstDayOfWeek)))
};
});
menu[/*LANG*/"Cancel"] = () => setTimeout(showEditRepeatMenu, 10, originalRepeat, originalDow, dowChangeCallback)
E.showMenu(menu);
}
function showEditTimerMenu(selectedTimer, timerIndex) {
var isNew = timerIndex === undefined;
var timer = require("sched").newDefaultTimer();
if (selectedTimer) {
Object.assign(timer, selectedTimer);
}
var time = require("time_utils").decodeTime(timer.timer);
const menu = {
"": { "title": isNew ? /*LANG*/"New Timer" : /*LANG*/"Edit Timer" },
"< Back": () => {
saveTimer(timer, timerIndex, time);
showMainMenu();
},
/*LANG*/"Hours": {
value: time.h,
min: 0,
max: 23,
wrap: true,
onchange: v => time.h = v
},
/*LANG*/"Minutes": {
value: time.m,
min: 0,
max: 59,
wrap: true,
onchange: v => time.m = v
},
/*LANG*/"Enabled": {
value: timer.on,
onchange: v => timer.on = v
},
/*LANG*/"Vibrate": require("buzz_menu").pattern(timer.vibrate, v => timer.vibrate = v),
};
if (!isNew) {
menu[/*LANG*/"Delete"] = () => {
E.showPrompt(/*LANG*/"Are you sure?", { title: /*LANG*/"Delete Timer" }).then((confirm) => {
if (confirm) {
alarms.splice(timerIndex, 1);
saveAndReload();
showMainMenu();
} else {
timer.timer = require("time_utils").encodeTime(time);
setTimeout(showEditTimerMenu, 10, timer, timerIndex)
}
});
};
}
E.showMenu(menu);
}
function saveTimer(timer, timerIndex, time) {
timer.timer = require("time_utils").encodeTime(time);
timer.t = require("time_utils").getCurrentTimeMillis() + timer.timer;
timer.last = 0;
if (timerIndex === undefined) {
alarms.push(timer);
} else {
alarms[timerIndex] = timer;
}
saveAndReload();
}
function showAdvancedMenu() {
E.showMenu({
"": { "title": /*LANG*/"Advanced" },
"< Back": () => showMainMenu(),
/*LANG*/"Scheduler Settings": () => eval(require("Storage").read("sched.settings.js"))(() => showAdvancedMenu()),
/*LANG*/"Enable All": () => enableAll(true),
/*LANG*/"Disable All": () => enableAll(false),
/*LANG*/"Delete All": () => deleteAll()
});
}
function enableAll(on) {
if (alarms.filter(e => e.on == !on).length == 0) {
E.showAlert(
on ? /*LANG*/"Nothing to Enable" : /*LANG*/"Nothing to Disable",
on ? /*LANG*/"Enable All" : /*LANG*/"Disable All"
).then(() => showAdvancedMenu());
} else {
E.showPrompt(/*LANG*/"Are you sure?", { title: on ? /*LANG*/"Enable All" : /*LANG*/"Disable All" }).then((confirm) => {
if (confirm) {
alarms.forEach(alarm => alarm.on = on);
saveAndReload();
showMainMenu();
} else {
showAdvancedMenu();
}
});
}
}
function deleteAll() {
if (alarms.length == 0) {
E.showAlert(/*LANG*/"Nothing to delete", /*LANG*/"Delete All").then(() => showAdvancedMenu());
} else {
E.showPrompt(/*LANG*/"Are you sure?", {
title: /*LANG*/"Delete All"
}).then((confirm) => {
if (confirm) {
alarms = [];
saveAndReload();
}
showMainMenu();
} else {
showAdvancedMenu();
}
});
}
}
showMainMenu();

View File

@ -2,7 +2,7 @@
"id": "alarm",
"name": "Alarms & Timers",
"shortName": "Alarms",
"version": "0.24",
"version": "0.29",
"description": "Set alarms and timers on your Bangle",
"icon": "app.png",
"tags": "tool,alarm,widget",
@ -13,5 +13,18 @@
{ "name": "alarm.app.js", "url": "app.js" },
{ "name": "alarm.img", "url": "app-icon.js", "evaluate": true },
{ "name": "alarm.wid.js", "url": "widget.js" }
],
"screenshots": [
{ "url": "screenshot-1.png" },
{ "url": "screenshot-2.png" },
{ "url": "screenshot-3.png" },
{ "url": "screenshot-4.png" },
{ "url": "screenshot-5.png" },
{ "url": "screenshot-6.png" },
{ "url": "screenshot-7.png" },
{ "url": "screenshot-8.png" },
{ "url": "screenshot-9.png" },
{ "url": "screenshot-10.png" },
{ "url": "screenshot-11.png" }
]
}

BIN
apps/alarm/screenshot-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
apps/alarm/screenshot-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
apps/alarm/screenshot-3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
apps/alarm/screenshot-4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
apps/alarm/screenshot-5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
apps/alarm/screenshot-6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
apps/alarm/screenshot-7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
apps/alarm/screenshot-8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
apps/alarm/screenshot-9.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -7,3 +7,5 @@
0.06: Option to keep messages after a disconnect (default false) (fix #1186)
0.07: Include charging state in battery updates to phone
0.08: Handling of alarms
0.09: Alarm vibration, repeat, and auto-snooze now handled by sched
0.10: Fix SMS bug

View File

@ -21,7 +21,6 @@ of Gadgetbridge - making your phone make noise so you can find it.
* `Keep Msgs` - default is `Off`. When Gadgetbridge disconnects, should Bangle.js
keep any messages it has received, or should it delete them?
* `Messages` - launches the messages app, showing a list of messages
* `Alarms` - opens a submenu where you can set default settings for alarms such as vibration pattern, repeat, and auto snooze
## How it works

View File

@ -3,6 +3,7 @@
Bluetooth.println("");
Bluetooth.println(JSON.stringify(message));
}
var lastMsg;
var settings = require("Storage").readJSON("android.settings.json",1)||{};
//default alarm settings
@ -18,7 +19,17 @@
/* TODO: Call handling, fitness */
var HANDLERS = {
// {t:"notify",id:int, src,title,subject,body,sender,tel:string} add
"notify" : function() { Object.assign(event,{t:"add",positive:true, negative:true});require("messages").pushMessage(event); },
"notify" : function() {
Object.assign(event,{t:"add",positive:true, negative:true});
// Detect a weird GadgetBridge bug and fix it
// For some reason SMS messages send two GB notifications, with different sets of info
if (lastMsg && event.body == lastMsg.body && lastMsg.src == undefined && event.src == "Messages") {
// Mutate the other message
event.id = lastMsg.id;
}
lastMsg = event;
require("messages").pushMessage(event);
},
// {t:"notify~",id:int, title:string} // modified
"notify~" : function() { event.t="modify";require("messages").pushMessage(event); },
// {t:"notify-",id:int} // remove
@ -67,17 +78,13 @@
var dow = event.d[j].rep;
if (!dow) dow = 127; //if no DOW selected, set alarm to all DOW
var last = (event.d[j].h * 3600000 + event.d[j].m * 60000 < currentTime) ? (new Date()).getDate() : 0;
var a = {
id : "gb"+j,
appid : "gbalarms",
on : true,
t : event.d[j].h * 3600000 + event.d[j].m * 60000,
dow : ((dow&63)<<1) | (dow>>6), // Gadgetbridge sends DOW in a different format
last : last,
rp : settings.rp,
as : settings.as,
vibrate : settings.vibrate
};
var a = require("sched").newDefaultAlarm();
a.id = "gb"+j;
a.appid = "gbalarms";
a.on = true;
a.t = event.d[j].h * 3600000 + event.d[j].m * 60000;
a.dow = ((dow&63)<<1) | (dow>>6); // Gadgetbridge sends DOW in a different format
a.last = last;
alarms.push(a);
}
sched.setAlarms(alarms);

View File

@ -2,7 +2,7 @@
"id": "android",
"name": "Android Integration",
"shortName": "Android",
"version": "0.08",
"version": "0.10",
"description": "Display notifications/music/etc sent from the Gadgetbridge app on Android. This replaces the old 'Gadgetbridge' Bangle.js widget.",
"icon": "app.png",
"tags": "tool,system,messages,notifications,gadgetbridge",

View File

@ -25,27 +25,6 @@
}
},
/*LANG*/"Messages" : ()=>load("messages.app.js"),
/*LANG*/"Alarms" : () => E.showMenu({
"" : { "title" : /*LANG*/"Alarms" },
"< Back" : ()=>E.showMenu(mainmenu),
/*LANG*/"Vibrate": require("buzz_menu").pattern(settings.vibrate, v => {settings.vibrate = v; updateSettings();}),
/*LANG*/"Repeat": {
value: settings.rp,
format : v=>v?/*LANG*/"Yes":/*LANG*/"No",
onchange: v => {
settings.rp = v;
updateSettings();
}
},
/*LANG*/"Auto snooze": {
value: settings.as,
format : v=>v?/*LANG*/"Yes":/*LANG*/"No",
onchange: v => {
settings.as = v;
updateSettings();
}
},
})
};
E.showMenu(mainmenu);
})

View File

@ -7,3 +7,5 @@
0.07: Update to use Bangle.setUI instead of setWatch
0.08: Use theme colors, Layout library
0.09: Fix time/date disappearing after fullscreen notification
0.10: Use ClockFace library
0.11: Use ClockFace.is12Hour

View File

@ -3,7 +3,6 @@
* A simple digital clock showing seconds as a bar
**/
// Check settings for what type our clock should be
const is12Hour = (require("Storage").readJSON("setting.json", 1) || {})["12hour"];
let locale = require("locale");
{ // add some more info to locale
let date = new Date();
@ -11,13 +10,9 @@ let locale = require("locale");
date.setMonth(1, 3); // februari: months are zero-indexed
const localized = locale.date(date, true);
locale.dayFirst = /3.*2/.test(localized);
locale.hasMeridian = false;
if (typeof locale.meridian==="function") { // function does not exist if languages app is not installed
locale.hasMeridian = (locale.meridian(date)!=="");
}
}
Bangle.loadWidgets();
function renderBar(l) {
if (!this.fraction) {
// zero-size fillRect stills draws one line of pixels, we don't want that
@ -27,35 +22,9 @@ function renderBar(l) {
g.fillRect(l.x, l.y, l.x+width-1, l.y+l.height-1);
}
const Layout = require("Layout");
const layout = new Layout({
type: "v", c: [
{
type: "h", c: [
{id: "time", label: "88:88", type: "txt", font: "6x8:5", bgCol: g.theme.bg}, // size updated below
{id: "ampm", label: " ", type: "txt", font: "6x8:2", bgCol: g.theme.bg},
],
},
{id: "bar", type: "custom", fraction: 0, fillx: 1, height: 6, col: g.theme.fg2, render: renderBar},
{height: 40},
{id: "date", type: "txt", font: "10%", valign: 1},
],
}, {lazy: true});
// adjustments based on screen size and whether we display am/pm
let thickness; // bar thickness, same as time font "pixel block" size
if (is12Hour) {
// Maximum font size = (<screen width> - <ampm: 2chars * (2*6)px>) / (5chars * 6px)
thickness = Math.floor((g.getWidth()-24)/(5*6));
} else {
layout.ampm.label = "";
thickness = Math.floor(g.getWidth()/(5*6));
}
layout.bar.height = thickness+1;
layout.time.font = "6x8:"+thickness;
layout.update();
function timeText(date) {
if (!is12Hour) {
if (!clock.is12Hour) {
return locale.time(date, true);
}
const date12 = new Date(date.getTime());
@ -68,7 +37,7 @@ function timeText(date) {
return locale.time(date12, true);
}
function ampmText(date) {
return (is12Hour && locale.hasMeridian)? locale.meridian(date) : "";
return (clock.is12Hour && locale.hasMeridian) ? locale.meridian(date) : "";
}
function dateText(date) {
const dayName = locale.dow(date, true),
@ -78,31 +47,48 @@ function dateText(date) {
return `${dayName} ${dayMonth}`;
}
draw = function draw(force) {
if (!Bangle.isLCDOn()) {return;} // no drawing, also no new update scheduled
const date = new Date();
layout.time.label = timeText(date);
layout.ampm.label = ampmText(date);
layout.date.label = dateText(date);
const SECONDS_PER_MINUTE = 60;
layout.bar.fraction = date.getSeconds()/SECONDS_PER_MINUTE;
if (force) {
Bangle.drawWidgets();
layout.forgetLazyState();
}
layout.render();
// schedule update at start of next second
const millis = date.getMilliseconds();
setTimeout(draw, 1000-millis);
};
// Show launcher when button pressed
Bangle.setUI("clock");
Bangle.on("lcdPower", function(on) {
if (on) {
draw(true);
const ClockFace = require("ClockFace"),
clock = new ClockFace({
precision:1,
init: function() {
const Layout = require("Layout");
this.layout = new Layout({
type: "v", c: [
{
type: "h", c: [
{id: "time", label: "88:88", type: "txt", font: "6x8:5", col:g.theme.fg, bgCol: g.theme.bg}, // size updated below
{id: "ampm", label: " ", type: "txt", font: "6x8:2", col:g.theme.fg, bgCol: g.theme.bg},
],
},
{id: "bar", type: "custom", fraction: 0, fillx: 1, height: 6, col: g.theme.fg2, render: renderBar},
{height: 40},
{id: "date", type: "txt", font: "10%", valign: 1},
],
}, {lazy: true});
// adjustments based on screen size and whether we display am/pm
let thickness; // bar thickness, same as time font "pixel block" size
if (this.is12Hour) {
// Maximum font size = (<screen width> - <ampm: 2chars * (2*6)px>) / (5chars * 6px)
thickness = Math.floor((Bangle.appRect.w-24)/(5*6));
} else {
this.layout.ampm.label = "";
thickness = Math.floor(Bangle.appRect.w/(5*6));
}
this.layout.bar.height = thickness+1;
this.layout.time.font = "6x8:"+thickness;
this.layout.update();
},
update: function(date, c) {
if (c.m) this.layout.time.label = timeText(date);
if (c.h) this.layout.ampm.label = ampmText(date);
if (c.d) this.layout.date.label = dateText(date);
const SECONDS_PER_MINUTE = 60;
if (c.s) this.layout.bar.fraction = date.getSeconds()/SECONDS_PER_MINUTE;
this.layout.render();
},
resume: function() {
this.layout.forgetLazyState();
},
});
g.reset().clear();
Bangle.drawWidgets();
draw();
clock.start();

View File

@ -1,7 +1,7 @@
{
"id": "barclock",
"name": "Bar Clock",
"version": "0.09",
"version": "0.11",
"description": "A simple digital clock showing seconds as a bar",
"icon": "clock-bar.png",
"screenshots": [{"url":"screenshot.png"},{"url":"screenshot_pm.png"}],

View File

@ -51,3 +51,4 @@
0.45: Fix 0.44 regression (auto-add semi-colon between each boot code chunk)
0.46: Fix no clock found error on Bangle.js 2
0.47: Add polyfill for setUI with an object as an argument (fix regression for 2v12 devices after Layout module changed)
0.48: Workaround for BTHRM issues on Bangle.js 1 (write .boot files in chunks)

View File

@ -197,8 +197,18 @@ bootFiles.forEach(bootFile=>{
require('Storage').write('.boot0',"//"+bootFile+"\n",fileOffset);
fileOffset+=2+bootFile.length+1;
var bf = require('Storage').read(bootFile);
require('Storage').write('.boot0',bf,fileOffset);
fileOffset+=bf.length;
// we can't just write 'bf' in one go because at least in 2v13 and earlier
// Espruino wants to read the whole file into RAM first, and on Bangle.js 1
// it can be too big (especially BTHRM).
var bflen = bf.length;
var bfoffset = 0;
while (bflen) {
var bfchunk = Math.min(bflen, 2048);
require('Storage').write('.boot0',bf.substr(bfoffset, bfchunk),fileOffset);
fileOffset+=bfchunk;
bfoffset+=bfchunk;
bflen-=bfchunk;
}
require('Storage').write('.boot0',";\n",fileOffset);
fileOffset+=2;
});

View File

@ -1,7 +1,7 @@
{
"id": "boot",
"name": "Bootloader",
"version": "0.47",
"version": "0.48",
"description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings",
"icon": "bootloader.png",
"type": "bootloader",

View File

@ -1,2 +1,3 @@
0.01: New App
0.02: app keeps track of statistics now
0.03: Fix bug in valid word detection

View File

@ -110,7 +110,12 @@ class Wordle {
}
}
addGuess(w) {
if ((this.words.indexOf(w.toLowerCase())%5)!=0) {
let idx = -1;
do{
idx = this.words.indexOf(w.toLowerCase(), idx+1);
}
while(idx !== -1 && idx%5 !== 0);
if(idx%5 !== 0) {
E.showAlert(w+"\nis not a word", "Invalid word").then(function() {
layout = getKeyLayout("");
wordle.render(true);

View File

@ -2,7 +2,7 @@
"name": "Bordle",
"shortName":"Bordle",
"icon": "app.png",
"version":"0.02",
"version":"0.03",
"description": "Bangle version of a popular word search game",
"supports" : ["BANGLEJS2"],
"readme": "README.md",

View File

@ -1,14 +1,18 @@
{ "id": "bowserWF",
{
"id": "bowserWF",
"name": "Bowser Watchface",
"shortName":"Bowser Watchface",
"version":"0.01",
"version":"0.02",
"description": "Let bowser show you the time",
"icon": "app.png",
"tags": "",
"type": "clock",
"tags": "clock",
"supports" : ["BANGLEJS2"],
"allow_emulator": true,
"readme": "README.md",
"storage": [
{"name":"bowserWF.app.js","url":"app.js"},
{"name":"bowserWF.img","url":"app-icon.js","evaluate":true}
]
],
"data": [{"name":"bowserWF.json"}]
}

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwcCkGSpEgwQCChICFkgCBgkQoMEyFJAoICByVBkgLBkkSpIaDEwWShEkFgcIBAIdCEYQCBAoQdBAoYsBC4Q7BpICBEYQCDF4Q7CEYYCCEYUSKYYUDyRlCJQQIBNYYvBMoQCBkgjBFgxxCL4REDFgaPEHYgmCIgosCNYZEEDoZ0CNwY7CIIYgDEYtB9+e/dg/4AB2EJkYEB/mC/fn33Ivvz598v4MB/0BgoRCyVHvmW7Mg2EA8uD/EAh/IkGP/8AgVLtkA5El+FJvoRBgmf4Mkh0HkEQo9kyEfkeQofsgf4kmPCIP+h/gwULkkCncEu/ZsmRI4cEv0H8ESpdgEwMjwXI9kTCIOANYkSEYOCncF+UAjuR/ED+FBg/3/f8RgNgiVPkYdBtkT/Egv0Il+AoMfI4PgyX7vkW799F4Nl//4//woH/+0Ztvx7Fs335sk//5EB/IRBhACB77CBpEkgEIgGQoDRBgEggVBgDdBgGAgPv317ku+5cj334t+OSoI+B8gCBtlx7dkuFfgvx4N8yPbvgOB8ACBR4MA9mf4Egz3IgeChEDwDOBx/AjuCoN8y/JgkX4ME2FBjuQn65BgMtwELkGOEYOO4Mh2EJh+Sh/jOIMd+3fskRcwMTEwOWo98gCSBwFJkm2pfgx3II4PBk++/aABhEfwEInpZBvkX7MkJQMl2FHfANBjgCBlmQhHsgwjB33IkeyBAOChMcEwM9+/ZsBHBboMJtv2hd9+FHZANBVoM7kGC/fv2FJ9+GEYOAh//+UIaIMBkkQpEAHwIIBoMgiFJBANJEAMIkGShEkwQIChIIBhIIBhIaCkmQpIFCgmSEwYpDEYwCCpAICBwUEiQdFEwIICyAIDHwQ7CEYYpCEYWSpA7FDocSEwojBCgIaDIgYCBNwR0BNYYjFEwZTDLgQjGOgYvBEYQ7ENYlJFgQCCDohuGTYpBFkhoCSoQICEYIA="))

115
apps/bradbury/app.js Normal file

File diff suppressed because one or more lines are too long

BIN
apps/bradbury/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -0,0 +1,14 @@
{ "id": "bradbury",
"name": "Bradbury Watch",
"shortName":"Bradbury",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
"version":"0.01",
"description": "A watch face based on the classic Seiko model worn by one of my favorite authors. I didn't follow the original lcd layout exactly, opting for larger font for more easily readable time, and adding date, battery level, and step count; read from the device. Tapping the screen toggles visibility of widgets.",
"type": "clock",
"supports":["BANGLEJS2"],
"storage": [
{"name":"bradbury.app.js","url":"app.js"},
{"name":"bradbury.img","url":"app-icon.js","evaluate":true}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

@ -21,3 +21,4 @@
Adds some preset modes and a custom one
Restructure the settings menu
0.08: Allow scanning for devices in settings
0.09: Misc Fixes and improvements (https://github.com/espruino/BangleApps/pull/1655)

View File

@ -2,7 +2,7 @@
When this app is installed it overrides Bangle.js's build in heart rate monitor with an external Bluetooth one.
HRM is requested it searches on Bluetooth for a heart rate monitor, connects, and sends data back using the `Bangle.on('HRM'` event as if it came from the on board monitor.
HRM is requested it searches on Bluetooth for a heart rate monitor, connects, and sends data back using the `Bangle.on('HRM')` event as if it came from the on board monitor.
This means it's compatible with many Bangle.js apps including:
@ -16,19 +16,23 @@ as that requires live sensor data (rather than just BPM readings).
Just install the app, then install an app that uses the heart rate monitor.
Once installed it'll automatically try and connect to the first bluetooth
heart rate monitor it finds.
Once installed you will have to go into this app's settings while your heart rate monitor
is available for bluetooth pairing and scan for devices.
**To disable this and return to normal HRM, uninstall the app**
## Compatible Heart Rate Monitors
This works with any heart rate monitor providing the standard Bluetooth
Heart Rate Service (`180D`) and characteristic (`2A37`).
Heart Rate Service (`180D`) and characteristic (`2A37`). It additionally supports
the location (`2A38`) characteristic and the Battery Service (`180F`), reporting
that information in the `BTHRM` event when they are available.
So far it has been tested on:
* CooSpo Bluetooth Heart Rate Monitor
* Polar H10
* Polar OH1
* Wahoo TICKR X 2
## Internals
@ -38,7 +42,6 @@ This replaces `Bangle.setHRMPower` with its own implementation.
## TODO
* A widget to show connection state?
* Specify a specific device by address?
## Creator

View File

@ -18,34 +18,33 @@
if (settings.enabled){
function clearCache(){
var clearCache = function() {
return require('Storage').erase("bthrm.cache.json");
}
};
function getCache(){
var getCache = function() {
var cache = require('Storage').readJSON("bthrm.cache.json", true) || {};
if (settings.btname && settings.btname == cache.name) return cache;
if (settings.btid && settings.btid === cache.id) return cache;
clearCache();
return {};
}
};
function addNotificationHandler(characteristic){
var addNotificationHandler = function(characteristic) {
log("Setting notification handler: " + supportedCharacteristics[characteristic.uuid].handler);
characteristic.on('characteristicvaluechanged', supportedCharacteristics[characteristic.uuid].handler);
}
characteristic.on('characteristicvaluechanged', (ev) => supportedCharacteristics[characteristic.uuid].handler(ev.target.value));
};
function writeCache(cache){
var writeCache = function(cache) {
var oldCache = getCache();
if (oldCache != cache) {
if (oldCache !== cache) {
log("Writing cache");
require('Storage').writeJSON("bthrm.cache.json", cache)
require('Storage').writeJSON("bthrm.cache.json", cache);
} else {
log("No changes, don't write cache");
}
};
}
function characteristicsToCache(characteristics){
var characteristicsToCache = function(characteristics) {
log("Cache characteristics");
var cache = getCache();
if (!cache.characteristics) cache.characteristics = {};
@ -60,9 +59,9 @@
};
}
writeCache(cache);
}
};
function characteristicsFromCache(){
var characteristicsFromCache = function() {
log("Read cached characteristics");
var cache = getCache();
if (!cache.characteristics) return [];
@ -81,26 +80,22 @@
restored.push(r);
}
return restored;
}
};
log("Start");
var lastReceivedData={
};
var serviceFilters = [{
services: [ "180d" ]
}];
supportedServices = [
"0x180d", "0x180f"
var supportedServices = [
"0x180d", // Heart Rate
"0x180f", // Battery
];
var supportedCharacteristics = {
"0x2a37": {
//Heart rate measurement
handler: function (event){
var dv = event.target.value;
handler: function (dv){
var flags = dv.getUint8(0);
var bpm = (flags & 1) ? (dv.getUint16(1) / 100 /* ? */ ) : dv.getUint8(1); // 8 or 16 bit
@ -108,7 +103,7 @@
var sensorContact;
if (flags & 2){
sensorContact = (flags & 4) ? true : false;
sensorContact = !!(flags & 4);
}
var idx = 2 + (flags&1);
@ -121,11 +116,11 @@
var interval;
if (flags & 16) {
interval = [];
maxIntervalBytes = (dv.byteLength - idx);
var maxIntervalBytes = (dv.byteLength - idx);
log("Found " + (maxIntervalBytes / 2) + " rr data fields");
for(var i = 0 ; i < maxIntervalBytes / 2; i++){
interval[i] = dv.getUint16(idx,1); // in milliseconds
idx += 2
idx += 2;
}
}
@ -140,14 +135,14 @@
}
if (settings.replace){
var newEvent = {
var repEvent = {
bpm: bpm,
confidence: (sensorContact || sensorContact === undefined)? 100 : 0,
src: "bthrm"
};
log("Emitting HRM: ", newEvent);
Bangle.emit("HRM", newEvent);
log("Emitting HRM: ", repEvent);
Bangle.emit("HRM", repEvent);
}
var newEvent = {
@ -166,19 +161,18 @@
},
"0x2a38": {
//Body sensor location
handler: function(data){
handler: function(dv){
if (!lastReceivedData["0x180d"]) lastReceivedData["0x180d"] = {};
if (!lastReceivedData["0x180d"]["0x2a38"]) lastReceivedData["0x180d"]["0x2a38"] = data.target.value;
lastReceivedData["0x180d"]["0x2a38"] = parseInt(dv.buffer, 10);
}
},
"0x2a19": {
//Battery
handler: function (event){
handler: function (dv){
if (!lastReceivedData["0x180f"]) lastReceivedData["0x180f"] = {};
if (!lastReceivedData["0x180f"]["0x2a19"]) lastReceivedData["0x180f"]["0x2a19"] = event.target.value.getUint8(0);
lastReceivedData["0x180f"]["0x2a19"] = dv.getUint8(0);
}
}
};
var device;
@ -195,7 +189,7 @@
maxInterval: 1500
};
function waitingPromise(timeout) {
var waitingPromise = function(timeout) {
return new Promise(function(resolve){
log("Start waiting for " + timeout);
setTimeout(()=>{
@ -203,7 +197,7 @@
resolve();
}, timeout);
});
}
};
if (settings.enabled){
Bangle.isBTHRMOn = function(){
@ -215,7 +209,6 @@
};
}
if (settings.replace){
var origIsHRMOn = Bangle.isHRMOn;
@ -229,15 +222,15 @@
};
}
function clearRetryTimeout(){
var clearRetryTimeout = function() {
if (currentRetryTimeout){
log("Clearing timeout " + currentRetryTimeout);
clearTimeout(currentRetryTimeout);
currentRetryTimeout = undefined;
}
}
};
function retry(){
var retry = function() {
log("Retry");
if (!currentRetryTimeout){
@ -252,17 +245,17 @@
initBt();
}, clampedTime);
retryTime = Math.pow(retryTime, 1.1);
retryTime = Math.pow(clampedTime, 1.1);
if (retryTime > maxRetryTime){
retryTime = maxRetryTime;
}
} else {
log("Already in retry...");
}
}
};
var buzzing = false;
function onDisconnect(reason) {
var onDisconnect = function(reason) {
log("Disconnect: " + reason);
log("GATT: ", gatt);
log("Characteristics: ", characteristics);
@ -277,11 +270,23 @@
if (Bangle.isBTHRMOn()){
retry();
}
}
};
function createCharacteristicPromise(newCharacteristic){
var createCharacteristicPromise = function(newCharacteristic) {
log("Create characteristic promise: ", newCharacteristic);
var result = Promise.resolve();
// For values that can be read, go ahead and read them, even if we might be notified in the future
// Allows for getting initial state of infrequently updating characteristics, like battery
if (newCharacteristic.readValue){
result = result.then(()=>{
log("Reading data for " + JSON.stringify(newCharacteristic));
return newCharacteristic.readValue().then((data)=>{
if (supportedCharacteristics[newCharacteristic.uuid] && supportedCharacteristics[newCharacteristic.uuid].handler) {
supportedCharacteristics[newCharacteristic.uuid].handler(data);
}
});
});
}
if (newCharacteristic.properties.notify){
result = result.then(()=>{
log("Starting notifications for: ", newCharacteristic);
@ -290,31 +295,23 @@
log("Add " + settings.gracePeriodNotification + "ms grace period after starting notifications");
startPromise = startPromise.then(()=>{
log("Wait after connect");
waitingPromise(settings.gracePeriodNotification)
return waitingPromise(settings.gracePeriodNotification);
});
}
return startPromise;
});
} else if (newCharacteristic.read){
result = result.then(()=>{
readData(newCharacteristic);
log("Reading data for " + newCharacteristic);
return newCharacteristic.read().then((data)=>{
supportedCharacteristics[newCharacteristic.uuid].handler(data);
});
});
}
return result.then(()=>log("Handled characteristic: ", newCharacteristic));
}
};
function attachCharacteristicPromise(promise, characteristic){
var attachCharacteristicPromise = function(promise, characteristic) {
return promise.then(()=>{
log("Handling characteristic:", characteristic);
return createCharacteristicPromise(characteristic);
});
}
};
function createCharacteristicsPromise(newCharacteristics){
var createCharacteristicsPromise = function(newCharacteristics) {
log("Create characteristics promise: ", newCharacteristics);
var result = Promise.resolve();
for (var c of newCharacteristics){
@ -328,9 +325,9 @@
result = attachCharacteristicPromise(result, c);
}
return result.then(()=>log("Handled characteristics"));
}
};
function createServicePromise(service){
var createServicePromise = function(service) {
log("Create service promise: ", service);
var result = Promise.resolve();
result = result.then(()=>{
@ -338,15 +335,13 @@
return service.getCharacteristics().then((c)=>createCharacteristicsPromise(c));
});
return result.then(()=>log("Handled service" + service.uuid));
}
};
function attachServicePromise(promise, service){
var attachServicePromise = function(promise, service) {
return promise.then(()=>createServicePromise(service));
}
};
var reUseCounter = 0;
function initBt() {
var initBt = function () {
log("initBt with blockInit: " + blockInit);
if (blockInit){
retry();
@ -355,22 +350,18 @@
blockInit = true;
if (reUseCounter > 10){
log("Reuse counter to high");
gatt=undefined;
reUseCounter = 0;
}
var promise;
var filters;
if (!device){
var filters = serviceFilters;
if (settings.btname){
log("Configured device name", settings.btname);
filters = [{name: settings.btname}];
if (settings.btid){
log("Configured device id", settings.btid);
filters = [{ id: settings.btid }];
} else {
return;
}
log("Requesting device with filters", filters);
promise = NRF.requestDevice({ filters: filters });
promise = NRF.requestDevice({ filters: filters, active: true });
if (settings.gracePeriodRequest){
log("Add " + settings.gracePeriodRequest + "ms grace period after request");
@ -386,7 +377,6 @@
log("Wait after request");
return waitingPromise(settings.gracePeriodRequest);
});
} else {
promise = Promise.resolve();
log("Reuse device: ", device);
@ -398,13 +388,13 @@
} else {
log("GATT is new: ", gatt);
characteristics = [];
var cachedName = getCache().name;
if (device.name != cachedName){
log("Device name changed from " + cachedName + " to " + device.name + ", clearing cache");
var cachedId = getCache().id;
if (device.id !== cachedId){
log("Device ID changed from " + cachedId + " to " + device.id + ", clearing cache");
clearCache();
}
var newCache = getCache();
newCache.name = device.name;
newCache.id = device.id;
writeCache(newCache);
gatt = device.gatt;
}
@ -428,15 +418,27 @@
}
});
/* promise = promise.then(() => {
log(JSON.stringify(gatt.getSecurityStatus()));
if (gatt.getSecurityStatus()['bonded']) {
log("Already bonded");
return Promise.resolve();
} else {
log("Start bonding");
return gatt.startBonding()
.then(() => console.log(gatt.getSecurityStatus()));
}
});*/
promise = promise.then(()=>{
if (!characteristics || characteristics.length == 0){
if (!characteristics || characteristics.length === 0){
characteristics = characteristicsFromCache();
}
});
promise = promise.then(()=>{
var characteristicsPromise = Promise.resolve();
if (characteristics.length == 0){
if (characteristics.length === 0){
characteristicsPromise = characteristicsPromise.then(()=>{
log("Getting services");
return gatt.getPrimaryServices();
@ -454,12 +456,11 @@
log("Add " + settings.gracePeriodService + "ms grace period after services");
result = result.then(()=>{
log("Wait after services");
return waitingPromise(settings.gracePeriodService)
return waitingPromise(settings.gracePeriodService);
});
}
return result;
});
} else {
for (var characteristic of characteristics){
characteristicsPromise = attachCharacteristicPromise(characteristicsPromise, characteristic, true);
@ -469,9 +470,8 @@
return characteristicsPromise;
});
promise = promise.then(()=>{
return promise.then(()=>{
log("Connection established, waiting for notifications");
reUseCounter = 0;
characteristicsToCache(characteristics);
clearRetryTimeout();
}).catch((e) => {
@ -479,7 +479,7 @@
log("Error:", e);
onDisconnect(e);
});
}
};
Bangle.setBTHRMPower = function(isOn, app) {
// Do app power handling
@ -487,7 +487,7 @@
if (Bangle._PWR===undefined) Bangle._PWR={};
if (Bangle._PWR.BTHRM===undefined) Bangle._PWR.BTHRM=[];
if (isOn && !Bangle._PWR.BTHRM.includes(app)) Bangle._PWR.BTHRM.push(app);
if (!isOn && Bangle._PWR.BTHRM.includes(app)) Bangle._PWR.BTHRM = Bangle._PWR.BTHRM.filter(a=>a!=app);
if (!isOn && Bangle._PWR.BTHRM.includes(app)) Bangle._PWR.BTHRM = Bangle._PWR.BTHRM.filter(a=>a!==app);
isOn = Bangle._PWR.BTHRM.length;
// so now we know if we're really on
if (isOn) {
@ -526,10 +526,9 @@
};
}
var fallbackInterval;
function switchInternalHrm(){
var switchInternalHrm = function() {
if (settings.allowFallback && !fallbackInterval){
log("Fallback to HRM enabled");
origSetHRMPower(1, "bthrm_fallback");
@ -542,7 +541,7 @@
}
}, settings.fallbackTimeout);
}
}
};
if (settings.replace){
log("Replace HRM event");
@ -561,7 +560,7 @@
E.on("kill", ()=>{
if (gatt && gatt.connected){
log("Got killed, trying to disconnect");
var promise = gatt.disconnect().then(()=>log("Disconnected on kill")).catch((e)=>log("Error during disconnnect on kill", e));
gatt.disconnect().then(()=>log("Disconnected on kill")).catch((e)=>log("Error during disconnnect on kill", e));
}
});
}

View File

@ -1,7 +1,16 @@
var btm = g.getHeight()-1;
var intervalInt;
var intervalBt;
var BODY_LOCS = {
0: 'Other',
1: 'Chest',
2: 'Wrist',
3: 'Finger',
4: 'Hand',
5: 'Ear Lobe',
6: 'Foot',
}
function clear(y){
g.reset();
g.clearRect(0,y,g.getWidth(),y+75);
@ -15,17 +24,17 @@ function draw(y, type, event) {
g.setFontAlign(0,0);
g.setFontVector(40).drawString(str,px,y+20);
str = "Event: " + type;
if (type == "HRM") {
if (type === "HRM") {
str += " Confidence: " + event.confidence;
g.setFontVector(12).drawString(str,px,y+40);
str = " Source: " + (event.src ? event.src : "internal");
g.setFontVector(12).drawString(str,px,y+50);
}
if (type == "BTHRM"){
if (type === "BTHRM"){
if (event.battery) str += " Bat: " + (event.battery ? event.battery : "");
g.setFontVector(12).drawString(str,px,y+40);
str= "";
if (event.location) str += "Loc: " + event.location.toFixed(0) + "ms";
if (event.location) str += "Loc: " + BODY_LOCS[event.location];
if (event.rr && event.rr.length > 0) str += " RR: " + event.rr.join(",");
g.setFontVector(12).drawString(str,px,y+50);
str= "";
@ -45,7 +54,7 @@ function onBtHrm(e) {
firstEventBt = false;
}
draw(100, "BTHRM", e);
if (e.bpm == 0){
if (e.bpm === 0){
Bangle.buzz(100,0.2);
}
if (intervalBt){

View File

@ -7,10 +7,10 @@
"allowFallback": true,
"warnDisconnect": false,
"fallbackTimeout": 10,
"custom_replace": false,
"custom_replace": true,
"custom_debuglog": false,
"custom_startWithHrm": false,
"custom_allowFallback": false,
"custom_startWithHrm": true,
"custom_allowFallback": true,
"custom_warnDisconnect": false,
"custom_fallbackTimeout": 10,
"gracePeriodNotification": 0,

View File

@ -2,11 +2,11 @@
"id": "bthrm",
"name": "Bluetooth Heart Rate Monitor",
"shortName": "BT HRM",
"version": "0.08",
"version": "0.09",
"description": "Overrides Bangle.js's build in heart rate monitor with an external Bluetooth one.",
"icon": "app.png",
"type": "app",
"tags": "health,bluetooth",
"tags": "health,bluetooth,hrm,bthrm",
"supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
"storage": [

View File

@ -61,12 +61,13 @@
}
};
if (settings.btname){
var name = "Clear " + settings.btname;
if (settings.btname || settings.btid){
var name = "Clear " + (settings.btname || settings.btid);
mainmenu[name] = function() {
E.showPrompt("Clear current device name?").then((r)=>{
E.showPrompt("Clear current device?").then((r)=>{
if (r) {
writeSettings("btname",undefined);
writeSettings("btid",undefined);
}
E.showMenu(buildMainMenu());
});
@ -79,8 +80,6 @@
return mainmenu;
}
var submenu_debug = {
'' : { title: "Debug"},
'< Back': function() { E.showMenu(buildMainMenu()); },
@ -103,35 +102,39 @@
function createMenuFromScan(){
E.showMenu();
E.showMessage("Scanning");
E.showMessage("Scanning for 4 seconds");
var submenu_scan = {
'' : { title: "Scan"},
'< Back': function() { E.showMenu(buildMainMenu()); }
};
var packets=10;
var scanStart=Date.now();
NRF.setScan(function(d) {
packets--;
if (packets<=0 || Date.now() - scanStart > 5000){
NRF.setScan();
E.showMenu(submenu_scan);
} else if (d.name){
NRF.findDevices(function(devices) {
submenu_scan[''] = { title: `Scan (${devices.length} found)`};
if (devices.length === 0) {
E.showAlert("No devices found")
.then(() => E.showMenu(buildMainMenu()));
return;
} else {
devices.forEach((d) => {
print("Found device", d);
submenu_scan[d.name] = function(){
E.showPrompt("Set "+d.name+"?").then((r)=>{
var shown = (d.name || d.id.substr(0, 17));
submenu_scan[shown] = function () {
E.showPrompt("Set " + shown + "?").then((r) => {
if (r) {
writeSettings("btid", d.id);
// Store the name for displaying later. Will connect by ID
if (d.name) {
writeSettings("btname", d.name);
}
}
E.showMenu(buildMainMenu());
});
};
});
}
}, { filters: [{services: [ "180d" ]}]});
E.showMenu(submenu_scan);
}, { timeout: 4000, active: true, filters: [{services: [ "180d" ]}]});
}
var submenu_custom = {
'' : { title: "Custom mode"},
'< Back': function() { E.showMenu(buildMainMenu()); },
@ -213,50 +216,5 @@
}
};
var submenu = {
'' : { title: "Grace periods"},
'< Back': function() { E.showMenu(buildMainMenu()); },
'Request': {
value: settings.gracePeriodRequest,
min: 0,
max: 3000,
step: 100,
format: v=>v+"ms",
onchange: v => {
writeSettings("gracePeriodRequest",v);
}
},
'Connect': {
value: settings.gracePeriodConnect,
min: 0,
max: 3000,
step: 100,
format: v=>v+"ms",
onchange: v => {
writeSettings("gracePeriodConnect",v);
}
},
'Notification': {
value: settings.gracePeriodNotification,
min: 0,
max: 3000,
step: 100,
format: v=>v+"ms",
onchange: v => {
writeSettings("gracePeriodNotification",v);
}
},
'Service': {
value: settings.gracePeriodService,
min: 0,
max: 3000,
step: 100,
format: v=>v+"ms",
onchange: v => {
writeSettings("gracePeriodService",v);
}
}
};
E.showMenu(buildMainMenu());
})
});

View File

@ -4,3 +4,6 @@
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.
0.07: Improved positioning.
0.08: Select the color of widgets correctly. Additional settings to hide colon.
0.09: Larger font size if colon is hidden to improve readability further.

View File

@ -8,6 +8,7 @@
- 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.
- The colon (e.g. 7:35 = 735) can be hidden now in the settings.
## Thanks to
<a href="https://www.flaticon.com/free-icons/" title="Icons">Icons created by Flaticon</a>

View File

@ -18,6 +18,7 @@ const H = g.getHeight();
let settings = {
fullscreen: false,
showLock: true,
hideColon: false,
showInfo: 0,
};
@ -33,11 +34,25 @@ for (const key in saved_settings) {
// Manrope font
Graphics.prototype.setLargeFont = function(scale) {
// Actual height 49 (50 - 2)
this.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/wAAAAAAAAD/AAAAAAAAAP8AAAAAAAAA/wAAAAAAAAD/AAAAAAAAAP8AAAAAAAAA/wAAAAAAAAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcAAAAAAAAAfwAAAAAAAAf/AAAAAAAAf/8AAAAAAAf//wAAAAAAP///AAAAAAP///8AAAAAP////wAAAAP////4AAAAP////8AAAAH////8AAAAH////8AAAAB////8AAAAAH///+AAAAAAf//+AAAAAAB//+AAAAAAAH/+AAAAAAAAf+AAAAAAAAB/AAAAAAAAAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///gAAAAAD////4AAAAA/////4AAAAH/////4AAAA//////wAAAH//////gAAA///////AAAH//////+AAA///////4AAD/4AAAH/wAAP+AAAAP/AAB/wAAAAf8AAH/AAAAA/4AAf4AAAAB/gAB/gAAAAH+AAP8AAAAAf4AA/wAAAAB/gAD/AAAAAH+AAP8AAAAAf4AAf4AAAAB/gAB/gAAAAH+AAH+AAAAA/4AAf8AAAAH/AAB/4AAAA/8AAD/4AAAH/wAAP/8AAH/+AAAf//////4AAA///////AAAB//////4AAAD//////AAAAH/////4AAAAP////+AAAAAP////gAAAAAD///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+AAAAAAAAB/wAAAAAAAAH/AAAAAAAAA/4AAAAAAAAH/gAAAAAAAAf8AAAAAAAAD/gAAAAAAAAP+AAAAAAAAB///////8AAH///////wAAf///////AAB///////8AAH///////wAAf///////AAB///////8AAH///////wAAP///////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAAfwAAAH8AAAD/AAAB/wAAAf8AAAP/AAAD/wAAB/8AAAf/AAAP/wAAD/8AAB//AAAf/wAAH/8AAD//AAA//gAAf/8AAD/wAAB//wAAf+AAAP//AAB/wAAB//8AAH+AAAP//wAAf4AAB///AAD/AAAP/v8AAP8AAB/8/wAA/wAAP/j/AAD/AAB/8P8AAH+AAH/g/wAAf4AA/8D/AAB/wAH/gP8AAH/AA/+A/wAAf/AP/wD/AAA//D/+AP8AAD////wA/wAAH///+AD/AAAP///wAP8AAAf//+AA/wAAA///wAD/AAAB//+AAP8AAAB//gAA/wAAAB/4AAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAD+AAAAHwAAAf4AAAAfwAAB/gAAAB/gAAH+AAAAP/AAAf4AAAA/8AAB/gAAAD/4AAH+ADAAH/wAAf4AeAAP/AAB/gD+AAP8AAH+Af+AA/4AAf4D/4AB/gAB/gP/AAH+AAH+B/8AAf4AAf4P/wAB/gAB/h//AAH+AAH+P/8AAf4AAf5//wAB/gAB/v//gAP+AAH+//+AA/4AAf//f8AH/AAB//5/8B/8AAH//D////gAAf/4P///+AAB//Af///wAAH/4A///+AAAf/AB///wAAB/4AD//+AAAH/AAH//gAAAP4AAD/4AAAAAAAAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AAAAAAAAA/+AAAAAAAAP/4AAAAAAAH//gAAAAAAB//+AAAAAAAf//4AAAAAAH///gAAAAAB///+AAAAAAf///4AAAAAH//9/gAAAAD///H+AAAAA///wf4AAAAP//8B/gAAAD///AH+AAAA///wAf4AAAH//8AB/gAAAf//AAH+AAAB//gAAf4AAAH/4AAB/gAAAf+AAAH+AAAB/gAf///8AAH4AB////wAAeAAH////AABgAAf///8AAAAAB////wAAAAAH////AAAAAAf///8AAAAAB////wAAAAAH////AAAAAAAAf4AAAAAAAAB/gAAAAAAAAH+AAAAAAAAAfwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAeAAAAAf//AB+AAAH///+AP8AAAf///4A/4AAB////gD/wAAH////Af/gAAf///8B/+AAB////wB/8AAH///+AB/wAAf4Af4AD/gAB/gB/AAP+AAH+AP8AAf4AAf4A/wAB/gAB/gD+AAH+AAH+AP4AAf4AAf4A/gAB/gAB/gD/AAH+AAH+AP8AAf4AAf4A/wAD/gAB/gD/gAf8AAH+AH/AD/wAAf4Af/Af+AAB/gB////4AAH+AD////AAAf4AH///8AAB/gAP///gAAH+AA///8AAAAAAA///AAAAAAAB//4AAAAAAAB/+AAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB///gAAAAAB////4AAAAAf////4AAAAH/////4AAAA//////wAAAH//////gAAA///////AAAH//////+AAAf//////4AAD/4D/wH/wAAP+AP8AP/AAB/wB/gAf8AAH/AH8AA/4AAf4A/wAB/gAB/gD/AAH+AAH8AP4AAf4AA/wA/gAB/gAD/AD+AAH+AAH8AP8AAf4AAf4A/wAB/gAB/gD/AAP+AAH+AP+AB/wAAf8Af8AP/AAA/4B/8B/8AAD/gH////gAAP8AP///8AAAfgAf///wAAA8AB///+AAADgAD///wAAAAAAD//+AAAAAAAH//gAAAAAAAH/4AAAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/gAAAAAAAAH+AAAAAAAAAf4AAAAAAAAB/gAAAAAAAAH+AAAAAAAAAf4AAAAADgAB/gAAAAA+AAH+AAAAAf4AAf4AAAAH/gAB/gAAAD/+AAH+AAAA//4AAf4AAAf//gAB/gAAH//+AAH+AAD///wAAf4AA///8AAB/gAf//+AAAH+AH///gAAAf4D///wAAAB/g///8AAAAH+f//+AAAAAf////gAAAAB////wAAAAAH///8AAAAAAf//+AAAAAAB///gAAAAAAH//wAAAAAAAf/8AAAAAAAB/+AAAAAAAAH/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/wAAAAB/wB//wAAAAf/wf//gAAAD//z///AAAAf/////+AAAD//////8AAAf//////4AAD///////gAAP////w//AAB/+f/8Af8AAH/Af/gA/4AAf4A/8AD/gAB/gB/wAH+AAP8AH+AAf4AA/wAf4AB/gAD/AB/gAH+AAP8AH+AAf4AA/wAf4AB/gAB/gB/wAH+AAH+AP/AAf4AAf8A/+AD/gAB/8f/8Af8AAD////4H/wAAP//////+AAAf//////4AAA///////AAAD//////8AAAH//z///gAAAH/+H//4AAAAH/gH//AAAAAAAAH/wAAAAAAAABwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAAAP/wAAAAAAAD//wAAAAAAAf//wAAAAAAH///gABgAAA////AAPAAAD///+AB+AAAf///4AP4AAD////wB/wAAP/AP/AH/AAB/4Af+AP+AAH/AA/4A/4AAf4AB/gB/gAB/gAH+AH+AAP8AAP4Af4AA/wAA/gB/gAD/AAD+AH+AAP8AAP4Af4AA/4AB/gB/gAB/gAH+AH+AAH+AAfwA/4AAf8AD/AH/AAB/4Af4A/8AAD/4H/gP/wAAP//////+AAAf//////wAAA///////AAAB//////4AAAD//////AAAAH/////wAAAAH////+AAAAAH////AAAAAAAf/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf4AP8AAAAAB/gA/wAAAAAH+AD/AAAAAAf4AP8AAAAAB/gA/wAAAAAH+AD/AAAAAAf4AP8AAAAAB/gA/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"), 46, atob("ExwqHCYlJyYoIicoFg=="), 64+(scale<<8)+(1<<16));
// Actual height 48 (49 - 2)
this.setFontCustom(
E.toString(require('heatshrink').decompress(atob('AFcH+AHFh/gA4sf4AHFn+AA4t/E43+AwsB/gHFgf4PH4AMgJ9Ngf/Pot//6bF/59F///PokfA4J9DEgIABEwYkB/7DDEgIlFCoRMDEgQsEDoRLEEgpoBA4JhGOIsHZ40PdwwA/L4SjHNAgGCP4cHA4wWDA4aVCA4gGDA4SNBe4IiBA4MPHYRBBEwScCA4d/EQUBaoRKDA4UBLQYECgb+EAgMHYYcHa4MPHoLBCBgMfYgcfBgM/PIc/BgN/A4YECIIQEDHwkDHwQHDGwQHENQUHA4d/QIQnCRIJJCSgYTCA4hqCA4hqCA4hiCA4ZCEA4RFBGYbrFAHxDGSohdDcgagFAAjPCEzicDToU/A4jPCAwbQCBwgrBgIHEFYKrDWoa7DaggA/AC0PAYV+AYSBCgKpCg4DDVIUfAYZ9BToIDDPoKVBAYfARoQDDXgMPFwTIBdYSYCv4LCv7zCXgYKCXAK8CHoUPXgY9Cn/vEYMPEwX/z46Bj4mBgf+n77CDwX4v54EIIIzCOgX/4I+CAQI9BHYQCCQ4I7CRASDBHYQHCv/Aj4+BGYIeBGAI+Bj/8AIIRBQIZjCRIiWBXgYHCPQgHBBgJ6DA4IEBPQaKBGYQ+BbgiCCAGZFDIIUBaAZBCgYHCQAQTBA4SACUwS8DDYQHBQAbVCQAYwBA4SABgYEBPoQCBFgU/CQWACgRDCHwKVCIYX+aYRDCHwMPAgY+Cn4EDHwX/AgY+B8bEFj/HA4RGCn+f94MBv45Cv+fA4J6C//+j5gBGIMBFoJWBQoRMB8E//4DBHIJcBv4HBEwJUCA4ImCj5MBA4KZCPYQHBZgRBCE4LICvwaCXAYA5PgQAEMIQAEUwQADQAJlCAARlBWYIACT4JtDAAMPA4IWESgg8CAwI+EEoPhHwYlCgY+DEoP4g4+DEoPAh4+CEoReBHwUfLYU/CwgMBXARqBHYQCCGoIjBgI+CgZSCHwcHAYY+Ch4lBJ4IbCjhACPwqUBPwqFCPwhQBIQZ+DOAKVFXooHCXop9DFAi8EFAT0GPoYAygwFEgOATISLDwBWDTQc/A4L6CTQKkCVQX+BYIHBDwX+BYIHBVQX8B4KqD+/wA4aBBj/AgK8CQIIJBA4a/BBIMBAgL/BAgUDYgL/BAII7BAQXgAII7BAQXAYQQxBYARrCMwQ0BAgV/HwYECHwgEBgY+EA4MPGwI8BA4UfGwI8BgYHBPofAQYOHPoeAR4QmBHwQHCEwI+CA4RVBHwQHCaggnBDwQHEHoIAEEQIA6v5NFfgSECBwZtEf4IHFOYQHEj4HGDwYHCDwPgv/jA4UHXQS8E/ED/AHDZ4MPSYKlCv+AYwIHDDwL7EgL7DAgTzCEwIpCeYTZBg4CBeYIJBAgICBFgIJBAgICBeYIEDHII0BAgg+EgI5CMocHGwJBCA4MfGwMD/h/BwF/PoQHC451CJIMDSgIjBA4PAA4QmBA4IhBA4JVBgEMA4bUDV4QeCAAf/HoIAENIIApOoIAEW4QAEW4QAEW4QAEWQRSFNIcDfYQMDny8DO4Q7BAQQjCewh+EHwcPToQ+Dv//ewkHUoI+En68DeIS0EHwMf/46CeYYlCHwQ0BKIY+BGgJ4Dh/nGgZZCAwKPEHYLpFDoKuFGgj4JgY0EHwQ0EYhIA6MAkf+BRBLIa5BQAJSCBgP4R4iVB/YHERoIACA4QGDE4SFBAoV/A4MH/ggBWIL7C8EfVoL4DwBHBFYIHBfYIRBAgT7CDgQEBgP4BgUBEIMDDgIMBgYMBg/gBgS5Ch/ABgUPFIMf4EHA4IEBHwUPCgJGCIIM/CgLgCAQJlBFIQFB44HBEIUBQYc/EIIHDAAIuBA4oeBRoSfBLAIHC/gHBEwIXC+AHBZghHBDwQADj4WCAHEPAwpWBKYYOCLwIHELYJUBghlDA4UcQogHBvgeDD4K0DDwIHBWgQeB4CyBh68CUAMf8DeCdIYHDdIfAfYjxCAgj2BAgbHCvwJCIIYCBBIMDHIX4BgUHFwMD+AMCA4Q0BAgg5CHwxICAQY5BdgQHBEgMDIYV/DgR1CA4PwP4KvDRgIACEYIHFWggABMQQHEZwd/Dwq1DHoTFEdooA/ACrBBcAZmC8DTCAATGBaYR+DwDTCRwbYDAASLBCIIGCFgQRBAG4='))),
46,
atob("EhooGyUkJiUnISYnFQ=="),
63+(scale<<8)+(1<<16)
);
return this;
};
Graphics.prototype.setXLargeFont = function(scale) {
// Actual height 53 (55 - 3)
this.setFontCustom(
E.toString(require('heatshrink').decompress(atob('AHM/8AIG/+AA4sD/wQGh/4EWQA/AC8YA40HNA0BRY8/RY0P/6LFgf//4iFA4IiFj4HBEQkHCAQiDHIIZGv4HCFQY5BDAo5CAAIpDDAfACA3wLYv//hsFKYxcCMgoiBOooiBQwwiBS40AHIgA/ACS/DLYjYCBAjQEBAYQDBAgHDUAbyDZQi3CegoHEVQQZFagUfW4Y0DaAgECaIJSEFYMPbIYNDv5ACGAIrBCgJ1EFYILCAAQWCj4zDGgILCegcDEQRNDHIIiCHgZ2BEQShFIqUDFYidCh5ODg4NCn40DAgd/AYR5BDILZEAAIMDAAYVCh7aHdYhKDbQg4Dv7rGBAihFCAwIDCAgA/AB3/eoa7GAAk/dgbVGDJrvCDK67DDIjaGdYpbCdYonCcQjjDEVUBEQ4A/AEMcAYV/NAUHcYUDawd/cYUPRYSmBBgaLBToP8BgYiBSgIiCj4iCg//EQSuDW4IMDVwYiCBgIiBBgrRDCATeBaIYqCv70DCgT4CEQMfIgQZBBoRnDv/3EQIvBDIffEQMHFwReBRYUfOgX/+IiDKIeHEQRRECwUHKwIuB8AiDIoJEBCwZFCv/4HIZaBIgPAEQS2CUYQiCD4SABEQcfOwIZBEQaHBO4RcEAAI/BEQQgBSIQiDTIRZBEQZuBVYQiDHoKWCEQQICFQIiDBAQeCEQQA/AANwA40BLIJ5BO4JWCBAUPAYR5En7RBUIQECN4SYCQQIiEh6CCEQk/BoQiBgYeCBoTrCAgT0CCgIfCFYQiBg4IBGgIiDj6rBg4rCBYLRDFYIiBbYIfBLgQiBIQYiD4JCCLgf/bQIWDBYV/EQV/BYXz/5FBgIiD5//IowZBD4M/NAX/BIPgDIJoC//5GgKUDn//4f/8KLE/wTBAAI8BEQPwj4HBVwYmBDgIZDN4QZCGYKJCHQP/JoSgCBATrCh5dBKITVDG4gICAAbvDAH5SCL4QADK4J5CCAiTCCAp1BCAqCDCAgiGCAIiFCAQiFeoIiFg6/FCAgiECAXnEQgQB/kfEQYQC4F/EQYQCgIiDfoIQBg4iDCAUAEQZUCcgIiDDIIQBEQhuBBoIiENoYiFDwQiECAQiFwEBPQQNCAQKDDEYMDDoMfRh4iGUwqvEESBiBaQ5oEbgr0FNAo+EEIwA+oAHGgJoFRAMHe4L0CAALNBBAT0BfwScDCAXweAL0DWgUPQYQiDwF/QYQiC/zTB+C0FBAL0CEQYIBGgMPCgIxBg4rCJIKsCh5IBBwTPCj4WBgYLBZ4V/MAIiBBQQrBEQYtCBYQiCO4QLFCwgiDIQIiGIoMHEQpFBn5FFD4JoENwRoGDgSUCAoKfBw//DgIiCT4auCFwN/T4RRET4TaCEQKoCDIQiCGgK/DAAQICdYQACHoIqCBAoQFEwIhFAH4AFQIROEj4IGXwIIGNwIACbgIhEBAiRCVwoqDTogHEW4QZFXgIZB/z9Cv49CF4MPBwI0Ca4LlB8ATCJoP4AoINDfQPAg7PBg4cBBwUfD4MfFYILCCwgOCf4QLEwEPCwILCgJaBn4WBBYQxCIQQiD+EDCYI5CBYRQBIo4fBMQIuBC4N/NAv8AoIcBSgU/FYIIBZIYrCW4hOCXIQZCgYUBv7jEh4uBZAscewZ8CgEgUYT0EEoQIBA4gICFQQIEHYQA+KQzdDAArdCAArpCEScHaIQiEvwiGe4QiFUwQiEbgIiFYIL0DEQTkBEQrJEEQc/cYYiCg4HBDIQiCfoRoEHQLaDEQQHBbQYiBCAT8Dn/BCAoXBJYP/OgZKC/6OEEARLCEQZLEEQZLEEQjKFEQI6EEQZLDEQbsGEQLjGYYYA/JIxzEg/AfgJSDAoPgfgiDC8COFAoPnaQj6CAAR+CW4TCFA4i6CDIqhCDIfwHoYHCYIN/GgKuBJ4JDBFYUf/C5CBYIZBv/Ag4ZBg4rBBYQTBAQIcBg4FBn5UBAQUfFwIfCEQeAgYfBAQUBFAKbCAQQiCGwIiE+A2BwBFNwE/AoM/EQJoIWwKCCh4cBFYKUERYV/W46uHFYIZGaJA0B/glBGYT0JIITiEMIJvCFQQAEHYQA/ABBlEOIhdGQAIRFSgQIBgQICn4IB8EAjiBCUYglCbQYeBEoQZCTwM/CYIZD/gEBUwIzBJ4UHYAU/EwIrBh4rCAoIXCn4rBCgUDAQN/FYMfBYIXBCYJnCBYXggf8HgQLCwEPEQQuBgJOECwILDCwgiLHIUHBYJFGD4IxBgYWCn4rBBwJoFDIYNBCgPADgKHBRYfDBQN/GAIrBToTLDVwYACDILiCWAb8DAAYzBYAjTCAAI9BAARNCBAoqCBAgQDFgbYCAH4AufgQACf4T8CAAT/CfgQACBwITCAAYOBCYQioh4iEAHQA=='))),
46,
atob("FR4uHyopKyksJSssGA=="),
70+(scale<<8)+(1<<16)
);
};
Graphics.prototype.setMediumFont = function(scale) {
// Actual height 41 (42 - 2)
@ -259,11 +274,12 @@ function draw() {
function drawDate(){
// Draw background
var y = H/5*2 + (settings.fullscreen ? 0 : 8);
var y = H/5*2;
g.reset().clearRect(0,0,W,W);
// Draw date
y -= settings.fullscreen ? 8 : 0;
y = parseInt(y/2);
y += settings.fullscreen ? 2 : 15;
var date = new Date();
var dateStr = date.getDate();
dateStr = ("0" + dateStr).substr(-2);
@ -276,14 +292,14 @@ function drawDate(){
var dayW = Math.max(g.stringWidth(dayStr), g.stringWidth(monthStr));
var fullDateW = dateW + 10 + dayW;
g.setFontAlign(-1,1);
g.setFontAlign(-1,0);
g.setMediumFont();
g.setColor(g.theme.fg);
g.drawString(dateStr, W/2 - fullDateW / 2, y+5);
g.drawString(dateStr, W/2 - fullDateW / 2, y+1);
g.setSmallFont();
g.drawString(monthStr, W/2 - fullDateW/2 + 10 + dateW, y+3);
g.drawString(dayStr, W/2 - fullDateW/2 + 10 + dateW, y-23);
g.drawString(dayStr, W/2 - fullDateW/2 + 10 + dateW, y-12);
g.drawString(monthStr, W/2 - fullDateW/2 + 10 + dateW, y+11);
}
@ -296,9 +312,16 @@ function drawTime(){
// Draw time
g.setColor(g.theme.bg);
g.setFontAlign(0,-1);
var timeStr = locale.time(date,1);
y += settings.fullscreen ? 14 : 10;
g.setFontAlign(0,0);
var hours = String(date.getHours());
var minutes = date.getMinutes();
minutes = minutes < 10 ? String("0") + minutes : minutes;
var colon = settings.hideColon ? "" : ":";
var timeStr = hours + colon + minutes;
// Set y coordinates correctly
y += parseInt((H - y)/2) + 5;
var infoEntry = getInfoEntry();
var infoStr = infoEntry[0];
@ -307,9 +330,13 @@ function drawTime(){
// Show large or small time depending on info entry
if(infoStr == null){
y += 10;
g.setLargeFont();
if(settings.hideColon){
g.setXLargeFont();
} else {
g.setLargeFont();
}
} else {
y -= 15;
g.setMediumFont();
}
g.drawString(timeStr, W/2, y);
@ -319,7 +346,7 @@ function drawTime(){
return;
}
y += H/5*2-5;
y += 35;
g.setFontAlign(0,0);
g.setSmallFont();
var imgWidth = 0;
@ -370,17 +397,6 @@ function queueDraw() {
}
/*
* Load clock, widgets and listen for events
*/
Bangle.loadWidgets();
// Clear the screen once, at startup and set the correct theme.
var bgOrig = g.theme.bg
var fgOrig = g.theme.fg
g.setTheme({bg:fgOrig,fg:bgOrig}).clear();
draw();
// Stop updates when LCD is off, restart when on
Bangle.on('lcdPower',on=>{
if (on) {
@ -446,5 +462,17 @@ E.on("kill", function(){
});
/*
* Draw clock the first time
*/
// The upper part is inverse i.e. light if dark and dark if light theme
// is enabled. In order to draw the widgets correctly, we invert the
// dark/light theme as well as the colors.
g.setTheme({bg:g.theme.fg,fg:g.theme.bg, dark:!g.theme.dark}).clear();
// Load widgets and draw clock the first time
Bangle.loadWidgets();
draw();
// Show launcher when middle button pressed
Bangle.setUI("clock");

View File

@ -1,7 +1,7 @@
{
"id": "bwclk",
"name": "BW Clock",
"version": "0.06",
"version": "0.09",
"description": "BW Clock.",
"readme": "README.md",
"icon": "app.png",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -6,6 +6,7 @@
let settings = {
fullscreen: false,
showLock: true,
hideColon: false,
};
let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings;
for (const key in saved_settings) {
@ -35,6 +36,14 @@
settings.showLock = !settings.showLock;
save();
},
},
'Hide Colon': {
value: settings.hideColon,
format: () => (settings.hideColon ? 'Yes' : 'No'),
onchange: () => {
settings.hideColon = !settings.hideColon;
save();
},
}
});
})

View File

@ -5,3 +5,5 @@
0.05: Update calendar weekend colors for start on Sunday
0.06: Use larger font for dates
0.07: Fix off-by-one-error on previous month
0.08: Do not register as watch, manually start clock on button
read start of week from system settings

View File

@ -18,8 +18,7 @@ const blue = "#0000ff";
const yellow = "#ffff00";
let settings = require('Storage').readJSON("calendar.json", true) || {};
if (settings.startOnSun === undefined)
settings.startOnSun = false;
let startOnSun = ((require("Storage").readJSON("setting.json", true) || {}).firstDayOfWeek || 0) === 0;
if (settings.ndColors === undefined)
if (process.env.HWVERSION == 2) {
settings.ndColors = true;
@ -50,14 +49,14 @@ function getDowLbls(locale) {
case "de_AT":
case "de_CH":
case "de_DE":
if (settings.startOnSun) {
if (startOnSun) {
dowLbls = ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"];
} else {
dowLbls = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"];
}
break;
case "nl_NL":
if (settings.startOnSun) {
if (startOnSun) {
dowLbls = ["zo", "ma", "di", "wo", "do", "vr", "za"];
} else {
dowLbls = ["ma", "di", "wo", "do", "vr", "za", "zo"];
@ -66,14 +65,14 @@ function getDowLbls(locale) {
case "fr_BE":
case "fr_CH":
case "fr_FR":
if (settings.startOnSun) {
if (startOnSun) {
dowLbls = ["Di", "Lu", "Ma", "Me", "Je", "Ve", "Sa"];
} else {
dowLbls = ["Lu", "Ma", "Me", "Je", "Ve", "Sa", "Di"];
}
break;
case "sv_SE":
if (settings.startOnSun) {
if (startOnSun) {
dowLbls = ["Di", "Lu", "Ma", "Me", "Je", "Ve", "Sa"];
} else {
dowLbls = ["Lu", "Ma", "Me", "Je", "Ve", "Sa", "Di"];
@ -81,21 +80,21 @@ function getDowLbls(locale) {
break;
case "it_CH":
case "it_IT":
if (settings.startOnSun) {
if (startOnSun) {
dowLbls = ["Do", "Lu", "Ma", "Me", "Gi", "Ve", "Sa"];
} else {
dowLbls = ["Lu", "Ma", "Me", "Gi", "Ve", "Sa", "Do"];
}
break;
case "oc_FR":
if (settings.startOnSun) {
if (startOnSun) {
dowLbls = ["dg", "dl", "dm", "dc", "dj", "dv", "ds"];
} else {
dowLbls = ["dl", "dm", "dc", "dj", "dv", "ds", "dg"];
}
break;
default:
if (settings.startOnSun) {
if (startOnSun) {
dowLbls = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
} else {
dowLbls = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"];
@ -110,7 +109,7 @@ function drawCalendar(date) {
g.clearRect(0, 0, maxX, maxY);
g.setBgColor(bgColorMonth);
g.clearRect(0, 0, maxX, headerH);
if (settings.startOnSun){
if (startOnSun){
g.setBgColor(bgColorWeekend);
g.clearRect(0, headerH + rowH, colW, maxY);
g.setBgColor(bgColorDow);
@ -150,7 +149,7 @@ function drawCalendar(date) {
});
date.setDate(1);
const dow = date.getDay() + (settings.startOnSun ? 1 : 0);
const dow = date.getDay() + (startOnSun ? 1 : 0);
const dowNorm = dow === 0 ? 7 : dow;
const monthMaxDayMap = {
@ -242,5 +241,5 @@ Bangle.on("touch", area => {
});
// Show launcher when button pressed
Bangle.setUI("clock"); // TODO: ideally don't set 'clock' mode
setWatch(() => load(), process.env.HWVERSION === 2 ? BTN : BTN3, { repeat: false, edge: "falling" });
// No space for widgets!

View File

@ -1,7 +1,7 @@
{
"id": "calendar",
"name": "Calendar",
"version": "0.07",
"version": "0.08",
"description": "Simple calendar",
"icon": "calendar.png",
"screenshots": [{"url":"screenshot_calendar.png"}],

View File

@ -1,8 +1,6 @@
(function (back) {
var FILE = "calendar.json";
var settings = require('Storage').readJSON(FILE, true) || {};
if (settings.startOnSun === undefined)
settings.startOnSun = false;
if (settings.ndColors === undefined)
if (process.env.HWVERSION == 2) {
settings.ndColors = true;
@ -17,14 +15,6 @@
E.showMenu({
"": { "title": "Calendar" },
"< Back": () => back(),
'Start Sunday': {
value: settings.startOnSun,
format: v => v ? "Yes" : "No",
onchange: v => {
settings.startOnSun = v;
writeSettings();
}
},
'B2 Colors': {
value: settings.ndColors,
format: v => v ? "Yes" : "No",

View File

@ -0,0 +1,11 @@
# Banglejs - Touchscreen calibration
A simple calibration app for the touchscreen
## Usage
Once lauched touch the cross that appear on the screen to make
another spawn elsewhere.
each new touch on the screen will help to calibrate the offset
of your finger on the screen. After five or more input, press
the button to save the calibration and close the application.

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwkB/4AJ+EPBhQXg+BBDCyJaGGR5zIDBoQEL4QYOLYR3GBIouJR5AYBGBILBU5QMGFwgiFX4wwIEI4XGGBAgHd44+HD44XHNw4XWM5IIHCIoXWV5IXICQgXvLxAAKCYYXh5nMC6n8C4PPC5MAAA8PC4ZxBACAXOI653hU5zvJABASEC5PwHI4XcMBIXICIoXXJBAXHCAwXXJBAXHB5AfGC4ygJEAwXGQ5BoIQxoiDBYgXECwIuIBgb5ECIQJFGBQmCC4QHEDBwAFCxoYICx5ZELZoZJFiIXpA="))

85
apps/calibration/app.js Normal file
View File

@ -0,0 +1,85 @@
class BanglejsApp {
constructor() {
this.x = 0;
this.y = 0;
this.settings = {
xoffset: 0,
yoffset: 0,
};
}
load_settings() {
let settings = require('Storage').readJSON('calibration.json', true) || {active: false};
// do nothing if the calibration is deactivated
if (settings.active === true) {
// cancel the calibration offset
Bangle.on('touch', function(button, xy) {
xy.x += settings.xoffset;
xy.y += settings.yoffset;
});
}
if (!settings.xoffset) settings.xoffset = 0;
if (!settings.yoffset) settings.yoffset = 0;
console.log('loaded settings:');
console.log(settings);
return settings;
}
save_settings() {
this.settings.active = true;
this.settings.reload = false;
require('Storage').writeJSON('calibration.json', this.settings);
console.log('saved settings:');
console.log(this.settings);
}
explain() {
/*
* TODO:
* Present how to use the application
*
*/
}
drawTarget() {
this.x = 16 + Math.floor(Math.random() * (g.getWidth() - 32));
this.y = 40 + Math.floor(Math.random() * (g.getHeight() - 80));
g.clearRect(0, 24, g.getWidth(), g.getHeight() - 24);
g.drawLine(this.x, this.y - 5, this.x, this.y + 5);
g.drawLine(this.x - 5, this.y, this.x + 5, this.y);
g.setFont('Vector', 10);
g.drawString('current offset: ' + this.settings.xoffset + ', ' + this.settings.yoffset, 0, 24);
}
setOffset(xy) {
this.settings.xoffset = Math.round((this.settings.xoffset + (this.x - Math.floor((this.x + xy.x)/2)))/2);
this.settings.yoffset = Math.round((this.settings.yoffset + (this.y - Math.floor((this.y + xy.y)/2)))/2);
}
}
E.srand(Date.now());
Bangle.loadWidgets();
Bangle.drawWidgets();
calibration = new BanglejsApp();
calibration.load_settings();
let modes = {
mode : 'custom',
btn : function(n) {
calibration.save_settings(this.settings);
load();
},
touch : function(btn, xy) {
calibration.setOffset(xy);
calibration.drawTarget();
},
};
Bangle.setUI(modes);
calibration.drawTarget();

14
apps/calibration/boot.js Normal file
View File

@ -0,0 +1,14 @@
let cal_settings = require('Storage').readJSON("calibration.json", true) || {active: false};
Bangle.on('touch', function(button, xy) {
// do nothing if the calibration is deactivated
if (cal_settings.active === false) return;
// reload the calibration offset at each touch event /!\ bad for the flash memory
if (cal_settings.reload === true) {
cal_settings = require('Storage').readJSON("calibration.json", true);
}
// apply the calibration offset
xy.x += cal_settings.xoffset;
xy.y += cal_settings.yoffset;
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 B

View File

@ -0,0 +1,17 @@
{ "id": "calibration",
"name": "Touchscreen Calibration",
"shortName":"Calibration",
"icon": "calibration.png",
"version":"1.00",
"description": "A simple calibration app for the touchscreen",
"supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
"tags": "tool",
"storage": [
{"name":"calibration.app.js","url":"app.js"},
{"name":"calibration.boot.js","url":"boot.js"},
{"name":"calibration.settings.js","url":"settings.js"},
{"name":"calibration.img","url":"app-icon.js","evaluate":true}
],
"data": [{"name":"calibration.json"}]
}

View File

@ -0,0 +1,23 @@
(function(back) {
var FILE = "calibration.json";
var settings = Object.assign({
active: true,
}, require('Storage').readJSON(FILE, true) || {});
function writeSettings() {
require('Storage').writeJSON(FILE, settings);
}
E.showMenu({
"" : { "title" : "Calibration" },
"< Back" : () => back(),
'Active': {
value: !!settings.active,
format: v => v? "On":"Off",
onchange: v => {
settings.active = v;
writeSettings();
}
},
});
})

View File

@ -23,3 +23,4 @@
0.11: New color option: foreground color
Improve performance, reduce memory usage
Small optical adjustments
0.12: Allow configuration of update interval

View File

@ -848,8 +848,8 @@ Bangle.loadWidgets();
// schedule a draw for the next minute
setTimeout(function() {
// draw every 60 seconds
setInterval(draw,60000);
// draw in interval
setInterval(draw, settings.updateInterval * 1000);
}, 60000 - (Date.now() % 60000));
draw();

View File

@ -21,5 +21,6 @@
"circle2colorizeIcon": true,
"circle3colorizeIcon": true,
"circle4colorizeIcon": false,
"hrmValidity": 60
"hrmValidity": 60,
"updateInterval": 60
}

View File

@ -1,7 +1,7 @@
{ "id": "circlesclock",
"name": "Circles clock",
"shortName":"Circles clock",
"version":"0.11",
"version":"0.12",
"description": "A clock with three or four circles for different data at the bottom in a probably familiar style",
"icon": "app.png",
"screenshots": [{"url":"screenshot-dark.png"}, {"url":"screenshot-light.png"}, {"url":"screenshot-dark-4.png"}, {"url":"screenshot-light-4.png"}],

View File

@ -58,6 +58,16 @@
min: 0, max: 2,
format: v => weatherData[v],
onchange: x => save('weatherCircleData', weatherData[x]),
},
/*LANG*/'update interval': {
value: settings.updateInterval,
min: 0,
max : 3600,
step: 30,
format: x => {
return x + 's';
},
onchange: x => save('updateInterval', x),
}
};
E.showMenu(menu);
@ -100,7 +110,7 @@
/*LANG*/'valid period': {
value: settings.hrmValidity,
min: 10,
max : 600,
max : 1800,
step: 10,
format: x => {
return x + "s";
@ -117,9 +127,9 @@
/*LANG*/'< Back': ()=>showMainMenu(),
/*LANG*/'goal': {
value: settings.stepGoal,
min: 2000,
min: 1000,
max : 50000,
step: 2000,
step: 500,
format: x => {
return x;
},
@ -127,9 +137,9 @@
},
/*LANG*/'distance goal': {
value: settings.stepDistanceGoal,
min: 2000,
max : 30000,
step: 1000,
min: 1000,
max : 50000,
step: 500,
format: x => {
return x;
},

1
apps/diceroll/ChangeLog Normal file
View File

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

View File

@ -0,0 +1 @@
E.toArrayBuffer(atob("ICABAAAAAAAAAAAAAAAAAAHAAAAP8AAAfn4AA/APwA+DwfAPg8HwD+AH8Az4HzAMPnwwDAfgMAwBgDAMCYAwDA2YMAwhmDAMIZAwDCGDMA2BgzAMgYAwDAGAMA8BgPADwYPAAPGPgAB9ngAAH/gAAAfgAAABgAAAAAAAAAAAAAAAAAA="))

108
apps/diceroll/app.js Normal file
View File

@ -0,0 +1,108 @@
var init_message = true;
var acc_data;
var die_roll = 1;
var selected_die = 0;
var roll = 0;
const dices = [4, 6, 10, 12, 20];
g.setFontAlign(0,0);
Bangle.on('touch', function(button, xy) {
// Change die if not rolling
if(roll < 1){
if(selected_die <= 3){
selected_die++;
}else{
selected_die = 0;
}
}
//Disable initial message
init_message = false;
});
function rect(){
x1 = g.getWidth()/2 - 35;
x2 = g.getWidth()/2 + 35;
y1 = g.getHeight()/2 - 35;
y2 = g.getHeight()/2 + 35;
g.drawRect(x1, y1, x2, y2);
}
function pentagon(){
x1 = g.getWidth()/2;
y1 = g.getHeight()/2 - 50;
x2 = g.getWidth()/2 - 50;
y2 = g.getHeight()/2 - 10;
x3 = g.getWidth()/2 - 30;
y3 = g.getHeight()/2 + 30;
x4 = g.getWidth()/2 + 30;
y4 = g.getHeight()/2 + 30;
x5 = g.getWidth()/2 + 50;
y5 = g.getHeight()/2 - 10;
g.drawPoly([x1, y1, x2, y2, x3, y3, x4, y4, x5, y5], true);
}
function triangle(){
x1 = g.getWidth()/2;
y1 = g.getHeight()/2 - 57;
x2 = g.getWidth()/2 - 50;
y2 = g.getHeight()/2 + 23;
x3 = g.getWidth()/2 + 50;
y3 = g.getHeight()/2 + 23;
g.drawPoly([x1, y1, x2, y2, x3, y3], true);
}
function drawDie(variant) {
if(variant == 1){
//Rect, 6
rect();
}else if(variant == 3){
//Pentagon, 12
pentagon();
}else{
//Triangle, 4, 10, 20
triangle();
}
}
function initMessage(){
g.setFont("6x8", 2);
g.drawString("Dice-n-Roll", g.getWidth()/2, 20);
g.drawString("Shake to roll", g.getWidth()/2, 60);
g.drawString("Tap to change", g.getWidth()/2, 80);
g.drawString("Tap to start", g.getWidth()/2, 150);
}
function rollDie(){
acc_data = Bangle.getAccel();
if(acc_data.diff > 0.3){
roll = 3;
}
//Mange the die "roll" by chaning the number a few times
if(roll > 0){
g.drawString("Rolling!", g.getWidth()/2, 150);
die_roll = Math.abs(E.hwRand()) % dices[selected_die] + 1;
roll--;
}
//Draw dice graphics
drawDie(selected_die);
//Draw dice number
g.setFontAlign(0,0);
g.setFont("Vector", 45);
g.drawString(die_roll, g.getWidth()/2, g.getHeight()/2);
//Draw selected die in right corner
g.setFont("6x8", 2);
g.drawString(dices[selected_die], g.getWidth()-15, 15);
}
function main() {
g.clear();
if(init_message){
initMessage();
}else{
rollDie();
}
Bangle.setLCDPower(1);
}
var interval = setInterval(main, 300);

BIN
apps/diceroll/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,14 @@
{ "id": "diceroll",
"name": "Dice-n-Roll",
"shortName":"Dice-n-Roll",
"icon": "app.png",
"version":"0.01",
"description": "A dice app with a few different dice.",
"screenshots": [{"url":"diceroll_screenshot.png"}],
"tags": "game",
"supports": ["BANGLEJS2"],
"storage": [
{"name":"diceroll.app.js","url":"app.js"},
{"name":"diceroll.img","url":"app-icon.js","evaluate":true}
]
}

17
apps/dinoClock/README.md Normal file
View File

@ -0,0 +1,17 @@
# dinoClock
Watchface with T-Rex Dinosaur from Chrome.
It displays current temperature and weather.
**Warning**: Element position and styles can change in the future.
Based on the [Weather Clock](https://github.com/espruino/BangleApps/tree/master/apps/weatherClock).
# Requirements
**This clock requires Gadgetbridge and the weather app in order to get weather data!**
See the [Bangle.js Gadgetbridge documentation](https://www.espruino.com/Gadgetbridge) for instructions on setting up Gadgetbridge and weather.
![Screenshot](screens/screen1.png)

219
apps/dinoClock/app.js Normal file
View File

@ -0,0 +1,219 @@
const storage = require('Storage');
const locale = require("locale");
// add modifiied 4x5 numeric font
(function(graphics) {
graphics.prototype.setFont4x5NumPretty = function() {
this.setFontCustom(atob("IQAQDJgH4/An4QXr0Fa/BwnwdrcH63BCHwfr8Ha/"),45,atob("AwIEBAQEBAQEBAQEBA=="),5);
};
})(Graphics);
// add font for days of the week
(function(graphics) {
graphics.prototype.setFontDoW = function() {
this.setFontCustom(atob("///////ADgB//////+AHAD//////gAAAH//////4D8B+A///////4AcAOAH//////4AcAOAAAAAB//////wA4AcAP//////wAAAAAAAA//////4AcAP//////wA4Af//////gAAAH//////5z85+c/OfnOAA4AcAOAH//////4AcAOAAAAAB//////wcAOAHB//////wAAAAAAAA///////ODnBzg5wc4AAAAD//////84OcH//8/+fAAAAAAAAAAAAA/z/5/8/OfnPz/5/8/wAAAD//////84OcH//////AAAAAAAAAAAAA/z/5/8/OfnPz/5/8/wAAAD//////gBwA///////AAAAAAAAAAAAA"),48,24,13);
};
})(Graphics);
const SUN = 1;
const PART_SUN = 2;
const CLOUD = 3;
const SNOW = 4;
const RAIN = 5;
const STORM = 6;
const ERR = 7;
/**
Choose weather icon based on weather const
Weather icons from https://icons8.com/icon/set/weather/ios-glyphs
Error icon from https://icons8.com/icon/set/error-cloud/ios-glyphs
**/
function weatherIcon(weather) {
switch (weather) {
case SUN:
return atob("Hh4BAAAAAAAMAAAAMAAAAMAAAAMAABgMBgBwADgA4AHAAY/GAAB/gAAD/wAAH/4AAP/8AAP/8AfP/8+fP/8+AP/8AAP/8AAH/4AAD/wAAB/gAAY/GAA4AHABwADgBgMBgAAMAAAAMAAAAMAAAAMAAAAAAAA=");
case PART_SUN:
return atob("Hh4BAAAAAAAAAAAMAAAAMAAAEMIAAOAcAAGAYAAAeAAAA/AAAB/gAA5/gAA5/g+AB+D/gA4H/wAR//wGD//4OD//4EH//4AH//4Af//+Af//+A////A////A////A///+Af//+AH//4AAAAAAAAAAAAAAAA=");
case CLOUD:
return atob("Hh4BAAAAAAAAAAAAAAAAAAAAAAAAAAAH4AAAf+AAA//AAB//gAf//gB///wB///wD///wD///wP///8f///+f///+////////////////////f///+f///+P///8D///wAAAAAAAAAAAAAAAAAAAAAAAAAA=");
case SNOW:
return atob("Hh4BAAAAAAAAAAAAAAAAAHwAAAf8AAA/+AAH/+AAf//AAf8/AA/8/AB/gHgH/wP4H/wP4P/gH8P/8/8P/8/8P///4H///4B///gAAAAAAMAAAAMAAAB/gGAA/AfgA/AfgB/gfgAMAfgAMAGAAAAAAAAAAAA=");
case RAIN:
return atob("Hh4BAAAAAAAAAAAAAAAAAHwAAAf8AAA/+AAH/+AAf//AAf//AA///AB///gH///4H///4P///8P///8P///8P///4H///4B///gAAAAAAAAAABgBgABgBgABhhhgABgBgABgBgAAAAAAAAAAAAAAAAAAAAA=");
case STORM:
return atob("Hh4BAAAAAAAAAAAAAAAAAHwAAAf8AAA/+AAH/+AAf//AAf//AA///AB///gH///4H/x/4P/g/8P/k/8P/E/8P/M/4H+MP4B+cHgAAfgAAA/gABg/AABgHAABgGBgAAGBgAAEBgAAEAAAAAAAAAAAAAAAAAA=");
case ERR:
default:
return atob("Hh4BAAAAAAAAAAAAAAAAAAAAAAAAAAAH4AAAf+AAA//AAB//gAf//gB///wB/z/wD/z/wD/z/wP/z/8f/z/+f/z/+//z//////////////z//f/z/+f///+P///8D///wAAAAAAAAAAAAAAAAAAAAAAAAAA=");
}
}
/**
Choose weather icon to display based on condition.
Based on function from the Bangle weather app so it should handle all of the conditions
sent from gadget bridge.
*/
function chooseIcon(condition) {
condition = condition.toLowerCase();
if (condition.includes("thunderstorm")) return weatherIcon(STORM);
if (condition.includes("freezing")||condition.includes("snow")||
condition.includes("sleet")) {
return weatherIcon(SNOW);
}
if (condition.includes("drizzle")||
condition.includes("shower")) {
return weatherIcon(RAIN);
}
if (condition.includes("rain")) return weatherIcon(RAIN);
if (condition.includes("clear")) return weatherIcon(SUN);
if (condition.includes("few clouds")) return weatherIcon(PART_SUN);
if (condition.includes("scattered clouds")) return weatherIcon(CLOUD);
if (condition.includes("clouds")) return weatherIcon(CLOUD);
if (condition.includes("mist") ||
condition.includes("smoke") ||
condition.includes("haze") ||
condition.includes("sand") ||
condition.includes("dust") ||
condition.includes("fog") ||
condition.includes("ash") ||
condition.includes("squalls") ||
condition.includes("tornado")) {
return weatherIcon(CLOUD);
}
return weatherIcon(CLOUD);
}
/*
* Choose weather icon to display based on weather conditition code
* https://openweathermap.org/weather-conditions#Weather-Condition-Codes-2
*/
function chooseIconByCode(code) {
const codeGroup = Math.round(code / 100);
switch (codeGroup) {
case 2: return weatherIcon(STORM);
case 3: return weatherIcon(RAIN);
case 5: return weatherIcon(RAIN);
case 6: return weatherIcon(SNOW);
case 7: return weatherIcon(CLOUD);
case 8:
switch (code) {
case 800: return weatherIcon(SUN);
case 801: return weatherIcon(PART_SUN);
default: return weatherIcon(CLOUD);
}
default: return weatherIcon(CLOUD);
}
}
/**
Get weather stored in json file by weather app.
*/
function getWeather() {
let jsonWeather = storage.readJSON('weather.json');
return jsonWeather;
}
// 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));
}
// only draw the first time
function drawBg() {
var bgImg = require("heatshrink").decompress(atob("2E7wINKn///+AEaIVUgIUB//wCs/5CtRXrCvMD8AVTg4LFCv4VZ/iSLCrwWMCrMOAQMPCp7cBCojjFCo/xFgIVQgeHCopABCpcH44Vuh/AQQX/wAV7+F/Cq/nCsw/CCqyvRCvgODCqfAgEDCp4QCSIIVQgIOBDQgGDABX/NgIECCp8HCrM/CgP4CqKaCCqSfCCqq1BCqBuB54VqgYVG/gCECp0BwgCDCp8HgYCDCo/wCo0MgHAjACBj7rDABS1Bv4lBv4rPAAsPCo3+gbbPJAIVFiAXMFZ2AUQsAuAQHiOAgJeEA"));
g.reset();
g.drawImage(bgImg,0,101);
}
function square(x,y,w,e) {
g.setColor("#000").fillRect(x,y,x+w,y+w);
g.setColor("#fff").fillRect(x+e,y+e,x+w-e,y+w-e);
}
function draw() {
var d = new Date();
var h = d.getHours(), m = d.getMinutes();
h = ("0"+h).substr(-2);
m = ("0"+m).substr(-2);
var day = d.getDate(), mon = d.getMonth(), dow = d.getDay();
day = ("0"+day).substr(-2);
mon = ("0"+(mon+1)).substr(-2);
dow = ((dow+6)%7).toString();
date = day+"."+mon;
var weatherJson = getWeather();
var wIcon;
var temp;
if(weatherJson && weatherJson.weather){
var currentWeather = weatherJson.weather;
temp = locale.temp(currentWeather.temp-273.15).match(/^(\D*\d*)(.*)$/);
const code = currentWeather.code||-1;
if (code > 0) {
wIcon = chooseIconByCode(code);
} else {
wIcon = chooseIcon(currentWeather.txt);
}
}else{
temp = "";
wIcon = weatherIcon(ERR);
}
g.reset();
g.clearRect(22,35,153,75);
g.setFont("4x5NumPretty",8);
g.fillRect(84,42,92,49);
g.fillRect(84,60,92,67);
g.drawString(h,22,35);
g.drawString(m,98,35);
g.clearRect(22,95,22+4*2*4+2*4,95+2*5);
g.setFont("4x5NumPretty",2);
g.drawString(date,22,95);
g.clearRect(22,79,22+24,79+13);
g.setFont("DoW");
g.drawString(dow,22,79);
g.drawImage(wIcon,126,81);
g.clearRect(108,114,176,114+4*5);
if (temp != "") {
var tempWidth;
const mid=126+15;
if (temp[1][0]=="-") {
// do not account for - when aligning
const minusWidth=3*4;
tempWidth = minusWidth+(temp[1].length-1)*4*4;
x = mid-Math.round((tempWidth-minusWidth)/2)-minusWidth;
} else {
tempWidth = temp[1].length*4*4;
x = mid-Math.round(tempWidth/2);
}
g.setFont("4x5NumPretty",4);
g.drawString(temp[1],x,114);
square(x+tempWidth,114,6,2);
}
// queue draw in one minute
queueDraw();
}
g.clear();
drawBg();
Bangle.setUI("clock"); // Show launcher when middle button pressed
Bangle.loadWidgets();
Bangle.drawWidgets();
draw();

BIN
apps/dinoClock/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

1
apps/dinoClock/icon.js Normal file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwgJC/AAVh/E/hgFC/O/AoMB8EZwc8AoUYgYFBgFgjAXDAowXBAo8B/ARBn4FGAAsBmAFE2ADBhwFEj4VEn+AgPvAontgfwv+ABIMCMwIVCgf4FIWAAoN3sAFCwERoEB0MHwF3gEF0MPwFEAoW/4ALD/4tCg/hAoYhB/5ZDwF+Aok0gEIkEf/4AB8eMBoM2bkw="))

View File

@ -0,0 +1,17 @@
{
"id": "dinoClock",
"name": "Dino Clock",
"description": "Clock with dino from Chrome",
"screenshots": [{"url":"screens/screen1.png"}],
"icon": "app.png",
"version": "0.01",
"type": "clock",
"tags": "clock, weather, dino, trex, chrome",
"supports": ["BANGLEJS2"],
"allow_emulator": true,
"readme": "README.md",
"storage": [
{"name":"dinoClock.app.js","url":"app.js"},
{"name":"dinoClock.img","url":"icon.js","evaluate":true}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -11,3 +11,4 @@
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.
0.13: Added swipeExit setting so that left-right to exit is an option
0.14: Don't move pages when doing exit swipe.

View File

@ -29,6 +29,6 @@ Bangle 2:
**Touch** - icon to select, scond touch launches app
**Swipe Left** - move to next page of app icons
**Swipe Left/Up** - move to next page of app icons
**Swipe Right** - move to previous page of app icons
**Swipe Right/Down** - move to previous page of app icons

View File

@ -93,7 +93,7 @@ Bangle.on("swipe",(dirLeftRight, dirUpDown)=>{
if (dirUpDown==-1||dirLeftRight==-1){
++page; if (page>maxPage) page=0;
drawPage(page);
} else if (dirUpDown==1||dirLeftRight==1){
} else if (dirUpDown==1||(dirLeftRight==1 && !settings.swipeExit)){
--page; if (page<0) page=maxPage;
drawPage(page);
}

View File

@ -1,7 +1,7 @@
{
"id": "dtlaunch",
"name": "Desktop Launcher",
"version": "0.13",
"version": "0.14",
"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/f9lander/ChangeLog Normal file
View File

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

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

@ -0,0 +1,33 @@
# F9 Lander
Land a Falcon 9 booster on a drone ship.
## Game play
Attempt to land your Falcon 9 booster on a drone ship before running out of fuel.
A successful landing requires:
* setting down on the ship
* the booster has to be mostly vertical
* the landing speed cannot be too high
## Controls
The angle of the booster is controlled by tilting the watch side-to-side. The
throttle level is controlled by tilting the watch forward and back:
* screen horizontal (face up) means no throttle
* screen vertical corresponds to full throttle
The fuel burn rate is proportional to the throttle level.
## Creators
Liam Kl. B.
Marko Kl. B.
## Screenshots
![](f9lander_screenshot1.png)
![](f9lander_screenshot2.png)
![](f9lander_screenshot3.png)

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwcA/4AD/P8yVJkgCCye27dt2wRE//kCIuSuwRIBwgCCpwRQpIRRnYRQkmdCIvPCJICBEZ4RG/IRP/15CJ/z5IRPz4RM/gQB/n+BxICCn/z/P/BxQCDz7mIAX4Cq31/CJ+ebpiYE/IR/CNP/5IROnn//4jP5DFQ5sJCKAjPk3oCMMk4QRQAX4Ckn7jBAA/5CK8nCJPJNHA"))

150
apps/f9lander/app.js Normal file
View File

@ -0,0 +1,150 @@
const falcon9 = Graphics.createImage(`
xxxxx
xxxxx xxxxx
x x
x x
xxxxx
xxxxx
xxxxx
xxxxx
xxxxx
xxxxx
xxxxx
xxxxx
xxxxx
xxxxx
xxxxx
xxxxx
xxxxx
xxxxx
xxxxxxxxx
xx xxxxx xx
xx xx`);
const droneShip = Graphics.createImage(`
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
`);
const droneX = Math.floor(Math.random()*(g.getWidth()-droneShip.width-40) + 20)
const cloudOffs = Math.floor(Math.random()*g.getWidth()/2);
const oceanHeight = g.getHeight()*0.1;
const targetY = g.getHeight()-oceanHeight-falcon9.height/2;
var booster = { x : g.getWidth()/4 + Math.random()*g.getWidth()/2,
y : 20,
vx : 0,
vy : 0,
mass : 100,
fuel : 100 };
var exploded = false;
var nExplosions = 0;
var landed = false;
const gravity = 4;
const dt = 0.1;
const fuelBurnRate = 20*(176/g.getHeight());
const maxV = 12;
function flameImageGen (throttle) {
var str = " xxx \n xxx \n";
str += "xxxxx\n".repeat(throttle);
str += " xxx \n x \n";
return Graphics.createImage(str);
}
function drawFalcon(x, y, throttle, angle) {
g.setColor(1, 1, 1).drawImage(falcon9, x, y, {rotate:angle});
if (throttle>0) {
var flameImg = flameImageGen(throttle);
var r = falcon9.height/2 + flameImg.height/2-1;
var xoffs = -Math.sin(angle)*r;
var yoffs = Math.cos(angle)*r;
if (Math.random()>0.7) g.setColor(1, 0.5, 0);
else g.setColor(1, 1, 0);
g.drawImage(flameImg, x+xoffs, y+yoffs, {rotate:angle});
}
}
function drawBG() {
g.setBgColor(0.2, 0.2, 1).clear();
g.setColor(0, 0, 1).fillRect(0, g.getHeight()-oceanHeight, g.getWidth()-1, g.getHeight()-1);
g.setColor(0.5, 0.5, 1).fillCircle(cloudOffs+34, 30, 15).fillCircle(cloudOffs+60, 35, 20).fillCircle(cloudOffs+75, 20, 10);
g.setColor(1, 1, 0).fillCircle(g.getWidth(), 0, 20);
g.setColor(1, 1, 1).drawImage(droneShip, droneX, g.getHeight()-oceanHeight-1);
}
function showFuel() {
g.setColor(0, 0, 0).setFont("4x6:2").setFontAlign(-1, -1, 0).drawString("Fuel: "+Math.abs(booster.fuel).toFixed(0), 4, 4);
}
function renderScreen(input) {
drawBG();
showFuel();
drawFalcon(booster.x, booster.y, Math.floor(input.throttle*12), input.angle);
}
function getInputs() {
var accel = Bangle.getAccel();
var a = Math.PI/2 + Math.atan2(accel.y, accel.x);
var t = (1+accel.z);
if (t > 1) t = 1;
if (t < 0) t = 0;
if (booster.fuel<=0) t = 0;
return {throttle: t, angle: a};
}
function epilogue(str) {
g.setFont("Vector", 24).setFontAlign(0, 0, 0).setColor(0, 0, 0).drawString(str, g.getWidth()/2, g.getHeight()/2).flip();
g.setFont("Vector", 16).drawString("<= again exit =>", g.getWidth()/2, g.getHeight()/2+20);
clearInterval(stepInterval);
Bangle.on("swipe", (d) => { if (d>0) load(); else load('f9lander.app.js'); });
}
function gameStep() {
if (exploded) {
if (nExplosions++ < 15) {
var r = Math.random()*25;
var x = Math.random()*30 - 15;
var y = Math.random()*30 - 15;
g.setColor(1, Math.random()*0.5+0.5, 0).fillCircle(booster.x+x, booster.y+y, r);
if (nExplosions==1) Bangle.buzz(600);
}
else epilogue("You crashed!");
}
else {
var input = getInputs();
if (booster.y >= targetY) {
// console.log(booster.x + " " + booster.y + " " + booster.vy + " " + droneX + " " + input.angle);
if (Math.abs(booster.x-droneX-droneShip.width/2)<droneShip.width/2 && Math.abs(input.angle)<Math.PI/8 && booster.vy<maxV) {
renderScreen({angle:0, throttle:0});
epilogue("You landed!");
}
else exploded = true;
}
else {
booster.x += booster.vx*dt;
booster.y += booster.vy*dt;
booster.vy += gravity*dt;
booster.fuel -= input.throttle*dt*fuelBurnRate;
booster.vy += -Math.cos(input.angle)*input.throttle*gravity*3*dt;
booster.vx += Math.sin(input.angle)*input.throttle*gravity*3*dt;
renderScreen(input);
}
}
}
var stepInterval;
Bangle.setLCDTimeout(0);
renderScreen({angle:0, throttle:0});
g.setFont("Vector", 24).setFontAlign(0, 0, 0).setColor(0, 0, 0).drawString("Swipe to start", g.getWidth()/2, g.getHeight()/2);
Bangle.on("swipe", () => {
stepInterval = setInterval(gameStep, Math.floor(1000*dt));
Bangle.removeListener("swipe");
});

BIN
apps/f9lander/f9lander.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 722 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

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