Merge branch 'espruino:master' into master

master
Giles Booth 2022-12-24 15:41:59 +00:00 committed by GitHub
commit faea3b62b3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1476 changed files with 50854 additions and 15996 deletions

View File

@ -3,4 +3,5 @@ apps/banglerun/rollup.config.js
apps/schoolCalendar/fullcalendar/main.js
apps/authentiwatch/qr_packed.js
apps/qrcode/qr-scanner.umd.min.js
apps/gipy/pkg/gpconv.js
*.test.js

View File

@ -22,9 +22,6 @@ jobs:
- name: Install typescript dependencies
working-directory: ./typescript
run: npm ci
- name: Build types
working-directory: ./typescript
run: npm run build:types
- name: Build all TS apps and widgets
working-directory: ./typescript
run: npm run build

4
.gitignore vendored
View File

@ -11,3 +11,7 @@ tests/Layout/testresult.bmp
apps.local.json
_site
.jekyll-cache
.owncloudsync.log
Desktop.ini
.sync_*.db*
*.swp

3
.gitmodules vendored
View File

@ -1,3 +1,6 @@
[submodule "EspruinoAppLoaderCore"]
path = core
url = https://github.com/espruino/EspruinoAppLoaderCore.git
[submodule "webtools"]
path = webtools
url = https://github.com/espruino/EspruinoWebTools.git

View File

