Merge branch 'espruino:master' into master

pull/1886/head
pidajo 2022-05-26 18:28:42 +02:00 committed by GitHub
commit 16ce6105d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
107 changed files with 1810 additions and 763 deletions

View File

@ -256,6 +256,7 @@ and which gives information about the app for the Launcher.
// 'clock' - a clock - required for clocks to automatically start
// 'widget' - a widget
// '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

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

@ -27,3 +27,5 @@
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

@ -37,8 +37,8 @@ function handleFirstDayOfWeek(dow) {
return dow;
}
// Check the first day of week and update the dow field accordingly.
alarms.forEach(alarm => alarm.dow = handleFirstDayOfWeek(alarm.dow));
// 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() {
const menu = {
@ -50,7 +50,7 @@ function showMainMenu() {
alarms.forEach((e, index) => {
var label = e.timer
? require("time_utils").formatDuration(e.timer)
: require("time_utils").formatTime(e.t) + (e.dow > 0 ? (" " + decodeDOW(e)) : "");
: 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)
@ -111,8 +111,8 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex) {
},
/*LANG*/"Repeat": {
value: decodeDOW(alarm),
onchange: () => setTimeout(showEditRepeatMenu, 100, alarm.dow, dow => {
alarm.rp = dow > 0;
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);
@ -158,14 +158,14 @@ function saveAlarm(alarm, alarmIndex, time) {
}
function saveAndReload() {
// Before saving revert the dow to the standard format
alarms.forEach(a => a.dow = handleFirstDayOfWeek(a.dow, firstDayOfWeek));
// 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.forEach(a => a.dow = handleFirstDayOfWeek(a.dow, firstDayOfWeek));
alarms.filter(e => e.timer === undefined).forEach(a => a.dow = handleFirstDayOfWeek(a.dow));
}
function decodeDOW(alarm) {
@ -178,42 +178,51 @@ function decodeDOW(alarm) {
: "Once"
}
function showEditRepeatMenu(dow, dowChangeCallback) {
function showEditRepeatMenu(repeat, dow, dowChangeCallback) {
var originalRepeat = repeat;
var originalDow = dow;
var isCustom = dow > 0 && dow != WORKDAYS && dow != WEEKEND && dow != EVERY_DAY;
var isCustom = repeat && dow != WORKDAYS && dow != WEEKEND && dow != EVERY_DAY;
const menu = {
"": { "title": /*LANG*/"Repeat Alarm" },
"< Back": () => dowChangeCallback(dow),
/*LANG*/"Once": { // No days set: the alarm will fire once
value: dow == 0,
onchange: () => dowChangeCallback(0)
"< 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: dow == WORKDAYS,
onchange: () => dowChangeCallback(WORKDAYS)
value: repeat && dow == WORKDAYS,
onchange: () => dowChangeCallback(true, WORKDAYS)
},
/*LANG*/"Weekends": {
value: dow == WEEKEND,
onchange: () => dowChangeCallback(WEEKEND)
value: repeat && dow == WEEKEND,
onchange: () => dowChangeCallback(true, WEEKEND)
},
/*LANG*/"Every Day": {
value: dow == EVERY_DAY,
onchange: () => dowChangeCallback(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, originalDow)
onchange: () => setTimeout(showCustomDaysMenu, 10, isCustom ? dow : EVERY_DAY, dowChangeCallback, originalRepeat, originalDow)
}
};
E.showMenu(menu);
}
function showCustomDaysMenu(dow, dowChangeCallback, originalDow) {
function showCustomDaysMenu(dow, dowChangeCallback, originalRepeat, originalDow) {
const menu = {
"": { "title": /*LANG*/"Custom Days" },
"< Back": () => dowChangeCallback(dow),
"< 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) => {
@ -223,7 +232,7 @@ function showCustomDaysMenu(dow, dowChangeCallback, originalDow) {
};
});
menu[/*LANG*/"Cancel"] = () => setTimeout(showEditRepeatMenu, 10, originalDow, dowChangeCallback)
menu[/*LANG*/"Cancel"] = () => setTimeout(showEditRepeatMenu, 10, originalRepeat, originalDow, dowChangeCallback)
E.showMenu(menu);
}
@ -311,12 +320,12 @@ function showAdvancedMenu() {
function enableAll(on) {
if (alarms.filter(e => e.on == !on).length == 0) {
E.showPrompt(on ? /*LANG*/"Nothing to Enable" : /*LANG*/"Nothing to Disable", {
title: on ? /*LANG*/"Enable All" : /*LANG*/"Disable All",
buttons: { /*LANG*/"Ok": true }
}).then(() => showAdvancedMenu());
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) => {
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();
@ -330,7 +339,7 @@ function enableAll(on) {
function deleteAll() {
if (alarms.length == 0) {
E.showPrompt(/*LANG*/"Nothing to delete", { title: /*LANG*/"Delete All", buttons: { /*LANG*/"Ok": true } }).then(() => showAdvancedMenu());
E.showAlert(/*LANG*/"Nothing to delete", /*LANG*/"Delete All").then(() => showAdvancedMenu());
} else {
E.showPrompt(/*LANG*/"Are you sure?", {
title: /*LANG*/"Delete All"

View File

@ -2,7 +2,7 @@
"id": "alarm",
"name": "Alarms & Timers",
"shortName": "Alarms",
"version": "0.27",
"version": "0.29",
"description": "Set alarms and timers on your Bangle",
"icon": "app.png",
"tags": "tool,alarm,widget",

View File

@ -8,3 +8,4 @@
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

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

View File

@ -2,7 +2,7 @@
"id": "android",
"name": "Android Integration",
"shortName": "Android",
"version": "0.09",
"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

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

@ -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){
print("Found device", d);
submenu_scan[d.name] = function(){
E.showPrompt("Set "+d.name+"?").then((r)=>{
if (r) {
writeSettings("btname",d.name);
}
E.showMenu(buildMainMenu());
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);
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

@ -6,3 +6,4 @@
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

@ -44,6 +44,16 @@ Graphics.prototype.setLargeFont = function(scale) {
return this;
};
Graphics.prototype.setXLargeFont = function(scale) {
// Actual height 53 (55 - 3)
this.setFontCustom(
E.toString(require('heatshrink').decompress(atob('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'))),
46,
atob("FR4uHyopKyksJSssGA=="),
70+(scale<<8)+(1<<16)
);
};
Graphics.prototype.setMediumFont = function(scale) {
// Actual height 41 (42 - 2)
this.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAA/AAAAAAAA/AAAAAAAA/AAAAAAAA/AAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAAAAAAB/AAAAAAAP/AAAAAAD//AAAAAA///AAAAAP///AAAAB///8AAAAf///AAAAH///wAAAB///+AAAAH///gAAAAH//4AAAAAH/+AAAAAAH/wAAAAAAH8AAAAAAAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA///8AAAAH////AAAAP////wAAAf////4AAA/////8AAB/////+AAD/gAAH+AAD+AAAD/AAH8AAAB/AAH4AAAA/gAH4AAAAfgAH4AAAAfgAPwAAAAfgAPwAAAAfgAPwAAAAfgAHwAAAAfgAH4AAAAfgAH4AAAA/gAH8AAAA/AAD+AAAD/AAD/gAAH/AAB/////+AAB/////8AAA/////4AAAf////wAAAH////gAAAB///+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAAAAAAfwAAAAAAA/gAAAAAAA/AAAAAAAB/AAAAAAAD+AAAAAAAD8AAAAAAAH8AAAAAAAH//////AAH//////AAH//////AAH//////AAH//////AAH//////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4AAA/AAAP4AAB/AAAf4AAD/AAA/4AAD/AAB/4AAH/AAD/4AAP/AAH/AAAf/AAH8AAA//AAH4AAB//AAP4AAD//AAPwAAH+/AAPwAAP8/AAPwAAf4/AAPwAA/4/AAPwAA/w/AAPwAB/g/AAPwAD/A/AAP4AH+A/AAH8AP8A/AAH/A/4A/AAD///wA/AAD///gA/AAB///AA/AAA//+AA/AAAP/8AA/AAAD/wAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAH4AAAHwAAH4AAAH4AAH4AAAH8AAH4AAAP+AAH4AAAH+AAH4A4AB/AAH4A+AA/AAH4B/AA/gAH4D/AAfgAH4H+AAfgAH4P+AAfgAH4f+AAfgAH4/+AAfgAH5/+AAfgAH5//AAfgAH7+/AA/gAH/8/gB/AAH/4f4H/AAH/wf//+AAH/gP//8AAH/AH//8AAH+AD//wAAH8AB//gAAD4AAf+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AAAAAAAD/AAAAAAAP/AAAAAAB//AAAAAAH//AAAAAAf//AAAAAB///AAAAAH///AAAAAf/8/AAAAB//w/AAAAH/+A/AAAA//4A/AAAD//gA/AAAH/+AA/AAAH/4AA/AAAH/gAA/AAAH+AAA/AAAHwAAA/AAAHAAf///AAEAAf///AAAAAf///AAAAAf///AAAAAf///AAAAAf///AAAAAAA/AAAAAAAA/AAAAAAAA/AAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAAAAP/AHgAAH///AP4AAH///gP8AAH///gP8AAH///gP+AAH///gD/AAH/A/AB/AAH4A/AA/gAH4A+AAfgAH4B+AAfgAH4B+AAfgAH4B8AAfgAH4B8AAfgAH4B+AAfgAH4B+AAfgAH4B+AA/gAH4B/AA/AAH4A/gD/AAH4A/4H+AAH4Af//+AAH4AP//8AAH4AP//4AAHwAD//wAAAAAB//AAAAAAAf8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA///8AAAAD////AAAAP////wAAAf////4AAA/////8AAB/////+AAD/gP4H+AAD/AfgD/AAH8A/AB/AAH8A/AA/gAH4B+AAfgAH4B+AAfgAPwB8AAfgAPwB8AAfgAPwB+AAfgAPwB+AAfgAH4B+AAfgAH4B/AA/gAH8B/AB/AAH+A/wD/AAD+A/8P+AAB8Af//+AAB4AP//8AAAwAH//4AAAAAD//gAAAAAA//AAAAAAAP4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPwAAAAAAAPwAAAAAAAPwAAAAAAAPwAAAAAAAPwAAAAHAAPwAAAA/AAPwAAAD/AAPwAAAf/AAPwAAB//AAPwAAP//AAPwAA//8AAPwAH//wAAPwAf/+AAAPwB//4AAAPwP//AAAAPw//8AAAAP3//gAAAAP//+AAAAAP//wAAAAAP//AAAAAAP/4AAAAAAP/gAAAAAAP+AAAAAAAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+AAAAH+A//gAAAf/h//4AAA//z//8AAB/////+AAD/////+AAD///+H/AAH+H/4B/AAH8B/wA/gAH4A/gAfgAH4A/gAfgAPwA/AAfgAPwA/AAfgAPwA/AAfgAPwA/AAfgAH4A/gAfgAH4A/gAfgAH8B/wA/gAH/H/4B/AAD///+H/AAD/////+AAB/////+AAA//z//8AAAf/h//4AAAH+A//gAAAAAAH+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/gAAAAAAD/8AAAAAAP/+AAAAAAf//AAcAAA///gA8AAB///wB+AAD/x/4B/AAD+AP4B/AAH8AH8A/gAH4AH8A/gAH4AD8AfgAP4AD8AfgAPwAB8AfgAPwAB8AfgAPwAB8AfgAPwAB8AfgAH4AD8AfgAH4AD4A/gAH8AH4B/AAD+APwD/AAD/g/wP+AAB/////+AAA/////8AAAf////4AAAP////wAAAH////AAAAA///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8APwAAAAD8APwAAAAD8APwAAAAD8APwAAAAD8APwAAAAD8APwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="), 46, atob("DxcjFyAfISAiHCAiEg=="), 54+(scale<<8)+(1<<16));
@ -304,17 +314,13 @@ function drawTime(){
g.setColor(g.theme.bg);
g.setFontAlign(0,0);
var timeStr;
if(settings.hideColon){
var hours = date.getHours();
hours -= hours >=12 ? 12 : 0;
var minutes = date.getMinutes();
minutes = minutes < 10 ? String("0") + minutes : minutes;
timeStr = String(hours) + minutes;
} else {
timeStr = locale.time(date,1);
}
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();
@ -324,7 +330,11 @@ function drawTime(){
// Show large or small time depending on info entry
if(infoStr == null){
g.setLargeFont();
if(settings.hideColon){
g.setXLargeFont();
} else {
g.setLargeFont();
}
} else {
y -= 15;
g.setMediumFont();

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

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

View File

@ -1,2 +1,4 @@
0.01: New Clock Nifty A
0.02: Shows the current week number (ISO8601), can be disabled via settings ""
0.02: Shows the current week number (ISO8601), can be disabled via settings
0.03: Call setUI before loading widgets
Improve settings page

View File

@ -1,13 +1,12 @@
# Nifty-A Clock
Colors are black/white - photos have non correct camera color "blue"
Colors are black/white - photos have non correct camera color "blue".
## This is the clock
This is the clock:
![](screenshot_nifty.png)
## The week number (ISO8601) can be turned of in settings
(default is **"On"**)
The week number (ISO8601) can be turned off in settings (default is `On`)
![](screenshot_settings_nifty.png)

View File

@ -1,6 +1,6 @@
const locale = require("locale");
const is12Hour = (require("Storage").readJSON("setting.json", 1) || {})["12hour"];
const CFG = require('Storage').readJSON("ffcniftya.json", 1) || {showWeekNum: true};
const is12Hour = Object.assign({ "12hour": false }, require("Storage").readJSON("setting.json", true))["12hour"];
const showWeekNum = Object.assign({ showWeekNum: true }, require('Storage').readJSON("ffcniftya.json", true))["showWeekNum"];
/* Clock *********************************************/
const scale = g.getWidth() / 176;
@ -17,16 +17,17 @@ const center = {
y: Math.round(((viewport.height - widget) / 2) + widget),
}
function ISO8601_week_no(date) { //copied from: https://gist.github.com/IamSilviu/5899269#gistcomment-3035480
var tdt = new Date(date.valueOf());
var dayn = (date.getDay() + 6) % 7;
tdt.setDate(tdt.getDate() - dayn + 3);
var firstThursday = tdt.valueOf();
tdt.setMonth(0, 1);
if (tdt.getDay() !== 4) {
tdt.setMonth(0, 1 + ((4 - tdt.getDay()) + 7) % 7);
}
return 1 + Math.ceil((firstThursday - tdt) / 604800000);
// copied from: https://gist.github.com/IamSilviu/5899269#gistcomment-3035480
function ISO8601_week_no(date) {
var tdt = new Date(date.valueOf());
var dayn = (date.getDay() + 6) % 7;
tdt.setDate(tdt.getDate() - dayn + 3);
var firstThursday = tdt.valueOf();
tdt.setMonth(0, 1);
if (tdt.getDay() !== 4) {
tdt.setMonth(0, 1 + ((4 - tdt.getDay()) + 7) % 7);
}
return 1 + Math.ceil((firstThursday - tdt) / 604800000);
}
function d02(value) {
@ -59,7 +60,7 @@ function draw() {
g.drawString(year, centerDatesScaleX, center.y - 62 * scale);
g.drawString(month, centerDatesScaleX, center.y - 44 * scale);
g.drawString(day, centerDatesScaleX, center.y - 26 * scale);
if (CFG.showWeekNum) g.drawString(d02(ISO8601_week_no(now)), centerDatesScaleX, center.y + 15 * scale);
if (showWeekNum) g.drawString(weekNum, centerDatesScaleX, center.y + 15 * scale);
g.drawString(monthName, centerDatesScaleX, center.y + 48 * scale);
g.drawString(dayName, centerDatesScaleX, center.y + 66 * scale);
}
@ -79,7 +80,6 @@ function clearTickTimer() {
function queueNextTick() {
clearTickTimer();
tickTimer = setTimeout(tick, 60000 - (Date.now() % 60000));
// tickTimer = setTimeout(tick, 3000);
}
function tick() {
@ -91,21 +91,16 @@ function tick() {
// Clear the screen once, at startup
g.clear();
// Start ticking
tick();
// Stop updates when LCD is off, restart when on
Bangle.on('lcdPower', (on) => {
if (on) {
tick(); // Start ticking
tick();
} else {
clearTickTimer(); // stop ticking
clearTickTimer();
}
});
// Load widgets
Bangle.setUI("clock");
Bangle.loadWidgets();
Bangle.drawWidgets();
// Show launcher when middle button pressed
Bangle.setUI("clock");

View File

@ -1,7 +1,7 @@
{
"id": "ffcniftya",
"name": "Nifty-A Clock",
"version": "0.02",
"version": "0.03",
"description": "A nifty clock with time and date",
"icon": "app.png",
"screenshots": [{"url":"screenshot_nifty.png"}],

View File

@ -1,22 +1,14 @@
(function(back) {
var FILE = "ffcniftya.json";
// Load settings
var cfg = require('Storage').readJSON(FILE, 1) || { showWeekNum: true };
(function (back) {
const settings = Object.assign({ showWeekNum: true }, require("Storage").readJSON("ffcniftya.json", true));
function writeSettings() {
require('Storage').writeJSON(FILE, cfg);
}
// Show the menu
E.showMenu({
"" : { "title" : "Nifty-A Clock" },
"< Back" : () => back(),
'week number?': {
value: cfg.showWeekNum,
format: v => v?"On":"Off",
"": { "title": "Nifty-A Clock" },
"< Back": () => back(),
/*LANG*/"Show Week Number": {
value: settings.showWeekNum,
onchange: v => {
cfg.showWeekNum = v;
writeSettings();
settings.showWeekNum = v;
require("Storage").writeJSON("ffcniftya.json", settings);
}
}
});

View File

@ -1,2 +1,5 @@
0.01: New Clock Nifty B
0.02: Added configuration
0.03: Call setUI before loading widgets
Fix bug with black being unselectable
Improve settings page

View File

@ -1,9 +1,6 @@
# Nifty Series B Clock
- Display Time and Date
- Color Configuration
##
- Colour Configuration
![](screenshot.png)

View File

@ -1,9 +1,5 @@
const locale = require("locale");
const storage = require('Storage');
const is12Hour = (storage.readJSON("setting.json", 1) || {})["12hour"];
const color = (storage.readJSON("ffcniftyb.json", 1) || {})["color"] || 63488 /* red */;
const is12Hour = Object.assign({ "12hour": false }, require("Storage").readJSON("setting.json", true))["12hour"];
const color = Object.assign({ color: 63488 }, require("Storage").readJSON("ffcniftyb.json", true)).color; // Default to RED
/* Clock *********************************************/
const scale = g.getWidth() / 176;
@ -19,7 +15,7 @@ const center = {
};
function d02(value) {
return ('0' + value).substr(-2);
return ("0" + value).substr(-2);
}
function renderEllipse(g) {
@ -35,8 +31,8 @@ function renderText(g) {
const month = d02(now.getMonth() + 1);
const year = now.getFullYear();
const month2 = locale.month(now, 3);
const day2 = locale.dow(now, 3);
const month2 = require("locale").month(now, 3);
const day2 = require("locale").dow(now, 3);
g.setFontAlign(1, 0).setFont("Vector", 90 * scale);
g.drawString(hour, center.x + 32 * scale, center.y - 31 * scale);
@ -96,7 +92,6 @@ function startTick(run) {
stopTick();
run();
ticker = setTimeout(() => startTick(run), 60000 - (Date.now() % 60000));
// ticker = setTimeout(() => startTick(run), 3000);
}
/* Init **********************************************/
@ -104,7 +99,7 @@ function startTick(run) {
g.clear();
startTick(draw);
Bangle.on('lcdPower', (on) => {
Bangle.on("lcdPower", (on) => {
if (on) {
startTick(draw);
} else {
@ -112,7 +107,6 @@ Bangle.on('lcdPower', (on) => {
}
});
Bangle.setUI("clock");
Bangle.loadWidgets();
Bangle.drawWidgets();
Bangle.setUI("clock");

View File

@ -1,8 +1,8 @@
{
"id": "ffcniftyb",
"name": "Nifty-B Clock",
"version": "0.02",
"description": "A nifty clock (series B) with time, date and color configuration",
"version": "0.03",
"description": "A nifty clock (series B) with time, date and colour configuration",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
"type": "clock",

View File

@ -1,49 +1,31 @@
(function (back) {
const storage = require('Storage');
const SETTINGS_FILE = "ffcniftyb.json";
const settings = Object.assign({ color: 63488 }, require("Storage").readJSON("ffcniftyb.json", true));
const colors = {
65535: 'White',
63488: 'Red',
65504: 'Yellow',
2047: 'Cyan',
2016: 'Green',
31: 'Blue',
0: 'Black',
65535: /*LANG*/"White",
63488: /*LANG*/"Red",
65504: /*LANG*/"Yellow",
2047: /*LANG*/"Cyan",
2016: /*LANG*/"Green",
31: /*LANG*/"Blue",
0: /*LANG*/"Black"
}
function load(settings) {
return Object.assign(settings, storage.readJSON(SETTINGS_FILE, 1) || {});
}
const menu = {};
menu[""] = { title: "Nifty-B Clock" };
menu["< Back"] = back;
function save(settings) {
storage.write(SETTINGS_FILE, settings)
}
const settings = load({
color: 63488 /* red */,
Object.keys(colors).forEach(color => {
var label = colors[color];
menu[label] = {
value: settings.color == color,
onchange: () => {
settings.color = color;
require("Storage").write("ffcniftyb.json", settings);
setTimeout(load, 10);
}
};
});
const saveColor = (color) => () => {
settings.color = color;
save(settings);
back();
};
function showMenu(items, opt) {
items[''] = opt || {};
items['< Back'] = back;
E.showMenu(items);
}
showMenu(
Object.keys(colors).reduce((menu, color) => {
menu[colors[color]] = saveColor(color);
return menu;
}, {}),
{
title: 'Color',
selected: Object.keys(colors).indexOf(settings.color)
}
);
E.showMenu(menu);
});

View File

@ -2,7 +2,7 @@
Logs health data to a file every 10 minutes, and provides an app to view it
**BETA - requires firmware 2v11**
**BETA - requires firmware 2v11 or later**
## Usage

View File

@ -0,0 +1,2 @@
0.01: Initial release
0.02: implemented "direct launch" and "one click exit" settings

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

@ -0,0 +1,12 @@
# Icon launcher
A launcher inspired by smartphones, with an icon-only scrollable menu.
This launcher shows 9 apps per screen, making it much faster to navigate versus the default launcher.
![A screenshot](screenshot1.png)
![Another screenshot](screenshot2.png)
## Technical note
The app uses `E.showScroller`'s code in the app but not the function itself because `E.showScroller` doesn't report the position of a press to the select function.

209
apps/iconlaunch/app.js Normal file
View File

@ -0,0 +1,209 @@
const s = require("Storage");
const settings = s.readJSON("launch.json", true) || { showClocks: true, fullscreen: false,direct:false,oneClickExit:false };
if( settings.oneClickExit)
setWatch(_=> load(), BTN1);
if (!settings.fullscreen) {
Bangle.loadWidgets();
Bangle.drawWidgets();
}
var apps = s
.list(/\.info$/)
.map((app) => {
var a = s.readJSON(app, 1);
return (
a && {
name: a.name,
type: a.type,
icon: a.icon,
sortorder: a.sortorder,
src: a.src,
}
);
})
.filter(
(app) =>
app &&
(app.type == "app" ||
(app.type == "clock" && settings.showClocks) ||
!app.type)
);
apps.sort((a, b) => {
var n = (0 | a.sortorder) - (0 | b.sortorder);
if (n) return n; // do sortorder first
if (a.name < b.name) return -1;
if (a.name > b.name) return 1;
return 0;
});
apps.forEach((app) => {
if (app.icon) app.icon = s.read(app.icon); // should just be a link to a memory area
});
let scroll = 0;
let selectedItem = -1;
const R = Bangle.appRect;
const iconSize = 48;
const appsN = Math.floor(R.w / iconSize);
const whitespace = (R.w - appsN * iconSize) / (appsN + 1);
const itemSize = iconSize + whitespace;
function drawItem(itemI, r) {
g.clearRect(r.x, r.y, r.x + r.w - 1, r.y + r.h - 1);
let x = 0;
for (let i = itemI * appsN; i < appsN * (itemI + 1); i++) {
if (!apps[i]) break;
x += whitespace;
if (!apps[i].icon) {
g.setFontAlign(0,0,0).setFont("12x20:2").drawString("?", x + r.x+iconSize/2, r.y + iconSize/2);
} else {
g.drawImage(apps[i].icon, x + r.x, r.y);
}
if (selectedItem == i) {
g.drawRect(
x + r.x - 1,
r.y - 1,
x + r.x + iconSize + 1,
r.y + iconSize + 1
);
}
x += iconSize;
}
drawText(itemI);
}
function drawItemAuto(i) {
var y = idxToY(i);
g.reset().setClipRect(R.x, y, R.x2, y + itemSize);
drawItem(i, {
x: R.x,
y: y,
w: R.w,
h: itemSize
});
g.setClipRect(0, 0, g.getWidth() - 1, g.getHeight() - 1);
}
let lastIsDown = false;
function drawText(i) {
const selectedApp = apps[selectedItem];
const idy = (selectedItem - (selectedItem % 3)) / 3;
if (!selectedApp || i != idy) return;
const appY = idxToY(idy) + iconSize / 2;
g.setFontAlign(0, 0, 0);
g.setFont("12x20");
const rect = g.stringMetrics(selectedApp.name);
g.clearRect(
R.w / 2 - rect.width / 2,
appY - rect.height / 2,
R.w / 2 + rect.width / 2,
appY + rect.height / 2
);
g.drawString(selectedApp.name, R.w / 2, appY);
}
function selectItem(id, e) {
const iconN = E.clip(Math.floor((e.x - R.x) / itemSize), 0, appsN - 1);
const appId = id * appsN + iconN;
if( settings.direct && apps[appId])
{
load(apps[appId].src);
return;
}
if (appId == selectedItem && apps[appId]) {
const app = apps[appId];
if (!app.src || s.read(app.src) === undefined) {
E.showMessage( /*LANG*/ "App Source\nNot found");
} else {
load(app.src);
}
}
selectedItem = appId;
drawItems();
}
function idxToY(i) {
return i * itemSize + R.y - (scroll & ~1);
}
function YtoIdx(y) {
return Math.floor((y + (scroll & ~1) - R.y) / itemSize);
}
function drawItems() {
g.reset().clearRect(R.x, R.y, R.x2, R.y2);
g.setClipRect(R.x, R.y, R.x2, R.y2);
var a = YtoIdx(R.y);
var b = Math.min(YtoIdx(R.y2), 99);
for (var i = a; i <= b; i++)
drawItem(i, {
x: R.x,
y: idxToY(i),
w: R.w,
h: itemSize,
});
g.setClipRect(0, 0, g.getWidth() - 1, g.getHeight() - 1);
}
drawItems();
g.flip();
const itemsN = Math.ceil(apps.length / appsN);
Bangle.setUI({
mode: "custom",
drag: (e) => {
let dy = e.dy;
if (scroll + R.h - dy > itemsN * itemSize) {
dy = scroll + R.h - itemsN * itemSize;
}
if (scroll - dy < 0) {
dy = scroll;
}
scroll -= dy;
scroll = E.clip(scroll, 0, itemSize * (itemsN - 1));
g.setClipRect(R.x, R.y, R.x2, R.y2);
g.scroll(0, dy);
if (dy < 0) {
g.setClipRect(R.x, R.y2 - (1 - dy), R.x2, R.y2);
let i = YtoIdx(R.y2 - (1 - dy));
let y = idxToY(i);
while (y < R.y2) {
drawItem(i, {
x: R.x,
y: y,
w: R.w,
h: itemSize,
});
i++;
y += itemSize;
}
} else {
// d>0
g.setClipRect(R.x, R.y, R.x2, R.y + dy);
let i = YtoIdx(R.y + dy);
let y = idxToY(i);
while (y > R.y - itemSize) {
drawItem(i, {
x: R.x,
y: y,
w: R.w,
h: itemSize,
});
y -= itemSize;
i--;
}
}
g.setClipRect(0, 0, g.getWidth() - 1, g.getHeight() - 1);
},
touch: (_, e) => {
if (e.y < R.y - 4) return;
var i = YtoIdx(e.y);
selectItem(i, e);
},
});

BIN
apps/iconlaunch/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,18 @@
{
"id": "iconlaunch",
"name": "Icon Launcher",
"shortName" : "Icon launcher",
"version": "0.02",
"icon": "app.png",
"description": "A launcher inspired by smartphones, with an icon-only scrollable menu.",
"tags": "tool,system,launcher",
"type": "launch",
"supports": ["BANGLEJS2"],
"storage": [
{ "name": "iconlaunch.app.js", "url": "app.js" },
{ "name": "iconlaunch.settings.js", "url": "settings.js" }
],
"screenshots": [{ "url": "screenshot1.png" }, { "url": "screenshot2.png" }],
"readme": "README.md",
"sortorder": -10
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

@ -0,0 +1,38 @@
// make sure to enclose the function in parentheses
(function(back) {
let settings = Object.assign({
showClocks: true,
fullscreen: false
}, require("Storage").readJSON("launch.json", true) || {});
let fonts = g.getFonts();
function save(key, value) {
settings[key] = value;
require("Storage").write("launch.json",settings);
}
const appMenu = {
"": { "title": /*LANG*/"Launcher" },
/*LANG*/"< Back": back,
/*LANG*/"Show Clocks": {
value: settings.showClocks == true,
format: v => v ? /*LANG*/"Yes" : /*LANG*/"No",
onchange: (m) => { save("showClocks", m) }
},
/*LANG*/"Fullscreen": {
value: settings.fullscreen == true,
format: v => v ? /*LANG*/"Yes" : /*LANG*/"No",
onchange: (m) => { save("fullscreen", m) }
},
/*LANG*/"Direct launch": {
value: settings.direct == true,
format: v => v ? /*LANG*/"Yes" : /*LANG*/"No",
onchange: (m) => { save("direct", m) }
},
/*LANG*/"One click exit": {
value: settings.oneClickExit == true,
format: v => v ? /*LANG*/"Yes" : /*LANG*/"No",
onchange: (m) => { save("oneClickExit", m) }
}
};
E.showMenu(appMenu);
});

View File

@ -11,6 +11,8 @@ const speedUnits = { // how many kph per X?
"kmh": 1,
"kph": 1,
"km/h": 1,
"kmt": 1,
"km/tim": 1,
"mph": 1.60934,
"kts": 1.852
};

View File

@ -50,4 +50,4 @@
0.35: Reset graphics colors before rendering a message (possibly fix #1752)
0.36: Ensure a new message plus an almost immediate deletion of that message doesn't load the messages app (fix #1362)
0.37: Now use the setUI 'back' icon in the top left rather than specific buttons/menu items
0.38: Add telegram foss handling

View File

@ -67,100 +67,6 @@ function saveMessages() {
require("Storage").writeJSON("messages.json",MESSAGES)
}
function getNotificationImage() {
return atob("HBKBAD///8H///iP//8cf//j4//8f5//j/x/8//j/H//H4//4PB//EYj/44HH/Hw+P4//8fH//44///xH///g////A==");
}
function getFBIcon() {
return atob("GBiBAAAAAAAAAAAYAAD/AAP/wAf/4A/48A/g8B/g+B/j+B/n+D/n/D8A/B8A+B+B+B/n+A/n8A/n8Afn4APnwADnAAAAAAAAAAAAAA==");
}
function getPosImage() {
return atob("GRSBAAAAAYAAAcAAAeAAAfAAAfAAAfAAAfAAAfAAAfBgAfA4AfAeAfAPgfAD4fAA+fAAP/AAD/AAA/AAAPAAADAAAA==");
}
function getNegImage() {
return atob("FhaBADAAMeAB78AP/4B/fwP4/h/B/P4D//AH/4AP/AAf4AB/gAP/AB/+AP/8B/P4P4fx/A/v4B//AD94AHjAAMA=");
}
/*
* icons should be 24x24px with 1bpp colors and 'Transparency to Color'
* http://www.espruino.com/Image+Converter
*/
function getMessageImage(msg) {
if (msg.img) return atob(msg.img);
var s = (msg.src||"").toLowerCase();
if (s=="alarm" || s =="alarmclockreceiver") return atob("GBjBAP////8AAAAAAAACAEAHAOAefng5/5wTgcgHAOAOGHAMGDAYGBgYGBgYGBgYGBgYDhgYBxgMATAOAHAHAOADgcAB/4AAfgAAAAAAAAA=");
if (s=="bibel") return atob("GBgBAAAAA//wD//4D//4H//4H/f4H/f4H+P4H4D4H4D4H/f4H/f4H/f4H/f4H/f4H//4H//4H//4GAAAEAAAEAAACAAAB//4AAAA");
if (s=="calendar") return atob("GBiBAAAAAAAAAAAAAA//8B//+BgAGBgAGBgAGB//+B//+B//+B9m2B//+B//+Btm2B//+B//+Btm+B//+B//+A//8AAAAAAAAAAAAA==");
if (s=="corona-warn") return atob("GBgBAAAAABwAAP+AAf/gA//wB/PwD/PgDzvAHzuAP8EAP8AAPAAAPMAAP8AAH8AAHzsADzuAB/PAB/PgA//wAP/gAH+AAAwAAAAA");
if (s=="discord") return atob("GBgBAAAAAAAAAAAAAIEABwDgDP8wH//4H//4P//8P//8P//8Pjx8fhh+fzz+f//+f//+e//ePH48HwD4AgBAAAAAAAAAAAAAAAAA");
if (s=="facebook") return getFBIcon();
if (s=="gmail") return getNotificationImage();
if (s=="google home") return atob("GBiCAAAAAAAAAAAAAAAAAAAAAoAAAAAACqAAAAAAKqwAAAAAqroAAAACquqAAAAKq+qgAAAqr/qoAACqv/6qAAKq//+qgA6r///qsAqr///6sAqv///6sAqv///6sAqv///6sA6v///6sA6v///qsA6qqqqqsA6qqqqqsA6qqqqqsAP7///vwAAAAAAAAAAAAAAAAA==");
if (s=="hangouts") return atob("FBaBAAH4AH/gD/8B//g//8P//H5n58Y+fGPnxj5+d+fmfj//4//8H//B//gH/4A/8AA+AAHAABgAAAA=");
if (s=="home assistant") return atob("FhaBAAAAAADAAAeAAD8AAf4AD/3AfP8D7fwft/D/P8ec572zbzbNsOEhw+AfD8D8P4fw/z/D/P8P8/w/z/AAAAA=");
if (s=="instagram") return atob("GBiBAAAAAAAAAAAAAAAAAAP/wAYAYAwAMAgAkAh+EAjDEAiBEAiBEAiBEAiBEAjDEAh+EAgAEAwAMAYAYAP/wAAAAAAAAAAAAAAAAA==");
if (s=="kalender") return atob("GBgBBgBgBQCgff++RQCiRgBiQAACf//+QAACQAACR//iRJkiRIEiR//iRNsiRIEiRJkiR//iRIEiRIEiR//iQAACQAACf//+AAAA");
if (s=="lieferando") return atob("GBgBABgAAH5wAP9wAf/4A//4B//4D//4H//4P/88fV8+fV4//V4//Vw/HVw4HVw4HBg4HBg4HBg4HDg4Hjw4Hj84Hj44Hj44Hj44");
if (s=="mail") return getNotificationImage();
if (s=="messenger") return getFBIcon();
if (s=="nina") return atob("GBgBAAAABAAQCAAICAAIEAAEEgAkJAgSJBwSKRxKSj4pUn8lVP+VVP+VUgAlSgApKQBKJAASJAASEgAkEAAECAAICAAIBAAQAAAA");
if (s=="outlook mail") return atob("HBwBAAAAAAAAAAAIAAAfwAAP/gAB/+AAP/5/A//v/D/+/8P/7/g+Pv8Dye/gPd74w5znHDnOB8Oc4Pw8nv/Dwe/8Pj7/w//v/D/+/8P/7/gf/gAA/+AAAfwAAACAAAAAAAAAAAA=");
if (s=="phone") return atob("FxeBABgAAPgAAfAAB/AAD+AAH+AAP8AAP4AAfgAA/AAA+AAA+AAA+AAB+AAB+AAB+OAB//AB//gB//gA//AA/8AAf4AAPAA=");
if (s=="post & dhl") return atob("GBgBAPgAE/5wMwZ8NgN8NgP4NgP4HgP4HgPwDwfgD//AB/+AAf8AAAAABs7AHcdgG4MwAAAAGESAFESAEkSAEnyAEkSAFESAGETw");
if (s=="signal") return atob("GBgBAAAAAGwAAQGAAhggCP8QE//AB//oJ//kL//wD//0D//wT//wD//wL//0J//kB//oA//ICf8ABfxgBYBAADoABMAABAAAAAAA");
if (s=="skype") return atob("GhoBB8AAB//AA//+Af//wH//+D///w/8D+P8Afz/DD8/j4/H4fP5/A/+f4B/n/gP5//B+fj8fj4/H8+DB/PwA/x/A/8P///B///gP//4B//8AD/+AAA+AA==");
if (s=="slack") return atob("GBiBAAAAAAAAAABAAAHvAAHvAADvAAAPAB/PMB/veD/veB/mcAAAABzH8B3v+B3v+B3n8AHgAAHuAAHvAAHvAADGAAAAAAAAAAAAAA==");
if (s=="sms message") return getNotificationImage();
if (s=="snapchat") return atob("GBgBAAAAAAAAAH4AAf+AAf+AA//AA//AA//AA//AA//AH//4D//wB//gA//AB//gD//wH//4f//+P//8D//wAf+AAH4AAAAAAAAA");
if (s=="teams") return atob("GBgBAAAAAAAAAAQAAB4AAD8IAA8cP/M+f/scf/gIeDgAfvvefvvffvvffvvffvvff/vff/veP/PeAA/cAH/AAD+AAD8AAAQAAAAA");
if (s=="telegram") return atob("GBiBAAAAAAAAAAAAAAAAAwAAHwAA/wAD/wAf3gD/Pgf+fh/4/v/z/P/H/D8P/Acf/AM//AF/+AF/+AH/+ADz+ADh+ADAcAAAMAAAAA==");
if (s=="threema") return atob("GBjB/4Yx//8AAAAAAAAAAAAAfgAB/4AD/8AH/+AH/+AP//AP2/APw/APw/AHw+AH/+AH/8AH/4AH/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=");
if (s=="to do") return atob("GBgBAAAAAAAAAAAwAAB4AAD8AAH+AAP/DAf/Hg//Px/+f7/8///4///wf//gP//AH/+AD/8AB/4AA/wAAfgAAPAAAGAAAAAAAAAA");
if (s=="twitch") return atob("GBgBH//+P//+P//+eAAGeAAGeAAGeDGGeDOGeDOGeDOGeDOGeDOGeDOGeAAOeAAOeAAcf4/4f5/wf7/gf//Af/+AA/AAA+AAAcAA");
if (s=="twitter") return atob("GhYBAABgAAB+JgA/8cAf/ngH/5+B/8P8f+D///h///4f//+D///g///wD//8B//+AP//gD//wAP/8AB/+AB/+AH//AAf/AAAYAAA");
if (s=="whatsapp") return atob("GBiBAAB+AAP/wAf/4A//8B//+D///H9//n5//nw//vw///x///5///4///8e//+EP3/APn/wPn/+/j///H//+H//8H//4H//wMB+AA==");
if (s=="wordfeud") return atob("GBgCWqqqqqqlf//////9v//////+v/////++v/////++v8///Lu+v8///L++v8///P/+v8v//P/+v9v//P/+v+fx/P/+v+Pk+P/+v/PN+f/+v/POuv/+v/Ofdv/+v/NvM//+v/I/Y//+v/k/k//+v/i/w//+v/7/6//+v//////+v//////+f//////9Wqqqqqql");
if (s=="youtube") return atob("GBgBAAAAAAAAAAAAAAAAAf8AH//4P//4P//8P//8P5/8P4/8f4P8f4P8P4/8P5/8P//8P//8P//4H//4Af8AAAAAAAAAAAAAAAAA");
if (msg.id=="music") return atob("FhaBAH//+/////////////h/+AH/4Af/gB/+H3/7/f/v9/+/3/7+f/vB/w8H+Dwf4PD/x/////////////3//+A=");
return getNotificationImage();
}
function getMessageImageCol(msg,def) {
return {
// generic colors, using B2-safe colors
"alarm": "#fff",
"mail": "#ff0",
"music": "#f0f",
"phone": "#0f0",
"sms message": "#0ff",
// brands, according to https://www.schemecolor.com/?s (picking one for multicolored logos)
// all dithered on B2, but we only use the color for the icons. (Could maybe pick the closest 3-bit color for B2?)
"bibel": "#54342c",
"discord": "#738adb",
"facebook": "#4267b2",
"gmail": "#ea4335",
"google home": "#fbbc05",
"hangouts": "#1ba261",
"home assistant": "#fff", // ha-blue is #41bdf5, but that's the background
"instagram": "#dd2a7b",
"liferando": "#ee5c00",
"messenger": "#0078ff",
"nina": "#e57004",
"outlook mail": "#0072c6",
"post & dhl": "#f2c101",
"signal": "#00f",
"skype": "#00aff0",
"slack": "#e51670",
"snapchat": "#ff0",
"teams": "#464eb8",
"telegram": "#0088cc",
"threema": "#000",
"to do": "#3999e5",
"twitch": "#6441A4",
"twitter": "#1da1f2",
"whatsapp": "#4fce5d",
"wordfeud": "#e7d3c7",
"youtube": "#f00",
}[(msg.src||"").toLowerCase()]||(def !== undefined?def:g.theme.fg);
}
function showMapMessage(msg) {
active = "map";
var m;
@ -387,7 +293,7 @@ function showMessage(msgid) {
var buttons = [
];
if (msg.positive) {
buttons.push({type:"btn", src:getPosImage(), cb:()=>{
buttons.push({type:"btn", src:atob("GRSBAAAAAYAAAcAAAeAAAfAAAfAAAfAAAfAAAfAAAfBgAfA4AfAeAfAPgfAD4fAA+fAAP/AAD/AAA/AAAPAAADAAAA=="), cb:()=>{
msg.new = false; saveMessages();
cancelReloadTimeout(); // don't auto-reload to clock now
Bangle.messageResponse(msg,true);
@ -396,7 +302,7 @@ function showMessage(msgid) {
}
if (msg.negative) {
if (buttons.length) buttons.push({width:32}); // nasty hack...
buttons.push({type:"btn", src:getNegImage(), cb:()=>{
buttons.push({type:"btn", src:atob("FhaBADAAMeAB78AP/4B/fwP4/h/B/P4D//AH/4AP/AAf4AB/gAP/AB/+AP/8B/P4P4fx/A/v4B//AD94AHjAAMA="), cb:()=>{
msg.new = false; saveMessages();
cancelReloadTimeout(); // don't auto-reload to clock now
Bangle.messageResponse(msg,false);
@ -411,7 +317,7 @@ function showMessage(msgid) {
{type:"txt", font:fontSmall, label:msg.src||/*LANG*/"Message", bgCol:g.theme.bg2, col: g.theme.fg2, fillx:1, pad:2, halign:1 },
title?{type:"txt", font:titleFont, label:title, bgCol:g.theme.bg2, col: g.theme.fg2, fillx:1, pad:2 }:{},
]},
{ type:"btn", src:getMessageImage(msg), col:getMessageImageCol(msg), pad: 3, cb:()=>{
{ type:"btn", src:require("messages").getMessageImage(msg), col:require("messages").getMessageImageCol(msg), pad: 3, cb:()=>{
cancelReloadTimeout(); // don't auto-reload to clock now
showMessageSettings(msg);
}},
@ -467,14 +373,14 @@ function checkMessages(options) {
g.clearRect(r.x,r.y,r.x+r.w, r.y+r.h);
if (!msg) return;
var x = r.x+2, title = msg.title, body = msg.body;
var img = getMessageImage(msg);
var img = require("messages").getMessageImage(msg);
if (msg.id=="music") {
title = msg.artist || /*LANG*/"Music";
body = msg.track;
}
if (img) {
var fg = g.getColor();
g.setColor(getMessageImageCol(msg,fg)).drawImage(img, x+24, r.y+24, {rotate:0}) // force centering
g.setColor(require("messages").getMessageImageCol(msg,fg)).drawImage(img, x+24, r.y+24, {rotate:0}) // force centering
.setColor(fg); // only color the icon
x += 50;
}

View File

@ -104,3 +104,84 @@ exports.clearAll = function(event) {
if (global.WIDGETS && WIDGETS.messages)
WIDGETS.messages.hide();
}
exports.getMessageImage = function(msg) {
/*
* icons should be 24x24px with 1bpp colors and 'Transparency to Color'
* http://www.espruino.com/Image+Converter
*/
if (msg.img) return atob(msg.img);
var s = (msg.src||"").toLowerCase();
if (s=="alarm" || s =="alarmclockreceiver") return atob("GBjBAP////8AAAAAAAACAEAHAOAefng5/5wTgcgHAOAOGHAMGDAYGBgYGBgYGBgYGBgYDhgYBxgMATAOAHAHAOADgcAB/4AAfgAAAAAAAAA=");
if (s=="bibel") return atob("GBgBAAAAA//wD//4D//4H//4H/f4H/f4H+P4H4D4H4D4H/f4H/f4H/f4H/f4H/f4H//4H//4H//4GAAAEAAAEAAACAAAB//4AAAA");
if (s=="calendar") return atob("GBiBAAAAAAAAAAAAAA//8B//+BgAGBgAGBgAGB//+B//+B//+B9m2B//+B//+Btm2B//+B//+Btm+B//+B//+A//8AAAAAAAAAAAAA==");
if (s=="corona-warn") return atob("GBgBAAAAABwAAP+AAf/gA//wB/PwD/PgDzvAHzuAP8EAP8AAPAAAPMAAP8AAH8AAHzsADzuAB/PAB/PgA//wAP/gAH+AAAwAAAAA");
if (s=="discord") return atob("GBgBAAAAAAAAAAAAAIEABwDgDP8wH//4H//4P//8P//8P//8Pjx8fhh+fzz+f//+f//+e//ePH48HwD4AgBAAAAAAAAAAAAAAAAA");
if (s=="facebook" || s=="messenger") return atob("GBiBAAAAAAAAAAAYAAD/AAP/wAf/4A/48A/g8B/g+B/j+B/n+D/n/D8A/B8A+B+B+B/n+A/n8A/n8Afn4APnwADnAAAAAAAAAAAAAA==");
if (s=="google home") return atob("GBiCAAAAAAAAAAAAAAAAAAAAAoAAAAAACqAAAAAAKqwAAAAAqroAAAACquqAAAAKq+qgAAAqr/qoAACqv/6qAAKq//+qgA6r///qsAqr///6sAqv///6sAqv///6sAqv///6sA6v///6sA6v///qsA6qqqqqsA6qqqqqsA6qqqqqsAP7///vwAAAAAAAAAAAAAAAAA==");
if (s=="hangouts") return atob("FBaBAAH4AH/gD/8B//g//8P//H5n58Y+fGPnxj5+d+fmfj//4//8H//B//gH/4A/8AA+AAHAABgAAAA=");
if (s=="home assistant") return atob("FhaBAAAAAADAAAeAAD8AAf4AD/3AfP8D7fwft/D/P8ec572zbzbNsOEhw+AfD8D8P4fw/z/D/P8P8/w/z/AAAAA=");
if (s=="instagram") return atob("GBiBAAAAAAAAAAAAAAAAAAP/wAYAYAwAMAgAkAh+EAjDEAiBEAiBEAiBEAiBEAjDEAh+EAgAEAwAMAYAYAP/wAAAAAAAAAAAAAAAAA==");
if (s=="kalender") return atob("GBgBBgBgBQCgff++RQCiRgBiQAACf//+QAACQAACR//iRJkiRIEiR//iRNsiRIEiRJkiR//iRIEiRIEiR//iQAACQAACf//+AAAA");
if (s=="lieferando") return atob("GBgBABgAAH5wAP9wAf/4A//4B//4D//4H//4P/88fV8+fV4//V4//Vw/HVw4HVw4HBg4HBg4HBg4HDg4Hjw4Hj84Hj44Hj44Hj44");
if (s=="nina") return atob("GBgBAAAABAAQCAAICAAIEAAEEgAkJAgSJBwSKRxKSj4pUn8lVP+VVP+VUgAlSgApKQBKJAASJAASEgAkEAAECAAICAAIBAAQAAAA");
if (s=="outlook mail") return atob("HBwBAAAAAAAAAAAIAAAfwAAP/gAB/+AAP/5/A//v/D/+/8P/7/g+Pv8Dye/gPd74w5znHDnOB8Oc4Pw8nv/Dwe/8Pj7/w//v/D/+/8P/7/gf/gAA/+AAAfwAAACAAAAAAAAAAAA=");
if (s=="phone") return atob("FxeBABgAAPgAAfAAB/AAD+AAH+AAP8AAP4AAfgAA/AAA+AAA+AAA+AAB+AAB+AAB+OAB//AB//gB//gA//AA/8AAf4AAPAA=");
if (s=="post & dhl") return atob("GBgBAPgAE/5wMwZ8NgN8NgP4NgP4HgP4HgPwDwfgD//AB/+AAf8AAAAABs7AHcdgG4MwAAAAGESAFESAEkSAEnyAEkSAFESAGETw");
if (s=="signal") return atob("GBgBAAAAAGwAAQGAAhggCP8QE//AB//oJ//kL//wD//0D//wT//wD//wL//0J//kB//oA//ICf8ABfxgBYBAADoABMAABAAAAAAA");
if (s=="skype") return atob("GhoBB8AAB//AA//+Af//wH//+D///w/8D+P8Afz/DD8/j4/H4fP5/A/+f4B/n/gP5//B+fj8fj4/H8+DB/PwA/x/A/8P///B///gP//4B//8AD/+AAA+AA==");
if (s=="slack") return atob("GBiBAAAAAAAAAABAAAHvAAHvAADvAAAPAB/PMB/veD/veB/mcAAAABzH8B3v+B3v+B3n8AHgAAHuAAHvAAHvAADGAAAAAAAAAAAAAA==");
if (s=="snapchat") return atob("GBgBAAAAAAAAAH4AAf+AAf+AA//AA//AA//AA//AA//AH//4D//wB//gA//AB//gD//wH//4f//+P//8D//wAf+AAH4AAAAAAAAA");
if (s=="teams") return atob("GBgBAAAAAAAAAAQAAB4AAD8IAA8cP/M+f/scf/gIeDgAfvvefvvffvvffvvffvvff/vff/veP/PeAA/cAH/AAD+AAD8AAAQAAAAA");
if (s=="telegram" || s=="telegram foss") return atob("GBiBAAAAAAAAAAAAAAAAAwAAHwAA/wAD/wAf3gD/Pgf+fh/4/v/z/P/H/D8P/Acf/AM//AF/+AF/+AH/+ADz+ADh+ADAcAAAMAAAAA==");
if (s=="threema") return atob("GBjB/4Yx//8AAAAAAAAAAAAAfgAB/4AD/8AH/+AH/+AP//AP2/APw/APw/AHw+AH/+AH/8AH/4AH/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=");
if (s=="to do") return atob("GBgBAAAAAAAAAAAwAAB4AAD8AAH+AAP/DAf/Hg//Px/+f7/8///4///wf//gP//AH/+AD/8AB/4AA/wAAfgAAPAAAGAAAAAAAAAA");
if (s=="twitch") return atob("GBgBH//+P//+P//+eAAGeAAGeAAGeDGGeDOGeDOGeDOGeDOGeDOGeDOGeAAOeAAOeAAcf4/4f5/wf7/gf//Af/+AA/AAA+AAAcAA");
if (s=="twitter") return atob("GhYBAABgAAB+JgA/8cAf/ngH/5+B/8P8f+D///h///4f//+D///g///wD//8B//+AP//gD//wAP/8AB/+AB/+AH//AAf/AAAYAAA");
if (s=="whatsapp") return atob("GBiBAAB+AAP/wAf/4A//8B//+D///H9//n5//nw//vw///x///5///4///8e//+EP3/APn/wPn/+/j///H//+H//8H//4H//wMB+AA==");
if (s=="wordfeud") return atob("GBgCWqqqqqqlf//////9v//////+v/////++v/////++v8///Lu+v8///L++v8///P/+v8v//P/+v9v//P/+v+fx/P/+v+Pk+P/+v/PN+f/+v/POuv/+v/Ofdv/+v/NvM//+v/I/Y//+v/k/k//+v/i/w//+v/7/6//+v//////+v//////+f//////9Wqqqqqql");
if (s=="youtube") return atob("GBgBAAAAAAAAAAAAAAAAAf8AH//4P//4P//8P//8P5/8P4/8f4P8f4P8P4/8P5/8P//8P//8P//4H//4Af8AAAAAAAAAAAAAAAAA");
if (msg.id=="music") return atob("FhaBAH//+/////////////h/+AH/4Af/gB/+H3/7/f/v9/+/3/7+f/vB/w8H+Dwf4PD/x/////////////3//+A=");
// if (s=="sms message" || s=="mail" || s=="gmail") // .. default icon (below)
return atob("HBKBAD///8H///iP//8cf//j4//8f5//j/x/8//j/H//H4//4PB//EYj/44HH/Hw+P4//8fH//44///xH///g////A==");
};
exports.getMessageImageCol = function(msg,def) {
return {
// generic colors, using B2-safe colors
"alarm": "#fff",
"mail": "#ff0",
"music": "#f0f",
"phone": "#0f0",
"sms message": "#0ff",
// brands, according to https://www.schemecolor.com/?s (picking one for multicolored logos)
// all dithered on B2, but we only use the color for the icons. (Could maybe pick the closest 3-bit color for B2?)
"bibel": "#54342c",
"discord": "#738adb",
"facebook": "#4267b2",
"gmail": "#ea4335",
"google home": "#fbbc05",
"hangouts": "#1ba261",
"home assistant": "#fff", // ha-blue is #41bdf5, but that's the background
"instagram": "#dd2a7b",
"liferando": "#ee5c00",
"messenger": "#0078ff",
"nina": "#e57004",
"outlook mail": "#0072c6",
"post & dhl": "#f2c101",
"signal": "#00f",
"skype": "#00aff0",
"slack": "#e51670",
"snapchat": "#ff0",
"teams": "#464eb8",
"telegram": "#0088cc",
"telegram foss": "#0088cc",
"threema": "#000",
"to do": "#3999e5",
"twitch": "#6441A4",
"twitter": "#1da1f2",
"whatsapp": "#4fce5d",
"wordfeud": "#e7d3c7",
"youtube": "#f00",
}[(msg.src||"").toLowerCase()]||(def !== undefined?def:g.theme.fg);
};

View File

@ -1,7 +1,7 @@
{
"id": "messages",
"name": "Messages",
"version": "0.37",
"version": "0.38",
"description": "App to display notifications from iOS and Gadgetbridge/Android",
"icon": "app.png",
"type": "app",

View File

@ -1 +1,2 @@
0.01: Initial version
0.02: Update for time_utils module

View File

@ -73,7 +73,7 @@ function showAlarm(alarm) {
const settings = require("sched").getSettings();
let msg = "";
msg += require("sched").formatTime(alarm.timer);
if (alarm.timer) msg += require("time_utils").formatTime(alarm.timer);
if (alarm.msg) {
msg += "\n"+alarm.msg;
}
@ -86,7 +86,7 @@ function showAlarm(alarm) {
if (alarm.data.hm && alarm.data.hm == true) {
//hard mode extends auto-snooze time
buzzCount = buzzCount * 2;
buzzCount = buzzCount * 3;
startHM();
}

View File

@ -258,7 +258,7 @@ function editTimer(idx, a) {
a.last = 0;
a.data.ot = a.timer;
a.appid = "multitimer";
a.js = "load('multitimer.alarm.js')";
a.js = "(require('Storage').read('multitimer.alarm.js') !== undefined) ? load('multitimer.alarm.js') : load('sched.js')";
if (idx < 0) alarms.push(a);
else alarms[timerIdx[idx]] = a;
require("sched").setAlarms(alarms);
@ -585,7 +585,7 @@ function editAlarm(idx, a) {
var menu = {
"": { "title": "Alarm" },
"< Back": () => {
if (a.data.hm == true) a.js = "load('multitimer.alarm.js')";
if (a.data.hm == true) a.js = "(require('Storage').read('multitimer.alarm.js') !== undefined) ? load('multitimer.alarm.js') : load('sched.js')";
if (a.data.hm == false && a.js) delete a.js;
if (idx >= 0) alarms[alarmIdx[idx]] = a;
else alarms.push(a);

View File

@ -1,7 +1,7 @@
{
"id": "multitimer",
"name": "Multi Timer",
"version": "0.01",
"version": "0.02",
"description": "Set timers and chronographs (stopwatches) and watch them count down in real time. Pause, create, edit, and delete timers and chronos, and add custom labels/messages. Also sets alarms.",
"icon": "app.png",
"screenshots": [

View File

@ -4,3 +4,4 @@
0.04: Fixed issue selecting Frankfurt not saved
0.05: Fixed issue with back option
0.06: renamed source files to match standard
0.07: Move mylocation app into 'Settings -> Apps'

View File

@ -2,6 +2,8 @@
*Sets and stores GPS lat and lon of your preferred city*
To access, go to `Settings -> Apps -> My Location`
* Select one of the preset Cities or setup through the GPS
* Other Apps can read this information to do calculations based on location
* When the City shows ??? it means the location has been set through the GPS

View File

@ -1 +0,0 @@
require("heatshrink").decompress(atob("mEw4UA///gH4AYPO/QPDgNVqtADY/1BYNfBQ0PBQIAB+ALFmoLDrgLF6oLDq4KEgYKDBYPABYcNBYlVuAuIGAwuEAANUBYYKFHgg6Bq4ZCr4DBHgQLBvWq2te1WlBYZGBBYOr1Wq1qSDBYNqBIILDKgQLLgoLHqBqDBfJHLBZBrOgKPCBYiPCU4NaBYe1WYrABBQLCCfgYGCrwVBa4kAirvKNgIAErgLDKgIAEKQQ8EAAY6DBZhIDIww8GHQg8GHQgwGFwowEFwx5EOog8GHQ0AlWpBYNq1AKFWIILBAYOgBYbICytWAgQKCgTgDcwYXGAAgvGAAY8EEgYWGBgoVEA=="))

View File

@ -2,16 +2,15 @@
"name": "My Location",
"shortName":"My Location",
"icon": "app.png",
"type": "app",
"type": "settings",
"screenshots": [{"url":"screenshot_1.png"}],
"version":"0.06",
"version":"0.07",
"description": "Sets and stores the lat and long of your preferred City or it can be set from the GPS. mylocation.json can be used by other apps that need your main location lat and lon. See README",
"readme": "README.md",
"tags": "tool,utility",
"supports": ["BANGLEJS", "BANGLEJS2"],
"storage": [
{"name":"mylocation.app.js","url":"app.js"},
{"name":"mylocation.img","url":"icon.js","evaluate": true }
{"name":"mylocation.settings.js","url":"settings.js"}
],
"data": [
{"name":"mylocation.json"}

View File

@ -1,5 +1,4 @@
Bangle.loadWidgets();
Bangle.drawWidgets();
(function(back) {
const SETTINGS_FILE = "mylocation.json";
let settings;
@ -18,7 +17,7 @@ function loadSettings() {
}
}
function save() {
function saveSettings() {
settings = s;
require('Storage').write(SETTINGS_FILE, settings);
}
@ -34,29 +33,29 @@ function setFromGPS() {
//console.log("fix from GPS");
s = {'lat': gps.lat, 'lon': gps.lon, 'location': '???' };
Bangle.buzz(1500); // buzz on first position
Bangle.setGPSPower(0);
save();
Bangle.setGPSPower(0, "mylocation");
saveSettings();
Bangle.setUI("updown", ()=>{ load(); });
E.showPrompt("Location has been saved from the GPS fix",{
title:"Location Saved",
buttons : {"OK":1}
E.showPrompt(/*LANG*/"Location has been saved from the GPS fix",{
title:/*LANG*/"Location Saved",
buttons : {/*LANG*/"OK":1}
}).then(function(v) {
load(); // load default clock
});
});
Bangle.setGPSPower(1);
E.showMessage("Waiting for GPS fix. Place watch in the open. Could take 10 minutes. Long press to abort", "GPS Running");
Bangle.setGPSPower(1, "mylocation");
E.showMessage(/*LANG*/"Waiting for GPS fix. Place watch in the open. Could take 10 minutes. Long press to abort", "GPS Running");
Bangle.setUI("updown", undefined);
}
function showMainMenu() {
//console.log("showMainMenu");
const mainmenu = {
'': { 'title': 'My Location' },
'< Back': ()=>{ load(); },
'City': {
'': { 'title': /*LANG*/'My Location' },
'< Back': ()=>{ back(); },
/*LANG*/'City': {
value: 0 | locations.indexOf(s.location),
min: 0, max: locations.length - 1,
format: v => locations[v],
@ -65,14 +64,15 @@ function showMainMenu() {
s.location = locations[v];
s.lat = lats[v];
s.lon = lons[v];
save();
saveSettings();
}
}
},
'Set From GPS': ()=>{ setFromGPS(); }
/*LANG*/'Set From GPS': ()=>{ setFromGPS(); }
};
return E.showMenu(mainmenu);
}
loadSettings();
showMainMenu();
})

View File

@ -1 +1,2 @@
0.01: New face :)
0.02: Color image compressed

View File

@ -1,7 +1,17 @@
# About this Watchface
# Poketch Clock
Based on the electronic device from the Pokemon game.
A clock based on the Poketch electronic device found in Sinnoh
# Features
![](https://user-images.githubusercontent.com/44651387/157491789-1b608c11-8af2-4519-a90f-41b8a58a9a14.png)
## Features
Has a dark mode
## Requests
If you have any issues or would like to suggest a feature, click here to send a message -> [here](https://github.com/elykittytee/BangleApps/issues/new?title=Poketch%20Clock%20Bug).
## Creator
Eleanor Tayam

File diff suppressed because one or more lines are too long

View File

@ -2,7 +2,7 @@
"id": "pokeclk",
"name": "Poketch Clock",
"shortName":"Poketch Clock",
"version": "0.01",
"version": "0.02",
"description": "A clock based on the Poketch electronic device found in Sinnoh",
"icon": "app.png",
"type": "clock",

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

1
apps/r2d2clk/ChangeLog Normal file
View File

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

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

@ -0,0 +1,11 @@
# R2D2 Clock
A clock with R2D2's shiny metal face on it. :)
![](screenshot.png)
## Creator
Made by [Noah Howard](https://github.com/nh-99)
Based on [Interlaced Clock](https://github.com/espruino/BangleApps/tree/master/apps/intclock)

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

@ -0,0 +1 @@
require("heatshrink").decompress(atob("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"))

67
apps/r2d2clk/app.js Normal file
View File

@ -0,0 +1,67 @@
Graphics.prototype.setFontUndo = function(scale) {
// Actual height 19 (20 - 2)
this.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKqKAqooCqigKqKAAAACgAAKAAAoAACgAAAAAAAAACgAAKAAAoAACgAAAAAAKCgAoKAKqqAqqoCqqgKqqAKCgAoKAKqqAqqoCqqgKqqAKCgAoKAAAAAKgoAqCgKqKAqooKiioqKKiooqKiioKKqAoqoCgqAKCoAAAACoCgKgKAiCoCIKgKioAqKgACoAAKgACoAAKgACoqAKioCoIgKgiAoCoCgKgAAAAKqgAqqAKqqAqqoKiioqKKiooqKiioKAKAoAoCgCgKAKAAAACgAAKAAAoAACgAAAAAAKqgAqqAKqqAqqoCgCgKAKAAAACgCgKAKAqqoCqqgCqoAKqgAAAACigAKKAAqoACqgAqoACqgAKqAAqoAAqoACqgAKKAAooAAAAAAoAACgAAKAAAoAAqqACqoAKqgAqqAAKAAAoAACgAAKAAAAAAACoAAKgAAqAACoAAAAAoAACgAAKAAAoAACgAAKAAAoAACgAAKAAAoAACgAAKAAAAAAACgAAKAAAoAACgAAAAAAoAACgAAqAACoAAqAACoAAqAACoAAqAACoAAqAACoAAqAACoAAKAAAoAAAAAACqoAKqgCqqgKqqAoAoCgCgKAKAoAoCqqgKqqAKqgAqqAAAAAqqoCqqgKqqAqqoAAAAKCqAoKoCiqgKKqAoooCiigKKKAoooCqigKqKAKgoAqCgAAAAoAoCgCgKAKAoAoCiigKKKAoooCiigKqqAqqoAqqACqoAAAACqAAKoAAqoACqgAAKAAAoAACgAAKAAqqoCqqgKqqAqqoAAAAKoKAqgoCqigKqKAoooCiigKKKAoooCiqgKKqAoKgCgqAAAAAKqgAqqAKqqAqqoCiigKKKAoooCiigKKqAoqoCgqAKCoAAAACgAAKAAAoAACgAAKAAAoAACgAAKAAAqqoCqqgCqqAKqoAAAACqoAKqgCqqgKqqAoooCiigKKKAoooCqqgKqqAKqgAqqAAAAAKgAAqAAKqAAqoACigAKKAAooACigAKqqAqqoAqqgCqqAAAACgCgKAKAoAoCgCgAAAAoAqCgCoKAKgoAqAAAAAKAAAoAAKoAAqgAKqgAqqAKgqAqCoCgCgKAKAAAAAoKACgoAKCgAoKACgoAKCgAoKACgoAKCgAoKACgoAKCgAAAAKAKAoAoCoKgKgqAKqgAqqAAqgACqAACgAAKAAAAACgAAKAAAoAACgAAKKKAoooCiigKKKAqoACqgACoAAKgAAAAACqqAKqoCqqgKqqAoAACgAAKCoAoKgCiqgKKqAoooCiigKqqAqqoAqqACqoAAAAAqqgCqqAqqoCqqgKKAAooACigAKKAAqqoCqqgCqqAKqoAAAAKqqAqqoCqqgKqqAoooCiigKKKAoooCqqgKqqAKCgAoKAAAAAKqgAqqAKqqAqqoCgCgKAKAoAoCgCgKAKAoAoCgCgKAKAAAACqqgKqqAqqoCqqgKAKAoAoCoKgKgqAKqgAqqAAqgACqAAAAACqoAKqgCqqgKqqAoooCiigKKKAoooCgCgKAKAoAoCgCgAAAAKqoAqqgKqqAqqoCigAKKAAooACigAKAAAoAACgAAKAAAAAAAqqACqoAqqoCqqgKAKAoAoCiigKKKAoqoCiqgKKqAoqoAAAAKqqAqqoCqqgKqqAAoAACgAAKAAAoACqqgKqqAqqoCqqgAAAAoAoCgCgKAKAoAoCqqgKqqAqqoCqqgKAKAoAoCgCgKAKAAAAAAKAAAoAACoAAKgAAKAAAoAACgAAKAqqoCqqgKqoAqqgAAAAKqqAqqoCqqgKqqACqAAKoACqoAKqgCoKgKgqAoAoCgCgAAAAqqgCqqAKqqAqqoAACgAAKAAAoAACgAAKAAAoAACgAAKAAAACqqgKqqAqqoCqqgCoAAKgAAKgAAqAAKgAAqAAKqqAqqoCqqgKqqAAAACqqgKqqAqqoCqqgCoAAKgAAKgAAqAAqqoCqqgKqqAqqoAAAACqoAKqgCqqgKqqAoAoCgCgKAKAoAoCqqgKqqAKqgAqqAAAAAqqoCqqgKqqAqqoCigAKKAAooACigAKqAAqoAAqAACoAAAAAAqqACqoAqqoCqqgKAKAoAoCgKgKAqAqqoCqqgCqqAKqoAAAAKqqAqqoCqqgKqqAooACigAKKgAoqACqqgKqqAKioAqKgAAAAKgoAqCgKqKAqooCiigKKKAoooCiigKKqAoqoCgqAKCoAAAACgAAKAAAoAACgAAKqqAqqoCqqgKqqAoAACgAAKAAAoAAAAAAKqoAqqgCqqgKqqAAAoAACgAAKAAAoCqqgKqqAqqgCqqAAAAAqqACqoAKqoAqqgAAKgAAqAACoAAKgKqoAqqgCqoAKqgAAAACqqgKqqAqqoCqqgACoAAKgACoAAKgAAKgAAqAKqqAqqoCqqgKqqAAAACoKgKgqAqqoCqqgAqgACqAAKoAAqgAqqoCqqgKgqAqCoAAAAKoAAqgACqgAKqAAAqoACqgAKqAAqoCqgAKqAAqgACqAAAAAAoCoCgKgKCqAoKoCiqgKKqAqooCqigKoKAqgoCoCgKgKAAAACqqgKqqAqqoCqqgKAKAoAoAAAAKAAAoAACoAAKgAAKgAAqAAAqAACoAACoAAKgAAKgAAqAAAqAACoAACgAAKAAAACgCgKAKAqqoCqqgKqqAqqoAAA"), 32, atob("DQULDw0RDQUHBw0NBQ0FEQ0FDQ0NDQ0NDQ0FBQsNCw0RDQ0NDQ0NDQ0NDQ0NDw0NDQ0NDQ0NDQ8NDQ0HEQc="), 22+(scale<<8)+(1<<16));
return this;
};
var IMAGEWIDTH = 117;
var IMAGEHEIGHT = 60;
var r2d2 = require("heatshrink").decompress(atob("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"));
// 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));
}
function draw() {
//reset().clearRect(0,24,g.getWidth(),g.getHeight()-IMAGEHEIGHT);
// draw r2d2
g.drawImage(r2d2, (g.getWidth()/2)-(IMAGEWIDTH/2), g.getHeight()-IMAGEHEIGHT);
var x = g.getWidth()/2;
var y = g.getHeight()/2 - 30;
g.reset();
// work out locale-friendly date/time
var date = new Date();
var timeStr = require("locale").time(date,1).trim();
var dateStr = require("locale").date(date).toUpperCase();
// draw time
g.setFontAlign(0,0).setFont("Undo:3");
g.clearRect(0,y-30,g.getWidth(),y+30); // clear the background
g.drawString(timeStr,x,y);
// draw date
y += 40;
g.setFontAlign(0,0).setFont("Undo");
g.clearRect(0,y-10,g.getWidth(),y+20); // clear the background
g.drawString(dateStr,x,y);
// queue draw in one minute
queueDraw();
}
// Clear the screen once, at startup
g.clear();
// draw immediately at first, queue update
draw();
// Stop updates when LCD is off, restart when on
Bangle.on('lcdPower',on=>{
if (on) {
draw(); // draw immediately, queue redraw
} else { // stop draw timer
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
}
});
// Show launcher when middle button pressed
Bangle.setUI("clock");
// Load widgets
Bangle.loadWidgets();
Bangle.drawWidgets();

BIN
apps/r2d2clk/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -0,0 +1,16 @@
{ "id": "r2d2clk",
"name": "R2D2 Clock",
"shortName":"R2D2 Clock",
"version":"0.01",
"description": "A clock with R2D2's shiny metal face on it. :)",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
"type": "clock",
"tags": "clock",
"supports" : ["BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name":"r2d2clk.app.js","url":"app.js"},
{"name":"r2d2clk.img","url":"app-icon.js","evaluate":true}
]
}

BIN
apps/r2d2clk/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -4,4 +4,5 @@
0.04: Fixed icon and png to 48x48 pixels
0.05: added charging icon
0.06: Add 12h support and autocycle control
0.07: added localization; removed deprecated code
0.07: added localization, removed deprecated code
0.08: removed unused font, fix autocycle, imported suncalc and trimmed, removed pedometer dependency, "tap to cycle" setting

BIN
apps/rebble/KdamThmor.ttf Normal file

Binary file not shown.

View File

@ -2,11 +2,11 @@
"id": "rebble",
"name": "Rebble Clock",
"shortName": "Rebble",
"version": "0.07",
"version": "0.08",
"description": "A Pebble style clock, with configurable background, three sidebars including steps, day, date, sunrise, sunset, long live the rebellion",
"readme": "README.md",
"icon": "rebble.png",
"dependencies": {"mylocation":"app", "widpedom":"app"},
"dependencies": {"mylocation":"app"},
"screenshots": [{"url":"screenshot_rebble.png"}],
"type": "clock",
"tags": "clock",
@ -14,6 +14,7 @@
"storage": [
{"name":"rebble.app.js","url":"rebble.app.js"},
{"name":"rebble.settings.js","url":"rebble.settings.js"},
{"name":"rebble.img","url":"rebble.icon.js","evaluate":true}
{"name":"rebble.img","url":"rebble.icon.js","evaluate":true},
{"name":"suncalc","url":"suncalc.js"}
]
}

File diff suppressed because one or more lines are too long

View File

@ -2,12 +2,14 @@
const SETTINGS_FILE = "rebble.json";
// initialize with default settings...
let localSettings = {'bg': '#0f0', 'color': 'Green', 'autoCycle': true}
let localSettings = {'bg': '#0f0', 'color': 'Green', 'autoCycle': true, 'sideTap':0};
//sideTap 0 = on| 1= sideBar1 | 2 = ...
// ...and overwrite them with any saved values
// This way saved values are preserved if a new version adds more settings
const storage = require('Storage')
let settings = storage.readJSON(SETTINGS_FILE, 1) || localSettings;
const saved = settings || {}
for (const key in saved) {
localSettings[key] = saved[key]
@ -21,26 +23,55 @@
var color_options = ['Green','Orange','Cyan','Purple','Red','Blue'];
var bg_code = ['#0f0','#ff0','#0ff','#f0f','#f00','#00f'];
E.showMenu({
'': { 'title': 'Rebble Clock' },
'< Back': back,
'Colour': {
value: 0 | color_options.indexOf(localSettings.color),
min: 0, max: 5,
format: v => color_options[v],
onchange: v => {
localSettings.color = color_options[v];
localSettings.bg = bg_code[v];
save();
function showMenu()
{
const menu={
'': { 'title': 'Rebble Clock' },
'< Back': back,
'Colour': {
value: 0 | color_options.indexOf(localSettings.color),
min: 0, max: 5,
format: v => color_options[v],
onchange: v => {
localSettings.color = color_options[v];
localSettings.bg = bg_code[v];
save();
},
},
},
'Auto Cycle': {
value: "autoCycle" in localSettings ? localSettings.autoCycle : true,
format: () => (localSettings.autoCycle ? 'Yes' : 'No'),
onchange: () => {
localSettings.autoCycle = !localSettings.autoCycle;
save();
'Auto Cycle': {
value: localSettings.autoCycle,
onchange: (v) => {
localSettings.autoCycle = v;
save();
showMenu();
}
}
};
if( !localSettings.autoCycle)
{
menu['Tap to Cycle']= {
value: localSettings.sideTap,
min: 0,
max: 3,
step: 1,
format: v => NumberToSideTap(v),
onchange: v => {
localSettings.sideTap=v
save();
setTimeout(showMenu, 10);
}
};
}
});
E.showMenu(menu);
}
function NumberToSideTap(Number)
{
if(Number==0)
return 'on';
return Number+"";
}
showMenu();
})

143
apps/rebble/suncalc.js Normal file
View File

@ -0,0 +1,143 @@
/*
(c) 2011-2015, Vladimir Agafonkin
SunCalc is a JavaScript library for calculating sun/moon position and light phases.
https://github.com/mourner/suncalc
edit for banglejs
*/
(function () { 'use strict';
// shortcuts for easier to read formulas
var PI = Math.PI,
sin = Math.sin,
cos = Math.cos,
tan = Math.tan,
asin = Math.asin,
atan = Math.atan2,
acos = Math.acos,
rad = PI / 180;
// sun calculations are based on http://aa.quae.nl/en/reken/zonpositie.html formulas
// date/time constants and conversions
var dayMs = 1000 * 60 * 60 * 24,
J1970 = 2440588,
J2000 = 2451545;
function toJulian(date) { return date.valueOf() / dayMs - 0.5 + J1970; }
function fromJulian(j) { return new Date((j + 0.5 - J1970) * dayMs); }
function toDays(date) { return toJulian(date) - J2000; }
// general calculations for position
var e = rad * 23.4397; // obliquity of the Earth
function declination(l, b) { return asin(sin(b) * cos(e) + cos(b) * sin(e) * sin(l)); }
// general sun calculations
function solarMeanAnomaly(d) { return rad * (357.5291 + 0.98560028 * d); }
function eclipticLongitude(M) {
var C = rad * (1.9148 * sin(M) + 0.02 * sin(2 * M) + 0.0003 * sin(3 * M)), // equation of center
P = rad * 102.9372; // perihelion of the Earth
return M + C + P + PI;
}
var SunCalc = {};
// sun times configuration (angle, morning name, evening name)
var times = SunCalc.times = [
[-0.833, 'sunrise', 'sunset' ],
[ -0.3, 'sunriseEnd', 'sunsetStart' ],
[ -6, 'dawn', 'dusk' ],
[ -12, 'nauticalDawn', 'nauticalDusk'],
[ -18, 'nightEnd', 'night' ],
[ 6, 'goldenHourEnd', 'goldenHour' ]
];
// calculations for sun times
var J0 = 0.0009;
function julianCycle(d, lw) { return Math.round(d - J0 - lw / (2 * PI)); }
function approxTransit(Ht, lw, n) { return J0 + (Ht + lw) / (2 * PI) + n; }
function solarTransitJ(ds, M, L) { return J2000 + ds + 0.0053 * sin(M) - 0.0069 * sin(2 * L); }
function hourAngle(h, phi, d) { return acos((sin(h) - sin(phi) * sin(d)) / (cos(phi) * cos(d))); }
function observerAngle(height) { return -2.076 * Math.sqrt(height) / 60; }
// returns set time for the given sun altitude
function getSetJ(h, lw, phi, dec, n, M, L) {
var w = hourAngle(h, phi, dec),
a = approxTransit(w, lw, n);
return solarTransitJ(a, M, L);
}
// calculates sun times for a given date, latitude/longitude, and, optionally,
// the observer height (in meters) relative to the horizon
SunCalc.getTimes = function (date, lat, lng, height) {
height = height || 0;
var lw = rad * -lng,
phi = rad * lat,
dh = observerAngle(height),
d = toDays(date),
n = julianCycle(d, lw),
ds = approxTransit(0, lw, n),
M = solarMeanAnomaly(ds),
L = eclipticLongitude(M),
dec = declination(L, 0),
Jnoon = solarTransitJ(ds, M, L),
i, len, time, h0, Jset, Jrise;
var result = {
solarNoon: fromJulian(Jnoon),
nadir: fromJulian(Jnoon - 0.5)
};
for (i = 0, len = times.length; i < len; i += 1) {
time = times[i];
h0 = (time[0] + dh) * rad;
Jset = getSetJ(h0, lw, phi, dec, n, M, L);
Jrise = Jnoon - (Jset - Jnoon);
result[time[1]] = fromJulian(Jrise);
result[time[2]] = fromJulian(Jset);
}
return result;
};
// export as Node module / AMD module / browser variable
if (typeof exports === 'object' && typeof module !== 'undefined') module.exports = SunCalc;
else if (typeof define === 'function' && define.amd) define(SunCalc);
else window.SunCalc = SunCalc;
}());

View File

@ -6,7 +6,7 @@
"icon": "rndmclk.png",
"type": "widget",
"tags": "widget,clock",
"supports": ["BANGLEJS"],
"supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name":"rndmclk.wid.js","url":"widget.js"}

View File

@ -12,3 +12,4 @@
0.11: Notifications fixes
0.12: Fix for recorder not stopping at end of run. Bug introduced in 0.11
0.13: Revert #1578 (stop duplicate entries) as with 2v12 menus it causes other boxes to be wiped (fix #1643)
0.14: Fix Bangle.js 1 issue where after the 'overwrite track' menu, the start/stop button stopped working

View File

@ -55,6 +55,7 @@ function onStartStop() {
prepPromises.push(
WIDGETS["recorder"].setRecording(true).then(() => {
isMenuDisplayed = false;
layout.setUI(); // grab our input handling again
layout.forgetLazyState();
layout.render();
})

View File

@ -1,6 +1,6 @@
{ "id": "run",
"name": "Run",
"version":"0.13",
"version":"0.14",
"description": "Displays distance, time, steps, cadence, pace and more for runners.",
"icon": "app.png",
"tags": "run,running,fitness,outdoors,gps",

View File

@ -6,6 +6,7 @@
0.06: Refactor some methods to library
0.07: Update settings
Correct `decodeTime(t)` to return a more likely expected time
0.08: add day of week check to getActiveAlarms()
0.08: Add day of week check to getActiveAlarms()
0.09: Move some functions to new time_utils module
0.10: Default to sched.js if custom js not found
0.11: Fix default dow

View File

@ -8,12 +8,12 @@
var time = new Date();
var currentTime = (time.getHours()*3600000)+(time.getMinutes()*60000)+(time.getSeconds()*1000);
var d = time.getDate();
var active = alarms.filter(
a=>a.on && // enabled
a.last!=d && // not already fired today
a.t+60000>currentTime && // is not in the past by >1 minute
(a.dow>>time.getDay())&1 && // is allowed on this day of the week
(!a.date || a.date==time.toISOString().substr(0,10)) // is allowed on this date
var active = alarms.filter(a =>
a.on // enabled
&& (a.last != d) // not already fired today
&& (a.t + 60000 > currentTime) // is not in the past by >1 minute
&& (a.dow >> time.getDay() & 1) // is allowed on this day of the week
&& (!a.date || a.date == time.toISOString().substr(0, 10)) // is allowed on this date
);
if (active.length) {
active = active.sort((a,b)=>a.t-b.t); // sort by time
@ -24,7 +24,7 @@
will then clearInterval() to get rid of this call so it can proceed
normally.
If active[0].js is defined, just run that code as-is and not alarm.js */
Bangle.SCHED = setTimeout(require("Storage").read(active[0].js)!==undefined ? active[0].js : 'load("sched.js")',t);
Bangle.SCHED = setTimeout(active[0].js||'load("sched.js")',t);
} else { // check for new alarms at midnight (so day of week works)
Bangle.SCHED = setTimeout('eval(require("Storage").read("sched.boot.js"))', 86400000 - (Date.now()%86400000));
}

View File

@ -11,16 +11,19 @@ exports.getAlarm = function(id) {
return exports.getAlarms().find(a=>a.id==id);
};
// Given a list of alarms from getAlarms, return a list of active alarms for the given time (or current time if time not specified)
exports.getActiveAlarms = function(alarms, time) {
exports.getActiveAlarms = function (alarms, time) {
if (!time) time = new Date();
var currentTime = (time.getHours()*3600000)+(time.getMinutes()*60000)+(time.getSeconds()*1000)
+10000;// get current time - 10s in future to ensure we alarm if we've started the app a tad early
return alarms.filter(a=>a.on
&&(a.t<currentTime)
&&(a.last!=time.getDate())
&& (!a.date || a.date==time.toISOString().substr(0,10))
&& a.dow >> (time).getDay() & 1)
.sort((a,b)=>a.t-b.t);
// get current time 10s in future to ensure we alarm if we've started the app a tad early
var currentTime = (time.getHours() * 3600000) + (time.getMinutes() * 60000) + (time.getSeconds() * 1000) + 10000;
return alarms
.filter(a =>
a.on // enabled
&& (a.last != time.getDate()) // not already fired today
&& (a.t < currentTime)
&& (a.dow >> time.getDay() & 1) // is allowed on this day of the week
&& (!a.date || a.date == time.toISOString().substr(0, 10)) // is allowed on this date
)
.sort((a, b) => a.t - b.t);
}
// Set an alarm object based on ID. Leave 'alarm' undefined to remove it
exports.setAlarm = function(id, alarm) {
@ -66,7 +69,7 @@ exports.newDefaultAlarm = function () {
on: true,
rp: settings.defaultRepeat,
as: settings.defaultAutoSnooze,
dow: settings.defaultRepeat ? 0b1111111 : 0b0000000,
dow: 0b1111111,
last: 0,
vibrate: settings.defaultAlarmPattern,
};

View File

@ -1,7 +1,7 @@
{
"id": "sched",
"name": "Scheduler",
"version": "0.10",
"version": "0.11",
"description": "Scheduling library for alarms and timers",
"icon": "app.png",
"type": "scheduler",

View File

@ -48,3 +48,5 @@
0.43: Add some Bangle 1 colours to theme customizer
0.44: Add "Start Week On X" option (#1780)
UI improvements to Locale and Date & Time menu
0.45: Add calibrate battery option
0.46: Fix regression after making 'calibrate battery' only for Bangle.js 2

View File

@ -13,7 +13,6 @@ This is Bangle.js's settings menu
* **LCD** Configure settings about the screen. How long it stays on, how bright it is, and when it turns on - see below.
* **Theme** Adjust the colour scheme
* **Utils** Utilities - including resetting settings (see below)
* **Turn Off** Turn Bangle.js off
## BLE - Bluetooth Settings
@ -61,5 +60,7 @@ The exact effects depend on the app. In general the watch will not wake up by i
* **Compact Storage** Removes deleted/old files from Storage - this will speed up your Bangle.js
* **Rewrite Settings** Should not normally be required, but if `.boot0` has been deleted/corrupted (and so no settings are being loaded) this will fix it.
* **Flatten Battery** Turns on all devices and draws as much power as possible, attempting to flatten the Bangle.js battery. This can still take 5+ hours.
* **Calibrate Battery** If you're finding your battery percentage meter isn't accurate, leave your Bangle.js on charge for at least 3 hours, and then choose this menu option. It will measure the battery voltage when full and will allow Bangle.js to report a more accurate battery percentage.
* **Reset Settings** Reset the settings (as set in this app) to defaults. Does not reset settings for other apps.
* **Factory Reset** (not available on Bangle.js 1) - wipe **everything** and return to a factory state
* **Turn Off** Turn Bangle.js off

View File

@ -1,7 +1,7 @@
{
"id": "setting",
"name": "Settings",
"version": "0.44",
"version": "0.46",
"description": "A menu for setting up Bangle.js",
"icon": "settings.png",
"tags": "tool,system",

View File

@ -545,8 +545,22 @@ function showUtilMenu() {
setInterval(function() {
var i=1000;while (i--);
}, 1);
},
/*LANG*/'Reset Settings': () => {
}
};
if (BANGLEJS2)
menu[/*LANG*/'Calibrate Battery'] = () => {
E.showPrompt(/*LANG*/"Is the battery fully charged?",{title:/*LANG*/"Calibrate"}).then(ok => {
if (ok) {
var s=require("Storage").readJSON("setting.json");
s.batFullVoltage = (analogRead(D3)+analogRead(D3)+analogRead(D3)+analogRead(D3))/4;
require("Storage").writeJSON("setting.json",s);
E.showAlert(/*LANG*/"Calibrated!").then(() => load("settings.app.js"));
} else {
E.showAlert(/*LANG*/"Please charge Bangle.js for 3 hours and try again").then(() => load("settings.app.js"));
}
});
};
menu[/*LANG*/'Reset Settings'] = () => {
E.showPrompt(/*LANG*/'Reset to Defaults?',{title:/*LANG*/"Settings"}).then((v) => {
if (v) {
E.showMessage('Resetting');
@ -554,9 +568,9 @@ function showUtilMenu() {
setTimeout(showMainMenu, 50);
} else showUtilMenu();
});
},
/*LANG*/'Turn Off': ()=>{ if (Bangle.softOff) Bangle.softOff(); else Bangle.off() }
};
};
menu[/*LANG*/'Turn Off'] = ()=>{ if (Bangle.softOff) Bangle.softOff(); else Bangle.off() };
if (Bangle.factoryReset) {
menu[/*LANG*/'Factory Reset'] = ()=>{
E.showPrompt(/*LANG*/'This will remove everything!',{title:/*LANG*/"Factory Reset"}).then((v) => {

View File

@ -13,3 +13,4 @@
1.14: Add VMG and coordinates screens
1.43: Adds mirroring of the watch face to an Android device. See README.md
1.49: Droidscript mirroring prog automatically uses last connection address. Auto connects when run.
1.50: Add configuration item Wpt File Suffix. A one character suffix to append to the waypoints.json file. A number of other apps also use this file name. Using the file name suffix allows the speedalt2 waypoints to be retained if one of these other apps is installed for a different use.

View File

@ -78,6 +78,10 @@ Waypoints are used in Distance and VMG modes. Create a file waypoints.json and w
The [GPS Navigation](https://banglejs.com/apps/#gps%20navigation) app in the App Loader has a really nice waypoints file editor. (Must be connected to your Bangle.JS and then click on the Download icon.)
By default the waypoints file is called waypoints.json
**Note** : The waypoints.json file is used by a number of different gps apps. The setting 'Wpt File Suffix' allows one of waypoints1.json, waypoints2.json or waypoints3.json to be used instead. This allows the other apps to be used with a different set of waypoints without losing the speedalt2 waypoint set.
Sample waypoints.json (My sailing waypoints)
<pre>

View File

@ -5,8 +5,9 @@ Mike Bennett mike[at]kereru.com
1.14 : Add VMG screen
1.34 : Add bluetooth data stream for Droidscript
1.43 : Keep GPS in SuperE mode while using Droiscript screen mirroring
1.50 : Add cfg.wptSfx one char suffix to append to waypoints.json filename. Protects speedalt2 waypoints from other apps that use the same file name for waypoints.
*/
var v = '1.49';
var v = '1.50';
var vDroid = '1.50'; // Required DroidScript program version
/*kalmanjs, Wouter Bulten, MIT, https://github.com/wouterbulten/kalmanjs */
@ -209,7 +210,7 @@ function nxtWp(){
}
function loadWp() {
var w = require("Storage").readJSON('waypoints.json')||[{name:"NONE"}];
var w = require("Storage").readJSON('waypoints'+cfg.wptSfx+'.json')||[{name:"NONE"}];
if (cfg.wp>=w.length) cfg.wp=0;
if (cfg.wp<0) cfg.wp = w.length-1;
savSettings();
@ -718,6 +719,7 @@ cfg.primSpd = cfg.primSpd||0; // 1 = Spd in primary, 0 = Spd in secondary
cfg.spdFilt = cfg.spdFilt==undefined?true:cfg.spdFilt;
cfg.altFilt = cfg.altFilt==undefined?true:cfg.altFilt;
cfg.touch = cfg.touch==undefined?true:cfg.touch;
cfg.wptSfx = cfg.wptSfx==undefined?'':cfg.wptSfx;
if ( cfg.spdFilt ) var spdFilter = new KalmanFilter({R: 0.1 , Q: 1 });
if ( cfg.altFilt ) var altFilter = new KalmanFilter({R: 0.01, Q: 2 });

View File

@ -2,7 +2,7 @@
"id": "speedalt2",
"name": "GPS Adventure Sports II",
"shortName":"GPS Adv Sport II",
"version":"1.49",
"version":"1.50",
"description": "GPS speed, altitude and distance to waypoint display. Designed for easy viewing and use during outdoor activities such as para-gliding, hang-gliding, sailing, cycling etc.",
"icon": "app.png",
"type": "app",
@ -15,5 +15,11 @@
{"name":"speedalt2.img","url":"app-icon.js","evaluate":true},
{"name":"speedalt2.settings.js","url":"settings.js"}
],
"data": [{"name":"speedalt2.json"}]
"data": [
{"name":"speedalt2.json"},
{"name":"waypoints.json"},
{"name":"waypoints1.json"},
{"name":"waypoints2.json"},
{"name":"waypoints3.json"}
]
}

View File

@ -30,6 +30,11 @@
writeSettings();
}
function setSfx(s) {
settings.wptSfx = s;
writeSettings();
}
const appMenu = {
'': {'title': 'GPS Adv Sprt II'},
@ -38,6 +43,7 @@
'Units' : function() { E.showMenu(unitsMenu); },
'Colours' : function() { E.showMenu(colMenu); },
'Kalman Filter' : function() { E.showMenu(kalMenu); },
'Wpt File Suffix' : function() { E.showMenu(sfxMenu); },
'Touch' : {
value : settings.touch,
format : v => v?"On":"Off",
@ -69,6 +75,15 @@
'Inverted' : function() { setColour(3); }
};
const sfxMenu = {
'': {'title': 'Wpt File Suffix'},
'< Back': function() { E.showMenu(appMenu); },
'Default' : function() { setSfx(''); },
'1' : function() { setSfx('1'); },
'2' : function() { setSfx('2'); },
'3' : function() { setSfx('3'); }
};
const kalMenu = {
'': {'title': 'Kalman Filter'},
'< Back': function() { E.showMenu(appMenu); },

View File

@ -3,3 +3,4 @@
0.03: Add Banglejs 1 compatibility
0.04: Fix settings bug
0.05: Add altitude display (only Bangle.js 2)
0.06: Add power related settings to control the HR and pressure(altitude) sensor from the watchface

View File

@ -8,3 +8,8 @@ It can display :
- hrm
- motion
- steps
"Power saving" setting control the HR and pressure (altitude) sensors.
If "Off" they will always be on.
If "On" the sensors will be turned on every "Power on interval" minutes for 45 secondes

View File

@ -3,15 +3,14 @@ var fontColor = g.theme.dark ? "#0f0" : "#000";
var heartRate = 0;
var altitude = -9001;
// handling the differents versions of the Banglejs smartwatch
// handling the differents versions of the Banglejs smartwatch screen sizes
if (process.env.HWVERSION == 1){
var paddingY = 3;
var font6x8At4Size = 48;
var font6x8At2Size = 27;
var font6x8FirstTextSize = 6;
var font6x8DefaultTextSize = 3;
}
else{
} else{
var paddingY = 2;
var font6x8At4Size = 32;
var font6x8At2Size = 18;
@ -66,7 +65,7 @@ function drawDate(now, pos){
drawLine(locale_date, pos);
}
function drawInput(now, pos){
function drawInput(pos){
clearField(pos);
drawLine(">", pos);
}
@ -129,16 +128,52 @@ function draw(){
drawStepCount(curPos);
curPos++;
}
drawInput(now, curPos);
drawInput(curPos);
}
function turnOnServices(){
if(settings.showHRM){
Bangle.setHRMPower(true, "terminalclock");
}
if(settings.showAltitude && process.env.HWVERSION != 1){
Bangle.setBarometerPower(true, "terminalclock");
}
if(settings.powerSaving){
setTimeout(function () {
turnOffServices();
}, 45000);
}
}
function turnOffServices(){
if(settings.showHRM){
Bangle.setHRMPower(false, "terminalclock");
}
if(settings.showAltitude && process.env.HWVERSION != 1){
Bangle.setBarometerPower(false, "terminalclock");
}
}
var unlockDrawIntervalID = -1;
Bangle.on('lock', function(on){
if(!on){ // unclock
if(settings.powerSaving){
turnOnServices();
}
unlockDrawIntervalID = setInterval(draw, 1000); // every second
}
if(on && unlockDrawIntervalID != -1){ // lock
clearInterval(unlockDrawIntervalID);
}
});
Bangle.on('HRM',function(hrmInfo) {
if(hrmInfo.confidence >= settings.HRMinConfidence)
heartRate = hrmInfo.bpm;
});
var MEDIANLENGTH = 20;
var avr = [], median;
var MEDIANLENGTH = 20; // technical
var avr = [], median; // technical
Bangle.on('pressure', function(e) {
while (avr.length>MEDIANLENGTH) avr.pop();
avr.unshift(e.altitude);
@ -161,18 +196,20 @@ var settings = Object.assign({
showActivity: true,
showStepCount: true,
showAltitude: process.env.HWVERSION != 1 ? true : false,
powerSaving: true,
PowerOnInterval: 15,
}, require('Storage').readJSON("terminalclock.json", true) || {});
if(settings.showAltitude && process.env.HWVERSION != 1){
Bangle.setBarometerPower(true, "app");
// turn the services before drawing anything
turnOnServices();
if(settings.powerSaving){
setInterval(turnOnServices, settings.PowerOnInterval*60000); // every PowerOnInterval min
}
// Show launcher when middle button pressed
Bangle.setUI("clock");
// Load widgets
// Load and draw widgets
Bangle.loadWidgets();
Bangle.drawWidgets();
// draw immediately at first
draw();
var secondInterval = setInterval(draw, 10000);
setInterval(draw, 10000); // every 10 seconds

View File

@ -3,7 +3,7 @@
"name": "Terminal Clock",
"shortName":"Terminal Clock",
"description": "A terminal cli like clock displaying multiple sensor data",
"version":"0.05",
"version":"0.06",
"icon": "app.png",
"type": "clock",
"tags": "clock",

View File

@ -8,6 +8,8 @@
showHRM: true,
showActivity: true,
showStepCount: true,
powerSaving: true,
PowerOnInterval: 15,
}, require('Storage').readJSON(FILE, true) || {});
function writeSettings() {
@ -65,6 +67,25 @@
settings.showStepCount = v;
writeSettings();
}
},
'Power saving': {
value: settings.powerSaving,
format: v => v?"On":"Off",
onchange: v => {
settings.powerSaving = v;
writeSettings();
}
},
'Power on interval': {
value: settings.PowerOnInterval,
min: 3, max: 60,
onchange: v => {
settings.PowerOnInterval = v;
writeSettings();
},
format: x => {
return x + " min";
}
}
}
if (process.env.HWVERSION == 1) {

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