1
0
Fork 0

Merge branch 'master' of github.com:wagnerf42/BangleApps

master
frederic wagner 2022-11-25 21:00:12 +01:00
commit 38a07a3c9c
419 changed files with 8351 additions and 2667 deletions

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

@ -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,304 +1,160 @@
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);
function bigThenSmall(big, small, x, y) { function bigThenSmall(big, small, x, y) {
g.setFont("7x11Numeric7Seg", 2); g.setFont("7x11Numeric7Seg", 2);
g.drawString(big, x, y); g.drawString(big, x, y);
x += g.stringWidth(big); x += g.stringWidth(big);
g.setFont("8x12"); g.setFont("8x12");
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;
var drawTimeout; var drawTimeout;
function queueDraw() { function queueDraw() {
if (drawTimeout) clearTimeout(drawTimeout); if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = setTimeout(function() { drawTimeout = setTimeout(function() {
drawTimeout = undefined; drawTimeout = undefined;
draw(); draw();
}, 60000 - (Date.now() % 60000)); }, 60000 - (Date.now() % 60000));
} }
function clearIntervals() { function clearIntervals() {
if (rocketInterval) clearInterval(rocketInterval); if (rocketInterval) clearInterval(rocketInterval);
rocketInterval = undefined; rocketInterval = undefined;
if (drawTimeout) clearTimeout(drawTimeout); if (drawTimeout) clearTimeout(drawTimeout);
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; g.setColor(0, 0, 0);
var h = 116; g.drawString(require("locale").time(new Date(), 1), 70, 60);
g.drawRect(l, t, w, h); g.setFont("8x12", 2);
g.fillRect(l, t, w, h); g.drawString(require("locale").dow(new Date(), 2).toUpperCase(), 18, 130);
g.setColor(0, 0, 0); g.setFont("8x12");
g.drawString(require("locale").time(new Date(), 1), 76, 60); g.drawString(require("locale").month(new Date(), 2).toUpperCase(), 80, 126);
g.setFont("8x12", 2);
// day const time = new Date().getDate();
//g.setFont("8x12", 1); g.drawString(time < 10 ? "0" + time : time, 78, 137);
//g.setFont("9x18", 1);
//g.drawString(require("locale").dow(new Date(), 2).toUpperCase(), 25, 136);
g.setFont("8x12", 2);
g.drawString(require("locale").dow(new Date(), 2), 18, 130);
// month
g.setFont("8x12");
g.drawString(require("locale").month(new Date(), 2).toUpperCase(), 80, 127);
// day nb
g.setFont("8x12", 2);
const time = new Date().getDate();
g.drawString(time < 10 ? "0" + time : time, 78, 137);
} }
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{ steps = Math.round(steps/1000);
if (WIDGETS.wpedom !== undefined) { return steps + "k";
steps = WIDGETS.wpedom.getSteps();
} else if (WIDGETS.activepedom !== undefined) {
steps = WIDGETS.activepedom.getSteps();
} else {
steps = Bangle.getHealthStatus("day").steps;
}
} catch(ex) {
// In case we failed, we can only show 0 steps.
return "? k";
}
steps = Math.round(steps/1000);
return steps + "k";
} }
function draw() { function draw() {
queueDraw();
queueDraw(); g.clear(1);
g.setColor(0, 255, 255);
g.fillRect(0, 0, g.getWidth(), g.getHeight());
let background = getBackgroundImage();
g.drawImage(background, 0, 0, { scale: 1 });
g.setColor(0, 0, 0);
g.setFont("6x12");
g.drawString("Launching Process", 30, 20);
g.setFont("8x12");
g.drawString("ACTIVATE", 40, 35);
g.reset(); g.setFontAlign(0,-1);
g.clear(); g.setFont("8x12", 2);
g.setColor(255, 255, 255); g.drawString(getTemperature(), 155, 132);
g.fillRect(0, 0, g.getWidth(), g.getHeight()); g.drawString(Math.round(Bangle.getHealthStatus("last").bpm), 109, 98);
let background = getBackgroundImage(); g.drawString(getSteps(), 158, 98);
g.drawImage(background, 0, 0, { scale: 1 });
g.setFontAlign(-1,-1);
drawClock();
drawRocket();
drawBattery();
g.setColor(0, 0, 0); // Hide widgets
if(dataJson && dataJson.weather) drawWeather(dataJson.weather); for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";}
if(dataJson && dataJson.tasks) drawTasks(dataJson.tasks);
g.setFontAlign(0,-1);
g.setFont("8x12", 2);
drawSteps();
g.setFontAlign(-1,-1);
drawClock();
drawBattery();
drawTimer();
// Hide widgets
for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";}
} }
// save batt power, does not seem to work although...
var canTouch = true;
Bangle.on("lcdPower", (on) => { Bangle.on("lcdPower", (on) => {
if (on) { if (on) {
draw(); draw();
} else { } else {
canTouch = false; clearIntervals();
clearIntervals(); }
}
}); });
Bangle.on("lock", (locked) => { 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,7 +5,7 @@
"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,41 +8,52 @@ 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
} // What about:
catch(e) { // NAV-TIMEUTC (0x01 0x10)
console.log("error:", e); // NAV-PV (0x01 0x03)
} // or AGPS.zip uses AID-INI (0x0B 0x01)
return false;
eval(initCommands);
try {
writeChunks(atob(b64), resolve);
} catch (e) {
console.log("error:", e);
reject();
}
});
} }
function jsFromBase64(b64) { var chunkI = 0;
var bin = atob(b64); function writeChunks(bin, resolve) {
var chunkSize = 128; return new Promise(function(resolve2) {
var js = "Bangle.setGPSPower(1);\n"; // turn GPS on const chunkSize = 128;
var gnsstype = settings.gnsstype || 1; // default GPS setTimeout(function() {
js += `Serial1.println("${CASIC_CHECKSUM("$PCAS04,"+gnsstype)}")\n`; // set GNSS mode if (chunkI < bin.length) {
// What about: var chunk = bin.substr(chunkI, chunkSize);
// NAV-TIMEUTC (0x01 0x10) js = `Serial1.write(atob("${btoa(chunk)}"))\n`;
// NAV-PV (0x01 0x03) eval(js);
// or AGPS.zip uses AID-INI (0x0B 0x01)
for (var i=0;i<bin.length;i+=chunkSize) { chunkI += chunkSize;
var chunk = bin.substr(i,chunkSize); writeChunks(bin, resolve);
js += `Serial1.write(atob("${btoa(chunk)}"))\n`; } else {
} if (resolve)
return js; resolve(); // call outer resolve
}
}, 200);
});
} }
function CASIC_CHECKSUM(cmd) { function CASIC_CHECKSUM(cmd) {
var cs = 0; var cs = 0;
for (var i=1;i<cmd.length;i++) for (var i = 1; i < cmd.length; i++)
cs = cs ^ cmd.charCodeAt(i); cs = cs ^ cmd.charCodeAt(i);
return cmd+"*"+cs.toString(16).toUpperCase().padStart(2, '0'); return cmd + "*" + cs.toString(16).toUpperCase().padStart(2, '0');
} }
function updateLastUpdate() { function updateLastUpdate() {
@ -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)
updateLastUpdate(); .then(r => {
if (successCallback) successCallback(); updateLastUpdate();
} else { if (successCallback)
console.log("error applying AGPS data"); successCallback();
if (failureCallback) failureCallback("Error applying AGPS data"); })
} .catch((e) => {
}).catch((e)=>{ console.log("error", e);
console.log("error", e); if (failureCallback)
if (failureCallback) failureCallback(e); 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

@ -14,3 +14,4 @@
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

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);
}, },
@ -170,7 +166,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

View File

@ -2,7 +2,7 @@
"id": "android", "id": "android",
"name": "Android Integration", "name": "Android Integration",
"shortName": "Android", "shortName": "Android",
"version": "0.16", "version": "0.17",
"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",

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

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

View File

@ -59,3 +59,5 @@
Add patch to ensure that compass heading is corrected on pre-2v15.68 firmware 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 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.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