@ -255,8 +255,11 @@ and which gives information about the app for the Launcher.
// 'app' - an application
// 'clock' - a clock - required for clocks to automatically start
// 'widget' - a widget
// 'module' - this provides a module that can be used with 'require'.
// 'provides_modules' should be used if type:module is specified
// 'bootloader' - an app that at startup (app.boot.js) but doesn't have a launcher entry for 'app.js'
// 'settings' - apps that appear in Settings->Apps (with appname.settings.js) but that have no 'app.js'
// 'clkinfo' - Provides a 'myapp.clkinfo.js' file that can be used to display info in clocks - see modules/clock_info.js
// 'RAM' - code that runs and doesn't upload anything to storage
// 'launch' - replacement 'Launcher'
// 'textinput' - provides a 'textinput' library that allows text to be input on the Bangle
@ -266,10 +269,24 @@ and which gives information about the app for the Launcher.
// 'locale' - provides 'locale' library for language-specific date/distance/etc
// (a version of 'locale' is included in the firmware)
"tags": "", // comma separated tag list for searching
// common types are:
// 'clock' - it's a clock
// 'widget' - it is (or provides) a widget
// 'outdoors' - useful for outdoor activities
// 'tool' - a useful utility (timer, calculator, etc)
// 'game' - a game
// 'bluetooth' - uses Bluetooth LE
// 'system' - used by the system
// 'clkinfo' - provides or uses clock_info module for data on your clock face (see modules/clock_info.js)
"supports": ["BANGLEJS2"], // List of device IDs supported, either BANGLEJS or BANGLEJS2
"dependencies" : { "notify":"type" } // optional, app 'types' we depend on (see "type" above)
"dependencies" : { "messages":"app" } // optional, depend on a specific app ID
// for instance this will use notify/notifyfs is they exist, or will pull in 'notify'
"dependencies" : { "messageicons":"module" } // optional, depend on a specific library to be used with 'require' - see provides_modules
"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'
"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)
// A 'Read more...' link will be added under the app
@ -324,7 +341,7 @@ and which gives information about the app for the Launcher.
```
* name, icon and description present the app in the app loader.
* tags is used for grouping apps in the library, separate multiple entries by comma. Known tags are `tool`, `system`, `clock`, `game`, `sound`, `gps`, `widget`, `launcher` or empty.
* tags is used for grouping apps in the library, separate multiple entries by comma. Known tags are `tool`, `system`, `clock`, `game`, `sound`, `gps`, `widget`, `launcher`, `bluetooth` or empty.
* storage is used to identify the app files and how to handle them
* data is used to clean up files when the app is uninstalled
@ -454,7 +471,10 @@ It should also add `myappid.json` to `data`, to make sure it is cleaned up when
## Modules
You can include any of [Espruino's modules](https://www.espruino.com/Modules) as
normal with `require("modulename")`. If you want to develop your own module for your
normal with `require("modulename")`. To include [Bangle's modules](modules) for use in the Web
IDE, [upload the modules to internal storage](modules#upload-the-module-to-the-bangles-internal-storage)
or [change the IDE's search path](modules#change-the-web-ide-search-path-to-include-banglejs-modules).
If you want to develop your own module for your
app(s) then you can do that too. Just add the module into the `modules` folder
then you can use it from your app as normal.

View File

@ -170,10 +170,10 @@
</div>
</footer>
<script src="https://www.puck-js.com/puck.js"></script>
<script src="webtools/puck.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/lib/heatshrink.js"></script>
<script src="core/js/utils.js"></script>
<script src="loader.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js"></script> <!-- for backup.js -->

View File

@ -149,11 +149,11 @@ function drawHSeg(x1,y1,x2,y2,Num,Color,Size) {
if (Color == "fg") {
g.setColor(g.theme.fg);
} else {
g.setColor(mColor[0],mColor[1],mColor[2]);
g.setColor(mColor[0],mColor[1],mColor[2]);
}
g.fillCircle(x1+Dx+(i-1)*(x2-x1)/7,y1+Dy+(j-1)*(y2-y1)/7,Size);
} else {
g.setColor(bColor[0],bColor[1],bColor[2]);
g.setColor(bColor[0],bColor[1],bColor[2]);
g.fillCircle(x1+Dx+(i-1)*(x2-x1)/7,y1+Dy+(j-1)*(y2-y1)/7,1);
}
}
@ -166,7 +166,7 @@ function drawSSeg(x1,y1,x2,y2,Num,Color,Size) {
for (let j = 1; j < 8; j++) {
if (Font[Num][j-1][i-1] == 1) {
if (Color == "fg") {
g.setColor(sColor[0],sColor[1],sColor[2]);
g.setColor(sColor[0],sColor[1],sColor[2]);
} else {
g.setColor(g.theme.fg);
//g.setColor(0.7,0.7,0.7);
@ -253,8 +253,8 @@ function actions(v){
if(BTN1.read() === true) {
print("BTN pressed");
Bangle.showLauncher();
}
}
if(v==-1){
print("up swipe event");
if(settings.swupApp != "") load(settings.swupApp);
@ -269,7 +269,7 @@ function actions(v){
}
// Get Messages status
var messages = require("Storage").readJSON("messages.json",1)||[];
var messages_installed = require("Storage").read("messages") !== undefined;
//var BTconnected = NRF.getSecurityStatus().connected;
//NRF.on('connect',BTconnected = NRF.getSecurityStatus().connected);
@ -289,27 +289,27 @@ function drawWidgeds() {
g.setColor((g.getBPP()>8) ? "#07f" : (g.theme.dark ? "#0ff" : "#00f"));
else
g.setColor(g.theme.dark ? "#666" : "#999");
g.drawImage(atob("CxQBBgDgFgJgR4jZMawfAcA4D4NYybEYIwTAsBwDAA=="),x1Bt,y1Bt);
g.drawImage(atob("CxQBBgDgFgJgR4jZMawfAcA4D4NYybEYIwTAsBwDAA=="),x1Bt,y1Bt);
//Battery
//print(E.getBattery());
//print(Bangle.isCharging());
var x1B = 130;
var y1B = 2;
var x2B = x1B + 20;
var y2B = y1B + 15;
g.setColor(g.theme.bg);
g.clearRect(x1B,y1B,x2B,y2B);
g.setColor(g.theme.fg);
g.drawRect(x1B,y1B,x2B,y2B);
g.fillRect(x1B,y1B,x1B+(E.getBattery()*(x2B-x1B)/100),y2B);
g.fillRect(x2B,y1B+(y2B-y1B)/2-3,x2B+4,y1B+(y2B-y1B)/2+3);
//Messages
@ -318,25 +318,25 @@ function drawWidgeds() {
var x2M = x1M + 25;
var y2M = y2B;
if (messages.some(m=>m.new)) {
if (messages_installed && require("messages").status() == "new") {
g.setColor(g.theme.fg);
g.fillRect(x1M,y1M,x2M,y2M);
g.setColor(g.theme.bg);
g.drawLine(x1M,y1M,x1M+(x2M-x1M)/2,y1M+(y2M-y1M)/2);
g.drawLine(x1M+(x2M-x1M)/2,y1M+(y2M-y1M)/2,x2M,y1M);
}
var strDow = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
var d = new Date();
var dow = d.getDay(),day = d.getDate(), month = d.getMonth() + 1, year = d.getFullYear();
print(strDow[dow] + ' ' + day + '.' + month + ' ' + year);
g.setColor(g.theme.fg);
g.setFontAlign(-1, -1,0);
g.setFont("Vector", 20);
g.drawString(strDow[dow] + ' ' + day, 0, 0, true);
}
@ -354,7 +354,7 @@ function SetFull(on) {
} else {
Ys = 30;
Bangle.setUI("updown",actions);
Bangle.on('swipe', function(direction) {
Bangle.on('swipe', function(direction) {
switch (direction) {
case 1:
print("swipe left event");
@ -362,7 +362,7 @@ function SetFull(on) {
print(settings.swleftApp);
break;
case -1:
print("swipe right event");
print("swipe right event");
if(settings.swrightApp != "") load(settings.swrightApp);
print(settings.swrightApp);
break;
@ -374,7 +374,7 @@ function SetFull(on) {
SegH = (Ye-Ys)/2;
Dy = SegH/16;
draw();
if (on != true) {

View File

@ -1,2 +1,3 @@
0.01: Initial version for upload
0.02: better theme support, configurable colors, small improvements
0.02: Better theme support, configurable colors, small improvements
0.03: Use `messages` library to check for new messages

View File

@ -1,7 +1,7 @@
{ "id": "7x7dotsclock",
"name": "7x7 Dots Clock",
"shortName":"7x7 Dots Clock",
"version":"0.02",
"version":"0.03",
"description": "A clock with a big 7x7 dots Font",
"icon": "dotsfontclock.png",
"tags": "clock",

View File

@ -1,2 +1,3 @@
0.01: New App!
0.02: Fullscreen settings.
0.02: Fullscreen settings.
0.03: Tell clock widgets to hide.

View File

@ -115,6 +115,9 @@ function draw() {
}
}
// Show launcher when middle button pressed
Bangle.setUI("clock");
Bangle.loadWidgets();
// Clear the screen once, at startup
@ -140,5 +143,3 @@ Bangle.on('lock', function(isLocked) {
});
// Show launcher when middle button pressed
Bangle.setUI("clock");

View File

@ -1,7 +1,7 @@
{
"id": "90sclk",
"name": "90s Clock",
"version": "0.02",
"version": "0.03",
"description": "A 90s style watch-face",
"readme": "README.md",
"icon": "app.png",

View File

@ -0,0 +1 @@
0.01: Initial version

View File

@ -0,0 +1,13 @@
# a_dndtoggle - Toggle Quiet Mode of the watch
When Quiet mode is off, just start this app to set quiet mode. Start it again to turn off quiet mode.
Work in progress.
#ToDo
Settings page, current status indicator.
## Creator
Hank - contact at http://forum.espruino.com

View File

@ -0,0 +1,43 @@
const modeNames = [/*LANG*/"Noisy", /*LANG*/"Alarms", /*LANG*/"Silent"];
let bSettings = require('Storage').readJSON('setting.json',true)||{};
let current = 0|bSettings.quiet;
//0 off
//1 alarms
//2 silent
console.log("old: " + current);
switch (current) {
case 0:
bSettings.quiet = 2;
Bangle.buzz();
setTimeout('Bangle.buzz();',500);
break;
case 1:
bSettings.quiet = 0;
Bangle.buzz();
break;
case 2:
bSettings.quiet = 0;
Bangle.buzz();
break;
default:
bSettings.quiet = 0;
Bangle.buzz();
}
console.log("new: " + bSettings.quiet);
E.showMessage(modeNames[current] + " -> " + modeNames[bSettings.quiet]);
setTimeout('exitApp();', 2000);
function exitApp(){
require("Storage").writeJSON("setting.json", bSettings);
// reload clocks with new theme, otherwise just wait for user to switch apps
load()
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 486 B

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwJC/AAl/Agf/AAUAgIFDwEHAofgh/g/0Ag/wj+AnwVB/EegEfEIN4nkAh+AgE8vgVBAoV4Aoce/EAgfADQIFcjwpFHYIFCnxBFJopZBn5ZCMopxFPoqJFSowA/gA="))

View File

@ -0,0 +1,16 @@
{
"id": "a_dndtoggle",
"name": "a_dndtoggle - Toggle Quiet Mode of the watch",
"shortName": "A_DND Toggle",
"version": "0.01",
"description": "Toggle Quiet Mode of the watch just by starting this app.",
"icon": "a_dndtoggle.png",
"type": "app",
"tags": "tool",
"supports": ["BANGLEJS","BANGLEJS2"],
"storage": [
{"name":"a_dndtoggle.app.js","url":"a_dndtoggle.app.js"},
{"name":"a_dndtoggle.img","url":"app-icon.js","evaluate":true}
],
"readme": "README.md"
}

View File

@ -10,3 +10,5 @@
0.10: Added separate Bangle.js 2 file with Bangle.js 2 kickstarter pixels (as of 28 Oct 2021)
0.11: Bangle.js2: New pixels, btn1 to exit
0.12: Actual pixels as of 29th Nov 2021
0.13: Bangle.js 2: Use setUI to add software back button
0.14: Add automatic translation of more strings

View File

@ -11,8 +11,8 @@ g.drawString("BANGLEJS.COM",120,y-4);
} else {
y=-(4+h); // small screen, start right at top
}
g.drawString("Powered by Espruino",0,y+=4+h);
g.drawString("Version "+ENV.VERSION,0,y+=h);
g.drawString(/*LANG*/"Powered by Espruino",0,y+=4+h);
g.drawString(/*LANG*/"Version "+ENV.VERSION,0,y+=h);
g.drawString("Commit "+ENV.GIT_COMMIT,0,y+=h);
function getVersion(name,file) {
var j = s.readJSON(file,1);
@ -24,9 +24,9 @@ getVersion("Launcher","launch.info");
getVersion("Settings","setting.info");
y+=h;
g.drawString(MEM.total+" JS Variables available",0,y+=h);
g.drawString("Storage: "+(require("Storage").getFree()>>10)+"k free",0,y+=h);
if (ENV.STORAGE) g.drawString(" "+(ENV.STORAGE>>10)+"k total",0,y+=h);
g.drawString(MEM.total+/*LANG*/" JS Variables available",0,y+=h);
g.drawString("Storage: "+(require("Storage").getFree()>>10)+/*LANG*/"k free",0,y+=h);
if (ENV.STORAGE) g.drawString(" "+(ENV.STORAGE>>10)+/*LANG*/"k total",0,y+=h);
if (ENV.SPIFLASH) g.drawString("SPI Flash: "+(ENV.SPIFLASH>>10)+"k",0,y+=h);
g.setFontAlign(0,-1);
g.flip();

View File

@ -10,7 +10,7 @@ var img = atob("sIwDkm2S66DYwA2AAAAAHAHGSRxJEkAAgmGGBxDIADIdAFJIbAHF9HP00kBUC6Dt
var imgHeight = g.imageMetrics(img).height;
var imgScroll = Math.floor(Math.random()*imgHeight);
g.reset().setFont("6x15").setFontAlign(0,0);
g.clear(1).setFont("6x15").setFontAlign(0,0);
g.drawString(ENV.VERSION + " " + NRF.getAddress(), g.getWidth()/2, 171);
g.drawImage(img,0,24);
@ -35,17 +35,17 @@ function drawInfo() {
g.setFont("4x6").setFontAlign(0,0).drawString("BANGLEJS.COM",W-30,56);
var h=8, y = 24-h;
g.setFont("6x8").setFontAlign(-1,-1);
g.drawString("Powered by Espruino",0,y+=4+h);
g.drawString("Version "+ENV.VERSION,0,y+=h);
g.drawString(/*LANG*/"Powered by Espruino",0,y+=4+h);
g.drawString(/*LANG*/"Version "+ENV.VERSION,0,y+=h);
g.drawString("Commit "+ENV.GIT_COMMIT,0,y+=h);
getVersion("Bootloader","boot.info");
getVersion("Launcher","launch.info");
getVersion("Settings","setting.info");
g.drawString(MEM.total+" JS Vars",0,y+=h);
g.drawString("Storage: "+(require("Storage").getFree()>>10)+"k free",0,y+=h);
if (ENV.STORAGE) g.drawString(" "+(ENV.STORAGE>>10)+"k total",0,y+=h);
g.drawString(MEM.total+/*LANG*/" JS Vars",0,y+=h);
g.drawString("Storage: "+(require("Storage").getFree()>>10)+/*LANG*/"k free",0,y+=h);
if (ENV.STORAGE) g.drawString(" "+(ENV.STORAGE>>10)+/*LANG*/"k total",0,y+=h);
if (ENV.SPIFLASH) g.drawString("SPI Flash: "+(ENV.SPIFLASH>>10)+"k",0,y+=h);
imageTop = y+h;
imgScroll = imgHeight-imageTop;
@ -69,4 +69,7 @@ function drawImage() {
// TODO: a nice little animation before
setTimeout(drawInfo, 1000);
setWatch(_=>load(), BTN1);
Bangle.setUI({
mode : "custom",
back : load
});

View File

@ -1,7 +1,7 @@
{
"id": "about",
"name": "About",
"version": "0.12",
"version": "0.14",
"description": "Bangle.js About page - showing software version, stats, and a collaborative mural from the Bangle.js KickStarter backers",
"icon": "app.png",
"tags": "tool,system",

View File

@ -2,3 +2,4 @@
0.02: Use the new multiplatform 'Layout' library
0.03: Exit as first menu option, dont show decimal places for seconds
0.04: Localisation, change Exit->Back to allow back-arrow to appear on 2v13 firmware
0.05: Add max G values during recording, record actual G values and magnitude to CSV

View File

@ -1,5 +1,6 @@
var fileNumber = 0;
var MAXLOGS = 9;
var logRawData = false;
function getFileName(n) {
return "accellog."+n+".csv";
@ -24,6 +25,11 @@ function showMenu() {
/*LANG*/"View Logs" : function() {
viewLogs();
},
/*LANG*/"Log raw data" : {
value : logRawData,
format : v => v?/*LANG*/"Yes":/*LANG*/"No",
onchange : v => { logRawData=v; }
},
};
E.showMenu(menu);
}
@ -78,6 +84,7 @@ function viewLogs() {
}
function startRecord(force) {
var stopped = false;
if (!force) {
// check for existing file
var f = require("Storage").open(getFileName(fileNumber), "r");
@ -92,39 +99,101 @@ function startRecord(force) {
var Layout = require("Layout");
var layout = new Layout({ type: "v", c: [
{type:"txt", font:"6x8", label:/*LANG*/"Samples", pad:2},
{type:"txt", id:"samples", font:"6x8:2", label:" - ", pad:5, bgCol:g.theme.bg},
{type:"txt", font:"6x8", label:/*LANG*/"Time", pad:2},
{type:"txt", id:"time", font:"6x8:2", label:" - ", pad:5, bgCol:g.theme.bg},
{type:"txt", font:"6x8:2", label:/*LANG*/"RECORDING", bgCol:"#f00", pad:5, fillx:1},
]
},{btns:[ // Buttons...
{label:/*LANG*/"STOP", cb:()=>{
Bangle.removeListener('accel', accelHandler);
showMenu();
{ type: "h", c: [
{ type: "v", c: [
{type:"txt", font:"6x8", label:/*LANG*/"Samples", pad:2},
{type:"txt", id:"samples", font:"6x8:2", label:" - ", pad:5, bgCol:g.theme.bg},
]},
{ type: "v", c: [
{type:"txt", font:"6x8", label:/*LANG*/"Time", pad:2},
{type:"txt", id:"time", font:"6x8:2", label:" - ", pad:5, bgCol:g.theme.bg},
]},
]},
{ type: "h", c: [
{ type: "v", c: [
{type:"txt", font:"6x8", label:/*LANG*/"Max X", pad:2},
{type:"txt", id:"maxX", font:"6x8", label:" - ", pad:5, bgCol:g.theme.bg},
]},
{ type: "v", c: [
{type:"txt", font:"6x8", label:/*LANG*/"Max Y", pad:2},
{type:"txt", id:"maxY", font:"6x8", label:" - ", pad:5, bgCol:g.theme.bg},
]},
{ type: "v", c: [
{type:"txt", font:"6x8", label:/*LANG*/"Max Z", pad:2},
{type:"txt", id:"maxZ", font:"6x8", label:" - ", pad:5, bgCol:g.theme.bg},
]},
]},
{type:"txt", font:"6x8", label:/*LANG*/"Max G", pad:2},
{type:"txt", id:"maxMag", font:"6x8:4", label:" - ", pad:5, bgCol:g.theme.bg},
{type:"txt", id:"state", font:"6x8:2", label:/*LANG*/"RECORDING", bgCol:"#f00", pad:5, fillx:1},
]},
{
btns:[ // Buttons...
{id: "btnStop", label:/*LANG*/"STOP", cb:()=>{
if (stopped) {
showMenu();
}
else {
Bangle.removeListener('accel', accelHandler);
layout.state.label = /*LANG*/"STOPPED";
layout.state.bgCol = /*LANG*/"#0f0";
stopped = true;
layout.render();
}
}}
]});
layout.render();
// now start writing
var f = require("Storage").open(getFileName(fileNumber), "w");
f.write("Time (ms),X,Y,Z\n");
f.write("Time (ms),X,Y,Z,Total\n");
var start = getTime();
var sampleCount = 0;
var maxMag = 0;
var maxX = 0;
var maxY = 0;
var maxZ = 0;
function accelHandler(accel) {
var t = getTime()-start;
f.write([
t*1000,
accel.x*8192,
accel.y*8192,
accel.z*8192].map(n=>Math.round(n)).join(",")+"\n");
if (logRawData) {
f.write([
t*1000,
accel.x*8192,
accel.y*8192,
accel.z*8192,
accel.mag*8192,
].map(n=>Math.round(n)).join(",")+"\n");
} else {
f.write([
Math.round(t*1000),
accel.x,
accel.y,
accel.z,
accel.mag,
].join(",")+"\n");
}
if (accel.mag > maxMag) {
maxMag = accel.mag.toFixed(2);
}
if (accel.x > maxX) {
maxX = accel.x.toFixed(2);
}
if (accel.y > maxY) {
maxY = accel.y.toFixed(2);
}
if (accel.z > maxZ) {
maxZ = accel.z.toFixed(2);
}
sampleCount++;
layout.samples.label = sampleCount;
layout.time.label = Math.round(t)+"s";
layout.render(layout.samples);
layout.render(layout.time);
layout.maxX.label = maxX;
layout.maxY.label = maxY;
layout.maxZ.label = maxZ;
layout.maxMag.label = maxMag;
layout.render();
}
Bangle.setPollInterval(80); // 12.5 Hz - the default

View File

@ -2,7 +2,7 @@
"id": "accellog",
"name": "Acceleration Logger",
"shortName": "Accel Log",
"version": "0.04",
"version": "0.05",
"description": "Logs XYZ acceleration data to a CSV file that can be downloaded to your PC",
"icon": "app.png",
"tags": "outdoor",

View File

@ -1,6 +1,11 @@
# Active Pedometer
Pedometer that filters out arm movement and displays a step goal progress.
**Note:** Since creation of this app, Bangle.js's step counting algorithm has
improved significantly - and as a result the algorithm in this app (which
runs *on top* of Bangle.js's algorithm) may no longer be accurate.
I changed the step counting algorithm completely.
Now every step is counted when in status 'active', if the time difference between two steps is not too short or too long.
To get in 'active' mode, you have to reach the step threshold before the active timer runs out.
@ -9,6 +14,7 @@ When you reach the step threshold, the steps needed to reach the threshold are c
Steps are saved to a datafile every 5 minutes. You can watch a graph using the app.
## Screenshots
* 600 steps
![](600.png)
@ -70,4 +76,4 @@ Steps are saved to a datafile every 5 minutes. You can watch a graph using the a
## Requests
If you have any feature requests, please post in this forum thread: http://forum.espruino.com/conversations/345754/
If you have any feature requests, please post in this forum thread: http://forum.espruino.com/conversations/345754/

View File

@ -3,7 +3,7 @@
"name": "Active Pedometer",
"shortName": "Active Pedometer",
"version": "0.09",
"description": "Pedometer that filters out arm movement and displays a step goal progress. Steps are saved to a daily file and can be viewed as graph.",
"description": "(NOT RECOMMENDED) Pedometer that filters out arm movement and displays a step goal progress. Steps are saved to a daily file and can be viewed as graph. The `Health` app now provides step logging and graphs.",
"icon": "app.png",
"tags": "outdoors,widget",
"supports": ["BANGLEJS"],

View File

@ -6,3 +6,5 @@
0.06: Add a temperature threshold to detect (and not alert) if the BJS isn't worn. Better support for the peoples using the app at night
0.07: Fix bug on the cutting edge firmware
0.08: Use default Bangle formatter for booleans
0.09: New app screen (instead of showing settings or the alert) and some optimisations
0.10: Add software back button via setUI

View File

@ -0,0 +1,37 @@
(function () {
// load variable before defining functions cause it can trigger a ReferenceError
const activityreminder = require("activityreminder");
const storage = require("Storage");
let activityreminder_data = activityreminder.loadData();
function run() {
E.showPrompt("Inactivity detected", {
title: "Activity reminder",
buttons: { "Ok": 1, "Dismiss": 2, "Pause": 3 }
}).then(function (v) {
if (v == 1) {
activityreminder_data.okDate = new Date();
}
if (v == 2) {
activityreminder_data.dismissDate = new Date();
}
if (v == 3) {
activityreminder_data.pauseDate = new Date();
}
activityreminder.saveData(activityreminder_data);
load();
});
// Obey system quiet mode:
if (!(storage.readJSON('setting.json', 1) || {}).quiet) {
Bangle.buzz(400);
}
setTimeout(load, 20000);
}
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
run();
})();

View File

@ -1,46 +1,58 @@
(function () {
// load variable before defining functions cause it can trigger a ReferenceError
const activityreminder = require("activityreminder");
const storage = require("Storage");
const activityreminder_settings = activityreminder.loadSettings();
let activityreminder_data = activityreminder.loadData();
// load variable before defining functions cause it can trigger a ReferenceError
const activityreminder = require("activityreminder");
let activityreminder_data = activityreminder.loadData();
let W = g.getWidth();
// let H = g.getHeight();
function drawAlert() {
E.showPrompt("Inactivity detected", {
title: "Activity reminder",
buttons: { "Ok": 1, "Dismiss": 2, "Pause": 3 }
}).then(function (v) {
if (v == 1) {
activityreminder_data.okDate = new Date();
}
if (v == 2) {
activityreminder_data.dismissDate = new Date();
}
if (v == 3) {
activityreminder_data.pauseDate = new Date();
}
activityreminder.saveData(activityreminder_data);
load();
});
// Obey system quiet mode:
if (!(storage.readJSON('setting.json', 1) || {}).quiet) {
Bangle.buzz(400);
}
setTimeout(load, 20000);
}
function run() {
if (activityreminder.mustAlert(activityreminder_data, activityreminder_settings)) {
drawAlert();
} else {
eval(storage.read("activityreminder.settings.js"))(() => load());
}
}
function getHoursMins(date){
var h = date.getHours();
var m = date.getMinutes();
return (""+h).substr(-2) + ":" + ("0"+m).substr(-2);
}
function drawData(name, value, y){
g.drawString(name, 10, y);
g.drawString(value, 100, y);
}
function drawInfo() {
var h=18, y = h;
g.setColor(g.theme.fg);
g.setFont("Vector",h).setFontAlign(-1,-1);
// Header
g.drawLine(0,25,W,25);
g.drawLine(0,26,W,26);
g.drawString("Current Cycle", 10, y+=h);
drawData("Start", getHoursMins(activityreminder_data.stepsDate), y+=h);
drawData("Steps", getCurrentSteps(), y+=h);
/*
g.drawString("Button Press", 10, y+=h*2);
drawData("Ok", getHoursMins(activityreminder_data.okDate), y+=h);
drawData("Dismiss", getHoursMins(activityreminder_data.dismissDate), y+=h);
drawData("Pause", getHoursMins(activityreminder_data.pauseDate), y+=h);
*/
}
function getCurrentSteps(){
let health = Bangle.getHealthStatus("day");
return health.steps - activityreminder_data.stepsOnDate;
}
function run() {
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
run();
})();
drawInfo();
Bangle.setUI({
mode : "custom",
back : load
})
}
run();
})();

View File

@ -1,70 +1,81 @@
(function () {
// load variable before defining functions cause it can trigger a ReferenceError
const activityreminder = require("activityreminder");
const activityreminder_settings = activityreminder.loadSettings();
let activityreminder_data = activityreminder.loadData();
if (activityreminder_data.firstLoad) {
activityreminder_data.firstLoad = false;
// load variable before defining functions cause it can trigger a ReferenceError
const activityreminder = require("activityreminder");
const activityreminder_settings = activityreminder.loadSettings();
let activityreminder_data = activityreminder.loadData();
if (activityreminder_data.firstLoad) {
activityreminder_data.firstLoad = false;
activityreminder.saveData(activityreminder_data);
}
function run() {
if (isNotWorn()) return;
let now = new Date();
let h = now.getHours();
if (isDuringAlertHours(h)) {
let health = Bangle.getHealthStatus("day");
if (health.steps - activityreminder_data.stepsOnDate >= activityreminder_settings.minSteps // more steps made than needed
|| health.steps < activityreminder_data.stepsOnDate) { // new day or reboot of the watch
activityreminder_data.stepsOnDate = health.steps;
activityreminder_data.stepsDate = now;
activityreminder.saveData(activityreminder_data);
}
function run() {
if (isNotWorn()) return;
let now = new Date();
let h = now.getHours();
if (isDuringAlertHours(h)) {
let health = Bangle.getHealthStatus("day");
if (health.steps - activityreminder_data.stepsOnDate >= activityreminder_settings.minSteps // more steps made than needed
|| health.steps < activityreminder_data.stepsOnDate) { // new day or reboot of the watch
activityreminder_data.stepsOnDate = health.steps;
activityreminder_data.stepsDate = now;
activityreminder.saveData(activityreminder_data);
/* todo in a futur release
Add settimer to trigger like 30 secs after going in this part cause the person have been walking
(pass some argument to run() to handle long walks and not triggering so often)
*/
}
if (activityreminder.mustAlert(activityreminder_data, activityreminder_settings)) {
load('activityreminder.app.js');
}
}
}
function isNotWorn() {
return (Bangle.isCharging() || activityreminder_settings.tempThreshold >= E.getTemperature());
}
function isDuringAlertHours(h) {
if (activityreminder_settings.startHour < activityreminder_settings.endHour) { // not passing through midnight
return (h >= activityreminder_settings.startHour && h < activityreminder_settings.endHour);
} else { // passing through midnight
return (h >= activityreminder_settings.startHour || h < activityreminder_settings.endHour);
}
}
Bangle.on('midnight', function () {
/*
Usefull trick to have the app working smothly for people using it at night
*/
let now = new Date();
let h = now.getHours();
if (activityreminder_settings.enabled && isDuringAlertHours(h)) {
// updating only the steps and keeping the original stepsDate on purpose
activityreminder_data.stepsOnDate = 0;
activityreminder.saveData(activityreminder_data);
}
});
if (activityreminder_settings.enabled) {
setInterval(run, 60000);
/* todo in a futur release
increase setInterval time to something that is still sensible (5 mins ?)
when we added a settimer
Add settimer to trigger like 30 secs after going in this part cause the person have been walking
(pass some argument to run() to handle long walks and not triggering so often)
*/
}
if (mustAlert(now)) {
load('activityreminder.alert.js');
}
}
}
function isNotWorn() {
return (Bangle.isCharging() || activityreminder_settings.tempThreshold >= E.getTemperature());
}
function isDuringAlertHours(h) {
if (activityreminder_settings.startHour < activityreminder_settings.endHour) { // not passing through midnight
return (h >= activityreminder_settings.startHour && h < activityreminder_settings.endHour);
} else { // passing through midnight
return (h >= activityreminder_settings.startHour || h < activityreminder_settings.endHour);
}
}
function mustAlert(now) {
if ((now - activityreminder_data.stepsDate) / 60000 > activityreminder_settings.maxInnactivityMin) { // inactivity detected
if ((now - activityreminder_data.okDate) / 60000 > 3 && // last alert anwsered with ok was more than 3 min ago
(now - activityreminder_data.dismissDate) / 60000 > activityreminder_settings.dismissDelayMin && // last alert was more than dismissDelayMin ago
(now - activityreminder_data.pauseDate) / 60000 > activityreminder_settings.pauseDelayMin) { // last alert was more than pauseDelayMin ago
return true;
}
}
return false;
}
Bangle.on('midnight', function () {
/*
Usefull trick to have the app working smothly for people using it at night
*/
let now = new Date();
let h = now.getHours();
if (activityreminder_settings.enabled && isDuringAlertHours(h)) {
// updating only the steps and keeping the original stepsDate on purpose
activityreminder_data.stepsOnDate = 0;
activityreminder.saveData(activityreminder_data);
}
});
if (activityreminder_settings.enabled) {
setInterval(run, 60000);
/* todo in a futur release
increase setInterval time to something that is still sensible (5 mins ?)
when we added a settimer
*/
}
})();

View File

@ -1,56 +1,44 @@
exports.loadSettings = function () {
return Object.assign({
enabled: true,
startHour: 9,
endHour: 20,
maxInnactivityMin: 30,
dismissDelayMin: 15,
pauseDelayMin: 120,
minSteps: 50,
tempThreshold: 27
}, require("Storage").readJSON("activityreminder.s.json", true) || {});
return Object.assign({
enabled: true,
startHour: 9,
endHour: 20,
maxInnactivityMin: 30,
dismissDelayMin: 15,
pauseDelayMin: 120,
minSteps: 50,
tempThreshold: 27
}, require("Storage").readJSON("activityreminder.s.json", true) || {});
};
exports.writeSettings = function (settings) {
require("Storage").writeJSON("activityreminder.s.json", settings);
require("Storage").writeJSON("activityreminder.s.json", settings);
};
exports.saveData = function (data) {
require("Storage").writeJSON("activityreminder.data.json", data);
require("Storage").writeJSON("activityreminder.data.json", data);
};
exports.loadData = function () {
let health = Bangle.getHealthStatus("day");
let data = Object.assign({
firstLoad: true,
stepsDate: new Date(),
stepsOnDate: health.steps,
okDate: new Date(1970),
dismissDate: new Date(1970),
pauseDate: new Date(1970),
},
let health = Bangle.getHealthStatus("day");
let data = Object.assign({
firstLoad: true,
stepsDate: new Date(),
stepsOnDate: health.steps,
okDate: new Date(1970),
dismissDate: new Date(1970),
pauseDate: new Date(1970),
},
require("Storage").readJSON("activityreminder.data.json") || {});
if(typeof(data.stepsDate) == "string")
data.stepsDate = new Date(data.stepsDate);
if(typeof(data.okDate) == "string")
data.okDate = new Date(data.okDate);
if(typeof(data.dismissDate) == "string")
data.dismissDate = new Date(data.dismissDate);
if(typeof(data.pauseDate) == "string")
data.pauseDate = new Date(data.pauseDate);
if (typeof (data.stepsDate) == "string")
data.stepsDate = new Date(data.stepsDate);
if (typeof (data.okDate) == "string")
data.okDate = new Date(data.okDate);
if (typeof (data.dismissDate) == "string")
data.dismissDate = new Date(data.dismissDate);
if (typeof (data.pauseDate) == "string")
data.pauseDate = new Date(data.pauseDate);
return data;
return data;
};
exports.mustAlert = function(activityreminder_data, activityreminder_settings) {
let now = new Date();
if ((now - activityreminder_data.stepsDate) / 60000 > activityreminder_settings.maxInnactivityMin) { // inactivity detected
if ((now - activityreminder_data.okDate) / 60000 > 3 && // last alert anwsered with ok was more than 3 min ago
(now - activityreminder_data.dismissDate) / 60000 > activityreminder_settings.dismissDelayMin && // last alert was more than dismissDelayMin ago
(now - activityreminder_data.pauseDate) / 60000 > activityreminder_settings.pauseDelayMin) { // last alert was more than pauseDelayMin ago
return true;
}
}
return false;
}

View File

@ -3,7 +3,7 @@
"name": "Activity Reminder",
"shortName":"Activity Reminder",
"description": "A reminder to take short walks for the ones with a sedentary lifestyle",
"version":"0.08",
"version":"0.10",
"icon": "app.png",
"type": "app",
"tags": "tool,activity",
@ -13,11 +13,12 @@
{"name": "activityreminder.app.js", "url":"app.js"},
{"name": "activityreminder.boot.js", "url": "boot.js"},
{"name": "activityreminder.settings.js", "url": "settings.js"},
{"name": "activityreminder.alert.js", "url": "alert.js"},
{"name": "activityreminder", "url": "lib.js"},
{"name": "activityreminder.img", "url": "app-icon.js", "evaluate": true}
],
"data": [
{"name": "activityreminder.s.json"},
{"name": "activityreminder.data.json"}
{"name": "activityreminder.data.json", "storageFile": true}
]
}

View File

@ -1,80 +1,86 @@
(function (back) {
// Load settings
const activityreminder = require("activityreminder");
let settings = activityreminder.loadSettings();
// Load settings
const activityreminder = require("activityreminder");
let settings = activityreminder.loadSettings();
// Show the menu
E.showMenu({
"": { "title": "Activity Reminder" },
"< Back": () => back(),
'Enable': {
value: settings.enabled,
onchange: v => {
settings.enabled = v;
activityreminder.writeSettings(settings);
}
},
'Start hour': {
value: settings.startHour,
min: 0, max: 24,
onchange: v => {
settings.startHour = v;
activityreminder.writeSettings(settings);
}
},
'End hour': {
value: settings.endHour,
min: 0, max: 24,
onchange: v => {
settings.endHour = v;
activityreminder.writeSettings(settings);
}
},
'Max inactivity': {
value: settings.maxInnactivityMin,
min: 15, max: 120,
onchange: v => {
settings.maxInnactivityMin = v;
activityreminder.writeSettings(settings);
},
format: x => x + "m"
},
'Dismiss delay': {
value: settings.dismissDelayMin,
min: 5, max: 60,
onchange: v => {
settings.dismissDelayMin = v;
activityreminder.writeSettings(settings);
},
format: x => x + "m"
},
'Pause delay': {
value: settings.pauseDelayMin,
min: 30, max: 240, step: 5,
onchange: v => {
settings.pauseDelayMin = v;
activityreminder.writeSettings(settings);
},
format: x => {
return x + "m";
}
},
'Min steps': {
value: settings.minSteps,
min: 10, max: 500, step: 10,
onchange: v => {
settings.minSteps = v;
activityreminder.writeSettings(settings);
}
},
'Temp Threshold': {
value: settings.tempThreshold,
min: 20, max: 40, step: 0.5,
format: v => v + "°C",
onchange: v => {
settings.tempThreshold = v;
activityreminder.writeSettings(settings);
}
function getMainMenu(){
var mainMenu = {
"": { "title": "Activity Reminder" },
"< Back": () => back(),
'Enable': {
value: settings.enabled,
onchange: v => {
settings.enabled = v;
activityreminder.writeSettings(settings);
}
});
},
'Start hour': {
value: settings.startHour,
min: 0, max: 24,
onchange: v => {
settings.startHour = v;
activityreminder.writeSettings(settings);
}
},
'End hour': {
value: settings.endHour,
min: 0, max: 24,
onchange: v => {
settings.endHour = v;
activityreminder.writeSettings(settings);
}
},
'Max inactivity': {
value: settings.maxInnactivityMin,
min: 15, max: 120,
onchange: v => {
settings.maxInnactivityMin = v;
activityreminder.writeSettings(settings);
},
format: x => x + "m"
},
'Dismiss delay': {
value: settings.dismissDelayMin,
min: 5, max: 60,
onchange: v => {
settings.dismissDelayMin = v;
activityreminder.writeSettings(settings);
},
format: x => x + "m"
},
'Pause delay': {
value: settings.pauseDelayMin,
min: 30, max: 240, step: 5,
onchange: v => {
settings.pauseDelayMin = v;
activityreminder.writeSettings(settings);
},
format: x => {
return x + "m";
}
},
'Min steps': {
value: settings.minSteps,
min: 10, max: 500, step: 10,
onchange: v => {
settings.minSteps = v;
activityreminder.writeSettings(settings);
}
},
'Temp Threshold': {
value: settings.tempThreshold,
min: 20, max: 40, step: 0.5,
format: v => v + "°C",
onchange: v => {
settings.tempThreshold = v;
activityreminder.writeSettings(settings);
}
}
};
return mainMenu;
}
// Show the menu
E.showMenu(getMainMenu());
})

View File

@ -1 +1,4 @@
0.01: AdvCasio first version
0.02: Remove un-needed fonts to improve memory usage
0.03: Tell clock widgets to hide.
0.04: Swipe down to see widgets, step counter now just uses getHealthStatus

View File

@ -1,306 +1,160 @@
const storage = require('Storage');
require("Font6x12").add(Graphics);
require("Font6x8").add(Graphics);
require("Font8x12").add(Graphics);
require("Font7x11Numeric7Seg").add(Graphics);
function bigThenSmall(big, small, x, y) {
g.setFont("7x11Numeric7Seg", 2);
g.drawString(big, x, y);
x += g.stringWidth(big);
g.setFont("8x12");
g.drawString(small, x, y);
g.setFont("7x11Numeric7Seg", 2);
g.drawString(big, x, y);
x += g.stringWidth(big);
g.setFont("8x12");
g.drawString(small, x, y);
}
function getClockBg() {
return require("heatshrink").decompress(atob("icVgf/ABv8v4DBx4CB+PH8F+nAGB48fwEHBwXjxwqBuPH//+nAGBBwIjCAwI2D/wGBgIyDI4QGDwAGBHYX/4AGBn4UFEYQpCEYYpCAAMfMhP4FIgABwJ8OEBIA=="));
}
// sun, cloud, rain, thunder
var iconsWeather = [
require("heatshrink").decompress(atob("i8Ugf/ACcfA434BA/AAwsAv0/8F/BAcDwEHHIpECFI3wn4GC/gOC+PAGoXggEH/+ODQgXBGQv/wAbBBAnguEACIn4gfxI4JXFwJmG/kPBA3jSynw")), require("heatshrink").decompress(atob("i0Ugf/AEXggIGE/0A/kPBAmBCIN/A4Y8CgAICwEHBYoUE/ACCj4sDn4CBC4YyDwBrDCgYA3A")), require("heatshrink").decompress(atob("h8Rgf/AAuBAgf8h4FDCwM/AgPA/gFC/0HgEBBQPwnEfDoWAg4jC/gOCAoQmBAQXjFIV//8f//4IQP4j/+gAIB4EcHII4CAoI+DLQJXF/AA==")), require("heatshrink").decompress(atob("h0Pgf/AA8fAYX+g4EC8EBAgXADAeAgAECgAOC/wrCDQIOBBYfwgAaC/kAn4EB/EAv4aDHAeBIg38"))
];
function getBackgroundImage() {
return require("heatshrink").decompress(atob("2GwghC/AH4A/AH4AMl////wAwURiQECgUzmcxBQQCBiYUBBARW+LAcCAgcPBYgFBkAIFG7kQiAKIiIKBgISOAAJBD//zKQfxK4vyAoMQCgn/ERBhBBYR5BAwR1DB4Y2DgYPCGIQRCCQcP+EfGJI0FEgRSCGAQCCX4JXCkAhDn4lI+HyK4YWBFIPzJYJXHAIMSK4cwJ4I3CAYMzA4cfcRMBdwytBK4i6FK4IUCMgYAEGIITBK4cCaAPwgJXB+fzK4sAgYtCK5EfA4pXR+AmBaIZYCK6KcCAwSjDEYXx/8vK5QRCK4kPK6cDkJREBIMBfgIrDK5svUAIQBAwIaCK4w+DK4YGBK7IaBboIuCK4gFCJwYBBiBCCCgQhHHYgGDgArBK5IGDAYMgJ4Xwn53BGgLVDmBXKAAinDLpJXCAAYhHR4YODn/wJIPyTYZXDE4RXD+ECNILIDAIPwj4xIAAYNCR4fyVIYLFA4KEBBAglKAGUCmcykEAiMQBIURBYM/BgIUEgcz+bTKAH4A/AH4A/AHP/AGY1d+BWCh5X/LCpW1K74fgG/5X/AH5X/K9Bg/K63wK/5XWgBX/K6pWBK/5XU+BWBh5J/K6auCK/5XTVwRfFAH5XOKwRX/K6auDh5I/K6SuDWP5XSVwYADWX6vXK/5XQWQpW/K6auDJP5XWV35XT+Cu/K7Ku/K65H/K6hW/K7EPI35XWIv5XWAH5X/K/4A/K/5X/K/4A/K9cAAH4A/AFzz/AHRX/K/5X/AH5X/K/5X/AH5X/K/4A/K/5X/K/4A/K/5X/K/4A/K/5X/AH5X/K/5X/AH5X/K/5X/AH5X/K/4A/K/5X/K/4A/K/5X/K/4A/K/5X/AH5X/K/5X/AH5X/K/5X/AH5X/K/4A/K/5X/K/4A/K/5X/K/4A/K/5X/AH5X/K/5X/AH5X/K/5X/AH5X/K/4A/K/5X/K/4A/K/5X/K/4A/K/5X/AH5X/K/5X/AH5X/K/40VAH4A/AFzLb+EPDm4AdK/5X/K+PwgEAHy5X9HgMAK/5XXH6xX/H65X/K/5X/K98AK7sAgBX3DjBWFO644DSTHwGzJXED4RXaDoLqcK7weWDIQcXK8I6YK77KXK4o8DPbY6ZK7qvDDy6vdR7JXDh60EDyw5BAIRXYSwjMbAgIhUDwJZCHwJX0GwjRWNwIAEHSwBCDSpXFH4pXzDS5XIEARXVSYbQEDaYzCK+6vcKaxXNDypX9HwQkbHS40COSpXKK2A6CHgRXcPIhX0SwpXYVuQ6EgBX/K644YODBXkSDJX/K/5X/DtRX6gA3YOkRWbLDZX4KwYA/AG8F5vdABncKH4AGhpRJAYXNAgPAKP4AF5vMJwoDBAQIKE6BR/AAvc5vO9wAB7oCB9veAoPcAoPcK+kwh8AgcA98An//gH/+sD//wCISgBJ4IABAYpaC9vdK4UP/9AAQNQr/zgHwEYNQFYQAh+EP+FegH+A4QBCMQIKBAAPNK4yxBA4RXCV4YZBE4IjChwCDmApCK8VdmHggHgFYf0SQJXE5nMK4anCAoYHC5pXCaQJXBop+BqAGEK7f/AAQeEKwQrBqCtDAILjBCQfNK4JTCAYZXF7qvD//gV4S2DgEFFIYAECgIACMC8PKoIBB8n1K4ivF5vc5xOCWYZbBAYavHU4RXCr4pEAEMDfoNQGoMEgEwYQPwAoIBBAAPM5ipC7oDCVIIAE7hXCD4SdBiEP+gGBgihCFYIAz5pXBAAnN7oIB7nc5gOBK4QA/K4pNCWgSpCBInNK/4AGhncKIStC7gCBA4QAC4BR/AAysCABZW/AHwA="));
return require("heatshrink").decompress(atob("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"));
}
function getRocketSequences() {
return {
1: require("heatshrink").decompress(atob("qFGwkCkQAiiEBEkUgKQhPhE8ogCE8YhCiQoEE7pKEPIgncTQ4neEwpQCPoh1eJYYwCJ7QmHKAh1hZIpOjPAUBJ0ZQCTzEhExZ1lPAZ1kKDQmOJ65O2E65OPOy5O2E64mPOyxO/J2wnPJyx2QJ35O/J2khE0p2POq52PEy4nOiQnlOrEhiSfMJrEggQnLJzB1CPBQmZkInMEzBQDPBImbPBR1ZEoRMCZYImhgQgEE0BzFKAgmaDwLDFKAbqdYQwHBOrcgDgLBFJrsiiRNGYbpLBY4Ymhd4omkkUhE0pQEEwUBJjrHBd4QmCdzoiBDwYrCPLyZHF4QnagQeCE8UgJwYniJwgnIOzwfFO0wJCJzMQE4gyFEzR2FBQombkInDQI4AakAnBTYS+ZE5BMDE0LEES7YnLE0R3FAEQA=")),
2: require("heatshrink").decompress(atob("qFGwkCkQAikMAgIliKYon/AA0gEAQniEwIhCAgYndEIjqBE8CaGKogmgKAp1fKAgncExBQBBQR1gKAp7BJ0IndExR4CE0idaOpYnbExqeYJxxPYEx0BJ0x2XExx2XJ20QE6xONJi5OPGwJOlBwLFkLoLFlBwJOkOwJOlE4JOkTjBOOE/52Pdi5OPEy7FnE5wmXE5xOZT5gmYEoMiiB1lgR4KTLAkDPBJ1WIAYDDKA4mWJwchDwYEDTjQiDJQh4GYLAhHFosSJy6OCTIxaEEywbBKYwjEEzMgUQxQFBogAURwZOGOjTKJdTYnOEryfHE0JQEfIpQgYQMAgJLeAgrtfTI4ndgSaFE4h0bdQkSZQpOfEAgIBO0AnEdrh2FJAb1EdbInEBIpObOwhOEEzYnFXzZ2HE4QlhE4QlDFMKcDYooniO0QnDT0YnCE0ciA")),
3: require("heatshrink").decompress(atob("qFGwkEogAjiMUEkVAKYgnhPYolgOQIniOYZ4FOcLqBE8CaGKojpgKAomhEYUQE7gmHKAIxCE0QkCPYR1gZIgnZExR4CJ0idmE7ZONYzImNgEUJ0p3YJRh2ZJJwnXOpQhBdkpaETsMEGQhOhE7jFLUYpOfTzgmKE4hOiE4hOigEUJ0rvCEywnPEqx2OTjBOOE7ImOTsqeZE5zFYoJOmT5kBJzEAih4LdK5mBAQInKOqoYDEgR4JEypHDEYbxJOq5ABdgZ7CEzZOEJQgnGihOYEIzJFTionCKYxWGEy9ADAYnGUIYmWog/EdBFAEy7KIKAwnjKwLqWE5pMeT48CVQpQfgMjKEtEiAnfEQJQCgJSCTcB6FJzkEdYcUE8FAdQghDOzonKTjh2EZAidcDoInHJzodBOwx/BE8JxcOwsAOwQmhJgSXDObwnFEwUUO0LFGE8aeiE4YmiokQE0tE")),
4: require("heatshrink").decompress(atob("qFGwkCkQAjiMSEkRTFE/4AGkMAgQCBE8MgEIYEDE7whDdQIngTQxVEE0ChFTjxQFE7jnFKAgxCOsBQFZgJ1gE7wmKPAROkTrTEHGAwnYiBHJFAaeXOoyXBEQZPac5AsFgJOhAoh2XJwwnFKoROdE4J9GJzwnIiQmVkInPAC0QE5AJFE64mHY5DFdE4SBEYr5JDJ0hKDJ0jCZJxoACgInmKLAmOTq5OOEy5OPTsxOYE5wmXO5wlYkAnMOqshiRNCgR4LOC8CkJCCEzxHDAgYnJOqpAEDoZ4HEyodDEQpQHdCsQOwwFHEyzoCPYzJGEy0gEwaZGA4acVEQSjHKAomXkQYEYAwlZeRKYDE8gjCYa7zJEwcCkImfKAb4FAD0hdTh4LgRSBOcR0CJz0gYYrrgN4QnEYrxOEE4bEeiAnGF4J2idL6VDE8ohBE0gnFE0J0BE4QGBiROgdIQABgJ2hJoTtjYgZSEE8ScgE4omikUQTcQADA=")),
5: require("heatshrink").decompress(atob("qFGwkCkQAikMAgIliKYonhiAnjkEATIIniEwIhCAgYndEIhQFYUZVEE0BQFOr5QEeQQmiKAL1DOr5QEE7ROCDgZVEAoInZDwchFQQoDPAJOdEQYrBdrZFDOYwncEJDsDVIpOXgJxEE4pObEAgGFgJOaE48BaIhOZJ5ZObY5ROcE441CE6xOGPAwtCJzpGCJ0hHDkI1DJzwoEJzInLFg52dUo5O/J35OzE54mWOx4mXJxx1XE54mXkUhExkSJzCfMOrAlBPBiZXgQDBAQQmgJgh4JOqoYEFYwmaDoZzEFgh1YDgkiiAFEKAroXJJAGFiQmVkCNDTIz5EJy57HKAomXkQYEJoqaYeRadEJrAnJEQUAgJPiAoYmeT4cCkAnBE0BKCJkT1EkDCeJYYiDOkLDFFL5wBE4guCPDhEBEwQiDY70CkInDiQnCJzkhOwhKDdzp2Idb4nEE0B0Bdo4niE0J0CeYhOhgESUYYnidsgnEE0KeCE0gnDE0ciA")),
6: require("heatshrink").decompress(atob("qFGwkCkQA/ABEgKQZPhEwgABEsAoGJkBxBE8JKEAowAbJIhQEgLDiPooAdKA4ncTZAndSwhQEFoInaJQkSKAwlZdgwnfSgYADE4h1ZDwInlcggnIOzAdCE8i7EY5J3XDgYhGd4pOZEI52bSYwGCOAJ2bYIodEOzZOFFAjFcEwwAIE6xOHABBO/J34ndEyx2PJ00BJ00SJ0p1XE54mXOxxO/J5wmYgQnMOrB2BPBgkWiJ1CPBbBYAYR4KiTAXRwIrFTjgZDJYZ4IEyoiEIwrDcEJJQFOqwiBDARxFFwgmXkAYDEogsBF4QmXEQJ7GUYYkBEzDKJAgYmdEQbKFEzonEKYgngJwgmfZggmjKQghgiBRGkBzeTgUikJRgc47LDErTnDEAkQJzkCJwYnEJzonEJIaddOwhJEJzgdBE4hYEJzieJADgnEE0KUCXzoAGkJLEiB2hOgQDBT0TsDT0YmlE4YmjkQ=")),
7: require("heatshrink").decompress(atob("qFGwkCkQAhkIpBiQlhkBSEJ8InlEIIoFE7whEE8pQFE7giBJQoneI4MCTYhQDE7YdCYYondEQYnEPwZ1bE5BQCJzonHkR2ZEAkBE4pNBE7zHFYrYhFUgonaXAQeEEwruZEYcgiROHJ7AfDAwxOeAAURiAmHE65HIOzwmOJ35OPE6xOPO35O/J35O/J1gnPEyx2PEy5OOOq5OnE5xOYO5omZgJQMJrQnLiQnagR4JOq5nCDgZ1fEYRLDE5DoZkUQNoZ4GOrJKGAoomXOw7lCAwYmYDgJSEAAUBA4QDBJzB6FOQrDXJwTJFdLjJKE9jDYZRAmkKAwmhKAgmiKAYmBkApdJIgjCKYIncOQYvJYTovGE84lagR2DE4xOakBOEgJXFOjYnEJAbtdOwggEkAmbDgInDE0B0BE4QgcE5AkiXYbpCOLonGYo4nhPMYnCUEgnBY0kiA==")),
8: require("heatshrink").decompress(atob("qFGwkCkQA/ABBSEJ8MgE4kBEsBPFE7xMCOIJ3hOYgFEE7rCGE70gE4pQBiAndYQwjBUohOZD4ZQFE7YkBE5AICYbZ2GE7sggJRCAA8iYzZOITroALE7EhExh4CAC0QExpPXOponZExx2XJ24nWdh52XdhzF/Yu5O/J35O0E55OXOx5O/J2omXE5x1XO54mYgQnMJrR4LOrciiAmiJgR4KEzIjDPBAlYiAiEeI51YkEBE4J5CD4KceTQQcBJgRQFdTZDCJIjDcNIqhGdTQmCkByFTTInDKgoAEE7ZEEJwhPdE1R1FE0InEE0R3DEwTGcDwomEE7hKFPYqafE8ROCE5DJbE5B/IEqh2ED4gnCJrMCJwgnEiB2bE4qeFEzUggQmIBQLEaEQImHLIImaE4YfcOw4lEFMLECS7onJO8wmkE4QljAAIA==")),
};
}
let rocketSequence = 1;
let settings = storage.readJSON("cassioWatch.settings.json", true) || {};
let rocketSpeed = settings.rocketSpeed || 700;
delete settings;
// schedule a draw for the next minute
let rocketInterval;
var drawTimeout;
function queueDraw() {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = setTimeout(function() {
drawTimeout = undefined;
draw();
}, 60000 - (Date.now() % 60000));
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = setTimeout(function() {
drawTimeout = undefined;
draw();
}, 60000 - (Date.now() % 60000));
}
function clearIntervals() {
if (rocketInterval) clearInterval(rocketInterval);
rocketInterval = undefined;
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
if (rocketInterval) clearInterval(rocketInterval);
rocketInterval = undefined;
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
}
////////////////////////////////////////////
// TIMER FUNC
//
var timer_time = 0;
var alreadyListenTouch = false;
function initTouchTimer () {
if (alreadyListenTouch) return;
alreadyListenTouch = true;
Bangle.on('swipe', function(dirX,dirY) {
if (canTouch === false) return;
var njson = getDataJson();
if (!njson) return;
if (dirX === -1) {
timer_time = 0;
delete njson.timer;
setDataJson(njson);
}
else if (dirX === 1) {
var now = new Date().getTime();
njson.timer = now + (timer_time * 1000 * 60);
Bangle.setLocked(true);
setDataJson(njson);
Bangle.buzz(200, 0);
timer_time = 0;
}
else if (dirY === -1) {
if (canTouch === false || njson.timer) return;
timer_time = timer_time + 5;
}
else if (dirY === 1) {
if (canTouch === false || njson.timer) return;
timer_time = timer_time - 5;
}
draw();
});
}
setTimeout(() => {
initTouchTimer ();
});
function getTimerTime() {
// if timer_time !== -1, take it
if (timer_time !== 0) {
return timer_time + "m";
} else {
// else, show diff between njsontime and now
var njson = getDataJson();
if (!njson) return false;
var now = new Date().getTime();
var diff = Math.round((njson.timer - now) / (1000 * 60));
//console.log(123, njson, diff, now, njson.timer - now);
if (diff > 0) return diff + "m";
else if (njson.timer) {
Bangle.buzz(1000, 1);
console.log("END OF TIMER");
delete njson.timer;
setDataJson(njson);
return false;
} else {
return false;
}
// if diff is <0, delete timer from json
}
}
function drawTimer() {
//g.drawString(getTimerTime(), 100, 100);
g.setFont("8x12", 2);
var t = 97;
var l = 105;
var time = getTimerTime();
if (time || timer_time !== 0) g.drawString(time, l+5, t+0);
if (time && timer_time === 0) g.drawImage(getClockBg(), l-20, t+2, { scale: 1 });
}
////////////////////////////////////////////
// DATA READING
//
function getDataJson(){
var res = {"tasks":"", "weather":[]};
try {
res = storage.readJSON('advcasio.data.json');
} catch(ex) {
return res;
}
return res;
}
function setDataJson(resJson){
try {
res = storage.writeJSON('advcasio.data.json', resJson);
} catch(ex) {
return res;
}
return res;
}
var dataJson = getDataJson();
////////////////////////////////////////////
// WEATHER!
//
function drawWeather(arr) {
g.setFont("6x8", 1);
var p = {l: 8, tText: 40, tIcon:20, decal:25};
var today = new Date().getTime();
var yesterday = today - (1000 * 60 * 60 * 24);
var testday = today + (1000 * 60 * 60 * 24 * 2);
//12h auj > 12h hier qui est sup a 0h auj
//23h59 hier est sup a 0h auj
var j = 0;
for(var i = 0; i<arr.length;i++) {
if (arr[i][2] > yesterday && j < 4) {
g.drawString(arr[i][0], p.l + p.decal*j + 4, p.tText);
g.drawImage(iconsWeather[arr[i][1]], p.l + p.decal*j, p.tIcon, { scale: 1 });
j++
}
}
}
////////////////////////////////////////////
// DRAWING FUNCS
//
function drawTasks(str) {
g.setFont("6x8", 1);
var t = 57;
var l = 0;
g.drawString(str, l+5, t+0);
}
function drawSteps() {
g.setFont("8x12", 2);
var t = 132;
var l = 150;
g.drawString(getSteps(), l+5, t+0);
}
function drawClock() {
g.setFont("7x11Numeric7Seg", 3);
g.clearRect(80, 57, 170, 96);
g.setColor(255, 255, 255);
var l = 77;
var t = 57;
var w = 170;
var h = 116;
g.drawRect(l, t, w, h);
g.fillRect(l, t, w, h);
g.setColor(0, 0, 0);
g.drawString(require("locale").time(new Date(), 1), 76, 60);
// day
//g.setFont("8x12", 1);
//g.setFont("9x18", 1);
//g.drawString(require("locale").dow(new Date(), 2).toUpperCase(), 25, 136);
g.setFont("8x12", 2);
g.drawString(require("locale").dow(new Date(), 2), 18, 130);
// month
g.setFont("8x12");
g.drawString(require("locale").month(new Date(), 2).toUpperCase(), 80, 127);
// day nb
g.setFont("8x12", 2);
const time = new Date().getDate();
g.drawString(time < 10 ? "0" + time : time, 78, 137);
g.setFont("7x11Numeric7Seg", 3);
g.clearRect(80, 57, 170, 96);
g.setColor(0, 255, 255);
g.drawRect(80, 57, 170, 96);
g.fillRect(80, 57, 170, 96);
g.setColor(0, 0, 0);
g.drawString(require("locale").time(new Date(), 1), 70, 60);
g.setFont("8x12", 2);
g.drawString(require("locale").dow(new Date(), 2).toUpperCase(), 18, 130);
g.setFont("8x12");
g.drawString(require("locale").month(new Date(), 2).toUpperCase(), 80, 126);
g.setFont("8x12", 2);
const time = new Date().getDate();
g.drawString(time < 10 ? "0" + time : time, 78, 137);
}
function drawBattery() {
bigThenSmall(E.getBattery(), "%", 140, 23);
bigThenSmall(E.getBattery(), "%", 135, 21);
}
function drawRocket() {
let Rocket = getRocketSequences();
g.clearRect(5, 62, 63, 115);
g.setColor(0, 255, 255);
g.drawRect(5, 62, 63, 115);
g.fillRect(5, 62, 63, 115);
g.drawImage(Rocket[rocketSequence], 5, 65, { scale: 0.7 });
g.setColor(0, 0, 0);
rocketSequence = rocketSequence + 1;
if(rocketSequence > 8) rocketSequence = 1;
}
function getTemperature(){
try {
var weatherJson = storage.readJSON('weather.json');
var weather = weatherJson.weather;
return Math.round(weather.temp-273.15);
} catch(ex) {
print(ex)
return "?"
}
}
function getSteps() {
var steps = 0;
try{
if (WIDGETS.wpedom !== undefined) {
steps = WIDGETS.wpedom.getSteps();
} else if (WIDGETS.activepedom !== undefined) {
steps = WIDGETS.activepedom.getSteps();
} else {
steps = Bangle.getHealthStatus("day").steps;
}
} catch(ex) {
// In case we failed, we can only show 0 steps.
return "? k";
}
steps = Math.round(steps/1000);
return steps + "k";
var steps = Bangle.getHealthStatus("day").steps;
steps = Math.round(steps/1000);
return steps + "k";
}
function draw() {
queueDraw();
queueDraw();
g.reset();
g.clear();
g.setColor(255, 255, 255);
g.fillRect(0, 0, g.getWidth(), g.getHeight());
let background = getBackgroundImage();
g.drawImage(background, 0, 0, { scale: 1 });
g.setColor(0, 0, 0);
g.setFont("6x12");
if(dataJson && dataJson.weather) drawWeather(dataJson.weather);
if(dataJson && dataJson.tasks) drawTasks(dataJson.tasks);
g.clear(1);
g.setColor(0, 255, 255);
g.fillRect(0, 0, g.getWidth(), g.getHeight());
let background = getBackgroundImage();
g.drawImage(background, 0, 0, { scale: 1 });
g.setColor(0, 0, 0);
g.setFont("6x12");
g.drawString("Launching Process", 30, 20);
g.setFont("8x12");
g.drawString("ACTIVATE", 40, 35);
g.setFontAlign(0,-1);
g.setFont("8x12", 2);
g.setFontAlign(0,-1);
g.setFont("8x12", 2);
g.drawString(getTemperature(), 155, 132);
g.drawString(Math.round(Bangle.getHealthStatus("last").bpm), 109, 98);
g.drawString(getSteps(), 158, 98);
drawSteps();
g.setFontAlign(-1,-1);
drawClock();
drawBattery();
drawTimer();
// Hide widgets
for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";}
g.setFontAlign(-1,-1);
drawClock();
drawRocket();
drawBattery();
// Hide widgets
for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";}
}
// save batt power, does not seem to work although...
var canTouch = true;
Bangle.on("lcdPower", (on) => {
if (on) {
draw();
} else {
canTouch = false;
clearIntervals();
}
if (on) {
draw();
} else {
clearIntervals();
}
});
Bangle.on("lock", (locked) => {
clearIntervals();
draw();
if (!locked) {
canTouch = true;
} else {
canTouch = false;
}
clearIntervals();
draw();
if (!locked) {
rocketInterval = setInterval(drawRocket, rocketSpeed);
}
});
Bangle.setUI("clock");
// Load widgets, but don't show them
Bangle.loadWidgets();
Bangle.setUI("clock");
g.reset();
g.clear();
require("widget_utils").swipeOn(); // hide widgets, make them visible with a swipe
g.clear(1);
draw();

View File

@ -1,7 +1,7 @@
{ "id": "advcasio",
"name": "Advanced Casio Clock",
"shortName":"advcasio",
"version":"0.01",
"version":"0.04",
"description": "An over-engineered clock inspired by Casio watches. It has a 4 days weather, a timer using swipe and a scratchpad. Can be updated using a dedicated webapp.",
"icon": "app.png",
"tags": "clock",
@ -12,7 +12,7 @@
{ "url": "screenshot-clock-3.jpg" },
{ "url": "screenshot-webapp.jpg" }
],
"supports" : ["BANGLEJS", "BANGLEJS2"],
"supports" : ["BANGLEJS", "BANGLEJS2"],
"readme": "README.md",
"allow_emulator":true,
"storage": [

View File

@ -1 +1,11 @@
0.01: Basic agenda with events from GB
0.02: Added settings page to force calendar sync
0.03: Disable past events display from settings
0.04: Added awareness of allDay field
0.05: Displaying calendar colour and name
0.06: Added clkinfo for clocks.
0.07: Clkinfo improvements.
0.08: Fix error in clkinfo (didn't require Storage & locale)
Fix clkinfo icon
0.09: Ensure Agenda supplies an image for clkinfo items
0.10: Update clock_info to avoid a redraw

View File

@ -1,3 +1,30 @@
# Agenda
Basic agenda reading the events synchronised from GadgetBridge
Basic agenda reading the events synchronised from GadgetBridge.
### Functionalities
* List all events in the next week (or whatever is synchronized)
* Optionally view past events (until GB removes them)
* Show start time and location of the events in the list
* Show the colour of the calendar in the list
* Display description, location and calendar name after tapping on events
### Troubleshooting
For the events sync to work, GadgetBridge needs to have the calendar permission and calendar sync should be enabled in the devices settings (gear sign in GB, also check the blacklisted calendars there, if events are missing).
Keep in mind that GadgetBridge won't synchronize all events on your calendar, just the ones in a time window of 7 days (you don't want your watch to explode), ideally every day old events get deleted since they appear out of such window.
#### Force Sync
If for any reason events still cannot sync or some are missing, you can try any of the following (just one, you normally don't need to do this):
1. from GB open the burger menu (side), tap debug and set time.
2. from the bangle, open settings > apps > agenda > Force calendar sync, then select not to delete the local events (this is equivalent to option 1).
3. do like option 2 but delete events, GB will synchronize a fresh database instead of patching the old one (good in case you somehow cannot get rid of older events)
After any of the options, you may need to disconnect/force close Gadgetbridge before reconnecting and let it sync (give it some time for that too), restart the agenda app on the bangle after a while to see the changes.
### Report a bug
You can easily open an issue in the espruino repo, but I won't be notified and it might take time.
If you want a (hopefully) quicker response, just report [on my fork](https://github.com/glemco/BangleApps).

View File

@ -0,0 +1,29 @@
(function() {
var agendaItems = {
name: "Agenda",
img: atob("GBiBAAAAAAAAAADGMA///w///wf//wAAAA///w///w///w///x///h///h///j///D///X//+f//8wAABwAADw///w///wf//gAAAA=="),
items: []
};
var locale = require("locale");
var now = new Date();
var agenda = require("Storage").readJSON("android.calendar.json")
.filter(ev=>ev.timestamp + ev.durationInSeconds > now/1000)
.sort((a,b)=>a.timestamp - b.timestamp);
agenda.forEach((entry, i) => {
var title = entry.title.slice(0,12);
var date = new Date(entry.timestamp*1000);
var dateStr = locale.date(date).replace(/\d\d\d\d/,"");
dateStr += entry.durationInSeconds < 86400 ? "/ " + locale.time(date,1) : "";
agendaItems.items.push({
name: "Agenda "+i,
get: () => ({ text: title + "\n" + dateStr, img: agendaItems.img }),
show: function() {},
hide: function () {}
});
});
return agendaItems;
})

View File

@ -6,6 +6,8 @@
title,
description,
location,
color:int,
calName,
allDay: bool,
}
*/
@ -24,19 +26,23 @@ var fontLarge = g.getFonts().includes("6x15")?"6x15:2":"6x8:4";
//FIXME maybe write the end from GB already? Not durationInSeconds here (or do while receiving?)
var CALENDAR = require("Storage").readJSON("android.calendar.json",true)||[];
var settings = require("Storage").readJSON("agenda.settings.json",true)||{};
CALENDAR=CALENDAR.sort((a,b)=>a.timestamp - b.timestamp)
CALENDAR=CALENDAR.sort((a,b)=>a.timestamp - b.timestamp);
function getDate(timestamp) {
return new Date(timestamp*1000);
}
function formatDateLong(date, includeDay) {
if(includeDay)
return Locale.date(date)+" "+Locale.time(date,1);
return Locale.time(date,1);
function formatDateLong(date, includeDay, allDay) {
let shortTime = Locale.time(date,1)+Locale.meridian(date);
if(allDay) shortTime = "";
if(includeDay || allDay)
return Locale.date(date)+" "+shortTime;
return shortTime;
}
function formatDateShort(date) {
return Locale.date(date).replace(/\d\d\d\d/,"")+Locale.time(date,1);
function formatDateShort(date, allDay) {
return Locale.date(date).replace(/\d\d\d\d/,"")+(allDay?
"" : Locale.time(date,1)+Locale.meridian(date));
}
var lines = [];
@ -45,7 +51,7 @@ function showEvent(ev) {
if(!ev) return;
g.setFont(bodyFont);
//var lines = [];
if (ev.title) lines = g.wrapString(ev.title, g.getWidth()-10)
if (ev.title) lines = g.wrapString(ev.title, g.getWidth()-10);
var titleCnt = lines.length;
var start = getDate(ev.timestamp);
var end = getDate((+ev.timestamp) + (+ev.durationInSeconds));
@ -53,22 +59,24 @@ function showEvent(ev) {
if (titleCnt) lines.push(""); // add blank line after title
if(start.getDay() == end.getDay() && start.getMonth() == end.getMonth())
includeDay = false;
if(includeDay) {
if(includeDay || ev.allDay) {
lines = lines.concat(
/*LANG*/"Start:",
g.wrapString(formatDateLong(start, includeDay), g.getWidth()-10),
g.wrapString(formatDateLong(start, includeDay, ev.allDay), g.getWidth()-10),
/*LANG*/"End:",
g.wrapString(formatDateLong(end, includeDay), g.getWidth()-10));
g.wrapString(formatDateLong(end, includeDay, ev.allDay), g.getWidth()-10));
} else {
lines = lines.concat(
g.wrapString(Locale.date(start), g.getWidth()-10),
g.wrapString(/*LANG*/"Start"+": "+formatDateLong(start, includeDay), g.getWidth()-10),
g.wrapString(/*LANG*/"End"+": "+formatDateLong(end, includeDay), g.getWidth()-10));
g.wrapString(/*LANG*/"Start"+": "+formatDateLong(start, includeDay, ev.allDay), g.getWidth()-10),
g.wrapString(/*LANG*/"End"+": "+formatDateLong(end, includeDay, ev.allDay), g.getWidth()-10));
}
if(ev.location)
lines = lines.concat(/*LANG*/"Location"+": ", g.wrapString(ev.location, g.getWidth()-10));
if(ev.description)
lines = lines.concat("",g.wrapString(ev.description, g.getWidth()-10));
if(ev.calName)
lines = lines.concat(/*LANG*/"Calendar"+": ", g.wrapString(ev.calName, g.getWidth()-10));
lines = lines.concat(["",/*LANG*/"< Back"]);
E.showScroller({
h : g.getFontHeight(), // height of each menu item in pixels
@ -89,6 +97,12 @@ function showEvent(ev) {
}
function showList() {
//it might take time for GB to delete old events, decide whether to show them grayed out or hide entirely
if(!settings.pastEvents) {
let now = new Date();
//TODO add threshold here?
CALENDAR = CALENDAR.filter(ev=>ev.timestamp + ev.durationInSeconds > now/1000);
}
if(CALENDAR.length == 0) {
E.showMessage("No events");
return;
@ -101,24 +115,21 @@ function showList() {
g.setColor(g.theme.fg);
g.clearRect(r.x,r.y,r.x+r.w, r.y+r.h);
if (!ev) return;
var isPast = ev.timestamp + ev.durationInSeconds < (new Date())/1000;
var isPast = false;
var x = r.x+2, title = ev.title;
var body = formatDateShort(getDate(ev.timestamp))+"\n"+ev.location;
var m = ev.title+"\n"+ev.location, longBody=false;
var body = formatDateShort(getDate(ev.timestamp),ev.allDay)+"\n"+(ev.location?ev.location:/*LANG*/"No location");
if(settings.pastEvents) isPast = ev.timestamp + ev.durationInSeconds < (new Date())/1000;
if (title) g.setFontAlign(-1,-1).setFont(fontBig)
.setColor(isPast ? "#888" : g.theme.fg).drawString(title, x,r.y+2);
.setColor(isPast ? "#888" : g.theme.fg).drawString(title, x+4,r.y+2);
if (body) {
g.setFontAlign(-1,-1).setFont(fontMedium).setColor(isPast ? "#888" : g.theme.fg);
var l = g.wrapString(body, r.w-(x+14));
if (l.length>3) {
l = l.slice(0,3);
l[l.length-1]+="...";
}
longBody = l.length>2;
g.drawString(l.join("\n"), x+10,r.y+20);
g.drawString(body, x+10,r.y+20);
}
//if (!longBody && msg.src) g.setFontAlign(1,1).setFont("6x8").drawString(msg.src, r.x+r.w-2, r.y+r.h-2);
g.setColor("#888").fillRect(r.x,r.y+r.h-1,r.x+r.w-1,r.y+r.h-1); // dividing line between items
if(ev.color) {
g.setColor("#"+(0x1000000+Number(ev.color)).toString(16).padStart(6,"0"));
g.fillRect(r.x,r.y+4,r.x+3, r.y+r.h-4);
}
},
select : idx => showEvent(CALENDAR[idx]),
back : () => load()

View File

@ -1,17 +1,19 @@
{
"id": "agenda",
"name": "Agenda",
"version": "0.02",
"version": "0.10",
"description": "Simple agenda",
"icon": "agenda.png",
"screenshots": [{"url":"screenshot_agenda_overview.png"}, {"url":"screenshot_agenda_event1.png"}, {"url":"screenshot_agenda_event2.png"}],
"tags": "agenda",
"tags": "agenda,clkinfo",
"supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
"allow_emulator": true,
"storage": [
{"name":"agenda.app.js","url":"agenda.js"},
{"name":"agenda.settings.js","url":"settings.js"},
{"name":"agenda.clkinfo.js","url":"agenda.clkinfo.js"},
{"name":"agenda.img","url":"agenda-icon.js","evaluate":true}
]
],
"data": [{"name":"agenda.settings.json"}]
}

View File

@ -3,6 +3,10 @@
Bluetooth.println("");
Bluetooth.println(JSON.stringify(message));
}
var settings = require("Storage").readJSON("agenda.settings.json",1)||{};
function updateSettings() {
require("Storage").writeJSON("agenda.settings.json", settings);
}
var CALENDAR = require("Storage").readJSON("android.calendar.json",true)||[];
var mainmenu = {
"" : { "title" : "Agenda" },
@ -32,6 +36,13 @@
E.showAlert(/*LANG*/"You are not connected").then(()=>E.showMenu(mainmenu));
}
},
/*LANG*/"Show past events" : {
value : !!settings.pastEvents,
onchange: v => {
settings.pastEvents = v;
updateSettings();
}
},
};
E.showMenu(mainmenu);
})

View File

@ -1 +1,5 @@
0.01: First, proof of concept
0.02: Load AGPS data on app start and automatically in background
0.03: Do not load AGPS data on boot
Increase minimum interval to 6 hours
0.04: Write AGPS data chunks with delay to improve reliability

View File

@ -1,18 +1,19 @@
# A-GPS Data
Load assisted GPS data directly to the watch using the new http requests on Android GadgetBridge.
Load assisted GPS (A-GPS) data directly to your Bangle.js using the new http requests on Android GadgetBridge.
Will download A-GPS data in background (if enabled in settings).
The GNSS type can be configured in the settings.
Make sure:
* your GadgetBridge version supports http requests
* turn on internet access in GadgetBridge settings
Currently proof of concept on Bangle2 only. Will eventually add a widget for automatic download.
![](screenshot.png)
![](screenshot2.png)
![](screenshot3.png)
![](screenshot4.png)
![](screenshot5.png)
Currently proof of concept on Bangle.js 2 only.
## Creator
[@pidajo](https://github.com/pidajo)
## Contributor
[@myxor](https://github.com/myxor)

View File

@ -1,125 +1,54 @@
var _GB = global.GB;
var counter = 0;
function GB(msg) {
console.log(msg);
if (msg.t == "http") {
display("Received", "(" + msg.resp.length + ") Touch to apply", () => {
display("Apply data..", "");
setTimeout(() => {
if (setAGPS(msg.resp)) {
display("Success", "Touch for restart", httpTest);
}
else {
display("Error", "Touch for restart", httpTest);
}
}, 1);
});
}
if (_GB) {
_GB(msg);
}
}
function setAGPS(data) {
var js = jsFromBase64(data);
console.log(js);
try {
eval(js);
return true;
}
catch(e) {
console.log("Error:", e);
}
return false;
}
function jsFromBase64(b64) {
var bin = atob(b64);
var chunkSize = 128;
var js = "Bangle.setGPSPower(1);\n"; // turn GPS on
var gnss_select="1";
js += `Serial1.println("${CASIC_CHECKSUM("$PCAS04,"+gnss_select)}")\n`; // set GNSS mode
// 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) {
var chunk = bin.substr(i,chunkSize);
js += `Serial1.write(atob("${btoa(chunk)}"))\n`;
}
return js;
}
function CASIC_CHECKSUM(cmd) {
var cs = 0;
for (var i=1;i<cmd.length;i++)
cs = cs ^ cmd.charCodeAt(i);
return cmd+"*"+cs.toString(16).toUpperCase().padStart(2, '0');
}
function display(text1, text2, next) {
function display(text1, text2) {
g.reset();
g.clear();
var img = require("Storage").read("agpsdata.img");
if (img) {
g.drawImage(img, g.getWidth() - 48, g.getHeight()-48-24);
g.drawImage(img, g.getWidth() - 48, g.getHeight() - 48 - 24);
}
g.setFont("Vector", 30);
g.setFont("Vector", 18);
g.setFontAlign(0, 1);
g.drawString(text1, g.getWidth() / 2, g.getHeight() / 3 + 24);
g.setFont("Vector", 15);
g.setFontAlign(-1, -1);
g.drawString(text2, 0, g.getHeight() / 3 + 29);
if (text2 != undefined) {
g.setFont("Vector", 12);
g.setFontAlign(-1, -1);
g.drawString(text2, 5, g.getHeight() / 3 + 29);
}
Bangle.drawWidgets();
nextStep = null;
if (next) {
nextStep = next;
}
}
function httpTest() {
counter++;
display("AGPS Data", "Touch for start", () => {
display("Request...", "Touch for restart", httpTest);
if (Bluetooth.println) {
console.log("On device");
Bluetooth.println(JSON.stringify({t:"info", msg:"HTTP Request"}));
Bluetooth.println(JSON.stringify({t:"http", url:"https://www.espruino.com/agps/casic.base64"}));
}
else {
console.log("Testing on Emulator");
setTimeout(() => {
GB({t:"http", resp:testData});
}, 1);
}
});
}
var nextStep = null;
Bangle.on("touch", () => {
if (nextStep) {
nextStep();
}
});
httpTest();
// Show launcher when middle button pressed
// Load widgets
Bangle.loadWidgets();
Bangle.drawWidgets();
let waiting = false;
/*
require("Storage").write("httptest.info",{
"id":"httptest",
"name":"Http Test",
"src":"httptest.js",
"icon":"wristlight.img"
});
*/
function start() {
g.reset();
g.clear();
waiting = false;
display("Retry?", "touch to retry");
Bangle.on("touch", () => { updateAgps(); });
}
var testData = "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";
function updateAgps() {
g.reset();
g.clear();
if (!waiting) {
waiting = true;
display("Updating A-GPS...", "takes ~ 10 seconds");
require("agpsdata").pull(function() {
waiting = false;
display("A-GPS updated.", "touch to close");
Bangle.on("touch", () => { load(); });
},
function(error) {
waiting = false;
E.showAlert(error, "Error")
.then(() => { start(); });
});
} else {
display("Waiting...");
}
}
updateAgps();

26
apps/agpsdata/boot.js Normal file
View File

@ -0,0 +1,26 @@
(function() {
let waiting = false;
let settings = require("Storage").readJSON("agpsdata.settings.json", 1) || {
enabled: true,
refresh: 1440
};
if (settings.refresh == undefined) settings.refresh = 1440;
function successCallback(){
waiting = false;
}
function errorCallback(){
waiting = false;
}
if (settings.enabled) {
setInterval(() => {
if (!waiting && NRF.getSecurityStatus().connected){
waiting = true;
require("agpsdata").pull(successCallback, errorCallback);
}
}, settings.refresh * 1000 * 60);
}
})();

View File

@ -0,0 +1 @@
{"enabled":true,"refresh":1440,"gnsstype":1}

93
apps/agpsdata/lib.js Normal file
View File

@ -0,0 +1,93 @@
function readSettings() {
settings = Object.assign(
require('Storage').readJSON("agpsdata.default.json", true) || {},
require('Storage').readJSON(FILE, true) || {});
}
var FILE = "agpsdata.settings.json";
var settings;
readSettings();
function setAGPS(b64) {
return new Promise(function(resolve, reject) {
var initCommands = "Bangle.setGPSPower(1);\n"; // turn GPS on
const gnsstype = settings.gnsstype || 1; // default GPS
initCommands += `Serial1.println("${CASIC_CHECKSUM("$PCAS04," + gnsstype)}")\n`; // set GNSS mode
// What about:
// NAV-TIMEUTC (0x01 0x10)
// NAV-PV (0x01 0x03)
// or AGPS.zip uses AID-INI (0x0B 0x01)
eval(initCommands);
try {
writeChunks(atob(b64), resolve);
} catch (e) {
console.log("error:", e);
reject();
}
});
}
var chunkI = 0;
function writeChunks(bin, resolve) {
return new Promise(function(resolve2) {
const chunkSize = 128;
setTimeout(function() {
if (chunkI < bin.length) {
var chunk = bin.substr(chunkI, chunkSize);
js = `Serial1.write(atob("${btoa(chunk)}"))\n`;
eval(js);
chunkI += chunkSize;
writeChunks(bin, resolve);
} else {
if (resolve)
resolve(); // call outer resolve
}
}, 200);
});
}
function CASIC_CHECKSUM(cmd) {
var cs = 0;
for (var i = 1; i < cmd.length; i++)
cs = cs ^ cmd.charCodeAt(i);
return cmd + "*" + cs.toString(16).toUpperCase().padStart(2, '0');
}
function updateLastUpdate() {
const file = "agpsdata.json";
let data = require("Storage").readJSON(file, 1) || {};
data.lastUpdate = Math.round(Date.now());
require("Storage").writeJSON(file, data);
}
exports.pull = function(successCallback, failureCallback) {
const uri = "https://www.espruino.com/agps/casic.base64";
if (Bangle.http) {
Bangle.http(uri, {timeout : 10000})
.then(event => {
setAGPS(event.resp)
.then(r => {
updateLastUpdate();
if (successCallback)
successCallback();
})
.catch((e) => {
console.log("error", e);
if (failureCallback)
failureCallback(e);
});
})
.catch((e) => {
console.log("error", e);
if (failureCallback)
failureCallback(e);
});
} else {
console.log("error: No http method found");
if (failureCallback)
failureCallback(/*LANG*/ "No http method");
}
};

View File

@ -1,16 +1,24 @@
{ "id": "agpsdata",
"name": "A-GPS Data",
"shortName":"AGPS Data",
"name": "A-GPS Data Downloader App",
"shortName":"A-GPS Data",
"icon": "agpsdata.png",
"version":"0.01",
"description": "Download assisted GPS data directly to watch",
"tags": "assisted,gps,agps,http",
"version":"0.04",
"description": "Once installed, this app allows you to download assisted GPS (A-GPS) data directly to your Bangle.js **via Gadgetbridge on an Android phone** when you run the app. If you just want to upload the latest AGPS data from this app loader, please use the `Assisted GPS Update (AGPS)` app.",
"tags": "boot,tool,assisted,gps,agps,http",
"allow_emulator":true,
"supports": ["BANGLEJS2"],
"readme":"README.md",
"screenshots" : [ { "url":"screenshot.png" }, { "url":"screenshot2.png" }, { "url":"screenshot3.png" }, { "url":"screenshot4.png" }, { "url":"screenshot5.png" } ],
"screenshots" : [ { "url":"screenshot.png" }, { "url":"screenshot2.png" } ],
"storage": [
{"name":"agpsdata.app.js","url":"app.js"},
{"name":"agpsdata.img","url":"agpsdata-icon.js","evaluate":true}
{"name":"agpsdata.img","url":"agpsdata-icon.js","evaluate":true},
{"name":"agpsdata.default.json","url":"default.json"},
{"name":"agpsdata.boot.js","url":"boot.js"},
{"name":"agpsdata","url":"lib.js"},
{"name":"agpsdata.settings.js","url":"settings.js"}
],
"data": [
{"name": "agpsdata.json"},
{"name": "agpsdata.settings.json"}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 959 B

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 613 B

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 695 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 533 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 578 B

71
apps/agpsdata/settings.js Normal file
View File

@ -0,0 +1,71 @@
(function(back) {
function writeSettings(key, value) {
var s = Object.assign(
require('Storage').readJSON(settingsDefaultFile, true) || {},
require('Storage').readJSON(settingsFile, true) || {});
s[key] = value;
require('Storage').writeJSON(settingsFile, s);
readSettings();
}
function readSettings() {
settings = Object.assign(
require('Storage').readJSON(settingsDefaultFile, true) || {},
require('Storage').readJSON(settingsFile, true) || {});
}
var settingsFile = "agpsdata.settings.json";
var settingsDefaultFile = "agpsdata.default.json";
var settings;
readSettings();
const gnsstypes = [
"", "GPS", "BDS", "GPS+BDS", "GLONASS", "GPS+GLONASS", "BDS+GLONASS",
"GPS+BDS+GLON."
];
function buildMainMenu() {
var mainmenu = {
'' : {'title' : 'AGPS download'},
'< Back' : back,
"Enabled" : {
value : !!settings.enabled,
onchange : v => { writeSettings("enabled", v); }
},
"Refresh every" : {
value : settings.refresh / 60,
min : 6,
max : 168,
step : 1,
format : v => v + "h",
onchange : v => { writeSettings("refresh", Math.round(v * 60)); }
},
"GNSS type" : {
value : settings.gnsstype,
min : 1,
max : 7,
step : 1,
format : v => gnsstypes[v],
onchange : x => writeSettings('gnsstype', x)
},
"Force refresh" : () => {
E.showMessage("Loading A-GPS data");
require("agpsdata")
.pull(
function() {
E.showAlert("Success").then(
() => { E.showMenu(buildMainMenu()); });
},
function(error) {
E.showAlert(error, "Error")
.then(() => { E.showMenu(buildMainMenu()); });
});
}
};
return mainmenu;
}
E.showMenu(buildMainMenu());
});

5
apps/aiclock/ChangeLog Normal file
View File

@ -0,0 +1,5 @@
0.01: New app!
0.02: Design improvements and fixes.
0.03: Indicate battery level through line occurrence.
0.04: Use widget_utils module.
0.05: Support for clkinfo.

25
apps/aiclock/README.md Normal file
View File

@ -0,0 +1,25 @@
# AI Clock
This clock was designed by stable diffusion ([paper](https://arxiv.org/abs/2112.10752)) using the following prompt:
`A rectangle banglejs watchface`
The original output of stable diffusion is shown here:
![](orig.png)
My implementation is shown below. Note that horizontal lines occur randomly, but the
probability is correlated with the battery level. So if your screen contains only
a few lines its time to charge your bangle again ;) Also note that the upper text
implementes the clkinfo module and can be configured via touch left/right/up/down.
Touch at the center to trigger the selected action.
![](impl.png)
# Thanks to
The great open-source community: I used an open-source diffusion model (https://github.com/CompVis/stable-diffusion)
to generate a watch face for the open-source smartwatch BangleJs.
## Creator
- [David Peer](https://github.com/peerdavid).

437
apps/aiclock/aiclock.app.js Normal file
View File

@ -0,0 +1,437 @@
/************************************************
* AI Clock
*/
const storage = require('Storage');
const clock_info = require("clock_info");
/************************************************
* Assets
*/
require("Font7x11Numeric7Seg").add(Graphics);
Graphics.prototype.setFontGochiHand = function(scale) {
// Actual height 27 (29 - 3)
this.setFontCustom(
atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAA8AAAAADwAAAAAPAAAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAA/+AAAB//4AAH///gAH///gAAf//AAAB/+AAAAH8AAAAAAAAAAAAAAAAAAAAH8AAAAB/8AAAAP/4AAAB//wAAAPx/AAAB8B+AAAHgD4AAA+AHgAADwAeAAAPAB4AAA8AHgAAD4AeAAAPgB4AAAeAPgAAB8A8AAAH4HwAAAP/+AAAAf/wAAAA/+AAAAB/wAAAAB8AAAAAAAAAAADgAAAAAfAAAAAB4AAAAAPAAAAAB8AAAAAHgAAAAA8AAAAADwAAAAAf4AAAAB//8AAAD//4AAAH//gAAAD/+AAAAAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4AHgAAHgA+AAA/AD4AAD4AfgAAfAD+AAB4Af4AAHgD/gAAeAfeAAB4D54AAHw/HgAAf/4fAAA//B8AAD/4DwAAH+APAAAHgA8AAAAADwAAAAAOAAAAAAAAABgAAAAAPAAAAAB8AAAAAHwAYAAAeAD4AAD4APwAAPA4fgAA8Hw+AADwfB4AAPh4HwAA+HgPAAB/+A8AAH/4DwAAP/weAAAf/j4AAAc//gAAAB/8AAAAD/gAAAAD8AAAAAAAAAAAAAAAAAADAAAAAA+AAAAAP4AAAAB/wAAAAP/AAAAD+8AAAAfzwAAAf8HAAAB/gcAAAH/hwAAAf//gAAA//+AAAAf//gAAAP//gAAAD/+AAAAB/4AAAAH/AAAAAeAAAAAAgAAAAAAAAAAAAcAAAB8H8AAAP4f4AAA/x/wAAD/H/gAAf+A+AAB74B4AAHnwHgAAefAfAAB58A8AAHj4DwAAePgPAAB4fA8AAHh+HgAAeD8+AAB4P/4AAHgf/AAAeA/4AAAAA+AAAAAAAAAAAAAAAAAAHgAAAAD/wAAAA//gAAAH//AAAA//+AAAD4H8AAAfA/wAAB4D/AAAHgP+AAAeB54AAB4HngAAHweeAAAfB54AAA4HngAAAAeeAAAAB/4AAAAH/AAAAAP4AAAAAfAAADwAAAAAPAAAAAA8HgAAADweAAAAPB4AAAA8HgAAADweAAAAPh4AAAA+HgAAAB4eAAAAHx4AAAAf//8AAA///wAAD//+AAAH//4AAAAeAAAAAB4AAAAAHgAAAAAeAAAAAB4AAAAAHgAAAAAAAAAAAAAAAAAAD+AAAA+f+AAAH//8AAA///wAAH/4fgAAePgeAAB4+B4AAHj4HwAAePgPAAB4+A8AAHz4DwAAfngeAAA//B4AAD/+HgAAH//8AAAP//wAAAAf+AAAAA/wAAAAAYAAAAAAAAAAA/gAAAAH/AAAAA/8AAAAD34AAAAeHgAAAB4eAAAAHh4AAAA8HgAAADweAAAAPDwAAAA8PAAAADx4AAAAPvgAAAAf///AAB///8AAH///wAAP///AAA/wA4AABwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOA4AAAA8DwAAADwPAAAAPA8AAAAYBgAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=='),
46,
atob("DQoXEBQVExUUFRYUDQ=="),
40+(scale<<8)+(1<<16)
);
return this;
}
/************************************************
* Set some important constants such as width, height and center
*/
var W = g.getWidth(),R=W/2;
var H = g.getHeight();
var cx = W/2;
var cy = H/2;
var drawTimeout;
var lock_input = false;
/************************************************
* SETTINGS
*/
const SETTINGS_FILE = "aiclock.setting.json";
let settings = {
menuPosX: 0,
menuPosY: 0,
};
let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings;
for (const key in saved_settings) {
settings[key] = saved_settings[key]
}
/************************************************
* Menu
*/
function getDate(){
var date = new Date();
return ("0"+date.getDate()).substr(-2) + "/" + ("0"+(date.getMonth()+1)).substr(-2)
}
// Custom clockItems menu - therefore, its added here and not in a clkinfo.js file.
var clockItems = {
name: getDate(),
img: null,
items: [
{ name: "Week",
get: () => ({ text: "Week " + weekOfYear(), img: null}),
show: function() { clockItems.items[0].emit("redraw"); },
hide: function () {}
},
]
};
function weekOfYear() {
var date = new Date();
date.setHours(0, 0, 0, 0);
// Thursday in current week decides the year.
date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7);
// January 4 is always in week 1.
var week1 = new Date(date.getFullYear(), 0, 4);
// Adjust to Thursday in week 1 and count number of weeks from date to week1.
return 1 + Math.round(((date.getTime() - week1.getTime()) / 86400000
- 3 + (week1.getDay() + 6) % 7) / 7);
}
// Load menu
var menu = clock_info.load();
menu = menu.concat(clockItems);
// Ensure that our settings are still in range (e.g. app uninstall). Otherwise reset the position it.
if(settings.menuPosX >= menu.length || settings.menuPosY > menu[settings.menuPosX].items.length ){
settings.menuPosX = 0;
settings.menuPosY = 0;
}
// Set draw functions for each item
menu.forEach((menuItm, x) => {
menuItm.items.forEach((item, y) => {
function drawItem() {
// For the clock, we have a special case, as we don't wanna redraw
// immediately when something changes. Instead, we update data each minute
// to save some battery etc. Therefore, we hide (and disable the listener)
// immedeately after redraw...
item.hide();
// After drawing the item, we enable inputs again...
lock_input = false;
var info = item.get();
drawMenuItem(info.text, info.img);
}
item.on('redraw', drawItem);
})
});
function canRunMenuItem(){
if(settings.menuPosY == 0){
return false;
}
var menuEntry = menu[settings.menuPosX];
var item = menuEntry.items[settings.menuPosY-1];
return item.run !== undefined;
}
function runMenuItem(){
if(settings.menuPosY == 0){
return;
}
var menuEntry = menu[settings.menuPosX];
var item = menuEntry.items[settings.menuPosY-1];
try{
var ret = item.run();
if(ret){
Bangle.buzz(300, 0.6);
}
} catch (ex) {
// Simply ignore it...
}
}
/*
* Based on the great multi clock from https://github.com/jeffmer/BangleApps/
*/
Graphics.prototype.drawRotRect = function(w, r1, r2, angle) {
angle = angle % 360;
var w2=w/2, h=r2-r1, theta=angle*Math.PI/180;
return this.fillPoly(this.transformVertices([-w2,0,-w2,-h,w2,-h,w2,0],
{x:cx+r1*Math.sin(theta),y:cy-r1*Math.cos(theta),rotate:theta}));
};
function drawBackground() {
g.setFontAlign(0,0);
g.setColor(g.theme.fg);
var bat = E.getBattery() / 100.0;
var y = 0;
while(y < H){
// Show less lines in case of small battery level.
if(Math.random() > bat){
y += 5;
continue;
}
y += 3 + Math.floor(Math.random() * 10);
g.drawLine(0, y, W, y);
g.drawLine(0, y+1, W, y+1);
g.drawLine(0, y+2, W, y+2);
y += 2;
}
}
function drawCircle(isLocked){
g.setColor(g.theme.fg);
g.fillCircle(cx, cy, 12);
var c = isLocked ? "#f00" : g.theme.bg;
g.setColor(c);
g.fillCircle(cx, cy, 6);
}
function toAngle(a){
if (a < 0){
return 360 + a;
}
if(a > 360) {
return 360 - a;
}
return a
}
function drawMenuItem(text, image){
if(text == null){
drawTime();
return
}
// image = atob("GBiBAAD+AAH+AAH+AAH+AAH/AAOHAAYBgAwAwBgwYBgwYBgwIBAwOBAwOBgYIBgMYBgAYAwAwAYBgAOHAAH/AAH+AAH+AAH+AAD+AA==");
text = String(text);
g.reset().setBgColor("#fff").setColor("#000");
g.setFontAlign(0,0);
g.setFont("Vector", 20);
var imgWidth = image == null ? 0 : 24;
var strWidth = g.stringWidth(text);
var strHeight = text.split('\n').length > 1 ? 40 : Math.max(24, imgWidth+2);
var w = imgWidth + strWidth;
g.clearRect(cx-w/2-8, 40-strHeight/2-1, cx+w/2+4, 40+strHeight/2)
// Draw right line as designed by stable diffusion
g.drawLine(cx+w/2+5, 40-strHeight/2-1, cx+w/2+5, 40+strHeight/2);
g.drawLine(cx+w/2+6, 40-strHeight/2-1, cx+w/2+6, 40+strHeight/2);
g.drawLine(cx+w/2+7, 40-strHeight/2-1, cx+w/2+7, 40+strHeight/2);
// And finally the text
g.drawString(text, cx+imgWidth/2, 42);
g.drawString(text, cx+1+imgWidth/2, 41);
if(image != null) {
var scale = image.width ? imgWidth / image.width : 1;
g.drawImage(image, W/2 + -strWidth/2-4 - parseInt(imgWidth/2), 41-12, {scale: scale});
}
drawTime();
}
function drawTime(){
// Draw digital time first
drawDigits();
// And now the analog time
var drawHourHand = g.drawRotRect.bind(g,8,12,R-38);
var drawMinuteHand = g.drawRotRect.bind(g,6,12,R-12 );
g.setFontAlign(0,0);
// Compute angles
var date = new Date();
var m = parseInt(date.getMinutes() * 360 / 60);
var h = date.getHours();
h = h > 12 ? h-12 : h;
h += date.getMinutes()/60.0;
h = parseInt(h*360/12);
// Draw minute and hour fg
g.setColor(g.theme.fg);
drawHourHand(h);
drawMinuteHand(m);
}
function drawDigits(){
var date = new Date();
g.setFontAlign(0,0);
g.setFont("7x11Numeric7Seg",3);
var text = ("0"+date.getHours()).substr(-2) + ":" + ("0"+date.getMinutes()).substr(-2); //Bangle.getHealthStatus("day").steps;
var w = g.stringWidth(text);
g.setColor(g.theme.bg);
g.fillRect(cx-w/2-4, 120, cx+w/2+4, 140+20);
// Draw right line as designed by stable diffusion
g.setColor(g.theme.fg);
g.drawLine(cx+w/2+5, 120, cx+w/2+5, 140+20);
g.drawLine(cx+w/2+6, 120, cx+w/2+6, 140+20);
g.drawLine(cx+w/2+7, 120, cx+w/2+7, 140+20);
// And the 7set text
g.setColor("#BBB");
g.drawString("88:88", cx, 140);
g.drawString("88:88", cx+1, 140);
g.drawString("88:88", cx, 141);
g.setColor(g.theme.fg);
g.drawString(text, cx, 140);
g.drawString(text, cx+1, 140);
g.drawString(text, cx, 141);
}
function drawDate(){
var menuEntry = menu[settings.menuPosX];
// The first entry is the overview...
if(settings.menuPosY == 0){
drawMenuItem(menuEntry.name, menuEntry.img);
return;
}
// Draw item if needed
lock_input = true;
var item = menuEntry.items[settings.menuPosY-1];
item.show();
}
function draw(){
// Queue draw in one minute
queueDraw();
g.reset();
g.clearRect(0, 0, g.getWidth(), g.getHeight());
g.setColor(1,1,1);
drawBackground();
drawDate();
drawCircle(Bangle.isLocked());
}
/*
* Listeners
*/
Bangle.on('lcdPower',on=>{
if (on) {
draw(true);
} else { // stop draw timer
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
}
});
Bangle.on('lock', function(isLocked) {
drawCircle(isLocked);
});
Bangle.on('touch', function(btn, e){
var left = parseInt(g.getWidth() * 0.22);
var right = g.getWidth() - left;
var upper = parseInt(g.getHeight() * 0.22);
var lower = g.getHeight() - upper;
var is_upper = e.y < upper;
var is_lower = e.y > lower;
var is_left = e.x < left && !is_upper && !is_lower;
var is_right = e.x > right && !is_upper && !is_lower;
var is_center = !is_upper && !is_lower && !is_left && !is_right;
if(lock_input){
return;
}
if(is_lower){
Bangle.buzz(40, 0.6);
settings.menuPosY = (settings.menuPosY+1) % (menu[settings.menuPosX].items.length+1);
draw();
}
if(is_upper){
Bangle.buzz(40, 0.6);
settings.menuPosY = settings.menuPosY-1;
settings.menuPosY = settings.menuPosY < 0 ? menu[settings.menuPosX].items.length : settings.menuPosY;
draw();
}
if(is_right){
Bangle.buzz(40, 0.6);
settings.menuPosX = (settings.menuPosX+1) % menu.length;
settings.menuPosY = 0;
draw();
}
if(is_left){
Bangle.buzz(40, 0.6);
settings.menuPosY = 0;
settings.menuPosX = settings.menuPosX-1;
settings.menuPosX = settings.menuPosX < 0 ? menu.length-1 : settings.menuPosX;
draw();
}
if(is_center){
if(canRunMenuItem()){
runMenuItem();
}
}
});
E.on("kill", function(){
try{
storage.write(SETTINGS_FILE, settings);
} catch(ex){
// If this fails, we still kill the app...
}
});
/*
* Some helpers
*/
function queueDraw() {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = setTimeout(function() {
drawTimeout = undefined;
draw();
}, 60000 - (Date.now() % 60000));
}
/*
* Lets start widgets, listen for btn etc.
*/
// Show launcher when middle button pressed
Bangle.setUI("clock");
Bangle.loadWidgets();
/*
* we are not drawing the widgets as we are taking over the whole screen
* so we will blank out the draw() functions of each widget and change the
* area to the top bar doesn't get cleared.
*/
require('widget_utils').hide();
// Clear the screen once, at startup and draw clock
g.setTheme({bg:"#fff",fg:"#000",dark:false}).clear();
draw();
// After drawing the watch face, we can draw the widgets
// Bangle.drawWidgets();

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwgP/ACfAEZU/ECZELIKhSR/+PAoWAv4FDhk/x/ggP+j0fx/AgP8n8PCIX8CwIFC/F/w4FBgP4gEHC4QFE//w//DC4QFB8YFC+P/8IdCAoYdBAoPxDoQAd+CiKh4dQwDhfAA4A="))

BIN
apps/aiclock/aiclock.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
apps/aiclock/impl.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
apps/aiclock/impl_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
apps/aiclock/impl_3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,22 @@
{
"id": "aiclock",
"name": "AI Clock",
"shortName":"AI Clock",
"icon": "aiclock.png",
"version":"0.05",
"readme": "README.md",
"supports": ["BANGLEJS2"],
"description": "A watch face that was designed by an AI (stable diffusion) and implemented by a human.",
"type": "clock",
"tags": "clock",
"screenshots": [
{"url":"orig.png"},
{"url":"impl.png"},
{"url":"impl_2.png"},
{"url":"impl_3.png"}
],
"storage": [
{"name":"aiclock.app.js","url":"aiclock.app.js"},
{"name":"aiclock.img","url":"aiclock.icon.js","evaluate":true}
]
}

BIN
apps/aiclock/orig.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

@ -33,4 +33,7 @@
0.31: Add seconds to timers
0.32: Fix wrong hidden filter
Add option for auto-delete a timer after it expires
0.33: Allow hiding timers&alarms
0.34: Add "Confirm" option to alarm/timer edit menus
0.35: Add automatic translation of more strings
0.36: alarm widget moved out of app

View File

@ -124,7 +124,16 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex) {
value: alarm.as,
onchange: v => alarm.as = v
},
/*LANG*/"Cancel": () => showMainMenu()
/*LANG*/"Hidden": {
value: alarm.hidden || false,
onchange: v => alarm.hidden = v
},
/*LANG*/"Cancel": () => showMainMenu(),
/*LANG*/"Confirm": () => {
prepareAlarmForSave(alarm, alarmIndex, time);
saveAndReload();
showMainMenu();
}
};
if (!isNew) {
@ -174,7 +183,7 @@ function decodeDOW(alarm) {
.map((day, index) => alarm.dow & (1 << (index + firstDayOfWeek)) ? day : "_")
.join("")
.toLowerCase()
: "Once"
: /*LANG*/"Once"
}
function showEditRepeatMenu(repeat, dow, dowChangeCallback) {
@ -284,8 +293,17 @@ function showEditTimerMenu(selectedTimer, timerIndex) {
value: timer.del,
onchange: v => timer.del = v
},
/*LANG*/"Hidden": {
value: timer.hidden || false,
onchange: v => timer.hidden = v
},
/*LANG*/"Vibrate": require("buzz_menu").pattern(timer.vibrate, v => timer.vibrate = v),
/*LANG*/"Cancel": () => showMainMenu()
/*LANG*/"Cancel": () => showMainMenu(),
/*LANG*/"Confirm": () => {
prepareTimerForSave(timer, timerIndex, time);
saveAndReload();
showMainMenu();
}
};
if (!isNew) {

View File

@ -2,17 +2,16 @@
"id": "alarm",
"name": "Alarms & Timers",
"shortName": "Alarms",
"version": "0.32",
"version": "0.36",
"description": "Set alarms and timers on your Bangle",
"icon": "app.png",
"tags": "tool,alarm,widget",
"tags": "tool,alarm",
"supports": [ "BANGLEJS", "BANGLEJS2" ],
"readme": "README.md",
"dependencies": { "scheduler":"type" },
"dependencies": { "scheduler":"type", "alarm":"widget" },
"storage": [
{ "name": "alarm.app.js", "url": "app.js" },
{ "name": "alarm.img", "url": "app-icon.js", "evaluate": true },
{ "name": "alarm.wid.js", "url": "widget.js" }
{ "name": "alarm.img", "url": "app-icon.js", "evaluate": true }
],
"screenshots": [
{ "url": "screenshot-1.png" },

2
apps/alpinenav/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.01: New App!
0.02: Added adjustment for Bangle.js magnetometer heading fix

View File

@ -224,7 +224,7 @@ Bangle.on('mag', function (m) {
if (isNaN(m.heading))
compass_heading = "---";
else
compass_heading = 360 - Math.round(m.heading);
compass_heading = Math.round(m.heading);
current_colour = g.getColor();
g.reset();
g.setColor(background_colour);

View File

@ -1,7 +1,7 @@
{
"id": "alpinenav",
"name": "Alpine Nav",
"version": "0.01",
"version": "0.02",
"description": "App that performs GPS monitoring to track and display position relative to a given origin in realtime",
"icon": "app-icon.png",
"tags": "outdoors,gps",

View File

@ -9,4 +9,12 @@
0.08: Handling of alarms
0.09: Alarm vibration, repeat, and auto-snooze now handled by sched
0.10: Fix SMS bug
0.11: Use default Bangle formatter for booleans
0.12: Use default Bangle formatter for booleans
0.13: Added Bangle.http function (see Readme file for more info)
0.14: Fix timeout of http function not being cleaned up
0.15: Allow method/body/headers to be specified for `http` (needs Gadgetbridge 0.68.0b or later)
0.16: Bangle.http now fails immediately if there is no Bluetooth connection (fix #2152)
0.17: Now kick off Calendar sync as soon as connected to Gadgetbridge
0.18: Use new message library
If connected to Gadgetbridge, allow GPS forwarding from phone (Gadgetbridge code still not merged)
0.19: Add automatic translation for a couple of strings.

View File

@ -20,6 +20,8 @@ It contains:
of Gadgetbridge - making your phone make noise so you can find it.
* `Keep Msgs` - default is `Off`. When Gadgetbridge disconnects, should Bangle.js
keep any messages it has received, or should it delete them?
* `Overwrite GPS` - when GPS is requested by an app, this doesn't use Bangle.js's GPS
but instead asks Gadgetbridge on the phone to use the phone's GPS
* `Messages` - launches the messages app, showing a list of messages
## How it works
@ -32,6 +34,25 @@ Responses are sent back to Gadgetbridge simply as one line of JSON.
More info on message formats on http://www.espruino.com/Gadgetbridge
## Functions provided
The boot code also provides some useful functions:
* `Bangle.messageResponse = function(msg,response)` - send a yes/no response to a message. `msg` is a message object, and `response` is a boolean.
* `Bangle.musicControl = function(cmd)` - control music, cmd = `play/pause/next/previous/volumeup/volumedown`
* `Bangle.http = function(url,options)` - make an HTTPS request to a URL and return a promise with the data. Requires the [internet enabled `Bangle.js Gadgetbridge` app](http://www.espruino.com/Gadgetbridge#http-requests). `options` can contain:
* `id` - a custom (string) ID
* `timeout` - a timeout for the request in milliseconds (default 30000ms)
* `xpath` an xPath query to run on the request (but right now the URL requested must be XML - HTML is rarely XML compliant)
eg:
```
Bangle.http("https://pur3.co.uk/hello.txt").then(data=>{
console.log("Got ",data);
});
```
## Testing
Bangle.js can only hold one connection open at a time, so it's hard to see

View File

@ -57,7 +57,7 @@
t:event.cmd=="incoming"?"add":"remove",
id:"call", src:"Phone",
positive:true, negative:true,
title:event.name||"Call", body:"Incoming call\n"+event.number});
title:event.name||/*LANG*/"Call", body:/*LANG*/"Incoming call\n"+event.number});
require("messages").pushMessage(event);
},
"alarm" : function() {
@ -91,10 +91,6 @@
sched.reload();
},
//TODO perhaps move those in a library (like messages), used also for viewing events?
//simple package with events all together
"calendarevents" : function() {
require("Storage").writeJSON("android.calendar.json", event.events);
},
//add and remove events based on activity on phone (pebble-like)
"calendar" : function() {
var cal = require("Storage").readJSON("android.calendar.json",true);
@ -109,7 +105,7 @@
"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)) return;
if (!cal || !Array.isArray(cal)) cal = [];
cal = cal.filter(e=>e.id!=event.id);
require("Storage").writeJSON("android.calendar.json", cal);
},
@ -118,15 +114,74 @@
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)});
},
"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
},
"gps": function() {
const settings = require("Storage").readJSON("android.settings.json",1)||{};
if (!settings.overwriteGps) return;
delete event.t;
event.satellites = NaN;
event.course = NaN;
event.fix = 1;
Bangle.emit('gps', event);
},
"is_gps_active": function() {
gbSend({ t: "gps_power", status: Bangle._PWR && Bangle._PWR.GPS && Bangle._PWR.GPS.length>0 });
}
};
var h = HANDLERS[event.t];
if (h) h(); else console.log("GB Unknown",event);
};
// 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.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;
}
// Battery monitor
function sendBattery() { gbSend({ t: "status", bat: E.getBattery(), chg: Bangle.isCharging()?1:0 }); }
NRF.on("connect", () => setTimeout(sendBattery, 2000));
NRF.on("connect", () => setTimeout(function() {
sendBattery();
GB({t:"force_calendar_sync_start"}); // send a list of our calendar entries to start off the sync process
}, 2000));
Bangle.on("charging", sendBattery);
if (!settings.keep)
NRF.on("disconnect", () => require("messages").clearAll()); // remove all messages on disconnect
@ -146,6 +201,30 @@
if (isFinite(msg.id)) return gbSend({ t: "notify", n:response?"OPEN":"DISMISS", id: msg.id });
// error/warn here?
};
// GPS overwrite logic
if (settings.overwriteGps) { // if the overwrite option is set../
// Save current logic
const originalSetGpsPower = Bangle.setGPSPower;
// Replace set GPS power logic to suppress activation of gps (and instead request it from the phone)
Bangle.setGPSPower = (isOn, appID) => {
// if not connected, use old logic
if (!NRF.getSecurityStatus().connected) return originalSetGpsPower(isOn, appID);
// Emulate old GPS power logic
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);
let pwr = Bangle._PWR.GPS.length>0;
gbSend({ t: "gps_power", status: pwr });
return pwr;
}
// Replace check if the GPS is on to check the _PWR variable
Bangle.isGPSOn = () => {
return Bangle._PWR && Bangle._PWR.GPS && Bangle._PWR.GPS.length>0;
}
}
// remove settings object so it's not taking up RAM
delete settings;
})();

View File

@ -2,11 +2,11 @@
"id": "android",
"name": "Android Integration",
"shortName": "Android",
"version": "0.12",
"version": "0.19",
"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",
"dependencies": {"messages":"app"},
"dependencies": {"messages":"module"},
"supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
"storage": [

View File

@ -1,4 +1,7 @@
(function(back) {
function gb(j) {
Bluetooth.println(JSON.stringify(j));
}
@ -23,7 +26,17 @@
updateSettings();
}
},
/*LANG*/"Messages" : ()=>load("messages.app.js"),
/*LANG*/"Overwrite GPS" : {
value : !!settings.overwriteGps,
onchange: newValue => {
if (newValue) {
Bangle.setGPSPower(false, 'android');
}
settings.overwriteGps = newValue;
updateSettings();
}
},
/*LANG*/"Messages" : ()=>require("message").openGUI(),
};
E.showMenu(mainmenu);
})

View File

@ -1,3 +1,5 @@
0.01: New App!
0.02: Fix bug if image clock wasn't installed
0.03: Update to use setUI
0.04: Tell clock widgets to hide. Move loadWidgets() so it only runs on
startup and not on every draw.

View File

@ -87,7 +87,6 @@ if (g.drawImages) {
draw();
var secondInterval = setInterval(draw,100);
// load widgets
Bangle.loadWidgets();
Bangle.drawWidgets();
// Stop when LCD goes off
Bangle.on('lcdPower',on=>{
@ -104,3 +103,5 @@ if (g.drawImages) {
}
// Show launcher when button pressed
Bangle.setUI("clock");
Bangle.loadWidgets();

View File

@ -2,7 +2,7 @@
"id": "animclk",
"name": "Animated Clock",
"shortName": "Anim Clock",
"version": "0.03",
"version": "0.04",
"description": "An animated clock face using Mark Ferrari's amazing 8 bit game art and palette cycling: http://www.markferrari.com/art/8bit-game-art",
"icon": "app.png",
"type": "clock",

View File

@ -10,4 +10,7 @@
week is buffered until date or timezone changes
0.07: align default settings with app.js (otherwise the initial displayed settings will be confusing to users)
0.08: fixed calendar weeknumber not shortened to two digits
0.09: Use default Bangle formatter for booleans
0.09: Use default Bangle formatter for booleans
0.10: Use Bangle.setUI({remove:...}) to allow loading the launcher without a full reset on 2v16
0.11: Moved enhanced Anton clock to 'Anton Clock Plus' and stripped this clock back down to make it faster for new users (270ms -> 170ms)
Modified to avoid leaving functions defined when using setUI({remove:...})

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -1,9 +1,8 @@
{
"id": "antonclk",
"name": "Anton Clock",
"version": "0.09",
"description": "A clock using the bold Anton font, optionally showing seconds and date in ISO-8601 format.",
"readme":"README.md",
"version": "0.11",
"description": "A simple clock using the bold Anton font. See `Anton Clock Plus` for an enhanced version",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
"type": "clock",
@ -12,8 +11,6 @@
"allow_emulator": true,
"storage": [
{"name":"antonclk.app.js","url":"app.js"},
{"name":"antonclk.settings.js","url":"settings.js"},
{"name":"antonclk.img","url":"app-icon.js","evaluate":true}
],
"data": [{"name":"antonclk.json"}]
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -0,0 +1,15 @@
0.01: New App!
0.02: Load widgets after setUI so widclk knows when to hide
0.03: Clock now shows day of week under date.
0.04: Clock can optionally show seconds, date optionally in ISO-8601 format, weekdays and uppercase configurable, too.
0.05: Clock can optionally show ISO-8601 calendar weeknumber (default: Off)
when weekday name "Off": week #:<num>
when weekday name "On": weekday name is cut at 6th position and .#<week num> is added
0.06: fixes #1271 - wrong settings name
when weekday name and calendar weeknumber are on then display is <weekday short> #<calweek>
week is buffered until date or timezone changes
0.07: align default settings with app.js (otherwise the initial displayed settings will be confusing to users)
0.08: fixed calendar weeknumber not shortened to two digits
0.09: Use default Bangle formatter for booleans
0.10: Use Bangle.setUI({remove:...}) to allow loading the launcher without a full reset on 2v16
Modified to avoid leaving functions defined when using setUI({remove:...})

View File

@ -1,6 +1,6 @@
# Anton Clock - Large font digital watch with seconds and date
# Anton Clock Plus - Large font digital watch with seconds and date
Anton clock uses the "Anton" bold font to show the time in a clear, easily readable manner. On the Bangle.js 2, the time can be read easily even if the screen is locked and unlit.
Anton Clock Plus uses the "Anton" bold font to show the time in a clear, easily readable manner. On the Bangle.js 2, the time can be read easily even if the screen is locked and unlit.
## Features
@ -16,16 +16,16 @@ The basic time representation only shows hours and minutes of the current time.
## Usage
Install Anton clock through the Bangle.js app loader.
Configure it through the default Bangle.js configuration mechanism
* Install Anton Clock Plus through the Bangle.js app loader.
* Configure it through the default Bangle.js configuration mechanism
(Settings app, "Apps" menu, "Anton clock" submenu).
If you like it, make it your default watch face
* If you like it, make it your default watch face
(Settings app, "System" menu, "Clock" submenu, select "Anton clock").
## Configuration
Anton clock is configured by the standard settings mechanism of Bangle.js's operating system:
Open the "Settings" app, then the "Apps" submenu and below it the "Anton clock" menu.
Anton Clock is configured by the standard settings mechanism of Bangle.js's operating system:
Open the `Settings` app, then the `Apps` submenu and below it the `Anton Clock+` menu.
You configure Anton clock through several "on/off" switches in two menus.
### The main menu

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwgf/AH4At/l/Aofgh4DB+EAj4REQoM/AgP4AoeACIoLCg4FB4AFDCIwLCgAROgYIB8EBAoUH/gVBCIxQBCKYHBCJp9DI4ICBLJYRCn4RQEYMOR5ARDIgIRMYQZZBgARGZwZBDCKQrCgEDR5AdBUIQRJDoLXFCJD7J/xrICIQFCn4RH/4LDAoTaCCI4Ar/LLDCBfypMkCgMkyV/CJOSCIOf5IRGFwOfCJNP//JnmT588z/+pM/BYIRCk4RC/88+f/n4RCngRCz1JCIf5/nzGoQRIHwXPCIPJI4f8CJHJGQJKCCI59LCI5ZCCJ/+v/kBoM/+V/HIJrHBYJWB/JKB5x9JEYP8AQKdBpwRL841Dp41KZoTxBHYTXBWY77PCKKhJ/4/CcgMkXoQAiA="))

238
apps/antonclkplus/app.js Normal file

File diff suppressed because one or more lines are too long

BIN
apps/antonclkplus/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,20 @@
{
"id": "antonclkplus",
"name": "Anton Clock Plus",
"shortName": "Anton Clock+",
"version": "0.10",
"description": "A clock using the bold Anton font, optionally showing seconds and date in ISO-8601 format.",
"readme":"README.md",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS","BANGLEJS2"],
"allow_emulator": true,
"storage": [
{"name":"antonclkplus.app.js","url":"app.js"},
{"name":"antonclkplus.settings.js","url":"settings.js"},
{"name":"antonclkplus.img","url":"app-icon.js","evaluate":true}
],
"data": [{"name":"antonclkplus.json"}]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -5,6 +5,7 @@
"version": "0.08",
"description": "A clock based on the portal series",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS2"],

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@ -1,11 +1,12 @@
{
"id": "assistedgps",
"name": "Assisted GPS Update (AGPS)",
"name": "Assisted GPS Updater (AGPS)",
"version": "0.03",
"description": "Downloads assisted GPS (AGPS) data to Bangle.js 1 or 2 for faster GPS startup and more accurate fixes. **No app will be installed**, this just uploads new data to the GPS chip.",
"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",
"type": "RAM",
"tags": "tool,outdoors,agps",
"tags": "tool,outdoors,agps,gps,a-gps",
"supports": ["BANGLEJS","BANGLEJS2"],
"custom": "custom.html",
"customConnect": true,

View File

@ -1,3 +1,5 @@
0.01: Create astral clock app
0.02: Fixed Whirlpool galaxy RA/DA, larger compass display, fixed moonphase overlapping battery widget
0.03: Update to use Bangle.setUI instead of setWatch
0.04: Tell clock widgets to hide.
0.05: Added adjustment for Bangle.js magnetometer heading fix

View File

@ -767,6 +767,24 @@ function draw() {
g.clear();
current_moonphase = getMoonPhase();
Bangle.setUI("clockupdown", btn => {
if (btn==0) {
if (!processing) {
if (!modeswitch) {
modeswitch = true;
if (mode == "planetary") mode = "extras";
else mode = "planetary";
}
else
modeswitch = false;
}
} else {
if (!processing)
ready_to_compute = true;
}
});
// Load widgets
Bangle.loadWidgets();
Bangle.drawWidgets();
@ -799,23 +817,6 @@ Bangle.setGPSPower(1);
// Show launcher when button pressed
Bangle.setClockMode();
Bangle.setUI("clockupdown", btn => {
if (btn==0) {
if (!processing) {
if (!modeswitch) {
modeswitch = true;
if (mode == "planetary") mode = "extras";
else mode = "planetary";
}
else
modeswitch = false;
}
} else {
if (!processing)
ready_to_compute = true;
}
});
setWatch(function () {
if (!astral_settings.astral_default) {
colours_switched = true;
@ -833,7 +834,7 @@ Bangle.on('mag', function (m) {
if (isNaN(m.heading))
compass_heading = "---";
else
compass_heading = 360 - Math.round(m.heading);
compass_heading = Math.round(m.heading);
// g.setColor("#000000");
// g.fillRect(160, 10, 160, 20);
g.setColor(display_colour);

View File

@ -1,7 +1,7 @@
{
"id": "astral",
"name": "Astral Clock",
"version": "0.03",
"version": "0.05",
"description": "Clock that calculates and displays Alt Az positions of all planets, Sun as well as several other astronomy targets (customizable) and current Moon phase. Coordinates are calculated by GPS & time and onscreen compass assists orienting. See Readme before using.",
"icon": "app-icon.png",
"type": "clock",

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