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"]
path = core
url = https://github.com/espruino/EspruinoAppLoaderCore.git
[submodule "webtools"]
path = webtools
url = https://github.com/espruino/EspruinoWebTools.git

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,7 +5,7 @@
"description": "Simple agenda",
"icon": "agenda.png",
"screenshots": [{"url":"screenshot_agenda_overview.png"}, {"url":"screenshot_agenda_event1.png"}, {"url":"screenshot_agenda_event2.png"}],
"tags": "agenda",
"tags": "agenda,clkinfo",
"supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
"allow_emulator": true,

View File

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

View File

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

View File

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

View File

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

View File

@ -14,3 +14,4 @@
0.14: Fix timeout of http function not being cleaned up
0.15: Allow method/body/headers to be specified for `http` (needs Gadgetbridge 0.68.0b or later)
0.16: Bangle.http now fails immediately if there is no Bluetooth connection (fix #2152)
0.17: Now kick off Calendar sync as soon as connected to Gadgetbridge

View File

@ -91,10 +91,6 @@
sched.reload();
},
//TODO perhaps move those in a library (like messages), used also for viewing events?
//simple package with events all together
"calendarevents" : function() {
require("Storage").writeJSON("android.calendar.json", event.events);
},
//add and remove events based on activity on phone (pebble-like)
"calendar" : function() {
var cal = require("Storage").readJSON("android.calendar.json",true);
@ -109,7 +105,7 @@
"calendar-" : function() {
var cal = require("Storage").readJSON("android.calendar.json",true);
//if any of those happen we are out of sync!
if (!cal || !Array.isArray(cal)) return;
if (!cal || !Array.isArray(cal)) cal = [];
cal = cal.filter(e=>e.id!=event.id);
require("Storage").writeJSON("android.calendar.json", cal);
},
@ -170,7 +166,10 @@
// Battery monitor
function sendBattery() { gbSend({ t: "status", bat: E.getBattery(), chg: Bangle.isCharging()?1:0 }); }
NRF.on("connect", () => setTimeout(sendBattery, 2000));
NRF.on("connect", () => setTimeout(function() {
sendBattery();
GB({t:"force_calendar_sync_start"}); // send a list of our calendar entries to start off the sync process
}, 2000));
Bangle.on("charging", sendBattery);
if (!settings.keep)
NRF.on("disconnect", () => require("messages").clearAll()); // remove all messages on disconnect

View File

@ -2,7 +2,7 @@
"id": "android",
"name": "Android Integration",
"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.",
"icon": "app.png",
"tags": "tool,system,messages,notifications,gadgetbridge",

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

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
Ensure clock is only fast-loaded if it doesn't contain widgets
0.52: Ensure heading patch for pre-2v15.68 firmware applies to getCompass
0.53: Add polyfills for pre-2v15.135 firmware for Bangle.load and Bangle.showClock
0.54: Fix for invalid version comparison in polyfill

View File

@ -4,6 +4,7 @@ of the time. */
E.showMessage(/*LANG*/"Updating boot0...");
var s = require('Storage').readJSON('setting.json',1)||{};
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 = "";
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);
@ -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.rotate) boot+=`g.setRotation(${s.rotate&3},${s.rotate>>2});\n` // screen rotation
// ================================================== FIXING OLDER FIRMWARES
// 2v15.68 and before had compass heading inverted.
if (process.version.replace("v","")<215.68)
if (FWVERSION<215.068) // 2v15.68 and before had compass heading inverted.
boot += `Bangle.on('mag',e=>{if(!isNaN(e.heading))e.heading=360-e.heading;});
Bangle.getCompass=(c=>(()=>{e=c();if(!isNaN(e.heading))e.heading=360-e.heading;return e;}))(Bangle.getCompass);`;
// deleting stops us getting confused by our own decl. builtins can't be deleted
// this is a polyfill without fastloading capability
delete Bangle.showClock;
if (!Bangle.showClock) boot += `Bangle.showClock = ()=>{load(".bootcde")};\n`;
delete Bangle.load;
if (!Bangle.load) boot += `Bangle.load = load;\n`;
// ================================================== BOOT.JS
// Append *.boot.js files
// These could change bleServices/bleServiceOptions if needed

View File

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

View File

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

View File