@ -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);
@ -77,11 +78,17 @@ if (s.passkey!==undefined && s.passkey.length==6) boot+=`NRF.setSecurity({passke
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`;
if (s.rotate) boot+=`g.setRotation(${s.rotate&3},${s.rotate>>2});\n` // screen rotation if (s.rotate) boot+=`g.setRotation(${s.rotate&3},${s.rotate>>2});\n` // screen rotation
// ================================================== FIXING OLDER FIRMWARES // ================================================== FIXING OLDER FIRMWARES
// 2v15.68 and before had compass heading inverted. if (FWVERSION<215.068) // 2v15.68 and before had compass heading inverted.
if (process.version.replace("v","")<215.68)
boot += `Bangle.on('mag',e=>{if(!isNaN(e.heading))e.heading=360-e.heading;}); 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);`; Bangle.getCompass=(c=>(()=>{e=c();if(!isNaN(e.heading))e.heading=360-e.heading;return e;}))(Bangle.getCompass);`;
// deleting stops us getting confused by our own decl. builtins can't be deleted
// this is a polyfill without fastloading capability
delete Bangle.showClock;
if (!Bangle.showClock) boot += `Bangle.showClock = ()=>{load(".bootcde")};\n`;
delete Bangle.load;
if (!Bangle.load) boot += `Bangle.load = load;\n`;
// ================================================== BOOT.JS // ================================================== BOOT.JS
// Append *.boot.js files // Append *.boot.js files
// These could change bleServices/bleServiceOptions if needed // These could change bleServices/bleServiceOptions if needed

View File

@ -1,7 +1,7 @@
{ {
"id": "boot", "id": "boot",
"name": "Bootloader", "name": "Bootloader",
"version": "0.52", "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

@ -30,3 +30,13 @@
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.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

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

View File

@ -106,9 +106,10 @@ exports.enable = () => {
var bpm = (flags & 1) ? (dv.getUint16(1) / 100 /* ? */ ) : dv.getUint8(1); // 8 or 16 bit var bpm = (flags & 1) ? (dv.getUint16(1) / 100 /* ? */ ) : dv.getUint8(1); // 8 or 16 bit
supportedCharacteristics["0x2a37"].active = bpm > 0; supportedCharacteristics["0x2a37"].active = bpm > 0;
log("BTHRM BPM " + supportedCharacteristics["0x2a37"].active); log("BTHRM BPM " + supportedCharacteristics["0x2a37"].active);
if (supportedCharacteristics["0x2a37"].active) stopFallback(); switchFallback();
if (bpmTimeout) clearTimeout(bpmTimeout); if (bpmTimeout) clearTimeout(bpmTimeout);
bpmTimeout = setTimeout(()=>{ bpmTimeout = setTimeout(()=>{
bpmTimeout = undefined;
supportedCharacteristics["0x2a37"].active = false; supportedCharacteristics["0x2a37"].active = false;
startFallback(); startFallback();
}, 3000); }, 3000);
@ -147,15 +148,15 @@ exports.enable = () => {
battery = lastReceivedData["0x180f"]["0x2a19"]; battery = lastReceivedData["0x180f"]["0x2a19"];
} }
if (settings.replace){ if (settings.replace && bpm > 0){
var repEvent = { var repEvent = {
bpm: bpm, bpm: bpm,
confidence: (sensorContact || sensorContact === undefined)? 100 : 0, confidence: (sensorContact || sensorContact === undefined)? 100 : 0,
src: "bthrm" src: "bthrm"
}; };
log("Emitting HRM", repEvent); log("Emitting HRM_R(bt)", repEvent);
Bangle.emit("HRM_int", repEvent); Bangle.emit("HRM_R", repEvent);
} }
var newEvent = { var newEvent = {
@ -254,7 +255,7 @@ exports.enable = () => {
var retry = function() { var retry = function() {
log("Retry"); log("Retry");
if (!currentRetryTimeout){ if (!currentRetryTimeout && !powerdownRequested){
var clampedTime = retryTime < 100 ? 100 : retryTime; var clampedTime = retryTime < 100 ? 100 : retryTime;
@ -280,9 +281,13 @@ exports.enable = () => {
log("Disconnect: " + reason); log("Disconnect: " + reason);
log("GATT", gatt); log("GATT", gatt);
log("Characteristics", characteristics); log("Characteristics", characteristics);
clearRetryTimeout(reason != "Connection Timeout");
var retryTimeResetNeeded = true;
retryTimeResetNeeded &= reason != "Connection Timeout";
retryTimeResetNeeded &= reason != "No device found matching filters";
clearRetryTimeout(retryTimeResetNeeded);
supportedCharacteristics["0x2a37"].active = false; supportedCharacteristics["0x2a37"].active = false;
startFallback(); if (!powerdownRequested) startFallback();
blockInit = false; blockInit = false;
if (settings.warnDisconnect && !buzzing){ if (settings.warnDisconnect && !buzzing){
buzzing = true; buzzing = true;
@ -312,13 +317,13 @@ exports.enable = () => {
result = result.then(()=>{ result = result.then(()=>{
log("Starting notifications", newCharacteristic); log("Starting notifications", newCharacteristic);
var startPromise = newCharacteristic.startNotifications().then(()=>log("Notifications started", newCharacteristic)); var startPromise = newCharacteristic.startNotifications().then(()=>log("Notifications started", newCharacteristic));
if (settings.gracePeriodNotification > 0){
log("Add " + settings.gracePeriodNotification + "ms grace period after starting notifications"); log("Add " + settings.gracePeriodNotification + "ms grace period after starting notifications");
startPromise = startPromise.then(()=>{ startPromise = startPromise.then(()=>{
log("Wait after connect"); log("Wait after connect");
return waitingPromise(settings.gracePeriodNotification); return waitingPromise(settings.gracePeriodNotification);
}); });
}
return startPromise; return startPromise;
}); });
} }
@ -364,7 +369,7 @@ exports.enable = () => {
var initBt = function () { var initBt = function () {
log("initBt with blockInit: " + blockInit); log("initBt with blockInit: " + blockInit);
if (blockInit){ if (blockInit && !powerdownRequested){
retry(); retry();
return; return;
} }
@ -382,7 +387,13 @@ exports.enable = () => {
return; return;
} }
log("Requesting device with filters", filters); log("Requesting device with filters", filters);
promise = NRF.requestDevice({ filters: filters, active: true }); try {
promise = NRF.requestDevice({ filters: filters, active: settings.active });
} catch (e){
log("Error during initial request:", e);
onDisconnect(e);
return;
}
if (settings.gracePeriodRequest){ if (settings.gracePeriodRequest){
log("Add " + settings.gracePeriodRequest + "ms grace period after request"); log("Add " + settings.gracePeriodRequest + "ms grace period after request");
@ -429,30 +440,30 @@ exports.enable = () => {
var connectPromise = gatt.connect(connectSettings).then(function() { var connectPromise = gatt.connect(connectSettings).then(function() {
log("Connected."); log("Connected.");
}); });
if (settings.gracePeriodConnect > 0){ log("Add " + settings.gracePeriodConnect + "ms grace period after connecting");
log("Add " + settings.gracePeriodConnect + "ms grace period after connecting"); connectPromise = connectPromise.then(()=>{
connectPromise = connectPromise.then(()=>{ log("Wait after connect");
log("Wait after connect"); return waitingPromise(settings.gracePeriodConnect);
return waitingPromise(settings.gracePeriodConnect); });
});
}
return connectPromise; return connectPromise;
} else { } else {
return Promise.resolve(); return Promise.resolve();
} }
}); });
/* promise = promise.then(() => { if (settings.bonding){
log(JSON.stringify(gatt.getSecurityStatus())); promise = promise.then(() => {
if (gatt.getSecurityStatus()['bonded']) { log(JSON.stringify(gatt.getSecurityStatus()));
log("Already bonded"); if (gatt.getSecurityStatus()['bonded']) {
return Promise.resolve(); log("Already bonded");
} else { return Promise.resolve();
log("Start bonding"); } else {
return gatt.startBonding() log("Start bonding");
.then(() => console.log(gatt.getSecurityStatus())); return gatt.startBonding()
} .then(() => log("Security status" + gatt.getSecurityStatus()));
});*/ }
});
}
promise = promise.then(()=>{ promise = promise.then(()=>{
if (!characteristics || characteristics.length === 0){ if (!characteristics || characteristics.length === 0){
@ -476,13 +487,11 @@ exports.enable = () => {
log("Supporting service", service.uuid); log("Supporting service", service.uuid);
result = attachServicePromise(result, service); result = attachServicePromise(result, service);
} }
if (settings.gracePeriodService > 0) { log("Add " + settings.gracePeriodService + "ms grace period after services");
log("Add " + settings.gracePeriodService + "ms grace period after services"); result = result.then(()=>{
result = result.then(()=>{ log("Wait after services");
log("Wait after services"); return waitingPromise(settings.gracePeriodService);
return waitingPromise(settings.gracePeriodService); });
});
}
return result; return result;
}); });
} else { } else {
@ -505,6 +514,8 @@ exports.enable = () => {
}); });
}; };
var powerdownRequested = false;
Bangle.setBTHRMPower = function(isOn, app) { Bangle.setBTHRMPower = function(isOn, app) {
// Do app power handling // Do app power handling
if (!app) app="?"; if (!app) app="?";
@ -515,11 +526,14 @@ exports.enable = () => {
isOn = Bangle._PWR.BTHRM.length; isOn = Bangle._PWR.BTHRM.length;
// so now we know if we're really on // so now we know if we're really on
if (isOn) { if (isOn) {
powerdownRequested = false;
switchFallback(); switchFallback();
if (!Bangle.isBTHRMConnected()) initBt(); if (!Bangle.isBTHRMConnected()) initBt();
} else { // not on } else { // not on
log("Power off for " + app); log("Power off for " + app);
powerdownRequested = true;
clearRetryTimeout(true); clearRetryTimeout(true);
stopFallback();
if (gatt) { if (gatt) {
if (gatt.connected){ if (gatt.connected){
log("Disconnect with gatt", gatt); log("Disconnect with gatt", gatt);
@ -538,39 +552,44 @@ exports.enable = () => {
}; };
if (settings.replace){ if (settings.replace){
// register a listener for original HRM events and emit as HRM_int
Bangle.on("HRM", (e) => { Bangle.on("HRM", (e) => {
e.modified = true; e.modified = true;
log("Emitting HRM_int", e);
Bangle.emit("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);
}
}); });
Bangle.origOn = Bangle.on; // force all apps wanting to listen to HRM to actually get events for HRM_R
Bangle.on = function(name, callback) { Bangle.on = ( o => (name, cb) => {
if (name == "HRM") { o = o.bind(Bangle);
Bangle.origOn("HRM_int", callback); if (name == "HRM") o("HRM_R", cb);
} else { else o(name, cb);
Bangle.origOn(name, callback); })(Bangle.on);
}
};
Bangle.origRemoveListener = Bangle.removeListener;
Bangle.removeListener = function(name, callback) {
if (name == "HRM") {
Bangle.origRemoveListener("HRM_int", callback);
} else {
Bangle.origRemoveListener(name, callback);
}
};
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; Bangle.origSetHRMPower = Bangle.setHRMPower;
if (settings.startWithHrm){ if (settings.startWithHrm){
Bangle.setHRMPower = function(isOn, app) { Bangle.setHRMPower = function(isOn, app) {
log("setHRMPower for " + app + ": " + (isOn?"on":"off")); log("setHRMPower for " + app + ": " + (isOn?"on":"off"));
if (settings.enabled){ if (settings.enabled){
Bangle.setBTHRMPower(isOn, app); 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){ if ((settings.enabled && !settings.replace) || !settings.enabled){
Bangle.origSetHRMPower(isOn, app); Bangle.origSetHRMPower(isOn, app);
@ -626,7 +645,11 @@ exports.enable = () => {
E.on("kill", ()=>{ E.on("kill", ()=>{
if (gatt && gatt.connected){ if (gatt && gatt.connected){
log("Got killed, trying to disconnect"); log("Got killed, trying to disconnect");
gatt.disconnect().then(()=>log("Disconnected on kill")).catch((e)=>log("Error during disconnnect on kill", e)); 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.13", "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",

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

@ -4,3 +4,4 @@
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.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();
@ -402,38 +404,42 @@ if (process.env.HWVERSION==1) {
swipeEnabled = false; swipeEnabled = false;
drawGlobal(); drawGlobal();
} else { // touchscreen? } else { // touchscreen?
setWatch(_ => {load();}, BTN1, {edge:'falling'}); // Exit with a short press to physical button selected = "NONE";
selected = "NONE";
swipeEnabled = true; swipeEnabled = true;
prepareScreen(numbers, numbersGrid, COLORS.DEFAULT); prepareScreen(numbers, numbersGrid, COLORS.DEFAULT);
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)=>{
for (var key in screen) { Bangle.setUI({
if (typeof screen[key] == "undefined") break; mode : 'custom',
var r = screen[key].xy; back : load, // Clicking physical button or pressing upper left corner turns off (where red back button would be)
if (e.x>=r[0] && e.y>=r[1] && touch : (n,e)=>{
e.x<r[2] && e.y<r[3]) { for (var key in screen) {
//print("Press "+key); if (typeof screen[key] == "undefined") break;
buttonPress(""+key); var r = screen[key].xy;
if (e.x>=r[0] && e.y>=r[1] && e.x<r[2] && e.y<r[3]) {
//print("Press "+key);
buttonPress(""+key);
}
}
},
swipe : (LR, UD) => {
if (LR == 1) { // right
drawSpecials();
}
if (LR == -1) { // left
drawOperators();
}
if (UD == 1) { // down
drawNumbers();
}
if (UD == -1) { // up
drawNumbers();
} }
} }
}); });
Bangle.on('swipe', (LR, UD) => {
if (LR == 1) { // right
drawSpecials();
}
if (LR == -1) { // left
drawOperators();
}
if (UD == 1) { // down
drawNumbers();
}
if (UD == -1) { // up
drawNumbers();
}
});
} }
displayOutput(0); displayOutput(0);

View File

@ -2,7 +2,7 @@
"id": "calculator", "id": "calculator",
"name": "Calculator", "name": "Calculator",
"shortName": "Calculator", "shortName": "Calculator",
"version": "0.06", "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

@ -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

@ -30,3 +30,4 @@
0.15: Use Bangle.setUI({remove:...}) to allow loading the launcher without a full reset on 2v16 0.15: Use Bangle.setUI({remove:...}) to allow loading the launcher without a full reset on 2v16
0.16: Fix const error 0.16: Fix const error
Use widget_utils if available Use widget_utils if available
0.17: Load circles from clkinfo

View File

@ -1,3 +1,4 @@
let clock_info = require("clock_info");
let locale = require("locale"); let locale = require("locale");
let storage = require("Storage"); let storage = require("Storage");
Graphics.prototype.setFontRobotoRegular50NumericOnly = function(scale) { Graphics.prototype.setFontRobotoRegular50NumericOnly = function(scale) {
@ -18,6 +19,7 @@ let settings = Object.assign(
storage.readJSON(SETTINGS_FILE, true) || {} storage.readJSON(SETTINGS_FILE, true) || {}
); );
//TODO deprecate this (and perhaps use in the clkinfo module)
// Load step goal from health app and pedometer widget as fallback // Load step goal from health app and pedometer widget as fallback
if (settings.stepGoal == undefined) { if (settings.stepGoal == undefined) {
let d = storage.readJSON("health.json", true) || {}; let d = storage.readJSON("health.json", true) || {};
@ -29,7 +31,7 @@ if (settings.stepGoal == undefined) {
} }
} }
let timerHrm; let timerHrm; //TODO deprecate this
let drawTimeout; let drawTimeout;
/* /*
@ -44,10 +46,9 @@ let showWidgets = settings.showWidgets || false;
let circleCount = settings.circleCount || 3; let circleCount = settings.circleCount || 3;
let showBigWeather = settings.showBigWeather || false; let showBigWeather = settings.showBigWeather || false;
let hrtValue; let hrtValue; //TODO deprecate this
let now = Math.round(new Date().getTime() / 1000); let now = Math.round(new Date().getTime() / 1000);
// layout values: // layout values:
let colorFg = g.theme.dark ? '#fff' : '#000'; let colorFg = g.theme.dark ? '#fff' : '#000';
let colorBg = g.theme.dark ? '#000' : '#fff'; let colorBg = g.theme.dark ? '#000' : '#fff';
@ -91,8 +92,20 @@ let circleFontSmall = circleCount == 3 ? "Vector:14" : "Vector:10";
let circleFont = circleCount == 3 ? "Vector:15" : "Vector:11"; let circleFont = circleCount == 3 ? "Vector:15" : "Vector:11";
let circleFontBig = circleCount == 3 ? "Vector:16" : "Vector:12"; let circleFontBig = circleCount == 3 ? "Vector:16" : "Vector:12";
let iconOffset = circleCount == 3 ? 6 : 8; let iconOffset = circleCount == 3 ? 6 : 8;
let defaultCircleTypes = ["steps", "hr", "battery", "weather"]; let defaultCircleTypes = ["Bangle/Steps", "Bangle/HRM", "Bangle/Battery", "weather"];
let circleInfoNum = [
0, // circle1
0, // circle2
0, // circle3
0, // circle4
];
let circleItemNum = [
0, // circle1
1, // circle2
2, // circle3
3, // circle4
];
function hideWidgets() { function hideWidgets() {
/* /*
@ -177,6 +190,15 @@ function drawCircle(index) {
let w = getCircleXPosition(type); let w = getCircleXPosition(type);
switch (type) { switch (type) {
case "weather":
drawWeather(w);
break;
case "sunprogress":
case "sunProgress":
drawSunProgress(w);
break;
//TODO those are going to be deprecated, keep for backwards compatibility for now
//ideally all data should come from some clkinfo
case "steps": case "steps":
drawSteps(w); drawSteps(w);
break; break;
@ -189,13 +211,6 @@ function drawCircle(index) {
case "battery": case "battery":
drawBattery(w); drawBattery(w);
break; break;
case "weather":
drawWeather(w);
break;
case "sunprogress":
case "sunProgress":
drawSunProgress(w);
break;
case "temperature": case "temperature":
drawTemperature(w); drawTemperature(w);
break; break;
@ -205,9 +220,12 @@ function drawCircle(index) {
case "altitude": case "altitude":
drawAltitude(w); drawAltitude(w);
break; break;
//end deprecated
case "empty": case "empty":
// we draw nothing here // we draw nothing here
return; return;
default:
drawClkInfo(index, w);
} }
} }
@ -304,6 +322,102 @@ function getImage(graphic, color) {
} }
} }
function drawWeather(w) {
if (!w) w = getCircleXPosition("weather");
let weather = getWeather();
let tempString = weather ? locale.temp(weather.temp - 273.15) : undefined;
let code = weather ? weather.code : -1;
drawCircleBackground(w);
let color = getCircleColor("weather");
let percent;
let data = settings.weatherCircleData;
switch (data) {
case "humidity":
let humidity = weather ? weather.hum : undefined;
if (humidity >= 0) {
percent = humidity / 100;
drawGauge(w, h3, percent, color);
}
break;
case "wind":
if (weather) {
let wind = locale.speed(weather.wind).match(/^(\D*\d*)(.*)$/);
if (wind[1] >= 0) {
if (wind[2] == "kmh") {
wind[1] = windAsBeaufort(wind[1]);
}
// wind goes from 0 to 12 (see https://en.wikipedia.org/wiki/Beaufort_scale)
percent = wind[1] / 12;
drawGauge(w, h3, percent, color);
}
}
break;
case "empty":
break;
}
drawInnerCircleAndTriangle(w);
writeCircleText(w, tempString ? tempString : "?");
if (code > 0) {
let icon = getWeatherIconByCode(code);
if (icon) g.drawImage(getImage(icon, getCircleIconColor("weather", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset);
} else {
g.drawString("?", w, h3 + radiusOuter);
}
}
function drawSunProgress(w) {
if (!w) w = getCircleXPosition("sunprogress");
let percent = getSunProgress();
// sunset icons:
let sunSetDown = atob("EBCBAAAAAAABgAAAAAATyAZoBCB//gAAAAAGYAPAAYAAAAAA");
let sunSetUp = atob("EBCBAAAAAAABgAAAAAATyAZoBCB//gAAAAABgAPABmAAAAAA");
drawCircleBackground(w);
let color = getCircleColor("sunprogress");
drawGauge(w, h3, percent, color);
drawInnerCircleAndTriangle(w);
let icon = sunSetDown;
let text = "?";
let times = getSunData();
if (times != undefined) {
let sunRise = Math.round(times.sunrise.getTime() / 1000);
let sunSet = Math.round(times.sunset.getTime() / 1000);
if (!isDay()) {
// night
if (now > sunRise) {
// after sunRise
let upcomingSunRise = sunRise + 60 * 60 * 24;
text = formatSeconds(upcomingSunRise - now);
} else {
text = formatSeconds(sunRise - now);
}
icon = sunSetUp;
} else {
// day, approx sunrise tomorrow:
text = formatSeconds(sunSet - now);
icon = sunSetDown;
}
}
writeCircleText(w, text);
g.drawImage(getImage(icon, getCircleIconColor("sunprogress", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset);
}
/*
* Deprecated but nice as references for clkinfo
*/
function drawSteps(w) { function drawSteps(w) {
if (!w) w = getCircleXPosition("steps"); if (!w) w = getCircleXPosition("steps");
let steps = getSteps(); let steps = getSteps();
@ -406,99 +520,6 @@ function drawBattery(w) {
g.drawImage(getImage(powerIcon, getCircleIconColor("battery", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset); g.drawImage(getImage(powerIcon, getCircleIconColor("battery", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset);
} }
function drawWeather(w) {
if (!w) w = getCircleXPosition("weather");
let weather = getWeather();
let tempString = weather ? locale.temp(weather.temp - 273.15) : undefined;
let code = weather ? weather.code : -1;
drawCircleBackground(w);
let color = getCircleColor("weather");
let percent;
let data = settings.weatherCircleData;
switch (data) {
case "humidity":
let humidity = weather ? weather.hum : undefined;
if (humidity >= 0) {
percent = humidity / 100;
drawGauge(w, h3, percent, color);
}
break;
case "wind":
if (weather) {
let wind = locale.speed(weather.wind).match(/^(\D*\d*)(.*)$/);
if (wind[1] >= 0) {
if (wind[2] == "kmh") {
wind[1] = windAsBeaufort(wind[1]);
}
// wind goes from 0 to 12 (see https://en.wikipedia.org/wiki/Beaufort_scale)
percent = wind[1] / 12;
drawGauge(w, h3, percent, color);
}
}
break;
case "empty":
break;
}
drawInnerCircleAndTriangle(w);
writeCircleText(w, tempString ? tempString : "?");
if (code > 0) {
let icon = getWeatherIconByCode(code);
if (icon) g.drawImage(getImage(icon, getCircleIconColor("weather", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset);
} else {
g.drawString("?", w, h3 + radiusOuter);
}
}
function drawSunProgress(w) {
if (!w) w = getCircleXPosition("sunprogress");
let percent = getSunProgress();
// sunset icons:
let sunSetDown = atob("EBCBAAAAAAABgAAAAAATyAZoBCB//gAAAAAGYAPAAYAAAAAA");
let sunSetUp = atob("EBCBAAAAAAABgAAAAAATyAZoBCB//gAAAAABgAPABmAAAAAA");
drawCircleBackground(w);
let color = getCircleColor("sunprogress");
drawGauge(w, h3, percent, color);
drawInnerCircleAndTriangle(w);
let icon = sunSetDown;
let text = "?";
let times = getSunData();
if (times != undefined) {
let sunRise = Math.round(times.sunrise.getTime() / 1000);
let sunSet = Math.round(times.sunset.getTime() / 1000);
if (!isDay()) {
// night
if (now > sunRise) {
// after sunRise
let upcomingSunRise = sunRise + 60 * 60 * 24;
text = formatSeconds(upcomingSunRise - now);
} else {
text = formatSeconds(sunRise - now);
}
icon = sunSetUp;
} else {
// day, approx sunrise tomorrow:
text = formatSeconds(sunSet - now);
icon = sunSetDown;
}
}
writeCircleText(w, text);
g.drawImage(getImage(icon, getCircleIconColor("sunprogress", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset);
}
function drawTemperature(w) { function drawTemperature(w) {
if (!w) w = getCircleXPosition("temperature"); if (!w) w = getCircleXPosition("temperature");
@ -577,6 +598,113 @@ function drawAltitude(w) {
}); });
} }
function shortValue(v) {
if (isNaN(v)) return '-';
if (v <= 999) return v;
if (v >= 1000 && v < 10000) {
v = Math.floor(v / 100) * 100;
return (v / 1000).toFixed(1).replace(/\.0$/, '') + 'k';
}
if (v >= 10000) {
v = Math.floor(v / 1000) * 1000;
return (v / 1000).toFixed(1).replace(/\.0$/, '') + 'k';
}
}
function getSteps() {
if (Bangle.getHealthStatus) {
return Bangle.getHealthStatus("day").steps;
}
if (WIDGETS && WIDGETS.wpedom !== undefined) {
return WIDGETS.wpedom.getSteps();
}
return 0;
}
function getPressureValue(type) {
return new Promise((resolve) => {
if (Bangle.getPressure) {
if (!pressureLocked) {
pressureLocked = true;
if (pressureCache && pressureCache[type]) {
resolve(pressureCache[type]);
}
Bangle.getPressure().then(function(d) {
pressureLocked = false;
if (d) {
pressureCache = d;
if (d[type]) {
resolve(d[type]);
}
}
}).catch(() => {});
} else {
if (pressureCache && pressureCache[type]) {
resolve(pressureCache[type]);
}
}
}
});
}
/*
* end deprecated
*/
var menu = null;
function reloadMenu() {
menu = clock_info.load();
for(var i=1; i<5; i++)
if(settings['circle'+i].includes("/")) {
let parts = settings['circle'+i].split("/");
let infoName = parts[0], itemName = parts[1];
let infoNum = menu.findIndex(e=>e.name==infoName);
let itemNum = 0;
//suppose unnamed are varying (like timers or events), pick the first
if(itemName)
itemNum = menu[infoNum].items.findIndex(it=>it.name==itemName);
circleInfoNum[i-1] = infoNum;
circleItemNum[i-1] = itemNum;
}
}
//reload periodically for changes?
reloadMenu();
function drawEmpty(img, w, color) {
drawGauge(w, h3, 0, color);
drawInnerCircleAndTriangle(w);
writeCircleText(w, "?");
if(img)
g.setColor(getGradientColor(color, 0))
.drawImage(img, w - iconOffset, h3 + radiusOuter - iconOffset, {scale: 16/24});
}
function drawClkInfo(index, w) {
var info = menu[circleInfoNum[index-1]];
var type = settings['circle'+index];
if (!w) w = getCircleXPosition(type);
drawCircleBackground(w);
const color = getCircleColor(type);
if(!info || !info.items.length) {
drawEmpty(info? info.img : null, w, color);
return;
}
var item = info.items[circleItemNum[index-1]];
//TODO do hide()+get() here
item.show();
item.hide();
item=item.get();
var img = item.img;
if(!img) img = info.img;
let percent = (item.v-item.min) / item.max;
if(isNaN(percent)) percent = 1; //fill it up
drawGauge(w, h3, percent, color);
drawInnerCircleAndTriangle(w);
writeCircleText(w, item.text);
g.setColor(getCircleIconColor(type, color, percent))
.drawImage(img, w - iconOffset, h3 + radiusOuter - iconOffset, {scale: 16/24});
}
/* /*
* wind goes from 0 to 12 (see https://en.wikipedia.org/wiki/Beaufort_scale) * wind goes from 0 to 12 (see https://en.wikipedia.org/wiki/Beaufort_scale)
*/ */
@ -770,125 +898,15 @@ function writeCircleText(w, content) {
g.drawString(content, w, h3); g.drawString(content, w, h3);
} }
function shortValue(v) {
if (isNaN(v)) return '-';
if (v <= 999) return v;
if (v >= 1000 && v < 10000) {
v = Math.floor(v / 100) * 100;
return (v / 1000).toFixed(1).replace(/\.0$/, '') + 'k';
}
if (v >= 10000) {
v = Math.floor(v / 1000) * 1000;
return (v / 1000).toFixed(1).replace(/\.0$/, '') + 'k';
}
}
function getSteps() {
if (Bangle.getHealthStatus) {
return Bangle.getHealthStatus("day").steps;
}
if (WIDGETS && WIDGETS.wpedom !== undefined) {
return WIDGETS.wpedom.getSteps();
}
return 0;
}
function getWeather() { function getWeather() {
let jsonWeather = storage.readJSON('weather.json'); let jsonWeather = storage.readJSON('weather.json');
return jsonWeather && jsonWeather.weather ? jsonWeather.weather : undefined; return jsonWeather && jsonWeather.weather ? jsonWeather.weather : undefined;
} }
function enableHRMSensor() {
Bangle.setHRMPower(1, "circleclock");
if (hrtValue == undefined) {
hrtValue = '...';
drawHeartRate();
}
}
let pressureLocked = false;
let pressureCache;
function getPressureValue(type) {
return new Promise((resolve) => {
if (Bangle.getPressure) {
if (!pressureLocked) {
pressureLocked = true;
if (pressureCache && pressureCache[type]) {
resolve(pressureCache[type]);
}
Bangle.getPressure().then(function(d) {
pressureLocked = false;
if (d) {
pressureCache = d;
if (d[type]) {
resolve(d[type]);
}
}
}).catch(() => {});
} else {
if (pressureCache && pressureCache[type]) {
resolve(pressureCache[type]);
}
}
}
});
}
function onLock(isLocked) {
if (!isLocked) {
draw();
if (isCircleEnabled("hr")) {
enableHRMSensor();
}
} else {
Bangle.setHRMPower(0, "circleclock");
}
}
Bangle.on('lock', onLock);
function onHRM(hrm) {
if (isCircleEnabled("hr")) {
if (hrm.confidence >= (settings.confidence)) {
hrtValue = hrm.bpm;
if (Bangle.isLCDOn()) {
drawHeartRate();
}
}
// Let us wait before we overwrite "good" HRM values:
if (Bangle.isLCDOn()) {
if (timerHrm) clearTimeout(timerHrm);
timerHrm = setTimeout(() => {
hrtValue = '...';
drawHeartRate();
}, settings.hrmValidity * 1000);
}
}
}
Bangle.on('HRM', onHRM);
function onCharging(charging) {
if (isCircleEnabled("battery")) drawBattery();
}
Bangle.on('charging', onCharging);
if (isCircleEnabled("hr")) {
enableHRMSensor();
}
Bangle.setUI({ Bangle.setUI({
mode : "clock", mode : "clock",
remove : function() { remove : function() {
// Called to unload all of the clock app // Called to unload all of the clock app
Bangle.removeListener('charging', onCharging);
Bangle.removeListener('lock', onLock);
Bangle.removeListener('HRM', onHRM);
Bangle.setHRMPower(0, "circleclock");
if (timerHrm) clearTimeout(timerHrm);
timerHrm = undefined;
if (drawTimeout) clearTimeout(drawTimeout); if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined; drawTimeout = undefined;

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,7 +15,15 @@
"circle2colorizeIcon": true, "circle2colorizeIcon": true,
"circle3colorizeIcon": true, "circle3colorizeIcon": true,
"circle4colorizeIcon": false, "circle4colorizeIcon": false,
"hrmValidity": 60,
"updateInterval": 60, "updateInterval": 60,
"showBigWeather": false "showBigWeather": false,
"minHR": 40,
"maxHR": 200,
"confidence": 0,
"stepGoal": 10000,
"stepDistanceGoal": 8000,
"stepLength": 0.8,
"hrmValidity": 60
} }

View File

@ -1,7 +1,7 @@
{ "id": "circlesclock", { "id": "circlesclock",
"name": "Circles clock", "name": "Circles clock",
"shortName":"Circles clock", "shortName":"Circles clock",
"version":"0.16", "version":"0.17",
"description": "A clock with three or four circles for different data at the bottom in a probably familiar style", "description": "A clock with three or four circles for different data at the bottom in a probably familiar style",
"icon": "app.png", "icon": "app.png",
"screenshots": [{"url":"screenshot-dark.png"}, {"url":"screenshot-light.png"}, {"url":"screenshot-dark-4.png"}, {"url":"screenshot-light-4.png"}], "screenshots": [{"url":"screenshot-dark.png"}, {"url":"screenshot-light.png"}, {"url":"screenshot-dark-4.png"}, {"url":"screenshot-light-4.png"}],

View File

@ -1,6 +1,7 @@
(function(back) { (function(back) {
const SETTINGS_FILE = "circlesclock.json"; const SETTINGS_FILE = "circlesclock.json";
const storage = require('Storage'); const storage = require('Storage');
const clock_info = require("clock_info");
let settings = Object.assign( let settings = Object.assign(
storage.readJSON("circlesclock.default.json", true) || {}, storage.readJSON("circlesclock.default.json", true) || {},
storage.readJSON(SETTINGS_FILE, true) || {} storage.readJSON(SETTINGS_FILE, true) || {}
@ -11,8 +12,25 @@
storage.write(SETTINGS_FILE, settings); storage.write(SETTINGS_FILE, settings);
} }
const valuesCircleTypes = ["empty", "steps", "stepsDist", "hr", "battery", "weather", "sunprogress", "temperature", "pressure", "altitude"]; //const valuesCircleTypes = ["empty", "steps", "stepsDist", "hr", "battery", "weather", "sunprogress", "temperature", "pressure", "altitude", "timer"];
const namesCircleTypes = ["empty", "steps", "distance", "heart", "battery", "weather", "sun", "temperature", "pressure", "altitude"]; //const namesCircleTypes = ["empty", "steps", "distance", "heart", "battery", "weather", "sun", "temperature", "pressure", "altitude", "timer"];
var valuesCircleTypes = ["empty","weather", "sunprogress"];
var namesCircleTypes = ["empty","weather", "sun"];
clock_info.load().forEach(e=>{
//TODO filter for hasRange and other
if(!e.items.length || !e.items[0].name) {
//suppose unnamed are varying (like timers or events), pick the first
item = e.items[0];
valuesCircleTypes = valuesCircleTypes.concat([e.name+"/"]);
namesCircleTypes = namesCircleTypes.concat([e.name]);
} else {
let values = e.items.map(i=>e.name+"/"+i.name);
let names =e.name=="Bangle" ? e.items.map(i=>i.name) : values;
valuesCircleTypes = valuesCircleTypes.concat(values);
namesCircleTypes = namesCircleTypes.concat(names);
}
})
const valuesColors = ["", "#ff0000", "#00ff00", "#0000ff", "#ffff00", "#ff00ff", const valuesColors = ["", "#ff0000", "#00ff00", "#0000ff", "#ffff00", "#ff00ff",
"#00ffff", "#fff", "#000", "green-red", "red-green", "fg"]; "#00ffff", "#fff", "#000", "green-red", "red-green", "fg"];
@ -36,8 +54,6 @@
/*LANG*/'circle 2': ()=>showCircleMenu(2), /*LANG*/'circle 2': ()=>showCircleMenu(2),
/*LANG*/'circle 3': ()=>showCircleMenu(3), /*LANG*/'circle 3': ()=>showCircleMenu(3),
/*LANG*/'circle 4': ()=>showCircleMenu(4), /*LANG*/'circle 4': ()=>showCircleMenu(4),
/*LANG*/'heartrate': ()=>showHRMenu(),
/*LANG*/'steps': ()=>showStepMenu(),
/*LANG*/'battery warn': { /*LANG*/'battery warn': {
value: settings.batteryWarn, value: settings.batteryWarn,
min: 10, min: 10,
@ -78,91 +94,6 @@
E.showMenu(menu); E.showMenu(menu);
} }
function showHRMenu() {
let menu = {
'': { 'title': /*LANG*/'Heartrate' },
/*LANG*/'< Back': ()=>showMainMenu(),
/*LANG*/'minimum': {
value: settings.minHR,
min: 0,
max : 250,
step: 5,
format: x => {
return x + " bpm";
},
onchange: x => save('minHR', x),
},
/*LANG*/'maximum': {
value: settings.maxHR,
min: 20,
max : 250,
step: 5,
format: x => {
return x + " bpm";
},
onchange: x => save('maxHR', x),
},
/*LANG*/'min. confidence': {
value: settings.confidence,
min: 0,
max : 100,
step: 10,
format: x => {
return x + "%";
},
onchange: x => save('confidence', x),
},
/*LANG*/'valid period': {
value: settings.hrmValidity,
min: 10,
max : 1800,
step: 10,
format: x => {
return x + "s";
},
onchange: x => save('hrmValidity', x),
},
};
E.showMenu(menu);
}
function showStepMenu() {
let menu = {
'': { 'title': /*LANG*/'Steps' },
/*LANG*/'< Back': ()=>showMainMenu(),
/*LANG*/'goal': {
value: settings.stepGoal,
min: 1000,
max : 50000,
step: 500,
format: x => {
return x;
},
onchange: x => save('stepGoal', x),
},
/*LANG*/'distance goal': {
value: settings.stepDistanceGoal,
min: 1000,
max : 50000,
step: 500,
format: x => {
return x;
},
onchange: x => save('stepDistanceGoal', x),
},
/*LANG*/'step length': {
value: settings.stepLength,
min: 0.1,
max : 1.5,
step: 0.01,
format: x => {
return x;
},
onchange: x => save('stepLength', x),
}
};
E.showMenu(menu);
}
function showCircleMenu(circleId) { function showCircleMenu(circleId) {
const circleName = "circle" + circleId; const circleName = "circle" + circleId;
const colorKey = circleName + "color"; const colorKey = circleName + "color";
@ -192,6 +123,5 @@
E.showMenu(menu); E.showMenu(menu);
} }
showMainMenu(); showMainMenu();
}); });

View File

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

BIN
apps/clkinfosunrise/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 661 B

View File

@ -0,0 +1,33 @@
(function() {
// get today's sunlight times for lat/lon
var sunrise, sunset;
function calculate() {
var SunCalc = require("https://raw.githubusercontent.com/mourner/suncalc/master/suncalc.js");
const locale = require("locale");
var location = require("Storage").readJSON("mylocation.json",1)||{};
location.lat = location.lat||51.5072;
location.lon = location.lon||0.1276;
location.location = location.location||"London";
var times = SunCalc.getTimes(new Date(), location.lat, location.lon);
sunrise = locale.time(times.sunrise,1);
sunset = locale.time(times.sunset,1);
/* do we want to re-calculate this every day? Or we just assume
that 'show' will get called once a day? */
}
return {
name: "Bangle",
items: [
{ name : "Sunrise",
get : () => ({ text : sunrise,
img : atob("GBiBAAAAAAAAAAAAAAAYAAA8AAB+AAD/AAAAAAAAAAAAAAAYAAAYAAQYIA4AcAYAYAA8AAB+AAD/AAH/gD///D///AAAAAAAAAAAAA==") }),
show : calculate, hide : () => {}
}, { name : "Sunset",
get : () => ({ text : sunset,
img : atob("GBiBAAAAAAAAAAAAAAB+AAA8AAAYAAAYAAAAAAAAAAAAAAAYAAAYAAQYIA4AcAYAYAA8AAB+AAD/AAH/gD///D///AAAAAAAAAAAAA==") }),
show : calculate, hide : () => {}
}
]
};
})

View File

@ -0,0 +1,12 @@
{ "id": "clkinfosunrise",
"name": "Sunrise Clockinfo",
"version":"0.01",
"description": "For clocks that display 'clockinfo' (messages that can be cycled through using the clock_info module) this displays sunrise and sunset based on the location from the 'My Location' app",
"icon": "app.png",
"type": "clkinfo",
"tags": "clkinfo,sunrise",
"supports" : ["BANGLEJS2"],
"storage": [
{"name":"sunrise.clkinfo.js","url":"clkinfo.js"}
]
}

View File

@ -12,6 +12,5 @@
"storage": [ "storage": [
{"name":"demoapp.app.js","url":"app.js"}, {"name":"demoapp.app.js","url":"app.js"},
{"name":"demoapp.img","url":"app-icon.js","evaluate":true} {"name":"demoapp.img","url":"app-icon.js","evaluate":true}
], ]
"sortorder": -9
} }

View File

@ -3,3 +3,4 @@
0.03: Made the code shorter and somewhat more readable by writing some functions. Also made it work as a library where it returns the text once finished. The keyboard is now made to exit correctly when the 'back' event is called. The keyboard now uses theme colors correctly, although it still looks best with dark theme. The numbers row is now solidly green - except for highlights. 0.03: Made the code shorter and somewhat more readable by writing some functions. Also made it work as a library where it returns the text once finished. The keyboard is now made to exit correctly when the 'back' event is called. The keyboard now uses theme colors correctly, although it still looks best with dark theme. The numbers row is now solidly green - except for highlights.
0.04: Now displays the opened text string at launch. 0.04: Now displays the opened text string at launch.
0.05: Now scrolls text when string gets longer than screen width. 0.05: Now scrolls text when string gets longer than screen width.
0.06: The code is now more reliable and the input snappier. Widgets will be drawn if present.

View File

@ -1,12 +1,9 @@
//Keep banglejs screen on for 100 sec at 0.55 power level for development purposes
//Bangle.setLCDTimeout(30);
//Bangle.setLCDPower(1);
exports.input = function(options) { exports.input = function(options) {
options = options||{}; options = options||{};
var text = options.text; var text = options.text;
if ("string"!=typeof text) text=""; if ("string"!=typeof text) text="";
var R = Bangle.appRect;
var BGCOLOR = g.theme.bg; var BGCOLOR = g.theme.bg;
var HLCOLOR = g.theme.fg; var HLCOLOR = g.theme.fg;
var ABCCOLOR = g.toColor(1,0,0);//'#FF0000'; var ABCCOLOR = g.toColor(1,0,0);//'#FF0000';
@ -17,35 +14,38 @@ exports.input = function(options) {
var SMALLFONTWIDTH = parseInt(SMALLFONT.charAt(0)*parseInt(SMALLFONT.charAt(-1))); var SMALLFONTWIDTH = parseInt(SMALLFONT.charAt(0)*parseInt(SMALLFONT.charAt(-1)));
var ABC = 'abcdefghijklmnopqrstuvwxyz'.toUpperCase(); var ABC = 'abcdefghijklmnopqrstuvwxyz'.toUpperCase();
var ABCPADDING = (g.getWidth()-6*ABC.length)/2; var ABCPADDING = ((R.x+R.w)-6*ABC.length)/2;
var NUM = ' 1234567890!?,.- '; var NUM = ' 1234567890!?,.- ';
var NUMHIDDEN = ' 1234567890!?,.- '; var NUMHIDDEN = ' 1234567890!?,.- ';
var NUMPADDING = (g.getWidth()-6*NUM.length)/2; var NUMPADDING = ((R.x+R.w)-6*NUM.length)/2;
var rectHeight = 40; var rectHeight = 40;
var delSpaceLast; var delSpaceLast;
function drawAbcRow() { function drawAbcRow() {
g.clear(); g.clear();
try { // Draw widgets if they are present in the current app.
if (WIDGETS) Bangle.drawWidgets();
} catch (_) {}
g.setFont(SMALLFONT); g.setFont(SMALLFONT);
g.setColor(ABCCOLOR); g.setColor(ABCCOLOR);
g.drawString(ABC, ABCPADDING, g.getHeight()/2); g.setFontAlign(-1, -1, 0);
g.fillRect(0, g.getHeight()-26, g.getWidth(), g.getHeight()); g.drawString(ABC, ABCPADDING, (R.y+R.h)/2);
g.fillRect(0, (R.y+R.h)-26, (R.x+R.w), (R.y+R.h));
} }
function drawNumRow() { function drawNumRow() {
g.setFont(SMALLFONT); g.setFont(SMALLFONT);
g.setColor(NUMCOLOR); g.setColor(NUMCOLOR);
g.drawString(NUM, NUMPADDING, g.getHeight()/4); g.setFontAlign(-1, -1, 0);
g.drawString(NUM, NUMPADDING, (R.y+R.h)/4);
g.fillRect(NUMPADDING, g.getHeight()-rectHeight*4/3, g.getWidth()-NUMPADDING, g.getHeight()-rectHeight*2/3); g.fillRect(NUMPADDING, (R.y+R.h)-rectHeight*4/3, (R.x+R.w)-NUMPADDING, (R.y+R.h)-rectHeight*2/3);
} }
function updateTopString() { function updateTopString() {
"ram"
g.setColor(BGCOLOR); g.setColor(BGCOLOR);
g.fillRect(0,4+20,176,13+20); g.fillRect(0,4+20,176,13+20);
g.setColor(0.2,0,0); g.setColor(0.2,0,0);
@ -54,13 +54,10 @@ exports.input = function(options) {
g.setColor(0.7,0,0); g.setColor(0.7,0,0);
g.fillRect(rectLen+5,4+20,rectLen+10,13+20); g.fillRect(rectLen+5,4+20,rectLen+10,13+20);
g.setColor(1,1,1); g.setColor(1,1,1);
g.setFontAlign(-1, -1, 0);
g.drawString(text.length<=27? text.substr(-27, 27) : '<- '+text.substr(-24,24), 5, 5+20); g.drawString(text.length<=27? text.substr(-27, 27) : '<- '+text.substr(-24,24), 5, 5+20);
} }
drawAbcRow();
drawNumRow();
updateTopString();
var abcHL; var abcHL;
var abcHLPrev = -10; var abcHLPrev = -10;
var numHL; var numHL;
@ -70,192 +67,180 @@ exports.input = function(options) {
var largeCharOffset = 6; var largeCharOffset = 6;
function resetChars(char, HLPrev, typePadding, heightDivisor, rowColor) { function resetChars(char, HLPrev, typePadding, heightDivisor, rowColor) {
"ram" "ram";
// Small character in list // Small character in list
g.setColor(rowColor); g.setColor(rowColor);
g.setFont(SMALLFONT); g.setFont(SMALLFONT);
g.drawString(char, typePadding + HLPrev*6, g.getHeight()/heightDivisor); g.setFontAlign(-1, -1, 0);
g.drawString(char, typePadding + HLPrev*6, (R.y+R.h)/heightDivisor);
// Large character // Large character
g.setColor(BGCOLOR); g.setColor(BGCOLOR);
g.fillRect(0,g.getHeight()/3,176,g.getHeight()/3+24); g.fillRect(0,(R.y+R.h)/3,176,(R.y+R.h)/3+24);
//g.drawString(charSet.charAt(HLPrev), typePadding + HLPrev*6 -largeCharOffset, g.getHeight()/3);; //Old implementation where I find the shape and place of letter to remove instead of just a rectangle. //g.drawString(charSet.charAt(HLPrev), typePadding + HLPrev*6 -largeCharOffset, (R.y+R.h)/3);; //Old implementation where I find the shape and place of letter to remove instead of just a rectangle.
// mark in the list // mark in the list
} }
function showChars(char, HL, typePadding, heightDivisor) { function showChars(char, HL, typePadding, heightDivisor) {
"ram" "ram";
// mark in the list // mark in the list
g.setColor(HLCOLOR); g.setColor(HLCOLOR);
g.setFont(SMALLFONT); g.setFont(SMALLFONT);
if (char != 'del' && char != 'space') g.drawString(char, typePadding + HL*6, g.getHeight()/heightDivisor); g.setFontAlign(-1, -1, 0);
if (char != 'del' && char != 'space') g.drawString(char, typePadding + HL*6, (R.y+R.h)/heightDivisor);
// show new large character // show new large character
g.setFont(BIGFONT); g.setFont(BIGFONT);
g.drawString(char, typePadding + HL*6 -largeCharOffset, g.getHeight()/3); g.drawString(char, typePadding + HL*6 -largeCharOffset, (R.y+R.h)/3);
g.setFont(SMALLFONT); g.setFont(SMALLFONT);
} }
function initDraw() {
//var R = Bangle.appRect; // To make sure it's properly updated. Not sure if this is needed.
drawAbcRow();
drawNumRow();
updateTopString();
}
initDraw();
//setTimeout(initDraw, 0); // So Bangle.appRect reads the correct environment. It would draw off to the side sometimes otherwise.
function changeCase(abcHL) { function changeCase(abcHL) {
g.setColor(BGCOLOR); g.setColor(BGCOLOR);
g.drawString(ABC, ABCPADDING, g.getHeight()/2); g.setFontAlign(-1, -1, 0);
g.drawString(ABC, ABCPADDING, (R.y+R.h)/2);
if (ABC.charAt(abcHL) == ABC.charAt(abcHL).toUpperCase()) ABC = ABC.toLowerCase(); if (ABC.charAt(abcHL) == ABC.charAt(abcHL).toUpperCase()) ABC = ABC.toLowerCase();
else ABC = ABC.toUpperCase(); else ABC = ABC.toUpperCase();
g.setColor(ABCCOLOR); g.setColor(ABCCOLOR);
g.drawString(ABC, ABCPADDING, g.getHeight()/2); g.drawString(ABC, ABCPADDING, (R.y+R.h)/2);
} }
return new Promise((resolve,reject) => { return new Promise((resolve,reject) => {
// Interpret touch input // Interpret touch input
Bangle.setUI({ Bangle.setUI({
mode: 'custom', mode: 'custom',
back: ()=>{ back: ()=>{
Bangle.setUI(); Bangle.setUI();
g.clearRect(Bangle.appRect); g.clearRect(Bangle.appRect);
resolve(text); resolve(text);
}, },
drag: function(event) { drag: function(event) {
"ram";
// ABCDEFGHIJKLMNOPQRSTUVWXYZ
// Choose character by draging along red rectangle at bottom of screen
if (event.y >= ( (R.y+R.h) - 12 )) {
// Translate x-position to character
if (event.x < ABCPADDING) { abcHL = 0; }
else if (event.x >= 176-ABCPADDING) { abcHL = 25; }
else { abcHL = Math.floor((event.x-ABCPADDING)/6); }
// ABCDEFGHIJKLMNOPQRSTUVWXYZ // Datastream for development purposes
// Choose character by draging along red rectangle at bottom of screen //print(event.x, event.y, event.b, ABC.charAt(abcHL), ABC.charAt(abcHLPrev));
if (event.y >= ( g.getHeight() - 12 )) {
// Translate x-position to character
if (event.x < ABCPADDING) { abcHL = 0; }
else if (event.x >= 176-ABCPADDING) { abcHL = 25; }
else { abcHL = Math.floor((event.x-ABCPADDING)/6); }
// Datastream for development purposes // Unmark previous character and mark the current one...
//print(event.x, event.y, event.b, ABC.charAt(abcHL), ABC.charAt(abcHLPrev)); // Handling switching between letters and numbers/punctuation
if (typePrev != 'abc') resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
// Unmark previous character and mark the current one... if (abcHL != abcHLPrev) {
// Handling switching between letters and numbers/punctuation resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
if (typePrev != 'abc') resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR); showChars(ABC.charAt(abcHL), abcHL, ABCPADDING, 2);
if (abcHL != abcHLPrev) {
resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
showChars(ABC.charAt(abcHL), abcHL, ABCPADDING, 2);
} }
// Print string at top of screen // Print string at top of screen
if (event.b == 0) { if (event.b == 0) {
text = text + ABC.charAt(abcHL); text = text + ABC.charAt(abcHL);
updateTopString();
// Autoswitching letter case
if (ABC.charAt(abcHL) == ABC.charAt(abcHL).toUpperCase()) changeCase(abcHL);
}
// Update previous character to current one
abcHLPrev = abcHL;
typePrev = 'abc';
}
// 12345678901234567890
// Choose number or puctuation by draging on green rectangle
else if ((event.y < ( g.getHeight() - 12 )) && (event.y > ( g.getHeight() - 52 ))) {
// Translate x-position to character
if (event.x < NUMPADDING) { numHL = 0; }
else if (event.x > 176-NUMPADDING) { numHL = NUM.length-1; }
else { numHL = Math.floor((event.x-NUMPADDING)/6); }
// Datastream for development purposes
//print(event.x, event.y, event.b, NUM.charAt(numHL), NUM.charAt(numHLPrev));
// Unmark previous character and mark the current one...
// Handling switching between letters and numbers/punctuation
if (typePrev != 'num') resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
if (numHL != numHLPrev) {
resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
showChars(NUM.charAt(numHL), numHL, NUMPADDING, 4);
}
// Print string at top of screen
if (event.b == 0) {
g.setColor(HLCOLOR);
// Backspace if releasing before list of numbers/punctuation
if (event.x < NUMPADDING) {
// show delete sign
showChars('del', 0, g.getWidth()/2 +6 -27 , 4);
delSpaceLast = 1;
text = text.slice(0, -1);
updateTopString();
//print(text);
}
// Append space if releasing after list of numbers/punctuation
else if (event.x > g.getWidth()-NUMPADDING) {
//show space sign
showChars('space', 0, g.getWidth()/2 +6 -6*3*5/2 , 4);
delSpaceLast = 1;
text = text + ' ';
updateTopString();
//print(text);
}
// Append selected number/punctuation
else {
text = text + NUMHIDDEN.charAt(numHL);
updateTopString(); updateTopString();
// Autoswitching letter case // Autoswitching letter case
if ((text.charAt(text.length-1) == '.') || (text.charAt(text.length-1) == '!')) changeCase(); if (ABC.charAt(abcHL) == ABC.charAt(abcHL).toUpperCase()) changeCase(abcHL);
} }
// Update previous character to current one
abcHLPrev = abcHL;
typePrev = 'abc';
} }
// Update previous character to current one
numHLPrev = numHL;
typePrev = 'num';
}
// 12345678901234567890
// Choose number or puctuation by draging on green rectangle
else if ((event.y < ( (R.y+R.h) - 12 )) && (event.y > ( (R.y+R.h) - 52 ))) {
// Translate x-position to character
if (event.x < NUMPADDING) { numHL = 0; }
else if (event.x > 176-NUMPADDING) { numHL = NUM.length-1; }
else { numHL = Math.floor((event.x-NUMPADDING)/6); }
// Datastream for development purposes
//print(event.x, event.y, event.b, NUM.charAt(numHL), NUM.charAt(numHLPrev));
// Unmark previous character and mark the current one...
// Handling switching between letters and numbers/punctuation
if (typePrev != 'num') resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
if (numHL != numHLPrev) {
// Make a space or backspace by swiping right or left on screen above green rectangle
else if (event.y > 20+4) {
if (event.b == 0) {
g.setColor(HLCOLOR);
if (event.x < g.getWidth()/2) {
resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR); resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
showChars(NUM.charAt(numHL), numHL, NUMPADDING, 4);
// show delete sign
showChars('del', 0, g.getWidth()/2 +6 -27 , 4);
delSpaceLast = 1;
// Backspace and draw string upper right corner
text = text.slice(0, -1);
updateTopString();
if (text.length==0) changeCase(abcHL);
//print(text, 'undid');
} }
else { // Print string at top of screen
resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR); if (event.b == 0) {
resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR); g.setColor(HLCOLOR);
// Backspace if releasing before list of numbers/punctuation
if (event.x < NUMPADDING) {
// show delete sign
showChars('del', 0, (R.x+R.w)/2 +6 -27 , 4);
delSpaceLast = 1;
text = text.slice(0, -1);
updateTopString();
//print(text);
}
// Append space if releasing after list of numbers/punctuation
else if (event.x > (R.x+R.w)-NUMPADDING) {
//show space sign
showChars('space', 0, (R.x+R.w)/2 +6 -6*3*5/2 , 4);
delSpaceLast = 1;
text = text + ' ';
updateTopString();
//print(text);
}
// Append selected number/punctuation
else {
text = text + NUMHIDDEN.charAt(numHL);
updateTopString();
//show space sign // Autoswitching letter case
showChars('space', 0, g.getWidth()/2 +6 -6*3*5/2 , 4); if ((text.charAt(text.length-1) == '.') || (text.charAt(text.length-1) == '!')) changeCase();
delSpaceLast = 1; }
}
// Update previous character to current one
numHLPrev = numHL;
typePrev = 'num';
}
// Append space and draw string upper right corner // Make a space or backspace by swiping right or left on screen above green rectangle
text = text + NUMHIDDEN.charAt(0); else if (event.y > 20+4) {
updateTopString(); if (event.b == 0) {
//print(text, 'made space'); g.setColor(HLCOLOR);
if (event.x < (R.x+R.w)/2) {
resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
// show delete sign
showChars('del', 0, (R.x+R.w)/2 +6 -27 , 4);
delSpaceLast = 1;
// Backspace and draw string upper right corner
text = text.slice(0, -1);
updateTopString();
if (text.length==0) changeCase(abcHL);
//print(text, 'undid');
}
else {
resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
//show space sign
showChars('space', 0, (R.x+R.w)/2 +6 -6*3*5/2 , 4);
delSpaceLast = 1;
// Append space and draw string upper right corner
text = text + NUMHIDDEN.charAt(0);
updateTopString();
//print(text, 'made space');
}
} }
} }
} }
} });
}); });
});
/* return new Promise((resolve,reject) => {
Bangle.setUI({mode:"custom", back:()=>{
Bangle.setUI();
g.clearRect(Bangle.appRect);
Bangle.setUI();
resolve(text);
}});
}); */
}; };

View File

@ -1,6 +1,6 @@
{ "id": "dragboard", { "id": "dragboard",
"name": "Dragboard", "name": "Dragboard",
"version":"0.05", "version":"0.06",
"description": "A library for text input via swiping keyboard", "description": "A library for text input via swiping keyboard",
"icon": "app.png", "icon": "app.png",
"type":"textinput", "type":"textinput",

View File

@ -14,3 +14,13 @@
0.14: Don't move pages when doing exit swipe - Bangle 2. 0.14: Don't move pages when doing exit swipe - Bangle 2.
0.15: 'Swipe to exit'-code is slightly altered to be more reliable - Bangle 2. 0.15: 'Swipe to exit'-code is slightly altered to be more reliable - Bangle 2.
0.16: Use default Bangle formatter for booleans 0.16: Use default Bangle formatter for booleans
0.17: Bangle 2: Fast loading on exit to clock face. Added option for exit to
clock face by timeout.
0.18: Bangle 2: Move interactions inside setUI. Replace "one click exit" with
back-functionality through setUI, adding the red back button as well. Hardware
button to exit is no longer an option.
0.19: Bangle 2: Utilize new Bangle.load(), Bangle.showClock() functions to
facilitate 'fast switching' of apps where available.
0.20: Bangle 2: Revert use of Bangle.load() to classic load() calls since
widgets would still be loaded when they weren't supposed to.

View File

@ -1,61 +1,59 @@
/* Desktop launcher { // must be inside our own scope here so that when we are unloaded everything disappears
*
*/
var settings = Object.assign({ /* Desktop launcher
showClocks: true, *
showLaunchers: true, */
direct: false,
oneClickExit:false,
swipeExit: false
}, require('Storage').readJSON("dtlaunch.json", true) || {});
if( settings.oneClickExit) let settings = Object.assign({
setWatch(_=> load(), BTN1); showClocks: true,
showLaunchers: true,
direct: false,
swipeExit: false,
timeOut: "Off"
}, require('Storage').readJSON("dtlaunch.json", true) || {});
var s = require("Storage"); let s = require("Storage");
var apps = s.list(/\.info$/).map(app=>{ var apps = s.list(/\.info$/).map(app=>{
var a=s.readJSON(app,1); let a=s.readJSON(app,1);
return a && { return a && {
name:a.name, type:a.type, icon:a.icon, sortorder:a.sortorder, src:a.src name:a.name, type:a.type, icon:a.icon, sortorder:a.sortorder, src:a.src
};}).filter( };}).filter(
app=>app && (app.type=="app" || (app.type=="clock" && settings.showClocks) || (app.type=="launch" && settings.showLaunchers) || !app.type)); app=>app && (app.type=="app" || (app.type=="clock" && settings.showClocks) || (app.type=="launch" && settings.showLaunchers) || !app.type));
apps.sort((a,b)=>{ apps.sort((a,b)=>{
var n=(0|a.sortorder)-(0|b.sortorder); let n=(0|a.sortorder)-(0|b.sortorder);
if (n) return n; // do sortorder first if (n) return n; // do sortorder first
if (a.name<b.name) return -1; if (a.name<b.name) return -1;
if (a.name>b.name) return 1; if (a.name>b.name) return 1;
return 0; return 0;
}); });
apps.forEach(app=>{ apps.forEach(app=>{
if (app.icon) if (app.icon)
app.icon = s.read(app.icon); // should just be a link to a memory area app.icon = s.read(app.icon); // should just be a link to a memory area
}); });
var Napps = apps.length; let Napps = apps.length;
var Npages = Math.ceil(Napps/4); let Npages = Math.ceil(Napps/4);
var maxPage = Npages-1; let maxPage = Npages-1;
var selected = -1; let selected = -1;
var oldselected = -1; let oldselected = -1;
var page = 0; let page = 0;
const XOFF = 24; const XOFF = 24;
const YOFF = 30; const YOFF = 30;
function draw_icon(p,n,selected) { let drawIcon= function(p,n,selected) {
var x = (n%2)*72+XOFF; let x = (n%2)*72+XOFF;
var y = n>1?72+YOFF:YOFF; let y = n>1?72+YOFF:YOFF;
(selected?g.setColor(g.theme.fgH):g.setColor(g.theme.bg)).fillRect(x+11,y+3,x+60,y+52); (selected?g.setColor(g.theme.fgH):g.setColor(g.theme.bg)).fillRect(x+11,y+3,x+60,y+52);
g.clearRect(x+12,y+4,x+59,y+51); g.clearRect(x+12,y+4,x+59,y+51);
g.setColor(g.theme.fg); g.setColor(g.theme.fg);
try{g.drawImage(apps[p*4+n].icon,x+12,y+4);} catch(e){} try{g.drawImage(apps[p*4+n].icon,x+12,y+4);} catch(e){}
g.setFontAlign(0,-1,0).setFont("6x8",1); g.setFontAlign(0,-1,0).setFont("6x8",1);
var txt = apps[p*4+n].name.replace(/([a-z])([A-Z])/g, "$1 $2").split(" "); let txt = apps[p*4+n].name.replace(/([a-z])([A-Z])/g, "$1 $2").split(" ");
var lineY = 0; let lineY = 0;
var line = ""; let line = "";
while (txt.length > 0){ while (txt.length > 0){
var c = txt.shift(); let c = txt.shift();
if (c.length + 1 + line.length > 13){ if (c.length + 1 + line.length > 13){
if (line.length > 0){ if (line.length > 0){
g.drawString(line.trim(),x+36,y+54+lineY*8); g.drawString(line.trim(),x+36,y+54+lineY*8);
@ -67,70 +65,91 @@ function draw_icon(p,n,selected) {
} }
} }
g.drawString(line.trim(),x+36,y+54+lineY*8); g.drawString(line.trim(),x+36,y+54+lineY*8);
} };
function drawPage(p){ let drawPage = function(p){
g.reset(); g.reset();
g.clearRect(0,24,175,175); g.clearRect(0,24,175,175);
var O = 88+YOFF/2-12*(Npages/2); let O = 88+YOFF/2-12*(Npages/2);
for (var j=0;j<Npages;j++){ for (let j=0;j<Npages;j++){
var y = O+j*12; let y = O+j*12;
g.setColor(g.theme.fg); g.setColor(g.theme.fg);
if (j==page) g.fillCircle(XOFF/2,y,4); if (j==page) g.fillCircle(XOFF/2,y,4);
else g.drawCircle(XOFF/2,y,4); else g.drawCircle(XOFF/2,y,4);
} }
for (var i=0;i<4;i++) { for (let i=0;i<4;i++) {
if (!apps[p*4+i]) return i; if (!apps[p*4+i]) return i;
draw_icon(p,i,selected==i && !settings.direct); drawIcon(p,i,selected==i && !settings.direct);
} }
g.flip(); g.flip();
} };
Bangle.on("swipe",(dirLeftRight, dirUpDown)=>{ Bangle.loadWidgets();
drawPage(0);
let swipeListenerDt = function(dirLeftRight, dirUpDown){
updateTimeoutToClock();
selected = 0; selected = 0;
oldselected=-1; oldselected=-1;
if(settings.swipeExit && dirLeftRight==1) load(); if(settings.swipeExit && dirLeftRight==1) Bangle.showClock();
if (dirUpDown==-1||dirLeftRight==-1){ if (dirUpDown==-1||dirLeftRight==-1){
++page; if (page>maxPage) page=0; ++page; if (page>maxPage) page=0;
drawPage(page); drawPage(page);
} else if (dirUpDown==1||(dirLeftRight==1 && !settings.swipeExit)){ } else if (dirUpDown==1||(dirLeftRight==1 && !settings.swipeExit)){
--page; if (page<0) page=maxPage; --page; if (page<0) page=maxPage;
drawPage(page); drawPage(page);
} }
}); };
function isTouched(p,n){ let isTouched = function(p,n){
if (n<0 || n>3) return false; if (n<0 || n>3) return false;
var x1 = (n%2)*72+XOFF; var y1 = n>1?72+YOFF:YOFF; let x1 = (n%2)*72+XOFF; let y1 = n>1?72+YOFF:YOFF;
var x2 = x1+71; var y2 = y1+81; let x2 = x1+71; let y2 = y1+81;
return (p.x>x1 && p.y>y1 && p.x<x2 && p.y<y2); return (p.x>x1 && p.y>y1 && p.x<x2 && p.y<y2);
} };
Bangle.on("touch",(_,p)=>{ let touchListenerDt = function(_,p){
var i; updateTimeoutToClock();
let i;
for (i=0;i<4;i++){ for (i=0;i<4;i++){
if((page*4+i)<Napps){ if((page*4+i)<Napps){
if (isTouched(p,i)) { if (isTouched(p,i)) {
draw_icon(page,i,true && !settings.direct); drawIcon(page,i,true && !settings.direct);
if (selected>=0 || settings.direct) { if (selected>=0 || settings.direct) {
if (selected!=i && !settings.direct){ if (selected!=i && !settings.direct){
draw_icon(page,selected,false); drawIcon(page,selected,false);
} else { } else {
load(apps[page*4+i].src); load(apps[page*4+i].src);
}
}
selected=i;
break;
} }
}
selected=i;
break;
} }
}
} }
if ((i==4 || (page*4+i)>Napps) && selected>=0) { if ((i==4 || (page*4+i)>Napps) && selected>=0) {
draw_icon(page,selected,false); drawIcon(page,selected,false);
selected=-1; selected=-1;
} }
}); };
Bangle.loadWidgets(); Bangle.setUI({
g.clear(); mode : 'custom',
Bangle.drawWidgets(); back : Bangle.showClock,
drawPage(0); swipe : swipeListenerDt,
touch : touchListenerDt,
remove : ()=>{if (timeoutToClock) clearTimeout(timeoutToClock);}
});
// taken from Icon Launcher with minor alterations
let timeoutToClock;
const updateTimeoutToClock = function(){
if (settings.timeOut!="Off"){
let time=parseInt(settings.timeOut); //the "s" will be trimmed by the parseInt
if (timeoutToClock) clearTimeout(timeoutToClock);
timeoutToClock = setTimeout(Bangle.showClock,time*1000);
}
};
updateTimeoutToClock();
} // end of app scope

View File

@ -1,7 +1,7 @@
{ {
"id": "dtlaunch", "id": "dtlaunch",
"name": "Desktop Launcher", "name": "Desktop Launcher",
"version": "0.16", "version": "0.20",
"description": "Desktop style App Launcher with six (four for Bangle 2) apps per page - fast access if you have lots of apps installed.", "description": "Desktop style App Launcher with six (four for Bangle 2) apps per page - fast access if you have lots of apps installed.",
"screenshots": [{"url":"shot1.png"},{"url":"shot2.png"},{"url":"shot3.png"}], "screenshots": [{"url":"shot1.png"},{"url":"shot2.png"},{"url":"shot3.png"}],
"icon": "icon.png", "icon": "icon.png",

View File

@ -5,51 +5,56 @@
showClocks: true, showClocks: true,
showLaunchers: true, showLaunchers: true,
direct: false, direct: false,
oneClickExit:false, swipeExit: false,
swipeExit: false timeOut: "Off"
}, require('Storage').readJSON(FILE, true) || {}); }, require('Storage').readJSON(FILE, true) || {});
function writeSettings() { function writeSettings() {
require('Storage').writeJSON(FILE, settings); require('Storage').writeJSON(FILE, settings);
} }
const timeOutChoices = [/*LANG*/"Off", "10s", "15s", "20s", "30s"];
E.showMenu({ E.showMenu({
"" : { "title" : "Desktop launcher" }, "" : { "title" : "Desktop launcher" },
"< Back" : () => back(), /*LANG*/"< Back" : () => back(),
'Show clocks': { /*LANG*/'Show clocks': {
value: settings.showClocks, value: settings.showClocks,
onchange: v => { onchange: v => {
settings.showClocks = v; settings.showClocks = v;
writeSettings(); writeSettings();
} }
}, },
'Show launchers': { /*LANG*/'Show launchers': {
value: settings.showLaunchers, value: settings.showLaunchers,
onchange: v => { onchange: v => {
settings.showLaunchers = v; settings.showLaunchers = v;
writeSettings(); writeSettings();
} }
}, },
'Direct launch': { /*LANG*/'Direct launch': {
value: settings.direct, value: settings.direct,
onchange: v => { onchange: v => {
settings.direct = v; settings.direct = v;
writeSettings(); writeSettings();
} }
}, },
'Swipe Exit': { /*LANG*/'Swipe Exit': {
value: settings.swipeExit, value: settings.swipeExit,
onchange: v => { onchange: v => {
settings.swipeExit = v; settings.swipeExit = v;
writeSettings(); writeSettings();
} }
}, },
'One click exit': { /*LANG*/'Time Out': { // Adapted from Icon Launcher
value: settings.oneClickExit, value: timeOutChoices.indexOf(settings.timeOut),
min: 0,
max: timeOutChoices.length-1,
format: v => timeOutChoices[v],
onchange: v => { onchange: v => {
settings.oneClickExit = v; settings.timeOut = timeOutChoices[v];
writeSettings(); writeSettings();
} }
} }
}); });
}) });

View File

@ -5,3 +5,4 @@
0.03: Improve bootloader update safety. Now sets unsafeFlash:1 to allow flash with 2v11 and later 0.03: Improve bootloader update safety. Now sets unsafeFlash:1 to allow flash with 2v11 and later
Add CRC checks for common bootloaders that we know don't work Add CRC checks for common bootloaders that we know don't work
0.04: Include a precompiled bootloader for easy bootloader updates 0.04: Include a precompiled bootloader for easy bootloader updates
0.05: Rename Bootloader->DFU and add explanation to avoid confusion with Bootloader app

View File

@ -3,7 +3,7 @@
<link rel="stylesheet" href="../../css/spectre.min.css"> <link rel="stylesheet" href="../../css/spectre.min.css">
</head> </head>
<body> <body>
<p>This tool allows you to update the bootloader on <a href="https://www.espruino.com/Bangle.js2">Bangle.js 2</a> devices <p>This tool allows you to update the firmware on <a href="https://www.espruino.com/Bangle.js2">Bangle.js 2</a> devices
from within the App Loader.</p> from within the App Loader.</p>
<div id="fw-unknown"> <div id="fw-unknown">
@ -12,27 +12,41 @@
<a href="https://www.espruino.com/Bangle.js#firmware-updates" target="_blank">see the Bangle.js 1 instructions</a></b></p> <a href="https://www.espruino.com/Bangle.js#firmware-updates" target="_blank">see the Bangle.js 1 instructions</a></b></p>
</div> </div>
<ul> <ul>
<p>Your current firmware version is <span id="fw-version" style="font-weight:bold">unknown</span> and bootloader is <span id="boot-version" style="font-weight:bold">unknown</span></p> <p>Your current firmware version is <span id="fw-version" style="font-weight:bold">unknown</span> and DFU is <span id="boot-version" style="font-weight:bold">unknown</span></p>
</ul> </ul>
<div id="fw-ok" style="display:none"> <div id="fw-ok" style="display:none">
<p>If you have an early (KickStarter or developer) Bangle.js device and still have the old 2v10.x bootloader, the Firmware Update <p>If you have an early (KickStarter or developer) Bangle.js device and still have the old 2v10.x DFU, the Firmware Update
will fail with a message about the bootloader version. If so, please <a href="bootloader_espruino_2v12_banglejs2.hex" class="fw-link">click here to update to bootloader 2v12</a> and then click the 'Upload' button that appears.</p> will fail with a message about the DFU version. If so, please <a href="bootloader_espruino_2v12_banglejs2.hex" class="fw-link">click here to update to DFU 2v12</a> and then click the 'Upload' button that appears.</p>
<div id="latest-firmware" style="display:none"> <div id="latest-firmware" style="display:none">
<p>The currently available Espruino firmware releases are:</p> <p>The currently available Espruino firmware releases are:</p>
<ul id="latest-firmware-list"> <ul id="latest-firmware-list">
</ul> </ul>
<p>To update, click a link above and then click the 'Upload' button that appears.</p> <p>To update, click a link above and then click the 'Upload' button that appears.</p>
</div> </div>
<a href="#" id="advanced-btn">Advanced ▼</a>
<p><a href="#" id="info-btn">What is DFU? ▼</a></p>
<div id="info-div" style="display:none">
<p><b>What is DFU?</b></p>
<p><b>DFU</b> stands for <b>Device Firmware Update</b>. This is the first
bit of code that runs when Bangle.js starts, and it is able to update the
Bangle.js firmware. Normally you would update firmware via this Firmware
Updater app, but if for some reason Bangle.js will not boot, you can
<a href="https://www.espruino.com/Bangle.js2#firmware-updates">always use DFU to to the update manually</a>.</p>
<p>DFU is itself a bootloader, but here we're calling it DFU to avoid confusion
with the Bootloader app in the app loader (which prepares Bangle.js for running apps).</p>
</div>
<p><a href="#" id="advanced-btn">Advanced ▼</a></p>
<div id="advanced-div" style="display:none"> <div id="advanced-div" style="display:none">
<p><b>Advanced</b></p>
<p>Firmware updates via this tool work differently to the NRF Connect method mentioned on <p>Firmware updates via this tool work differently to the NRF Connect method mentioned on
<a href="https://www.espruino.com/Bangle.js2#firmware-updates">the Bangle.js 2 page</a>. Firmware <a href="https://www.espruino.com/Bangle.js2#firmware-updates">the Bangle.js 2 page</a>. Firmware
is uploaded to a file on the Bangle. Once complete the Bangle reboots and the bootloader copies is uploaded to a file on the Bangle. Once complete the Bangle reboots and DFU copies
the new firmware into internal Storage.</p> the new firmware into internal Storage.</p>
<p>In addition to the links above, you can upload a hex or zip file directly below. This file should be an <code>.app_hex</code> <p>In addition to the links above, you can upload a hex or zip file directly below. This file should be an <code>.app_hex</code>
file, *not* the normal <code>.hex</code> (as that contains the bootloader as well).</p> file, *not* the normal <code>.hex</code> (as that contains the DFU as well).</p>
<p><b>DANGER!</b> No verification is performed on uploaded ZIP or HEX files - you could <p><b>DANGER!</b> No verification is performed on uploaded ZIP or HEX files - you could
potentially overwrite your bootloader with the wrong binary and brick your Bangle.</p> potentially overwrite your DFU with the wrong binary and brick your Bangle.</p>
<input class="form-input" type="file" id="fileLoader" accept=".hex,.app_hex,.zip"/><br> <input class="form-input" type="file" id="fileLoader" accept=".hex,.app_hex,.zip"/><br>
</div> </div>
<p><button id="upload" class="btn btn-primary" style="display:none">Upload</button></p> <p><button id="upload" class="btn btn-primary" style="display:none">Upload</button></p>
@ -73,7 +87,7 @@ function onInit(device) {
document.getElementById("fw-ok").style = ""; document.getElementById("fw-ok").style = "";
} }
Puck.eval("E.CRC32(E.memoryArea(0xF7000,0x7000))", crc => { Puck.eval("E.CRC32(E.memoryArea(0xF7000,0x7000))", crc => {
console.log("Bootloader CRC = "+crc); console.log("DFU CRC = "+crc);
var version = `unknown (CRC ${crc})`; var version = `unknown (CRC ${crc})`;
var ok = true; var ok = true;
if (crc==1339551013) { version = "2v10.219"; ok = false; } if (crc==1339551013) { version = "2v10.219"; ok = false; }
@ -299,8 +313,8 @@ function createJS_app(binary, startAddress, endAddress) {
bin32[3] = VERSION; // VERSION! Use this to test ourselves bin32[3] = VERSION; // VERSION! Use this to test ourselves
console.log("CRC 0x"+bin32[2].toString(16)); console.log("CRC 0x"+bin32[2].toString(16));
hexJS = "";//`\x10if (E.CRC32(E.memoryArea(${startAddress},${endAddress-startAddress}))==${bin32[2]}) { print("FIRMWARE UP TO DATE!"); load();}\n`; hexJS = "";//`\x10if (E.CRC32(E.memoryArea(${startAddress},${endAddress-startAddress}))==${bin32[2]}) { print("FIRMWARE UP TO DATE!"); load();}\n`;
hexJS += `\x10if (E.CRC32(E.memoryArea(0xF7000,0x7000))==1339551013) { print("BOOTLOADER 2v10.219 needs update"); load();}\n`; hexJS += `\x10if (E.CRC32(E.memoryArea(0xF7000,0x7000))==1339551013) { print("DFU 2v10.219 needs update"); load();}\n`;
hexJS += `\x10if (E.CRC32(E.memoryArea(0xF7000,0x7000))==1207580954) { print("BOOTLOADER 2v10.236 needs update"); load();}\n`; hexJS += `\x10if (E.CRC32(E.memoryArea(0xF7000,0x7000))==1207580954) { print("DFU 2v10.236 needs update"); load();}\n`;
hexJS += '\x10var s = require("Storage");\n'; hexJS += '\x10var s = require("Storage");\n';
hexJS += '\x10s.erase(".firmware");\n'; hexJS += '\x10s.erase(".firmware");\n';
var CHUNKSIZE = 2048; var CHUNKSIZE = 2048;
@ -320,7 +334,7 @@ function createJS_app(binary, startAddress, endAddress) {
function createJS_bootloader(binary, startAddress, endAddress) { function createJS_bootloader(binary, startAddress, endAddress) {
var crc = CRC32(binary); var crc = CRC32(binary);
console.log("CRC 0x"+crc.toString(16)); console.log("CRC 0x"+crc.toString(16));
hexJS = `\x10if (E.CRC32(E.memoryArea(${startAddress},${endAddress-startAddress}))==${crc}) { print("BOOTLOADER UP TO DATE!"); load();}\n`; hexJS = `\x10if (E.CRC32(E.memoryArea(${startAddress},${endAddress-startAddress}))==${crc}) { print("DFU UP TO DATE!"); load();}\n`;
hexJS += `\x10var _fw = new Uint8Array(${binary.length})\n`; hexJS += `\x10var _fw = new Uint8Array(${binary.length})\n`;
var CHUNKSIZE = 1024; var CHUNKSIZE = 1024;
for (var i=0;i<binary.length;i+=CHUNKSIZE) { for (var i=0;i<binary.length;i+=CHUNKSIZE) {
@ -330,14 +344,14 @@ function createJS_bootloader(binary, startAddress, endAddress) {
hexJS += '\x10_fw.set(atob("'+chunk+'"), 0x'+(i).toString(16)+');\n'; hexJS += '\x10_fw.set(atob("'+chunk+'"), 0x'+(i).toString(16)+');\n';
} }
hexJS += `\x10(function() { if (E.CRC32(_fw)!=${crc}) throw "Invalid CRC: 0x"+E.CRC32(_fw).toString(16);\n`; hexJS += `\x10(function() { if (E.CRC32(_fw)!=${crc}) throw "Invalid CRC: 0x"+E.CRC32(_fw).toString(16);\n`;
hexJS += 'E.showMessage("Flashing Bootloader...")\n'; hexJS += 'E.showMessage("Flashing DFU...")\n';
hexJS += 'E.setFlags({unsafeFlash:1})\n'; hexJS += 'E.setFlags({unsafeFlash:1})\n';
hexJS += 'var f = require("Flash");\n'; hexJS += 'var f = require("Flash");\n';
for (var i=startAddress;i<endAddress;i+=4096) for (var i=startAddress;i<endAddress;i+=4096)
hexJS += 'f.erasePage(0x'+i.toString(16)+');\n'; hexJS += 'f.erasePage(0x'+i.toString(16)+');\n';
hexJS += `f.write(_fw,${startAddress});\n`; hexJS += `f.write(_fw,${startAddress});\n`;
hexJS += `})()\n`; hexJS += `})()\n`;
log("Bootloader ready for upload"); log("DFU ready for upload");
} }
function hexFileLoaded(hexString) { function hexFileLoaded(hexString) {
@ -383,7 +397,7 @@ function hexFileLoaded(hexString) {
}); });
if (startAddress == 0xf7000) { if (startAddress == 0xf7000) {
console.log("Bootloader - Writing to internal flash"); console.log("DFU - Writing to internal flash");
createJS_bootloader(new Uint8Array(binary.buffer, HEADER_LEN), startAddress, endAddress); createJS_bootloader(new Uint8Array(binary.buffer, HEADER_LEN), startAddress, endAddress);
} else { } else {
console.log("App - Writing to external flash"); console.log("App - Writing to external flash");
@ -406,6 +420,10 @@ function handleUpload() {
document.getElementById('fileLoader').addEventListener('change', handleFileSelect, false); document.getElementById('fileLoader').addEventListener('change', handleFileSelect, false);
document.getElementById("upload").addEventListener("click", handleUpload); document.getElementById("upload").addEventListener("click", handleUpload);
document.getElementById("info-btn").addEventListener("click", function() {
document.getElementById("info-btn").style = "display:none";
document.getElementById("info-div").style = "";
});
document.getElementById("advanced-btn").addEventListener("click", function() { document.getElementById("advanced-btn").addEventListener("click", function() {
document.getElementById("advanced-btn").style = "display:none"; document.getElementById("advanced-btn").style = "display:none";
document.getElementById("advanced-div").style = ""; document.getElementById("advanced-div").style = "";

View File

@ -1,7 +1,7 @@
{ {
"id": "fwupdate", "id": "fwupdate",
"name": "Firmware Update", "name": "Firmware Update",
"version": "0.04", "version": "0.05",
"description": "Uploads new Espruino firmwares to Bangle.js 2", "description": "Uploads new Espruino firmwares to Bangle.js 2",
"icon": "app.png", "icon": "app.png",
"type": "RAM", "type": "RAM",
@ -10,5 +10,5 @@
"custom": "custom.html", "custom": "custom.html",
"customConnect": true, "customConnect": true,
"storage": [], "storage": [],
"sortorder": 20 "sortorder": -11
} }

2
apps/gallery/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.01: New app!
0.02: Submitted to app loader

18
apps/gallery/README.md Normal file
View File

@ -0,0 +1,18 @@
# Gallery
A simple gallery app
## Usage
Upon opening the gallery app, you will be presented with a list of images that you can display. Tap the image to show it. Brightness will be set to full, and the screen timeout will be disabled. When you are done viewing the image, you can tap the screen to go back to the list of images. Press BTN1 to flip the image upside down.
## Adding images
1. The gallery app does not perform any scaling, and does not support panning. Therefore, you should use your favorite image editor to produce an image of the appropriate size for your watch. (240x240 for Bangle 1 or 176x176 for Bangle 2.) How you achieve this is up to you. If on a Bangle 2, I recommend adjusting the colors here to comply with the color restrictions.
2. Upload your image to the [Espruino image converter](https://www.espruino.com/Image+Converter). I recommend enabling compression and choosing one of the following color settings:
* 16 bit RGB565 for Bangle 1
* 3 bit RGB for Bangle 2
* 1 bit black/white for monochrome images that you want to respond to your system theme. (White will be rendered as your foreground color and black will be rendered as your background color.)
3. Set the output format to an image string, copy it into the [IDE](https://www.espruino.com/ide/), and set the destination to a file in storage. The file name should begin with "gal-" (without the quotes) and end with ".img" (without the quotes) to appear in the gallery. Note that the gal- prefix and .img extension will be removed in the UI. Upload the file.

52
apps/gallery/app.js Normal file
View File

@ -0,0 +1,52 @@
const storage = require('Storage');
let imageFiles = storage.list(/^gal-.*\.img/).sort();
let imageMenu = { '': { 'title': 'Gallery' } };
for (let fileName of imageFiles) {
let displayName = fileName.substr(4, fileName.length - 8); // Trim off the 'gal-' and '.img' for a friendly display name
imageMenu[displayName] = eval(`() => { drawImage("${fileName}"); }`); // Unfortunately, eval is the only reasonable way to do this
}
let cachedOptions = Bangle.getOptions(); // We will change the backlight and timeouts later, and need to restore them when displaying the menu
let backlightSetting = storage.readJSON('setting.json').brightness; // LCD brightness is not included in there for some reason
let angle = 0; // Store the angle of rotation
let image; // Cache the image here because we access it in multiple places
function drawMenu() {
Bangle.removeListener('touch', drawMenu); // We no longer want touching to reload the menu
Bangle.setOptions(cachedOptions); // The drawImage function set no timeout, undo that
Bangle.setLCDBrightness(backlightSetting); // Restore backlight
image = undefined; // Delete the image from memory
E.showMenu(imageMenu);
}
function drawImage(fileName) {
E.showMenu(); // Remove the menu to prevent it from breaking things
setTimeout(() => { Bangle.on('touch', drawMenu); }, 300); // Touch the screen to go back to the image menu (300ms timeout to allow user to lift finger)
Bangle.setOptions({ // Disable display power saving while showing the image
lockTimeout: 0,
lcdPowerTimeout: 0,
backlightTimeout: 0
});
Bangle.setLCDBrightness(1); // Full brightness
image = eval(storage.read(fileName)); // Sadly, the only reasonable way to do this
g.clear().reset().drawImage(image, 88, 88, { rotate: angle });
}
setWatch(info => {
if (image) {
if (angle == 0) angle = Math.PI;
else angle = 0;
Bangle.buzz();
g.clear().reset().drawImage(image, 88, 88, { rotate: angle })
}
}, BTN1, { repeat: true });
// We don't load the widgets because there is no reasonable way to unload them
drawMenu();

1
apps/gallery/icon.js Normal file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwgIOLgf/AAX8Av4FBJgkMAos/CIfMAv4Fe4AF/Apq5EAAw"))

BIN
apps/gallery/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 B

View File

@ -0,0 +1,26 @@
{
"id": "gallery",
"name": "Gallery",
"version": "0.02",
"description": "A gallery that lets you view images uploaded with the IDE (see README)",
"readme": "README.md",
"icon": "icon.png",
"type": "app",
"tags": "tools",
"supports": [
"BANGLEJS2",
"BANGLEJS"
],
"allow_emulator": true,
"storage": [
{
"name": "gallery.app.js",
"url": "app.js"
},
{
"name": "gallery.img",
"url": "icon.js",
"evaluate": true
}
]
}

View File

@ -5,7 +5,7 @@
"description": "An application that displays information about altitude, lat/lon, satellites and time", "description": "An application that displays information about altitude, lat/lon, satellites and time",
"icon": "gps-info.png", "icon": "gps-info.png",
"type": "app", "type": "app",
"tags": "gps", "tags": "gps,outdoors",
"supports": ["BANGLEJS","BANGLEJS2"], "supports": ["BANGLEJS","BANGLEJS2"],
"storage": [ "storage": [
{"name":"gpsinfo.app.js","url":"gps-info.js"}, {"name":"gpsinfo.app.js","url":"gps-info.js"},

View File

@ -8,3 +8,8 @@
Fix widget adding listeners more than once Fix widget adding listeners more than once
0.07: Show checkered flag for target markers 0.07: Show checkered flag for target markers
Single waypoints are now shown in the compass view Single waypoints are now shown in the compass view
0.08: Better handle state in widget
Slightly faster drawing by doing some caching
Reconstruct battery voltage by using calibrated batFullVoltage
Averaging for smoothing compass headings
Save state if route or waypoint has been chosen

View File

@ -1,48 +1,69 @@
{ //run in own scope for fast switch
const STORAGE = require("Storage"); const STORAGE = require("Storage");
const showWidgets = true; const BAT_FULL = require("Storage").readJSON("setting.json").batFullVoltage || 0.3144;
let numberOfSlices=4;
let init = function(){
global.screen = 1;
global.drawTimeout = undefined;
global.lastDrawnScreen = 0;
global.firstDraw = true;
global.slices = [];
global.maxScreens = 1;
global.scheduleDraw = false;
if (showWidgets){
Bangle.loadWidgets(); Bangle.loadWidgets();
} WIDGETS.gpstrek.start(false);
if (!WIDGETS.gpstrek.getState().numberOfSlices) WIDGETS.gpstrek.getState().numberOfSlices = 3;
};
let state = WIDGETS.gpstrek.getState(); let cleanup = function(){
WIDGETS.gpstrek.start(false); if (global.drawTimeout) clearTimeout(global.drawTimeout);
delete global.screen;
delete global.drawTimeout;
delete global.lastDrawnScreen;
delete global.firstDraw;
delete global.slices;
delete global.maxScreens;
};
function parseNumber(toParse){ init();
scheduleDraw = true;
let parseNumber = function(toParse){
if (toParse.includes(".")) return parseFloat(toParse); if (toParse.includes(".")) return parseFloat(toParse);
return parseFloat("" + toParse + ".0"); return parseFloat("" + toParse + ".0");
} };
function parseWaypoint(filename, offset, result){ let parseWaypoint = function(filename, offset, result){
result.lat = parseNumber(STORAGE.read(filename, offset, 11)); result.lat = parseNumber(STORAGE.read(filename, offset, 11));
result.lon = parseNumber(STORAGE.read(filename, offset += 11, 12)); result.lon = parseNumber(STORAGE.read(filename, offset += 11, 12));
return offset + 12; return offset + 12;
} };
function parseWaypointWithElevation(filename, offset, result){ let parseWaypointWithElevation = function (filename, offset, result){
offset = parseWaypoint(filename, offset, result); offset = parseWaypoint(filename, offset, result);
result.alt = parseNumber(STORAGE.read(filename, offset, 6)); result.alt = parseNumber(STORAGE.read(filename, offset, 6));
return offset + 6; return offset + 6;
} };
function parseWaypointWithName(filename, offset, result){ let parseWaypointWithName = function(filename, offset, result){
offset = parseWaypoint(filename, offset, result); offset = parseWaypoint(filename, offset, result);
return parseName(filename, offset, result); return parseName(filename, offset, result);
} };
function parseName(filename, offset, result){ let parseName = function(filename, offset, result){
let nameLength = STORAGE.read(filename, offset, 2) - 0; let nameLength = STORAGE.read(filename, offset, 2) - 0;
result.name = STORAGE.read(filename, offset += 2, nameLength); result.name = STORAGE.read(filename, offset += 2, nameLength);
return offset + nameLength; return offset + nameLength;
} };
function parseWaypointWithElevationAndName(filename, offset, result){ let parseWaypointWithElevationAndName = function(filename, offset, result){
offset = parseWaypointWithElevation(filename, offset, result); offset = parseWaypointWithElevation(filename, offset, result);
return parseName(filename, offset, result); return parseName(filename, offset, result);
} };
function getEntry(filename, offset, result){ let getEntry = function(filename, offset, result){
result.fileOffset = offset; result.fileOffset = offset;
let type = STORAGE.read(filename, offset++, 1); let type = STORAGE.read(filename, offset++, 1);
if (type == "") return -1; if (type == "") return -1;
@ -68,12 +89,12 @@ function getEntry(filename, offset, result){
result.fileLength = offset - result.fileOffset; result.fileLength = offset - result.fileOffset;
//print(result); //print(result);
return offset; return offset;
} };
const labels = ["N","NE","E","SE","S","SW","W","NW"]; const labels = ["N","NE","E","SE","S","SW","W","NW"];
const loc = require("locale"); const loc = require("locale");
function matchFontSize(graphics, text, height, width){ let matchFontSize = function(graphics, text, height, width){
graphics.setFontVector(height); graphics.setFontVector(height);
let metrics; let metrics;
let size = 1; let size = 1;
@ -81,13 +102,19 @@ function matchFontSize(graphics, text, height, width){
size -= 0.05; size -= 0.05;
graphics.setFont("Vector",Math.floor(height*size)); graphics.setFont("Vector",Math.floor(height*size));
} }
} };
function getDoubleLineSlice(title1,title2,provider1,provider2,refreshTime){ let getDoubleLineSlice = function(title1,title2,provider1,provider2,refreshTime){
let lastDrawn = Date.now() - Math.random()*refreshTime; let lastDrawn = Date.now() - Math.random()*refreshTime;
let lastValue1 = 0;
let lastValue2 = 0;
return { return {
refresh: function (){ refresh: function (){
return Date.now() - lastDrawn > (Bangle.isLocked()?(refreshTime?refreshTime:5000):(refreshTime?refreshTime*2:10000)); let bigChange1 = (Math.abs(lastValue1 - provider1()) > 1);
let bigChange2 = (Math.abs(lastValue2 - provider2()) > 1);
let refresh = (Bangle.isLocked()?(refreshTime?refreshTime*5:10000):(refreshTime?refreshTime*2:1000));
let old = (Date.now() - lastDrawn) > refresh;
return (bigChange1 || bigChange2) && old;
}, },
draw: function (graphics, x, y, height, width){ draw: function (graphics, x, y, height, width){
lastDrawn = Date.now(); lastDrawn = Date.now();
@ -95,29 +122,29 @@ function getDoubleLineSlice(title1,title2,provider1,provider2,refreshTime){
if (typeof title2 == "function") title2 = title2(); if (typeof title2 == "function") title2 = title2();
graphics.clearRect(x,y,x+width,y+height); graphics.clearRect(x,y,x+width,y+height);
let value = provider1(); lastValue1 = provider1();
matchFontSize(graphics, title1 + value, Math.floor(height*0.5), width); matchFontSize(graphics, title1 + lastValue1, Math.floor(height*0.5), width);
graphics.setFontAlign(-1,-1); graphics.setFontAlign(-1,-1);
graphics.drawString(title1, x+2, y); graphics.drawString(title1, x+2, y);
graphics.setFontAlign(1,-1); graphics.setFontAlign(1,-1);
graphics.drawString(value, x+width, y); graphics.drawString(lastValue1, x+width, y);
value = provider2(); lastValue2 = provider2();
matchFontSize(graphics, title2 + value, Math.floor(height*0.5), width); matchFontSize(graphics, title2 + lastValue2, Math.floor(height*0.5), width);
graphics.setFontAlign(-1,-1); graphics.setFontAlign(-1,-1);
graphics.drawString(title2, x+2, y+(height*0.5)); graphics.drawString(title2, x+2, y+(height*0.5));
graphics.setFontAlign(1,-1); graphics.setFontAlign(1,-1);
graphics.drawString(value, x+width, y+(height*0.5)); graphics.drawString(lastValue2, x+width, y+(height*0.5));
} }
}; };
} };
function getTargetSlice(targetDataSource){ let getTargetSlice = function(targetDataSource){
let nameIndex = 0; let nameIndex = 0;
let lastDrawn = Date.now() - Math.random()*3000; let lastDrawn = Date.now() - Math.random()*3000;
return { return {
refresh: function (){ refresh: function (){
return Date.now() - lastDrawn > (Bangle.isLocked()?10000:3000); return Date.now() - lastDrawn > (Bangle.isLocked()?3000:10000);
}, },
draw: function (graphics, x, y, height, width){ draw: function (graphics, x, y, height, width){
lastDrawn = Date.now(); lastDrawn = Date.now();
@ -174,9 +201,9 @@ function getTargetSlice(targetDataSource){
} }
} }
}; };
} };
function drawCompass(graphics, x, y, height, width, increment, start){ let drawCompass = function(graphics, x, y, height, width, increment, start){
graphics.setFont12x20(); graphics.setFont12x20();
graphics.setFontAlign(0,-1); graphics.setFontAlign(0,-1);
graphics.setColor(graphics.theme.fg); graphics.setColor(graphics.theme.fg);
@ -197,14 +224,19 @@ function drawCompass(graphics, x, y, height, width, increment, start){
xpos+=increment*15; xpos+=increment*15;
if (xpos > width + 20) break; if (xpos > width + 20) break;
} }
} };
function getCompassSlice(compassDataSource){ let getCompassSlice = function(compassDataSource){
let lastDrawn = Date.now() - Math.random()*2000; let lastDrawn = Date.now() - Math.random()*2000;
let lastDrawnValue = 0;
const buffers = 4; const buffers = 4;
let buf = []; let buf = [];
return { return {
refresh : function (){return Bangle.isLocked()?(Date.now() - lastDrawn > 2000):true;}, refresh : function (){
let bigChange = (Math.abs(lastDrawnValue - compassDataSource.getCourse()) > 2);
let old = (Bangle.isLocked()?(Date.now() - lastDrawn > 2000):true);
return bigChange && old;
},
draw: function (graphics, x,y,height,width){ draw: function (graphics, x,y,height,width){
lastDrawn = Date.now(); lastDrawn = Date.now();
const max = 180; const max = 180;
@ -212,12 +244,14 @@ function getCompassSlice(compassDataSource){
graphics.clearRect(x,y,x+width,y+height); graphics.clearRect(x,y,x+width,y+height);
var start = compassDataSource.getCourse() - 90; lastDrawnValue = compassDataSource.getCourse();
if (isNaN(compassDataSource.getCourse())) start = -90;
var start = lastDrawnValue - 90;
if (isNaN(lastDrawnValue)) start = -90;
if (start<0) start+=360; if (start<0) start+=360;
start = start % 360; start = start % 360;
if (state.acc && compassDataSource.getCourseType() == "MAG"){ if (WIDGETS.gpstrek.getState().acc && compassDataSource.getCourseType() == "MAG"){
drawCompass(graphics,0,y+width*0.05,height-width*0.05,width,increment,start); drawCompass(graphics,0,y+width*0.05,height-width*0.05,width,increment,start);
} else { } else {
drawCompass(graphics,0,y,height,width,increment,start); drawCompass(graphics,0,y,height,width,increment,start);
@ -226,7 +260,8 @@ function getCompassSlice(compassDataSource){
if (compassDataSource.getPoints){ if (compassDataSource.getPoints){
for (let p of compassDataSource.getPoints()){ for (let p of compassDataSource.getPoints()){
var bpos = p.bearing - compassDataSource.getCourse(); g.reset();
var bpos = p.bearing - lastDrawnValue;
if (bpos>180) bpos -=360; if (bpos>180) bpos -=360;
if (bpos<-180) bpos +=360; if (bpos<-180) bpos +=360;
bpos+=120; bpos+=120;
@ -251,6 +286,7 @@ function getCompassSlice(compassDataSource){
} }
if (compassDataSource.getMarkers){ if (compassDataSource.getMarkers){
for (let m of compassDataSource.getMarkers()){ for (let m of compassDataSource.getMarkers()){
g.reset();
g.setColor(m.fillcolor); g.setColor(m.fillcolor);
let mpos = m.xpos * width; let mpos = m.xpos * width;
if (m.xpos < 0.05) mpos = Math.floor(width*0.05); if (m.xpos < 0.05) mpos = Math.floor(width*0.05);
@ -263,9 +299,9 @@ function getCompassSlice(compassDataSource){
graphics.setColor(g.theme.fg); graphics.setColor(g.theme.fg);
graphics.fillRect(x,y,Math.floor(width*0.05),y+height); graphics.fillRect(x,y,Math.floor(width*0.05),y+height);
graphics.fillRect(Math.ceil(width*0.95),y,width,y+height); graphics.fillRect(Math.ceil(width*0.95),y,width,y+height);
if (state.acc && compassDataSource.getCourseType() == "MAG") { if (WIDGETS.gpstrek.getState().acc && compassDataSource.getCourseType() == "MAG") {
let xh = E.clip(width*0.5-height/2+(((state.acc.x+1)/2)*height),width*0.5 - height/2, width*0.5 + height/2); let xh = E.clip(width*0.5-height/2+(((WIDGETS.gpstrek.getState().acc.x+1)/2)*height),width*0.5 - height/2, width*0.5 + height/2);
let yh = E.clip(y+(((state.acc.y+1)/2)*height),y,y+height); let yh = E.clip(y+(((WIDGETS.gpstrek.getState().acc.y+1)/2)*height),y,y+height);
graphics.fillRect(width*0.5 - height/2, y, width*0.5 + height/2, y + Math.floor(width*0.05)); graphics.fillRect(width*0.5 - height/2, y, width*0.5 + height/2, y + Math.floor(width*0.05));
@ -287,44 +323,48 @@ function getCompassSlice(compassDataSource){
graphics.drawRect(Math.floor(width*0.05),y,Math.ceil(width*0.95),y+height); graphics.drawRect(Math.floor(width*0.05),y,Math.ceil(width*0.95),y+height);
} }
}; };
} };
function radians(a) { let radians = function(a) {
return a*Math.PI/180; return a*Math.PI/180;
} };
function degrees(a) { let degrees = function(a) {
var d = a*180/Math.PI; let d = a*180/Math.PI;
return (d+360)%360; return (d+360)%360;
} };
function bearing(a,b){ let bearing = function(a,b){
if (!a || !b || !a.lon || !a.lat || !b.lon || !b.lat) return Infinity; if (!a || !b || !a.lon || !a.lat || !b.lon || !b.lat) return Infinity;
var delta = radians(b.lon-a.lon); let delta = radians(b.lon-a.lon);
var alat = radians(a.lat); let alat = radians(a.lat);
var blat = radians(b.lat); let blat = radians(b.lat);
var y = Math.sin(delta) * Math.cos(blat); let y = Math.sin(delta) * Math.cos(blat);
var x = Math.cos(alat)*Math.sin(blat) - let x = Math.cos(alat)*Math.sin(blat) -
Math.sin(alat)*Math.cos(blat)*Math.cos(delta); Math.sin(alat)*Math.cos(blat)*Math.cos(delta);
return Math.round(degrees(Math.atan2(y, x))); return Math.round(degrees(Math.atan2(y, x)));
} };
function distance(a,b){ let distance = function(a,b){
if (!a || !b || !a.lon || !a.lat || !b.lon || !b.lat) return Infinity; if (!a || !b || !a.lon || !a.lat || !b.lon || !b.lat) return Infinity;
var x = radians(a.lon-b.lon) * Math.cos(radians((a.lat+b.lat)/2)); let x = radians(a.lon-b.lon) * Math.cos(radians((a.lat+b.lat)/2));
var y = radians(b.lat-a.lat); let y = radians(b.lat-a.lat);
return Math.round(Math.sqrt(x*x + y*y) * 6371000); return Math.round(Math.sqrt(x*x + y*y) * 6371000);
} };
function triangle (x, y, width, height){ let getAveragedCompass = function(){
return Math.round(WIDGETS.gpstrek.getState().avgComp);
};
let triangle = function(x, y, width, height){
return [ return [
Math.round(x),Math.round(y), Math.round(x),Math.round(y),
Math.round(x+width * 0.5), Math.round(y+height), Math.round(x+width * 0.5), Math.round(y+height),
Math.round(x-width * 0.5), Math.round(y+height) Math.round(x-width * 0.5), Math.round(y+height)
]; ];
} };
function onSwipe(dir){ let onSwipe = function(dir){
if (dir < 0) { if (dir < 0) {
nextScreen(); nextScreen();
} else if (dir > 0) { } else if (dir > 0) {
@ -332,9 +372,9 @@ function onSwipe(dir){
} else { } else {
nextScreen(); nextScreen();
} }
} };
function setButtons(){ let setButtons = function(){
let options = { let options = {
mode: "custom", mode: "custom",
swipe: onSwipe, swipe: onSwipe,
@ -342,9 +382,9 @@ function setButtons(){
touch: nextScreen touch: nextScreen
}; };
Bangle.setUI(options); Bangle.setUI(options);
} };
function getApproxFileSize(name){ let getApproxFileSize = function(name){
let currentStart = STORAGE.getStats().totalBytes; let currentStart = STORAGE.getStats().totalBytes;
let currentSize = 0; let currentSize = 0;
for (let i = currentStart; i > 500; i/=2){ for (let i = currentStart; i > 500; i/=2){
@ -358,9 +398,9 @@ function getApproxFileSize(name){
currentSize += currentDiff; currentSize += currentDiff;
} }
return currentSize; return currentSize;
} };
function parseRouteData(filename, progressMonitor){ let parseRouteData = function(filename, progressMonitor){
let routeInfo = {}; let routeInfo = {};
routeInfo.filename = filename; routeInfo.filename = filename;
@ -406,40 +446,40 @@ function parseRouteData(filename, progressMonitor){
set(routeInfo, 0); set(routeInfo, 0);
return routeInfo; return routeInfo;
} };
function hasPrev(route){ let hasPrev = function(route){
if (route.mirror) return route.index < (route.count - 1); if (route.mirror) return route.index < (route.count - 1);
return route.index > 0; return route.index > 0;
} };
function hasNext(route){ let hasNext = function(route){
if (route.mirror) return route.index > 0; if (route.mirror) return route.index > 0;
return route.index < (route.count - 1); return route.index < (route.count - 1);
} };
function next(route){ let next = function(route){
if (!hasNext(route)) return; if (!hasNext(route)) return;
if (route.mirror) set(route, --route.index); if (route.mirror) set(route, --route.index);
if (!route.mirror) set(route, ++route.index); if (!route.mirror) set(route, ++route.index);
} };
function set(route, index){ let set = function(route, index){
route.currentWaypoint = {}; route.currentWaypoint = {};
route.index = index; route.index = index;
getEntry(route.filename, route.refs[index], route.currentWaypoint); getEntry(route.filename, route.refs[index], route.currentWaypoint);
} };
function prev(route){ let prev = function(route){
if (!hasPrev(route)) return; if (!hasPrev(route)) return;
if (route.mirror) set(route, ++route.index); if (route.mirror) set(route, ++route.index);
if (!route.mirror) set(route, --route.index); if (!route.mirror) set(route, --route.index);
} };
let lastMirror; let lastMirror;
let cachedLast; let cachedLast;
function getLast(route){ let getLast = function(route){
let wp = {}; let wp = {};
if (lastMirror != route.mirror){ if (lastMirror != route.mirror){
if (route.mirror) getEntry(route.filename, route.refs[0], wp); if (route.mirror) getEntry(route.filename, route.refs[0], wp);
@ -448,14 +488,14 @@ function getLast(route){
cachedLast = wp; cachedLast = wp;
} }
return cachedLast; return cachedLast;
} };
function removeMenu(){ let removeMenu = function(){
E.showMenu(); E.showMenu();
switchNav(); switchNav();
} };
function showProgress(progress, title, max){ let showProgress = function(progress, title, max){
//print("Progress",progress,max) //print("Progress",progress,max)
let message = title? title: "Loading"; let message = title? title: "Loading";
if (max){ if (max){
@ -466,17 +506,17 @@ function showProgress(progress, title, max){
for (let i = dots; i < 4; i++) message += " "; for (let i = dots; i < 4; i++) message += " ";
} }
E.showMessage(message); E.showMessage(message);
} };
function handleLoading(c){ let handleLoading = function(c){
E.showMenu(); E.showMenu();
state.route = parseRouteData(c, showProgress); WIDGETS.gpstrek.getState().route = parseRouteData(c, showProgress);
state.waypoint = null; WIDGETS.gpstrek.getState().waypoint = null;
WIDGETS.gpstrek.getState().route.mirror = false;
removeMenu(); removeMenu();
state.route.mirror = false; };
}
function showRouteSelector (){ let showRouteSelector = function(){
var menu = { var menu = {
"" : { "" : {
back : showRouteMenu, back : showRouteMenu,
@ -488,9 +528,9 @@ function showRouteSelector (){
}); });
E.showMenu(menu); E.showMenu(menu);
} };
function showRouteMenu(){ let showRouteMenu = function(){
var menu = { var menu = {
"" : { "" : {
"title" : "Route", "title" : "Route",
@ -499,48 +539,48 @@ function showRouteMenu(){
"Select file" : showRouteSelector "Select file" : showRouteSelector
}; };
if (state.route){ if (WIDGETS.gpstrek.getState().route){
menu.Mirror = { menu.Mirror = {
value: state && state.route && !!state.route.mirror || false, value: WIDGETS.gpstrek.getState() && WIDGETS.gpstrek.getState().route && !!WIDGETS.gpstrek.getState().route.mirror || false,
onchange: v=>{ onchange: v=>{
state.route.mirror = v; WIDGETS.gpstrek.getState().route.mirror = v;
} }
}; };
menu['Select closest waypoint'] = function () { menu['Select closest waypoint'] = function () {
if (state.currentPos && state.currentPos.lat){ if (WIDGETS.gpstrek.getState().currentPos && WIDGETS.gpstrek.getState().currentPos.lat){
setClosestWaypoint(state.route, null, showProgress); removeMenu(); setClosestWaypoint(WIDGETS.gpstrek.getState().route, null, showProgress); removeMenu();
} else { } else {
E.showAlert("No position").then(()=>{E.showMenu(menu);}); E.showAlert("No position").then(()=>{E.showMenu(menu);});
} }
}; };
menu['Select closest waypoint (not visited)'] = function () { menu['Select closest waypoint (not visited)'] = function () {
if (state.currentPos && state.currentPos.lat){ if (WIDGETS.gpstrek.getState().currentPos && WIDGETS.gpstrek.getState().currentPos.lat){
setClosestWaypoint(state.route, state.route.index, showProgress); removeMenu(); setClosestWaypoint(WIDGETS.gpstrek.getState().route, WIDGETS.gpstrek.getState().route.index, showProgress); removeMenu();
} else { } else {
E.showAlert("No position").then(()=>{E.showMenu(menu);}); E.showAlert("No position").then(()=>{E.showMenu(menu);});
} }
}; };
menu['Select waypoint'] = { menu['Select waypoint'] = {
value : state.route.index, value : WIDGETS.gpstrek.getState().route.index,
min:1,max:state.route.count,step:1, min:1,max:WIDGETS.gpstrek.getState().route.count,step:1,
onchange : v => { set(state.route, v-1); } onchange : v => { set(WIDGETS.gpstrek.getState().route, v-1); }
}; };
menu['Select waypoint as current position'] = function (){ menu['Select waypoint as current position'] = function (){
state.currentPos.lat = state.route.currentWaypoint.lat; WIDGETS.gpstrek.getState().currentPos.lat = WIDGETS.gpstrek.getState().route.currentWaypoint.lat;
state.currentPos.lon = state.route.currentWaypoint.lon; WIDGETS.gpstrek.getState().currentPos.lon = WIDGETS.gpstrek.getState().route.currentWaypoint.lon;
state.currentPos.alt = state.route.currentWaypoint.alt; WIDGETS.gpstrek.getState().currentPos.alt = WIDGETS.gpstrek.getState().route.currentWaypoint.alt;
removeMenu(); removeMenu();
}; };
} }
if (state.route && hasPrev(state.route)) if (WIDGETS.gpstrek.getState().route && hasPrev(WIDGETS.gpstrek.getState().route))
menu['Previous waypoint'] = function() { prev(state.route); removeMenu(); }; menu['Previous waypoint'] = function() { prev(WIDGETS.gpstrek.getState().route); removeMenu(); };
if (state.route && hasNext(state.route)) if (WIDGETS.gpstrek.getState().route && hasNext(WIDGETS.gpstrek.getState().route))
menu['Next waypoint'] = function() { next(state.route); removeMenu(); }; menu['Next waypoint'] = function() { next(WIDGETS.gpstrek.getState().route); removeMenu(); };
E.showMenu(menu); E.showMenu(menu);
} };
function showWaypointSelector(){ let showWaypointSelector = function(){
let waypoints = require("waypoints").load(); let waypoints = require("waypoints").load();
var menu = { var menu = {
"" : { "" : {
@ -550,41 +590,41 @@ function showWaypointSelector(){
waypoints.forEach((wp,c)=>{ waypoints.forEach((wp,c)=>{
menu[waypoints[c].name] = function (){ menu[waypoints[c].name] = function (){
state.waypoint = waypoints[c]; WIDGETS.gpstrek.getState().waypoint = waypoints[c];
state.waypointIndex = c; WIDGETS.gpstrek.getState().waypointIndex = c;
state.route = null; WIDGETS.gpstrek.getState().route = null;
removeMenu(); removeMenu();
}; };
}); });
E.showMenu(menu); E.showMenu(menu);
} };
function showCalibrationMenu(){ let showCalibrationMenu = function(){
let menu = { let menu = {
"" : { "" : {
"title" : "Calibration", "title" : "Calibration",
back : showMenu, back : showMenu,
}, },
"Barometer (GPS)" : ()=>{ "Barometer (GPS)" : ()=>{
if (!state.currentPos || isNaN(state.currentPos.alt)){ if (!WIDGETS.gpstrek.getState().currentPos || isNaN(WIDGETS.gpstrek.getState().currentPos.alt)){
E.showAlert("No GPS altitude").then(()=>{E.showMenu(menu);}); E.showAlert("No GPS altitude").then(()=>{E.showMenu(menu);});
} else { } else {
state.calibAltDiff = state.altitude - state.currentPos.alt; WIDGETS.gpstrek.getState().calibAltDiff = WIDGETS.gpstrek.getState().altitude - WIDGETS.gpstrek.getState().currentPos.alt;
E.showAlert("Calibrated Altitude Difference: " + state.calibAltDiff.toFixed(0)).then(()=>{removeMenu();}); E.showAlert("Calibrated Altitude Difference: " + WIDGETS.gpstrek.getState().calibAltDiff.toFixed(0)).then(()=>{removeMenu();});
} }
}, },
"Barometer (Manual)" : { "Barometer (Manual)" : {
value : Math.round(state.currentPos && (state.currentPos.alt != undefined && !isNaN(state.currentPos.alt)) ? state.currentPos.alt: state.altitude), value : Math.round(WIDGETS.gpstrek.getState().currentPos && (WIDGETS.gpstrek.getState().currentPos.alt != undefined && !isNaN(WIDGETS.gpstrek.getState().currentPos.alt)) ? WIDGETS.gpstrek.getState().currentPos.alt: WIDGETS.gpstrek.getState().altitude),
min:-2000,max: 10000,step:1, min:-2000,max: 10000,step:1,
onchange : v => { state.calibAltDiff = state.altitude - v; } onchange : v => { WIDGETS.gpstrek.getState().calibAltDiff = WIDGETS.gpstrek.getState().altitude - v; }
}, },
"Reset Compass" : ()=>{ Bangle.resetCompass(); removeMenu();}, "Reset Compass" : ()=>{ Bangle.resetCompass(); removeMenu();},
}; };
E.showMenu(menu); E.showMenu(menu);
} };
function showWaypointMenu(){ let showWaypointMenu = function(){
let menu = { let menu = {
"" : { "" : {
"title" : "Waypoint", "title" : "Waypoint",
@ -593,21 +633,21 @@ function showWaypointMenu(){
"Select waypoint" : showWaypointSelector, "Select waypoint" : showWaypointSelector,
}; };
E.showMenu(menu); E.showMenu(menu);
} };
function showBackgroundMenu(){ let showBackgroundMenu = function(){
let menu = { let menu = {
"" : { "" : {
"title" : "Background", "title" : "Background",
back : showMenu, back : showMenu,
}, },
"Start" : ()=>{ E.showPrompt("Start?").then((v)=>{ if (v) {WIDGETS.gpstrek.start(true); removeMenu();} else {showMenu();}}).catch(()=>{E.showMenu(mainmenu);});}, "Start" : ()=>{ E.showPrompt("Start?").then((v)=>{ if (v) {WIDGETS.gpstrek.start(true); removeMenu();} else {showMenu();}}).catch(()=>{showMenu();});},
"Stop" : ()=>{ E.showPrompt("Stop?").then((v)=>{ if (v) {WIDGETS.gpstrek.stop(true); removeMenu();} else {showMenu();}}).catch(()=>{E.showMenu(mainmenu);});}, "Stop" : ()=>{ E.showPrompt("Stop?").then((v)=>{ if (v) {WIDGETS.gpstrek.stop(true); removeMenu();} else {showMenu();}}).catch(()=>{showMenu();});},
}; };
E.showMenu(menu); E.showMenu(menu);
} };
function showMenu(){ let showMenu = function(){
var mainmenu = { var mainmenu = {
"" : { "" : {
"title" : "Main", "title" : "Main",
@ -617,50 +657,55 @@ function showMenu(){
"Waypoint" : showWaypointMenu, "Waypoint" : showWaypointMenu,
"Background" : showBackgroundMenu, "Background" : showBackgroundMenu,
"Calibration": showCalibrationMenu, "Calibration": showCalibrationMenu,
"Reset" : ()=>{ E.showPrompt("Do Reset?").then((v)=>{ if (v) {WIDGETS.gpstrek.resetState(); removeMenu();} else {E.showMenu(mainmenu);}});}, "Reset" : ()=>{ E.showPrompt("Do Reset?").then((v)=>{ if (v) {WIDGETS.gpstrek.resetState(); removeMenu();} else {E.showMenu(mainmenu);}}).catch(()=>{E.showMenu(mainmenu);});},
"Info rows" : { "Info rows" : {
value : numberOfSlices, value : WIDGETS.gpstrek.getState().numberOfSlices,
min:1,max:6,step:1, min:1,max:6,step:1,
onchange : v => { setNumberOfSlices(v); } onchange : v => { WIDGETS.gpstrek.getState().numberOfSlices = v; }
}, },
}; };
E.showMenu(mainmenu); E.showMenu(mainmenu);
} };
let scheduleDraw = true;
function switchMenu(){ let switchMenu = function(){
screen = 0; stopDrawing();
scheduleDraw = false; showMenu();
showMenu(); };
}
function drawInTimeout(){ let stopDrawing = function(){
setTimeout(()=>{ if (drawTimeout) clearTimeout(drawTimeout);
scheduleDraw = false;
};
let drawInTimeout = function(){
if (global.drawTimeout) clearTimeout(drawTimeout);
drawTimeout = setTimeout(()=>{
drawTimeout = undefined;
draw(); draw();
if (scheduleDraw) },50);
setTimeout(drawInTimeout, 0); };
},0);
}
function switchNav(){ let switchNav = function(){
if (!screen) screen = 1; if (!screen) screen = 1;
setButtons(); setButtons();
scheduleDraw = true; scheduleDraw = true;
firstDraw = true;
drawInTimeout(); drawInTimeout();
} };
function nextScreen(){ let nextScreen = function(){
screen++; screen++;
if (screen > maxScreens){ if (screen > maxScreens){
screen = 1; screen = 1;
} }
} drawInTimeout();
};
function setClosestWaypoint(route, startindex, progress){ let setClosestWaypoint = function(route, startindex, progress){
if (startindex >= state.route.count) startindex = state.route.count - 1; if (startindex >= WIDGETS.gpstrek.getState().route.count) startindex = WIDGETS.gpstrek.getState().route.count - 1;
if (!state.currentPos.lat){ if (!WIDGETS.gpstrek.getState().currentPos.lat){
set(route, startindex); set(route, startindex);
return; return;
} }
@ -670,7 +715,7 @@ function setClosestWaypoint(route, startindex, progress){
if (progress && (i % 5 == 0)) progress(i-(startindex?startindex:0), "Searching", route.count); if (progress && (i % 5 == 0)) progress(i-(startindex?startindex:0), "Searching", route.count);
let wp = {}; let wp = {};
getEntry(route.filename, route.refs[i], wp); getEntry(route.filename, route.refs[i], wp);
let curDist = distance(state.currentPos, wp); let curDist = distance(WIDGETS.gpstrek.getState().currentPos, wp);
if (curDist < minDist){ if (curDist < minDist){
minDist = curDist; minDist = curDist;
minIndex = i; minIndex = i;
@ -679,30 +724,28 @@ function setClosestWaypoint(route, startindex, progress){
} }
} }
set(route, minIndex); set(route, minIndex);
} };
let screen = 1;
const finishIcon = atob("CggB//meZmeZ+Z5n/w=="); const finishIcon = atob("CggB//meZmeZ+Z5n/w==");
const compassSliceData = { const compassSliceData = {
getCourseType: function(){ getCourseType: function(){
return (state.currentPos && state.currentPos.course) ? "GPS" : "MAG"; return (WIDGETS.gpstrek.getState().currentPos && WIDGETS.gpstrek.getState().currentPos.course) ? "GPS" : "MAG";
}, },
getCourse: function (){ getCourse: function (){
if(compassSliceData.getCourseType() == "GPS") return state.currentPos.course; if(compassSliceData.getCourseType() == "GPS") return WIDGETS.gpstrek.getState().currentPos.course;
return state.compassHeading?state.compassHeading:undefined; return getAveragedCompass();
}, },
getPoints: function (){ getPoints: function (){
let points = []; let points = [];
if (state.currentPos && state.currentPos.lon && state.route && state.route.currentWaypoint){ if (WIDGETS.gpstrek.getState().currentPos && WIDGETS.gpstrek.getState().currentPos.lon && WIDGETS.gpstrek.getState().route && WIDGETS.gpstrek.getState().route.currentWaypoint){
points.push({bearing:bearing(state.currentPos, state.route.currentWaypoint), color:"#0f0"}); points.push({bearing:bearing(WIDGETS.gpstrek.getState().currentPos, WIDGETS.gpstrek.getState().route.currentWaypoint), color:"#0f0"});
} }
if (state.currentPos && state.currentPos.lon && state.route){ if (WIDGETS.gpstrek.getState().currentPos && WIDGETS.gpstrek.getState().currentPos.lon && WIDGETS.gpstrek.getState().route){
points.push({bearing:bearing(state.currentPos, getLast(state.route)), icon: finishIcon}); points.push({bearing:bearing(WIDGETS.gpstrek.getState().currentPos, getLast(WIDGETS.gpstrek.getState().route)), icon: finishIcon});
} }
if (state.currentPos && state.currentPos.lon && state.waypoint){ if (WIDGETS.gpstrek.getState().currentPos && WIDGETS.gpstrek.getState().currentPos.lon && WIDGETS.gpstrek.getState().waypoint){
points.push({bearing:bearing(state.currentPos, state.waypoint), icon: finishIcon}); points.push({bearing:bearing(WIDGETS.gpstrek.getState().currentPos, WIDGETS.gpstrek.getState().waypoint), icon: finishIcon});
} }
return points; return points;
}, },
@ -714,79 +757,74 @@ const compassSliceData = {
const waypointData = { const waypointData = {
icon: atob("EBCBAAAAAAAAAAAAcIB+zg/uAe4AwACAAAAAAAAAAAAAAAAA"), icon: atob("EBCBAAAAAAAAAAAAcIB+zg/uAe4AwACAAAAAAAAAAAAAAAAA"),
getProgress: function() { getProgress: function() {
return (state.route.index + 1) + "/" + state.route.count; return (WIDGETS.gpstrek.getState().route.index + 1) + "/" + WIDGETS.gpstrek.getState().route.count;
}, },
getTarget: function (){ getTarget: function (){
if (distance(state.currentPos,state.route.currentWaypoint) < 30 && hasNext(state.route)){ if (distance(WIDGETS.gpstrek.getState().currentPos,WIDGETS.gpstrek.getState().route.currentWaypoint) < 30 && hasNext(WIDGETS.gpstrek.getState().route)){
next(state.route); next(WIDGETS.gpstrek.getState().route);
Bangle.buzz(1000); Bangle.buzz(1000);
} }
return state.route.currentWaypoint; return WIDGETS.gpstrek.getState().route.currentWaypoint;
}, },
getStart: function (){ getStart: function (){
return state.currentPos; return WIDGETS.gpstrek.getState().currentPos;
} }
}; };
const finishData = { const finishData = {
icon: atob("EBABAAA/4DmgJmAmYDmgOaAmYD/gMAAwADAAMAAwAAAAAAA="), icon: atob("EBABAAA/4DmgJmAmYDmgOaAmYD/gMAAwADAAMAAwAAAAAAA="),
getTarget: function (){ getTarget: function (){
if (state.route) return getLast(state.route); if (WIDGETS.gpstrek.getState().route) return getLast(WIDGETS.gpstrek.getState().route);
if (state.waypoint) return state.waypoint; if (WIDGETS.gpstrek.getState().waypoint) return WIDGETS.gpstrek.getState().waypoint;
}, },
getStart: function (){ getStart: function (){
return state.currentPos; return WIDGETS.gpstrek.getState().currentPos;
} }
}; };
let sliceHeight; let getSliceHeight = function(number){
function setNumberOfSlices(number){ return Math.floor(Bangle.appRect.h/WIDGETS.gpstrek.getState().numberOfSlices);
numberOfSlices = number; };
sliceHeight = Math.floor((g.getHeight()-(showWidgets?24:0))/numberOfSlices);
}
let slices = [];
let maxScreens = 1;
setNumberOfSlices(3);
let compassSlice = getCompassSlice(compassSliceData); let compassSlice = getCompassSlice(compassSliceData);
let waypointSlice = getTargetSlice(waypointData); let waypointSlice = getTargetSlice(waypointData);
let finishSlice = getTargetSlice(finishData); let finishSlice = getTargetSlice(finishData);
let eleSlice = getDoubleLineSlice("Up","Down",()=>{ let eleSlice = getDoubleLineSlice("Up","Down",()=>{
return loc.distance(state.up,3) + "/" + (state.route ? loc.distance(state.route.up,3):"---"); return loc.distance(WIDGETS.gpstrek.getState().up,3) + "/" + (WIDGETS.gpstrek.getState().route ? loc.distance(WIDGETS.gpstrek.getState().route.up,3):"---");
},()=>{ },()=>{
return loc.distance(state.down,3) + "/" + (state.route ? loc.distance(state.route.down,3): "---"); return loc.distance(WIDGETS.gpstrek.getState().down,3) + "/" + (WIDGETS.gpstrek.getState().route ? loc.distance(WIDGETS.gpstrek.getState().route.down,3): "---");
}); });
let statusSlice = getDoubleLineSlice("Speed","Alt",()=>{ let statusSlice = getDoubleLineSlice("Speed","Alt",()=>{
let speed = 0; let speed = 0;
if (state.currentPos && state.currentPos.speed) speed = state.currentPos.speed; if (WIDGETS.gpstrek.getState().currentPos && WIDGETS.gpstrek.getState().currentPos.speed) speed = WIDGETS.gpstrek.getState().currentPos.speed;
return loc.speed(speed,2); return loc.speed(speed,2);
},()=>{ },()=>{
let alt = Infinity; let alt = Infinity;
if (!isNaN(state.altitude)){ if (!isNaN(WIDGETS.gpstrek.getState().altitude)){
alt = isNaN(state.calibAltDiff) ? state.altitude : (state.altitude - state.calibAltDiff); alt = isNaN(WIDGETS.gpstrek.getState().calibAltDiff) ? WIDGETS.gpstrek.getState().altitude : (WIDGETS.gpstrek.getState().altitude - WIDGETS.gpstrek.getState().calibAltDiff);
} }
if (state.currentPos && state.currentPos.alt) alt = state.currentPos.alt; if (WIDGETS.gpstrek.getState().currentPos && WIDGETS.gpstrek.getState().currentPos.alt) alt = WIDGETS.gpstrek.getState().currentPos.alt;
if (isNaN(alt)) return "---";
return loc.distance(alt,3); return loc.distance(alt,3);
}); });
let status2Slice = getDoubleLineSlice("Compass","GPS",()=>{ let status2Slice = getDoubleLineSlice("Compass","GPS",()=>{
return (state.compassHeading?Math.round(state.compassHeading):"---") + "°"; return getAveragedCompass() + "°";
},()=>{ },()=>{
let course = "---°"; let course = "---°";
if (state.currentPos && state.currentPos.course) course = state.currentPos.course + "°"; if (WIDGETS.gpstrek.getState().currentPos && WIDGETS.gpstrek.getState().currentPos.course) course = WIDGETS.gpstrek.getState().currentPos.course + "°";
return course; return course;
},200); },200);
let healthSlice = getDoubleLineSlice("Heart","Steps",()=>{ let healthSlice = getDoubleLineSlice("Heart","Steps",()=>{
return state.bpm; return WIDGETS.gpstrek.getState().bpm || "---";
},()=>{ },()=>{
return state.steps; return !isNaN(WIDGETS.gpstrek.getState().steps)? WIDGETS.gpstrek.getState().steps: "---";
}); });
let system2Slice = getDoubleLineSlice("Bat","",()=>{ let system2Slice = getDoubleLineSlice("Bat","",()=>{
return (Bangle.isCharging()?"+":"") + E.getBattery().toFixed(0)+"% " + NRF.getBattery().toFixed(2) + "V"; return (Bangle.isCharging()?"+":"") + E.getBattery().toFixed(0)+"% " + (analogRead(D3)*4.2/BAT_FULL).toFixed(2) + "V";
},()=>{ },()=>{
return ""; return "";
}); });
@ -798,17 +836,17 @@ let systemSlice = getDoubleLineSlice("RAM","Storage",()=>{
return (STORAGE.getFree()/1024).toFixed(0)+"kB"; return (STORAGE.getFree()/1024).toFixed(0)+"kB";
}); });
function updateSlices(){ let updateSlices = function(){
slices = []; slices = [];
slices.push(compassSlice); slices.push(compassSlice);
if (state.currentPos && state.currentPos.lat && state.route && state.route.currentWaypoint && state.route.index < state.route.count - 1) { if (WIDGETS.gpstrek.getState().currentPos && WIDGETS.gpstrek.getState().currentPos.lat && WIDGETS.gpstrek.getState().route && WIDGETS.gpstrek.getState().route.currentWaypoint && WIDGETS.gpstrek.getState().route.index < WIDGETS.gpstrek.getState().route.count - 1) {
slices.push(waypointSlice); slices.push(waypointSlice);
} }
if (state.currentPos && state.currentPos.lat && (state.route || state.waypoint)) { if (WIDGETS.gpstrek.getState().currentPos && WIDGETS.gpstrek.getState().currentPos.lat && (WIDGETS.gpstrek.getState().route || WIDGETS.gpstrek.getState().waypoint)) {
slices.push(finishSlice); slices.push(finishSlice);
} }
if ((state.route && state.route.down !== undefined) || state.down != undefined) { if ((WIDGETS.gpstrek.getState().route && WIDGETS.gpstrek.getState().route.down !== undefined) || WIDGETS.gpstrek.getState().down != undefined) {
slices.push(eleSlice); slices.push(eleSlice);
} }
slices.push(statusSlice); slices.push(statusSlice);
@ -816,42 +854,44 @@ function updateSlices(){
slices.push(healthSlice); slices.push(healthSlice);
slices.push(systemSlice); slices.push(systemSlice);
slices.push(system2Slice); slices.push(system2Slice);
maxScreens = Math.ceil(slices.length/numberOfSlices); maxScreens = Math.ceil(slices.length/WIDGETS.gpstrek.getState().numberOfSlices);
} };
function clear() { let clear = function() {
g.clearRect(0,(showWidgets ? 24 : 0), g.getWidth(),g.getHeight()); g.clearRect(Bangle.appRect);
} };
let lastDrawnScreen;
let firstDraw = true;
function draw(){ let draw = function(){
if (!screen) return; if (!global.screen) return;
let ypos = showWidgets ? 24 : 0; let ypos = Bangle.appRect.y;
let firstSlice = (screen-1)*numberOfSlices; let firstSlice = (screen-1)*WIDGETS.gpstrek.getState().numberOfSlices;
updateSlices(); updateSlices();
let force = lastDrawnScreen != screen || firstDraw; let force = lastDrawnScreen != screen || firstDraw;
if (force){ if (force){
clear(); clear();
if (showWidgets){
Bangle.drawWidgets();
}
} }
if (firstDraw) Bangle.drawWidgets();
lastDrawnScreen = screen; lastDrawnScreen = screen;
for (let slice of slices.slice(firstSlice,firstSlice + numberOfSlices)) { let sliceHeight = getSliceHeight();
for (let slice of slices.slice(firstSlice,firstSlice + WIDGETS.gpstrek.getState().numberOfSlices)) {
g.reset(); g.reset();
if (!slice.refresh || slice.refresh() || force) slice.draw(g,0,ypos,sliceHeight,g.getWidth()); if (!slice.refresh || slice.refresh() || force) slice.draw(g,0,ypos,sliceHeight,g.getWidth());
ypos += sliceHeight+1; ypos += sliceHeight+1;
g.drawLine(0,ypos-1,g.getWidth(),ypos-1); g.drawLine(0,ypos-1,g.getWidth(),ypos-1);
} }
if (scheduleDraw){
drawInTimeout();
}
firstDraw = false; firstDraw = false;
} };
switchNav(); switchNav();
g.clear(); clear();
}

View File

@ -1,7 +1,7 @@
{ {
"id": "gpstrek", "id": "gpstrek",
"name": "GPS Trekking", "name": "GPS Trekking",
"version": "0.07", "version": "0.08",
"description": "Helper for tracking the status/progress during hiking. Do NOT depend on this for navigation!", "description": "Helper for tracking the status/progress during hiking. Do NOT depend on this for navigation!",
"icon": "icon.png", "icon": "icon.png",
"screenshots": [{"url":"screen1.png"},{"url":"screen2.png"},{"url":"screen3.png"},{"url":"screen4.png"}], "screenshots": [{"url":"screen1.png"},{"url":"screen2.png"},{"url":"screen3.png"},{"url":"screen4.png"}],

View File

@ -1,6 +1,28 @@
(() => { (() => {
const SAMPLES=5;
function initState(){
//cleanup volatile state here
state = {};
state.compassSamples = new Array(SAMPLES).fill(0);
state.lastSample = 0;
state.sampleIndex = 0;
state.currentPos={};
state.steps = 0;
state.calibAltDiff = 0;
state.numberOfSlices = 3;
state.steps = 0;
state.up = 0;
state.down = 0;
state.saved = 0;
state.avgComp = 0;
}
const STORAGE=require('Storage'); const STORAGE=require('Storage');
let state = STORAGE.readJSON("gpstrek.state.json")||{}; let state = STORAGE.readJSON("gpstrek.state.json");
if (!state) {
state = {};
initState();
}
let bgChanged = false; let bgChanged = false;
function saveState(){ function saveState(){
@ -8,12 +30,13 @@ function saveState(){
STORAGE.writeJSON("gpstrek.state.json", state); STORAGE.writeJSON("gpstrek.state.json", state);
} }
E.on("kill",()=>{ function onKill(){
if (bgChanged){ if (bgChanged || state.route || state.waypoint){
saveState(); saveState();
} }
}); }
E.on("kill", onKill);
function onPulse(e){ function onPulse(e){
state.bpm = e.bpm; state.bpm = e.bpm;
@ -23,27 +46,47 @@ function onGPS(fix) {
if(fix.fix) state.currentPos = fix; if(fix.fix) state.currentPos = fix;
} }
function onMag(e) { let radians = function(a) {
if (!state.compassHeading) state.compassHeading = e.heading; return a*Math.PI/180;
};
//if (a+180)mod 360 == b then let degrees = function(a) {
//return (a+b)/2 mod 360 and ((a+b)/2 mod 360) + 180 (they are both the solution, so you may choose one depending if you prefer counterclockwise or clockwise direction) let d = a*180/Math.PI;
//else return (d+360)%360;
//return arctan( (sin(a)+sin(b)) / (cos(a)+cos(b) ) };
/* function average(samples){
let average; let s = 0;
let a = radians(compassHeading); let c = 0;
let b = radians(e.heading); for (let h of samples){
if ((a+180) % 360 == b){ s += Math.sin(radians(h));
average = ((a+b)/2 % 360); //can add 180 depending on rotation c += Math.cos(radians(h));
} else { }
average = Math.atan( (Math.sin(a)+Math.sin(b))/(Math.cos(a)+Math.cos(b)) ); s /= samples.length;
c /= samples.length;
let result = degrees(Math.atan(s/c));
if (c < 0) result += 180;
if (s < 0 && c > 0) result += 360;
result%=360;
return result;
}
function onMag(e) {
if (!isNaN(e.heading)){
if (Bangle.isLocked() || (Bangle.getGPSFix() && Bangle.getGPSFix().lon))
state.avgComp = e.heading;
else {
state.compassSamples[state.sampleIndex++] = e.heading;
state.lastSample = Date.now();
if (state.sampleIndex > SAMPLES - 1){
state.sampleIndex = 0;
let avg = average(state.compassSamples);
state.avgComp = average([state.avgComp,avg]);
}
}
} }
print("Angle",compassHeading,e.heading, average);
compassHeading = (compassHeading + degrees(average)) % 360;
*/
state.compassHeading = Math.round(e.heading);
} }
function onStep(e) { function onStep(e) {
@ -73,6 +116,16 @@ function onAcc (e){
state.acc = e; state.acc = e;
} }
function update(){
if (state.active){
start(false);
}
if (state.active == !(WIDGETS.gpstrek.width)) {
if(WIDGETS.gpstrek) WIDGETS.gpstrek.width = state.active?24:0;
Bangle.drawWidgets();
}
}
function start(bg){ function start(bg){
Bangle.removeListener('GPS', onGPS); Bangle.removeListener('GPS', onGPS);
Bangle.removeListener("HRM", onPulse); Bangle.removeListener("HRM", onPulse);
@ -94,9 +147,9 @@ function start(bg){
if (bg){ if (bg){
if (!state.active) bgChanged = true; if (!state.active) bgChanged = true;
state.active = true; state.active = true;
update();
saveState(); saveState();
} }
Bangle.drawWidgets();
} }
function stop(bg){ function stop(bg){
@ -114,22 +167,10 @@ function stop(bg){
Bangle.removeListener("step", onStep); Bangle.removeListener("step", onStep);
Bangle.removeListener("pressure", onPressure); Bangle.removeListener("pressure", onPressure);
Bangle.removeListener('accel', onAcc); Bangle.removeListener('accel', onAcc);
E.removeListener("kill", onKill);
} }
update();
saveState(); saveState();
Bangle.drawWidgets();
}
function initState(){
//cleanup volatile state here
state.currentPos={};
state.steps = Bangle.getStepCount();
state.calibAltDiff = 0;
state.up = 0;
state.down = 0;
}
if (state.saved && state.saved < Date.now() - 60000){
initState();
} }
if (state.active){ if (state.active){
@ -141,11 +182,15 @@ WIDGETS["gpstrek"]={
width:state.active?24:0, width:state.active?24:0,
resetState: initState, resetState: initState,
getState: function() { getState: function() {
if (state.saved && Date.now() - state.saved > 60000 || !state){
initState();
}
return state; return state;
}, },
start:start, start:start,
stop:stop, stop:stop,
draw:function() { draw:function() {
update();
if (state.active){ if (state.active){
g.reset(); g.reset();
g.drawImage(atob("GBiBAAAAAAAAAAAYAAAYAAAYAAA8AAA8AAB+AAB+AADbAADbAAGZgAGZgAMYwAMYwAcY4AYYYA5+cA3/sB/D+B4AeBAACAAAAAAAAA=="), this.x, this.y); g.drawImage(atob("GBiBAAAAAAAAAAAYAAAYAAAYAAA8AAA8AAB+AAB+AADbAADbAAGZgAGZgAMYwAMYwAcY4AYYYA5+cA3/sB/D+B4AeBAACAAAAAAAAA=="), this.x, this.y);

View File

@ -5,7 +5,7 @@
"description": "Integrates your BangleJS into HomeAssistant.", "description": "Integrates your BangleJS into HomeAssistant.",
"icon": "ha.png", "icon": "ha.png",
"type": "app", "type": "app",
"tags": "tool", "tags": "tool,clkinfo",
"readme": "README.md", "readme": "README.md",
"supports": ["BANGLEJS2"], "supports": ["BANGLEJS2"],
"custom": "custom.html", "custom": "custom.html",

View File

@ -7,3 +7,5 @@
0.21: Add Settings 0.21: Add Settings
0.22: Use default Bangle formatter for booleans 0.22: Use default Bangle formatter for booleans
0.23: Added note to configure position in "my location" if not done yet. Small fixes. 0.23: Added note to configure position in "my location" if not done yet. Small fixes.
0.24: Added fast load
0.25: Minor code optimization

View File

@ -1,3 +1,5 @@
{ // must be inside our own scope here so that when we are unloaded everything disappears
// ------- Settings file // ------- Settings file
const SETTINGSFILE = "hworldclock.json"; const SETTINGSFILE = "hworldclock.json";
var secondsMode; var secondsMode;
@ -153,15 +155,15 @@ function updatePos() {
function drawSeconds() { function drawSeconds() {
// get date // get date
var d = new Date(); let d = new Date();
var da = d.toString().split(" "); let da = d.toString().split(" ");
// default draw styles // default draw styles
g.reset().setBgColor(g.theme.bg).setFontAlign(0, 0); g.reset().setBgColor(g.theme.bg).setFontAlign(0, 0);
// draw time // draw time
var time = da[4].split(":"); let time = da[4].split(":");
var seconds = time[2]; let seconds = time[2];
g.setFont("5x9Numeric7Seg",primaryTimeFontSize - 3); g.setFont("5x9Numeric7Seg",primaryTimeFontSize - 3);
if (g.theme.dark) { if (g.theme.dark) {
@ -184,15 +186,15 @@ function drawSeconds() {
function draw() { function draw() {
// get date // get date
var d = new Date(); let d = new Date();
var da = d.toString().split(" "); let da = d.toString().split(" ");
// default draw styles // default draw styles
g.reset().setBgColor(g.theme.bg).setFontAlign(0, 0); g.reset().setBgColor(g.theme.bg).setFontAlign(0, 0);
// draw time // draw time
var time = da[4].split(":"); let time = da[4].split(":");
var hours = time[0], let hours = time[0],
minutes = time[1]; minutes = time[1];
@ -223,7 +225,7 @@ function draw() {
// am / PM ? // am / PM ?
if (_12hour){ if (_12hour){
//do 12 hour stuff //do 12 hour stuff
//var ampm = require("locale").medidian(new Date()); Not working //let ampm = require("locale").medidian(new Date()); Not working
g.setFont("Vector", 17); g.setFont("Vector", 17);
g.drawString(ampm, xyCenterSeconds, yAmPm, true); g.drawString(ampm, xyCenterSeconds, yAmPm, true);
} }
@ -232,14 +234,14 @@ function draw() {
// draw Day, name of month, Date // draw Day, name of month, Date
//DATE //DATE
var localDate = require("locale").date(new Date(), 1); let localDate = require("locale").date(new Date(), 1);
localDate = localDate.substring(0, localDate.length - 5); localDate = localDate.substring(0, localDate.length - 5);
g.setFont("Vector", 17); g.setFont("Vector", 17);
g.drawString(require("locale").dow(new Date(), 1).toUpperCase() + ", " + localDate, xyCenter, yposDate, true); g.drawString(require("locale").dow(new Date(), 1).toUpperCase() + ", " + localDate, xyCenter, yposDate, true);
g.setFont(font, primaryDateFontSize); g.setFont(font, primaryDateFontSize);
// set gmt to UTC+0 // set gmt to UTC+0
var gmt = new Date(d.getTime() + d.getTimezoneOffset() * 60 * 1000); let gmt = new Date(d.getTime() + d.getTimezoneOffset() * 60 * 1000);
// Loop through offset(s) and render // Loop through offset(s) and render
offsets.forEach((offset, index) => { offsets.forEach((offset, index) => {
@ -249,7 +251,7 @@ function draw() {
if (offsets.length === 1) { if (offsets.length === 1) {
var date = [require("locale").dow(new Date(), 1), require("locale").date(new Date(), 1)]; let date = [require("locale").dow(new Date(), 1), require("locale").date(new Date(), 1)];
// For a single secondary timezone, draw it bigger and drop time zone to second line // For a single secondary timezone, draw it bigger and drop time zone to second line
const xOffset = 30; const xOffset = 30;
g.setFont(font, secondaryTimeFontSize).drawString(`${hours}:${minutes}`, xyCenter, yposTime2, true); g.setFont(font, secondaryTimeFontSize).drawString(`${hours}:${minutes}`, xyCenter, yposTime2, true);
@ -295,8 +297,18 @@ g.clear();
// Init the settings of the app // Init the settings of the app
loadMySettings(); loadMySettings();
// Show launcher when button pressed // Show launcher when middle button pressed
Bangle.setUI("clock"); Bangle.setUI({
mode : "clock",
remove : function() {
// Called to unload all of the clock app
if (PosInterval) clearInterval(PosInterval);
PosInterval = undefined;
if (drawTimeoutSeconds) clearTimeout(drawTimeoutSeconds);
drawTimeoutSeconds = undefined;
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
}});
Bangle.loadWidgets(); Bangle.loadWidgets();
Bangle.drawWidgets(); Bangle.drawWidgets();
@ -307,7 +319,7 @@ draw();
if (!Bangle.isLocked()) { // Initial state if (!Bangle.isLocked()) { // Initial state
if (showSunInfo) { if (showSunInfo) {
if (PosInterval != 0) clearInterval(PosInterval); if (PosInterval != 0 && typeof PosInterval != 'undefined') clearInterval(PosInterval);
PosInterval = setInterval(updatePos, 60*10E3); // refesh every 10 mins PosInterval = setInterval(updatePos, 60*10E3); // refesh every 10 mins
updatePos(); updatePos();
} }
@ -333,7 +345,7 @@ if (!Bangle.isLocked()) { // Initial state
drawTimeout = undefined; drawTimeout = undefined;
if (showSunInfo) { if (showSunInfo) {
if (PosInterval != 0) clearInterval(PosInterval); if (PosInterval != 0 && typeof PosInterval != 'undefined') clearInterval(PosInterval);
PosInterval = setInterval(updatePos, 60*60E3); // refesh every 60 mins PosInterval = setInterval(updatePos, 60*60E3); // refesh every 60 mins
updatePos(); updatePos();
} }
@ -379,3 +391,4 @@ Bangle.on('lock',on=>{
draw(); // draw immediately, queue redraw draw(); // draw immediately, queue redraw
} }
}); });
}

View File

@ -2,7 +2,7 @@
"id": "hworldclock", "id": "hworldclock",
"name": "Hanks World Clock", "name": "Hanks World Clock",
"shortName": "Hanks World Clock", "shortName": "Hanks World Clock",
"version": "0.23", "version": "0.25",
"description": "Current time zone plus up to three others", "description": "Current time zone plus up to three others",
"allow_emulator":true, "allow_emulator":true,
"icon": "app.png", "icon": "app.png",

View File

@ -12,3 +12,9 @@
used Object.assing for the settings used Object.assing for the settings
fix cache not deleted when "showClocks" options is changed fix cache not deleted when "showClocks" options is changed
added timeOut to return to the clock added timeOut to return to the clock
0.11: Cleanup timeout when changing to clock
Reset timeout on swipe and drag
0.12: Use Bangle.load and Bangle.showClock
0.13: Fix automatic switch to clock
0.14: Revert use of Bangle.load to classic load calls since widgets would
still be loaded when they weren't supposed to.

View File

@ -9,7 +9,6 @@
timeOut:"Off" timeOut:"Off"
}, s.readJSON("iconlaunch.json", true) || {}); }, s.readJSON("iconlaunch.json", true) || {});
console.log(settings);
if (!settings.fullscreen) { if (!settings.fullscreen) {
Bangle.loadWidgets(); Bangle.loadWidgets();
Bangle.drawWidgets(); Bangle.drawWidgets();
@ -133,6 +132,7 @@
g.flip(); g.flip();
const itemsN = Math.ceil(launchCache.apps.length / appsN); const itemsN = Math.ceil(launchCache.apps.length / appsN);
let onDrag = function(e) { let onDrag = function(e) {
updateTimeout();
g.setColor(g.theme.fg); g.setColor(g.theme.fg);
g.setBgColor(g.theme.bg); g.setBgColor(g.theme.bg);
let dy = e.dy; let dy = e.dy;
@ -182,36 +182,27 @@
drag: onDrag, drag: onDrag,
touch: (_, e) => { touch: (_, e) => {
if (e.y < R.y - 4) return; if (e.y < R.y - 4) return;
updateTimeout();
let i = YtoIdx(e.y); let i = YtoIdx(e.y);
selectItem(i, e); selectItem(i, e);
}, },
swipe: (h,_) => { if(settings.swipeExit && h==1) { returnToClock(); } }, swipe: (h,_) => { if(settings.swipeExit && h==1) { Bangle.showClock(); } },
btn: _=> { if (settings.oneClickExit) Bangle.showClock(); },
remove: function() {
if (timeout) clearTimeout(timeout);
}
}; };
const returnToClock = function() { let timeout;
Bangle.setUI(); const updateTimeout = function(){
delete launchCache;
delete launchHash;
delete drawItemAuto;
delete drawText;
delete selectItem;
delete onDrag;
delete drawItems;
delete drawItem;
delete returnToClock;
delete idxToY;
delete YtoIdx;
delete settings;
setTimeout(eval, 0, s.read(".bootcde"));
};
if (settings.oneClickExit) mode.btn = returnToClock;
if (settings.timeOut!="Off"){ if (settings.timeOut!="Off"){
let time=parseInt(settings.timeOut); //the "s" will be trimmed by the parseInt let time=parseInt(settings.timeOut); //the "s" will be trimmed by the parseInt
setTimeout(returnToClock,time*1000); if (timeout) clearTimeout(timeout);
} timeout = setTimeout(Bangle.showClock,time*1000);
}
};
updateTimeout();
Bangle.setUI(mode); Bangle.setUI(mode);
} }

View File

@ -2,7 +2,7 @@
"id": "iconlaunch", "id": "iconlaunch",
"name": "Icon Launcher", "name": "Icon Launcher",
"shortName" : "Icon launcher", "shortName" : "Icon launcher",
"version": "0.10", "version": "0.14",
"icon": "app.png", "icon": "app.png",
"description": "A launcher inspired by smartphones, with an icon-only scrollable menu.", "description": "A launcher inspired by smartphones, with an icon-only scrollable menu.",
"tags": "tool,system,launcher", "tags": "tool,system,launcher",

View File

@ -12,3 +12,9 @@
0.10: Fix clock not correctly refreshing when drawing in timeouts option is not on 0.10: Fix clock not correctly refreshing when drawing in timeouts option is not on
0.11: Additional option in customizer to force drawing directly 0.11: Additional option in customizer to force drawing directly
Fix some problems in handling timeouts Fix some problems in handling timeouts
0.12: Use widget_utils module
Fix colorsetting in promises in generated code
Some performance improvements by caching lookups
Activate UI after first draw is complete to prevent drawing over launcher
0.13: Use widget_utils swipeOn()
Allows minification by combining all but picture data into one file

View File

@ -1,7 +1,12 @@
let unlockedDrawInterval = []; let s = {};
let lockedDrawInterval = []; // unlocked draw intervals
let showWidgets = false; s.udi = [];
let firstDraw = true; // locked draw intervals
s.ldi = [];
// full draw
s.fd = true;
// performance log
s.pl = {};
{ {
let x = g.getWidth()/2; let x = g.getWidth()/2;
@ -21,12 +26,10 @@ let firstDraw = true;
let precompiledJs = eval(require("Storage").read("imageclock.draw.js")); let precompiledJs = eval(require("Storage").read("imageclock.draw.js"));
let settings = require('Storage').readJSON("imageclock.json", true) || {}; let settings = require('Storage').readJSON("imageclock.json", true) || {};
let performanceLog = {};
let startPerfLog = () => {}; let startPerfLog = () => {};
let endPerfLog = () => {}; let endPerfLog = () => {};
Bangle.printPerfLog = () => {print("Deactivated");}; Bangle.printPerfLog = () => {print("Deactivated");};
Bangle.resetPerfLog = () => {performanceLog = {};}; Bangle.resetPerfLog = () => {s.pl = {};};
let colormap={ let colormap={
"#000":0, "#000":0,
@ -64,35 +67,37 @@ let firstDraw = true;
if (settings.perflog){ if (settings.perflog){
startPerfLog = function(name){ startPerfLog = function(name){
let time = getTime(); let time = getTime();
if (!performanceLog.start) performanceLog.start={}; if (!s.pl.start) s.pl.start={};
performanceLog.start[name] = time; s.pl.start[name] = time;
}; };
endPerfLog = function (name){ endPerfLog = function (name, once){
let time = getTime(); let time = getTime();
if (!performanceLog.last) performanceLog.last={}; if (!s.pl.start[name]) return;
let duration = time - performanceLog.start[name]; if (!s.pl.last) s.pl.last={};
performanceLog.last[name] = duration; let duration = time - s.pl.start[name];
if (!performanceLog.cum) performanceLog.cum={}; s.pl.last[name] = duration;
if (!performanceLog.cum[name]) performanceLog.cum[name] = 0; if (!s.pl.cum) s.pl.cum={};
performanceLog.cum[name] += duration; if (!s.pl.cum[name]) s.pl.cum[name] = 0;
if (!performanceLog.count) performanceLog.count={}; s.pl.cum[name] += duration;
if (!performanceLog.count[name]) performanceLog.count[name] = 0; if (!s.pl.count) s.pl.count={};
performanceLog.count[name]++; if (!s.pl.count[name]) s.pl.count[name] = 0;
s.pl.count[name]++;
if (once){s.pl.start[name] = undefined}
}; };
Bangle.printPerfLog = function(){ Bangle.printPerfLog = function(){
let result = ""; let result = "";
let keys = []; let keys = [];
for (let c in performanceLog.cum){ for (let c in s.pl.cum){
keys.push(c); keys.push(c);
} }
keys.sort(); keys.sort();
for (let k of keys){ for (let k of keys){
print(k, "last:", (performanceLog.last[k] * 1000).toFixed(0), "average:", (performanceLog.cum[k]/performanceLog.count[k]*1000).toFixed(0), "count:", performanceLog.count[k], "total:", (performanceLog.cum[k] * 1000).toFixed(0)); print(k, "last:", (s.pl.last[k] * 1000).toFixed(0), "average:", (s.pl.cum[k]/s.pl.count[k]*1000).toFixed(0), "count:", s.pl.count[k], "total:", (s.pl.cum[k] * 1000).toFixed(0));
} }
}; };
} }
startPerfLog("fullDraw");
startPerfLog("loadFunctions"); startPerfLog("loadFunctions");
let delayTimeouts = {}; let delayTimeouts = {};
@ -202,27 +207,39 @@ let firstDraw = true;
let firstDigitY = element.Y; let firstDigitY = element.Y;
let imageIndex = element.ImageIndex ? element.ImageIndex : 0; let imageIndex = element.ImageIndex ? element.ImageIndex : 0;
let firstImage; let firstImage = element.cachedFirstImage;
if (imageIndex){ if (!firstImage && !element.cachedFirstImageMissing){
firstImage = getByPath(resources, [], "" + (0 + imageIndex)); if (imageIndex){
} else { firstImage = getByPath(resources, [], "" + (0 + imageIndex));
firstImage = getByPath(resources, element.ImagePath, 0); } else {
firstImage = getByPath(resources, element.ImagePath, 0);
}
element.cachedFirstImage = firstImage;
if (!firstImage) element.cachedFirstImageMissing = true;
} }
let minusImage; let minusImage = element.cachedMinusImage;
if (imageIndexMinus){ if (!minusImage && !element.cachedMinusImageMissing){
minusImage = getByPath(resources, [], "" + (0 + imageIndexMinus)); if (imageIndexMinus){
} else { minusImage = getByPath(resources, [], "" + (0 + imageIndexMinus));
minusImage = getByPath(resources, element.ImagePath, "minus"); } else {
minusImage = getByPath(resources, element.ImagePath, "minus");
}
element.cachedMinusImage = minusImage;
if (!minusImage) element.cachedMinusImageMissing = true;
} }
let unitImage; let unitImage = element.cachedUnitImage;
//print("Get image for unit", imageIndexUnit); //print("Get image for unit", imageIndexUnit);
if (imageIndexUnit !== undefined){ if (!unitImage && !element.cachedUnitImageMissing){
unitImage = getByPath(resources, [], "" + (0 + imageIndexUnit)); if (imageIndexUnit !== undefined){
//print("Unit image is", unitImage); unitImage = getByPath(resources, [], "" + (0 + imageIndexUnit));
} else if (element.Unit){ //print("Unit image is", unitImage);
unitImage = getByPath(resources, element.ImagePath, getMultistate(element.Unit, "unknown")); } else if (element.Unit){
unitImage = getByPath(resources, element.ImagePath, getMultistate(element.Unit, "unknown"));
}
unitImage = element.cachedUnitImage;
if (!unitImage) element.cachedUnitImageMissing = true;
} }
let numberWidth = (numberOfDigits * firstImage.width) + (Math.max((numberOfDigits - 1),0) * spacing); let numberWidth = (numberOfDigits * firstImage.width) + (Math.max((numberOfDigits - 1),0) * spacing);
@ -292,14 +309,7 @@ let firstDraw = true;
if (resource){ if (resource){
prepareImg(resource); prepareImg(resource);
//print("lastElem", typeof resource) //print("lastElem", typeof resource)
if (resource) { element.cachedImage[cacheKey] = resource;
element.cachedImage[cacheKey] = resource;
//print("cache res ",typeof element.cachedImage[cacheKey]);
} else {
element.cachedImage[cacheKey] = null;
//print("cache null",typeof element.cachedImage[cacheKey]);
//print("Could not create image from", resource);
}
} else { } else {
//print("Could not get resource from", element, lastElem); //print("Could not get resource from", element, lastElem);
} }
@ -604,18 +614,22 @@ let firstDraw = true;
promise.then(()=>{ promise.then(()=>{
let currentDrawingTime = Date.now(); let currentDrawingTime = Date.now();
if (showWidgets && global.WIDGETS){
//print("Draw widgets");
restoreWidgetDraw();
Bangle.drawWidgets();
g.setColor(g.theme.fg);
g.drawLine(0,24,g.getWidth(),24);
}
lastDrawTime = Date.now() - start; lastDrawTime = Date.now() - start;
isDrawing=false; isDrawing=false;
firstDraw=false; s.fd=false;
requestRefresh = false; requestRefresh = false;
endPerfLog("initialDraw"); endPerfLog("initialDraw");
endPerfLog("fullDraw", true);
if (!Bangle.uiRemove){
setUi();
let orig = Bangle.drawWidgets;
Bangle.drawWidgets = ()=>{};
Bangle.loadWidgets();
Bangle.drawWidgets = orig;
require("widget_utils").swipeOn();
Bangle.drawWidgets();
}
}).catch((e)=>{ }).catch((e)=>{
print("Error during drawing", e); print("Error during drawing", e);
}); });
@ -699,16 +713,16 @@ let firstDraw = true;
let handleLock = function(isLocked, forceRedraw){ let handleLock = function(isLocked, forceRedraw){
//print("isLocked", Bangle.isLocked()); //print("isLocked", Bangle.isLocked());
for (let i of unlockedDrawInterval){ for (let i of s.udi){
//print("Clearing unlocked", i); //print("Clearing unlocked", i);
clearInterval(i); clearInterval(i);
} }
for (let i of lockedDrawInterval){ for (let i of s.ldi){
//print("Clearing locked", i); //print("Clearing locked", i);
clearInterval(i); clearInterval(i);
} }
unlockedDrawInterval = []; s.udi = [];
lockedDrawInterval = []; s.ldi = [];
if (!isLocked){ if (!isLocked){
if (forceRedraw || !redrawEvents || (redrawEvents.includes("unlock"))){ if (forceRedraw || !redrawEvents || (redrawEvents.includes("unlock"))){
@ -724,7 +738,7 @@ let firstDraw = true;
initialDraw(watchfaceResources, watchface); initialDraw(watchfaceResources, watchface);
},unlockedRedraw, (v)=>{ },unlockedRedraw, (v)=>{
//print("New matched unlocked interval", v); //print("New matched unlocked interval", v);
unlockedDrawInterval.push(v); s.udi.push(v);
}, lastDrawTime); }, lastDrawTime);
if (!events || events.includes("HRM")) Bangle.setHRMPower(1, "imageclock"); if (!events || events.includes("HRM")) Bangle.setHRMPower(1, "imageclock");
if (!events || events.includes("pressure")) Bangle.setBarometerPower(1, 'imageclock'); if (!events || events.includes("pressure")) Bangle.setBarometerPower(1, 'imageclock');
@ -742,54 +756,13 @@ let firstDraw = true;
initialDraw(watchfaceResources, watchface); initialDraw(watchfaceResources, watchface);
},lockedRedraw, (v)=>{ },lockedRedraw, (v)=>{
//print("New matched locked interval", v); //print("New matched locked interval", v);
lockedDrawInterval.push(v); s.ldi.push(v);
}, lastDrawTime); }, lastDrawTime);
Bangle.setHRMPower(0, "imageclock"); Bangle.setHRMPower(0, "imageclock");
Bangle.setBarometerPower(0, 'imageclock'); Bangle.setBarometerPower(0, 'imageclock');
} }
}; };
let showWidgetsChanged = false;
let currentDragDistance = 0;
let restoreWidgetDraw = function(){
if (global.WIDGETS) {
for (let w in global.WIDGETS) {
let wd = global.WIDGETS[w];
wd.draw = originalWidgetDraw[w];
wd.area = originalWidgetArea[w];
}
}
};
let handleDrag = function(e){
//print("handleDrag");
currentDragDistance += e.dy;
if (Math.abs(currentDragDistance) < 10) return;
dragDown = currentDragDistance > 0;
currentDragDistance = 0;
if (!showWidgets && dragDown){
//print("Enable widgets");
restoreWidgetDraw();
showWidgetsChanged = true;
}
if (showWidgets && !dragDown){
//print("Disable widgets");
clearWidgetsDraw();
firstDraw = true;
showWidgetsChanged = true;
}
if (showWidgetsChanged){
showWidgetsChanged = false;
//print("Draw after widget change");
showWidgets = dragDown;
initialDraw();
}
};
Bangle.on('drag', handleDrag);
if (!events || events.includes("pressure")){ if (!events || events.includes("pressure")){
Bangle.on('pressure', handlePressure); Bangle.on('pressure', handlePressure);
try{ try{
@ -809,68 +782,42 @@ let firstDraw = true;
Bangle.on('charging', handleCharging); Bangle.on('charging', handleCharging);
} }
let originalWidgetDraw = {};
let originalWidgetArea = {};
let clearWidgetsDraw = function(){
//print("Clear widget draw calls");
if (global.WIDGETS) {
originalWidgetDraw = {};
originalWidgetArea = {};
for (let w in global.WIDGETS) {
let wd = global.WIDGETS[w];
originalWidgetDraw[w] = wd.draw;
originalWidgetArea[w] = wd.area;
wd.draw = () => {};
wd.area = "";
}
}
}
handleLock(Bangle.isLocked(), true); handleLock(Bangle.isLocked(), true);
Bangle.setUI({ let setUi = function(){
mode : "clock", Bangle.setUI({
remove : function() { mode : "clock",
//print("remove calls"); remove : function() {
// Called to unload all of the clock app //print("remove calls");
Bangle.setHRMPower(0, "imageclock"); // Called to unload all of the clock app
Bangle.setBarometerPower(0, 'imageclock'); Bangle.setHRMPower(0, "imageclock");
Bangle.setBarometerPower(0, 'imageclock');
Bangle.removeListener('drag', handleDrag); Bangle.removeListener('lock', handleLock);
Bangle.removeListener('lock', handleLock); Bangle.removeListener('charging', handleCharging);
Bangle.removeListener('charging', handleCharging); Bangle.removeListener('HRM', handleHrm);
Bangle.removeListener('HRM', handleHrm); Bangle.removeListener('pressure', handlePressure);
Bangle.removeListener('pressure', handlePressure);
if (deferredTimout) clearTimeout(deferredTimout); if (deferredTimout) clearTimeout(deferredTimout);
if (initialDrawTimeoutUnlocked) clearTimeout(initialDrawTimeoutUnlocked); if (initialDrawTimeoutUnlocked) clearTimeout(initialDrawTimeoutUnlocked);
if (initialDrawTimeoutLocked) clearTimeout(initialDrawTimeoutLocked); if (initialDrawTimeoutLocked) clearTimeout(initialDrawTimeoutLocked);
for (let i of unlockedDrawInterval){ for (let i of global.s.udi){
//print("Clearing unlocked", i); //print("Clearing unlocked", i);
clearInterval(i); clearInterval(i);
}
for (let i of global.s.ldi){
//print("Clearing locked", i);
clearInterval(i);
}
delete Bangle.printPerfLog;
if (settings.perflog){
delete Bangle.resetPerfLog;
}
cleanupDelays();
require("widget_utils").show();
} }
delete unlockedDrawInterval; });
for (let i of lockedDrawInterval){ }
//print("Clearing locked", i);
clearInterval(i);
}
delete lockedDrawInterval;
delete showWidgets;
delete firstDraw;
delete Bangle.printPerfLog;
if (settings.perflog){
delete Bangle.resetPerfLog;
delete performanceLog;
}
cleanupDelays();
restoreWidgetDraw();
}
});
Bangle.loadWidgets();
clearWidgetsDraw();
} }

View File

@ -4,8 +4,8 @@
</head> </head>
<body> <body>
<script src="../../core/lib/heatshrink.js"></script> <script src="../../webtools/heatshrink.js"></script>
<script src="../../core/lib/imageconverter.js"></script> <script src="../../webtools/imageconverter.js"></script>
<script src="../../core/lib/customize.js"></script> <script src="../../core/lib/customize.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.js"></script>
@ -25,6 +25,8 @@
<label for="timeoutwrap">Wrap draw calls in timeouts (Slower, more RAM use, better interactivity)</label></br> <label for="timeoutwrap">Wrap draw calls in timeouts (Slower, more RAM use, better interactivity)</label></br>
<input type="checkbox" id="forceOrigPlane" name="mode" disabled="true"/> <input type="checkbox" id="forceOrigPlane" name="mode" disabled="true"/>
<label for="forceOrigPlane">Force use of direct drawing (Even faster, but will produce visible artifacts on not optimized watch faces)</label></br> <label for="forceOrigPlane">Force use of direct drawing (Even faster, but will produce visible artifacts on not optimized watch faces)</label></br>
<input type="checkbox" id="separateFiles" name="mode"/>
<label for="separateFiles">Do not create combined app flle (slower but more flexible for debugging, incompatible with minification)</label></br>
<input type="checkbox" id="debugprints" name="mode"/> <input type="checkbox" id="debugprints" name="mode"/>
<label for="debugprints">Add debug prints to generated code</label></br> <label for="debugprints">Add debug prints to generated code</label></br>
</p> </p>
@ -656,7 +658,7 @@
var checkcode = ""; var checkcode = "";
if (!(properties.Redraw && properties.Redraw.Clear)){ if (!(properties.Redraw && properties.Redraw.Clear)){
checkcode = 'firstDraw'; checkcode = 's.fd';
for (var i = 0; i< layerElements.length; i++){ for (var i = 0; i< layerElements.length; i++){
var layerElement = layerElements[i]; var layerElement = layerElements[i];
var referencedElement = elements[layerElements[i].index]; var referencedElement = elements[layerElements[i].index];
@ -664,9 +666,9 @@
console.log("Check for change:", layerElement, referencedElement); console.log("Check for change:", layerElement, referencedElement);
if (layerElement.element.Value){ if (layerElement.element.Value){
if (elementType == "MultiState" && layerElement.element.Value) { if (elementType == "MultiState" && layerElement.element.Value) {
checkcode += '| isChangedMultistate(wf.Collapsed[' + layerElement.index + '].value)'; checkcode += '| isChangedMultistate(wf.c[' + layerElement.index + '].value)';
} else { } else {
checkcode += '| isChangedNumber(wf.Collapsed[' + layerElement.index + '].value)'; checkcode += '| isChangedNumber(wf.c[' + layerElement.index + '].value)';
} }
checkForLayerChange = true; checkForLayerChange = true;
} }
@ -693,7 +695,7 @@
if (c.value.Type == "Once"){ if (c.value.Type == "Once"){
if (condition.length > 0) condition += " && "; if (condition.length > 0) condition += " && ";
condition += "firstDraw"; condition += "s.fd";
} }
var planeName = "p" + plane; var planeName = "p" + plane;
@ -714,15 +716,15 @@
} }
if (addDebug()) code += 'print("Element condition is ' + condition + '");' + "\n"; if (addDebug()) code += 'print("Element condition is ' + condition + '");' + "\n";
code += "" + colorsetting;
code += (condition.length > 0 ? "if (" + condition + "){\n" : ""); code += (condition.length > 0 ? "if (" + condition + "){\n" : "");
if (wrapInTimeouts && (plane != 0 || forceUseOrigPlane)){ if (wrapInTimeouts && (plane != 0 || forceUseOrigPlane)){
code += "p = p.then(()=>delay(0)).then(()=>{\n"; code += "p = p.then(()=>delay(0)).then(()=>{\n";
} else { } else {
code += "p = p.then(()=>{\n"; code += "p = p.then(()=>{\n";
} }
code += "" + colorsetting;
if (addDebug()) code += 'print("Drawing element ' + elementIndex + ' with type ' + c.type + ' on plane ' + planeName + '");' + "\n"; if (addDebug()) code += 'print("Drawing element ' + elementIndex + ' with type ' + c.type + ' on plane ' + planeName + '");' + "\n";
code += "draw" + c.type + "(" + planeName + ", wr, wf.Collapsed[" + elementIndex + "].value);\n"; code += "draw" + c.type + "(" + planeName + ", wr, wf.c[" + elementIndex + "].value);\n";
code += "});\n"; code += "});\n";
code += (condition.length > 0 ? "}\n" : ""); code += (condition.length > 0 ? "}\n" : "");
@ -744,9 +746,9 @@
console.log("Created data file", resourceDataString, resourceDataOffset, resultJson); console.log("Created data file", resourceDataString, resourceDataOffset, resultJson);
var properties = faceJson.Properties; var properties = faceJson.Properties;
faceJson = { Properties: properties, Collapsed: collapseTree(faceJson,{X:0,Y:0})}; faceJson = { Properties: properties, c: collapseTree(faceJson,{X:0,Y:0})};
console.log("After collapsing", faceJson); console.log("After collapsing", faceJson);
precompiledJs = convertToCode(faceJson.Collapsed, properties, document.getElementById('timeoutwrap').checked, document.getElementById('forceOrigPlane').checked); precompiledJs = convertToCode(faceJson.c, properties, document.getElementById('timeoutwrap').checked, document.getElementById('forceOrigPlane').checked);
console.log("After precompiling", precompiledJs); console.log("After precompiling", precompiledJs);
} }
@ -1011,22 +1013,45 @@
}); });
document.getElementById("btnUpload").addEventListener("click", function() { document.getElementById("btnUpload").addEventListener("click", function() {
console.log("Fetching app");
fetch('app.js').then((r) => {
console.log("Got response", r);
return r.text();
}
).then((imageclockSrc) => {
console.log("Got src", imageclockSrc)
if (!document.getElementById('separateFiles').checked){
if (precompiledJs.length > 0){
const replacementString = 'eval(require("Storage").read("imageclock.draw.js"))';
console.log("Can replace:", imageclockSrc.includes(replacementString));
imageclockSrc = imageclockSrc.replace(replacementString, precompiledJs);
}
imageclockSrc = imageclockSrc.replace('require("Storage").readJSON("imageclock.face.json")', JSON.stringify(faceJson));
imageclockSrc = imageclockSrc.replace('require("Storage").readJSON("imageclock.resources.json")', JSON.stringify(resultJson));
}
var appDef = { var appDef = {
id : "imageclock", id : "imageclock",
storage:[ storage:[
{name:"imageclock.app.js", url:"app.js"},
{name:"imageclock.resources.json", content: JSON.stringify(resultJson)},
{name:"imageclock.img", url:"app-icon.js", evaluate:true}, {name:"imageclock.img", url:"app-icon.js", evaluate:true},
] ]
}; };
if (document.getElementById('separateFiles').checked){
appDef.storage.push({name:"imageclock.app.js", url:"app.js"});
if (precompiledJs.length > 0){
appDef.storage.push({name:"imageclock.draw.js", content:precompiledJs});
}
appDef.storage.push({name:"imageclock.face.json", content: JSON.stringify(faceJson)});
appDef.storage.push({name:"imageclock.resources.json", content: JSON.stringify(resultJson)});
} else {
appDef.storage.push({name:"imageclock.app.js", url:"pleaseminifycontent.js", content:imageclockSrc});
}
if (resourceDataString.length > 0){ if (resourceDataString.length > 0){
appDef.storage.push({name:"imageclock.resources.data", content: resourceDataString}); appDef.storage.push({name:"imageclock.resources.data", content: resourceDataString});
} }
appDef.storage.push({name:"imageclock.draw.js", content: precompiledJs.length > 0 ? precompiledJs : "//empty"});
appDef.storage.push({name:"imageclock.face.json", content: JSON.stringify(faceJson)});
console.log("Uploading app:", appDef); console.log("Uploading app:", appDef);
sendCustomizedApp(appDef); sendCustomizedApp(appDef);
});
}); });

View File

@ -2,7 +2,7 @@
"id": "imageclock", "id": "imageclock",
"name": "Imageclock", "name": "Imageclock",
"shortName": "Imageclock", "shortName": "Imageclock",
"version": "0.11", "version": "0.13",
"type": "clock", "type": "clock",
"description": "BETA!!! File formats still subject to change --- This app is a highly customizable watchface. To use it, you need to select a watchface. You can build the watchfaces yourself without programming anything. All you need to do is write some json and create image files.", "description": "BETA!!! File formats still subject to change --- This app is a highly customizable watchface. To use it, you need to select a watchface. You can build the watchfaces yourself without programming anything. All you need to do is write some json and create image files.",
"icon": "app.png", "icon": "app.png",

View File

@ -10,7 +10,7 @@
</div> </div>
<script src="../../core/lib/customize.js"></script> <script src="../../core/lib/customize.js"></script>
<script src="../../core/lib/imageconverter.js"></script> <script src="../../webtools/imageconverter.js"></script>
<script> <script>
var faces = []; var faces = [];

3
apps/infoclk/ChangeLog Normal file
View File

@ -0,0 +1,3 @@
0.01: New app!
0.02-0.07: Bug fixes
0.08: Submitted to the app loader

33
apps/infoclk/README.md Normal file
View File

@ -0,0 +1,33 @@
# Informational clock
A configurable clock with extra info and shortcuts when unlocked, but large time when locked
## Information
The clock has two different screen arrangements, depending on whether the watch is locked or unlocked. The most commonly viewed piece of information is the time, so when the watch is locked it optimizes for the time being visible at a glance without the backlight. The hours and minutes take up nearly the entire top half of the display, with the date and seconds taking up nearly the entire bottom half. The day progress bar is between them if enabled, unless configured to be on the bottom row. The bottom row can be configured to display a weather summary, step count, step count and heart rate, the daily progress bar, or nothing.
When the watch is unlocked, it can be assumed that the backlight is on and the user is actively looking at the watch, so instead we can optimize for information density. The bottom half of the display becomes shortcuts, and the top half of the display becomes 4 rows of information (date and time, step count and heart rate, 2 line weather summary) + an optional daily progress bar. (The daily progress bar can be independently enabled when locked and unlocked.)
Most things are self-explanatory, but the day progress bar might not be. The day progress bar is intended to show approximately how far through the day you are, in the form of a progress bar. You might want to configure it to show how far you are through your waking hours, or you might want to use it to show how far you are through your work or school day.
## Shortcuts
There are generally a few apps that the user uses far more frequently than the others. For example, they might use a timer, alarm clock, and calculator every day, while everything else (such as the settings app) gets used only occasionally. This clock has space for 8 apps in the bottom half of the screen only one tap away, avoiding the need to wait for the launcher to open and then scroll through it. Tapping the top of the watch opens the launcher, eliminating the need for the button (which still opens the launcher due to bangle.js conventions). There is also handling for left, right, and vertical swipes. A vertical swipe by default opens the messages app, mimicking mobile operating systems which use a swipe down to view the notification shade.
## Configurability
Displaying the seconds allows for more precise timing, but waking up the CPU to refresh the display more often consumes battery. The user can enable or disable them completely, but can also configure them to be enabled or disabled automatically based on some hueristics:
* They can be hidden while the display is locked, if the user expects to unlock their watch when they need the seconds.
* They can be hidden when the battery is too low, to make the last portion of the battery last a little bit longer.
* They can be hidden during a period of time such as when the user is asleep and therefore unlikely to need very much precision.
The date format can be changed.
As described earlier, the contents of the bottom row when locked can be changed.
The 8 tap-based shortcuts on the bottom and the 3 swipe-based shortcuts can be changed to nothing, the launcher, or any app on the watch.
The start and end time of the day progress bar can be changed. It can be enabled or disabled separately when the watch is locked and unlocked. The color can be changed. The time when it resets from full to empty can be changed.
When the battery is below a defined point, the watch's color can change to another chosen color to help the user notice that the battery is low.

405
apps/infoclk/app.js Normal file
View File

@ -0,0 +1,405 @@
const SETTINGS_FILE = "infoclk.json";
const FONT = require('infoclk-font.js');
const storage = require("Storage");
const locale = require("locale");
const weather = require('weather');
let config = Object.assign({
seconds: {
// Displaying the seconds can reduce battery life because the CPU must wake up more often to update the display.
// The seconds will be shown unless one of these conditions is enabled here, and currently true.
hideLocked: false, // Hide the seconds when the display is locked.
hideBattery: 20, // Hide the seconds when the battery is at or below a defined percentage.
hideTime: true, // Hide the seconds when between a certain period of time. Useful for when you are sleeping and don't need the seconds
hideStart: 2200, // The time when the seconds are hidden: first 2 digits are hours on a 24 hour clock, last 2 are minutes
hideEnd: 700, // The time when the seconds are shown again
hideAlways: false, // Always hide (never show) the seconds
},
date: {
// Settings related to the display of the date
mmdd: true, // If true, display the month first. If false, display the date first.
separator: '-', // The character that goes between the month and date
monthName: false, // If false, display the month as a number. If true, display the name.
monthFullName: false, // If displaying the name: If false, display an abbreviation. If true, display a full name.
dayFullName: false, // If false, display the day of the week's abbreviation. If true, display the full name.
},
bottomLocked: {
display: 'weather' // What to display in the bottom row when locked:
// 'weather': The current temperature and weather description
// 'steps': Step count
// 'health': Step count and bpm
// 'progress': Day progress bar
// false: Nothing
},
shortcuts: [
//8 shortcuts, displayed in the bottom half of the screen (2 rows of 4 shortcuts) when unlocked
// false = no shortcut
// '#LAUNCHER' = open the launcher
// any other string = name of app to open
'stlap', 'keytimer', 'pomoplus', 'alarm',
'rpnsci', 'calendar', 'torch', 'weather'
],
swipe: {
// 3 shortcuts to launch upon swiping:
// false = no shortcut
// '#LAUNCHER' = open the launcher
// any other string = name of app to open
up: 'messages', // Swipe up or swipe down, due to limitation of event handler
left: '#LAUNCHER',
right: '#LAUNCHER',
},
dayProgress: {
// A progress bar representing how far through the day you are
enabledLocked: true, // Whether this bar is enabled when the watch is locked
enabledUnlocked: false, // Whether the bar is enabled when the watch is unlocked
color: [0, 0, 1], // The color of the bar
start: 700, // The time of day that the bar starts filling
end: 2200, // The time of day that the bar becomes full
reset: 300 // The time of day when the progress bar resets from full to empty
},
lowBattColor: {
// The text can change color to indicate that the battery is low
level: 20, // The percentage where this happens
color: [1, 0, 0] // The color that the text changes to
}
}, storage.readJSON(SETTINGS_FILE));
// Return whether the given time (as a date object) is between start and end (as a number where the first 2 digits are hours on a 24 hour clock and the last 2 are minutes), with end time wrapping to next day if necessary
function timeInRange(start, time, end) {
// Convert the given date object to a time number
let timeNumber = time.getHours() * 100 + time.getMinutes();
// Normalize to prevent the numbers from wrapping around at midnight
if (end <= start) {
end += 2400;
if (timeNumber < start) timeNumber += 2400;
}
return start <= timeNumber && timeNumber <= end;
}
// Return whether settings should be displayed based on the user's configuration
function shouldDisplaySeconds(now) {
return !(
(config.seconds.hideAlways) ||
(config.seconds.hideLocked && Bangle.isLocked()) ||
(E.getBattery() <= config.seconds.hideBattery) ||
(config.seconds.hideTime && timeInRange(config.seconds.hideStart, now, config.seconds.hideEnd))
);
}
// Determine the font size needed to fit a string of the given length widthin maxWidth number of pixels, clamped between minSize and maxSize
function getFontSize(length, maxWidth, minSize, maxSize) {
let size = Math.floor(maxWidth / length); //Number of pixels of width available to character
size *= (20 / 12); //Convert to height, assuming 20 pixels of height for every 12 of width
// Clamp to within range
if (size < minSize) return minSize;
else if (size > maxSize) return maxSize;
else return Math.floor(size);
}
// Get the current day of the week according to user settings
function getDayString(now) {
if (config.date.dayFullName) return ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][now.getDay()];
else return ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][now.getDay()];
}
// Pad a number with zeros to be the given number of digits
function pad(number, digits) {
let result = '' + number;
while (result.length < digits) result = '0' + result;
return result;
}
// Get the current date formatted according to the user settings
function getDateString(now) {
let month;
if (!config.date.monthName) month = pad(now.getMonth() + 1, 2);
else if (config.date.monthFullName) month = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'][now.getMonth()];
else month = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][now.getMonth()];
if (config.date.mmdd) return `${month}${config.date.separator}${pad(now.getDate(), 2)}`;
else return `${pad(now.getDate(), 2)}${config.date.separator}${month}`;
}
// Get a floating point number from 0 to 1 representing how far between the user-defined start and end points we are
function getDayProgress(now) {
let start = config.dayProgress.start;
let current = now.getHours() * 100 + now.getMinutes();
let end = config.dayProgress.end;
let reset = config.dayProgress.reset;
// Normalize
if (end <= start) end += 2400;
if (current < start) current += 2400;
if (reset < start) reset += 2400;
// Convert an hhmm number into a floating-point hours
function toDecimalHours(time) {
let hours = Math.floor(time / 100);
let minutes = time % 100;
return hours + (minutes / 60);
}
start = toDecimalHours(start);
current = toDecimalHours(current);
end = toDecimalHours(end);
reset = toDecimalHours(reset);
let progress = (current - start) / (end - start);
if (progress < 0 || progress > 1) {
if (current < reset) return 1;
else return 0;
} else {
return progress;
}
}
// Get a Gadgetbridge weather string
function getWeatherString() {
let current = weather.get();
if (current) return locale.temp(current.temp - 273.15) + ', ' + current.txt;
else return 'Weather unknown!';
}
// Get a second weather row showing humidity, wind speed, and wind direction
function getWeatherRow2() {
let current = weather.get();
if (current) return `${current.hum}%, ${locale.speed(current.wind)} ${current.wrose}`;
else return 'Check Gadgetbridge';
}
// Get a step string
function getStepsString() {
return '' + Bangle.getHealthStatus('day').steps + ' steps';
}
// Get a health string including daily steps and recent bpm
function getHealthString() {
return `${Bangle.getHealthStatus('day').steps} steps ${Bangle.getHealthStatus('last').bpm} bpm`;
}
// Set the next timeout to draw the screen
let drawTimeout;
function setNextDrawTimeout() {
if (drawTimeout) {
clearTimeout(drawTimeout);
drawTimeout = undefined;
}
let time;
let now = new Date();
if (shouldDisplaySeconds(now)) time = 1000 - (now.getTime() % 1000);
else time = 60000 - (now.getTime() % 60000);
drawTimeout = setTimeout(draw, time);
}
const DIGIT_WIDTH = 40; // How much width is allocated for each digit, 37 pixels + 3 pixels of space (which will go off of the screen on the right edge)
const COLON_WIDTH = 19; // How much width is allocated for the colon, 16 pixels + 3 pixels of space
const HHMM_TOP = 27; // 24 pixels for widgets + 3 pixels of space
const DIGIT_HEIGHT = 64; // How tall the digits are
const SECONDS_TOP = HHMM_TOP + DIGIT_HEIGHT + 3; // The top edge of the seconds, top of hours and minutes + digit height + space
const SECONDS_LEFT = 2 * DIGIT_WIDTH + COLON_WIDTH; // The left edge of the seconds: displayed after 2 digits and the colon
const DATE_LETTER_HEIGHT = DIGIT_HEIGHT / 2; // Each letter of the day of week and date will be half the height of the time digits
const DATE_CENTER_X = SECONDS_LEFT / 2; // Day of week and date will be centered between left edge of screen and where seconds start
const DOW_CENTER_Y = SECONDS_TOP + (DATE_LETTER_HEIGHT / 2); // Day of week will be the top row
const DATE_CENTER_Y = DOW_CENTER_Y + DATE_LETTER_HEIGHT; // Date will be the bottom row
const DOW_DATE_CENTER_Y = SECONDS_TOP + (DIGIT_HEIGHT / 2); // When displaying both on one row, center it
const BOTTOM_CENTER_Y = ((SECONDS_TOP + DIGIT_HEIGHT + 3) + g.getHeight()) / 2;
// Draw the clock
function draw() {
//Prepare to draw
g.reset()
.setFontAlign(0, 0);
if (E.getBattery() <= config.lowBattColor.level) {
let color = config.lowBattColor.color;
g.setColor(color[0], color[1], color[2]);
}
now = new Date();
if (Bangle.isLocked()) { //When the watch is locked
g.clearRect(0, 24, g.getWidth(), g.getHeight());
//Draw the hours and minutes
let x = 0;
for (let digit of locale.time(now, 1)) { //apparently this is how you get an hh:mm time string adjusting for the user's 12/24 hour preference
if (digit != ' ') g.drawImage(FONT[digit], x, HHMM_TOP);
if (digit == ':') x += COLON_WIDTH;
else x += DIGIT_WIDTH;
}
if (storage.readJSON('setting.json')['12hour']) g.drawImage(FONT[(now.getHours() < 12) ? 'am' : 'pm'], 0, HHMM_TOP);
//Draw the seconds if necessary
if (shouldDisplaySeconds(now)) {
let tens = Math.floor(now.getSeconds() / 10);
let ones = now.getSeconds() % 10;
g.drawImage(FONT[tens], SECONDS_LEFT, SECONDS_TOP)
.drawImage(FONT[ones], SECONDS_LEFT + DIGIT_WIDTH, SECONDS_TOP);
// Draw the day of week and date assuming the seconds are displayed
g.setFont('Vector', getFontSize(getDayString(now).length, SECONDS_LEFT, 6, DATE_LETTER_HEIGHT))
.drawString(getDayString(now), DATE_CENTER_X, DOW_CENTER_Y)
.setFont('Vector', getFontSize(getDateString(now).length, SECONDS_LEFT, 6, DATE_LETTER_HEIGHT))
.drawString(getDateString(now), DATE_CENTER_X, DATE_CENTER_Y);
} else {
//Draw the day of week and date without the seconds
let string = getDayString(now) + ' ' + getDateString(now);
g.setFont('Vector', getFontSize(string.length, g.getWidth(), 6, DATE_LETTER_HEIGHT))
.drawString(string, g.getWidth() / 2, DOW_DATE_CENTER_Y);
}
// Draw the bottom area
if (config.bottomLocked.display == 'progress') {
let color = config.dayProgress.color;
g.setColor(color[0], color[1], color[2])
.fillRect(0, SECONDS_TOP + DIGIT_HEIGHT + 3, g.getWidth() * getDayProgress(now), g.getHeight());
} else {
let bottomString;
if (config.bottomLocked.display == 'weather') bottomString = getWeatherString();
else if (config.bottomLocked.display == 'steps') bottomString = getStepsString();
else if (config.bottomLocked.display == 'health') bottomString = getHealthString();
else bottomString = ' ';
g.setFont('Vector', getFontSize(bottomString.length, 176, 6, g.getHeight() - (SECONDS_TOP + DIGIT_HEIGHT + 3)))
.drawString(bottomString, g.getWidth() / 2, BOTTOM_CENTER_Y);
}
// Draw the day progress bar between the rows if necessary
if (config.dayProgress.enabledLocked && config.bottomLocked.display != 'progress') {
let color = config.dayProgress.color;
g.setColor(color[0], color[1], color[2])
.fillRect(0, HHMM_TOP + DIGIT_HEIGHT, g.getWidth() * getDayProgress(now), SECONDS_TOP);
}
} else {
//If the watch is unlocked
g.clearRect(0, 24, g.getWidth(), g.getHeight() / 2);
rows = [
`${getDayString(now)} ${getDateString(now)} ${locale.time(now, 1)}`,
getHealthString(),
getWeatherString(),
getWeatherRow2()
];
if (shouldDisplaySeconds(now)) rows[0] += ':' + pad(now.getSeconds(), 2);
if (storage.readJSON('setting.json')['12hour']) rows[0] += ((now.getHours() < 12) ? ' AM' : ' PM');
let maxHeight = ((g.getHeight() / 2) - HHMM_TOP) / (config.dayProgress.enabledUnlocked ? (rows.length + 1) : rows.length);
let y = HHMM_TOP + maxHeight / 2;
for (let row of rows) {
let size = getFontSize(row.length, g.getWidth(), 6, maxHeight);
g.setFont('Vector', size)
.drawString(row, g.getWidth() / 2, y);
y += maxHeight;
}
if (config.dayProgress.enabledUnlocked) {
let color = config.dayProgress.color;
g.setColor(color[0], color[1], color[2])
.fillRect(0, y - maxHeight / 2, 176 * getDayProgress(now), y + maxHeight / 2);
}
}
setNextDrawTimeout();
}
// Draw the icons. This is done separately from the main draw routine to avoid having to scale and draw a bunch of images repeatedly.
function drawIcons() {
g.reset().clearRect(0, 24, g.getWidth(), g.getHeight());
for (let i = 0; i < 8; i++) {
let x = [0, 44, 88, 132, 0, 44, 88, 132][i];
let y = [88, 88, 88, 88, 132, 132, 132, 132][i];
let appId = config.shortcuts[i];
let appInfo = storage.readJSON(appId + '.info', 1);
if (!appInfo) continue;
icon = storage.read(appInfo.icon);
g.drawImage(icon, x, y, {
scale: 0.916666666667
});
}
}
weather.on("update", draw);
Bangle.on("step", draw);
Bangle.on('lock', locked => {
//If the watch is unlocked, draw the icons
if (!locked) drawIcons();
draw();
});
// Show launcher when middle button pressed
Bangle.setUI("clock");
// Load widgets
Bangle.loadWidgets();
Bangle.drawWidgets();
// Launch an app given the current ID. Handles special cases:
// false: Do nothing
// '#LAUNCHER': Open the launcher
// nonexistent app: Do nothing
function launch(appId) {
if (appId == false) return;
else if (appId == '#LAUNCHER') {
Bangle.buzz();
Bangle.showLauncher();
} else {
let appInfo = storage.readJSON(appId + '.info', 1);
if (appInfo) {
Bangle.buzz();
load(appInfo.src);
}
}
}
//Set up touch to launch the selected app
Bangle.on('touch', function (button, xy) {
let x = Math.floor(xy.x / 44);
if (x < 0) x = 0;
else if (x > 3) x = 3;
let y = Math.floor(xy.y / 44);
if (y < 0) y = -1;
else if (y > 3) y = 1;
else y -= 2;
if (y < 0) {
Bangle.buzz();
Bangle.showLauncher();
} else {
let i = 4 * y + x;
launch(config.shortcuts[i]);
}
});
//Set up swipe handler
Bangle.on('swipe', function (direction) {
if (direction == -1) launch(config.swipe.left);
else if (direction == 0) launch(config.swipe.up);
else launch(config.swipe.right);
});
if (!Bangle.isLocked()) drawIcons();
draw();

23
apps/infoclk/font.js Normal file
View File

@ -0,0 +1,23 @@
const heatshrink = require("heatshrink")
function decompress(string) {
return heatshrink.decompress(atob(string))
}
exports = {
'0': decompress("ktAwIEB////EAj4EB+EDAYP/8E/AgWDAYX+CIX/+IDC//PBoYIDAAvwgEHAgOAgAnB/kAgIvCgEPAgJCBv5CCHwXAI4X+KAYk/En4kmAA4qBAAP7BAePAYX4BofBAYX8F4Q+BEwRHBIQI5BA"),
'1': decompress("ktAwIGDj/4AgX/4ADBg/+BAU/+ADBgP/wAEBh/8BoV/8ADBgf/En4k/En4k/EgQ="),
'2': decompress("ktAwMA/4AB/EHAgXwn4EC8IDC/+PAYX+v4EC+YND74NDBAYAE4A0Bg/+HIU/+ADBgP/wAEBh/8BoV/8ADBgf/BAUf/AECElQdBPA2HAYX8OYfHBAYRD8Z3Dj6TG/kPPYZm4EiwAHO4f7BAfPfI/xBoaTEPAfgQwY"),
'3': decompress("ktAwMA/4AB/EHAgXwn4EC8IDC/+PAYX+v4EC+YND74NDBAYAE4A0Bg/+HIU/+ADBgP/wAEBh/8BoV/8ADBgf/BAUf/AECElJWIAEpu/EhpgS34DC/IID54DC/l/AgXDAYX4j57DA"),
'4': decompress("ktAwMA//AgEf//+BYP///wgEHAgOAgE///8gEBBAPggEPAgIWBv///EAgYIBEn4kXABf9AgfnAgY4BAAP4BAfDAYX+EwfwIQRRCJIJRBJIRRBJIQICj5RBJIRRBJIJRCNwJRBNwQk/Ei4A=="),
'5': decompress("ktAwIEB/4AB/EfAgXDAYX+n4EC+YDC/+fAYX9BAfvAgYAJ+AwBgP/wAEBh/8H4V/8ADBgf/BAUf/AEC//AAYMH/wICn4kpPYUPAgXgv4EC4JfDg4DC/iFD8ANDwaTDCQfwEoZ2/EhrXNAAm/AYX5BAfPQoaTD4ahDj57DA=="),
'6': decompress("ktAwIEB/4AB/EfAgXDAYX+n4EC+YDC/+fAYX9BAfvAgYAJ+AwBgP/wAEBh/8H4V/8ADBgf/BAUf/AEC//AAYMH/wICn4kpPYUPAgXgv6AG/6JD/gID84ED358NJIIsCKIQ0BKIRJCFgJJCSYcHAgJuBXYJuBKIQkpAA58D/YIDx6PDBofBQoYvCHwImCI4KUCwA="),
'7': decompress("ktAwMA/4AB/EHAgXwn4EC8IDC/+PAYX+v4EC+YND74NDBAYAE4A0Bg/+HIU/+ADBgP/wAEBh/8BoV/8ADBgf/BAUf/AECEn4k/En4kVA"),
'8': decompress("ktAwIEB////EAj4EB+EDAYP/8E/AgWDAYX+CIX/+IDC//PBoYIDAAvwgEHAgOAgAnB/kAgIvCgEPAgJCBv5CCHwXAI4X+KAYkpAFpu/EhwAHFQIAB/YIDx4DC/AND4IDC/ieD4AmCI4JCBHIIA=="),
'9': decompress("ktAwIEB////EAj4EB+EDAYP/8E/AgWDAYX+CIX/+IDC//PBoYIDAAvwgEHAgOAgAnB/kAgIvCgEPAgJCBv5CCHwXAI4X+KAYkpABf9AgfnAgaFD/AID4Z8DEwfwIQRRCJIJRBJIRRBJIQICj5RBJIRRBJIJRCNwJRBNwQkoPhoAE34DC/L0H/iwBQAv4WAJ7CA=="),
':': decompress("iFAwITQg/gj/4n/8v/+AIP/ABQPDCoIZBDoJTfH94A=="),
'am': decompress("jFAwIEBngCEvwCH/4CFwEBAQkD//AgfnAQcH4fgAQsPwPwAQf/+Ef//4AQn8n0AvgCCHQN+vkAnwCC/EAj4CF+EAh4CCNIoLFC4v8gE/AQv+gF/AQpwB/4CDwICG+/D94CD8/v+fn54CC+P/x4CF+H/IgICFvwCEngCD"),
'pm': decompress("jFAwMAn///l///+/4AE+EAh4CaEYoABFgX8BwMAAUwAFIIv4gEfAQX8OYICF/0Av4CF/8AKQICCwICG+/D94CD8/v+fn54CC+P/x4CF+H/IgICFvwCEngCDA")
}

BIN
apps/infoclk/font/am.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 360 B

BIN
apps/infoclk/font/colon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 B

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