1
0
Fork 0

Merge branch 'espruino:master' into master

master
berkenbu 2022-11-25 20:00:21 -05:00 committed by GitHub
commit 32a170d0a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
307 changed files with 6188 additions and 2354 deletions

View File

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

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

View File

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

View File

@ -1,11 +1,11 @@
{
"id": "agenda",
"name": "Agenda",
"version": "0.07",
"version": "0.08",
"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

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

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

@ -34,3 +34,9 @@
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

@ -17,5 +17,6 @@
"gracePeriodConnect": 0,
"gracePeriodService": 0,
"gracePeriodRequest": 0,
"bonding": false
"bonding": false,
"active": true
}

View File

@ -106,7 +106,7 @@ 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;
@ -148,14 +148,14 @@ 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 aggregated HRM", repEvent);
log("Emitting HRM_R(bt)", repEvent);
Bangle.emit("HRM_R", repEvent);
}
@ -255,7 +255,7 @@ exports.enable = () => {
var retry = function() {
log("Retry");
if (!currentRetryTimeout){
if (!currentRetryTimeout && !powerdownRequested){
var clampedTime = retryTime < 100 ? 100 : retryTime;
@ -287,7 +287,7 @@ exports.enable = () => {
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;
@ -369,7 +369,7 @@ exports.enable = () => {
var initBt = function () {
log("initBt with blockInit: " + blockInit);
if (blockInit){
if (blockInit && !powerdownRequested){
retry();
return;
}
@ -387,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");
@ -454,7 +460,7 @@ exports.enable = () => {
} else {
log("Start bonding");
return gatt.startBonding()
.then(() => console.log(gatt.getSecurityStatus()));
.then(() => log("Security status" + gatt.getSecurityStatus()));
}
});
}
@ -508,6 +514,8 @@ exports.enable = () => {
});
};
var powerdownRequested = false;
Bangle.setBTHRMPower = function(isOn, app) {
// Do app power handling
if (!app) app="?";
@ -518,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);
@ -544,9 +555,11 @@ exports.enable = () => {
// 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);
}
});
@ -572,6 +585,11 @@ exports.enable = () => {
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);
@ -627,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.14",
"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

@ -102,6 +102,12 @@
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

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

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

@ -16,6 +16,11 @@
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: Move interactions inside setUI. Replace "one click exit" with
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,47 +1,47 @@
{ // must be inside our own scope here so that when we are unloaded everything disappears
/* Desktop launcher
*
*/
/* Desktop launcher
*
*/
let settings = Object.assign({
showClocks: true,
showLaunchers: true,
direct: false,
swipeExit: false,
timeOut: "Off"
}, require('Storage').readJSON("dtlaunch.json", true) || {});
let settings = Object.assign({
showClocks: true,
showLaunchers: true,
direct: false,
swipeExit: false,
timeOut: "Off"
}, require('Storage').readJSON("dtlaunch.json", true) || {});
let s = require("Storage");
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));
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)=>{
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=>{
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
});
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;
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;
let drawIcon= function(p,n,selected) {
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);
@ -65,95 +65,91 @@ let drawIcon= function(p,n,selected) {
}
}
g.drawString(line.trim(),x+36,y+54+lineY*8);
};
};
let drawPage = function(p){
let drawPage = function(p){
g.reset();
g.clearRect(0,24,175,175);
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);
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 (let i=0;i<4;i++) {
if (!apps[p*4+i]) return i;
drawIcon(p,i,selected==i && !settings.direct);
if (!apps[p*4+i]) return i;
drawIcon(p,i,selected==i && !settings.direct);
}
g.flip();
};
};
Bangle.loadWidgets();
//g.clear();
//Bangle.drawWidgets();
drawPage(0);
Bangle.loadWidgets();
drawPage(0);
let swipeListenerDt = function(dirLeftRight, dirUpDown){
let swipeListenerDt = function(dirLeftRight, dirUpDown){
updateTimeoutToClock();
selected = 0;
oldselected=-1;
if(settings.swipeExit && dirLeftRight==1) returnToClock();
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);
}
};
};
let isTouched = function(p,n){
let isTouched = function(p,n){
if (n<0 || n>3) return false;
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);
};
};
let touchListenerDt = function(_,p){
let touchListenerDt = function(_,p){
updateTimeoutToClock();
let i;
for (i=0;i<4;i++){
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((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) {
drawIcon(page,selected,false);
selected=-1;
drawIcon(page,selected,false);
selected=-1;
}
};
};
const returnToClock = function() {
Bangle.setUI();
setTimeout(eval, 0, s.read(".bootcde"));
};
Bangle.setUI({
mode : 'custom',
back : Bangle.showClock,
swipe : swipeListenerDt,
touch : touchListenerDt,
remove : ()=>{if (timeoutToClock) clearTimeout(timeoutToClock);}
});
Bangle.setUI({
mode : 'custom',
back : returnToClock,
swipe : swipeListenerDt,
touch : touchListenerDt
});
// taken from Icon Launcher with minor alterations
var 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(returnToClock,time*1000);
}
};
updateTimeoutToClock();
// 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.18",
"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,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
}

65
apps/gipy/ChangeLog Normal file
View File

@ -0,0 +1,65 @@
0.01: Initial code
0.05:
* We now buzz before reaching a waypoint.
* Display is only updated when not locked.
* We detect leaving path and finding path again.
* We display remaining distance to next point.
0.06:
* Special display for points with steep turns.
* Buzz on points with steep turns and unlock.
* Losing gps is now displayed.
0.07:
* We now use orientation to detect current segment
when segments overlap going in both directions.
* File format is now versioned.
0.08:
* Don't use gps course anymore but figure it from previous positions.
* Bugfix: path colors are back.
* Always buzz when reaching waypoint even if unlocked.
0.09:
* We now display interest points.
* Menu to choose which file to load.
0.10:
* Display performances enhancement.
* Waypoints information is embedded in file and extracted from comments on
points.
* Bugfix in map display (last segment was missing + wrong colors).
* Waypoint detections using OSM + sharp angles
* New algorith for direction detection
0.11:
* Better fonts (more free space, still readable).
* Display direction to nearest point when lost.
* Display average speed.
* Turn off gps when locked and between points
0.12:
* Bugfix in speed computation.
* Bugfix in current segment detection.
* Bugfix : lost direction.
* Larger fonts.
* Detecting next point correctly when going back.
0.13:
* Bugfix in lost direction.
* Buzzing 100m ahead instead of 50m.
* Detect sharp turns.
* Display instant speed.
* New instant speed algorithm.
* Bugfix for remaining distance when going back.
0.14:
* Detect starting distance to compute a good average speed.
* Settings
* Account for breaks in average speed.
0.15:
* Record traveled distance to get a good average speed.
* Breaks (low speed) will not count in average speed.
* Bugfix in average speed.

109
apps/gipy/README.md Normal file
View File

