1
0
Fork 0

Merge branch 'espruino:master' into development

master
xxDUxx 2022-12-03 14:59:15 +01:00 committed by GitHub
commit dfd1132bcf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
737 changed files with 17781 additions and 9013 deletions

View File

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

3
.gitmodules vendored
View File

@ -1,3 +1,6 @@
[submodule "EspruinoAppLoaderCore"] [submodule "EspruinoAppLoaderCore"]
path = core path = core
url = https://github.com/espruino/EspruinoAppLoaderCore.git 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 // 'app' - an application
// 'clock' - a clock - required for clocks to automatically start // 'clock' - a clock - required for clocks to automatically start
// 'widget' - a widget // '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' // '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' // '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 // 'RAM' - code that runs and doesn't upload anything to storage
// 'launch' - replacement 'Launcher' // 'launch' - replacement 'Launcher'
// 'textinput' - provides a 'textinput' library that allows text to be input on the Bangle // 'textinput' - provides a 'textinput' library that allows text to be input on the Bangle
@ -266,10 +269,21 @@ and which gives information about the app for the Launcher.
// 'locale' - provides 'locale' library for language-specific date/distance/etc // 'locale' - provides 'locale' library for language-specific date/distance/etc
// (a version of 'locale' is included in the firmware) // (a version of 'locale' is included in the firmware)
"tags": "", // comma separated tag list for searching "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 "supports": ["BANGLEJS2"], // List of device IDs supported, either BANGLEJS or BANGLEJS2
"dependencies" : { "notify":"type" } // optional, app 'types' we depend on (see "type" above) "dependencies" : { "notify":"type" } // optional, app 'types' we depend on (see "type" above)
"dependencies" : { "messages":"app" } // optional, depend on a specific app ID "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' // 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'
"provides_modules" : ["messageicons"] // optional, this app provides a module that can be used with 'require'
"readme": "README.md", // if supplied, a link to a markdown-style text file "readme": "README.md", // if supplied, a link to a markdown-style text file
// that contains more information about this app (usage, etc) // that contains more information about this app (usage, etc)
// A 'Read more...' link will be added under the app // A 'Read more...' link will be added under the app
@ -454,7 +468,10 @@ It should also add `myappid.json` to `data`, to make sure it is cleaned up when
## Modules ## Modules
You can include any of [Espruino's modules](https://www.espruino.com/Modules) as 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 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. then you can use it from your app as normal.

View File

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

View File

@ -11,3 +11,4 @@
0.11: Bangle.js2: New pixels, btn1 to exit 0.11: Bangle.js2: New pixels, btn1 to exit
0.12: Actual pixels as of 29th Nov 2021 0.12: Actual pixels as of 29th Nov 2021
0.13: Bangle.js 2: Use setUI to add software back button 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 { } else {
y=-(4+h); // small screen, start right at top y=-(4+h); // small screen, start right at top
} }
g.drawString("Powered by Espruino",0,y+=4+h); g.drawString(/*LANG*/"Powered by Espruino",0,y+=4+h);
g.drawString("Version "+ENV.VERSION,0,y+=h); g.drawString(/*LANG*/"Version "+ENV.VERSION,0,y+=h);
g.drawString("Commit "+ENV.GIT_COMMIT,0,y+=h); g.drawString("Commit "+ENV.GIT_COMMIT,0,y+=h);
function getVersion(name,file) { function getVersion(name,file) {
var j = s.readJSON(file,1); var j = s.readJSON(file,1);
@ -24,9 +24,9 @@ getVersion("Launcher","launch.info");
getVersion("Settings","setting.info"); getVersion("Settings","setting.info");
y+=h; y+=h;
g.drawString(MEM.total+" JS Variables available",0,y+=h); g.drawString(MEM.total+/*LANG*/" JS Variables available",0,y+=h);
g.drawString("Storage: "+(require("Storage").getFree()>>10)+"k free",0,y+=h); g.drawString("Storage: "+(require("Storage").getFree()>>10)+/*LANG*/"k free",0,y+=h);
if (ENV.STORAGE) g.drawString(" "+(ENV.STORAGE>>10)+"k total",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); if (ENV.SPIFLASH) g.drawString("SPI Flash: "+(ENV.SPIFLASH>>10)+"k",0,y+=h);
g.setFontAlign(0,-1); g.setFontAlign(0,-1);
g.flip(); g.flip();

View File

@ -35,17 +35,17 @@ function drawInfo() {
g.setFont("4x6").setFontAlign(0,0).drawString("BANGLEJS.COM",W-30,56); g.setFont("4x6").setFontAlign(0,0).drawString("BANGLEJS.COM",W-30,56);
var h=8, y = 24-h; var h=8, y = 24-h;
g.setFont("6x8").setFontAlign(-1,-1); g.setFont("6x8").setFontAlign(-1,-1);
g.drawString("Powered by Espruino",0,y+=4+h); g.drawString(/*LANG*/"Powered by Espruino",0,y+=4+h);
g.drawString("Version "+ENV.VERSION,0,y+=h); g.drawString(/*LANG*/"Version "+ENV.VERSION,0,y+=h);
g.drawString("Commit "+ENV.GIT_COMMIT,0,y+=h); g.drawString("Commit "+ENV.GIT_COMMIT,0,y+=h);
getVersion("Bootloader","boot.info"); getVersion("Bootloader","boot.info");
getVersion("Launcher","launch.info"); getVersion("Launcher","launch.info");
getVersion("Settings","setting.info"); getVersion("Settings","setting.info");
g.drawString(MEM.total+" JS Vars",0,y+=h); g.drawString(MEM.total+/*LANG*/" JS Vars",0,y+=h);
g.drawString("Storage: "+(require("Storage").getFree()>>10)+"k free",0,y+=h); g.drawString("Storage: "+(require("Storage").getFree()>>10)+/*LANG*/"k free",0,y+=h);
if (ENV.STORAGE) g.drawString(" "+(ENV.STORAGE>>10)+"k total",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); if (ENV.SPIFLASH) g.drawString("SPI Flash: "+(ENV.SPIFLASH>>10)+"k",0,y+=h);
imageTop = y+h; imageTop = y+h;
imgScroll = imgHeight-imageTop; imgScroll = imgHeight-imageTop;

View File

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

View File

@ -1,6 +1,11 @@
# Active Pedometer # Active Pedometer
Pedometer that filters out arm movement and displays a step goal progress. 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. 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. 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. 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. Steps are saved to a datafile every 5 minutes. You can watch a graph using the app.
## Screenshots ## Screenshots
* 600 steps * 600 steps
![](600.png) ![](600.png)

View File

@ -3,7 +3,7 @@
"name": "Active Pedometer", "name": "Active Pedometer",
"shortName": "Active Pedometer", "shortName": "Active Pedometer",
"version": "0.09", "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", "icon": "app.png",
"tags": "outdoors,widget", "tags": "outdoors,widget",
"supports": ["BANGLEJS"], "supports": ["BANGLEJS"],

View File

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

View File

@ -1,5 +1,6 @@
const storage = require('Storage'); const storage = require('Storage');
require("Font6x12").add(Graphics);
require("Font8x12").add(Graphics); require("Font8x12").add(Graphics);
require("Font7x11Numeric7Seg").add(Graphics); require("Font7x11Numeric7Seg").add(Graphics);
@ -11,22 +12,27 @@ function bigThenSmall(big, small, x, y) {
g.drawString(small, x, y); 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() { 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("2GwwkGIf4AfgMRkUiiIHCiMRiAMDAwYCCBAYVDAHMv/4ACkBIBAgPxBgM/BYXyAoICBCowA5gRADKQUDKAYMCmYCBiBXBCo4A5J4MxiMSKQUf+YBBBgSiBgc/kBXBBAMyCoK2CK/btCiUhfAJLCkBkDiMQgBXDCoUvNAJX+AAU/+MB/8wAQIAC+cQK5hoDgIEBBIQFEAYIPHBIgBBAQQIDBwZXSKIMxgJaBgEjmZYCmBXLgLBBkkAgUhiMxBIM0iMSCoMRkZECkQJEichBINDiETAgISBiQTDK6MvJAXzVIQrBBYMCK5E/K4kwGIJXFgdAMgQQBiYiCDgU0HQSlCgMikIEBEAMTDYJXQ+UikYDBj6nCAAMTWoJ6BK4oVEK4c0oQ+BK4MjAgMDJoJXHNYJXHBwa0BohcDY4QAKgJQE+LzBNwJVBkQMEkBXBCoyvFJAVAKISaBiMiHQRIDkVBoSyCK5CvBAgavNDAJAC+cQn5DCgSpBl4MDgBXBgCsBCoYoMLAKREgIKDBJIdKK5oA/AH4A/AH4A/ADUBIH4APiAFEi1mAGUADrkRKwUGK2ZXes1gK2xXfD8A3/K/4AWgxX/ACtga2AwIHLkAgCwvJw6RcDgIABK+w4cK/I4dsEGP5BXtSAQ6BV/5XSG4RX/K6Y3fK+42CK/5XTGwcGK/5XSVwY5cK+o1DAAayYsAhDsCv4K7BTBK4YeYK7CyFVzJXFFIpXtVwYiYK/rmZKYYDDELJXXG4YiaK/Y0aKgQAEK+gkdKt5XGKzqv5GTpX6ETlgK4xWrKTyxKVthXmAGRX/K/5X/AH5X/K/4gBAH4A/AFz/uAH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AHNggEGHfEAgAEHKyQXVK0qTCAggbUK+6SDAApzXK/5BRDYZX3KxBBSYqxXngyvaV25XEd4ZCSsAcBAoRZ2dQZXBLwgaQCIYeCAGirCS4YGCDSJXCC6ZaodYICBZzSw4S4I+XDgSv4K4rzCK/47RAQTMaWHI9YV3TscV3aVagByBK3SwCSqyt8AAQ+XK/4A/AH4A/AH4A3gAA/AH4AuZbdggwc3ADpX/K/5XxsEAgA+XK/o8BgBX/K64/WK/4/XK/5X/K/5XvgBX/K64cYHrw4CSTFggCuXK4oDCEQJXYDS6ScDgg4CPKyRCAAZX0HAgBDK+LlYK4oeBAwZ9aK+lgAoQGBgyvzDIIDBK66sCG4JXYCwIBDK7ADCK+xZCHwJXzGoQ8BK7DpBAAaSXSgRXZO4okCK+IaXV4oABEILSWSYjRCHSo3BDSxXEAAIcBAISvyKawcIAYIGCK/4cUH4YlaHS0AHgI1XOg5YBPrY6WHgRXfAGRXDHzBX8VoJX/K68ADjRX6sBX/K/5X/K8wdcK/UAG7B0iKzZYbK/BWDAH4A/hWpzWhIf4ASgOpzIAB0EAhhH/AB8ZzGJ1WazMA4pH/AB+pxOZxOpzVMqA2ugUzmcgD7cKVYOqzGqpnRFw8ykchK8kviEBmQFBgMiFocSCAcSkUQAgMikRsHhWqxOq0Ut4mqBw0DC4IxBD4wpBHAQMCA4cCGJIAFj8hDIQuBkMTCwU/AYQJBiUxFoPxiIVDK4kyxUz4cxl+KK5MfDQXyD4UCmMSmAEBAQQHDgMTmIxHAAqpBmaqCFwMDEYZRBgEjCQQBB+USK5E/ns/0Uzwc6K48ykYkCK4IfCc4I4CK4QHEBAYAMiICBmYuDmQEBh8iAgRXCLISvJO4MqwcklEiK5CADV4oaBV4oHEK6Eve4JNCbwRfCiMTFoMDkMRSAJXCD49azWp0UqzWayJXIQwcAO4cCkMCFIJOCA4XxK6KPBkR6DTwYyBAwYPEAggfFzORpWK1OZyAOHJ4QfERAUSEgQxIIIgAr1URWIOZzOgGtwAhgMZzWq1OaIv4ASKgOqzTkvAEmq1WgFtQA=="));
} }
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 // schedule a draw for the next minute
let rocketInterval; let rocketInterval;
@ -47,237 +53,91 @@ function clearIntervals() {
drawTimeout = undefined; 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() { function drawClock() {
g.setFont("7x11Numeric7Seg", 3); g.setFont("7x11Numeric7Seg", 3);
g.clearRect(80, 57, 170, 96); g.clearRect(80, 57, 170, 96);
g.setColor(255, 255, 255); g.setColor(0, 255, 255);
var l = 77; g.drawRect(80, 57, 170, 96);
var t = 57; g.fillRect(80, 57, 170, 96);
var w = 170;
var h = 116;
g.drawRect(l, t, w, h);
g.fillRect(l, t, w, h);
g.setColor(0, 0, 0); g.setColor(0, 0, 0);
g.drawString(require("locale").time(new Date(), 1), 76, 60); g.drawString(require("locale").time(new Date(), 1), 70, 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.setFont("8x12", 2);
g.drawString(require("locale").dow(new Date(), 2), 18, 130); g.drawString(require("locale").dow(new Date(), 2).toUpperCase(), 18, 130);
// month
g.setFont("8x12"); g.setFont("8x12");
g.drawString(require("locale").month(new Date(), 2).toUpperCase(), 80, 127); g.drawString(require("locale").month(new Date(), 2).toUpperCase(), 80, 126);
// day nb
g.setFont("8x12", 2); g.setFont("8x12", 2);
const time = new Date().getDate(); const time = new Date().getDate();
g.drawString(time < 10 ? "0" + time : time, 78, 137); g.drawString(time < 10 ? "0" + time : time, 78, 137);
} }
function drawBattery() { 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() { function getSteps() {
var steps = 0; var steps = Bangle.getHealthStatus("day").steps;
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); steps = Math.round(steps/1000);
return steps + "k"; return steps + "k";
} }
function draw() { function draw() {
queueDraw(); queueDraw();
g.reset(); g.clear(1);
g.clear(); g.setColor(0, 255, 255);
g.setColor(255, 255, 255);
g.fillRect(0, 0, g.getWidth(), g.getHeight()); g.fillRect(0, 0, g.getWidth(), g.getHeight());
let background = getBackgroundImage(); let background = getBackgroundImage();
g.drawImage(background, 0, 0, { scale: 1 }); g.drawImage(background, 0, 0, { scale: 1 });
g.setColor(0, 0, 0); g.setColor(0, 0, 0);
if(dataJson && dataJson.weather) drawWeather(dataJson.weather); g.setFont("6x12");
if(dataJson && dataJson.tasks) drawTasks(dataJson.tasks); g.drawString("Launching Process", 30, 20);
g.setFont("8x12");
g.drawString("ACTIVATE", 40, 35);
g.setFontAlign(0,-1); g.setFontAlign(0,-1);
g.setFont("8x12", 2); 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); g.setFontAlign(-1,-1);
drawClock(); drawClock();
drawRocket();
drawBattery(); drawBattery();
drawTimer();
// Hide widgets // Hide widgets
for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";} 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) => { Bangle.on("lcdPower", (on) => {
if (on) { if (on) {
draw(); draw();
} else { } else {
canTouch = false;
clearIntervals(); clearIntervals();
} }
}); });
@ -287,18 +147,14 @@ Bangle.on("lock", (locked) => {
clearIntervals(); clearIntervals();
draw(); draw();
if (!locked) { if (!locked) {
canTouch = true; rocketInterval = setInterval(drawRocket, rocketSpeed);
} else {
canTouch = false;
} }
}); });
Bangle.setUI("clock"); Bangle.setUI("clock");
// Load widgets, but don't show them // Load widgets, but don't show them
Bangle.loadWidgets(); Bangle.loadWidgets();
require("widget_utils").swipeOn(); // hide widgets, make them visible with a swipe
g.reset(); g.clear(1);
g.clear();
draw(); draw();

