Merge branch 'master' of github.com:nh-99/BangleApps
commit
d7f8272337
|
|
@ -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> <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>
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
require('Storage').readJSON("bthrm.default.json", true) || {},
|
||||
require('Storage').readJSON("bthrm.json", true) || {}
|
||||
);
|
||||
|
||||
|
||||
var log = function(text, param){
|
||||
if (settings.debuglog){
|
||||
var logline = new Date().toISOString() + " - " + text;
|
||||
|
|
@ -13,39 +13,38 @@
|
|||
print(logline);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
log("Settings: ", settings);
|
||||
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
function writeCache(cache){
|
||||
characteristic.on('characteristicvaluechanged', (ev) => supportedCharacteristics[characteristic.uuid].handler(ev.target.value));
|
||||
};
|
||||
|
||||
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,38 +80,34 @@
|
|||
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
|
||||
|
||||
|
||||
var sensorContact;
|
||||
|
||||
|
||||
if (flags & 2){
|
||||
sensorContact = (flags & 4) ? true : false;
|
||||
sensorContact = !!(flags & 4);
|
||||
}
|
||||
|
||||
|
||||
var idx = 2 + (flags&1);
|
||||
|
||||
|
||||
var energyExpended;
|
||||
if (flags & 8){
|
||||
energyExpended = dv.getUint16(idx,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,45 +135,44 @@
|
|||
}
|
||||
|
||||
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 = {
|
||||
bpm: bpm
|
||||
};
|
||||
|
||||
|
||||
if (location) newEvent.location = location;
|
||||
if (interval) newEvent.rr = interval;
|
||||
if (energyExpended) newEvent.energy = energyExpended;
|
||||
if (battery) newEvent.battery = battery;
|
||||
if (sensorContact) newEvent.contact = sensorContact;
|
||||
|
||||
|
||||
log("Emitting BTHRM: ", newEvent);
|
||||
Bangle.emit("BTHRM", newEvent);
|
||||
}
|
||||
},
|
||||
"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){
|
||||
|
|
@ -324,13 +321,13 @@
|
|||
if (c.properties.notify){
|
||||
addNotificationHandler(c);
|
||||
}
|
||||
|
||||
|
||||
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){
|
||||
return promise.then(()=>createServicePromise(service));
|
||||
}
|
||||
|
||||
var reUseCounter = 0;
|
||||
};
|
||||
|
||||
function initBt() {
|
||||
var attachServicePromise = function(promise, service) {
|
||||
return promise.then(()=>createServicePromise(service));
|
||||
};
|
||||
|
||||
var initBt = function () {
|
||||
log("initBt with blockInit: " + blockInit);
|
||||
if (blockInit){
|
||||
retry();
|
||||
|
|
@ -355,63 +350,58 @@
|
|||
|
||||
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");
|
||||
}
|
||||
|
||||
|
||||
promise = promise.then((d)=>{
|
||||
log("Got device: ", d);
|
||||
d.on('gattserverdisconnected', onDisconnect);
|
||||
device = d;
|
||||
});
|
||||
|
||||
|
||||
promise = promise.then(()=>{
|
||||
log("Wait after request");
|
||||
return waitingPromise(settings.gracePeriodRequest);
|
||||
});
|
||||
|
||||
} else {
|
||||
promise = Promise.resolve();
|
||||
log("Reuse device: ", device);
|
||||
}
|
||||
|
||||
|
||||
promise = promise.then(()=>{
|
||||
if (gatt){
|
||||
log("Reuse GATT: ", gatt);
|
||||
} 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;
|
||||
}
|
||||
|
||||
|
||||
return Promise.resolve(gatt);
|
||||
});
|
||||
|
||||
|
||||
promise = promise.then((gatt)=>{
|
||||
if (!gatt.connected){
|
||||
var connectPromise = gatt.connect(connectSettings);
|
||||
|
|
@ -427,16 +417,28 @@
|
|||
return Promise.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/* 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,24 +456,22 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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) {
|
||||
|
|
@ -510,7 +510,7 @@
|
|||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
var origSetHRMPower = Bangle.setHRMPower;
|
||||
|
||||
if (settings.startWithHrm){
|
||||
|
|
@ -525,11 +525,10 @@
|
|||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
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");
|
||||
|
|
@ -557,11 +556,11 @@
|
|||
}
|
||||
switchInternalHrm();
|
||||
}
|
||||
|
||||
|
||||
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));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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){
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
|
|
|
|||
|
|
@ -5,14 +5,14 @@
|
|||
require('Storage').writeJSON(FILE, s);
|
||||
readSettings();
|
||||
}
|
||||
|
||||
|
||||
function readSettings(){
|
||||
settings = Object.assign(
|
||||
require('Storage').readJSON("bthrm.default.json", true) || {},
|
||||
require('Storage').readJSON(FILE, true) || {}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
var FILE="bthrm.json";
|
||||
var settings;
|
||||
readSettings();
|
||||
|
|
@ -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());
|
||||
});
|
||||
|
|
@ -78,9 +79,7 @@
|
|||
mainmenu.Debug = function() { E.showMenu(submenu_debug); };
|
||||
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()); },
|
||||
|
|
@ -167,7 +170,7 @@
|
|||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
var submenu_grace = {
|
||||
'' : { title: "Grace periods"},
|
||||
'< Back': function() { E.showMenu(submenu_debug); },
|
||||
|
|
@ -212,51 +215,6 @@
|
|||
}
|
||||
}
|
||||
};
|
||||
|
||||
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());
|
||||
})
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
0.01: Initial release
|
||||
0.02: implemented "direct launch" and "one click exit" settings
|
||||
|
|
@ -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.
|
||||
|
||||

|
||||

|
||||
|
||||
## 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.
|
||||
|
|
@ -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);
|
||||
},
|
||||
});
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
|
|
@ -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 |
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -1 +1,2 @@
|
|||
0.01: Initial version
|
||||
0.01: Initial version
|
||||
0.02: Update for time_utils module
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
|
|
@ -19,4 +19,4 @@
|
|||
],
|
||||
"data": [{"name":"multitimer.json"}],
|
||||
"dependencies": {"scheduler":"type"}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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); },
|
||||
|
|
|
|||
2
core
2
core
|
|
@ -1 +1 @@
|
|||
Subproject commit 147892754eaf50c8581ebfb4d8651b9ec24aa44e
|
||||
Subproject commit 404e981834f2e8df9c505a8fab12ae12fe3bd562
|
||||
|
|
@ -140,7 +140,7 @@ declare const require: ((module: 'heatshrink') => {
|
|||
|
||||
declare const Bangle: {
|
||||
// functions
|
||||
buzz: () => void;
|
||||
buzz: (duration?: number, intensity?: number) => Promise<void>;
|
||||
drawWidgets: () => void;
|
||||
isCharging: () => boolean;
|
||||
// events
|
||||
|
|
@ -158,9 +158,9 @@ declare type Image = {
|
|||
};
|
||||
|
||||
declare type GraphicsApi = {
|
||||
reset: () => void;
|
||||
reset: () => GraphicsApi;
|
||||
flip: () => void;
|
||||
setColor: (color: string) => void; // TODO we can most likely type color more usefully than this
|
||||
setColor: (color: string) => GraphicsApi; // TODO we can most likely type color more usefully than this
|
||||
drawImage: (
|
||||
image: string | Image | ArrayBuffer,
|
||||
xOffset: number,
|
||||
|
|
@ -169,7 +169,7 @@ declare type GraphicsApi = {
|
|||
rotate?: number;
|
||||
scale?: number;
|
||||
}
|
||||
) => void;
|
||||
) => GraphicsApi;
|
||||
// TODO add more
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue