Merge branch 'master' into clock_backgrounds

pull/3328/head
Gordon Williams 2024-04-19 15:29:19 +01:00
commit 9561b412fe
124 changed files with 2412 additions and 748 deletions

9
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,9 @@
Contributing to BangleApps
==========================
https://github.com/espruino/BangleApps?tab=readme-ov-file#getting-started
has some links to tutorials on developing for Bangle.js.
Please check out the Wiki to get an idea what sort of things
we'd like to see for contributed apps: https://github.com/espruino/BangleApps/wiki/App-Contribution

View File

@ -9,3 +9,4 @@
0.09: New app screen (instead of showing settings or the alert) and some optimisations
0.10: Add software back button via setUI
0.11: Add setting to unlock screen
0.12: Fix handling that dates can be given as ms since epoch.

View File

@ -3,7 +3,7 @@
"name": "Activity Reminder",
"shortName":"Activity Reminder",
"description": "A reminder to take short walks for the ones with a sedentary lifestyle",
"version":"0.11",
"version":"0.12",
"icon": "app.png",
"type": "app",
"tags": "tool,activity",

View File

@ -8,3 +8,5 @@
0.08: Changed month to day and text color to black on date
0.09: Changed day color back to white
0.10: Add blinking when charging
0.11: Changed battery to buzz instead of blink and fixed battery counter
0.12: Got rid of battery counter

View File