@ -16,5 +16,7 @@
"gracePeriodNotification": 0,
"gracePeriodConnect": 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
supportedCharacteristics["0x2a37"].active = bpm > 0;
log("BTHRM BPM " + supportedCharacteristics["0x2a37"].active);
if (supportedCharacteristics["0x2a37"].active) stopFallback();
switchFallback();
if (bpmTimeout) clearTimeout(bpmTimeout);
bpmTimeout = setTimeout(()=>{
bpmTimeout = undefined;
supportedCharacteristics["0x2a37"].active = false;
startFallback();
}, 3000);
@ -147,15 +148,15 @@ exports.enable = () => {
battery = lastReceivedData["0x180f"]["0x2a19"];
}
if (settings.replace){
if (settings.replace && bpm > 0){
var repEvent = {
bpm: bpm,
confidence: (sensorContact || sensorContact === undefined)? 100 : 0,
src: "bthrm"
};
log("Emitting HRM", repEvent);
Bangle.emit("HRM_int", repEvent);
log("Emitting HRM_R(bt)", repEvent);
Bangle.emit("HRM_R", repEvent);
}
var newEvent = {
@ -254,7 +255,7 @@ exports.enable = () => {
var retry = function() {
log("Retry");
if (!currentRetryTimeout){
if (!currentRetryTimeout && !powerdownRequested){
var clampedTime = retryTime < 100 ? 100 : retryTime;
@ -280,9 +281,13 @@ exports.enable = () => {
log("Disconnect: " + reason);
log("GATT", gatt);
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;
startFallback();
if (!powerdownRequested) startFallback();
blockInit = false;
if (settings.warnDisconnect && !buzzing){
buzzing = true;
@ -312,13 +317,13 @@ exports.enable = () => {
result = result.then(()=>{
log("Starting notifications", newCharacteristic);
var startPromise = newCharacteristic.startNotifications().then(()=>log("Notifications started", newCharacteristic));
if (settings.gracePeriodNotification > 0){
log("Add " + settings.gracePeriodNotification + "ms grace period after starting notifications");
startPromise = startPromise.then(()=>{
log("Wait after connect");
return waitingPromise(settings.gracePeriodNotification);
});
}
log("Add " + settings.gracePeriodNotification + "ms grace period after starting notifications");
startPromise = startPromise.then(()=>{
log("Wait after connect");
return waitingPromise(settings.gracePeriodNotification);
});
return startPromise;
});
}
@ -364,7 +369,7 @@ exports.enable = () => {
var initBt = function () {
log("initBt with blockInit: " + blockInit);
if (blockInit){
if (blockInit && !powerdownRequested){
retry();
return;
}
@ -382,7 +387,13 @@ exports.enable = () => {
return;
}
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){
log("Add " + settings.gracePeriodRequest + "ms grace period after request");
@ -429,30 +440,30 @@ exports.enable = () => {
var connectPromise = gatt.connect(connectSettings).then(function() {
log("Connected.");
});
if (settings.gracePeriodConnect > 0){
log("Add " + settings.gracePeriodConnect + "ms grace period after connecting");
connectPromise = connectPromise.then(()=>{
log("Wait after connect");
return waitingPromise(settings.gracePeriodConnect);
});
}
log("Add " + settings.gracePeriodConnect + "ms grace period after connecting");
connectPromise = connectPromise.then(()=>{
log("Wait after connect");
return waitingPromise(settings.gracePeriodConnect);
});
return connectPromise;
} else {
return Promise.resolve();
}
});
/* promise = promise.then(() => {
log(JSON.stringify(gatt.getSecurityStatus()));
if (gatt.getSecurityStatus()['bonded']) {
log("Already bonded");
return Promise.resolve();
} else {
log("Start bonding");
return gatt.startBonding()
.then(() => console.log(gatt.getSecurityStatus()));
}
});*/
if (settings.bonding){
promise = promise.then(() => {
log(JSON.stringify(gatt.getSecurityStatus()));
if (gatt.getSecurityStatus()['bonded']) {
log("Already bonded");
return Promise.resolve();
} else {
log("Start bonding");
return gatt.startBonding()
.then(() => log("Security status" + gatt.getSecurityStatus()));
}
});
}
promise = promise.then(()=>{
if (!characteristics || characteristics.length === 0){
@ -476,13 +487,11 @@ exports.enable = () => {
log("Supporting service", service.uuid);
result = attachServicePromise(result, service);
}
if (settings.gracePeriodService > 0) {
log("Add " + settings.gracePeriodService + "ms grace period after services");
result = result.then(()=>{
log("Wait after services");
return waitingPromise(settings.gracePeriodService);
});
}
log("Add " + settings.gracePeriodService + "ms grace period after services");
result = result.then(()=>{
log("Wait after services");
return waitingPromise(settings.gracePeriodService);
});
return result;
});
} else {
@ -505,6 +514,8 @@ exports.enable = () => {
});
};
var powerdownRequested = false;
Bangle.setBTHRMPower = function(isOn, app) {
// Do app power handling
if (!app) app="?";
@ -515,11 +526,14 @@ exports.enable = () => {
isOn = Bangle._PWR.BTHRM.length;
// so now we know if we're really on
if (isOn) {
powerdownRequested = false;
switchFallback();
if (!Bangle.isBTHRMConnected()) initBt();
} else { // not on
log("Power off for " + app);
powerdownRequested = true;
clearRetryTimeout(true);
stopFallback();
if (gatt) {
if (gatt.connected){
log("Disconnect with gatt", gatt);
@ -538,39 +552,44 @@ exports.enable = () => {
};
if (settings.replace){
// register a listener for original HRM events and emit as HRM_int
Bangle.on("HRM", (e) => {
e.modified = true;
log("Emitting HRM_int", e);
Bangle.emit("HRM_int", e);
if (fallbackActive){
// if fallback to internal HRM is active, emit as HRM_R to which everyone listens
log("Emitting HRM_R(int)", e);
Bangle.emit("HRM_R", e);
}
});
Bangle.origOn = Bangle.on;
Bangle.on = function(name, callback) {
if (name == "HRM") {
Bangle.origOn("HRM_int", callback);
} else {
Bangle.origOn(name, callback);
}
};
Bangle.origRemoveListener = Bangle.removeListener;
Bangle.removeListener = function(name, callback) {
if (name == "HRM") {
Bangle.origRemoveListener("HRM_int", callback);
} else {
Bangle.origRemoveListener(name, callback);
}
};
// force all apps wanting to listen to HRM to actually get events for HRM_R
Bangle.on = ( o => (name, cb) => {
o = o.bind(Bangle);
if (name == "HRM") o("HRM_R", cb);
else o(name, cb);
})(Bangle.on);
Bangle.removeListener = ( o => (name, cb) => {
o = o.bind(Bangle);
if (name == "HRM") o("HRM_R", cb);
else o(name, cb);
})(Bangle.removeListener);
}
Bangle.origSetHRMPower = Bangle.setHRMPower;
if (settings.startWithHrm){
Bangle.setHRMPower = function(isOn, app) {
log("setHRMPower for " + app + ": " + (isOn?"on":"off"));
if (settings.enabled){
Bangle.setBTHRMPower(isOn, app);
if (Bangle._PWR && Bangle._PWR.HRM && Object.keys(Bangle._PWR.HRM).length == 0) {
Bangle._PWR.BTHRM = [];
Bangle.setBTHRMPower(0);
if (!isOn) stopFallback();
}
}
if ((settings.enabled && !settings.replace) || !settings.enabled){
Bangle.origSetHRMPower(isOn, app);
@ -626,7 +645,11 @@ exports.enable = () => {
E.on("kill", ()=>{
if (gatt && gatt.connected){
log("Got killed, trying to disconnect");
gatt.disconnect().then(()=>log("Disconnected on kill")).catch((e)=>log("Error during disconnnect on kill", e));
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",
"name": "Bluetooth Heart Rate Monitor",
"shortName": "BT HRM",
"version": "0.13",
"version": "0.16",
"description": "Overrides Bangle.js's build in heart rate monitor with an external Bluetooth one.",
"icon": "app.png",
"type": "app",

View File

@ -38,35 +38,32 @@
recorders.hrmint = function() {
var active = false;
var bpmTimeout;
var bpm = "", bpmConfidence = "", src="";
var bpm = "", bpmConfidence = "";
function onHRM(h) {
bpmConfidence = h.confidence;
bpm = h.bpm;
srv = h.src;
if (h.bpm > 0){
active = true;
print("active" + h.bpm);
if (bpmTimeout) clearTimeout(bpmTimeout);
bpmTimeout = setTimeout(()=>{
print("inactive");
active = false;
},3000);
}
}
return {
name : "HR int",
fields : ["Heartrate", "Confidence"],
fields : ["Int Heartrate", "Int Confidence"],
getValues : () => {
var r = [bpm,bpmConfidence,src];
bpm = ""; bpmConfidence = ""; src="";
var r = [bpm,bpmConfidence];
bpm = ""; bpmConfidence = "";
return r;
},
start : () => {
Bangle.origOn('HRM', onHRM);
Bangle.on('HRM_int', onHRM);
if (Bangle.origSetHRMPower) Bangle.origSetHRMPower(1,"recorder");
},
stop : () => {
Bangle.removeListener('HRM', onHRM);
Bangle.removeListener('HRM_int', onHRM);
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)

View File

@ -96,6 +96,18 @@
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); }
};

View File

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

View File

@ -4,3 +4,4 @@
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.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
Bangle.js 1
- UP: BTN1
- DOWN: BTN3
- LEFT: BTN4
- RIGHT: BTN5
- 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
<https://twitter.com/fredericrous>
## Contributors
[thyttan](https://github.com/thyttan)

View File

@ -3,6 +3,8 @@
*
* Original Author: Frederic Rousseau https://github.com/fredericrous
* Created: April 2020
*
* Contributors: thyttan https://github.com/thyttan
*/
g.clear();
@ -402,38 +404,42 @@ if (process.env.HWVERSION==1) {
swipeEnabled = false;
drawGlobal();
} else { // touchscreen?
setWatch(_ => {load();}, BTN1, {edge:'falling'}); // Exit with a short press to physical button
selected = "NONE";
selected = "NONE";
swipeEnabled = true;
prepareScreen(numbers, numbersGrid, COLORS.DEFAULT);
prepareScreen(operators, operatorsGrid, COLORS.OPERATOR);
prepareScreen(specials, specialsGrid, COLORS.SPECIAL);
drawNumbers();
Bangle.on('touch',(n,e)=>{
for (var key in screen) {
if (typeof screen[key] == "undefined") break;
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);
Bangle.setUI({
mode : 'custom',
back : load, // Clicking physical button or pressing upper left corner turns off (where red back button would be)
touch : (n,e)=>{
for (var key in screen) {
if (typeof screen[key] == "undefined") break;
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);

View File

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

View File

@ -10,3 +10,4 @@
0.9: Remove ESLint spaces
0.10: Show daily steps, heartrate and the temperature if weather information is available.
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**:
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 weather = weatherJson.weather;
return Math.round(weather.temp-273.15);
} catch(ex) {
print(ex)
return "?"
@ -99,20 +98,7 @@ function getTemperature(){
}
function getSteps() {
var steps = 0;
try{
if (WIDGETS.wpedom !== undefined) {
steps = WIDGETS.wpedom.getSteps();
} else if (WIDGETS.activepedom !== undefined) {
steps = WIDGETS.activepedom.getSteps();
} else {
steps = Bangle.getHealthStatus("day").steps;
}
} catch(ex) {
// In case we failed, we can only show 0 steps.
return "? k";
}
var steps = Bangle.getHealthStatus("day").steps;
steps = Math.round(steps/1000);
return steps + "k";
}
@ -121,8 +107,7 @@ function getSteps() {
function draw() {
queueDraw();
g.reset();
g.clear();
g.clear(1);
g.setColor(0, 255, 255);
g.fillRect(0, 0, g.getWidth(), g.getHeight());
let background = getBackgroundImage();
@ -143,9 +128,6 @@ function draw() {
drawClock();
drawRocket();
drawBattery();
// Hide widgets
for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";}
}
Bangle.on("lcdPower", (on) => {
@ -169,7 +151,6 @@ Bangle.setUI("clock");
// Load widgets, but don't show them
Bangle.loadWidgets();
g.reset();
g.clear();
require("widget_utils").swipeOn(); // hide widgets, make them visible with a swipe
g.clear(1);
draw();

View File

@ -4,7 +4,7 @@
"description": "Animated Clock with Space Cassio Watch Style",
"screenshots": [{ "url": "screens/screen_night.png" },{ "url": "screens/screen_day.png" }],
"icon": "app.png",
"version": "0.11",
"version": "0.12",
"type": "clock",
"tags": "clock, weather, cassio, retro",
"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.16: Fix const error
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 storage = require("Storage");
Graphics.prototype.setFontRobotoRegular50NumericOnly = function(scale) {
@ -18,6 +19,7 @@ let settings = Object.assign(
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
if (settings.stepGoal == undefined) {
let d = storage.readJSON("health.json", true) || {};
@ -29,7 +31,7 @@ if (settings.stepGoal == undefined) {
}
}
let timerHrm;
let timerHrm; //TODO deprecate this
let drawTimeout;
/*
@ -44,10 +46,9 @@ let showWidgets = settings.showWidgets || false;
let circleCount = settings.circleCount || 3;
let showBigWeather = settings.showBigWeather || false;
let hrtValue;
let hrtValue; //TODO deprecate this
let now = Math.round(new Date().getTime() / 1000);
// layout values:
let colorFg = g.theme.dark ? '#fff' : '#000';
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 circleFontBig = circleCount == 3 ? "Vector:16" : "Vector:12";
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() {
/*
@ -177,6 +190,15 @@ function drawCircle(index) {
let w = getCircleXPosition(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":
drawSteps(w);
break;
@ -189,13 +211,6 @@ function drawCircle(index) {
case "battery":
drawBattery(w);
break;
case "weather":
drawWeather(w);
break;
case "sunprogress":
case "sunProgress":
drawSunProgress(w);
break;
case "temperature":
drawTemperature(w);
break;
@ -205,9 +220,12 @@ function drawCircle(index) {
case "altitude":
drawAltitude(w);
break;
//end deprecated
case "empty":
// we draw nothing here
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) {
if (!w) w = getCircleXPosition("steps");
let steps = getSteps();
@ -406,99 +520,6 @@ function drawBattery(w) {
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) {
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)
*/
@ -770,125 +898,15 @@ function writeCircleText(w, content) {
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() {
let jsonWeather = storage.readJSON('weather.json');
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({
mode : "clock",
remove : function() {
// 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);
drawTimeout = undefined;

View File

@ -1,17 +1,11 @@
{
"minHR": 40,
"maxHR": 200,
"confidence": 0,
"stepGoal": 10000,
"stepDistanceGoal": 8000,
"stepLength": 0.8,
"batteryWarn": 30,
"showWidgets": false,
"weatherCircleData": "humidity",
"circleCount": 3,
"circle1": "hr",
"circle2": "steps",
"circle3": "battery",
"circle1": "Bangle/HRM",
"circle2": "Bangle/Steps",
"circle3": "Bangle/Battery",
"circle4": "weather",
"circle1color": "green-red",
"circle2color": "#0000ff",
@ -21,7 +15,15 @@
"circle2colorizeIcon": true,
"circle3colorizeIcon": true,
"circle4colorizeIcon": false,
"hrmValidity": 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",
"name": "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",
"icon": "app.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) {
const SETTINGS_FILE = "circlesclock.json";
const storage = require('Storage');
const clock_info = require("clock_info");
let settings = Object.assign(
storage.readJSON("circlesclock.default.json", true) || {},
storage.readJSON(SETTINGS_FILE, true) || {}
@ -11,8 +12,25 @@
storage.write(SETTINGS_FILE, settings);
}
const valuesCircleTypes = ["empty", "steps", "stepsDist", "hr", "battery", "weather", "sunprogress", "temperature", "pressure", "altitude"];
const namesCircleTypes = ["empty", "steps", "distance", "heart", "battery", "weather", "sun", "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", "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",
"#00ffff", "#fff", "#000", "green-red", "red-green", "fg"];
@ -36,8 +54,6 @@
/*LANG*/'circle 2': ()=>showCircleMenu(2),
/*LANG*/'circle 3': ()=>showCircleMenu(3),
/*LANG*/'circle 4': ()=>showCircleMenu(4),
/*LANG*/'heartrate': ()=>showHRMenu(),
/*LANG*/'steps': ()=>showStepMenu(),
/*LANG*/'battery warn': {
value: settings.batteryWarn,
min: 10,
@ -78,91 +94,6 @@
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) {
const circleName = "circle" + circleId;
const colorKey = circleName + "color";
@ -192,6 +123,5 @@
E.showMenu(menu);
}
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": [
{"name":"demoapp.app.js","url":"app.js"},
{"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.04: Now displays the opened text string at launch.
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) {
options = options||{};
var text = options.text;
if ("string"!=typeof text) text="";
var R = Bangle.appRect;
var BGCOLOR = g.theme.bg;
var HLCOLOR = g.theme.fg;
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 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 NUMHIDDEN = ' 1234567890!?,.- ';
var NUMPADDING = (g.getWidth()-6*NUM.length)/2;
var NUMPADDING = ((R.x+R.w)-6*NUM.length)/2;
var rectHeight = 40;
var delSpaceLast;
function drawAbcRow() {
g.clear();
try { // Draw widgets if they are present in the current app.
if (WIDGETS) Bangle.drawWidgets();
} catch (_) {}
g.setFont(SMALLFONT);
g.setColor(ABCCOLOR);
g.drawString(ABC, ABCPADDING, g.getHeight()/2);
g.fillRect(0, g.getHeight()-26, g.getWidth(), g.getHeight());
g.setFontAlign(-1, -1, 0);
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() {
g.setFont(SMALLFONT);
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() {
"ram"
g.setColor(BGCOLOR);
g.fillRect(0,4+20,176,13+20);
g.setColor(0.2,0,0);
@ -54,13 +54,10 @@ exports.input = function(options) {
g.setColor(0.7,0,0);
g.fillRect(rectLen+5,4+20,rectLen+10,13+20);
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);
}
drawAbcRow();
drawNumRow();
updateTopString();
var abcHL;
var abcHLPrev = -10;
var numHL;
@ -70,192 +67,180 @@ exports.input = function(options) {
var largeCharOffset = 6;
function resetChars(char, HLPrev, typePadding, heightDivisor, rowColor) {
"ram"
"ram";
// Small character in list
g.setColor(rowColor);
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
g.setColor(BGCOLOR);
g.fillRect(0,g.getHeight()/3,176,g.getHeight()/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.fillRect(0,(R.y+R.h)/3,176,(R.y+R.h)/3+24);
//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
}
function showChars(char, HL, typePadding, heightDivisor) {
"ram"
"ram";
// mark in the list
g.setColor(HLCOLOR);
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
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);
}
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) {
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();
else ABC = ABC.toUpperCase();
g.setColor(ABCCOLOR);
g.drawString(ABC, ABCPADDING, g.getHeight()/2);
g.drawString(ABC, ABCPADDING, (R.y+R.h)/2);
}
return new Promise((resolve,reject) => {
// Interpret touch input
// Interpret touch input
Bangle.setUI({
mode: 'custom',
back: ()=>{
Bangle.setUI();
g.clearRect(Bangle.appRect);
resolve(text);
},
drag: function(event) {
mode: 'custom',
back: ()=>{
Bangle.setUI();
g.clearRect(Bangle.appRect);
resolve(text);
},
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
// Choose character by draging along red rectangle at bottom of screen
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
//print(event.x, event.y, event.b, ABC.charAt(abcHL), ABC.charAt(abcHLPrev));
// Datastream for development purposes
//print(event.x, event.y, event.b, ABC.charAt(abcHL), ABC.charAt(abcHLPrev));
// Unmark previous character and mark the current one...
// 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...
// Handling switching between letters and numbers/punctuation
if (typePrev != 'abc') resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
if (abcHL != abcHLPrev) {
resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
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
if (event.b == 0) {
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);
// Print string at top of screen
if (event.b == 0) {
text = text + ABC.charAt(abcHL);
updateTopString();
// 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);
// 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);
if (numHL != numHLPrev) {
resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
// 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');
showChars(NUM.charAt(numHL), numHL, NUMPADDING, 4);
}
else {
resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
// 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, (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
showChars('space', 0, g.getWidth()/2 +6 -6*3*5/2 , 4);
delSpaceLast = 1;
// Autoswitching letter case
if ((text.charAt(text.length-1) == '.') || (text.charAt(text.length-1) == '!')) changeCase();
}
}
// Update previous character to current one
numHLPrev = numHL;
typePrev = 'num';
}
// Append space and draw string upper right corner
text = text + NUMHIDDEN.charAt(0);
updateTopString();
//print(text, 'made space');
// 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 < (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",
"name": "Dragboard",
"version":"0.05",
"version":"0.06",
"description": "A library for text input via swiping keyboard",
"icon": "app.png",
"type":"textinput",

View File

@ -14,3 +14,13 @@
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.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({
showClocks: true,
showLaunchers: true,
direct: false,
oneClickExit:false,
swipeExit: false
}, require('Storage').readJSON("dtlaunch.json", true) || {});
/* Desktop launcher
*
*/
if( settings.oneClickExit)
setWatch(_=> load(), BTN1);
let settings = Object.assign({
showClocks: true,
showLaunchers: true,
direct: false,
swipeExit: false,
timeOut: "Off"
}, require('Storage').readJSON("dtlaunch.json", true) || {});
var s = require("Storage");
var apps = s.list(/\.info$/).map(app=>{
var a=s.readJSON(app,1);
return a && {
name:a.name, type:a.type, icon:a.icon, sortorder:a.sortorder, src:a.src
};}).filter(
app=>app && (app.type=="app" || (app.type=="clock" && settings.showClocks) || (app.type=="launch" && settings.showLaunchers) || !app.type));
let s = require("Storage");
var apps = s.list(/\.info$/).map(app=>{
let a=s.readJSON(app,1);
return a && {
name:a.name, type:a.type, icon:a.icon, sortorder:a.sortorder, src:a.src
};}).filter(
app=>app && (app.type=="app" || (app.type=="clock" && settings.showClocks) || (app.type=="launch" && settings.showLaunchers) || !app.type));
apps.sort((a,b)=>{
var n=(0|a.sortorder)-(0|b.sortorder);
if (n) return n; // do sortorder first
if (a.name<b.name) return -1;
if (a.name>b.name) return 1;
return 0;
});
apps.forEach(app=>{
apps.sort((a,b)=>{
let n=(0|a.sortorder)-(0|b.sortorder);
if (n) return n; // do sortorder first
if (a.name<b.name) return -1;
if (a.name>b.name) return 1;
return 0;
});
apps.forEach(app=>{
if (app.icon)
app.icon = s.read(app.icon); // should just be a link to a memory area
});
var Napps = apps.length;
var Npages = Math.ceil(Napps/4);
var maxPage = Npages-1;
var selected = -1;
var oldselected = -1;
var page = 0;
const XOFF = 24;
const YOFF = 30;
let Napps = apps.length;
let Npages = Math.ceil(Napps/4);
let maxPage = Npages-1;
let selected = -1;
let oldselected = -1;
let page = 0;
const XOFF = 24;
const YOFF = 30;
function draw_icon(p,n,selected) {
var x = (n%2)*72+XOFF;
var y = n>1?72+YOFF:YOFF;
let drawIcon= function(p,n,selected) {
let x = (n%2)*72+XOFF;
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);
g.clearRect(x+12,y+4,x+59,y+51);
g.setColor(g.theme.fg);
try{g.drawImage(apps[p*4+n].icon,x+12,y+4);} catch(e){}
g.setFontAlign(0,-1,0).setFont("6x8",1);
var txt = apps[p*4+n].name.replace(/([a-z])([A-Z])/g, "$1 $2").split(" ");
var lineY = 0;
var line = "";
let txt = apps[p*4+n].name.replace(/([a-z])([A-Z])/g, "$1 $2").split(" ");
let lineY = 0;
let line = "";
while (txt.length > 0){
var c = txt.shift();
let c = txt.shift();
if (c.length + 1 + line.length > 13){
if (line.length > 0){
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);
}
};
function drawPage(p){
let drawPage = function(p){
g.reset();
g.clearRect(0,24,175,175);
var O = 88+YOFF/2-12*(Npages/2);
for (var j=0;j<Npages;j++){
var y = O+j*12;
g.setColor(g.theme.fg);
if (j==page) g.fillCircle(XOFF/2,y,4);
else g.drawCircle(XOFF/2,y,4);
let O = 88+YOFF/2-12*(Npages/2);
for (let j=0;j<Npages;j++){
let y = O+j*12;
g.setColor(g.theme.fg);
if (j==page) g.fillCircle(XOFF/2,y,4);
else g.drawCircle(XOFF/2,y,4);
}
for (var i=0;i<4;i++) {
if (!apps[p*4+i]) return i;
draw_icon(p,i,selected==i && !settings.direct);
for (let i=0;i<4;i++) {
if (!apps[p*4+i]) return i;
drawIcon(p,i,selected==i && !settings.direct);
}
g.flip();
}
};
Bangle.on("swipe",(dirLeftRight, dirUpDown)=>{
Bangle.loadWidgets();
drawPage(0);
let swipeListenerDt = function(dirLeftRight, dirUpDown){
updateTimeoutToClock();
selected = 0;
oldselected=-1;
if(settings.swipeExit && dirLeftRight==1) load();
if(settings.swipeExit && dirLeftRight==1) Bangle.showClock();
if (dirUpDown==-1||dirLeftRight==-1){
++page; if (page>maxPage) page=0;
drawPage(page);
++page; if (page>maxPage) page=0;
drawPage(page);
} else if (dirUpDown==1||(dirLeftRight==1 && !settings.swipeExit)){
--page; if (page<0) page=maxPage;
drawPage(page);
--page; if (page<0) page=maxPage;
drawPage(page);
}
});
};
function isTouched(p,n){
let isTouched = function(p,n){
if (n<0 || n>3) return false;
var x1 = (n%2)*72+XOFF; var y1 = n>1?72+YOFF:YOFF;
var x2 = x1+71; var y2 = y1+81;
let x1 = (n%2)*72+XOFF; let y1 = n>1?72+YOFF:YOFF;
let x2 = x1+71; let y2 = y1+81;
return (p.x>x1 && p.y>y1 && p.x<x2 && p.y<y2);
}
};
Bangle.on("touch",(_,p)=>{
var i;
let touchListenerDt = function(_,p){
updateTimeoutToClock();
let i;
for (i=0;i<4;i++){
if((page*4+i)<Napps){
if (isTouched(p,i)) {
draw_icon(page,i,true && !settings.direct);
if (selected>=0 || settings.direct) {
if (selected!=i && !settings.direct){
draw_icon(page,selected,false);
} else {
load(apps[page*4+i].src);
}
}
selected=i;
break;
if((page*4+i)<Napps){
if (isTouched(p,i)) {
drawIcon(page,i,true && !settings.direct);
if (selected>=0 || settings.direct) {
if (selected!=i && !settings.direct){
drawIcon(page,selected,false);
} else {
load(apps[page*4+i].src);
}
}
selected=i;
break;
}
}
}
if ((i==4 || (page*4+i)>Napps) && selected>=0) {
draw_icon(page,selected,false);
selected=-1;
drawIcon(page,selected,false);
selected=-1;
}
});
};
Bangle.loadWidgets();
g.clear();
Bangle.drawWidgets();
drawPage(0);
Bangle.setUI({
mode : 'custom',
back : Bangle.showClock,
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",
"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.",
"screenshots": [{"url":"shot1.png"},{"url":"shot2.png"},{"url":"shot3.png"}],
"icon": "icon.png",

View File

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

View File

@ -5,3 +5,4 @@
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
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">
</head>
<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>
<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>
</div>
<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>
<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
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>
<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 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">
<p>The currently available Espruino firmware releases are:</p>
<ul id="latest-firmware-list">
</ul>
<p>To update, click a link above and then click the 'Upload' button that appears.</p>
</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">
<p><b>Advanced</b></p>
<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
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>
<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
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>
</div>
<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 = "";
}
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 ok = true;
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
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(0xF7000,0x7000))==1339551013) { print("BOOTLOADER 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))==1339551013) { print("DFU 2v10.219 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 += '\x10s.erase(".firmware");\n';
var CHUNKSIZE = 2048;
@ -320,7 +334,7 @@ function createJS_app(binary, startAddress, endAddress) {
function createJS_bootloader(binary, startAddress, endAddress) {
var crc = CRC32(binary);
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`;
var CHUNKSIZE = 1024;
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(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 += 'var f = require("Flash");\n';
for (var i=startAddress;i<endAddress;i+=4096)
hexJS += 'f.erasePage(0x'+i.toString(16)+');\n';
hexJS += `f.write(_fw,${startAddress});\n`;
hexJS += `})()\n`;
log("Bootloader ready for upload");
log("DFU ready for upload");
}
function hexFileLoaded(hexString) {
@ -383,7 +397,7 @@ function hexFileLoaded(hexString) {
});
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);
} else {
console.log("App - Writing to external flash");
@ -406,6 +420,10 @@ function handleUpload() {
document.getElementById('fileLoader').addEventListener('change', handleFileSelect, false);
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").style = "display:none";
document.getElementById("advanced-div").style = "";

View File

@ -1,7 +1,7 @@
{
"id": "fwupdate",
"name": "Firmware Update",
"version": "0.04",
"version": "0.05",
"description": "Uploads new Espruino firmwares to Bangle.js 2",
"icon": "app.png",
"type": "RAM",
@ -10,5 +10,5 @@
"custom": "custom.html",
"customConnect": true,
"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",
"icon": "gps-info.png",
"type": "app",
"tags": "gps",
"tags": "gps,outdoors",
"supports": ["BANGLEJS","BANGLEJS2"],
"storage": [
{"name":"gpsinfo.app.js","url":"gps-info.js"},

View File

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

View File

@ -1,7 +1,7 @@
{
"id": "gpstrek",
"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!",
"icon": "icon.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');
let state = STORAGE.readJSON("gpstrek.state.json")||{};
let state = STORAGE.readJSON("gpstrek.state.json");
if (!state) {
state = {};
initState();
}
let bgChanged = false;
function saveState(){
@ -8,12 +30,13 @@ function saveState(){
STORAGE.writeJSON("gpstrek.state.json", state);
}
E.on("kill",()=>{
if (bgChanged){
function onKill(){
if (bgChanged || state.route || state.waypoint){
saveState();
}
});
}
E.on("kill", onKill);
function onPulse(e){
state.bpm = e.bpm;
@ -23,27 +46,47 @@ function onGPS(fix) {
if(fix.fix) state.currentPos = fix;
}
function onMag(e) {
if (!state.compassHeading) state.compassHeading = e.heading;
let radians = function(a) {
return a*Math.PI/180;
};
//if (a+180)mod 360 == b then
//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)
//else
//return arctan( (sin(a)+sin(b)) / (cos(a)+cos(b) )
let degrees = function(a) {
let d = a*180/Math.PI;
return (d+360)%360;
};
/*
let average;
let a = radians(compassHeading);
let b = radians(e.heading);
if ((a+180) % 360 == b){
average = ((a+b)/2 % 360); //can add 180 depending on rotation
} else {
average = Math.atan( (Math.sin(a)+Math.sin(b))/(Math.cos(a)+Math.cos(b)) );
function average(samples){
let s = 0;
let c = 0;
for (let h of samples){
s += Math.sin(radians(h));
c += Math.cos(radians(h));
}
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) {
@ -73,6 +116,16 @@ function onAcc (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){
Bangle.removeListener('GPS', onGPS);
Bangle.removeListener("HRM", onPulse);
@ -94,9 +147,9 @@ function start(bg){
if (bg){
if (!state.active) bgChanged = true;
state.active = true;
update();
saveState();
}
Bangle.drawWidgets();
}
function stop(bg){
@ -114,22 +167,10 @@ function stop(bg){
Bangle.removeListener("step", onStep);
Bangle.removeListener("pressure", onPressure);
Bangle.removeListener('accel', onAcc);
E.removeListener("kill", onKill);
}
update();
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){
@ -141,11 +182,15 @@ WIDGETS["gpstrek"]={
width:state.active?24:0,
resetState: initState,
getState: function() {
if (state.saved && Date.now() - state.saved > 60000 || !state){
initState();
}
return state;
},
start:start,
stop:stop,
draw:function() {
update();
if (state.active){
g.reset();
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.",
"icon": "ha.png",
"type": "app",
"tags": "tool",
"tags": "tool,clkinfo",
"readme": "README.md",
"supports": ["BANGLEJS2"],
"custom": "custom.html",

View File

@ -7,3 +7,5 @@
0.21: Add Settings
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.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
const SETTINGSFILE = "hworldclock.json";
var secondsMode;
@ -153,15 +155,15 @@ function updatePos() {
function drawSeconds() {
// get date
var d = new Date();
var da = d.toString().split(" ");
let d = new Date();
let da = d.toString().split(" ");
// default draw styles
g.reset().setBgColor(g.theme.bg).setFontAlign(0, 0);
// draw time
var time = da[4].split(":");
var seconds = time[2];
let time = da[4].split(":");
let seconds = time[2];
g.setFont("5x9Numeric7Seg",primaryTimeFontSize - 3);
if (g.theme.dark) {
@ -184,15 +186,15 @@ function drawSeconds() {
function draw() {
// get date
var d = new Date();
var da = d.toString().split(" ");
let d = new Date();
let da = d.toString().split(" ");
// default draw styles
g.reset().setBgColor(g.theme.bg).setFontAlign(0, 0);
// draw time
var time = da[4].split(":");
var hours = time[0],
let time = da[4].split(":");
let hours = time[0],
minutes = time[1];
@ -223,7 +225,7 @@ function draw() {
// am / PM ?
if (_12hour){
//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.drawString(ampm, xyCenterSeconds, yAmPm, true);
}
@ -232,14 +234,14 @@ function draw() {
// draw Day, name of month, 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);
g.setFont("Vector", 17);
g.drawString(require("locale").dow(new Date(), 1).toUpperCase() + ", " + localDate, xyCenter, yposDate, true);
g.setFont(font, primaryDateFontSize);
// 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
offsets.forEach((offset, index) => {
@ -249,7 +251,7 @@ function draw() {
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
const xOffset = 30;
g.setFont(font, secondaryTimeFontSize).drawString(`${hours}:${minutes}`, xyCenter, yposTime2, true);
@ -295,8 +297,18 @@ g.clear();
// Init the settings of the app
loadMySettings();
// Show launcher when button pressed
Bangle.setUI("clock");
// Show launcher when middle button pressed
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.drawWidgets();
@ -307,7 +319,7 @@ draw();
if (!Bangle.isLocked()) { // Initial state
if (showSunInfo) {
if (PosInterval != 0) clearInterval(PosInterval);
if (PosInterval != 0 && typeof PosInterval != 'undefined') clearInterval(PosInterval);
PosInterval = setInterval(updatePos, 60*10E3); // refesh every 10 mins
updatePos();
}
@ -333,7 +345,7 @@ if (!Bangle.isLocked()) { // Initial state
drawTimeout = undefined;
if (showSunInfo) {
if (PosInterval != 0) clearInterval(PosInterval);
if (PosInterval != 0 && typeof PosInterval != 'undefined') clearInterval(PosInterval);
PosInterval = setInterval(updatePos, 60*60E3); // refesh every 60 mins
updatePos();
}
@ -379,3 +391,4 @@ Bangle.on('lock',on=>{
draw(); // draw immediately, queue redraw
}
});
}

View File

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

View File

@ -12,3 +12,9 @@
used Object.assing for the settings
fix cache not deleted when "showClocks" options is changed
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"
}, s.readJSON("iconlaunch.json", true) || {});
console.log(settings);
if (!settings.fullscreen) {
Bangle.loadWidgets();
Bangle.drawWidgets();
@ -133,6 +132,7 @@
g.flip();
const itemsN = Math.ceil(launchCache.apps.length / appsN);
let onDrag = function(e) {
updateTimeout();
g.setColor(g.theme.fg);
g.setBgColor(g.theme.bg);
let dy = e.dy;
@ -182,36 +182,27 @@
drag: onDrag,
touch: (_, e) => {
if (e.y < R.y - 4) return;
updateTimeout();
let i = YtoIdx(e.y);
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() {
Bangle.setUI();
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;
let timeout;
const updateTimeout = function(){
if (settings.timeOut!="Off"){
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);
}

View File

@ -2,7 +2,7 @@
"id": "iconlaunch",
"name": "Icon Launcher",
"shortName" : "Icon launcher",
"version": "0.10",
"version": "0.14",
"icon": "app.png",
"description": "A launcher inspired by smartphones, with an icon-only scrollable menu.",
"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.11: Additional option in customizer to force drawing directly
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 lockedDrawInterval = [];
let showWidgets = false;
let firstDraw = true;
let s = {};
// unlocked draw intervals
s.udi = [];
// locked draw intervals
s.ldi = [];
// full draw
s.fd = true;
// performance log
s.pl = {};
{
let x = g.getWidth()/2;
@ -21,12 +26,10 @@ let firstDraw = true;
let precompiledJs = eval(require("Storage").read("imageclock.draw.js"));
let settings = require('Storage').readJSON("imageclock.json", true) || {};
let performanceLog = {};
let startPerfLog = () => {};
let endPerfLog = () => {};
Bangle.printPerfLog = () => {print("Deactivated");};
Bangle.resetPerfLog = () => {performanceLog = {};};
Bangle.resetPerfLog = () => {s.pl = {};};
let colormap={
"#000":0,
@ -64,35 +67,37 @@ let firstDraw = true;
if (settings.perflog){
startPerfLog = function(name){
let time = getTime();
if (!performanceLog.start) performanceLog.start={};
performanceLog.start[name] = time;
if (!s.pl.start) s.pl.start={};
s.pl.start[name] = time;
};
endPerfLog = function (name){
endPerfLog = function (name, once){
let time = getTime();
if (!performanceLog.last) performanceLog.last={};
let duration = time - performanceLog.start[name];
performanceLog.last[name] = duration;
if (!performanceLog.cum) performanceLog.cum={};
if (!performanceLog.cum[name]) performanceLog.cum[name] = 0;
performanceLog.cum[name] += duration;
if (!performanceLog.count) performanceLog.count={};
if (!performanceLog.count[name]) performanceLog.count[name] = 0;
performanceLog.count[name]++;
if (!s.pl.start[name]) return;
if (!s.pl.last) s.pl.last={};
let duration = time - s.pl.start[name];
s.pl.last[name] = duration;
if (!s.pl.cum) s.pl.cum={};
if (!s.pl.cum[name]) s.pl.cum[name] = 0;
s.pl.cum[name] += duration;
if (!s.pl.count) s.pl.count={};
if (!s.pl.count[name]) s.pl.count[name] = 0;
s.pl.count[name]++;
if (once){s.pl.start[name] = undefined}
};
Bangle.printPerfLog = function(){
let result = "";
let keys = [];
for (let c in performanceLog.cum){
for (let c in s.pl.cum){
keys.push(c);
}
keys.sort();
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");
let delayTimeouts = {};
@ -202,27 +207,39 @@ let firstDraw = true;
let firstDigitY = element.Y;
let imageIndex = element.ImageIndex ? element.ImageIndex : 0;
let firstImage;
if (imageIndex){
firstImage = getByPath(resources, [], "" + (0 + imageIndex));
} else {
firstImage = getByPath(resources, element.ImagePath, 0);
let firstImage = element.cachedFirstImage;
if (!firstImage && !element.cachedFirstImageMissing){
if (imageIndex){
firstImage = getByPath(resources, [], "" + (0 + imageIndex));
} else {
firstImage = getByPath(resources, element.ImagePath, 0);
}
element.cachedFirstImage = firstImage;
if (!firstImage) element.cachedFirstImageMissing = true;
}
let minusImage;
if (imageIndexMinus){
minusImage = getByPath(resources, [], "" + (0 + imageIndexMinus));
} else {
minusImage = getByPath(resources, element.ImagePath, "minus");
let minusImage = element.cachedMinusImage;
if (!minusImage && !element.cachedMinusImageMissing){
if (imageIndexMinus){
minusImage = getByPath(resources, [], "" + (0 + imageIndexMinus));
} 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);
if (imageIndexUnit !== undefined){
unitImage = getByPath(resources, [], "" + (0 + imageIndexUnit));
//print("Unit image is", unitImage);
} else if (element.Unit){
unitImage = getByPath(resources, element.ImagePath, getMultistate(element.Unit, "unknown"));
if (!unitImage && !element.cachedUnitImageMissing){
if (imageIndexUnit !== undefined){
unitImage = getByPath(resources, [], "" + (0 + imageIndexUnit));
//print("Unit image is", unitImage);
} 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);
@ -292,14 +309,7 @@ let firstDraw = true;
if (resource){
prepareImg(resource);
//print("lastElem", typeof resource)
if (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);
}
element.cachedImage[cacheKey] = resource;
} else {
//print("Could not get resource from", element, lastElem);
}
@ -604,18 +614,22 @@ let firstDraw = true;
promise.then(()=>{
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;
isDrawing=false;
firstDraw=false;
s.fd=false;
requestRefresh = false;
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)=>{
print("Error during drawing", e);
});
@ -699,16 +713,16 @@ let firstDraw = true;
let handleLock = function(isLocked, forceRedraw){
//print("isLocked", Bangle.isLocked());
for (let i of unlockedDrawInterval){
for (let i of s.udi){
//print("Clearing unlocked", i);
clearInterval(i);
}
for (let i of lockedDrawInterval){
for (let i of s.ldi){
//print("Clearing locked", i);
clearInterval(i);
}
unlockedDrawInterval = [];
lockedDrawInterval = [];
s.udi = [];
s.ldi = [];
if (!isLocked){
if (forceRedraw || !redrawEvents || (redrawEvents.includes("unlock"))){
@ -724,7 +738,7 @@ let firstDraw = true;
initialDraw(watchfaceResources, watchface);
},unlockedRedraw, (v)=>{
//print("New matched unlocked interval", v);
unlockedDrawInterval.push(v);
s.udi.push(v);
}, lastDrawTime);
if (!events || events.includes("HRM")) Bangle.setHRMPower(1, "imageclock");
if (!events || events.includes("pressure")) Bangle.setBarometerPower(1, 'imageclock');
@ -742,54 +756,13 @@ let firstDraw = true;
initialDraw(watchfaceResources, watchface);
},lockedRedraw, (v)=>{
//print("New matched locked interval", v);
lockedDrawInterval.push(v);
s.ldi.push(v);
}, lastDrawTime);
Bangle.setHRMPower(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")){
Bangle.on('pressure', handlePressure);
try{
@ -809,68 +782,42 @@ let firstDraw = true;
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);
Bangle.setUI({
mode : "clock",
remove : function() {
//print("remove calls");
// Called to unload all of the clock app
Bangle.setHRMPower(0, "imageclock");
Bangle.setBarometerPower(0, 'imageclock');
let setUi = function(){
Bangle.setUI({
mode : "clock",
remove : function() {
//print("remove calls");
// Called to unload all of the clock app
Bangle.setHRMPower(0, "imageclock");
Bangle.setBarometerPower(0, 'imageclock');
Bangle.removeListener('drag', handleDrag);
Bangle.removeListener('lock', handleLock);
Bangle.removeListener('charging', handleCharging);
Bangle.removeListener('HRM', handleHrm);
Bangle.removeListener('pressure', handlePressure);
Bangle.removeListener('lock', handleLock);
Bangle.removeListener('charging', handleCharging);
Bangle.removeListener('HRM', handleHrm);
Bangle.removeListener('pressure', handlePressure);
if (deferredTimout) clearTimeout(deferredTimout);
if (initialDrawTimeoutUnlocked) clearTimeout(initialDrawTimeoutUnlocked);
if (initialDrawTimeoutLocked) clearTimeout(initialDrawTimeoutLocked);
if (deferredTimout) clearTimeout(deferredTimout);
if (initialDrawTimeoutUnlocked) clearTimeout(initialDrawTimeoutUnlocked);
if (initialDrawTimeoutLocked) clearTimeout(initialDrawTimeoutLocked);
for (let i of unlockedDrawInterval){
//print("Clearing unlocked", i);
clearInterval(i);
for (let i of global.s.udi){
//print("Clearing unlocked", 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>
<body>
<script src="../../core/lib/heatshrink.js"></script>
<script src="../../core/lib/imageconverter.js"></script>
<script src="../../webtools/heatshrink.js"></script>
<script src="../../webtools/imageconverter.js"></script>
<script src="../../core/lib/customize.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>
<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>
<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"/>
<label for="debugprints">Add debug prints to generated code</label></br>
</p>
@ -656,7 +658,7 @@
var checkcode = "";
if (!(properties.Redraw && properties.Redraw.Clear)){
checkcode = 'firstDraw';
checkcode = 's.fd';
for (var i = 0; i< layerElements.length; i++){
var layerElement = layerElements[i];
var referencedElement = elements[layerElements[i].index];
@ -664,9 +666,9 @@
console.log("Check for change:", layerElement, referencedElement);
if (layerElement.element.Value){
if (elementType == "MultiState" && layerElement.element.Value) {
checkcode += '| isChangedMultistate(wf.Collapsed[' + layerElement.index + '].value)';
checkcode += '| isChangedMultistate(wf.c[' + layerElement.index + '].value)';
} else {
checkcode += '| isChangedNumber(wf.Collapsed[' + layerElement.index + '].value)';
checkcode += '| isChangedNumber(wf.c[' + layerElement.index + '].value)';
}
checkForLayerChange = true;
}
@ -693,7 +695,7 @@
if (c.value.Type == "Once"){
if (condition.length > 0) condition += " && ";
condition += "firstDraw";
condition += "s.fd";
}
var planeName = "p" + plane;
@ -714,15 +716,15 @@
}
if (addDebug()) code += 'print("Element condition is ' + condition + '");' + "\n";
code += "" + colorsetting;
code += (condition.length > 0 ? "if (" + condition + "){\n" : "");
if (wrapInTimeouts && (plane != 0 || forceUseOrigPlane)){
code += "p = p.then(()=>delay(0)).then(()=>{\n";
} else {
code += "p = p.then(()=>{\n";
}
code += "" + colorsetting;
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 += (condition.length > 0 ? "}\n" : "");
@ -744,9 +746,9 @@
console.log("Created data file", resourceDataString, resourceDataOffset, resultJson);
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);
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);
}
@ -1011,22 +1013,45 @@
});
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 = {
id : "imageclock",
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},
]
};
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){
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);
sendCustomizedApp(appDef);
});
});

View File

@ -2,7 +2,7 @@
"id": "imageclock",
"name": "Imageclock",
"shortName": "Imageclock",
"version": "0.11",
"version": "0.13",
"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.",
"icon": "app.png",

View File

@ -10,7 +10,7 @@
</div>
<script src="../../core/lib/customize.js"></script>
<script src="../../core/lib/imageconverter.js"></script>
<script src="../../webtools/imageconverter.js"></script>
<script>
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