Merge branch 'espruino:master' into pushups
|
@ -112,6 +112,7 @@ module.exports = {
|
|||
"getSerial": "readonly",
|
||||
"getTime": "readonly",
|
||||
"global": "readonly",
|
||||
"globalThis": "readonly",
|
||||
"HIGH": "readonly",
|
||||
"I2C1": "readonly",
|
||||
"Infinity": "readonly",
|
||||
|
|
|
@ -289,6 +289,7 @@ and which gives information about the app for the Launcher.
|
|||
"dependencies" : { "message":"widget" } // optional, depend on a specific type of widget - see provides_widgets
|
||||
"provides_modules" : ["messageicons"] // optional, this app provides a module that can be used with 'require'
|
||||
"provides_widgets" : ["battery"] // optional, this app provides a type of widget - 'alarm/battery/bluetooth/pedometer/message'
|
||||
"provides_features" : ["welcome"] // optional, this app provides some feature, used to ensure two aren't installed at once. Currently just 'welcome'
|
||||
"default" : true, // set if an app is the default implementer of something (a widget/module/etc)
|
||||
"readme": "README.md", // if supplied, a link to a markdown-style text file
|
||||
// that contains more information about this app (usage, etc)
|
||||
|
|
16
android.html
|
@ -187,14 +187,18 @@
|
|||
</details>
|
||||
</div>
|
||||
<div id="more-deviceinfo" style="display:none">
|
||||
<h3>Device info</h3>
|
||||
<div id="more-deviceinfo-content"></div>
|
||||
<h3>Device info</h3>
|
||||
<div id="more-deviceinfo-content"></div>
|
||||
<div class="editor--terminal">
|
||||
<div class="editor__canvas" style="position:relative;height:20rem;display:none;"></div>
|
||||
<button class="btn" id="terminalEnable">Enable Terminal</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="floating hidden">
|
||||
<!-- Install button, hidden by default -->
|
||||
<!-- PWA Install button, hidden by default -->
|
||||
<div id="installContainer" class="hidden">
|
||||
<button id="butInstall" type="button">
|
||||
Install
|
||||
|
@ -203,7 +207,7 @@
|
|||
</footer>
|
||||
|
||||
<script src="webtools/puck.js"></script>
|
||||
<script src="webtools/heatshrink.js"></script>
|
||||
<script src="webtools/heatshrink.js"></script>
|
||||
<script src="core/lib/marked.min.js"></script>
|
||||
<script src="core/lib/espruinotools.js"></script>
|
||||
<script src="core/js/utils.js"></script>
|
||||
|
@ -416,7 +420,7 @@ if (el) el.addEventListener("click", event=>{
|
|||
if (webrtc) showWebRTCID(webrtc.peerId);
|
||||
else {
|
||||
webrtc = webrtcInit({
|
||||
bridge:true,
|
||||
bridge:true,
|
||||
onStatus : function(s) {
|
||||
showToast(s);
|
||||
},
|
||||
|
@ -432,7 +436,7 @@ if (el) el.addEventListener("click", event=>{
|
|||
onPortDisconnect : function(serialPort) {
|
||||
},
|
||||
onPortWrite : function(data, cb) {
|
||||
Puck.write(data, cb);
|
||||
Puck.write(data, cb);
|
||||
}
|
||||
});
|
||||
connection.on("data", function(d) {
|
||||
|
|
|
@ -24,10 +24,10 @@
|
|||
var dateStr = require("locale").date(date);
|
||||
// draw time
|
||||
g.setFontAlign(0,0).setFont("Vector",48);
|
||||
g.clearRect(0,y-15,g.getWidth(),y+25); // clear the background
|
||||
g.clearRect(0,y-20,g.getWidth(),y+25); // clear the background
|
||||
g.drawString(timeStr,x,y);
|
||||
// draw date
|
||||
y += 35;
|
||||
y += 30;
|
||||
g.setFontAlign(0,0).setFont("6x8");
|
||||
g.clearRect(0,y-4,g.getWidth(),y+4); // clear the background
|
||||
g.drawString(dateStr,x,y);
|
||||
|
@ -41,6 +41,8 @@
|
|||
// Show launcher when middle button pressed
|
||||
Bangle.setUI({mode:"clock", remove:function() {
|
||||
// free any memory we allocated to allow fast loading
|
||||
if (drawTimeout) clearTimeout(drawTimeout);
|
||||
drawTimeout = undefined;
|
||||
}});
|
||||
// Load widgets
|
||||
Bangle.loadWidgets();
|
||||
|
|
|
@ -29,5 +29,4 @@
|
|||
}
|
||||
|
||||
E.showMenu(buildMainMenu());
|
||||
});
|
||||
|
||||
})
|
||||
|
|
|
@ -109,4 +109,4 @@
|
|||
},
|
||||
};
|
||||
E.showMenu(menu);
|
||||
});
|
||||
})
|
||||
|
|
|
@ -68,4 +68,4 @@ function buildMainMenu() {
|
|||
}
|
||||
|
||||
E.showMenu(buildMainMenu());
|
||||
});
|
||||
})
|
||||
|
|
|
@ -51,3 +51,7 @@
|
|||
0.46: Show alarm groups if the Show Group setting is ON. Scroll alarms menu back to previous position when getting back to it.
|
||||
0.47: Fix wrap around when snoozed through midnight
|
||||
0.48: Use datetimeinput for Events, if available. Scroll back when getting out of group. Menu date format setting for shorter dates on current year.
|
||||
0.49: fix uncaught error if no scroller (Bangle 1). Would happen when trying
|
||||
to select an alarm in the main menu.
|
||||
0.50: Bangle.js 2: Long touch of alarm in main menu toggle it on/off. Touching the icon on
|
||||
the right will do the same.
|
||||
|
|
|
@ -20,6 +20,8 @@ It uses the [`sched` library](https://github.com/espruino/BangleApps/blob/master
|
|||
- `Disable All` → Disable _all_ enabled alarms & timers
|
||||
- `Delete All` → Delete _all_ alarms & timers
|
||||
|
||||
On Bangle.js 2 it's possible to toggle alarms, timers and events from the main menu. This is done by clicking the indicator icons of corresponding entries. Or long pressing anywhere on them.
|
||||
|
||||
## Creator
|
||||
|
||||
- [Gordon Williams](https://github.com/gfwilliams)
|
||||
|
@ -29,6 +31,7 @@ It uses the [`sched` library](https://github.com/espruino/BangleApps/blob/master
|
|||
- [Alessandro Cocco](https://github.com/alessandrococco) - New UI, full rewrite, new features
|
||||
- [Sabin Iacob](https://github.com/m0n5t3r) - Auto snooze support
|
||||
- [storm64](https://github.com/storm64) - Fix redrawing in submenus
|
||||
- [thyttan](https://github.com/thyttan) - Toggle alarms directly from main menu.
|
||||
|
||||
## Attributions
|
||||
|
||||
|
|
|
@ -88,13 +88,23 @@ function showMainMenu(scroll, group, scrollback) {
|
|||
const getGroups = settings.showGroup && !group;
|
||||
const groups = getGroups ? {} : undefined;
|
||||
var showAlarm;
|
||||
const getIcon = (e)=>{return e.on ? (e.timer ? iconTimerOn : iconAlarmOn) : (e.timer ? iconTimerOff : iconAlarmOff);};
|
||||
|
||||
alarms.forEach((e, index) => {
|
||||
showAlarm = !settings.showGroup || (group ? e.group === group : !e.group);
|
||||
if(showAlarm) {
|
||||
menu[trimLabel(getLabel(e),40)] = {
|
||||
value: e.on ? (e.timer ? iconTimerOn : iconAlarmOn) : (e.timer ? iconTimerOff : iconAlarmOff),
|
||||
onchange: () => setTimeout(e.timer ? showEditTimerMenu : showEditAlarmMenu, 10, e, index, undefined, scroller.scroll, group)
|
||||
const label = trimLabel(getLabel(e),40);
|
||||
menu[label] = {
|
||||
value: e.on,
|
||||
onchange: (v, touch) => {
|
||||
if (touch && (2==touch.type || 145<touch.x)) { // Long touch or touched icon.
|
||||
e.on = v;
|
||||
saveAndReload();
|
||||
} else {
|
||||
setTimeout(e.timer ? showEditTimerMenu : showEditAlarmMenu, 10, e, index, undefined, scroller?scroller.scroll:undefined, group);
|
||||
}
|
||||
},
|
||||
format: v=>getIcon(e)
|
||||
};
|
||||
} else if (getGroups) {
|
||||
groups[e.group] = undefined;
|
||||
|
@ -102,7 +112,7 @@ function showMainMenu(scroll, group, scrollback) {
|
|||
});
|
||||
|
||||
if (!group) {
|
||||
Object.keys(groups).sort().forEach(g => menu[g] = () => showMainMenu(null, g, scroller.scroll));
|
||||
Object.keys(groups).sort().forEach(g => menu[g] = () => showMainMenu(null, g, scroller?scroller.scroll:undefined));
|
||||
menu[/*LANG*/"Advanced"] = () => showAdvancedMenu();
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"id": "alarm",
|
||||
"name": "Alarms & Timers",
|
||||
"shortName": "Alarms",
|
||||
"version": "0.48",
|
||||
"version": "0.50",
|
||||
"description": "Set alarms and timers on your Bangle",
|
||||
"icon": "app.png",
|
||||
"tags": "tool,alarm",
|
||||
|
|
|
@ -48,4 +48,4 @@
|
|||
};
|
||||
|
||||
E.showMenu(appMenu);
|
||||
});
|
||||
})
|
||||
|
|
|
@ -2,13 +2,26 @@ Alpine Navigator
|
|||
================
|
||||
App that performs GPS monitoring to track and display position relative to a given origin in realtime.
|
||||
|
||||

|
||||

|
||||
|
||||
[compass 5]
|
||||
|
||||
altitude
|
||||
[start 1] [current 2]
|
||||
|
||||
distance
|
||||
[from start 3] [track 4]
|
||||
|
||||
|
||||
[btn1 -- screen lock]
|
||||
[btn2 -- remove points]
|
||||
[btn3 -- pause]
|
||||
|
||||
Functions
|
||||
---------
|
||||
Note if you've not used GPS yet I suggest using one of the GPS apps to get your first fix and confirm as I've found that helps initially.
|
||||
Note if you've not used GPS yet, I suggest using one of the GPS apps to get your first fix and confirm, as I've found that helps initially.
|
||||
|
||||
The GPS and magnetometer will be turned on and after a few moments, when the watch buzzes and the dot turns from red to pink, that means the GPS is fixed. all your movements now will be displayed with a line drawn back to show your position relative to the start. New waypoints will be added based on checking every 10 seconds for at least 5 meters of movement. The map will scale to your distance travelled so the route will always remain within the window, the accelerometer/pedometer is not used - this is a purely GPS and compass solution so can be used for driving/cycling etc. A log file will be recorded that tracks upto 1000 waypoints, this isn't a big file and you could remove the limit but I've kept it fairly conservative here as it's not intended as a main feature, there's already good GPS recorders for the Bangle. The following other items are displayed:
|
||||
The GPS and magnetometer will be turned on and after a few moments, when the watch buzzes and the dot turns from red to pink, that means the GPS is fixed. All your movements now will be displayed with a line drawn back to show your position relative to the start. New waypoints will be added based on checking every 10 seconds for at least 5 meters of movement. The map will scale to your distance travelled so the route will always remain within the window, the accelerometer/pedometer is not used - this is a purely GPS and compass solution so can be used for driving/cycling etc. A log file will be recorded that tracks upto 1000 waypoints, this isn't a big file and you could remove the limit, but I've kept it fairly conservative here, as it's not intended as a main feature, there's already good GPS recorders for the Bangle. The following other items are displayed:
|
||||
|
||||
1. altitude at origin, this is displayed left of the centre.
|
||||
2. current altitude, displayed centre right
|
||||
|
@ -16,12 +29,12 @@ The GPS and magnetometer will be turned on and after a few moments, when the wat
|
|||
4. distance travelled, bottom right (meters)
|
||||
5. compass heading, at the top
|
||||
|
||||
For the display, the route is kept at a set resolution, so there's no risk of running into memory problems if you run this for long periods or any length of time because the waypoints will be reduced when it reaches a set threshold so you may see the path smooth out slightly at intervals.
|
||||
For the display, the route is kept at a set resolution, so there's no risk of running into memory problems if you run this for long periods or any length of time, because the waypoints will be reduced when it reaches a set threshold, so you may see the path smooth out slightly at intervals.
|
||||
|
||||
If you get strange values or dashes for the compass, it just needs calibration so you need to move the watch around briefly for this each time, ideally 360 degrees around itself, which involves taking the watch off. If you don't want to do that you can also just wave your hand around for a few seconds like you're at a rave or Dr Strange making a Sling Ring but often just moving your wrist a bit is enough.
|
||||
If you get strange values or dashes for the compass, it just needs calibration so you need to move the watch around briefly for this each time, ideally 360 degrees around itself, which involves taking the watch off. If you don't want to do that you can also just wave your hand around for a few seconds like you're at a rave or Dr Strange making a Sling Ring, but often just moving your wrist a bit is enough.
|
||||
|
||||
The buttons do the following:
|
||||
BTN1: this will display an 'X' in the bottom of the screen and lock all the buttons, this is to prevent you accidentally pressing either of the below. Remember to press this again to unlock it! soft and hard reset will both still work.
|
||||
BTN1: this will display an 'X' in the bottom of the screen and lock all the buttons, this is to prevent you accidentally pressing either of the below. Remember to press this again to unlock it! Soft and hard reset will both still work.
|
||||
BTN2: this removes all waypoints aside from the origin and your current location; sometimes during smaller journeys and walks, the GPS can give sporadic differences in locations because of the error margins of GPS and this can add noise to your route.
|
||||
BTN3: this will pause the GPS and magnetometer, useful for saving power for situations where you don't necessarily need to track parts of your route e.g. you're going indoors/shelter for some time. You'll know it's paused because the compass won't update it's reading and all the metrics will be blacked out on the screen.
|
||||
|
||||
|
|
|
@ -25,4 +25,4 @@
|
|||
};
|
||||
|
||||
E.showMenu(appMenu);
|
||||
});
|
||||
})
|
||||
|
|
|
@ -36,4 +36,7 @@
|
|||
0.34: Implement API for activity tracks fetching (Recorder app logs).
|
||||
0.35: Implement API to enable/disable acceleration data tracking.
|
||||
0.36: Move from wrapper function to {} and let - faster execution at boot
|
||||
Allow `calendar-` to take an array of items to remove
|
||||
Allow `calendar-` to take an array of items to remove
|
||||
0.37: Support Gadgetbridge canned responses
|
||||
0.38: Don't rewrite settings file on every boot!
|
||||
0.39: Move GB message handling into a library to reduce boot time from 40ms->13ms
|
|
@ -49,11 +49,21 @@ The boot code also provides some useful functions:
|
|||
* `body` the body of the HTTP request
|
||||
* `headers` an object of headers, eg `{HeaderOne : "headercontents"}`
|
||||
|
||||
`Bangle.http` returns a promise which contains:
|
||||
|
||||
```JS
|
||||
{
|
||||
t:"http",
|
||||
id: // the ID of this HTTP request
|
||||
resp: "...." // a string containing the response
|
||||
}
|
||||
```
|
||||
|
||||
eg:
|
||||
|
||||
```
|
||||
```JS
|
||||
Bangle.http("https://pur3.co.uk/hello.txt").then(data=>{
|
||||
console.log("Got ",data);
|
||||
console.log("Got ",data.resp);
|
||||
});
|
||||
```
|
||||
|
||||
|
|
|
@ -1,350 +1,24 @@
|
|||
/* global GB */
|
||||
{
|
||||
let gbSend = function(message) {
|
||||
Bluetooth.println("");
|
||||
Bluetooth.println(JSON.stringify(message));
|
||||
}
|
||||
let lastMsg; // for music messages - may not be needed now...
|
||||
let actInterval; // Realtime activity reporting interval when `act` is true
|
||||
let actHRMHandler; // For Realtime activity reporting
|
||||
let gpsState = {}; // keep information on GPS via Gadgetbridge
|
||||
|
||||
// this settings var is deleted after this executes to save memory
|
||||
let settings = require("Storage").readJSON("android.settings.json",1)||{};
|
||||
//default alarm settings
|
||||
if (settings.rp == undefined) settings.rp = true;
|
||||
if (settings.as == undefined) settings.as = true;
|
||||
if (settings.vibrate == undefined) settings.vibrate = "..";
|
||||
require('Storage').writeJSON("android.settings.json", settings);
|
||||
// settings var is deleted after this executes to save memory
|
||||
let settings = Object.assign({rp:true,as:true,vibrate:".."},
|
||||
require("Storage").readJSON("android.settings.json",1)||{}
|
||||
);
|
||||
let _GB = global.GB;
|
||||
let fetchRecInterval;
|
||||
global.GB = (event) => {
|
||||
global.GB = e => {
|
||||
// feed a copy to other handlers if there were any
|
||||
if (_GB) setTimeout(_GB,0,Object.assign({},event));
|
||||
|
||||
|
||||
/* TODO: Call handling, fitness */
|
||||
var HANDLERS = {
|
||||
// {t:"notify",id:int, src,title,subject,body,sender,tel:string} add
|
||||
"notify" : function() {
|
||||
Object.assign(event,{t:"add",positive:true, negative:true});
|
||||
// Detect a weird GadgetBridge bug and fix it
|
||||
// For some reason SMS messages send two GB notifications, with different sets of info
|
||||
if (lastMsg && event.body == lastMsg.body && lastMsg.src == undefined && event.src == "Messages") {
|
||||
// Mutate the other message
|
||||
event.id = lastMsg.id;
|
||||
}
|
||||
lastMsg = event;
|
||||
require("messages").pushMessage(event);
|
||||
},
|
||||
// {t:"notify~",id:int, title:string} // modified
|
||||
"notify~" : function() { event.t="modify";require("messages").pushMessage(event); },
|
||||
// {t:"notify-",id:int} // remove
|
||||
"notify-" : function() { event.t="remove";require("messages").pushMessage(event); },
|
||||
// {t:"find", n:bool} // find my phone
|
||||
"find" : function() {
|
||||
if (Bangle.findDeviceInterval) {
|
||||
clearInterval(Bangle.findDeviceInterval);
|
||||
delete Bangle.findDeviceInterval;
|
||||
}
|
||||
if (event.n) // Ignore quiet mode: we always want to find our watch
|
||||
Bangle.findDeviceInterval = setInterval(_=>Bangle.buzz(),1000);
|
||||
},
|
||||
// {t:"musicstate", state:"play/pause",position,shuffle,repeat}
|
||||
"musicstate" : function() {
|
||||
require("messages").pushMessage({t:"modify",id:"music",title:"Music",state:event.state});
|
||||
},
|
||||
// {t:"musicinfo", artist,album,track,dur,c(track count),n(track num}
|
||||
"musicinfo" : function() {
|
||||
require("messages").pushMessage(Object.assign(event, {t:"modify",id:"music",title:"Music"}));
|
||||
},
|
||||
// {"t":"call","cmd":"incoming/end","name":"Bob","number":"12421312"})
|
||||
"call" : function() {
|
||||
Object.assign(event, {
|
||||
t:event.cmd=="incoming"?"add":"remove",
|
||||
id:"call", src:"Phone",
|
||||
positive:true, negative:true,
|
||||
title:event.name||/*LANG*/"Call", body:/*LANG*/"Incoming call\n"+event.number});
|
||||
require("messages").pushMessage(event);
|
||||
},
|
||||
// {"t":"alarm", "d":[{h:int,m:int,rep:int},... }
|
||||
"alarm" : function() {
|
||||
//wipe existing GB alarms
|
||||
var sched;
|
||||
try { sched = require("sched"); } catch (e) {}
|
||||
if (!sched) return; // alarms may not be installed
|
||||
var gbalarms = sched.getAlarms().filter(a=>a.appid=="gbalarms");
|
||||
for (var i = 0; i < gbalarms.length; i++)
|
||||
sched.setAlarm(gbalarms[i].id, undefined);
|
||||
var alarms = sched.getAlarms();
|
||||
var time = new Date();
|
||||
var currentTime = time.getHours() * 3600000 +
|
||||
time.getMinutes() * 60000 +
|
||||
time.getSeconds() * 1000;
|
||||
for (var j = 0; j < event.d.length; j++) {
|
||||
// prevents all alarms from going off at once??
|
||||
var dow = event.d[j].rep;
|
||||
var rp = false;
|
||||
if (!dow) {
|
||||
dow = 127; //if no DOW selected, set alarm to all DOW
|
||||
} else {
|
||||
rp = true;
|
||||
}
|
||||
var last = (event.d[j].h * 3600000 + event.d[j].m * 60000 < currentTime) ? (new Date()).getDate() : 0;
|
||||
var a = require("sched").newDefaultAlarm();
|
||||
a.id = "gb"+j;
|
||||
a.appid = "gbalarms";
|
||||
a.on = event.d[j].on !== undefined ? event.d[j].on : true;
|
||||
a.t = event.d[j].h * 3600000 + event.d[j].m * 60000;
|
||||
a.dow = ((dow&63)<<1) | (dow>>6); // Gadgetbridge sends DOW in a different format
|
||||
a.rp = rp;
|
||||
a.last = last;
|
||||
alarms.push(a);
|
||||
}
|
||||
sched.setAlarms(alarms);
|
||||
sched.reload();
|
||||
},
|
||||
//TODO perhaps move those in a library (like messages), used also for viewing events?
|
||||
//add and remove events based on activity on phone (pebble-like)
|
||||
// {t:"calendar", id:int, type:int, timestamp:seconds, durationInSeconds, title:string, description:string,location:string,calName:string.color:int,allDay:bool
|
||||
"calendar" : function() {
|
||||
var cal = require("Storage").readJSON("android.calendar.json",true);
|
||||
if (!cal || !Array.isArray(cal)) cal = [];
|
||||
var i = cal.findIndex(e=>e.id==event.id);
|
||||
if(i<0)
|
||||
cal.push(event);
|
||||
else
|
||||
cal[i] = event;
|
||||
require("Storage").writeJSON("android.calendar.json", cal);
|
||||
},
|
||||
// {t:"calendar-", id:int}
|
||||
"calendar-" : function() {
|
||||
var cal = require("Storage").readJSON("android.calendar.json",true);
|
||||
//if any of those happen we are out of sync!
|
||||
if (!cal || !Array.isArray(cal)) cal = [];
|
||||
if (Array.isArray(event.id))
|
||||
cal = cal.filter(e=>!event.id.includes(e.id));
|
||||
else
|
||||
cal = cal.filter(e=>e.id!=event.id);
|
||||
require("Storage").writeJSON("android.calendar.json", cal);
|
||||
},
|
||||
//triggered by GB, send all ids
|
||||
// { t:"force_calendar_sync_start" }
|
||||
"force_calendar_sync_start" : function() {
|
||||
var cal = require("Storage").readJSON("android.calendar.json",true);
|
||||
if (!cal || !Array.isArray(cal)) cal = [];
|
||||
gbSend({t:"force_calendar_sync", ids: cal.map(e=>e.id)});
|
||||
},
|
||||
// {t:"http",resp:"......",[id:"..."]}
|
||||
"http":function() {
|
||||
//get the promise and call the promise resolve
|
||||
if (Bangle.httpRequest === undefined) return;
|
||||
var request=Bangle.httpRequest[event.id];
|
||||
if (request === undefined) return; //already timedout or wrong id
|
||||
delete Bangle.httpRequest[event.id];
|
||||
clearTimeout(request.t); //t = timeout variable
|
||||
if(event.err!==undefined) //if is error
|
||||
request.j(event.err); //r = reJect function
|
||||
else
|
||||
request.r(event); //r = resolve function
|
||||
},
|
||||
// {t:"gps", lat, lon, alt, speed, course, time, satellites, hdop, externalSource:true }
|
||||
"gps": function() {
|
||||
if (!settings.overwriteGps) return;
|
||||
// modify event for using it as Bangle GPS event
|
||||
delete event.t;
|
||||
if (!isFinite(event.satellites)) event.satellites = NaN;
|
||||
if (!isFinite(event.course)) event.course = NaN;
|
||||
event.fix = 1;
|
||||
if (event.long!==undefined) { // for earlier Gadgetbridge implementations
|
||||
event.lon = event.long;
|
||||
delete event.long;
|
||||
}
|
||||
if (event.time){
|
||||
event.time = new Date(event.time);
|
||||
}
|
||||
|
||||
if (!gpsState.lastGPSEvent) {
|
||||
// this is the first event, save time of arrival and deactivate internal GPS
|
||||
Bangle.moveGPSPower(0);
|
||||
} else {
|
||||
// this is the second event, store the intervall for expecting the next GPS event
|
||||
gpsState.interval = Date.now() - gpsState.lastGPSEvent;
|
||||
}
|
||||
gpsState.lastGPSEvent = Date.now();
|
||||
// in any case, cleanup the GPS state in case no new events arrive
|
||||
if (gpsState.timeoutGPS) clearTimeout(gpsState.timeoutGPS);
|
||||
gpsState.timeoutGPS = setTimeout(()=>{
|
||||
// reset state
|
||||
gpsState.lastGPSEvent = undefined;
|
||||
gpsState.timeoutGPS = undefined;
|
||||
gpsState.interval = undefined;
|
||||
// did not get an expected GPS event but have GPS clients, switch back to internal GPS
|
||||
if (Bangle.isGPSOn()) Bangle.moveGPSPower(1);
|
||||
}, (gpsState.interval || 10000) + 1000);
|
||||
Bangle.emit('GPS', event);
|
||||
},
|
||||
// {t:"is_gps_active"}
|
||||
"is_gps_active": function() {
|
||||
gbSend({ t: "gps_power", status: Bangle.isGPSOn() });
|
||||
},
|
||||
// {t:"act", hrm:bool, stp:bool, int:int}
|
||||
"act": function() {
|
||||
if (actInterval) clearInterval(actInterval);
|
||||
actInterval = undefined;
|
||||
if (actHRMHandler)
|
||||
actHRMHandler = undefined;
|
||||
Bangle.setHRMPower(event.hrm,"androidact");
|
||||
if (!(event.hrm || event.stp)) return;
|
||||
if (!isFinite(event.int)) event.int=1;
|
||||
var lastSteps = Bangle.getStepCount();
|
||||
var lastBPM = 0;
|
||||
actHRMHandler = function(e) {
|
||||
lastBPM = e.bpm;
|
||||
};
|
||||
Bangle.on('HRM',actHRMHandler);
|
||||
actInterval = setInterval(function() {
|
||||
var steps = Bangle.getStepCount();
|
||||
gbSend({ t: "act", stp: steps-lastSteps, hrm: lastBPM, rt:1 });
|
||||
lastSteps = steps;
|
||||
}, event.int*1000);
|
||||
},
|
||||
// {t:"actfetch", ts:long}
|
||||
"actfetch": function() {
|
||||
gbSend({t: "actfetch", state: "start"});
|
||||
var actCount = 0;
|
||||
var actCb = function(r) {
|
||||
// The health lib saves the samples at the start of the 10-minute block
|
||||
// However, GB expects them at the end of the block, so let's offset them
|
||||
// here to keep a consistent API in the health lib
|
||||
var sampleTs = r.date.getTime() + 600000;
|
||||
if (sampleTs >= event.ts) {
|
||||
gbSend({
|
||||
t: "act",
|
||||
ts: sampleTs,
|
||||
stp: r.steps,
|
||||
hrm: r.bpm,
|
||||
mov: r.movement
|
||||
});
|
||||
actCount++;
|
||||
}
|
||||
}
|
||||
if (event.ts != 0) {
|
||||
require("health").readAllRecordsSince(new Date(event.ts - 600000), actCb);
|
||||
} else {
|
||||
require("health").readFullDatabase(actCb);
|
||||
}
|
||||
gbSend({t: "actfetch", state: "end", count: actCount});
|
||||
},
|
||||
//{t:"listRecs", id:"20230616a"}
|
||||
"listRecs": function() {
|
||||
let recs = require("Storage").list(/^recorder\.log.*\.csv$/,{sf:true}).map(s => s.slice(12, 21));
|
||||
if (event.id.length > 2) { // Handle if there was no id supplied. Then we send a list all available recorder logs back.
|
||||
let firstNonsyncedIdx = recs.findIndex((logId) => logId > event.id);
|
||||
if (-1 == firstNonsyncedIdx) {
|
||||
recs = []
|
||||
} else {
|
||||
recs = recs.slice(firstNonsyncedIdx);
|
||||
}
|
||||
}
|
||||
gbSend({t:"actTrksList", list: recs}); // TODO: split up in multiple transmissions?
|
||||
},
|
||||
//{t:"fetchRec", id:"20230616a"}
|
||||
"fetchRec": function() {
|
||||
// TODO: Decide on what names keys should have.
|
||||
if (fetchRecInterval) {
|
||||
clearInterval(fetchRecInterval);
|
||||
fetchRecInterval = undefined;
|
||||
}
|
||||
if (event.id=="stop") {
|
||||
return
|
||||
} else {
|
||||
let log = require("Storage").open("recorder.log"+event.id+".csv","r");
|
||||
let lines = "init";// = log.readLine();
|
||||
let pkgcnt = 0;
|
||||
gbSend({t:"actTrk", log:event.id, lines:"erase", cnt:pkgcnt}); // "erase" will prompt Gadgetbridge to erase the contents of a already fetched log so we can rewrite it without keeping lines from the previous (probably failed) fetch.
|
||||
let sendlines = ()=>{
|
||||
lines = log.readLine();
|
||||
for (var i = 0; i < 3; i++) {
|
||||
let line = log.readLine();
|
||||
if (line) lines += line;
|
||||
}
|
||||
pkgcnt++;
|
||||
gbSend({t:"actTrk", log:event.id, lines:lines, cnt:pkgcnt});
|
||||
if (!lines && fetchRecInterval) {
|
||||
clearInterval(fetchRecInterval);
|
||||
fetchRecInterval = undefined;
|
||||
}
|
||||
}
|
||||
fetchRecInterval = setInterval(sendlines, 50)
|
||||
}
|
||||
},
|
||||
"nav": function() {
|
||||
event.id="nav";
|
||||
if (event.instr) {
|
||||
event.t="add";
|
||||
event.src="maps"; // for the icon
|
||||
event.title="Navigation";
|
||||
if (require("messages").getMessages().find(m=>m.id=="nav"))
|
||||
event.t = "modify";
|
||||
} else {
|
||||
event.t="remove";
|
||||
}
|
||||
require("messages").pushMessage(event);
|
||||
},
|
||||
"cards" : function() {
|
||||
// we receive all, just override what we have
|
||||
if (Array.isArray(event.d))
|
||||
require("Storage").writeJSON("android.cards.json", event.d);
|
||||
},
|
||||
"accelsender": function () {
|
||||
require("Storage").writeJSON("accelsender.json", {enabled: event.enable, interval: event.interval});
|
||||
load();
|
||||
}
|
||||
};
|
||||
var h = HANDLERS[event.t];
|
||||
if (h) h(); else console.log("GB Unknown",event);
|
||||
if (_GB) setTimeout(_GB,0,Object.assign({},e));
|
||||
Bangle.emit("GB",e);
|
||||
require("android").gbHandler(e);
|
||||
};
|
||||
// HTTP request handling - see the readme
|
||||
// options = {id,timeout,xpath}
|
||||
Bangle.http = (url,options)=>{
|
||||
options = options||{};
|
||||
if (!NRF.getSecurityStatus().connected)
|
||||
return Promise.reject(/*LANG*/"Not connected to Bluetooth");
|
||||
if (Bangle.httpRequest === undefined)
|
||||
Bangle.httpRequest={};
|
||||
if (options.id === undefined) {
|
||||
// try and create a unique ID
|
||||
do {
|
||||
options.id = Math.random().toString().substr(2);
|
||||
} while( Bangle.httpRequest[options.id]!==undefined);
|
||||
}
|
||||
//send the request
|
||||
var req = {t: "http", url:url, id:options.id};
|
||||
if (options.xpath) req.xpath = options.xpath;
|
||||
if (options.return) req.return = options.return; // for xpath
|
||||
if (options.method) req.method = options.method;
|
||||
if (options.body) req.body = options.body;
|
||||
if (options.headers) req.headers = options.headers;
|
||||
gbSend(req);
|
||||
//create the promise
|
||||
var promise = new Promise(function(resolve,reject) {
|
||||
//save the resolve function in the dictionary and create a timeout (30 seconds default)
|
||||
Bangle.httpRequest[options.id]={r:resolve,j:reject,t:setTimeout(()=>{
|
||||
//if after "timeoutMillisec" it still hasn't answered -> reject
|
||||
delete Bangle.httpRequest[options.id];
|
||||
reject("Timeout");
|
||||
},options.timeout||30000)};
|
||||
});
|
||||
return promise;
|
||||
};
|
||||
|
||||
Bangle.http = (url,options)=>require("android").httpHandler(url,options);
|
||||
// Battery monitor
|
||||
let sendBattery = function() { gbSend({ t: "status", bat: E.getBattery(), chg: Bangle.isCharging()?1:0 }); }
|
||||
let sendBattery = function() { require("android").gbSend({ t: "status", bat: E.getBattery(), chg: Bangle.isCharging()?1:0 }); }
|
||||
Bangle.on("charging", sendBattery);
|
||||
NRF.on("connect", () => setTimeout(function() {
|
||||
sendBattery();
|
||||
gbSend({t: "ver", fw: process.env.VERSION, hw: process.env.HWVERSION});
|
||||
require("android").gbSend({t: "ver", fw: process.env.VERSION, hw: process.env.HWVERSION});
|
||||
GB({t:"force_calendar_sync_start"}); // send a list of our calendar entries to start off the sync process
|
||||
}, 2000));
|
||||
NRF.on("disconnect", () => {
|
||||
|
@ -358,81 +32,24 @@
|
|||
setInterval(sendBattery, 10*60*1000);
|
||||
// Health tracking - if 'realtime' data is sent with 'rt:1', but let's still send our activity log every 10 mins
|
||||
Bangle.on('health', h=>{
|
||||
gbSend({ t: "act", stp: h.steps, hrm: h.bpm, mov: h.movement });
|
||||
require("android").gbSend({ t: "act", stp: h.steps, hrm: h.bpm, mov: h.movement });
|
||||
});
|
||||
// Music control
|
||||
Bangle.musicControl = cmd => {
|
||||
// play/pause/next/previous/volumeup/volumedown
|
||||
gbSend({ t: "music", n:cmd });
|
||||
require("android").gbSend({ t: "music", n:cmd });
|
||||
};
|
||||
// Message response
|
||||
Bangle.messageResponse = (msg,response) => {
|
||||
if (msg.id=="call") return gbSend({ t: "call", n:response?"ACCEPT":"REJECT" });
|
||||
if (isFinite(msg.id)) return gbSend({ t: "notify", n:response?"OPEN":"DISMISS", id: msg.id });
|
||||
if (msg.id=="call") return require("android").gbSend({ t: "call", n:response?"ACCEPT":"REJECT" });
|
||||
if (isFinite(msg.id)) return require("android").gbSend({ t: "notify", n:response?"OPEN":"DISMISS", id: msg.id });
|
||||
// error/warn here?
|
||||
};
|
||||
Bangle.messageIgnore = msg => {
|
||||
if (isFinite(msg.id)) return gbSend({ t: "notify", n:"MUTE", id: msg.id });
|
||||
if (isFinite(msg.id)) return require("android").gbSend({ t: "notify", n:"MUTE", id: msg.id });
|
||||
};
|
||||
// GPS overwrite logic
|
||||
if (settings.overwriteGps) { // if the overwrite option is set..
|
||||
const origSetGPSPower = Bangle.setGPSPower;
|
||||
Bangle.moveGPSPower = (state) => {
|
||||
if (Bangle.isGPSOn()){
|
||||
let orig = Bangle._PWR.GPS;
|
||||
delete Bangle._PWR.GPS;
|
||||
origSetGPSPower(state);
|
||||
Bangle._PWR.GPS = orig;
|
||||
}
|
||||
};
|
||||
|
||||
// work around Serial1 for GPS not working when connected to something
|
||||
let serialTimeout;
|
||||
let wrap = function(f){
|
||||
return (s)=>{
|
||||
if (serialTimeout) clearTimeout(serialTimeout);
|
||||
origSetGPSPower(1, "androidgpsserial");
|
||||
f(s);
|
||||
serialTimeout = setTimeout(()=>{
|
||||
serialTimeout = undefined;
|
||||
origSetGPSPower(0, "androidgpsserial");
|
||||
}, 10000);
|
||||
};
|
||||
};
|
||||
Serial1.println = wrap(Serial1.println);
|
||||
Serial1.write = wrap(Serial1.write);
|
||||
|
||||
// replace set GPS power logic to suppress activation of gps (and instead request it from the phone)
|
||||
Bangle.setGPSPower = ((isOn, appID) => {
|
||||
let pwr;
|
||||
if (!this.lastGPSEvent){
|
||||
// use internal GPS power function if no gps event has arrived from GadgetBridge
|
||||
pwr = origSetGPSPower(isOn, appID);
|
||||
} else {
|
||||
// we are currently expecting the next GPS event from GadgetBridge, keep track of GPS state per app
|
||||
if (!Bangle._PWR) Bangle._PWR={};
|
||||
if (!Bangle._PWR.GPS) Bangle._PWR.GPS=[];
|
||||
if (!appID) appID="?";
|
||||
if (isOn && !Bangle._PWR.GPS.includes(appID)) Bangle._PWR.GPS.push(appID);
|
||||
if (!isOn && Bangle._PWR.GPS.includes(appID)) Bangle._PWR.GPS.splice(Bangle._PWR.GPS.indexOf(appID),1);
|
||||
pwr = Bangle._PWR.GPS.length>0;
|
||||
// stop internal GPS, no clients left
|
||||
if (!pwr) origSetGPSPower(0);
|
||||
}
|
||||
// always update Gadgetbridge on current power state
|
||||
gbSend({ t: "gps_power", status: pwr });
|
||||
return pwr;
|
||||
}).bind(gpsState);
|
||||
// allow checking for GPS via GadgetBridge
|
||||
Bangle.isGPSOn = () => {
|
||||
return !!(Bangle._PWR && Bangle._PWR.GPS && Bangle._PWR.GPS.length>0);
|
||||
};
|
||||
// stop GPS on boot if not activated
|
||||
setTimeout(()=>{
|
||||
if (!Bangle.isGPSOn()) gbSend({ t: "gps_power", status: false });
|
||||
},3000);
|
||||
}
|
||||
|
||||
if (settings.overwriteGps) require("android").overwriteGPS();
|
||||
// remove settings object so it's not taking up RAM
|
||||
delete settings;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,388 @@
|
|||
exports.gbSend = function(message) {
|
||||
Bluetooth.println("");
|
||||
Bluetooth.println(JSON.stringify(message));
|
||||
}
|
||||
let lastMsg, // for music messages - may not be needed now...
|
||||
gpsState = {}, // keep information on GPS via Gadgetbridge
|
||||
settings = Object.assign({rp:true,as:true,vibrate:".."},
|
||||
require("Storage").readJSON("android.settings.json",1)||{}
|
||||
);
|
||||
|
||||
exports.gbHandler = (event) => {
|
||||
var HANDLERS = {
|
||||
// {t:"notify",id:int, src,title,subject,body,sender,tel:string} add
|
||||
"notify" : function() {
|
||||
print("notify",event);
|
||||
Object.assign(event,{t:"add",positive:true, negative:true});
|
||||
// Detect a weird GadgetBridge bug and fix it
|
||||
// For some reason SMS messages send two GB notifications, with different sets of info
|
||||
if (lastMsg && event.body == lastMsg.body && lastMsg.src == undefined && event.src == "Messages") {
|
||||
// Mutate the other message
|
||||
event.id = lastMsg.id;
|
||||
}
|
||||
lastMsg = event;
|
||||
require("messages").pushMessage(event);
|
||||
},
|
||||
// {t:"notify~",id:int, title:string} // modified
|
||||
"notify~" : function() { event.t="modify";require("messages").pushMessage(event); },
|
||||
// {t:"notify-",id:int} // remove
|
||||
"notify-" : function() { event.t="remove";require("messages").pushMessage(event); },
|
||||
// {t:"find", n:bool} // find my phone
|
||||
"find" : function() {
|
||||
if (Bangle.findDeviceInterval) {
|
||||
clearInterval(Bangle.findDeviceInterval);
|
||||
delete Bangle.findDeviceInterval;
|
||||
}
|
||||
if (event.n) // Ignore quiet mode: we always want to find our watch
|
||||
Bangle.findDeviceInterval = setInterval(_=>Bangle.buzz(),1000);
|
||||
},
|
||||
// {t:"musicstate", state:"play/pause",position,shuffle,repeat}
|
||||
"musicstate" : function() {
|
||||
require("messages").pushMessage({t:"modify",id:"music",title:"Music",state:event.state});
|
||||
},
|
||||
// {t:"musicinfo", artist,album,track,dur,c(track count),n(track num}
|
||||
"musicinfo" : function() {
|
||||
require("messages").pushMessage(Object.assign(event, {t:"modify",id:"music",title:"Music"}));
|
||||
},
|
||||
// {"t":"call","cmd":"incoming/end/start/outgoing","name":"Bob","number":"12421312"})
|
||||
"call" : function() {
|
||||
Object.assign(event, {
|
||||
t:event.cmd=="incoming"?"add":"remove",
|
||||
id:"call", src:"Phone",
|
||||
positive:true, negative:true,
|
||||
title:event.name||/*LANG*/"Call", body:/*LANG*/"Incoming call\n"+event.number});
|
||||
require("messages").pushMessage(event);
|
||||
},
|
||||
"canned_responses_sync" : function() {
|
||||
require("Storage").writeJSON("replies.json", event.d);
|
||||
},
|
||||
// {"t":"alarm", "d":[{h:int,m:int,rep:int},... }
|
||||
"alarm" : function() {
|
||||
//wipe existing GB alarms
|
||||
var sched;
|
||||
try { sched = require("sched"); } catch (e) {}
|
||||
if (!sched) return; // alarms may not be installed
|
||||
var gbalarms = sched.getAlarms().filter(a=>a.appid=="gbalarms");
|
||||
for (var i = 0; i < gbalarms.length; i++)
|
||||
sched.setAlarm(gbalarms[i].id, undefined);
|
||||
var alarms = sched.getAlarms();
|
||||
var time = new Date();
|
||||
var currentTime = time.getHours() * 3600000 +
|
||||
time.getMinutes() * 60000 +
|
||||
time.getSeconds() * 1000;
|
||||
for (var j = 0; j < event.d.length; j++) {
|
||||
// prevents all alarms from going off at once??
|
||||
var dow = event.d[j].rep;
|
||||
var rp = false;
|
||||
if (!dow) {
|
||||
dow = 127; //if no DOW selected, set alarm to all DOW
|
||||
} else {
|
||||
rp = true;
|
||||
}
|
||||
var last = (event.d[j].h * 3600000 + event.d[j].m * 60000 < currentTime) ? (new Date()).getDate() : 0;
|
||||
var a = require("sched").newDefaultAlarm();
|
||||
a.id = "gb"+j;
|
||||
a.appid = "gbalarms";
|
||||
a.on = event.d[j].on !== undefined ? event.d[j].on : true;
|
||||
a.t = event.d[j].h * 3600000 + event.d[j].m * 60000;
|
||||
a.dow = ((dow&63)<<1) | (dow>>6); // Gadgetbridge sends DOW in a different format
|
||||
a.rp = rp;
|
||||
a.last = last;
|
||||
alarms.push(a);
|
||||
}
|
||||
sched.setAlarms(alarms);
|
||||
sched.reload();
|
||||
},
|
||||
//TODO perhaps move those in a library (like messages), used also for viewing events?
|
||||
//add and remove events based on activity on phone (pebble-like)
|
||||
// {t:"calendar", id:int, type:int, timestamp:seconds, durationInSeconds, title:string, description:string,location:string,calName:string.color:int,allDay:bool
|
||||
"calendar" : function() {
|
||||
var cal = require("Storage").readJSON("android.calendar.json",true);
|
||||
if (!cal || !Array.isArray(cal)) cal = [];
|
||||
var i = cal.findIndex(e=>e.id==event.id);
|
||||
if(i<0)
|
||||
cal.push(event);
|
||||
else
|
||||
cal[i] = event;
|
||||
require("Storage").writeJSON("android.calendar.json", cal);
|
||||
},
|
||||
// {t:"calendar-", id:int}
|
||||
"calendar-" : function() {
|
||||
var cal = require("Storage").readJSON("android.calendar.json",true);
|
||||
//if any of those happen we are out of sync!
|
||||
if (!cal || !Array.isArray(cal)) cal = [];
|
||||
if (Array.isArray(event.id))
|
||||
cal = cal.filter(e=>!event.id.includes(e.id));
|
||||
else
|
||||
cal = cal.filter(e=>e.id!=event.id);
|
||||
require("Storage").writeJSON("android.calendar.json", cal);
|
||||
},
|
||||
//triggered by GB, send all ids
|
||||
// { t:"force_calendar_sync_start" }
|
||||
"force_calendar_sync_start" : function() {
|
||||
var cal = require("Storage").readJSON("android.calendar.json",true);
|
||||
if (!cal || !Array.isArray(cal)) cal = [];
|
||||
exports.gbSend({t:"force_calendar_sync", ids: cal.map(e=>e.id)});
|
||||
},
|
||||
// {t:"http",resp:"......",[id:"..."]}
|
||||
"http":function() {
|
||||
//get the promise and call the promise resolve
|
||||
if (Bangle.httpRequest === undefined) return;
|
||||
var request=Bangle.httpRequest[event.id];
|
||||
if (request === undefined) return; //already timedout or wrong id
|
||||
delete Bangle.httpRequest[event.id];
|
||||
clearTimeout(request.t); //t = timeout variable
|
||||
if(event.err!==undefined) //if is error
|
||||
request.j(event.err); //r = reJect function
|
||||
else
|
||||
request.r(event); //r = resolve function
|
||||
},
|
||||
// {t:"gps", lat, lon, alt, speed, course, time, satellites, hdop, externalSource:true }
|
||||
"gps": function() {
|
||||
if (!settings.overwriteGps) return;
|
||||
// modify event for using it as Bangle GPS event
|
||||
delete event.t;
|
||||
if (!isFinite(event.satellites)) event.satellites = NaN;
|
||||
if (!isFinite(event.course)) event.course = NaN;
|
||||
event.fix = 1;
|
||||
if (event.long!==undefined) { // for earlier Gadgetbridge implementations
|
||||
event.lon = event.long;
|
||||
delete event.long;
|
||||
}
|
||||
if (event.time){
|
||||
event.time = new Date(event.time);
|
||||
}
|
||||
|
||||
if (!gpsState.lastGPSEvent) {
|
||||
// this is the first event, save time of arrival and deactivate internal GPS
|
||||
Bangle.moveGPSPower(0);
|
||||
} else {
|
||||
// this is the second event, store the intervall for expecting the next GPS event
|
||||
gpsState.interval = Date.now() - gpsState.lastGPSEvent;
|
||||
}
|
||||
gpsState.lastGPSEvent = Date.now();
|
||||
// in any case, cleanup the GPS state in case no new events arrive
|
||||
if (gpsState.timeoutGPS) clearTimeout(gpsState.timeoutGPS);
|
||||
gpsState.timeoutGPS = setTimeout(()=>{
|
||||
// reset state
|
||||
gpsState.lastGPSEvent = undefined;
|
||||
gpsState.timeoutGPS = undefined;
|
||||
gpsState.interval = undefined;
|
||||
// did not get an expected GPS event but have GPS clients, switch back to internal GPS
|
||||
if (Bangle.isGPSOn()) Bangle.moveGPSPower(1);
|
||||
}, (gpsState.interval || 10000) + 1000);
|
||||
Bangle.emit('GPS', event);
|
||||
},
|
||||
// {t:"is_gps_active"}
|
||||
"is_gps_active": function() {
|
||||
exports.gbSend({ t: "gps_power", status: Bangle.isGPSOn() });
|
||||
},
|
||||
// {t:"act", hrm:bool, stp:bool, int:int}
|
||||
"act": function() {
|
||||
if (exports.actInterval) clearInterval(exports.actInterval);
|
||||
exports.actInterval = undefined;
|
||||
if (exports.actHRMHandler)
|
||||
exports.actHRMHandler = undefined;
|
||||
Bangle.setHRMPower(event.hrm,"androidact");
|
||||
if (!(event.hrm || event.stp)) return;
|
||||
if (!isFinite(event.int)) event.int=1;
|
||||
var lastSteps = Bangle.getStepCount();
|
||||
var lastBPM = 0;
|
||||
exports.actHRMHandler = function(e) {
|
||||
lastBPM = e.bpm;
|
||||
};
|
||||
Bangle.on('HRM',exports.actHRMHandler);
|
||||
exports.actInterval = setInterval(function() {
|
||||
var steps = Bangle.getStepCount();
|
||||
exports.gbSend({ t: "act", stp: steps-lastSteps, hrm: lastBPM, rt:1 });
|
||||
lastSteps = steps;
|
||||
}, event.int*1000);
|
||||
},
|
||||
// {t:"actfetch", ts:long}
|
||||
"actfetch": function() {
|
||||
exports.gbSend({t: "actfetch", state: "start"});
|
||||
var actCount = 0;
|
||||
var actCb = function(r) {
|
||||
// The health lib saves the samples at the start of the 10-minute block
|
||||
// However, GB expects them at the end of the block, so let's offset them
|
||||
// here to keep a consistent API in the health lib
|
||||
var sampleTs = r.date.getTime() + 600000;
|
||||
if (sampleTs >= event.ts) {
|
||||
exports.gbSend({
|
||||
t: "act",
|
||||
ts: sampleTs,
|
||||
stp: r.steps,
|
||||
hrm: r.bpm,
|
||||
mov: r.movement
|
||||
});
|
||||
actCount++;
|
||||
}
|
||||
}
|
||||
if (event.ts != 0) {
|
||||
require("health").readAllRecordsSince(new Date(event.ts - 600000), actCb);
|
||||
} else {
|
||||
require("health").readFullDatabase(actCb);
|
||||
}
|
||||
exports.gbSend({t: "actfetch", state: "end", count: actCount});
|
||||
},
|
||||
//{t:"listRecs", id:"20230616a"}
|
||||
"listRecs": function() {
|
||||
let recs = require("Storage").list(/^recorder\.log.*\.csv$/,{sf:true}).map(s => s.slice(12, 21));
|
||||
if (event.id.length > 2) { // Handle if there was no id supplied. Then we send a list all available recorder logs back.
|
||||
let firstNonsyncedIdx = recs.findIndex((logId) => logId > event.id);
|
||||
if (-1 == firstNonsyncedIdx) {
|
||||
recs = []
|
||||
} else {
|
||||
recs = recs.slice(firstNonsyncedIdx);
|
||||
}
|
||||
}
|
||||
exports.gbSend({t:"actTrksList", list: recs}); // TODO: split up in multiple transmissions?
|
||||
},
|
||||
//{t:"fetchRec", id:"20230616a"}
|
||||
"fetchRec": function() {
|
||||
// TODO: Decide on what names keys should have.
|
||||
if (exports.fetchRecInterval) {
|
||||
clearInterval(exports.fetchRecInterval);
|
||||
exports.fetchRecInterval = undefined;
|
||||
}
|
||||
if (event.id=="stop") {
|
||||
return;
|
||||
} else {
|
||||
let log = require("Storage").open("recorder.log"+event.id+".csv","r");
|
||||
let lines = "init";// = log.readLine();
|
||||
let pkgcnt = 0;
|
||||
exports.gbSend({t:"actTrk", log:event.id, lines:"erase", cnt:pkgcnt}); // "erase" will prompt Gadgetbridge to erase the contents of a already fetched log so we can rewrite it without keeping lines from the previous (probably failed) fetch.
|
||||
let sendlines = ()=>{
|
||||
lines = log.readLine();
|
||||
for (var i = 0; i < 3; i++) {
|
||||
let line = log.readLine();
|
||||
if (line) lines += line;
|
||||
}
|
||||
pkgcnt++;
|
||||
exports.gbSend({t:"actTrk", log:event.id, lines:lines, cnt:pkgcnt});
|
||||
if (!lines && exports.fetchRecInterval) {
|
||||
clearInterval(exports.fetchRecInterval);
|
||||
exports.fetchRecInterval = undefined;
|
||||
}
|
||||
};
|
||||
exports.fetchRecInterval = setInterval(sendlines, 50);
|
||||
}
|
||||
},
|
||||
"nav": function() {
|
||||
event.id="nav";
|
||||
if (event.instr) {
|
||||
event.t="add";
|
||||
event.src="maps"; // for the icon
|
||||
event.title="Navigation";
|
||||
if (require("messages").getMessages().find(m=>m.id=="nav"))
|
||||
event.t = "modify";
|
||||
} else {
|
||||
event.t="remove";
|
||||
}
|
||||
require("messages").pushMessage(event);
|
||||
},
|
||||
"cards" : function() {
|
||||
// we receive all, just override what we have
|
||||
if (Array.isArray(event.d))
|
||||
require("Storage").writeJSON("android.cards.json", event.d);
|
||||
},
|
||||
"accelsender": function () {
|
||||
require("Storage").writeJSON("accelsender.json", {enabled: event.enable, interval: event.interval});
|
||||
load();
|
||||
}
|
||||
};
|
||||
var h = HANDLERS[event.t];
|
||||
if (h) h(); else console.log("GB Unknown",event);
|
||||
};
|
||||
|
||||
// HTTP request handling - see the readme
|
||||
// options = {id,timeout,xpath}
|
||||
exports.httpHandler = (url,options) => {
|
||||
options = options||{};
|
||||
if (!NRF.getSecurityStatus().connected)
|
||||
return Promise.reject(/*LANG*/"Not connected to Bluetooth");
|
||||
if (Bangle.httpRequest === undefined)
|
||||
Bangle.httpRequest={};
|
||||
if (options.id === undefined) {
|
||||
// try and create a unique ID
|
||||
do {
|
||||
options.id = Math.random().toString().substr(2);
|
||||
} while( Bangle.httpRequest[options.id]!==undefined);
|
||||
}
|
||||
//send the request
|
||||
var req = {t: "http", url:url, id:options.id};
|
||||
if (options.xpath) req.xpath = options.xpath;
|
||||
if (options.return) req.return = options.return; // for xpath
|
||||
if (options.method) req.method = options.method;
|
||||
if (options.body) req.body = options.body;
|
||||
if (options.headers) req.headers = options.headers;
|
||||
exports.gbSend(req);
|
||||
//create the promise
|
||||
var promise = new Promise(function(resolve,reject) {
|
||||
//save the resolve function in the dictionary and create a timeout (30 seconds default)
|
||||
Bangle.httpRequest[options.id]={r:resolve,j:reject,t:setTimeout(()=>{
|
||||
//if after "timeoutMillisec" it still hasn't answered -> reject
|
||||
delete Bangle.httpRequest[options.id];
|
||||
reject("Timeout");
|
||||
},options.timeout||30000)};
|
||||
});
|
||||
return promise;
|
||||
};
|
||||
|
||||
exports.overwriteGPS = () => { // if the overwrite option is set, call this on init..
|
||||
const origSetGPSPower = Bangle.setGPSPower;
|
||||
Bangle.moveGPSPower = (state) => {
|
||||
if (Bangle.isGPSOn()){
|
||||
let orig = Bangle._PWR.GPS;
|
||||
delete Bangle._PWR.GPS;
|
||||
origSetGPSPower(state);
|
||||
Bangle._PWR.GPS = orig;
|
||||
}
|
||||
};
|
||||
|
||||
// work around Serial1 for GPS not working when connected to something
|
||||
let serialTimeout;
|
||||
let wrap = function(f){
|
||||
return (s)=>{
|
||||
if (serialTimeout) clearTimeout(serialTimeout);
|
||||
origSetGPSPower(1, "androidgpsserial");
|
||||
f(s);
|
||||
serialTimeout = setTimeout(()=>{
|
||||
serialTimeout = undefined;
|
||||
origSetGPSPower(0, "androidgpsserial");
|
||||
}, 10000);
|
||||
};
|
||||
};
|
||||
Serial1.println = wrap(Serial1.println);
|
||||
Serial1.write = wrap(Serial1.write);
|
||||
|
||||
// replace set GPS power logic to suppress activation of gps (and instead request it from the phone)
|
||||
Bangle.setGPSPower = ((isOn, appID) => {
|
||||
let pwr;
|
||||
if (!this.lastGPSEvent){
|
||||
// use internal GPS power function if no gps event has arrived from GadgetBridge
|
||||
pwr = origSetGPSPower(isOn, appID);
|
||||
} else {
|
||||
// we are currently expecting the next GPS event from GadgetBridge, keep track of GPS state per app
|
||||
if (!Bangle._PWR) Bangle._PWR={};
|
||||
if (!Bangle._PWR.GPS) Bangle._PWR.GPS=[];
|
||||
if (!appID) appID="?";
|
||||
if (isOn && !Bangle._PWR.GPS.includes(appID)) Bangle._PWR.GPS.push(appID);
|
||||
if (!isOn && Bangle._PWR.GPS.includes(appID)) Bangle._PWR.GPS.splice(Bangle._PWR.GPS.indexOf(appID),1);
|
||||
pwr = Bangle._PWR.GPS.length>0;
|
||||
// stop internal GPS, no clients left
|
||||
if (!pwr) origSetGPSPower(0);
|
||||
}
|
||||
// always update Gadgetbridge on current power state
|
||||
require("android").gbSend({ t: "gps_power", status: pwr });
|
||||
return pwr;
|
||||
}).bind(gpsState);
|
||||
// allow checking for GPS via GadgetBridge
|
||||
Bangle.isGPSOn = () => {
|
||||
return !!(Bangle._PWR && Bangle._PWR.GPS && Bangle._PWR.GPS.length>0);
|
||||
};
|
||||
// stop GPS on boot if not activated
|
||||
setTimeout(()=>{
|
||||
if (!Bangle.isGPSOn()) require("android").gbSend({ t: "gps_power", status: false });
|
||||
},3000);
|
||||
};
|
|
@ -2,7 +2,7 @@
|
|||
"id": "android",
|
||||
"name": "Android Integration",
|
||||
"shortName": "Android",
|
||||
"version": "0.36",
|
||||
"version": "0.39",
|
||||
"description": "Display notifications/music/etc sent from the Gadgetbridge app on Android. This replaces the old 'Gadgetbridge' Bangle.js widget.",
|
||||
"icon": "app.png",
|
||||
"tags": "tool,system,messages,notifications,gadgetbridge",
|
||||
|
@ -13,7 +13,8 @@
|
|||
{"name":"android.app.js","url":"app.js"},
|
||||
{"name":"android.settings.js","url":"settings.js"},
|
||||
{"name":"android.img","url":"app-icon.js","evaluate":true},
|
||||
{"name":"android.boot.js","url":"boot.js"}
|
||||
{"name":"android.boot.js","url":"boot.js"},
|
||||
{"name":"android","url":"lib.js"}
|
||||
],
|
||||
"data": [{"name":"android.settings.json"}, {"name":"android.calendar.json"}, {"name":"android.cards.json"}],
|
||||
"sortorder": -8
|
||||
|
|
|
@ -94,4 +94,4 @@
|
|||
|
||||
E.showMenu(mainmenu);
|
||||
|
||||
});
|
||||
})
|
||||
|
|
|
@ -3,4 +3,5 @@
|
|||
0.03: Select GNSS systems to use for Bangle.js 2
|
||||
0.04: Now turns GPS off after upload
|
||||
0.05: Fix regression in 0.04 that caused AGPS data not to get loaded
|
||||
0.06: Auto-set GPS output sentences - newer Bangle.js 2 don't include RMC (GPS direction + time) by default
|
||||
0.06: Auto-set GPS output sentences - newer Bangle.js 2 don't include RMC (GPS direction + time) by default
|
||||
0.07: Bangle.js 2 now gets estimated time + lat/lon from the browser (~3x faster fix)
|
|
@ -60,6 +60,7 @@
|
|||
<script>
|
||||
var isB1; // is Bangle.js 1?
|
||||
var isB2; // is Bangle.js 2?
|
||||
var currentPosition;
|
||||
|
||||
// When the 'upload' button is clicked...
|
||||
document.getElementById("upload").addEventListener("click", function() {
|
||||
|
@ -120,6 +121,7 @@
|
|||
}
|
||||
|
||||
// =================================================== Bangle.js 2 CASIC
|
||||
// https://www.espruino.com/Bangle.js2+Technical#gps
|
||||
|
||||
function CASIC_CHECKSUM(cmd) {
|
||||
var cs = 0;
|
||||
|
@ -128,6 +130,61 @@
|
|||
return cmd+"*"+cs.toString(16).toUpperCase().padStart(2, '0');
|
||||
}
|
||||
|
||||
// Send a binary CASIC packet, eg: {classId:6, messageId:0, payload:[]}
|
||||
function CASIC_PKT(pkt) {
|
||||
pkt.payload = pkt.payload || [];
|
||||
var plen = pkt.payload.length;
|
||||
var msg = new Uint8Array(10+pkt.payload.length);
|
||||
msg.set([0xBA,0xCE,
|
||||
plen, // LENGTH
|
||||
0x00,
|
||||
pkt.classId, // CLASS ID
|
||||
pkt.messageId]); // MESSAGE ID
|
||||
msg.set(pkt.payload, 6);
|
||||
var dv = new DataView(msg.buffer);
|
||||
// checksum
|
||||
var ckSum = 0;
|
||||
for (i = -4; i < plen; i+=4)
|
||||
ckSum = 0|(ckSum+dv.getUint32(6+i, true));
|
||||
dv.setUint32(6+plen, ckSum, true);
|
||||
return msg;
|
||||
}
|
||||
|
||||
// Send AID_INI message, {lat,lon,alt}
|
||||
function AID_INI(pos) {
|
||||
var msg = new Uint8Array(56);
|
||||
var dv = new DataView(msg.buffer);
|
||||
/*
|
||||
double xOrLat, yOrLon, zOrAlt;
|
||||
double tow; // 24
|
||||
float df; // 32
|
||||
float posAcc; // 36
|
||||
float tAcc; // 40
|
||||
float fAcc; // 44
|
||||
unsigned int res; // 48
|
||||
unsigned short int wn; // 52
|
||||
unsigned char timeSource; // 54
|
||||
unsigned char flags; // 55
|
||||
*/
|
||||
var ms = Date.now();
|
||||
var wk = (ms-new Date("1980-01-06T00:00:00Z")) / 604800000;
|
||||
var wn = Math.floor(wk); // week number
|
||||
var tow = (wk-wn) * 604800; // seconds in week
|
||||
dv.setFloat64(0, pos.lat, true); // xOrLat
|
||||
dv.setFloat64(8, pos.lon, true); // yOrLon
|
||||
dv.setFloat64(16, pos.alt, true); // zOrAlt
|
||||
dv.setFloat64(24, tow, true); // tow
|
||||
dv.setFloat32(32, 0, true); // df
|
||||
dv.setFloat32(36, 0, true); // posAcc
|
||||
dv.setFloat32(40, 0, true); // tAcc
|
||||
dv.setFloat32(44, 0, true); // fAcc
|
||||
dv.setUint32(48, 0, true); // res
|
||||
dv.setUint16(52, wn, true); // wn
|
||||
dv.setUint8(54,0); // timeSource
|
||||
dv.setUint8(55, 0x23); // flags ( lat/lon and clock valid, no drift data )
|
||||
return CASIC_PKT({classId:0x0B, messageId:0x01, payload:msg});
|
||||
}
|
||||
|
||||
// ===================================================
|
||||
|
||||
function jsFromBase64(b64) {
|
||||
|
@ -140,7 +197,6 @@
|
|||
js += `\x10Serial1.write(atob("${btoa(String.fromCharCode.apply(null,UBX_MGA_INI_TIME_UTC()))}"))\n`; // set GPS time
|
||||
}
|
||||
if (isB2) { // CASIC
|
||||
|
||||
// Select what GNSS System to use for decreased fix time.
|
||||
var radios = document.getElementsByName('gnss_select');
|
||||
var gnss_select="1";
|
||||
|
@ -150,11 +206,11 @@
|
|||
js += `\x10var t=getTime()+0.5;while (getTime()<t);\n`; // This is nasty - but we just wait here until the GPS has had time to boot
|
||||
js += `\x10Serial1.println("${CASIC_CHECKSUM("$PCAS04,"+gnss_select)}")\n`; // set GNSS mode
|
||||
js += `\x10Serial1.println("${CASIC_CHECKSUM("$PCAS03,1,0,0,1,1,0,0,0")}")\n`; // enable GGA,GSV,RMC packets (new Bangle.js 2 GPS firmwares don't include RMC by default!)
|
||||
// If the browser let us have the current location, give it to the GPS chip to get a faster fix
|
||||
if (currentPosition) {
|
||||
js += `\x10Serial1.write([${AID_INI(currentPosition).join(",")}])\n`;
|
||||
}
|
||||
// Serial1.println("$PCAS06,0*1B") gets the current firmware version
|
||||
// What about:
|
||||
// NAV-TIMEUTC (0x01 0x10)
|
||||
// NAV-PV (0x01 0x03)
|
||||
// or AGPS.zip uses AID-INI (0x0B 0x01)
|
||||
}
|
||||
|
||||
for (var i=0;i<bin.length;i+=chunkSize) {
|
||||
|
@ -184,8 +240,20 @@
|
|||
document.getElementById("banglejs1-info").style = isB1?"":"display:none";
|
||||
document.getElementById("banglejs2-info").style = isB2?"":"display:none";
|
||||
document.getElementById("upload-wrap").style = "";
|
||||
|
||||
if (isB2) {
|
||||
// get current position for AGPS improvement
|
||||
navigator.geolocation.getCurrentPosition(function(position) {
|
||||
currentPosition = {
|
||||
lat : position.coords.latitude,
|
||||
lon : position.coords.longitude,
|
||||
alt : 0|position.coords.altitude
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"id": "assistedgps",
|
||||
"name": "Assisted GPS Updater (AGPS)",
|
||||
"shortName": "AGPS",
|
||||
"version": "0.06",
|
||||
"version": "0.07",
|
||||
"description": "Downloads assisted GPS (AGPS) data to Bangle.js for faster GPS startup and more accurate fixes. **No app will be installed**, this just uploads new data to the GPS chip.",
|
||||
"sortorder": -1,
|
||||
"icon": "app.png",
|
||||
|
|
|
@ -1,2 +1,7 @@
|
|||
1.00: initial release
|
||||
1.01: added tap event to scroll METAR and toggle seconds display
|
||||
1.02: continue showing METAR during AVWX updates (show update status next to time)
|
||||
re-try GPS fix if it takes too long
|
||||
bug fix
|
||||
don't attempt to update METAR if Bluetooth is NOT connected
|
||||
toggle seconds display on front double-taps (if un-locked) to avoid accidential enabling
|
||||
|
|
|
@ -19,6 +19,7 @@ const APP_NAME = 'aviatorclk';
|
|||
const horizontalCenter = g.getWidth()/2;
|
||||
const mainTimeHeight = 38;
|
||||
const secondaryFontHeight = 22;
|
||||
require("Font8x16").add(Graphics); // tertiary font
|
||||
const dateColour = ( g.theme.dark ? COLOUR_YELLOW : COLOUR_BLUE );
|
||||
const UTCColour = ( g.theme.dark ? COLOUR_LIGHT_CYAN : COLOUR_DARK_CYAN );
|
||||
const separatorColour = ( g.theme.dark ? COLOUR_LIGHT_GREY : COLOUR_DARK_GREY );
|
||||
|
@ -37,6 +38,7 @@ var settings = Object.assign({
|
|||
var drawTimeout;
|
||||
var secondsInterval;
|
||||
var avwxTimeout;
|
||||
var gpsTimeout;
|
||||
|
||||
var AVWXrequest;
|
||||
var METAR = '';
|
||||
|
@ -92,16 +94,51 @@ function drawAVWX() {
|
|||
if (! avwxTimeout) { avwxTimeout = setTimeout(updateAVWX, 5 * 60000); }
|
||||
}
|
||||
|
||||
// show AVWX update status
|
||||
function showUpdateAVWXstatus(status) {
|
||||
let y = Bangle.appRect.y + 10;
|
||||
g.setBgColor(g.theme.bg);
|
||||
g.clearRect(0, y, horizontalCenter - 54, y + 16);
|
||||
if (status) {
|
||||
g.setFontAlign(0, -1).setFont("8x16").setColor( g.theme.dark ? COLOUR_ORANGE : COLOUR_DARK_YELLOW );
|
||||
g.drawString(status, horizontalCenter - 71, y, true);
|
||||
}
|
||||
}
|
||||
|
||||
// re-try if the GPS doesn't return a fix in time
|
||||
function GPStookTooLong() {
|
||||
Bangle.setGPSPower(false, APP_NAME);
|
||||
if (gpsTimeout) clearTimeout(gpsTimeout);
|
||||
gpsTimeout = undefined;
|
||||
|
||||
showUpdateAVWXstatus('X');
|
||||
|
||||
if (! avwxTimeout) { avwxTimeout = setTimeout(updateAVWX, 5 * 60000); }
|
||||
}
|
||||
|
||||
// update the METAR info
|
||||
function updateAVWX() {
|
||||
if (avwxTimeout) clearTimeout(avwxTimeout);
|
||||
avwxTimeout = undefined;
|
||||
if (gpsTimeout) clearTimeout(gpsTimeout);
|
||||
gpsTimeout = undefined;
|
||||
|
||||
METAR = '\nGetting GPS fix';
|
||||
METARlinesCount = 0; METARscollLines = 0;
|
||||
METARts = undefined;
|
||||
if (! NRF.getSecurityStatus().connected) {
|
||||
// if Bluetooth is NOT connected, try again in 5min
|
||||
showUpdateAVWXstatus('X');
|
||||
avwxTimeout = setTimeout(updateAVWX, 5 * 60000);
|
||||
return;
|
||||
}
|
||||
|
||||
showUpdateAVWXstatus('GPS');
|
||||
if (! METAR) {
|
||||
METAR = '\nUpdating METAR';
|
||||
METARlinesCount = 0; METARscollLines = 0;
|
||||
METARts = undefined;
|
||||
}
|
||||
drawAVWX();
|
||||
|
||||
gpsTimeout = setTimeout(GPStookTooLong, 30 * 60000);
|
||||
Bangle.setGPSPower(true, APP_NAME);
|
||||
Bangle.on('GPS', fix => {
|
||||
// prevent multiple, simultaneous requests
|
||||
|
@ -109,12 +146,18 @@ function updateAVWX() {
|
|||
|
||||
if ('fix' in fix && fix.fix != 0 && fix.satellites >= 4) {
|
||||
Bangle.setGPSPower(false, APP_NAME);
|
||||
if (gpsTimeout) clearTimeout(gpsTimeout);
|
||||
gpsTimeout = undefined;
|
||||
|
||||
let lat = fix.lat;
|
||||
let lon = fix.lon;
|
||||
|
||||
METAR = '\nRequesting METAR';
|
||||
METARlinesCount = 0; METARscollLines = 0;
|
||||
METARts = undefined;
|
||||
showUpdateAVWXstatus('AVWX');
|
||||
if (! METAR) {
|
||||
METAR = '\nUpdating METAR';
|
||||
METARlinesCount = 0; METARscollLines = 0;
|
||||
METARts = undefined;
|
||||
}
|
||||
drawAVWX();
|
||||
|
||||
// get latest METAR from nearest airport (via AVWX API)
|
||||
|
@ -146,6 +189,7 @@ function updateAVWX() {
|
|||
METARts = undefined;
|
||||
}
|
||||
|
||||
showUpdateAVWXstatus('');
|
||||
drawAVWX();
|
||||
AVWXrequest = undefined;
|
||||
|
||||
|
@ -155,6 +199,7 @@ function updateAVWX() {
|
|||
METAR = 'ERR: ' + error;
|
||||
METARlinesCount = 0; METARscollLines = 0;
|
||||
METARts = undefined;
|
||||
showUpdateAVWXstatus('');
|
||||
drawAVWX();
|
||||
AVWXrequest = undefined;
|
||||
});
|
||||
|
@ -268,10 +313,10 @@ Bangle.on('tap', data => {
|
|||
case 'bottom':
|
||||
scrollAVWX(1);
|
||||
break;
|
||||
case 'left':
|
||||
case 'right':
|
||||
// toggle seconds display on double taps left or right
|
||||
if (data.double) {
|
||||
case 'front':
|
||||
// toggle seconds display on double tap on front/watch-face
|
||||
// (if watch is un-locked)
|
||||
if (data.double && ! Bangle.isLocked()) {
|
||||
if (settings.showSeconds) {
|
||||
clearInterval(secondsInterval);
|
||||
let y = Bangle.appRect.y + mainTimeHeight - 3;
|
||||
|
@ -295,7 +340,7 @@ Bangle.loadWidgets();
|
|||
Bangle.drawWidgets();
|
||||
|
||||
// draw static separator line
|
||||
y = Bangle.appRect.y + mainTimeHeight + secondaryFontHeight;
|
||||
let y = Bangle.appRect.y + mainTimeHeight + secondaryFontHeight;
|
||||
g.setColor(separatorColour);
|
||||
g.drawLine(0, y, g.getWidth(), y);
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"id": "aviatorclk",
|
||||
"name": "Aviator Clock",
|
||||
"shortName":"AV8R Clock",
|
||||
"version":"1.01",
|
||||
"version":"1.02",
|
||||
"description": "A clock for aviators, with local time and UTC - and the latest METAR for the nearest airport",
|
||||
"icon": "aviatorclk.png",
|
||||
"screenshots": [{ "url": "screenshot.png" }, { "url": "screenshot2.png" }],
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
0.01: New App!
|
||||
0.02: Don't fire if the app uses swipes already.
|
||||
0.03: Only count defined handlers in the handler array.
|
||||
0.04: Fix messages auto opened by `messagegui` could not be blacklisted. Needs
|
||||
a refresh by deselecting and reselecting the "Messages" app throught Back Swipe
|
||||
settings.
|
||||
0.04: Fix messages auto opened by `messagegui` could not be blacklisted. Needs a refresh by deselecting and reselecting the "Messages" app throught Back Swipe settings.
|
||||
0.05: React on swipes before the active app (for the most part) by using `prependListener`.
|
||||
|
|
|
@ -38,6 +38,7 @@
|
|||
// if we're in an app that has a back button, run the callback for it
|
||||
if (global.BACK && countHandlers("swipe")<=settings.standardNumSwipeHandlers && countHandlers("drag")<=settings.standardNumDragHandlers) {
|
||||
global.BACK();
|
||||
E.stopEventPropagation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -56,5 +57,5 @@
|
|||
}
|
||||
|
||||
// Listen to left to right swipe
|
||||
Bangle.on("swipe", goBack);
|
||||
Bangle.prependListener("swipe", goBack);
|
||||
})();
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{ "id": "backswipe",
|
||||
"name": "Back Swipe",
|
||||
"shortName":"BackSwipe",
|
||||
"version":"0.04",
|
||||
"version":"0.05",
|
||||
"description": "Service that allows you to use an app's back button using left to right swipe gesture",
|
||||
"icon": "app.png",
|
||||
"tags": "back,gesture,swipe",
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
0.01: New app!
|
||||
0.02: Minor code improvements
|
||||
0.03: Remove clearing of the screen (will break running apps) and fix lint errors
|
|
@ -2,8 +2,8 @@
|
|||
"id": "banglebridge",
|
||||
"name": "BangleBridge",
|
||||
"shortName": "BangleBridge",
|
||||
"version": "0.02",
|
||||
"description": "Widget that allows Bangle Js to record pair and end data using Bluetooth Low Energy in combination with the BangleBridge Android App",
|
||||
"version": "0.03",
|
||||
"description": "Widget that allows Bangle.js to record pair and end data using Bluetooth Low Energy in combination with the BangleBridge Android App (**Note:** this has nothing to do with Gadgetbridge)",
|
||||
"icon": "widget.png",
|
||||
"type": "widget",
|
||||
"tags": "widget",
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
(() => {
|
||||
/**
|
||||
* Widget measurements
|
||||
* Description:
|
||||
* Description:
|
||||
* name: connection.wid.js
|
||||
*icon: conectionIcon.icon
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
//Font
|
||||
|
@ -24,7 +24,7 @@
|
|||
|
||||
//Sensors code
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @author Jorge
|
||||
*/
|
||||
function accel() {
|
||||
|
@ -35,8 +35,7 @@
|
|||
});
|
||||
|
||||
setInterval(function () {
|
||||
|
||||
acclS = accelN.x + "##" + accelN.y + "##" + accelN.z + "\n" + accelN.diff + "##" + accelN.mag;
|
||||
//acclS = accelN.x + "##" + accelN.y + "##" + accelN.z + "\n" + accelN.diff + "##" + accelN.mag;
|
||||
data[3] = accelN;
|
||||
}, 2 * 1000);
|
||||
|
||||
|
@ -45,8 +44,7 @@
|
|||
function btt() {
|
||||
|
||||
setInterval(function () {
|
||||
|
||||
bttS = E.getBattery(); //return String
|
||||
//bttS = E.getBattery(); //return String
|
||||
data[2] = E.getBattery();
|
||||
}, 15 * 1000);
|
||||
|
||||
|
@ -65,9 +63,9 @@
|
|||
|
||||
setInterval(function () {
|
||||
|
||||
compssS = "A: " + compssN.x + " ## " + compssN.y + " ## " + compssN.z + "\n" +
|
||||
/*compssS = "A: " + compssN.x + " ## " + compssN.y + " ## " + compssN.z + "\n" +
|
||||
"B: " + compssN.dx + " ## " + compssN.dy + " ## " + compssN.dz + " ## " + "\n" +
|
||||
"C: " + compssN.heading; //return String
|
||||
"C: " + compssN.heading; *///return String
|
||||
data[4] = compssN;
|
||||
}, 2 * 1000);
|
||||
|
||||
|
@ -86,8 +84,8 @@
|
|||
|
||||
setInterval(function () {
|
||||
|
||||
gpsS = "A: " + gpsN.lat + " ## " + gpsN.lon + " ## " + gpsN.alt + "\n" + "B: " + gpsN.speed + " ## " + gpsN.course + " ## " + gpsN.time + "\n" +
|
||||
"C: " + gpsN.satellites + " ## " + gpsN.fix; //return String
|
||||
/*gpsS = "A: " + gpsN.lat + " ## " + gpsN.lon + " ## " + gpsN.alt + "\n" + "B: " + gpsN.speed + " ## " + gpsN.course + " ## " + gpsN.time + "\n" +
|
||||
"C: " + gpsN.satellites + " ## " + gpsN.fix; *///return String
|
||||
// work out how to display the current time
|
||||
var d = new Date();
|
||||
var year = d.getFullYear();
|
||||
|
@ -150,7 +148,7 @@
|
|||
//console.log("Index ==> "+ index);
|
||||
msr[indexFinal] = nueva;
|
||||
|
||||
item = nueva;
|
||||
//item = nueva;
|
||||
lastInsert = indexFinal;
|
||||
|
||||
}
|
||||
|
@ -180,7 +178,7 @@
|
|||
|
||||
hrmN = normalize(hrmN);
|
||||
var roundedRate = parseFloat(hrmN).toFixed(2);
|
||||
hrmS = String.valueOf(roundedRate); //return String
|
||||
//hrmS = String.valueOf(roundedRate); //return String
|
||||
//console.log("array----->" + msr);
|
||||
data[0] = roundedRate;
|
||||
|
||||
|
@ -205,7 +203,7 @@
|
|||
|
||||
setInterval(function () {
|
||||
|
||||
stepS = String.valueOf(stepN); //return String
|
||||
//stepS = String.valueOf(stepN); //return String
|
||||
data[1] = stepN;
|
||||
}, 2 * 1000);
|
||||
|
||||
|
@ -240,12 +238,11 @@
|
|||
g.setFont("Vector", 45);
|
||||
g.drawString(prueba,100,200);*/
|
||||
if (flip == 1) { //when off
|
||||
|
||||
|
||||
flip = 0;
|
||||
//Bangle.buzz(1000);
|
||||
g.clear();
|
||||
} else { //when on
|
||||
|
||||
|
||||
flip = 1;
|
||||
g.setFont("Vector", 30);
|
||||
g.drawString(data[0], 65, 180);
|
||||
|
@ -283,7 +280,7 @@
|
|||
com: data[4],
|
||||
gps: data[5]
|
||||
};
|
||||
/* g.clear();
|
||||
/*
|
||||
g.drawString(compssS,100,200);
|
||||
*/
|
||||
|
||||
|
@ -293,7 +290,7 @@
|
|||
//draw();
|
||||
|
||||
}, 5 * 1000);
|
||||
|
||||
|
||||
WIDGETS["banglebridge"]={
|
||||
area: "tl",
|
||||
width: 10,
|
||||
|
|
|
@ -17,4 +17,4 @@
|
|||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
|
|
|
@ -32,4 +32,4 @@
|
|||
}
|
||||
require("ClockFace_menu").addItems(menu, save, items);
|
||||
E.showMenu(menu);
|
||||
});
|
||||
})
|
||||
|
|
|
@ -23,4 +23,4 @@
|
|||
};
|
||||
E.showMenu(mainmenu);
|
||||
|
||||
});
|
||||
})
|
||||
|
|
|
@ -68,4 +68,4 @@
|
|||
|
||||
E.showMenu(appMenu);
|
||||
|
||||
});
|
||||
})
|
||||
|
|
|
@ -1 +1,4 @@
|
|||
0.10: New app introduced to the app loader!
|
||||
0.20: skipped (internal revision)
|
||||
0.30: skipped (internal revision)
|
||||
0.40: added functionality for customizing colors
|
||||
|
|
|
@ -9,5 +9,9 @@ From top to bottom the watch face shows four rows of leds:
|
|||
* day (yellow leds, top row)
|
||||
* month (yellow leds, bottom row)
|
||||
|
||||
As usual, luminous leds represent a logical one, dark leds a logcal '0'.
|
||||
The colors are default colors and can be changed at the settings page, also, the outer ring can be disabled.
|
||||
|
||||
The rightmost LED represents 1, the second 2, the third 4, the next 8 and so on, i.e. values are the powers of two.
|
||||
|
||||
As usual, luminous leds represent a logical one, and "dark" leds a logcal '0'. Dark means the color of the background.
|
||||
Widgets aren't affected and are shown as normal.
|
||||
|
|
294
apps/blc/blc.js
|
@ -2,135 +2,181 @@
|
|||
|
||||
{ // must be inside our own scope here so that when we are unloaded everything disappears
|
||||
// we also define functions using 'let fn = function() {..}' for the same reason. function decls are global
|
||||
let drawTimeout;
|
||||
|
||||
// Actually draw the watch face
|
||||
let draw = function()
|
||||
{
|
||||
// Bangle.js2 -> 176x176
|
||||
var x_rgt = g.getWidth();
|
||||
var y_bot = g.getHeight();
|
||||
//var x_cntr = x_rgt / 2;
|
||||
var y_cntr = y_bot / 18*7; // not to high because of widget-field (1/3 is to high)
|
||||
g.reset().clearRect(Bangle.appRect); // clear whole background (w/o widgets)
|
||||
|
||||
let white = [1,1,1];
|
||||
let red = [1,0,0];
|
||||
let green = [0,1,0];
|
||||
//let blue = [0,0,1];
|
||||
let yellow = [1,1,0];
|
||||
//let magenta = [1,0,1];
|
||||
//let cyan = [0,1,1];
|
||||
let black = [0,0,0];
|
||||
let bord_col = white;
|
||||
let col_off = black;
|
||||
|
||||
var col = new Array(red, green, yellow, yellow); // [R,G,B]
|
||||
|
||||
let pot_2 = [1, 2, 4, 8, 16, 32]; // array with powers of two, because power-op (**)
|
||||
// doesn't work -> maybe also faster
|
||||
|
||||
|
||||
var nr_lines = 4; // 4 rows: hour (hr), minute (min), day (day), month (mon)
|
||||
|
||||
// Arrays: [hr, min, day, mon]
|
||||
//No of Bits: 5 6 5 4
|
||||
let msbits = [4, 5, 4, 3]; // MSB = No bits - 1
|
||||
let rad = [12, 12, 8, 8]; // radiuses for each row
|
||||
var x_dist = 28;
|
||||
let y_dist = [0, 30, 60, 85]; // y-position from y_centr for each row from top
|
||||
// don't calc. automatic as for x, because of different spaces
|
||||
var x_offs_rgt = 16; // distance from right border (layout)
|
||||
const SETTINGSFILE = "BinaryClk.settings.json";
|
||||
|
||||
// Date-Time-Array: 4x6 Bit
|
||||
//var idx_hr = 0;
|
||||
//var idx_min = 1;
|
||||
//var idx_day = 2;
|
||||
//var idx_mon = 3;
|
||||
var dt_bit_arr = [[0, 0, 0, 0, 0, 0],[0, 0, 0, 0, 0, 0],[0, 0, 0, 0, 0, 0],[0, 0, 0, 0, 0, 0]];
|
||||
// variables defined from settings
|
||||
let HourCol;
|
||||
let MinCol;
|
||||
let DayCol;
|
||||
let MonCol;
|
||||
let RingOn;
|
||||
|
||||
var date_time = new Date();
|
||||
var hr = date_time.getHours(); // 0..23
|
||||
var min = date_time.getMinutes(); // 0..59
|
||||
var day = date_time.getDate(); // 1..31
|
||||
var mon = date_time.getMonth() + 1; // GetMonth() -> 0..11
|
||||
// color arrays
|
||||
// !!! don't change order unless change oder in BinaryClk.settings.js !!!
|
||||
// !!! order must correspond to each other between arrays !!!
|
||||
let LED_Colors = ["#FFF", "#F00", "#0F0", "#00F", "#FF0", "#F0F", "#0FF", "#000"];
|
||||
let LED_ColorNames = ["white", "red", "green", "blue", "yellow", "magenta", "cyan", "black"];
|
||||
|
||||
let dt_array = [hr, min, day, mon];
|
||||
|
||||
|
||||
////////////////////////////////////////
|
||||
// compute bit-pattern from time/date and draw leds
|
||||
////////////////////////////////////////
|
||||
var line_cnt = 0;
|
||||
var cnt = 0;
|
||||
var bit_cnt = 0;
|
||||
|
||||
while (line_cnt < nr_lines)
|
||||
{
|
||||
|
||||
////////////////////////////////////////
|
||||
// compute bit-pattern
|
||||
bit_cnt = msbits[line_cnt];
|
||||
|
||||
while (bit_cnt >= 0)
|
||||
{
|
||||
if (dt_array[line_cnt] >= pot_2[bit_cnt])
|
||||
{
|
||||
dt_array[line_cnt] -= pot_2[bit_cnt];
|
||||
dt_bit_arr[line_cnt][bit_cnt] = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
dt_bit_arr[line_cnt][bit_cnt] = 0;
|
||||
}
|
||||
bit_cnt--;
|
||||
}
|
||||
|
||||
////////////////////////////////////////
|
||||
// draw leds (first white border for black screen, then led itself)
|
||||
cnt = 0;
|
||||
|
||||
while (cnt <= msbits[line_cnt])
|
||||
{
|
||||
g.setColor(bord_col[0], bord_col[1], bord_col[2]);
|
||||
g.drawCircle(x_rgt-x_offs_rgt-cnt*x_dist, y_cntr-20+y_dist[line_cnt], rad[line_cnt]);
|
||||
|
||||
if (dt_bit_arr[line_cnt][cnt] == 1)
|
||||
{
|
||||
g.setColor(col[line_cnt][0], col[line_cnt][1], col[line_cnt][2]);
|
||||
}
|
||||
else
|
||||
{
|
||||
g.setColor(col_off[0], col_off[1], col_off[2]);
|
||||
}
|
||||
g.fillCircle(x_rgt-x_offs_rgt-cnt*x_dist, y_cntr-20+y_dist[line_cnt], rad[line_cnt]-1);
|
||||
cnt++;
|
||||
}
|
||||
line_cnt++;
|
||||
}
|
||||
|
||||
// queue next draw
|
||||
if (drawTimeout) clearTimeout(drawTimeout);
|
||||
drawTimeout = setTimeout(function()
|
||||
// load settings
|
||||
let loadSettings = function()
|
||||
{
|
||||
drawTimeout = undefined;
|
||||
draw();
|
||||
}, 60000 - (Date.now() % 60000));
|
||||
};
|
||||
function def (value, def) {return value !== undefined ? value : def;}
|
||||
|
||||
// Show launcher when middle button pressed
|
||||
Bangle.setUI(
|
||||
{
|
||||
mode : "clock",
|
||||
remove : function()
|
||||
{
|
||||
// Called to unload all of the clock app
|
||||
if (drawTimeout) clearTimeout(drawTimeout);
|
||||
drawTimeout = undefined;
|
||||
var settings = require('Storage').readJSON(SETTINGSFILE, true) || {};
|
||||
// get name from setting, find index of name and assign corresponding color code by index
|
||||
HourCol = LED_Colors[LED_ColorNames.indexOf(def(settings.HourCol, "red"))];
|
||||
MinCol = LED_Colors[LED_ColorNames.indexOf(def(settings.MinCol, "green"))];
|
||||
DayCol = LED_Colors[LED_ColorNames.indexOf(def(settings.DayCol, "yellow"))];
|
||||
MonCol = LED_Colors[LED_ColorNames.indexOf(def(settings.MonCol, "yellow"))];
|
||||
RingOn = def(settings.RingOn, true);
|
||||
|
||||
delete settings; // settings in local var -> no more required
|
||||
}
|
||||
});
|
||||
// Load widgets
|
||||
Bangle.loadWidgets();
|
||||
draw();
|
||||
setTimeout(Bangle.drawWidgets,0);
|
||||
|
||||
let drawTimeout;
|
||||
|
||||
// actually draw the watch face
|
||||
let draw = function()
|
||||
{
|
||||
// Bangle.js2 -> 176x176
|
||||
var x_rgt = g.getWidth();
|
||||
var y_bot = g.getHeight();
|
||||
//var x_cntr = x_rgt / 2;
|
||||
var y_cntr = y_bot / 18*7;
|
||||
g.reset().clearRect(Bangle.appRect); // clear whole background (w/o widgets)
|
||||
|
||||
var white = "#FFF";
|
||||
var black = "#000";
|
||||
var bord_col = white;
|
||||
var col_off = black;
|
||||
|
||||
var col = new Array(HourCol, MinCol, DayCol, MonCol); // each #rgb
|
||||
if (g.theme.dark)
|
||||
{
|
||||
bord_col = white;
|
||||
col_off = black;
|
||||
}
|
||||
else
|
||||
{
|
||||
bord_col = black;
|
||||
col_off = white;
|
||||
}
|
||||
|
||||
let pwr2 = [1, 2, 4, 8, 16, 32]; // array with powers of 2, because poweroperator '**' doesnt work
|
||||
// maybe also faster
|
||||
|
||||
|
||||
var no_lines = 4; // 4 rows: hour (hr), minute (min), day (day), month (mon)
|
||||
var no_hour = 5;
|
||||
var no_min = 6;
|
||||
var no_day = 5;
|
||||
var no_mon = 4;
|
||||
|
||||
// arrays: [hr, min, day, mon]
|
||||
let msbits = [no_hour-1, no_min-1, no_day-1, no_mon-1]; // MSB = No bits - 1
|
||||
let rad = [13, 13, 9, 9]; // radiuses for each row
|
||||
var x_dist = 29;
|
||||
let y_dist = [0, 35, 75, 100]; // y-position from y_centr for each row from top
|
||||
// don't calc. automatic as for x, because of different spaces
|
||||
var x_offs_rgt = 15; // offset from right border (layout)
|
||||
var y_offs_cntr = 25; // vertical offset from center
|
||||
|
||||
////////////////////////////////////////
|
||||
// compute bit-pattern from time/date and draw leds
|
||||
////////////////////////////////////////
|
||||
|
||||
// date-time-array: 4x6 bit
|
||||
//var idx_hour = 0;
|
||||
//var idx_min = 1;
|
||||
//var idx_day = 2;
|
||||
//var idx_mon = 3;
|
||||
var dt_bit_arr = [[0, 0, 0, 0, 0, 0],[0, 0, 0, 0, 0, 0],[0, 0, 0, 0, 0, 0],[0, 0, 0, 0, 0, 0]];
|
||||
|
||||
var date_time = new Date();
|
||||
var hour = date_time.getHours(); // 0..23
|
||||
var min = date_time.getMinutes(); // 0..59
|
||||
var day = date_time.getDate(); // 1..31
|
||||
var mon = date_time.getMonth() + 1; // GetMonth() -> 0..11
|
||||
|
||||
let dt_array = [hour, min, day, mon];
|
||||
|
||||
var line_cnt = 0;
|
||||
var cnt = 0;
|
||||
var bit_cnt = 0;
|
||||
|
||||
while (line_cnt < no_lines)
|
||||
{
|
||||
|
||||
////////////////////////////////////////
|
||||
// compute bit-pattern
|
||||
bit_cnt = msbits[line_cnt];
|
||||
|
||||
while (bit_cnt >= 0)
|
||||
{
|
||||
if (dt_array[line_cnt] >= pwr2[bit_cnt])
|
||||
{
|
||||
dt_array[line_cnt] -= pwr2[bit_cnt];
|
||||
dt_bit_arr[line_cnt][bit_cnt] = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
dt_bit_arr[line_cnt][bit_cnt] = 0;
|
||||
}
|
||||
bit_cnt--;
|
||||
}
|
||||
|
||||
////////////////////////////////////////
|
||||
// draw leds (and border, if enabled)
|
||||
cnt = 0;
|
||||
|
||||
while (cnt <= msbits[line_cnt])
|
||||
{
|
||||
if (RingOn) // draw outer ring, if enabled
|
||||
{
|
||||
g.setColor(bord_col);
|
||||
g.drawCircle(x_rgt-x_offs_rgt-cnt*x_dist, y_cntr-y_offs_cntr+y_dist[line_cnt], rad[line_cnt]);
|
||||
}
|
||||
if (dt_bit_arr[line_cnt][cnt] == 1)
|
||||
{
|
||||
g.setColor(col[line_cnt]);
|
||||
}
|
||||
else
|
||||
{
|
||||
g.setColor(col_off);
|
||||
}
|
||||
g.fillCircle(x_rgt-x_offs_rgt-cnt*x_dist, y_cntr-y_offs_cntr+y_dist[line_cnt], rad[line_cnt]-1);
|
||||
cnt++;
|
||||
}
|
||||
line_cnt++;
|
||||
}
|
||||
|
||||
// queue next draw
|
||||
if (drawTimeout) clearTimeout(drawTimeout);
|
||||
drawTimeout = setTimeout(function()
|
||||
{
|
||||
drawTimeout = undefined;
|
||||
draw();
|
||||
}, 60000 - (Date.now() % 60000));
|
||||
}
|
||||
|
||||
|
||||
// Init the settings of the app
|
||||
loadSettings();
|
||||
|
||||
// Show launcher when middle button pressed
|
||||
Bangle.setUI(
|
||||
{
|
||||
mode : "clock",
|
||||
remove : function()
|
||||
{
|
||||
// Called to unload all of the clock app
|
||||
if (drawTimeout) clearTimeout(drawTimeout);
|
||||
drawTimeout = undefined;
|
||||
}
|
||||
});
|
||||
// Load widgets
|
||||
Bangle.loadWidgets();
|
||||
draw();
|
||||
setTimeout(Bangle.drawWidgets,0);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
// Change settings for BinaryClk
|
||||
|
||||
(function(back){
|
||||
|
||||
// color array -- don't change order unless change oder in BinaryClk.js
|
||||
let LED_ColorNames = ["white", "red", "green", "blue", "yellow", "magenta", "cyan", "black"];
|
||||
|
||||
var FILE = "BinaryClk.settings.json";
|
||||
// Load settings
|
||||
var settings = Object.assign({
|
||||
HourCol: "red",
|
||||
MinCol: "green",
|
||||
DayCol: "yellow",
|
||||
MonCol: "yellow",
|
||||
RingOn: true,
|
||||
}, require('Storage').readJSON(FILE, true) || {});
|
||||
|
||||
function writeSettings(){
|
||||
require('Storage').writeJSON(FILE, settings);
|
||||
}
|
||||
|
||||
// Helper method which uses int-based menu item for set of string values
|
||||
function stringItems(startvalue, writer, values) {
|
||||
return{
|
||||
value: (startvalue === undefined ? 0 : values.indexOf(startvalue)),
|
||||
format: v => values[v],
|
||||
min: 0,
|
||||
max: values.length - 1,
|
||||
wrap: true,
|
||||
step: 1,
|
||||
onchange: v => {
|
||||
writer(values[v]);
|
||||
writeSettings();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Helper method which breaks string set settings down to local settings object
|
||||
function stringInSettings(name, values) {
|
||||
return stringItems(settings[name], v => settings[name] = v, values);
|
||||
}
|
||||
|
||||
// Show the menu
|
||||
var mainmenu = {
|
||||
"" : {
|
||||
"title" : "BinaryCLK"
|
||||
},
|
||||
"< Back" : () => back(),
|
||||
'Color Hour.:': stringInSettings("HourCol", LED_ColorNames),
|
||||
'Color Minute:': stringInSettings("MinCol", LED_ColorNames),
|
||||
'Color Day': stringInSettings("DayCol", LED_ColorNames),
|
||||
'Color Month:': stringInSettings("MonCol", LED_ColorNames),
|
||||
'LED ring on/off': {
|
||||
value: (settings.RingOn !== undefined ? settings.RingOn : true),
|
||||
onchange: v => {
|
||||
settings.RingOn = v;
|
||||
writeSettings();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// Show submenues
|
||||
//var submenu1 = {
|
||||
//"": {
|
||||
// "title": "Show sub1..."
|
||||
//},
|
||||
//"< Back": () => E.showMenu(mainmenu),
|
||||
//"ItemName": stringInSettings("settingsVar", ["Yes", "No", "DontCare"]),
|
||||
//};
|
||||
|
||||
E.showMenu(mainmenu);
|
||||
})
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"HourCol": "red",
|
||||
"MinCol": "green",
|
||||
"DayCol": "yellow",
|
||||
"MonCol": "yellow",
|
||||
"RingOn": true
|
||||
}
|
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
"id":"blc",
|
||||
"name":"Binary LED Clock",
|
||||
"version": "0.10",
|
||||
"description": "Binary LED Clock with date",
|
||||
"version": "0.40",
|
||||
"description": "a binary LED-Clock with time and date and customizable LED-colors",
|
||||
"icon":"blc-icon.png",
|
||||
"screenshots": [{"url":"screenshot_blc.bmp"}],
|
||||
"screenshots": [{"url":"screenshot_blc_1.bmp"},{"url":"screenshot_blc_2.bmp"}],
|
||||
"type": "clock",
|
||||
"tags": "clock",
|
||||
"supports": ["BANGLEJS2"],
|
||||
|
@ -12,6 +12,8 @@
|
|||
"readme": "README.md",
|
||||
"storage": [
|
||||
{"name":"blc.app.js","url":"blc.js"},
|
||||
{"name":"blc.settings.js","url":"blc.settings.js"},
|
||||
{"name":"blc.img","url":"blc-icon.js","evaluate":true}
|
||||
]
|
||||
],
|
||||
"data": [{"name":"blc.settings.json"}]
|
||||
}
|
||||
|
|
Before Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 91 KiB |
After Width: | Height: | Size: 91 KiB |
|
@ -70,5 +70,3 @@
|
|||
};
|
||||
return ci;
|
||||
})
|
||||
|
||||
|
||||
|
|
|
@ -72,3 +72,7 @@
|
|||
0.61: Instead of breaking execution with an Exception when updating boot, just use if..else (fix 'Uncaught undefined')
|
||||
0.62: Handle setting for configuring BLE privacy
|
||||
0.63: Only set BLE `display:1` if we have a passkey
|
||||
0.64: Automatically create .widcache and .clkinfocache to speed up loads
|
||||
Bangle.loadWidgets overwritten with fast version on success
|
||||
Refuse to work on firmware <2v16 and remove old polyfills
|
||||
0.65: Only display interpreter errors if log is nonzero
|
|
@ -12,14 +12,13 @@ if (DEBUG) {
|
|||
boot += "var _tm=Date.now()\n";
|
||||
bootPost += "delete _tm;";
|
||||
}
|
||||
if (require('Storage').hash) { // new in 2v11 - helps ensure files haven't changed
|
||||
let CRC = E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\.boot\.js/)+E.CRC32(process.env.GIT_COMMIT);
|
||||
boot += `if(E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\\.boot\\.js/)+E.CRC32(process.env.GIT_COMMIT)!=${CRC})`;
|
||||
} else {
|
||||
let CRC = E.CRC32(require('Storage').read('setting.json'))+E.CRC32(require('Storage').list(/\.boot\.js/))+E.CRC32(process.env.GIT_COMMIT);
|
||||
boot += `if(E.CRC32(require('Storage').read('setting.json'))+E.CRC32(require('Storage').list(/\\.boot\\.js/))+E.CRC32(process.env.GIT_COMMIT)!=${CRC})`;
|
||||
if (FWVERSION < 216) {
|
||||
E.showMessage(/*LANG*/"Please update Bangle.js firmware\n\nCurrent = "+process.env.VERSION,{title:"ERROR"});
|
||||
throw new Error("Old firmware");
|
||||
}
|
||||
boot += `{eval(require('Storage').read('bootupdate.js'));print("Storage Updated!")}else{\n`;
|
||||
let CRC = E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\.js$/)+E.CRC32(process.env.GIT_COMMIT);
|
||||
boot += `if(E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\\.js$/)+E.CRC32(process.env.GIT_COMMIT)!=${CRC})`;
|
||||
boot += `{eval(require('Storage').read('bootupdate.js'));}else{\n`;
|
||||
boot += `E.setFlags({pretokenise:1});\n`;
|
||||
boot += `var bleServices = {}, bleServiceOptions = { uart : true};\n`;
|
||||
bootPost += `NRF.setServices(bleServices,bleServiceOptions);delete bleServices,bleServiceOptions;\n`; // executed after other boot code
|
||||
|
@ -44,7 +43,7 @@ LoopbackA.setConsole(true);\n`;
|
|||
boot += `
|
||||
Bluetooth.line="";
|
||||
Bluetooth.on('data',function(d) {
|
||||
var l = (Bluetooth.line + d).split(/[\\n\\r]/);
|
||||
let l = (Bluetooth.line + d).split(/[\\n\\r]/);
|
||||
Bluetooth.line = l.pop();
|
||||
l.forEach(n=>Bluetooth.emit("line",n));
|
||||
});
|
||||
|
@ -67,12 +66,12 @@ if (s.ble===false) boot += `if (!NRF.getSecurityStatus().connected) NRF.sleep();
|
|||
if (s.timeout!==undefined) boot += `Bangle.setLCDTimeout(${s.timeout});\n`;
|
||||
if (!s.timeout) boot += `Bangle.setLCDPower(1);\n`;
|
||||
boot += `E.setTimeZone(${s.timezone});`;
|
||||
// Draw out of memory errors onto the screen
|
||||
boot += `E.on('errorFlag', function(errorFlags) {
|
||||
// Draw out of memory errors onto the screen if logging enabled
|
||||
if (s.log) boot += `E.on('errorFlag', function(errorFlags) {
|
||||
g.reset(1).setColor("#ff0000").setFont("6x8").setFontAlign(0,1).drawString(errorFlags,g.getWidth()/2,g.getHeight()-1).flip();
|
||||
print("Interpreter error:", errorFlags);
|
||||
E.getErrorFlags(); // clear flags so we get called next time
|
||||
});\n`;
|
||||
E.getErrorFlags();
|
||||
});\n`;// E.getErrorFlags() -> clear flags so we get called next time
|
||||
// stop users doing bad things!
|
||||
if (global.save) boot += `global.save = function() { throw new Error("You can't use save() on Bangle.js without overwriting the bootloader!"); }\n`;
|
||||
// Apply any settings-specific stuff
|
||||
|
@ -86,30 +85,18 @@ if (s.bleprivacy || (s.passkey!==undefined && s.passkey.length==6)) {
|
|||
if (s.blename === false) boot+=`NRF.setAdvertising({},{showName:false});\n`;
|
||||
if (s.whitelist && !s.whitelist_disabled) boot+=`NRF.on('connect', function(addr) { if (!NRF.ignoreWhitelist) { let whitelist = (require('Storage').readJSON('setting.json',1)||{}).whitelist; if (NRF.resolveAddress !== undefined) { let resolvedAddr = NRF.resolveAddress(addr); if (resolvedAddr !== undefined) addr = resolvedAddr + " (resolved)"; } if (!whitelist.includes(addr)) NRF.disconnect(); }});\n`;
|
||||
if (s.rotate) boot+=`g.setRotation(${s.rotate&3},${s.rotate>>2});\n` // screen rotation
|
||||
boot+=`Bangle.loadWidgets=function(){if(!global.WIDGETS)eval(require("Storage").read(".widcache"))};\n`;
|
||||
// ================================================== FIXING OLDER FIRMWARES
|
||||
if (FWVERSION<215.068) // 2v15.68 and before had compass heading inverted.
|
||||
boot += `Bangle.on('mag',e=>{if(!isNaN(e.heading))e.heading=360-e.heading;});
|
||||
Bangle.getCompass=(c=>(()=>{e=c();if(!isNaN(e.heading))e.heading=360-e.heading;return e;}))(Bangle.getCompass);`;
|
||||
|
||||
// deleting stops us getting confused by our own decl. builtins can't be deleted
|
||||
// this is a polyfill without fastloading capability
|
||||
delete Bangle.showClock;
|
||||
if (!Bangle.showClock) boot += `Bangle.showClock = ()=>{load(".bootcde")};\n`;
|
||||
delete Bangle.load;
|
||||
if (!Bangle.load) boot += `Bangle.load = load;\n`;
|
||||
let date = new Date();
|
||||
delete date.toLocalISOString; // toLocalISOString was only introduced in 2v15
|
||||
if (!date.toLocalISOString) boot += `Date.prototype.toLocalISOString = function() {
|
||||
var o = this.getTimezoneOffset();
|
||||
var d = new Date(this.getTime() - o*60000);
|
||||
var sign = o>0?"-":"+";
|
||||
o = Math.abs(o);
|
||||
return d.toISOString().slice(0,-1)+sign+Math.floor(o/60).toString().padStart(2,0)+(o%60).toString().padStart(2,0);
|
||||
};\n`;
|
||||
|
||||
// show timings
|
||||
if (DEBUG) boot += `print(".boot0",0|(Date.now()-_tm),"ms");_tm=Date.now();\n`
|
||||
// ================================================== BOOT.JS
|
||||
// ================================================== .BOOT0
|
||||
// Append *.boot.js files.
|
||||
// Name files with a number - eg 'foo.5.boot.js' to enforce order (lowest first). Numbered files get placed before non-numbered
|
||||
// These could change bleServices/bleServiceOptions if needed
|
||||
|
@ -128,17 +115,47 @@ let bootFiles = require('Storage').list(/\.boot\.js$/).sort((a,b)=>{
|
|||
});
|
||||
// precalculate file size
|
||||
bootPost += "}";
|
||||
let fileSize = boot.length + bootPost.length;
|
||||
bootFiles.forEach(bootFile=>{
|
||||
// match the size of data we're adding below in bootFiles.forEach
|
||||
if (DEBUG) fileSize += 2+bootFile.length+1; // `//${bootFile}\n` comment
|
||||
fileSize += require('Storage').read(bootFile).length+2; // boot code plus ";\n"
|
||||
if (DEBUG) fileSize += 48+E.toJS(bootFile).length; // `print(${E.toJS(bootFile)},0|(Date.now()-_tm),"ms");_tm=Date.now();\n`
|
||||
});
|
||||
// write file in chunks (so as not to use up all RAM)
|
||||
require('Storage').write('.boot0',boot,0,fileSize);
|
||||
let fileOffset = boot.length;
|
||||
bootFiles.forEach(bootFile=>{
|
||||
let fileOffset,fileSize;
|
||||
/* code to output a file, plus preable and postable
|
||||
when called with dst==undefined it just increments
|
||||
fileOffset so we can see ho wbig the file has to be */
|
||||
let outputFile = (dst,src,pre,post) => {"ram";
|
||||
if (DEBUG) {
|
||||
if (dst) require('Storage').write(dst,`//${src}\n`,fileOffset);
|
||||
fileOffset+=2+src.length+1;
|
||||
}
|
||||
if (pre) {
|
||||
if (dst) require('Storage').write(dst,pre,fileOffset);
|
||||
fileOffset+=pre.length;
|
||||
}
|
||||
let f = require('Storage').read(src);
|
||||
if (src.endsWith("clkinfo.js") && f[0]!="(") {
|
||||
/* we shouldn't have to do this but it seems sometimes (sched 0.28) folks have
|
||||
used libraries which get added into the clockinfo, and we can't use them directly
|
||||
to we have to revert back to eval */
|
||||
f = `eval(require('Storage').read(${E.toJS(src)}))`;
|
||||
}
|
||||
if (dst) {
|
||||
// we can't just write 'f' in one go because it can be too big
|
||||
let len = f.length;
|
||||
let offset = 0;
|
||||
while (len) {
|
||||
let chunk = Math.min(len, 2048);
|
||||
require('Storage').write(dst,f.substr(offset, chunk),fileOffset);
|
||||
fileOffset+=chunk;
|
||||
offset+=chunk;
|
||||
len-=chunk;
|
||||
}
|
||||
} else
|
||||
fileOffset+=f.length;
|
||||
if (dst) require('Storage').write(dst,post,fileOffset);
|
||||
fileOffset+=post.length;
|
||||
if (DEBUG) {
|
||||
if (dst) require('Storage').write(dst,`print(${E.toJS(src)},0|(Date.now()-_tm),"ms");_tm=Date.now();\n`,fileOffset);
|
||||
fileOffset += 48+E.toJS(src).length;
|
||||
}
|
||||
};
|
||||
let outputFileComplete = (dst,fn) => {
|
||||
// we add a semicolon so if the file is wrapped in (function(){ ... }()
|
||||
// with no semicolon we don't end up with (function(){ ... }()(function(){ ... }()
|
||||
// which would cause an error!
|
||||
|
@ -146,31 +163,48 @@ bootFiles.forEach(bootFile=>{
|
|||
// "//"+bootFile+"\n"+require('Storage').read(bootFile)+";\n";
|
||||
// but we need to do this without ever loading everything into RAM as some
|
||||
// boot files seem to be getting pretty big now.
|
||||
if (DEBUG) {
|
||||
require('Storage').write('.boot0',`//${bootFile}\n`,fileOffset);
|
||||
fileOffset+=2+bootFile.length+1;
|
||||
}
|
||||
let bf = require('Storage').read(bootFile);
|
||||
// we can't just write 'bf' in one go because at least in 2v13 and earlier
|
||||
// Espruino wants to read the whole file into RAM first, and on Bangle.js 1
|
||||
// it can be too big (especially BTHRM).
|
||||
let bflen = bf.length;
|
||||
let bfoffset = 0;
|
||||
while (bflen) {
|
||||
let bfchunk = Math.min(bflen, 2048);
|
||||
require('Storage').write('.boot0',bf.substr(bfoffset, bfchunk),fileOffset);
|
||||
fileOffset+=bfchunk;
|
||||
bfoffset+=bfchunk;
|
||||
bflen-=bfchunk;
|
||||
}
|
||||
require('Storage').write('.boot0',";\n",fileOffset);
|
||||
fileOffset+=2;
|
||||
if (DEBUG) {
|
||||
require('Storage').write('.boot0',`print(${E.toJS(bootFile)},0|(Date.now()-_tm),"ms");_tm=Date.now();\n`,fileOffset);
|
||||
fileOffset += 48+E.toJS(bootFile).length
|
||||
}
|
||||
});
|
||||
outputFile(dst,fn,"",";\n");
|
||||
};
|
||||
fileOffset = boot.length + bootPost.length;
|
||||
bootFiles.forEach(fn=>outputFileComplete(undefined,fn)); // just get sizes
|
||||
fileSize = fileOffset;
|
||||
require('Storage').write('.boot0',boot,0,fileSize);
|
||||
fileOffset = boot.length;
|
||||
bootFiles.forEach(fn=>outputFileComplete('.boot0',fn));
|
||||
require('Storage').write('.boot0',bootPost,fileOffset);
|
||||
delete boot,bootPost,bootFiles;
|
||||
// ================================================== .WIDCACHE for widgets
|
||||
let widgetFiles = require("Storage").list(/\.wid\.js$/);
|
||||
let widget = `// Made by bootupdate.js\nglobal.WIDGETS={};`, widgetPost = `var W=WIDGETS;WIDGETS={};
|
||||
Object.keys(W).sort((a,b)=>(0|W[b].sortorder)-(0|W[a].sortorder)).forEach(k=>WIDGETS[k]=W[k]);`; // sort
|
||||
if (DEBUG) widget+="var _tm=Date.now();";
|
||||
outputFileComplete = (dst,fn) => {
|
||||
outputFile(dst,fn,"try{",`}catch(e){print(${E.toJS(fn)},e,e.stack)}\n`);
|
||||
};
|
||||
fileOffset = widget.length + widgetPost.length;
|
||||
widgetFiles.forEach(fn=>outputFileComplete(undefined,fn)); // just get sizes
|
||||
fileSize = fileOffset;
|
||||
require('Storage').write('.widcache',widget,0,fileSize);
|
||||
fileOffset = widget.length;
|
||||
widgetFiles.forEach(fn=>outputFileComplete('.widcache',fn));
|
||||
require('Storage').write('.widcache',widgetPost,fileOffset);
|
||||
delete widget,widgetPost,widgetFiles;
|
||||
// ================================================== .clkinfocache for clockinfos
|
||||
let ciFiles = require("Storage").list(/\.clkinfo\.js$/);
|
||||
let ci = `// Made by bootupdate.js\n`;
|
||||
if (DEBUG) ci+="var _tm=Date.now();";
|
||||
outputFileComplete = (dst,fn) => {
|
||||
outputFile(dst,fn,"try{let fn=",`;let a=fn(),b=menu.find(x=>x.name===a.name);if(b)b.items=b.items.concat(a.items)else menu=menu.concat(a);}catch(e){print(${E.toJS(fn)},e,e.stack)}\n`);
|
||||
};
|
||||
fileOffset = ci.length;
|
||||
ciFiles.forEach(fn=>outputFileComplete(undefined,fn)); // just get sizes
|
||||
fileSize = fileOffset;
|
||||
require('Storage').write('.clkinfocache',ci,0,fileSize);
|
||||
fileOffset = ci.length;
|
||||
ciFiles.forEach(fn=>outputFileComplete('.clkinfocache',fn));
|
||||
delete ci,ciFiles;
|
||||
// test with require("clock_info").load()
|
||||
// ================================================== END
|
||||
E.showMessage(/*LANG*/"Reloading...");
|
||||
}
|
||||
// .bootcde should be run automatically after if required, since
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "boot",
|
||||
"name": "Bootloader",
|
||||
"version": "0.63",
|
||||
"version": "0.65",
|
||||
"description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings",
|
||||
"icon": "bootloader.png",
|
||||
"type": "bootloader",
|
||||
|
@ -11,6 +11,9 @@
|
|||
{"name":".boot0","url":"boot0.js"},
|
||||
{"name":".bootcde","url":"bootloader.js"},
|
||||
{"name":"bootupdate.js","url":"bootupdate.js"}
|
||||
],"data": [
|
||||
{"name":".widcache"},
|
||||
{"name":".clkinfocache"}
|
||||
],
|
||||
"sortorder": -10
|
||||
}
|
||||
|
|
|
@ -3,4 +3,14 @@
|
|||
0.03: Allows showing the month in short or long format by setting `"shortMonth"` to true or false
|
||||
0.04: Improves touchscreen drag handling for background apps such as Pattern Launcher
|
||||
0.05: Fixes step count not resetting after a new day starts
|
||||
0.06 Added clockbackground app functionality
|
||||
0.06: Added clockbackground app functionality
|
||||
0.07: Allow custom backgrounds per boxclk config and from the clockbg module
|
||||
0.08: Improves performance, responsiveness, and bug fixes
|
||||
- [+] Added box size caching to reduce calculations
|
||||
- [+] Improved step count with real-time updates
|
||||
- [+] Improved battery level update logic to reduce unnecessary refreshes
|
||||
- [+] Fixed optional seconds not displaying in time
|
||||
- [+] Fixed drag handler by adding E.stopEventPropagation()
|
||||
- [+] General code optimization and cleanup
|
||||
0.09: Revised event handler code
|
||||
0.10: Revised suffix formatting in getDate function
|
|
@ -1,58 +1,123 @@
|
|||
{
|
||||
/**
|
||||
* ---------------------------------------------------------------
|
||||
* 1. Module dependencies and initial configurations
|
||||
* ---------------------------------------------------------------
|
||||
*/
|
||||
|
||||
// 1. Module dependencies and initial configurations
|
||||
let background = require("clockbg");
|
||||
let storage = require("Storage");
|
||||
let locale = require("locale");
|
||||
let widgets = require("widget_utils");
|
||||
let date = new Date();
|
||||
let bgImage;
|
||||
let configNumber = (storage.readJSON("boxclk.json", 1) || {}).selectedConfig || 0;
|
||||
let fileName = 'boxclk' + (configNumber > 0 ? `-${configNumber}` : '') + '.json';
|
||||
// Add a condition to check if the file exists, if it does not, default to 'boxclk.json'
|
||||
if (!storage.read(fileName)) {
|
||||
fileName = 'boxclk.json';
|
||||
}
|
||||
let boxesConfig = storage.readJSON(fileName, 1) || {};
|
||||
let boxes = {};
|
||||
let boxPos = {};
|
||||
let isDragging = {};
|
||||
let wasDragging = {};
|
||||
let isDragging = false;
|
||||
let doubleTapTimer = null;
|
||||
let g_setColor;
|
||||
|
||||
let saveIcon = require("heatshrink").decompress(atob("mEwwkEogA/AHdP/4AK+gWVDBQWNAAIuVGBAIB+UQdhMfGBAHBCxUAgIXHIwPyCxQwEJAgXB+MAl/zBwQGBn8ggQjBGAQXG+EA/4XI/8gBIQXTGAMPC6n/C6HzkREBC6YACC6QAFC57aHCYIXOOgLsEn4XPABIX/C6vykQAEl6/WgCQBC5imFAAT2BC5gCBI4oUCC5x0IC/4X/C4K8Bl4XJ+TCCC4wKBABkvC4tEEoMQCxcBB4IWEC4XyDBUBFwIXGJAIAOIwowDABoWGGB4uHDBwWJAH4AzA"));
|
||||
|
||||
/**
|
||||
* ---------------------------------------------------------------
|
||||
* 2. Graphical and visual configurations
|
||||
* ---------------------------------------------------------------
|
||||
*/
|
||||
|
||||
// 2. Graphical and visual configurations
|
||||
let w = g.getWidth();
|
||||
let h = g.getHeight();
|
||||
let totalWidth, totalHeight;
|
||||
let drawTimeout;
|
||||
|
||||
/**
|
||||
* ---------------------------------------------------------------
|
||||
* 3. Touchscreen Handlers
|
||||
* ---------------------------------------------------------------
|
||||
*/
|
||||
|
||||
let touchHandler;
|
||||
let dragHandler;
|
||||
let movementDistance = 0;
|
||||
|
||||
/**
|
||||
* ---------------------------------------------------------------
|
||||
* 4. Font loading function
|
||||
* ---------------------------------------------------------------
|
||||
*/
|
||||
// 3. Event handlers
|
||||
let touchHandler = function(zone, e) {
|
||||
let boxTouched = false;
|
||||
let touchedBox = null;
|
||||
|
||||
for (let boxKey in boxes) {
|
||||
if (touchInText(e, boxes[boxKey])) {
|
||||
touchedBox = boxKey;
|
||||
boxTouched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (boxTouched) {
|
||||
// Toggle the selected state of the touched box
|
||||
boxes[touchedBox].selected = !boxes[touchedBox].selected;
|
||||
|
||||
// Update isDragging based on whether any box is selected
|
||||
isDragging = Object.values(boxes).some(box => box.selected);
|
||||
|
||||
if (isDragging) {
|
||||
widgets.hide();
|
||||
} else {
|
||||
deselectAllBoxes();
|
||||
}
|
||||
} else {
|
||||
// If tapped outside any box, deselect all boxes
|
||||
deselectAllBoxes();
|
||||
}
|
||||
|
||||
// Always redraw after a touch event
|
||||
draw();
|
||||
|
||||
// Handle double tap for saving
|
||||
if (!boxTouched && !isDragging) {
|
||||
if (doubleTapTimer) {
|
||||
clearTimeout(doubleTapTimer);
|
||||
doubleTapTimer = null;
|
||||
for (let boxKey in boxes) {
|
||||
boxesConfig[boxKey].boxPos.x = (boxes[boxKey].pos.x / w).toFixed(3);
|
||||
boxesConfig[boxKey].boxPos.y = (boxes[boxKey].pos.y / h).toFixed(3);
|
||||
}
|
||||
storage.write(fileName, JSON.stringify(boxesConfig));
|
||||
displaySaveIcon();
|
||||
return;
|
||||
}
|
||||
|
||||
doubleTapTimer = setTimeout(() => {
|
||||
doubleTapTimer = null;
|
||||
}, 500);
|
||||
}
|
||||
};
|
||||
|
||||
let dragHandler = function(e) {
|
||||
if (!isDragging) return;
|
||||
|
||||
// Stop propagation of the drag event to prevent other handlers
|
||||
E.stopEventPropagation();
|
||||
|
||||
for (let key in boxes) {
|
||||
if (boxes[key].selected) {
|
||||
let boxItem = boxes[key];
|
||||
calcBoxSize(boxItem);
|
||||
let newX = boxItem.pos.x + e.dx;
|
||||
let newY = boxItem.pos.y + e.dy;
|
||||
|
||||
if (newX - boxItem.cachedSize.width / 2 >= 0 &&
|
||||
newX + boxItem.cachedSize.width / 2 <= w &&
|
||||
newY - boxItem.cachedSize.height / 2 >= 0 &&
|
||||
newY + boxItem.cachedSize.height / 2 <= h) {
|
||||
boxItem.pos.x = newX;
|
||||
boxItem.pos.y = newY;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
draw();
|
||||
};
|
||||
|
||||
let stepHandler = function(up) {
|
||||
if (boxes.step && !isDragging) {
|
||||
boxes.step.string = formatStr(boxes.step, Bangle.getHealthStatus("day").steps);
|
||||
boxes.step.cachedSize = null;
|
||||
draw();
|
||||
}
|
||||
};
|
||||
|
||||
let lockHandler = function(isLocked) {
|
||||
if (isLocked) {
|
||||
deselectAllBoxes();
|
||||
draw();
|
||||
}
|
||||
};
|
||||
|
||||
// 4. Font loading function
|
||||
let loadCustomFont = function() {
|
||||
Graphics.prototype.setFontBrunoAce = function() {
|
||||
// Actual height 23 (24 - 2)
|
||||
|
@ -60,42 +125,43 @@
|
|||
E.toString(require('heatshrink').decompress(atob('ABMHwADBh4DKg4bKgIPDAYUfAYV/AYX/AQMD/gmC+ADBn/AByE/GIU8AYUwLxcfAYX/8AnB//4JIP/FgMP4F+CQQBBjwJBFYRbBAd43DHoJpBh/g/xPEK4ZfDgEEORKDDAY8////wADLfZrTCgITBnhEBAYJMBAYMPw4DCM4QDjhwDCjwDBn0+AYMf/gDBh/4AYMH+ADBLpc4ToK/NGYZfnAYcfL4U/x5fBW4LvB/7vC+LvBgHAsBfIn76Cn4WBcYQDFEgJ+CQQYDyH4L/BAZbHLNYjjCAZc8ngDunycBZ4KkBa4KwBnEHY4UB+BfMgf/ZgMH/4XBc4cf4F/gE+ZgRjwAYcfj5jBM4U4M4RQBM4UA8BjIngDFEYJ8BAYUDAYQvCM4ZxBC4V+AYQvBnkBQ4M8gabBJQPAI4WAAYM/GYQaBAYJKCnqyCn5OCn4aBAYIaBAYJPCU4IABnBhIuDXCFAMD+Z/BY4IDBQwOPwEfv6TDAYUPAcwrDAYQ7BAYY/BI4cD8bLCK4RfEAA0BRYTeDcwIrFn0Pw43Bg4DugYDBjxBBU4SvDMYMH/5QBgP/LAQAP8EHN4UPwADHB4YAHA'))),
|
||||
46,
|
||||
atob("CBEdChgYGhgaGBsaCQ=="),
|
||||
32|65536
|
||||
32 | 65536
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* ---------------------------------------------------------------
|
||||
* 5. Initial settings of boxes and their positions
|
||||
* ---------------------------------------------------------------
|
||||
// 5. Initial settings of boxes and their positions
|
||||
let isBool = (val, defaultVal) => val !== undefined ? Boolean(val) : defaultVal;
|
||||
|
||||
for (let key in boxesConfig) {
|
||||
if (key === 'bg' && boxesConfig[key].img) {
|
||||
bgImage = storage.read(boxesConfig[key].img);
|
||||
} else if (key !== 'selectedConfig') {
|
||||
boxes[key] = Object.assign({}, boxesConfig[key]);
|
||||
// Set default values for short, shortMonth, and disableSuffix
|
||||
boxes[key].short = isBool(boxes[key].short, true);
|
||||
boxes[key].shortMonth = isBool(boxes[key].shortMonth, true);
|
||||
boxes[key].disableSuffix = isBool(boxes[key].disableSuffix, false);
|
||||
|
||||
// Set box position
|
||||
boxes[key].pos = {
|
||||
x: w * boxes[key].boxPos.x,
|
||||
y: h * boxes[key].boxPos.y
|
||||
};
|
||||
// Cache box size
|
||||
boxes[key].cachedSize = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Text and drawing functions
|
||||
|
||||
/*
|
||||
Overwrite the setColor function to allow the
|
||||
use of (x) in g.theme.x as a string
|
||||
in your JSON config ("fg", "bg", "fg2", "bg2", "fgH", "bgH")
|
||||
*/
|
||||
|
||||
let boxKeys = Object.keys(boxes);
|
||||
|
||||
boxKeys.forEach((key) => {
|
||||
let boxConfig = boxes[key];
|
||||
boxPos[key] = {
|
||||
x: w * boxConfig.boxPos.x,
|
||||
y: h * boxConfig.boxPos.y
|
||||
};
|
||||
isDragging[key] = false;
|
||||
wasDragging[key] = false;
|
||||
});
|
||||
|
||||
/**
|
||||
* ---------------------------------------------------------------
|
||||
* 6. Text and drawing functions
|
||||
* ---------------------------------------------------------------
|
||||
*/
|
||||
|
||||
// Overwrite the setColor function to allow the
|
||||
// use of (x) in g.theme.x as a string
|
||||
// in your JSON config ("fg", "bg", "fg2", "bg2", "fgH", "bgH")
|
||||
let modSetColor = function() {
|
||||
// Save the original setColor function
|
||||
g_setColor = g.setColor;
|
||||
// Overwrite setColor with the new function
|
||||
g.setColor = function(color) {
|
||||
if (typeof color === "string" && color in g.theme) {
|
||||
g_setColor.call(g, g.theme[color]);
|
||||
|
@ -106,7 +172,6 @@
|
|||
};
|
||||
|
||||
let restoreSetColor = function() {
|
||||
// Restore the original setColor function
|
||||
if (g_setColor) {
|
||||
g.setColor = g_setColor;
|
||||
}
|
||||
|
@ -130,25 +195,6 @@
|
|||
}
|
||||
};
|
||||
|
||||
let calcBoxSize = function(boxItem) {
|
||||
g.reset();
|
||||
g.setFontAlign(0,0);
|
||||
g.setFont(boxItem.font, boxItem.fontSize);
|
||||
let strWidth = g.stringWidth(boxItem.string) + 2 * boxItem.outline;
|
||||
let fontHeight = g.getFontHeight() + 2 * boxItem.outline;
|
||||
totalWidth = strWidth + 2 * boxItem.xPadding;
|
||||
totalHeight = fontHeight + 2 * boxItem.yPadding;
|
||||
};
|
||||
|
||||
let calcBoxPos = function(boxKey) {
|
||||
return {
|
||||
x1: boxPos[boxKey].x - totalWidth / 2,
|
||||
y1: boxPos[boxKey].y - totalHeight / 2,
|
||||
x2: boxPos[boxKey].x + totalWidth / 2,
|
||||
y2: boxPos[boxKey].y + totalHeight / 2
|
||||
};
|
||||
};
|
||||
|
||||
let displaySaveIcon = function() {
|
||||
draw(boxes);
|
||||
g.drawImage(saveIcon, w / 2 - 24, h / 2 - 24);
|
||||
|
@ -159,33 +205,26 @@
|
|||
}, 2000);
|
||||
};
|
||||
|
||||
/**
|
||||
* ---------------------------------------------------------------
|
||||
* 7. String forming helper functions
|
||||
* ---------------------------------------------------------------
|
||||
*/
|
||||
|
||||
let isBool = function(val, defaultVal) {
|
||||
return typeof val !== 'undefined' ? Boolean(val) : defaultVal;
|
||||
};
|
||||
|
||||
// 7. String forming helper functions
|
||||
let getDate = function(short, shortMonth, disableSuffix) {
|
||||
const date = new Date();
|
||||
const dayOfMonth = date.getDate();
|
||||
const day = date.getDate();
|
||||
const month = shortMonth ? locale.month(date, 1) : locale.month(date, 0);
|
||||
const year = date.getFullYear();
|
||||
let suffix;
|
||||
if ([1, 21, 31].includes(dayOfMonth)) {
|
||||
suffix = "st";
|
||||
} else if ([2, 22].includes(dayOfMonth)) {
|
||||
suffix = "nd";
|
||||
} else if ([3, 23].includes(dayOfMonth)) {
|
||||
suffix = "rd";
|
||||
} else {
|
||||
suffix = "th";
|
||||
}
|
||||
let dayOfMonthStr = disableSuffix ? dayOfMonth : dayOfMonth + suffix;
|
||||
return month + " " + dayOfMonthStr + (short ? '' : (", " + year)); // not including year for short version
|
||||
|
||||
const getSuffix = (day) => {
|
||||
if (day >= 11 && day <= 13) return 'th';
|
||||
const lastDigit = day % 10;
|
||||
switch (lastDigit) {
|
||||
case 1: return 'st';
|
||||
case 2: return 'nd';
|
||||
case 3: return 'rd';
|
||||
default: return 'th';
|
||||
}
|
||||
};
|
||||
|
||||
const dayStr = disableSuffix ? day : `${day}${getSuffix(day)}`;
|
||||
return `${month} ${dayStr}${short ? '' : `, ${year}`}`; // not including year for short version
|
||||
};
|
||||
|
||||
let getDayOfWeek = function(date, short) {
|
||||
|
@ -198,187 +237,215 @@
|
|||
return short ? meridian[0] : meridian;
|
||||
};
|
||||
|
||||
let modString = function(boxItem, data) {
|
||||
let prefix = boxItem.prefix || '';
|
||||
let suffix = boxItem.suffix || '';
|
||||
return prefix + data + suffix;
|
||||
let formatStr = function(boxItem, data) {
|
||||
return `${boxItem.prefix || ''}${data}${boxItem.suffix || ''}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* ---------------------------------------------------------------
|
||||
* 8. Main draw function
|
||||
* ---------------------------------------------------------------
|
||||
*/
|
||||
// 8. Main draw function and update logic
|
||||
let lastDay = -1;
|
||||
const BATTERY_UPDATE_INTERVAL = 300000;
|
||||
|
||||
let draw = (function() {
|
||||
let updatePerMinute = true; // variable to track the state of time display
|
||||
let updateBoxData = function() {
|
||||
let date = new Date();
|
||||
let currentDay = date.getDate();
|
||||
let now = Date.now();
|
||||
|
||||
return function(boxes) {
|
||||
date = new Date();
|
||||
g.clear();
|
||||
background.fillRect(Bangle.appRect);
|
||||
if (boxes.time || boxes.meridian || boxes.date || boxes.dow) {
|
||||
if (boxes.time) {
|
||||
boxes.time.string = modString(boxes.time, locale.time(date, isBool(boxes.time.short, true) ? 1 : 0));
|
||||
updatePerMinute = isBool(boxes.time.short, true);
|
||||
}
|
||||
if (boxes.meridian) {
|
||||
boxes.meridian.string = modString(boxes.meridian, locale.meridian(date, isBool(boxes.meridian.short, true)));
|
||||
}
|
||||
if (boxes.date) {
|
||||
boxes.date.string = (
|
||||
modString(boxes.date,
|
||||
getDate(isBool(boxes.date.short, true),
|
||||
isBool(boxes.date.shortMonth, true),
|
||||
isBool(boxes.date.disableSuffix, false)
|
||||
)));
|
||||
}
|
||||
if (boxes.dow) {
|
||||
boxes.dow.string = modString(boxes.dow, getDayOfWeek(date, isBool(boxes.dow.short, true)));
|
||||
}
|
||||
if (boxes.batt) {
|
||||
boxes.batt.string = modString(boxes.batt, E.getBattery());
|
||||
}
|
||||
if (boxes.step) {
|
||||
boxes.step.string = modString(boxes.step, Bangle.getHealthStatus("day").steps);
|
||||
}
|
||||
boxKeys.forEach((boxKey) => {
|
||||
let boxItem = boxes[boxKey];
|
||||
calcBoxSize(boxItem);
|
||||
const pos = calcBoxPos(boxKey);
|
||||
if (isDragging[boxKey]) {
|
||||
g.setColor(boxItem.border);
|
||||
g.drawRect(pos.x1, pos.y1, pos.x2, pos.y2);
|
||||
let showSeconds = !boxes.time.short;
|
||||
let timeString = locale.time(date, 1).trim();
|
||||
if (showSeconds) {
|
||||
let seconds = date.getSeconds().toString().padStart(2, '0');
|
||||
timeString += ':' + seconds;
|
||||
}
|
||||
let newTimeString = formatStr(boxes.time, timeString);
|
||||
if (newTimeString !== boxes.time.string) {
|
||||
boxes.time.string = newTimeString;
|
||||
boxes.time.cachedSize = null;
|
||||
}
|
||||
g.drawString(
|
||||
boxItem,
|
||||
boxItem.string,
|
||||
boxPos[boxKey].x + boxItem.xOffset,
|
||||
boxPos[boxKey].y + boxItem.yOffset
|
||||
);
|
||||
});
|
||||
if (!Object.values(isDragging).some(Boolean)) {
|
||||
if (drawTimeout) clearTimeout(drawTimeout);
|
||||
let interval = updatePerMinute ? 60000 - (Date.now() % 60000) : 1000;
|
||||
drawTimeout = setTimeout(() => draw(boxes), interval);
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
/**
|
||||
* ---------------------------------------------------------------
|
||||
* 9. Helper function for touch event
|
||||
* ---------------------------------------------------------------
|
||||
*/
|
||||
if (boxes.meridian) {
|
||||
let newMeridianString = formatStr(boxes.meridian, locale.meridian(date, boxes.meridian.short));
|
||||
if (newMeridianString !== boxes.meridian.string) {
|
||||
boxes.meridian.string = newMeridianString;
|
||||
boxes.meridian.cachedSize = null;
|
||||
}
|
||||
}
|
||||
|
||||
let touchInText = function(e, boxItem, boxKey) {
|
||||
if (boxes.date && currentDay !== lastDay) {
|
||||
let newDateString = formatStr(boxes.date,
|
||||
getDate(boxes.date.short,
|
||||
boxes.date.shortMonth,
|
||||
boxes.date.noSuffix)
|
||||
);
|
||||
if (newDateString !== boxes.date.string) {
|
||||
boxes.date.string = newDateString;
|
||||
boxes.date.cachedSize = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (boxes.dow) {
|
||||
let newDowString = formatStr(boxes.dow, getDayOfWeek(date, boxes.dow.short));
|
||||
if (newDowString !== boxes.dow.string) {
|
||||
boxes.dow.string = newDowString;
|
||||
boxes.dow.cachedSize = null;
|
||||
}
|
||||
}
|
||||
|
||||
lastDay = currentDay;
|
||||
}
|
||||
|
||||
if (boxes.step) {
|
||||
let newStepCount = Bangle.getHealthStatus("day").steps;
|
||||
let newStepString = formatStr(boxes.step, newStepCount);
|
||||
if (newStepString !== boxes.step.string) {
|
||||
boxes.step.string = newStepString;
|
||||
boxes.step.cachedSize = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (boxes.batt) {
|
||||
if (!boxes.batt.lastUpdate || now - boxes.batt.lastUpdate >= BATTERY_UPDATE_INTERVAL) {
|
||||
let currentLevel = E.getBattery();
|
||||
if (currentLevel !== boxes.batt.lastLevel) {
|
||||
let newBattString = formatStr(boxes.batt, currentLevel);
|
||||
if (newBattString !== boxes.batt.string) {
|
||||
boxes.batt.string = newBattString;
|
||||
boxes.batt.cachedSize = null;
|
||||
boxes.batt.lastLevel = currentLevel;
|
||||
}
|
||||
}
|
||||
boxes.batt.lastUpdate = now;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let draw = function() {
|
||||
g.clear();
|
||||
|
||||
// Always draw backgrounds full screen
|
||||
if (bgImage) { // Check for bg in boxclk config
|
||||
g.drawImage(bgImage, 0, 0);
|
||||
} else { // Otherwise use clockbg module
|
||||
background.fillRect(0, 0, g.getWidth(), g.getHeight());
|
||||
}
|
||||
|
||||
if (!isDragging) {
|
||||
updateBoxData();
|
||||
}
|
||||
|
||||
for (let boxKey in boxes) {
|
||||
let boxItem = boxes[boxKey];
|
||||
|
||||
// Set font and alignment for each box individually
|
||||
g.setFont(boxItem.font, boxItem.fontSize);
|
||||
g.setFontAlign(0, 0);
|
||||
|
||||
calcBoxSize(boxItem);
|
||||
|
||||
const pos = calcBoxPos(boxItem);
|
||||
|
||||
if (boxItem.selected) {
|
||||
g.setColor(boxItem.border);
|
||||
g.drawRect(pos.x1, pos.y1, pos.x2, pos.y2);
|
||||
}
|
||||
|
||||
g.drawString(
|
||||
boxItem,
|
||||
boxItem.string,
|
||||
boxItem.pos.x + boxItem.xOffset,
|
||||
boxItem.pos.y + boxItem.yOffset
|
||||
);
|
||||
}
|
||||
|
||||
if (!isDragging) {
|
||||
if (drawTimeout) clearTimeout(drawTimeout);
|
||||
let updateInterval = boxes.time && !isBool(boxes.time.short, true) ? 1000 : 60000 - (Date.now() % 60000);
|
||||
drawTimeout = setTimeout(draw, updateInterval);
|
||||
}
|
||||
};
|
||||
|
||||
// 9. Helper function for touch event
|
||||
let calcBoxPos = function(boxItem) {
|
||||
calcBoxSize(boxItem);
|
||||
const pos = calcBoxPos(boxKey);
|
||||
return {
|
||||
x1: boxItem.pos.x - boxItem.cachedSize.width / 2,
|
||||
y1: boxItem.pos.y - boxItem.cachedSize.height / 2,
|
||||
x2: boxItem.pos.x + boxItem.cachedSize.width / 2,
|
||||
y2: boxItem.pos.y + boxItem.cachedSize.height / 2
|
||||
};
|
||||
};
|
||||
|
||||
// Use cached size if available, otherwise calculate and cache
|
||||
let calcBoxSize = function(boxItem) {
|
||||
if (boxItem.cachedSize) {
|
||||
return boxItem.cachedSize;
|
||||
}
|
||||
|
||||
g.setFont(boxItem.font, boxItem.fontSize);
|
||||
g.setFontAlign(0, 0);
|
||||
|
||||
let strWidth = g.stringWidth(boxItem.string) + 2 * boxItem.outline;
|
||||
let fontHeight = g.getFontHeight() + 2 * boxItem.outline;
|
||||
let totalWidth = strWidth + 2 * boxItem.xPadding;
|
||||
let totalHeight = fontHeight + 2 * boxItem.yPadding;
|
||||
|
||||
boxItem.cachedSize = {
|
||||
width: totalWidth,
|
||||
height: totalHeight
|
||||
};
|
||||
|
||||
return boxItem.cachedSize;
|
||||
};
|
||||
|
||||
let touchInText = function(e, boxItem) {
|
||||
calcBoxSize(boxItem);
|
||||
const pos = calcBoxPos(boxItem);
|
||||
return e.x >= pos.x1 &&
|
||||
e.x <= pos.x2 &&
|
||||
e.y >= pos.y1 &&
|
||||
e.y <= pos.y2;
|
||||
e.x <= pos.x2 &&
|
||||
e.y >= pos.y1 &&
|
||||
e.y <= pos.y2;
|
||||
};
|
||||
|
||||
let deselectAllBoxes = function() {
|
||||
Object.keys(isDragging).forEach((boxKey) => {
|
||||
isDragging[boxKey] = false;
|
||||
});
|
||||
isDragging = false;
|
||||
for (let boxKey in boxes) {
|
||||
boxes[boxKey].selected = false;
|
||||
}
|
||||
restoreSetColor();
|
||||
widgets.show();
|
||||
widgets.swipeOn();
|
||||
modSetColor();
|
||||
};
|
||||
|
||||
/**
|
||||
* ---------------------------------------------------------------
|
||||
* 10. Setup function to configure event handlers
|
||||
* ---------------------------------------------------------------
|
||||
*/
|
||||
|
||||
// 10. Setup function to configure event handlers
|
||||
let setup = function() {
|
||||
// ------------------------------------
|
||||
// Define the touchHandler function
|
||||
// ------------------------------------
|
||||
touchHandler = function(zone, e) {
|
||||
wasDragging = Object.assign({}, isDragging);
|
||||
let boxTouched = false;
|
||||
boxKeys.forEach((boxKey) => {
|
||||
if (touchInText(e, boxes[boxKey], boxKey)) {
|
||||
isDragging[boxKey] = true;
|
||||
wasDragging[boxKey] = true;
|
||||
boxTouched = true;
|
||||
}
|
||||
});
|
||||
if (!boxTouched) {
|
||||
if (!Object.values(isDragging).some(Boolean)) { // check if no boxes are being dragged
|
||||
deselectAllBoxes();
|
||||
if (doubleTapTimer) {
|
||||
clearTimeout(doubleTapTimer);
|
||||
doubleTapTimer = null;
|
||||
// Save boxesConfig on double tap outside of any box and when no boxes are being dragged
|
||||
Object.keys(boxPos).forEach((boxKey) => {
|
||||
boxesConfig[boxKey].boxPos.x = (boxPos[boxKey].x / w).toFixed(3);
|
||||
boxesConfig[boxKey].boxPos.y = (boxPos[boxKey].y / h).toFixed(3);
|
||||
});
|
||||
storage.write(fileName, JSON.stringify(boxesConfig));
|
||||
displaySaveIcon();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// if any box is being dragged, just deselect all without saving
|
||||
deselectAllBoxes();
|
||||
}
|
||||
}
|
||||
if (Object.values(wasDragging).some(Boolean) || !boxTouched) {
|
||||
draw(boxes);
|
||||
}
|
||||
doubleTapTimer = setTimeout(() => {
|
||||
doubleTapTimer = null;
|
||||
}, 500); // Increase or decrease this value based on the desired double tap timing
|
||||
movementDistance = 0;
|
||||
};
|
||||
|
||||
// ------------------------------------
|
||||
// Define the dragHandler function
|
||||
// ------------------------------------
|
||||
dragHandler = function(e) {
|
||||
// Check if any box is being dragged
|
||||
if (!Object.values(isDragging).some(Boolean)) return;
|
||||
// Calculate the movement distance
|
||||
movementDistance += Math.abs(e.dx) + Math.abs(e.dy);
|
||||
// Check if the movement distance exceeds a threshold
|
||||
if (movementDistance > 1) {
|
||||
boxKeys.forEach((boxKey) => {
|
||||
if (isDragging[boxKey]) {
|
||||
widgets.hide();
|
||||
let boxItem = boxes[boxKey];
|
||||
calcBoxSize(boxItem);
|
||||
let newX = boxPos[boxKey].x + e.dx;
|
||||
let newY = boxPos[boxKey].y + e.dy;
|
||||
if (newX - totalWidth / 2 >= 0 &&
|
||||
newX + totalWidth / 2 <= w &&
|
||||
newY - totalHeight / 2 >= 0 &&
|
||||
newY + totalHeight / 2 <= h ) {
|
||||
boxPos[boxKey].x = newX;
|
||||
boxPos[boxKey].y = newY;
|
||||
}
|
||||
const pos = calcBoxPos(boxKey);
|
||||
g.clearRect(pos.x1, pos.y1, pos.x2, pos.y2);
|
||||
}
|
||||
});
|
||||
draw(boxes);
|
||||
}
|
||||
};
|
||||
|
||||
Bangle.on('lock', lockHandler);
|
||||
Bangle.on('touch', touchHandler);
|
||||
Bangle.on('drag', dragHandler);
|
||||
|
||||
|
||||
if (boxes.step) {
|
||||
boxes.step.string = formatStr(boxes.step, Bangle.getHealthStatus("day").steps);
|
||||
Bangle.on('step', stepHandler);
|
||||
}
|
||||
|
||||
if (boxes.batt) {
|
||||
boxes.batt.lastLevel = E.getBattery();
|
||||
boxes.batt.string = formatStr(boxes.batt, boxes.batt.lastLevel);
|
||||
boxes.batt.lastUpdate = Date.now();
|
||||
}
|
||||
|
||||
Bangle.setUI({
|
||||
mode : "clock",
|
||||
remove : function() {
|
||||
// Remove event handlers, stop draw timer, remove custom font if used
|
||||
mode: "clock",
|
||||
remove: function() {
|
||||
// Remove event handlers, stop draw timer, remove custom font
|
||||
Bangle.removeListener('touch', touchHandler);
|
||||
Bangle.removeListener('drag', dragHandler);
|
||||
Bangle.removeListener('lock', lockHandler);
|
||||
if (boxes.step) {
|
||||
Bangle.removeListener('step', stepHandler);
|
||||
}
|
||||
if (drawTimeout) clearTimeout(drawTimeout);
|
||||
drawTimeout = undefined;
|
||||
delete Graphics.prototype.setFontBrunoAce;
|
||||
|
@ -388,16 +455,12 @@
|
|||
widgets.show();
|
||||
}
|
||||
});
|
||||
|
||||
loadCustomFont();
|
||||
draw(boxes);
|
||||
draw();
|
||||
};
|
||||
|
||||
/**
|
||||
* ---------------------------------------------------------------
|
||||
* 11. Main execution part
|
||||
* ---------------------------------------------------------------
|
||||
*/
|
||||
|
||||
// 11. Main execution
|
||||
Bangle.loadWidgets();
|
||||
widgets.swipeOn();
|
||||
modSetColor();
|
||||
|
|
|
@ -12,14 +12,14 @@
|
|||
"yOffset": 0,
|
||||
"boxPos": {
|
||||
"x": "0.5",
|
||||
"y": "0.33"
|
||||
"y": "0.739"
|
||||
}
|
||||
},
|
||||
"dow": {
|
||||
"font": "6x8",
|
||||
"fontSize": 3,
|
||||
"outline": 1,
|
||||
"color": "#5ccd73",
|
||||
"outline": 2,
|
||||
"color": "bgH",
|
||||
"outlineColor": "fg",
|
||||
"border": "#f0f",
|
||||
"xPadding": -1,
|
||||
|
@ -28,15 +28,15 @@
|
|||
"yOffset": 0,
|
||||
"boxPos": {
|
||||
"x": "0.5",
|
||||
"y": "0.57"
|
||||
"y": "0.201"
|
||||
},
|
||||
"short": false
|
||||
},
|
||||
"date": {
|
||||
"font": "6x8",
|
||||
"fontSize": 2,
|
||||
"outline": 1,
|
||||
"color": "#5ccd73",
|
||||
"outline": 2,
|
||||
"color": "bgH",
|
||||
"outlineColor": "fg",
|
||||
"border": "#f0f",
|
||||
"xPadding": -0.5,
|
||||
|
@ -45,7 +45,7 @@
|
|||
"yOffset": 0,
|
||||
"boxPos": {
|
||||
"x": "0.5",
|
||||
"y": "0.75"
|
||||
"y": "0.074"
|
||||
},
|
||||
"shortMonth": false,
|
||||
"disableSuffix": true
|
||||
|
@ -63,7 +63,7 @@
|
|||
"yOffset": 1,
|
||||
"boxPos": {
|
||||
"x": "0.5",
|
||||
"y": "0.92"
|
||||
"y": "0.926"
|
||||
},
|
||||
"prefix": "Steps: "
|
||||
},
|
||||
|
@ -79,8 +79,8 @@
|
|||
"xOffset": 2,
|
||||
"yOffset": 2,
|
||||
"boxPos": {
|
||||
"x": "0.85",
|
||||
"y": "0.08"
|
||||
"x": "0.8",
|
||||
"y": "0.427"
|
||||
},
|
||||
"suffix": "%"
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "boxclk",
|
||||
"name": "Box Clock",
|
||||
"version": "0.05",
|
||||
"version": "0.10",
|
||||
"description": "A customizable clock with configurable text boxes that can be positioned to show your favorite background",
|
||||
"icon": "app.png",
|
||||
"dependencies" : { "clockbg":"module" },
|
||||
|
@ -24,4 +24,4 @@
|
|||
"data": [
|
||||
{"name":"boxclk.json","url":"boxclk.json"}
|
||||
]
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 11 KiB |
|
@ -91,4 +91,4 @@
|
|||
});
|
||||
|
||||
E.showMenu(menu);
|
||||
});
|
||||
})
|
||||
|
|
|
@ -720,7 +720,7 @@ const hook = (enable: boolean) => {
|
|||
// --- intervals ---
|
||||
|
||||
const setIntervals = (
|
||||
locked: boolean = Bangle.isLocked(),
|
||||
locked: ShortBoolean = Bangle.isLocked(),
|
||||
connected: boolean = NRF.getSecurityStatus().connected,
|
||||
) => {
|
||||
changeInterval(
|
||||
|
|
|
@ -371,4 +371,4 @@
|
|||
};
|
||||
|
||||
E.showMenu(buildMainMenu());
|
||||
});
|
||||
})
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
0.01: Back to the future clock first version.
|
||||
0.02: Added more icons to the status field. Made it posible to custemize the step goal thrue the Health Tracking app.
|
|
@ -0,0 +1,38 @@
|
|||
# Back to the future Clock
|
||||
|
||||

|
||||
|
||||
A watchface inspierd by <a target="_blank" href="https://apps.garmin.com/apps/d181bcf9-5421-42a5-b460-863e5e76d798">this garmin watchface</a>.<br/>
|
||||
|
||||
## Todo
|
||||
|
||||
- Improving quality of Fonts.
|
||||
- More status icons.
|
||||
|
||||
## Functionalities
|
||||
|
||||
- Current time
|
||||
- Current day and month
|
||||
- Battery
|
||||
- Steps
|
||||
- Step goal can be set white the <a target="_blank" href="https://github.com/espruino/BangleApps/tree/master/apps/health">Health Tracking</a> app defult is 10000
|
||||
- Bluetooth connected icon
|
||||
- Alarm icon
|
||||
- Notification icon
|
||||
|
||||
## Screenshots
|
||||
Clock:<br/>
|
||||
|
||||
|
||||
## Usage
|
||||
Install it and enjoy
|
||||
|
||||
|
||||
## Links
|
||||
### code ispired by
|
||||
advCasioBangleClock <a target="_blank" href="https://github.com/dotgreg/advCasioBangleClock">https://github.com/dotgreg/advCasioBangleClock</a>
|
||||
|
||||
93dub <a target="_blank" href="https://github.com/espruino/BangleApps/tree/master/apps/93dub">https://github.com/espruino/BangleApps/tree/master/apps/93dub</a>
|
||||
|
||||
### Creator
|
||||
<a target="_blank" href="https://github.com/NoobEjby">https://github.com/NoobEjby</a>
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwyEAhN0AMF1AIl2AKAXFH8Jbny4BKu5j/LZWWJ4W4AIXYhPZAJAPDzBnIMc3GjABFLZBZB25XDgP5gUaAIU7AJUbgP6AIRnB3JlEMYw/KLbAjIGIJbBWIJZDrcCvkCzkC3oBMzsCvsCrkCnhnDZomXH6BfU9HkAIIfGLoP4gU6LYRZB7sC/sDj0DnwBCrwBEBIYPBj0C7xnCMoZjD3I/QLqwhJLYpJCKIOege/gffAIX/AIwJBB4IBBv8Dn5lEZYNcH6ZfPDo4hHLYl+I4X/gkfgk/glfAJgPBAIMfM4WfgdfMYW+H6ZffW4W+LI1/gmfAKATBAIIZBMYLNBMYM/L8bfPToJbYMpZjD74/TL5ZhLBIv9vJdCLbpjJn4tBH58JupfTAJP+oJhCLsYBCFIItBH55fOBoIBBywbHF4YBDMMotJLZGYhN3L6GWhO3gUagWcgcdgeegkfGYvvmxhhEIIlBRY0fgefgcegV+gUcgO6L4V2MJhfD7ECrcC3sDr0D/8Er43FAIZhdEplfHIMDr8C70CnsB7bBCL5YJBAIO3hP5gV7gX9XocEv47JTIhdYcYaDIGoLBCgX+gV9gP8hO5YIRfLNoO4gUagWcb4MD78En4/LfYxdWDp0/HoMDn8Cz0CjkB3RfOy0J7ECncC3sDrzjBc4JDPUJBdMbYZ7Or49BgdfgW+gU9gPbhOYWYRfNrcC7pfPI4oBDMJoVVL6DBHA4OXhPZL4l+gkfgl/VKKrDJZLVDLqQBBv5fE70CrpfS/JfXJ4oBDJ4oNNL9C/FrwhBL6JTHL5ZdSAINfL4m+gU9L4WZL6E7gW9L4lfHKRVC982AIIFBA44jTL/ZhFL4pdWL4ffgc/gWeL4mYL5gLB3ECjcCzsDnwhBgk/HqzDID61/HIMD38Dj5fCjsB3cJvBfPgP7gV9gX+gYnBj4pCL+sfHoMC/8Cv0B/pfSy8B7UCrkC70DcYP/c4JfX/1BAIJfYHIQ9BgW/gVegP8gOaL5wBBy8J3MCjcCzsDjzBZL7i9EgcfgWegUdgPchOZhN3KohfFYIu4gP7gVdYInfgk/MKZfaFoM/GoMDn69E/sB3a9ML4+XgPagUcgV9gX+MK5ZB982AIJfSFIIxB/41BgX/gV+XocBzS9CL54BBCYOYgPbgU9cIQrBv5hTL6y7DGIRdCz8Cny9KL5bBFMIO5gP8MIyRB38Ej6XBMZZfSXIQlBRYK7ELoNeLomZXqDBHCoLBB3RhISoOfS4LFCMZBZB/1BAIJfILYa5DEoJdGgUdgPcgOaXqhhHPIJhB3ZhEv0CSoKZBYoJjDY4JlDr5fIBYS3ELYO/EIUfFIJdMIYJdTMIp5BDoN4MIkdGIQ1BMY4HBMoRfHK4QBBCIK3CLYm/RYU+gP9LoW6Loi9DLqZhL3QtBMIU+HIQ9BIIJNBn5LCr/GjABFBYQPBn4VBW4l/EoS5BLoO8LsRhLzTFCMYrHBMoOfLY4BHLIlfDoUeXIgrBzJdkMI5jDzDFCMYQ/BMoZVF9HkAIJfGLIf+LY2aLZBdhMIrFGhOZY4pdJMJMB7sB3gbC3UJvJduMI5jHvD7BLpZhHW4hbLLtJjPu5fTLIhb3MZxPDMJINFLIRb9MZhhJLoxZFLf4BIKooBJJ/5jbI/4B/AP4B/AP4BLA"))
|
|
@ -0,0 +1,239 @@
|
|||
require("Font8x16").add(Graphics);
|
||||
require("Font7x11Numeric7Seg").add(Graphics);
|
||||
require("Font5x7Numeric7Seg").add(Graphics);
|
||||
require("Font4x5").add(Graphics);
|
||||
|
||||
const timeTextY = 4;
|
||||
const timeDataY = timeTextY+19;
|
||||
const DateTextY = 48;
|
||||
const DateDataY = DateTextY+19;
|
||||
const stepGoalBatTextY = 100;
|
||||
const stepGoalBatdataY = stepGoalBatTextY+19;
|
||||
const statusTextY = 140;
|
||||
const statusDataY = statusTextY+19;
|
||||
let stepGoal = (require("Storage").readJSON("health.json",1)||10000).stepGoal;
|
||||
let steps = 0;
|
||||
let alarmStatus = (require('Storage').readJSON('sched.json',1)||[]).some(alarm=>alarm.on);
|
||||
|
||||
|
||||
const bluetoothOnIcon = require("heatshrink").decompress(atob("iEQwYROg3AAokYAgUMg0DAoUBwwFDgE2CIYdHAogREDoopFGoodGABI="));
|
||||
|
||||
const bluetoothOffIcon = require("heatshrink").decompress(atob("iEQwYLIgwFF4ADBgYFBjAKCsEGBAIABhgFEgOA7AdDmApKmwpCC4OGFIYjFGoVgIIkMEZAAD"));
|
||||
|
||||
const alarmIcon = require("heatshrink").decompress(atob("iEQyBC/AA3/8ABBB7INHA4YLLDqIHVApJRJCZodNCJ4dPHqqPJGp4RLOaozZT8btLF64hJFJpFbAEYA="));
|
||||
|
||||
const notificationIcon = require("heatshrink").decompress(atob("iEQyBC/AB3/8ABBD+4bHEa4VJD6YTNEKIf/D/rTDAJ7jTADo5hK+IA=="));
|
||||
|
||||
|
||||
//the following 2 sections are used from waveclk to schedule minutely updates
|
||||
// timeout used to update every minute
|
||||
var drawTimeout;
|
||||
|
||||
// schedule a draw for the next minute
|
||||
function queueDraw() {
|
||||
if (drawTimeout) clearTimeout(drawTimeout);
|
||||
drawTimeout = setTimeout(function() {
|
||||
drawTimeout = undefined;
|
||||
draw();
|
||||
}, 60000 - (Date.now() % 60000));
|
||||
}
|
||||
|
||||
function getSteps() {
|
||||
steps = Bangle.getHealthStatus("day").steps;
|
||||
}
|
||||
|
||||
function drawBackground() {
|
||||
//g.setBgColor(1,1,1);
|
||||
g.setBgColor('#555555');
|
||||
g.setColor(1,1,1);
|
||||
g.clear();
|
||||
//g.drawImage(imgBg,0,0);
|
||||
g.reset();
|
||||
}
|
||||
function drawBlackBox() {
|
||||
g.reset();
|
||||
g.setBgColor(1,0,0);
|
||||
g.setColor(0,0,0);
|
||||
|
||||
//Hour, Min and Sec
|
||||
g.fillRect(50, timeDataY,50+33,timeDataY+22);
|
||||
g.fillRect(90, timeDataY,90+33, timeDataY+22);
|
||||
g.fillRect(128, timeDataY+8,130+24, timeDataY+8+14);
|
||||
//Day, Month, Day and Year
|
||||
g.fillRect(9, DateDataY,9+24, DateDataY+15);
|
||||
g.fillRect(42, DateDataY,42+40, DateDataY+15);
|
||||
g.fillRect(91, DateDataY,91+24, DateDataY+15);
|
||||
g.fillRect(124, DateDataY,124+43, DateDataY+15);
|
||||
//Present day
|
||||
g.fillRect(60, 86,60+47, 86+7);
|
||||
//Middle line
|
||||
g.drawLine(0,95,176,95);
|
||||
//Step and bat
|
||||
g.fillRect(3, stepGoalBatdataY-1, 62, stepGoalBatdataY+15);
|
||||
g.fillRect(121, stepGoalBatdataY-1, 150, stepGoalBatdataY+15);
|
||||
|
||||
//Status
|
||||
g.fillRect(62, statusDataY-1, 62+49, statusDataY+15);
|
||||
}
|
||||
function drawGoal() {
|
||||
var goal = stepGoal <= steps;
|
||||
g.reset();
|
||||
g.setColor(0,0,0);
|
||||
|
||||
g.fillRect(84, stepGoalBatdataY-1, 92, stepGoalBatdataY+15);
|
||||
|
||||
if (goal){
|
||||
g.reset();
|
||||
g.setColor(0,1,0);
|
||||
g.fillRect(84, stepGoalBatdataY, 92, stepGoalBatdataY+7);
|
||||
} else {
|
||||
g.reset();
|
||||
g.setColor(1,0,0);
|
||||
g.fillRect(84, stepGoalBatdataY+7, 92, stepGoalBatdataY+14);
|
||||
}
|
||||
}
|
||||
function drawRedkBox() {
|
||||
g.reset();
|
||||
g.setBgColor(1,0,0);
|
||||
g.setColor(1,0,0);
|
||||
//Hour, Min and Sec
|
||||
g.fillRect(50, timeTextY,50+33,timeTextY+15);
|
||||
g.fillRect(90, timeTextY,90+33, timeTextY+15);
|
||||
g.fillRect(128, timeTextY+8,130+24, timeTextY+8+15);
|
||||
//Day, Month, Day and Year
|
||||
g.fillRect(9, DateTextY,9+24, DateTextY+15);
|
||||
g.fillRect(42, DateTextY,42+40, DateTextY+15);
|
||||
g.fillRect(91, DateTextY,91+24, DateTextY+15);
|
||||
g.fillRect(124, DateTextY,124+43, DateTextY+15);
|
||||
//Step, Goal and Bat
|
||||
g.fillRect(2, stepGoalBatTextY,2+61, stepGoalBatTextY+15);
|
||||
g.fillRect(70, stepGoalBatTextY,72+33, stepGoalBatTextY+15);
|
||||
g.fillRect(120, stepGoalBatTextY,120+31, stepGoalBatTextY+15);
|
||||
//Status
|
||||
g.fillRect(62, statusTextY,62+49, statusTextY+15);
|
||||
}
|
||||
|
||||
function draw(){
|
||||
drawBackground();
|
||||
getSteps();
|
||||
drawBlackBox();
|
||||
drawRedkBox();
|
||||
drawGoal();
|
||||
var date = new Date();
|
||||
var h = date.getHours(), m = date.getMinutes(), s = date.getSeconds();
|
||||
var d = date.getDate(), y = date.getFullYear();//, w = date.getDay();
|
||||
|
||||
if (h<10) {
|
||||
h = ("0"+h).substr(-2);
|
||||
}
|
||||
if (m<10) {
|
||||
m = ("0"+m).substr(-2);
|
||||
}
|
||||
if (s<10) {
|
||||
s = ("0"+s).substr(-2);
|
||||
}
|
||||
if (d<10) {
|
||||
d = ("0"+d).substr(-2);
|
||||
}
|
||||
|
||||
g.reset();
|
||||
g.setBgColor(1,0,0);
|
||||
g.setColor(1,1,1);
|
||||
//Draw text
|
||||
g.setFont("8x16");
|
||||
g.drawString('HOUR', 51, timeTextY+1);
|
||||
g.drawString('MIN', 96, timeTextY+1);
|
||||
g.drawString('SEC', 130, timeTextY+9);
|
||||
|
||||
g.drawString('DAY', 10, DateTextY+1);
|
||||
g.drawString('MONTH', 43, DateTextY+1);
|
||||
g.drawString('DAY', 92, DateTextY+1);
|
||||
g.drawString(' YEAR ', 125, DateTextY+1);
|
||||
|
||||
g.drawString('STEPS', 15, stepGoalBatTextY+1);
|
||||
g.drawString('GOAL', 72, stepGoalBatTextY+1);
|
||||
g.drawString(' BAT ', 120, stepGoalBatTextY+1);
|
||||
g.drawString('STATUS', 64, statusTextY+1);
|
||||
|
||||
//time
|
||||
g.reset();
|
||||
g.setBgColor(0,0,0);
|
||||
g.setColor(1,0,0);
|
||||
g.setFont("5x7Numeric7Seg",2);
|
||||
g.drawString(s, 131, timeDataY+8);
|
||||
g.setFont("7x11Numeric7Seg",2);
|
||||
g.drawString(h, 53, timeDataY);
|
||||
g.drawString(m, 93, timeDataY);
|
||||
//Date
|
||||
g.reset();
|
||||
g.setBgColor(0,0,0);
|
||||
g.setColor(0,1,0);
|
||||
g.setFont("5x7Numeric7Seg",2);
|
||||
g.drawString(d, 13, DateDataY);
|
||||
g.drawString(y, 127, DateDataY);
|
||||
g.setFont("8x16");
|
||||
g.drawString(require("locale").month(new Date(), 2).toUpperCase(), 52, DateDataY);
|
||||
g.drawString(require("locale").dow(new Date(), 2).toUpperCase(), 92, DateDataY);
|
||||
|
||||
|
||||
//status
|
||||
g.reset();
|
||||
g.setBgColor(0,0,0);
|
||||
g.setColor(1,1,0);
|
||||
g.setFont("5x7Numeric7Seg",2);
|
||||
var step = steps;
|
||||
var stepl = steps.toString().length;
|
||||
var stepdDrawX = 4+(36-(stepl*6))+(4*(6-stepl));
|
||||
g.drawString(step, stepdDrawX, stepGoalBatdataY);
|
||||
var bat = E.getBattery();
|
||||
var batl = bat.toString().length;
|
||||
var batDrawX = 122+(18-(batl*6))+(4*(3-batl));
|
||||
g.drawString(bat, batDrawX, stepGoalBatdataY);
|
||||
|
||||
//status
|
||||
var b = bluetoothOffIcon;
|
||||
if (NRF.getSecurityStatus().connected){
|
||||
b = bluetoothOnIcon;
|
||||
}
|
||||
g.drawImage(b, 62, statusDataY-1);
|
||||
if (alarmStatus){
|
||||
g.drawImage(alarmIcon, 78, statusDataY-1);
|
||||
}
|
||||
if ((require('Storage').readJSON('messages.json',1)||[]).some(messag=>messag.new==true)){
|
||||
g.drawImage(notificationIcon, 94, statusDataY-1);
|
||||
}
|
||||
|
||||
g.reset();
|
||||
g.setBgColor(0,0,0);
|
||||
g.setColor(1,1,1);
|
||||
g.setFont("4x5");
|
||||
g.drawString('Present day', 62, 88);
|
||||
|
||||
queueDraw();
|
||||
}
|
||||
|
||||
/**
|
||||
* This watch is mostly dark, it does not make sense to respect the
|
||||
* light theme as you end up with a white strip at the top for the
|
||||
* widgets and black watch. So set the colours to the dark theme.
|
||||
*
|
||||
*/
|
||||
g.setTheme({bg:"#000",fg:"#fff",dark:true}).clear();
|
||||
//draw();
|
||||
//the following section is from waveclk
|
||||
Bangle.on('lcdPower',on=>{
|
||||
if (on) {
|
||||
draw(); // draw immediately, queue redraw
|
||||
} else { // stop draw timer
|
||||
if (drawTimeout) clearTimeout(drawTimeout);
|
||||
drawTimeout = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Bangle.setUI("clock");
|
||||
// Load widgets, but don't show them
|
||||
Bangle.loadWidgets();
|
||||
require("widget_utils").swipeOn(); // hide widgets, make them visible with a swipe
|
||||
g.clear(1);
|
||||
draw();
|
After Width: | Height: | Size: 6.5 KiB |
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"id": "bttfclock",
|
||||
"name": "Back To The Future",
|
||||
"version": "0.02",
|
||||
"description": "The watch of Marty McFly",
|
||||
"readme": "README.md",
|
||||
"icon": "app.png",
|
||||
"screenshots": [{"url":"screenshot.png"}],
|
||||
"type": "clock",
|
||||
"tags": "clock",
|
||||
"supports": ["BANGLEJS2"],
|
||||
"allow_emulator": true,
|
||||
"storage": [
|
||||
{"name":"bttfclock.app.js","url":"app.js"},
|
||||
{"name":"bttfclock.img","url":"app-icon.js","evaluate":true}
|
||||
]
|
||||
}
|
After Width: | Height: | Size: 4.0 KiB |
|
@ -5,3 +5,4 @@
|
|||
0.05: Grid positioning and swipe controls to switch between numbers, operators and special (for Bangle.js 2)
|
||||
0.06: Bangle.js 2: Exit with a short press of the physical button
|
||||
0.07: Bangle.js 2: Exit by pressing upper left corner of the screen
|
||||
0.08: truncate long numbers (and append '...' to displayed value)
|
||||
|
|
|
@ -10,9 +10,9 @@
|
|||
g.clear();
|
||||
require("Font7x11Numeric7Seg").add(Graphics);
|
||||
|
||||
var DEFAULT_SELECTION_NUMBERS = '5', DEFAULT_SELECTION_OPERATORS = '=', DEFAULT_SELECTION_SPECIALS = 'R';
|
||||
var RIGHT_MARGIN = 20;
|
||||
var DEFAULT_SELECTION_NUMBERS = '5';
|
||||
var RESULT_HEIGHT = 40;
|
||||
var RESULT_MAX_LEN = Math.floor((g.getWidth() - 20) / 14);
|
||||
var COLORS = {
|
||||
// [normal, selected]
|
||||
DEFAULT: ['#7F8183', '#A6A6A7'],
|
||||
|
@ -88,28 +88,11 @@ function prepareScreen(screen, grid, defaultColor) {
|
|||
}
|
||||
|
||||
function drawKey(name, k, selected) {
|
||||
var rMargin = 0;
|
||||
var bMargin = 0;
|
||||
var color = k.color || COLORS.DEFAULT;
|
||||
g.setColor(color[selected ? 1 : 0]);
|
||||
g.setFont('Vector', 20).setFontAlign(0,0);
|
||||
g.fillRect(k.xy[0], k.xy[1], k.xy[2], k.xy[3]);
|
||||
g.setColor(-1);
|
||||
// correct margins to center the texts
|
||||
if (name == '0') {
|
||||
rMargin = (RIGHT_MARGIN * 2) - 7;
|
||||
} else if (name === '/') {
|
||||
rMargin = 5;
|
||||
} else if (name === '*') {
|
||||
bMargin = 5;
|
||||
rMargin = 3;
|
||||
} else if (name === '-') {
|
||||
rMargin = 3;
|
||||
} else if (name === 'R' || name === 'N') {
|
||||
rMargin = k.val === 'C' ? 0 : -9;
|
||||
} else if (name === '%') {
|
||||
rMargin = -3;
|
||||
}
|
||||
g.drawString(k.val || name, (k.xy[0] + k.xy[2])/2, (k.xy[1] + k.xy[3])/2);
|
||||
}
|
||||
|
||||
|
@ -138,29 +121,21 @@ function drawGlobal() {
|
|||
screen[k] = specials[k];
|
||||
}
|
||||
drawKeys();
|
||||
var selected = DEFAULT_SELECTION_NUMBERS;
|
||||
var prevSelected = DEFAULT_SELECTION_NUMBERS;
|
||||
}
|
||||
function drawNumbers() {
|
||||
screen = numbers;
|
||||
screenColor = COLORS.DEFAULT;
|
||||
drawKeys();
|
||||
var selected = DEFAULT_SELECTION_NUMBERS;
|
||||
var prevSelected = DEFAULT_SELECTION_NUMBERS;
|
||||
}
|
||||
function drawOperators() {
|
||||
screen = operators;
|
||||
screenColor =COLORS.OPERATOR;
|
||||
drawKeys();
|
||||
var selected = DEFAULT_SELECTION_OPERATORS;
|
||||
var prevSelected = DEFAULT_SELECTION_OPERATORS;
|
||||
}
|
||||
function drawSpecials() {
|
||||
screen = specials;
|
||||
screenColor = COLORS.SPECIAL;
|
||||
drawKeys();
|
||||
var selected = DEFAULT_SELECTION_SPECIALS;
|
||||
var prevSelected = DEFAULT_SELECTION_SPECIALS;
|
||||
}
|
||||
|
||||
function getIntWithPrecision(x) {
|
||||
|
@ -218,8 +193,6 @@ function doMath(x, y, operator) {
|
|||
}
|
||||
|
||||
function displayOutput(num) {
|
||||
var len;
|
||||
var minusMarge = 0;
|
||||
g.setBgColor(0).clearRect(0, 0, g.getWidth(), RESULT_HEIGHT-1);
|
||||
g.setColor(-1);
|
||||
if (num === Infinity || num === -Infinity || isNaN(num)) {
|
||||
|
@ -230,9 +203,7 @@ function displayOutput(num) {
|
|||
num = '-INFINITY';
|
||||
} else {
|
||||
num = 'NOT A NUMBER';
|
||||
minusMarge = -25;
|
||||
}
|
||||
len = (num + '').length;
|
||||
currNumber = null;
|
||||
results = null;
|
||||
isDecimal = false;
|
||||
|
@ -261,6 +232,9 @@ function displayOutput(num) {
|
|||
num = num.toString();
|
||||
num = num.replace("-","- "); // fix padding for '-'
|
||||
g.setFont('7x11Numeric7Seg', 2);
|
||||
if (num.length > RESULT_MAX_LEN) {
|
||||
num = num.substr(0, RESULT_MAX_LEN - 1)+'...';
|
||||
}
|
||||
}
|
||||
g.setFontAlign(1,0);
|
||||
g.drawString(num, g.getWidth()-20, RESULT_HEIGHT/2);
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"id": "calculator",
|
||||
"name": "Calculator",
|
||||
"shortName": "Calculator",
|
||||
"version": "0.07",
|
||||
"version": "0.08",
|
||||
"description": "Basic calculator reminiscent of MacOs's one. Handy for small calculus.",
|
||||
"icon": "calculator.png",
|
||||
"screenshots": [{"url":"screenshot_calculator.png"}],
|
||||
|
|
|
@ -17,4 +17,4 @@
|
|||
}
|
||||
};
|
||||
E.showMenu(menu);
|
||||
});
|
||||
})
|
||||
|
|
|
@ -91,4 +91,4 @@
|
|||
|
||||
settings = readSettings();
|
||||
showMainMenu();
|
||||
});
|
||||
})
|
||||
|
|
|
@ -92,4 +92,4 @@
|
|||
}
|
||||
|
||||
showMainMenu();
|
||||
});
|
||||
})
|
||||
|
|
|
@ -47,4 +47,4 @@
|
|||
},
|
||||
'< Back': back,
|
||||
});
|
||||
});
|
||||
})
|
||||
|
|
|
@ -34,4 +34,4 @@
|
|||
}
|
||||
});
|
||||
|
||||
});
|
||||
})
|
||||
|
|
|
@ -124,4 +124,4 @@
|
|||
};
|
||||
|
||||
return info;
|
||||
});
|
||||
})
|
||||
|
|
|
@ -58,4 +58,4 @@
|
|||
};
|
||||
|
||||
return info;
|
||||
});
|
||||
})
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
0.01: First release
|
|
@ -0,0 +1,18 @@
|
|||
# Messages Clockinfo
|
||||
|
||||
A simple messages counter for clockinfo enabled watchfaces
|
||||
|
||||
## Usage
|
||||
|
||||
You can choose between read and unread counter.
|
||||
Tap to go to messages UI.
|
||||
|
||||
## Todo / Known Issues
|
||||
|
||||
* GB triggers for message read on phone are not handled
|
||||
* Icons are not consistent
|
||||
* Maybe use messageicons for handling icon from last notification
|
||||
|
||||
## Attributions
|
||||
|
||||
All icons used in this app are from [icons8](https://icons8.com).
|
After Width: | Height: | Size: 326 B |
|
@ -0,0 +1,84 @@
|
|||
(function() {
|
||||
|
||||
var unreadImg = function() {
|
||||
return atob("GBiBAAAAAAAAAAAAAB//+D///D///D///D///D///D///D5mfD5mfD///D///D///D///D///D///B//+APgAAOAAAOAAAAAAAAAAA==");
|
||||
}
|
||||
var allImg = function() {
|
||||
return atob("GBiBAAAAAAAAAAB+AAD/AAPDwA8A8B4AeDgAHDgAHDwAPD8A/D/D/D/n/D///D///D///D///D///D///B//+AAAAAAAAAAAAAAAAA==");
|
||||
}
|
||||
|
||||
var debug = function(o) {
|
||||
//console.log(o);
|
||||
}
|
||||
var msgUnread;
|
||||
var msgAll;
|
||||
var msgs = require("messages");
|
||||
|
||||
var getAllMSGs = function() {
|
||||
if (msgAll === undefined) {
|
||||
debug("msgAll is undefined");
|
||||
msgAll = msgs.getMessages().filter(m => !['call', 'map', 'music'].includes(m.id)).length;
|
||||
}
|
||||
return msgAll;
|
||||
}
|
||||
|
||||
|
||||
var getUnreadMGS = function() {
|
||||
if (msgUnread === undefined) {
|
||||
debug("msgUnread is undefined");
|
||||
msgUnread = msgs.getMessages().filter(m => m.new && !['call', 'map', 'music'].includes(m.id)).length;
|
||||
}
|
||||
return msgUnread;
|
||||
}
|
||||
|
||||
var msgCounter = function(type, msg) {
|
||||
var msgsNow = msgs.getMessages(msg);
|
||||
msgUnread = msgsNow.filter(m => m.new && !['call', 'map', 'music'].includes(m.id)).length;
|
||||
msgAll = msgsNow.filter(m => !['call', 'map', 'music'].includes(m.id)).length;
|
||||
//TODO find nicer way to redraw current shown CI counter
|
||||
info.items[0].emit("redraw");
|
||||
info.items[1].emit("redraw");
|
||||
}
|
||||
|
||||
var info = {
|
||||
name: "Messages",
|
||||
img: unreadImg(),
|
||||
items: [
|
||||
{ name : "Unread",
|
||||
get : () => {
|
||||
return {
|
||||
text : getUnreadMGS(),
|
||||
img : unreadImg()
|
||||
};
|
||||
},
|
||||
show : function() {
|
||||
Bangle.on("message", msgCounter);
|
||||
},
|
||||
hide : function() {
|
||||
Bangle.removeListener("message", msgCounter);
|
||||
},
|
||||
run : () => {
|
||||
require("messages").openGUI();
|
||||
}
|
||||
},
|
||||
{ name : "All",
|
||||
get : () => {
|
||||
return {
|
||||
text : getAllMSGs(),
|
||||
img : allImg()
|
||||
};
|
||||
},
|
||||
show : function() {
|
||||
Bangle.on("message", msgCounter);
|
||||
},
|
||||
hide : function() {
|
||||
Bangle.removeListener("message", msgCounter);
|
||||
},
|
||||
run : () => {
|
||||
require("messages").openGUI();
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
return info;
|
||||
})
|
|
@ -0,0 +1,15 @@
|
|||
{ "id": "clkinfomsg",
|
||||
"name": "Messages Clockinfo",
|
||||
"version":"0.01",
|
||||
"description": "For clocks that display 'clockinfo', this displays the messages count",
|
||||
"icon": "app.png",
|
||||
"type": "clkinfo",
|
||||
"screenshots": [{"url":"screenshot.png"}],
|
||||
"readme":"README.md",
|
||||
"tags": "clkinfo",
|
||||
"supports" : ["BANGLEJS2"],
|
||||
"dependencies" : { "messages":"app" },
|
||||
"storage": [
|
||||
{"name":"clkinfomsg.clkinfo.js","url":"clkinfo.js"}
|
||||
]
|
||||
}
|
After Width: | Height: | Size: 2.1 KiB |
|
@ -74,4 +74,4 @@
|
|||
}
|
||||
]
|
||||
};
|
||||
});
|
||||
})
|
||||
|
|
|
@ -80,4 +80,4 @@
|
|||
}
|
||||
]
|
||||
};
|
||||
}) satisfies ClockInfoFunc
|
||||
}) satisfies ClockInfoFunc // FIXME: semi-colon added automatically when Typescript generates Javascript file?
|
||||
|
|
|
@ -10,4 +10,6 @@
|
|||
0.09: Save clkinfo settings on kill and remove
|
||||
0.10: Fix focus bug when changing focus between two clock infos
|
||||
0.11: Prepend swipe listener if possible
|
||||
0.12: Add drawFilledImage to allow drawing icons with a separately coloured middle
|
||||
0.12: Add drawFilledImage to allow drawing icons with a separately coloured middle
|
||||
0.13: Cache loaded ClockInfos so if we have clockInfoWidget and a clock, we don't load them twice (saves ~300ms)
|
||||
0.14: Check for .clkinfocache and use that if exists (from boot 0.64)
|
|
@ -50,7 +50,7 @@ extensions).
|
|||
`load()` returns an array of menu objects, where each object contains a list of menu items:
|
||||
* `name` : text to display and identify menu object (e.g. weather)
|
||||
* `img` : a 24x24px image
|
||||
* `dynamic` : if `true`, items are not constant but are sorted (e.g. calendar events sorted by date)
|
||||
* `dynamic` : if `true`, items are not constant but are sorted (e.g. calendar events sorted by date). This is only used by a few clocks, for example `circlesclock`
|
||||
* `items` : menu items such as temperature, humidity, wind etc.
|
||||
|
||||
Note that each item is an object with:
|
||||
|
|
|
@ -14,6 +14,8 @@ if (stepGoal == undefined) {
|
|||
exports.loadCount = 0;
|
||||
/// A list of all the instances returned by addInteractive
|
||||
exports.clockInfos = [];
|
||||
/// A list of loaded clockInfos
|
||||
exports.clockInfoMenus = undefined;
|
||||
|
||||
/// Load the settings, with defaults
|
||||
exports.loadSettings = function() {
|
||||
|
@ -29,6 +31,8 @@ exports.loadSettings = function() {
|
|||
|
||||
/// Load a list of ClockInfos - this does not cache and reloads each time
|
||||
exports.load = function() {
|
||||
if (exports.clockInfoMenus)
|
||||
return exports.clockInfoMenus;
|
||||
var settings = exports.loadSettings();
|
||||
delete settings.apps; // keep just the basic settings in memory
|
||||
// info used for drawing...
|
||||
|
@ -131,10 +135,14 @@ exports.load = function() {
|
|||
hide : function() { clearInterval(this.interval); delete this.interval; },
|
||||
});
|
||||
}
|
||||
|
||||
var clkInfoCache = require('Storage').read('.clkinfocache');
|
||||
if (clkInfoCache!==undefined) {
|
||||
// note: code below is included in clkinfocache by bootupdate.js
|
||||
// we use clkinfocache if it exists as it's faster
|
||||
eval(clkInfoCache);
|
||||
} else require("Storage").list(/clkinfo\.js$/).forEach(fn => {
|
||||
// In case there exists already a menu object b with the same name as the next
|
||||
// object a, we append the items. Otherwise we add the new object a to the list.
|
||||
require("Storage").list(/clkinfo.js$/).forEach(fn => {
|
||||
try{
|
||||
var a = eval(require("Storage").read(fn))();
|
||||
var b = menu.find(x => x.name === a.name);
|
||||
|
@ -146,6 +154,7 @@ exports.load = function() {
|
|||
});
|
||||
|
||||
// return it all!
|
||||
exports.clockInfoMenus = menu;
|
||||
return menu;
|
||||
};
|
||||
|
||||
|
@ -345,6 +354,9 @@ exports.addInteractive = function(menu, options) {
|
|||
menuHideItem(menu[options.menuA].items[options.menuB]);
|
||||
exports.loadCount--;
|
||||
delete exports.clockInfos[options.index];
|
||||
// If nothing loaded now, clear our list of loaded menus
|
||||
if (exports.loadCount==0)
|
||||
exports.clockInfoMenus = undefined;
|
||||
};
|
||||
options.redraw = function() {
|
||||
drawItem(menu[options.menuA].items[options.menuB]);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{ "id": "clock_info",
|
||||
"name": "Clock Info Module",
|
||||
"shortName": "Clock Info",
|
||||
"version":"0.12",
|
||||
"version":"0.14",
|
||||
"description": "A library used by clocks to provide extra information on the clock face (Altitude, BPM, etc)",
|
||||
"icon": "app.png",
|
||||
"type": "module",
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
0.01: New App!
|
||||
0.02: Moved settings into 'Settings->Apps'
|
||||
0.03: Add 'Squares' option for random squares background
|
||||
0.03: Add 'Squares' option for random squares background
|
||||
0.04: More options for different background colors
|
||||
'Plasma' generative background
|
||||
Add a 'view' option in settings menu to view the current background
|
||||
0.05: Random square+plasma speed improvements (~2x faster)
|
||||
0.06: 25% speed improvement if Math.randInt exists (2v25 fw)
|
|
@ -15,6 +15,7 @@ You can either:
|
|||
* `Random Color` - a new color every time the clock starts
|
||||
* `Image` - choose from a previously uploaded image
|
||||
* `Squares` - a randomly generated pattern of squares in the selected color palette
|
||||
* `Plasma` - a randomly generated 'plasma' pattern of squares in the selected color palette (random noise with a gaussian filter applied)
|
||||
|
||||
|
||||
## Usage in code
|
||||
|
@ -30,6 +31,9 @@ background.fillRect(Bangle.appRect);
|
|||
|
||||
// to fill just one part of the screen
|
||||
background.fillRect(x1, y1, x2, y2);
|
||||
|
||||
// if you ever need to reload to a new background (this could take ~100ms)
|
||||
background.reload();
|
||||
```
|
||||
|
||||
You should also add `"dependencies" : { "clockbg":"module" },` to your app's metadata to
|
||||
|
@ -39,8 +43,9 @@ ensure that the clock background library is automatically loaded.
|
|||
|
||||
A few features could be added that would really improve functionality:
|
||||
|
||||
* When 'fast loading', 'random' backgrounds don't update at the moment
|
||||
* When 'fast loading', 'random' backgrounds don't update at the moment (calling `.reload` can fix this now, but it slows things down)
|
||||
* Support for >1 image to be uploaded (requires some image management in `interface.html`), and choose randomly between them
|
||||
* Support for gradients (random colors)
|
||||
* More types of auto-generated pattern (as long as they can be generated quickly or in the background)
|
||||
* Storing 'clear' areas of uploaded images so clocks can easily position themselves
|
||||
* Storing 'clear' areas of uploaded images so clocks can easily position themselves
|
||||
* Some backgrounds could update themselves in the background (eg a mandelbrot could calculate the one it should display next time while the watch is running)
|
|
@ -1,25 +1,53 @@
|
|||
let settings = Object.assign({
|
||||
style : "randomcolor",
|
||||
colors : ["#F00","#0F0","#00F"]
|
||||
},require("Storage").readJSON("clockbg.json")||{});
|
||||
if (settings.style=="image")
|
||||
settings.img = require("Storage").read(settings.fn);
|
||||
else if (settings.style=="randomcolor") {
|
||||
settings.style = "color";
|
||||
let n = (0|(Math.random()*settings.colors.length)) % settings.colors.length;
|
||||
settings.color = settings.colors[n];
|
||||
delete settings.colors;
|
||||
} else if (settings.style=="squares") {
|
||||
settings.style = "image";
|
||||
let bpp = (settings.colors.length>4)?4:2;
|
||||
let bg = Graphics.createArrayBuffer(11,11,bpp,{msb:true});
|
||||
E.mapInPlace(bg.buffer, bg.buffer, ()=>Math.random()*256); // random pixels
|
||||
bg.palette = new Uint16Array(1<<bpp);
|
||||
bg.palette.set(settings.colors.map(c=>g.toColor(c)));
|
||||
settings.img = bg.asImage("string");
|
||||
settings.imgOpt = {scale:16};
|
||||
delete settings.colors;
|
||||
}
|
||||
let settings;
|
||||
|
||||
exports.reload = function() {
|
||||
//let t = Date.now();
|
||||
settings = Object.assign({
|
||||
style : "randomcolor",
|
||||
colors : ["#F00","#0F0","#00F"]
|
||||
},require("Storage").readJSON("clockbg.json")||{});
|
||||
if (settings.style=="image")
|
||||
settings.img = require("Storage").read(settings.fn);
|
||||
else if (settings.style=="randomcolor") {
|
||||
settings.style = "color";
|
||||
let n = (0|(Math.random()*settings.colors.length)) % settings.colors.length;
|
||||
settings.color = settings.colors[n];
|
||||
delete settings.colors;
|
||||
} else if (settings.style=="squares") { // 32ms
|
||||
settings.style = "image";
|
||||
let bpp = (settings.colors.length>4)?4:2;
|
||||
let bg = Graphics.createArrayBuffer(11,11,bpp,{msb:true});
|
||||
let u32 = new Uint32Array(bg.buffer); // faster to do 1/4 of the ops of E.mapInPlace(bg.buffer, bg.buffer, ()=>Math.random()*256);
|
||||
if (Math.randInt) E.mapInPlace(u32, u32, Math.randInt); // random pixels
|
||||
else E.mapInPlace(u32, u32, function(r,n){"ram";return r()*n}.bind(null,Math.random,0x100000000)); // random pixels
|
||||
bg.buffer[bg.buffer.length-1]=Math.random()*256; // 11x11 isn't a multiple of 4 bytes - we need to set the last one!
|
||||
bg.palette = new Uint16Array(1<<bpp);
|
||||
bg.palette.set(settings.colors.map(c=>g.toColor(c)));
|
||||
settings.img = bg;
|
||||
settings.imgOpt = {scale:16};
|
||||
delete settings.colors;
|
||||
} else if (settings.style=="plasma") { // ~47ms
|
||||
settings.style = "image";
|
||||
let bg = Graphics.createArrayBuffer(16,16,4,{msb:true});
|
||||
let u32 = new Uint32Array(bg.buffer); // faster to do 1/4 of the ops of E.mapInPlace(bg.buffer, bg.buffer, ()=>Math.random()*256);
|
||||
if (Math.randInt) E.mapInPlace(u32, u32, Math.randInt); // random pixels
|
||||
else E.mapInPlace(u32, u32, function(r,n){"ram";return r()*n}.bind(null,Math.random,0x100000000)); // random pixels
|
||||
bg.filter([ // a gaussian filter to smooth out
|
||||
1, 4, 7, 4, 1,
|
||||
4,16,26,16, 4,
|
||||
7,26,41,26, 7,
|
||||
4,16,26,16, 4,
|
||||
1, 4, 7, 4, 1
|
||||
], { w:5, h:5, div:120, offset:-800 });
|
||||
bg.palette = new Uint16Array(16);
|
||||
bg.palette.set(settings.colors.map(c=>g.toColor(c)));
|
||||
settings.img = bg;
|
||||
settings.imgOpt = {scale:11};
|
||||
delete settings.colors;
|
||||
}
|
||||
//console.log("bg",Date.now()-t);
|
||||
};
|
||||
exports.reload();
|
||||
|
||||
// Fill a rectangle with the current background style, rect = {x,y,w,h}
|
||||
// eg require("clockbg").fillRect({x:10,y:10,w:50,h:50})
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
{ "id": "clockbg",
|
||||
"name": "Clock Backgrounds",
|
||||
"shortName":"Backgrounds",
|
||||
"version": "0.03",
|
||||
"version": "0.06",
|
||||
"description": "Library that allows clocks to include a custom background (generated on demand or uploaded).",
|
||||
"icon": "app.png",
|
||||
"screenshots": [{"url":"screenshot.png"},{"url":"screenshot2.png"}],
|
||||
"screenshots": [{"url":"screenshot.png"},{"url":"screenshot2.png"},{"url":"screenshot3.png"}],
|
||||
"type": "module",
|
||||
"readme": "README.md",
|
||||
"provides_modules" : ["clockbg"],
|
||||
|
|
After Width: | Height: | Size: 2.3 KiB |
|
@ -1,122 +1,171 @@
|
|||
(function(back) {
|
||||
let settings = Object.assign({
|
||||
style : "randomcolor",
|
||||
colors : ["#F00","#0F0","#00F"]
|
||||
},require("Storage").readJSON("clockbg.json")||{});
|
||||
let settings = Object.assign({
|
||||
style : "randomcolor",
|
||||
colors : ["#F00","#0F0","#00F"]
|
||||
},require("Storage").readJSON("clockbg.json")||{});
|
||||
|
||||
function saveSettings() {
|
||||
if (settings.style!="image")
|
||||
delete settings.fn;
|
||||
if (settings.style!="color")
|
||||
delete settings.color;
|
||||
if (settings.style!="randomcolor" && settings.style!="squares")
|
||||
delete settings.colors;
|
||||
require("Storage").writeJSON("clockbg.json", settings);
|
||||
}
|
||||
function saveSettings() {
|
||||
if (settings.style!="image")
|
||||
delete settings.fn;
|
||||
if (settings.style!="color")
|
||||
delete settings.color;
|
||||
if (!["randomcolor","squares","plasma"].includes(settings.style))
|
||||
delete settings.colors;
|
||||
require("Storage").writeJSON("clockbg.json", settings);
|
||||
}
|
||||
|
||||
function getColorsImage(cols) {
|
||||
var bpp = 1;
|
||||
if (cols.length>4) bpp=4;
|
||||
else if (cols.length>2) bpp=2;
|
||||
var w = (cols.length>8)?8:16;
|
||||
var b = Graphics.createArrayBuffer(w*cols.length,16,bpp);
|
||||
b.palette = new Uint16Array(1<<bpp);
|
||||
cols.forEach((c,i)=>{
|
||||
b.setColor(i).fillRect(i*w,0,i*w+w-1,15);
|
||||
b.palette[i] = g.toColor(c);
|
||||
});
|
||||
return "\0"+b.asImage("string");
|
||||
}
|
||||
function getColorsImage(cols) {
|
||||
var bpp = 1;
|
||||
if (cols.length>4) bpp=4;
|
||||
else if (cols.length>2) bpp=2;
|
||||
var w = (cols.length>8)?8:16;
|
||||
var b = Graphics.createArrayBuffer(w*cols.length,16,bpp);
|
||||
b.palette = new Uint16Array(1<<bpp);
|
||||
cols.forEach((c,i)=>{
|
||||
b.setColor(i).fillRect(i*w,0,i*w+w-1,15);
|
||||
b.palette[i] = g.toColor(c);
|
||||
});
|
||||
return "\0"+b.asImage("string");
|
||||
}
|
||||
|
||||
function showModeMenu() {
|
||||
E.showMenu({
|
||||
"" : {title:/*LANG*/"Background", back:showMainMenu},
|
||||
/*LANG*/"Solid Color" : function() {
|
||||
var cols = ["#F00","#0F0","#FF0",
|
||||
"#00F","#F0F","#0FF",
|
||||
"#000","#888","#fff",];
|
||||
var menu = {"":{title:/*LANG*/"Colors", back:showModeMenu}};
|
||||
cols.forEach(col => {
|
||||
menu["-"+getColorsImage([col])] = () => {
|
||||
settings.style = "color";
|
||||
settings.color = col;
|
||||
saveSettings();
|
||||
showMainMenu();
|
||||
};
|
||||
});
|
||||
E.showMenu(menu);
|
||||
},
|
||||
/*LANG*/"Random Color" : function() {
|
||||
var cols = [
|
||||
["#F00","#0F0","#FF0","#00F","#F0F","#0FF"],
|
||||
["#F00","#0F0","#00F"],
|
||||
// Please add some more!
|
||||
];
|
||||
var menu = {"":{title:/*LANG*/"Colors", back:showModeMenu}};
|
||||
cols.forEach(col => {
|
||||
menu[getColorsImage(col)] = () => {
|
||||
settings.style = "randomcolor";
|
||||
settings.colors = col;
|
||||
saveSettings();
|
||||
showMainMenu();
|
||||
};
|
||||
});
|
||||
E.showMenu(menu);
|
||||
},
|
||||
/*LANG*/"Image" : function() {
|
||||
let images = require("Storage").list(/clockbg\..*\.img/);
|
||||
if (images.length) {
|
||||
var menu = {"":{title:/*LANG*/"Images", back:showModeMenu}};
|
||||
images.forEach(im => {
|
||||
menu[im.slice(8,-4)] = () => {
|
||||
settings.style = "image";
|
||||
settings.fn = im;
|
||||
function showModeMenu() {
|
||||
E.showMenu({
|
||||
"" : {title:/*LANG*/"Background", back:showMainMenu},
|
||||
/*LANG*/"Solid Color" : function() {
|
||||
var cols = ["#F00","#0F0","#FF0",
|
||||
"#00F","#F0F","#0FF",
|
||||
"#000","#888","#fff",];
|
||||
var menu = {"":{title:/*LANG*/"Colors", back:showModeMenu}};
|
||||
cols.forEach(col => {
|
||||
menu["-"+getColorsImage([col])] = () => {
|
||||
settings.style = "color";
|
||||
settings.color = col;
|
||||
saveSettings();
|
||||
showMainMenu();
|
||||
};
|
||||
});
|
||||
E.showMenu(menu);
|
||||
},
|
||||
/*LANG*/"Random Color" : function() {
|
||||
var cols = [
|
||||
["#F00","#0F0","#FF0","#00F","#F0F","#0FF"],
|
||||
["#F00","#0F0","#00F"],
|
||||
["#FF0","#F0F","#0FF"],
|
||||
["#00f","#0bf","#0f7","#3f0","#ff0","#f30","#f07","#b0f"],
|
||||
["#66f","#6df","#6fb","#8f6","#ff6","#f86","#f6b","#d6f"],
|
||||
["#007","#057","#073","#170","#770","#710","#703","#507"]
|
||||
// Please add some more!
|
||||
];
|
||||
var menu = {"":{title:/*LANG*/"Colors", back:showModeMenu}};
|
||||
cols.forEach(col => {
|
||||
menu[getColorsImage(col)] = () => {
|
||||
settings.style = "randomcolor";
|
||||
settings.colors = col;
|
||||
saveSettings();
|
||||
showMainMenu();
|
||||
};
|
||||
});
|
||||
E.showMenu(menu);
|
||||
},
|
||||
/*LANG*/"Image" : function() {
|
||||
let images = require("Storage").list(/clockbg\..*\.img/);
|
||||
if (images.length) {
|
||||
var menu = {"":{title:/*LANG*/"Images", back:showModeMenu}};
|
||||
images.forEach(im => {
|
||||
menu[im.slice(8,-4)] = () => {
|
||||
settings.style = "image";
|
||||
settings.fn = im;
|
||||
saveSettings();
|
||||
showMainMenu();
|
||||
};
|
||||
});
|
||||
E.showMenu(menu);
|
||||
} else {
|
||||
E.showAlert("Please use App Loader to upload images").then(showModeMenu);
|
||||
}
|
||||
},
|
||||
/*LANG*/"Squares" : function() {
|
||||
var cols = [ // list of color palettes used as possible square colours - either 4 or 16 entries
|
||||
["#00f","#05f","#0bf","#0fd","#0f7","#0f1","#3f0","#9f0","#ff0","#f90","#f30","#f01","#f07","#f0d","#b0f","#50f"],
|
||||
["#44f","#48f","#4df","#4fe","#4fa","#4f6","#7f4","#bf4","#ff4","#fb4","#f74","#f46","#f4a","#f4e","#d4f","#84f"],
|
||||
["#009","#039","#079","#098","#094","#091","#290","#590","#990","#950","#920","#901","#904","#908","#709","#309"],
|
||||
["#0FF","#0CC","#088","#044"],
|
||||
["#FFF","#FBB","#F66","#F44"],
|
||||
["#FFF","#BBB","#666","#000"],
|
||||
["#fff","#bbf","#77f","#33f"],
|
||||
["#fff","#bff","#7fe","#3fd"]
|
||||
// Please add some more! 4 or 16 only!
|
||||
];
|
||||
var menu = {"":{title:/*LANG*/"Squares", back:showModeMenu}};
|
||||
cols.forEach(col => {
|
||||
menu[getColorsImage(col)] = () => {
|
||||
settings.style = "squares";
|
||||
settings.colors = col;
|
||||
saveSettings();
|
||||
showMainMenu();
|
||||
};
|
||||
});
|
||||
E.showMenu(menu);
|
||||
},
|
||||
/*LANG*/"Plasma" : function() {
|
||||
var cols = [ // list of color palettes used as possible square colours - 16 entries
|
||||
["#00f","#05f","#0bf","#0fd","#0f7","#0f1","#3f0","#9f0","#ff0","#f90","#f30","#f01","#f07","#f0d","#b0f","#50f"],
|
||||
["#44f","#48f","#4df","#4fe","#4fa","#4f6","#7f4","#bf4","#ff4","#fb4","#f74","#f46","#f4a","#f4e","#d4f","#84f"],
|
||||
["#009","#039","#079","#098","#094","#091","#290","#590","#990","#950","#920","#901","#904","#908","#709","#309"],
|
||||
["#fff","#fef","#fdf","#fcf","#fbf","#fae","#f9e","#f8e","#f7e","#f6e","#f5d","#f4d","#f3d","#f2d","#f1d","#f0c"],
|
||||
["#fff","#eff","#dff","#cef","#bef","#adf","#9df","#8df","#7cf","#6cf","#5bf","#4bf","#3bf","#2af","#1af","#09f"],
|
||||
["#000","#010","#020","#130","#140","#250","#260","#270","#380","#390","#4a0","#4b0","#5c0","#5d0","#5e0","#6f0"]
|
||||
// Please add some more!
|
||||
];
|
||||
var menu = {"":{title:/*LANG*/"Plasma", back:showModeMenu}};
|
||||
cols.forEach(col => {
|
||||
menu[getColorsImage(col)] = () => {
|
||||
settings.style = "plasma";
|
||||
settings.colors = col;
|
||||
saveSettings();
|
||||
showMainMenu();
|
||||
};
|
||||
});
|
||||
E.showMenu(menu);
|
||||
} else {
|
||||
E.showAlert("Please use App Loader to upload images").then(showModeMenu);
|
||||
}
|
||||
},
|
||||
/*LANG*/"Squares" : function() {
|
||||
/*
|
||||
a = new Array(16);
|
||||
a.fill(0);
|
||||
print(a.map((n,i)=>E.HSBtoRGB(0 + i/16,1,1,24).toString(16).padStart(6,0).replace(/(.).(.).(.)./,"\"#$1$2$3\"")).join(","))
|
||||
*/
|
||||
var cols = [ // list of color palettes used as possible square colours - either 4 or 16 entries
|
||||
["#00f","#05f","#0bf","#0fd","#0f7","#0f1","#3f0","#9f0","#ff0","#f90","#f30","#f01","#f07","#f0d","#b0f","#50f"],
|
||||
["#0FF","#0CC","#088","#044"],
|
||||
["#FFF","#FBB","#F66","#F44"],
|
||||
["#FFF","#BBB","#666","#000"]
|
||||
// Please add some more!
|
||||
];
|
||||
var menu = {"":{title:/*LANG*/"Squares", back:showModeMenu}};
|
||||
cols.forEach(col => {
|
||||
menu[getColorsImage(col)] = () => {
|
||||
settings.style = "squares";
|
||||
settings.colors = col;
|
||||
console.log(settings);
|
||||
saveSettings();
|
||||
showMainMenu();
|
||||
};
|
||||
});
|
||||
E.showMenu(menu);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showMainMenu() {
|
||||
E.showMenu({
|
||||
"" : {title:/*LANG*/"Clock Background", back:back},
|
||||
/*LANG*/"Mode" : {
|
||||
value : settings.style,
|
||||
onchange : showModeMenu
|
||||
}
|
||||
});
|
||||
}
|
||||
function showMainMenu() {
|
||||
E.showMenu({
|
||||
"" : {title:/*LANG*/"Clock Background", back:back},
|
||||
/*LANG*/"Mode" : {
|
||||
value : settings.style,
|
||||
onchange : showModeMenu
|
||||
},
|
||||
/*LANG*/"View" : () => {
|
||||
Bangle.setUI({mode:"custom",touch:showMainMenu,btn:showMainMenu});
|
||||
require("clockbg").reload();
|
||||
require("clockbg").fillRect(Bangle.appRect);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
showMainMenu();
|
||||
})
|
||||
/* Scripts for generating colors. Change the values in HSBtoRGB to generate different effects
|
||||
|
||||
|
||||
a = new Array(16);
|
||||
a.fill(0);
|
||||
g.clear();
|
||||
w = Math.floor(g.getWidth()/a.length);
|
||||
print(a.map((n,i)=>{
|
||||
var j = i/(a.length-1); // 0..1
|
||||
var c = E.HSBtoRGB(j,1,1,24); // rainbow
|
||||
var c = E.HSBtoRGB(j,0.6,1,24); // faded rainbow
|
||||
var c = E.HSBtoRGB(0.8, j,1,24); // purple->white
|
||||
var c = E.HSBtoRGB(0.1, j,1,24); // blue->white
|
||||
var c = E.HSBtoRGB(0.4, 1,j,24); // black->green
|
||||
var col = c.toString(16).padStart(6,0).replace(/(.).(.).(.)./,"\"#$1$2$3\"");
|
||||
g.setColor(eval(col)).fillRect(i*w,0, i*w+w-1,31);
|
||||
return col;
|
||||
}).join(","))
|
||||
|
||||
*/
|
||||
|
||||
showMainMenu();
|
||||
})
|
|
@ -114,4 +114,4 @@
|
|||
};
|
||||
// Show the menu
|
||||
E.showMenu(menu);
|
||||
});
|
||||
})
|
||||
|
|
|
@ -7,4 +7,4 @@
|
|||
"showDate", "hideWidgets"
|
||||
]);
|
||||
E.showMenu(menu);
|
||||
});
|
||||
})
|
||||
|
|
|
@ -12,3 +12,4 @@
|
|||
0.30: Added options to show widgets and date on twist and tap. New fonts.
|
||||
0.31: Bugfix, no more freeze.
|
||||
0.32: Minor code improvements
|
||||
0.33: Messages would sometimes halt the clock. This should be fixed now.
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
{
|
||||
let drawTimeout;
|
||||
let extrasTimeout;
|
||||
let onLock;
|
||||
//let onTap;
|
||||
//let onTwist;
|
||||
let extrasTimer=0;
|
||||
let settings = require('Storage').readJSON("contourclock.json", true) || {};
|
||||
if (settings.fontIndex == undefined) {
|
||||
settings.fontIndex = 0;
|
||||
|
@ -16,9 +13,9 @@
|
|||
require('Storage').writeJSON("contourclock.json", settings);
|
||||
}
|
||||
require("FontTeletext10x18Ascii").add(Graphics);
|
||||
let extrasShown = (!settings.hidewhenlocked) && (!Bangle.isLocked());
|
||||
let installedFonts = require('Storage').readJSON("contourclock-install.json") || {};
|
||||
if (installedFonts.n > 0) { //New install - check for unused font files
|
||||
// New install - check for unused font files. This should probably be handled by the installer instead
|
||||
if (installedFonts.n > 0) {
|
||||
settings.fontIndex = E.clip(settings.fontIndex, -installedFonts.n + 1, installedFonts.n - 1);
|
||||
require('Storage').writeJSON("contourclock.json", settings);
|
||||
for (let n = installedFonts.n;; n++) {
|
||||
|
@ -27,14 +24,22 @@
|
|||
}
|
||||
require("Storage").erase("contourclock-install.json");
|
||||
}
|
||||
let showExtras = function() { //show extras for a limited time
|
||||
let onLock = function(locked) {if (!locked) showExtras();};
|
||||
let showExtras = function() { //show extras for 5s
|
||||
drawExtras();
|
||||
if (extrasTimeout) clearTimeout(extrasTimeout);
|
||||
extrasTimeout = setTimeout(() => {
|
||||
extrasTimeout = undefined;
|
||||
hideExtras();
|
||||
}, 5000);
|
||||
extrasShown = false;
|
||||
extrasTimer = 5000-60000-(Date.now()%60000);
|
||||
if (extrasTimer<0) { //schedule next redraw early to hide extras
|
||||
if (drawTimeout) clearTimeout(drawTimeout);
|
||||
drawTimeout = setTimeout(function() {
|
||||
drawTimeout = undefined;
|
||||
draw();
|
||||
}, 5000);
|
||||
}
|
||||
};
|
||||
let hideExtras = function() {
|
||||
g.reset();
|
||||
g.clearRect(0, 138, g.getWidth() - 1, 176);
|
||||
if (settings.widgets) require("widget_utils").hide();
|
||||
};
|
||||
let drawExtras = function() { //draw date, day of the week and widgets
|
||||
let date = new Date();
|
||||
|
@ -43,38 +48,27 @@
|
|||
g.setFont("Teletext10x18Ascii").setFontAlign(0, 1);
|
||||
if (settings.weekday) g.drawString(require("locale").dow(date).toUpperCase(), g.getWidth() / 2, g.getHeight() - 18);
|
||||
if (settings.date) g.drawString(require('locale').date(date, 1), g.getWidth() / 2, g.getHeight());
|
||||
require("widget_utils").show();
|
||||
extrasShown = true;
|
||||
};
|
||||
let hideExtras = function() {
|
||||
if (extrasTimeout) clearTimeout(extrasTimeout);
|
||||
extrasTimeout = undefined; //NEW
|
||||
g.reset();
|
||||
g.clearRect(0, 138, g.getWidth() - 1, 176);
|
||||
require("widget_utils").hide();
|
||||
extrasShown = false; ///NEW
|
||||
if (settings.widgets) require("widget_utils").show();
|
||||
};
|
||||
let draw = function() {
|
||||
if (drawTimeout) clearTimeout(drawTimeout); //NEW
|
||||
drawTimeout = setTimeout(function() {
|
||||
drawTimeout = undefined;
|
||||
draw();
|
||||
}, 60000 - (Date.now() % 60000));
|
||||
if (extrasTimer>0) { //schedule next draw early to remove extras
|
||||
drawTimeout = setTimeout(function() {
|
||||
drawTimeout = undefined;
|
||||
draw();
|
||||
}, extrasTimer);
|
||||
extrasTimer=0;
|
||||
} else {
|
||||
if (settings.hideWhenLocked) hideExtras();
|
||||
drawTimeout = setTimeout(function() {
|
||||
drawTimeout = undefined;
|
||||
draw();
|
||||
}, 60000 - (Date.now() % 60000));
|
||||
}
|
||||
g.reset();
|
||||
if (extrasShown) drawExtras();
|
||||
else hideExtras();
|
||||
if (!settings.hideWhenLocked) drawExtras();
|
||||
require('contourclock').drawClock(settings.fontIndex);
|
||||
};
|
||||
if (settings.hideWhenLocked) {
|
||||
onLock = locked => {
|
||||
if (!locked) {
|
||||
require("widget_utils").show();
|
||||
drawExtras();
|
||||
} else {
|
||||
require("widget_utils").hide();
|
||||
hideExtras();
|
||||
}
|
||||
};
|
||||
Bangle.on('lock', onLock);
|
||||
if (settings.tapToShow) Bangle.on('tap', showExtras);
|
||||
if (settings.twistToShow) Bangle.on('twist', showExtras);
|
||||
|
@ -82,14 +76,16 @@
|
|||
Bangle.setUI({
|
||||
mode: "clock",
|
||||
remove: function() {
|
||||
Bangle.removeListener('lock', onLock);
|
||||
Bangle.removeListener('tap', showExtras);
|
||||
Bangle.removeListener('twist', showExtras);
|
||||
if (drawTimeout) clearTimeout(drawTimeout);
|
||||
if (extrasTimeout) clearTimeout(extrasTimeout);
|
||||
drawTimeout = undefined;
|
||||
extrasTimeout = undefined;
|
||||
if (settings.hideWhenLocked) require("widget_utils").show();
|
||||
if (settings.hideWhenLocked) {
|
||||
Bangle.removeListener('lock', onLock);
|
||||
if (settings.tapToShow) Bangle.removeListener('tap', showExtras);
|
||||
if (settings.twistToShow) Bangle.removeListener('twist', showExtras);
|
||||
}
|
||||
if (drawTimeout) {
|
||||
clearTimeout(drawTimeout);
|
||||
drawTimeout = undefined;
|
||||
}
|
||||
if (settings.hideWhenLocked && settings.widgets) require("widget_utils").show();
|
||||
g.reset();
|
||||
g.clear();
|
||||
}
|
||||
|
@ -97,7 +93,8 @@
|
|||
g.clear();
|
||||
if (settings.widgets) {
|
||||
Bangle.loadWidgets();
|
||||
setTimeout(Bangle.drawWidgets,0); //NEW
|
||||
Bangle.drawWidgets();
|
||||
}
|
||||
draw();
|
||||
if (!settings.hideWhenLocked || !Bangle.isLocked()) showExtras();
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{ "id": "contourclock",
|
||||
"name": "Contour Clock",
|
||||
"shortName" : "Contour Clock",
|
||||
"version": "0.32",
|
||||
"version": "0.33",
|
||||
"icon": "app.png",
|
||||
"readme": "README.md",
|
||||
"description": "A Minimalist clockface with large Digits.",
|
||||
|
|
|
@ -2,3 +2,4 @@
|
|||
0.02: Added decrement and touch functions
|
||||
0.03: Set color - ensures widgets don't end up coloring the counter's text
|
||||
0.04: Adopted for BangleJS 2
|
||||
0.05: Support translations
|
||||
|
|
|
@ -95,9 +95,9 @@ if (BANGLEJS2) {
|
|||
g.clear(1).setFont("6x8");
|
||||
g.setBgColor(g.theme.bg).setColor(g.theme.fg);
|
||||
if (BANGLEJS2) {
|
||||
g.drawString('Swipe up to increase\nSwipe down to decrease\nPress button to reset.', x, 100 + y);
|
||||
g.drawString([/*LANG*/"Swipe up to increase", /*LANG*/"Swipe down to decrease", /*LANG*/"Press button to reset"].join("\n"), x, 100 + y);
|
||||
} else {
|
||||
g.drawString('Tap right or BTN1 to increase\nTap left or BTN3 to decrease\nPress BTN2 to reset.', x, 100 + y);
|
||||
g.drawString([/*LANG*/"Tap right or BTN1 to increase", /*LANG*/"Tap left or BTN3 to decrease", /*LANG*/"Press BTN2 to reset"].join("\n"), x, 100 + y);
|
||||
}
|
||||
|
||||
Bangle.loadWidgets();
|
||||
|
|