View File

@ -1,7 +1,7 @@
{ "id": "advcasio", { "id": "advcasio",
"name": "Advanced Casio Clock", "name": "Advanced Casio Clock",
"shortName":"advcasio", "shortName":"advcasio",
"version":"0.03", "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.", "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", "icon": "app.png",
"tags": "clock", "tags": "clock",

View File

@ -5,3 +5,5 @@
0.05: Displaying calendar colour and name 0.05: Displaying calendar colour and name
0.06: Added clkinfo for clocks. 0.06: Added clkinfo for clocks.
0.07: Clkinfo improvements. 0.07: Clkinfo improvements.
0.08: Fix error in clkinfo (didn't require Storage & locale)
Fix clkinfo icon

View File

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

View File

@ -1,11 +1,11 @@
{ {
"id": "agenda", "id": "agenda",
"name": "Agenda", "name": "Agenda",
"version": "0.07", "version": "0.08",
"description": "Simple agenda", "description": "Simple agenda",
"icon": "agenda.png", "icon": "agenda.png",
"screenshots": [{"url":"screenshot_agenda_overview.png"}, {"url":"screenshot_agenda_event1.png"}, {"url":"screenshot_agenda_event2.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"], "supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md", "readme": "README.md",
"allow_emulator": true, "allow_emulator": true,

View File

@ -2,3 +2,4 @@
0.02: Load AGPS data on app start and automatically in background 0.02: Load AGPS data on app start and automatically in background
0.03: Do not load AGPS data on boot 0.03: Do not load AGPS data on boot
Increase minimum interval to 6 hours Increase minimum interval to 6 hours
0.04: Write AGPS data chunks with delay to improve reliability

View File

@ -36,7 +36,7 @@ function updateAgps() {
g.clear(); g.clear();
if (!waiting) { if (!waiting) {
waiting = true; waiting = true;
display("Updating A-GPS..."); display("Updating A-GPS...", "takes ~ 10 seconds");
require("agpsdata").pull(function() { require("agpsdata").pull(function() {
waiting = false; waiting = false;
display("A-GPS updated.", "touch to close"); display("A-GPS updated.", "touch to close");

View File

@ -8,34 +8,45 @@ var FILE = "agpsdata.settings.json";
var settings; var settings;
readSettings(); readSettings();
function setAGPS(data) { function setAGPS(b64) {
var js = jsFromBase64(data); return new Promise(function(resolve, reject) {
try { var initCommands = "Bangle.setGPSPower(1);\n"; // turn GPS on
eval(js); const gnsstype = settings.gnsstype || 1; // default GPS
return true; initCommands += `Serial1.println("${CASIC_CHECKSUM("$PCAS04," + gnsstype)}")\n`; // set GNSS mode
}
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 gnsstype = settings.gnsstype || 1; // default GPS
js += `Serial1.println("${CASIC_CHECKSUM("$PCAS04,"+gnsstype)}")\n`; // set GNSS mode
// What about: // What about:
// NAV-TIMEUTC (0x01 0x10) // NAV-TIMEUTC (0x01 0x10)
// NAV-PV (0x01 0x03) // NAV-PV (0x01 0x03)
// or AGPS.zip uses AID-INI (0x0B 0x01) // or AGPS.zip uses AID-INI (0x0B 0x01)
for (var i=0;i<bin.length;i+=chunkSize) { eval(initCommands);
var chunk = bin.substr(i,chunkSize);
js += `Serial1.write(atob("${btoa(chunk)}"))\n`; try {
writeChunks(atob(b64), resolve);
} catch (e) {
console.log("error:", e);
reject();
} }
return js; });
}
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) { function CASIC_CHECKSUM(cmd) {
@ -53,23 +64,30 @@ function updateLastUpdate() {
} }
exports.pull = function(successCallback, failureCallback) { exports.pull = function(successCallback, failureCallback) {
let uri = "https://www.espruino.com/agps/casic.base64"; const uri = "https://www.espruino.com/agps/casic.base64";
if (Bangle.http) { if (Bangle.http) {
Bangle.http(uri, {timeout:10000}).then(event => { Bangle.http(uri, {timeout : 10000})
let result = setAGPS(event.resp); .then(event => {
if (result) { setAGPS(event.resp)
.then(r => {
updateLastUpdate(); updateLastUpdate();
if (successCallback) successCallback(); if (successCallback)
} else { successCallback();
console.log("error applying AGPS data"); })
if (failureCallback) failureCallback("Error applying AGPS data"); .catch((e) => {
}
}).catch((e)=>{
console.log("error", e); console.log("error", e);
if (failureCallback) failureCallback(e); if (failureCallback)
failureCallback(e);
});
})
.catch((e) => {
console.log("error", e);
if (failureCallback)
failureCallback(e);
}); });
} else { } else {
console.log("error: No http method found"); console.log("error: No http method found");
if (failureCallback) failureCallback(/*LANG*/"No http method"); if (failureCallback)
failureCallback(/*LANG*/ "No http method");
} }
}; };

View File

@ -2,7 +2,7 @@
"name": "A-GPS Data Downloader App", "name": "A-GPS Data Downloader App",
"shortName":"A-GPS Data", "shortName":"A-GPS Data",
"icon": "agpsdata.png", "icon": "agpsdata.png",
"version":"0.03", "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.", "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", "tags": "boot,tool,assisted,gps,agps,http",
"allow_emulator":true, "allow_emulator":true,

View File

@ -1,2 +1,4 @@
0.01: New app! 0.01: New app!
0.02: Design improvements and fixes. 0.02: Design improvements and fixes.
0.03: Indicate battery level through line occurrence.
0.04: Use widget_utils module.

View File

@ -8,14 +8,16 @@ The original output of stable diffusion is shown here:
![](orig.png) ![](orig.png)
And my implementation is shown here: My implementation is shown below. Note that horizontal lines occur randomly, but the
probability is correlated with the battery level. So if your screen contains only
a few lines its time to charge your bangle again ;)
![](impl.png) ![](impl.png)
# Thanks to # Thanks to
The great open source community: I used an open source diffusion model (https://github.com/CompVis/stable-diffusion) 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. to generate a watch face for the open-source smartwatch BangleJs.
## Creator ## Creator
- [David Peer](https://github.com/peerdavid). - [David Peer](https://github.com/peerdavid).

View File

@ -37,8 +37,15 @@ function drawBackground() {
g.setFontAlign(0,0); g.setFontAlign(0,0);
g.setColor(g.theme.fg); g.setColor(g.theme.fg);
y = 0; var bat = E.getBattery() / 100.0;
var y = 0;
while(y < H){ 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); y += 3 + Math.floor(Math.random() * 10);
g.drawLine(0, y, W, y); g.drawLine(0, y, W, y);
g.drawLine(0, y+1, W, y+1); g.drawLine(0, y+1, W, y+1);
@ -103,7 +110,7 @@ function drawDate(){
g.setFontAlign(0,0); g.setFontAlign(0,0);
g.setFontGochiHand(); g.setFontGochiHand();
var text = ("0"+date.getDate()).substr(-2) + "/" + ("0"+date.getMonth()).substr(-2); var text = ("0"+date.getDate()).substr(-2) + "/" + ("0"+(date.getMonth()+1)).substr(-2);
var w = g.stringWidth(text); var w = g.stringWidth(text);
g.setColor(g.theme.bg); g.setColor(g.theme.bg);
g.fillRect(cx-w/2-4, 20, cx+w/2+4, 40+12); g.fillRect(cx-w/2-4, 20, cx+w/2+4, 40+12);
@ -208,8 +215,7 @@ Bangle.loadWidgets();
* so we will blank out the draw() functions of each widget and change the * so we will blank out the draw() functions of each widget and change the
* area to the top bar doesn't get cleared. * area to the top bar doesn't get cleared.
*/ */
for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";} require('widget_utils').hide();
// Clear the screen once, at startup and draw clock // Clear the screen once, at startup and draw clock
g.setTheme({bg:"#fff",fg:"#000",dark:false}).clear(); g.setTheme({bg:"#fff",fg:"#000",dark:false}).clear();
draw(); draw();

View File

@ -3,7 +3,7 @@
"name": "AI Clock", "name": "AI Clock",
"shortName":"AI Clock", "shortName":"AI Clock",
"icon": "aiclock.png", "icon": "aiclock.png",
"version":"0.02", "version":"0.04",
"readme": "README.md", "readme": "README.md",
"supports": ["BANGLEJS2"], "supports": ["BANGLEJS2"],
"description": "A watch face that was designed by an AI (stable diffusion) and implemented by a human.", "description": "A watch face that was designed by an AI (stable diffusion) and implemented by a human.",

View File

@ -34,4 +34,6 @@
0.32: Fix wrong hidden filter 0.32: Fix wrong hidden filter
Add option for auto-delete a timer after it expires Add option for auto-delete a timer after it expires
0.33: Allow hiding timers&alarms 0.33: Allow hiding timers&alarms
0.34: Add "Confirm" option to alarm/timer edit menus
0.35: Add automatic translation of more strings

View File

@ -128,7 +128,12 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex) {
value: alarm.hidden || false, value: alarm.hidden || false,
onchange: v => alarm.hidden = v onchange: v => alarm.hidden = v
}, },
/*LANG*/"Cancel": () => showMainMenu() /*LANG*/"Cancel": () => showMainMenu(),
/*LANG*/"Confirm": () => {
prepareAlarmForSave(alarm, alarmIndex, time);
saveAndReload();
showMainMenu();
}
}; };
if (!isNew) { if (!isNew) {
@ -178,7 +183,7 @@ function decodeDOW(alarm) {
.map((day, index) => alarm.dow & (1 << (index + firstDayOfWeek)) ? day : "_") .map((day, index) => alarm.dow & (1 << (index + firstDayOfWeek)) ? day : "_")
.join("") .join("")
.toLowerCase() .toLowerCase()
: "Once" : /*LANG*/"Once"
} }
function showEditRepeatMenu(repeat, dow, dowChangeCallback) { function showEditRepeatMenu(repeat, dow, dowChangeCallback) {
@ -293,7 +298,12 @@ function showEditTimerMenu(selectedTimer, timerIndex) {
onchange: v => timer.hidden = v onchange: v => timer.hidden = v
}, },
/*LANG*/"Vibrate": require("buzz_menu").pattern(timer.vibrate, v => timer.vibrate = 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) { if (!isNew) {

View File

@ -2,7 +2,7 @@
"id": "alarm", "id": "alarm",
"name": "Alarms & Timers", "name": "Alarms & Timers",
"shortName": "Alarms", "shortName": "Alarms",
"version": "0.33", "version": "0.35",
"description": "Set alarms and timers on your Bangle", "description": "Set alarms and timers on your Bangle",
"icon": "app.png", "icon": "app.png",
"tags": "tool,alarm,widget", "tags": "tool,alarm,widget",

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)) if (isNaN(m.heading))
compass_heading = "---"; compass_heading = "---";
else else
compass_heading = 360 - Math.round(m.heading); compass_heading = Math.round(m.heading);
current_colour = g.getColor(); current_colour = g.getColor();
g.reset(); g.reset();
g.setColor(background_colour); g.setColor(background_colour);

View File

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

View File

@ -14,3 +14,6 @@
0.14: Fix timeout of http function not being cleaned up 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.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.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)

View File

@ -20,6 +20,8 @@ It contains:
of Gadgetbridge - making your phone make noise so you can find it. of Gadgetbridge - making your phone make noise so you can find it.
* `Keep Msgs` - default is `Off`. When Gadgetbridge disconnects, should Bangle.js * `Keep Msgs` - default is `Off`. When Gadgetbridge disconnects, should Bangle.js
keep any messages it has received, or should it delete them? 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 * `Messages` - launches the messages app, showing a list of messages
## How it works ## How it works

View File

@ -91,10 +91,6 @@
sched.reload(); sched.reload();
}, },
//TODO perhaps move those in a library (like messages), used also for viewing events? //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) //add and remove events based on activity on phone (pebble-like)
"calendar" : function() { "calendar" : function() {
var cal = require("Storage").readJSON("android.calendar.json",true); var cal = require("Storage").readJSON("android.calendar.json",true);
@ -109,7 +105,7 @@
"calendar-" : function() { "calendar-" : function() {
var cal = require("Storage").readJSON("android.calendar.json",true); var cal = require("Storage").readJSON("android.calendar.json",true);
//if any of those happen we are out of sync! //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); cal = cal.filter(e=>e.id!=event.id);
require("Storage").writeJSON("android.calendar.json", cal); require("Storage").writeJSON("android.calendar.json", cal);
}, },
@ -130,6 +126,18 @@
request.j(event.err); //r = reJect function request.j(event.err); //r = reJect function
else else
request.r(event); //r = resolve function 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]; var h = HANDLERS[event.t];
@ -170,7 +178,10 @@
// Battery monitor // Battery monitor
function sendBattery() { gbSend({ t: "status", bat: E.getBattery(), chg: Bangle.isCharging()?1:0 }); } 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); Bangle.on("charging", sendBattery);
if (!settings.keep) if (!settings.keep)
NRF.on("disconnect", () => require("messages").clearAll()); // remove all messages on disconnect NRF.on("disconnect", () => require("messages").clearAll()); // remove all messages on disconnect
@ -190,6 +201,30 @@
if (isFinite(msg.id)) return gbSend({ t: "notify", n:response?"OPEN":"DISMISS", id: msg.id }); if (isFinite(msg.id)) return gbSend({ t: "notify", n:response?"OPEN":"DISMISS", id: msg.id });
// error/warn here? // 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 // remove settings object so it's not taking up RAM
delete settings; delete settings;
})(); })();

View File

@ -2,11 +2,11 @@
"id": "android", "id": "android",
"name": "Android Integration", "name": "Android Integration",
"shortName": "Android", "shortName": "Android",
"version": "0.16", "version": "0.18",
"description": "Display notifications/music/etc sent from the Gadgetbridge app on Android. This replaces the old 'Gadgetbridge' Bangle.js widget.", "description": "Display notifications/music/etc sent from the Gadgetbridge app on Android. This replaces the old 'Gadgetbridge' Bangle.js widget.",
"icon": "app.png", "icon": "app.png",
"tags": "tool,system,messages,notifications,gadgetbridge", "tags": "tool,system,messages,notifications,gadgetbridge",
"dependencies": {"messages":"app"}, "dependencies": {"messages":"module"},
"supports": ["BANGLEJS","BANGLEJS2"], "supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md", "readme": "README.md",
"storage": [ "storage": [

View File

@ -1,4 +1,7 @@
(function(back) { (function(back) {
function gb(j) { function gb(j) {
Bluetooth.println(JSON.stringify(j)); Bluetooth.println(JSON.stringify(j));
} }
@ -23,7 +26,17 @@
updateSettings(); 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); E.showMenu(mainmenu);
}) })

View File

@ -12,3 +12,5 @@
0.08: fixed calendar weeknumber not shortened to two digits 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.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", "id": "antonclk",
"name": "Anton Clock", "name": "Anton Clock",
"version": "0.10", "version": "0.11",
"description": "A clock using the bold Anton font, optionally showing seconds and date in ISO-8601 format.", "description": "A simple clock using the bold Anton font. See `Anton Clock Plus` for an enhanced version",
"readme":"README.md",
"icon": "app.png", "icon": "app.png",
"screenshots": [{"url":"screenshot.png"}], "screenshots": [{"url":"screenshot.png"}],
"type": "clock", "type": "clock",
@ -12,8 +11,6 @@
"allow_emulator": true, "allow_emulator": true,
"storage": [ "storage": [
{"name":"antonclk.app.js","url":"app.js"}, {"name":"antonclk.app.js","url":"app.js"},
{"name":"antonclk.settings.js","url":"settings.js"},
{"name":"antonclk.img","url":"app-icon.js","evaluate":true} {"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 ## Features
@ -16,16 +16,16 @@ The basic time representation only shows hours and minutes of the current time.
## Usage ## Usage
Install Anton clock through the Bangle.js app loader. * Install Anton Clock Plus through the Bangle.js app loader.
Configure it through the default Bangle.js configuration mechanism * Configure it through the default Bangle.js configuration mechanism
(Settings app, "Apps" menu, "Anton clock" submenu). (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"). (Settings app, "System" menu, "Clock" submenu, select "Anton clock").
## Configuration ## Configuration
Anton clock is configured by the standard settings mechanism of Bangle.js's operating system: 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. 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. You configure Anton clock through several "on/off" switches in two menus.
### The main menu ### 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", "version": "0.08",
"description": "A clock based on the portal series", "description": "A clock based on the portal series",
"icon": "app.png", "icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
"type": "clock", "type": "clock",
"tags": "clock", "tags": "clock",
"supports": ["BANGLEJS2"], "supports": ["BANGLEJS2"],

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@ -2,3 +2,4 @@
0.02: Fixed Whirlpool galaxy RA/DA, larger compass display, fixed moonphase overlapping battery widget 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.03: Update to use Bangle.setUI instead of setWatch
0.04: Tell clock widgets to hide. 0.04: Tell clock widgets to hide.
0.05: Added adjustment for Bangle.js magnetometer heading fix

View File

@ -834,7 +834,7 @@ Bangle.on('mag', function (m) {
if (isNaN(m.heading)) if (isNaN(m.heading))
compass_heading = "---"; compass_heading = "---";
else else
compass_heading = 360 - Math.round(m.heading); compass_heading = Math.round(m.heading);
// g.setColor("#000000"); // g.setColor("#000000");
// g.fillRect(160, 10, 160, 20); // g.fillRect(160, 10, 160, 20);
g.setColor(display_colour); g.setColor(display_colour);

View File

@ -1,7 +1,7 @@
{ {
"id": "astral", "id": "astral",
"name": "Astral Clock", "name": "Astral Clock",
"version": "0.04", "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.", "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", "icon": "app-icon.png",
"type": "clock", "type": "clock",

View File

@ -13,3 +13,4 @@
0.13: Add font setting 0.13: Add font setting
0.14: Use ClockFace_menu.addItems 0.14: Use ClockFace_menu.addItems
0.15: Add Power saving option 0.15: Add Power saving option
0.16: Support Fast Loading

View File

@ -1,4 +1,5 @@
/* jshint esversion: 6 */ /* jshint esversion: 6 */
{
/** /**
* A simple digital clock showing seconds as a bar * A simple digital clock showing seconds as a bar
**/ **/
@ -14,7 +15,7 @@ let locale = require("locale");
} }
let barW = 0, prevX = 0; let barW = 0, prevX = 0;
function renderBar(l) { const renderBar = function (l) {
"ram"; "ram";
if (l) prevX = 0; // called from Layout: drawing area was cleared if (l) prevX = 0; // called from Layout: drawing area was cleared
else l = clock.layout.bar; else l = clock.layout.bar;
@ -27,7 +28,7 @@ function renderBar(l) {
prevX = x2; prevX = x2;
} }
function timeText(date) { const timeText = function(date) {
if (!clock.is12Hour) { if (!clock.is12Hour) {
return locale.time(date, true); return locale.time(date, true);
} }
@ -40,16 +41,14 @@ function timeText(date) {
} }
return locale.time(date12, true); return locale.time(date12, true);
} }
function ampmText(date) { const ampmText = date => (clock.is12Hour && locale.hasMeridian) ? locale.meridian(date) : "";
return (clock.is12Hour && locale.hasMeridian) ? locale.meridian(date) : ""; const dateText = date => {
}
function dateText(date) {
const dayName = locale.dow(date, true), const dayName = locale.dow(date, true),
month = locale.month(date, true), month = locale.month(date, true),
day = date.getDate(); day = date.getDate();
const dayMonth = locale.dayFirst ? `${day} ${month}` : `${month} ${day}`; const dayMonth = locale.dayFirst ? `${day} ${month}` : `${month} ${day}`;
return `${dayName} ${dayMonth}`; return `${dayName} ${dayMonth}`;
} };
const ClockFace = require("ClockFace"), const ClockFace = require("ClockFace"),
clock = new ClockFace({ clock = new ClockFace({
@ -110,15 +109,20 @@ const ClockFace = require("ClockFace"),
prevX = 0; // force redraw of bar prevX = 0; // force redraw of bar
this.layout.forgetLazyState(); this.layout.forgetLazyState();
}, },
remove: function() {
if (this.onLock) Bangle.removeListener("lock", this.onLock);
},
}); });
// power saving: only update once a minute while locked, hide bar // power saving: only update once a minute while locked, hide bar
if (clock.powerSave) { if (clock.powerSave) {
Bangle.on("lock", lock => { clock.onLock = lock => {
clock.precision = lock ? 60 : 1; clock.precision = lock ? 60 : 1;
clock.tick(); clock.tick();
renderBar(); // hide/redraw bar right away renderBar(); // hide/redraw bar right away
}); }
Bangle.on("lock", clock.onLock);
} }
clock.start(); clock.start();
}

View File

@ -1,7 +1,7 @@
{ {
"id": "barclock", "id": "barclock",
"name": "Bar Clock", "name": "Bar Clock",
"version": "0.15", "version": "0.16",
"description": "A simple digital clock showing seconds as a bar", "description": "A simple digital clock showing seconds as a bar",
"icon": "clock-bar.png", "icon": "clock-bar.png",
"screenshots": [{"url":"screenshot.png"},{"url":"screenshot_pm.png"}], "screenshots": [{"url":"screenshot.png"},{"url":"screenshot_pm.png"}],

View File

@ -1,3 +1,4 @@
0.01: Display pressure as number and hand 0.01: Display pressure as number and hand
0.02: Use theme color 0.02: Use theme color
0.03: workaround for some firmwares that return 'undefined' for first call to barometer 0.03: workaround for some firmwares that return 'undefined' for first call to barometer
0.04: Update every second, go back with short button press

View File

@ -59,6 +59,7 @@ function drawTicks(){
function drawScaleLabels(){ function drawScaleLabels(){
g.setColor(g.theme.fg); g.setColor(g.theme.fg);
g.setFont("Vector",12); g.setFont("Vector",12);
g.setFontAlign(-1,-1);
let label = MIN; let label = MIN;
for (let i=0;i <= NUMBER_OF_LABELS; i++){ for (let i=0;i <= NUMBER_OF_LABELS; i++){
@ -103,22 +104,29 @@ function drawIcons() {
} }
g.setBgColor(g.theme.bg); g.setBgColor(g.theme.bg);
try {
function baroHandler(data) {
g.clear(); g.clear();
drawTicks(); drawTicks();
drawScaleLabels(); drawScaleLabels();
drawIcons(); drawIcons();
if (data!==undefined) {
try {
function baroHandler(data) {
if (data===undefined) // workaround for https://github.com/espruino/BangleApps/issues/1429
setTimeout(() => Bangle.getPressure().then(baroHandler), 500);
else
drawHand(Math.round(data.pressure)); drawHand(Math.round(data.pressure));
} }
}
Bangle.getPressure().then(baroHandler); Bangle.getPressure().then(baroHandler);
setInterval(() => Bangle.getPressure().then(baroHandler), 1000);
} catch(e) { } catch(e) {
if (e !== undefined) {
print(e.message); print(e.message);
print("barometer not supporter, show a demo value"); }
print("barometer not supported, show a demo value");
drawHand(MIN); drawHand(MIN);
} }
Bangle.setUI({
mode : "custom",
back : function() {load();}
});

View File

@ -1,7 +1,7 @@
{ "id": "barometer", { "id": "barometer",
"name": "Barometer", "name": "Barometer",
"shortName":"Barometer", "shortName":"Barometer",
"version":"0.03", "version":"0.04",
"description": "A simple barometer that displays the current air pressure", "description": "A simple barometer that displays the current air pressure",
"icon": "barometer.png", "icon": "barometer.png",
"tags": "tool,outdoors", "tags": "tool,outdoors",

1
apps/barwatch/ChangeLog Normal file
View File

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

5
apps/barwatch/README.md Normal file
View File

@ -0,0 +1,5 @@
# BarWatch - an experimental watch
For too long the watches have shown the time with digits or hands. No more!
With this stylish watch the time is represented by bars. Up to 24 as the day goes by.
Practical? Not really, but a different look!

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("l0uwkE/4A/AH4A/AB0gicQmUB+EPgEigExh8gj8A+ECAgMQn4WCgcACyotWC34W/C34W/CycACw0wgYWFBYIWCAAc/+YGHCAgNFACkxl8hGYwAMLYUvCykQC34WycoIW/C34W0gAWTmUjkUzkbmSAFY="))

76
apps/barwatch/app.js Normal file
View File

@ -0,0 +1,76 @@
// timeout used to update every minute
var drawTimeout;
// schedule a draw for the next minute
function queueDraw() {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = setTimeout(function() {
drawTimeout = undefined;
draw();
}, 60000 - (Date.now() % 60000));
}
function draw() {
g.reset();
if(g.theme.dark){
g.setColor(1,1,1);
}else{
g.setColor(0,0,0);
}
// work out how to display the current time
var d = new Date();
var h = d.getHours(), m = d.getMinutes();
// hour bars
var bx_offset = 10, by_offset = 35;
var b_width = 8, b_height = 60;
var b_space = 5;
for(var i=0; i<h; i++){
if(i > 11){
by_offset = 105;
}
var iter = i % 12;
//console.log(iter);
g.fillRect(bx_offset+(b_width*(iter+1))+(b_space*iter),
by_offset,
bx_offset+(b_width*iter)+(b_space*iter),
by_offset+b_height);
}
// minute bar
if(h > 11){
by_offset = 105;
}
var m_bar = h % 12;
if(m != 0){
g.fillRect(bx_offset+(b_width*(m_bar+1))+(b_space*m_bar),
by_offset+b_height-m,
bx_offset+(b_width*m_bar)+(b_space*m_bar),
by_offset+b_height);
}
// queue draw in one minute
queueDraw();
}
// Clear the screen once, at startup
g.clear();
// draw immediately at first
draw();
// Stop updates when LCD is off, restart when on
Bangle.on('lcdPower',on=>{
if (on) {
draw(); // draw immediately, queue redraw
} else { // stop draw timer
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
}
});
Bangle.setUI("clock");
Bangle.loadWidgets();
Bangle.drawWidgets();

BIN
apps/barwatch/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 973 B

View File

@ -0,0 +1,18 @@
{
"id": "barwatch",
"name": "BarWatch",
"shortName":"BarWatch",
"version":"0.01",
"description": "A watch that displays the time using bars. One bar for each hour.",
"readme": "README.md",
"icon": "screenshot.png",
"tags": "clock",
"type": "clock",
"allow_emulator":true,
"screenshots" : [ { "url": "screenshot.png" } ],
"supports" : ["BANGLEJS2"],
"storage": [
{"name":"barwatch.app.js","url":"app.js"},
{"name":"barwatch.img","url":"app-icon.js","evaluate":true}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

3
apps/beer/ChangeLog Normal file
View File

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

View File

@ -1 +1 @@
require("heatshrink").decompress(atob("mEwghC/AB0O/4AG8AXNgYXHmAXl94XH+AXNn4XH/wXW+YX/C6oWHAAIXN7sz9vdAAoXN9sznvuAAXf/vuC53jC4Xd7wXQ93jn3u9vv9vt7wXT/4tBAgIXQ7wvCC4PgC5sO6czIQJfBC6PumaPDC6wwCC50NYAJcBVgIDBCxrAFbgYXP7yoDF6TADL4YXPVAIXCRyAXC7wXW9zwBC6cNC9zABC4gWQC653CR4fQC6x3TF6gXXI4M9d6wAEC9EN73dAAZfQgczAAkwC/4XXAH4")) require("heatshrink").decompress(atob("mEw4cA///wH9/++1P+u3//3/qv/gv+KHkJkmABxcBBwNJkmQCJYOByQCCCBUCCItJkARQkgQHggLBku25IRDJQ4LCtu27Mt2RKJCInbAQIRLpYROglt24OB6wSC7dwLQ4LB9u2EgfbsARJ8u2mwRO+u3CNJtHCJFpCINALJoRCpCiGBoMSdQcpegIRGyaPB+QRDkARIyQRBc4YRKyet23iCJxHB6QRBzOJCJ+dCJY1CpfMGphrCp2YNZlL54CBEZgLBAQoRBiTFFCNMvmQRPndiEcJHEyQQECJMpAYIRQyARQwAROI4IAGB4wCBNAoRmhIRHCA4A/AAo"))

View File

@ -127,6 +127,8 @@
var img_nofix = require("heatshrink").decompress(atob("mUyxH+ACYhJDygtYGsqLVF8u02gziGBoyhQ5gwDGRozRGCQydGCgybGCwyZC5gAaGPQwnGRAwpGQ4xwGFYyFDKsrlYxYDCsBmUyg4yXLyUsFwMyq1WAgUsNCRjUmVXroAEq8yMbcllkskwCEkplDmQwDq0sC54xEHQ9RqQAGqIwCFgOBAASYBSgMBltRAA0sgJsOGJeBxAAGwMrgIXIloxOJYNSvl8CwIDCqMBlYxNC4wxQDIOCwVYDIIDBGJ9YwV8rADBwRJCSqAVCAYaVMC4oxCPYYxQSo4xMSpIxPY4T5HY54XIMbIxKgwXKfKjhEllWGJNWlgXJGLNXruCGI+CrtXGKP+GJB9HMZ6VO/wxJcI8lfJclfKAxKfJEAGJIXLGKSvBWYQZCMZbfEqTHBGJYyFfIo1DGJ4tDGJQwCGJB9IMZyVNGIYyEfJQxPfJgwEMgoZJgAxMltRAA0tGJQyEksslkmAQklGINXxDTBFwIDCq8rC4YACC4gwJMowAJldWAAwwBABowIGJ4AYGJIymGBQylGBgyjGBwyhGCAzeF6YycGCwzYF7IzVF7o1PDqYA==")); var img_nofix = require("heatshrink").decompress(atob("mUyxH+ACYhJDygtYGsqLVF8u02gziGBoyhQ5gwDGRozRGCQydGCgybGCwyZC5gAaGPQwnGRAwpGQ4xwGFYyFDKsrlYxYDCsBmUyg4yXLyUsFwMyq1WAgUsNCRjUmVXroAEq8yMbcllkskwCEkplDmQwDq0sC54xEHQ9RqQAGqIwCFgOBAASYBSgMBltRAA0sgJsOGJeBxAAGwMrgIXIloxOJYNSvl8CwIDCqMBlYxNC4wxQDIOCwVYDIIDBGJ9YwV8rADBwRJCSqAVCAYaVMC4oxCPYYxQSo4xMSpIxPY4T5HY54XIMbIxKgwXKfKjhEllWGJNWlgXJGLNXruCGI+CrtXGKP+GJB9HMZ6VO/wxJcI8lfJclfKAxKfJEAGJIXLGKSvBWYQZCMZbfEqTHBGJYyFfIo1DGJ4tDGJQwCGJB9IMZyVNGIYyEfJQxPfJgwEMgoZJgAxMltRAA0tGJQyEksslkmAQklGINXxDTBFwIDCq8rC4YACC4gwJMowAJldWAAwwBABowIGJ4AYGJIymGBQylGBgyjGBwyhGCAzeF6YycGCwzYF7IzVF7o1PDqYA=="));
var img_fix = require("heatshrink").decompress(atob("mUyxH+ACYhJDygtYGsqLVF94zaDYkq6wAOlQyYJo2A63VAAIoC2m0GI16My5/H5/V64ABGQIwBGQ+rTKwWHkhiBGIYwDGQ3VZioVIqoiBGAJhEGRFPGSYTIYwQxCGA4yFqodJGKeqSgQwJGQmkGKQSJfAYwLGQfPDxQwRgHVfAi/EAA4xLGQwRLYwb5BABoxQCBcA43G5wABAgIAMEBgxQ0QxB54xB5gAG4xgBBYOiGJ4PMGInPGIhcCGIt4EJoxPvHM5oxBGAnO6xrCGoXMqgxdpwxD5qQFL4QADlQxdgAhBGILIDMYoADEBwwPgCHBfQzHDAAb4NACTIIAA74OACLIIMo7GOACQoBZAoHBHQPNA4QwggGiZBA5B54HBY0DIKMYtUGMMqFYLIGY4jGhZAr6FAAYwiZAgxIY0TIFfQgADvAfR/zISGJTGR/wxRkj6CGJBiSGKL6DGP4xOGSKVDGAwxRGAQxU5oxcGR75DGJEkGCYxPlXM5vPGA/MlQxUGR1OGIL4I5lOGCgyOqgxBShHMqgwVGJt4GJd4GKwyMvHG5vGABAxMGBQyM1mtABWsGC4yLGBYABGDAyKGKwwQGZKVUF6b/OABowWGbAvZGaovdGp4dTA")); var img_fix = require("heatshrink").decompress(atob("mUyxH+ACYhJDygtYGsqLVF94zaDYkq6wAOlQyYJo2A63VAAIoC2m0GI16My5/H5/V64ABGQIwBGQ+rTKwWHkhiBGIYwDGQ3VZioVIqoiBGAJhEGRFPGSYTIYwQxCGA4yFqodJGKeqSgQwJGQmkGKQSJfAYwLGQfPDxQwRgHVfAi/EAA4xLGQwRLYwb5BABoxQCBcA43G5wABAgIAMEBgxQ0QxB54xB5gAG4xgBBYOiGJ4PMGInPGIhcCGIt4EJoxPvHM5oxBGAnO6xrCGoXMqgxdpwxD5qQFL4QADlQxdgAhBGILIDMYoADEBwwPgCHBfQzHDAAb4NACTIIAA74OACLIIMo7GOACQoBZAoHBHQPNA4QwggGiZBA5B54HBY0DIKMYtUGMMqFYLIGY4jGhZAr6FAAYwiZAgxIY0TIFfQgADvAfR/zISGJTGR/wxRkj6CGJBiSGKL6DGP4xOGSKVDGAwxRGAQxU5oxcGR75DGJEkGCYxPlXM5vPGA/MlQxUGR1OGIL4I5lOGCgyOqgxBShHMqgwVGJt4GJd4GKwyMvHG5vGABAxMGBQyM1mtABWsGC4yLGBYABGDAyKGKwwQGZKVUF6b/OABowWGbAvZGaovdGp4dTA"));
var W = g.getWidth(), H = g.getHeight();
// https://github.com/Leaflet/Leaflet/blob/master/src/geo/projection/Projection.SphericalMercator.js // https://github.com/Leaflet/Leaflet/blob/master/src/geo/projection/Projection.SphericalMercator.js
function project(latlong) { function project(latlong) {
var d = Math.PI / 180, var d = Math.PI / 180,
@ -170,32 +172,30 @@ Bangle.on('GPS', function(f) {
Bangle.on('mag', function(m) { Bangle.on('mag', function(m) {
if (!Bangle.isLCDOn()) return; if (!Bangle.isLCDOn()) return;
var headingrad = m.heading*Math.PI/180; // in radians var headingrad = (360-m.heading)*Math.PI/180; // in radians
if (!isFinite(headingrad)) headingrad=0; if (!isFinite(headingrad)) headingrad=0;
if (nearest) if (nearest)
g.drawImage(img_fix,120,120,{ g.drawImage(img_fix,W/2,H/2,{
rotate: (Math.PI/2)+headingrad-nearestangle, rotate: (Math.PI/2)+headingrad-nearestangle,
scale:3, scale:3,
}); });
else else
g.drawImage(img_nofix,120,120,{ g.drawImage(img_nofix,W/2,H/2,{
rotate: headingrad, rotate: headingrad,
scale:2, scale:2,
}); });
g.clearRect(60,0,180,24); g.clearRect(0,0,W,24).setFontAlign(0,0).setFont("6x8");
g.setFontAlign(0,0);
g.setFont("6x8");
if (fix.fix) { if (fix.fix) {
g.drawString(nearest ? nearest.name : "---",120,4); g.drawString(nearest ? nearest.name : "---",W/2,4);
g.setFont("6x8",2); g.setFont("6x8",2);
g.drawString(nearest ? Math.round(nearestdist)+"m" : "---",120,16); g.drawString(nearest ? Math.round(nearestdist)+"m" : "---",W/2,16);
} else { } else {
g.drawString(fix.satellites+" satellites",120,4); g.drawString(fix.satellites+" satellites",W/2,4);
} }
}); });
Bangle.setCompassPower(1); Bangle.setCompassPower(1);
Bangle.setGPSPower(1); Bangle.setGPSPower(1);
g.clear();`; g.setColor("#fff").setBgColor("#000").clear();`;
sendCustomizedApp({ sendCustomizedApp({
storage:[ storage:[

View File

@ -1,11 +1,11 @@
{ {
"id": "beer", "id": "beer",
"name": "Beer Compass", "name": "Beer Compass",
"version": "0.01", "version": "0.02",
"description": "Uploads all the pubs in an area onto your watch, so it can always point you at the nearest one", "description": "Uploads all the pubs in an area onto your watch, so it can always point you at the nearest one",
"icon": "app.png", "icon": "app.png",
"tags": "", "tags": "",
"supports": ["BANGLEJS"], "supports": ["BANGLEJS","BANGLEJS2"],
"custom": "custom.html", "custom": "custom.html",
"storage": [ "storage": [
{"name":"beer.app.js"}, {"name":"beer.app.js"},

View File

@ -53,3 +53,11 @@
0.47: Add polyfill for setUI with an object as an argument (fix regression for 2v12 devices after Layout module changed) 0.47: Add polyfill for setUI with an object as an argument (fix regression for 2v12 devices after Layout module changed)
0.48: Workaround for BTHRM issues on Bangle.js 1 (write .boot files in chunks) 0.48: Workaround for BTHRM issues on Bangle.js 1 (write .boot files in chunks)
0.49: Store first found clock as a setting to speed up further boots 0.49: Store first found clock as a setting to speed up further boots
0.50: Allow setting of screen rotation
Remove support for 2v11 and earlier firmware
0.51: Remove patches for 2v10 firmware (BEEPSET and setUI)
Add patch to ensure that compass heading is corrected on pre-2v15.68 firmware
Ensure clock is only fast-loaded if it doesn't contain widgets
0.52: Ensure heading patch for pre-2v15.68 firmware applies to getCompass
0.53: Add polyfills for pre-2v15.135 firmware for Bangle.load and Bangle.showClock
0.54: Fix for invalid version comparison in polyfill

View File

@ -1,5 +1,10 @@
// This runs after a 'fresh' boot // This runs after a 'fresh' boot
var s = require("Storage").readJSON("setting.json",1)||{}; var s = require("Storage").readJSON("setting.json",1)||{};
/* If were being called from JS code in order to load the clock quickly (eg from a launcher)
and the clock in question doesn't have widgets, force a normal 'load' as this will then
reset everything and remove the widgets. */
if (global.__FILE__ && !s.clockHasWidgets) {load();throw "Clock has no widgets, can't fast load";}
// Otherwise continue to try and load the clock
var _clkApp = require("Storage").read(s.clock); var _clkApp = require("Storage").read(s.clock);
if (!_clkApp) { if (!_clkApp) {
_clkApp = require("Storage").list(/\.info$/) _clkApp = require("Storage").list(/\.info$/)
@ -14,6 +19,7 @@ if (!_clkApp) {
if (_clkApp){ if (_clkApp){
s.clock = _clkApp.src; s.clock = _clkApp.src;
_clkApp = require("Storage").read(_clkApp.src); _clkApp = require("Storage").read(_clkApp.src);
s.clockHasWidgets = _clkApp.includes("Bangle.loadWidgets");
require("Storage").writeJSON("setting.json", s); require("Storage").writeJSON("setting.json", s);
} }
} }

View File

@ -4,6 +4,7 @@ of the time. */
E.showMessage(/*LANG*/"Updating boot0..."); E.showMessage(/*LANG*/"Updating boot0...");
var s = require('Storage').readJSON('setting.json',1)||{}; var s = require('Storage').readJSON('setting.json',1)||{};
var BANGLEJS2 = process.env.HWVERSION==2; // Is Bangle.js 2 var BANGLEJS2 = process.env.HWVERSION==2; // Is Bangle.js 2
var FWVERSION = parseFloat(process.env.VERSION.replace("v","").replace(/\.(\d\d)$/,".0$1"));
var boot = "", bootPost = ""; var boot = "", bootPost = "";
if (require('Storage').hash) { // new in 2v11 - helps ensure files haven't changed if (require('Storage').hash) { // new in 2v11 - helps ensure files haven't changed
var CRC = E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\.boot\.js/)+E.CRC32(process.env.GIT_COMMIT); var CRC = E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\.boot\.js/)+E.CRC32(process.env.GIT_COMMIT);
@ -62,23 +63,6 @@ if (s.ble===false) boot += `if (!NRF.getSecurityStatus().connected) NRF.sleep();
if (s.timeout!==undefined) boot += `Bangle.setLCDTimeout(${s.timeout});\n`; if (s.timeout!==undefined) boot += `Bangle.setLCDTimeout(${s.timeout});\n`;
if (!s.timeout) boot += `Bangle.setLCDPower(1);\n`; if (!s.timeout) boot += `Bangle.setLCDPower(1);\n`;
boot += `E.setTimeZone(${s.timezone});`; boot += `E.setTimeZone(${s.timezone});`;
// Set vibrate, beep, etc IF on older firmwares
if (!Bangle.F_BEEPSET) {
if (!s.vibrate) boot += `Bangle.buzz=Promise.resolve;\n`
if (s.beep===false) boot += `Bangle.beep=Promise.resolve;\n`
else if (s.beep=="vib" && !BANGLEJS2) boot += `Bangle.beep = function (time, freq) {
return new Promise(function(resolve) {
if ((0|freq)<=0) freq=4000;
if ((0|time)<=0) time=200;
if (time>5000) time=5000;
analogWrite(D13,0.1,{freq:freq});
setTimeout(function() {
digitalWrite(D13,0);
resolve();
}, time);
});
};\n`;
}
// Draw out of memory errors onto the screen // Draw out of memory errors onto the screen
boot += `E.on('errorFlag', function(errorFlags) { boot += `E.on('errorFlag', function(errorFlags) {
g.reset(1).setColor("#ff0000").setFont("6x8").setFontAlign(0,1).drawString(errorFlags,g.getWidth()/2,g.getHeight()-1).flip(); g.reset(1).setColor("#ff0000").setFont("6x8").setFontAlign(0,1).drawString(errorFlags,g.getWidth()/2,g.getHeight()-1).flip();
@ -92,76 +76,20 @@ if (s.options) boot+=`Bangle.setOptions(${E.toJS(s.options)});\n`;
if (s.brightness && s.brightness!=1) boot+=`Bangle.setLCDBrightness(${s.brightness});\n`; if (s.brightness && s.brightness!=1) boot+=`Bangle.setLCDBrightness(${s.brightness});\n`;
if (s.passkey!==undefined && s.passkey.length==6) boot+=`NRF.setSecurity({passkey:${E.toJS(s.passkey.toString())}, mitm:1, display:1});\n`; if (s.passkey!==undefined && s.passkey.length==6) boot+=`NRF.setSecurity({passkey:${E.toJS(s.passkey.toString())}, mitm:1, display:1});\n`;
if (s.whitelist) boot+=`NRF.on('connect', function(addr) { if (!(require('Storage').readJSON('setting.json',1)||{}).whitelist.includes(addr)) NRF.disconnect(); });\n`; if (s.whitelist) boot+=`NRF.on('connect', function(addr) { if (!(require('Storage').readJSON('setting.json',1)||{}).whitelist.includes(addr)) NRF.disconnect(); });\n`;
// Pre-2v10 firmwares without a theme/setUI if (s.rotate) boot+=`g.setRotation(${s.rotate&3},${s.rotate>>2});\n` // screen rotation
delete g.theme; // deleting stops us getting confused by our own decl. builtins can't be deleted // ================================================== FIXING OLDER FIRMWARES
if (!g.theme) { if (FWVERSION<215.068) // 2v15.68 and before had compass heading inverted.
boot += `g.theme={fg:-1,bg:0,fg2:-1,bg2:7,fgH:-1,bgH:0x02F7,dark:true};\n`; boot += `Bangle.on('mag',e=>{if(!isNaN(e.heading))e.heading=360-e.heading;});
} Bangle.getCompass=(c=>(()=>{e=c();if(!isNaN(e.heading))e.heading=360-e.heading;return e;}))(Bangle.getCompass);`;
try {
Bangle.setUI({}); // In 2v12.xx we added the option for mode to be an object - for 2v12 and earlier, add a fix if it fails with an object supplied
} catch(e) {
boot += `Bangle._setUI = Bangle.setUI;
Bangle.setUI=function(mode, cb) {
if (Bangle.uiRemove) {
Bangle.uiRemove();
delete Bangle.uiRemove;
}
if ("object"==typeof mode) {
// TODO: handle mode.back?
mode = mode.mode;
}
Bangle._setUI(mode, cb);
};\n`;
}
delete E.showScroller; // deleting stops us getting confused by our own decl. builtins can't be deleted
if (!E.showScroller) { // added in 2v11 - this is a limited functionality polyfill
boot += `E.showScroller = (function(a){function n(){g.reset();b>=l+c&&(c=1+b-l);b<c&&(c=b);g.setColor(g.theme.fg);for(var d=0;d<l;d++){var m=d+c;if(0>m||m>=a.c)break;var f=24+d*a.h;a.draw(m,{x:0,y:f,w:h,h:a.h});d+c==b&&g.setColor(g.theme.fg).drawRect(0,f,h-1,f+a.h-1).drawRect(1,f+1,h-2,f+a.h-2)}g.setColor(c?g.theme.fg:g.theme.bg);g.fillPoly([e,6,e-14,20,e+14,20]);g.setColor(a.c>l+c?g.theme.fg:g.theme.bg);g.fillPoly([e,k-7,e-14,k-21,e+14,k-21])}if(!a)return Bangle.setUI();var b=0,c=0,h=g.getWidth(),
k=g.getHeight(),e=h/2,l=Math.floor((k-48)/a.h);g.reset().clearRect(0,24,h-1,k-1);n();Bangle.setUI("updown",d=>{d?(b+=d,0>b&&(b=a.c-1),b>=a.c&&(b=0),n()):a.select(b)})});\n`;
}
delete g.imageMetrics; // deleting stops us getting confused by our own decl. builtins can't be deleted
if (!g.imageMetrics) { // added in 2v11 - this is a limited functionality polyfill
boot += `Graphics.prototype.imageMetrics=function(src) {
if (src[0]) return {width:src[0],height:src[1]};
else if ('object'==typeof src) return {
width:("width" in src) ? src.width : src.getWidth(),
height:("height" in src) ? src.height : src.getHeight()};
var im = E.toString(src);
return {width:im.charCodeAt(0), height:im.charCodeAt(1)};
};\n`;
}
delete g.stringMetrics; // deleting stops us getting confused by our own decl. builtins can't be deleted
if (!g.stringMetrics) { // added in 2v11 - this is a limited functionality polyfill
boot += `Graphics.prototype.stringMetrics=function(txt) {
txt = txt.toString().split("\\n");
return {width:Math.max.apply(null,txt.map(x=>g.stringWidth(x))), height:this.getFontHeight()*txt.length};
};\n`;
}
delete g.wrapString; // deleting stops us getting confused by our own decl. builtins can't be deleted
if (!g.wrapString) { // added in 2v11 - this is a limited functionality polyfill
boot += `Graphics.prototype.wrapString=function(str, maxWidth) {
var lines = [];
for (var unwrappedLine of str.split("\\n")) {
var words = unwrappedLine.split(" ");
var line = words.shift();
for (var word of words) {
if (g.stringWidth(line + " " + word) > maxWidth) {
lines.push(line);
line = word;
} else {
line += " " + word;
}
}
lines.push(line);
}
return lines;
};\n`;
}
delete Bangle.appRect; // deleting stops us getting confused by our own decl. builtins can't be deleted
if (!Bangle.appRect) { // added in 2v11 - polyfill for older firmwares
boot += `Bangle.appRect = ((y,w,h)=>({x:0,y:0,w:w,h:h,x2:w-1,y2:h-1}))(g.getWidth(),g.getHeight());
(lw=>{ Bangle.loadWidgets = () => { lw(); Bangle.appRect = ((y,w,h)=>({x:0,y:y,w:w,h:h-y,x2:w-1,y2:h-(1+h)}))(global.WIDGETS?24:0,g.getWidth(),g.getHeight()); }; })(Bangle.loadWidgets);\n`;
}
// deleting stops us getting confused by our own decl. builtins can't be deleted
// this is a polyfill without fastloading capability
delete Bangle.showClock;
if (!Bangle.showClock) boot += `Bangle.showClock = ()=>{load(".bootcde")};\n`;
delete Bangle.load;
if (!Bangle.load) boot += `Bangle.load = load;\n`;
// ================================================== BOOT.JS
// Append *.boot.js files // Append *.boot.js files
// These could change bleServices/bleServiceOptions if needed // These could change bleServices/bleServiceOptions if needed
var bootFiles = require('Storage').list(/\.boot\.js$/).sort((a,b)=>{ var bootFiles = require('Storage').list(/\.boot\.js$/).sort((a,b)=>{

View File

@ -1,7 +1,7 @@
{ {
"id": "boot", "id": "boot",
"name": "Bootloader", "name": "Bootloader",
"version": "0.49", "version": "0.54",
"description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings", "description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings",
"icon": "bootloader.png", "icon": "bootloader.png",
"type": "bootloader", "type": "bootloader",

View File

@ -29,3 +29,14 @@
Use default boolean formatter in custom menu and directly apply config if useful Use default boolean formatter in custom menu and directly apply config if useful
Allow recording unmodified internal HR Allow recording unmodified internal HR
Better connection retry handling Better connection retry handling
0.13: Less time used during boot if disabled
0.14: Allow bonding (Debug menu)
Prevent mixing of BT and internal HRM events if both are enabled
Always use a grace period (default 0 ms) to decouple some connection steps
Device not found errors now utilize increasing timeouts
0.15: Fix recording internal sensor
Handle fallback to internal sensor consistently if BT bpm is 0
Power internal sensor down if not needed for fallback
0.16: Set powerdownRequested correctly on BTHRM power on
Additional logging on errors
Add debug option for disabling active scanning

View File

@ -1,633 +1 @@
(function() { if ((require('Storage').readJSON("bthrm.json", true) || {}).enabled != false) require("bthrm").enable();
var settings = Object.assign(
require('Storage').readJSON("bthrm.default.json", true) || {},
require('Storage').readJSON("bthrm.json", true) || {}
);
var log = function(text, param){
if (global.showStatusInfo)
showStatusInfo(text);
if (settings.debuglog){
var logline = new Date().toISOString() + " - " + text;
if (param) logline += ": " + JSON.stringify(param);
print(logline);
}
};
log("Settings: ", settings);
if (settings.enabled){
var clearCache = function() {
return require('Storage').erase("bthrm.cache.json");
};
var getCache = function() {
var cache = require('Storage').readJSON("bthrm.cache.json", true) || {};
if (settings.btid && settings.btid === cache.id) return cache;
clearCache();
return {};
};
var addNotificationHandler = function(characteristic) {
log("Setting notification handler"/*supportedCharacteristics[characteristic.uuid].handler*/);
characteristic.on('characteristicvaluechanged', (ev) => supportedCharacteristics[characteristic.uuid].handler(ev.target.value));
};
var writeCache = function(cache) {
var oldCache = getCache();
if (oldCache !== cache) {
log("Writing cache");
require('Storage').writeJSON("bthrm.cache.json", cache);
} else {
log("No changes, don't write cache");
}
};
var characteristicsToCache = function(characteristics) {
log("Cache characteristics");
var cache = getCache();
if (!cache.characteristics) cache.characteristics = {};
for (var c of characteristics){
//"handle_value":16,"handle_decl":15
log("Saving handle " + c.handle_value + " for characteristic: ", c);
cache.characteristics[c.uuid] = {
"handle": c.handle_value,
"uuid": c.uuid,
"notify": c.properties.notify,
"read": c.properties.read
};
}
writeCache(cache);
};
var characteristicsFromCache = function(device) {
var service = { device : device }; // fake a BluetoothRemoteGATTService
log("Read cached characteristics");
var cache = getCache();
if (!cache.characteristics) return [];
var restored = [];
for (var c in cache.characteristics){
var cached = cache.characteristics[c];
var r = new BluetoothRemoteGATTCharacteristic();
log("Restoring characteristic ", cached);
r.handle_value = cached.handle;
r.uuid = cached.uuid;
r.properties = {};
r.properties.notify = cached.notify;
r.properties.read = cached.read;
r.service = service;
addNotificationHandler(r);
log("Restored characteristic: ", r);
restored.push(r);
}
return restored;
};
log("Start");
var lastReceivedData={
};
var supportedServices = [
"0x180d", // Heart Rate
"0x180f", // Battery
];
var bpmTimeout;
var supportedCharacteristics = {
"0x2a37": {
//Heart rate measurement
active: false,
handler: function (dv){
var flags = dv.getUint8(0);
var bpm = (flags & 1) ? (dv.getUint16(1) / 100 /* ? */ ) : dv.getUint8(1); // 8 or 16 bit
supportedCharacteristics["0x2a37"].active = bpm > 0;
log("BTHRM BPM " + supportedCharacteristics["0x2a37"].active);
if (supportedCharacteristics["0x2a37"].active) stopFallback();
if (bpmTimeout) clearTimeout(bpmTimeout);
bpmTimeout = setTimeout(()=>{
supportedCharacteristics["0x2a37"].active = false;
startFallback();
}, 3000);
var sensorContact;
if (flags & 2){
sensorContact = !!(flags & 4);
}
var idx = 2 + (flags&1);
var energyExpended;
if (flags & 8){
energyExpended = dv.getUint16(idx,1);
idx += 2;
}
var interval;
if (flags & 16) {
interval = [];
var maxIntervalBytes = (dv.byteLength - idx);
log("Found " + (maxIntervalBytes / 2) + " rr data fields");
for(var i = 0 ; i < maxIntervalBytes / 2; i++){
interval[i] = dv.getUint16(idx,1); // in milliseconds
idx += 2;
}
}
var location;
if (lastReceivedData && lastReceivedData["0x180d"] && lastReceivedData["0x180d"]["0x2a38"]){
location = lastReceivedData["0x180d"]["0x2a38"];
}
var battery;
if (lastReceivedData && lastReceivedData["0x180f"] && lastReceivedData["0x180f"]["0x2a19"]){
battery = lastReceivedData["0x180f"]["0x2a19"];
}
if (settings.replace){
var repEvent = {
bpm: bpm,
confidence: (sensorContact || sensorContact === undefined)? 100 : 0,
src: "bthrm"
};
log("Emitting HRM", repEvent);
Bangle.emit("HRM_int", repEvent);
}
var newEvent = {
bpm: bpm
};
if (location) newEvent.location = location;
if (interval) newEvent.rr = interval;
if (energyExpended) newEvent.energy = energyExpended;
if (battery) newEvent.battery = battery;
if (sensorContact) newEvent.contact = sensorContact;
log("Emitting BTHRM", newEvent);
Bangle.emit("BTHRM", newEvent);
}
},
"0x2a38": {
//Body sensor location
handler: function(dv){
if (!lastReceivedData["0x180d"]) lastReceivedData["0x180d"] = {};
lastReceivedData["0x180d"]["0x2a38"] = parseInt(dv.buffer, 10);
}
},
"0x2a19": {
//Battery
handler: function (dv){
if (!lastReceivedData["0x180f"]) lastReceivedData["0x180f"] = {};
lastReceivedData["0x180f"]["0x2a19"] = dv.getUint8(0);
}
}
};
var device;
var gatt;
var characteristics = [];
var blockInit = false;
var currentRetryTimeout;
var initialRetryTime = 40;
var maxRetryTime = 60000;
var retryTime = initialRetryTime;
var connectSettings = {
minInterval: 7.5,
maxInterval: 1500
};
var waitingPromise = function(timeout) {
return new Promise(function(resolve){
log("Start waiting for " + timeout);
setTimeout(()=>{
log("Done waiting for " + timeout);
resolve();
}, timeout);
});
};
if (settings.enabled){
Bangle.isBTHRMActive = function (){
return supportedCharacteristics["0x2a37"].active;
};
Bangle.isBTHRMOn = function(){
return (Bangle._PWR && Bangle._PWR.BTHRM && Bangle._PWR.BTHRM.length > 0);
};
Bangle.isBTHRMConnected = function(){
return gatt && gatt.connected;
};
}
if (settings.replace){
Bangle.origIsHRMOn = Bangle.isHRMOn;
Bangle.isHRMOn = function() {
if (settings.enabled && !settings.replace){
return Bangle.origIsHRMOn();
} else if (settings.enabled && settings.replace){
return Bangle.isBTHRMOn();
}
return Bangle.origIsHRMOn() || Bangle.isBTHRMOn();
};
}
var clearRetryTimeout = function(resetTime) {
if (currentRetryTimeout){
log("Clearing timeout " + currentRetryTimeout);
clearTimeout(currentRetryTimeout);
currentRetryTimeout = undefined;
}
if (resetTime) {
log("Resetting retry time");
retryTime = initialRetryTime;
}
};
var retry = function() {
log("Retry");
if (!currentRetryTimeout){
var clampedTime = retryTime < 100 ? 100 : retryTime;
log("Set timeout for retry as " + clampedTime);
clearRetryTimeout();
currentRetryTimeout = setTimeout(() => {
log("Retrying");
currentRetryTimeout = undefined;
initBt();
}, clampedTime);
retryTime = Math.pow(clampedTime, 1.1);
if (retryTime > maxRetryTime){
retryTime = maxRetryTime;
}
} else {
log("Already in retry...");
}
};
var buzzing = false;
var onDisconnect = function(reason) {
log("Disconnect: " + reason);
log("GATT", gatt);
log("Characteristics", characteristics);
clearRetryTimeout(reason != "Connection Timeout");
supportedCharacteristics["0x2a37"].active = false;
startFallback();
blockInit = false;
if (settings.warnDisconnect && !buzzing){
buzzing = true;
Bangle.buzz(500,0.3).then(()=>waitingPromise(4500)).then(()=>{buzzing = false;});
}
if (Bangle.isBTHRMOn()){
retry();
}
};
var createCharacteristicPromise = function(newCharacteristic) {
log("Create characteristic promise", newCharacteristic);
var result = Promise.resolve();
// For values that can be read, go ahead and read them, even if we might be notified in the future
// Allows for getting initial state of infrequently updating characteristics, like battery
if (newCharacteristic.readValue){
result = result.then(()=>{
log("Reading data", newCharacteristic);
return newCharacteristic.readValue().then((data)=>{
if (supportedCharacteristics[newCharacteristic.uuid] && supportedCharacteristics[newCharacteristic.uuid].handler) {
supportedCharacteristics[newCharacteristic.uuid].handler(data);
}
});
});
}
if (newCharacteristic.properties.notify){
result = result.then(()=>{
log("Starting notifications", newCharacteristic);
var startPromise = newCharacteristic.startNotifications().then(()=>log("Notifications started", newCharacteristic));
if (settings.gracePeriodNotification > 0){
log("Add " + settings.gracePeriodNotification + "ms grace period after starting notifications");
startPromise = startPromise.then(()=>{
log("Wait after connect");
return waitingPromise(settings.gracePeriodNotification);
});
}
return startPromise;
});
}
return result.then(()=>log("Handled characteristic", newCharacteristic));
};
var attachCharacteristicPromise = function(promise, characteristic) {
return promise.then(()=>{
log("Handling characteristic:", characteristic);
return createCharacteristicPromise(characteristic);
});
};
var createCharacteristicsPromise = function(newCharacteristics) {
log("Create characteristics promis ", newCharacteristics);
var result = Promise.resolve();
for (var c of newCharacteristics){
if (!supportedCharacteristics[c.uuid]) continue;
log("Supporting characteristic", c);
characteristics.push(c);
if (c.properties.notify){
addNotificationHandler(c);
}
result = attachCharacteristicPromise(result, c);
}
return result.then(()=>log("Handled characteristics"));
};
var createServicePromise = function(service) {
log("Create service promise", service);
var result = Promise.resolve();
result = result.then(()=>{
log("Handling service" + service.uuid);
return service.getCharacteristics().then((c)=>createCharacteristicsPromise(c));
});
return result.then(()=>log("Handled service" + service.uuid));
};
var attachServicePromise = function(promise, service) {
return promise.then(()=>createServicePromise(service));
};
var initBt = function () {
log("initBt with blockInit: " + blockInit);
if (blockInit){
retry();
return;
}
blockInit = true;
var promise;
var filters;
if (!device){
if (settings.btid){
log("Configured device id", settings.btid);
filters = [{ id: settings.btid }];
} else {
return;
}
log("Requesting device with filters", filters);
promise = NRF.requestDevice({ filters: filters, active: true });
if (settings.gracePeriodRequest){
log("Add " + settings.gracePeriodRequest + "ms grace period after request");
}
promise = promise.then((d)=>{
log("Got device", d);
d.on('gattserverdisconnected', onDisconnect);
device = d;
});
promise = promise.then(()=>{
log("Wait after request");
return waitingPromise(settings.gracePeriodRequest);
});
} else {
promise = Promise.resolve();
log("Reuse device", device);
}
promise = promise.then(()=>{
if (gatt){
log("Reuse GATT", gatt);
} else {
log("GATT is new", gatt);
characteristics = [];
var cachedId = getCache().id;
if (device.id !== cachedId){
log("Device ID changed from " + cachedId + " to " + device.id + ", clearing cache");
clearCache();
}
var newCache = getCache();
newCache.id = device.id;
writeCache(newCache);
gatt = device.gatt;
}
return Promise.resolve(gatt);
});
promise = promise.then((gatt)=>{
if (!gatt.connected){
log("Connecting...");
var connectPromise = gatt.connect(connectSettings).then(function() {
log("Connected.");
});
if (settings.gracePeriodConnect > 0){
log("Add " + settings.gracePeriodConnect + "ms grace period after connecting");
connectPromise = connectPromise.then(()=>{
log("Wait after connect");
return waitingPromise(settings.gracePeriodConnect);
});
}
return connectPromise;
} else {
return Promise.resolve();
}
});
/* promise = promise.then(() => {
log(JSON.stringify(gatt.getSecurityStatus()));
if (gatt.getSecurityStatus()['bonded']) {
log("Already bonded");
return Promise.resolve();
} else {
log("Start bonding");
return gatt.startBonding()
.then(() => console.log(gatt.getSecurityStatus()));
}
});*/
promise = promise.then(()=>{
if (!characteristics || characteristics.length === 0){
characteristics = characteristicsFromCache(device);
}
});
promise = promise.then(()=>{
var characteristicsPromise = Promise.resolve();
if (characteristics.length === 0){
characteristicsPromise = characteristicsPromise.then(()=>{
log("Getting services");
return gatt.getPrimaryServices();
});
characteristicsPromise = characteristicsPromise.then((services)=>{
log("Got services", services);
var result = Promise.resolve();
for (var service of services){
if (!(supportedServices.includes(service.uuid))) continue;
log("Supporting service", service.uuid);
result = attachServicePromise(result, service);
}
if (settings.gracePeriodService > 0) {
log("Add " + settings.gracePeriodService + "ms grace period after services");
result = result.then(()=>{
log("Wait after services");
return waitingPromise(settings.gracePeriodService);
});
}
return result;
});
} else {
for (var characteristic of characteristics){
characteristicsPromise = attachCharacteristicPromise(characteristicsPromise, characteristic, true);
}
}
return characteristicsPromise;
});
return promise.then(()=>{
log("Connection established, waiting for notifications");
characteristicsToCache(characteristics);
clearRetryTimeout(true);
}).catch((e) => {
characteristics = [];
log("Error:", e);
onDisconnect(e);
});
};
Bangle.setBTHRMPower = function(isOn, app) {
// Do app power handling
if (!app) app="?";
if (Bangle._PWR===undefined) Bangle._PWR={};
if (Bangle._PWR.BTHRM===undefined) Bangle._PWR.BTHRM=[];
if (isOn && !Bangle._PWR.BTHRM.includes(app)) Bangle._PWR.BTHRM.push(app);
if (!isOn && Bangle._PWR.BTHRM.includes(app)) Bangle._PWR.BTHRM = Bangle._PWR.BTHRM.filter(a=>a!==app);
isOn = Bangle._PWR.BTHRM.length;
// so now we know if we're really on
if (isOn) {
switchFallback();
if (!Bangle.isBTHRMConnected()) initBt();
} else { // not on
log("Power off for " + app);
clearRetryTimeout(true);
if (gatt) {
if (gatt.connected){
log("Disconnect with gatt", gatt);
try{
gatt.disconnect().then(()=>{
log("Successful disconnect");
}).catch((e)=>{
log("Error during disconnect promise", e);
});
} catch (e){
log("Error during disconnect attempt", e);
}
}
}
}
};
if (settings.replace){
Bangle.on("HRM", (e) => {
e.modified = true;
Bangle.emit("HRM_int", e);
});
Bangle.origOn = Bangle.on;
Bangle.on = function(name, callback) {
if (name == "HRM") {
Bangle.origOn("HRM_int", callback);
} else {
Bangle.origOn(name, callback);
}
};
Bangle.origRemoveListener = Bangle.removeListener;
Bangle.removeListener = function(name, callback) {
if (name == "HRM") {
Bangle.origRemoveListener("HRM_int", callback);
} else {
Bangle.origRemoveListener(name, callback);
}
};
}
Bangle.origSetHRMPower = Bangle.setHRMPower;
if (settings.startWithHrm){
Bangle.setHRMPower = function(isOn, app) {
log("setHRMPower for " + app + ": " + (isOn?"on":"off"));
if (settings.enabled){
Bangle.setBTHRMPower(isOn, app);
}
if ((settings.enabled && !settings.replace) || !settings.enabled){
Bangle.origSetHRMPower(isOn, app);
}
};
}
var fallbackActive = false;
var inSwitch = false;
var stopFallback = function(){
if (fallbackActive){
Bangle.origSetHRMPower(0, "bthrm_fallback");
fallbackActive = false;
log("Fallback to HRM disabled");
}
};
var startFallback = function(){
if (!fallbackActive && settings.allowFallback) {
fallbackActive = true;
Bangle.origSetHRMPower(1, "bthrm_fallback");
log("Fallback to HRM enabled");
}
};
var switchFallback = function() {
log("Check falling back to HRM");
if (!inSwitch){
inSwitch = true;
if (Bangle.isBTHRMActive()){
stopFallback();
} else {
startFallback();
}
}
inSwitch = false;
};
if (settings.replace){
log("Replace HRM event");
if (Bangle._PWR && Bangle._PWR.HRM){
for (var i = 0; i < Bangle._PWR.HRM.length; i++){
var app = Bangle._PWR.HRM[i];
log("Moving app " + app);
Bangle.origSetHRMPower(0, app);
Bangle.setBTHRMPower(1, app);
if (Bangle._PWR.HRM===undefined) break;
}
}
}
E.on("kill", ()=>{
if (gatt && gatt.connected){
log("Got killed, trying to disconnect");
gatt.disconnect().then(()=>log("Disconnected on kill")).catch((e)=>log("Error during disconnnect on kill", e));
}
});
}
})();

View File

@ -16,5 +16,7 @@
"gracePeriodNotification": 0, "gracePeriodNotification": 0,
"gracePeriodConnect": 0, "gracePeriodConnect": 0,
"gracePeriodService": 0, "gracePeriodService": 0,
"gracePeriodRequest": 0 "gracePeriodRequest": 0,
"bonding": false,
"active": true
} }

656
apps/bthrm/lib.js Normal file
View File

@ -0,0 +1,656 @@
exports.enable = () => {
var settings = Object.assign(
require('Storage').readJSON("bthrm.default.json", true) || {},
require('Storage').readJSON("bthrm.json", true) || {}
);
var log = function(text, param){
if (global.showStatusInfo)
showStatusInfo(text);
if (settings.debuglog){
var logline = new Date().toISOString() + " - " + text;
if (param) logline += ": " + JSON.stringify(param);
print(logline);
}
};
log("Settings: ", settings);
if (settings.enabled){
var clearCache = function() {
return require('Storage').erase("bthrm.cache.json");
};
var getCache = function() {
var cache = require('Storage').readJSON("bthrm.cache.json", true) || {};
if (settings.btid && settings.btid === cache.id) return cache;
clearCache();
return {};
};
var addNotificationHandler = function(characteristic) {
log("Setting notification handler"/*supportedCharacteristics[characteristic.uuid].handler*/);
characteristic.on('characteristicvaluechanged', (ev) => supportedCharacteristics[characteristic.uuid].handler(ev.target.value));
};
var writeCache = function(cache) {
var oldCache = getCache();
if (oldCache !== cache) {
log("Writing cache");
require('Storage').writeJSON("bthrm.cache.json", cache);
} else {
log("No changes, don't write cache");
}
};
var characteristicsToCache = function(characteristics) {
log("Cache characteristics");
var cache = getCache();
if (!cache.characteristics) cache.characteristics = {};
for (var c of characteristics){
//"handle_value":16,"handle_decl":15
log("Saving handle " + c.handle_value + " for characteristic: ", c);
cache.characteristics[c.uuid] = {
"handle": c.handle_value,
"uuid": c.uuid,
"notify": c.properties.notify,
"read": c.properties.read
};
}
writeCache(cache);
};
var characteristicsFromCache = function(device) {
var service = { device : device }; // fake a BluetoothRemoteGATTService
log("Read cached characteristics");
var cache = getCache();
if (!cache.characteristics) return [];
var restored = [];
for (var c in cache.characteristics){
var cached = cache.characteristics[c];
var r = new BluetoothRemoteGATTCharacteristic();
log("Restoring characteristic ", cached);
r.handle_value = cached.handle;
r.uuid = cached.uuid;
r.properties = {};
r.properties.notify = cached.notify;
r.properties.read = cached.read;
r.service = service;
addNotificationHandler(r);
log("Restored characteristic: ", r);
restored.push(r);
}
return restored;
};
log("Start");
var lastReceivedData={
};
var supportedServices = [
"0x180d", // Heart Rate
"0x180f", // Battery
];
var bpmTimeout;
var supportedCharacteristics = {
"0x2a37": {
//Heart rate measurement
active: false,
handler: function (dv){
var flags = dv.getUint8(0);
var bpm = (flags & 1) ? (dv.getUint16(1) / 100 /* ? */ ) : dv.getUint8(1); // 8 or 16 bit
supportedCharacteristics["0x2a37"].active = bpm > 0;
log("BTHRM BPM " + supportedCharacteristics["0x2a37"].active);
switchFallback();
if (bpmTimeout) clearTimeout(bpmTimeout);
bpmTimeout = setTimeout(()=>{
bpmTimeout = undefined;
supportedCharacteristics["0x2a37"].active = false;
startFallback();
}, 3000);
var sensorContact;
if (flags & 2){
sensorContact = !!(flags & 4);
}
var idx = 2 + (flags&1);
var energyExpended;
if (flags & 8){
energyExpended = dv.getUint16(idx,1);
idx += 2;
}
var interval;
if (flags & 16) {
interval = [];
var maxIntervalBytes = (dv.byteLength - idx);
log("Found " + (maxIntervalBytes / 2) + " rr data fields");
for(var i = 0 ; i < maxIntervalBytes / 2; i++){
interval[i] = dv.getUint16(idx,1); // in milliseconds
idx += 2;
}
}
var location;
if (lastReceivedData && lastReceivedData["0x180d"] && lastReceivedData["0x180d"]["0x2a38"]){
location = lastReceivedData["0x180d"]["0x2a38"];
}
var battery;
if (lastReceivedData && lastReceivedData["0x180f"] && lastReceivedData["0x180f"]["0x2a19"]){
battery = lastReceivedData["0x180f"]["0x2a19"];
}
if (settings.replace && bpm > 0){
var repEvent = {
bpm: bpm,
confidence: (sensorContact || sensorContact === undefined)? 100 : 0,
src: "bthrm"
};
log("Emitting HRM_R(bt)", repEvent);
Bangle.emit("HRM_R", repEvent);
}
var newEvent = {
bpm: bpm
};
if (location) newEvent.location = location;
if (interval) newEvent.rr = interval;
if (energyExpended) newEvent.energy = energyExpended;
if (battery) newEvent.battery = battery;
if (sensorContact) newEvent.contact = sensorContact;
log("Emitting BTHRM", newEvent);
Bangle.emit("BTHRM", newEvent);
}
},
"0x2a38": {
//Body sensor location
handler: function(dv){
if (!lastReceivedData["0x180d"]) lastReceivedData["0x180d"] = {};
lastReceivedData["0x180d"]["0x2a38"] = parseInt(dv.buffer, 10);
}
},
"0x2a19": {
//Battery
handler: function (dv){
if (!lastReceivedData["0x180f"]) lastReceivedData["0x180f"] = {};
lastReceivedData["0x180f"]["0x2a19"] = dv.getUint8(0);
}
}
};
var device;
var gatt;
var characteristics = [];
var blockInit = false;
var currentRetryTimeout;
var initialRetryTime = 40;
var maxRetryTime = 60000;
var retryTime = initialRetryTime;
var connectSettings = {
minInterval: 7.5,
maxInterval: 1500
};
var waitingPromise = function(timeout) {
return new Promise(function(resolve){
log("Start waiting for " + timeout);
setTimeout(()=>{
log("Done waiting for " + timeout);
resolve();
}, timeout);
});
};
if (settings.enabled){
Bangle.isBTHRMActive = function (){
return supportedCharacteristics["0x2a37"].active;
};
Bangle.isBTHRMOn = function(){
return (Bangle._PWR && Bangle._PWR.BTHRM && Bangle._PWR.BTHRM.length > 0);
};
Bangle.isBTHRMConnected = function(){
return gatt && gatt.connected;
};
}
if (settings.replace){
Bangle.origIsHRMOn = Bangle.isHRMOn;
Bangle.isHRMOn = function() {
if (settings.enabled && !settings.replace){
return Bangle.origIsHRMOn();
} else if (settings.enabled && settings.replace){
return Bangle.isBTHRMOn();
}
return Bangle.origIsHRMOn() || Bangle.isBTHRMOn();
};
}
var clearRetryTimeout = function(resetTime) {
if (currentRetryTimeout){
log("Clearing timeout " + currentRetryTimeout);
clearTimeout(currentRetryTimeout);
currentRetryTimeout = undefined;
}
if (resetTime) {
log("Resetting retry time");
retryTime = initialRetryTime;
}
};
var retry = function() {
log("Retry");
if (!currentRetryTimeout && !powerdownRequested){
var clampedTime = retryTime < 100 ? 100 : retryTime;
log("Set timeout for retry as " + clampedTime);
clearRetryTimeout();
currentRetryTimeout = setTimeout(() => {
log("Retrying");
currentRetryTimeout = undefined;
initBt();
}, clampedTime);
retryTime = Math.pow(clampedTime, 1.1);
if (retryTime > maxRetryTime){
retryTime = maxRetryTime;
}
} else {
log("Already in retry...");
}
};
var buzzing = false;
var onDisconnect = function(reason) {
log("Disconnect: " + reason);
log("GATT", gatt);
log("Characteristics", characteristics);
var retryTimeResetNeeded = true;
retryTimeResetNeeded &= reason != "Connection Timeout";
retryTimeResetNeeded &= reason != "No device found matching filters";
clearRetryTimeout(retryTimeResetNeeded);
supportedCharacteristics["0x2a37"].active = false;
if (!powerdownRequested) startFallback();
blockInit = false;
if (settings.warnDisconnect && !buzzing){
buzzing = true;
Bangle.buzz(500,0.3).then(()=>waitingPromise(4500)).then(()=>{buzzing = false;});
}
if (Bangle.isBTHRMOn()){
retry();
}
};
var createCharacteristicPromise = function(newCharacteristic) {
log("Create characteristic promise", newCharacteristic);
var result = Promise.resolve();
// For values that can be read, go ahead and read them, even if we might be notified in the future
// Allows for getting initial state of infrequently updating characteristics, like battery
if (newCharacteristic.readValue){
result = result.then(()=>{
log("Reading data", newCharacteristic);
return newCharacteristic.readValue().then((data)=>{
if (supportedCharacteristics[newCharacteristic.uuid] && supportedCharacteristics[newCharacteristic.uuid].handler) {
supportedCharacteristics[newCharacteristic.uuid].handler(data);
}
});
});
}
if (newCharacteristic.properties.notify){
result = result.then(()=>{
log("Starting notifications", newCharacteristic);
var startPromise = newCharacteristic.startNotifications().then(()=>log("Notifications started", newCharacteristic));
log("Add " + settings.gracePeriodNotification + "ms grace period after starting notifications");
startPromise = startPromise.then(()=>{
log("Wait after connect");
return waitingPromise(settings.gracePeriodNotification);
});
return startPromise;
});
}
return result.then(()=>log("Handled characteristic", newCharacteristic));
};
var attachCharacteristicPromise = function(promise, characteristic) {
return promise.then(()=>{
log("Handling characteristic:", characteristic);
return createCharacteristicPromise(characteristic);
});
};
var createCharacteristicsPromise = function(newCharacteristics) {
log("Create characteristics promis ", newCharacteristics);
var result = Promise.resolve();
for (var c of newCharacteristics){
if (!supportedCharacteristics[c.uuid]) continue;
log("Supporting characteristic", c);
characteristics.push(c);
if (c.properties.notify){
addNotificationHandler(c);
}
result = attachCharacteristicPromise(result, c);
}
return result.then(()=>log("Handled characteristics"));
};
var createServicePromise = function(service) {
log("Create service promise", service);
var result = Promise.resolve();
result = result.then(()=>{
log("Handling service" + service.uuid);
return service.getCharacteristics().then((c)=>createCharacteristicsPromise(c));
});
return result.then(()=>log("Handled service" + service.uuid));
};
var attachServicePromise = function(promise, service) {
return promise.then(()=>createServicePromise(service));
};
var initBt = function () {
log("initBt with blockInit: " + blockInit);
if (blockInit && !powerdownRequested){
retry();
return;
}
blockInit = true;
var promise;
var filters;
if (!device){
if (settings.btid){
log("Configured device id", settings.btid);
filters = [{ id: settings.btid }];
} else {
return;
}
log("Requesting device with filters", filters);
try {
promise = NRF.requestDevice({ filters: filters, active: settings.active });
} catch (e){
log("Error during initial request:", e);
onDisconnect(e);
return;
}
if (settings.gracePeriodRequest){
log("Add " + settings.gracePeriodRequest + "ms grace period after request");
}
promise = promise.then((d)=>{
log("Got device", d);
d.on('gattserverdisconnected', onDisconnect);
device = d;
});
promise = promise.then(()=>{
log("Wait after request");
return waitingPromise(settings.gracePeriodRequest);
});
} else {
promise = Promise.resolve();
log("Reuse device", device);
}
promise = promise.then(()=>{
if (gatt){
log("Reuse GATT", gatt);
} else {
log("GATT is new", gatt);
characteristics = [];
var cachedId = getCache().id;
if (device.id !== cachedId){
log("Device ID changed from " + cachedId + " to " + device.id + ", clearing cache");
clearCache();
}
var newCache = getCache();
newCache.id = device.id;
writeCache(newCache);
gatt = device.gatt;
}
return Promise.resolve(gatt);
});
promise = promise.then((gatt)=>{
if (!gatt.connected){
log("Connecting...");
var connectPromise = gatt.connect(connectSettings).then(function() {
log("Connected.");
});
log("Add " + settings.gracePeriodConnect + "ms grace period after connecting");
connectPromise = connectPromise.then(()=>{
log("Wait after connect");
return waitingPromise(settings.gracePeriodConnect);
});
return connectPromise;
} else {
return Promise.resolve();
}
});
if (settings.bonding){
promise = promise.then(() => {
log(JSON.stringify(gatt.getSecurityStatus()));
if (gatt.getSecurityStatus()['bonded']) {
log("Already bonded");
return Promise.resolve();
} else {
log("Start bonding");
return gatt.startBonding()
.then(() => log("Security status" + gatt.getSecurityStatus()));
}
});
}
promise = promise.then(()=>{
if (!characteristics || characteristics.length === 0){
characteristics = characteristicsFromCache(device);
}
});
promise = promise.then(()=>{
var characteristicsPromise = Promise.resolve();
if (characteristics.length === 0){
characteristicsPromise = characteristicsPromise.then(()=>{
log("Getting services");
return gatt.getPrimaryServices();
});
characteristicsPromise = characteristicsPromise.then((services)=>{
log("Got services", services);
var result = Promise.resolve();
for (var service of services){
if (!(supportedServices.includes(service.uuid))) continue;
log("Supporting service", service.uuid);
result = attachServicePromise(result, service);
}
log("Add " + settings.gracePeriodService + "ms grace period after services");
result = result.then(()=>{
log("Wait after services");
return waitingPromise(settings.gracePeriodService);
});
return result;
});
} else {
for (var characteristic of characteristics){
characteristicsPromise = attachCharacteristicPromise(characteristicsPromise, characteristic, true);
}
}
return characteristicsPromise;
});
return promise.then(()=>{
log("Connection established, waiting for notifications");
characteristicsToCache(characteristics);
clearRetryTimeout(true);
}).catch((e) => {
characteristics = [];
log("Error:", e);
onDisconnect(e);
});
};
var powerdownRequested = false;
Bangle.setBTHRMPower = function(isOn, app) {
// Do app power handling
if (!app) app="?";
if (Bangle._PWR===undefined) Bangle._PWR={};
if (Bangle._PWR.BTHRM===undefined) Bangle._PWR.BTHRM=[];
if (isOn && !Bangle._PWR.BTHRM.includes(app)) Bangle._PWR.BTHRM.push(app);
if (!isOn && Bangle._PWR.BTHRM.includes(app)) Bangle._PWR.BTHRM = Bangle._PWR.BTHRM.filter(a=>a!==app);
isOn = Bangle._PWR.BTHRM.length;
// so now we know if we're really on
if (isOn) {
powerdownRequested = false;
switchFallback();
if (!Bangle.isBTHRMConnected()) initBt();
} else { // not on
log("Power off for " + app);
powerdownRequested = true;
clearRetryTimeout(true);
stopFallback();
if (gatt) {
if (gatt.connected){
log("Disconnect with gatt", gatt);
try{
gatt.disconnect().then(()=>{
log("Successful disconnect");
}).catch((e)=>{
log("Error during disconnect promise", e);
});
} catch (e){
log("Error during disconnect attempt", e);
}
}
}
}
};
if (settings.replace){
// register a listener for original HRM events and emit as HRM_int
Bangle.on("HRM", (e) => {
e.modified = true;
log("Emitting HRM_int", e);
Bangle.emit("HRM_int", e);
if (fallbackActive){
// if fallback to internal HRM is active, emit as HRM_R to which everyone listens
log("Emitting HRM_R(int)", e);
Bangle.emit("HRM_R", e);
}
});
// force all apps wanting to listen to HRM to actually get events for HRM_R
Bangle.on = ( o => (name, cb) => {
o = o.bind(Bangle);
if (name == "HRM") o("HRM_R", cb);
else o(name, cb);
})(Bangle.on);
Bangle.removeListener = ( o => (name, cb) => {
o = o.bind(Bangle);
if (name == "HRM") o("HRM_R", cb);
else o(name, cb);
})(Bangle.removeListener);
}
Bangle.origSetHRMPower = Bangle.setHRMPower;
if (settings.startWithHrm){
Bangle.setHRMPower = function(isOn, app) {
log("setHRMPower for " + app + ": " + (isOn?"on":"off"));
if (settings.enabled){
Bangle.setBTHRMPower(isOn, app);
if (Bangle._PWR && Bangle._PWR.HRM && Object.keys(Bangle._PWR.HRM).length == 0) {
Bangle._PWR.BTHRM = [];
Bangle.setBTHRMPower(0);
if (!isOn) stopFallback();
}
}
if ((settings.enabled && !settings.replace) || !settings.enabled){
Bangle.origSetHRMPower(isOn, app);
}
};
}
var fallbackActive = false;
var inSwitch = false;
var stopFallback = function(){
if (fallbackActive){
Bangle.origSetHRMPower(0, "bthrm_fallback");
fallbackActive = false;
log("Fallback to HRM disabled");
}
};
var startFallback = function(){
if (!fallbackActive && settings.allowFallback) {
fallbackActive = true;
Bangle.origSetHRMPower(1, "bthrm_fallback");
log("Fallback to HRM enabled");
}
};
var switchFallback = function() {
log("Check falling back to HRM");
if (!inSwitch){
inSwitch = true;
if (Bangle.isBTHRMActive()){
stopFallback();
} else {
startFallback();
}
}
inSwitch = false;
};
if (settings.replace){
log("Replace HRM event");
if (Bangle._PWR && Bangle._PWR.HRM){
for (var i = 0; i < Bangle._PWR.HRM.length; i++){
var app = Bangle._PWR.HRM[i];
log("Moving app " + app);
Bangle.origSetHRMPower(0, app);
Bangle.setBTHRMPower(1, app);
if (Bangle._PWR.HRM===undefined) break;
}
}
}
E.on("kill", ()=>{
if (gatt && gatt.connected){
log("Got killed, trying to disconnect");
try {
gatt.disconnect().then(()=>log("Disconnected on kill")).catch((e)=>log("Error during disconnnect promise on kill", e));
} catch (e) {
log("Error during disconnnect on kill", e)
}
}
});
}
};

View File

@ -2,7 +2,7 @@
"id": "bthrm", "id": "bthrm",
"name": "Bluetooth Heart Rate Monitor", "name": "Bluetooth Heart Rate Monitor",
"shortName": "BT HRM", "shortName": "BT HRM",
"version": "0.12", "version": "0.16",
"description": "Overrides Bangle.js's build in heart rate monitor with an external Bluetooth one.", "description": "Overrides Bangle.js's build in heart rate monitor with an external Bluetooth one.",
"icon": "app.png", "icon": "app.png",
"type": "app", "type": "app",
@ -15,6 +15,7 @@
{"name":"bthrm.0.boot.js","url":"boot.js"}, {"name":"bthrm.0.boot.js","url":"boot.js"},
{"name":"bthrm.img","url":"app-icon.js","evaluate":true}, {"name":"bthrm.img","url":"app-icon.js","evaluate":true},
{"name":"bthrm.settings.js","url":"settings.js"}, {"name":"bthrm.settings.js","url":"settings.js"},
{"name":"bthrm","url":"lib.js"},
{"name":"bthrm.default.json","url":"default.json"} {"name":"bthrm.default.json","url":"default.json"}
] ]
} }

View File

@ -38,35 +38,32 @@
recorders.hrmint = function() { recorders.hrmint = function() {
var active = false; var active = false;
var bpmTimeout; var bpmTimeout;
var bpm = "", bpmConfidence = "", src=""; var bpm = "", bpmConfidence = "";
function onHRM(h) { function onHRM(h) {
bpmConfidence = h.confidence; bpmConfidence = h.confidence;
bpm = h.bpm; bpm = h.bpm;
srv = h.src;
if (h.bpm > 0){ if (h.bpm > 0){
active = true; active = true;
print("active" + h.bpm);
if (bpmTimeout) clearTimeout(bpmTimeout); if (bpmTimeout) clearTimeout(bpmTimeout);
bpmTimeout = setTimeout(()=>{ bpmTimeout = setTimeout(()=>{
print("inactive");
active = false; active = false;
},3000); },3000);
} }
} }
return { return {
name : "HR int", name : "HR int",
fields : ["Heartrate", "Confidence"], fields : ["Int Heartrate", "Int Confidence"],
getValues : () => { getValues : () => {
var r = [bpm,bpmConfidence,src]; var r = [bpm,bpmConfidence];
bpm = ""; bpmConfidence = ""; src=""; bpm = ""; bpmConfidence = "";
return r; return r;
}, },
start : () => { start : () => {
Bangle.origOn('HRM', onHRM); Bangle.on('HRM_int', onHRM);
if (Bangle.origSetHRMPower) Bangle.origSetHRMPower(1,"recorder"); if (Bangle.origSetHRMPower) Bangle.origSetHRMPower(1,"recorder");
}, },
stop : () => { stop : () => {
Bangle.removeListener('HRM', onHRM); Bangle.removeListener('HRM_int', onHRM);
if (Bangle.origSetHRMPower) Bangle.origSetHRMPower(0,"recorder"); if (Bangle.origSetHRMPower) Bangle.origSetHRMPower(0,"recorder");
}, },
draw : (x,y) => g.setColor(( Bangle.origIsHRMOn && Bangle.origIsHRMOn() && active)?"#0f0":"#8f8").drawImage(atob("DAwBAAAAMMeef+f+f+P8H4DwBgAA"),x,y) draw : (x,y) => g.setColor(( Bangle.origIsHRMOn && Bangle.origIsHRMOn() && active)?"#0f0":"#8f8").drawImage(atob("DAwBAAAAMMeef+f+f+P8H4DwBgAA"),x,y)

View File

@ -96,6 +96,18 @@
writeSettings("debuglog",v); writeSettings("debuglog",v);
} }
}, },
'Use bonding': {
value: !!settings.bonding,
onchange: v => {
writeSettings("bonding",v);
}
},
'Use active scanning': {
value: !!settings.active,
onchange: v => {
writeSettings("active",v);
}
},
'Grace periods': function() { E.showMenu(submenu_grace); } 'Grace periods': function() { E.showMenu(submenu_grace); }
}; };

View File

@ -7,7 +7,7 @@
"icon": "app.png", "icon": "app.png",
"screenshots": [{"url":"screenshot.png"}, {"url":"screenshot_2.png"}, {"url":"screenshot_3.png"}, {"url":"screenshot_4.png"}], "screenshots": [{"url":"screenshot.png"}, {"url":"screenshot_2.png"}, {"url":"screenshot_3.png"}, {"url":"screenshot_4.png"}],
"type": "clock", "type": "clock",
"tags": "clock", "tags": "clock,clkinfo",
"supports": ["BANGLEJS2"], "supports": ["BANGLEJS2"],
"allow_emulator": true, "allow_emulator": true,
"storage": [ "storage": [

View File

@ -2,3 +2,4 @@
0.02: More compact rendering & app icon 0.02: More compact rendering & app icon
0.03: Tell clock widgets to hide. 0.03: Tell clock widgets to hide.
0.04: Improve current time readability in light theme. 0.04: Improve current time readability in light theme.
0.05: Show calendar colors & improved all day events.

View File

@ -20,41 +20,55 @@ function zp(str) {
} }
function drawEventHeader(event, y) { function drawEventHeader(event, y) {
g.setFont("Vector", 24); var x = 0;
var time = isActive(event) ? new Date() : new Date(event.timestamp * 1000); var time = isActive(event) ? new Date() : new Date(event.timestamp * 1000);
//Don't need to know what time the event is at if its all day
if (isActive(event) || !event.allDay) {
g.setFont("Vector", 24);
var timeStr = zp(time.getHours()) + ":" + zp(time.getMinutes()); var timeStr = zp(time.getHours()) + ":" + zp(time.getMinutes());
g.drawString(timeStr, 5, y); g.drawString(timeStr, 0, y);
y += 24; y += 3;
x = 13*timeStr.length+5;
}
g.setFont("12x20", 1); g.setFont("12x20", 1);
if (isActive(event)) { if (isActive(event)) {
g.drawString(zp(time.getDate())+". " + require("locale").month(time,1),15*timeStr.length,y-21); g.drawString(zp(time.getDate())+". " + require("locale").month(time,1),x,y);
} else { } else {
var offset = 0-time.getTimezoneOffset()/1440; var offset = 0-time.getTimezoneOffset()/1440;
var days = Math.floor((time.getTime()/1000)/86400+offset)-Math.floor(getTime()/86400+offset); var days = Math.floor((time.getTime()/1000)/86400+offset)-Math.floor(getTime()/86400+offset);
if(days > 0) { if(days > 0 || event.allDay) {
var daysStr = days===1?/*LANG*/"tomorrow":/*LANG*/"in "+days+/*LANG*/" days"; var daysStr = days===1?/*LANG*/"tomorrow":/*LANG*/"in "+days+/*LANG*/" days";
g.drawString(daysStr,15*timeStr.length,y-21); g.drawString(daysStr,x,y);
} }
} }
y += 21;
return y; return y;
} }
function drawEventBody(event, y) { function drawEventBody(event, y) {
g.setFont("12x20", 1); g.setFont("12x20", 1);
var lines = g.wrapString(event.title, g.getWidth()-10); var lines = g.wrapString(event.title, g.getWidth()-15);
var yStart = y;
if (lines.length > 2) { if (lines.length > 2) {
lines = lines.slice(0,2); lines = lines.slice(0,2);
lines[1] = lines[1].slice(0,-3)+"..."; lines[1] = lines[1].slice(0,-3)+"...";
} }
g.drawString(lines.join('\n'), 5, y); g.drawString(lines.join('\n'),10,y);
y+=20 * lines.length; y+=20 * lines.length;
if(event.location) { if(event.location) {
g.drawImage(atob("DBSBAA8D/H/nDuB+B+B+B3Dn/j/B+A8A8AYAYAYAAAAAAA=="),5,y); g.drawImage(atob("DBSBAA8D/H/nDuB+B+B+B3Dn/j/B+A8A8AYAYAYAAAAAAA=="),10,y);
g.drawString(event.location, 20, y); g.drawString(event.location,25,y);
y+=20; y+=20;
} }
if (event.color) {
var oldColor = g.getColor();
g.setColor("#"+(0x1000000+Number(event.color)).toString(16).padStart(6,"0"));
g.fillRect(0,yStart,5,y-3);
g.setColor(oldColor);
}
y+=5; y+=5;
return y; return y;
} }
@ -68,8 +82,8 @@ function drawEvent(event, y) {
var curEventHeight = 0; var curEventHeight = 0;
function drawCurrentEvents(y) { function drawCurrentEvents(y) {
g.setColor(g.theme.dark ? "#0ff" : "#0000ff"); g.setColor(g.theme.dark ? "#0ff" : "#00f");
g.clearRect(5, y, g.getWidth() - 5, y + curEventHeight); g.clearRect(0,y,g.getWidth()-5,y+curEventHeight);
curEventHeight = y; curEventHeight = y;
if(current.length === 0) { if(current.length === 0) {
@ -94,7 +108,7 @@ function drawFutureEvents(y) {
} }
function fullRedraw() { function fullRedraw() {
g.clearRect(5,24,g.getWidth()-5,g.getHeight()); g.clearRect(0,24,g.getWidth()-5,g.getHeight());
updateCalendar(); updateCalendar();
var y = 30; var y = 30;
y = drawCurrentEvents(y); y = drawCurrentEvents(y);
@ -117,3 +131,4 @@ var minuteInterval = setInterval(redraw, 60 * 1000);
Bangle.setUI("clock"); Bangle.setUI("clock");
Bangle.loadWidgets(); Bangle.loadWidgets();
Bangle.drawWidgets(); Bangle.drawWidgets();

View File

@ -2,7 +2,7 @@
"id": "calclock", "id": "calclock",
"name": "Calendar Clock", "name": "Calendar Clock",
"shortName": "CalClock", "shortName": "CalClock",
"version": "0.04", "version": "0.05",
"description": "Show the current and upcoming events synchronized from Gadgetbridge", "description": "Show the current and upcoming events synchronized from Gadgetbridge",
"icon": "calclock.png", "icon": "calclock.png",
"type": "clock", "type": "clock",

View File

@ -0,0 +1,32 @@
diff --git a/apps/calclock/calclock.js b/apps/calclock/calclock.js
index cb8c6100e..2092c1a4e 100644
--- a/apps/calclock/calclock.js
+++ b/apps/calclock/calclock.js
@@ -3,9 +3,24 @@ var current = [];
var next = [];
function updateCalendar() {
- calendar = require("Storage").readJSON("android.calendar.json",true)||[];
- calendar = calendar.filter(e => isActive(e) || getTime() <= e.timestamp);
- calendar.sort((a,b) => a.timestamp - b.timestamp);
+ calendar = [
+ {
+ t: "calendar",
+ id: 2, type: 0, timestamp: getTime(), durationInSeconds: 200,
+ title: "Capture Screenshot",
+ description: "Capture Screenshot",
+ location: "",
+ calName: "",
+ color: -7151168, allDay: true },
+ {
+ t: "calendar",
+ id: 7186, type: 0, timestamp: getTime() + 2000, durationInSeconds: 100,
+ title: "Upload to BangleApps",
+ description: "",
+ location: "",
+ calName: "",
+ color: -509406, allDay: false }
+ ];
current = calendar.filter(isActive);
next = calendar.filter(e=>!isActive(e));

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -3,3 +3,5 @@
0.03: Support for different screen sizes and touchscreen 0.03: Support for different screen sizes and touchscreen
0.04: Display current operation on LHS 0.04: Display current operation on LHS
0.05: Grid positioning and swipe controls to switch between numbers, operators and special (for Bangle.js 2) 0.05: Grid positioning and swipe controls to switch between numbers, operators and special (for Bangle.js 2)
0.06: Bangle.js 2: Exit with a short press of the physical button
0.07: Bangle.js 2: Exit by pressing upper left corner of the screen

View File

@ -12,12 +12,20 @@ Basic calculator reminiscent of MacOs's one. Handy for small calculus.
## Controls ## Controls
Bangle.js 1
- UP: BTN1 - UP: BTN1
- DOWN: BTN3 - DOWN: BTN3
- LEFT: BTN4 - LEFT: BTN4
- RIGHT: BTN5 - RIGHT: BTN5
- SELECT: BTN2 - SELECT: BTN2
Bangle.js 2
- Swipes to change visible buttons
- Click physical button to exit
- Press upper left corner of screen to exit (where the red back button would be)
## Creator ## Creator
<https://twitter.com/fredericrous> <https://twitter.com/fredericrous>
## Contributors
[thyttan](https://github.com/thyttan)

View File

@ -3,6 +3,8 @@
* *
* Original Author: Frederic Rousseau https://github.com/fredericrous * Original Author: Frederic Rousseau https://github.com/fredericrous
* Created: April 2020 * Created: April 2020
*
* Contributors: thyttan https://github.com/thyttan
*/ */
g.clear(); g.clear();
@ -408,37 +410,36 @@ if (process.env.HWVERSION==1) {
prepareScreen(operators, operatorsGrid, COLORS.OPERATOR); prepareScreen(operators, operatorsGrid, COLORS.OPERATOR);
prepareScreen(specials, specialsGrid, COLORS.SPECIAL); prepareScreen(specials, specialsGrid, COLORS.SPECIAL);
drawNumbers(); drawNumbers();
Bangle.on('touch',(n,e)=>{
Bangle.setUI({
mode : 'custom',
back : load, // Clicking physical button or pressing upper left corner turns off (where red back button would be)
touch : (n,e)=>{
for (var key in screen) { for (var key in screen) {
if (typeof screen[key] == "undefined") break; if (typeof screen[key] == "undefined") break;
var r = screen[key].xy; var r = screen[key].xy;
if (e.x>=r[0] && e.y>=r[1] && if (e.x>=r[0] && e.y>=r[1] && e.x<r[2] && e.y<r[3]) {
e.x<r[2] && e.y<r[3]) {
//print("Press "+key); //print("Press "+key);
buttonPress(""+key); buttonPress(""+key);
} }
} }
}); },
var lastX = 0, lastY = 0; swipe : (LR, UD) => {
Bangle.on('drag', (e) => { if (LR == 1) { // right
if (!e.b) {
if (lastX > 50) { // right
drawSpecials(); drawSpecials();
} else if (lastX < -50) { // left }
if (LR == -1) { // left
drawOperators(); drawOperators();
} else if (lastY > 50) { // down }
drawNumbers(); if (UD == 1) { // down
} else if (lastY < -50) { // up drawNumbers();
}
if (UD == -1) { // up
drawNumbers(); drawNumbers();
} }
lastX = 0;
lastY = 0;
} else {
lastX = lastX + e.dx;
lastY = lastY + e.dy;
} }
}); });
}
}
displayOutput(0); displayOutput(0);

View File

@ -2,7 +2,7 @@
"id": "calculator", "id": "calculator",
"name": "Calculator", "name": "Calculator",
"shortName": "Calculator", "shortName": "Calculator",
"version": "0.05", "version": "0.07",
"description": "Basic calculator reminiscent of MacOs's one. Handy for small calculus.", "description": "Basic calculator reminiscent of MacOs's one. Handy for small calculus.",
"icon": "calculator.png", "icon": "calculator.png",
"screenshots": [{"url":"screenshot_calculator.png"}], "screenshots": [{"url":"screenshot_calculator.png"}],

View File

@ -3,7 +3,7 @@
"shortName":"Calibration", "shortName":"Calibration",
"icon": "calibration.png", "icon": "calibration.png",
"version":"0.03", "version":"0.03",
"description": "A simple calibration app for the touchscreen", "description": "(NOT RECOMMENDED) A simple calibration app for the touchscreen. Please use the Touchscreen Calibration in the Settings app instead.",
"supports": ["BANGLEJS","BANGLEJS2"], "supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md", "readme": "README.md",
"tags": "tool", "tags": "tool",

View File

@ -10,3 +10,4 @@
0.9: Remove ESLint spaces 0.9: Remove ESLint spaces
0.10: Show daily steps, heartrate and the temperature if weather information is available. 0.10: Show daily steps, heartrate and the temperature if weather information is available.
0.11: Tell clock widgets to hide. 0.11: Tell clock widgets to hide.
0.12: Swipe down to see widgets, step counter now just uses getHealthStatus

View File

@ -8,4 +8,5 @@ It displays current temperature,day,steps,battery.heartbeat and weather.
**To-do**: **To-do**:
Align and change size of some elements.
* Align and change size of some elements

View File

@ -91,7 +91,6 @@ function getTemperature(){
var weatherJson = storage.readJSON('weather.json'); var weatherJson = storage.readJSON('weather.json');
var weather = weatherJson.weather; var weather = weatherJson.weather;
return Math.round(weather.temp-273.15); return Math.round(weather.temp-273.15);
} catch(ex) { } catch(ex) {
print(ex) print(ex)
return "?" return "?"
@ -99,20 +98,7 @@ function getTemperature(){
} }
function getSteps() { function getSteps() {
var steps = 0; var steps = Bangle.getHealthStatus("day").steps;
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); steps = Math.round(steps/1000);
return steps + "k"; return steps + "k";
} }
@ -121,8 +107,7 @@ function getSteps() {
function draw() { function draw() {
queueDraw(); queueDraw();
g.reset(); g.clear(1);
g.clear();
g.setColor(0, 255, 255); g.setColor(0, 255, 255);
g.fillRect(0, 0, g.getWidth(), g.getHeight()); g.fillRect(0, 0, g.getWidth(), g.getHeight());
let background = getBackgroundImage(); let background = getBackgroundImage();
@ -143,9 +128,6 @@ function draw() {
drawClock(); drawClock();
drawRocket(); drawRocket();
drawBattery(); drawBattery();
// Hide widgets
for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";}
} }
Bangle.on("lcdPower", (on) => { Bangle.on("lcdPower", (on) => {
@ -169,7 +151,6 @@ Bangle.setUI("clock");
// Load widgets, but don't show them // Load widgets, but don't show them
Bangle.loadWidgets(); Bangle.loadWidgets();
require("widget_utils").swipeOn(); // hide widgets, make them visible with a swipe
g.reset(); g.clear(1);
g.clear();
draw(); draw();

View File

@ -4,7 +4,7 @@
"description": "Animated Clock with Space Cassio Watch Style", "description": "Animated Clock with Space Cassio Watch Style",
"screenshots": [{ "url": "screens/screen_night.png" },{ "url": "screens/screen_day.png" }], "screenshots": [{ "url": "screens/screen_night.png" },{ "url": "screens/screen_day.png" }],
"icon": "app.png", "icon": "app.png",
"version": "0.11", "version": "0.12",
"type": "clock", "type": "clock",
"tags": "clock, weather, cassio, retro", "tags": "clock, weather, cassio, retro",
"supports": ["BANGLEJS2"], "supports": ["BANGLEJS2"],

View File

@ -26,3 +26,9 @@
0.12: Allow configuration of update interval 0.12: Allow configuration of update interval
0.13: Load step goal from Bangle health app as fallback 0.13: Load step goal from Bangle health app as fallback
Memory optimizations Memory optimizations
0.14: Support to show big weather info
0.15: Use Bangle.setUI({remove:...}) to allow loading the launcher without a full reset on 2v16
0.16: Fix const error
Use widget_utils if available
0.17: Load circles from clkinfo
0.18: Improved clkinfo handling and using it for the weather circle

View File

@ -9,10 +9,11 @@ It can show the following information (this can be configured):
* Steps distance * Steps distance
* Heart rate (automatically updates when screen is on and unlocked) * Heart rate (automatically updates when screen is on and unlocked)
* Battery (including charging status and battery low warning) * Battery (including charging status and battery low warning)
* Weather (requires [weather app](https://banglejs.com/apps/#weather)) * Weather (requires [OWM weather provider](https://banglejs.com/apps/?id=owmweather))
* Humidity or wind speed as circle progress * Humidity or wind speed as circle progress
* Temperature inside circle * Temperature inside circle
* Condition as icon below circle * Condition as icon below circle
* Big weather icon next to clock
* Time and progress until next sunrise or sunset (requires [my location app](https://banglejs.com/apps/#mylocation)) * Time and progress until next sunrise or sunset (requires [my location app](https://banglejs.com/apps/#mylocation))
* Temperature, air pressure or altitude from internal pressure sensor * Temperature, air pressure or altitude from internal pressure sensor
@ -27,6 +28,8 @@ The color of each circle can be configured. The following colors are available:
![Screenshot light theme](screenshot-light.png) ![Screenshot light theme](screenshot-light.png)
![Screenshot dark theme with four circles](screenshot-dark-4.png) ![Screenshot dark theme with four circles](screenshot-dark-4.png)
![Screenshot light theme with four circles](screenshot-light-4.png) ![Screenshot light theme with four circles](screenshot-light-4.png)
![Screenshot light theme with big weather enabled](screenshot-light-with-big-weather.png)
## Ideas ## Ideas
* Show compass heading * Show compass heading
@ -35,4 +38,5 @@ The color of each circle can be configured. The following colors are available:
Marco ([myxor](https://github.com/myxor)) Marco ([myxor](https://github.com/myxor))
## Icons ## Icons
Icons taken from [materialdesignicons](https://materialdesignicons.com) under Apache License 2.0 Most of the icons are taken from [materialdesignicons](https://materialdesignicons.com) under Apache License 2.0 except the big weather icons which are from
[icons8](https://icons8.com/icon/set/weather/small--static--black)

File diff suppressed because it is too large Load Diff

View File

@ -1,17 +1,11 @@
{ {
"minHR": 40,
"maxHR": 200,
"confidence": 0,
"stepGoal": 10000,
"stepDistanceGoal": 8000,
"stepLength": 0.8,
"batteryWarn": 30, "batteryWarn": 30,
"showWidgets": false, "showWidgets": false,
"weatherCircleData": "humidity", "weatherCircleData": "humidity",
"circleCount": 3, "circleCount": 3,
"circle1": "hr", "circle1": "Bangle/HRM",
"circle2": "steps", "circle2": "Bangle/Steps",
"circle3": "battery", "circle3": "Bangle/Battery",
"circle4": "weather", "circle4": "weather",
"circle1color": "green-red", "circle1color": "green-red",
"circle2color": "#0000ff", "circle2color": "#0000ff",
@ -21,6 +15,15 @@
"circle2colorizeIcon": true, "circle2colorizeIcon": true,
"circle3colorizeIcon": true, "circle3colorizeIcon": true,
"circle4colorizeIcon": false, "circle4colorizeIcon": false,
"hrmValidity": 60, "updateInterval": 60,
"updateInterval": 60 "showBigWeather": false,
"minHR": 40,
"maxHR": 200,
"confidence": 0,
"stepGoal": 10000,
"stepDistanceGoal": 8000,
"stepLength": 0.8,
"hrmValidity": 60
} }

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