@ -38,14 +38,8 @@ function drawbatrect() {
g.drawRect(Math.floor(mgn/2) + gap + 2 * pos, mgn + gap, Math.floor(mgn/2) + gap + 2 * pos + sq, mgn + gap + sq);
}
function clearbat() {
g.clearRect(Math.floor(mgn/2) + gap + 2 * pos, mgn + gap, Math.floor(mgn/2) + gap + 2 * pos + sq, mgn + gap + sq);
}
function draw() {
let i = 0;
var cnt = 0;
var dt = new Date();
var h = dt.getHours();
var m = dt.getMinutes();
@ -100,15 +94,9 @@ function draw() {
g.drawRect(Math.floor(mgn/2) + gap, mgn + gap, Math.floor(mgn/2) + gap + sq, mgn + gap + sq);
}
if (cnt == 0) {
if (settings.showbat) {
drawbat();
drawbatrect();
}
cnt++;
if (cnt > 29) {
cnt = 0;
}
if (settings.showbat) {
drawbat();
drawbatrect();
}
}
@ -121,21 +109,6 @@ if (!settings.fullscreen) {
Bangle.drawWidgets();
}
var blink = true;
function blinkbat() {
if (blink) {
clearbat();
} else {
drawbat();
}
drawbatrect();
blink = !blink;
}
function getcharging() {
if (Bangle.isCharging()) {
blinkbat();
}
}
setInterval(getcharging, 1000);
Bangle.on('charging', function(charging) {
if(charging) Bangle.buzz();
});

View File

@ -1,7 +1,7 @@
{
"id": "binaryclk",
"name": "Bin Clock",
"version": "0.10",
"version": "0.12",
"description": "Binary clock with date and battery",
"icon": "app-icon.png",
"screenshots": [{"url":"screenshot.png"}],

View File

@ -2,3 +2,4 @@
0.02: Fix double-button press if you press the next button within 30s (#3243)
0.03: Cope with identical duplicate buttons (fix #3260)
Set 'n' for buttons in Bangle.btHomeData correctly (avoids adding extra buttons on end of advertising)
0.04: Fix duplicate button on edit->save

View File

@ -1,7 +1,7 @@
{ "id": "bthome",
"name": "BTHome",
"shortName":"BTHome",
"version":"0.03",
"version":"0.04",
"description": "Allow your Bangle to advertise with BTHome and send events to Home Assistant via Bluetooth",
"icon": "icon.png",
"type": "app",

View File

@ -1,7 +1,11 @@
(function(back) {
var settings = require("Storage").readJSON("bthome.json",1)||{};
if (!(settings.buttons instanceof Array))
settings.buttons = [];
var settings;
function loadSettings() {
settings = require("Storage").readJSON("bthome.json",1)||{};
if (!(settings.buttons instanceof Array))
settings.buttons = [];
}
function saveSettings() {
require("Storage").writeJSON("bthome.json",settings)
@ -15,7 +19,10 @@
}
var actions = ["press","double_press","triple_press","long_press","long_double_press","long_triple_press"];
var menu = {
"":{title:isNew ? /*LANG*/"New Button" : /*LANG*/"Edit Button", back:showMenu},
"":{title:isNew ? /*LANG*/"New Button" : /*LANG*/"Edit Button", back: () => {
loadSettings(); // revert changes
showMenu();
}},
/*LANG*/"Icon" : {
value : "\0"+require("icons").getIcon(button.icon),
onchange : () => {
@ -49,7 +56,7 @@
onchange : v => button.n=v
},
/*LANG*/"Save" : () => {
settings.buttons.push(button);
if (isNew) settings.buttons.push(button);
saveSettings();
showMenu();
}
@ -94,5 +101,7 @@
});
E.showMenu(menu);
}
loadSettings();
showMenu();
})

View File

@ -42,3 +42,8 @@
Add debug option for disabling active scanning
0.17: New GUI based on layout library
0.18: Minor code improvements
0.19: Move caching of characteristics into settings app
Changed default of active scanning to false
Fix setHRMPower method not returning new state
Only buzz for disconnect after switching on if there already was an actual connection
Fix recorder not switching BTHRM on and off

View File

@ -21,6 +21,10 @@ Once installed you will have to go into this app's settings while your heart rat
**To disable this and return to normal HRM, uninstall the app or change the settings**
The characteristics of your selected sensor are cached in the settings. That means if your sensor changes, e.g. by firmware updates or similar, you will need to re-scan in the settings to update the cache of characteristics. This is done to take some complexity (and time) out of the boot process.
Scanning in the settings will do 10 retries and then give up on adding the sensor. Usually that works fine, if it does not for you just try multiple times. Currently saved sensor information is only replaced on a successful pairing. There are additional options in the Debug entry of the menu that can help with specific sensor oddities. Bonding and active scanning can help with connecting, but can also prevent some sensors from working. The "Grace Periods" just add some additional time at certain steps in the connection process which can help with stability or reconnect speed of some finicky sensors. Defaults should be fine for most.
### Modes
* Off - Internal HRM is used, no attempt on connecting to BT HRM.
@ -57,3 +61,7 @@ This replaces `Bangle.setHRMPower` with its own implementation.
## Creator
Gordon Williams
## Contributer
[halemmerich](https://github.com/halemmerich)

View File

@ -57,7 +57,7 @@ var layout = new Layout( {
{ type:undefined, height:8 } //dummy to protect debug output
]
}, {
lazy:true
lazy:false
});
var int,agg,bt;
@ -106,8 +106,7 @@ function draw(){
layout.btContact.label = "--";
layout.btEnergy.label = "--";
}
layout.update();
layout.clear();
layout.render();
let first = true;
for (let c of layout.l.c){
@ -122,26 +121,29 @@ function draw(){
// This can get called for the boot code to show what's happening
function showStatusInfo(txt) {
global.showStatusInfo = function(txt) {
var R = Bangle.appRect;
g.reset().clearRect(R.x,R.y2-8,R.x2,R.y2).setFont("6x8");
txt = g.wrapString(txt, R.w)[0];
g.setFontAlign(0,1).drawString(txt, (R.x+R.x2)/2, R.y2);
}
};
function onBtHrm(e) {
bt = e;
bt.time = Date.now();
draw();
}
function onInt(e) {
int = e;
int.time = Date.now();
draw();
}
function onAgg(e) {
agg = e;
agg.time = Date.now();
draw();
}
var settings = require('Storage').readJSON("bthrm.json", true) || {};
@ -162,7 +164,6 @@ Bangle.drawWidgets();
if (Bangle.setBTHRMPower){
g.reset().setFont("6x8",2).setFontAlign(0,0);
g.drawString("Please wait...",g.getWidth()/2,g.getHeight()/2);
setInterval(draw, 1000);
} else {
g.reset().setFont("6x8",2).setFontAlign(0,0);
g.drawString("BTHRM disabled",g.getWidth()/2,g.getHeight()/2);

View File

@ -15,8 +15,7 @@
"custom_fallbackTimeout": 10,
"gracePeriodNotification": 0,
"gracePeriodConnect": 0,
"gracePeriodService": 0,
"gracePeriodRequest": 0,
"bonding": false,
"active": true
"active": false
}

View File

@ -1,14 +1,14 @@
exports.enable = () => {
var settings = Object.assign(
let settings = Object.assign(
require('Storage').readJSON("bthrm.default.json", true) || {},
require('Storage').readJSON("bthrm.json", true) || {}
);
var log = function(text, param){
let log = function(text, param){
if (global.showStatusInfo)
global.showStatusInfo(text);
if (settings.debuglog){
var logline = new Date().toISOString() + " - " + text;
let logline = new Date().toISOString() + " - " + text;
if (param) logline += ": " + JSON.stringify(param);
print(logline);
}
@ -16,60 +16,33 @@ exports.enable = () => {
log("Settings: ", settings);
if (settings.enabled){
//this is for compatibility with 0.18 and older
let oldCache = require('Storage').readJSON("bthrm.cache.json", true);
if(oldCache){
settings.cache = oldCache;
require('Storage').writeJSON("bthrm.json", settings);
require('Storage').erase("bthrm.cache.json");
}
var clearCache = function() {
return require('Storage').erase("bthrm.cache.json");
};
if (settings.enabled && settings.cache){
var getCache = function() {
var cache = require('Storage').readJSON("bthrm.cache.json", true) || {};
if (settings.btid && settings.btid === cache.id) return cache;
clearCache();
return {};
};
log("Start");
var addNotificationHandler = function(characteristic) {
let 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
let characteristicsFromCache = function(device) {
let service = { device : device }; // fake a BluetoothRemoteGATTService
log("Read cached characteristics");
var cache = getCache();
let cache = settings.cache;
if (!cache.characteristics) return [];
var restored = [];
for (var c in cache.characteristics){
var cached = cache.characteristics[c];
var r = new BluetoothRemoteGATTCharacteristic();
let restored = [];
for (let c in cache.characteristics){
let cached = cache.characteristics[c];
let r = new BluetoothRemoteGATTCharacteristic();
log("Restoring characteristic ", cached);
r.handle_value = cached.handle;
r.uuid = cached.uuid;
@ -84,26 +57,14 @@ exports.enable = () => {
return restored;
};
log("Start");
var lastReceivedData={
};
var supportedServices = [
"0x180d", // Heart Rate
"0x180f", // Battery
];
var bpmTimeout;
var supportedCharacteristics = {
let supportedCharacteristics = {
"0x2a37": {
//Heart rate measurement
active: false,
handler: function (dv){
var flags = dv.getUint8(0);
let flags = dv.getUint8(0);
var bpm = (flags & 1) ? (dv.getUint16(1) / 100 /* ? */ ) : dv.getUint8(1); // 8 or 16 bit
let 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);
switchFallback();
@ -114,42 +75,42 @@ exports.enable = () => {
startFallback();
}, 3000);
var sensorContact;
let sensorContact;
if (flags & 2){
sensorContact = !!(flags & 4);
}
var idx = 2 + (flags&1);
let idx = 2 + (flags&1);
var energyExpended;
let energyExpended;
if (flags & 8){
energyExpended = dv.getUint16(idx,1);
idx += 2;
}
var interval;
let interval;
if (flags & 16) {
interval = [];
var maxIntervalBytes = (dv.byteLength - idx);
let maxIntervalBytes = (dv.byteLength - idx);
log("Found " + (maxIntervalBytes / 2) + " rr data fields");
for(var i = 0 ; i < maxIntervalBytes / 2; i++){
for(let i = 0 ; i < maxIntervalBytes / 2; i++){
interval[i] = dv.getUint16(idx,1); // in milliseconds
idx += 2;
}
}
var location;
let location;
if (lastReceivedData && lastReceivedData["0x180d"] && lastReceivedData["0x180d"]["0x2a38"]){
location = lastReceivedData["0x180d"]["0x2a38"];
}
var battery;
let battery;
if (lastReceivedData && lastReceivedData["0x180f"] && lastReceivedData["0x180f"]["0x2a19"]){
battery = lastReceivedData["0x180f"]["0x2a19"];
}
if (settings.replace && bpm > 0){
var repEvent = {
let repEvent = {
bpm: bpm,
confidence: (sensorContact || sensorContact === undefined)? 100 : 0,
src: "bthrm"
@ -159,7 +120,7 @@ exports.enable = () => {
Bangle.emit("HRM_R", repEvent);
}
var newEvent = {
let newEvent = {
bpm: bpm
};
@ -177,6 +138,7 @@ exports.enable = () => {
//Body sensor location
handler: function(dv){
if (!lastReceivedData["0x180d"]) lastReceivedData["0x180d"] = {};
log("Got location", dv);
lastReceivedData["0x180d"]["0x2a38"] = parseInt(dv.buffer, 10);
}
},
@ -184,26 +146,27 @@ exports.enable = () => {
//Battery
handler: function (dv){
if (!lastReceivedData["0x180f"]) lastReceivedData["0x180f"] = {};
log("Got battery", dv);
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
let lastReceivedData={
};
var waitingPromise = function(timeout) {
let bpmTimeout;
let device;
let gatt;
let characteristics = [];
let blockInit = false;
let currentRetryTimeout;
let initialRetryTime = 40;
let maxRetryTime = 60000;
let retryTime = initialRetryTime;
let waitingPromise = function(timeout) {
return new Promise(function(resolve){
log("Start waiting for " + timeout);
setTimeout(()=>{
@ -240,7 +203,7 @@ exports.enable = () => {
};
}
var clearRetryTimeout = function(resetTime) {
let clearRetryTimeout = function(resetTime) {
if (currentRetryTimeout){
log("Clearing timeout " + currentRetryTimeout);
clearTimeout(currentRetryTimeout);
@ -252,12 +215,12 @@ exports.enable = () => {
}
};
var retry = function() {
let retry = function() {
log("Retry");
if (!currentRetryTimeout && !powerdownRequested){
var clampedTime = retryTime < 100 ? 100 : retryTime;
let clampedTime = retryTime < 100 ? 100 : retryTime;
log("Set timeout for retry as " + clampedTime);
clearRetryTimeout();
@ -276,20 +239,21 @@ exports.enable = () => {
}
};
var buzzing = false;
var onDisconnect = function(reason) {
let initialDisconnects = true;
let buzzing = false;
let onDisconnect = function(reason) {
log("Disconnect: " + reason);
log("GATT", gatt);
log("Characteristics", characteristics);
var retryTimeResetNeeded = true;
let retryTimeResetNeeded = true;
retryTimeResetNeeded &= reason != "Connection Timeout";
retryTimeResetNeeded &= reason != "No device found matching filters";
clearRetryTimeout(retryTimeResetNeeded);
supportedCharacteristics["0x2a37"].active = false;
if (!powerdownRequested) startFallback();
blockInit = false;
if (settings.warnDisconnect && !buzzing){
if (settings.warnDisconnect && !buzzing && !initialDisconnects){
buzzing = true;
Bangle.buzz(500,0.3).then(()=>waitingPromise(4500)).then(()=>{buzzing = false;});
}
@ -298,9 +262,9 @@ exports.enable = () => {
}
};
var createCharacteristicPromise = function(newCharacteristic) {
let createCharacteristicPromise = function(newCharacteristic) {
log("Create characteristic promise", newCharacteristic);
var result = Promise.resolve();
let 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){
@ -316,58 +280,29 @@ exports.enable = () => {
if (newCharacteristic.properties.notify){
result = result.then(()=>{
log("Starting notifications", newCharacteristic);
var startPromise = newCharacteristic.startNotifications().then(()=>log("Notifications started", newCharacteristic));
log("Add " + settings.gracePeriodNotification + "ms grace period after starting notifications");
startPromise = startPromise.then(()=>{
log("Wait after connect");
return waitingPromise(settings.gracePeriodNotification);
});
let startPromise = newCharacteristic.startNotifications().then(()=>log("Notifications started", newCharacteristic));
if (settings.gracePeriodNotification){
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) {
let 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 () {
let initBt = function () {
log("initBt with blockInit: " + blockInit);
if (blockInit && !powerdownRequested){
retry();
@ -376,8 +311,8 @@ exports.enable = () => {
blockInit = true;
var promise;
var filters;
let promise;
let filters;
if (!device){
if (settings.btid){
@ -397,6 +332,10 @@ exports.enable = () => {
if (settings.gracePeriodRequest){
log("Add " + settings.gracePeriodRequest + "ms grace period after request");
promise = promise.then((d)=>{
log("Wait after request");
return waitingPromise(settings.gracePeriodRequest).then(()=>Promise.resolve(d));
});
}
promise = promise.then((d)=>{
@ -404,100 +343,42 @@ exports.enable = () => {
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;
}
gatt = device.gatt;
return Promise.resolve(gatt);
});
promise = promise.then((gatt)=>{
if (!gatt.connected){
log("Connecting...");
var connectPromise = gatt.connect(connectSettings).then(function() {
let connectPromise = gatt.connect().then(function() {
log("Connected.");
});
log("Add " + settings.gracePeriodConnect + "ms grace period after connecting");
connectPromise = connectPromise.then(()=>{
log("Wait after connect");
return waitingPromise(settings.gracePeriodConnect);
});
if (settings.gracePeriodConnect){
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();
}
});
if (settings.bonding){
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(() => log("Security status" + gatt.getSecurityStatus()));
}
});
}
promise = promise.then(()=>{
if (!characteristics || characteristics.length === 0){
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);
}
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);
}
let characteristicsPromise = Promise.resolve();
for (let characteristic of characteristics){
characteristicsPromise = attachCharacteristicPromise(characteristicsPromise, characteristic, true);
}
return characteristicsPromise;
@ -505,7 +386,7 @@ exports.enable = () => {
return promise.then(()=>{
log("Connection established, waiting for notifications");
characteristicsToCache(characteristics);
initialDisconnects = false;
clearRetryTimeout(true);
}).catch((e) => {
characteristics = [];
@ -514,7 +395,7 @@ exports.enable = () => {
});
};
var powerdownRequested = false;
let powerdownRequested = false;
Bangle.setBTHRMPower = function(isOn, app) {
// Do app power handling
@ -526,6 +407,7 @@ exports.enable = () => {
isOn = Bangle._PWR.BTHRM.length;
// so now we know if we're really on
if (isOn) {
initialDisconnects = true;
powerdownRequested = false;
switchFallback();
if (!Bangle.isBTHRMConnected()) initBt();
@ -598,17 +480,18 @@ exports.enable = () => {
Bangle.setBTHRMPower(0);
if (!isOn) stopFallback();
}
return Bangle.isBTHRMOn() || Bangle.isHRMOn();
}
if ((settings.enabled && !settings.replace) || !settings.enabled){
Bangle.origSetHRMPower(isOn, app);
return Bangle.origSetHRMPower(isOn, app);
}
};
}
var fallbackActive = false;
var inSwitch = false;
let fallbackActive = false;
let inSwitch = false;
var stopFallback = function(){
let stopFallback = function(){
if (fallbackActive){
Bangle.origSetHRMPower(0, "bthrm_fallback");
fallbackActive = false;
@ -616,7 +499,7 @@ exports.enable = () => {
}
};
var startFallback = function(){
let startFallback = function(){
if (!fallbackActive && settings.allowFallback) {
fallbackActive = true;
Bangle.origSetHRMPower(1, "bthrm_fallback");
@ -624,7 +507,7 @@ exports.enable = () => {
}
};
var switchFallback = function() {
let switchFallback = function() {
log("Check falling back to HRM");
if (!inSwitch){
inSwitch = true;
@ -640,8 +523,8 @@ exports.enable = () => {
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];
for (let i = 0; i < Bangle._PWR.HRM.length; i++){
let app = Bangle._PWR.HRM[i];
log("Moving app " + app);
Bangle.origSetHRMPower(0, app);
Bangle.setBTHRMPower(1, app);

View File

@ -2,7 +2,7 @@
"id": "bthrm",
"name": "Bluetooth Heart Rate Monitor",
"shortName": "BT HRM",
"version": "0.18",
"version": "0.19",
"description": "Overrides Bangle.js's build in heart rate monitor with an external Bluetooth one.",
"icon": "app.png",
"screenshots": [{"url":"screen.png"}],

View File

@ -26,11 +26,11 @@
},
start : () => {
Bangle.on('BTHRM', onHRM);
if (Bangle.setBTRHMPower) Bangle.setBTHRMPower(1,"recorder");
if (Bangle.setBTHRMPower) Bangle.setBTHRMPower(1,"recorder");
},
stop : () => {
Bangle.removeListener('BTHRM', onHRM);
if (Bangle.setBTRHMPower) Bangle.setBTHRMPower(0,"recorder");
if (Bangle.setBTHRMPower) Bangle.setBTHRMPower(0,"recorder");
},
draw : (x,y) => g.setColor((Bangle.isBTHRMActive && Bangle.isBTHRMActive())?"#00f":"#88f").drawImage(atob("DAwBAAAAMMeef+f+f+P8H4DwBgAA"),x,y)
};

View File

@ -1,6 +1,6 @@
(function(back) {
function writeSettings(key, value) {
var s = require('Storage').readJSON(FILE, true) || {};
let s = require('Storage').readJSON(FILE, true) || {};
s[key] = value;
require('Storage').writeJSON(FILE, s);
readSettings();
@ -13,10 +13,14 @@
);
}
var FILE="bthrm.json";
var settings;
let FILE="bthrm.json";
let settings;
readSettings();
let log = ()=>{};
if (settings.debuglog)
log = print;
function applyCustomSettings(){
writeSettings("enabled",true);
writeSettings("replace",settings.custom_replace);
@ -26,7 +30,7 @@
}
function buildMainMenu(){
var mainmenu = {
let mainmenu = {
'': { 'title': 'Bluetooth HRM' },
'< Back': back,
'Mode': {
@ -63,12 +67,13 @@
};
if (settings.btname || settings.btid){
var name = "Clear " + (settings.btname || settings.btid);
let name = "Clear " + (settings.btname || settings.btid);
mainmenu[name] = function() {
E.showPrompt("Clear current device?").then((r)=>{
if (r) {
writeSettings("btname",undefined);
writeSettings("btid",undefined);
writeSettings("cache", undefined);
}
E.showMenu(buildMainMenu());
});
@ -81,7 +86,7 @@
return mainmenu;
}
var submenu_debug = {
let submenu_debug = {
'' : { title: "Debug"},
'< Back': function() { E.showMenu(buildMainMenu()); },
'Alert on disconnect': {
@ -111,11 +116,135 @@
'Grace periods': function() { E.showMenu(submenu_grace); }
};
let supportedServices = [
"0x180d", // Heart Rate
"0x180f", // Battery
];
let supportedCharacteristics = [
"0x2a37", // Heart Rate
"0x2a38", // Body sensor location
"0x2a19", // Battery
];
var characteristicsToCache = function(characteristics) {
log("Cache characteristics");
let cache = {};
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.uuid);
cache.characteristics[c.uuid] = {
"handle": c.handle_value,
"uuid": c.uuid,
"notify": c.properties.notify,
"read": c.properties.read
};
}
writeSettings("cache", cache);
};
let createCharacteristicPromise = function(newCharacteristic) {
log("Create characteristic promise", newCharacteristic.uuid);
return Promise.resolve().then(()=>log("Handled characteristic", newCharacteristic.uuid));
};
let attachCharacteristicPromise = function(promise, characteristic) {
return promise.then(()=>{
log("Handling characteristic:", characteristic.uuid);
return createCharacteristicPromise(characteristic);
});
};
let characteristics;
let createCharacteristicsPromise = function(newCharacteristics) {
log("Create characteristics promise ", newCharacteristics.length);
let result = Promise.resolve();
for (let c of newCharacteristics){
if (!supportedCharacteristics.includes(c.uuid)) continue;
log("Supporting characteristic", c.uuid);
characteristics.push(c);
result = attachCharacteristicPromise(result, c);
}
return result.then(()=>log("Handled characteristics"));
};
let createServicePromise = function(service) {
log("Create service promise", service.uuid);
let 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));
};
let attachServicePromise = function(promise, service) {
return promise.then(()=>createServicePromise(service));
};
function cacheDevice(deviceId){
let promise;
let filters;
let gatt;
characteristics = [];
filters = [{ id: deviceId }];
log("Requesting device with filters", filters);
promise = NRF.requestDevice({ filters: filters, active: settings.active });
promise = promise.then((d)=>{
log("Got device", d);
gatt = d.gatt;
log("Connecting...");
return gatt.connect().then(function() {
log("Connected.");
});
});
if (settings.bonding){
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(() => log("Security status after bonding" + gatt.getSecurityStatus()));
}
});
}
promise = promise.then(()=>{
log("Getting services");
return gatt.getPrimaryServices();
});
promise = promise.then((services)=>{
log("Got services", services.length);
let result = Promise.resolve();
for (let service of services){
if (!(supportedServices.includes(service.uuid))) continue;
log("Supporting service", service.uuid);
result = attachServicePromise(result, service);
}
return result;
});
return promise.then(()=>{
log("Connection established, saving cache");
characteristicsToCache(characteristics);
});
}
function createMenuFromScan(){
E.showMenu();
E.showMessage("Scanning for 4 seconds");
var submenu_scan = {
let submenu_scan = {
'< Back': function() { E.showMenu(buildMainMenu()); }
};
NRF.findDevices(function(devices) {
@ -126,18 +255,41 @@
return;
} else {
devices.forEach((d) => {
print("Found device", d);
var shown = (d.name || d.id.substr(0, 17));
log("Found device", d);
let shown = (d.name || d.id.substr(0, 17));
submenu_scan[shown] = function () {
E.showPrompt("Set " + shown + "?").then((r) => {
E.showPrompt("Connect to\n" + shown + "?", {title: "Pairing"}).then((r) => {
if (r) {
writeSettings("btid", d.id);
// Store the name for displaying later. Will connect by ID
if (d.name) {
writeSettings("btname", d.name);
}
E.showMessage("Connecting...", {img:require("Storage").read("bthrm.img")});
let count = 0;
const successHandler = ()=>{
E.showPrompt("Success!", {
img:require("Storage").read("bthrm.img"),
buttons: { "OK":true }
}).then(()=>{
writeSettings("btid", d.id);
// Store the name for displaying later. Will connect by ID
if (d.name) {
writeSettings("btname", d.name);
}
E.showMenu(buildMainMenu());
});
};
const errorHandler = (e)=>{
count++;
log("ERROR", e);
if (count <= 10){
E.showMessage("Error during caching\nRetry " + count + "/10", e);
return cacheDevice(d.id).then(successHandler).catch(errorHandler);
} else {
E.showAlert("Error during caching", e).then(()=>{
E.showMenu(buildMainMenu());
});
}
};
return cacheDevice(d.id).then(successHandler).catch(errorHandler);
}
E.showMenu(buildMainMenu());
});
};
});
@ -146,7 +298,7 @@
}, { timeout: 4000, active: true, filters: [{services: [ "180d" ]}]});
}
var submenu_custom = {
let submenu_custom = {
'' : { title: "Custom mode"},
'< Back': function() { E.showMenu(buildMainMenu()); },
'Replace HRM': {
@ -183,7 +335,7 @@
},
};
var submenu_grace = {
let submenu_grace = {
'' : { title: "Grace periods"},
'< Back': function() { E.showMenu(submenu_debug); },
'Request': {
@ -215,16 +367,6 @@
onchange: v => {
writeSettings("gracePeriodNotification",v);
}
},
'Service': {
value: settings.gracePeriodService,
min: 0,
max: 3000,
step: 100,
format: v=>v+"ms",
onchange: v => {
writeSettings("gracePeriodService",v);
}
}
};

1
apps/chronlog/ChangeLog Normal file
View File

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

53
apps/chronlog/README.md Normal file
View File

@ -0,0 +1,53 @@
# Chrono Logger
Record times active on a task, course, work or anything really.
**Disclaimer:** No one is responsible for any loss of data you recorded with this app. If you run into problems please report as advised under **Requests** below.
With time on your side and a little help from your friends - you'll surely triumph over Lavos in the end!
![dump](dump.png) ![dump1](dump1.png) ![dump2](dump2.png) ![dump3](dump3.png) ![dump4](dump4.png) ![dump5](dump5.png) ![dump6](dump6.png)
## Usage
Click the large green button to log the start of your activity. Click the now red button again to log that you stopped.
## Features
- Saves to file on every toggling of the active state.
- csv file contents looks like:
```
1,Start,2024-03-02T15:18:09 GMT+0200
2,Note,Critical hit!
3,Stop,2024-03-02T15:19:17 GMT+0200
```
- Add annotations to the log.
- Create and switch between multiple logs.
- Sync log files to an Android device through Gadgetbridge (Needs pending code changes to Gadgetbridge).
- App state is restored when you start the app again.
## Controls
- Large button to toggle active state.
- Menu icon to access additional functionality.
- Hardware button exits menus, closes the app on the main screen.
## TODO and notes
- Delete individual tasks/logs through the app?
- Reset everything through the app?
- Scan for chronlog storage files that somehow no longer have tasks associated with it?
- Complete the Gadgetbridge side of things for sync.
- Sync to iOS?
- Inspect log files through the app, similarly to Recorder app?
- Changes to Android file system permissions makes it not always trivial to access the synced files.
## Requests
Tag @thyttan in an issue to https://gitbub.com/espruino/BangleApps/issues to report problems or suggestions.
## Creator
[thyttan](https://github.com/thyttan)

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEw4UA///gElq3X0ELJf4AiitAAYMBqgKEgNVrgEBmtVCAQABgtVr/Agf1qtQEQlpq6QB6tpEgkVywLDywLEq2uyoLB6wEBBZAECBYda32lBYIECBZ9W3wjDAgILPquWqoACAgILEtILDAgKOEAAyQCRwIAGSAUVBY6ECBZYGD7WnAoYLF9WrBYupAoWq1QECtQLBtWdBYt21QLC1LfBBYVfA4ILBlWq1f9rWVv/q1WoBYMKCgOvTYP6AoOgBYMCAoIAFwCQCBY6nDGAIAEFwQkIEQZVCBQZRCAAcGBYeQBYoYDCwwYECw5KC0gKIAH4APA="))

376
apps/chronlog/app.js Normal file
View File

@ -0,0 +1,376 @@
// TODO:
// - Add more /*LANG*/ tags for translations.
// - Check if there are chronlog storage files that should be added to tasks.
{
const storage = require("Storage");
let appData = storage.readJSON("chronlog.json", true) || {
currentTask : "default",
tasks : {
default: {
file : "chronlog_default.csv", // Existing default task log file
state : "stopped",
lineNumber : 0,
lastLine : "",
lastSyncedLine : "",
},
// Add more tasks as needed
},
};
let currentTask = appData.currentTask;
let tasks = appData.tasks;
delete appData;
let themeColors = g.theme;
let logEntry; // Avoid previous lint warning
// Function to draw the Start/Stop button with play and pause icons
let drawButton = ()=>{
var btnWidth = g.getWidth() - 40;
var btnHeight = 50;
var btnX = 20;
var btnY = (g.getHeight() - btnHeight) / 2;
var cornerRadius = 25;
var isStopped = tasks[currentTask].state === "stopped";
g.setColor(isStopped ? "#0F0" : "#F00"); // Set color to green when stopped and red when started
// Draw rounded corners of the button
g.fillCircle(btnX + cornerRadius, btnY + cornerRadius, cornerRadius);
g.fillCircle(btnX + btnWidth - cornerRadius, btnY + cornerRadius, cornerRadius);
g.fillCircle(btnX + cornerRadius, btnY + btnHeight - cornerRadius, cornerRadius);
g.fillCircle(btnX + btnWidth - cornerRadius, btnY + btnHeight - cornerRadius, cornerRadius);
// Draw rectangles to fill in the button
g.fillRect(btnX + cornerRadius, btnY, btnX + btnWidth - cornerRadius, btnY + btnHeight);
g.fillRect(btnX, btnY + cornerRadius, btnX + btnWidth, btnY + btnHeight - cornerRadius);
g.setColor(themeColors.bg); // Set icon color to contrast against the button's color
// Center the icon within the button
var iconX = btnX + btnWidth / 2;
var iconY = btnY + btnHeight / 2;
if (isStopped) {
// Draw play icon
var playSize = 10; // Side length of the play triangle
var offset = playSize / Math.sqrt(3) - 3;
g.fillPoly([
iconX - playSize, iconY - playSize + offset,
iconX - playSize, iconY + playSize + offset,
iconX + playSize * 2 / Math.sqrt(3), iconY + offset
]);
} else {
// Draw pause icon
var barWidth = 5; // Width of pause bars
var barHeight = btnHeight / 2; // Height of pause bars
var barSpacing = 5; // Spacing between pause bars
g.fillRect(iconX - barSpacing / 2 - barWidth, iconY - barHeight / 2, iconX - barSpacing / 2, iconY + barHeight / 2);
g.fillRect(iconX + barSpacing / 2, iconY - barHeight / 2, iconX + barSpacing / 2 + barWidth, iconY + barHeight / 2);
}
};
let drawHamburgerMenu = ()=>{
var x = g.getWidth() / 2; // Center the hamburger menu horizontally
var y = (7/8)*g.getHeight(); // Position it near the bottom
var lineLength = 18; // Length of the hamburger lines
var spacing = 6; // Space between the lines
g.setColor(themeColors.fg); // Set color to foreground color for the icon
// Draw three horizontal lines
for (var i = -1; i <= 1; i++) {
g.fillRect(x - lineLength/2, y + i * spacing - 1, x + lineLength/2, y + i * spacing + 1);
}
};
// Function to draw the task name centered between the widget field and the start/stop button
let drawTaskName = ()=>{
g.setFont("Vector", 20); // Set a smaller font for the task name display
// Calculate position to center the task name horizontally
var x = (g.getWidth()) / 2;
// Calculate position to center the task name vertically between the widget field and the start/stop button
var y = g.getHeight()/4; // Center vertically
g.setColor(themeColors.fg).setFontAlign(0,0); // Set text color to foreground color
g.drawString(currentTask, x, y); // Draw the task name centered on the screen
};
// Function to draw the last log entry of the current task
let drawLastLogEntry = ()=>{
g.setFont("Vector", 10); // Set a smaller font for the task name display
// Calculate position to center the log entry horizontally
var x = (g.getWidth()) / 2;
// Calculate position to place the log entry properly between the start/stop button and hamburger menu
var btnBottomY = (g.getHeight() + 50) / 2; // Y-coordinate of the bottom of the start/stop button
var menuBtnYTop = g.getHeight() * (5 / 6); // Y-coordinate of the top of the hamburger menu button
var y = btnBottomY + (menuBtnYTop - btnBottomY) / 2 + 2; // Center vertically between button and menu
g.setColor(themeColors.fg).setFontAlign(0,0); // Set text color to foreground color
g.drawString(g.wrapString(tasks[currentTask].lastLine, 150).join("\n"), x, y);
};
/*
// Helper function to read the last log entry from the current task's log file
let updateLastLogEntry = ()=>{
var filename = tasks[currentTask].file;
var file = require("Storage").open(filename, "r");
var lastLine = "";
var line;
while ((line = file.readLine()) !== undefined) {
lastLine = line; // Keep reading until the last line
}
tasks[currentTask].lastLine = lastLine;
};
*/
// Main UI drawing function
let drawMainMenu = ()=>{
g.clear();
Bangle.drawWidgets(); // Draw any active widgets
g.setColor(themeColors.bg); // Set color to theme's background color
g.fillRect(Bangle.appRect); // Fill the app area with the background color
drawTaskName(); // Draw the centered task name
drawLastLogEntry(); // Draw the last log entry of the current task
drawButton(); // Draw the Start/Stop toggle button
drawHamburgerMenu(); // Draw the hamburger menu button icon
//g.flip(); // Send graphics to the display
};
// Function to toggle the active state
let toggleChronlog = ()=>{
var dateObj = new Date();
var dateObjStrSplit = dateObj.toString().split(" ");
var currentTime = dateObj.getFullYear().toString() + "-" + (dateObj.getMonth()<10?"0":"") + dateObj.getMonth().toString() + "-" + (dateObj.getDate()<10?"0":"") + dateObj.getDate().toString() + "T" + (dateObj.getHours()<10?"0":"") + dateObj.getHours().toString() + ":" + (dateObj.getMinutes()<10?"0":"") + dateObj.getMinutes().toString() + ":" + (dateObj.getSeconds()<10?"0":"") + dateObj.getSeconds().toString() + " " + dateObjStrSplit[dateObjStrSplit.length-1];
tasks[currentTask].lineNumber = Number(tasks[currentTask].lineNumber) + 1;
logEntry = tasks[currentTask].lineNumber + (tasks[currentTask].state === "stopped" ? ",Start," : ",Stop,") + currentTime + "\n";
var filename = tasks[currentTask].file;
// Open the appropriate file and append the log entry
var file = require("Storage").open(filename, "a");
file.write(logEntry);
tasks[currentTask].lastLine = logEntry;
// Toggle the state and update the button text
tasks[currentTask].state = tasks[currentTask].state === "stopped" ? "started" : "stopped";
drawMainMenu(); // Redraw the main UI
};
// Define the touch handler function for the main menu
let handleMainMenuTouch = (button, xy)=>{
var btnTopY = (g.getHeight() - 50) / 2;
var btnBottomY = btnTopY + 50;
var menuBtnYTop = (7/8)*g.getHeight() - 15;
var menuBtnYBottom = (7/8)*g.getHeight() + 15;
var menuBtnXLeft = (g.getWidth() / 2) - 15;
var menuBtnXRight = (g.getWidth() / 2) + 15;
// Detect if the touch is within the toggle button area
if (xy.x >= 20 && xy.x <= (g.getWidth() - 20) && xy.y > btnTopY && xy.y < btnBottomY) {
toggleChronlog();
}
// Detect if the touch is within the hamburger menu button area
else if (xy.x >= menuBtnXLeft && xy.x <= menuBtnXRight && xy.y >= menuBtnYTop && xy.y <= menuBtnYBottom) {
showMenu();
}
};
// Function to attach the touch event listener
let setMainUI = ()=>{
Bangle.setUI({
mode: "custom",
back: load,
touch: handleMainMenuTouch
});
};
let saveAppState = ()=>{
let appData = {
currentTask : currentTask,
tasks : tasks,
};
require("Storage").writeJSON("chronlog.json", appData);
};
// Set up a listener for the 'kill' event
E.on('kill', saveAppState);
// Function to switch to a selected task
let switchTask = (taskName)=>{
currentTask = taskName; // Update the current task
// Reinitialize the UI elements
setMainUI();
drawMainMenu(); // Redraw UI to reflect the task change and the button state
};
// Function to create a new task
let createNewTask = ()=>{
// Prompt the user to input the task's name
require("textinput").input({
text: "" // Default empty text for new task
}).then(result => {
var taskName = result; // Store the result from text input
if (taskName) {
if (tasks.hasOwnProperty(taskName)) {
// Task already exists, handle this case as needed
E.showAlert(/*LANG*/"Task already exists", "Error").then(drawMainMenu);
} else {
// Create a new task log file for the new task
var filename = "chronlog_" + taskName.replace(/\W+/g, "_") + ".csv";
tasks[taskName] = {
file : filename,
state : "stopped",
lineNumber : 0,
lastLine : "",
lastSyncedLine : "",
};
currentTask = taskName;
setMainUI();
drawMainMenu(); // Redraw UI with the new task
}
} else {
setMainUI();
drawMainMenu(); // User cancelled, redraw main menu
}
}).catch(e => {
console.log("Text input error", e);
setMainUI();
drawMainMenu(); // In case of error also redraw main menu
});
};
// Function to display the list of tasks for selection
let chooseTask = ()=>{
// Construct the tasks menu from the tasks object
var taskMenu = {
"": { "title": /*LANG*/"Choose Task",
"back" : function() {
setMainUI(); // Reattach when the menu is closed
drawMainMenu(); // Cancel task selection
}
}
};
for (var taskName in tasks) {
if (!tasks.hasOwnProperty(taskName)) continue;
taskMenu[taskName] = (function(name) {
return function() {
switchTask(name);
};
})(taskName);
}
// Add a menu option for creating a new task
taskMenu[/*LANG*/"Create New Task"] = createNewTask;
E.showMenu(taskMenu); // Display the task selection
};
// Function to annotate the current or last work session
let annotateTask = ()=>{
// Prompt the user to input the annotation text
require("textinput").input({
text: "" // Default empty text for annotation
}).then(result => {
var annotationText = result.trim();
if (annotationText) {
// Append annotation to the last or current log entry
tasks[currentTask].lineNumber ++;
var annotatedEntry = tasks[currentTask].lineNumber + /*LANG*/",Note," + annotationText + "\n";
var filename = tasks[currentTask].file;
var file = require("Storage").open(filename, "a");
file.write(annotatedEntry);
tasks[currentTask].lastLine = annotatedEntry;
setMainUI();
drawMainMenu(); // Redraw UI after adding the annotation
} else {
// User cancelled, so we do nothing and just redraw the main menu
setMainUI();
drawMainMenu();
}
}).catch(e => {
console.log("Annotation input error", e);
setMainUI();
drawMainMenu(); // In case of error also redraw main menu
});
};
let syncToAndroid = (taskName, isFullSync)=>{
let mode = "a";
if (isFullSync) mode = "w";
let lastSyncedLine = tasks[taskName].lastSyncedLine || 0;
let taskNameValidFileName = taskName.replace(" ","_"); // FIXME: Should use something similar to replaceAll using a regular expression to catch all illegal characters.
let storageFile = require("Storage").open("chronlog_"+taskNameValidFileName+".csv", "r");
let contents = storageFile.readLine();
let lineNumber = contents ? contents.slice(0, contents.indexOf(",")) : 0;
let shouldSyncLine = ()=>{return (contents && (isFullSync || (Number(lineNumber)>Number(lastSyncedLine))));};
let doSyncLine = (mde)=>{Bluetooth.println(JSON.stringify({t:"file", n:"chronlog_"+taskNameValidFileName+".csv", c:contents, m:mde}));};
if (shouldSyncLine()) doSyncLine(mode);
contents = storageFile.readLine();
while (contents) {
lineNumber = contents.slice(0, contents.indexOf(",")); // Could theoretically do with `lineNumber++`, but this is more robust in case numbering in file ended up irregular.
if (shouldSyncLine()) doSyncLine("a");
contents = storageFile.readLine();
}
tasks[taskName].lastSyncedLine = lineNumber;
};
// Function to display the list of tasks for selection
let syncTasks = ()=>{
let isToDoFullSync = false;
// Construct the tasks menu from the tasks object
var syncMenu = {
"": { "title": /*LANG*/"Sync Tasks",
"back" : function() {
setMainUI(); // Reattach when the menu is closed
drawMainMenu(); // Cancel task selection
}
}
};
syncMenu[/*LANG*/"Full Resyncs"] = {
value: !!isToDoFullSync, // !! converts undefined to false
onchange: ()=>{
isToDoFullSync = !isToDoFullSync
},
}
for (var taskName in tasks) {
if (!tasks.hasOwnProperty(taskName)) continue;
syncMenu[taskName] = (function(name) {
return function() {syncToAndroid(name,isToDoFullSync);};
})(taskName);
}
E.showMenu(syncMenu); // Display the task selection
};
let showMenu = ()=>{
var menu = {
"": { "title": /*LANG*/"Menu",
"back": function() {
setMainUI(); // Reattach when the menu is closed
drawMainMenu(); // Redraw the main UI when closing the menu
},
},
/*LANG*/"Annotate": annotateTask, // Now calls the real annotation function
/*LANG*/"Change Task": chooseTask, // Opens the task selection screen
/*LANG*/"Sync to Android": syncTasks,
};
E.showMenu(menu);
};
Bangle.loadWidgets();
drawMainMenu(); // Draw the main UI when the app starts
// When the application starts, attach the touch event listener
setMainUI();
}

BIN
apps/chronlog/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
apps/chronlog/dump.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
apps/chronlog/dump1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
apps/chronlog/dump2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
apps/chronlog/dump3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
apps/chronlog/dump4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
apps/chronlog/dump5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
apps/chronlog/dump6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -0,0 +1,14 @@
{ "id": "chronlog",
"name": "Chrono Logger",
"version":"0.01",
"description": "Record time active on a task, course, work or anything really.",
"icon": "app.png",
"tags": "logging, record, work, tasks",
"supports" : ["BANGLEJS2"],
"readme": "README.md",
"screenshots" : [ { "url":"dump.png"}, { "url":"dump1.png" }, { "url":"dump2.png" }, { "url":"dump3.png" }, { "url":"dump4.png" }, { "url":"dump5.png" }, { "url":"dump6.png" } ],
"storage": [
{"name":"chronlog.app.js","url":"app.js"},
{"name":"chronlog.img","url":"app-icon.js","evaluate":true}
]
}

View File

@ -6,3 +6,5 @@
0.06: Tell clock widgets to hide.
0.07: Convert Yes/No On/Off in settings to checkboxes
0.08: Fixed typo in settings.js for DRAGDOWN to make option work
0.09: You can now back out of the calendar using the button
0.10: Fix linter warnings

View File

@ -7,23 +7,24 @@ I know that it seems redundant because there already **is** a *time&cal*-app, bu
|:--:|:-|
|![locked screen](screenshot.png)|locked: triggers only one minimal update/min|
|![unlocked screen](screenshot2.png)|unlocked: smaller clock, but with seconds|
|![big calendar](screenshot3.png)|swipe up for big calendar, (up down to scroll, left/right to exit)|
|![big calendar](screenshot3.png)|swipe up for big calendar<br>⬆️/⬇️ to scroll<br> ⬅️/➡️ to exit|
## Configurable Features
- Number of calendar rows (weeks)
- Buzz on connect/disconnect (I know, this should be an extra widget, but for now, it is included)
- Buzz on connect/disconnect (feel free to disable and use a widget)
- Clock Mode (24h/12h). (No am/pm indicator)
- First day of the week
- Red Saturday/Sunday
- Swipe/Drag gestures to launch features or apps.
## Auto detects your message/music apps:
- swiping down will search your files for an app with the string "message" in its filename and launch it. (configurable)
- swiping right will search your files for an app with the string "music" in its filename and launch it. (configurable)
## Integrated swipe launcher: (Configure in Settings)
- ⬇️ (down) will search your files for an app with the string "**message**"
- ➡️ (right) will search your files for an app with the string "**music**"
- ⬅️ (left) will search your files for an app with the string "**agenda**"
- ⬆️ (up) will show the **internal full calendar**
## Feedback
The clock works for me in a 24h/MondayFirst/WeekendFree environment but is not well-tested with other settings.
So if something isn't working, please tell me: https://github.com/foostuff/BangleApps/issues
If something isn't working, please tell me: https://github.com/Stuff-etc/BangleApps/issues (I moved my github repo)
## Planned features:
- Internal lightweight music control, because switching apps has a loading time.

View File

@ -24,15 +24,25 @@ const DEBUG = false;
var state = "watch";
var monthOffset = 0;
// FIXME: These variables should maybe be defined inside relevant functions below. The linter complained they were not defined (i.e. they were added to global scope if I understand correctly).
let dayInterval;
let secondInterval;
let minuteInterval;
let newmonth;
let bottomrightY;
let bottomrightX;
let rMonth;
let dimSeconds;
/*
* Calendar features
*/
function drawFullCalendar(monthOffset) {
addMonths = function (_d, _am) {
var ay = 0, m = _d.getMonth(), y = _d.getFullYear();
const addMonths = function (_d, _am) {
let ay = 0, m = _d.getMonth(), y = _d.getFullYear();
while ((m + _am) > 11) { ay++; _am -= 12; }
while ((m + _am) < 0) { ay--; _am += 12; }
n = new Date(_d.getTime());
let n = new Date(_d.getTime());
n.setMonth(m + _am);
n.setFullYear(y + ay);
return n;
@ -45,10 +55,10 @@ function drawFullCalendar(monthOffset) {
if (typeof dayInterval !== "undefined") clearTimeout(dayInterval);
if (typeof secondInterval !== "undefined") clearTimeout(secondInterval);
if (typeof minuteInterval !== "undefined") clearTimeout(minuteInterval);
d = addMonths(Date(), monthOffset);
tdy = Date().getDate() + "." + Date().getMonth();
var d = addMonths(Date(), monthOffset);
let tdy = Date().getDate() + "." + Date().getMonth();
newmonth = false;
c_y = 0;
let c_y = 0;
g.reset();
g.setBgColor(0);
g.clear();
@ -60,8 +70,8 @@ function drawFullCalendar(monthOffset) {
rD.setDate(rD.getDate() - dow);
var rDate = rD.getDate();
bottomrightY = c_y - 3;
clrsun = s.REDSUN ? '#f00' : '#fff';
clrsat = s.REDSUN ? '#f00' : '#fff';
let clrsun = s.REDSUN ? '#f00' : '#fff';
let clrsat = s.REDSUN ? '#f00' : '#fff';
var fg = [clrsun, '#fff', '#fff', '#fff', '#fff', '#fff', clrsat];
for (var y = 1; y <= 11; y++) {
bottomrightY += CELL_H;
@ -90,7 +100,7 @@ function caldrawMonth(rDate, c, m, rD) {
g.setColor(c);
g.setFont("Vector", 18);
g.setFontAlign(-1, 1, 1);
drawyear = ((rMonth % 11) == 0) ? String(rD.getFullYear()).substr(-2) : "";
let drawyear = ((rMonth % 11) == 0) ? String(rD.getFullYear()).substr(-2) : "";
g.drawString(m + drawyear, bottomrightX, bottomrightY - CELL_H, 1);
newmonth = false;
}
@ -124,7 +134,7 @@ function drawMinutes() {
var d = new Date();
var hours = s.MODE24 ? d.getHours().toString().padStart(2, ' ') : ((d.getHours() + 24) % 12 || 12).toString().padStart(2, ' ');
var minutes = d.getMinutes().toString().padStart(2, '0');
var textColor = NRF.getSecurityStatus().connected ? '#99f' : '#fff';
var textColor = NRF.getSecurityStatus().connected ? '#fff' : '#f00';
var size = 50;
var clock_x = (w - 20) / 2;
if (dimSeconds) {
@ -156,7 +166,7 @@ function drawSeconds() {
}
function drawWatch() {
if (DEBUG) console.log("CALENDAR");
if (DEBUG) console.log("DRAWWATCH");
monthOffset = 0;
state = "watch";
var d = new Date();
@ -197,6 +207,7 @@ function drawWatch() {
if (DEBUG) console.log("Next Day:" + (nextday / 3600));
if (typeof dayInterval !== "undefined") clearTimeout(dayInterval);
dayInterval = setTimeout(drawWatch, nextday * 1000);
if (DEBUG) console.log("ended DRAWWATCH. next refresh in " + nextday + "s");
}
function BTevent() {
@ -211,8 +222,12 @@ function action(a) {
g.reset();
if (typeof secondInterval !== "undefined") clearTimeout(secondInterval);
if (DEBUG) console.log("action:" + a);
state = "unknown";
console.log("state -> unknown");
let l;
switch (a) {
case "[ignore]":
drawWatch();
break;
case "[calend.]":
drawFullCalendar();
@ -229,6 +244,12 @@ function action(a) {
load(l[0]);
} else E.showAlert("Message app not found", "Not found").then(drawWatch);
break;
case "[AI:agenda]":
l = require("Storage").list(RegExp("agenda.*app.js"));
if (l.length > 0) {
load(l[0]);
} else E.showAlert("Agenda app not found", "Not found").then(drawWatch);
break;
default:
l = require("Storage").list(RegExp(a + ".app.js"));
if (l.length > 0) {
@ -276,7 +297,6 @@ function input(dir) {
drawWatch();
}
break;
}
}
@ -309,3 +329,10 @@ NRF.on('disconnect', BTevent);
dimSeconds = Bangle.isLocked();
drawWatch();
setWatch(function() {
if (state == "watch") {
Bangle.showLauncher()
} else if (state == "calendar") {
drawWatch();
}
}, BTN1, {repeat:true, edge:"falling"});

View File

@ -1,7 +1,7 @@
{
"id": "clockcal",
"name": "Clock & Calendar",
"version": "0.08",
"version": "0.10",
"description": "Clock with Calendar",
"readme":"README.md",
"icon": "app.png",

View File

@ -1,6 +1,6 @@
(function (back) {
var FILE = "clockcal.json";
defaults={
const defaults={
CAL_ROWS: 4, //number of calendar rows.(weeks) Shouldn't exceed 5 when using widgets.
BUZZ_ON_BT: true, //2x slow buzz on disconnect, 2x fast buzz on connect. Will be extra widget eventually
MODE24: true, //24h mode vs 12h mode
@ -9,19 +9,19 @@
REDSAT: true, // Use red color for saturday?
DRAGDOWN: "[AI:messg]",
DRAGRIGHT: "[AI:music]",
DRAGLEFT: "[ignore]",
DRAGLEFT: "[AI:agenda]",
DRAGUP: "[calend.]"
};
settings = Object.assign(defaults, require('Storage').readJSON(FILE, true) || {});
let settings = Object.assign(defaults, require('Storage').readJSON(FILE, true) || {});
actions = ["[ignore]","[calend.]","[AI:music]","[AI:messg]"];
let actions = ["[ignore]","[calend.]","[AI:music]","[AI:messg]","[AI:agenda]"];
require("Storage").list(RegExp(".app.js")).forEach(element => actions.push(element.replace(".app.js","")));
function writeSettings() {
require('Storage').writeJSON(FILE, settings);
}
menu = {
const menu = {
"": { "title": "Clock & Calendar" },
"< Back": () => back(),
'Buzz(dis)conn.?': {

4
apps/counter2/ChangeLog Normal file
View File

@ -0,0 +1,4 @@
0.01: New App!
0.02: Added Settings & readme
0.03: Fix lint warnings
0.04: Fix lint warnings

24
apps/counter2/README.md Normal file
View File

@ -0,0 +1,24 @@
# Counter2 by Michael
I needed an HP/XP-Tracker for a game, so i made one.
The counter state gets saved. Best to use this with pattern launcher or ClockCal
- Colored Background Mode
- ![color bg](https://stuff-etc.github.io/BangleApps/apps/counter2/counter2-screenshot.png)
- Colored Text Mode
- ![color text](https://stuff-etc.github.io/BangleApps/apps/counter2/counter2dark-screenshot.png)
## Howto
- Tap top side or swipe up to increase counter
- Tap bottom side or swipe down to decrease counter
- Hold (600ms) to reset to default value (configurable)
- Press button to exit
## Configurable Features
- Default value Counter 1
- Default value Counter 2
- Buzz on interact
- Colored Text/Background
## Feedback
If something isn't working, please tell me: https://github.com/Stuff-etc/BangleApps/issues

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwcAyVJkgCFAwwCBAgd5CI+eCI2T/IRH/wR7n//AAPyCIdPBAX8CKpr/CLTpSCOipB8gRFXoPJCIknCJAIBOoYRCagLNCa4f8Q4gREI4tP8mT/41HCKJHFGoQRG+QKBLI4RHLIx9CCJ7zBGpxZCPoyhQYpIIBYor7kCP4R8YoX/WY69DAIM/BAT+BdIYICeYQRTGqKP/CNIA=="))

95
apps/counter2/app.js Normal file
View File

@ -0,0 +1,95 @@
Bangle.loadWidgets();
var s = Object.assign({
counter0:10,
counter1:20,
max0:15,
max1:25,
buzz: true,
colortext: true,
}, require('Storage').readJSON("counter2.json", true) || {});
const f1 = (s.colortext) ? "#f00" : "#fff";
const f2 = (s.colortext) ? "#00f" : "#fff";
const b1 = (s.colortext) ? g.theme.bg : "#f00";
const b2 = (s.colortext) ? g.theme.bg : "#00f";
var drag;
const screenwidth = g.getWidth();
const screenheight = g.getHeight();
const halfwidth = screenwidth / 2;
const halfheight = screenheight / 2;
const counter = [];
counter[0] = s.counter0;
counter[1] = s.counter1;
const defaults = [];
defaults[0] = s.max0;
defaults[1] = s.max1;
function saveSettings() {
s.counter0 = counter[0];
s.counter1 = counter[1];
s.max0 = defaults[0];
s.max1 = defaults[1];
require('Storage').writeJSON("counter2.json", s);
}
let ignoreonce = false;
var dragtimeout;
function updateScreen() {
g.setBgColor(b1);
g.clearRect(0, 0, halfwidth, screenheight);
g.setBgColor(b2);
g.clearRect(halfwidth, 0, screenwidth, screenheight);
g.setFont("Vector", 60).setFontAlign(0, 0);
g.setColor(f1);
g.drawString(Math.floor(counter[0]), halfwidth * 0.5, halfheight);
g.setColor(f2);
g.drawString(Math.floor(counter[1]), halfwidth * 1.5, halfheight);
saveSettings();
if (s.buzz) Bangle.buzz(50,.5);
Bangle.drawWidgets();
}
Bangle.on("drag", e => {
const c = (e.x < halfwidth) ? 0 : 1;
if (!drag) {
if (ignoreonce) {
ignoreonce = false;
return;
}
drag = { x: e.x, y: e.y };
dragtimeout = setTimeout(function () { resetcounter(c); }, 600); //if dragging for 500ms, reset counter
}
else if (drag && !e.b) { // released
let adjust = 0;
const dx = e.x - drag.x, dy = e.y - drag.y;
if (Math.abs(dy) > Math.abs(dx) + 30) {
adjust = (dy > 0) ? -1 : 1;
} else {
adjust = (e.y > halfwidth) ? -1 : 1;
}
counter[c] += adjust;
updateScreen();
drag = undefined;
clearTimeout(dragtimeout);
}
});
function resetcounter(which) {
counter[which] = defaults[which];
console.log("resetting counter ", which);
updateScreen();
drag = undefined;
ignoreonce = true;
}
updateScreen();
setWatch(function() {
load();
}, BTN1, {repeat:true, edge:"falling"});

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,18 @@
{
"id": "counter2",
"name": "Counter2",
"version": "0.04",
"description": "Dual Counter",
"readme":"README.md",
"icon": "counter2-icon.png",
"tags": "tool",
"supports": ["BANGLEJS2"],
"screenshots": [{"url":"counter2-screenshot.png"},{"url":"counter2dark-screenshot.png"}],
"allow_emulator": true,
"storage": [
{"name":"counter2.app.js","url":"app.js"},
{"name":"counter2.settings.js","url":"settings.js"},
{"name":"counter2.img","url":"app-icon.js","evaluate":true}
],
"data": [{"name":"counter2.json"}]
}

55
apps/counter2/settings.js Normal file
View File

@ -0,0 +1,55 @@
(function (back) {
var FILE = "counter2.json";
const defaults={
counter0:12,
counter1:0,
max0:12,
max1:0,
buzz: true,
colortext: true,
};
const settings = Object.assign(defaults, require('Storage').readJSON(FILE, true) || {});
function writeSettings() {
require('Storage').writeJSON(FILE, settings);
}
const menu = {
"": { "title": "Counter2" },
"< Back": () => back(),
'Default C1': {
value: settings[0],
min: -99, max: 99,
onchange: v => {
settings.max0 = v;
writeSettings();
}
},
'Default C2': {
value: settings[2],
min: -99, max: 99,
onchange: v => {
settings.max1 = v;
writeSettings();
}
},
'Color': {
value: settings.colortext,
format: v => v?"Text":"Backg",
onchange: v => {
settings.colortext = v;
console.log("Color",v);
writeSettings();
}
},
'Vibrate': {
value: settings.buzz,
onchange: v => {
settings.buzz = v;
writeSettings();
}
}
};
// Show the menu
E.showMenu(menu);
});

View File

@ -1,11 +1,17 @@
#### ⚠EXPERIMENTAL⚠
# Fastload Utils
*EXPERIMENTAL* Use this with caution. When you find something misbehaving please check if the problem actually persists when removing this app.
Use this with caution. When you find something misbehaving please check if the problem actually persists when removing this app.
This allows fast loading of all apps with two conditions:
* Loaded app contains `Bangle.loadWidgets`. This is needed to prevent problems with apps not expecting widgets to be already loaded.
* Current app can be removed completely from RAM.
#### ⚠️ KNOWN ISSUES ⚠️
* Fastload currently does not play nice with the automatic reload option of the apploader. App installs and upgrades are unreliable since the fastload causes code to run after reset and interfere with the upload process.
## Settings
* Activate app history and navigate back through recent apps instead of immediately loading the clock face

View File

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

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

@ -0,0 +1,17 @@
# Fonts (Korean)
This library provides an Korean font that can be used to display messages.
The font is the 16px high [GNU Unifont](https://unifoundry.com/unifont/index.html).
Korean characters from Unicode codepoint 32-255, 0x1100-0x11FF, 0x3130-0x318F, 0xA960-0xA97F
## Usage
See [the BangleApps README file](https://github.com/espruino/BangleApps/blob/master/README.md#api-reference)
for more information on fonts.
## Recreating fontkorean.pbf
* Go to `bin` directory
* Run `./font_creator.js "Korean" ../apps/fontkorean/font.pbf`

BIN
apps/fontkorean/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 494 B

1
apps/fontkorean/boot.js Normal file
View File

@ -0,0 +1 @@
Graphics.prototype.setFontIntl = function() { return this.setFontPBF(require("Storage").read("fontkorean.pbf")); };

BIN
apps/fontkorean/font.pbf Normal file

Binary file not shown.

3
apps/fontkorean/lib.js Normal file
View File

@ -0,0 +1,3 @@
exports.getFont = (options) => {
return "Intl"; // placeholder for now - see https://github.com/espruino/BangleApps/issues/3109
};

View File

@ -0,0 +1,16 @@
{ "id": "fontkorean",
"name": "Korean font",
"version":"0.01",
"description": "Installs a font data, Unifont characters for Korean **Requires 420 KB storage**",
"icon": "app.png",
"tags": "font",
"type": "module",
"provides_modules" : ["font"],
"supports" : ["BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name":"font","url":"lib.js"},
{"name":"fontkorean.boot.js","url":"boot.js"},
{"name":"fontkorean.pbf","url":"font.pbf"}
]
}

View File

@ -9,3 +9,4 @@
0.10: Show satellites "in view" separated by GNS-system
0.11: Show number of packets received
0.12: Fix number of packets received
0.13: Minor code improvements

View File

@ -42,21 +42,21 @@ function getMaidenHead(param1,param2){
lon = lon + 180;
var t = lon/20;
fLon = Math.floor(t);
const fLon = Math.floor(t);
t = (t % fLon)*10;
sqLon = Math.floor(t);
const sqLon = Math.floor(t);
t = (t-sqLon)*24;
subLon = Math.floor(t);
extLon = Math.floor((t-subLon)*10);
const subLon = Math.floor(t);
const extLon = Math.floor((t-subLon)*10);
lat = lat + 90;
t = lat/10;
fLat = Math.floor(t);
const fLat = Math.floor(t);
t = (t % fLat)*10;
sqLat = Math.floor(t);
const sqLat = Math.floor(t);
t=(t-sqLat)*24;
subLat = Math.floor(t);
extLat = Math.floor((t-subLat)*10);
const subLat = Math.floor(t);
const extLat = Math.floor((t-subLat)*10);
return U[fLon]+U[fLat]+sqLon+sqLat+L[subLon]+L[subLat]+extLon+extLat;
}

View File

@ -1,7 +1,7 @@
{
"id": "gpsinfo",
"name": "GPS Info",
"version": "0.12",
"version": "0.13",
"description": "An application that displays information about latitude, longitude, altitude, speed, satellites and time",
"icon": "gps-info.png",
"type": "app",

View File

@ -1 +1,2 @@
0.01: New App!
0.02: Get health data for the day

View File

@ -91,7 +91,7 @@ const runHassio = () => {
});
const updateSensor = () => {
hassioAttributes.health = Bangle.getHealthStatus();
hassioAttributes.health = Bangle.getHealthStatus("day");
hassioAttributes.accel = Bangle.getAccel();
hassioAttributes.battery = getBattery();
hassioAttributes.compass = Bangle.getCompass();

View File

@ -2,7 +2,7 @@
"name": "Home Assistant API Interface",
"shortName":"Hassio",
"icon": "hassio.png",
"version":"0.01",
"version":"0.02",
"description": "This app gives access to viewing Home Assistant data and sends health, compass, accelerometer, and battery information to Home Assistant as a sensor through the Home Assistant REST API.",
"tags": "tool,sensors",
"supports": ["BANGLEJS2"],

View File

@ -31,3 +31,4 @@
Fix daily summaries for 31st of the month
0.28: Calculate distance from steps if myprofile is installed and stride length is set
0.29: Minor code improvements
0.30: Minor code improvements

View File

@ -176,7 +176,6 @@ function barChart(label, dt) {
chart_label = label;
chart_data = dt;
drawBarChart();
swipe_enabled = true;
}
function drawBarChart() {

View File

@ -2,7 +2,7 @@
"id": "health",
"name": "Health Tracking",
"shortName": "Health",
"version": "0.29",
"version": "0.30",
"description": "Logs health data and provides an app to view it",
"icon": "app.png",
"tags": "tool,system,health",

View File

@ -10,3 +10,4 @@
0.10: Autoscale raw graph to maximum value seen
0.11: Automatic translation of strings.
0.12: Minor code improvements
0.13: Minor code improvements

View File

@ -61,7 +61,6 @@ var scale = 2000;
/* On newer (2v10) firmwares we can subscribe to get
HRM events as they happen */
Bangle.on('HRM-raw', function(v) {
h=v;
hrmOffset++;
if (hrmOffset>g.getWidth()) {
let thousands = Math.round(rawMax / 1000) * 1000;
@ -76,7 +75,7 @@ Bangle.on('HRM-raw', function(v) {
if (rawMax < v.raw) {
rawMax = v.raw;
}
y = E.clip(btm-(8+v.filt/3000),btm-24,btm);
let y = E.clip(btm-(8+v.filt/3000),btm-24,btm);
g.setColor(1,0,0).fillRect(hrmOffset,btm, hrmOffset, y);
y = E.clip(btm - (v.raw/scale*84),84,btm);
g.setColor(g.theme.fg).drawLine(lastHrmPt[0],lastHrmPt[1],hrmOffset, y);
@ -120,7 +119,7 @@ function readHRM() {
for (var i=0;i<2;i++) {
var a = hrmInfo.raw[hrmOffset];
hrmOffset++;
y = E.clip(170 - (a*2),100,230);
let y = E.clip(170 - (a*2),100,230);
g.setColor(g.theme.fg).drawLine(lastHrmPt[0],lastHrmPt[1],hrmOffset, y);
lastHrmPt = [hrmOffset, y];
}

View File

@ -1,7 +1,7 @@
{
"id": "hrm",
"name": "Heart Rate Monitor",
"version": "0.12",
"version": "0.13",
"description": "Measure your heart rate and see live sensor data",
"icon": "heartrate.png",
"tags": "health",

View File

@ -10,3 +10,4 @@
0.09: Bangle.js 2 compatibility
0.10: Tell clock widgets to hide.
0.11: Allow fullscreen clock faces with hidden widgets
0.12: Fixed a bug where other app's graphics would not get cleared.

View File

@ -3,6 +3,7 @@ Draws a fullscreen image from flash memory
Saves a small image to flash which is just the area where the clock is
Keeps an offscreen buffer and draws the time to that
*/
g.clear(); //clears other apps's graphics
var is12Hour = (require("Storage").readJSON("setting.json",1)||{})["12hour"];
var inf = require("Storage").readJSON("imgclock.face.json");
var img = require("Storage").read("imgclock.face.img");

View File

@ -2,7 +2,7 @@
"id": "imgclock",
"name": "Image background clock",
"shortName": "Image Clock",
"version": "0.11",
"version": "0.12",
"description": "A clock with an image as a background. **Note:** this clock shows seconds, so has higher than average power consumption.",
"icon": "app.png",
"type": "clock",

2
apps/jclock/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.01: Created
0.02: Changed side bar color to blue for better clarity when it's locked

26
apps/jclock/README.md Normal file
View File

@ -0,0 +1,26 @@
# jclock
I have used Rebble clock since I bought my Banglejs 2, and wanted to make my own clock with much simpler features and to switch the time window and the feature window because I'm wearing my watch on my left wrist and about a half (left side) of the screen is covered by the sleeve of my jacket or shirts. Of course it won't happen during summer, but I decided to make my first Bagle app with these changes. See Features below for the items displayed on the screen.
- The layout is inspired by the Rebble clock.
- The big font KdamThmor is copied from the Rebble clock.
## Features
- Single screen
- No settings
- Time on the right side with big font
- On the sidebar on the left
- Day of week
- Day
- Month
- Steps
- Bluetooth connection status
- Battery %
- Update time and status every 1 minute
## Screenshots
![](jclock_screenshot_no_BT.png)
![](jclock_screenshot_BT.png)
## Creator
Written by [JeonLab](https://jeonlab.wordpress.com)

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

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEw4f/AAIHB7ue4cYrPO0cQtUy2WUHU0kyVJARAQEhIRLkgQCgQOKAQWACIYbHIImQAYMSpQRLgmSCIVSCJcACIWSKJARPzO9gETm+4BwNACI8Etu28GN23fIgIRIg14hnw/UI7wRGltvCIMjuEO7BCBCI97twRCsEICIMO7gRKnYRCju2/A1Hr4RHEY5ZDGokJzeACJRZB+EAgPbCIxrDgd4g347kBEY8rLIUHUIPA9qhICIcA/LFBj830ARLAAwR/CMkd3wOBozXHCIcE5oRCswRLg/RCIMD3gRLcoIRBgOwCJ8Z+ARIfYQRDx8ACI97CI1uCI9K7YRFglt23b4ARGuQROpPXAQI1EAAJHHkgCCg4gB+ARVyQRIdI+SdgVSCKFKCJcEyADBgVJlIRJhMkCIUAEQgCIwAXECJUgHYoRJPQIAlA="))

68
apps/jclock/app.js Normal file
View File

@ -0,0 +1,68 @@
// single screen, clock on the right, sidebar without image (date/steps/bluetooth connection status/batt%)
// Large font KdamThmor taken from Rebble clock
Graphics.prototype.setFontKdamThmor = function(scale) {
// Actual height 70 (69 - 0)
this.setFontCustom(
E.toString(require('heatshrink').decompress(atob('AH4AMgfABZM/BZMB/4WJg/+BZMf/ALJ//gIpP/wAugLpUAvyBKsDC/ACKYJQIKYJgaYKv6YJh7HJeoP8VxLSJg//+D0JIhMf/7RIf4JPJv//LX5a6CwLvJn5aJLYIKJgY4IADn/KpKvBAAKvIAARiGBQanGOwILJBQgLFFogvGIgZHGWAIAEdwg5FNYreBAAjvDeoIAFYQcfBYy3DEQRKEKQQiCAoRiCIogoDCIJGDEQLlEIwZoBCwYLCHQQoBQwgGEj7aFGoKuDKwYSFE4LZFv41Ch6dEIITICn5FEDwQuDeAwuEBQgeEB4b8EFwbADNIZdaHQoSBFwUfNIoGEv5GFXYpGEIoJBCZgjZGHQILDCwIpDj//GgQoBMggcBAApkDBQwiDDoQAEEQY0BERJGBERBGCERC8BBYrYFBQj8FLwrBGBQbkFEYoKFBYgtFL4jLFZ4gKJAH4AciALKRA73DbIgAFj/ABZLOGEQjDEj40En6tEv4oDgLPEAoLRFCIcHDgouJDgP4FxAiFFwt//xXEFwcDEQouEj4iEFwv/EQguEEQJ6EFwgiBS4guE/5uEFwiiBAAyiDBQwdDCw4uCIoIAGFwSLBF34unAAy7EAAy7EAAzqEAArqEF34ukAH4AGgfgNJWAAod8Cwn+SQn4RggFEv4oE/4FDg//FAYFFn4oEAoidBFAYFFh//YIYFBFwd//7BDAoIuCgf/YIYFBFwcfFAgFFDgIoDDgIFCEQpcBFwZFFn4uEAoJcEFwYFBLgouDQoo/BAwcf/hcEFwgiELgPfFwQRBEQYVBFwcPDYYzB+YSDn55DKwOPFwgbCKwP8CQYuBXIouEKIZcBIIgbF/BBEDYZcB4ASFDYI5BCgIuEHQSzCFwo6CeYQuEv4nBOYIPBFwa7Ddoa7FJoLtCFwhNBAAQfBFwiTBAAXAT4oKDCYSfFAAQ9BFwg6BAAQHBFwhDCLgQuFIwY5BFwhGDDwT9FOQI5CFwpSDDoYuDBYQWCFwoLCAgQuFCIsHFwgAFh4uEAH4AWjgLKvwGFj6LDP4sBcgjhCCwaGDn4LEgKjDAgKXEh61Dg7LEdQIuDj7AEZgIpDfYPACIgdCFwLjDdIQRCFwIoDEQJdEFAgiBJgYoEEQoLCAoRFFBYRjCFAIWDQII0Dv6SFv40CRYg1DHQRXBBQg1BFISpDBwQSEEQTQDj4SCDYJKBh42Cv4uCh4TCn4aBIIIuDCYIHBDQIeBFwYPBg4aCe4YPDfAYuHv4uNLo6bBLpJ4EFwYTBEQIHBCQYbBHQIqBEwIGCXYl/IQTwDD4P+CwIfBFILCCBAQACwACBEQQQBAArlDn4LGcoY3BGAIlEHQYAB+YiGMQIAB54DCOgRGD/0fEQpGD+A+CEQZ6BLYhFEKQX8HwYKDBYXgHwQ5DBYQpBBYQ5DHYRWDUQQAGgK5DADsBBZUfb4IAIOYoAETgJcFAAbLBBRBoBUQg5FRYxQDRYJGIZQQ5KFxDtCFxDpCFw7dIfAouICwQuHHIP+FxBQB8YuHf4UPFw6KCn4uGKAWAFw6KB/glBHJHAFw5QCQQIuGRQLzBFww5CKgRQH/A9BFwxQCFw45BCYQuGKAI5BFwwGBKAIuHRQRVCFwhQDFw6KBKAIuHfwQAEGAYKGGgbQCAAowCFwIAGF34ugAAjqHTojqFfQrqFcYoWJF0f+CxMH8ALJAEkCBZU8BRMB/CCKOw0DA4V/OwqhBA4IDBwAKFVoTlBBQytCn6xDBQX/IQQDDAgIACSwIRBTQQWDGwUHHQYzBAAK5CHQk/Fwo6EFwppBNoQuGgIPDFwYeCOoguC34eCh74DEASMCCQI+CDYQCBCQYuDDYMPFwQ6BFwYbBn4uCg4uE8ASBFwUfFwqIBCQV/FwsfLpAbBPgZdFFwpdGFwhdHDwQPELoYeCHwYbD/46CAYaMEBwLqFFwRGCv5RDFYUfBYIWBGQQuDv7iDMIQuCNIIADCwQuCfIgiDFwT5DEQYuDHQIiFVAc/EQyJDIwYiDc4RGDNAYuBCAJGDRYQHBCAQLDCwcPCAR+BHIgAEBYQKHEYQtDAH4Ak/gKJZALMBRhLGDAAjSGWYgLCEY7qDBYwtCXhBEBewzpF/5fGj4LDdYwKD//gKBBeHKAZGGHIX+gJGGKAQfBHQoSBCYQEB+A5GA4InBHQiJEQgKKGOIUPHQg5CFQU/HQaKDVgR1ERQQeCIwK8DBQPvDwUHFwZQB/0/DwUfFwaKB+IeDv4PCHIWHFw45B/geDFwjBCDwYPDEQKsCLoxFB+CIDCQIPCP4OAj6MCj4uEBAN/FQV/SAS0CFwIqBXYioCA4ZYBVwYbBHoIaCQAY+CHoPACwKADGwa+CEQcPFQIfBAARVCgE+dgiGCBYRVCHQLiFganEEQsIZQgiFAAZFGAAZGDNAYADcQSLDAAhSCVwYLHHI4LCCxC5FAH4AIJhRYBXgQAGh5vJgE/VI4uDSRAuJoAuJg4uKvguJg/wFxN/OAQuGaoIuJv/8FxAWBFxN/T4YuFCwIuJCwIuICwQuICwIuICwQGDFwgWCEQQuECwQpDFwk/BQIdDFwYPBCwguECwwuDCw4uDCw4uCCw4uDCw4uCCxAuCCxAuBCwYKEFwQWCRIYuD8YWIEAO/CxEPCoQWGLQYWHFwIWJJ4YWHFwYKGFwYWHFwYKHFwQWIFwQKHFwQWIFwQKIFwIWJdQQuJ8ALJAH8f/BuK/gIFv6RDBYqlBwEBSIIjFA4OAWgSSEA4WAv4LGA4TXC//Ab4v+j4LCwBYDAwP8DQTNEAwXzAYTCDFQfvAYRSDFQYADIwYqDAAZGCEQYAB8A6ENARHCDoI6DAgKKCD4N/HQQIB8ACBCYQGBAYMHE4IxBIQIPBHQU/DYIOBA4ISCDYQHBh4iCh7ICD4IaEAYJpCB4d/GwQuEGwasBDwYPBA4MHFw4HCj4uHA4QuULqyUDRgxCCRhC0Cn46CEwYbB+DhCYQa7DAAQyBcoIaBdQoLBawYrCAApRCHQILGKIT/C//7Eoh1DAAPvAYRRCIwkfEQpGD/AyDBQSBBCQQiGKQX+HwYiDKQXwGQRFDBYYyDNAYLCAwILCBQg+FHIgAEC4IKIQwKtCAH4AWnwKJPoKrEOAi3GaY4WJ/6KHW4ShIfwTbFAAMDCwX8A4UYHIrQE8AiFeYcHHwQiDKQZ6DEQZSCgYmDEQZGCj4uCEQQZBCYRtDNAPAg46Cg5hDv5aBBYI6Bn4aCRYInBDQIpCFwQTBGwQaBGQIuCn59Cn4uBSAgbDHoYuCE4JlCEwJjBCQUPEQUH/hjCFwaUCj/wHIKzDSgd/4AWBQAhhDcYTpDFwg5BUYYuE8Y5ELoufHIhdFaoguBYYbJESgjWDGgQHCH4IiDBQZZBCIIiCKAa7CIwIWCKAbPC8AWCKAZpCCgRQFIQhQGHQQADKAhOEKApGDAARQEIwZQHIwpQFBYpQFKQgWHPwYWHBYQWIEYREGL4YKJAH4AegIEDsCxGPIfgCwr/Dn6nFh6jCgKcGn/wEQQbDXgYqCn/4BQkDDwYPDFzV/JoUfB4RdOgI1DnjG/ACoA='))),
46,
atob("GBo2NjY2NjY2NjY2Gg=="),
94+(scale<<8)+(1<<16)
);
return this;
};
var drawTimeout;
// schedule a draw for the next minute
function queueDraw() {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = setTimeout(function() {
drawTimeout = undefined;
draw();
}, 60000 - (Date.now() % 60000));
}
const zeroPad = (num, places) => String(num).padStart(places, '0');
function draw() {
let barWidth = 64;
let date = new Date();
// queue next draw in one minute
queueDraw();
// clean screen
g.reset().clearRect(Bangle.appRect);
// draw side bar in blue
g.setColor('#00f');
g.fillRect(0, 0, barWidth, g.getHeight());
// show time on the right
g.setColor(g.theme.fg);
g.setFontKdamThmor().setFontAlign(0,-1).drawString(zeroPad(date.getHours(),2), 120, 10);
g.setFontKdamThmor().setFontAlign(0,-1).drawString(zeroPad(date.getMinutes(),2), 120, g.getHeight()/2+10);
// show date
g.setFont('Vector', 20).setFontAlign(0, -1).setColor('#fff');
g.drawString(require("date_utils").dow(date.getDay(),1).toUpperCase(), barWidth/2, 3);
g.drawString(date.getDate(), barWidth/2, 28);
g.drawString(require("date_utils").month(date.getMonth()+1,1).toUpperCase(), barWidth/2, 53);
// divider, place holder for any other info
g.drawString('=====', barWidth/2, 78);
// show daily steps
g.drawString(Bangle.getHealthStatus("day").steps, barWidth/2, 103);
// show battery remaining percentage
g.drawString(E.getBattery() + '%', barWidth/2, 153);
// Bluetooth connection status
if (NRF.getSecurityStatus().connected) g.drawString('>BT<', barWidth/2, 128);
}
draw();
Bangle.setUI("clock");

BIN
apps/jclock/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 824 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

16
apps/jclock/metadata.json Normal file
View File

@ -0,0 +1,16 @@
{ "id":"jclock",
"name":"jclock",
"shortName":"jclock",
"icon":"app.png",
"version":"0.02",
"description":"Similar layout to Rebble clock, but much simpler features with switched time and the feature window. The time is on the right side. This is my first Bangle app.",
"type": "clock",
"tags": "clock",
"supports" : ["BANGLEJS2"],
"screenshots": [{"url":"jclock_screenshot_no_BT.png"},{"url":"jclock_screenshot_BT.png"}],
"storage": [
{"name":"jclock.app.js","url":"app.js"},
{"name":"jclock.img","url":"app-icon.js","evaluate":true}
],
"readme":"README.md"
}

View File

@ -59,13 +59,6 @@ module.exports = {
"no-unused-vars"
]
},
"sleeplog/settings.js": {
"hash": "bd5e3e1382321df6682ef1cb718b0e15ab355422bef77278eb086f213f643021",
"rules": [
"no-unused-vars",
"no-undef"
]
},
"showimg/app.js": {
"hash": "71cbbaa488e2d08c5bf28f7d56178d5e7694eb9761cd4752bbc9733e825d4bcf",
"rules": [
@ -276,13 +269,6 @@ module.exports = {
"no-undef"
]
},
"sleeplog/app.js": {
"hash": "336da552e4b04677447cf76a253b40bc259a597ea11d455121933f93afe99794",
"rules": [
"no-unused-vars",
"no-undef"
]
},
"qmsched/app.js": {
"hash": "4b7dbabed6c252021531d6b0449c16a3adc2e405f2ddda33ca0a65f5fa42c663",
"rules": [
@ -562,13 +548,6 @@ module.exports = {
"no-undef"
]
},
"sleeplog/lib.js": {
"hash": "755e0d4c02b92181281fd6990df39c9446c73ff896b50b64d7e14cb1c0188556",
"rules": [
"no-unused-vars",
"no-undef"
]
},
"doztime/app-bangle1.js": {
"hash": "1e9598c201175180ae77d1c3bc47e8138b339b72eb58782b5057fb7aefdc88a1",
"rules": [
@ -666,12 +645,6 @@ module.exports = {
"no-undef"
]
},
"taglaunch/app.js": {
"hash": "944689f0600e59bbe4d9e5e2684baeefabe4457a6edd938aae451dc4cd659ad3",
"rules": [
"no-undef"
]
},
"tabanchi/app.js": {
"hash": "6ad6dc1d6b0f539f9f659d5773b5a26d19eb6dacafe7b4682469e6f3c412647e",
"rules": [
@ -762,12 +735,6 @@ module.exports = {
"no-undef"
]
},
"sleeplog/boot.js": {
"hash": "b4c9d8e3c3e7cdf44ea10e29a9e3b53f958b86c21ca91d88e4efb85901c3bde9",
"rules": [
"no-undef"
]
},
"scicalc/app.js": {
"hash": "416c7b2eb12a5d10bcc3a99d89d8f6f54ecd2b47cce2d1f4d55c3e3bc602b31a",
"rules": [
@ -798,12 +765,6 @@ module.exports = {
"no-undef"
]
},
"ratchet_launch/app.js": {
"hash": "592d432301d7836aa54e288d465ae8952ecb891d628f824ea9f62479a2a01631",
"rules": [
"no-undef"
]
},
"rclock/rclock.app.js": {
"hash": "8e698787730601a1bba71aff03204c2adfaf7eeb77b35dc706534755f63f613b",
"rules": [
@ -1068,12 +1029,6 @@ module.exports = {
"no-undef"
]
},
"hrm/heartrate.js": {
"hash": "beb8e433f10d3639b343b060f0d5583ea665445f92b2171daff7612eaf135596",
"rules": [
"no-undef"
]
},
"hebrew_calendar/app.js": {
"hash": "3077d581b9fcf73816e265e61105a0692356b89e8ed41a82be51960ae26fc8de",
"rules": [
@ -1086,12 +1041,6 @@ module.exports = {
"no-undef"
]
},
"health/app.js": {
"hash": "6d612eed04ee5a844be6ad47c326624cd3e204fecf1c28c99a57ca963b3d7a7b",
"rules": [
"no-undef"
]
},
"hassio/hassio.app.js": {
"hash": "b8fbb03cf4a7595299e65a46c4f850394bf57cd4cba879d5524eafbf40ccc32e",
"rules": [
@ -1110,12 +1059,6 @@ module.exports = {
"no-undef"
]
},
"gpsinfo/gps-info.js": {
"hash": "1eb77f45d4182613879b8214dc174f84c7333b4a541c2b43cba6014a16f470ee",
"rules": [
"no-undef"
]
},
"glbasic/glbasic.app.js": {
"hash": "7d12a030d6f0ef69a0e5a9783229fd49c0a6a06bf751e3ac562145d2ce8350e9",
"rules": [

1
apps/meridian/ChangeLog Normal file
View File

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

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

@ -0,0 +1,11 @@
# Meridian Clock
An elegant clock with 2 clock info
## Usage
Tap on a widget and swipe left/right/up/down to change the displayed info
## Creator
Spioune

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwgMAkEAwEEBIMQAo8IDpMYAq0wAoosCAoU8gNgAoV+gwRDvgXDsAFFEYgFR4AFQIgQFHgIFC8AFDg4HBhwWEngFE+AEDgYuEh4QEDgoASiGII4kMAYLWBgDKBggzEgb/YICSJBGwIFDghCDAoQ"))

153
apps/meridian/app.js Normal file
View File

@ -0,0 +1,153 @@
function getArcXY(centerX,centerY,radius,angle){
var s,r = [];
s = 2 * Math.PI * angle / 360;
r.push(centerX + Math.round(Math.cos(s) * radius));
r.push(centerY + Math.round(Math.sin(s) * radius));
return r;
}
function getArc(centerX,centerY,radius,startAngle,endAngle){
var r = [], actAngle = startAngle;
var stepAngle = (radius + radius) * Math.PI / 60;
stepAngle = 6;
while(actAngle < endAngle){
r = r.concat(getArcXY(centerX,centerY,radius,actAngle));
actAngle += stepAngle;
actAngle = Math.min(actAngle,endAngle);
}
return r.concat(getArcXY(centerX,centerY,radius,endAngle));
}
function fillLine(x1,y1,x2,y2,thickness){
const angle = Math.atan2(y2 - y1, x2 - x1);
const offset_x = thickness * Math.sin(angle) / 2;
const offset_y = thickness * Math.cos(angle) / 2;
g.fillPoly([
x1 + offset_x,
y1 - offset_y,
x1 - offset_x,
y1 + offset_y,
x2 - offset_x,
y2 + offset_y,
x2 + offset_x,
y2 - offset_y
],true);
}
function drawInfoClock(itm,info,options){
g.reset();
if (options.focus)
g.drawCircle(options.x+options.w/2, options.y+options.h/2, options.w/2+3);
if (info.img)
g.drawImage(info.img, info.img.width ? options.x+options.w/2-info.img.width/2 : options.x, options.y);
if(info.text)
g.setFont("6x8").setFontAlign(0,1).drawString(info.text, options.x+options.w/2,options.y+options.h);
}
var clockInfoItems = require("clock_info").load();
clockInfoItems[0].items.unshift({
name : "BatteryRing",
hasRange : true,
get : () => {
var s = 30;
var mid=s/2;
var v = E.getBattery();
var g = Graphics.createArrayBuffer(s,s,4);
const outerarc = getArc(mid,mid,14,-90,Math.max(v*3.6, 10)-90);
const innerarc = getArc(mid,mid,11,-92,Math.max(v*3.6, 10)-88);
g.reset();
g.transparent=0;
g.setColor('#00FF00').fillPoly([mid, mid].concat(outerarc));
g.setColor('#000').fillPoly([mid, mid].concat(innerarc));
g.setFont("6x8").setColor('#FFF').setFontAlign(0, 0).drawString(v, mid, mid);
return { v : v, min:0, max:100, img : g.asImage("object") };
},
show : function() { },
hide : function() { },
});
var topleft = require("clock_info").addInteractive(clockInfoItems, {
x : g.getWidth()*(1/4)-15, y: g.getHeight()*(1/4)-15, w: 30, h:30,
draw : (itm,info,options)=>{
topleft.info = info;
topleft.options = options;
if(typeof draw === 'function') draw();
}
});
var topright = require("clock_info").addInteractive(clockInfoItems, {
x : g.getWidth()*(3/4)-15, y: g.getHeight()*(1/4)-15, w: 30, h:30,
draw : (itm,info,options)=>{
topright.info = info;
topright.options = options;
if(typeof draw === 'function') draw();
}
});
var timeout;
function draw(){
if(timeout){
clearTimeout(timeout);
timeout = undefined;
}
g.setTheme({fg:0xFFFF, bg:0});
g.reset().clear();
const mid=g.getWidth()/2;
for(let i = 0; i<12;i++){
const angle = i*Math.PI/6;
fillLine(mid, mid, mid+Math.cos(angle)*120, mid+Math.sin(angle)*120, 3);
}
g.clearRect(10,10,g.getWidth()-10,g.getHeight()-10);
if(topleft && topleft.info && topleft.options)
drawInfoClock(topleft.itm, topleft.info, topleft.options);
if(topright && topright.info && topright.options)
drawInfoClock(topright.itm, topright.info, topright.options);
const now = new Date();
g.setFont("Vector",14);
g.setColor('#FFF');
g.setFontAlign(0,0);
// Date (ex. MON 8)
g.drawString(require("locale").dow(now, 1).toUpperCase() + " " + now.getDate(), g.getWidth()/2, g.getHeight()*(3/4));
let rhour = (now.getHours()*Math.PI/6)+(now.getMinutes()*Math.PI/30/12)-Math.PI/2;
let rmin = now.getMinutes()*Math.PI/30-Math.PI/2;
// Middle circle
g.fillCircle(mid,mid,4);
// Hour hand
fillLine(mid, mid, mid+Math.cos(rhour)*10, mid+Math.sin(rhour)*10,3);
fillLine(mid+Math.cos(rhour)*10, mid+Math.sin(rhour)*10, mid+Math.cos(rhour)*50, mid+Math.sin(rhour)*50,7);
// Minute hand
fillLine(mid, mid, mid+Math.cos(rmin)*10, mid+Math.sin(rmin)*10,3);
fillLine(mid+Math.cos(rmin)*10, mid+Math.sin(rmin)*10, mid+Math.cos(rmin)*76, mid+Math.sin(rmin)*76,7);
if(new Date().getMinutes()==0){
Bangle.buzz();
}
timeout = setTimeout(()=>{
timeout = undefined;
draw();
}, 60000 - (Date.now() % 60000));
}
draw();
Bangle.setUI("clock");

BIN
apps/meridian/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,17 @@
{
"id": "meridian",
"name": "Meridian Clock",
"shortName": "Meridian",
"version": "0.01",
"description": "An elegant clock",
"screenshots": [{ "url": "screenshot.png" }],
"icon": "icon.png",
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS2"],
"readme": "README.md",
"storage": [
{ "name": "meridian.app.js", "url": "app.js" },
{ "name": "meridian.img", "url": "app-icon.js", "evaluate": true }
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -104,3 +104,4 @@
0.75: Handle text with images in messages list by just displaying the first line
0.76: Swipe up/down on a shown message to show the next newer/older message.
0.77: Messages can now use international fonts if they are installed
0.78: Fix: When user taps on a new message, clear the unread timeout

View File

@ -233,6 +233,7 @@ function showMusicMessage(msg) {
}
function showMessageScroller(msg) {
cancelReloadTimeout();
active = "scroller";
var bodyFont = fontBig;
g.setFont(bodyFont);

View File

@ -2,7 +2,7 @@
"id": "messagegui",
"name": "Message UI",
"shortName": "Messages",
"version": "0.77",
"version": "0.78",
"description": "Default app to display notifications from iOS and Gadgetbridge/Android",
"icon": "app.png",
"type": "app",

View File

@ -5,3 +5,9 @@
0.05: Fix the overlay keeping the LCD on
0.06: Better low memory handling
Fix first message beeing displayed again on unlock
0.07: Adds settings
Automatic discard of oldest messages
Indicator for multiple messages in queue
Some optimization in the rendering code
Track handler changes done by background code
0.08: Fix linter warnings

View File

@ -2,23 +2,26 @@
This app handles the display of messages and message notifications as an overlay pop up.
It is a GUI replacement for the `messages` apps.
It is a GUI replacement for the `messagesgui` app.
Messages are ephemeral and not stored on the Bangle.
## Usage
Close app by tapping the X and scroll by swiping. The border of the pop up changes color if the Bangle is locked. The color depends on your currently active theme.
Close app by tapping the X and scroll by swiping. The title background of the pop up changes color if the Bangle is locked. The color depends on your currently active theme.
## Firmware hint
Current stable firmware draws incorrect colors for emojis. Nightly firmware builds correct this.
## Theme support
Using the system theme needs more RAM since it uses a 16 bit color buffer for normal message display. Selecting the "low RAM" theme reduces that to a 4 bit buffer.
16 bit buffer with a small message takes ~4K RAM blocks while 4 bit buffer only needs about 1.5K.
## Low memory mode
If free memory is below 2000 blocks, the overlay automatically only uses 1 bit depth. Default uses roundabout 1300 blocks, while low memory mode uses about 600.
If the overlay estimates that showing the next message would get under the configured minimum free memory limit it automatically only tries to use 1 bit depth. Low memory mode uses about 0.8K blocks plus memory needed for messages. If dropping to 1 bit depth is not enough the oldest messages are discarded to keep the overlay working.
## Creator
[halemmerich](https://github.com/halemmerich)
Forked from messages_light by Rarder44

View File

@ -0,0 +1,6 @@
{
"autoclear": 30,
"border": 10,
"minfreemem": 2,
"systemTheme": true
}

View File

@ -1,49 +1,69 @@
const MIN_FREE_MEM = 1000;
const LOW_MEM = 2000;
const ovrx = 10;
const ovry = 10;
let lockListener;
let ovr;
let clearingTimeout;
// Converts a espruino version to a semantiv versioning object
const toSemantic = function (v){
return {
major: v.substring(0,v.indexOf("v")),
minor: v.substring(v.indexOf("v") + 1, v.includes(".") ? v.indexOf(".") : v.length),
patch: v.includes(".") ? v.substring(v.indexOf(".") + 1, v.length) : 0
};
};
const isNewer = function(espruinoVersion, baseVersion){
const s = toSemantic(espruinoVersion);
const b = toSemantic(baseVersion);
return s.major >= b.major &&
s.minor >= b.major &&
s.patch > b.patch;
};
let needsWorkaround;
let settings = Object.assign(
require('Storage').readJSON("messagesoverlay.default.json", true) || {},
require('Storage').readJSON("messagesoverlay.json", true) || {}
);
settings = Object.assign({
fontSmall:"6x8",
fontMedium:"6x15",
fontBig: "12x20"
}, settings);
const ovrx = settings.border;
const ovry = ovrx;
const ovrw = g.getWidth()-2*ovrx;
const ovrh = g.getHeight()-2*ovry;
let _g = g;
let lockListener;
let quiet;
let LOG=()=>{};
//LOG = function() { print.apply(null, arguments);};
let LOG = function() {
//print.apply(null, arguments);
};
let isQuiet = function(){
if (quiet == undefined) quiet = (require('Storage').readJSON('setting.json', 1) || {}).quiet;
return quiet;
};
let settings = {
fontSmall:"6x8",
fontMedium:"Vector:14",
fontBig:"Vector:20",
fontLarge:"Vector:30",
const isQuiet = function(){
return (require('Storage').readJSON('setting.json', 1) || {}).quiet;
};
let eventQueue = [];
let callInProgress = false;
let buzzing = false;
let show = function(ovr){
let img = ovr;
LOG("show", img.getBPP());
const show = function(){
let img = ovr.asImage();
LOG("show", img.bpp);
if (ovr.getBPP() == 1) {
img = ovr.asImage();
img.palette = new Uint16Array([_g.theme.fg,_g.theme.bg]);
img.palette = new Uint16Array([g.theme.fg,g.theme.bg]);
}
Bangle.setLCDOverlay(img, ovrx, ovry);
};
let manageEvent = function(ovr, event) {
const manageEvent = function(event) {
event.new = true;
LOG("manageEvent");
if (event.id == "call") {
showCall(ovr, event);
showCall(event);
return;
}
switch (event.t) {
@ -51,7 +71,7 @@ let manageEvent = function(ovr, event) {
eventQueue.unshift(event);
if (!callInProgress)
showMessage(ovr, event);
showMessage(event);
break;
case "modify": {
@ -66,15 +86,15 @@ let manageEvent = function(ovr, event) {
eventQueue.unshift(event);
if (!callInProgress)
showMessage(ovr, event);
showMessage(event);
break;
}
case "remove":
if (eventQueue.length == 0 && !callInProgress)
next(ovr);
next();
if (!callInProgress && eventQueue[0] !== undefined && eventQueue[0].id == event.id)
next(ovr);
next();
else
eventQueue = [];
@ -82,7 +102,7 @@ let manageEvent = function(ovr, event) {
}
};
let roundedRect = function(ovr, x,y,w,h,filled){
const roundedRect = function(x,y,w,h,filled){
var poly = [
x,y+4,
x+4,y,
@ -94,115 +114,153 @@ let roundedRect = function(ovr, x,y,w,h,filled){
x,y+h-5,
x,y+4
];
if (filled){
let c = ovr.getColor();
ovr.setColor(ovr.getBgColor());
ovr.fillPoly(poly,true);
ovr.setColor(c);
}
ovr.drawPoly(poly,true);
if (filled) ovr.fillPoly(poly,true);
};
let drawScreen = function(ovr, title, titleFont, src, iconcolor, icon){
ovr.setBgColor(ovr.theme.bg2);
ovr.clearRect(2,2,ovr.getWidth()-3,37);
const DIVIDER = 38;
const drawScreen = function(title, src, iconcolor, icon){
setColors(false);
drawBorder();
setColors(true);
ovr.clearRect(2,2,ovr.getWidth()-3, DIVIDER - 1);
ovr.setColor(ovr.theme.fg2);
ovr.setFont(settings.fontSmall);
ovr.setFontAlign(0,-1);
let textCenter = (ovr.getWidth()+35-26)/2;
const textCenter = (ovr.getWidth()+34-24)/2-1;
if (src) {
let shortened = src;
while (ovr.stringWidth(shortened) > ovr.getWidth()-80) shortened = shortened.substring(0,shortened.length-2);
if (shortened.length != src.length) shortened += "...";
ovr.drawString(shortened, textCenter, 2);
const w = ovr.getWidth() - 35 - 26;
if (title)
drawTitle(title, textCenter, w, 8, DIVIDER - 8, 0);
if (src)
drawSource(src, textCenter, w, 2, -1);
if (ovr.getBPP() > 1) {
let old = ovr.getBgColor();
ovr.setBgColor("#888");
roundedRect(4, 5, 30, 30,true);
ovr.setBgColor(old);
old = ovr.getColor();
ovr.setColor(iconcolor);
ovr.drawImage(icon,7,8);
ovr.setColor(old);
} else {
roundedRect(4, 5, 30, 30,true);
ovr.drawImage(icon,7,8);
}
roundedRect(ovr.getWidth()-26,5,22,30,true);
ovr.setFontAlign(0,0);
ovr.setFont(titleFont);
if (title) ovr.drawString(title, textCenter, 38/2 + 5);
ovr.setColor(ovr.theme.fg2);
ovr.setFont(settings.fontMedium);
roundedRect(ovr, ovr.getWidth()-26,5,22,30,false);
ovr.setFont("Vector:16");
ovr.drawString("X",ovr.getWidth()-14,21);
ovr.setColor("#888");
roundedRect(ovr, 5,5,30,30,true);
ovr.setColor(ovr.getBPP() != 1 ? iconcolor : ovr.theme.bg2);
ovr.drawImage(icon,8,8);
ovr.drawString("X",ovr.getWidth()-14,20);
};
let showMessage = function(ovr, msg) {
LOG("showMessage");
ovr.setBgColor(ovr.theme.bg);
const drawSource = function(src, center, w, y, align) {
ovr.setFont(settings.fontSmall);
while (ovr.stringWidth(src) > w) src = src.substring(0,src.length-2);
if (src.length != src.length) src += "...";
ovr.setFontAlign(0,align);
ovr.drawString(src, center, y);
};
if (typeof msg.CanscrollDown === "undefined")
msg.CanscrollDown = false;
if (typeof msg.CanscrollUp === "undefined")
msg.CanscrollUp = false;
const drawTitle = function(title, center, w, y, h) {
let size = 30;
// Normal text message display
let title = msg.title,
titleFont = settings.fontLarge,
lines;
if (title) {
let w = ovr.getWidth() - 35 - 26;
if (ovr.setFont(titleFont).stringWidth(title) > w)
titleFont = settings.fontMedium;
if (ovr.setFont(titleFont).stringWidth(title) > w) {
lines = ovr.wrapString(title, w);
title = (lines.length > 2) ? lines.slice(0, 2).join("\n") + "..." : lines.join("\n");
while (ovr.setFont("Vector:" + size).stringWidth(title) > w){
size -= 2;
if (size < 14){
ovr.setFont(settings.fontMedium);
break;
}
}
drawScreen(ovr, title, titleFont, msg.src || /*LANG*/ "Message", require("messageicons").getColor(msg), require("messageicons").getImage(msg));
let dh;
let a;
if (ovr.stringWidth(title) > w) {
let ws = ovr.wrapString(title, w);
if (ws.length >= 2 && ovr.stringWidth(ws[1]) > w - 8){
ws[1] = ws[1].substring(0, ws[1].length - 2);
ws[1] += "...";
}
title = ws.slice(0, 2).join("\n");
a = -1;
dh = y + 2;
} else {
a = 0;
dh = y + h/2;
}
ovr.setFontAlign(0, a);
ovr.drawString(title, center, dh);
};
const setColors = function(lockRelevant) {
if (lockRelevant && !Bangle.isLocked()){
ovr.setColor(ovr.theme.fg2);
ovr.setBgColor(ovr.theme.bg2);
} else {
ovr.setColor(ovr.theme.fg);
ovr.setBgColor(ovr.theme.bg);
}
};
const showMessage = function(msg) {
LOG("showMessage");
ovr.setClipRect(0,0,ovr.getWidth(),ovr.getHeight());
drawScreen(msg.title, msg.src || /*LANG*/ "Message", require("messageicons").getColor(msg), require("messageicons").getImage(msg));
if (!Bangle.isLocked()){
ovr.setColor(ovr.theme.fg);
ovr.setBgColor(ovr.theme.bg);
}
drawMessage(msg);
if (!isQuiet() && msg.new) {
msg.new = false;
Bangle.buzz();
if (!buzzing){
buzzing = true;
Bangle.buzz().then(()=>{setTimeout(()=>{buzzing = false;},2000);});
}
Bangle.setLCDPower(1);
}
drawMessage(ovr, msg);
};
let drawBorder = function(img) {
const drawBorder = function() {
LOG("drawBorder", isQuiet());
if (img) ovr=img;
if (Bangle.isLocked())
ovr.setColor(ovr.theme.fgH);
else
ovr.setColor(ovr.theme.fg);
ovr.drawRect(0,0,ovr.getWidth()-1,ovr.getHeight()-1);
ovr.drawRect(1,1,ovr.getWidth()-2,ovr.getHeight()-2);
show(ovr);
ovr.drawRect(2,DIVIDER,ovr.getWidth()-2,DIVIDER+1);
show();
};
let showCall = function(ovr, msg) {
const showCall = function(msg) {
LOG("showCall");
LOG(msg);
if (msg.t == "remove") {
LOG("hide call screen");
next(ovr); //dont shift
next(); //dont shift
return;
}
callInProgress = true;
let title = msg.title,
titleFont = settings.fontLarge,
lines;
if (title) {
let w = ovr.getWidth() - 35 -26;
if (ovr.setFont(titleFont).stringWidth(title) > w)
titleFont = settings.fontMedium;
if (ovr.setFont(titleFont).stringWidth(title) > w) {
lines = ovr.wrapString(title, w);
title = (lines.length > 2) ? lines.slice(0, 2).join("\n") + "..." : lines.join("\n");
}
}
drawScreen(ovr, title, titleFont, msg.src || /*LANG*/ "Message", require("messageicons").getColor(msg), require("messageicons").getImage(msg));
drawScreen(msg.title, msg.src || /*LANG*/ "Message", require("messageicons").getColor(msg), require("messageicons").getImage(msg));
stopCallBuzz();
if (!isQuiet()) {
@ -216,10 +274,10 @@ let showCall = function(ovr, msg) {
Bangle.buzz(500);
}
}
drawMessage(ovr, msg);
drawMessage(msg);
};
let next = function(ovr) {
const next = function() {
LOG("next");
stopCallBuzz();
@ -230,203 +288,378 @@ let next = function(ovr) {
if (eventQueue.length == 0) {
LOG("no element in queue - closing");
cleanup();
return;
return false;
}
showMessage(ovr, eventQueue[0]);
showMessage(eventQueue[0]);
return true;
};
let callBuzzTimer = null;
let stopCallBuzz = function() {
const stopCallBuzz = function() {
if (callBuzzTimer) {
clearInterval(callBuzzTimer);
callBuzzTimer = undefined;
}
};
let drawTriangleUp = function(ovr) {
ovr.reset();
ovr.fillPoly([ovr.getWidth()-9, 46,ovr.getWidth()-14, 56,ovr.getWidth()-4, 56]);
const drawTriangleUp = function() {
ovr.fillPoly([ovr.getWidth()-10, 46,ovr.getWidth()-15, 56,ovr.getWidth()-5, 56]);
};
let drawTriangleDown = function(ovr) {
ovr.reset();
ovr.fillPoly([ovr.getWidth()-9, ovr.getHeight()-6, ovr.getWidth()-14, ovr.getHeight()-16, ovr.getWidth()-4, ovr.getHeight()-16]);
const drawTriangleDown = function() {
ovr.fillPoly([ovr.getWidth()-10, ovr.getHeight()-6, ovr.getWidth()-15, ovr.getHeight()-16, ovr.getWidth()-5, ovr.getHeight()-16]);
};
let linesScroll = 6;
let scrollUp = function(ovr) {
msg = eventQueue[0];
const scrollUp = function() {
const msg = eventQueue[0];
LOG("up", msg);
if (typeof msg.FirstLine === "undefined")
msg.FirstLine = 0;
if (typeof msg.CanscrollUp === "undefined")
msg.CanscrollUp = false;
if (!msg.CanscrollUp) return;
msg.FirstLine = msg.FirstLine > 0 ? msg.FirstLine - linesScroll : 0;
drawMessage(ovr, msg);
msg.FirstLine = msg.FirstLine > 0 ? msg.FirstLine - 1 : 0;
drawMessage(msg);
};
let scrollDown = function(ovr) {
msg = eventQueue[0];
const scrollDown = function() {
const msg = eventQueue[0];
LOG("down", msg);
if (typeof msg.FirstLine === "undefined")
msg.FirstLine = 0;
if (typeof msg.CanscrollDown === "undefined")
msg.CanscrollDown = false;
if (!msg.CanscrollDown) return;
msg.FirstLine = msg.FirstLine + linesScroll;
drawMessage(ovr, msg);
msg.FirstLine = msg.FirstLine + 1;
drawMessage(msg);
};
let drawMessage = function(ovr, msg) {
let MyWrapString = function(str, maxWidth) {
const drawMessage = function(msg) {
setColors(false);
const getStringHeight = function(str){
"jit";
const metrics = ovr.stringMetrics(str);
if (needsWorkaround === undefined)
needsWorkaround = isNewer("2v21.13", process.version);
if (needsWorkaround && metrics.maxImageHeight > 16)
metrics.maxImageHeight = metrics.height;
return Math.max(metrics.height, metrics.maxImageHeight);
};
const wrapString = function(str, maxWidth) {
str = str.replace("\r\n", "\n").replace("\r", "\n");
return ovr.wrapString(str, maxWidth);
};
const wrappedStringHeight = function(strArray){
let r = 0;
strArray.forEach((line, i) => {
r += getStringHeight(line);
});
return r;
};
if (typeof msg.FirstLine === "undefined") msg.FirstLine = 0;
if (msg.FirstLine === undefined) msg.FirstLine = 0;
let bodyFont = typeof msg.bodyFont === "undefined" ? settings.fontMedium : msg.bodyFont;
let Padding = 3;
if (typeof msg.lines === "undefined") {
const padding = eventQueue.length > 1 ? (eventQueue.length > 3 ? 7 : 5) : 3;
const yText = DIVIDER+2;
let yLine = yText + 4;
ovr.setClipRect(2, yText, ovr.getWidth() - 3, ovr.getHeight() - 3);
const maxTextHeight = ovr.getHeight() - yLine - padding + 2;
if (!msg.lines) {
let bodyFont = settings.fontBig;
ovr.setFont(bodyFont);
msg.lines = MyWrapString(msg.body, ovr.getWidth() - (Padding * 2));
if (msg.lines.length <= 2) {
bodyFont = ovr.getFonts().includes("Vector") ? "Vector:20" : "6x8:3";
msg.lines = wrapString(msg.body, ovr.getWidth() - 4 - padding);
if (wrappedStringHeight(msg.lines) > maxTextHeight) {
bodyFont = settings.fontMedium;
ovr.setFont(bodyFont);
msg.lines = MyWrapString(msg.body, ovr.getWidth() - (Padding * 2));
msg.bodyFont = bodyFont;
msg.lines = wrapString(msg.body, ovr.getWidth() - 4 - padding);
}
msg.bodyFont = bodyFont;
msg.lineHeights = [];
msg.lines.forEach((line, i) => {
msg.lineHeights[i] = getStringHeight(line);
});
}
let NumLines = 7;
LOG("Prepared message", msg);
let linesToPrint = (msg.lines.length > NumLines) ? msg.lines.slice(msg.FirstLine, msg.FirstLine + NumLines) : msg.lines;
ovr.setFont(msg.bodyFont);
ovr.clearRect(2, yText, ovr.getWidth()-3, ovr.getHeight()-3);
let yText = 40;
let xText = 4;
ovr.setBgColor(ovr.theme.bg);
ovr.setColor(ovr.theme.fg);
ovr.clearRect(2, yText, ovrw-3, ovrh-3);
let xText = Padding;
yText += Padding;
ovr.setFont(bodyFont);
let HText = ovr.getFontHeight();
yText = ((ovrh - yText) / 2) - (linesToPrint.length * HText / 2) + yText;
if (linesToPrint.length <= 3) {
if (msg.bodyFont == settings.fontBig) {
ovr.setFontAlign(0, -1);
xText = ovr.getWidth() / 2;
} else
xText = Math.round(ovr.getWidth() / 2 - (padding - 3) / 2) + 1;
yLine = (ovr.getHeight() + yLine) / 2 - (wrappedStringHeight(msg.lines) / 2);
ovr.drawString(msg.lines.join("\n"), xText, yLine);
} else {
ovr.setFontAlign(-1, -1);
}
let currentLine = msg.FirstLine;
linesToPrint.forEach((line, i) => {
ovr.drawString(line, xText, yText + HText * i);
});
let drawnHeight = 0;
while(drawnHeight < maxTextHeight && msg.lines.length > currentLine) {
const lineHeight = msg.lineHeights[currentLine];
ovr.drawString(msg.lines[currentLine], xText, yLine + drawnHeight);
drawnHeight += lineHeight;
currentLine++;
}
if (eventQueue.length > 1){
ovr.drawLine(ovr.getWidth()-4,ovr.getHeight()/2,ovr.getWidth()-4,ovr.getHeight()-4);
ovr.drawLine(ovr.getWidth()/2,ovr.getHeight()-4,ovr.getWidth()-4,ovr.getHeight()-4);
}
if (eventQueue.length > 3){
ovr.drawLine(ovr.getWidth()-6,ovr.getHeight()*0.6,ovr.getWidth()-6,ovr.getHeight()-6);
ovr.drawLine(ovr.getWidth()*0.6,ovr.getHeight()-6,ovr.getWidth()-6,ovr.getHeight()-6);
}
if (msg.FirstLine != 0) {
msg.CanscrollUp = true;
drawTriangleUp(ovr);
drawTriangleUp();
} else
msg.CanscrollUp = false;
if (msg.FirstLine + linesToPrint.length < msg.lines.length) {
if (currentLine < msg.lines.length) {
msg.CanscrollDown = true;
drawTriangleDown(ovr);
drawTriangleDown();
} else
msg.CanscrollDown = false;
show(ovr);
if (!isQuiet()) Bangle.setLCDPower(1);
show();
};
let getSwipeHandler = function(ovr){
return (lr, ud) => {
if (ud == 1) {
scrollUp(ovr);
} else if (ud == -1){
scrollDown(ovr);
const getDragHandler = function(){
return (e) => {
if (e.dy > 0) {
scrollUp();
} else if (e.dy < 0){
scrollDown();
}
};
};
let getTouchHandler = function(ovr){
const getTouchHandler = function(){
return (_, xy) => {
if (xy.y < ovry + 40){
next(ovr);
if (xy.y < ovry + DIVIDER){
next();
}
};
};
let restoreHandler = function(event){
LOG("Restore", event, backup[event]);
Bangle.removeAllListeners(event);
Bangle["#on" + event]=backup[event];
backup[event] = undefined;
const EVENTS=["touch", "drag", "swipe"];
let hasBackup = false;
const origOn = Bangle.on;
const backupOn = function(event, handler){
if (EVENTS.includes(event)){
if (!backup[event])
backup[event] = [];
backup[event].push(handler);
}
else origOn.call(Bangle, event, handler);
};
let backupHandler = function(event){
if (backupDone) return; // do not backup, overlay is already up
backup[event] = Bangle["#on" + event];
LOG("Backed up", backup[event]);
Bangle.removeAllListeners(event);
const origClearWatch = clearWatch;
const backupClearWatch = function(w) {
if (w)
backup.watches.filter((e)=>e.index != w);
else
backup.watches = [];
};
let cleanup = function(){
const origSetWatch = setWatch;
const backupSetWatch = function(){
if (!backup.watches)
backup.watches = [];
LOG("backup for watch", arguments);
let i = backup.watches.map((e)=>e.index).sort().pop() + 1;
backup.watches.push({index:i, args:arguments});
return i;
};
const origRemove = Bangle.removeListener;
const backupRemove = function(event, handler){
if (EVENTS.includes(event) && backup[event]){
LOG("backup for " + event + ": " + backup[event]);
backup[event] = backup[event].filter(e=>e!==handler);
}
else origRemove.call(Bangle, event, handler);
};
const origRemoveAll = Bangle.removeAllListeners;
const backupRemoveAll = function(event){
if (backup[event])
backup[event] = undefined;
origRemoveAll.call(Bangle);
};
const restoreHandlers = function(){
if (!hasBackup){
LOG("No backup available");
return;
}
for (const event of EVENTS){
LOG("Restore", backup[event]);
origRemoveAll.call(Bangle, event);
if (backup[event] && backup[event].length == 1)
backup[event] = backup[event][0];
Bangle["#on" + event]=backup[event];
backup[event] = undefined;
}
if (backup.watches){
let toRemove = [];
origClearWatch.call(global);
for(let i = 0; i < backup.watches.length; i++){
let w = backup.watches[i];
LOG("Restoring watch", w);
if (w) {
origSetWatch.apply(global, w);
} else {
toRemove.push(i+1);
origSetWatch.call(global, ()=>{}, BTN);
}
}
LOG("Remove watches", toRemove, global["\xff"].watches);
for(let c of toRemove){
origClearWatch.call(global, c);
}
}
global.setWatch = origSetWatch;
global.clearWatch = origClearWatch;
Bangle.on = origOn;
Bangle.removeListener = origRemove;
Bangle.removeAllListeners = origRemoveAll;
hasBackup = false;
};
const backupHandlers = function(){
if (hasBackup){
LOG("Backup already exists");
return false; // do not backup, overlay is already up
}
for (const event of EVENTS){
backup[event] = Bangle["#on" + event];
if (typeof backup[event] == "function")
backup[event] = [ backup[event] ];
LOG("Backed up", backup[event], event);
Bangle.removeAllListeners(event);
}
backup.watches = [];
for (let i = 1; i < global["\xff"].watches.length; i++){
let w = global["\xff"].watches[i];
LOG("Transform watch", w);
if (w) {
w = [
w.callback,
w.pin,
w
];
delete w[2].callback;
delete w[2].pin;
w[2].debounce = Math.round(w[2].debounce / 1048.576);
} else {
w = null;
}
LOG("Transformed to", w);
backup.watches.push(w);
}
LOG("Backed up watches", backup.watches);
clearWatch();
global.setWatch = backupSetWatch;
global.clearWatch = backupClearWatch;
Bangle.on = backupOn;
Bangle.removeListener = backupRemove;
Bangle.removeAllListeners = backupRemoveAll;
hasBackup = true;
return true;
};
const cleanup = function(){
if (lockListener) {
Bangle.removeListener("lock", lockListener);
lockListener = undefined;
}
restoreHandler("touch");
restoreHandler("swipe");
restoreHandler("drag");
restoreHandlers();
Bangle.setLCDOverlay();
backupDone = false;
ovr = undefined;
quiet = undefined;
};
let backup = {};
const backup = {};
let backupDone = false;
let main = function(ovr, event) {
LOG("Main", event, settings);
const main = function(event) {
LOG("Main", event.t);
const didBackup = backupHandlers();
if (!lockListener) {
lockListener = function (){
drawBorder();
lockListener = function (e){
updateClearingTimeout();
showMessage(eventQueue[0]);
};
Bangle.on('lock', lockListener);
LOG("Add overlay lock handlers");
origOn.call(Bangle, 'lock', lockListener);
}
backupHandler("touch");
backupHandler("swipe");
backupHandler("drag");
if (!backupDone){
Bangle.on('touch', getTouchHandler(ovr));
Bangle.on('swipe', getSwipeHandler(ovr));
if (didBackup){
LOG("Add overlay UI handlers");
origOn.call(Bangle, 'touch', getTouchHandler(ovr));
origOn.call(Bangle, 'drag', getDragHandler(ovr));
}
backupDone=true;
if (event !== undefined){
drawBorder(ovr);
manageEvent(ovr, event);
manageEvent(event);
} else {
LOG("No event given");
cleanup();
}
};
let ovr;
const updateClearingTimeout = ()=>{
LOG("updateClearingTimeout");
if (settings.autoclear <= 0)
return;
LOG("Remove clearing timeout", clearingTimeout);
if (clearingTimeout) clearTimeout(clearingTimeout);
if (Bangle.isLocked()){
LOG("Set new clearing timeout");
clearingTimeout = setTimeout(()=>{
LOG("setNewTimeout");
const event = eventQueue.pop();
if (event)
showMessage(event);
if (eventQueue.length > 0){
LOG("still got elements");
updateClearingTimeout();
} else {
cleanup();
}
}, settings.autoclear * 1000);
} else {
clearingTimeout = undefined;
}
};
exports.message = function(type, event) {
LOG("Got message", type, event);
@ -434,13 +667,40 @@ exports.message = function(type, event) {
if(!(type=="text" || type == "call")) return;
if(type=="text" && event.id == "nav") return;
if(event.handled) return;
if(event.messagesoverlayignore) return;
bpp = 4;
if (process.memory().free < LOW_MEM)
let free = process.memory().free;
let bpp = settings.systemTheme ? 16 : 4;
let estimatedMemUse = bpp == 16 ? 4096 : (bpp == 4 ? 1536 : 768);
// reduce estimation if ovr already exists and uses memory;
if (ovr)
estimatedMemUse -= ovr.getBPP() == 16 ? 4096 : (ovr.getBPP() == 4 ? 1536 : 768);
if (process.memory().free - estimatedMemUse < settings.minfreemem * 1024) {
// we are going to be under our minfreemem setting if we proceed
bpp = 1;
if (ovr && ovr.getBPP() > 1){
// can reduce memory by going 1 bit
let saves = ovr.getBPP() == 16 ? 4096 - 768 : 768;
estimatedMemUse -= saves;
LOG("Go to 1 bit, saving", saves);
} else {
estimatedMemUse = 768;
}
}
while (process.memory().free < MIN_FREE_MEM && eventQueue.length > 0){
let dropped = eventQueue.pop();
if (E.getSizeOf){
let e = E.getSizeOf(eventQueue);
estimatedMemUse += e;
LOG("EventQueue has", e, "blocks");
}
LOG("Free ", free, "estimated use", estimatedMemUse, "for", bpp, "BPP");
while (process.memory().free - estimatedMemUse < settings.minfreemem * 1024 && eventQueue.length > 0){
const dropped = eventQueue.pop();
print("Dropped message because of memory constraints", dropped);
}
@ -448,19 +708,37 @@ exports.message = function(type, event) {
ovr = Graphics.createArrayBuffer(ovrw, ovrh, bpp, {
msb: true
});
} else {
ovr.clear();
if(E.getSizeOf)
LOG("New overlay uses", E.getSizeOf(ovr), "blocks");
}
g = ovr;
ovr.reset();
if (bpp == 4)
ovr.theme = g.theme;
else
ovr.theme = { fg:0, bg:1, fg2:1, bg2:0, fgH:1, bgH:0 };
if (bpp > 1){
if (settings.systemTheme){
ovr.theme = g.theme;
} else {
ovr.theme = {
fg: g.theme.dark ? 15: 0,
bg: g.theme.dark ? 0: 15,
fg2: g.theme.dark ? 15: 0,
bg2: g.theme.dark ? 9 : 8,
fgH: g.theme.dark ? 15 : 0,
bgH: g.theme.dark ? 9: 8,
};
}
}
else {
if (g.theme.dark)
ovr.theme = { fg:1, bg:0, fg2:0, bg2:1, fgH:0, bgH:1 };
else
ovr.theme = { fg:0, bg:1, fg2:1, bg2:0, fgH:1, bgH:0 };
}
main(event);
updateClearingTimeout();
main(ovr, event);
if (!isQuiet()) Bangle.setLCDPower(1);
event.handled = true;
g = _g;
g.flip();
};

View File

@ -1,7 +1,7 @@
{
"id": "messagesoverlay",
"name": "Messages Overlay",
"version": "0.06",
"version": "0.08",
"description": "An overlay based implementation of a messages UI (display notifications from iOS and Gadgetbridge/Android)",
"icon": "app.png",
"type": "bootloader",
@ -11,7 +11,10 @@
"readme": "README.md",
"storage": [
{"name":"messagesoverlay","url":"lib.js"},
{"name":"messagesoverlay.0.boot.js","url":"boot.js"}
{"name":"messagesoverlay.0.boot.js","url":"boot.js"},
{"name":"messagesoverlay.settings.js","url":"settings.js"},
{"name":"messagesoverlay.default.json","url":"default.json"}
],
"data": [{"name":"bthrm.json"}],
"screenshots": [{"url":"screen_call.png"} ,{"url":"screen_message.png"} ]
}

View File

@ -0,0 +1,66 @@
(function(back) {
function writeSettings(key, value) {
var s = require('Storage').readJSON(FILE, true) || {};
s[key] = value;
require('Storage').writeJSON(FILE, s);
readSettings();
}
function readSettings(){
settings = Object.assign(
require('Storage').readJSON("messagesoverlay.default.json", true) || {},
require('Storage').readJSON(FILE, true) || {}
);
}
var FILE="messagesoverlay.json";
var settings;
readSettings();
function buildMainMenu(){
var mainmenu = {
'' : { title: "Messages Overlay"},
'< Back': back,
'Border': {
value: settings.border,
min: 0,
max: Math.floor(g.getWidth()/2-50),
step: 1,
format: v=>v + "px",
onchange: v => {
writeSettings("border",v);
}
},
'Autoclear after': {
value: settings.autoclear,
min: 0,
max: 3600,
step: 10,
format: v=>v>0?v+"s":"Off",
onchange: v => {
writeSettings("autoclear",v);
}
},
'Theme': {
value: settings.systemTheme,
format: v=>v?"System":"low RAM",
onchange: v => {
writeSettings("systemTheme",v);
}
},
'Min. free RAM': {
value: settings.minfreemem,
min: 0,
max: process.memory().total/1000,
step: 1,
format: v=>v + "k free",
onchange: v => {
writeSettings("minfreemem",v);
}
}
};
return mainmenu;
}
E.showMenu(buildMainMenu());
});

View File

@ -1,2 +1,3 @@
0.01: Initial release
0.02: Cache the app-launch info
0.03: Fix bugs that would make the launcher unusable on most watches

View File

@ -5,13 +5,13 @@ var font = g.getFonts().includes("6x15") ? "6x15" : "6x8:2";
var largeFont = g.getFonts().includes("12x20") ? "12x20" : "6x8:3";
var currentApp = 0;
var overscroll = 0;
var blankImage = Graphics.createImage(` `);
var blankImage = Graphics.createImage(`\n \n`);
var rowHeight = g.getHeight()/3;
// Load apps list
var apps;
var launchCache = s.readJSON("launch.cache.json", true)||{};
var launchCache = Storage.readJSON("launch.cache.json", true)||{};
var launchHash = require("Storage").hash(/\.info/);
if (launchCache.hash==launchHash) {
apps = launchCache.apps;
@ -39,7 +39,7 @@ if (launchCache.hash==launchHash) {
});
launchCache = { apps, hash: launchHash };
s.writeJSON("launch.cache.json", launchCache);
Storage.writeJSON("launch.cache.json", launchCache);
}
// Uncomment for testing in the emulator without apps:

View File

@ -2,7 +2,7 @@
"id": "ratchet_launch",
"name": "Ratchet Launcher",
"shortName": "Ratchet",
"version": "0.02",
"version": "0.03",
"description": "Launcher with discrete scrolling for quicker app selection",
"icon": "app.png",
"type": "launch",

View File

@ -52,3 +52,4 @@
0.41: Fix exit from plots and graphs would easily react twice, going back two
levels instead of one.
0.42: Minor code improvements
0.43: Fix interaction on clocks without widgets

View File

@ -11,7 +11,7 @@
{
name: "Toggle",
get: () => {
const w = WIDGETS && WIDGETS["recorder"];
const w = typeof WIDGETS !== "undefined" && WIDGETS["recorder"];
return w && w.isRecording() ? {
text: "Recording",
@ -24,7 +24,7 @@
};
},
run: () => {
const w = WIDGETS && WIDGETS["recorder"];
const w = typeof WIDGETS !== "undefined" && WIDGETS["recorder"];
if(w){
Bangle.buzz();
w.setRecording(!w.isRecording(), { force: "append" });

View File

@ -2,7 +2,7 @@
"id": "recorder",
"name": "Recorder",
"shortName": "Recorder",
"version": "0.42",
"version": "0.43",
"description": "Record GPS position, heart rate and more in the background, then download to your PC.",
"icon": "app.png",
"tags": "tool,outdoors,gps,widget,clkinfo",

View File

@ -6,3 +6,4 @@
0.60: Fixes typos, BTN1 to show launcher and show app icon
0.61: Minor code improvements
0.70: Better wrapping of the text base (dynamic instead of hardcoded)
0.80: Add analog watch, steps and date

View File

@ -1,7 +1,7 @@
{ "id": "rellotge",
"name": "Rellotge en catala",
"shortName":"Rellotge",
"version": "0.70",
"version": "0.80",
"description": "A clock with traditional naming of hours in Catalan",
"icon": "icona.png",
"readme": "README.md",

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