@ -0,0 +1,109 @@
# Gipy
Gipy allows you to follow gpx traces on your watch.
![Screenshot](screenshot1.png)
It is for now meant for bicycling and not hiking
(it uses your movement to figure out your orientation
and walking is too slow).
It is untested on Banglejs1. If you can try it, you would be welcome.
This software is not perfect but surprisingly useful.
## Features
It provides the following features :
- display the path with current position from gps
- detects and buzzes if you leave the path
- buzzes before sharp turns
- buzzes before nodes with comments
(for example when you need to turn in https://mapstogpx.com/)
- display instant / average speed
- display distance to next node
- display additional data from openstreetmap :
- water points
- toilets
- artwork
- bakeries
optionally it can also:
- try to turn off gps between crossroads to save battery
## Usage
### Preparing the file
You first need to have a trace file in *gpx* format.
Usually I download from [komoot](https://www.komoot.com/) or I export
from google maps using [mapstogpx](https://mapstogpx.com/).
Note that *mapstogpx* has a super nice feature in its advanced settings.
You can turn on 'next turn info' and be warned by the watch when you need to turn.
Once you have your gpx file you need to convert it to *gpc* which is my custom file format.
They are smaller than gpx and reduce the number of computations left to be done on the watch.
Just click the disk icon and select your gpx file.
This will request additional information from openstreetmap.
Your path will be displayed in svg.
### Starting Gipy
Once you start gipy you will have a menu for selecting your trace (if more than one).
Choose the one you want and here you go :
![Screenshot](screenshot2.png)
On your screen you can see :
- yourself (the big black dot)
- the path (the top of the screen is in front of you)
- if needed a projection of yourself on the path (small black dot)
- extremities of segments as white dots
- turning points as doubled white dots
- some text on the left (from top to bottom) :
* current time
* left distance till end of current segment
* distance from start of path / path length
* average speed / instant speed
- interest points from openstreetmap as color dots :
* red : bakery
* deep blue : water point
* cyan : toilets (often doubles as water point)
* green : artwork
- a *turn* indicator on the top right when you reach a turning point
- a *gps* indicator (blinking) on the top right if you lose gps signal
- a *lost* indicator on the top right if you stray too far away from path
- a black segment extending from you when you are lost, indicating the rough direction of where to go
### Settings
Few settings for now (feel free to suggest me more) :
- keep gps alive : if turned off, will try to save battery by turning the gps off on long segments
- max speed : used to compute how long to turn the gps off
### Caveats
It is good to use but you should know :
- the gps might take a long time to start initially (see the assisted gps update app).
- gps signal is noisy : there is therefore a small delay for instant speed. sometimes you may jump somewhere else.
- your gpx trace has been decimated and approximated : the **REAL PATH** might be **A FEW METERS AWAY**
- sometimes the watch will tell you that you are lost but you are in fact on the path.
- battery saving by turning off gps is not very well tested (disabled by default).
- buzzing does not always work: when there is a high load on the watch, the buzzes might just never happen :-(.
- buzzes are not strong enough to be always easily noticed.
- be careful when **GOING DOWNHILL AT VERY HIGH SPEED**. I already missed a few turning points and by the time I realized it,
I had to go back uphill by quite a distance.
## Creator
Feel free to give me feedback : is it useful for you ? what other features would you like ?
frederic.wagner@imag.fr

25
apps/gipy/TODO Normal file
View File

@ -0,0 +1,25 @@
* bugs
- when exactly on turn, distance to next point is still often 50m
-----> it does not buzz very often on turns
- when going backwards we have a tendencing to get a wrong current_segment
* additional features
- config screen
- are we on foot (and should use compass)
- we need to buzz 200m before sharp turns (or even better, 30seconds)
(and look at more than next point)
- display distance to next water/toilet ?
- dynamic map rescale
- display scale (100m)
- compress path ?
* misc
- code is becoming messy

1
apps/gipy/app-icon.js Normal file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwkBiIA/AE8VqoAGCy1RiN3CyYuBi93uIXJIBV3AAIuMBY4XjQ5YXPRAIAEOwIABPBC4LF54wGF6IwFC5jWGIwxIJC4xJFgDuJJAxJFC6TEIJBzEHGCIYPGA5JQC44YPGBBJKY4gwRfQL4DGCL4GGCAXPGAxGBAAJIMGAwWCGCoWGC55HHJB5HIC8pGDSChfXC5AWIL5ynOC45GJC4h3IIyYwCFxwADgB1SC44uSC4guSAH4Ab"))

766
apps/gipy/app.js Normal file
View File

@ -0,0 +1,766 @@
let simulated = false;
let file_version = 3;
let code_key = 47490;
var settings = Object.assign(
{
keep_gps_alive: true,
max_speed: 35,
},
require("Storage").readJSON("gipy.json", true) || {}
);
let interests_colors = [
0xf800, // Bakery, red
0x001f, // DrinkingWater, blue
0x07ff, // Toilets, cyan
0x07e0, // Artwork, green
];
function binary_search(array, x) {
let start = 0,
end = array.length - 1;
while (start <= end) {
let mid = Math.floor((start + end) / 2);
if (array[mid] < x) start = mid + 1;
else end = mid - 1;
}
return start;
}
class Status {
constructor(path) {
this.path = path;
this.on_path = false; // are we on the path or lost ?
this.position = null; // where we are
this.adjusted_cos_direction = null; // cos of where we look at
this.adjusted_sin_direction = null; // sin of where we look at
this.current_segment = null; // which segment is closest
this.reaching = null; // which waypoint are we reaching ?
this.distance_to_next_point = null; // how far are we from next point ?
this.paused_time = 0.0; // how long did we stop (stops don't count in avg speed)
this.paused_since = getTime();
let r = [0];
// let's do a reversed prefix computations on all distances:
// loop on all segments in reversed order
let previous_point = null;
for (let i = this.path.len - 1; i >= 0; i--) {
let point = this.path.point(i);
if (previous_point !== null) {
r.unshift(r[0] + point.distance(previous_point));
}
previous_point = point;
}
this.remaining_distances = r; // how much distance remains at start of each segment
this.starting_time = this.paused_since; // time we start
this.advanced_distance = 0.0;
this.gps_coordinates_counter = 0; // how many coordinates did we receive
this.old_points = [];
this.old_times = [];
}
new_position_reached(position) {
// we try to figure out direction by looking at previous points
// instead of the gps course which is not very nice.
this.gps_coordinates_counter += 1;
let now = getTime();
this.old_points.push(position);
this.old_times.push(now);
if (this.old_points.length == 1) {
return null;
}
let last_point = this.old_points[this.old_points.length - 1];
let oldest_point = this.old_points[0];
// every 7 points we count the distance
if (this.gps_coordinates_counter % 7 == 0) {
let distance = last_point.distance(oldest_point);
if (distance < 150.0) {
// to avoid gps glitches
this.advanced_distance += distance;
}
}
if (this.old_points.length == 8) {
let p1 = this.old_points[0]
.plus(this.old_points[1])
.plus(this.old_points[2])
.plus(this.old_points[3])
.times(1 / 4);
let p2 = this.old_points[4]
.plus(this.old_points[5])
.plus(this.old_points[6])
.plus(this.old_points[7])
.times(1 / 4);
let t1 = (this.old_times[1] + this.old_times[2]) / 2;
let t2 = (this.old_times[5] + this.old_times[6]) / 2;
this.instant_speed = p1.distance(p2) / (t2 - t1);
this.old_points.shift();
this.old_times.shift();
} else {
this.instant_speed =
oldest_point.distance(last_point) / (now - this.old_times[0]);
// update paused time if we are too slow
if (this.instant_speed < 2) {
if (this.paused_since === null) {
this.paused_since = now;
}
} else {
if (this.paused_since !== null) {
this.paused_time += now - this.paused_since;
this.paused_since = null;
}
}
}
// let's just take angle of segment between newest point and a point a bit before
let previous_index = this.old_points.length - 3;
if (previous_index < 0) {
previous_index = 0;
}
let diff = position.minus(this.old_points[previous_index]);
let angle = Math.atan2(diff.lat, diff.lon);
return angle;
}
update_position(new_position, maybe_direction) {
let direction = this.new_position_reached(new_position);
if (direction === null) {
if (maybe_direction === null) {
return;
} else {
direction = maybe_direction;
}
}
this.adjusted_cos_direction = Math.cos(-direction - Math.PI / 2.0);
this.adjusted_sin_direction = Math.sin(-direction - Math.PI / 2.0);
cos_direction = Math.cos(direction);
sin_direction = Math.sin(direction);
this.position = new_position;
// detect segment we are on now
let res = this.path.nearest_segment(
this.position,
Math.max(0, this.current_segment - 1),
Math.min(this.current_segment + 2, this.path.len - 1),
cos_direction,
sin_direction
);
let orientation = res[0];
let next_segment = res[1];
if (this.is_lost(next_segment)) {
// it did not work, try anywhere
res = this.path.nearest_segment(
this.position,
0,
this.path.len - 1,
cos_direction,
sin_direction
);
orientation = res[0];
next_segment = res[1];
}
// now check if we strayed away from path or back to it
let lost = this.is_lost(next_segment);
if (this.on_path == lost) {
// if status changes
if (lost) {
Bangle.buzz(); // we lost path
setTimeout(() => Bangle.buzz(), 500);
setTimeout(() => Bangle.buzz(), 1000);
setTimeout(() => Bangle.buzz(), 1500);
}
this.on_path = !lost;
}
this.current_segment = next_segment;
// check if we are nearing the next point on our path and alert the user
let next_point = this.current_segment + (1 - orientation);
this.distance_to_next_point = Math.ceil(
this.position.distance(this.path.point(next_point))
);
// disable gps when far from next point and locked
if (Bangle.isLocked() && !settings.keep_gps_alive) {
let time_to_next_point =
(this.distance_to_next_point * 3.6) / settings.max_speed;
if (time_to_next_point > 60) {
Bangle.setGPSPower(false, "gipy");
setTimeout(function () {
Bangle.setGPSPower(true, "gipy");
}, time_to_next_point);
}
}
if (this.reaching != next_point && this.distance_to_next_point <= 100) {
this.reaching = next_point;
let reaching_waypoint = this.path.is_waypoint(next_point);
if (reaching_waypoint) {
Bangle.buzz();
setTimeout(() => Bangle.buzz(), 500);
setTimeout(() => Bangle.buzz(), 1000);
setTimeout(() => Bangle.buzz(), 1500);
if (Bangle.isLocked()) {
Bangle.setLocked(false);
}
}
}
// re-display
this.display(orientation);
}
remaining_distance(orientation) {
let remaining_in_correct_orientation =
this.remaining_distances[this.current_segment + 1] +
this.position.distance(this.path.point(this.current_segment + 1));
if (orientation == 0) {
return remaining_in_correct_orientation;
} else {
return this.remaining_distances[0] - remaining_in_correct_orientation;
}
}
is_lost(segment) {
let distance_to_nearest = this.position.distance_to_segment(
this.path.point(segment),
this.path.point(segment + 1)
);
return distance_to_nearest > 50;
}
display(orientation) {
g.clear();
this.display_map();
this.display_interest_points();
this.display_stats(orientation);
Bangle.drawWidgets();
}
display_interest_points() {
// this is the algorithm in case we have a lot of interest points
// let's draw all points for 5 segments centered on current one
let starting_group = Math.floor(Math.max(this.current_segment - 2, 0) / 3);
let ending_group = Math.floor(
Math.min(this.current_segment + 2, this.path.len - 2) / 3
);
let starting_bucket = binary_search(
this.path.interests_starts,
starting_group
);
let ending_bucket = binary_search(
this.path.interests_starts,
ending_group + 0.5
);
// we have 5 points per bucket
let end_index = Math.min(
this.path.interests_types.length - 1,
ending_bucket * 5
);
for (let i = starting_bucket * 5; i <= end_index; i++) {
let index = this.path.interests_on_path[i];
let interest_point = this.path.interest_point(index);
let color = this.path.interest_color(i);
let c = interest_point.coordinates(
this.position,
this.adjusted_cos_direction,
this.adjusted_sin_direction
);
g.setColor(color).fillCircle(c[0], c[1], 5);
}
}
display_stats(orientation) {
let remaining_distance = this.remaining_distance(orientation);
let rounded_distance = Math.round(remaining_distance / 100) / 10;
let total = Math.round(this.remaining_distances[0] / 100) / 10;
let now = new Date();
let minutes = now.getMinutes().toString();
if (minutes.length < 2) {
minutes = "0" + minutes;
}
let hours = now.getHours().toString();
g.setFont("6x8:2")
.setFontAlign(-1, -1, 0)
.setColor(g.theme.fg)
.drawString(hours + ":" + minutes, 0, 30);
g.setFont("6x8:2").drawString(
"" + this.distance_to_next_point + "m",
0,
g.getHeight() - 49
);
let point_time = this.old_times[this.old_times.length - 1];
let done_in = point_time - this.starting_time - this.paused_time;
let approximate_speed = Math.round(
(this.advanced_distance * 3.6) / done_in
);
let approximate_instant_speed = Math.round(this.instant_speed * 3.6);
g.setFont("6x8:2")
.setFontAlign(-1, -1, 0)
.drawString(
"" + approximate_speed + "km/h (in." + approximate_instant_speed + ")",
0,
g.getHeight() - 15
);
g.setFont("6x8:2").drawString(
"" + rounded_distance + "/" + total,
0,
g.getHeight() - 32
);
if (this.distance_to_next_point <= 100) {
if (this.path.is_waypoint(this.reaching)) {
g.setColor(0.0, 1.0, 0.0)
.setFont("6x15")
.drawString("turn", g.getWidth() - 50, 30);
}
}
if (!this.on_path) {
g.setColor(1.0, 0.0, 0.0)
.setFont("6x15")
.drawString("lost", g.getWidth() - 55, 35);
}
}
display_map() {
// don't display all segments, only those neighbouring current segment
// this is most likely to be the correct display
// while lowering the cost a lot
//
// note that all code is inlined here to speed things up from 400ms to 200ms
let start = Math.max(this.current_segment - 4, 0);
let end = Math.min(this.current_segment + 6, this.path.len);
let pos = this.position;
let cos = this.adjusted_cos_direction;
let sin = this.adjusted_sin_direction;
let points = this.path.points;
let cx = pos.lon;
let cy = pos.lat;
let half_width = g.getWidth() / 2;
let half_height = g.getHeight() / 2;
let previous_x = null;
let previous_y = null;
for (let i = start; i < end; i++) {
let tx = (points[2 * i] - cx) * 40000.0;
let ty = (points[2 * i + 1] - cy) * 40000.0;
let rotated_x = tx * cos - ty * sin;
let rotated_y = tx * sin + ty * cos;
let x = half_width - Math.round(rotated_x); // x is inverted
let y = half_height + Math.round(rotated_y);
if (previous_x !== null) {
if (i == this.current_segment + 1) {
g.setColor(0.0, 1.0, 0.0);
} else {
g.setColor(1.0, 0.0, 0.0);
}
g.drawLine(previous_x, previous_y, x, y);
if (this.path.is_waypoint(i - 1)) {
g.setColor(g.theme.fg);
g.fillCircle(previous_x, previous_y, 6);
g.setColor(g.theme.bg);
g.fillCircle(previous_x, previous_y, 5);
}
g.setColor(g.theme.fg);
g.fillCircle(previous_x, previous_y, 4);
g.setColor(g.theme.bg);
g.fillCircle(previous_x, previous_y, 3);
}
previous_x = x;
previous_y = y;
}
if (this.path.is_waypoint(end - 1)) {
g.setColor(g.theme.fg);
g.fillCircle(previous_x, previous_y, 6);
g.setColor(g.theme.bg);
g.fillCircle(previous_x, previous_y, 5);
}
g.setColor(g.theme.fg);
g.fillCircle(previous_x, previous_y, 4);
g.setColor(g.theme.bg);
g.fillCircle(previous_x, previous_y, 3);
// now display ourselves
g.setColor(g.theme.fgH);
g.fillCircle(half_width, half_height, 5);
// display old points for direction debug
// for (let i = 0; i < this.old_points.length; i++) {
// let tx = (this.old_points[i].lon - cx) * 40000.0;
// let ty = (this.old_points[i].lat - cy) * 40000.0;
// let rotated_x = tx * cos - ty * sin;
// let rotated_y = tx * sin + ty * cos;
// let x = half_width - Math.round(rotated_x); // x is inverted
// let y = half_height + Math.round(rotated_y);
// g.setColor((i + 1) / 4.0, 0.0, 0.0);
// g.fillCircle(x, y, 3);
// }
// display current-segment's projection for debug
let projection = pos.closest_segment_point(
this.path.point(this.current_segment),
this.path.point(this.current_segment + 1)
);
let tx = (projection.lon - cx) * 40000.0;
let ty = (projection.lat - cy) * 40000.0;
let rotated_x = tx * cos - ty * sin;
let rotated_y = tx * sin + ty * cos;
let x = half_width - Math.round(rotated_x); // x is inverted
let y = half_height + Math.round(rotated_y);
g.setColor(g.theme.fg);
g.fillCircle(x, y, 4);
// display direction to next point if lost
if (!this.on_path) {
let next_point = this.path.point(this.current_segment + 1);
let diff = next_point.minus(this.position);
let angle = Math.atan2(diff.lat, diff.lon);
let tx = Math.cos(angle) * 50.0;
let ty = Math.sin(angle) * 50.0;
let rotated_x = tx * cos - ty * sin;
let rotated_y = tx * sin + ty * cos;
let x = half_width - Math.round(rotated_x); // x is inverted
let y = half_height + Math.round(rotated_y);
g.setColor(g.theme.fgH).drawLine(half_width, half_height, x, y);
}
}
}
function load_gpc(filename) {
let buffer = require("Storage").readArrayBuffer(filename);
let offset = 0;
// header
let header = Uint16Array(buffer, offset, 5);
offset += 5 * 2;
let key = header[0];
let version = header[1];
let points_number = header[2];
if (key != code_key || version > file_version) {
E.showMessage("Invalid gpc file");
load();
}
// path points
let points = Float64Array(buffer, offset, points_number * 2);
offset += 8 * points_number * 2;
// path waypoints
let waypoints_len = Math.ceil(points_number / 8.0);
let waypoints = Uint8Array(buffer, offset, waypoints_len);
offset += waypoints_len;
// interest points
let interests_number = header[3];
let interests_coordinates = Float64Array(
buffer,
offset,
interests_number * 2
);
offset += 8 * interests_number * 2;
let interests_types = Uint8Array(buffer, offset, interests_number);
offset += interests_number;
// interests on path
let interests_on_path_number = header[4];
let interests_on_path = Uint16Array(buffer, offset, interests_on_path_number);
offset += 2 * interests_on_path_number;
let starts_length = Math.ceil(interests_on_path_number / 5.0);
let interests_starts = Uint16Array(buffer, offset, starts_length);
offset += 2 * starts_length;
return [
points,
waypoints,
interests_coordinates,
interests_types,
interests_on_path,
interests_starts,
];
}
class Path {
constructor(arrays) {
this.points = arrays[0];
this.waypoints = arrays[1];
this.interests_coordinates = arrays[2];
this.interests_types = arrays[3];
this.interests_on_path = arrays[4];
this.interests_starts = arrays[5];
}
is_waypoint(point_index) {
let i = Math.floor(point_index / 8);
let subindex = point_index % 8;
let r = this.waypoints[i] & (1 << subindex);
return r != 0;
}
// execute op on all segments.
// start is index of first wanted segment
// end is 1 after index of last wanted segment
on_segments(op, start, end) {
let previous_point = null;
for (let i = start; i < end + 1; i++) {
let point = new Point(this.points[2 * i], this.points[2 * i + 1]);
if (previous_point !== null) {
op(previous_point, point, i);
}
previous_point = point;
}
}
// return point at given index
point(index) {
let lon = this.points[2 * index];
let lat = this.points[2 * index + 1];
return new Point(lon, lat);
}
interest_point(index) {
let lon = this.interests_coordinates[2 * index];
let lat = this.interests_coordinates[2 * index + 1];
return new Point(lon, lat);
}
interest_color(index) {
return interests_colors[this.interests_types[index]];
}
// return index of segment which is nearest from point.
// we need a direction because we need there is an ambiguity
// for overlapping segments which are taken once to go and once to come back.
// (in the other direction).
nearest_segment(point, start, end, cos_direction, sin_direction) {
// we are going to compute two min distances, one for each direction.
let indices = [0, 0];
let mins = [Number.MAX_VALUE, Number.MAX_VALUE];
this.on_segments(
function (p1, p2, i) {
// we use the dot product to figure out if oriented correctly
// let distance = point.fake_distance_to_segment(p1, p2);
let projection = point.closest_segment_point(p1, p2);
let distance = point.fake_distance(projection);
// let d = projection.minus(point).times(40000.0);
// let rotated_x = d.lon * acos - d.lat * asin;
// let rotated_y = d.lon * asin + d.lat * acos;
// let x = g.getWidth() / 2 - Math.round(rotated_x); // x is inverted
// let y = g.getHeight() / 2 + Math.round(rotated_y);
//
let diff = p2.minus(p1);
let dot = cos_direction * diff.lon + sin_direction * diff.lat;
let orientation = +(dot < 0); // index 0 is good orientation
// g.setColor(0.0, 0.0 + orientation, 1.0 - orientation).fillCircle(
// x,
// y,
// 10
// );
if (distance <= mins[orientation]) {
mins[orientation] = distance;
indices[orientation] = i - 1;
}
},
start,
end
);
// by default correct orientation (0) wins
// but if other one is really closer, return other one
if (mins[1] < mins[0] / 10.0) {
return [1, indices[1]];
} else {
return [0, indices[0]];
}
}
get len() {
return this.points.length / 2;
}
}
class Point {
constructor(lon, lat) {
this.lon = lon;
this.lat = lat;
}
coordinates(current_position, cos_direction, sin_direction) {
let translated = this.minus(current_position).times(40000.0);
let rotated_x =
translated.lon * cos_direction - translated.lat * sin_direction;
let rotated_y =
translated.lon * sin_direction + translated.lat * cos_direction;
return [
g.getWidth() / 2 - Math.round(rotated_x), // x is inverted
g.getHeight() / 2 + Math.round(rotated_y),
];
}
minus(other_point) {
let xdiff = this.lon - other_point.lon;
let ydiff = this.lat - other_point.lat;
return new Point(xdiff, ydiff);
}
plus(other_point) {
return new Point(this.lon + other_point.lon, this.lat + other_point.lat);
}
length_squared(other_point) {
let d = this.minus(other_point);
return d.lon * d.lon + d.lat * d.lat;
}
times(scalar) {
return new Point(this.lon * scalar, this.lat * scalar);
}
dot(other_point) {
return this.lon * other_point.lon + this.lat * other_point.lat;
}
distance(other_point) {
//see https://www.movable-type.co.uk/scripts/latlong.html
const R = 6371e3; // metres
const phi1 = (this.lat * Math.PI) / 180;
const phi2 = (other_point.lat * Math.PI) / 180;
const deltaphi = ((other_point.lat - this.lat) * Math.PI) / 180;
const deltalambda = ((other_point.lon - this.lon) * Math.PI) / 180;
const a =
Math.sin(deltaphi / 2) * Math.sin(deltaphi / 2) +
Math.cos(phi1) *
Math.cos(phi2) *
Math.sin(deltalambda / 2) *
Math.sin(deltalambda / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c; // in meters
}
fake_distance(other_point) {
return Math.sqrt(this.length_squared(other_point));
}
closest_segment_point(v, w) {
// from : https://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment
// Return minimum distance between line segment vw and point p
let l2 = v.length_squared(w); // i.e. |w-v|^2 - avoid a sqrt
if (l2 == 0.0) {
return v; // v == w case
}
// Consider the line extending the segment, parameterized as v + t (w - v).
// We find projection of point p onto the line.
// It falls where t = [(p-v) . (w-v)] / |w-v|^2
// We clamp t from [0,1] to handle points outside the segment vw.
let t = Math.max(0, Math.min(1, this.minus(v).dot(w.minus(v)) / l2));
return v.plus(w.minus(v).times(t)); // Projection falls on the segment
}
distance_to_segment(v, w) {
let projection = this.closest_segment_point(v, w);
return this.distance(projection);
}
fake_distance_to_segment(v, w) {
let projection = this.closest_segment_point(v, w);
return this.fake_distance(projection);
}
}
Bangle.loadWidgets();
let fake_gps_point = 0.0;
function simulate_gps(status) {
if (fake_gps_point > status.path.len - 1) {
return;
}
let point_index = Math.floor(fake_gps_point);
if (point_index >= status.path.len) {
return;
}
//let p1 = status.path.point(0);
//let n = status.path.len;
//let p2 = status.path.point(n - 1);
let p1 = status.path.point(point_index);
let p2 = status.path.point(point_index + 1);
let alpha = fake_gps_point - point_index;
let pos = p1.times(1 - alpha).plus(p2.times(alpha));
let old_pos = status.position;
fake_gps_point += 0.05; // advance simulation
status.update_position(pos, null);
}
function drawMenu() {
const menu = {
"": { title: "choose trace" },
};
var files = require("Storage").list(".gpc");
for (var i = 0; i < files.length; ++i) {
menu[files[i]] = start.bind(null, files[i]);
}
menu["Exit"] = function () {
load();
};
E.showMenu(menu);
}
function start(fn) {
E.showMenu();
console.log("loading", fn);
// let path = new Path(load_gpx("test.gpx"));
let path = new Path(load_gpc(fn));
let status = new Status(path);
if (simulated) {
status.position = new Point(status.path.point(0));
setInterval(simulate_gps, 500, status);
} else {
// let's display start while waiting for gps signal
let p1 = status.path.point(0);
let p2 = status.path.point(1);
let diff = p2.minus(p1);
let direction = Math.atan2(diff.lat, diff.lon);
Bangle.setLocked(false);
status.update_position(p1, direction);
let frame = 0;
let set_coordinates = function (data) {
frame += 1;
// 0,0 coordinates are considered invalid since we sometimes receive them out of nowhere
let valid_coordinates =
!isNaN(data.lat) &&
!isNaN(data.lon) &&
(data.lat != 0.0 || data.lon != 0.0);
if (valid_coordinates) {
status.update_position(new Point(data.lon, data.lat), null);
}
let gps_status_color;
if (frame % 2 == 0 || valid_coordinates) {
gps_status_color = g.theme.bg;
} else {
gps_status_color = g.theme.fg;
}
g.setColor(gps_status_color)
.setFont("6x8:2")
.drawString("gps", g.getWidth() - 40, 30);
};
Bangle.setGPSPower(true, "gipy");
Bangle.on("GPS", set_coordinates);
Bangle.on("lock", function (on) {
if (!on) {
Bangle.setGPSPower(true, "gipy"); // activate gps when unlocking
}
});
}
}
let files = require("Storage").list(".gpc");
if (files.length <= 1) {
if (files.length == 0) {
load();
} else {
start(files[0]);
}
} else {
drawMenu();
}

BIN
apps/gipy/gipy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

196
apps/gipy/interface.html Normal file
View File

@ -0,0 +1,196 @@
<html>
<head>
<link rel="stylesheet" href="../../css/spectre.min.css">
<style>
svg { width:95% }
</style>
</head>
<body>
<p>Please select a gpx file to be converted to gpc and loaded.</p>
gpx file : <input type="file" is="gpx_file" id="fileInput" accept=".gpx">
<br>
gpc filename : <input type="text" id="gpc_file" name="gpc_file" maxlength="24">.gpc (max 24 characters)
<br>
<input type="checkbox" id="osm" name="osm" checked>
<label for="osm">fetch interests from openstreetmap</label>
<table>
<tr>
<th><bold>OpenstreetMap <a href="https://wiki.openstreetmap.org/wiki/Tags">NODE Tags</a></bold></th>
</tr>
<tr>
<th>color</th><th>key</th><th>value</th>
</tr>
<tr>
<th style="color:red">red</th><th><input type="text" id="key1" name="key1" value="shop"></th><th><input type="text" id="value1" name="value1" value="bakery"></th>
</tr>
<tr>
<th style="color:blue">blue</th><th><input type="text" id="key2" name="key2" value="amenity"></th><th><input type="text" id="value2" name="value2" value="drinking_water"></th>
</tr>
<tr>
<th style="color:cyan">cyan</th><th><input type="text" id="key3" name="key3" value="amenity"></th><th><input type="text" id="value3" name="value3" value="toilets"></th>
</tr>
<tr>
<th style="color:green">green</th><th><input type="text" id="key4" name="key4" value="tourism"></th><th><input type="text" id="value4" name="value4" value="artwork"></th>
</tr>
</table>
<p>nice tags could be :
shop/bicycle, amenity/bank, shop/supermarket, leisure/picnic_table, tourism/information, amenity/pharmacy
</p>
<input type="button" id="convert" name="convert" value="Convert" disabled>
<input type="button" id="upload" name="upload" value="Upload" disabled>
<div id="status"></div>
<div id="map"></div>
<script src="../../core/lib/interface.js"></script>
<script>
function onInit() {
}
</script>
<script type="module">
function vec_to_string(vec) {
let final_string = '';
for (let i = 0 ; i < vec.length ; i++) {
final_string += String.fromCharCode(vec[i]);
}
return final_string;
}
import init, { convert_gpx_strings, convert_gpx_strings_no_osm, get_gpc, get_svg } from "./pkg/gpconv.js";
console.log("imported wasm");
let osm_checkbox = document.querySelector("input[name=osm]");
let with_osm = true;
osm_checkbox.addEventListener('change', function() {
if (this.checked) {
with_osm = true;
document.getElementById('key1').disabled = false;
document.getElementById('key2').disabled = false;
document.getElementById('key3').disabled = false;
document.getElementById('key4').disabled = false;
document.getElementById('value1').disabled = false;
document.getElementById('value2').disabled = false;
document.getElementById('value3').disabled = false;
document.getElementById('value4').disabled = false;
} else {
with_osm = false;
document.getElementById('key1').disabled = true;
document.getElementById('key2').disabled = true;
document.getElementById('key3').disabled = true;
document.getElementById('key4').disabled = true;
document.getElementById('value1').disabled = true;
document.getElementById('value2').disabled = true;
document.getElementById('value3').disabled = true;
document.getElementById('value4').disabled = true;
}
});
let status = document.getElementById("status");
let gpx_content = null;
let gpc_filename = null;
let gpc_content = null;
document
.getElementById("fileInput")
.addEventListener("change", function selectedFileChanged() {
document.getElementById('convert').disabled = true;
document.getElementById('upload').disabled = true;
if (this.files.length === 0) {
console.log("No file selected.");
return;
}
status.innerHTML = "reading file";
let gpx_filename = this.files[0].name;
if (gpc_filename === null || gpc_filename == "") {
if (gpx_filename.length <= 28) {
gpc_filename = gpx_filename.slice(0, gpx_filename.length - 4);
document.getElementById('gpc_file').value = gpc_filename;
}
}
const reader = new FileReader();
reader.onload = function fileReadCompleted() {
console.log("reading file completed");
status.innerHTML = "file reading completed";
gpx_content = reader.result;
document.getElementById('convert').disabled = false;
};
reader.readAsText(this.files[0]);
});
document
.getElementById("convert")
.addEventListener('click', function() {
console.log("starting conversion");
document.getElementById('convert').disabled = true;
document.getElementById('upload').disabled = true;
status.innerHTML = "please wait, converting file";
init().then(() => {
let gpc_svg;
if (with_osm) {
let key1 = document.getElementById('key1').value;
let key2 = document.getElementById('key2').value;
let key3 = document.getElementById('key3').value;
let key4 = document.getElementById('key4').value;
let value1 = document.getElementById('value1').value;
let value2 = document.getElementById('value2').value;
let value3 = document.getElementById('value3').value;
let value4 = document.getElementById('value4').value;
gpc_svg = convert_gpx_strings(gpx_content, key1, value1, key2, value2, key3, value3, key4, value4);
} else {
gpc_svg = convert_gpx_strings_no_osm(gpx_content);
}
gpc_svg.then(gs => {
status.innerHTML = "file converted";
let svg = get_svg(gs);
let svg_string = vec_to_string(svg);
let img = document.getElementById("map");
img.innerHTML = svg_string;
gpc_content = get_gpc(gs);
if (gpc_filename !== null) {
document.getElementById('upload').disabled = false;
}
});
});
});
document
.getElementById("gpc_file")
.addEventListener('change', function() {
gpc_filename = document.getElementById("gpc_file").value;
if (gpc_filename == "") {
document.getElementById("upload").disabled = true;
} else {
if (gpc_content !== null) {
document.getElementById("upload").disabled = false;
}
}
});
document
.getElementById("upload")
.addEventListener('click', function() {
status.innerHTML = "uploading file";
console.log("uploading");
let gpc_string = vec_to_string(gpc_content);
Util.writeStorage(gpc_filename + ".gpc", gpc_string, () => {
status.innerHTML = `${gpc_filename}.gpc uploaded`;
console.log("DONE");
});
});
</script>
</body>
</html>

23
apps/gipy/metadata.json Normal file
View File

@ -0,0 +1,23 @@
{
"id": "gipy",
"name": "Gipy",
"shortName": "Gipy",
"version": "0.15",
"description": "Follow gpx files",
"allow_emulator":false,
"icon": "gipy.png",
"type": "app",
"tags": "tool,outdoors,gps",
"screenshots": [],
"supports": ["BANGLEJS2"],
"readme": "README.md",
"interface": "interface.html",
"storage": [
{"name":"gipy.app.js","url":"app.js"},
{"name":"gipy.settings.js","url":"settings.js"},
{"name":"gipy.img","url":"app-icon.js","evaluate":true}
],
"data": [
{"name":"gipy.json"}
]
}

75
apps/gipy/pkg/gpconv.d.ts vendored Normal file
View File

@ -0,0 +1,75 @@
/* tslint:disable */
/* eslint-disable */
/**
* @param {GpcSvg} gpcsvg
* @returns {Uint8Array}
*/
export function get_gpc(gpcsvg: GpcSvg): Uint8Array;
/**
* @param {GpcSvg} gpcsvg
* @returns {Uint8Array}
*/
export function get_svg(gpcsvg: GpcSvg): Uint8Array;
/**
* @param {string} input_str
* @returns {Promise<GpcSvg>}
*/
export function convert_gpx_strings_no_osm(input_str: string): Promise<GpcSvg>;
/**
* @param {string} input_str
* @param {string} key1
* @param {string} value1
* @param {string} key2
* @param {string} value2
* @param {string} key3
* @param {string} value3
* @param {string} key4
* @param {string} value4
* @returns {Promise<GpcSvg>}
*/
export function convert_gpx_strings(input_str: string, key1: string, value1: string, key2: string, value2: string, key3: string, value3: string, key4: string, value4: string): Promise<GpcSvg>;
/**
*/
export class GpcSvg {
free(): void;
}
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
export interface InitOutput {
readonly memory: WebAssembly.Memory;
readonly __wbg_gpcsvg_free: (a: number) => void;
readonly get_gpc: (a: number, b: number) => void;
readonly get_svg: (a: number, b: number) => void;
readonly convert_gpx_strings_no_osm: (a: number, b: number) => number;
readonly convert_gpx_strings: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number, l: number, m: number, n: number, o: number, p: number, q: number, r: number) => number;
readonly __wbindgen_malloc: (a: number) => number;
readonly __wbindgen_realloc: (a: number, b: number, c: number) => number;
readonly __wbindgen_export_2: WebAssembly.Table;
readonly _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h0601691a32604cdd: (a: number, b: number, c: number) => void;
readonly __wbindgen_add_to_stack_pointer: (a: number) => number;
readonly __wbindgen_free: (a: number, b: number) => void;
readonly __wbindgen_exn_store: (a: number) => void;
readonly wasm_bindgen__convert__closures__invoke2_mut__h25ed812378167476: (a: number, b: number, c: number, d: number) => void;
}
export type SyncInitInput = BufferSource | WebAssembly.Module;
/**
* Instantiates the given `module`, which can either be bytes or
* a precompiled `WebAssembly.Module`.
*
* @param {SyncInitInput} module
*
* @returns {InitOutput}
*/
export function initSync(module: SyncInitInput): InitOutput;
/**
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
* for everything else, calls `WebAssembly.instantiate` directly.
*
* @param {InitInput | Promise<InitInput>} module_or_path
*
* @returns {Promise<InitOutput>}
*/
export default function init (module_or_path?: InitInput | Promise<InitInput>): Promise<InitOutput>;

645
apps/gipy/pkg/gpconv.js Normal file
View File

@ -0,0 +1,645 @@
let wasm;
const heap = new Array(32).fill(undefined);
heap.push(undefined, null, true, false);
function getObject(idx) { return heap[idx]; }
let heap_next = heap.length;
function dropObject(idx) {
if (idx < 36) return;
heap[idx] = heap_next;
heap_next = idx;
}
function takeObject(idx) {
const ret = getObject(idx);
dropObject(idx);
return ret;
}
let WASM_VECTOR_LEN = 0;
let cachedUint8Memory0 = new Uint8Array();
function getUint8Memory0() {
if (cachedUint8Memory0.byteLength === 0) {
cachedUint8Memory0 = new Uint8Array(wasm.memory.buffer);
}
return cachedUint8Memory0;
}
const cachedTextEncoder = new TextEncoder('utf-8');
const encodeString = (typeof cachedTextEncoder.encodeInto === 'function'
? function (arg, view) {
return cachedTextEncoder.encodeInto(arg, view);
}
: function (arg, view) {
const buf = cachedTextEncoder.encode(arg);
view.set(buf);
return {
read: arg.length,
written: buf.length
};
});
function passStringToWasm0(arg, malloc, realloc) {
if (realloc === undefined) {
const buf = cachedTextEncoder.encode(arg);
const ptr = malloc(buf.length);
getUint8Memory0().subarray(ptr, ptr + buf.length).set(buf);
WASM_VECTOR_LEN = buf.length;
return ptr;
}
let len = arg.length;
let ptr = malloc(len);
const mem = getUint8Memory0();
let offset = 0;
for (; offset < len; offset++) {
const code = arg.charCodeAt(offset);
if (code > 0x7F) break;
mem[ptr + offset] = code;
}
if (offset !== len) {
if (offset !== 0) {
arg = arg.slice(offset);
}
ptr = realloc(ptr, len, len = offset + arg.length * 3);
const view = getUint8Memory0().subarray(ptr + offset, ptr + len);
const ret = encodeString(arg, view);
offset += ret.written;
}
WASM_VECTOR_LEN = offset;
return ptr;
}
function isLikeNone(x) {
return x === undefined || x === null;
}
let cachedInt32Memory0 = new Int32Array();
function getInt32Memory0() {
if (cachedInt32Memory0.byteLength === 0) {
cachedInt32Memory0 = new Int32Array(wasm.memory.buffer);
}
return cachedInt32Memory0;
}
const cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
cachedTextDecoder.decode();
function getStringFromWasm0(ptr, len) {
return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len));
}
function addHeapObject(obj) {
if (heap_next === heap.length) heap.push(heap.length + 1);
const idx = heap_next;
heap_next = heap[idx];
heap[idx] = obj;
return idx;
}
function debugString(val) {
// primitive types
const type = typeof val;
if (type == 'number' || type == 'boolean' || val == null) {
return `${val}`;
}
if (type == 'string') {
return `"${val}"`;
}
if (type == 'symbol') {
const description = val.description;
if (description == null) {
return 'Symbol';
} else {
return `Symbol(${description})`;
}
}
if (type == 'function') {
const name = val.name;
if (typeof name == 'string' && name.length > 0) {
return `Function(${name})`;
} else {
return 'Function';
}
}
// objects
if (Array.isArray(val)) {
const length = val.length;
let debug = '[';
if (length > 0) {
debug += debugString(val[0]);
}
for(let i = 1; i < length; i++) {
debug += ', ' + debugString(val[i]);
}
debug += ']';
return debug;
}
// Test for built-in
const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val));
let className;
if (builtInMatches.length > 1) {
className = builtInMatches[1];
} else {
// Failed to match the standard '[object ClassName]'
return toString.call(val);
}
if (className == 'Object') {
// we're a user defined class or Object
// JSON.stringify avoids problems with cycles, and is generally much
// easier than looping through ownProperties of `val`.
try {
return 'Object(' + JSON.stringify(val) + ')';
} catch (_) {
return 'Object';
}
}
// errors
if (val instanceof Error) {
return `${val.name}: ${val.message}\n${val.stack}`;
}
// TODO we could test for more things here, like `Set`s and `Map`s.
return className;
}
function makeMutClosure(arg0, arg1, dtor, f) {
const state = { a: arg0, b: arg1, cnt: 1, dtor };
const real = (...args) => {
// First up with a closure we increment the internal reference
// count. This ensures that the Rust closure environment won't
// be deallocated while we're invoking it.
state.cnt++;
const a = state.a;
state.a = 0;
try {
return f(a, state.b, ...args);
} finally {
if (--state.cnt === 0) {
wasm.__wbindgen_export_2.get(state.dtor)(a, state.b);
} else {
state.a = a;
}
}
};
real.original = state;
return real;
}
function __wbg_adapter_24(arg0, arg1, arg2) {
wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h0601691a32604cdd(arg0, arg1, addHeapObject(arg2));
}
function _assertClass(instance, klass) {
if (!(instance instanceof klass)) {
throw new Error(`expected instance of ${klass.name}`);
}
return instance.ptr;
}
function getArrayU8FromWasm0(ptr, len) {
return getUint8Memory0().subarray(ptr / 1, ptr / 1 + len);
}
/**
* @param {GpcSvg} gpcsvg
* @returns {Uint8Array}
*/
export function get_gpc(gpcsvg) {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
_assertClass(gpcsvg, GpcSvg);
wasm.get_gpc(retptr, gpcsvg.ptr);
var r0 = getInt32Memory0()[retptr / 4 + 0];
var r1 = getInt32Memory0()[retptr / 4 + 1];
var v0 = getArrayU8FromWasm0(r0, r1).slice();
wasm.__wbindgen_free(r0, r1 * 1);
return v0;
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
}
}
/**
* @param {GpcSvg} gpcsvg
* @returns {Uint8Array}
*/
export function get_svg(gpcsvg) {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
_assertClass(gpcsvg, GpcSvg);
wasm.get_svg(retptr, gpcsvg.ptr);
var r0 = getInt32Memory0()[retptr / 4 + 0];
var r1 = getInt32Memory0()[retptr / 4 + 1];
var v0 = getArrayU8FromWasm0(r0, r1).slice();
wasm.__wbindgen_free(r0, r1 * 1);
return v0;
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
}
}
/**
* @param {string} input_str
* @returns {Promise<GpcSvg>}
*/
export function convert_gpx_strings_no_osm(input_str) {
const ptr0 = passStringToWasm0(input_str, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.convert_gpx_strings_no_osm(ptr0, len0);
return takeObject(ret);
}
/**
* @param {string} input_str
* @param {string} key1
* @param {string} value1
* @param {string} key2
* @param {string} value2
* @param {string} key3
* @param {string} value3
* @param {string} key4
* @param {string} value4
* @returns {Promise<GpcSvg>}
*/
export function convert_gpx_strings(input_str, key1, value1, key2, value2, key3, value3, key4, value4) {
const ptr0 = passStringToWasm0(input_str, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ptr1 = passStringToWasm0(key1, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
const ptr2 = passStringToWasm0(value1, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len2 = WASM_VECTOR_LEN;
const ptr3 = passStringToWasm0(key2, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len3 = WASM_VECTOR_LEN;
const ptr4 = passStringToWasm0(value2, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len4 = WASM_VECTOR_LEN;
const ptr5 = passStringToWasm0(key3, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len5 = WASM_VECTOR_LEN;
const ptr6 = passStringToWasm0(value3, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len6 = WASM_VECTOR_LEN;
const ptr7 = passStringToWasm0(key4, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len7 = WASM_VECTOR_LEN;
const ptr8 = passStringToWasm0(value4, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len8 = WASM_VECTOR_LEN;
const ret = wasm.convert_gpx_strings(ptr0, len0, ptr1, len1, ptr2, len2, ptr3, len3, ptr4, len4, ptr5, len5, ptr6, len6, ptr7, len7, ptr8, len8);
return takeObject(ret);
}
function handleError(f, args) {
try {
return f.apply(this, args);
} catch (e) {
wasm.__wbindgen_exn_store(addHeapObject(e));
}
}
function __wbg_adapter_69(arg0, arg1, arg2, arg3) {
wasm.wasm_bindgen__convert__closures__invoke2_mut__h25ed812378167476(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3));
}
/**
*/
export class GpcSvg {
static __wrap(ptr) {
const obj = Object.create(GpcSvg.prototype);
obj.ptr = ptr;
return obj;
}
__destroy_into_raw() {
const ptr = this.ptr;
this.ptr = 0;
return ptr;
}
free() {
const ptr = this.__destroy_into_raw();
wasm.__wbg_gpcsvg_free(ptr);
}
}
async function load(module, imports) {
if (typeof Response === 'function' && module instanceof Response) {
if (typeof WebAssembly.instantiateStreaming === 'function') {
try {
return await WebAssembly.instantiateStreaming(module, imports);
} catch (e) {
if (module.headers.get('Content-Type') != 'application/wasm') {
console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
} else {
throw e;
}
}
}
const bytes = await module.arrayBuffer();
return await WebAssembly.instantiate(bytes, imports);
} else {
const instance = await WebAssembly.instantiate(module, imports);
if (instance instanceof WebAssembly.Instance) {
return { instance, module };
} else {
return instance;
}
}
}
function getImports() {
const imports = {};
imports.wbg = {};
imports.wbg.__wbindgen_object_drop_ref = function(arg0) {
takeObject(arg0);
};
imports.wbg.__wbg_gpcsvg_new = function(arg0) {
const ret = GpcSvg.__wrap(arg0);
return addHeapObject(ret);
};
imports.wbg.__wbindgen_string_get = function(arg0, arg1) {
const obj = getObject(arg1);
const ret = typeof(obj) === 'string' ? obj : undefined;
var ptr0 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
var len0 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len0;
getInt32Memory0()[arg0 / 4 + 0] = ptr0;
};
imports.wbg.__wbindgen_string_new = function(arg0, arg1) {
const ret = getStringFromWasm0(arg0, arg1);
return addHeapObject(ret);
};
imports.wbg.__wbindgen_object_clone_ref = function(arg0) {
const ret = getObject(arg0);
return addHeapObject(ret);
};
imports.wbg.__wbg_fetch_386f87a3ebf5003c = function(arg0) {
const ret = fetch(getObject(arg0));
return addHeapObject(ret);
};
imports.wbg.__wbindgen_cb_drop = function(arg0) {
const obj = takeObject(arg0).original;
if (obj.cnt-- == 1) {
obj.a = 0;
return true;
}
const ret = false;
return ret;
};
imports.wbg.__wbg_fetch_749a56934f95c96c = function(arg0, arg1) {
const ret = getObject(arg0).fetch(getObject(arg1));
return addHeapObject(ret);
};
imports.wbg.__wbg_instanceof_Response_eaa426220848a39e = function(arg0) {
let result;
try {
result = getObject(arg0) instanceof Response;
} catch {
result = false;
}
const ret = result;
return ret;
};
imports.wbg.__wbg_url_74285ddf2747cb3d = function(arg0, arg1) {
const ret = getObject(arg1).url;
const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len0;
getInt32Memory0()[arg0 / 4 + 0] = ptr0;
};
imports.wbg.__wbg_status_c4ef3dd591e63435 = function(arg0) {
const ret = getObject(arg0).status;
return ret;
};
imports.wbg.__wbg_headers_fd64ad685cf22e5d = function(arg0) {
const ret = getObject(arg0).headers;
return addHeapObject(ret);
};
imports.wbg.__wbg_text_1169d752cc697903 = function() { return handleError(function (arg0) {
const ret = getObject(arg0).text();
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_newwithstrandinit_05d7180788420c40 = function() { return handleError(function (arg0, arg1, arg2) {
const ret = new Request(getStringFromWasm0(arg0, arg1), getObject(arg2));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_new_2d0053ee81e4dd2a = function() { return handleError(function () {
const ret = new Headers();
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_append_de37df908812970d = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
getObject(arg0).append(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4));
}, arguments) };
imports.wbg.__wbindgen_is_object = function(arg0) {
const val = getObject(arg0);
const ret = typeof(val) === 'object' && val !== null;
return ret;
};
imports.wbg.__wbg_newnoargs_b5b063fc6c2f0376 = function(arg0, arg1) {
const ret = new Function(getStringFromWasm0(arg0, arg1));
return addHeapObject(ret);
};
imports.wbg.__wbg_next_579e583d33566a86 = function(arg0) {
const ret = getObject(arg0).next;
return addHeapObject(ret);
};
imports.wbg.__wbindgen_is_function = function(arg0) {
const ret = typeof(getObject(arg0)) === 'function';
return ret;
};
imports.wbg.__wbg_value_1ccc36bc03462d71 = function(arg0) {
const ret = getObject(arg0).value;
return addHeapObject(ret);
};
imports.wbg.__wbg_iterator_6f9d4f28845f426c = function() {
const ret = Symbol.iterator;
return addHeapObject(ret);
};
imports.wbg.__wbg_new_0b9bfdd97583284e = function() {
const ret = new Object();
return addHeapObject(ret);
};
imports.wbg.__wbg_self_6d479506f72c6a71 = function() { return handleError(function () {
const ret = self.self;
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_window_f2557cc78490aceb = function() { return handleError(function () {
const ret = window.window;
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_globalThis_7f206bda628d5286 = function() { return handleError(function () {
const ret = globalThis.globalThis;
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_global_ba75c50d1cf384f4 = function() { return handleError(function () {
const ret = global.global;
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbindgen_is_undefined = function(arg0) {
const ret = getObject(arg0) === undefined;
return ret;
};
imports.wbg.__wbg_call_97ae9d8645dc388b = function() { return handleError(function (arg0, arg1) {
const ret = getObject(arg0).call(getObject(arg1));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_call_168da88779e35f61 = function() { return handleError(function (arg0, arg1, arg2) {
const ret = getObject(arg0).call(getObject(arg1), getObject(arg2));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_next_aaef7c8aa5e212ac = function() { return handleError(function (arg0) {
const ret = getObject(arg0).next();
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_done_1b73b0672e15f234 = function(arg0) {
const ret = getObject(arg0).done;
return ret;
};
imports.wbg.__wbg_new_9962f939219f1820 = function(arg0, arg1) {
try {
var state0 = {a: arg0, b: arg1};
var cb0 = (arg0, arg1) => {
const a = state0.a;
state0.a = 0;
try {
return __wbg_adapter_69(a, state0.b, arg0, arg1);
} finally {
state0.a = a;
}
};
const ret = new Promise(cb0);
return addHeapObject(ret);
} finally {
state0.a = state0.b = 0;
}
};
imports.wbg.__wbg_resolve_99fe17964f31ffc0 = function(arg0) {
const ret = Promise.resolve(getObject(arg0));
return addHeapObject(ret);
};
imports.wbg.__wbg_then_11f7a54d67b4bfad = function(arg0, arg1) {
const ret = getObject(arg0).then(getObject(arg1));
return addHeapObject(ret);
};
imports.wbg.__wbg_then_cedad20fbbd9418a = function(arg0, arg1, arg2) {
const ret = getObject(arg0).then(getObject(arg1), getObject(arg2));
return addHeapObject(ret);
};
imports.wbg.__wbg_buffer_3f3d764d4747d564 = function(arg0) {
const ret = getObject(arg0).buffer;
return addHeapObject(ret);
};
imports.wbg.__wbg_newwithbyteoffsetandlength_d9aa266703cb98be = function(arg0, arg1, arg2) {
const ret = new Uint8Array(getObject(arg0), arg1 >>> 0, arg2 >>> 0);
return addHeapObject(ret);
};
imports.wbg.__wbg_new_8c3f0052272a457a = function(arg0) {
const ret = new Uint8Array(getObject(arg0));
return addHeapObject(ret);
};
imports.wbg.__wbg_stringify_d6471d300ded9b68 = function() { return handleError(function (arg0) {
const ret = JSON.stringify(getObject(arg0));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_get_765201544a2b6869 = function() { return handleError(function (arg0, arg1) {
const ret = Reflect.get(getObject(arg0), getObject(arg1));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_has_8359f114ce042f5a = function() { return handleError(function (arg0, arg1) {
const ret = Reflect.has(getObject(arg0), getObject(arg1));
return ret;
}, arguments) };
imports.wbg.__wbg_set_bf3f89b92d5a34bf = function() { return handleError(function (arg0, arg1, arg2) {
const ret = Reflect.set(getObject(arg0), getObject(arg1), getObject(arg2));
return ret;
}, arguments) };
imports.wbg.__wbindgen_debug_string = function(arg0, arg1) {
const ret = debugString(getObject(arg1));
const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len0;
getInt32Memory0()[arg0 / 4 + 0] = ptr0;
};
imports.wbg.__wbindgen_throw = function(arg0, arg1) {
throw new Error(getStringFromWasm0(arg0, arg1));
};
imports.wbg.__wbindgen_memory = function() {
const ret = wasm.memory;
return addHeapObject(ret);
};
imports.wbg.__wbindgen_closure_wrapper947 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 147, __wbg_adapter_24);
return addHeapObject(ret);
};
return imports;
}
function initMemory(imports, maybe_memory) {
}
function finalizeInit(instance, module) {
wasm = instance.exports;
init.__wbindgen_wasm_module = module;
cachedInt32Memory0 = new Int32Array();
cachedUint8Memory0 = new Uint8Array();
return wasm;
}
function initSync(module) {
const imports = getImports();
initMemory(imports);
if (!(module instanceof WebAssembly.Module)) {
module = new WebAssembly.Module(module);
}
const instance = new WebAssembly.Instance(module, imports);
return finalizeInit(instance, module);
}
async function init(input) {
if (typeof input === 'undefined') {
input = new URL('gpconv_bg.wasm', import.meta.url);
}
const imports = getImports();
if (typeof input === 'string' || (typeof Request === 'function' && input instanceof Request) || (typeof URL === 'function' && input instanceof URL)) {
input = fetch(input);
}
initMemory(imports);
const { instance, module } = await load(await input, imports);
return finalizeInit(instance, module);
}
export { initSync }
export default init;

Binary file not shown.

16
apps/gipy/pkg/gpconv_bg.wasm.d.ts vendored Normal file
View File

@ -0,0 +1,16 @@
/* tslint:disable */
/* eslint-disable */
export const memory: WebAssembly.Memory;
export function __wbg_gpcsvg_free(a: number): void;
export function get_gpc(a: number, b: number): void;
export function get_svg(a: number, b: number): void;
export function convert_gpx_strings_no_osm(a: number, b: number): number;
export function convert_gpx_strings(a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number, l: number, m: number, n: number, o: number, p: number, q: number, r: number): number;
export function __wbindgen_malloc(a: number): number;
export function __wbindgen_realloc(a: number, b: number, c: number): number;
export const __wbindgen_export_2: WebAssembly.Table;
export function _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h0601691a32604cdd(a: number, b: number, c: number): void;
export function __wbindgen_add_to_stack_pointer(a: number): number;
export function __wbindgen_free(a: number, b: number): void;
export function __wbindgen_exn_store(a: number): void;
export function wasm_bindgen__convert__closures__invoke2_mut__h25ed812378167476(a: number, b: number, c: number, d: number): void;

View File

@ -0,0 +1,12 @@
{
"name": "gpconv",
"version": "0.1.0",
"files": [
"gpconv_bg.wasm",
"gpconv.js",
"gpconv.d.ts"
],
"module": "gpconv.js",
"types": "gpconv.d.ts",
"sideEffects": false
}

BIN
apps/gipy/screenshot1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
apps/gipy/screenshot2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

38
apps/gipy/settings.js Normal file
View File

@ -0,0 +1,38 @@
(function (back) {
var FILE = "gipy.json";
// Load settings
var settings = Object.assign(
{
keep_gps_alive: false,
max_speed: 35,
},
require("Storage").readJSON(FILE, true) || {}
);
function writeSettings() {
require("Storage").writeJSON(FILE, settings);
}
// Show the menu
E.showMenu({
"": { title: "Gipy" },
"< Back": () => back(),
"keep gps alive": {
value: !!settings.keep_gps_alive, // !! converts undefined to false
format: (v) => (v ? "Yes" : "No"),
onchange: (v) => {
settings.keep_gps_alive = v;
writeSettings();
},
},
"max speed": {
value: 35 | settings.max_speed, // 0| converts undefined to 0
min: 0,
max: 130,
onchange: (v) => {
settings.max_speed = v;
writeSettings();
},
},
});
});

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

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

@ -186,38 +186,21 @@
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;
if (timeout) clearTimeout(timeout);
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
if (timeout) clearTimeout(timeout);
timeout = setTimeout(returnToClock,time*1000);
timeout = setTimeout(Bangle.showClock,time*1000);
}
}
};
updateTimeout();

View File

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

View File

@ -16,3 +16,5 @@
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 = {};
@ -609,15 +614,22 @@ let firstDraw = true;
promise.then(()=>{
let currentDrawingTime = Date.now();
if (showWidgets){
restoreWidgetDraw();
}
lastDrawTime = Date.now() - start;
isDrawing=false;
firstDraw=false;
s.fd=false;
requestRefresh = false;
endPerfLog("initialDraw");
if (!Bangle.uiRemove) setUi();
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);
});
@ -701,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"))){
@ -726,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');
@ -744,43 +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 restoreWidgetDraw = function(){
require("widget_utils").show();
Bangle.drawWidgets();
};
let handleSwipe = function(lr, ud){
if (!showWidgets && ud == 1){
//print("Enable widgets");
restoreWidgetDraw();
showWidgetsChanged = true;
}
if (showWidgets && ud == -1){
//print("Disable widgets");
clearWidgetsDraw();
firstDraw = true;
showWidgetsChanged = true;
}
if (showWidgetsChanged){
showWidgetsChanged = false;
//print("Draw after widget change");
showWidgets = ud == 1;
initialDraw();
}
};
Bangle.on('swipe', handleSwipe);
if (!events || events.includes("pressure")){
Bangle.on('pressure', handlePressure);
try{
@ -800,14 +782,6 @@ let firstDraw = true;
Bangle.on('charging', handleCharging);
}
let originalWidgetDraw = {};
let originalWidgetArea = {};
let clearWidgetsDraw = function(){
//print("Clear widget draw calls");
require("widget_utils").hide();
}
handleLock(Bangle.isLocked(), true);
let setUi = function(){
@ -819,7 +793,6 @@ let firstDraw = true;
Bangle.setHRMPower(0, "imageclock");
Bangle.setBarometerPower(0, 'imageclock');
Bangle.removeListener('swipe', handleSwipe);
Bangle.removeListener('lock', handleLock);
Bangle.removeListener('charging', handleCharging);
Bangle.removeListener('HRM', handleHrm);
@ -829,31 +802,22 @@ let firstDraw = true;
if (initialDrawTimeoutUnlocked) clearTimeout(initialDrawTimeoutUnlocked);
if (initialDrawTimeoutLocked) clearTimeout(initialDrawTimeoutLocked);
for (let i of global.unlockedDrawInterval){
for (let i of global.s.udi){
//print("Clearing unlocked", i);
clearInterval(i);
}
delete global.unlockedDrawInterval;
for (let i of global.lockedDrawInterval){
for (let i of global.s.ldi){
//print("Clearing locked", i);
clearInterval(i);
}
delete global.lockedDrawInterval;
delete global.showWidgets;
delete global.firstDraw;
delete Bangle.printPerfLog;
if (settings.perflog){
delete Bangle.resetPerfLog;
delete performanceLog;
}
cleanupDelays();
restoreWidgetDraw();
require("widget_utils").show();
}
});
}
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;
@ -722,7 +724,7 @@
}
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.12",
"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 = [];

View File

@ -8,3 +8,4 @@
0.08: Added more app identifiers, added 'cannot display' in case a message goes empty because of replacements
0.09: Enable 'ams' on new firmwares (ams/ancs can now be enabled individually) (fix #1365)
0.10: Added more bundleIds
0.11: Added letters with caron to unicodeRemap, to properly display messages in Czech language

View File

@ -127,18 +127,34 @@ E.on('notify',msg=>{
'261':"a",
'262':"C",
'263':"c",
'268':"C",
'269':"c",
'270':"D",
'271':"d",
'280':"E",
'281':"e",
'282':"E",
'283':"e",
'321':"L",
'322':"l",
'323':"N",
'324':"n",
'327':"N",
'328':"n",
'344':"R",
'345':"r",
'346':"S",
'347':"s",
'352':"S",
'353':"s",
'356':"T",
'357':"t",
'377':"Z",
'378':"z",
'379':"Z",
'380':"z",
'381':"Z",
'382':"z",
};
var replacer = ""; //(n)=>print('Unknown unicode '+n.toString(16));
//if (appNames[msg.appId]) msg.a

View File

@ -1,7 +1,7 @@
{
"id": "ios",
"name": "iOS Integration",
"version": "0.10",
"version": "0.11",
"description": "Display notifications/music/etc from iOS devices",
"icon": "app.png",
"tags": "tool,system,ios,apple,messages,notifications",

View File

@ -3,3 +3,4 @@
0.03: Incorporated improvements from Peer David for accuracy, fix dark mode, widgets run in background
0.04: Changed clock to use 12/24 hour format based on locale
0.05: Tell clock widgets to hide.
0.06: Widgets can now be made visible by swiping down (#2196)

View File

@ -41,9 +41,6 @@ function draw() {
yy = ("0"+((new Date()).getFullYear())).substr(-2);
g.setFontCustom(font, 48, 8, 521);
g.drawString(dd + ':' + mo + ':' + yy, 88, 120, true);
// Hide widgets
for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";}
}
@ -61,4 +58,5 @@ Bangle.setUI("clock");
// Load widgets but hide them
Bangle.loadWidgets();
require("widget_utils").swipeOn(); // hide widgets, make them visible with a swipe
draw();

View File

@ -2,7 +2,7 @@
"name": "MacWatch2",
"shortName":"MacWatch2",
"icon": "app.png",
"version":"0.05",
"version":"0.06",
"description": "Classic Mac Finder clock",
"type": "clock",
"tags": "clock",

View File

@ -0,0 +1 @@
0.01: Moved message icons from messages into standalone library

BIN
apps/messageicons/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 B

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