Merge branch 'espruino:master' into master
|
@ -1,2 +1,3 @@
|
|||
0.01: New app!
|
||||
0.02: Design improvements and fixes.
|
||||
0.02: Design improvements and fixes.
|
||||
0.03: Indicate battery level through line occurrence.
|
|
@ -8,14 +8,16 @@ The original output of stable diffusion is shown here:
|
|||
|
||||

|
||||
|
||||
And my implementation is shown here:
|
||||
My implementation is shown below. Note that horizontal lines occur randomly, but the
|
||||
probability is correlated with the battery level. So if your screen contains only
|
||||
a few lines its time to charge your bangle again ;)
|
||||
|
||||

|
||||
|
||||
|
||||
# Thanks to
|
||||
The great open source community: I used an open source diffusion model (https://github.com/CompVis/stable-diffusion)
|
||||
to generate a watch face for the open source smartwatch BangleJs.
|
||||
The great open-source community: I used an open-source diffusion model (https://github.com/CompVis/stable-diffusion)
|
||||
to generate a watch face for the open-source smartwatch BangleJs.
|
||||
|
||||
## Creator
|
||||
- [David Peer](https://github.com/peerdavid).
|
|
@ -37,8 +37,15 @@ function drawBackground() {
|
|||
g.setFontAlign(0,0);
|
||||
g.setColor(g.theme.fg);
|
||||
|
||||
y = 0;
|
||||
var bat = E.getBattery() / 100.0;
|
||||
var y = 0;
|
||||
while(y < H){
|
||||
// Show less lines in case of small battery level.
|
||||
if(Math.random() > bat){
|
||||
y += 5;
|
||||
continue;
|
||||
}
|
||||
|
||||
y += 3 + Math.floor(Math.random() * 10);
|
||||
g.drawLine(0, y, W, y);
|
||||
g.drawLine(0, y+1, W, y+1);
|
||||
|
@ -103,7 +110,7 @@ function drawDate(){
|
|||
g.setFontAlign(0,0);
|
||||
g.setFontGochiHand();
|
||||
|
||||
var text = ("0"+date.getDate()).substr(-2) + "/" + ("0"+date.getMonth()).substr(-2);
|
||||
var text = ("0"+date.getDate()).substr(-2) + "/" + ("0"+(date.getMonth()+1)).substr(-2);
|
||||
var w = g.stringWidth(text);
|
||||
g.setColor(g.theme.bg);
|
||||
g.fillRect(cx-w/2-4, 20, cx+w/2+4, 40+12);
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"name": "AI Clock",
|
||||
"shortName":"AI Clock",
|
||||
"icon": "aiclock.png",
|
||||
"version":"0.02",
|
||||
"version":"0.03",
|
||||
"readme": "README.md",
|
||||
"supports": ["BANGLEJS2"],
|
||||
"description": "A watch face that was designed by an AI (stable diffusion) and implemented by a human.",
|
||||
|
|
|
@ -12,3 +12,5 @@
|
|||
0.08: fixed calendar weeknumber not shortened to two digits
|
||||
0.09: Use default Bangle formatter for booleans
|
||||
0.10: Use Bangle.setUI({remove:...}) to allow loading the launcher without a full reset on 2v16
|
||||
0.11: Moved enhanced Anton clock to 'Anton Clock Plus' and stripped this clock back down to make it faster for new users (270ms -> 170ms)
|
||||
Modified to avoid leaving functions defined when using setUI({remove:...})
|
||||
|
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 14 KiB |
|
@ -1,9 +1,8 @@
|
|||
{
|
||||
"id": "antonclk",
|
||||
"name": "Anton Clock",
|
||||
"version": "0.10",
|
||||
"description": "A clock using the bold Anton font, optionally showing seconds and date in ISO-8601 format.",
|
||||
"readme":"README.md",
|
||||
"version": "0.11",
|
||||
"description": "A simple clock using the bold Anton font. See `Anton Clock Plus` for an enhanced version",
|
||||
"icon": "app.png",
|
||||
"screenshots": [{"url":"screenshot.png"}],
|
||||
"type": "clock",
|
||||
|
@ -12,8 +11,6 @@
|
|||
"allow_emulator": true,
|
||||
"storage": [
|
||||
{"name":"antonclk.app.js","url":"app.js"},
|
||||
{"name":"antonclk.settings.js","url":"settings.js"},
|
||||
{"name":"antonclk.img","url":"app-icon.js","evaluate":true}
|
||||
],
|
||||
"data": [{"name":"antonclk.json"}]
|
||||
]
|
||||
}
|
||||
|
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 2.8 KiB |
|
@ -0,0 +1,15 @@
|
|||
0.01: New App!
|
||||
0.02: Load widgets after setUI so widclk knows when to hide
|
||||
0.03: Clock now shows day of week under date.
|
||||
0.04: Clock can optionally show seconds, date optionally in ISO-8601 format, weekdays and uppercase configurable, too.
|
||||
0.05: Clock can optionally show ISO-8601 calendar weeknumber (default: Off)
|
||||
when weekday name "Off": week #:<num>
|
||||
when weekday name "On": weekday name is cut at 6th position and .#<week num> is added
|
||||
0.06: fixes #1271 - wrong settings name
|
||||
when weekday name and calendar weeknumber are on then display is <weekday short> #<calweek>
|
||||
week is buffered until date or timezone changes
|
||||
0.07: align default settings with app.js (otherwise the initial displayed settings will be confusing to users)
|
||||
0.08: fixed calendar weeknumber not shortened to two digits
|
||||
0.09: Use default Bangle formatter for booleans
|
||||
0.10: Use Bangle.setUI({remove:...}) to allow loading the launcher without a full reset on 2v16
|
||||
Modified to avoid leaving functions defined when using setUI({remove:...})
|
|
@ -1,6 +1,6 @@
|
|||
# Anton Clock - Large font digital watch with seconds and date
|
||||
# Anton Clock Plus - Large font digital watch with seconds and date
|
||||
|
||||
Anton clock uses the "Anton" bold font to show the time in a clear, easily readable manner. On the Bangle.js 2, the time can be read easily even if the screen is locked and unlit.
|
||||
Anton Clock Plus uses the "Anton" bold font to show the time in a clear, easily readable manner. On the Bangle.js 2, the time can be read easily even if the screen is locked and unlit.
|
||||
|
||||
## Features
|
||||
|
||||
|
@ -16,16 +16,16 @@ The basic time representation only shows hours and minutes of the current time.
|
|||
|
||||
## Usage
|
||||
|
||||
Install Anton clock through the Bangle.js app loader.
|
||||
Configure it through the default Bangle.js configuration mechanism
|
||||
* Install Anton Clock Plus through the Bangle.js app loader.
|
||||
* Configure it through the default Bangle.js configuration mechanism
|
||||
(Settings app, "Apps" menu, "Anton clock" submenu).
|
||||
If you like it, make it your default watch face
|
||||
* If you like it, make it your default watch face
|
||||
(Settings app, "System" menu, "Clock" submenu, select "Anton clock").
|
||||
|
||||
## Configuration
|
||||
|
||||
Anton clock is configured by the standard settings mechanism of Bangle.js's operating system:
|
||||
Open the "Settings" app, then the "Apps" submenu and below it the "Anton clock" menu.
|
||||
Anton Clock is configured by the standard settings mechanism of Bangle.js's operating system:
|
||||
Open the `Settings` app, then the `Apps` submenu and below it the `Anton Clock+` menu.
|
||||
You configure Anton clock through several "on/off" switches in two menus.
|
||||
|
||||
### The main menu
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwgf/AH4At/l/Aofgh4DB+EAj4REQoM/AgP4AoeACIoLCg4FB4AFDCIwLCgAROgYIB8EBAoUH/gVBCIxQBCKYHBCJp9DI4ICBLJYRCn4RQEYMOR5ARDIgIRMYQZZBgARGZwZBDCKQrCgEDR5AdBUIQRJDoLXFCJD7J/xrICIQFCn4RH/4LDAoTaCCI4Ar/LLDCBfypMkCgMkyV/CJOSCIOf5IRGFwOfCJNP//JnmT588z/+pM/BYIRCk4RC/88+f/n4RCngRCz1JCIf5/nzGoQRIHwXPCIPJI4f8CJHJGQJKCCI59LCI5ZCCJ/+v/kBoM/+V/HIJrHBYJWB/JKB5x9JEYP8AQKdBpwRL841Dp41KZoTxBHYTXBWY77PCKKhJ/4/CcgMkXoQAiA="))
|
After Width: | Height: | Size: 1.9 KiB |
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"id": "antonclkplus",
|
||||
"name": "Anton Clock Plus",
|
||||
"shortName": "Anton Clock+",
|
||||
"version": "0.10",
|
||||
"description": "A clock using the bold Anton font, optionally showing seconds and date in ISO-8601 format.",
|
||||
"readme":"README.md",
|
||||
"icon": "app.png",
|
||||
"screenshots": [{"url":"screenshot.png"}],
|
||||
"type": "clock",
|
||||
"tags": "clock",
|
||||
"supports": ["BANGLEJS","BANGLEJS2"],
|
||||
"allow_emulator": true,
|
||||
"storage": [
|
||||
{"name":"antonclkplus.app.js","url":"app.js"},
|
||||
{"name":"antonclkplus.settings.js","url":"settings.js"},
|
||||
{"name":"antonclkplus.img","url":"app-icon.js","evaluate":true}
|
||||
],
|
||||
"data": [{"name":"antonclkplus.json"}]
|
||||
}
|
After Width: | Height: | Size: 1.6 KiB |
|
@ -1,3 +1,4 @@
|
|||
0.01: Display pressure as number and hand
|
||||
0.02: Use theme color
|
||||
0.03: workaround for some firmwares that return 'undefined' for first call to barometer
|
||||
0.04: Update every second, go back with short button press
|
||||
|
|
|
@ -59,6 +59,7 @@ function drawTicks(){
|
|||
function drawScaleLabels(){
|
||||
g.setColor(g.theme.fg);
|
||||
g.setFont("Vector",12);
|
||||
g.setFontAlign(-1,-1);
|
||||
|
||||
let label = MIN;
|
||||
for (let i=0;i <= NUMBER_OF_LABELS; i++){
|
||||
|
@ -103,22 +104,29 @@ function drawIcons() {
|
|||
}
|
||||
|
||||
g.setBgColor(g.theme.bg);
|
||||
g.clear();
|
||||
|
||||
drawTicks();
|
||||
drawScaleLabels();
|
||||
drawIcons();
|
||||
|
||||
try {
|
||||
function baroHandler(data) {
|
||||
if (data===undefined) // workaround for https://github.com/espruino/BangleApps/issues/1429
|
||||
setTimeout(() => Bangle.getPressure().then(baroHandler), 500);
|
||||
else
|
||||
g.clear();
|
||||
|
||||
drawTicks();
|
||||
drawScaleLabels();
|
||||
drawIcons();
|
||||
if (data!==undefined) {
|
||||
drawHand(Math.round(data.pressure));
|
||||
}
|
||||
}
|
||||
Bangle.getPressure().then(baroHandler);
|
||||
setInterval(() => Bangle.getPressure().then(baroHandler), 1000);
|
||||
} catch(e) {
|
||||
print(e.message);
|
||||
print("barometer not supporter, show a demo value");
|
||||
if (e !== undefined) {
|
||||
print(e.message);
|
||||
}
|
||||
print("barometer not supported, show a demo value");
|
||||
drawHand(MIN);
|
||||
}
|
||||
|
||||
Bangle.setUI({
|
||||
mode : "custom",
|
||||
back : function() {load();}
|
||||
});
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{ "id": "barometer",
|
||||
"name": "Barometer",
|
||||
"shortName":"Barometer",
|
||||
"version":"0.03",
|
||||
"version":"0.04",
|
||||
"description": "A simple barometer that displays the current air pressure",
|
||||
"icon": "barometer.png",
|
||||
"tags": "tool,outdoors",
|
||||
|
|
|
@ -29,3 +29,4 @@
|
|||
Use default boolean formatter in custom menu and directly apply config if useful
|
||||
Allow recording unmodified internal HR
|
||||
Better connection retry handling
|
||||
0.13: Less time used during boot if disabled
|
||||
|
|
|
@ -1,633 +1 @@
|
|||
(function() {
|
||||
var settings = Object.assign(
|
||||
require('Storage').readJSON("bthrm.default.json", true) || {},
|
||||
require('Storage').readJSON("bthrm.json", true) || {}
|
||||
);
|
||||
|
||||
var log = function(text, param){
|
||||
if (global.showStatusInfo)
|
||||
showStatusInfo(text);
|
||||
if (settings.debuglog){
|
||||
var logline = new Date().toISOString() + " - " + text;
|
||||
if (param) logline += ": " + JSON.stringify(param);
|
||||
print(logline);
|
||||
}
|
||||
};
|
||||
|
||||
log("Settings: ", settings);
|
||||
|
||||
if (settings.enabled){
|
||||
|
||||
var clearCache = function() {
|
||||
return require('Storage').erase("bthrm.cache.json");
|
||||
};
|
||||
|
||||
var getCache = function() {
|
||||
var cache = require('Storage').readJSON("bthrm.cache.json", true) || {};
|
||||
if (settings.btid && settings.btid === cache.id) return cache;
|
||||
clearCache();
|
||||
return {};
|
||||
};
|
||||
|
||||
var addNotificationHandler = function(characteristic) {
|
||||
log("Setting notification handler"/*supportedCharacteristics[characteristic.uuid].handler*/);
|
||||
characteristic.on('characteristicvaluechanged', (ev) => supportedCharacteristics[characteristic.uuid].handler(ev.target.value));
|
||||
};
|
||||
|
||||
var writeCache = function(cache) {
|
||||
var oldCache = getCache();
|
||||
if (oldCache !== cache) {
|
||||
log("Writing cache");
|
||||
require('Storage').writeJSON("bthrm.cache.json", cache);
|
||||
} else {
|
||||
log("No changes, don't write cache");
|
||||
}
|
||||
};
|
||||
|
||||
var characteristicsToCache = function(characteristics) {
|
||||
log("Cache characteristics");
|
||||
var cache = getCache();
|
||||
if (!cache.characteristics) cache.characteristics = {};
|
||||
for (var c of characteristics){
|
||||
//"handle_value":16,"handle_decl":15
|
||||
log("Saving handle " + c.handle_value + " for characteristic: ", c);
|
||||
cache.characteristics[c.uuid] = {
|
||||
"handle": c.handle_value,
|
||||
"uuid": c.uuid,
|
||||
"notify": c.properties.notify,
|
||||
"read": c.properties.read
|
||||
};
|
||||
}
|
||||
writeCache(cache);
|
||||
};
|
||||
|
||||
var characteristicsFromCache = function(device) {
|
||||
var service = { device : device }; // fake a BluetoothRemoteGATTService
|
||||
log("Read cached characteristics");
|
||||
var cache = getCache();
|
||||
if (!cache.characteristics) return [];
|
||||
var restored = [];
|
||||
for (var c in cache.characteristics){
|
||||
var cached = cache.characteristics[c];
|
||||
var r = new BluetoothRemoteGATTCharacteristic();
|
||||
log("Restoring characteristic ", cached);
|
||||
r.handle_value = cached.handle;
|
||||
r.uuid = cached.uuid;
|
||||
r.properties = {};
|
||||
r.properties.notify = cached.notify;
|
||||
r.properties.read = cached.read;
|
||||
r.service = service;
|
||||
addNotificationHandler(r);
|
||||
log("Restored characteristic: ", r);
|
||||
restored.push(r);
|
||||
}
|
||||
return restored;
|
||||
};
|
||||
|
||||
log("Start");
|
||||
|
||||
var lastReceivedData={
|
||||
};
|
||||
|
||||
var supportedServices = [
|
||||
"0x180d", // Heart Rate
|
||||
"0x180f", // Battery
|
||||
];
|
||||
|
||||
var bpmTimeout;
|
||||
|
||||
var supportedCharacteristics = {
|
||||
"0x2a37": {
|
||||
//Heart rate measurement
|
||||
active: false,
|
||||
handler: function (dv){
|
||||
var flags = dv.getUint8(0);
|
||||
|
||||
var bpm = (flags & 1) ? (dv.getUint16(1) / 100 /* ? */ ) : dv.getUint8(1); // 8 or 16 bit
|
||||
supportedCharacteristics["0x2a37"].active = bpm > 0;
|
||||
log("BTHRM BPM " + supportedCharacteristics["0x2a37"].active);
|
||||
if (supportedCharacteristics["0x2a37"].active) stopFallback();
|
||||
if (bpmTimeout) clearTimeout(bpmTimeout);
|
||||
bpmTimeout = setTimeout(()=>{
|
||||
supportedCharacteristics["0x2a37"].active = false;
|
||||
startFallback();
|
||||
}, 3000);
|
||||
|
||||
var sensorContact;
|
||||
|
||||
if (flags & 2){
|
||||
sensorContact = !!(flags & 4);
|
||||
}
|
||||
|
||||
var idx = 2 + (flags&1);
|
||||
|
||||
var energyExpended;
|
||||
if (flags & 8){
|
||||
energyExpended = dv.getUint16(idx,1);
|
||||
idx += 2;
|
||||
}
|
||||
var interval;
|
||||
if (flags & 16) {
|
||||
interval = [];
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
var location;
|
||||
if (lastReceivedData && lastReceivedData["0x180d"] && lastReceivedData["0x180d"]["0x2a38"]){
|
||||
location = lastReceivedData["0x180d"]["0x2a38"];
|
||||
}
|
||||
|
||||
var battery;
|
||||
if (lastReceivedData && lastReceivedData["0x180f"] && lastReceivedData["0x180f"]["0x2a19"]){
|
||||
battery = lastReceivedData["0x180f"]["0x2a19"];
|
||||
}
|
||||
|
||||
if (settings.replace){
|
||||
var repEvent = {
|
||||
bpm: bpm,
|
||||
confidence: (sensorContact || sensorContact === undefined)? 100 : 0,
|
||||
src: "bthrm"
|
||||
};
|
||||
|
||||
log("Emitting HRM", repEvent);
|
||||
Bangle.emit("HRM_int", 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(dv){
|
||||
if (!lastReceivedData["0x180d"]) lastReceivedData["0x180d"] = {};
|
||||
lastReceivedData["0x180d"]["0x2a38"] = parseInt(dv.buffer, 10);
|
||||
}
|
||||
},
|
||||
"0x2a19": {
|
||||
//Battery
|
||||
handler: function (dv){
|
||||
if (!lastReceivedData["0x180f"]) lastReceivedData["0x180f"] = {};
|
||||
lastReceivedData["0x180f"]["0x2a19"] = dv.getUint8(0);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var device;
|
||||
var gatt;
|
||||
var characteristics = [];
|
||||
var blockInit = false;
|
||||
var currentRetryTimeout;
|
||||
var initialRetryTime = 40;
|
||||
var maxRetryTime = 60000;
|
||||
var retryTime = initialRetryTime;
|
||||
|
||||
var connectSettings = {
|
||||
minInterval: 7.5,
|
||||
maxInterval: 1500
|
||||
};
|
||||
|
||||
var waitingPromise = function(timeout) {
|
||||
return new Promise(function(resolve){
|
||||
log("Start waiting for " + timeout);
|
||||
setTimeout(()=>{
|
||||
log("Done waiting for " + timeout);
|
||||
resolve();
|
||||
}, timeout);
|
||||
});
|
||||
};
|
||||
|
||||
if (settings.enabled){
|
||||
Bangle.isBTHRMActive = function (){
|
||||
return supportedCharacteristics["0x2a37"].active;
|
||||
};
|
||||
|
||||
Bangle.isBTHRMOn = function(){
|
||||
return (Bangle._PWR && Bangle._PWR.BTHRM && Bangle._PWR.BTHRM.length > 0);
|
||||
};
|
||||
|
||||
Bangle.isBTHRMConnected = function(){
|
||||
return gatt && gatt.connected;
|
||||
};
|
||||
}
|
||||
|
||||
if (settings.replace){
|
||||
Bangle.origIsHRMOn = Bangle.isHRMOn;
|
||||
|
||||
Bangle.isHRMOn = function() {
|
||||
if (settings.enabled && !settings.replace){
|
||||
return Bangle.origIsHRMOn();
|
||||
} else if (settings.enabled && settings.replace){
|
||||
return Bangle.isBTHRMOn();
|
||||
}
|
||||
return Bangle.origIsHRMOn() || Bangle.isBTHRMOn();
|
||||
};
|
||||
}
|
||||
|
||||
var clearRetryTimeout = function(resetTime) {
|
||||
if (currentRetryTimeout){
|
||||
log("Clearing timeout " + currentRetryTimeout);
|
||||
clearTimeout(currentRetryTimeout);
|
||||
currentRetryTimeout = undefined;
|
||||
}
|
||||
if (resetTime) {
|
||||
log("Resetting retry time");
|
||||
retryTime = initialRetryTime;
|
||||
}
|
||||
};
|
||||
|
||||
var retry = function() {
|
||||
log("Retry");
|
||||
|
||||
if (!currentRetryTimeout){
|
||||
|
||||
var clampedTime = retryTime < 100 ? 100 : retryTime;
|
||||
|
||||
log("Set timeout for retry as " + clampedTime);
|
||||
clearRetryTimeout();
|
||||
currentRetryTimeout = setTimeout(() => {
|
||||
log("Retrying");
|
||||
currentRetryTimeout = undefined;
|
||||
initBt();
|
||||
}, clampedTime);
|
||||
|
||||
retryTime = Math.pow(clampedTime, 1.1);
|
||||
if (retryTime > maxRetryTime){
|
||||
retryTime = maxRetryTime;
|
||||
}
|
||||
} else {
|
||||
log("Already in retry...");
|
||||
}
|
||||
};
|
||||
|
||||
var buzzing = false;
|
||||
var onDisconnect = function(reason) {
|
||||
log("Disconnect: " + reason);
|
||||
log("GATT", gatt);
|
||||
log("Characteristics", characteristics);
|
||||
clearRetryTimeout(reason != "Connection Timeout");
|
||||
supportedCharacteristics["0x2a37"].active = false;
|
||||
startFallback();
|
||||
blockInit = false;
|
||||
if (settings.warnDisconnect && !buzzing){
|
||||
buzzing = true;
|
||||
Bangle.buzz(500,0.3).then(()=>waitingPromise(4500)).then(()=>{buzzing = false;});
|
||||
}
|
||||
if (Bangle.isBTHRMOn()){
|
||||
retry();
|
||||
}
|
||||
};
|
||||
|
||||
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", 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", newCharacteristic);
|
||||
var startPromise = newCharacteristic.startNotifications().then(()=>log("Notifications started", newCharacteristic));
|
||||
if (settings.gracePeriodNotification > 0){
|
||||
log("Add " + settings.gracePeriodNotification + "ms grace period after starting notifications");
|
||||
startPromise = startPromise.then(()=>{
|
||||
log("Wait after connect");
|
||||
return waitingPromise(settings.gracePeriodNotification);
|
||||
});
|
||||
}
|
||||
return startPromise;
|
||||
});
|
||||
}
|
||||
return result.then(()=>log("Handled characteristic", newCharacteristic));
|
||||
};
|
||||
|
||||
var attachCharacteristicPromise = function(promise, characteristic) {
|
||||
return promise.then(()=>{
|
||||
log("Handling characteristic:", characteristic);
|
||||
return createCharacteristicPromise(characteristic);
|
||||
});
|
||||
};
|
||||
|
||||
var createCharacteristicsPromise = function(newCharacteristics) {
|
||||
log("Create characteristics promis ", newCharacteristics);
|
||||
var result = Promise.resolve();
|
||||
for (var c of newCharacteristics){
|
||||
if (!supportedCharacteristics[c.uuid]) continue;
|
||||
log("Supporting characteristic", c);
|
||||
characteristics.push(c);
|
||||
if (c.properties.notify){
|
||||
addNotificationHandler(c);
|
||||
}
|
||||
|
||||
result = attachCharacteristicPromise(result, c);
|
||||
}
|
||||
return result.then(()=>log("Handled characteristics"));
|
||||
};
|
||||
|
||||
var createServicePromise = function(service) {
|
||||
log("Create service promise", service);
|
||||
var result = Promise.resolve();
|
||||
result = result.then(()=>{
|
||||
log("Handling service" + service.uuid);
|
||||
return service.getCharacteristics().then((c)=>createCharacteristicsPromise(c));
|
||||
});
|
||||
return result.then(()=>log("Handled service" + service.uuid));
|
||||
};
|
||||
|
||||
var attachServicePromise = function(promise, service) {
|
||||
return promise.then(()=>createServicePromise(service));
|
||||
};
|
||||
|
||||
var initBt = function () {
|
||||
log("initBt with blockInit: " + blockInit);
|
||||
if (blockInit){
|
||||
retry();
|
||||
return;
|
||||
}
|
||||
|
||||
blockInit = true;
|
||||
|
||||
var promise;
|
||||
var filters;
|
||||
|
||||
if (!device){
|
||||
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, 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 cachedId = getCache().id;
|
||||
if (device.id !== cachedId){
|
||||
log("Device ID changed from " + cachedId + " to " + device.id + ", clearing cache");
|
||||
clearCache();
|
||||
}
|
||||
var newCache = getCache();
|
||||
newCache.id = device.id;
|
||||
writeCache(newCache);
|
||||
gatt = device.gatt;
|
||||
}
|
||||
|
||||
return Promise.resolve(gatt);
|
||||
});
|
||||
|
||||
promise = promise.then((gatt)=>{
|
||||
if (!gatt.connected){
|
||||
log("Connecting...");
|
||||
var connectPromise = gatt.connect(connectSettings).then(function() {
|
||||
log("Connected.");
|
||||
});
|
||||
if (settings.gracePeriodConnect > 0){
|
||||
log("Add " + settings.gracePeriodConnect + "ms grace period after connecting");
|
||||
connectPromise = connectPromise.then(()=>{
|
||||
log("Wait after connect");
|
||||
return waitingPromise(settings.gracePeriodConnect);
|
||||
});
|
||||
}
|
||||
return connectPromise;
|
||||
} else {
|
||||
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){
|
||||
characteristics = characteristicsFromCache(device);
|
||||
}
|
||||
});
|
||||
|
||||
promise = promise.then(()=>{
|
||||
var characteristicsPromise = Promise.resolve();
|
||||
if (characteristics.length === 0){
|
||||
characteristicsPromise = characteristicsPromise.then(()=>{
|
||||
log("Getting services");
|
||||
return gatt.getPrimaryServices();
|
||||
});
|
||||
|
||||
characteristicsPromise = characteristicsPromise.then((services)=>{
|
||||
log("Got services", services);
|
||||
var result = Promise.resolve();
|
||||
for (var service of services){
|
||||
if (!(supportedServices.includes(service.uuid))) continue;
|
||||
log("Supporting service", service.uuid);
|
||||
result = attachServicePromise(result, service);
|
||||
}
|
||||
if (settings.gracePeriodService > 0) {
|
||||
log("Add " + settings.gracePeriodService + "ms grace period after services");
|
||||
result = result.then(()=>{
|
||||
log("Wait after services");
|
||||
return waitingPromise(settings.gracePeriodService);
|
||||
});
|
||||
}
|
||||
return result;
|
||||
});
|
||||
} else {
|
||||
for (var characteristic of characteristics){
|
||||
characteristicsPromise = attachCharacteristicPromise(characteristicsPromise, characteristic, true);
|
||||
}
|
||||
}
|
||||
|
||||
return characteristicsPromise;
|
||||
});
|
||||
|
||||
return promise.then(()=>{
|
||||
log("Connection established, waiting for notifications");
|
||||
characteristicsToCache(characteristics);
|
||||
clearRetryTimeout(true);
|
||||
}).catch((e) => {
|
||||
characteristics = [];
|
||||
log("Error:", e);
|
||||
onDisconnect(e);
|
||||
});
|
||||
};
|
||||
|
||||
Bangle.setBTHRMPower = function(isOn, app) {
|
||||
// Do app power handling
|
||||
if (!app) app="?";
|
||||
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);
|
||||
isOn = Bangle._PWR.BTHRM.length;
|
||||
// so now we know if we're really on
|
||||
if (isOn) {
|
||||
switchFallback();
|
||||
if (!Bangle.isBTHRMConnected()) initBt();
|
||||
} else { // not on
|
||||
log("Power off for " + app);
|
||||
clearRetryTimeout(true);
|
||||
if (gatt) {
|
||||
if (gatt.connected){
|
||||
log("Disconnect with gatt", gatt);
|
||||
try{
|
||||
gatt.disconnect().then(()=>{
|
||||
log("Successful disconnect");
|
||||
}).catch((e)=>{
|
||||
log("Error during disconnect promise", e);
|
||||
});
|
||||
} catch (e){
|
||||
log("Error during disconnect attempt", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (settings.replace){
|
||||
Bangle.on("HRM", (e) => {
|
||||
e.modified = true;
|
||||
Bangle.emit("HRM_int", e);
|
||||
});
|
||||
|
||||
Bangle.origOn = Bangle.on;
|
||||
Bangle.on = function(name, callback) {
|
||||
if (name == "HRM") {
|
||||
Bangle.origOn("HRM_int", callback);
|
||||
} else {
|
||||
Bangle.origOn(name, callback);
|
||||
}
|
||||
};
|
||||
|
||||
Bangle.origRemoveListener = Bangle.removeListener;
|
||||
Bangle.removeListener = function(name, callback) {
|
||||
if (name == "HRM") {
|
||||
Bangle.origRemoveListener("HRM_int", callback);
|
||||
} else {
|
||||
Bangle.origRemoveListener(name, callback);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
Bangle.origSetHRMPower = Bangle.setHRMPower;
|
||||
|
||||
if (settings.startWithHrm){
|
||||
|
||||
Bangle.setHRMPower = function(isOn, app) {
|
||||
log("setHRMPower for " + app + ": " + (isOn?"on":"off"));
|
||||
if (settings.enabled){
|
||||
Bangle.setBTHRMPower(isOn, app);
|
||||
}
|
||||
if ((settings.enabled && !settings.replace) || !settings.enabled){
|
||||
Bangle.origSetHRMPower(isOn, app);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
var fallbackActive = false;
|
||||
var inSwitch = false;
|
||||
|
||||
var stopFallback = function(){
|
||||
if (fallbackActive){
|
||||
Bangle.origSetHRMPower(0, "bthrm_fallback");
|
||||
fallbackActive = false;
|
||||
log("Fallback to HRM disabled");
|
||||
}
|
||||
};
|
||||
|
||||
var startFallback = function(){
|
||||
if (!fallbackActive && settings.allowFallback) {
|
||||
fallbackActive = true;
|
||||
Bangle.origSetHRMPower(1, "bthrm_fallback");
|
||||
log("Fallback to HRM enabled");
|
||||
}
|
||||
};
|
||||
|
||||
var switchFallback = function() {
|
||||
log("Check falling back to HRM");
|
||||
if (!inSwitch){
|
||||
inSwitch = true;
|
||||
if (Bangle.isBTHRMActive()){
|
||||
stopFallback();
|
||||
} else {
|
||||
startFallback();
|
||||
}
|
||||
}
|
||||
inSwitch = false;
|
||||
};
|
||||
|
||||
if (settings.replace){
|
||||
log("Replace HRM event");
|
||||
if (Bangle._PWR && Bangle._PWR.HRM){
|
||||
for (var i = 0; i < Bangle._PWR.HRM.length; i++){
|
||||
var app = Bangle._PWR.HRM[i];
|
||||
log("Moving app " + app);
|
||||
Bangle.origSetHRMPower(0, app);
|
||||
Bangle.setBTHRMPower(1, app);
|
||||
if (Bangle._PWR.HRM===undefined) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
E.on("kill", ()=>{
|
||||
if (gatt && gatt.connected){
|
||||
log("Got killed, trying to disconnect");
|
||||
gatt.disconnect().then(()=>log("Disconnected on kill")).catch((e)=>log("Error during disconnnect on kill", e));
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
||||
if ((require('Storage').readJSON("bthrm.json", true) || {}).enabled != false) require("bthrm").enable();
|
||||
|
|
|
@ -0,0 +1,633 @@
|
|||
exports.enable = () => {
|
||||
var settings = Object.assign(
|
||||
require('Storage').readJSON("bthrm.default.json", true) || {},
|
||||
require('Storage').readJSON("bthrm.json", true) || {}
|
||||
);
|
||||
|
||||
var log = function(text, param){
|
||||
if (global.showStatusInfo)
|
||||
showStatusInfo(text);
|
||||
if (settings.debuglog){
|
||||
var logline = new Date().toISOString() + " - " + text;
|
||||
if (param) logline += ": " + JSON.stringify(param);
|
||||
print(logline);
|
||||
}
|
||||
};
|
||||
|
||||
log("Settings: ", settings);
|
||||
|
||||
if (settings.enabled){
|
||||
|
||||
var clearCache = function() {
|
||||
return require('Storage').erase("bthrm.cache.json");
|
||||
};
|
||||
|
||||
var getCache = function() {
|
||||
var cache = require('Storage').readJSON("bthrm.cache.json", true) || {};
|
||||
if (settings.btid && settings.btid === cache.id) return cache;
|
||||
clearCache();
|
||||
return {};
|
||||
};
|
||||
|
||||
var addNotificationHandler = function(characteristic) {
|
||||
log("Setting notification handler"/*supportedCharacteristics[characteristic.uuid].handler*/);
|
||||
characteristic.on('characteristicvaluechanged', (ev) => supportedCharacteristics[characteristic.uuid].handler(ev.target.value));
|
||||
};
|
||||
|
||||
var writeCache = function(cache) {
|
||||
var oldCache = getCache();
|
||||
if (oldCache !== cache) {
|
||||
log("Writing cache");
|
||||
require('Storage').writeJSON("bthrm.cache.json", cache);
|
||||
} else {
|
||||
log("No changes, don't write cache");
|
||||
}
|
||||
};
|
||||
|
||||
var characteristicsToCache = function(characteristics) {
|
||||
log("Cache characteristics");
|
||||
var cache = getCache();
|
||||
if (!cache.characteristics) cache.characteristics = {};
|
||||
for (var c of characteristics){
|
||||
//"handle_value":16,"handle_decl":15
|
||||
log("Saving handle " + c.handle_value + " for characteristic: ", c);
|
||||
cache.characteristics[c.uuid] = {
|
||||
"handle": c.handle_value,
|
||||
"uuid": c.uuid,
|
||||
"notify": c.properties.notify,
|
||||
"read": c.properties.read
|
||||
};
|
||||
}
|
||||
writeCache(cache);
|
||||
};
|
||||
|
||||
var characteristicsFromCache = function(device) {
|
||||
var service = { device : device }; // fake a BluetoothRemoteGATTService
|
||||
log("Read cached characteristics");
|
||||
var cache = getCache();
|
||||
if (!cache.characteristics) return [];
|
||||
var restored = [];
|
||||
for (var c in cache.characteristics){
|
||||
var cached = cache.characteristics[c];
|
||||
var r = new BluetoothRemoteGATTCharacteristic();
|
||||
log("Restoring characteristic ", cached);
|
||||
r.handle_value = cached.handle;
|
||||
r.uuid = cached.uuid;
|
||||
r.properties = {};
|
||||
r.properties.notify = cached.notify;
|
||||
r.properties.read = cached.read;
|
||||
r.service = service;
|
||||
addNotificationHandler(r);
|
||||
log("Restored characteristic: ", r);
|
||||
restored.push(r);
|
||||
}
|
||||
return restored;
|
||||
};
|
||||
|
||||
log("Start");
|
||||
|
||||
var lastReceivedData={
|
||||
};
|
||||
|
||||
var supportedServices = [
|
||||
"0x180d", // Heart Rate
|
||||
"0x180f", // Battery
|
||||
];
|
||||
|
||||
var bpmTimeout;
|
||||
|
||||
var supportedCharacteristics = {
|
||||
"0x2a37": {
|
||||
//Heart rate measurement
|
||||
active: false,
|
||||
handler: function (dv){
|
||||
var flags = dv.getUint8(0);
|
||||
|
||||
var bpm = (flags & 1) ? (dv.getUint16(1) / 100 /* ? */ ) : dv.getUint8(1); // 8 or 16 bit
|
||||
supportedCharacteristics["0x2a37"].active = bpm > 0;
|
||||
log("BTHRM BPM " + supportedCharacteristics["0x2a37"].active);
|
||||
if (supportedCharacteristics["0x2a37"].active) stopFallback();
|
||||
if (bpmTimeout) clearTimeout(bpmTimeout);
|
||||
bpmTimeout = setTimeout(()=>{
|
||||
supportedCharacteristics["0x2a37"].active = false;
|
||||
startFallback();
|
||||
}, 3000);
|
||||
|
||||
var sensorContact;
|
||||
|
||||
if (flags & 2){
|
||||
sensorContact = !!(flags & 4);
|
||||
}
|
||||
|
||||
var idx = 2 + (flags&1);
|
||||
|
||||
var energyExpended;
|
||||
if (flags & 8){
|
||||
energyExpended = dv.getUint16(idx,1);
|
||||
idx += 2;
|
||||
}
|
||||
var interval;
|
||||
if (flags & 16) {
|
||||
interval = [];
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
var location;
|
||||
if (lastReceivedData && lastReceivedData["0x180d"] && lastReceivedData["0x180d"]["0x2a38"]){
|
||||
location = lastReceivedData["0x180d"]["0x2a38"];
|
||||
}
|
||||
|
||||
var battery;
|
||||
if (lastReceivedData && lastReceivedData["0x180f"] && lastReceivedData["0x180f"]["0x2a19"]){
|
||||
battery = lastReceivedData["0x180f"]["0x2a19"];
|
||||
}
|
||||
|
||||
if (settings.replace){
|
||||
var repEvent = {
|
||||
bpm: bpm,
|
||||
confidence: (sensorContact || sensorContact === undefined)? 100 : 0,
|
||||
src: "bthrm"
|
||||
};
|
||||
|
||||
log("Emitting HRM", repEvent);
|
||||
Bangle.emit("HRM_int", 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(dv){
|
||||
if (!lastReceivedData["0x180d"]) lastReceivedData["0x180d"] = {};
|
||||
lastReceivedData["0x180d"]["0x2a38"] = parseInt(dv.buffer, 10);
|
||||
}
|
||||
},
|
||||
"0x2a19": {
|
||||
//Battery
|
||||
handler: function (dv){
|
||||
if (!lastReceivedData["0x180f"]) lastReceivedData["0x180f"] = {};
|
||||
lastReceivedData["0x180f"]["0x2a19"] = dv.getUint8(0);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var device;
|
||||
var gatt;
|
||||
var characteristics = [];
|
||||
var blockInit = false;
|
||||
var currentRetryTimeout;
|
||||
var initialRetryTime = 40;
|
||||
var maxRetryTime = 60000;
|
||||
var retryTime = initialRetryTime;
|
||||
|
||||
var connectSettings = {
|
||||
minInterval: 7.5,
|
||||
maxInterval: 1500
|
||||
};
|
||||
|
||||
var waitingPromise = function(timeout) {
|
||||
return new Promise(function(resolve){
|
||||
log("Start waiting for " + timeout);
|
||||
setTimeout(()=>{
|
||||
log("Done waiting for " + timeout);
|
||||
resolve();
|
||||
}, timeout);
|
||||
});
|
||||
};
|
||||
|
||||
if (settings.enabled){
|
||||
Bangle.isBTHRMActive = function (){
|
||||
return supportedCharacteristics["0x2a37"].active;
|
||||
};
|
||||
|
||||
Bangle.isBTHRMOn = function(){
|
||||
return (Bangle._PWR && Bangle._PWR.BTHRM && Bangle._PWR.BTHRM.length > 0);
|
||||
};
|
||||
|
||||
Bangle.isBTHRMConnected = function(){
|
||||
return gatt && gatt.connected;
|
||||
};
|
||||
}
|
||||
|
||||
if (settings.replace){
|
||||
Bangle.origIsHRMOn = Bangle.isHRMOn;
|
||||
|
||||
Bangle.isHRMOn = function() {
|
||||
if (settings.enabled && !settings.replace){
|
||||
return Bangle.origIsHRMOn();
|
||||
} else if (settings.enabled && settings.replace){
|
||||
return Bangle.isBTHRMOn();
|
||||
}
|
||||
return Bangle.origIsHRMOn() || Bangle.isBTHRMOn();
|
||||
};
|
||||
}
|
||||
|
||||
var clearRetryTimeout = function(resetTime) {
|
||||
if (currentRetryTimeout){
|
||||
log("Clearing timeout " + currentRetryTimeout);
|
||||
clearTimeout(currentRetryTimeout);
|
||||
currentRetryTimeout = undefined;
|
||||
}
|
||||
if (resetTime) {
|
||||
log("Resetting retry time");
|
||||
retryTime = initialRetryTime;
|
||||
}
|
||||
};
|
||||
|
||||
var retry = function() {
|
||||
log("Retry");
|
||||
|
||||
if (!currentRetryTimeout){
|
||||
|
||||
var clampedTime = retryTime < 100 ? 100 : retryTime;
|
||||
|
||||
log("Set timeout for retry as " + clampedTime);
|
||||
clearRetryTimeout();
|
||||
currentRetryTimeout = setTimeout(() => {
|
||||
log("Retrying");
|
||||
currentRetryTimeout = undefined;
|
||||
initBt();
|
||||
}, clampedTime);
|
||||
|
||||
retryTime = Math.pow(clampedTime, 1.1);
|
||||
if (retryTime > maxRetryTime){
|
||||
retryTime = maxRetryTime;
|
||||
}
|
||||
} else {
|
||||
log("Already in retry...");
|
||||
}
|
||||
};
|
||||
|
||||
var buzzing = false;
|
||||
var onDisconnect = function(reason) {
|
||||
log("Disconnect: " + reason);
|
||||
log("GATT", gatt);
|
||||
log("Characteristics", characteristics);
|
||||
clearRetryTimeout(reason != "Connection Timeout");
|
||||
supportedCharacteristics["0x2a37"].active = false;
|
||||
startFallback();
|
||||
blockInit = false;
|
||||
if (settings.warnDisconnect && !buzzing){
|
||||
buzzing = true;
|
||||
Bangle.buzz(500,0.3).then(()=>waitingPromise(4500)).then(()=>{buzzing = false;});
|
||||
}
|
||||
if (Bangle.isBTHRMOn()){
|
||||
retry();
|
||||
}
|
||||
};
|
||||
|
||||
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", 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", newCharacteristic);
|
||||
var startPromise = newCharacteristic.startNotifications().then(()=>log("Notifications started", newCharacteristic));
|
||||
if (settings.gracePeriodNotification > 0){
|
||||
log("Add " + settings.gracePeriodNotification + "ms grace period after starting notifications");
|
||||
startPromise = startPromise.then(()=>{
|
||||
log("Wait after connect");
|
||||
return waitingPromise(settings.gracePeriodNotification);
|
||||
});
|
||||
}
|
||||
return startPromise;
|
||||
});
|
||||
}
|
||||
return result.then(()=>log("Handled characteristic", newCharacteristic));
|
||||
};
|
||||
|
||||
var attachCharacteristicPromise = function(promise, characteristic) {
|
||||
return promise.then(()=>{
|
||||
log("Handling characteristic:", characteristic);
|
||||
return createCharacteristicPromise(characteristic);
|
||||
});
|
||||
};
|
||||
|
||||
var createCharacteristicsPromise = function(newCharacteristics) {
|
||||
log("Create characteristics promis ", newCharacteristics);
|
||||
var result = Promise.resolve();
|
||||
for (var c of newCharacteristics){
|
||||
if (!supportedCharacteristics[c.uuid]) continue;
|
||||
log("Supporting characteristic", c);
|
||||
characteristics.push(c);
|
||||
if (c.properties.notify){
|
||||
addNotificationHandler(c);
|
||||
}
|
||||
|
||||
result = attachCharacteristicPromise(result, c);
|
||||
}
|
||||
return result.then(()=>log("Handled characteristics"));
|
||||
};
|
||||
|
||||
var createServicePromise = function(service) {
|
||||
log("Create service promise", service);
|
||||
var result = Promise.resolve();
|
||||
result = result.then(()=>{
|
||||
log("Handling service" + service.uuid);
|
||||
return service.getCharacteristics().then((c)=>createCharacteristicsPromise(c));
|
||||
});
|
||||
return result.then(()=>log("Handled service" + service.uuid));
|
||||
};
|
||||
|
||||
var attachServicePromise = function(promise, service) {
|
||||
return promise.then(()=>createServicePromise(service));
|
||||
};
|
||||
|
||||
var initBt = function () {
|
||||
log("initBt with blockInit: " + blockInit);
|
||||
if (blockInit){
|
||||
retry();
|
||||
return;
|
||||
}
|
||||
|
||||
blockInit = true;
|
||||
|
||||
var promise;
|
||||
var filters;
|
||||
|
||||
if (!device){
|
||||
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, 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 cachedId = getCache().id;
|
||||
if (device.id !== cachedId){
|
||||
log("Device ID changed from " + cachedId + " to " + device.id + ", clearing cache");
|
||||
clearCache();
|
||||
}
|
||||
var newCache = getCache();
|
||||
newCache.id = device.id;
|
||||
writeCache(newCache);
|
||||
gatt = device.gatt;
|
||||
}
|
||||
|
||||
return Promise.resolve(gatt);
|
||||
});
|
||||
|
||||
promise = promise.then((gatt)=>{
|
||||
if (!gatt.connected){
|
||||
log("Connecting...");
|
||||
var connectPromise = gatt.connect(connectSettings).then(function() {
|
||||
log("Connected.");
|
||||
});
|
||||
if (settings.gracePeriodConnect > 0){
|
||||
log("Add " + settings.gracePeriodConnect + "ms grace period after connecting");
|
||||
connectPromise = connectPromise.then(()=>{
|
||||
log("Wait after connect");
|
||||
return waitingPromise(settings.gracePeriodConnect);
|
||||
});
|
||||
}
|
||||
return connectPromise;
|
||||
} else {
|
||||
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){
|
||||
characteristics = characteristicsFromCache(device);
|
||||
}
|
||||
});
|
||||
|
||||
promise = promise.then(()=>{
|
||||
var characteristicsPromise = Promise.resolve();
|
||||
if (characteristics.length === 0){
|
||||
characteristicsPromise = characteristicsPromise.then(()=>{
|
||||
log("Getting services");
|
||||
return gatt.getPrimaryServices();
|
||||
});
|
||||
|
||||
characteristicsPromise = characteristicsPromise.then((services)=>{
|
||||
log("Got services", services);
|
||||
var result = Promise.resolve();
|
||||
for (var service of services){
|
||||
if (!(supportedServices.includes(service.uuid))) continue;
|
||||
log("Supporting service", service.uuid);
|
||||
result = attachServicePromise(result, service);
|
||||
}
|
||||
if (settings.gracePeriodService > 0) {
|
||||
log("Add " + settings.gracePeriodService + "ms grace period after services");
|
||||
result = result.then(()=>{
|
||||
log("Wait after services");
|
||||
return waitingPromise(settings.gracePeriodService);
|
||||
});
|
||||
}
|
||||
return result;
|
||||
});
|
||||
} else {
|
||||
for (var characteristic of characteristics){
|
||||
characteristicsPromise = attachCharacteristicPromise(characteristicsPromise, characteristic, true);
|
||||
}
|
||||
}
|
||||
|
||||
return characteristicsPromise;
|
||||
});
|
||||
|
||||
return promise.then(()=>{
|
||||
log("Connection established, waiting for notifications");
|
||||
characteristicsToCache(characteristics);
|
||||
clearRetryTimeout(true);
|
||||
}).catch((e) => {
|
||||
characteristics = [];
|
||||
log("Error:", e);
|
||||
onDisconnect(e);
|
||||
});
|
||||
};
|
||||
|
||||
Bangle.setBTHRMPower = function(isOn, app) {
|
||||
// Do app power handling
|
||||
if (!app) app="?";
|
||||
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);
|
||||
isOn = Bangle._PWR.BTHRM.length;
|
||||
// so now we know if we're really on
|
||||
if (isOn) {
|
||||
switchFallback();
|
||||
if (!Bangle.isBTHRMConnected()) initBt();
|
||||
} else { // not on
|
||||
log("Power off for " + app);
|
||||
clearRetryTimeout(true);
|
||||
if (gatt) {
|
||||
if (gatt.connected){
|
||||
log("Disconnect with gatt", gatt);
|
||||
try{
|
||||
gatt.disconnect().then(()=>{
|
||||
log("Successful disconnect");
|
||||
}).catch((e)=>{
|
||||
log("Error during disconnect promise", e);
|
||||
});
|
||||
} catch (e){
|
||||
log("Error during disconnect attempt", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (settings.replace){
|
||||
Bangle.on("HRM", (e) => {
|
||||
e.modified = true;
|
||||
Bangle.emit("HRM_int", e);
|
||||
});
|
||||
|
||||
Bangle.origOn = Bangle.on;
|
||||
Bangle.on = function(name, callback) {
|
||||
if (name == "HRM") {
|
||||
Bangle.origOn("HRM_int", callback);
|
||||
} else {
|
||||
Bangle.origOn(name, callback);
|
||||
}
|
||||
};
|
||||
|
||||
Bangle.origRemoveListener = Bangle.removeListener;
|
||||
Bangle.removeListener = function(name, callback) {
|
||||
if (name == "HRM") {
|
||||
Bangle.origRemoveListener("HRM_int", callback);
|
||||
} else {
|
||||
Bangle.origRemoveListener(name, callback);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
Bangle.origSetHRMPower = Bangle.setHRMPower;
|
||||
|
||||
if (settings.startWithHrm){
|
||||
|
||||
Bangle.setHRMPower = function(isOn, app) {
|
||||
log("setHRMPower for " + app + ": " + (isOn?"on":"off"));
|
||||
if (settings.enabled){
|
||||
Bangle.setBTHRMPower(isOn, app);
|
||||
}
|
||||
if ((settings.enabled && !settings.replace) || !settings.enabled){
|
||||
Bangle.origSetHRMPower(isOn, app);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
var fallbackActive = false;
|
||||
var inSwitch = false;
|
||||
|
||||
var stopFallback = function(){
|
||||
if (fallbackActive){
|
||||
Bangle.origSetHRMPower(0, "bthrm_fallback");
|
||||
fallbackActive = false;
|
||||
log("Fallback to HRM disabled");
|
||||
}
|
||||
};
|
||||
|
||||
var startFallback = function(){
|
||||
if (!fallbackActive && settings.allowFallback) {
|
||||
fallbackActive = true;
|
||||
Bangle.origSetHRMPower(1, "bthrm_fallback");
|
||||
log("Fallback to HRM enabled");
|
||||
}
|
||||
};
|
||||
|
||||
var switchFallback = function() {
|
||||
log("Check falling back to HRM");
|
||||
if (!inSwitch){
|
||||
inSwitch = true;
|
||||
if (Bangle.isBTHRMActive()){
|
||||
stopFallback();
|
||||
} else {
|
||||
startFallback();
|
||||
}
|
||||
}
|
||||
inSwitch = false;
|
||||
};
|
||||
|
||||
if (settings.replace){
|
||||
log("Replace HRM event");
|
||||
if (Bangle._PWR && Bangle._PWR.HRM){
|
||||
for (var i = 0; i < Bangle._PWR.HRM.length; i++){
|
||||
var app = Bangle._PWR.HRM[i];
|
||||
log("Moving app " + app);
|
||||
Bangle.origSetHRMPower(0, app);
|
||||
Bangle.setBTHRMPower(1, app);
|
||||
if (Bangle._PWR.HRM===undefined) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
E.on("kill", ()=>{
|
||||
if (gatt && gatt.connected){
|
||||
log("Got killed, trying to disconnect");
|
||||
gatt.disconnect().then(()=>log("Disconnected on kill")).catch((e)=>log("Error during disconnnect on kill", e));
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
|
@ -2,7 +2,7 @@
|
|||
"id": "bthrm",
|
||||
"name": "Bluetooth Heart Rate Monitor",
|
||||
"shortName": "BT HRM",
|
||||
"version": "0.12",
|
||||
"version": "0.13",
|
||||
"description": "Overrides Bangle.js's build in heart rate monitor with an external Bluetooth one.",
|
||||
"icon": "app.png",
|
||||
"type": "app",
|
||||
|
@ -15,6 +15,7 @@
|
|||
{"name":"bthrm.0.boot.js","url":"boot.js"},
|
||||
{"name":"bthrm.img","url":"app-icon.js","evaluate":true},
|
||||
{"name":"bthrm.settings.js","url":"settings.js"},
|
||||
{"name":"bthrm","url":"lib.js"},
|
||||
{"name":"bthrm.default.json","url":"default.json"}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -26,3 +26,7 @@
|
|||
0.12: Allow configuration of update interval
|
||||
0.13: Load step goal from Bangle health app as fallback
|
||||
Memory optimizations
|
||||
0.14: Support to show big weather info
|
||||
0.15: Use Bangle.setUI({remove:...}) to allow loading the launcher without a full reset on 2v16
|
||||
0.16: Fix const error
|
||||
Use widget_utils if available
|
||||
|
|
|
@ -9,10 +9,11 @@ It can show the following information (this can be configured):
|
|||
* Steps distance
|
||||
* Heart rate (automatically updates when screen is on and unlocked)
|
||||
* Battery (including charging status and battery low warning)
|
||||
* Weather (requires [weather app](https://banglejs.com/apps/#weather))
|
||||
* Weather (requires [OWM weather provider](https://banglejs.com/apps/?id=owmweather))
|
||||
* Humidity or wind speed as circle progress
|
||||
* Temperature inside circle
|
||||
* Condition as icon below circle
|
||||
* Big weather icon next to clock
|
||||
* Time and progress until next sunrise or sunset (requires [my location app](https://banglejs.com/apps/#mylocation))
|
||||
* Temperature, air pressure or altitude from internal pressure sensor
|
||||
|
||||
|
@ -27,6 +28,8 @@ The color of each circle can be configured. The following colors are available:
|
|||

|
||||

|
||||

|
||||

|
||||
|
||||
|
||||
## Ideas
|
||||
* Show compass heading
|
||||
|
@ -35,4 +38,5 @@ The color of each circle can be configured. The following colors are available:
|
|||
Marco ([myxor](https://github.com/myxor))
|
||||
|
||||
## Icons
|
||||
Icons taken from [materialdesignicons](https://materialdesignicons.com) under Apache License 2.0
|
||||
Most of the icons are taken from [materialdesignicons](https://materialdesignicons.com) under Apache License 2.0 except the big weather icons which are from
|
||||
[icons8](https://icons8.com/icon/set/weather/small--static--black)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
const locale = require("locale");
|
||||
const storage = require("Storage");
|
||||
let locale = require("locale");
|
||||
let storage = require("Storage");
|
||||
Graphics.prototype.setFontRobotoRegular50NumericOnly = function(scale) {
|
||||
// Actual height 39 (40 - 2)
|
||||
this.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAAAB8AAAAAAAfAAAAAAAPwAAAAAAB8AAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAA4AAAAAAB+AAAAAAD/gAAAAAD/4AAAAAH/4AAAAAP/wAAAAAP/gAAAAAf/gAAAAAf/AAAAAA/+AAAAAB/+AAAAAB/8AAAAAD/4AAAAAH/4AAAAAD/wAAAAAA/wAAAAAAPgAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA///wAAAB////gAAA////8AAA/////gAAP////8AAH8AAA/gAB8AAAD4AA+AAAAfAAPAAAADwADwAAAA8AA8AAAAPAAPAAAADwADwAAAA8AA8AAAAPAAPgAAAHwAB8AAAD4AAfwAAD+AAD/////AAA/////wAAH////4AAAf///4AAAB///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAAAAPgAAAAAADwAAAAAAB8AAAAAAAfAAAAAAAHgAAAAAAD4AAAAAAA+AAAAAAAPAAAAAAAH/////wAB/////8AA//////AAP/////wAD/////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAAAfgAADwAAP4AAB8AAH+AAA/AAD/gAAfwAB/AAAf8AAfAAAP/AAPgAAH7wAD4AAD88AA8AAB+PAAPAAA/DwADwAAfg8AA8AAPwPAAPAAH4DwADwAH8A8AA+AD+APAAPwB/ADwAB/D/gA8AAf//gAPAAD//wADwAAf/wAA8AAD/4AAPAAAHwAADwAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAADgAAAHwAA+AAAD8AAP4AAB/AAD/AAA/wAA/wAAf4AAD+AAHwAAAPgAD4APAB8AA+ADwAPAAPAA8ADwADwAPAA8AA8ADwAPAAPAA8ADwADwAfAA8AA8AH4APAAPgD+AHwAB8B/wD4AAf7/+B+AAD//v//AAA//x//wAAD/4P/4AAAf8B/4AAAAYAH4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAAAAAHwAAAAAAH8AAAAAAD/AAAAAAD/wAAAAAD/8AAAAAB/vAAAAAB/jwAAAAA/g8AAAAA/wPAAAAAfwDwAAAAf4A8AAAAf4APAAAAP8ADwAAAP8AA8AAAH8AAPAAAD/////8AA//////AAP/////wAD/////8AA//////AAAAAAPAAAAAAADwAAAAAAA8AAAAAAAPAAAAAAADwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAAB/APwAAH//wD+AAD//8A/wAA///AH+AAP//wAPgAD/B4AB8AA8A+AAfAAPAPAADwADwDwAA8AA8A8AAPAAPAPAADwADwD4AA8AA8A+AAPAAPAPwAHwADwD8AD4AA8AfwD+AAPAH///AADwA///wAA8AH//4AAPAAf/4AAAAAB/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//AAAAAD//+AAAAD///4AAAD////AAAB////4AAA/78D/AAAfw8AH4AAPweAA+AAD4PgAHwAB8DwAA8AAfA8AAPAAHgPAADwAD4DwAA8AA+A8AAPAAPAPgAHwADwD4AB8AA8AfgA+AAPAH+B/gAAAA///wAAAAH//4AAAAA//8AAAAAH/8AAAAAAP4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAAAAAA8AAAAAAAPAAAAAAADwAAAAAAA8AAAABAAPAAAABwADwAAAB8AA8AAAB/AAPAAAB/wADwAAD/8AA8AAD/8AAPAAD/4AADwAD/4AAA8AD/4AAAPAH/wAAADwH/wAAAA8H/wAAAAPH/wAAAAD3/gAAAAA//gAAAAAP/gAAAAAD/gAAAAAA/AAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwA/4AAAH/Af/AAAH/8P/4AAD//n//AAA//7//4AAfx/+A+AAHwD+AHwAD4AfgB8AA8AHwAPAAPAA8ADwADwAPAA8AA8ADwAPAAPAA8ADwADwAfAA8AA+AH4AfAAHwD+AHwAB/D/4D4AAP/+/n+AAD//n//AAAf/w//gAAB/wH/wAAAHwA/4AAAAAABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAAAAD/8AAAAAD//wAAAAB//+AAAAA///wAAAAf4H+APAAH4AfgDwAD8AB8A8AA+AAfAPAAPAADwDwADwAA8B8AA8AAPAfAAPAADwHgADwAA8D4AA+AAeB+AAHwAHg/AAB+ADwfgAAP8D4/4AAD////8AAAf///8AAAB///+AAAAP//+AAAAAP/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAOAAAB8AAHwAAAfgAD8AAAH4AA/AAAB8AAHwAAAOAAA4AAAAAAAAAAAAAAAAAAAAAAAAAA"), 46, atob("DRUcHBwcHBwcHBwcDA=="), 50+(scale<<8)+(1<<16));
|
||||
|
@ -12,7 +12,7 @@ Graphics.prototype.setFontRobotoRegular21 = function(scale) {
|
|||
return this;
|
||||
};
|
||||
|
||||
const SETTINGS_FILE = "circlesclock.json";
|
||||
let SETTINGS_FILE = "circlesclock.json";
|
||||
let settings = Object.assign(
|
||||
storage.readJSON("circlesclock.default.json", true) || {},
|
||||
storage.readJSON(SETTINGS_FILE, true) || {}
|
||||
|
@ -22,13 +22,16 @@ let settings = Object.assign(
|
|||
if (settings.stepGoal == undefined) {
|
||||
let d = storage.readJSON("health.json", true) || {};
|
||||
settings.stepGoal = d != undefined && d.settings != undefined ? d.settings.stepGoal : undefined;
|
||||
|
||||
if (settings.stepGoal == undefined) {
|
||||
|
||||
if (settings.stepGoal == undefined) {
|
||||
d = storage.readJSON("wpedom.json", true) || {};
|
||||
settings.stepGoal = d != undefined && d.settings != undefined ? d.settings.goal : 10000;
|
||||
}
|
||||
}
|
||||
|
||||
let timerHrm;
|
||||
let drawTimeout;
|
||||
|
||||
/*
|
||||
* Read location from myLocation app
|
||||
*/
|
||||
|
@ -37,29 +40,30 @@ function getLocation() {
|
|||
}
|
||||
let location = getLocation();
|
||||
|
||||
const showWidgets = settings.showWidgets || false;
|
||||
const circleCount = settings.circleCount || 3;
|
||||
let showWidgets = settings.showWidgets || false;
|
||||
let circleCount = settings.circleCount || 3;
|
||||
let showBigWeather = settings.showBigWeather || false;
|
||||
|
||||
let hrtValue;
|
||||
let now = Math.round(new Date().getTime() / 1000);
|
||||
|
||||
|
||||
// layout values:
|
||||
const colorFg = g.theme.dark ? '#fff' : '#000';
|
||||
const colorBg = g.theme.dark ? '#000' : '#fff';
|
||||
const colorGrey = '#808080';
|
||||
const colorRed = '#ff0000';
|
||||
const colorGreen = '#008000';
|
||||
const colorBlue = '#0000ff';
|
||||
const colorYellow = '#ffff00';
|
||||
const widgetOffset = showWidgets ? 24 : 0;
|
||||
const dowOffset = circleCount == 3 ? 20 : 22; // dow offset relative to date
|
||||
const h = g.getHeight() - widgetOffset;
|
||||
const w = g.getWidth();
|
||||
const hOffset = (circleCount == 3 ? 34 : 30) - widgetOffset;
|
||||
const h1 = Math.round(1 * h / 5 - hOffset);
|
||||
const h2 = Math.round(3 * h / 5 - hOffset);
|
||||
const h3 = Math.round(8 * h / 8 - hOffset - 3); // circle y position
|
||||
let colorFg = g.theme.dark ? '#fff' : '#000';
|
||||
let colorBg = g.theme.dark ? '#000' : '#fff';
|
||||
let colorGrey = '#808080';
|
||||
let colorRed = '#ff0000';
|
||||
let colorGreen = '#008000';
|
||||
let colorBlue = '#0000ff';
|
||||
let colorYellow = '#ffff00';
|
||||
let widgetOffset = showWidgets ? 24 : 0;
|
||||
let dowOffset = circleCount == 3 ? 20 : 22; // dow offset relative to date
|
||||
let h = g.getHeight() - widgetOffset;
|
||||
let w = g.getWidth();
|
||||
let hOffset = (circleCount == 3 ? 34 : 30) - widgetOffset;
|
||||
let h1 = Math.round(1 * h / 5 - hOffset);
|
||||
let h2 = Math.round(3 * h / 5 - hOffset);
|
||||
let h3 = Math.round(8 * h / 8 - hOffset - 3); // circle y position
|
||||
|
||||
/*
|
||||
* circle x positions
|
||||
|
@ -73,21 +77,22 @@ const h3 = Math.round(8 * h / 8 - hOffset - 3); // circle y position
|
|||
* | (1) (2) (3) (4) |
|
||||
* => circles start at 1,3,5,7 / 8
|
||||
*/
|
||||
const parts = circleCount * 2;
|
||||
const circlePosX = [
|
||||
let parts = circleCount * 2;
|
||||
let circlePosX = [
|
||||
Math.round(1 * w / parts), // circle1
|
||||
Math.round(3 * w / parts), // circle2
|
||||
Math.round(5 * w / parts), // circle3
|
||||
Math.round(7 * w / parts), // circle4
|
||||
];
|
||||
|
||||
const radiusOuter = circleCount == 3 ? 25 : 20;
|
||||
const radiusInner = circleCount == 3 ? 20 : 15;
|
||||
const circleFontSmall = circleCount == 3 ? "Vector:14" : "Vector:10";
|
||||
const circleFont = circleCount == 3 ? "Vector:15" : "Vector:11";
|
||||
const circleFontBig = circleCount == 3 ? "Vector:16" : "Vector:12";
|
||||
const iconOffset = circleCount == 3 ? 6 : 8;
|
||||
const defaultCircleTypes = ["steps", "hr", "battery", "weather"];
|
||||
let radiusOuter = circleCount == 3 ? 25 : 20;
|
||||
let radiusInner = circleCount == 3 ? 20 : 15;
|
||||
let circleFontSmall = circleCount == 3 ? "Vector:14" : "Vector:10";
|
||||
let circleFont = circleCount == 3 ? "Vector:15" : "Vector:11";
|
||||
let circleFontBig = circleCount == 3 ? "Vector:16" : "Vector:12";
|
||||
let iconOffset = circleCount == 3 ? 6 : 8;
|
||||
let defaultCircleTypes = ["steps", "hr", "battery", "weather"];
|
||||
|
||||
|
||||
function hideWidgets() {
|
||||
/*
|
||||
|
@ -105,9 +110,16 @@ function hideWidgets() {
|
|||
|
||||
function draw() {
|
||||
g.clear(true);
|
||||
let widgetUtils;
|
||||
|
||||
try {
|
||||
widgetUtils = require("widget_utils");
|
||||
} catch (e) {
|
||||
}
|
||||
if (!showWidgets) {
|
||||
hideWidgets();
|
||||
if (widgetUtils) widgetUtils.hide(); else hideWidgets();
|
||||
} else {
|
||||
if (widgetUtils) widgetUtils.show();
|
||||
Bangle.drawWidgets();
|
||||
}
|
||||
|
||||
|
@ -116,27 +128,53 @@ function draw() {
|
|||
|
||||
// time
|
||||
g.setFontRobotoRegular50NumericOnly();
|
||||
g.setFontAlign(0, -1);
|
||||
g.setColor(colorFg);
|
||||
g.drawString(locale.time(new Date(), 1), w / 2, h1 + 6);
|
||||
if (!showBigWeather) {
|
||||
g.setFontAlign(0, -1);
|
||||
g.drawString(locale.time(new Date(), 1), w / 2, h1 + 6);
|
||||
}
|
||||
else {
|
||||
g.setFontAlign(-1, -1);
|
||||
g.drawString(locale.time(new Date(), 1), 2, h1 + 6);
|
||||
}
|
||||
now = Math.round(new Date().getTime() / 1000);
|
||||
|
||||
// date & dow
|
||||
g.setFontRobotoRegular21();
|
||||
g.setFontAlign(0, 0);
|
||||
g.drawString(locale.date(new Date()), w / 2, h2);
|
||||
g.drawString(locale.dow(new Date()), w / 2, h2 + dowOffset);
|
||||
|
||||
if (!showBigWeather) {
|
||||
g.setFontAlign(0, 0);
|
||||
g.drawString(locale.date(new Date()), w / 2, h2);
|
||||
g.drawString(locale.dow(new Date()), w / 2, h2 + dowOffset);
|
||||
} else {
|
||||
g.setFontAlign(-1, 0);
|
||||
g.drawString(locale.date(new Date()), 2, h2);
|
||||
g.drawString(locale.dow(new Date()), 2, h2 + dowOffset, 1);
|
||||
}
|
||||
|
||||
// weather
|
||||
if (showBigWeather) {
|
||||
let weather = getWeather();
|
||||
let tempString = weather ? locale.temp(weather.temp - 273.15) : undefined;
|
||||
g.setFontAlign(1, 0);
|
||||
if (tempString) g.drawString(tempString, w, h2);
|
||||
|
||||
let code = weather ? weather.code : -1;
|
||||
let icon = getWeatherIconByCode(code, true);
|
||||
if (icon) g.drawImage(icon, w - 48, h1, {scale:0.75});
|
||||
}
|
||||
|
||||
drawCircle(1);
|
||||
drawCircle(2);
|
||||
drawCircle(3);
|
||||
if (circleCount >= 4) drawCircle(4);
|
||||
|
||||
queueDraw();
|
||||
}
|
||||
|
||||
function drawCircle(index) {
|
||||
let type = settings['circle' + index];
|
||||
if (!type) type = defaultCircleTypes[index - 1];
|
||||
const w = getCircleXPosition(type);
|
||||
let w = getCircleXPosition(type);
|
||||
|
||||
switch (type) {
|
||||
case "steps":
|
||||
|
@ -188,7 +226,7 @@ function getCirclePosition(type) {
|
|||
return circlePositionsCache[type];
|
||||
}
|
||||
for (let i = 1; i <= circleCount; i++) {
|
||||
const setting = settings['circle' + i];
|
||||
let setting = settings['circle' + i];
|
||||
if (setting == type) {
|
||||
circlePositionsCache[type] = i - 1;
|
||||
return i - 1;
|
||||
|
@ -204,7 +242,7 @@ function getCirclePosition(type) {
|
|||
}
|
||||
|
||||
function getCircleXPosition(type) {
|
||||
const circlePos = getCirclePosition(type);
|
||||
let circlePos = getCirclePosition(type);
|
||||
if (circlePos != undefined) {
|
||||
return circlePosX[circlePos];
|
||||
}
|
||||
|
@ -216,14 +254,14 @@ function isCircleEnabled(type) {
|
|||
}
|
||||
|
||||
function getCircleColor(type) {
|
||||
const pos = getCirclePosition(type);
|
||||
const color = settings["circle" + (pos + 1) + "color"];
|
||||
let pos = getCirclePosition(type);
|
||||
let color = settings["circle" + (pos + 1) + "color"];
|
||||
if (color && color != "") return color;
|
||||
}
|
||||
|
||||
function getCircleIconColor(type, color, percent) {
|
||||
const pos = getCirclePosition(type);
|
||||
const colorizeIcon = settings["circle" + (pos + 1) + "colorizeIcon"] == true;
|
||||
let pos = getCirclePosition(type);
|
||||
let colorizeIcon = settings["circle" + (pos + 1) + "colorizeIcon"] == true;
|
||||
if (colorizeIcon) {
|
||||
return getGradientColor(color, percent);
|
||||
} else {
|
||||
|
@ -234,18 +272,18 @@ function getCircleIconColor(type, color, percent) {
|
|||
function getGradientColor(color, percent) {
|
||||
if (isNaN(percent)) percent = 0;
|
||||
if (percent > 1) percent = 1;
|
||||
const colorList = [
|
||||
let colorList = [
|
||||
'#00FF00', '#80FF00', '#FFFF00', '#FF8000', '#FF0000'
|
||||
];
|
||||
if (color == "fg") {
|
||||
color = colorFg;
|
||||
}
|
||||
if (color == "green-red") {
|
||||
const colorIndex = Math.round(colorList.length * percent);
|
||||
let colorIndex = Math.round(colorList.length * percent);
|
||||
return colorList[Math.min(colorIndex, colorList.length) - 1] || "#00ff00";
|
||||
}
|
||||
if (color == "red-green") {
|
||||
const colorIndex = colorList.length - Math.round(colorList.length * percent);
|
||||
let colorIndex = colorList.length - Math.round(colorList.length * percent);
|
||||
return colorList[Math.min(colorIndex, colorList.length)] || "#ff0000";
|
||||
}
|
||||
return color;
|
||||
|
@ -268,14 +306,14 @@ function getImage(graphic, color) {
|
|||
|
||||
function drawSteps(w) {
|
||||
if (!w) w = getCircleXPosition("steps");
|
||||
const steps = getSteps();
|
||||
let steps = getSteps();
|
||||
|
||||
drawCircleBackground(w);
|
||||
|
||||
const color = getCircleColor("steps");
|
||||
let color = getCircleColor("steps");
|
||||
|
||||
let percent;
|
||||
const stepGoal = settings.stepGoal;
|
||||
let stepGoal = settings.stepGoal;
|
||||
if (stepGoal > 0) {
|
||||
percent = steps / stepGoal;
|
||||
if (stepGoal < steps) percent = 1;
|
||||
|
@ -291,16 +329,16 @@ function drawSteps(w) {
|
|||
|
||||
function drawStepsDistance(w) {
|
||||
if (!w) w = getCircleXPosition("stepsDistance");
|
||||
const steps = getSteps();
|
||||
const stepDistance = settings.stepLength;
|
||||
const stepsDistance = Math.round(steps * stepDistance);
|
||||
let steps = getSteps();
|
||||
let stepDistance = settings.stepLength;
|
||||
let stepsDistance = Math.round(steps * stepDistance);
|
||||
|
||||
drawCircleBackground(w);
|
||||
|
||||
const color = getCircleColor("stepsDistance");
|
||||
let color = getCircleColor("stepsDistance");
|
||||
|
||||
let percent;
|
||||
const stepDistanceGoal = settings.stepDistanceGoal;
|
||||
let stepDistanceGoal = settings.stepDistanceGoal;
|
||||
if (stepDistanceGoal > 0) {
|
||||
percent = stepsDistance / stepDistanceGoal;
|
||||
if (stepDistanceGoal < stepsDistance) percent = 1;
|
||||
|
@ -317,16 +355,16 @@ function drawStepsDistance(w) {
|
|||
function drawHeartRate(w) {
|
||||
if (!w) w = getCircleXPosition("hr");
|
||||
|
||||
const heartIcon = atob("EBCBAAAAAAAeeD/8P/x//n/+P/w//B/4D/AH4APAAYAAAAAA");
|
||||
let heartIcon = atob("EBCBAAAAAAAeeD/8P/x//n/+P/w//B/4D/AH4APAAYAAAAAA");
|
||||
|
||||
drawCircleBackground(w);
|
||||
|
||||
const color = getCircleColor("hr");
|
||||
let color = getCircleColor("hr");
|
||||
|
||||
let percent;
|
||||
if (hrtValue != undefined) {
|
||||
const minHR = settings.minHR;
|
||||
const maxHR = settings.maxHR;
|
||||
let minHR = settings.minHR;
|
||||
let maxHR = settings.maxHR;
|
||||
percent = (hrtValue - minHR) / (maxHR - minHR);
|
||||
if (isNaN(percent)) percent = 0;
|
||||
drawGauge(w, h3, percent, color);
|
||||
|
@ -341,9 +379,9 @@ function drawHeartRate(w) {
|
|||
|
||||
function drawBattery(w) {
|
||||
if (!w) w = getCircleXPosition("battery");
|
||||
const battery = E.getBattery();
|
||||
let battery = E.getBattery();
|
||||
|
||||
const powerIcon = atob("EBCBAAAAA8ADwA/wD/AP8A/wD/AP8A/wD/AP8A/wD/AH4AAA");
|
||||
let powerIcon = atob("EBCBAAAAA8ADwA/wD/AP8A/wD/AP8A/wD/AP8A/wD/AH4AAA");
|
||||
|
||||
drawCircleBackground(w);
|
||||
|
||||
|
@ -371,18 +409,18 @@ function drawBattery(w) {
|
|||
|
||||
function drawWeather(w) {
|
||||
if (!w) w = getCircleXPosition("weather");
|
||||
const weather = getWeather();
|
||||
const tempString = weather ? locale.temp(weather.temp - 273.15) : undefined;
|
||||
const code = weather ? weather.code : -1;
|
||||
let weather = getWeather();
|
||||
let tempString = weather ? locale.temp(weather.temp - 273.15) : undefined;
|
||||
let code = weather ? weather.code : -1;
|
||||
|
||||
drawCircleBackground(w);
|
||||
|
||||
const color = getCircleColor("weather");
|
||||
let color = getCircleColor("weather");
|
||||
let percent;
|
||||
const data = settings.weatherCircleData;
|
||||
let data = settings.weatherCircleData;
|
||||
switch (data) {
|
||||
case "humidity":
|
||||
const humidity = weather ? weather.hum : undefined;
|
||||
let humidity = weather ? weather.hum : undefined;
|
||||
if (humidity >= 0) {
|
||||
percent = humidity / 100;
|
||||
drawGauge(w, h3, percent, color);
|
||||
|
@ -390,7 +428,7 @@ function drawWeather(w) {
|
|||
break;
|
||||
case "wind":
|
||||
if (weather) {
|
||||
const wind = locale.speed(weather.wind).match(/^(\D*\d*)(.*)$/);
|
||||
let wind = locale.speed(weather.wind).match(/^(\D*\d*)(.*)$/);
|
||||
if (wind[1] >= 0) {
|
||||
if (wind[2] == "kmh") {
|
||||
wind[1] = windAsBeaufort(wind[1]);
|
||||
|
@ -410,25 +448,24 @@ function drawWeather(w) {
|
|||
writeCircleText(w, tempString ? tempString : "?");
|
||||
|
||||
if (code > 0) {
|
||||
const icon = getWeatherIconByCode(code);
|
||||
let icon = getWeatherIconByCode(code);
|
||||
if (icon) g.drawImage(getImage(icon, getCircleIconColor("weather", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset);
|
||||
} else {
|
||||
g.drawString("?", w, h3 + radiusOuter);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function drawSunProgress(w) {
|
||||
if (!w) w = getCircleXPosition("sunprogress");
|
||||
const percent = getSunProgress();
|
||||
let percent = getSunProgress();
|
||||
|
||||
// sunset icons:
|
||||
const sunSetDown = atob("EBCBAAAAAAABgAAAAAATyAZoBCB//gAAAAAGYAPAAYAAAAAA");
|
||||
const sunSetUp = atob("EBCBAAAAAAABgAAAAAATyAZoBCB//gAAAAABgAPABmAAAAAA");
|
||||
let sunSetDown = atob("EBCBAAAAAAABgAAAAAATyAZoBCB//gAAAAAGYAPAAYAAAAAA");
|
||||
let sunSetUp = atob("EBCBAAAAAAABgAAAAAATyAZoBCB//gAAAAABgAPABmAAAAAA");
|
||||
|
||||
drawCircleBackground(w);
|
||||
|
||||
const color = getCircleColor("sunprogress");
|
||||
let color = getCircleColor("sunprogress");
|
||||
|
||||
drawGauge(w, h3, percent, color);
|
||||
|
||||
|
@ -436,15 +473,15 @@ function drawSunProgress(w) {
|
|||
|
||||
let icon = sunSetDown;
|
||||
let text = "?";
|
||||
const times = getSunData();
|
||||
let times = getSunData();
|
||||
if (times != undefined) {
|
||||
const sunRise = Math.round(times.sunrise.getTime() / 1000);
|
||||
const sunSet = Math.round(times.sunset.getTime() / 1000);
|
||||
let sunRise = Math.round(times.sunrise.getTime() / 1000);
|
||||
let sunSet = Math.round(times.sunset.getTime() / 1000);
|
||||
if (!isDay()) {
|
||||
// night
|
||||
if (now > sunRise) {
|
||||
// after sunRise
|
||||
const upcomingSunRise = sunRise + 60 * 60 * 24;
|
||||
let upcomingSunRise = sunRise + 60 * 60 * 24;
|
||||
text = formatSeconds(upcomingSunRise - now);
|
||||
} else {
|
||||
text = formatSeconds(sunRise - now);
|
||||
|
@ -468,12 +505,12 @@ function drawTemperature(w) {
|
|||
getPressureValue("temperature").then((temperature) => {
|
||||
drawCircleBackground(w);
|
||||
|
||||
const color = getCircleColor("temperature");
|
||||
let color = getCircleColor("temperature");
|
||||
|
||||
let percent;
|
||||
if (temperature) {
|
||||
const min = -40;
|
||||
const max = 85;
|
||||
let min = -40;
|
||||
let max = 85;
|
||||
percent = (temperature - min) / (max - min);
|
||||
drawGauge(w, h3, percent, color);
|
||||
}
|
||||
|
@ -482,7 +519,7 @@ function drawTemperature(w) {
|
|||
|
||||
if (temperature)
|
||||
writeCircleText(w, locale.temp(temperature));
|
||||
|
||||
|
||||
g.drawImage(getImage(atob("EBCBAAAAAYADwAJAAkADwAPAA8ADwAfgB+AH4AfgA8ABgAAA"), getCircleIconColor("temperature", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset);
|
||||
|
||||
});
|
||||
|
@ -494,12 +531,12 @@ function drawPressure(w) {
|
|||
getPressureValue("pressure").then((pressure) => {
|
||||
drawCircleBackground(w);
|
||||
|
||||
const color = getCircleColor("pressure");
|
||||
let color = getCircleColor("pressure");
|
||||
|
||||
let percent;
|
||||
if (pressure && pressure > 0) {
|
||||
const minPressure = 950;
|
||||
const maxPressure = 1050;
|
||||
let minPressure = 950;
|
||||
let maxPressure = 1050;
|
||||
percent = (pressure - minPressure) / (maxPressure - minPressure);
|
||||
drawGauge(w, h3, percent, color);
|
||||
}
|
||||
|
@ -520,12 +557,12 @@ function drawAltitude(w) {
|
|||
getPressureValue("altitude").then((altitude) => {
|
||||
drawCircleBackground(w);
|
||||
|
||||
const color = getCircleColor("altitude");
|
||||
let color = getCircleColor("altitude");
|
||||
|
||||
let percent;
|
||||
if (altitude) {
|
||||
const min = 0;
|
||||
const max = 10000;
|
||||
let min = 0;
|
||||
let max = 10000;
|
||||
percent = (altitude - min) / (max - min);
|
||||
drawGauge(w, h3, percent, color);
|
||||
}
|
||||
|
@ -544,7 +581,7 @@ function drawAltitude(w) {
|
|||
* wind goes from 0 to 12 (see https://en.wikipedia.org/wiki/Beaufort_scale)
|
||||
*/
|
||||
function windAsBeaufort(windInKmh) {
|
||||
const beaufort = [2, 6, 12, 20, 29, 39, 50, 62, 75, 89, 103, 118];
|
||||
let beaufort = [2, 6, 12, 20, 29, 39, 50, 62, 75, 89, 103, 118];
|
||||
let l = 0;
|
||||
while (l < beaufort.length && beaufort[l] < windInKmh) {
|
||||
l++;
|
||||
|
@ -557,20 +594,22 @@ function windAsBeaufort(windInKmh) {
|
|||
* Choose weather icon to display based on weather conditition code
|
||||
* https://openweathermap.org/weather-conditions#Weather-Condition-Codes-2
|
||||
*/
|
||||
function getWeatherIconByCode(code) {
|
||||
const codeGroup = Math.round(code / 100);
|
||||
function getWeatherIconByCode(code, big) {
|
||||
let codeGroup = Math.round(code / 100);
|
||||
if (big == undefined) big = false;
|
||||
|
||||
// weather icons:
|
||||
const weatherCloudy = atob("EBCBAAAAAAAAAAfgD/Af8H/4//7///////9//z/+AAAAAAAA");
|
||||
const weatherSunny = atob("EBCBAAAAAYAQCBAIA8AH4A/wb/YP8A/gB+ARiBAIAYABgAAA");
|
||||
const weatherMoon = atob("EBCBAAAAAYAP8B/4P/w//D/8f/5//j/8P/w//B/4D/ABgAAA");
|
||||
const weatherPartlyCloudy = atob("EBCBAAAAAAAYQAMAD8AIQBhoW+AOYBwwOBBgHGAGP/wf+AAA");
|
||||
const weatherRainy = atob("EBCBAAAAAYAH4AwwOBBgGEAOQAJBgjPOEkgGYAZgA8ABgAAA");
|
||||
const weatherPartlyRainy = atob("EBCBAAAAEEAQAAeADMAYaFvoTmAMMDgQIBxhhiGGG9wDwAGA");
|
||||
const weatherSnowy = atob("EBCBAAAAAAADwAGAEYg73C50BCAEIC50O9wRiAGAA8AAAAAA");
|
||||
const weatherFoggy = atob("EBCBAAAAAAADwAZgDDA4EGAcQAZAAgAAf74AAAAAd/4AAAAA");
|
||||
const weatherStormy = atob("EBCBAAAAAYAH4AwwOBBgGEAOQMJAgjmOGcgAgACAAAAAAAAA");
|
||||
|
||||
let weatherCloudy = big ? atob("QEDBAP//wxgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AAAAAAAAB//gAAAAAAAP//gAAAAAAD///AAAAAAAf4H+AAAAAAD8AD8AAAAAAfgAH4AAAAAB8AAPwAAAAAPgAAf/AAAAB8AAA//AAAAHgAAB/+AAAAeAAAH/8AAAH4AAAIH4AAB/AAAAAHwAAf8AAAAAPgAD/wAAAAAeAAPwAAAAAB4AB8AAAAAADwAHgAAAAAAPAA+AAAAAAA8ADwAAAAAADwA/AAAAAAAPAH8AAAAAAA8A/wAAAAAAHwH4AAAAAAAfg+AAAAAAAAfHwAAAAAAAA+eAAAAAAAAB54AAAAAAAAHvAAAAAAAAAP8AAAAAAAAA/wAAAAAAAAD/AAAAAAAAAP8AAAAAAAAA/wAAAAAAAAD3gAAAAAAAAeeAAAAAAAAB58AAAAAAAAPj4AAAAAAAB8H4AAAAAAAfgP////////8Af////////gA////////8AAf//////+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==") : atob("EBCBAAAAAAAAAAfgD/Af8H/4//7///////9//z/+AAAAAAAA");
|
||||
let weatherSunny = big ? atob("QEDBAP//wxgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAwADwADAAAAHgAPAAeAAAAfAA8AD4AAAA+ADwAfAAAAB8APAD4AAAAD4B+AfAAAAAHw//D4AAAAAPv//fAAAAAAf///4AAAAAA/4H/AAAAAAB+AH4AAAAAAPgAHwAAAAAA8AAPAAAAAAHwAA+AAAAAAeAAB4AAAAAB4AAHgAAAAAPAAAPAAAA//8AAA//8AD//wAAD//wAP//AAAP//AA//8AAA//8AAADwAADwAAAAAHgAAeAAAAAAeAAB4AAAAAB8AAPgAAAAADwAA8AAAAAAPgAHwAAAAAAfgB+AAAAAAD/gf8AAAAAAf///4AAAAAD7//3wAAAAAfD/8PgAAAAD4B+AfAAAAAfADwA+AAAAD4APAB8AAAAfAA8AD4AAAB4ADwAHgAAADAAPAAMAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==") : atob("EBCBAAAAAYAQCBAIA8AH4A/wb/YP8A/gB+ARiBAIAYABgAAA");
|
||||
let weatherMoon = big ? atob("QEDBAP//wxgAAAYAAAAPAAAAD4AAAA8AAAAPwAAADwAAAA/gAAAPAAAAB/APAP/wAAAH+A8A//AAAAf4DwD/8AAAB/wPAP/wAAAH/gAADwAAAAe+AAAPAAAAB54AAA8AAAAHngAADwAAAAePAAAAAAAAD48OAAAAAAAPDw+AAAAAAB8PD8AAAAAAHg8P4AAAAAA+DwPwAAAAAHwfAfgAAAAB+D4A/AAA8AfwfgB/8AD//+D+AD/8AP//wfgAH/4Af/8B8AAf/wB//APgAAgfgD+AA8AAAAfAH8AHwAAAA+AP8B+AAAAB4Af//4AAAAHgA///gAAAAPAA//8AAAAA8AAf/wAAAADwAAAAAAAAAPAAAAAAAAAA8AcAAAAAAADwD+AAAAAAAfAfgAAAAAAB+D4AAAAAAAB8fAAAAAAAAD54AAAAAAAAHngAAAAAAAAe8AAAAAAAAA/wAAAAAAAAD/AAAAAAAAAP8AAAAAAAAA/wAAAAAAAAD/AAAAAAAAAPeAAAAAAAAB54AAAAAAAAHnwAAAAAAAA+PgAAAAAAAHwfgAAAAAAB+A/////////wB////////+AD////////wAB///////4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==") : atob("EBCBAAAAAYAP8B/4P/w//D/8f/5//j/8P/w//B/4D/ABgAAA");
|
||||
let weatherPartlyCloudy = big ? atob("QEDBAP//wxgAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAABwAPAA4AAAAHgA8AHgAAAAfADwA+AAAAA+AfgHwAAAAB8P/w+AAAAAD7//3wAAAAAH///+BAAAAAP+B/wOAAAAAfgB+B8AAAAD4AD8H4AAAAPAA/wPwAAAB8AH+Af/AAAHgA/AA//AAAeAH4AB/+AADwAfAAH/8A//AD4AAIH4D/8AfAAAAHwP/wB4AAAAPg//AHgAAAAeAA8B+AAAAB4AB4fwAAAADwAHn/AAAAAPAAff8AAAAA8AA/8AAAAADwAD/AAAAAAPAEH4AAAAAA8A4PgAAAAAHwHgcAAAAAAfg+AwAAAAAAfHwAAAAAAAA+eAAAAAAAAB54AAAAAAAAHvAAAAAAAAAP8AAAAAAAAA/wAAAAAAAAD/AAAAAAAAAP8AAAAAAAAA/wAAAAAAAAD3gAAAAAAAAeeAAAAAAAAB58AAAAAAAAPj4AAAAAAAB8H4AAAAAAAfgP////////8Af////////gA////////8AAf//////+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==") : atob("EBCBAAAAAAAYQAMAD8AIQBhoW+AOYBwwOBBgHGAGP/wf+AAA");
|
||||
let weatherRainy = big ? atob("QEDBAP//wxgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AAAAAAAAB//gAAAAAAAP//gAAAAAAD///AAAAAAAf4H+AAAAAAD8AD8AAAAAAfgAH4AAAAAB8AAPwAAAAAPgAAf/AAAAB8AAA//AAAAHgAAB/+AAAAeAAAH/8AAAH4AAAIH4AAB/AAAAAHwAAf8AAAAAPgAD/wAAAAAeAAPwAAAAAB4AB8AAAAAADwAHgAAAAAAPAA+AAAAAAA8ADwAAAAAADwA/AAAAAAAPAH8AAAAAAA8A/wAAAAAAHwH4APAA8AAfg+AA8ADwAAfHwADwAPAAA+eAAPAA8AAB54AAAAAAAAHvAAAAAAAAAP8AAAAAAAAA/wAAAAAAAAD/AADw8PDwAP8AAPDw8PAA/wAA8PDw8AD3gADw8PDwAeeAAAAAAAAB58AAAAAAAAPj4AAAAAAAB8H4AAAAAAAfgP/w8PDw8P8Af/Dw8PDw/gA/8PDw8PD8AAfw8PDw8OAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAPAAAAAAAPAA8AAAAAAA8ADwAAAAAADwAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==") : atob("EBCBAAAAAYAH4AwwOBBgGEAOQAJBgjPOEkgGYAZgA8ABgAAA");
|
||||
let weatherPartlyRainy = big ? atob("QEDBAP//wxgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AAAAAAAAB//gAAAAAAAP//gAAAAAAD///AAAAAAAf4H+AAAAAAD8AD8AAAAAAfgAH4AAAAAB8AAPwAAAAAPgAAf/AAAAB8AAA//AAAAHgAAB/+AAAAeAAAH/8AAAH4AAAIH4AAB/AAAAAHwAAf8AAAAAPgAD/wAAAAAeAAPwAAAAAB4AB8AAAAAADwAHgAAAAAAPAA+AAAAAAA8ADwAAAAAADwA/AAAAAAAPAH8AAAAAAA8A/wAAAAAAHwH4AAAA8AAfg+AAAADwAAfHwAAAAPAAA+eAAAAA8AAB54AAAADwAAHvAAAAAPAAAP8AAAAA8AAA/wAAAADwAAD/AAAA8PAAAP8AAADw8AAA/wAAAPDwAAD3gAAA8PAAAeeAAADw8AAB58AAAPDwAAPj4AAA8PAAB8H4AADw8AAfgP//8PDw//8Af//w8PD//gA///Dw8P/8AAf/8PDw/+AAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==") : atob("EBCBAAAAEEAQAAeADMAYaFvoTmAMMDgQIBxhhiGGG9wDwAGA");
|
||||
let weatherSnowy = big ? atob("QEDBAP//wxgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AAAAAAAAB//gAAAAAAAP//gAAAAAAD///AAAAAAAf4H+AAAAAAD8AD8AAAAAAfgAH4AAAAAB8AAPwAAAAAPgAAf/AAAAB8AAA//AAAAHgAAB/+AAAAeAAAH/8AAAH4AAAIH4AAB/AAAAAHwAAf8AAAAAPgAD/wAAAAAeAAPwAAAAAB4AB8AAAAAADwAHgAAAAAAPAA+AAAAAAA8ADwAAAAAADwA/AAAAAAAPAH8AAAAAAA8A/wAAAAAAHwH4AAAADwAfg+AAAAAPAAfHwAAAAA8AA+eAAAAADwAB54AA8AD/8AHvAADwAP/wAP8AAPAA//AA/wAA8AD/8AD/AA//AA8AAP8AD/8ADwAA/wAP/wAPAAD3gA//AA8AAeeAAPAAAAAB58AA8AAAAAPj4ADwAAAAB8H4APAAAAAfgP/wAA8A//8Af/AADwD//gA/8AAPAP/8AAfwAA8A/+AAAAAA//AAAAAAAAD/8AAAAAAAAP/wAAAAAAAA//AAAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==") : atob("EBCBAAAAAAADwAGAEYg73C50BCAEIC50O9wRiAGAA8AAAAAA");
|
||||
let weatherFoggy = big ? atob("QEDBAP//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAwADwADAAAAHgAPAAeAAAAfAA8AD4AAAA+ADwAfAAAAB8APAD4AAAAD4B+AfAAAAAHw//D4AAAAAPv//fAAAAAAf///4AAAAAA/4H/AAAAAAB+AH4AAAAAAPgAHwAAAAAA8AAPAAAAAAHwAA+AAAAAAeAAB4AAAAAB4AAHgAAAAAPAAAPAAAAAAAAAA//8AAAAAAAD//wAAAAAAAP//AAAAAAAA//8AD///AADwAAAP//8AAeAAAA///wAB4AAAD///AAPgAAAAAAAAA8AAAAAAAAAHwAAAAAAAAB+AAAAAAAAAf8AAAAD///D/4AAAAP//8P3wAAAA///w8PgAAAD///CAfAAAAAAAAAA+AAAAAAAAAB8AAAAAAAAAD4AAAAAAAAAHgAAP//8PAAMAAA///w8AAAAAD///DwAAAAAP//8PAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==") : atob("EBCBAAAAAAADwAZgDDA4EGAcQAZAAgAAf74AAAAAd/4AAAAA");
|
||||
let weatherStormy = big ? atob("QEDBAP//wxgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AAAAAAAAB//gAAAAAAAP//gAAAAAAD///AAAAAAAf4H+AAAAAAD8AD8AAAAAAfgAH4AAAAAB8AAPwAAAAAPgAAf/AAAAB8AAA//AAAAHgAAB/+AAAAeAAAH/8AAAH4AAAIH4AAB/AAAAAHwAAf8AAAAAPgAD/wAAAAAeAAPwAAAAAB4AB8AAAAAADwAHgAAAAAAPAA+AAAAAAA8ADwAAAAAADwA/AAAAAAAPAH8AAAAAAA8A/wAAAAAAHwH4AAAAAAAfg+AAAAAAAAfHwAAAAAAAA+eAAAAAAAAB54AAAAD/AAHvAAAAAf4AAP8AAAAB/gAA/wAAAAP8AAD/AAAAA/gAAP8AAAAH+AAA/wAAAAfwAAD3gAAAD/AAAeeAAAAP4AAB58AAAB/AAAPj4AAAH8AAB8H4AAA/gAAfgP//+D//D/8Af//4f/4P/gA///B//B/8AAf/8P/8P+AAAAAAAPgAAAAAAAAB8AAAAAAAAAHwAAAAAAAAA+AAAAAAAAADwAAAAAAAAAfAAAAAAAAAB4AAAAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==") : atob("EBCBAAAAAYAH4AwwOBBgGEAOQMJAgjmOGcgAgACAAAAAAAAA");
|
||||
let unknown = big ? atob("QEDBAP//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/wAAAAAAAAf/4AAAAAAAH//4AAAAAAA///wAAAAAAH+B/gAAAAAA/AA/AAAAAAH4AB+AAAAAA/AAD4AAAAAD4H4HwAAAAAfB/4PgAAAAB8P/weAAAAAHg//h4AAAAA+Hw+HwAAAAD4eB8PAAAAAP/wDw8AAAAA//APDwAAAAD/8A8PAAAAAH/gDw8AAAAAAAAfDwAAAAAAAH4fAAAAAAAB/B4AAAAAAAf4HgAAAAAAD/A+AAAAAAAfwHwAAAAAAD8A+AAAAAAAPgH4AAAAAAB8B/AAAAAAAHgf4AAAAAAA+H+AAAAAAADwfwAAAAAAAPD8AAAAAAAA8PAAAAAAAAD/8AAAAAAAAP/wAAAAAAAA//AAAAAAAAB/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf+AAAAAAAAD/8AAAAAAAAP/wAAAAAAAA//AAAAAAAADw8AAAAAAAAPDwAAAAAAAA8PAAAAAAAADw8AAAAAAAAP/wAAAAAAAA//AAAAAAAAD/8AAAAAAAAH/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==") : undefined;
|
||||
|
||||
switch (codeGroup) {
|
||||
case 2:
|
||||
return weatherStormy;
|
||||
|
@ -607,16 +646,16 @@ function getWeatherIconByCode(code) {
|
|||
return weatherCloudy;
|
||||
}
|
||||
default:
|
||||
return undefined;
|
||||
return unknown;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function isDay() {
|
||||
const times = getSunData();
|
||||
let times = getSunData();
|
||||
if (times == undefined) return true;
|
||||
const sunRise = Math.round(times.sunrise.getTime() / 1000);
|
||||
const sunSet = Math.round(times.sunset.getTime() / 1000);
|
||||
let sunRise = Math.round(times.sunrise.getTime() / 1000);
|
||||
let sunSet = Math.round(times.sunset.getTime() / 1000);
|
||||
|
||||
return (now > sunRise && now < sunSet);
|
||||
}
|
||||
|
@ -633,7 +672,7 @@ function formatSeconds(s) {
|
|||
|
||||
function getSunData() {
|
||||
if (location != undefined && location.lat != undefined) {
|
||||
const SunCalc = require("https://raw.githubusercontent.com/mourner/suncalc/master/suncalc.js");
|
||||
let SunCalc = require("https://raw.githubusercontent.com/mourner/suncalc/master/suncalc.js");
|
||||
// get today's sunlight times for lat/lon
|
||||
return SunCalc ? SunCalc.getTimes(new Date(), location.lat, location.lon) : undefined;
|
||||
}
|
||||
|
@ -646,14 +685,14 @@ function getSunData() {
|
|||
* Taken from rebble app and modified
|
||||
*/
|
||||
function getSunProgress() {
|
||||
const times = getSunData();
|
||||
let times = getSunData();
|
||||
if (times == undefined) return 0;
|
||||
const sunRise = Math.round(times.sunrise.getTime() / 1000);
|
||||
const sunSet = Math.round(times.sunset.getTime() / 1000);
|
||||
let sunRise = Math.round(times.sunrise.getTime() / 1000);
|
||||
let sunSet = Math.round(times.sunset.getTime() / 1000);
|
||||
|
||||
if (isDay()) {
|
||||
// during day
|
||||
const dayLength = sunSet - sunRise;
|
||||
let dayLength = sunSet - sunRise;
|
||||
if (now > sunRise) {
|
||||
return (now - sunRise) / dayLength;
|
||||
} else {
|
||||
|
@ -662,10 +701,10 @@ function getSunProgress() {
|
|||
} else {
|
||||
// during night
|
||||
if (now < sunRise) {
|
||||
const prevSunSet = sunSet - 60 * 60 * 24;
|
||||
let prevSunSet = sunSet - 60 * 60 * 24;
|
||||
return 1 - (sunRise - now) / (sunRise - prevSunSet);
|
||||
} else {
|
||||
const upcomingSunRise = sunRise + 60 * 60 * 24;
|
||||
let upcomingSunRise = sunRise + 60 * 60 * 24;
|
||||
return (upcomingSunRise - now) / (upcomingSunRise - sunSet);
|
||||
}
|
||||
}
|
||||
|
@ -700,16 +739,16 @@ function radians(a) {
|
|||
* This draws the actual gauge consisting out of lots of little filled circles
|
||||
*/
|
||||
function drawGauge(cx, cy, percent, color) {
|
||||
const offset = 15;
|
||||
const end = 360 - offset;
|
||||
const radius = radiusInner + (circleCount == 3 ? 3 : 2);
|
||||
const size = radiusOuter - radiusInner - 2;
|
||||
let offset = 15;
|
||||
let end = 360 - offset;
|
||||
let radius = radiusInner + (circleCount == 3 ? 3 : 2);
|
||||
let size = radiusOuter - radiusInner - 2;
|
||||
|
||||
if (percent <= 0) return; // no gauge needed
|
||||
if (percent > 1) percent = 1;
|
||||
|
||||
const startRotation = -offset;
|
||||
const endRotation = startRotation - ((end - offset) * percent);
|
||||
let startRotation = -offset;
|
||||
let endRotation = startRotation - ((end - offset) * percent);
|
||||
|
||||
color = getGradientColor(color, percent);
|
||||
g.setColor(color);
|
||||
|
@ -723,7 +762,7 @@ function drawGauge(cx, cy, percent, color) {
|
|||
|
||||
function writeCircleText(w, content) {
|
||||
if (content == undefined) return;
|
||||
const font = String(content).length > 4 ? circleFontSmall : String(content).length > 3 ? circleFont : circleFontBig;
|
||||
let font = String(content).length > 4 ? circleFontSmall : String(content).length > 3 ? circleFont : circleFontBig;
|
||||
g.setFont(font);
|
||||
|
||||
g.setFontAlign(0, 0);
|
||||
|
@ -755,7 +794,7 @@ function getSteps() {
|
|||
}
|
||||
|
||||
function getWeather() {
|
||||
const jsonWeather = storage.readJSON('weather.json');
|
||||
let jsonWeather = storage.readJSON('weather.json');
|
||||
return jsonWeather && jsonWeather.weather ? jsonWeather.weather : undefined;
|
||||
}
|
||||
|
||||
|
@ -796,7 +835,7 @@ function getPressureValue(type) {
|
|||
});
|
||||
}
|
||||
|
||||
Bangle.on('lock', function(isLocked) {
|
||||
function onLock(isLocked) {
|
||||
if (!isLocked) {
|
||||
draw();
|
||||
if (isCircleEnabled("hr")) {
|
||||
|
@ -805,11 +844,10 @@ Bangle.on('lock', function(isLocked) {
|
|||
} else {
|
||||
Bangle.setHRMPower(0, "circleclock");
|
||||
}
|
||||
});
|
||||
}
|
||||
Bangle.on('lock', onLock);
|
||||
|
||||
|
||||
let timerHrm;
|
||||
Bangle.on('HRM', function(hrm) {
|
||||
function onHRM(hrm) {
|
||||
if (isCircleEnabled("hr")) {
|
||||
if (hrm.confidence >= (settings.confidence)) {
|
||||
hrtValue = hrm.bpm;
|
||||
|
@ -826,23 +864,48 @@ Bangle.on('HRM', function(hrm) {
|
|||
}, settings.hrmValidity * 1000);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
Bangle.on('HRM', onHRM);
|
||||
|
||||
Bangle.on('charging', function(charging) {
|
||||
function onCharging(charging) {
|
||||
if (isCircleEnabled("battery")) drawBattery();
|
||||
});
|
||||
}
|
||||
Bangle.on('charging', onCharging);
|
||||
|
||||
|
||||
if (isCircleEnabled("hr")) {
|
||||
enableHRMSensor();
|
||||
}
|
||||
|
||||
Bangle.setUI("clock");
|
||||
Bangle.setUI({
|
||||
mode : "clock",
|
||||
remove : function() {
|
||||
// Called to unload all of the clock app
|
||||
Bangle.removeListener('charging', onCharging);
|
||||
Bangle.removeListener('lock', onLock);
|
||||
Bangle.removeListener('HRM', onHRM);
|
||||
|
||||
Bangle.setHRMPower(0, "circleclock");
|
||||
|
||||
if (timerHrm) clearTimeout(timerHrm);
|
||||
timerHrm = undefined;
|
||||
if (drawTimeout) clearTimeout(drawTimeout);
|
||||
drawTimeout = undefined;
|
||||
|
||||
delete Graphics.prototype.setFontRobotoRegular50NumericOnly;
|
||||
delete Graphics.prototype.setFontRobotoRegular21;
|
||||
}});
|
||||
|
||||
Bangle.loadWidgets();
|
||||
|
||||
// schedule a draw for the next minute
|
||||
setTimeout(function() {
|
||||
// draw in interval
|
||||
setInterval(draw, settings.updateInterval * 1000);
|
||||
}, 60000 - (Date.now() % 60000));
|
||||
// schedule a draw for the next second or minute
|
||||
function queueDraw() {
|
||||
let queueMillis = settings.updateInterval * 1000;
|
||||
if (drawTimeout) clearTimeout(drawTimeout);
|
||||
drawTimeout = setTimeout(function() {
|
||||
drawTimeout = undefined;
|
||||
draw();
|
||||
}, queueMillis - (Date.now() % queueMillis));
|
||||
}
|
||||
|
||||
draw();
|
||||
|
|
|
@ -22,5 +22,6 @@
|
|||
"circle3colorizeIcon": true,
|
||||
"circle4colorizeIcon": false,
|
||||
"hrmValidity": 60,
|
||||
"updateInterval": 60
|
||||
"updateInterval": 60,
|
||||
"showBigWeather": false
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{ "id": "circlesclock",
|
||||
"name": "Circles clock",
|
||||
"shortName":"Circles clock",
|
||||
"version":"0.13",
|
||||
"version":"0.16",
|
||||
"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"}],
|
||||
|
|
After Width: | Height: | Size: 4.4 KiB |
|
@ -68,6 +68,11 @@
|
|||
return x + 's';
|
||||
},
|
||||
onchange: x => save('updateInterval', x),
|
||||
},
|
||||
/*LANG*/'show big weather': {
|
||||
value: !!settings.showBigWeather,
|
||||
format: () => (settings.showBigWeather ? 'Yes' : 'No'),
|
||||
onchange: x => save('showBigWeather', x),
|
||||
}
|
||||
};
|
||||
E.showMenu(menu);
|
||||
|
|
|
@ -1,2 +1,5 @@
|
|||
0.01: New App!
|
||||
0.02: Make selection of background activity more explicit
|
||||
0.03: Fix listener for accel always active
|
||||
Use custom UI with swipes instead of leftright
|
||||
0.04: Fix compass heading
|
||||
|
|
|
@ -13,6 +13,9 @@ Choose either a route or a waypoint as basis for the display.
|
|||
After this selection and availability of a GPS fix the compass will show a blue dot for your destination and a green one for possibly available waypoints on the way.
|
||||
Waypoints are shown with name if available and distance to waypoint.
|
||||
|
||||
As long as no GPS signal is available the compass shows the heading from the build in magnetometer. When a GPS fix becomes available, the compass display shows the GPS course. This can be differentiated by the display of bubble levels on top and sides of the compass.
|
||||
If they are on display, the source is the magnetometer and you should keep the bangle level. There is currently no tilt compensation for the compass display.
|
||||
|
||||
### Route
|
||||
|
||||
Routes can be created from .gpx files containing "trkpt" elements with this script: [createRoute.sh](createRoute.sh)
|
||||
|
|
|
@ -318,16 +318,24 @@ function triangle (x, y, width, height){
|
|||
];
|
||||
}
|
||||
|
||||
function onSwipe(dir){
|
||||
if (dir < 0) {
|
||||
nextScreen();
|
||||
} else if (dir > 0) {
|
||||
switchMenu();
|
||||
} else {
|
||||
nextScreen();
|
||||
}
|
||||
}
|
||||
|
||||
function setButtons(){
|
||||
Bangle.setUI("leftright", (dir)=>{
|
||||
if (dir < 0) {
|
||||
nextScreen();
|
||||
} else if (dir > 0) {
|
||||
switchMenu();
|
||||
} else {
|
||||
nextScreen();
|
||||
}
|
||||
});
|
||||
let options = {
|
||||
mode: "custom",
|
||||
swipe: onSwipe,
|
||||
btn: nextScreen,
|
||||
touch: nextScreen
|
||||
};
|
||||
Bangle.setUI(options);
|
||||
}
|
||||
|
||||
function getApproxFileSize(name){
|
||||
|
@ -605,7 +613,7 @@ function showMenu(){
|
|||
"Background" : showBackgroundMenu,
|
||||
"Calibration": showCalibrationMenu,
|
||||
"Reset" : ()=>{ E.showPrompt("Do Reset?").then((v)=>{ if (v) {WIDGETS.gpstrek.resetState(); removeMenu();} else {E.showMenu(mainmenu);}});},
|
||||
"Slices" : {
|
||||
"Info rows" : {
|
||||
value : numberOfSlices,
|
||||
min:1,max:6,step:1,
|
||||
onchange : v => { setNumberOfSlices(v); }
|
||||
|
@ -676,7 +684,7 @@ const compassSliceData = {
|
|||
},
|
||||
getCourse: function (){
|
||||
if(compassSliceData.getCourseType() == "GPS") return state.currentPos.course;
|
||||
return state.compassHeading?state.compassHeading:undefined;
|
||||
return state.compassHeading?360-state.compassHeading:undefined;
|
||||
},
|
||||
getPoints: function (){
|
||||
let points = [];
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "gpstrek",
|
||||
"name": "GPS Trekking",
|
||||
"version": "0.02",
|
||||
"version": "0.04",
|
||||
"description": "Helper for tracking the status/progress during hiking. Do NOT depend on this for navigation!",
|
||||
"icon": "icon.png",
|
||||
"screenshots": [{"url":"screen1.png"},{"url":"screen2.png"},{"url":"screen3.png"},{"url":"screen4.png"}],
|
||||
|
|
|
@ -23,10 +23,6 @@ function onGPS(fix) {
|
|||
if(fix.fix) state.currentPos = fix;
|
||||
}
|
||||
|
||||
Bangle.on('accel', function(e) {
|
||||
state.acc = e;
|
||||
});
|
||||
|
||||
function onMag(e) {
|
||||
if (!state.compassHeading) state.compassHeading = e.heading;
|
||||
|
||||
|
@ -73,12 +69,17 @@ function onPressure(e) {
|
|||
}
|
||||
}
|
||||
|
||||
function onAcc (e){
|
||||
state.acc = e;
|
||||
}
|
||||
|
||||
function start(bg){
|
||||
Bangle.on('GPS', onGPS);
|
||||
Bangle.on("HRM", onPulse);
|
||||
Bangle.on("mag", onMag);
|
||||
Bangle.on("step", onStep);
|
||||
Bangle.on("pressure", onPressure);
|
||||
Bangle.on('accel', onAcc);
|
||||
|
||||
Bangle.setGPSPower(1, "gpstrek");
|
||||
Bangle.setHRMPower(1, "gpstrek");
|
||||
|
@ -96,8 +97,19 @@ function stop(bg){
|
|||
if (bg){
|
||||
if (state.active) bgChanged = true;
|
||||
state.active = false;
|
||||
saveState();
|
||||
} else if (!state.active) {
|
||||
Bangle.setGPSPower(0, "gpstrek");
|
||||
Bangle.setHRMPower(0, "gpstrek");
|
||||
Bangle.setCompassPower(0, "gpstrek");
|
||||
Bangle.setBarometerPower(0, "gpstrek");
|
||||
Bangle.removeListener('GPS', onGPS);
|
||||
Bangle.removeListener("HRM", onPulse);
|
||||
Bangle.removeListener("mag", onMag);
|
||||
Bangle.removeListener("step", onStep);
|
||||
Bangle.removeListener("pressure", onPressure);
|
||||
Bangle.removeListener('accel', onAcc);
|
||||
}
|
||||
saveState();
|
||||
Bangle.drawWidgets();
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
0.01: New App!
|
||||
0.02: Refactor code to store grocery list in separate file
|
|
@ -0,0 +1,6 @@
|
|||
Modified version of the Grocery App - lets you upload an image with the products you need to shop - Display a list of product and track if you already put them in your cart.
|
||||
|
||||
Uses this API to do the OCR: https://rapidapi.com/serendi/api/pen-to-print-handwriting-ocr
|
||||
With a free account you get 100 API calls a month.
|
||||
|
||||

|
|
@ -0,0 +1,25 @@
|
|||
var filename = 'grocery_list_aug.json';
|
||||
var settings = require("Storage").readJSON(filename,1)|| { products: [] };
|
||||
|
||||
function updateSettings() {
|
||||
require("Storage").writeJSON(filename, settings);
|
||||
Bangle.buzz();
|
||||
}
|
||||
|
||||
|
||||
const mainMenu = settings.products.reduce(function(m, p, i){
|
||||
const name = p.name;
|
||||
m[name] = {
|
||||
value: p.ok,
|
||||
format: v => v?'[x]':'[ ]',
|
||||
onchange: v => {
|
||||
settings.products[i].ok = v;
|
||||
updateSettings();
|
||||
}
|
||||
};
|
||||
return m;
|
||||
}, {
|
||||
'': { 'title': 'Grocery list' }
|
||||
});
|
||||
mainMenu['< Back'] = ()=>{load();};
|
||||
E.showMenu(mainMenu);
|
|
@ -0,0 +1 @@
|
|||
E.toArrayBuffer(atob("MDCEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAiAAMAADAAAwAAMAAiAAMAAAAAAAA/8zP/Mz/zM/8z/zM/8zP/Mz/zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA///MzMzMzMzMzM/////8zP//zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA///MzMzMzMzMz//////8zP//zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA///MzMzMzMzMzM/////8zP//zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA///MzMzMzMzMz//////8zP//zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA/////////////MzMzMzMzP//zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAARE////////////////////////zERAAARE////////////////////////zERAAERE////////////////////////zEREAERE////////////////////////zEREAAREzMzMzMzMzMzMzMzMzMzMzMzMzERAAABEREREREREREREREREREREREREREQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="))
|
|
@ -0,0 +1,145 @@
|
|||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="../../css/spectre.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<a href="#" onclick="document.getElementById('apikeydiv').style.display='block';return false;" style="font-size:small;" id="apikeylink">Enter/change API key</a>
|
||||
<br>
|
||||
|
||||
<div id="apikeydiv" style="display:none;margin-bottom:30px;">
|
||||
<input type="text" name="apikey" id="apikey"> <button onclick="localStorage.setItem('apikey', document.getElementById('apikey').value);document.getElementById('apikeysuccess').style.display='inline';setTimeout(removeSuccessMessage,5000);document.getElementById('upload2').value=null;document.getElementById('area').value='';return false;">save API key</button>
|
||||
<span id="apikeysuccess" style="display:none; color:#198754;"><br>API key saved!</span>
|
||||
<br><small>If you don't have an API key, <a href="https://rapidapi.com/serendi/api/pen-to-print-handwriting-ocr" target="_blank">you can create one here</a>. You get 100 API calls a month for free.</small>
|
||||
</div>
|
||||
|
||||
|
||||
<h4>Products</h4>
|
||||
<form id="add_product_form">
|
||||
|
||||
|
||||
|
||||
<div class="columns">
|
||||
<textarea id="area" class="form-input" style="width: 80%;margin-left: 2%;margin-bottom: 5%;height: 150px;"></textarea>
|
||||
<div id="loadingstatus" style="display:none;margin-left:2%;">Processing image <div class="loading" id="loading"></div></div>
|
||||
<input style="margin-left:2%;" id="upload2" type=file name="files[]" size=30>
|
||||
</div>
|
||||
</form>
|
||||
<br><br>
|
||||
<button id="upload" class="btn btn-primary">Upload</button>
|
||||
|
||||
<script>
|
||||
function removeSuccessMessage(){
|
||||
document.getElementById('apikeysuccess').style.display = 'none';
|
||||
}
|
||||
|
||||
|
||||
if(localStorage.getItem('apikey')) {
|
||||
document.getElementById('apikey').value = localStorage.getItem('apikey');
|
||||
} else {
|
||||
document.getElementById('apikeylink').style.display = 'none';
|
||||
document.getElementById('apikeydiv').style.display = 'block';
|
||||
}
|
||||
</script>
|
||||
|
||||
<script src="../../core/lib/customize.js"></script>
|
||||
|
||||
<script>
|
||||
|
||||
|
||||
var antwort_val="";
|
||||
function handleFileSelect(evt) {
|
||||
|
||||
document.getElementById("loadingstatus").style.display = "block";
|
||||
|
||||
let files = evt.target.files;
|
||||
let f = files[0];
|
||||
const data = new FormData();
|
||||
data.append("srcImg", f);
|
||||
data.append("Session", "string");
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.withCredentials = false;
|
||||
|
||||
xhr.addEventListener("readystatechange", function () {
|
||||
if (this.readyState === this.DONE) {
|
||||
document.getElementById("loadingstatus").style.display = "none";
|
||||
console.log(this.responseText);
|
||||
var antwort =JSON.parse(this.responseText);
|
||||
antwort_val = antwort["value"];
|
||||
console.log(antwort_val);
|
||||
if(antwort_val == null){antwort_val = "Please enter an API key first, check that your key is valid and that you haven't exceeded your API quota!";}
|
||||
document.getElementById('area').value = antwort_val;
|
||||
}
|
||||
});
|
||||
|
||||
xhr.open("POST", "https://pen-to-print-handwriting-ocr.p.rapidapi.com/recognize/");
|
||||
xhr.setRequestHeader("x-rapidapi-host", "pen-to-print-handwriting-ocr.p.rapidapi.com");
|
||||
xhr.setRequestHeader("x-rapidapi-key", localStorage.getItem("apikey"));
|
||||
|
||||
|
||||
xhr.send(data);
|
||||
}
|
||||
|
||||
document.getElementById('upload2').addEventListener('change', handleFileSelect, false);
|
||||
|
||||
var products = []
|
||||
try{
|
||||
var stored = localStorage.getItem('grocery-product-list-aug')
|
||||
if(stored){
|
||||
products = JSON.parse(stored);
|
||||
console.log(products);
|
||||
}
|
||||
|
||||
}catch(e){}
|
||||
|
||||
var tstring = "";
|
||||
for(e in products) {
|
||||
tstring += products[e]["name"]+"\n";
|
||||
}
|
||||
document.getElementById("area").value = tstring;
|
||||
|
||||
|
||||
var $form = document.getElementById('add_product_form')
|
||||
var $list = document.getElementById('products')
|
||||
var $area = document.getElementById("area");
|
||||
var $lines = [];
|
||||
function getLines() {
|
||||
$lines = area.value.replace(/\r\n/g,"\n").split("\n");
|
||||
for (var i in $lines) {
|
||||
$lines[i] = $lines[i].trim();
|
||||
}
|
||||
}
|
||||
getLines();
|
||||
|
||||
|
||||
function save(){
|
||||
localStorage.removeItem('grocery-product-list-aug');
|
||||
localStorage.setItem('grocery-product-list-aug',JSON.stringify(products));
|
||||
}
|
||||
|
||||
|
||||
|
||||
document.getElementById("upload").addEventListener("click", function() {
|
||||
|
||||
products = [];
|
||||
getLines();
|
||||
for(var i in $lines) {
|
||||
var name = $lines[i];
|
||||
products.push({
|
||||
name,
|
||||
ok: false
|
||||
})
|
||||
}
|
||||
//alert("Products added");
|
||||
|
||||
save()
|
||||
|
||||
sendCustomizedApp({
|
||||
storage:[
|
||||
{ name:"grocery_list_aug.json", content: JSON.stringify({products: products}) }
|
||||
]
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
After Width: | Height: | Size: 5.9 KiB |
After Width: | Height: | Size: 5.7 MiB |
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"id": "groceryaug",
|
||||
"name": "Grocery Augmented",
|
||||
"version": "0.02",
|
||||
"description": "Modified version of the Grocery App - lets you upload an image with the products you need to shop - Display a list of product and track if you already put them in your cart.",
|
||||
"icon": "groceryaug.png",
|
||||
"readme":"README.md",
|
||||
"type": "app",
|
||||
"tags": "tool,outdoors,shopping,list",
|
||||
"supports": ["BANGLEJS", "BANGLEJS2"],
|
||||
"custom": "groceryaug.html",
|
||||
"allow_emulator": true,
|
||||
"storage": [
|
||||
{"name":"groceryaug.app.js","url":"app.js"},
|
||||
{"name":"groceryaug.img","url":"groceryaug-icon.js","evaluate":true}
|
||||
]
|
||||
}
|
|
@ -4,3 +4,6 @@
|
|||
0.04: Support new fast app switching
|
||||
0.05: Allow to directly eval apps instead of loading
|
||||
0.06: Cache apps for faster start
|
||||
0.07: Read app icons on demand
|
||||
Add swipe-to-exit
|
||||
0.08: Only use fast loading for switching to clock to prevent problems in full screen apps
|
||||
|
|
|
@ -13,4 +13,4 @@ The app uses `E.showScroller`'s code in the app but not the function itself beca
|
|||
|
||||
### Fastload option
|
||||
|
||||
Fastload clears up the memory used by the launcher and directly evals the code of the app to load. This means if widgets are loaded (fullscreen option) it is possible that widgets stay loaded in apps not expecting that and the widgets may draw over the app.
|
||||
Fastload clears up the memory used by the launcher and directly evals the code of the clock to load.
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
const s = require("Storage");
|
||||
const settings = s.readJSON("launch.json", true) || { showClocks: true, fullscreen: false,direct:false,oneClickExit:false };
|
||||
const settings = s.readJSON("launch.json", true) || { showClocks: true, fullscreen: false,direct:false,swipeExit:false,oneClickExit:false,fastload:false };
|
||||
if (!settings.fullscreen) {
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
|
@ -11,10 +11,10 @@
|
|||
launchCache = {
|
||||
hash : launchHash,
|
||||
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};})
|
||||
.map(app=>{let 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))
|
||||
.sort((a,b)=>{
|
||||
var n=(0|a.sortorder)-(0|b.sortorder);
|
||||
let 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;
|
||||
|
@ -22,10 +22,6 @@
|
|||
}) };
|
||||
s.writeJSON("launch.cache.json", launchCache);
|
||||
}
|
||||
let apps = launchCache.apps;
|
||||
apps.forEach((app) => {
|
||||
if (app.icon) app.icon = s.read(app.icon);
|
||||
});
|
||||
let scroll = 0;
|
||||
let selectedItem = -1;
|
||||
const R = Bangle.appRect;
|
||||
|
@ -37,12 +33,13 @@
|
|||
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;
|
||||
if (!launchCache.apps[i]) break;
|
||||
x += whitespace;
|
||||
if (!apps[i].icon) {
|
||||
if (!launchCache.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 (!launchCache.apps[i].icondata) launchCache.apps[i].icondata = s.read(launchCache.apps[i].icon);
|
||||
g.drawImage(launchCache.apps[i].icondata, x + r.x, r.y);
|
||||
}
|
||||
if (selectedItem == i) {
|
||||
g.drawRect(
|
||||
|
@ -57,7 +54,7 @@
|
|||
drawText(itemI);
|
||||
};
|
||||
let drawItemAuto = function(i) {
|
||||
var y = idxToY(i);
|
||||
let y = idxToY(i);
|
||||
g.reset().setClipRect(R.x, y, R.x2, y + itemSize);
|
||||
drawItem(i, {
|
||||
x: R.x,
|
||||
|
@ -69,7 +66,7 @@
|
|||
};
|
||||
let lastIsDown = false;
|
||||
let drawText = function(i) {
|
||||
const selectedApp = apps[selectedItem];
|
||||
const selectedApp = launchCache.apps[selectedItem];
|
||||
const idy = (selectedItem - (selectedItem % 3)) / 3;
|
||||
if (!selectedApp || i != idy) return;
|
||||
const appY = idxToY(idy) + iconSize / 2;
|
||||
|
@ -87,17 +84,17 @@
|
|||
let selectItem = function(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])
|
||||
if( settings.direct && launchCache.apps[appId])
|
||||
{
|
||||
loadApp(apps[appId].src);
|
||||
load(launchCache.apps[appId].src);
|
||||
return;
|
||||
}
|
||||
if (appId == selectedItem && apps[appId]) {
|
||||
const app = apps[appId];
|
||||
if (appId == selectedItem && launchCache.apps[appId]) {
|
||||
const app = launchCache.apps[appId];
|
||||
if (!app.src || s.read(app.src) === undefined) {
|
||||
E.showMessage( /*LANG*/ "App Source\nNot found");
|
||||
} else {
|
||||
loadApp(app.src);
|
||||
load(app.src);
|
||||
}
|
||||
}
|
||||
selectedItem = appId;
|
||||
|
@ -112,9 +109,9 @@
|
|||
let drawItems = function() {
|
||||
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++)
|
||||
let a = YtoIdx(R.y);
|
||||
let b = Math.min(YtoIdx(R.y2), 99);
|
||||
for (let i = a; i <= b; i++)
|
||||
drawItem(i, {
|
||||
x: R.x,
|
||||
y: idxToY(i),
|
||||
|
@ -125,7 +122,7 @@
|
|||
};
|
||||
drawItems();
|
||||
g.flip();
|
||||
const itemsN = Math.ceil(apps.length / appsN);
|
||||
const itemsN = Math.ceil(launchCache.apps.length / appsN);
|
||||
let onDrag = function(e) {
|
||||
g.setColor(g.theme.fg);
|
||||
g.setBgColor(g.theme.bg);
|
||||
|
@ -171,25 +168,22 @@
|
|||
}
|
||||
g.setClipRect(0, 0, g.getWidth() - 1, g.getHeight() - 1);
|
||||
};
|
||||
Bangle.setUI({
|
||||
let mode = {
|
||||
mode: "custom",
|
||||
drag: onDrag,
|
||||
touch: (_, e) => {
|
||||
if (e.y < R.y - 4) return;
|
||||
var i = YtoIdx(e.y);
|
||||
let i = YtoIdx(e.y);
|
||||
selectItem(i, e);
|
||||
},
|
||||
});
|
||||
const returnToClock = function() {
|
||||
loadApp(".bootcde");
|
||||
swipe: (h,_) => { if(settings.swipeExit && h==1) { returnToClock(); } },
|
||||
};
|
||||
let watch;
|
||||
let loadApp;
|
||||
if (settings.fastload){
|
||||
loadApp = function(name) {
|
||||
|
||||
const returnToClock = function() {
|
||||
if (settings.fastload == true){
|
||||
Bangle.setUI();
|
||||
if (watch) clearWatch(watch);
|
||||
apps = [];
|
||||
delete launchCache;
|
||||
delete launchHash;
|
||||
delete drawItemAuto;
|
||||
delete drawText;
|
||||
delete selectItem;
|
||||
|
@ -200,16 +194,14 @@
|
|||
delete idxToY;
|
||||
delete YtoIdx;
|
||||
delete settings;
|
||||
setTimeout(eval, 0, s.read(name));
|
||||
return;
|
||||
};
|
||||
} else {
|
||||
loadApp = function(name) {
|
||||
load(name);
|
||||
setTimeout(eval, 0, s.read(".bootcde"));
|
||||
} else {
|
||||
load();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
if (settings.oneClickExit) {
|
||||
watch = setWatch(returnToClock, BTN1);
|
||||
}
|
||||
if (settings.oneClickExit) mode.btn = returnToClock;
|
||||
|
||||
Bangle.setUI(mode);
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"id": "iconlaunch",
|
||||
"name": "Icon Launcher",
|
||||
"shortName" : "Icon launcher",
|
||||
"version": "0.06",
|
||||
"version": "0.08",
|
||||
"icon": "app.png",
|
||||
"description": "A launcher inspired by smartphones, with an icon-only scrollable menu.",
|
||||
"tags": "tool,system,launcher",
|
||||
|
|
|
@ -2,7 +2,11 @@
|
|||
(function(back) {
|
||||
let settings = Object.assign({
|
||||
showClocks: true,
|
||||
fullscreen: false
|
||||
fullscreen: false,
|
||||
direct: false,
|
||||
oneClickExit: false,
|
||||
swipeExit: false,
|
||||
fastload: false
|
||||
}, require("Storage").readJSON("launch.json", true) || {});
|
||||
|
||||
let fonts = g.getFonts();
|
||||
|
@ -29,6 +33,10 @@
|
|||
value: settings.oneClickExit == true,
|
||||
onchange: (m) => { save("oneClickExit", m) }
|
||||
},
|
||||
/*LANG*/"Swipe exit": {
|
||||
value: settings.swipeExit == true,
|
||||
onchange: m => { save("swipeExit", m) }
|
||||
},
|
||||
/*LANG*/"Fastload": {
|
||||
value: settings.fastload == true,
|
||||
onchange: (m) => { save("fastload", m) }
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
0.01: New App!
|
||||
0.02: Do first update request 5s after boot to boot up faster
|
||||
|
|
|
@ -1,22 +1,25 @@
|
|||
(function() {
|
||||
{
|
||||
let waiting = false;
|
||||
let settings = require("Storage").readJSON("owmweather.json", 1) || {
|
||||
enabled: false
|
||||
};
|
||||
|
||||
function completion(){
|
||||
let completion = function(){
|
||||
waiting = false;
|
||||
}
|
||||
|
||||
|
||||
if (settings.enabled) {
|
||||
let weather = require("Storage").readJSON('weather.json') || {};
|
||||
let lastUpdate;
|
||||
if (weather && weather.weather && weather.weather.time) lastUpdate = weather.weather.time;
|
||||
|
||||
if (!lastUpdate || lastUpdate + settings.refresh * 1000 * 60 < Date.now()){
|
||||
if (!waiting){
|
||||
waiting = true;
|
||||
require("owmweather").pull(completion);
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (!waiting){
|
||||
waiting = true;
|
||||
require("owmweather").pull(completion);
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
setInterval(() => {
|
||||
if (!waiting && NRF.getSecurityStatus().connected){
|
||||
|
@ -25,4 +28,4 @@
|
|||
}
|
||||
}, settings.refresh * 1000 * 60);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{ "id": "owmweather",
|
||||
"name": "OpenWeatherMap weather provider",
|
||||
"shortName":"OWM Weather",
|
||||
"version":"0.01",
|
||||
"version":"0.02",
|
||||
"description": "Pulls weather from OpenWeatherMap (OWM) API",
|
||||
"icon": "app.png",
|
||||
"type": "bootloader",
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
0.01: Inital release.
|
||||
0.02: Misc fixes. Add Search and play.
|
||||
0.03: Simplify "Search and play" function after some bugfixes to Podcast
|
||||
Addict.
|
||||
0.04: New layout.
|
||||
0.05: Add widget field, tweak layout.
|
|
@ -0,0 +1,21 @@
|
|||
Requires Gadgetbridge 71.0 or later. Allow intents in Gadgetbridge in order for this app to work.
|
||||
|
||||
Touch input:
|
||||
|
||||
Press the different ui elements to control Podcast Addict and open menus.
|
||||
Press left or right arrow to move backward/forward in current playlist.
|
||||
|
||||
Swipe input:
|
||||
|
||||
Swipe left/right to jump backward/forward within the current podcast episode.
|
||||
Swipe up/down to change the volume.
|
||||
|
||||
It's possible to start a podcast by searching with the remote. It's also possible to change the playback speed.
|
||||
|
||||
The swipe logic was inspired by the implementation in [rigrig](https://git.tubul.net/rigrig/)'s Scrolling Messages.
|
||||
|
||||
Podcast Addict Remote was created by [thyttan](https://github.com/thyttan/).
|
||||
|
||||
Podcast Addict is developed by [Xavier Guillemane](https://twitter.com/xguillem) and can be installed via the [Google Play Store](https://play.google.com/store/apps/details?id=com.bambuna.podcastaddict&hl=en_US&gl=US).
|
||||
|
||||
The Podcast Addict icon is used with permission.
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwgHEhvdABnQDwwVNAAYtTGI4WSGAgWTGAYXUGAJGUGAQXXCyoXKmf/AAPznogQn4WCAAQYP6YWFDB4WFJQhFSA4gwMIYogEGBffLg0zKAYwKRgwTDBQP9Ix09n7DCpowBJBKNEBwIXBAQIsBMwgXKIQReCDoRgJOwYQDLQU/poMBC5B2DIAUzLwIKBnoXBPBAXEIQQVDA4IXNCIQXaWAgXNI4kzNQoXLO4wXLU4a+CU4gXR7ovBcIoXIBobMFPAgXILQKPDmgxCR5omDc4QAHC5ITCC6hgCC6hICC6owBC6phBC6zcFAAMzeogALdQjdBC6AZCeYfTmczAwfQhocOAAwXYgAXVgAXVFwMAJCgXCDCYWDJKYWEGKAtEA=="))
|
|
@ -0,0 +1,375 @@
|
|||
/*
|
||||
Bluetooth.println(JSON.stringify({t:"intent", action:"", flags:["flag1", "flag2",...], categories:["category1","category2",...], mimetype:"", data:"", package:"", class:"", target:"", extra:{someKey:"someValueOrString"}}));
|
||||
|
||||
Podcast Addict is developed by Xavier Guillemane and can be downloaded on Google Play Store: https://play.google.com/store/apps/details?id=com.bambuna.podcastaddict&hl=en_US&gl=US
|
||||
|
||||
Podcast Addict can be controlled through the sending of remote commands called 'Intents'.
|
||||
Some 3rd parties apps specialized in task automation will then allow you to control Podcast Addict. For example, you will be able to wake up to the sound of your playlist or to start automatically playing when some NFC tag has been detected.
|
||||
In Tasker, you just need to copy/paste one of the following intent in the task Action field ("Misc" action type then select "Send Itent") .
|
||||
If you prefer Automate It, you can use the Podcast Addict plugin that will save you some configuration time (https://play.google.com/store/apps/details?id=com.smarterapps.podcastaddictplugin )
|
||||
Before using an intent make sure to set the following:
|
||||
Package: com.bambuna.podcastaddict
|
||||
Class (UPDATE intent only): com.bambuna.podcastaddict.receiver.PodcastAddictBroadcastReceiver
|
||||
Class (every other intent): com.bambuna.podcastaddict.receiver.PodcastAddictPlayerReceiver
|
||||
Here are the supported commands (Intents) :
|
||||
com.bambuna.podcastaddict.service.player.toggle – Toggle the playlist
|
||||
com.bambuna.podcastaddict.service.player.stop – Stop the player and release its resources
|
||||
com.bambuna.podcastaddict.service.player.play – Start playing the playlist
|
||||
com.bambuna.podcastaddict.service.player.pause – Pause the playlist
|
||||
com.bambuna.podcastaddict.service.player.nexttrack – Start playing next track
|
||||
com.bambuna.podcastaddict.service.player.previoustrack – Start playing previous track
|
||||
com.bambuna.podcastaddict.service.player.jumpforward – Jump 30s forward
|
||||
com.bambuna.podcastaddict.service.player.jumpbackward – Jump 15s backward
|
||||
com.bambuna.podcastaddict.service.player.1xspeed - Disable the variable playback speed
|
||||
com.bambuna.podcastaddict.service.player.1.5xspeed – Force the playback speed at 1.5x
|
||||
com.bambuna.podcastaddict.service.player.2xspeed – Force the playback speed at 2.0x
|
||||
com.bambuna.podcastaddict.service.player.stoptimer – Disable the timer
|
||||
com.bambuna.podcastaddict.service.player.15mntimer – Set the timer at 15 minutes
|
||||
com.bambuna.podcastaddict.service.player.30mntimer – Set the timer at 30 minutes
|
||||
com.bambuna.podcastaddict.service.player.60mntimer – Set the timer at 1 hour
|
||||
com.bambuna.podcastaddict.service.update – Trigger podcasts update
|
||||
com.bambuna.podcastaddict.openmainscreen – Open the app on the Main screen
|
||||
com.bambuna.podcastaddict.openplaylist – Open the app on the Playlist screen
|
||||
com.bambuna.podcastaddict.openplayer – Open the app on the Player screen
|
||||
com.bambuna.podcastaddict.opennewepisodes – Open the app on the New episodes screen
|
||||
com.bambuna.podcastaddict.opendownloadedepisodes – Open the app on the Downloaded episodes screen
|
||||
com.bambuna.podcastaddict.service.player.playfirstepisode – Start playing the first episode in the playlist
|
||||
com.bambuna.podcastaddict.service.player.customspeed – Select playback speed
|
||||
In order to use this intent you need to pass a float argument called "arg1". Valid values are within [0.1, 5.0]
|
||||
com.bambuna.podcastaddict.service.player.customtimer – Start a custom timer
|
||||
In order to use this intent you need to pass an int argument called "arg1" containing the number of minutes. Valid values are within [1, 1440]
|
||||
com.bambuna.podcastaddict.service.player.deletecurrentskipnexttrack – Delete the current episode and skip to the next one. It behaves the same way as long pressing on the player >| button, but doesn't display any confirmation popup.
|
||||
com.bambuna.podcastaddict.service.player.deletecurrentskipprevioustrack – Delete the current episode and skip to the previous one. It behaves the same way as long pressing on the player |< button, but doesn't display any confirmation popup.
|
||||
com.bambuna.podcastaddict.service.player.boostVolume – Toggle the Volume Boost audio effect
|
||||
You can pass a, optional boolean argument called "arg1" in order to create a ON or OFF button for the volume boost. Without this parameter the app will just toggle the current value
|
||||
com.bambuna.podcastaddict.service.player.quickBookmark – Creates a bookmark at the current playback position so you can easily retrieve it later.
|
||||
com.bambuna.podcastaddict.service.download.pause – Pause downloads
|
||||
com.bambuna.podcastaddict.service.download.resume – Resume downloads
|
||||
com.bambuna.podcastaddict.service. download.toggle – Toggle downloads
|
||||
com.bambuna.podcastaddict.service.player.favorite – Mark the current episode playing as favorite.
|
||||
com.bambuna.podcastaddict.openplaylist – Open the app on the Playlist screen
|
||||
You can pass an optional string argument called "arg1" in order to select the playlist to open. Without this parameter the app will open the current playlist
|
||||
Here's how it works:
|
||||
##AUDIO## will open the Audio playlist screen
|
||||
##VIDEO## will open the Video playlist screen
|
||||
##RADIO## will open the Radio screen
|
||||
Any other argument will be used as a CATEGORY name. The app will then open this category under the playlist CUSTOM tab
|
||||
You can pass an optional boolean argument called "arg2" in order to select if the app UI should be opened. Without this parameter the playlist will be displayed
|
||||
You can pass an optional boolean argument called "arg3" in order to select if the app should start playing the selected playlist. Without this parameter the playback won't start
|
||||
Since v2020.3
|
||||
com.bambuna.podcastaddict.service.full_backup – Trigger a full backup of the app data (relies on the app automatic backup settings for the folder and the # of backup to keep)
|
||||
This task takes a lot of resources and might take up to a minute to complete, so please avoid using the app at the same time
|
||||
Since v2020.15
|
||||
com.bambuna.podcastaddict.service.player.toggletimer – This will toggle the Sleep Timer using the last duration and parameter used in the app.
|
||||
Since v2020.16
|
||||
com.bambuna.podcastaddict.service.player.togglespeed – This will toggle the Playback speed for the episode currently playing (alternate between selected speed and 1.0x).
|
||||
*/
|
||||
|
||||
var R;
|
||||
var backToMenu = false;
|
||||
var dark = g.theme.dark; // bool
|
||||
|
||||
// The main layout of the app
|
||||
function gfx() {
|
||||
//Bangle.drawWidgets();
|
||||
R = Bangle.appRect;
|
||||
marigin = 8;
|
||||
// g.drawString(str, x, y, solid)
|
||||
g.clearRect(R);
|
||||
g.reset();
|
||||
|
||||
if (dark) {g.setColor(0xFD20);} else {g.setColor(0xF800);} // Orange on dark theme, RED on light theme.
|
||||
g.setFont("4x6:2");
|
||||
g.setFontAlign(1, 0, 0);
|
||||
g.drawString("->", R.x2 - marigin, R.y + R.h/2);
|
||||
|
||||
g.setFontAlign(-1, 0, 0);
|
||||
g.drawString("<-", R.x + marigin, R.y + R.h/2);
|
||||
|
||||
g.setFontAlign(-1, 0, 1);
|
||||
g.drawString("<-", R.x + R.w/2, R.y + marigin);
|
||||
|
||||
g.setFontAlign(1, 0, 1);
|
||||
g.drawString("->", R.x + R.w/2, R.y2 - marigin);
|
||||
|
||||
g.setFontAlign(0, 0, 0);
|
||||
g.drawString("Play\nPause", R.x + R.w/2, R.y + R.h/2);
|
||||
|
||||
g.setFontAlign(-1, -1, 0);
|
||||
g.drawString("Menu", R.x + 2*marigin, R.y + 2*marigin);
|
||||
|
||||
g.setFontAlign(-1, 1, 0);
|
||||
g.drawString("Wake", R.x + 2*marigin, R.y + R.h - 2*marigin);
|
||||
|
||||
g.setFontAlign(1, -1, 0);
|
||||
g.drawString("Srch", R.x + R.w - 2*marigin, R.y + 2*marigin);
|
||||
|
||||
g.setFontAlign(1, 1, 0);
|
||||
g.drawString("Speed", R.x + R.w - 2*marigin, R.y + R.h - 2*marigin);
|
||||
}
|
||||
|
||||
// Touch handler for main layout
|
||||
function touchHandler(_, xy) {
|
||||
x = xy.x;
|
||||
y = xy.y;
|
||||
len = (R.w<R.h+1)?(R.w/3):(R.h/3);
|
||||
|
||||
// doing a<b+1 seemed faster than a<=b, also using a>b-1 instead of a>b.
|
||||
if ((R.x-1<x && x<R.x+len) && (R.y-1<y && y<R.y+len)) {
|
||||
//Menu
|
||||
Bangle.removeAllListeners("touch");
|
||||
Bangle.removeAllListeners("swipe");
|
||||
backToMenu = true;
|
||||
E.showMenu(paMenu);
|
||||
} else if ((R.x-1<x && x<R.x+len) && (R.y2-len<y && y<R.y2+1)) {
|
||||
//Wake
|
||||
gadgetbridgeWake();
|
||||
gadgetbridgeWake();
|
||||
} else if ((R.x2-len<x && x<R.x2+1) && (R.y-1<y && y<R.y+len)) {
|
||||
//Srch
|
||||
Bangle.removeAllListeners("touch");
|
||||
Bangle.removeAllListeners("swipe");
|
||||
E.showMenu(searchMenu);
|
||||
} else if ((R.x2-len<x && x<R.x2+1) && (R.y2-len<y && y<R.y2+1)) {
|
||||
//Speed
|
||||
Bangle.removeAllListeners("touch");
|
||||
Bangle.removeAllListeners("swipe");
|
||||
E.showMenu(speedMenu);
|
||||
} else if ((R.x-1<x && x<R.x+len) && (R.y+R.h/2-len/2<y && y<R.y+R.h/2+len/2)) {
|
||||
//Previous
|
||||
btMsg("service", standardCls, "player.previoustrack");
|
||||
} else if ((R.x2-len+1<x && x<R.x2+1) && (R.y+R.h/2-len/2<y && y<R.y+R.h/2+len/2)) {
|
||||
//Next
|
||||
btMsg("service", standardCls, "player.nexttrack");
|
||||
} else if ((R.x-1<x && x<R.x2+1) && (R.y-1<y && y<R.y2+1)){
|
||||
//play/pause
|
||||
btMsg("service", standardCls, "player.toggle");
|
||||
}
|
||||
}
|
||||
|
||||
// Swipe handler for main layout, used to jump backward and forward within a podcast episode.
|
||||
function swipeHandler(LR, _) {
|
||||
if (LR==-1) {
|
||||
btMsg("service", standardCls, "player.jumpforward");
|
||||
}
|
||||
if (LR==1) {
|
||||
btMsg("service", standardCls, "player.jumpbackward");
|
||||
}
|
||||
}
|
||||
|
||||
// Navigation input on the main layout
|
||||
function setUI() {
|
||||
// Bangle.setUI code from rigrig's smessages app for volume control: https://git.tubul.net/rigrig/BangleApps/src/branch/personal/apps/smessages/app.js
|
||||
Bangle.setUI(
|
||||
{mode : "updown", back : load},
|
||||
ud => {
|
||||
if (ud) Bangle.musicControl(ud>0 ? "volumedown" : "volumeup");
|
||||
}
|
||||
);
|
||||
Bangle.on("touch", touchHandler);
|
||||
Bangle.on("swipe", swipeHandler);
|
||||
}
|
||||
|
||||
/*
|
||||
The functions for interacting with Android and the Podcast Addict app
|
||||
*/
|
||||
|
||||
pkg = "com.bambuna.podcastaddict";
|
||||
standardCls = pkg + ".receiver.PodcastAddictPlayerReceiver";
|
||||
updateCls = pkg + ".receiver.PodcastAddictBroadcastReceiver";
|
||||
speed = 1.0;
|
||||
|
||||
simpleSearch = "";
|
||||
|
||||
function simpleSearchTerm() { // input a simple search term without tags, overrides search with tags (artist and track)
|
||||
require("textinput").input({
|
||||
text: simpleSearch
|
||||
}).then(result => {
|
||||
simpleSearch = result;
|
||||
}).then(() => {
|
||||
E.showMenu(searchMenu);
|
||||
});
|
||||
}
|
||||
|
||||
function searchPlayWOTags() { //make a search and play using entered terms
|
||||
searchString = simpleSearch;
|
||||
Bluetooth.println(JSON.stringify({
|
||||
t: "intent",
|
||||
action: "android.media.action.MEDIA_PLAY_FROM_SEARCH",
|
||||
package: pkg,
|
||||
target: "activity",
|
||||
extra: {
|
||||
query: searchString
|
||||
},
|
||||
flags: ["FLAG_ACTIVITY_NEW_TASK"]
|
||||
}));
|
||||
}
|
||||
|
||||
function gadgetbridgeWake() {
|
||||
Bluetooth.println(JSON.stringify({
|
||||
t: "intent",
|
||||
target: "activity",
|
||||
flags: ["FLAG_ACTIVITY_NEW_TASK", "FLAG_ACTIVITY_CLEAR_TASK", "FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS", "FLAG_ACTIVITY_NO_ANIMATION"],
|
||||
package: "gadgetbridge",
|
||||
class: "nodomain.freeyourgadget.gadgetbridge.activities.WakeActivity"
|
||||
}));
|
||||
}
|
||||
|
||||
// For stringing together the action for Podcast Addict to perform
|
||||
function actFn(actName, activOrServ) {
|
||||
return "com.bambuna.podcastaddict." + (activOrServ == "service" ? "service." : "") + actName;
|
||||
}
|
||||
|
||||
// Send the intent message to Gadgetbridge
|
||||
function btMsg(activOrServ, cls, actName, xtra) {
|
||||
|
||||
Bluetooth.println(JSON.stringify({
|
||||
t: "intent",
|
||||
action: actFn(actName, activOrServ),
|
||||
package: pkg,
|
||||
class: cls,
|
||||
target: "broadcastreceiver",
|
||||
extra: xtra
|
||||
}));
|
||||
}
|
||||
|
||||
// Get back to the main layout
|
||||
function backToGfx() {
|
||||
E.showMenu();
|
||||
g.clear();
|
||||
g.reset();
|
||||
Bangle.removeAllListeners("touch");
|
||||
Bangle.removeAllListeners("swipe");
|
||||
setUI();
|
||||
gfx();
|
||||
backToMenu = false;
|
||||
}
|
||||
|
||||
// Podcast Addict Menu
|
||||
var paMenu = {
|
||||
"": {
|
||||
title: " ",
|
||||
back: backToGfx
|
||||
},
|
||||
"Controls": () => {
|
||||
E.showMenu(controlMenu);
|
||||
},
|
||||
"Speed Controls": () => {
|
||||
E.showMenu(speedMenu);
|
||||
},
|
||||
"Search and play": () => {
|
||||
E.showMenu(searchMenu);
|
||||
},
|
||||
"Navigate and play": () => {
|
||||
E.showMenu(navigationMenu);
|
||||
},
|
||||
"Wake the android": () => {
|
||||
gadgetbridgeWake();
|
||||
gadgetbridgeWake();
|
||||
},
|
||||
"Exit PA Remote": ()=>{load();}
|
||||
};
|
||||
|
||||
|
||||
var controlMenu = {
|
||||
"": {
|
||||
title: " ",
|
||||
back: () => {if (backToMenu) E.showMenu(paMenu);
|
||||
if (!backToMenu) backToGfx();
|
||||
}
|
||||
},
|
||||
"Toggle Play/Pause": () => {
|
||||
btMsg("service", standardCls, "player.toggle");
|
||||
},
|
||||
"Jump Backward": () => {
|
||||
btMsg("service", standardCls, "player.jumpbackward");
|
||||
},
|
||||
"Jump Forward": () => {
|
||||
btMsg("service", standardCls, "player.jumpforward");
|
||||
},
|
||||
"Previous": () => {
|
||||
btMsg("service", standardCls, "player.previoustrack");
|
||||
},
|
||||
"Next": () => {
|
||||
btMsg("service", standardCls, "player.nexttrack");
|
||||
},
|
||||
"Play": () => {
|
||||
btMsg("service", standardCls, "player.play");
|
||||
},
|
||||
"Pause": () => {
|
||||
btMsg("service", standardCls, "player.pause");
|
||||
},
|
||||
"Stop": () => {
|
||||
btMsg("service", standardCls, "player.stop");
|
||||
},
|
||||
"Update": () => {
|
||||
btMsg("service", updateCls, "update");
|
||||
},
|
||||
"Messages Music Controls": () => {
|
||||
load("messagesmusic.app.js");
|
||||
},
|
||||
};
|
||||
|
||||
var speedMenu = {
|
||||
"": {
|
||||
title: " ",
|
||||
back: () => {if (backToMenu) E.showMenu(paMenu);
|
||||
if (!backToMenu) backToGfx();
|
||||
}
|
||||
},
|
||||
"Regular Speed": () => {
|
||||
speed = 1.0;
|
||||
btMsg("service", standardCls, "player.1xspeed");
|
||||
},
|
||||
"1.5x Regular Speed": () => {
|
||||
speed = 1.5;
|
||||
btMsg("service", standardCls, "player.1.5xspeed");
|
||||
},
|
||||
"2x Regular Speed": () => {
|
||||
speed = 2.0;
|
||||
btMsg("service", standardCls, "player.2xspeed");
|
||||
},
|
||||
//"Faster" : ()=>{speed+=0.1; speed=((speed>5.0)?5.0:speed); btMsg("service",standardCls,"player.customspeed",{arg1:speed});},
|
||||
//"Slower" : ()=>{speed-=0.1; speed=((speed<0.1)?0.1:speed); btMsg("service",standardCls,"player.customspeed",{arg1:speed});},
|
||||
};
|
||||
|
||||
var searchMenu = {
|
||||
"": {
|
||||
title: " ",
|
||||
|
||||
back: () => {if (backToMenu) E.showMenu(paMenu);
|
||||
if (!backToMenu) backToGfx();}
|
||||
|
||||
},
|
||||
"Search term": () => {
|
||||
simpleSearchTerm();
|
||||
},
|
||||
"Execute search and play": () => {
|
||||
btMsg("service", standardCls, "player.play");
|
||||
setTimeout(() => {
|
||||
searchPlayWOTags();
|
||||
setTimeout(() => {
|
||||
btMsg("service", standardCls, "player.play");
|
||||
}, 200);
|
||||
}, 1500);
|
||||
},
|
||||
"Simpler search and play" : searchPlayWOTags,
|
||||
};
|
||||
|
||||
var navigationMenu = {
|
||||
"": {
|
||||
title: " ",
|
||||
back: () => {if (backToMenu) E.showMenu(paMenu);
|
||||
if (!backToMenu) backToGfx();}
|
||||
},
|
||||
"Open Main Screen": () => {
|
||||
btMsg("activity", standardCls, "openmainscreen");
|
||||
},
|
||||
"Open Player Screen": () => {
|
||||
btMsg("activity", standardCls, "openplayer");
|
||||
},
|
||||
};
|
||||
|
||||
Bangle.loadWidgets();
|
||||
setUI();
|
||||
gfx();
|
After Width: | Height: | Size: 2.1 KiB |
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"id": "podadrem",
|
||||
"name": "Podcast Addict Remote",
|
||||
"shortName": "PA Remote",
|
||||
"version": "0.05",
|
||||
"description": "Control Podcast Addict on your android device.",
|
||||
"readme": "README.md",
|
||||
"type": "app",
|
||||
"tags": "remote,podcast,podcasts,radio,player,intent,intents,gadgetbridge,podadrem,pa remote",
|
||||
"icon": "app.png",
|
||||
"screenshots" : [ {"url":"screenshot1.png"}, {"url":"screenshot2.png"} ],
|
||||
"supports": ["BANGLEJS2"],
|
||||
"dependencies": { "textinput":"type"},
|
||||
"storage": [
|
||||
{"name":"podadrem.app.js","url":"app.js"},
|
||||
{"name":"podadrem.img","url":"app-icon.js","evaluate":true}
|
||||
]
|
||||
}
|
After Width: | Height: | Size: 2.7 KiB |
After Width: | Height: | Size: 3.1 KiB |
|
@ -1,2 +1,4 @@
|
|||
0.01: Initial version
|
||||
0.02: Moved settings from launcher to settings->apps menu
|
||||
0.03: Better performance by not scanning on every boot
|
||||
0.04: Better performace by not scanning on boot at all
|
||||
|
|
|
@ -1,67 +1,24 @@
|
|||
(function() {
|
||||
var settings = Object.assign(require("Storage").readJSON("quicklaunch.json", true) || {});
|
||||
{
|
||||
let settings = require("Storage").readJSON("quicklaunch.json", true) || {};
|
||||
const storage = require("Storage");
|
||||
|
||||
//list all sources
|
||||
var apps = require("Storage").list(/\.info$/).map(app=>{var a=require("Storage").readJSON(app,1);return a&&{src:a.src};});
|
||||
|
||||
//populate empty app list
|
||||
|
||||
if (!settings.leftapp) {
|
||||
settings["leftapp"] = {"name":"(none)"};
|
||||
require("Storage").write("quicklaunch.json",settings);
|
||||
}
|
||||
if (!settings.rightapp) {
|
||||
settings["rightapp"] = {"name":"(none)"};
|
||||
require("Storage").write("quicklaunch.json",settings);
|
||||
}
|
||||
if (!settings.upapp) {
|
||||
settings["upapp"] = {"name":"(none)"};
|
||||
require("Storage").write("quicklaunch.json",settings);
|
||||
}
|
||||
if (!settings.downapp) {
|
||||
settings["downapp"] = {"name":"(none)"};
|
||||
require("Storage").write("quicklaunch.json",settings);
|
||||
}
|
||||
if (!settings.tapapp) {
|
||||
settings["tapapp"] = {"name":"(none)"};
|
||||
require("Storage").write("quicklaunch.json",settings);
|
||||
}
|
||||
let reset = function(name){
|
||||
if (!settings[name]) settings[name] = {"name":"(none)"};
|
||||
if (!require("Storage").read(settings[name].src)) settings[name] = {"name":"(none)"};
|
||||
storage.write("quicklaunch.json", settings);
|
||||
};
|
||||
|
||||
//activate on clock faces
|
||||
var sui = Bangle.setUI;
|
||||
Bangle.setUI = function(mode, cb) {
|
||||
sui(mode,cb);
|
||||
if(!mode) return;
|
||||
if ("object"==typeof mode) mode = mode.mode;
|
||||
if (!mode.startsWith("clock")) return;
|
||||
|
||||
function tap() {
|
||||
//tap, check if source exists, launch
|
||||
if ((settings.tapapp.src) && apps.some(e => e.src === settings.tapapp.src)) load (settings.tapapp.src);
|
||||
}
|
||||
|
||||
let drag;
|
||||
let e;
|
||||
|
||||
Bangle.on("touch",tap);
|
||||
Bangle.on("drag", e => {
|
||||
if (!drag) { // start dragging
|
||||
drag = {x: e.x, y: e.y};
|
||||
} else if (!e.b) { // released
|
||||
const dx = e.x-drag.x, dy = e.y-drag.y;
|
||||
drag = null;
|
||||
//horizontal swipes, check if source exists, launch
|
||||
if (Math.abs(dx)>Math.abs(dy)+10) {
|
||||
if ((settings.leftapp.src) && apps.some(e => e.src === settings.leftapp.src) && dx<0) load(settings.leftapp.src);
|
||||
if ((settings.rightapp.src) && apps.some(e => e.src === settings.rightapp.src) && dx>0) load(settings.rightapp.src);
|
||||
}
|
||||
//vertical swipes, check if source exists, launch
|
||||
else if (Math.abs(dy)>Math.abs(dx)+10) {
|
||||
if ((settings.upapp.src) && apps.some(e => e.src === settings.upapp.src) && dy<0) load(settings.upapp.src);
|
||||
if ((settings.downapp.src) && apps.some(e => e.src === settings.downapp.src) && dy>0) load(settings.downapp.src);
|
||||
}
|
||||
}
|
||||
Bangle.on("touch", () => {
|
||||
if (!Bangle.CLOCK) return;
|
||||
if (settings.tapapp.src){ if (!storage.read(settings.tapapp.src)) reset("tapapp"); else load(settings.tapapp.src); }
|
||||
});
|
||||
|
||||
};
|
||||
})();
|
||||
Bangle.on("swipe", (lr,ud) => {
|
||||
if (!Bangle.CLOCK) return;
|
||||
|
||||
if (lr == -1 && settings.leftapp && settings.leftapp.src){ if (!storage.read(settings.leftapp.src)) reset("leftapp"); else load(settings.leftapp.src); }
|
||||
if (lr == 1 && settings.rightapp && settings.rightapp.src){ if (!storage.read(settings.rightapp.src)) reset("rightapp"); else load(settings.rightapp.src); }
|
||||
if (ud == -1 && settings.upapp && settings.upapp.src){ if (!storage.read(settings.upapp.src)) reset("upapp"); else load(settings.upapp.src); }
|
||||
if (ud == 1 && settings.downapp && settings.downapp.src){ if (!storage.read(settings.downapp.src)) reset("downapp"); else load(settings.downapp.src); }
|
||||
});
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"id": "quicklaunch",
|
||||
"name": "Quick Launch",
|
||||
"icon": "app.png",
|
||||
"version":"0.02",
|
||||
"version":"0.04",
|
||||
"description": "Tap or swipe left/right/up/down on your clock face to launch up to five apps of your choice. Configurations can be accessed through Settings->Apps.",
|
||||
"type": "bootloader",
|
||||
"tags": "tools, system",
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
(function(back) {
|
||||
var settings = Object.assign(require("Storage").readJSON("quicklaunch.json", true) || {});
|
||||
|
||||
for (let c of ["leftapp","rightapp","upapp","downapp","tapapp"]){
|
||||
if (!settings[c]) settings[c] = {"name":"(none)"};
|
||||
}
|
||||
|
||||
var apps = require("Storage").list(/\.info$/).map(app=>{var a=require("Storage").readJSON(app,1);return a&&{name:a.name,type:a.type,sortorder:a.sortorder,src:a.src};}).filter(app=>app && (app.type=="app" || app.type=="launch" || app.type=="clock" || !app.type));
|
||||
|
||||
apps.sort((a,b)=>{
|
||||
|
@ -29,11 +33,11 @@ function showMainMenu() {
|
|||
mainmenu["Up: "+settings.upapp.name] = function() { E.showMenu(upmenu); };
|
||||
mainmenu["Down: "+settings.downapp.name] = function() { E.showMenu(downmenu); };
|
||||
mainmenu["Tap: "+settings.tapapp.name] = function() { E.showMenu(tapmenu); };
|
||||
|
||||
|
||||
return E.showMenu(mainmenu);
|
||||
}
|
||||
|
||||
//Left swipe menu
|
||||
|
||||
//Left swipe menu
|
||||
var leftmenu = {
|
||||
"" : { "title" : "Left Swipe" },
|
||||
"< Back" : showMainMenu
|
||||
|
@ -119,4 +123,4 @@ apps.forEach((a)=>{
|
|||
});
|
||||
|
||||
showMainMenu();
|
||||
});
|
||||
})
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
0.01: New App!
|
||||
0.02: Less time used during boot if disabled
|
||||
|
|
|
@ -1,351 +1 @@
|
|||
(function() {
|
||||
var settings = Object.assign(
|
||||
require('Storage').readJSON("sensortools.default.json", true) || {},
|
||||
require('Storage').readJSON("sensortools.json", true) || {}
|
||||
);
|
||||
|
||||
var log = function(text, param) {
|
||||
var logline = new Date().toISOString() + " - " + "Sensortools - " + text;
|
||||
if (param) logline += ": " + JSON.stringify(param);
|
||||
print(logline);
|
||||
};
|
||||
|
||||
if (settings.enabled) {
|
||||
|
||||
log("Enabled");
|
||||
const POWER_DELAY = 10000;
|
||||
|
||||
var onEvents = [];
|
||||
|
||||
Bangle.sensortoolsOrigOn = Bangle.on;
|
||||
Bangle.sensortoolsOrigEmit = Bangle.emit;
|
||||
Bangle.sensortoolsOrigRemoveListener = Bangle.removeListener;
|
||||
|
||||
Bangle.on = function(name, callback) {
|
||||
if (onEvents[name]) {
|
||||
log("Redirecting listener for", name, "to", name + "_mod");
|
||||
Bangle.sensortoolsOrigOn(name + "_mod", callback);
|
||||
Bangle.sensortoolsOrigOn(name, (e) => {
|
||||
log("Redirected event for", name, "to", name + "_mod");
|
||||
Bangle.sensortoolsOrigEmit(name + "_mod", onEvents[name](e));
|
||||
});
|
||||
} else {
|
||||
log("Pass through on call for", name, callback);
|
||||
Bangle.sensortoolsOrigOn(name, callback);
|
||||
}
|
||||
};
|
||||
|
||||
Bangle.removeListener = function(name, callback) {
|
||||
if (onEvents[name]) {
|
||||
log("Removing augmented listener for", name, onEvents[name]);
|
||||
Bangle.sensortoolsOrigRemoveListener(name + "_mod", callback);
|
||||
} else {
|
||||
log("Pass through remove listener for", name);
|
||||
Bangle.sensortoolsOrigRemoveListener(name, callback);
|
||||
}
|
||||
};
|
||||
|
||||
Bangle.emit = function(name, event) {
|
||||
if (onEvents[name]) {
|
||||
log("Augmenting emit call for", name, onEvents[name]);
|
||||
Bangle.sensortoolsOrigEmit(name + "_mod", event);
|
||||
} else {
|
||||
log("Pass through emit call for", name);
|
||||
Bangle.sensortoolsOrigEmit(name, event);
|
||||
}
|
||||
};
|
||||
|
||||
var createPowerFunction = function(type, name, origPower) {
|
||||
return function(isOn, app) {
|
||||
if (type == "nop") {
|
||||
return true;
|
||||
}else if (type == "delay") {
|
||||
setTimeout(() => {
|
||||
origPower(isOn, app);
|
||||
}, POWER_DELAY);
|
||||
} else if (type == "on") {
|
||||
origPower(1, "sensortools_force_on");
|
||||
} else if (type == "passthrough"){
|
||||
origPower(isOn, "app");
|
||||
} else if (type == "emulate"){
|
||||
if (!Bangle._PWR) Bangle._PWR={};
|
||||
if (!Bangle._PWR[name]) Bangle._PWR[name] = [];
|
||||
if (!app) app="?";
|
||||
if (isOn) {
|
||||
Bangle._PWR[name].push(app);
|
||||
return true;
|
||||
} else {
|
||||
Bangle._PWR[name] = Bangle._PWR[name].filter((v)=>{return v == app;});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
if (settings.hrm && settings.hrm.enabled) {
|
||||
log("HRM", settings.hrm);
|
||||
if (settings.hrm.power) {
|
||||
log("HRM power");
|
||||
Bangle.sensortoolsOrigSetHRMPower = Bangle.setHRMPower;
|
||||
Bangle.setHRMPower = createPowerFunction(settings.hrm.power, "HRM", Bangle.sensortoolsOrigSetHRMPower);
|
||||
}
|
||||
if (settings.hrm.mode == "modify") {
|
||||
if (settings.hrm.name == "bpmtrippled") {
|
||||
onEvents.HRM = (e) => {
|
||||
return {
|
||||
bpm: e.bpm * 3
|
||||
};
|
||||
};
|
||||
}
|
||||
} else if (settings.hrm.mode == "emulate") {
|
||||
if (settings.hrm.name == "sin") {
|
||||
setInterval(() => {
|
||||
Bangle.sensortoolsOrigEmit(60 + 3 * Math.sin(Date.now() / 10000));
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (settings.gps && settings.gps.enabled) {
|
||||
log("GPS", settings.gps);
|
||||
let modGps = function(dataProvider) {
|
||||
Bangle.getGPSFix = dataProvider;
|
||||
setInterval(() => {
|
||||
Bangle.sensortoolsOrigEmit("GPS", dataProvider());
|
||||
}, 1000);
|
||||
};
|
||||
if (settings.gps.power) {
|
||||
Bangle.sensortoolsOrigSetGPSPower = Bangle.setGPSPower;
|
||||
Bangle.setGPSPower = createPowerFunction(settings.gps.power, "GPS", Bangle.sensortoolsOrigSetGPSPower);
|
||||
}
|
||||
if (settings.gps.mode == "emulate") {
|
||||
function radians(a) {
|
||||
return a*Math.PI/180;
|
||||
}
|
||||
|
||||
function degrees(a) {
|
||||
var d = a*180/Math.PI;
|
||||
return (d+360)%360;
|
||||
}
|
||||
|
||||
function bearing(a,b){
|
||||
if (!a || !b || !a.lon || !a.lat || !b.lon || !b.lat) return Infinity;
|
||||
var delta = radians(b.lon-a.lon);
|
||||
var alat = radians(a.lat);
|
||||
var blat = radians(b.lat);
|
||||
var y = Math.sin(delta) * Math.cos(blat);
|
||||
var x = Math.cos(alat)*Math.sin(blat) -
|
||||
Math.sin(alat)*Math.cos(blat)*Math.cos(delta);
|
||||
return Math.round(degrees(Math.atan2(y, x)));
|
||||
}
|
||||
|
||||
function interpolate(a,b,progress){
|
||||
return {
|
||||
lat: a.lat * progress + b.lat * (1-progress),
|
||||
lon: a.lon * progress + b.lon * (1-progress),
|
||||
ele: a.ele * progress + b.ele * (1-progress)
|
||||
}
|
||||
}
|
||||
|
||||
function getSquareRoute(){
|
||||
return [
|
||||
{lat:"47.2577411",lon:"11.9927442",ele:2273},
|
||||
{lat:"47.266761",lon:"11.9926673",ele:2166},
|
||||
{lat:"47.2667605",lon:"12.0059511",ele:2245},
|
||||
{lat:"47.2577516",lon:"12.0059925",ele:1994}
|
||||
];
|
||||
}
|
||||
function getSquareRouteFuzzy(){
|
||||
return [
|
||||
{lat:"47.2578455",lon:"11.9929891",ele:2265},
|
||||
{lat:"47.258592",lon:"11.9923341",ele:2256},
|
||||
{lat:"47.2594506",lon:"11.9927412",ele:2230},
|
||||
{lat:"47.2603323",lon:"11.9924949",ele:2219},
|
||||
{lat:"47.2612056",lon:"11.9928175",ele:2199},
|
||||
{lat:"47.2621002",lon:"11.9929817",ele:2182},
|
||||
{lat:"47.2629025",lon:"11.9923915",ele:2189},
|
||||
{lat:"47.2637828",lon:"11.9926486",ele:2180},
|
||||
{lat:"47.2646733",lon:"11.9928167",ele:2191},
|
||||
{lat:"47.2655617",lon:"11.9930357",ele:2185},
|
||||
{lat:"47.2662862",lon:"11.992252",ele:2186},
|
||||
{lat:"47.2669305",lon:"11.993173",ele:2166},
|
||||
{lat:"47.266666",lon:"11.9944419",ele:2171},
|
||||
{lat:"47.2667579",lon:"11.99576",ele:2194},
|
||||
{lat:"47.2669409",lon:"11.9970579",ele:2207},
|
||||
{lat:"47.2666562",lon:"11.9983128",ele:2212},
|
||||
{lat:"47.2666027",lon:"11.9996335",ele:2262},
|
||||
{lat:"47.2667245",lon:"12.0009395",ele:2278},
|
||||
{lat:"47.2668457",lon:"12.002256",ele:2297},
|
||||
{lat:"47.2666126",lon:"12.0035373",ele:2303},
|
||||
{lat:"47.2664554",lon:"12.004841",ele:2251},
|
||||
{lat:"47.2669461",lon:"12.005948",ele:2245},
|
||||
{lat:"47.2660877",lon:"12.006323",ele:2195},
|
||||
{lat:"47.2652729",lon:"12.0057552",ele:2163},
|
||||
{lat:"47.2643926",lon:"12.0060123",ele:2131},
|
||||
{lat:"47.2634978",lon:"12.0058302",ele:2095},
|
||||
{lat:"47.2626129",lon:"12.0060759",ele:2066},
|
||||
{lat:"47.2617325",lon:"12.0058188",ele:2037},
|
||||
{lat:"47.2608668",lon:"12.0061784",ele:1993},
|
||||
{lat:"47.2600155",lon:"12.0057392",ele:1967},
|
||||
{lat:"47.2591203",lon:"12.0058233",ele:1949},
|
||||
{lat:"47.2582307",lon:"12.0059718",ele:1972},
|
||||
{lat:"47.2578014",lon:"12.004804",ele:2011},
|
||||
{lat:"47.2577232",lon:"12.0034834",ele:2044},
|
||||
{lat:"47.257745",lon:"12.0021656",ele:2061},
|
||||
{lat:"47.2578682",lon:"12.0008597",ele:2065},
|
||||
{lat:"47.2577082",lon:"11.9995526",ele:2071},
|
||||
{lat:"47.2575917",lon:"11.9982348",ele:2102},
|
||||
{lat:"47.2577401",lon:"11.996924",ele:2147},
|
||||
{lat:"47.257715",lon:"11.9956061",ele:2197},
|
||||
{lat:"47.2578996",lon:"11.9943081",ele:2228}
|
||||
];
|
||||
}
|
||||
|
||||
if (settings.gps.name == "staticfix") {
|
||||
modGps(() => { return {
|
||||
"lat": 52,
|
||||
"lon": 8,
|
||||
"alt": 100,
|
||||
"speed": 10,
|
||||
"course": 12,
|
||||
"time": Date.now(),
|
||||
"satellites": 7,
|
||||
"fix": 1,
|
||||
"hdop": 1
|
||||
};});
|
||||
} else if (settings.gps.name.includes("route")) {
|
||||
let route;
|
||||
let interpSteps;
|
||||
if (settings.gps.name == "routeFuzzy"){
|
||||
route = getSquareRouteFuzzy();
|
||||
interpSteps = 5;
|
||||
} else {
|
||||
route = getSquareRoute();
|
||||
interpSteps = 50;
|
||||
}
|
||||
|
||||
let step = 0;
|
||||
let routeIndex = 0;
|
||||
modGps(() => {
|
||||
let newIndex = (routeIndex + 1)%route.length;
|
||||
|
||||
let result = {
|
||||
"speed": Math.random() * 3 + 2,
|
||||
"time": Date.now(),
|
||||
"satellites": Math.floor(Math.random()*5)+3,
|
||||
"fix": 1,
|
||||
"hdop": Math.floor(Math.random(30)+1)
|
||||
};
|
||||
|
||||
let oldPos = route[routeIndex];
|
||||
if (step != 0){
|
||||
oldPos = interpolate(route[routeIndex], route[newIndex], E.clip(0,1,step/interpSteps));
|
||||
}
|
||||
let newPos = route[newIndex];
|
||||
if (step < interpSteps - 1){
|
||||
newPos = interpolate(route[routeIndex], route[newIndex], E.clip(0,1,(step+1)%interpSteps/interpSteps));
|
||||
}
|
||||
|
||||
if (step == interpSteps - 1){
|
||||
let followingIndex = (routeIndex + 2)%route.length;
|
||||
newPos = interpolate(route[newIndex], route[followingIndex], E.clip(0,1,1/interpSteps));
|
||||
}
|
||||
|
||||
result.lat = oldPos.lat;
|
||||
result.lon = oldPos.lon;
|
||||
result.alt = oldPos.ele;
|
||||
|
||||
result.course = bearing(oldPos,newPos);
|
||||
|
||||
step++;
|
||||
if (step == interpSteps){
|
||||
routeIndex = (routeIndex + 1) % route.length;
|
||||
step = 0;
|
||||
}
|
||||
return result;
|
||||
});
|
||||
} else if (settings.gps.name == "nofix") {
|
||||
modGps(() => { return {
|
||||
"lat": NaN,
|
||||
"lon": NaN,
|
||||
"alt": NaN,
|
||||
"speed": NaN,
|
||||
"course": NaN,
|
||||
"time": Date.now(),
|
||||
"satellites": 2,
|
||||
"fix": 0,
|
||||
"hdop": NaN
|
||||
};});
|
||||
} else if (settings.gps.name == "changingfix") {
|
||||
let currentSpeed=1;
|
||||
let currentLat=20;
|
||||
let currentLon=10;
|
||||
let currentCourse=10;
|
||||
let currentAlt=-100;
|
||||
let currentSats=5;
|
||||
modGps(() => {
|
||||
currentLat += 0.1;
|
||||
if (currentLat > 50) currentLat = 20;
|
||||
currentLon += 0.1;
|
||||
if (currentLon > 20) currentLon = 10;
|
||||
currentSpeed *= 10;
|
||||
if (currentSpeed > 1000) currentSpeed = 1;
|
||||
currentCourse += 12;
|
||||
if (currentCourse > 360) currentCourse -= 360;
|
||||
currentSats += 1;
|
||||
if (currentSats > 10) currentSats = 5;
|
||||
currentAlt *= 10;
|
||||
if (currentAlt > 1000) currentAlt = -100;
|
||||
return {
|
||||
"lat": currentLat,
|
||||
"lon": currentLon,
|
||||
"alt": currentAlt,
|
||||
"speed": currentSpeed,
|
||||
"course": currentCourse,
|
||||
"time": Date.now(),
|
||||
"satellites": currentSats,
|
||||
"fix": 1,
|
||||
"hdop": 1
|
||||
};});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.mag && settings.mag.enabled) {
|
||||
log("MAG", settings.mag);
|
||||
let modMag = function(data) {
|
||||
setInterval(() => {
|
||||
Bangle.getCompass = data;
|
||||
Bangle.sensortoolsOrigEmit("mag", data());
|
||||
}, 100);
|
||||
};
|
||||
if (settings.mag.power) {
|
||||
Bangle.sensortoolsOrigSetCompassPower = Bangle.setCompassPower;
|
||||
Bangle.setCompassPower = createPowerFunction(settings.mag.power, "Compass", Bangle.sensortoolsOrigSetCompassPower);
|
||||
}
|
||||
if (settings.mag.mode == "emulate") {
|
||||
if (settings.mag.name == "static") {
|
||||
modMag(()=>{return {
|
||||
x: 1,
|
||||
y: 1,
|
||||
z: 1,
|
||||
dx: 1,
|
||||
dy: 1,
|
||||
dz: 1,
|
||||
heading: 0
|
||||
};});
|
||||
} else if (settings.mag.name == "rotate"){
|
||||
let last = 0;
|
||||
modMag(()=>{return {
|
||||
x: 1,
|
||||
y: 1,
|
||||
z: 1,
|
||||
dx: 1,
|
||||
dy: 1,
|
||||
dz: 1,
|
||||
heading: last = (last+1)%360,
|
||||
};});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})();
|
||||
if ((require('Storage').readJSON("sensortools.json", true) || {}).enabled) require("sensortools").enable();
|
||||
|
|
|
@ -0,0 +1,348 @@
|
|||
exports.enable = () => {
|
||||
let settings = Object.assign(
|
||||
require('Storage').readJSON("sensortools.default.json", true) || {},
|
||||
require('Storage').readJSON("sensortools.json", true) || {}
|
||||
);
|
||||
|
||||
let log = function(text, param) {
|
||||
let logline = new Date().toISOString() + " - " + "Sensortools - " + text;
|
||||
if (param) logline += ": " + JSON.stringify(param);
|
||||
print(logline);
|
||||
};
|
||||
|
||||
log("Enabled");
|
||||
const POWER_DELAY = 10000;
|
||||
|
||||
let onEvents = [];
|
||||
|
||||
Bangle.sensortoolsOrigOn = Bangle.on;
|
||||
Bangle.sensortoolsOrigEmit = Bangle.emit;
|
||||
Bangle.sensortoolsOrigRemoveListener = Bangle.removeListener;
|
||||
|
||||
Bangle.on = function(name, callback) {
|
||||
if (onEvents[name]) {
|
||||
log("Redirecting listener for", name, "to", name + "_mod");
|
||||
Bangle.sensortoolsOrigOn(name + "_mod", callback);
|
||||
Bangle.sensortoolsOrigOn(name, (e) => {
|
||||
log("Redirected event for", name, "to", name + "_mod");
|
||||
Bangle.sensortoolsOrigEmit(name + "_mod", onEvents[name](e));
|
||||
});
|
||||
} else {
|
||||
log("Pass through on call for", name, callback);
|
||||
Bangle.sensortoolsOrigOn(name, callback);
|
||||
}
|
||||
};
|
||||
|
||||
Bangle.removeListener = function(name, callback) {
|
||||
if (onEvents[name]) {
|
||||
log("Removing augmented listener for", name, onEvents[name]);
|
||||
Bangle.sensortoolsOrigRemoveListener(name + "_mod", callback);
|
||||
} else {
|
||||
log("Pass through remove listener for", name);
|
||||
Bangle.sensortoolsOrigRemoveListener(name, callback);
|
||||
}
|
||||
};
|
||||
|
||||
Bangle.emit = function(name, event) {
|
||||
if (onEvents[name]) {
|
||||
log("Augmenting emit call for", name, onEvents[name]);
|
||||
Bangle.sensortoolsOrigEmit(name + "_mod", event);
|
||||
} else {
|
||||
log("Pass through emit call for", name);
|
||||
Bangle.sensortoolsOrigEmit(name, event);
|
||||
}
|
||||
};
|
||||
|
||||
let createPowerFunction = function(type, name, origPower) {
|
||||
return function(isOn, app) {
|
||||
if (type == "nop") {
|
||||
return true;
|
||||
}else if (type == "delay") {
|
||||
setTimeout(() => {
|
||||
origPower(isOn, app);
|
||||
}, POWER_DELAY);
|
||||
} else if (type == "on") {
|
||||
origPower(1, "sensortools_force_on");
|
||||
} else if (type == "passthrough"){
|
||||
origPower(isOn, "app");
|
||||
} else if (type == "emulate"){
|
||||
if (!Bangle._PWR) Bangle._PWR={};
|
||||
if (!Bangle._PWR[name]) Bangle._PWR[name] = [];
|
||||
if (!app) app="?";
|
||||
if (isOn) {
|
||||
Bangle._PWR[name].push(app);
|
||||
return true;
|
||||
} else {
|
||||
Bangle._PWR[name] = Bangle._PWR[name].filter((v)=>{return v == app;});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
if (settings.hrm && settings.hrm.enabled) {
|
||||
log("HRM", settings.hrm);
|
||||
if (settings.hrm.power) {
|
||||
log("HRM power");
|
||||
Bangle.sensortoolsOrigSetHRMPower = Bangle.setHRMPower;
|
||||
Bangle.setHRMPower = createPowerFunction(settings.hrm.power, "HRM", Bangle.sensortoolsOrigSetHRMPower);
|
||||
}
|
||||
if (settings.hrm.mode == "modify") {
|
||||
if (settings.hrm.name == "bpmtrippled") {
|
||||
onEvents.HRM = (e) => {
|
||||
return {
|
||||
bpm: e.bpm * 3
|
||||
};
|
||||
};
|
||||
}
|
||||
} else if (settings.hrm.mode == "emulate") {
|
||||
if (settings.hrm.name == "sin") {
|
||||
setInterval(() => {
|
||||
Bangle.sensortoolsOrigEmit(60 + 3 * Math.sin(Date.now() / 10000));
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (settings.gps && settings.gps.enabled) {
|
||||
log("GPS", settings.gps);
|
||||
let modGps = function(dataProvider) {
|
||||
Bangle.getGPSFix = dataProvider;
|
||||
setInterval(() => {
|
||||
Bangle.sensortoolsOrigEmit("GPS", dataProvider());
|
||||
}, 1000);
|
||||
};
|
||||
if (settings.gps.power) {
|
||||
Bangle.sensortoolsOrigSetGPSPower = Bangle.setGPSPower;
|
||||
Bangle.setGPSPower = createPowerFunction(settings.gps.power, "GPS", Bangle.sensortoolsOrigSetGPSPower);
|
||||
}
|
||||
if (settings.gps.mode == "emulate") {
|
||||
function radians(a) {
|
||||
return a*Math.PI/180;
|
||||
}
|
||||
|
||||
function degrees(a) {
|
||||
let d = a*180/Math.PI;
|
||||
return (d+360)%360;
|
||||
}
|
||||
|
||||
function bearing(a,b){
|
||||
if (!a || !b || !a.lon || !a.lat || !b.lon || !b.lat) return Infinity;
|
||||
let delta = radians(b.lon-a.lon);
|
||||
let alat = radians(a.lat);
|
||||
let blat = radians(b.lat);
|
||||
let y = Math.sin(delta) * Math.cos(blat);
|
||||
let x = Math.cos(alat)*Math.sin(blat) -
|
||||
Math.sin(alat)*Math.cos(blat)*Math.cos(delta);
|
||||
return Math.round(degrees(Math.atan2(y, x)));
|
||||
}
|
||||
|
||||
function interpolate(a,b,progress){
|
||||
return {
|
||||
lat: a.lat * progress + b.lat * (1-progress),
|
||||
lon: a.lon * progress + b.lon * (1-progress),
|
||||
ele: a.ele * progress + b.ele * (1-progress)
|
||||
}
|
||||
}
|
||||
|
||||
function getSquareRoute(){
|
||||
return [
|
||||
{lat:"47.2577411",lon:"11.9927442",ele:2273},
|
||||
{lat:"47.266761",lon:"11.9926673",ele:2166},
|
||||
{lat:"47.2667605",lon:"12.0059511",ele:2245},
|
||||
{lat:"47.2577516",lon:"12.0059925",ele:1994}
|
||||
];
|
||||
}
|
||||
function getSquareRouteFuzzy(){
|
||||
return [
|
||||
{lat:"47.2578455",lon:"11.9929891",ele:2265},
|
||||
{lat:"47.258592",lon:"11.9923341",ele:2256},
|
||||
{lat:"47.2594506",lon:"11.9927412",ele:2230},
|
||||
{lat:"47.2603323",lon:"11.9924949",ele:2219},
|
||||
{lat:"47.2612056",lon:"11.9928175",ele:2199},
|
||||
{lat:"47.2621002",lon:"11.9929817",ele:2182},
|
||||
{lat:"47.2629025",lon:"11.9923915",ele:2189},
|
||||
{lat:"47.2637828",lon:"11.9926486",ele:2180},
|
||||
{lat:"47.2646733",lon:"11.9928167",ele:2191},
|
||||
{lat:"47.2655617",lon:"11.9930357",ele:2185},
|
||||
{lat:"47.2662862",lon:"11.992252",ele:2186},
|
||||
{lat:"47.2669305",lon:"11.993173",ele:2166},
|
||||
{lat:"47.266666",lon:"11.9944419",ele:2171},
|
||||
{lat:"47.2667579",lon:"11.99576",ele:2194},
|
||||
{lat:"47.2669409",lon:"11.9970579",ele:2207},
|
||||
{lat:"47.2666562",lon:"11.9983128",ele:2212},
|
||||
{lat:"47.2666027",lon:"11.9996335",ele:2262},
|
||||
{lat:"47.2667245",lon:"12.0009395",ele:2278},
|
||||
{lat:"47.2668457",lon:"12.002256",ele:2297},
|
||||
{lat:"47.2666126",lon:"12.0035373",ele:2303},
|
||||
{lat:"47.2664554",lon:"12.004841",ele:2251},
|
||||
{lat:"47.2669461",lon:"12.005948",ele:2245},
|
||||
{lat:"47.2660877",lon:"12.006323",ele:2195},
|
||||
{lat:"47.2652729",lon:"12.0057552",ele:2163},
|
||||
{lat:"47.2643926",lon:"12.0060123",ele:2131},
|
||||
{lat:"47.2634978",lon:"12.0058302",ele:2095},
|
||||
{lat:"47.2626129",lon:"12.0060759",ele:2066},
|
||||
{lat:"47.2617325",lon:"12.0058188",ele:2037},
|
||||
{lat:"47.2608668",lon:"12.0061784",ele:1993},
|
||||
{lat:"47.2600155",lon:"12.0057392",ele:1967},
|
||||
{lat:"47.2591203",lon:"12.0058233",ele:1949},
|
||||
{lat:"47.2582307",lon:"12.0059718",ele:1972},
|
||||
{lat:"47.2578014",lon:"12.004804",ele:2011},
|
||||
{lat:"47.2577232",lon:"12.0034834",ele:2044},
|
||||
{lat:"47.257745",lon:"12.0021656",ele:2061},
|
||||
{lat:"47.2578682",lon:"12.0008597",ele:2065},
|
||||
{lat:"47.2577082",lon:"11.9995526",ele:2071},
|
||||
{lat:"47.2575917",lon:"11.9982348",ele:2102},
|
||||
{lat:"47.2577401",lon:"11.996924",ele:2147},
|
||||
{lat:"47.257715",lon:"11.9956061",ele:2197},
|
||||
{lat:"47.2578996",lon:"11.9943081",ele:2228}
|
||||
];
|
||||
}
|
||||
|
||||
if (settings.gps.name == "staticfix") {
|
||||
modGps(() => { return {
|
||||
"lat": 52,
|
||||
"lon": 8,
|
||||
"alt": 100,
|
||||
"speed": 10,
|
||||
"course": 12,
|
||||
"time": Date.now(),
|
||||
"satellites": 7,
|
||||
"fix": 1,
|
||||
"hdop": 1
|
||||
};});
|
||||
} else if (settings.gps.name.includes("route")) {
|
||||
let route;
|
||||
let interpSteps;
|
||||
if (settings.gps.name == "routeFuzzy"){
|
||||
route = getSquareRouteFuzzy();
|
||||
interpSteps = 5;
|
||||
} else {
|
||||
route = getSquareRoute();
|
||||
interpSteps = 50;
|
||||
}
|
||||
|
||||
let step = 0;
|
||||
let routeIndex = 0;
|
||||
modGps(() => {
|
||||
let newIndex = (routeIndex + 1)%route.length;
|
||||
|
||||
let result = {
|
||||
"speed": Math.random() * 3 + 2,
|
||||
"time": Date.now(),
|
||||
"satellites": Math.floor(Math.random()*5)+3,
|
||||
"fix": 1,
|
||||
"hdop": Math.floor(Math.random(30)+1)
|
||||
};
|
||||
|
||||
let oldPos = route[routeIndex];
|
||||
if (step != 0){
|
||||
oldPos = interpolate(route[routeIndex], route[newIndex], E.clip(0,1,step/interpSteps));
|
||||
}
|
||||
let newPos = route[newIndex];
|
||||
if (step < interpSteps - 1){
|
||||
newPos = interpolate(route[routeIndex], route[newIndex], E.clip(0,1,(step+1)%interpSteps/interpSteps));
|
||||
}
|
||||
|
||||
if (step == interpSteps - 1){
|
||||
let followingIndex = (routeIndex + 2)%route.length;
|
||||
newPos = interpolate(route[newIndex], route[followingIndex], E.clip(0,1,1/interpSteps));
|
||||
}
|
||||
|
||||
result.lat = oldPos.lat;
|
||||
result.lon = oldPos.lon;
|
||||
result.alt = oldPos.ele;
|
||||
|
||||
result.course = bearing(oldPos,newPos);
|
||||
|
||||
step++;
|
||||
if (step == interpSteps){
|
||||
routeIndex = (routeIndex + 1) % route.length;
|
||||
step = 0;
|
||||
}
|
||||
return result;
|
||||
});
|
||||
} else if (settings.gps.name == "nofix") {
|
||||
modGps(() => { return {
|
||||
"lat": NaN,
|
||||
"lon": NaN,
|
||||
"alt": NaN,
|
||||
"speed": NaN,
|
||||
"course": NaN,
|
||||
"time": Date.now(),
|
||||
"satellites": 2,
|
||||
"fix": 0,
|
||||
"hdop": NaN
|
||||
};});
|
||||
} else if (settings.gps.name == "changingfix") {
|
||||
let currentSpeed=1;
|
||||
let currentLat=20;
|
||||
let currentLon=10;
|
||||
let currentCourse=10;
|
||||
let currentAlt=-100;
|
||||
let currentSats=5;
|
||||
modGps(() => {
|
||||
currentLat += 0.1;
|
||||
if (currentLat > 50) currentLat = 20;
|
||||
currentLon += 0.1;
|
||||
if (currentLon > 20) currentLon = 10;
|
||||
currentSpeed *= 10;
|
||||
if (currentSpeed > 1000) currentSpeed = 1;
|
||||
currentCourse += 12;
|
||||
if (currentCourse > 360) currentCourse -= 360;
|
||||
currentSats += 1;
|
||||
if (currentSats > 10) currentSats = 5;
|
||||
currentAlt *= 10;
|
||||
if (currentAlt > 1000) currentAlt = -100;
|
||||
return {
|
||||
"lat": currentLat,
|
||||
"lon": currentLon,
|
||||
"alt": currentAlt,
|
||||
"speed": currentSpeed,
|
||||
"course": currentCourse,
|
||||
"time": Date.now(),
|
||||
"satellites": currentSats,
|
||||
"fix": 1,
|
||||
"hdop": 1
|
||||
};});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.mag && settings.mag.enabled) {
|
||||
log("MAG", settings.mag);
|
||||
let modMag = function(data) {
|
||||
setInterval(() => {
|
||||
Bangle.getCompass = data;
|
||||
Bangle.sensortoolsOrigEmit("mag", data());
|
||||
}, 100);
|
||||
};
|
||||
if (settings.mag.power) {
|
||||
Bangle.sensortoolsOrigSetCompassPower = Bangle.setCompassPower;
|
||||
Bangle.setCompassPower = createPowerFunction(settings.mag.power, "Compass", Bangle.sensortoolsOrigSetCompassPower);
|
||||
}
|
||||
if (settings.mag.mode == "emulate") {
|
||||
if (settings.mag.name == "static") {
|
||||
modMag(()=>{return {
|
||||
x: 1,
|
||||
y: 1,
|
||||
z: 1,
|
||||
dx: 1,
|
||||
dy: 1,
|
||||
dz: 1,
|
||||
heading: 0
|
||||
};});
|
||||
} else if (settings.mag.name == "rotate"){
|
||||
let last = 0;
|
||||
modMag(()=>{return {
|
||||
x: 1,
|
||||
y: 1,
|
||||
z: 1,
|
||||
dx: 1,
|
||||
dy: 1,
|
||||
dz: 1,
|
||||
heading: last = (last+1)%360,
|
||||
};});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
|
@ -2,7 +2,7 @@
|
|||
"id": "sensortools",
|
||||
"name": "Sensor tools",
|
||||
"shortName": "Sensor tools",
|
||||
"version": "0.01",
|
||||
"version": "0.02",
|
||||
"description": "Tools for testing and debugging apps that use sensor input",
|
||||
"icon": "icon.png",
|
||||
"type": "bootloader",
|
||||
|
@ -12,6 +12,7 @@
|
|||
"storage": [
|
||||
{"name":"sensortools.0.boot.js","url":"boot.js"},
|
||||
{"name":"sensortools.settings.js","url":"settings.js"},
|
||||
{"name":"sensortools","url":"lib.js"},
|
||||
{"name":"sensortools.default.json","url":"default.json"}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
0.01: New app.
|
||||
0.02: Restructure menu.
|
||||
0.03: change handling of intent extras.
|
||||
0.04: New layout.
|
||||
0.05: Add widgets field. Tweak layout.
|
|
@ -0,0 +1,21 @@
|
|||
Requires Gadgetbridge 71.0 or later. Allow intents in Gadgetbridge in order for this app to work.
|
||||
|
||||
Touch input:
|
||||
|
||||
Press the different ui elements to control Podcast Addict and open menus. Press left or right arrow to go to previous/next track.
|
||||
|
||||
Swipe input:
|
||||
|
||||
Swipe left/right to go to previous/next track. Swipe up/down to change the volume.
|
||||
|
||||
It's possible to start tracks by searching with the remote. Search term without tags will override search with tags.
|
||||
|
||||
To start playing 'from cold' the command for previous/next track via touch or swipe can be used. Pressing just play/pause is not guaranteed to initiate spotify in all circumstances (this will probably change with subsequent releases).
|
||||
|
||||
In order to search to play or start music from the 'Saved' menu the Android device must be awake and unlocked. The remote can wake and unlock the device if the Bangle.js has been added as a 'trusted device' under Android Settings->Security->Smart Lock->Trusted devices.
|
||||
|
||||
The swipe logic was inspired by the implementation in [rigrig](https://git.tubul.net/rigrig/)'s Scrolling Messages.
|
||||
|
||||
Spotify Remote was created by [thyttan](https://github.com/thyttan/).
|
||||
|
||||
<a target="_blank" href="https://icons8.com/icon/63316/spotify">Spotify</a> icon by <a target="_blank" href="https://icons8.com">Icons8</a>
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwhC/AFV3AAQVVDKQWHDB0HC5NwCyoYMCxZJKFxgwKCxowJC6xGOJBAWPGA4MGogXOIwdCmf/AAczkhIKC4VzCogAD+YZEC49PC5AABmgXO+czJYoYDC4gfCuRYGoUjDAZ4GUJlyn4XNukjIwMzmVHBAU/+YXKoZ0GmQLCDgQXIU5IVDC5JVCIwIECDA5HIR4hkBDAX0C5YAHOoIXJa4QRDoUikiOEm7vKE4YADmZ1FC5N/R48nC5tzFQMiokimYYHC4h4KJwX3Ow6QMOwoXGSAoAKIwrBNFxIXZJBxGHGB4WIGBouJDBgWLJJYWMDBIWODIwVRAH4AXA="))
|
|
@ -0,0 +1,281 @@
|
|||
/*
|
||||
Bluetooth.println(JSON.stringify({t:"intent", action:"", flags:["flag1", "flag2",...], categories:["category1","category2",...], mimetype:"", data:"", package:"", class:"", target:"", extra:{someKey:"someValueOrString"}}));
|
||||
*/
|
||||
|
||||
var R;
|
||||
var backToMenu = false;
|
||||
var isPaused = true;
|
||||
var dark = g.theme.dark; // bool
|
||||
|
||||
// The main layout of the app
|
||||
function gfx() {
|
||||
//Bangle.drawWidgets();
|
||||
R = Bangle.appRect;
|
||||
marigin = 8;
|
||||
// g.drawString(str, x, y, solid)
|
||||
g.clearRect(R);
|
||||
g.reset();
|
||||
|
||||
if (dark) {g.setColor(0x07E0);} else {g.setColor(0x03E0);} // Green on dark theme, DarkGreen on light theme.
|
||||
g.setFont("4x6:2");
|
||||
g.setFontAlign(1, 0, 0);
|
||||
g.drawString("->", R.x2 - marigin, R.y + R.h/2);
|
||||
|
||||
g.setFontAlign(-1, 0, 0);
|
||||
g.drawString("<-", R.x + marigin, R.y + R.h/2);
|
||||
|
||||
g.setFontAlign(-1, 0, 1);
|
||||
g.drawString("<-", R.x + R.w/2, R.y + marigin);
|
||||
|
||||
g.setFontAlign(1, 0, 1);
|
||||
g.drawString("->", R.x + R.w/2, R.y2 - marigin);
|
||||
|
||||
g.setFontAlign(0, 0, 0);
|
||||
g.drawString("Play\nPause", R.x + R.w/2, R.y + R.h/2);
|
||||
|
||||
g.setFontAlign(-1, -1, 0);
|
||||
g.drawString("Menu", R.x + 2*marigin, R.y + 2*marigin);
|
||||
|
||||
g.setFontAlign(-1, 1, 0);
|
||||
g.drawString("Wake", R.x + 2*marigin, R.y + R.h - 2*marigin);
|
||||
|
||||
g.setFontAlign(1, -1, 0);
|
||||
g.drawString("Srch", R.x + R.w - 2*marigin, R.y + 2*marigin);
|
||||
|
||||
g.setFontAlign(1, 1, 0);
|
||||
g.drawString("Saved", R.x + R.w - 2*marigin, R.y + R.h - 2*marigin);
|
||||
}
|
||||
|
||||
// Touch handler for main layout
|
||||
function touchHandler(_, xy) {
|
||||
x = xy.x;
|
||||
y = xy.y;
|
||||
len = (R.w<R.h+1)?(R.w/3):(R.h/3);
|
||||
|
||||
// doing a<b+1 seemed faster than a<=b, also using a>b-1 instead of a>b.
|
||||
if ((R.x-1<x && x<R.x+len) && (R.y-1<y && y<R.y+len)) {
|
||||
//Menu
|
||||
Bangle.removeAllListeners("touch");
|
||||
Bangle.removeAllListeners("swipe");
|
||||
backToMenu = true;
|
||||
E.showMenu(spotifyMenu);
|
||||
} else if ((R.x-1<x && x<R.x+len) && (R.y2-len<y && y<R.y2+1)) {
|
||||
//Wake
|
||||
gadgetbridgeWake();
|
||||
gadgetbridgeWake();
|
||||
} else if ((R.x2-len<x && x<R.x2+1) && (R.y-1<y && y<R.y+len)) {
|
||||
//Srch
|
||||
Bangle.removeAllListeners("touch");
|
||||
Bangle.removeAllListeners("swipe");
|
||||
E.showMenu(searchMenu);
|
||||
} else if ((R.x2-len<x && x<R.x2+1) && (R.y2-len<y && y<R.y2+1)) {
|
||||
//Saved
|
||||
Bangle.removeAllListeners("touch");
|
||||
Bangle.removeAllListeners("swipe");
|
||||
E.showMenu(savedMenu);
|
||||
} else if ((R.x-1<x && x<R.x+len) && (R.y+R.h/2-len/2<y && y<R.y+R.h/2+len/2)) {
|
||||
//Previous
|
||||
spotifyWidget("PREVIOUS");
|
||||
} else if ((R.x2-len+1<x && x<R.x2+1) && (R.y+R.h/2-len/2<y && y<R.y+R.h/2+len/2)) {
|
||||
//Next
|
||||
spotifyWidget("NEXT");
|
||||
} else if ((R.x-1<x && x<R.x2+1) && (R.y-1<y && y<R.y2+1)){
|
||||
//play/pause
|
||||
playPause = isPaused?"play":"pause";
|
||||
Bangle.musicControl(playPause);
|
||||
isPaused = !isPaused;
|
||||
}
|
||||
}
|
||||
|
||||
// Swipe handler for main layout, used to jump backward and forward within a podcast episode.
|
||||
function swipeHandler(LR, _) {
|
||||
if (LR==-1) {
|
||||
spotifyWidget("NEXT");
|
||||
}
|
||||
if (LR==1) {
|
||||
spotifyWidget("PREVIOUS");
|
||||
}
|
||||
}
|
||||
|
||||
// Navigation input on the main layout
|
||||
function setUI() {
|
||||
// Bangle.setUI code from rigrig's smessages app for volume control: https://git.tubul.net/rigrig/BangleApps/src/branch/personal/apps/smessages/app.js
|
||||
Bangle.setUI(
|
||||
{mode : "updown", back : load},
|
||||
ud => {
|
||||
if (ud) Bangle.musicControl(ud>0 ? "volumedown" : "volumeup");
|
||||
}
|
||||
);
|
||||
Bangle.on("touch", touchHandler);
|
||||
Bangle.on("swipe", swipeHandler);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Get back to the main layout
|
||||
function backToGfx() {
|
||||
E.showMenu();
|
||||
g.clear();
|
||||
g.reset();
|
||||
Bangle.removeAllListeners("touch");
|
||||
Bangle.removeAllListeners("swipe");
|
||||
setUI();
|
||||
gfx();
|
||||
backToMenu = false;
|
||||
}
|
||||
|
||||
/*
|
||||
The functions for interacting with Android and the Spotify app
|
||||
*/
|
||||
|
||||
simpleSearch = "";
|
||||
function simpleSearchTerm() { // input a simple search term without tags, overrides search with tags (artist and track)
|
||||
require("textinput").input({text:simpleSearch}).then(result => {simpleSearch = result;}).then(() => {E.showMenu(searchMenu);});
|
||||
}
|
||||
|
||||
artist = "";
|
||||
function artistSearchTerm() { // input artist to search for
|
||||
require("textinput").input({text:artist}).then(result => {artist = result;}).then(() => {E.showMenu(searchMenu);});
|
||||
}
|
||||
|
||||
track = "";
|
||||
function trackSearchTerm() { // input track to search for
|
||||
require("textinput").input({text:track}).then(result => {track = result;}).then(() => {E.showMenu(searchMenu);});
|
||||
}
|
||||
|
||||
album = "";
|
||||
function albumSearchTerm() { // input album to search for
|
||||
require("textinput").input({text:album}).then(result => {album = result;}).then(() => {E.showMenu(searchMenu);});
|
||||
}
|
||||
|
||||
function searchPlayWOTags() {//make a spotify search and play using entered terms
|
||||
searchString = simpleSearch;
|
||||
Bluetooth.println(JSON.stringify({t:"intent", action:"android.media.action.MEDIA_PLAY_FROM_SEARCH", categories:["android.intent.category.DEFAULT"], package:"com.spotify.music", target:"activity", extra:{query:searchString}, flags:["FLAG_ACTIVITY_NEW_TASK"]}));
|
||||
}
|
||||
|
||||
function searchPlayWTags() {//make a spotify search and play using entered terms
|
||||
searchString = (artist=="" ? "":("artist:\""+artist+"\"")) + ((artist!="" && track!="") ? " ":"") + (track=="" ? "":("track:\""+track+"\"")) + (((artist!="" && album!="") || (track!="" && album!="")) ? " ":"") + (album=="" ? "":(" album:\""+album+"\""));
|
||||
Bluetooth.println(JSON.stringify({t:"intent", action:"android.media.action.MEDIA_PLAY_FROM_SEARCH", categories:["android.intent.category.DEFAULT"], package:"com.spotify.music", target:"activity", extra:{query:searchString}, flags:["FLAG_ACTIVITY_NEW_TASK"]}));
|
||||
}
|
||||
|
||||
function playVreden() {//Play the track "Vreden" by Sara Parkman via spotify uri-link
|
||||
Bluetooth.println(JSON.stringify({t:"intent", action:"android.intent.action.VIEW", categories:["android.intent.category.DEFAULT"], package:"com.spotify.music", data:"spotify:track:5QEFFJ5tAeRlVquCUNpAJY:play", target:"activity" , flags:["FLAG_ACTIVITY_NEW_TASK", "FLAG_ACTIVITY_NO_ANIMATION"/*, "FLAG_ACTIVITY_CLEAR_TOP", "FLAG_ACTIVITY_PREVIOUS_IS_TOP"*/]}));
|
||||
}
|
||||
|
||||
function playVredenAlternate() {//Play the track "Vreden" by Sara Parkman via spotify uri-link
|
||||
Bluetooth.println(JSON.stringify({t:"intent", action:"android.intent.action.VIEW", categories:["android.intent.category.DEFAULT"], package:"com.spotify.music", data:"spotify:track:5QEFFJ5tAeRlVquCUNpAJY:play", target:"activity" , flags:["FLAG_ACTIVITY_NEW_TASK"]}));
|
||||
}
|
||||
|
||||
function searchPlayVreden() {//Play the track "Vreden" by Sara Parkman via search and play
|
||||
Bluetooth.println(JSON.stringify({t:"intent", action:"android.media.action.MEDIA_PLAY_FROM_SEARCH", categories:["android.intent.category.DEFAULT"], package:"com.spotify.music", target:"activity", extra:{query:'artist:"Sara Parkman" track:"Vreden"'}, flags:["FLAG_ACTIVITY_NEW_TASK"]}));
|
||||
}
|
||||
|
||||
function openAlbum() {//Play EP "The Blue Room" by Coldplay
|
||||
Bluetooth.println(JSON.stringify({t:"intent", action:"android.intent.action.VIEW", categories:["android.intent.category.DEFAULT"], package:"com.spotify.music", data:"spotify:album:3MVb2CWB36x7VwYo5sZmf2", target:"activity", flags:["FLAG_ACTIVITY_NEW_TASK"]}));
|
||||
}
|
||||
|
||||
function searchPlayAlbum() {//Play EP "The Blue Room" by Coldplay via search and play
|
||||
Bluetooth.println(JSON.stringify({t:"intent", action:"android.media.action.MEDIA_PLAY_FROM_SEARCH", categories:["android.intent.category.DEFAULT"], package:"com.spotify.music", target:"activity", extra:{query:'album:"The blue room" artist:"Coldplay"', "android.intent.extra.focus":"vnd.android.cursor.item/album"}, flags:["FLAG_ACTIVITY_NEW_TASK"]}));
|
||||
}
|
||||
|
||||
function spotifyWidget(action) {
|
||||
Bluetooth.println(JSON.stringify({t:"intent", action:("com.spotify.mobile.android.ui.widget."+action), package:"com.spotify.music", target:"broadcastreceiver"}));
|
||||
}
|
||||
|
||||
function gadgetbridgeWake() {
|
||||
Bluetooth.println(JSON.stringify({t:"intent", target:"activity", flags:["FLAG_ACTIVITY_NEW_TASK", "FLAG_ACTIVITY_CLEAR_TASK", "FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS", "FLAG_ACTIVITY_NO_ANIMATION"], package:"gadgetbridge", class:"nodomain.freeyourgadget.gadgetbridge.activities.WakeActivity"}));
|
||||
}
|
||||
|
||||
function spotifyPlaylistDW() {
|
||||
Bluetooth.println(JSON.stringify({t:"intent", action:"android.intent.action.VIEW", categories:["android.intent.category.DEFAULT"], package:"com.spotify.music", data:"spotify:user:spotify:playlist:37i9dQZEVXcRfaeEbxXIgb:play", target:"activity" , flags:["FLAG_ACTIVITY_NEW_TASK", "FLAG_ACTIVITY_NO_ANIMATION"/*, "FLAG_ACTIVITY_CLEAR_TOP", "FLAG_ACTIVITY_PREVIOUS_IS_TOP"*/]}));
|
||||
}
|
||||
|
||||
function spotifyPlaylistDM1() {
|
||||
Bluetooth.println(JSON.stringify({t:"intent", action:"android.intent.action.VIEW", categories:["android.intent.category.DEFAULT"], package:"com.spotify.music", data:"spotify:user:spotify:playlist:37i9dQZF1E365VyzxE0mxF:play", target:"activity" , flags:["FLAG_ACTIVITY_NEW_TASK", "FLAG_ACTIVITY_NO_ANIMATION"/*, "FLAG_ACTIVITY_CLEAR_TOP", "FLAG_ACTIVITY_PREVIOUS_IS_TOP"*/]}));
|
||||
}
|
||||
|
||||
function spotifyPlaylistDM2() {
|
||||
Bluetooth.println(JSON.stringify({t:"intent", action:"android.intent.action.VIEW", categories:["android.intent.category.DEFAULT"], package:"com.spotify.music", data:"spotify:user:spotify:playlist:37i9dQZF1E38LZHLFnrM61:play", target:"activity" , flags:["FLAG_ACTIVITY_NEW_TASK", "FLAG_ACTIVITY_NO_ANIMATION"/*, "FLAG_ACTIVITY_CLEAR_TOP", "FLAG_ACTIVITY_PREVIOUS_IS_TOP"*/]}));
|
||||
}
|
||||
|
||||
function spotifyPlaylistDM3() {
|
||||
Bluetooth.println(JSON.stringify({t:"intent", action:"android.intent.action.VIEW", categories:["android.intent.category.DEFAULT"], package:"com.spotify.music", data:"spotify:user:spotify:playlist:37i9dQZF1E36RU87qzgBFP:play", target:"activity" , flags:["FLAG_ACTIVITY_NEW_TASK", "FLAG_ACTIVITY_NO_ANIMATION"/*, "FLAG_ACTIVITY_CLEAR_TOP", "FLAG_ACTIVITY_PREVIOUS_IS_TOP"*/]}));
|
||||
}
|
||||
|
||||
function spotifyPlaylistDM4() {
|
||||
Bluetooth.println(JSON.stringify({t:"intent", action:"android.intent.action.VIEW", categories:["android.intent.category.DEFAULT"], package:"com.spotify.music", data:"spotify:user:spotify:playlist:37i9dQZF1E396gGyCXEBFh:play", target:"activity" , flags:["FLAG_ACTIVITY_NEW_TASK", "FLAG_ACTIVITY_NO_ANIMATION"/*, "FLAG_ACTIVITY_CLEAR_TOP", "FLAG_ACTIVITY_PREVIOUS_IS_TOP"*/]}));
|
||||
}
|
||||
|
||||
function spotifyPlaylistDM5() {
|
||||
Bluetooth.println(JSON.stringify({t:"intent", action:"android.intent.action.VIEW", categories:["android.intent.category.DEFAULT"], package:"com.spotify.music", data:"spotify:user:spotify:playlist:37i9dQZF1E37a0Tt6CKJLP:play", target:"activity" , flags:["FLAG_ACTIVITY_NEW_TASK", "FLAG_ACTIVITY_NO_ANIMATION"/*, "FLAG_ACTIVITY_CLEAR_TOP", "FLAG_ACTIVITY_PREVIOUS_IS_TOP"*/]}));
|
||||
}
|
||||
|
||||
function spotifyPlaylistDM6() {
|
||||
Bluetooth.println(JSON.stringify({t:"intent", action:"android.intent.action.VIEW", categories:["android.intent.category.DEFAULT"], package:"com.spotify.music", data:"spotify:user:spotify:playlist:37i9dQZF1E36UIQLQK79od:play", target:"activity" , flags:["FLAG_ACTIVITY_NEW_TASK", "FLAG_ACTIVITY_NO_ANIMATION"/*, "FLAG_ACTIVITY_CLEAR_TOP", "FLAG_ACTIVITY_PREVIOUS_IS_TOP"*/]}));
|
||||
}
|
||||
|
||||
function spotifyPlaylistDD() {
|
||||
Bluetooth.println(JSON.stringify({t:"intent", action:"android.intent.action.VIEW", categories:["android.intent.category.DEFAULT"], package:"com.spotify.music", data:"spotify:user:spotify:playlist:37i9dQZF1EfWFiI7QfIAKq:play", target:"activity" , flags:["FLAG_ACTIVITY_NEW_TASK", "FLAG_ACTIVITY_NO_ANIMATION"/*, "FLAG_ACTIVITY_CLEAR_TOP", "FLAG_ACTIVITY_PREVIOUS_IS_TOP"*/]}));
|
||||
}
|
||||
|
||||
function spotifyPlaylistRR() {
|
||||
Bluetooth.println(JSON.stringify({t:"intent", action:"android.intent.action.VIEW", categories:["android.intent.category.DEFAULT"], package:"com.spotify.music", data:"spotify:user:spotify:playlist:37i9dQZEVXbs0XkE2V8sMO:play", target:"activity" , flags:["FLAG_ACTIVITY_NEW_TASK", "FLAG_ACTIVITY_NO_ANIMATION"/*, "FLAG_ACTIVITY_CLEAR_TOP", "FLAG_ACTIVITY_PREVIOUS_IS_TOP"*/]}));
|
||||
}
|
||||
|
||||
// Spotify Remote Menu
|
||||
var spotifyMenu = {
|
||||
"" : { title : " ",
|
||||
back: backToGfx },
|
||||
"Controls" : ()=>{E.showMenu(controlMenu);},
|
||||
"Search and play" : ()=>{E.showMenu(searchMenu);},
|
||||
"Saved music" : ()=>{E.showMenu(savedMenu);},
|
||||
"Wake the android" : function() {gadgetbridgeWake();gadgetbridgeWake();},
|
||||
"Exit Spotify Remote" : ()=>{load();}
|
||||
};
|
||||
|
||||
|
||||
var controlMenu = {
|
||||
"" : { title : " ",
|
||||
back: () => {if (backToMenu) E.showMenu(spotifyMenu);
|
||||
if (!backToMenu) backToGfx();} },
|
||||
"Play" : ()=>{Bangle.musicControl("play");},
|
||||
"Pause" : ()=>{Bangle.musicControl("pause");},
|
||||
"Previous" : ()=>{spotifyWidget("PREVIOUS");},
|
||||
"Next" : ()=>{spotifyWidget("NEXT");},
|
||||
"Play (widget, next then previous)" : ()=>{spotifyWidget("NEXT"); spotifyWidget("PREVIOUS");},
|
||||
"Messages Music Controls" : ()=>{load("messagesmusic.app.js");},
|
||||
};
|
||||
|
||||
var searchMenu = {
|
||||
"" : { title : " ",
|
||||
back: () => {if (backToMenu) E.showMenu(spotifyMenu);
|
||||
if (!backToMenu) backToGfx();} },
|
||||
"Search term w/o tags" : ()=>{simpleSearchTerm();},
|
||||
"Execute search and play w/o tags" : ()=>{searchPlayWOTags();},
|
||||
"Search term w tag \"artist\"" : ()=>{artistSearchTerm();},
|
||||
"Search term w tag \"track\"" : ()=>{trackSearchTerm();},
|
||||
"Search term w tag \"album\"" : ()=>{albumSearchTerm();},
|
||||
"Execute search and play with tags" : ()=>{searchPlayWTags();},
|
||||
};
|
||||
|
||||
var savedMenu = {
|
||||
"" : { title : " ",
|
||||
back: () => {if (backToMenu) E.showMenu(spotifyMenu);
|
||||
if (!backToMenu) backToGfx();} },
|
||||
"Play Discover Weekly" : ()=>{spotifyPlaylistDW();},
|
||||
"Play Daily Mix 1" : ()=>{spotifyPlaylistDM1();},
|
||||
"Play Daily Mix 2" : ()=>{spotifyPlaylistDM2();},
|
||||
"Play Daily Mix 3" : ()=>{spotifyPlaylistDM3();},
|
||||
"Play Daily Mix 4" : ()=>{spotifyPlaylistDM4();},
|
||||
"Play Daily Mix 5" : ()=>{spotifyPlaylistDM5();},
|
||||
"Play Daily Mix 6" : ()=>{spotifyPlaylistDM6();},
|
||||
"Play Daily Drive" : ()=>{spotifyPlaylistDD();},
|
||||
"Play Release Radar" : ()=>{spotifyPlaylistRR();},
|
||||
"Play \"Vreden\" by Sara Parkman via uri-link" : ()=>{playVreden();},
|
||||
"Open \"The Blue Room\" EP (no autoplay)" : ()=>{openAlbum();},
|
||||
"Play \"The Blue Room\" EP via search&play" : ()=>{searchPlayAlbum();},
|
||||
};
|
||||
|
||||
Bangle.loadWidgets();
|
||||
setUI();
|
||||
gfx();
|
After Width: | Height: | Size: 1.8 KiB |
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"id": "spotrem",
|
||||
"name": "Remote for Spotify",
|
||||
"version": "0.05",
|
||||
"description": "Control spotify on your android device.",
|
||||
"readme": "README.md",
|
||||
"type": "app",
|
||||
"tags": "spotify,music,player,remote,control,intent,intents,gadgetbridge,spotrem",
|
||||
"icon": "app.png",
|
||||
"screenshots" : [ {"url":"screenshot1.png"}, {"url":"screenshot2.png"} ],
|
||||
"supports": ["BANGLEJS2"],
|
||||
"dependencies": { "textinput":"type"},
|
||||
"storage": [
|
||||
{"name":"spotrem.app.js","url":"app.js"},
|
||||
{"name":"spotrem.img","url":"app-icon.js","evaluate":true}
|
||||
]
|
||||
}
|
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 3.2 KiB |
|
@ -14,6 +14,34 @@ You can view the full report through the app:
|
|||
If using the `Bangle.js Gadgetbridge` app on your phone (as opposed to the standard F-Droid `Gadgetbridge`) you need to set the package name
|
||||
to `com.espruino.gadgetbridge.banglejs` in the settings of the weather app (`settings -> gadgetbridge support -> package name`).
|
||||
|
||||
## Android Weather Apps
|
||||
|
||||
There are two weather apps for Android that can connect with Gadgetbridge
|
||||
* Tiny Weather Forecast Germany
|
||||
** F-Droid - https://f-droid.org/en/packages/de.kaffeemitkoffein.tinyweatherforecastgermany/
|
||||
** Source code - https://codeberg.org/Starfish/TinyWeatherForecastGermany
|
||||
* QuickWeather
|
||||
** F-Droid - https://f-droid.org/en/packages/com.ominous.quickweather/
|
||||
** Google Play - https://play.google.com/store/apps/details?id=com.ominous.quickweather
|
||||
** Source code - https://github.com/TylerWilliamson/QuickWeather
|
||||
|
||||
### Tiny Weather Forecast Germany
|
||||
Even though Tiny Weather Forecast Germany is made for Germany, it can be used around the world. To do this:
|
||||
|
||||
1. Tap on the three dots in the top right hand corner and go to settings
|
||||
2. Go down to Location and tap on the checkbox labeled "Use location services". You may also want to check on the "Check Location checkbox". Alternatively, you may select the "manual" checkbox and choose your location.
|
||||
3. Scroll down further to the "other" section and tap "Gadgetbridge support". Then tap on "Enable". You may also choose to tap on "Send current time".
|
||||
4. If you're using the specific Gadgetbridge for Bangle.JS app, you'll want to tap on "Package name." In the dialog box that appears, you'll want to put in "com.espruino.gadgetbridge.banglejs" without the quotes. If you're using the original Gadgetbridge, leave this as the default.
|
||||
|
||||
|
||||
### QuickWeather
|
||||
QuickWeather requires an OpenWeatherMap API. You will need the "One Call By Call" plan, which is free if you're not making too many calls. Sign up or get more information at https://openweathermap.org/api
|
||||
|
||||
1. When you first load QuickWeather, it will take you through the setup process. You will fill out all the required information as well as put your API key in. If you do not have the "One Call By Call", or commonly known as "One Call", API, you will need to sign up for that. QuickWeather will work automatically with both the main version of Gadgetbridge and Gadgetbridge for bangle.JS.
|
||||
|
||||
### Weather Notification
|
||||
* Note - at one time, the Weather Notification app also worked with Gadgetbridge. However, many users are reporting it's no longer seeing the OpenWeatherMap API key as valid. The app has not received any updates since August of 2020, and may be unmaintained.
|
||||
|
||||
## Settings
|
||||
|
||||
* Expiration timespan can be set after which the local weather data is considered as invalid
|
||||
|
|
|
@ -3,3 +3,4 @@
|
|||
0.04: Fix regression stopping correct widget updates
|
||||
0.05: Don't show clock widget if already showing clock app
|
||||
0.06: Use 7 segment font, update *on* the minute, use less memory
|
||||
0.07: allow turning on/off when quick-switching apps
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "widclk",
|
||||
"name": "Digital clock widget",
|
||||
"version": "0.06",
|
||||
"version": "0.07",
|
||||
"description": "A simple digital clock widget",
|
||||
"icon": "widget.png",
|
||||
"type": "widget",
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
/* Simple clock that appears in the widget bar if no other clock
|
||||
is running. We update once per minute, but don't bother stopping
|
||||
if the */
|
||||
|
||||
// don't show widget if we know we have a clock app running
|
||||
if (!Bangle.CLOCK) WIDGETS["wdclk"]={area:"tl",width:52/* g.stringWidth("00:00") */,draw:function() {
|
||||
g.reset().setFontCustom(atob("AAAAAAAAAAIAAAQCAQAAAd0BgMBdwAAAAAAAdwAB0RiMRcAAAERiMRdwAcAQCAQdwAcERiMRBwAd0RiMRBwAAEAgEAdwAd0RiMRdwAcERiMRdwAFAAd0QiEQdwAdwRCIRBwAd0BgMBAAABwRCIRdwAd0RiMRAAAd0QiEQAAAAAAAAAA="), 32, atob("BgAAAAAAAAAAAAAAAAYCAAYGBgYGBgYGBgYCAAAAAAAABgYGBgYG"), 512+9);
|
||||
WIDGETS["wdclk"]={area:"tl",width:Bangle.CLOCK?0:52/* g.stringWidth("00:00") */,draw:function() {
|
||||
if (!Bangle.CLOCK == !this.width) { // if we're the wrong size for if we have a clock or not...
|
||||
this.width = Bangle.CLOCK?0:52;
|
||||
return setTimeout(Bangle.drawWidgets,1); // widget changed size - redraw
|
||||
}
|
||||
if (!this.width) return; // if size not right, return
|
||||
g.reset().setFontCustom(atob("AAAAAAAAAAIAAAQCAQAAAd0BgMBdwAAAAAAAdwAB0RiMRcAAAERiMRdwAcAQCAQdwAcERiMRBwAd0RiMRBwAAEAgEAdwAd0RiMRdwAcERiMRdwAFAAd0QiEQdwAdwRCIRBwAd0BgMBAAABwRCIRdwAd0RiMRAAAd0QiEQAAAAAAAAAA="), 32, atob("BgAAAAAAAAAAAAAAAAYCAAYGBgYGBgYGBgYCAAAAAAAABgYGBgYG"), 512+9);
|
||||
var time = require("locale").time(new Date(),1);
|
||||
g.drawString(time, this.x, this.y+3, true); // 5 * 6*2 = 60
|
||||
// queue draw in one minute
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
0.01: Fork of widclk v0.04 github.com/espruino/BangleApps/tree/master/apps/widclk
|
||||
0.02: Modification for bottom widget area and text color
|
||||
0.03: based in widclk v0.05 compatible at same time, bottom area and color
|
||||
|
||||
0.04: refactored to use less memory, and allow turning on/off when quick-switching apps
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
"id": "widclkbttm",
|
||||
"name": "Digital clock (Bottom) widget",
|
||||
"shortName": "Digital clock Bottom Widget",
|
||||
"version": "0.03",
|
||||
"description": "Displays time in the bottom area.",
|
||||
"version": "0.04",
|
||||
"description": "Displays time in the bottom of the screen (may not be compatible with some apps)",
|
||||
"icon": "widclkbttm.png",
|
||||
"type": "widget",
|
||||
"tags": "widget",
|
||||
|
|
|
@ -1,31 +1,16 @@
|
|||
(function() {
|
||||
// don't show widget if we know we have a clock app running
|
||||
if (Bangle.CLOCK) return;
|
||||
|
||||
let intervalRef = null;
|
||||
var width = 5 * 6*2;
|
||||
var text_color=0x07FF;//cyan
|
||||
|
||||
function draw() {
|
||||
g.reset().setFont("6x8", 2).setFontAlign(-1, 0).setColor(text_color);
|
||||
var time = require("locale").time(new Date(),1);
|
||||
g.drawString(time, this.x, this.y+11, true); // 5 * 6*2 = 60
|
||||
WIDGETS["wdclkbttm"]={area:"br",width:Bangle.CLOCK?0:60,draw:function() {
|
||||
if (!Bangle.CLOCK == !this.width) { // if we're the wrong size for if we have a clock or not...
|
||||
this.width = Bangle.CLOCK?0:60;
|
||||
return setTimeout(Bangle.drawWidgets,1); // widget changed size - redraw
|
||||
}
|
||||
function clearTimers(){
|
||||
if(intervalRef) {
|
||||
clearInterval(intervalRef);
|
||||
intervalRef = null;
|
||||
}
|
||||
}
|
||||
function startTimers(){
|
||||
intervalRef = setInterval(()=>WIDGETS["wdclkbttm"].draw(), 60*1000);
|
||||
WIDGETS["wdclkbttm"].draw();
|
||||
}
|
||||
Bangle.on('lcdPower', (on) => {
|
||||
clearTimers();
|
||||
if (on) startTimers();
|
||||
});
|
||||
|
||||
WIDGETS["wdclkbttm"]={area:"br",width:width,draw:draw};
|
||||
if (Bangle.isLCDOn) intervalRef = setInterval(()=>WIDGETS["wdclkbttm"].draw(), 60*1000);
|
||||
})()
|
||||
if (!this.width) return; // if size not right, return
|
||||
g.reset().setFont("6x8", 2).setFontAlign(-1, 0).setColor("#0ff"); // cyan
|
||||
var time = require("locale").time(new Date(),1);
|
||||
g.drawString(time, this.x, this.y+11, true); // 5 * 6*2 = 60
|
||||
// queue draw in one minute
|
||||
if (this.drawTimeout) clearTimeout(this.drawTimeout);
|
||||
this.drawTimeout = setTimeout(()=>{
|
||||
this.drawTimeout = undefined;
|
||||
this.draw();
|
||||
}, 60000 - (Date.now() % 60000));
|
||||
}};
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
0.01: New widget!
|
||||
0.01: New widget!
|
||||
0.02: allow turning on/off when quick-switching apps
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "widclose",
|
||||
"name": "Close Button",
|
||||
"version": "0.01",
|
||||
"version": "0.02",
|
||||
"description": "A button to close the current app",
|
||||
"readme": "README.md",
|
||||
"icon": "icon.png",
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
if (!Bangle.CLOCK) WIDGETS.close = {
|
||||
area: "tr", width: 24, sortorder: 10, // we want the right-most spot please
|
||||
WIDGETS.close = {
|
||||
area: "tr", width: Bangle.CLOCK?0:24, sortorder: 10, // we want the right-most spot please
|
||||
draw: function() {
|
||||
Bangle.removeListener("touch", this.touch);
|
||||
Bangle.on("touch", this.touch);
|
||||
g.reset().setColor("#f00").drawImage(atob( // hardcoded red to match setUI back button
|
||||
if (!Bangle.CLOCK == !this.width) { // if we're the wrong size for if we have a clock or not...
|
||||
this.width = Bangle.CLOCK?0:24;
|
||||
return setTimeout(Bangle.drawWidgets,1); // widget changed size - redraw
|
||||
}
|
||||
if (this.width) g.reset().setColor("#f00").drawImage(atob( // red to match setUI back button
|
||||
// b/w version of preview.png, 24x24
|
||||
"GBgBABgAAf+AB//gD//wH//4P//8P//8fn5+fjx+fxj+f4H+/8P//8P/f4H+fxj+fjx+fn5+P//8P//8H//4D//wB//gAf+AABgA"
|
||||
), this.x, this.y);
|
||||
}, touch: function(_, c) {
|
||||
const w = WIDGETS.close;
|
||||
if (w && c.x>=w.x && c.x<=w.x+24 && c.y>=w.y && c.y<=w.y+24) load();
|
||||
}, touch: function(_, c) { // if touched
|
||||
const w = WIDGETS.close; // if in range, go back to the clock
|
||||
if (w && c.x>=w.x && c.x<w.x+w.width && c.y>=w.y && c.y<=w.y+24) load();
|
||||
}
|
||||
};
|
||||
};
|
||||
Bangle.on("touch", WIDGETS.close.touch);
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
0.01: New widget!
|
||||
0.01: New widget!
|
||||
0.02: allow turning on/off when quick-switching apps
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "widcloselaunch",
|
||||
"name": "Close Button to launcher",
|
||||
"version": "0.01",
|
||||
"version": "0.02",
|
||||
"description": "A button to close the current app and go to launcher",
|
||||
"readme": "README.md",
|
||||
"icon": "icon.png",
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
if (!Bangle.CLOCK) WIDGETS.close = {
|
||||
area: "tr", width: 24, sortorder: 10, // we want the right-most spot please
|
||||
WIDGETS.close = {
|
||||
area: "tr", width: Bangle.CLOCK?0:24, sortorder: 10, // we want the right-most spot please
|
||||
draw: function() {
|
||||
Bangle.removeListener("touch", this.touch);
|
||||
Bangle.on("touch", this.touch);
|
||||
g.reset().setColor("#f00").drawImage(atob( // hardcoded red to match setUI back button
|
||||
if (!Bangle.CLOCK == !this.width) { // if we're the wrong size for if we have a clock or not...
|
||||
this.width = Bangle.CLOCK?0:24;
|
||||
return setTimeout(Bangle.drawWidgets,1); // widget changed size - redraw
|
||||
}
|
||||
if (this.width) g.reset().setColor("#f00").drawImage(atob( // red to match setUI back button
|
||||
// b/w version of preview.png, 24x24
|
||||
"GBgBABgAAf+AB//gD//wH//4P//8P//8fn5+fjx+fxj+f4H+/8P//8P/f4H+fxj+fjx+fn5+P//8P//8H//4D//wB//gAf+AABgA"
|
||||
), this.x, this.y);
|
||||
}, touch: function(_, c) {
|
||||
const w = WIDGETS.close;
|
||||
if (w && c.x>=w.x && c.x<=w.x+24 && c.y>=w.y && c.y<=w.y+24) Bangle.showLauncher();
|
||||
if (w && c.x>=w.x && c.x<w.x+w.width && c.y>=w.y && c.y<=w.y+24) Bangle.showLauncher();
|
||||
}
|
||||
};
|
||||
Bangle.on("touch", WIDGETS.close.touch);
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
"icon": "icon.png",
|
||||
"type": "widget",
|
||||
"tags": "widget,tool",
|
||||
"supports" : ["BANGLEJS2"],
|
||||
"supports" : ["BANGLEJS","BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
"storage": [
|
||||
{"name":"widdst.wid.js","url":"widget.js"},
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
0.01: New widget!
|
|
@ -0,0 +1,28 @@
|
|||
# Messages Grid Widget
|
||||
|
||||
Widget that displays multiple notification icons in a grid.
|
||||
The widget has a fixed size: if there are multiple notifications it uses smaller
|
||||
icons.
|
||||
It shows a single icon per application, so if you have two SMS messages, the
|
||||
grid only has one SMS icon.
|
||||
If there are multiple messages waiting, the total number is shown in the
|
||||
bottom-right corner.
|
||||
|
||||
Example: one SMS, one Signal, and two WhatsApp messages:
|
||||

|
||||
|
||||
## Installation
|
||||
This widget needs the [`messages`](/?id=messages) app to handle notifications.
|
||||
|
||||
You probably want to disable the default widget, to do so:
|
||||
1. Open `Settings`
|
||||
2. Navigate to `Apps`>`Messages`
|
||||
3. Scroll down to the `Widget messages` entry, and change it to `Hide`
|
||||
|
||||
## Settings
|
||||
This widget uses the `Widget` settings from the `messages` app:
|
||||
|
||||
### Widget
|
||||
* `Flash icon` Toggle flashing of the widget icons.
|
||||
<!-- * `Show read` - Also show the widget when there are only old messages. -->
|
||||
* `Widget messages` Not used by this widget, but you should select `Hide` to hide the default widget.
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"id": "widmsggrid",
|
||||
"name": "Messages Grid Widget",
|
||||
"version": "0.01",
|
||||
"description": "Widget that display notification icons in a grid",
|
||||
"icon": "widget.png",
|
||||
"type": "widget",
|
||||
"dependencies": {"messages":"app"},
|
||||
"tags": "tool,system",
|
||||
"supports": ["BANGLEJS","BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
"storage": [
|
||||
{"name":"widmsggrid.wid.js","url":"widget.js"}
|
||||
],
|
||||
"screenshots": [{"url":"screenshot.png"}]
|
||||
}
|
After Width: | Height: | Size: 10 KiB |
|
@ -0,0 +1,92 @@
|
|||
(function () {
|
||||
if (global.MESSAGES) return; // don't load widget while in the app
|
||||
let settings = require('Storage').readJSON("messages.settings.json", true) || {};
|
||||
const s = {
|
||||
flash: (settings.flash === undefined) ? true : !!settings.flash,
|
||||
showRead: !!settings.showRead,
|
||||
};
|
||||
delete settings;
|
||||
WIDGETS["msggrid"] = {
|
||||
area: "tl", width: 0,
|
||||
flash: s.flash,
|
||||
showRead: s.showRead,
|
||||
init: function() {
|
||||
// runs on first draw
|
||||
delete w.init; // don't run again
|
||||
Bangle.on("touch", w.touch);
|
||||
Bangle.on("message", w.listener);
|
||||
w.listener(); // update status now
|
||||
},
|
||||
draw: function () {
|
||||
if (w.init) w.init();
|
||||
// If we had a setTimeout queued from the last time we were called, remove it
|
||||
if (w.t) {
|
||||
clearTimeout(w.t);
|
||||
delete w.t;
|
||||
}
|
||||
if (!w.width) return;
|
||||
const b = w.flash && w.status === "new" && ((Date.now() / 1000) & 1), // Blink(= inverse colors) on this second?
|
||||
// show multiple icons in a grid, by scaling them down
|
||||
cols = Math.ceil(Math.sqrt(w.srcs.length - 0.1)); // cols===rows, -0.1 to work around rounding error
|
||||
g.reset().clearRect(w.x, w.y, w.x + w.width - 1, w.y + 24)
|
||||
.setClipRect(w.x, w.y, w.x + w.width - 1, w.y + 24); // guard against oversized icons
|
||||
let r = 0, c = 0; // row, column
|
||||
const offset = pos => Math.floor(pos / cols * 24); // pixel offset for position in row/column
|
||||
w.srcs.forEach(src => {
|
||||
const appColor = require("messages").getMessageImageCol(src, require("messages").getMessageImageCol("alert"));
|
||||
let colors = [g.theme.bg, g.setColor(appColor).getColor()];
|
||||
if (b) {
|
||||
if (colors[1] == g.theme.fg) colors = colors.reverse();
|
||||
else colors[1] = g.theme.fg;
|
||||
}
|
||||
g.setColor(colors[1]).setBgColor(colors[0]);
|
||||
g.drawImage(require("messages").getMessageImage(src, "alert"), w.x+offset(c), w.y+offset(r), { scale: 1 / cols });
|
||||
if (++c >= cols) {
|
||||
c = 0;
|
||||
r++;
|
||||
}
|
||||
});
|
||||
if (w.total > 1) {
|
||||
// show total number of messages in bottom-right corner
|
||||
g.reset();
|
||||
if (w.total < 10) g.fillCircle(w.x + w.width - 5, w.y + 20, 4); // single digits get a round background, double digits fill their rectangle
|
||||
g.setColor(g.theme.bg).setBgColor(g.theme.fg)
|
||||
.setFont('6x8').setFontAlign(1, 1)
|
||||
.drawString(w.total, w.x + w.width - 1, w.y + 24, w.total > 9);
|
||||
}
|
||||
if (w.flash && w.status === "new") w.t = setTimeout(w.draw, 1000); // schedule redraw while blinking
|
||||
}, show: function () {
|
||||
w.width = 24;
|
||||
w.srcs = require("messages").getMessages()
|
||||
.filter(m => !['call', 'map', 'music'].includes(m.id))
|
||||
.filter(m => m.new || w.showRead)
|
||||
.map(m => m.src);
|
||||
w.total = w.srcs.length;
|
||||
w.srcs = w.srcs.filter((src, i, uniq) => uniq.indexOf(src) === i); // keep unique entries only
|
||||
Bangle.drawWidgets();
|
||||
Bangle.setLCDPower(1); // turns screen on
|
||||
}, hide: function () {
|
||||
w.width = 0;
|
||||
w.srcs = [];
|
||||
w.total = 0;
|
||||
Bangle.drawWidgets();
|
||||
}, touch: function (b, c) {
|
||||
if (!w || !w.width) return; // widget not shown
|
||||
if (process.env.HWVERSION < 2) {
|
||||
// Bangle.js 1: open app when on clock we touch the side with widget
|
||||
if (!Bangle.CLOCK) return;
|
||||
const m = Bangle.appRect / 2;
|
||||
if ((w.x < m && b !== 1) || (w.x > m && b !== 2)) return;
|
||||
}
|
||||
// Bangle.js 2: open app when touching the widget
|
||||
else if (c.x < w.x || c.x > w.x + w.width || c.y < w.y || c.y > w.y + 24) return;
|
||||
load("messages.app.js");
|
||||
}, listener: function () {
|
||||
w.status = require("messages").status();
|
||||
if (w.status === "new" || (w.status === "old" && w.showRead)) w.show();
|
||||
else w.hide();
|
||||
}
|
||||
};
|
||||
delete s;
|
||||
const w = WIDGETS["msggrid"];
|
||||
})();
|
After Width: | Height: | Size: 10 KiB |
|
@ -0,0 +1,28 @@
|
|||
/* Utilities for handling widgets - mainly showing/hiding */
|
||||
|
||||
/// hide any visible widgets
|
||||
exports.hide = function() {
|
||||
if (!global.WIDGETS) return;
|
||||
g.reset(); // reset colors
|
||||
for (var w of global.WIDGETS) {
|
||||
if (w._draw) return; // already hidden
|
||||
w._draw = w.draw;
|
||||
w.draw = () => {};
|
||||
w._area = w.area;
|
||||
w.area = "";
|
||||
if (w.x!=undefined) g.clearRect(w.x,w.y,w.x+w.width-1,w.y+23);
|
||||
}
|
||||
};
|
||||
|
||||
/// Show any hidden widgets
|
||||
exports.show = function() {
|
||||
if (!global.WIDGETS) return;
|
||||
for (var w of global.WIDGETS) {
|
||||
if (!w._draw) return; // not hidden
|
||||
w.draw = w._draw;
|
||||
w.area = w._area;
|
||||
delete w._draw;
|
||||
delete w._area;
|
||||
w.draw(w);
|
||||
}
|
||||
};
|