1
0
Fork 0
David Peer 2022-01-19 10:13:42 +01:00
commit bc73b1dbb9
61 changed files with 1131 additions and 786 deletions

View File

@ -172,12 +172,13 @@ The widget example is available in [`apps/_example_widget`](apps/_example_widget
Widgets are just small bits of code that run whenever an app that supports them
calls `Bangle.loadWidgets()`. If they want to display something in the 24px high
widget bars at the top and bottom of the screen they can add themselves to
the global `WIDGETS` array with:
widget bar at the top of the screen they can add themselves to the global
`WIDGETS` array with:
```
WIDGETS["mywidget"]={
area:"tl", // tl (top left), tr (top right), bl (bottom left), br (bottom right)
area:"tl", // tl (top left), tr (top right)
sortorder:0, // (Optional) determines order of widgets in the same corner
width: 24, // how wide is the widget? You can change this and call Bangle.drawWidgets() to re-layout
draw:draw // called to draw the widget
};
@ -461,16 +462,13 @@ The screen is parted in a widget and app area for lcd mode `direct`(default).
| areas | as rectangle or point |
| :-:| :-: |
| Widget | (0,0,239,23) |
| Widget bottom bar (optional) | (0,216,239,239) |
| Apps | (0,24,239,239) (see below) |
| Apps | (0,24,239,239) |
| BTN1 | (230, 55) |
| BTN2 | (230, 140) |
| BTN3 | (230, 210) |
| BTN4 | (0,0,119, 239)|
| BTN5 | (120,0,239,239) |
- If there are widgets at the bottom of the screen, apps should actually keep the bottom 24px free, so should keep to the area (0,24,239,215)
- Use `g.setFontAlign(0, 0, 3)` to draw rotated string to BTN1-BTN3 with `g.drawString()`.
- For BTN4-5 the touch area is named

111
apps.json
View File

@ -137,9 +137,9 @@
"id": "ios",
"name": "iOS Integration",
"version": "0.08",
"description": "Display notifications/music/etc from iOS devices",
"description": "Display notifications/music/etc from iOS devices via ANCS",
"icon": "app.png",
"tags": "tool,system,ios,apple,messages,notifications",
"tags": "tool,system,ios,apple,messages,notifications,ancs",
"dependencies": {"messages":"app"},
"supports": ["BANGLEJS","BANGLEJS2"],
"storage": [
@ -170,16 +170,15 @@
"id": "launch",
"name": "Launcher",
"shortName": "Launcher",
"version": "0.10",
"version": "0.11",
"description": "This is needed to display a menu allowing you to choose your own applications. You can replace this with a customised launcher.",
"icon": "app.png",
"type": "launch",
"tags": "tool,system,launcher",
"supports": ["BANGLEJS","BANGLEJS2"],
"storage": [
{"name":"launch.app.js","url":"app-bangle1.js","supports":["BANGLEJS"]},
{"name":"launch.app.js","url":"app-bangle2.js","supports":["BANGLEJS2"]},
{"name":"launch.settings.js","url":"settings.js","supports":["BANGLEJS2"]}
{"name":"launch.app.js","url":"app.js"},
{"name":"launch.settings.js","url":"settings.js"}
],
"data": [{"name":"launch.json"}],
"sortorder": -10
@ -839,9 +838,10 @@
{
"id": "slevel",
"name": "Spirit Level",
"version": "0.02",
"version": "0.04",
"description": "Show the current angle of the watch, so you can use it to make sure something is absolutely flat",
"icon": "spiritlevel.png",
"screenshots": [{"url":"screenshot.png"}],
"tags": "tool",
"supports": ["BANGLEJS","BANGLEJS2"],
"storage": [
@ -942,13 +942,13 @@
{
"id": "widlock",
"name": "Lock Widget",
"version": "0.04",
"version": "0.05",
"description": "On devices with always-on display (Bangle.js 2) this displays lock icon whenever the display is locked",
"icon": "widget.png",
"type": "widget",
"tags": "widget,lock",
"supports": ["BANGLEJS","BANGLEJS2"],
"sortorder": -1,
"sortorder": -10,
"storage": [
{"name":"widlock.wid.js","url":"widget.js"}
]
@ -1359,12 +1359,12 @@
{
"id": "pparrot",
"name": "Party Parrot",
"version": "0.01",
"version": "0.02",
"description": "Party with a parrot on your wrist",
"icon": "party-parrot.png",
"type": "app",
"tags": "party,parrot,lol",
"supports": ["BANGLEJS"],
"supports": ["BANGLEJS", "BANGLEJS2"],
"allow_emulator": true,
"screenshots": [{"url":"bangle1-party-parrot-screenshot.png"}],
"storage": [
@ -1541,7 +1541,7 @@
{
"id": "gpsinfo",
"name": "GPS Info",
"version": "0.09",
"version": "0.10",
"description": "An application that displays information about altitude, lat/lon, satellites and time",
"icon": "gps-info.png",
"type": "app",
@ -2298,7 +2298,7 @@
},
{ "id": "run",
"name": "Run",
"version":"0.01",
"version":"0.02",
"description": "Displays distance, time, steps, cadence, pace and more for runners.",
"icon": "app.png",
"tags": "run,running,fitness,outdoors,gps",
@ -2307,8 +2307,10 @@
"readme": "README.md",
"storage": [
{"name":"run.app.js","url":"app.js"},
{"name":"run.img","url":"app-icon.js","evaluate":true}
]
{"name":"run.img","url":"app-icon.js","evaluate":true},
{"name":"run.settings.js","url":"settings.js"}
],
"data": [{"name":"run.json"}]
},
{
"id": "banglerun",
@ -2977,22 +2979,6 @@
{"name":"multiclock.img","url":"multiclock-icon.js","evaluate":true}
]
},
{
"id": "widancs",
"name": "Apple Notification Widget",
"shortName": "ANCS Widget",
"version": "0.07",
"description": "Displays call, message etc notifications from a paired iPhone. Read README before installation as it only works with compatible apps",
"icon": "widget.png",
"type": "widget",
"tags": "widget",
"supports": ["BANGLEJS"],
"readme": "README.md",
"storage": [
{"name":"widancs.wid.js","url":"ancs.min.js"},
{"name":"widancs.settings.js","url":"settings.js"}
]
},
{
"id": "accelrec",
"name": "Acceleration Recorder",
@ -4219,7 +4205,7 @@
{
"id": "vectorclock",
"name": "Vector Clock",
"version": "0.03",
"version": "0.04",
"description": "A digital clock that uses the built-in vector font.",
"icon": "app.png",
"type": "clock",
@ -4232,8 +4218,10 @@
],
"storage": [
{"name":"vectorclock.app.js","url":"app.js"},
{"name":"vectorclock.img","url":"app-icon.js","evaluate":true}
]
{"name":"vectorclock.img","url":"app-icon.js","evaluate":true},
{"name":"vectorclock.settings.js","url":"settings.js"}
],
"data": [{"name":"vectorclock.json"}]
},
{
"id": "fd6fdetect",
@ -4847,12 +4835,13 @@
"shortName":"Battery Theme",
"icon": "widbata.png",
"screenshots": [{"url":"screenshot_widbata_1.png"}],
"version":"0.01",
"version":"0.02",
"type": "widget",
"supports": ["BANGLEJS", "BANGLEJS2"],
"readme": "README.md",
"description": "Shows the current battery level status in the top right using the clocks colour theme",
"tags": "widget,battery",
"sortorder": -10,
"storage": [
{"name":"widbata.wid.js","url":"widbata.wid.js"}
]
@ -4923,7 +4912,7 @@
{ "id": "tinydraw",
"name": "TinyDraw",
"shortName":"TinyDraw",
"version":"0.01",
"version":"0.02",
"type": "app",
"description": "Draw stuff in your wrist",
"icon": "app.png",
@ -5151,11 +5140,10 @@
{ "id": "circlesclock",
"name": "Circles clock",
"shortName":"Circles clock",
"version":"0.05",
"version":"0.06",
"description": "A clock with circles for different data at the bottom in a probably familiar style",
"icon": "app.png",
"screenshots": [{"url":"screenshot-dark.png"}, {"url":"screenshot-light.png"}],
"dependencies": {"widpedom":"app"},
"type": "clock",
"tags": "clock",
"supports" : ["BANGLEJS2"],
@ -5514,7 +5502,7 @@
{
"id": "touchmenu",
"name": "TouchMenu",
"version": "0.01",
"version": "0.02",
"description": "Redesigned menu that uses the full touchscreen on the Bangle.js 2",
"screenshots": [{"url":"touchmenu.gif"}],
"icon": "touchmenu.png",
@ -5619,12 +5607,13 @@
"shortName":"Simple Pedometer",
"icon": "screenshot_widpa.png",
"screenshots": [{"url":"screenshot_widpa.png"}],
"version":"0.02",
"version":"0.03",
"type": "widget",
"supports": ["BANGLEJS", "BANGLEJS2"],
"readme": "README.md",
"description": "Displays the current step count from `Bangle.getHealthStatus(\"day\").steps` in 12x16 font, requires firmware v2.11.21 or later",
"tags": "widget,battery",
"sortorder": -1,
"storage": [
{"name":"widpa.wid.js","url":"widpa.wid.js"}
]
@ -5635,12 +5624,13 @@
"shortName":"Lato Pedometer",
"icon": "screenshot_widpb.png",
"screenshots": [{"url":"screenshot_widpb.png"}],
"version":"0.02",
"version":"0.04",
"type": "widget",
"supports": ["BANGLEJS", "BANGLEJS2"],
"readme": "README.md",
"description": "Displays the current step count from `Bangle.getHealthStatus(\"day\").steps` in the Lato font, requires firmware v2.11.21 or later",
"tags": "widget,battery",
"sortorder": -1,
"storage": [
{"name":"widpb.wid.js","url":"widpb.wid.js"}
]
@ -5665,8 +5655,8 @@
{ "id": "acmaze",
"name": "AccelaMaze",
"shortName":"AccelaMaze",
"version":"0.01",
"description": "Tilt the watch to roll a ball through a maze",
"version":"0.02",
"description": "Tilt the watch to roll a ball through a maze.",
"icon": "app.png",
"tags": "game",
"supports" : ["BANGLEJS2"],
@ -5676,5 +5666,40 @@
{"name":"acmaze.app.js","url":"app.js"},
{"name":"acmaze.img","url":"app-icon.js","evaluate":true}
]
},
{
"id": "crowclk",
"name": "Crow Clock",
"version": "0.01",
"description": "A simple clock based on Bold Clock that has MST3K's Crow T. Robot for a face",
"icon": "crow_clock.png",
"screenshots": [{"url":"screenshot_crow.png"}],
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS2"],
"readme": "README.md",
"allow_emulator": true,
"storage": [
{"name":"crowclk.app.js","url":"crow_clock.js"},
{"name":"crowclk.img","url":"crow_clock-icon.js","evaluate":true}
]
},
{
"id": "wid_edit",
"version": "0.01",
"name": "Widget Editor",
"icon": "icon.png",
"description": "Customize widget locations",
"supports": ["BANGLEJS", "BANGLEJS2"],
"readme": "README.md",
"type": "bootloader",
"tags": "widget,tool",
"storage": [
{"name":"wid_edit.boot.js","url":"boot.js"},
{"name":"wid_edit.settings.js","url":"settings.js"}
],
"data": [
{"name":"wid_edit.json"}
]
}
]

View File

@ -101,8 +101,8 @@
<script>
$(function () {
let ClockSize, ClockSizeURL
let ClockFace, ClockFaceNumerals, ClockFaceDots, ClockFaceURL
let ClockHands, SecondHand, ClockHandsURL, FillColor
let ClockFace, ClockFaceURL, ClockFaceNumerals, ClockFaceDots
let ClockHands, ClockHandsURL, SecondHand, FillColor
let ComplicationTL, ComplicationTLURL
let ComplicationT, ComplicationTURL
let ComplicationTR, ComplicationTRURL
@ -118,8 +118,8 @@
function backupConfiguration () {
let Configuration = {
ClockSize, ClockSizeURL,
ClockFace, ClockFaceNumerals, ClockFaceDots, ClockFaceURL,
ClockHands, SecondHand, ClockHandsURL, FillColor,
ClockFace, ClockFaceURL, ClockFaceNumerals, ClockFaceDots,
ClockHands, ClockHandsURL, SecondHand, FillColor,
ComplicationTL, ComplicationTLURL,
ComplicationT, ComplicationTURL,
ComplicationTR, ComplicationTRURL,
@ -130,7 +130,7 @@
ComplicationBR, ComplicationBRURL,
Foreground, Background
}
try {
localStorage.setItem('ac_ac',JSON.stringify(Configuration))
} catch (Signal) {
@ -311,11 +311,11 @@
function chosenClockFace () {
switch (ClockFace) {
case 'none': return "undefined"
case 'four-fold': return "require('https://raw.githubusercontent.com/rozek/banglejs-2-four-fold-clock-face/main/ClockFace.js')"
case 'twelve-fold': return "require('https://raw.githubusercontent.com/rozek/banglejs-2-twelve-fold-clock-face/main/ClockFace.js')"
case 'rainbow': return "require('https://raw.githubusercontent.com/rozek/banglejs-2-rainbow-clock-face/main/ClockFace.js')"
case 'custom': return "require('" + ClockFaceURL + "')"
case 'none': return "undefined"
case 'four-numbered': return "require('https://raw.githubusercontent.com/rozek/banglejs-2-four-numbered-clock-face/main/ClockFace.js')"
case 'twelve-numbered': return "require('https://raw.githubusercontent.com/rozek/banglejs-2-twelve-numbered-clock-face/main/ClockFace.js')"
case 'rainbow': return "require('https://raw.githubusercontent.com/rozek/banglejs-2-rainbow-clock-face/main/ClockFace.js')"
case 'custom': return "require('" + ClockFaceURL + "')"
}
}
@ -412,7 +412,7 @@ console.log(AppSource)
}
$('input[type="radio"]').on('change',retrieveAndValidateInputs)
$('input[type="url"]'). on('change',retrieveAndValidateInputs)
$('input[type="url"]'). on('input', retrieveAndValidateInputs)
$('select'). on('change',retrieveAndValidateInputs)
$('#UploadButton').on('click',createAndUploadApp)
})
@ -485,23 +485,23 @@ console.log(AppSource)
<input type="radio" name="clock-face" value="none" checked>
<img src="none.png"/>
</label><br>
none
(none)
</td>
<td>
<label class="Preview">
<input type="radio" name="clock-face" value="four-fold">
<img src="fourfoldClockFace.png"/>
<input type="radio" name="clock-face" value="four-numbered">
<img src="fournumberedClockFace.png"/>
</label><br>
four-fold
four-numbered
</td>
<td>
<label class="Preview">
<input type="radio" name="clock-face" value="twelve-fold">
<img src="twelvefoldClockFace.png"/>
<input type="radio" name="clock-face" value="twelve-numbered">
<img src="twelvenumberedClockFace.png"/>
</label><br>
twelve-fold
twelve-numbered
</td>
<td>
@ -521,25 +521,25 @@ console.log(AppSource)
</td>
</tr>
</tbody></table>
</p><p>
Clock faces are drawn in the configured foreground and background colors
(you may select them at the end of this form)
</p><p>
"Four-fold" clock faces may draw indian-arabic or roman numerals. Which do you prefer?
</p><p>
<input type="radio" name="clock-face-numerals" value="indian" checked> indian-arabic (3, 6, 9, 12)<br>
<input type="radio" name="clock-face-numerals" value="roman"> roman (III, VI, IX, XII)
</p><p>
The "twelve-fold" and "rainbow"-colored faces may be drawn with or without
dots marking the position of every minute. Which variant do you prefer?
</p><p>
<input type="radio" name="clock-face-dots" value="without-dots" checked> without dots <br>
<input type="radio" name="clock-face-dots" value="with-dots"> with dots
</p><p>
If you prefer a "custom" clock face, please enter the URL
of its JavaScript module below:
</p><p>
custom URL: <input type="url" id="clock-face-custom-url" size="50">
</p><p>
Clock faces are drawn in the configured foreground and background colors
(you may select them at the end of this form)
</p><p>
"Four-numbered" clock faces may draw indian-arabic or roman numerals. Which do you prefer?
</p><p>
<input type="radio" name="clock-face-numerals" value="indian" checked> indian-arabic (3, 6, 9, 12)<br>
<input type="radio" name="clock-face-numerals" value="roman"> roman (III, VI, IX, XII)
</p><p>
The "twelve-numbered" and "rainbow"-colored faces may be drawn with or without
dots marking the position of every minute. Which variant do you prefer?
</p><p>
<input type="radio" name="clock-face-dots" value="without-dots" checked> without dots <br>
<input type="radio" name="clock-face-dots" value="with-dots"> with dots
</p>
<h3>Clock Hands</h3>
@ -582,6 +582,11 @@ console.log(AppSource)
</td>
</tr>
</tbody></table>
</p><p>
If you prefer "custom" clock hands, please enter the URL
of their JavaScript module below:
</p><p>
custom URL: <input type="url" id="clock-hands-custom-url" size="50">
</p><p>
Clock hands are drawn in the configured foreground and background colors
(you may select them at the end of this form)
@ -631,11 +636,6 @@ console.log(AppSource)
<input type="radio" name="second-hand" value="#FF00FF" class="ColorPatch" style="background:#FF00FF"/>
<input type="radio" name="second-hand" value="#00FFFF" class="ColorPatch" style="background:#00FFFF"/>
<input type="radio" name="second-hand" value="#FFFFFF" class="ColorPatch" style="background:#FFFFFF"/>
</p><p>
If you prefer "custom" clock hands, please enter the URL
of their JavaScript module below:
</p><p>
custom URL: <input type="url" id="clock-hands-custom-url" size="50">
</p>
<h3>Complications</h3>

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -1 +1,2 @@
0.01: New App!
0.02: Faster maze generation

View File

@ -35,21 +35,56 @@ function Maze(n) {
this.walls[cell] = WALL_RIGHT|WALL_DOWN;
this.groups[cell] = cell;
}
// Candidates of walls to break when digging the maze.
// If candidate failed (breaking it would create a loop),
// it would never succeed, so no need to retry it.
let candidates_down = [],
candidates_right = [];
for (let r=0 ; r<n; r++) {
for (let c=0; c<n; c++) {
let cell = n*r+c;
if (r<(n-1)) { // Don't break wall down for bottom row.
candidates_down.push(cell);
}
if (c<(n-1)) { // Don't break wall right for rightmost column.
candidates_right.push(cell);
}
}
}
let from_group, to_group;
let ngroups = n*n;
while (--ngroups) {
// Abort if BTN1 pressed [grace period for menu]
// (for some reason setWatch() fails inside constructor)
if (ngroups<n*n-4 && digitalRead(BTN1)) {
if (ngroups<n*n-16 && digitalRead(BTN1)) {
aborting = true;
return;
}
from_group = to_group = -1;
while (from_group<0) {
if (Math.random()<0.5) { // try to break a wall right
let r = Math.floor(Math.random()*n);
let c = Math.floor(Math.random()*(n-1));
let cell = r*n+c;
let trying_down = false;
if (Math.random()<0.5 && candidates_down.length || !candidates_right.length) {
trying_down = true;
}
let candidates = trying_down ? candidates_down : candidates_right;
candidate_index = Math.floor(Math.random()*candidates.length),
cell = candidates.splice(candidate_index, 1)[0],
r = Math.floor(cell/n),
c = cell%n;
if (trying_down) { // try to break a wall down
if (this.groups[cell]!=this.groups[cell+n]) {
this.walls[cell] &= ~WALL_DOWN;
g.clearRect(
this.margin+c*this.wall_length+1,
this.margin+(r+1)*this.wall_length,
this.margin+(c+1)*this.wall_length-1,
this.margin+(r+1)*this.wall_length
);
g.flip(); // show progress.
from_group = this.groups[cell];
to_group = this.groups[cell+n];
}
} else { // try to break a wall right
if (this.groups[cell]!=this.groups[cell+1]) {
this.walls[cell] &= ~WALL_RIGHT;
g.clearRect(
@ -62,21 +97,6 @@ function Maze(n) {
from_group = this.groups[cell];
to_group = this.groups[cell+1];
}
} else { // try to break a wall down
let r = Math.floor(Math.random()*(n-1));
let c = Math.floor(Math.random()*n);
let cell = r*n+c;
if (this.groups[cell]!=this.groups[cell+n]) {
this.walls[cell] &= ~WALL_DOWN;
g.clearRect(
this.margin+c*this.wall_length+1,
this.margin+(r+1)*this.wall_length,
this.margin+(c+1)*this.wall_length-1,
this.margin+(r+1)*this.wall_length
);
from_group = this.groups[cell];
to_group = this.groups[cell+n];
}
}
}
for (let cell = 0; cell<n*n; cell++) {
@ -253,7 +273,6 @@ let maze_interval = setInterval(
function() {
if (maze) {
if (digitalRead(BTN1) || maze.status==STATUS_ABORTED) {
console.log(`aborting ${start_time}`);
maze = null;
start_time = duration = 0;
aborting = false;
@ -270,7 +289,7 @@ let maze_interval = setInterval(
duration = Date.now()-start_time;
g.setFontAlign(0,0).setColor(g.theme.fg);
g.setFont("Vector",18);
g.drawString(`Solved in\n ${timeToText(duration)} \nClick to play again`, g.getWidth()/2, g.getHeight()/2, true);
g.drawString(`Solved ${maze.n}X${maze.n} in\n ${timeToText(duration)} \nClick to play again`, g.getWidth()/2, g.getHeight()/2, true);
}
}
}, 25);

View File

@ -7,3 +7,7 @@
Make circles and text slightly bigger
0.05: Show correct percentage values in circles
Show humidity as weather circle data
0.06: Allow settings empty circles
Support to choose between humidity and wind speed for weather circle progress
Support to show time and progress until next sunrise or sunset
Load daily steps from Bangle health if available

View File

@ -5,23 +5,20 @@ A clock with circles for different data at the bottom in a probably familiar sty
By default the time, date and day of week is shown.
It can show the following information (this can be configured):
* Steps (requires [pedometer widget](https://banglejs.com/apps/#pedometer))
* Steps distance (depending on steps)
* Steps
* Steps distance
* Heart rate (automatically updates when screen is on and unlocked)
* Battery (including charging status and battery low warning)
* Weather (requires [weather app](https://banglejs.com/apps/#weather))
* Humidity as circle progress
* Humidity or wind speed as circle progress
* Temperature inside circle
* Condition as icon below circle
* Time and progress until next sunrise or sunset (requires [my location app](https://banglejs.com/apps/#mylocation))
## Screenshots
![Screenshot dark theme](screenshot-dark.png)
![Screenshot light theme](screenshot-light.png)
# TODO
* Add sunrise and sunset
* Display moon instead of sun during night on weather circle
## Creator
Marco ([myxor](https://github.com/myxor))

View File

@ -1,6 +1,7 @@
const locale = require("locale");
const heatshrink = require("heatshrink");
const storage = require("Storage");
const SunCalc = require("https://raw.githubusercontent.com/mourner/suncalc/master/suncalc.js");
const shoesIcon = heatshrink.decompress(atob("h0OwYJGgmAAgUBkgECgVJB4cSoAUDyEBkARDpADBhMAyQRBgVAkgmDhIUDAAuQAgY1DAAYA="));
const shoesIconGreen = heatshrink.decompress(atob("h0OwYJGhIEDgVIAgUEyQKDkmACgcggVACIeQAYMSgIRCgmApIbDiQUDAAkBkAFDGoYAD"));
@ -11,6 +12,7 @@ const powerIconRed = heatshrink.decompress(atob("h0OwYQNoAEDyAEDkgEDpIFDiVJBweSA
const weatherCloudy = heatshrink.decompress(atob("iEQwYWTgP//+AAoMPAoPwAoN/AocfAgP//0AAgQAB/AFEABgdDAAMDDohMRA"));
const weatherSunny = heatshrink.decompress(atob("iEQwYLIg3AAgVgAQMMAo8Am3YAgUB23bAoUNAoIUBjYFCsOwBYoFDDpFgHYI1JI4gFGAAYA="));
const weatherMoon = heatshrink.decompress(atob("iEQwIFCgOAh/wj/4n/8AId//wBBBIoRBCoIZBDoI"));
const weatherPartlyCloudy = heatshrink.decompress(atob("iEQwYQNv0AjgGDn4EDh///gFChwREC4MfxwIBv0//+AC4X4j4FCv/AgfwgED/wIBuAaBBwgFDgP4gf/AAXABwIEBDQQAEA=="));
const weatherRainy = heatshrink.decompress(atob("iEQwYLIg/gAgUB///wAFBh/AgfwgED/wIBuEAj4OCv0AjgaCh/4AocAnAFBFIU4EAM//gRBEAIOBhw1C/AmDAosAC4JNIAAg"));
const weatherPartlyRainy = heatshrink.decompress(atob("h0OwYJGjkAnAFCj+AAgU//4FCuEA8EAg8ch/4gEB4////AAoIIBCIMD/wgCg4bBg/8BwMD+AgBh4ZBDQf/FIIABh4IBgAA=="));
@ -18,6 +20,9 @@ const weatherSnowy = heatshrink.decompress(atob("iEQwYROn/8AocH8AECuAFBh0Agf+CIN
const weatherFoggy = heatshrink.decompress(atob("iEQwYROn/8AgUB/EfwAFBh/AgfwgED/wIBuEABwd/4EcDQgFDgE4Fosf///8f//A/Lj/xCQIRNA="));
const weatherStormy = heatshrink.decompress(atob("iEQwYLIg/gAgUB///wAFBh/AgfwgED/wIBuEAj4OCv0AjgaCh/4AoX8gE4AoQpBnAdBF4IRBDQMH/kOHgY7DAo4AOA=="));
const sunSetDown = heatshrink.decompress(atob("iEQwIHEgOAAocT5EGtEEkF//wLDg1ggfACoo"));
const sunSetUp = heatshrink.decompress(atob("iEQwIHEgOAAocT5EGtEEkF//wRFgfAg1gBIY"));
let settings;
function loadSettings() {
@ -29,6 +34,7 @@ function loadSettings() {
'stepLength': 0.8,
'batteryWarn': 30,
'showWidgets': false,
'weatherCircleData': 'humidity',
'circle1': 'hr',
'circle2': 'steps',
'circle3': 'battery'
@ -40,9 +46,21 @@ function loadSettings() {
}
}
loadSettings();
/*
* Read location from myLocation app
*/
function getLocation() {
return storage.readJSON("mylocation.json", 1) || undefined;
}
let location = getLocation();
const showWidgets = settings.showWidgets || false;
let hrtValue;
let now = Math.round(new Date().getTime() / 1000);
// layout values:
const colorFg = g.theme.dark ? '#fff' : '#000';
@ -64,7 +82,6 @@ const radiusOuter = 25;
const radiusInner = 20;
const circleFont = "Vector:15";
const circleFontBig = "Vector:16";
const circleFontSmall = "Vector:13";
function draw() {
g.clear(true);
@ -93,6 +110,7 @@ function draw() {
g.setFontAlign(0, -1);
g.setColor(colorFg);
g.drawString(locale.time(new Date(), 1), w / 2, h1 + 8);
now = Math.round(new Date().getTime() / 1000);
// date & dow
g.setFont("Vector:21");
@ -127,19 +145,42 @@ function drawCircle(index) {
case "weather":
drawWeather(w);
break;
case "sunprogress":
drawSunProgress(w);
break;
case "empty":
// we draw nothing here
return;
}
}
// serves as cache for quicker lookup of circle positions
let circlePositionsCache = [];
/*
* Looks in the following order if a circle with the given type is somewhere visible/configured
* 1. circlePositionsCache
* 2. settings
* 3. defaultCircleTypes
*
* In case 2 and 3 the circlePositionsCache will be updated
*/
function getCirclePosition(type) {
if (circlePositionsCache[type] >= 0) {
return circlePosX[circlePositionsCache[type]];
}
for (let i = 1; i <= 3; i++) {
const setting = settings['circle' + i];
if (setting == type) return circlePosX[i - 1];
if (setting == type) {
circlePositionsCache[type] = i - 1;
return circlePosX[i - 1];
}
}
for (let i = 0; i < defaultCircleTypes.length; i++) {
if (type == defaultCircleTypes[i] && (!settings || settings['circle' + (i + 1)] == undefined)) {
return circlePosX[i];
}
}
if (type == defaultCircleTypes[i] && (!settings || settings['circle' + (i + 1)] == undefined)) {
circlePositionsCache[type] = i;
return circlePosX[i];
}
}
return undefined;
}
@ -147,16 +188,12 @@ function isCircleEnabled(type) {
return getCirclePosition(type) != undefined;
}
function drawSteps(w) {
if (!w) w = getCirclePosition("steps");
const steps = getSteps();
// Draw rectangle background:
g.setColor(colorBg);
g.fillRect(w - radiusOuter - 3, h3 - radiusOuter - 3, w + radiusOuter + 3, h3 + radiusOuter + 3);
g.setColor(colorGrey);
g.fillCircle(w, h3, radiusOuter);
drawCircleBackground(w);
const stepGoal = settings.stepGoal || 10000;
if (stepGoal > 0) {
@ -165,15 +202,9 @@ function drawSteps(w) {
drawGauge(w, h3, percent, colorBlue);
}
g.setColor(colorBg);
g.fillCircle(w, h3, radiusInner);
drawInnerCircleAndTriangle(w);
g.fillPoly([w, h3, w - 15, h3 + radiusOuter + 5, w + 15, h3 + radiusOuter + 5]);
g.setFont(circleFont);
g.setFontAlign(0, 0);
g.setColor(colorFg);
g.drawString(shortValue(steps), w + 2, h3);
writeCircleText(w, shortValue(steps));
g.drawImage(shoesIcon, w - 6, h3 + radiusOuter - 6);
}
@ -184,12 +215,7 @@ function drawStepsDistance(w) {
const stepDistance = settings.stepLength || 0.8;
const stepsDistance = Math.round(steps * stepDistance);
// Draw rectangle background:
g.setColor(colorBg);
g.fillRect(w - radiusOuter - 3, h3 - radiusOuter - 3, w + radiusOuter + 3, h3 + radiusOuter + 3);
g.setColor(colorGrey);
g.fillCircle(w, h3, radiusOuter);
drawCircleBackground(w);
const stepDistanceGoal = settings.stepDistanceGoal || 8000;
if (stepDistanceGoal > 0) {
@ -198,15 +224,9 @@ function drawStepsDistance(w) {
drawGauge(w, h3, percent, colorGreen);
}
g.setColor(colorBg);
g.fillCircle(w, h3, radiusInner);
drawInnerCircleAndTriangle(w);
g.fillPoly([w, h3, w - 15, h3 + radiusOuter + 5, w + 15, h3 + radiusOuter + 5]);
g.setFont(circleFont);
g.setFontAlign(0, 0);
g.setColor(colorFg);
g.drawString(shortValue(stepsDistance), w + 2, h3);
writeCircleText(w, shortValue(stepsDistance));
g.drawImage(shoesIconGreen, w - 6, h3 + radiusOuter - 6);
}
@ -214,28 +234,18 @@ function drawStepsDistance(w) {
function drawHeartRate(w) {
if (!w) w = getCirclePosition("hr");
// Draw rectangle background:
g.setColor(colorBg);
g.fillRect(w - radiusOuter - 3, h3 - radiusOuter - 3, w + radiusOuter + 3, h3 + radiusOuter + 3);
drawCircleBackground(w);
g.setColor(colorGrey);
g.fillCircle(w, h3, radiusOuter);
if (hrtValue != undefined && hrtValue > 0) {
if (hrtValue != undefined) {
const minHR = settings.minHR || 40;
const percent = (hrtValue - minHR) / (settings.maxHR - minHR);
const maxHR = settings.maxHR || 200;
const percent = (hrtValue - minHR) / (maxHR - minHR);
drawGauge(w, h3, percent, colorRed);
}
g.setColor(colorBg);
g.fillCircle(w, h3, radiusInner);
drawInnerCircleAndTriangle(w);
g.fillPoly([w, h3, w - 15, h3 + radiusOuter + 5, w + 15, h3 + radiusOuter + 5]);
g.setFont(circleFontBig);
g.setFontAlign(0, 0);
g.setColor(colorFg);
g.drawString(hrtValue != undefined ? hrtValue : "-", w, h3);
writeCircleText(w, hrtValue != undefined ? hrtValue : "-");
g.drawImage(heartIcon, w - 6, h3 + radiusOuter - 6);
}
@ -244,25 +254,14 @@ function drawBattery(w) {
if (!w) w = getCirclePosition("battery");
const battery = E.getBattery();
// Draw rectangle background:
g.setColor(colorBg);
g.fillRect(w - radiusOuter - 3, h3 - radiusOuter - 3, w + radiusOuter + 3, h3 + radiusOuter + 3);
g.setColor(colorGrey);
g.fillCircle(w, h3, radiusOuter);
drawCircleBackground(w);
if (battery > 0) {
const percent = battery / 100;
drawGauge(w, h3, percent, colorYellow);
}
g.setColor(colorBg);
g.fillCircle(w, h3, radiusInner);
g.fillPoly([w, h3, w - 15, h3 + radiusOuter + 5, w + 15, h3 + radiusOuter + 5]);
g.setFont(circleFont);
g.setFontAlign(0, 0);
drawInnerCircleAndTriangle(w);
let icon = powerIcon;
let color = colorFg;
@ -275,8 +274,7 @@ function drawBattery(w) {
icon = powerIconRed;
}
}
g.setColor(color);
g.drawString(battery + '%', w, h3);
writeCircleText(w, battery + '%');
g.drawImage(icon, w - 6, h3 + radiusOuter - 6);
}
@ -285,30 +283,37 @@ function drawWeather(w) {
if (!w) w = getCirclePosition("weather");
const weather = getWeather();
const tempString = weather ? locale.temp(weather.temp - 273.15) : undefined;
const humidity = weather ? weather.hum : undefined;
const code = weather ? weather.code : -1;
// Draw rectangle background:
g.setColor(colorBg);
g.fillRect(w - radiusOuter - 3, h3 - radiusOuter - 3, w + radiusOuter + 3, h3 + radiusOuter + 3);
drawCircleBackground(w);
g.setColor(colorGrey);
g.fillCircle(w, h3, radiusOuter);
if (humidity >= 0) {
drawGauge(w, h3, humidity / 100, colorYellow);
const data = settings.weatherCircleData || "humidity";
switch (data) {
case "humidity":
const humidity = weather ? weather.hum : undefined;
if (humidity >= 0) {
drawGauge(w, h3, humidity / 100, colorYellow);
}
break;
case "wind":
if (weather) {
const 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)
drawGauge(w, h3, wind[1] / 12, colorYellow);
}
}
break;
case "empty":
break;
}
g.setColor(colorBg);
g.fillCircle(w, h3, radiusInner);
drawInnerCircleAndTriangle(w);
g.fillPoly([w, h3, w - 25, h3 + radiusOuter + 5, w + 25, h3 + radiusOuter + 5]);
const content = tempString ? tempString : "?";
g.setFont(content.length < 4 ? circleFont : circleFontSmall);
g.setFontAlign(0, 0);
g.setColor(colorFg);
g.drawString(content, w, h3);
writeCircleText(w, tempString ? tempString : "?");
if (code > 0) {
const icon = getWeatherIconByCode(code);
@ -316,6 +321,69 @@ function drawWeather(w) {
}
}
function drawSunProgress(w) {
if (!w) w = getCirclePosition("sunprogress");
const percent = getSunProgress();
drawCircleBackground(w);
drawGauge(w, h3, percent, colorYellow);
drawInnerCircleAndTriangle(w);
let icon = powerIcon;
let color = colorFg;
if (isDay()) {
// day
color = colorFg;
icon = sunSetDown;
} else {
// night
color = colorGrey;
icon = sunSetUp;
}
g.setColor(color);
let text = "?";
const times = getSunData();
if (times != undefined) {
const sunRise = Math.round(times.sunrise.getTime() / 1000);
const sunSet = Math.round(times.sunset.getTime() / 1000);
if (!isDay()) {
// night
if (now > sunRise) {
// after sunRise
const upcomingSunRise = sunRise + 60 * 60 * 24;
text = formatSeconds(upcomingSunRise - now);
} else {
text = formatSeconds(sunRise - now);
}
} else {
// day, approx sunrise tomorrow:
text = formatSeconds(sunSet - now);
}
}
writeCircleText(w, text);
g.drawImage(icon, w - 6, h3 + radiusOuter - 6);
}
/*
* wind goes from 0 to 12 (see https://en.wikipedia.org/wiki/Beaufort_scale)
*/
function windAsBeaufort(windInKmh) {
const beaufort = [2, 6, 12, 20, 29, 39, 50, 62, 75, 89, 103, 118];
let l = 0;
while (l < beaufort.length && beaufort[l] < windInKmh) {
l++;
}
return l;
}
/*
* Choose weather icon to display based on weather conditition code
* https://openweathermap.org/weather-conditions#Weather-Condition-Codes-2
@ -350,7 +418,7 @@ function getWeatherIconByCode(code) {
case 8:
switch (code) {
case 800:
return weatherSunny;
return isDay() ? weatherSunny : weatherMoon;
case 801:
return weatherPartlyCloudy;
case 802:
@ -365,32 +433,122 @@ function getWeatherIconByCode(code) {
return undefined;
}
function isDay() {
const times = getSunData();
if (times == undefined) return true;
const sunRise = Math.round(times.sunrise.getTime() / 1000);
const sunSet = Math.round(times.sunset.getTime() / 1000);
return (now > sunRise && now < sunSet);
}
function formatSeconds(s) {
if (s > 60 * 60) { // hours
return Math.round(s / (60 * 60)) + "h";
}
if (s > 60) { // minutes
return Math.round(s / 60) + "m";
}
return "<1m";
}
function getSunData() {
if (location != undefined && location.lat != undefined) {
// get today's sunlight times for lat/lon
return SunCalc.getTimes(new Date(), location.lat, location.lon);
}
return undefined;
}
/*
* Calculated progress of the sun between sunrise and sunset in percent
*
* Taken from rebble app and modified
*/
function getSunProgress() {
const times = getSunData();
if (times == undefined) return 0;
const sunRise = Math.round(times.sunrise.getTime() / 1000);
const sunSet = Math.round(times.sunset.getTime() / 1000);
if (isDay()) {
// during day
const dayLength = sunSet - sunRise;
if (now > sunRise) {
return (now - sunRise) / dayLength;
} else {
return (sunRise - now) / dayLength;
}
} else {
// during night
if (sunSet < sunRise) {
const upcomingSunRise = sunRise + 60 * 60 * 24;
return 1 - (upcomingSunRise - now) / (upcomingSunRise - sunSet);
} else {
const lastSunSet = sunSet - 60 * 60 * 24;
return (now - lastSunSet) / (sunRise - lastSunSet);
}
}
}
/*
* Draws the background and the grey circle
*/
function drawCircleBackground(w) {
// Draw rectangle background:
g.setColor(colorBg);
g.fillRect(w - radiusOuter - 3, h3 - radiusOuter - 3, w + radiusOuter + 3, h3 + radiusOuter + 3);
// Draw grey background circle:
g.setColor(colorGrey);
g.fillCircle(w, h3, radiusOuter);
}
function drawInnerCircleAndTriangle(w) {
// Draw inner circle
g.setColor(colorBg);
g.fillCircle(w, h3, radiusInner);
// Draw triangle which covers the bottom of the circle
g.fillPoly([w, h3, w - 15, h3 + radiusOuter + 5, w + 15, h3 + radiusOuter + 5]);
}
function radians(a) {
return a * Math.PI / 180;
}
/*
* This draws the actual gauge consisting out of lots of little filled circles
*/
function drawGauge(cx, cy, percent, color) {
const offset = 15;
const end = 345;
const r = radiusInner + 3;
const radius = radiusInner + 3;
const size = radiusOuter - radiusInner - 2;
if (percent <= 0) return;
if (percent > 1) percent = 1;
const startrot = -offset;
const endrot = startrot - ((end - offset) * percent);
const startRotation = -offset;
const endRotation = startRotation - ((end - offset) * percent);
g.setColor(color);
const size = radiusOuter - radiusInner - 2;
// draw gauge
for (let i = startrot; i > endrot - size; i -= size) {
x = cx + r * Math.sin(radians(i));
y = cy + r * Math.cos(radians(i));
for (let i = startRotation; i > endRotation - size; i -= size) {
x = cx + radius * Math.sin(radians(i));
y = cy + radius * Math.cos(radians(i));
g.fillCircle(x, y, size);
}
}
function writeCircleText(w, content) {
if (content == undefined) return;
g.setFont(content.length < 4 ? circleFontBig : circleFont);
g.setFontAlign(0, 0);
g.setColor(colorFg);
g.drawString(content, w, h3);
}
function shortValue(v) {
if (isNaN(v)) return '-';
if (v <= 999) return v;
@ -405,6 +563,9 @@ function shortValue(v) {
}
function getSteps() {
if (Bangle.getHealthStatus) {
return Bangle.getHealthStatus("day").steps;
}
if (WIDGETS && WIDGETS.wpedom !== undefined) {
return WIDGETS.wpedom.getSteps();
}

View File

@ -6,8 +6,12 @@
settings[key] = value;
storage.write(SETTINGS_FILE, settings);
}
var valuesCircleTypes = ["steps", "stepsDist", "hr", "battery", "weather"];
var namesCircleTypes = ["steps", "distance", "heart", "battery", "weather"];
const valuesCircleTypes = ["steps", "stepsDist", "hr", "battery", "weather", "sunprogress", "empty"];
const namesCircleTypes = ["steps", "distance", "heart", "battery", "weather", "sun progress", "empty"];
const weatherData = ["humidity", "wind", "empty"];
E.showMenu({
'': { 'title': 'circlesclock' },
'< Back': back,
@ -76,21 +80,27 @@
format: () => (settings.showWidgets ? 'Yes' : 'No'),
onchange: x => save('showWidgets', x),
},
'weather circle': {
value: settings.weatherCircleData ? weatherData.indexOf(settings.weatherCircleData) : 0,
min: 0, max: 2,
format: v => weatherData[v],
onchange: x => save('weatherCircleData', weatherData[x]),
},
'left': {
value: settings.circle1 ? valuesCircleTypes.indexOf(settings.circle1) : 0,
min: 0, max: 4,
min: 0, max: 6,
format: v => namesCircleTypes[v],
onchange: x => save('circle1', valuesCircleTypes[x]),
},
'middle': {
value: settings.circle2 ? valuesCircleTypes.indexOf(settings.circle2) : 2,
min: 0, max: 4,
min: 0, max: 6,
format: v => namesCircleTypes[v],
onchange: x => save('circle2', valuesCircleTypes[x]),
},
'right': {
value: settings.circle3 ? valuesCircleTypes.indexOf(settings.circle3) : 3,
min: 0, max: 4,
min: 0, max: 6,
format: v => namesCircleTypes[v],
onchange: x => save('circle3', valuesCircleTypes[x]),
}

1
apps/crowclk/ChangeLog Normal file
View File

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

11
apps/crowclk/README.md Normal file
View File

@ -0,0 +1,11 @@
# Crow Clock
Crow Clock features the face of Mystery Science Theater's Crow T. Robot.
The code is based almost entirely on Bold Clock.
## Features
Its got Crow right there on the face! What more do you need?
![](screenshot_crow.png)

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwkG/4AImcikUzBpIAHmURiIXBAYMjCx0hiU/AwfzA4wWIiYJHmMSCxUxgYLKERH/+UU8kvBY/zp1FBZEhp3uDA/yBQURFw8fl3tBoKJEmQWB9vk+MfFw3/n3SEwVCn/zkgGCkni/4wFFwP/mnjkQRB93UpoDBDoM96f/+JUEmKxB+nhl8ypvtolE73kofyj1PPYKSEWAXy8UimckFoXUiUzkUuC4IqBIwogB8gWBJAPd7pGCSAJECJAfxfAXzokSp3t7wvBAYPkkNNB4ZICRoIACokkFYIAE7viogPDkQCBMYkkonuolFqtVqgGCC4hgB+bEEmlNqEFolAAQfUoQPDLgPyVYku8sArx0BAQQGB6TTFOwYABnvVgFEqnkp1FokAhoXEIoPxeYn0CANEggXBoAGCoYQEibCDVAdFqgtBGIPlPII/EC46oBpqIB73tAYPURwioCVIQwFUQIACAoIuFC5AYBpwXD8idEC5fyd4tPC58093tCoIzBRooXCO43/n3u7wXBDQPiC58uI4vkBw0Ta4oABmq5BAAVFqQXITA0wgAAEgSdGj/yTI0lC4tUC44BBL41ACwcEO43xFoMTU47YC8ne8YNFRoTAG+jXMCgUxPAvzFwS/B9qFGIgXyMA00d4Xe6QLFRghgGGAfkFwxDEJAwkBqtVWY3/iQPEJA3zUwIhGUoQADiIXPkIeGGAkzmYXBkczDIZAHGAUyolE7vuqEA73tA4MjH44gCineaYYXBbQlBPo6SCrwQDFYIFD8qMEAA0hroqEAAXkqR8GRYshqotBAAdViQWLJQcVDIVVqJELGQ0ykUikYsJ"))

153
apps/crowclk/crow_clock.js Normal file
View File

@ -0,0 +1,153 @@
var img = require("heatshrink").decompress(atob("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"));
var hour_hand = {
width : 61, height : 8, bpp : 1,
transparent : 0,
buffer : E.toArrayBuffer(atob("/////////////////////////////////////////////////////////////////////////////////w=="))
};
var minute_hand = {
width : 110, height : 4, bpp : 1,
transparent : 0,
buffer : E.toArrayBuffer(atob("/////////////////////////////////////////////////////////////////////////w=="))
};
//g.fillRect(0,24,239,239); // Apps area
let intervalRef = null;
const p180 = Math.PI/180;
const clock_center = {x:Math.floor((g.getWidth()-1)/2), y:24+Math.floor((g.getHeight()-25)/2)};
// ={ x: 119, y: 131 }
const radius = Math.floor((g.getWidth()-24+1)/2); // =108
let tick0 = Graphics.createArrayBuffer(30,8,1,{msb:true});
tick0.fillRect(0,0,tick0.getWidth()-1, tick0.getHeight()-1);
let tick5 = Graphics.createArrayBuffer(20,6,1,{msb:true});
tick5.fillRect(0,0,tick5.getWidth()-1, tick5.getHeight()-1);
let tick1 = Graphics.createArrayBuffer(8,4,1,{msb:true});
tick1.fillRect(0,0,tick1.getWidth()-1, tick1.getHeight()-1);
// Adjust hand lengths to be within 'tick' points
minute_hand.width=radius-tick1.getWidth()-6;
hour_hand.width=radius-tick5.getWidth()-6;
function big_wheel_x(angle){
return clock_center.x + radius * Math.cos(angle*p180);
}
function big_wheel_y(angle){
return clock_center.y + radius * Math.sin(angle*p180);
}
function rotate_around_x(center_x, angle, tick){
return center_x + Math.cos(angle*p180) * tick.getWidth()/2;
}
function rotate_around_y(center_y, angle, tick){
return center_y + Math.sin(angle*p180) * tick.getWidth()/2;
}
function hour_pos_x(angle){
return clock_center.x + Math.cos(angle*p180) * hour_hand.width/2;
}
function hour_pos_y(angle){
return clock_center.y + Math.sin(angle*p180) * hour_hand.width/2;
}
function minute_pos_x(angle){
return clock_center.x + Math.cos(angle*p180) * minute_hand.width/2;
}
function minute_pos_y(angle){
return clock_center.y + Math.sin(angle*p180) * minute_hand.width/2;
}
function minute_angle(date){
//let minutes = date.getMinutes() + date.getSeconds()/60;
let minutes = date.getMinutes();
return 6*minutes - 90;
}
function hour_angle(date){
let hours= date.getHours() + date.getMinutes()/60;
return 30*hours - 90;
}
function draw_clock(){
//console.log("draw_clock");
let date = new Date();
g.reset();
g.clearRect(0,24,239,239); // clear app area
g.drawImage(img, 12, 24);
// draw cross lines for testing
// g.setColor(1,0,0);
// g.drawLine(clock_center.x - radius, clock_center.y, clock_center.x + radius, clock_center.y);
// g.drawLine(clock_center.x, clock_center.y - radius, clock_center.x, clock_center.y + radius);
g.setColor(g.theme.fg);
let ticks = [0, 90, 180, 270];
ticks.forEach((item)=>{
let agl = item+180;
g.drawImage(tick0.asImage(), rotate_around_x(big_wheel_x(item), agl, tick0), rotate_around_y(big_wheel_y(item), agl, tick0), {rotate:agl*p180});
});
ticks = [30, 60, 120, 150, 210, 240, 300, 330];
ticks.forEach((item)=>{
let agl = item+180;
g.drawImage(tick5.asImage(), rotate_around_x(big_wheel_x(item), agl, tick5), rotate_around_y(big_wheel_y(item), agl, tick5), {rotate:agl*p180});
});
let hour_agl = hour_angle(date);
let minute_agl = minute_angle(date);
g.drawImage(hour_hand, hour_pos_x(hour_agl), hour_pos_y(hour_agl), {rotate:hour_agl*p180}); //
g.drawImage(minute_hand, minute_pos_x(minute_agl), minute_pos_y(minute_agl), {rotate:minute_agl*p180}); //
g.setColor(g.theme.fg);
g.fillCircle(clock_center.x, clock_center.y, 6);
g.setColor(g.theme.bg);
g.fillCircle(clock_center.x, clock_center.y, 3);
// draw minute ticks. Takes long time to draw!
g.setColor(g.theme.fg);
for (var i=0; i<60; i++){
let agl = i*6+180;
g.drawImage(tick1.asImage(), rotate_around_x(big_wheel_x(i*6), agl, tick1), rotate_around_y(big_wheel_y(i*6), agl, tick1), {rotate:agl*p180});
}
g.flip();
//console.log(date);
}
function clearTimers(){
//console.log("clearTimers");
if(intervalRef) {
clearInterval(intervalRef);
intervalRef = null;
//console.log("interval is cleared");
}
}
function startTimers(){
//console.log("startTimers");
if(intervalRef) clearTimers();
intervalRef = setInterval(draw_clock, 60*1000);
//console.log("interval is set");
draw_clock();
}
Bangle.on('lcdPower', (on) => {
if (on) {
//console.log("lcdPower: on");
Bangle.drawWidgets();
startTimers();
} else {
//console.log("lcdPower: off");
clearTimers();
}
});
Bangle.on('faceUp',function(up){
//console.log("faceUp: " + up + " LCD: " + Bangle.isLCDOn());
if (up && !Bangle.isLCDOn()) {
//console.log("faceUp and LCD off");
clearTimers();
Bangle.setLCDPower(true);
}
});
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
startTimers();
// Show launcher when button pressed
Bangle.setUI("clock");

BIN
apps/crowclk/crow_clock.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@ -6,3 +6,4 @@
0.07: Resolve one FIFO_FULL case and exit App with button press
0.08: Leave GPS power switched on on exit (will switch off after 0.5 seconds anyway)
0.09: Fix FIFO_FULL error
0.10: Show satellites "in view" separated by GNS-system

View File

@ -16,9 +16,8 @@ var lastFix = {
time: 0,
satellites: 0
};
var SATinView = 0;
var nofBD = 0;
var nofGP = 0;
var SATinView = 0, lastSATinView = -1, nofGP = 0, nofBD = 0, nofGL = 0;
const leaveNofixLayout = 1; // 0 = stay on initial screen for debugging (default = 1)
var listenerGPSraw = 0;
function formatTime(now) {
@ -63,7 +62,7 @@ function getMaidenHead(param1,param2){
function onGPS(fix) {
if (lastFix.fix != fix.fix) {
// if fix is different, change the layout
if (fix.fix) {
if (fix.fix && leaveNofixLayout) {
layout = new Layout( {
type:"v", c: [
{type:"txt", font:"6x8:2", label:"GPS Info" },
@ -92,11 +91,12 @@ function onGPS(fix) {
g.clearRect(0,24,g.getWidth(),g.getHeight());
layout.render();
}
//lastFix = fix;
if (fix.fix) {
if (fix.fix && leaveNofixLayout) {
if (listenerGPSraw == 1) {
Bangle.removeListener('GPS-raw', onGPSraw);
listenerGPSraw = 0;
lastSATinView = -1;
Bangle.buzz(50);
}
var locale = require("locale");
var satellites = fix.satellites;
@ -115,27 +115,31 @@ function onGPS(fix) {
layout.sat.label = fix.satellites;
layout.render(layout.sat);
}
if (SATinView != lastFix.SATinView) {
if (SATinView != lastSATinView) {
if (!leaveNofixLayout) SATinView = -1;
lastSATinView = SATinView;
layout.clear(layout.progress);
layout.progress.label = "in view: " + SATinView;
layout.progress.label = "in view GP/BD/GL: " + nofGP + " " + nofBD + " " + nofGL;
// console.log("in view GP/BD/GL: " + nofGP + " " + nofBD + " " + nofGL);
layout.render(layout.progress);
}
}
//layout.render();
if (listenerGPSraw == 0 && !fix.fix) {
setTimeout(() => Bangle.on('GPS-raw', onGPSraw), 10);
listenerGPSraw = 1;
}
lastFix = fix;
lastFix.SATinView = SATinView;
}
function onGPSraw(nmea) {
if (nmea.slice(0,7) == "$BDGSV,") nofBD = Number(nmea.slice(11,13));
if (nmea.slice(0,7) == "$GPGSV,") nofGP = Number(nmea.slice(11,13));
SATinView = nofBD + nofGP;
if (nmea.slice(3,6) == "GSV") {
// console.log(nmea.slice(1,3) + " " + nmea.slice(11,13));
if (nmea.slice(0,7) == "$GPGSV,") nofGP = Number(nmea.slice(11,13));
if (nmea.slice(0,7) == "$BDGSV,") nofBD = Number(nmea.slice(11,13));
if (nmea.slice(0,7) == "$GLGSV,") nofGL = Number(nmea.slice(11,13));
SATinView = nofGP + nofBD + nofGL;
}
}

View File

@ -9,3 +9,4 @@
0.09: Bangle.js 2 - pressing the button goes back to clock (fix #971)
After 10s of being locked, the launcher goes back to the clock screen
0.10: added in selectable font in settings including scalable vector font
0.11: Merge Bangle.js 1 and 2 launchers, again

View File

@ -1,75 +0,0 @@
var s = require("Storage");
var apps = s.list(/\.info$/).map(app=>{var a=s.readJSON(app,1);return a&&{name:a.name,type:a.type,icon:a.icon,sortorder:a.sortorder,src:a.src};}).filter(app=>app && (app.type=="app" || app.type=="clock" || !app.type));
apps.sort((a,b)=>{
var n=(0|a.sortorder)-(0|b.sortorder);
if (n) return n; // do sortorder first
if (a.name<b.name) return -1;
if (a.name>b.name) return 1;
return 0;
});
var selected = 0;
var menuScroll = 0;
var menuShowing = false;
function drawMenu() {
g.reset().setFont("6x8",2).setFontAlign(-1,0);
var w = g.getWidth();
var h = g.getHeight();
var m = w/2;
var n = Math.floor((h-48)/64);
if (selected>=n+menuScroll) menuScroll = 1+selected-n;
if (selected<menuScroll) menuScroll = selected;
// arrows
g.setColor(menuScroll ? g.theme.fg : g.theme.bg);
g.fillPoly([m,6,m-14,20,m+14,20]);
g.setColor((apps.length>n+menuScroll) ? g.theme.fg : g.theme.bg);
g.fillPoly([m,h-7,m-14,h-21,m+14,h-21]);
// draw
g.setColor(g.theme.fg);
for (var i=0;i<n;i++) {
var app = apps[i+menuScroll];
if (!app) break;
var y = 24+i*64;
if (i+menuScroll==selected) {
g.setColor(g.theme.bgH).fillRect(0,y,w-1,y+63);
g.setColor(g.theme.fgH).drawRect(0,y,w-1,y+63);
} else {
g.clearRect(0, y, w-1, y+63);
g.setColor(g.theme.fg);
}
g.drawString(app.name,64,y+32);
var icon=undefined;
if (app.icon) icon = s.read(app.icon);
if (icon) try {g.drawImage(icon,8,y+8);} catch(e){}
}
}
g.clear();
drawMenu();
Bangle.setUI("updown",dir=>{
if (dir) {
selected += dir;
if (selected<0) selected = apps.length-1;
if (selected>=apps.length) selected = 0;
drawMenu();
} else {
if (!apps[selected].src) return;
if (require("Storage").read(apps[selected].src)===undefined) {
E.showMessage("App Source\nNot found");
setTimeout(drawMenu, 2000);
} else {
E.showMessage("Loading...");
load(apps[selected].src);
}
}
});
Bangle.loadWidgets();
Bangle.drawWidgets();
// 10s of inactivity goes back to clock
if (Bangle.setLocked) Bangle.setLocked(false); // unlock initially
var lockTimeout;
Bangle.on('lock', locked => {
if (lockTimeout) clearTimeout(lockTimeout);
lockTimeout = undefined;
if (locked)
lockTimeout = setTimeout(_=>load(), 10000);
});

View File

@ -63,8 +63,11 @@ E.showScroller({
}
});
// pressing button goes back
setWatch(_=>load(), BTN1, {edge:"falling"});
// on bangle.js 2, the screen is used for navigating, so the single button goes back
// on bangle.js 1, the buttons are used for navigating
if (process.env.HWVERSION==2) {
setWatch(_=>load(), BTN1, {edge:"falling"});
}
// 10s of inactivity goes back to clock
Bangle.setLocked(false); // unlock initially

View File

@ -1 +1,2 @@
0.01: New App!
0.02: Bangle.js 2 compatibility

View File

@ -5,16 +5,17 @@ var imgs = [
atob("qE5xH+AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4AdwOBlcAAAsrq2BJn+BqxMHKX5NFJhgAFqxO5TYcruug0HLAAoIBvdQCIZN11icDqBLHAA+gMYRQ0TgcrvhNOAAaiCeWWBTgZNSKAuBJ17rDvZOVeQK4C1hOxdaYAFvbxvdgZOGbgMlLCF8DwSgrJxSKCKALvRUFmslbsJJ4YMG0F1qElld10ATGgGBJ9BOCvaLHHYgNEIoqsBKAIJFUFDtCurbIvhPHcgcrAAL9DBQclUFDtCGQIAJIIUAcYQHDq2ArmAqxsDfIL2BKFAxCvhPK0F7uoODSYVWrgACwBXCLwYQDlaekE4ROKAA97CwJODAAJuCfwYABuqglwKeNAA9QMoJPFrjwDAAjxBlesd0hOSHgeAJwjwDWRCgh1guBqBPTHYKfHOBIIBqxPhEgN7J6yfFJ5VQBILwgwJPWWwJPK0DwpJ66LBd6OgJ8TvXuoXBJ6HLBINWJ8VQJ6d8HYROEwD5BkpP/bYpPFrgIBuoUHqEAlZPiZxAAMHYWAdw18CY91BYOsJ8WgJ6d7UAzuBN5JPCwJPivagUC4MrJwoeJJ4VcJ72BJ4UrJ6igCKALtCqASJqDvhJ4bwVHodWTwQcJ0AQCJz3+QASCMABclDYd1MBmBJ76ABkrYCvZPUR4QABBxInCT0DuCJYJ3CeKt1NJZdClesd0RKBFIbxVqEldtusdwJJDvgqBqDxVTxjtgdwd8Fgd7KC77MT0H+TwLQGKAT4CJ/9WTwxQGupPbd4QABldWq2B1hPbTwwvDVYJRCUbRxDAAhTCJyusSRowEktQAAKhXvd1qBSHKKmBdxIwFuotFUjQjBvgkElZPWlaOCABRPFChwAPEYpPSqy9GAGlWJ6JO7UJgA=="),
atob("qE5xH+AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4AtwNWlcAAAUrq2BJP5MJAApS/JoRMJAApR7JotPumfAA10VQlWJ2+AHwZMB4nDABHEz9PeoesJ2icDp5NLKIpkCKGesG4UlJoJOBKJQJBBYT1BeQRQwwJOCTgI/Bb4V0KAxIBp8llb+BCYlWJ2V0IYbzCgBCBJwgKDBoefkoFCUFuBGISWEIglPBI8rAAIYDCYeAJ2hPJcgmArmAqylGJ9ZODz5OEJ5IICq1cAAZQGlZOuJoiWFVQIICcgRPFrhLCuklAgOBJ0+AlZOJJAYABAwgUBJwtcDwMr4itCeE+sJwV0dgoAKU4UrwBPFeARuBUAMr1jslJyhPBIAMlJ5auCeEotCJyRPBMwRPLeAVWJ0eAE4NPJqLvR4iglwIlBFgQATH4ROFJ4qgmFYUAdqRPSWASghTwclJ6qPDJ4srBIIQGwCejT63DksAlZPHp4iD4gQCT0SfXb4ZPFA4N0CIl0eECeCZgSfWeA4hCz4QHJ7usEANPOgQtFeCagDOYV0OIpPCeDruCugkCZoqgUlbtDkpwGB4UAJ7mATYXEQoNPeC3EXYVWTwS/HJ8ErD4IlBeDXDEAQABNxBPfPQTqCEoSgXIIhtJf4ZOawIeBkomDUDYbCNhJPCwBOZwB6Hz8lK4oAT4lPNZRcCwJOheL10DJHEO4LuZdgUAugoHPAQ2JADDubToZOHAATVBBpb6YgGsJy1WdhJQIlYQMTylWJ05QEUQOfejd0EAOBJzElRh/EFwRRDAATuWkrBBJysrbaufKIhqCd1ydCJyZREQYIACDizuWJzLUDdoLyBDSq9CJ6eBJzYAbJ4WsTyuf4gAzT6usCoKfBp4AzlY4BwBPRCoQA5qxPRJ3ZQMA==")
];
var scale = g.getWidth()<200 ? 2 : 3;
function drawImg (i) {
g.drawImage({
width: 80, height: 57, bpp: 8,
buffer: require("heatshrink").decompress(imgs[i])
}, 0, 0, {scale: 3});
}, (g.getWidth()-80*scale)/2, (g.getHeight()-57*scale)/2, {scale: scale});
}
var currImg = 0;
g.clear();
g.setBgColor(0).clear();
drawImg(currImg);
setInterval(function() {
currImg = (currImg + 1) % imgs.length;

View File

@ -1 +1,3 @@
0.01: New App!
0.02: Set pace format to mm:ss, time format to h:mm:ss,
added settings to opt out of GPS and HRM

View File

@ -1,6 +1,6 @@
var B2 = process.env.HWVERSION==2;
var Layout = require("Layout");
var locale = require("locale")
var locale = require("locale");
var fontHeading = "6x8:2";
var fontValue = B2 ? "6x15:2" : "6x8:3";
var headingCol = "#888";
@ -21,21 +21,25 @@ Bangle.drawWidgets();
// ---------------------------
function formatTime(ms) {
var s = Math.round(ms/1000);
var min = Math.floor(s/60).toString();
s = (s%60).toString();
return min.padStart(2,0)+":"+s.padStart(2,0);
let hrs = Math.floor(ms/3600000).toString();
let mins = (Math.floor(ms/60000)%60).toString();
let secs = (Math.floor(ms/1000)%60).toString();
if (hrs === '0')
return mins.padStart(2,0)+":"+secs.padStart(2,0);
else
return hrs+":"+mins.padStart(2,0)+":"+secs.padStart(2,0); // dont pad hours
}
// Format speed in meters/second
function formatPace(speed) {
if (speed < 0.1667) {
return `__'__"`;
return `__:__`;
}
const pace = Math.round(1000 / speed); // seconds for 1km
const min = Math.floor(pace / 60); // minutes for 1km
const sec = pace % 60;
return ('0' + min).substr(-2) + `'` + ('0' + sec).substr(-2) + `"`;
return ('0' + min).substr(-2) + `:` + ('0' + sec).substr(-2);
}
// ---------------------------
@ -149,10 +153,12 @@ Bangle.on("step", function(steps) {
lastStepCount = steps;
});
let settings = require("Storage").readJSON('run.json',1)||{"use_gps":true,"use_hrm":true};
// We always call ourselves once a second, if only to update the time
setInterval(onTimer, 1000);
/* Turn GPS and HRM on right at the start to ensure
we get the highest chance of a lock. */
Bangle.setHRMPower(true,"app");
Bangle.setGPSPower(true,"app");
if (settings.use_hrm) Bangle.setHRMPower(true,"app");
if (settings.use_gps) Bangle.setGPSPower(true,"app");

44
apps/run/settings.js Normal file
View File

@ -0,0 +1,44 @@
(function(back) {
const SETTINGS_FILE = "run.json";
// initialize with default settings...
let s = {
'use_gps': true,
'use_hrm': true
}
// ...and overwrite them with any saved values
// This way saved values are preserved if a new version adds more settings
const storage = require('Storage')
let settings = storage.readJSON(SETTINGS_FILE, 1) || {}
const saved = settings || {}
for (const key in saved) {
s[key] = saved[key]
}
function save() {
settings = s
storage.write(SETTINGS_FILE, settings)
}
E.showMenu({
'': { 'title': 'Run' },
'< Back': back,
'Use GPS': {
value: s.use_gps,
format: () => (s.use_gps ? 'Yes' : 'No'),
onchange: () => {
s.use_gps = !s.use_gps;
save();
},
},
'Use HRM': {
value: s.use_hrm,
format: () => (s.use_hrm ? 'Yes' : 'No'),
onchange: () => {
s.use_hrm = !s.use_hrm;
save();
},
}
})
})

View File

@ -1,2 +1,4 @@
0.01: New App!
0.02: Updated to work with both Bangle.js 1 and 2.
0.03: Now also visible on Bangle.js 2
0.04: Now work with different themes

BIN
apps/slevel/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -2,6 +2,7 @@ g.clear();
var old = {x:0,y:0};
var W = g.getWidth();
var H = g.getHeight();
Bangle.on('accel',function(v) {
var max = Math.max(Math.abs(v.x),Math.abs(v.y),Math.abs(v.z));
if (Math.abs(v.y)==max) {
@ -12,21 +13,26 @@ Bangle.on('accel',function(v) {
var d = Math.sqrt(v.x*v.x+v.y*v.y);
var ang = Math.atan2(d,Math.abs(v.z))*180/Math.PI;
g.setColor(1,1,1);
g.setFont("6x8",2);
g.setFontAlign(0,-1);
g.clearRect(W*(1/4),0,W*(3/4),H*(1/16));
g.drawString(ang.toFixed(1),W/2,0);
g.reset();
g.clearRect(W*(1/4),0,W*(3/4),16);// clear behind text
g.setFont("6x8",2).setFontAlign(0,-1).drawString(ang.toFixed(1),W/2,0);
var n = {
x:E.clip(W/2+v.x*256,4,W-4),
y:E.clip(H/2+v.y*256,4,H-4)};
g.clearRect(old.x-3,old.y-3,old.x+6,old.y+6);
g.setColor(1,1,1);
g.fillRect(n.x-3,n.y-3,n.x+6,n.y+6);
g.setColor(1,0,0);
g.clearRect(old.x-3,old.y-3,old.x+6,old.y+6); // clear old marker
g.setColor("#0f0");
g.fillRect(n.x-3,n.y-3,n.x+6,n.y+6); // draw new marker
// draw rings
g.setColor("#f00");
g.drawCircle(W/2,H/2,W*(1/12));
g.drawCircle(W/2,H/2,W*(1/4));
g.drawCircle(W/2,H/2,W*(5/12));
old = n;
});
setWatch(_=>load(), BTN1);
if (global.BTN2) {
setWatch(_=>load(), BTN2);
setWatch(_=>load(), BTN3);
}

View File

@ -1 +1,2 @@
0.01: Initial release
0.02: Don't start drawing with white colour on white canvas

View File

@ -1,7 +1,7 @@
(function () {
var pen = 'circle';
var discard = null;
var kule = [255, 255, 255];
var kule = [0, 255, 255]; // R, G, B
var oldLock = false;
setInterval(() => {

View File

@ -1 +1,2 @@
0.01: App launched
0.02: Menu uses the correct screen area and properly resets when closed

View File

@ -1,10 +1,8 @@
E.showMenu = function(items) {
const gw = g.getWidth();
const gh = g.getHeight();
var ar = Bangle.appRect;
Bangle.removeAllListeners("drag");
if(!items){
delete m;
g.clearRect(0, 30, gw, gh - 30);
g.clearRect(ar.x, ar.y, ar.x2, ar.y2);
return false;
}
var loc = require("locale");
@ -26,45 +24,45 @@ E.showMenu = function(items) {
draw: () => {
g.reset().setFont('12x20');
m.info.predraw(g);
g.setColor(m.info.cB).fillRect(0, 50, gw, gh - 30).setColor(m.info.cF);
g.setColor(m.info.cB).fillRect(ar.x, ar.y + 20, ar.x2, ar.y2).setColor(m.info.cF);
m.items.forEach((e, i) => {
const s = (i * 48) - m.scroll + 50;
if(s < 30 || s > gh - 74){
const s = (i * 48) - m.scroll + ar.y + 20;
if(s < ar.y || s > ar.y2 - 44){
return false;
}
if(i == m.selected){
g.setColor(m.info.cHB).fillRect(0, s, gw, Math.min(s + 48, gh - 30)).setColor(m.info.cHF);
g.setColor(m.info.cHB).fillRect(ar.x, s, ar.x2, Math.min(s + 48, ar.y2)).setColor(m.info.cHF);
}else{
g.setColor(m.info.cF);
}
g.drawString(e.title, (e.icon ? 30 : 10), s + 5);
g.drawString(e.title, ar.x + (e.icon ? 30 : 10), s + 5);
if(e.icon){
g.drawImage(e.icon, 5, s + 5);
g.drawImage(e.icon, ar.x + 5, s + 5);
}
if(e.type && s < gh - 72){
if(e.type && s < ar.y2 - 42){
if(e.format){
g.setFontAlign(1, -1, 0).drawString(e.format(e.value), gw - 10, s + 25).setFontAlign(-1, -1, 0);
g.setFontAlign(1, -1, 0).drawString(e.format(e.value), ar.x2 - 10, s + 25).setFontAlign(-1, -1, 0);
}else{
g.setFontAlign(1, -1, 0).drawString(e.value, gw - 10, s + 25).setFontAlign(-1, -1, 0);
g.setFontAlign(1, -1, 0).drawString(e.value, ar.x2 - 10, s + 25).setFontAlign(-1, -1, 0);
}
}
});
g.setColor(m.info.cAB).fillRect(0, 30, gw, 50);
g.setColor(m.info.cAF).drawString(m.info.title, (m.back ? 30 : 10), 32);
g.setColor(m.info.cAB).fillRect(ar.x, ar.y, ar.x2, ar.y + 20);
g.setColor(m.info.cAF).drawString(m.info.title, ar.x + (m.back ? 30 : 10), ar.y + 2);
if(m.back){
g.drawLine(5, 40, 20, 40);
g.drawLine(5, 40, 15, 33);
g.drawLine(5, 40, 15, 47);
g.drawLine(ar.x + 5, ar.y + 10, ar.x + 20, ar.y + 10);
g.drawLine(ar.x + 5, ar.y + 10, ar.x + 15, ar.y + 17);
g.drawLine(ar.x + 5, ar.y + 10, ar.x + 15, ar.y + 3);
}
m.info.preflip(g, m.scroll > 0, m.scroll < (m.items.length - 1) * 48);
},
select: (x, y) => {
if(m.selected == -1 || m.selected !== Math.max(Math.min(Math.floor((y + m.scroll - 50) / 48), m.items.length - 1), 0)){
if(m.selected == -1 || m.selected !== Math.max(Math.min(Math.floor((y + m.scroll - ar.y - 20) / 48), m.items.length - 1), 0)){
if(y){
if(y < 50 || y > gh - 30){
if(y < ar.y + 20 || y > ar.y2){
return false;
}else{
m.selected = Math.max(Math.min(Math.floor((y + m.scroll - 50) / 48), m.items.length - 1), 0);
m.selected = Math.max(Math.min(Math.floor((y + m.scroll - ar.y - 20) / 48), m.items.length - 1), 0);
}
}else{
m.selected = Math.floor(m.scroll / 48);
@ -76,7 +74,7 @@ E.showMenu = function(items) {
m.items[m.selected].onchange(m.items[m.selected].value);
m.draw();
}else if(m.items[m.selected].type && m.items[m.selected].type === "number"){
if(x && x < (gw / 2)){
if(x && x < ((ar.x + ar.x2) / 2)){
m.items[m.selected].value = m.items[m.selected].value - (m.items[m.selected].step ? m.items[m.selected].step : 1);
}else{
m.items[m.selected].value = m.items[m.selected].value + (m.items[m.selected].step ? m.items[m.selected].step : 1);
@ -99,7 +97,7 @@ E.showMenu = function(items) {
move: d => {
m.scroll += (d * 48);
m.scroll = Math.min(Math.max(m.scroll, 0), (m.items.length - 1) * 48);
m.selected = Math.max(Math.min(Math.floor((m.scroll - 50) / 48), m.items.length - 1), 0);
m.selected = Math.max(Math.min(Math.floor((m.scroll - ar.y - 20) / 48), m.items.length - 1), 0);
m.draw();
},
};
@ -127,7 +125,7 @@ E.showMenu = function(items) {
return false;
}
if(d.dx == 0 && d.dy == 0){
if(d.x < 30 && d.y < 50){
if(d.x < ar.x + 30 && d.y < ar.y + 20){
m.back();
return false;
}
@ -195,3 +193,9 @@ E.showPrompt = function (e, t){
E.showMenu(menu);
});
};
const bsl = Bangle.showLauncher;
Bangle.showLauncher = function (){
Bangle.removeAllListeners("drag");
bsl();
};

View File

@ -1,3 +1,4 @@
0.01: New watch face
0.02: Use Bangle.setUI for button/launcher handling
0.03: Bangle.js 2 support
0.04: Adds costumizable colours and the respective settings menu

View File

@ -1,5 +1,10 @@
const is12Hour = (require("Storage").readJSON("setting.json",1)||{})["12hour"];
const locale = require("locale");
var settings = require('Storage').readJSON("vectorclock.json", true) || {};
var dowcol = settings.dowcol || g.theme.fg;
var timecol = settings.timecol || g.theme.fg;
var datecol = settings.datecol || g.theme.fg;
function padNum(n, l) {
return ("0".repeat(l)+n).substr(-l);
@ -26,8 +31,8 @@ function executeCommands() {
commands = [];
}
function drawVectorText(text, size, x, y, alignX, alignY) {
g.setFont("Vector", size).setFontAlign(alignX, alignY).drawString(text, x, y);
function drawVectorText(text, size, x, y, alignX, alignY, color) {
g.setFont("Vector", size).setColor(color).setFontAlign(alignX, alignY).drawString(text, x, y);
var m = g.stringMetrics(text);
return {
x1: x - m.width * (alignX / 2 + 0.5),
@ -64,15 +69,15 @@ function draw() {
let x = 2;
let y = 24 + spacer;
pushCommand(drawVectorText, timeText, timeFontSize, x, y, -1, -1);
pushCommand(drawVectorText, meridian, timeFontSize*9/20, x + width, y, 1, -1);
if (showSeconds) pushCommand(drawVectorText, secondsText, timeFontSize*9/20, x + width, y + timeHeight, 1, 1);
pushCommand(drawVectorText, timeText, timeFontSize, x, y, -1, -1, timecol);
pushCommand(drawVectorText, meridian, timeFontSize*9/20, x + width, y, 1, -1, timecol);
if (showSeconds) pushCommand(drawVectorText, secondsText, timeFontSize*9/20, x + width, y + timeHeight, 1, 1, timecol);
y += timeHeight + spacer;
pushCommand(drawVectorText, dowText, dowFontSize, x + width/2, y, 0, -1);
pushCommand(drawVectorText, dowText, dowFontSize, x + width/2, y, 0, -1, dowcol);
y += dowHeight + spacer;
pushCommand(drawVectorText, dateText, dateFontSize, x + width/2, y, 0, -1);
pushCommand(drawVectorText, dateText, dateFontSize, x + width/2, y, 0, -1, datecol);
executeCommands();
}

View File

@ -0,0 +1,45 @@
(function(back) {
var FILE = "vectorclock.json";
// Load settings
var settings = Object.assign({
}, require('Storage').readJSON(FILE, true) || {});
function writeSettings() {
require('Storage').writeJSON(FILE, settings);
}
var colnames = ["white", "yellow", "green", "cyan", "red", "orange", "magenta", "black"];
var colvalues = [0xFFFF, 0xFFE0, 0x07E0, 0x07FF, 0xF800, 0xFD20, 0xF81F, 0x0000];
// Show the menu
E.showMenu({
"" : { "title" : "VectorClock colours" },
"< Back" : () => back(),
'Time': {
value: Math.max(0 | colvalues.indexOf(settings.timecol),0),
min: 0, max: colvalues.length-1,
format: v => colnames[v],
onchange: v => {
settings.timecol = colvalues[v];
writeSettings();
}
},
'Weekday': {
value: Math.max(0 | colvalues.indexOf(settings.dowcol),0),
min: 0, max: colvalues.length-1,
format: v => colnames[v],
onchange: v => {
settings.dowcol = colvalues[v];
writeSettings();
}
},
'Date': {
value: Math.max(0 | colvalues.indexOf(settings.datecol),0),
min: 0, max: colvalues.length-1,
format: v => colnames[v],
onchange: v => {
settings.datecol = colvalues[v];
writeSettings();
}
},
});
})

1
apps/wid_edit/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: new Widget Editor!

16
apps/wid_edit/README.md Normal file
View File

@ -0,0 +1,16 @@
# Widget Editor
This adds a setting menu which allows you to change the location of widgets.
## Settings
There is no app icon in the launcher; you can find the settings under
`Apps`->`Widget Editor`.
For every widget, you have these options:
* **Side**: On which side to draw the widget.
* **Sort Order**: Changes the order if several widgets use the same side.
## Creator
Richard de Boer <rigrig+banglejs@tubul.net>

24
apps/wid_edit/boot.js Normal file
View File

@ -0,0 +1,24 @@
Bangle.loadWidgets = function() {
global.WIDGETS={};
require("Storage").list(/\.wid\.js$/)
.forEach(w=>{
try { eval(require("Storage").read(w)); }
catch (e) { print(w, e); }
});
const s = require("Storage").readJSON("wid_edit.json", 1) || {},
c = s.custom || {};
for (const w in c){
if (!(w in WIDGETS)) continue; // widget no longer exists
// store defaults of customized values in _WIDGETS
global._WIDGETS=global._WIDGETS||{};
_WIDGETS[w] = {};
Object.keys(c[w]).forEach(k => _WIDGETS[w][k] = WIDGETS[w][k]);
Object.assign(WIDGETS[w], c[w]);
}
const W = WIDGETS;
WIDGETS = {};
Object.keys(W)
.sort()
.sort((a, b) => (0|W[b].sortorder)-(0|W[a].sortorder))
.forEach(k => WIDGETS[k] = W[k]);
}

BIN
apps/wid_edit/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

190
apps/wid_edit/settings.js Normal file
View File

@ -0,0 +1,190 @@
/**
* @param {function} back Use back() to return to settings menu
*/
(function(back) {
const names = {};
const settings = require("Storage").readJSON("wid_edit.json", 1) || {};
if (!('custom' in settings)) settings.custom = {};
global._WIDGETS = global._WIDGETS || {};
let cleanup = false;
for (const id in settings.custom) {
if (!(id in WIDGETS)) {
// widget which no longer exists
cleanup = true;
delete settings.custom[id];
}
}
if (cleanup) {
if (!Object.keys(settings.custom).length) delete settings.custom;
require("Storage").writeJSON("wid_edit.json", settings);
}
/**
* Sort & redraw all widgets
*/
function redrawWidgets() {
let W = WIDGETS;
global.WIDGETS = {};
Object.keys(W)
.sort()
.sort((a, b) => (0|W[b].sortorder)-(0|W[a].sortorder))
.forEach(k => {WIDGETS[k] = W[k]});
Bangle.drawWidgets();
}
/**
* Try to find app name for widget
* @param {string} widget WIDGETS key
* @return {string} widget name
*/
function name(widget) {
if (!(widget in names)) {
let infoFile = widget+".info";
// widget names don't always correspond to appid :-(
// so we try both with and without 'wid'-prefix
if (!require("Storage").list(new RegExp(`^${infoFile}$`)).length) {
infoFile = (widget.substr(0, 3)==="wid") ? infoFile.substr(3) : ("wid"+infoFile);
}
names[widget] = (require("Storage").readJSON(infoFile, 1) || {}).name || widget;
}
return names[widget];
}
function edit(id) {
let WIDGET = WIDGETS[id],
def = {area: WIDGET.area, sortorder: WIDGET.sortorder|0}; // default values
Object.assign(def, _WIDGETS[id]||{}); // defaults were saved in _WIDGETS
settings.custom = settings.custom||{};
let saved = settings.custom[id] || {},
area = saved.area || def.area,
sortorder = ("sortorder" in saved) ? saved.sortorder : def.sortorder;
/**
* Draw highlighted widget
*/
function highlight() {
if (WIDGET.width > 0) {
// draw widget, then draw a highlighted border on top
WIDGET.draw();
g.setColor(g.theme.fgH)
.drawRect(WIDGET.x, WIDGET.y, WIDGET.x+WIDGET.width-1, WIDGET.y+23);
} else {
// hidden widget: fake a width and provide our own draw()
const draw = WIDGET.draw, width = WIDGET.width;
WIDGET.width = 24;
WIDGET.draw = function() {
g.setColor(g.theme.bgH).setColor(g.theme.fgH)
.clearRect(this.x, this.y, this.x+23, this.y+23)
.drawRect(this.x, this.y, this.x+23, this.y+23)
.drawLine(this.x, this.y, this.x+23, this.y+23)
.drawLine(this.x, this.y+23, this.x+23, this.y);
};
// re-layout+draw all widgets with our placeholder in between
redrawWidgets();
// and restore original values
WIDGET.draw = draw;
WIDGET.width = width;
}
}
highlight();
/**
* Save widget and redraw with new settings
*/
function save() {
// we only save non-default values
saved = {};
if ((area!==def.area) || (sortorder!==def.sortorder)) {
if (area!==def.area) saved.area = area;
if (sortorder!==def.sortorder) saved.sortorder = sortorder;
settings.custom = settings.custom || {};
settings.custom[id] = saved;
} else if (settings.custom) {
delete settings.custom[id]
}
if (!Object.keys(settings.custom).length) delete settings.custom;
require("Storage").writeJSON("wid_edit.json", settings);
Object.assign(WIDGET, def, saved);
if (WIDGET.sortorder === undefined) delete WIDGET.sortorder; // default can be undefined, but don't put that in the widget
// if we assigned custom values, store defaults in _WIDGETS
let _W = {};
if (saved.area) _W.area = def.area;
if ('sortorder' in saved) _W.sortorder = def.sortorder;
if (Object.keys(_W).length) _WIDGETS[id] = _W;
else delete _WIDGETS[id];
// drawWidgets won't clear e.g. bottom bar if we just disabled the last bottom widget
redrawWidgets();
highlight();
m.draw();
}
const menu = {
"": {"title": name(id)},
/*LANG*/"< Back": () => {
redrawWidgets();
mainMenu();
},
/*LANG*/"Side": {
value: (area === 'tl'),
format: tl => tl ? /*LANG*/"Left" : /*LANG*/"Right",
onchange: tl => {
area = tl ? "tl" : "tr";
save();
}
},
/*LANG*/"Sort Order": {
value: sortorder,
onchange: o => {
sortorder = o;
save();
}
},
/*LANG*/"Reset": () => {
area = def.area;
sortorder = def.sortorder;
save();
mainMenu(); // changing multiple values made the rest of the menu wrong, so take the easy out
}
}
let m = E.showMenu(menu);
}
function mainMenu() {
let menu = {
"": {"title": /*LANG*/"Widgets"},
};
menu[/*LANG*/"< Back"] = ()=>{
if (!Object.keys(_WIDGETS).length) delete _WIDGETS; // no defaults to remember
back();
};
Object.keys(WIDGETS).forEach(id=>{
// mark customized widgets with asterisk
menu[name(id)+((id in _WIDGETS) ? " *" : "")] = () => edit(id);
});
if (Object.keys(_WIDGETS).length) { // only show reset if there is anything to reset
menu[/*LANG*/"Reset All"] = () => {
E.showPrompt(/*LANG*/"Reset all widgets?").then(confirm => {
if (confirm) {
delete settings.custom;
require("Storage").writeJSON("wid_edit.json", settings);
for(let id in _WIDGETS) {
Object.assign(WIDGETS[id], _WIDGETS[id]) // restore defaults
}
global._WIDGETS = {};
redrawWidgets();
}
mainMenu(); // reload with reset widgets
})
}
}
E.showMenu(menu);
}
mainMenu();
});

View File

@ -1,7 +0,0 @@
0.01: New Widget!
0.02: Version using connect back
0.03: Version using modified firmware
0.04: Works on both standard and modified firmware
0.05: Bug fixes w.r.t. reconnection
0.06: Update README - Release version
0.07: Respect Quiet Mode

View File

@ -1,70 +0,0 @@
## ANCS - iPhone notifications for Bangle.js
The ANCS widget allows you to answer or cancel iPhone incoming calls and also displays messages and notifications. It connects to the Apple Notification Center Service which is already on all iPhones, so you do not need to install any additional iPhone apps to use this widget.
## Firmware
The widget will run on the standard firmware, however, installation of a slightly modified version - the zip file is available from [this directory](https://github.com/jeffmer/JeffsBangleAppsDev/tree/master/apps/widancs) - will increase the performance of the app by an order of magnitude in terms of the time to connect or reconnect to the iPhone. In addition, the Bangle will stay connected to the iPhone over a greater separation distance than with the standard firmware.
![](widget_pic.jpg)
## Installation
After the widget is uploaded to the Bangle, it needs to be enabled in the Bangle Settings app:- `ANCS Widget` will appear in `APP/Widget settings`. There is also a menu in these settings to let you configure the categories of notifications that you want to be displayed. You must disconnect from the App Loader before enabling the widget.
## Compatible Apps
The widget will only run with a compatible app - for the reason for this see Issue 1 below. The apps that are compatible with the ANCS widget are:- **Multi Clock**, **Navigation Compass** and **GPS Navigation**. When you switch to an app that is not compatible, the ANCS phone icon will not appear.
## iPhone Pairing
Once enabled, the widget icon should be displayed coloured grey (its green in the photo). Go to the phone's Bluetooth settings menu and your Bangle should appear under Other devices. If this is the first time you have connected with the Bangle from your iPhone, it may be named Accessory. Click on the name and the iPhone should connect and start pairing. The widget icon will turn red and the iPhone will ask you to enter a pairing code - the traditional 123456. You have 10 seconds to enter this after which you will need to start pairing again. After that, the iPhone may also ask to allow the device access to ANCS. Once pairing is complete, the widget icon should go blue and eventually green. The range of colours is:
* **Grey** - not connected - advertising
* **Red** - connected - not paired.
* **Blue** - paired and connected - getting services
* **Yellow** - got Services.
* **Green** - waiting for new notifications.
After pairing the first time, the Bangle should connect automatically when the widget is running. Sometimes you may need you to click on the Bangle name in `Settings:Bluetooth:My devices` on the iPhone or disable and then enable Bluetooth to start connection. If you need to load other apps from the iPhone, it will be necessary to ask the iPhone to forget the pairing and you will also need to disable the widget in Settings and restart the Bangle by turning it off in Settings and then pressing BTN1 to restart. If you are loading apps from a different device, you simply need to turn off the iPhone bluetooth which will retain the pairing. You still need to disable the widget and restart the Bangle.
![](message_pic.jpg)
## Messages & Calls
Messages are displayed as shown above until BTN2 is pressed to dismiss it. I strongly advise disabling the BTN2 LCD wake function in the Settings App as otherwise when the screen times out and you press BTN2 to wake the LCD, the screen will turn on and the Message Alert will be dismissed!. Calls can be answered or dropped.
![](call_pic.jpg) ![](missed_pic.jpg)
## Issues
1. With GadgetBridge, the Android phone has a Central-Client role with the Bangle as Peripheral-Server. With the ANCS widget there is the fairly unusual situation in which the Bangle is Peripheral-Client to the iPhone's Central-Server role. Since Espruino does not deal explicitly with Bangle as Peripheral-Client an additional function has been added in the modified firmware: `var gatt = NRF.getGattforCentralServer(addr);`. This returns a bluetooth remote GATT server given the address of the iPhone which has just connected to the Bangle. With the standard firmware, the widget reconnects to the iPhone as a Client - however this has greatly degraded performance. See [Issue 1800.](https://github.com/espruino/Espruino/issues/1800) for more details.
2. When the Bangle switches apps, all state - including widget state - is lost unless explicitly stored. The consequence of this is that when the Bangle switches apps, the connection to iPhone has to be re-established to restore the remote GATT server and characteristics state. This is quite slow. To minimise reconnection, the widget needs to grab the screen from the running app to signal messages and calls. To allow this to work, the app needs to implement the `SCREENACCESS` interface. In essence, the widget only connects when running with compatible apps that implement this interface. An example implementation is:
```
var SCREENACCESS = {
withApp:true,
request:function(){
this.withApp=false;
stopdraw(); //clears redraw timers etc
clearWatch(); //clears button handlers
},
release:function(){
this.withApp=true;
startdraw(); //redraw app screen, restart timers etc
setButtons(); //install button event handlers
}
}
Bangle.on('lcdPower',function(on) {
if (!SCREENACCESS.withApp) return;
if (on) {
startdraw();
} else {
stopdraw();
}
});
```
## Support
Please report bugs etc. by raising an issue [here](https://github.com/jeffmer/JeffsBangleAppsDev).

View File

@ -1,266 +0,0 @@
(() => {
var s = require("Storage").readJSON("widancs.json",1)||{settings:{enabled:false, category:[1,2,4]}};
var ENABLED = s.settings.enabled;
var CATEGORY = s.settings.category;
function advert(){
NRF.setAdvertising([
0x02, //length
0x01, //flags
0x06, //
0x11, //length
0x15, //solicited Service UUID
0xD0,0x00,0x2D,0x12,0x1E,0x4B,
0x0F,0xA4,
0x99,0x4E,
0xCE,0xB5,
0x31,0xF4,0x05,0x79],{connectable:true,discoverable:true,interval:375});
}
var state = {
gatt:null,
ancs:null,
current:{cat:0,uid:0},
notqueue:[],
msgTO:undefined,
com:new Uint8Array([0,0,0,0,0,1,20,0,3,100,0]),
buf:new Uint8Array(132),
inp:0,
store:function(b){
var i = this.inp;
if (i+b.length<=132){
this.buf.set(b,i);
this.inp+=b.length;
}
},
gotmsg:function(){
var n = this.inp;
var vw = DataView(this.buf.buffer);
if (n<8) return null;
var tn=vw.getUint16(6,true);
if (n<(tn+8)) return null;
var mn=vw.getUint16(9+tn,true);
if (n<(mn+tn+11)) return null;
return {tlen:tn, mlen:mn};
}
};
//stop advertising when peripheral link disconnected
if (!NRF.getGattforCentralServer && ENABLED && typeof SCREENACCESS!='undefined')
NRF.on('disconnect',function(reason){
NRF.sleep();
});
if (ENABLED && typeof SCREENACCESS!='undefined')
NRF.on('connect',function(addr){
if(NRF.getGattforCentralServer)
do_bond(NRF.getGattforCentralServer(addr));
else
NRF.connect(addr).then(do_bond);
});
function do_bond(g) {
var tval, ival;
state.gatt = g;
function cleanup(){
drawIcon(0); //disconnect from iPhone
delete state.gatt;
delete state.ancs;
if(!NRF.getGattforCentralServer) NRF.disconnect();
setTimeout(()=>{NRF.wake();},500);
}
drawIcon(1); //connect from iPhone
state.gatt.device.on('gattserverdisconnected', function(reason) {
if (ival) clearInterval(ival);
if (tval) clearInterval(tval);
cleanup();
});
E.on("kill",function(){
state.gatt.disconnect().then(function(){NRF.sleep();});
});
NRF.setSecurity({passkey:"123456",mitm:1,display:1});
tval = setTimeout(function(){
if (ival) clearInterval(ival);
state.gatt.disconnect().then(cleanup);
},10000);
state.gatt.startBonding().then(function(){
ival = setInterval(function(){
var sec = state.gatt.getSecurityStatus();
if (!sec.connected) {clearInterval(ival); clearTimeout(tval); return;}
if (sec.connected && sec.encrypted){
clearInterval(ival);
clearTimeout(tval);
drawIcon(2); //bonded to iPhone
do_ancs();
return;
}
},1000);
}).catch(function(e){
Terminal.println("ERROR "+e);
});
}
function do_ancs() {
state.ancs = {primary:null, notify:null, control:null, data:null};
state.gatt.getPrimaryService("7905F431-B5CE-4E99-A40F-4B1E122D00D0").then(function(s) {
state.ancs.primary=s;
return s.getCharacteristic("9FBF120D-6301-42D9-8C58-25E699A21DBD");
}).then(function(c) {
state.ancs.notify=c;
return state.ancs.primary.getCharacteristic("69D1D8F3-45E1-49A8-9821-9BBDFDAAD9D9");
}).then(function(c) {
state.ancs.control=c;
return state.ancs.primary.getCharacteristic("22EAC6E9-24D6-4BB5-BE44-B36ACE7C7BFB");
}).then(function(c) {
state.ancs.data =c;
drawIcon(3);//got remote services
state.ancs.notify.on('characteristicvaluechanged', function(ev) {
getnotify(ev.target.value);
});
state.ancs.data.on('characteristicvaluechanged', function(e) {
state.store(e.target.value.buffer);
var inds = state.gotmsg();
if (inds) printmsg(state.buf,inds);
});
state.ancs.notify.startNotifications().then(function(){
state.ancs.data.startNotifications().then(function(){
drawIcon(4); //ready for messages
});
});
}).catch(function(e){
Terminal.println("ERROR "+e);
});
}
function wordwrap(s){
var txt = s.split("\n");
var MAXCHARS = 18;
for (var i = 0; i < txt.length; i++) {
txt[i] = txt[i].trim();
var l = txt[i];
if (l.length > MAXCHARS) {
var p = MAXCHARS;
while (p > MAXCHARS - 8 && !" \t-_".includes(l[p]))
p--;
if (p == MAXCHARS - 8) p = MAXCHARS;
txt[i] = l.substr(0, p);
txt.splice(i + 1, 0, l.substr(p));
}
}
return txt.join("\n");
}
var buzzing =false;
var screentimeout = undefined;
var inalert = false;
function release_screen(){
screentimeout= setTimeout(() => {
SCREENACCESS.release();
screentimeout = undefined;
inalert=false;
next_notify();
}, 500);
}
function printmsg(buf,inds){
function send_action(tf){
var bb = new Uint8Array(6);
var v = DataView(bb.buffer);
v.setUint8(0,2);
v.setUint32(1,state.current.uid,true);
v.setUint8(5,tf?0:1 );
state.ancs.control.writeValue(bb).then(release_screen);
}
if (state.msgTO) clearTimeout(state.msgTO);
var title="";
for (var i=8;i<8+inds.tlen; ++i) title+=String.fromCharCode(buf[i]);
var message = "";
for (var j=11+inds.tlen;j<11+inds.tlen+inds.mlen;++j) {
message+=String.fromCharCode(buf[j]);
}
message = wordwrap(message);
//we may already be displaying a prompt, so clear it
E.showPrompt();
if (screentimeout) clearTimeout(screentimeout);
if (!(require('Storage').readJSON('setting.json',1)||{}).quiet) {
Bangle.setLCDPower(true);
}
SCREENACCESS.request();
if (!buzzing && !(require('Storage').readJSON('setting.json',1)||{}).quiet){
buzzing=true;
Bangle.buzz(500).then(()=>{buzzing=false;});
}
if (state.current.cat!=1){
E.showAlert(message,title).then(send_action.bind(null,false));
} else {
E.showPrompt(message,{title:title,buttons:{"Accept":true,"Cancel":false}}).then(send_action);
}
}
var notifyTO;
function getnotify(d){
var eid = d.getUint8(0);
var ct = d.getUint8(2);
var id = d.getUint32(4,true);
if (eid>1) return;
if (notifyTO) clearTimeout(notifyTO);
if(!CATEGORY.includes(ct)) return;
var len = state.notqueue.length;
if (ct == 1) { // it's a call so pre-empt
if (inalert) {state.notqueue.push(state.current); inalert=false;}
state.notqueue.push({cat:ct, uid:id});
} else if (len<16)
state.notqueue[len] = {cat:ct, uid:id};
notifyTO = setTimeout(next_notify,1000);
}
function next_notify(){
if(state.notqueue.length==0 || inalert) return;
inalert=true;
state.current = state.notqueue.pop();
var v = DataView(state.com.buffer);
if (state.current.cat==6) v.setUint8(8,2); else v.setUint8(8,3);//get email title
v.setUint32(1,state.current.uid,true);
state.inp=0;
state.ancs.control.writeValue(state.com).then(function(){
state.msgTO=setTimeout(()=>{
inalert=false;
state.msgTO=undefined;
next_notify();
},1000);
});
}
var stage = 5;
//grey, pink, lightblue, yellow, green
function draw(){
var colors = new Uint16Array([0xc618,0xf818,0x3ff,0xffe0,0x07e0,0x0000]);
var img = E.toArrayBuffer(atob("GBgBAAAABAAADgAAHwAAPwAAf4AAP4AAP4AAP4AAHwAAH4AAD8AAB+AAA/AAAfgAAf3gAH/4AD/8AB/+AA/8AAf4AAHwAAAgAAAA"));
g.setColor(colors[stage]);
g.drawImage(img,this.x,this.y);
}
WIDGETS["ancs"] ={area:"tl", width:24,draw:draw};
function drawIcon(id){
stage = id;
WIDGETS["ancs"].draw();
}
if (ENABLED && typeof SCREENACCESS!='undefined') {
stage = 0;
NRF.setServices(undefined,{uart:false});
NRF.sleep();
NRF.wake();
advert();
}
})();

View File

@ -1,10 +0,0 @@
(function(){function t(a){function e(){k(0);delete b.gatt;delete b.ancs;NRF.getGattforCentralServer||NRF.disconnect();setTimeout(function(){NRF.wake()},500)}var d;b.gatt=a;k(1);b.gatt.device.on("gattserverdisconnected",function(a){d&&clearInterval(d);c&&clearInterval(c);e()});E.on("kill",function(){b.gatt.disconnect().then(function(){NRF.sleep()})});NRF.setSecurity({passkey:"123456",mitm:1,display:1});var c=setTimeout(function(){d&&clearInterval(d);b.gatt.disconnect().then(e)},1E4);b.gatt.startBonding().then(function(){d=
setInterval(function(){var a=b.gatt.getSecurityStatus();a.connected?a.connected&&a.encrypted&&(clearInterval(d),clearTimeout(c),k(2),v()):(clearInterval(d),clearTimeout(c))},1E3)})["catch"](function(a){Terminal.println("ERROR "+a)})}function v(){b.ancs={primary:null,notify:null,control:null,data:null};b.gatt.getPrimaryService("7905F431-B5CE-4E99-A40F-4B1E122D00D0").then(function(a){b.ancs.primary=a;return a.getCharacteristic("9FBF120D-6301-42D9-8C58-25E699A21DBD")}).then(function(a){b.ancs.notify=
a;return b.ancs.primary.getCharacteristic("69D1D8F3-45E1-49A8-9821-9BBDFDAAD9D9")}).then(function(a){b.ancs.control=a;return b.ancs.primary.getCharacteristic("22EAC6E9-24D6-4BB5-BE44-B36ACE7C7BFB")}).then(function(a){b.ancs.data=a;k(3);b.ancs.notify.on("characteristicvaluechanged",function(a){var e=a.target.value,c=e.getUint8(0);a=e.getUint8(2);e=e.getUint32(4,!0);1<c||(m&&clearTimeout(m),w.includes(a)&&(c=b.notqueue.length,1==a?(f&&(b.notqueue.push(b.current),f=!1),b.notqueue.push({cat:a,uid:e})):
16>c&&(b.notqueue[c]={cat:a,uid:e}),m=setTimeout(n,1E3)))});b.ancs.data.on("characteristicvaluechanged",function(a){b.store(a.target.value.buffer);(a=b.gotmsg())&&x(b.buf,a)});b.ancs.notify.startNotifications().then(function(){b.ancs.data.startNotifications().then(function(){k(4)})})})["catch"](function(a){Terminal.println("ERROR "+a)})}function y(a){a=a.split("\n");for(var b=0;b<a.length;b++){a[b]=a[b].trim();var d=a[b];if(18<d.length){for(var c=18;10<c&&!" \t-_".includes(d[c]);)c--;10==c&&(c=18);
a[b]=d.substr(0,c);a.splice(b+1,0,d.substr(c))}}return a.join("\n")}function z(){l=setTimeout(function(){SCREENACCESS.release();l=void 0;f=!1;n()},500)}function x(a,e){function d(a){var e=new Uint8Array(6),c=DataView(e.buffer);c.setUint8(0,2);c.setUint32(1,b.current.uid,!0);c.setUint8(5,a?0:1);b.ancs.control.writeValue(e).then(z)}b.msgTO&&clearTimeout(b.msgTO);for(var c="",h=8;h<8+e.tlen;++h)c+=String.fromCharCode(a[h]);h="";for(var f=11+e.tlen;f<11+e.tlen+e.mlen;++f)h+=String.fromCharCode(a[f]);
h=y(h);E.showPrompt();l&&clearTimeout(l);Bangle.setLCDPower(!0);SCREENACCESS.request();p||(p=!0,Bangle.buzz(500).then(function(){p=!1}));1!=b.current.cat?E.showAlert(h,c).then(d.bind(null,!1)):E.showPrompt(h,{title:c,buttons:{Accept:!0,Cancel:!1}}).then(d)}function n(){if(0!=b.notqueue.length&&!f){f=!0;b.current=b.notqueue.pop();var a=DataView(b.com.buffer);6==b.current.cat?a.setUint8(8,2):a.setUint8(8,3);a.setUint32(1,b.current.uid,!0);b.inp=0;b.ancs.control.writeValue(b.com).then(function(){b.msgTO=
setTimeout(function(){f=!1;b.msgTO=void 0;n()},1E3)})}}function k(a){q=a;WIDGETS.ancs.draw()}var u=require("Storage").readJSON("widancs.json",1)||{settings:{enabled:!1,category:[1,2,4]}},r=u.settings.enabled,w=u.settings.category,b={gatt:null,ancs:null,current:{cat:0,uid:0},notqueue:[],msgTO:void 0,com:new Uint8Array([0,0,0,0,0,1,20,0,3,100,0]),buf:new Uint8Array(132),inp:0,store:function(a){var b=this.inp;132>=b+a.length&&(this.buf.set(a,b),this.inp+=a.length)},gotmsg:function(){var a=this.inp,b=
DataView(this.buf.buffer);if(8>a)return null;var d=b.getUint16(6,!0);if(a<d+8)return null;b=b.getUint16(9+d,!0);return a<b+d+11?null:{tlen:d,mlen:b}}};if(!NRF.getGattforCentralServer&&r&&"undefined"!=typeof SCREENACCESS)NRF.on("disconnect",function(a){NRF.sleep()});if(r&&"undefined"!=typeof SCREENACCESS)NRF.on("connect",function(a){NRF.getGattforCentralServer?t(NRF.getGattforCentralServer(a)):NRF.connect(a).then(t)});var p=!1,l=void 0,f=!1,m,q=5;WIDGETS.ancs={area:"tl",width:24,draw:function(){var a=
new Uint16Array([50712,63512,1023,65504,2016,0]),b=E.toArrayBuffer(atob("GBgBAAAABAAADgAAHwAAPwAAf4AAP4AAP4AAP4AAHwAAH4AAD8AAB+AAA/AAAfgAAf3gAH/4AD/8AB/+AA/8AAf4AAHwAAAgAAAA"));g.setColor(a[q]);g.drawImage(b,this.x,this.y)}};r&&"undefined"!=typeof SCREENACCESS&&(q=0,NRF.setServices(void 0,{uart:!1}),NRF.sleep(),NRF.wake(),NRF.setAdvertising([2,1,6,17,21,208,0,45,18,30,75,15,164,153,78,206,181,49,244,5,121],{connectable:!0,discoverable:!0,interval:375}))})();

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

View File

@ -1,61 +0,0 @@
(function(back) {
const ANCSFILE = "widancs.json";
// initialize with default settings...
let s = {
'enabled': false,
'category':[1,2,4]
};
// ...and overwrite them with any saved values
// This way saved values are preserved if a new version adds more settings
const storage = require('Storage');
const d = storage.readJSON(ANCSFILE, 1) || {};
const saved = d.settings || {};
for (const key in saved) {
s[key] = saved[key];
}
function save() {
d.settings = s;
storage.write(ANCSFILE, d);
}
function setcategory(){
const names = ["Other","Call ","Missed Call","Voicemail","Messages ","Calendar","Email","News ","Fitness ","Busniness","Location ","Entertainment"];
function hascat(n){return s.category.includes(n);}
function setcat(n,v){
if (v)
s.category.push(n);
else
s.category = s.category.filter((v,i,a)=>{return v!=n;});
}
const menu = {
'': { 'title': 'Set Categories' }
};
for (var i=0; i<names.length;++i)
menu[names[i]]={
value:hascat(i),
format:v=>v?'Yes':'No',
onchange:setcat.bind(null,i)
};
menu['< Back'] = ()=>{save(); showMain();};
return E.showMenu(menu);
}
function showMain(){
return E.showMenu({
'Enable ANCS': {
value: s.enabled,
format: () => (s.enabled ? 'Yes' : 'No'),
onchange: () => {
s.enabled = !s.enabled;
save();
},
},
'Set Category':setcategory,
'< Back': back,
});
}
showMain();
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

View File

@ -1 +1,2 @@
0.01: Created
0.02: Set sort order to -10 so always display in right hand corner

View File

@ -2,3 +2,4 @@
0.02: Handle new firmwares with 'lock' event
0.03: Don't try to be fancy - just bail out on firmwares without a lock event
0.04: Set sortorder to -1 so that widget always takes up the furthest left position
0.05: Set sortorder to -10 so that others can take -1 etc

View File

@ -1,2 +1,3 @@
0.01: First release
0.02: Size widget after step count is reset
0.03: set sortorder to -1

View File

@ -1,2 +1,4 @@
0.01: First release
0.02: Fixed widget id to wibpb, Size widget after step count is reset
0.03: Fixed widget id in onStep() come on get it right!
0.04: set sortorder to -1

View File

@ -1,5 +1,5 @@
// on.step version
Bangle.on('step', function(s) { WIDGETS["bata"].draw(); });
Bangle.on('step', function(s) { WIDGETS["widpb"].draw(); });
Bangle.on('lcdPower', function(on) {
if (on) WIDGETS["widpb"].draw();
});

View File

@ -2,15 +2,20 @@
/* Scans for strings that may be in English in each app, and
outputs a list of strings that have been found.
Early work towards internationalisation.
See https://github.com/espruino/BangleApps/issues/136
See https://github.com/espruino/BangleApps/issues/1311
*/
var IGNORE_STRINGS = [
"5x5",
"5x9Numeric7Seg",
"Vector"
];
var BASEDIR = __dirname+"/../";
Espruino = require(BASEDIR+"core/lib/espruinotools.js");
var fs = require("fs");
var APPSDIR = BASEDIR+"apps/";
function ERROR(s) {
console.error("ERROR: "+s);
process.exit(1);
@ -18,6 +23,9 @@ function ERROR(s) {
function WARN(s) {
console.log("Warning: "+s);
}
function log(s) {
console.log(s);
}
var appsFile, apps;
try {
@ -39,31 +47,65 @@ function isNotString(s) {
if (s.endsWith(".json") || s.endsWith(".img")) return true; // a filename
if (s.endsWith("=")) return true; // probably base64
if (s.startsWith("BTN")) return true; // button name
if (IGNORE_STRINGS.includes(s)) return true; // one we know to ignore
return false;
}
var textStrings = [];
// A string that *could* be translated?
var untranslatedStrings = [];
// Strings that are marked with 'LANG'
var translatedStrings = [];
console.log("Scanning...");
console.log("Scanning apps...");
apps.forEach((app,appIdx) => {
var appDir = APPSDIR+app.id+"/";
app.storage.forEach((file) => {
if (!file.url || !file.name.endsWith(".js")) return;
var fileContents = fs.readFileSync(appDir+file.url).toString();
var lex = Espruino.Core.Utils.getLexer(fileContents);
var lastIdx = 0;
var tok = lex.next();
while (tok!==undefined) {
var previousString = fileContents.substring(lastIdx, tok.startIdx);
if (tok.type=="STRING") {
if (!isNotString(tok.value)) {
//console.log(tok.str);
if (!textStrings.includes(tok.value))
textStrings.push(tok.value);
if (previousString.includes("/*LANG*/")) { // translated!
if (!translatedStrings.includes(tok.value))
translatedStrings.push(tok.value);
} else { // untranslated - potential to translate?
if (!isNotString(tok.value)) {
if (!untranslatedStrings.includes(tok.value))
untranslatedStrings.push(tok.value);
}
}
}
lastIdx = tok.endIdx;
tok = lex.next();
}
});
});
console.log("Done");
textStrings.sort();
console.log(textStrings.join("\n"));
untranslatedStrings.sort();
translatedStrings.sort();
var report = "";
/* // too many! don't output these
log("Possible English Strings that could be translated");
log("=================================================================");
log("");
log("Add these to IGNORE_STRINGS if the don't make sense...");
log("");
log(untranslatedStrings.map(s=>JSON.stringify(s)).join(",\n"));*/
log("");
var languages = JSON.parse(fs.readFileSync(BASEDIR+"/lang/index.json").toString());
languages.forEach(language => {
console.log("Scanning "+language.code);
log(language.code);
log("==========");
var translations = JSON.parse(fs.readFileSync(BASEDIR+"/lang/"+language.url).toString());
translatedStrings.forEach(str => {
if (!translations.GLOBAL[str])
console.log(`Missing translation for ${JSON.stringify(str)}`);
});
log("");
});
console.log("Done.");

View File

@ -141,6 +141,11 @@
<input type="checkbox" id="settings-settime">
<i class="form-icon"></i> Always update time when we connect
</label>
<div class="form-group">
<select class="form-select form-inline" id="settings-lang" style="width: 10em">
<option value="">None (English)</option>
</select>&nbsp;&nbsp;<span>Translations (<a href="https://github.com/espruino/BangleApps/issues/1311" target="_blank">BETA - more info</a>)</span>
</div>
<button class="btn" id="defaultsettings">Default settings</button>
</div>
</div>

View File

@ -1,9 +1,9 @@
{
"//":"British English language translations - the default strings in apps are all english anyway, so no need to have translations for most things",
"GLOBAL": {
"//":"Translations that apply for all apps",
"//":"Translations that apply for all apps"
},
"alarm": {
"//":"App-specific overrides",
"//":"App-specific overrides"
}
}

View File

@ -164,6 +164,48 @@ window.addEventListener('load', (event) => {
showToast("App Install failed, "+err,"error");
});
});
// Load language list
httpGet("lang/index.json").then(languagesJSON=>{
var languages;
try {
languages = JSON.parse(languagesJSON);
} catch(e) {
console.error("lang/index.json Corrupted", e);
}
function reloadLanguage() {
LANGUAGE = undefined;
if (SETTINGS.language) {
var language = languages.find(l=>l.code==SETTINGS.language);
if (language) {
var langURL = "lang/"+language.url;
httpGet(langURL).then(languageJSON=>{
try {
LANGUAGE = JSON.parse(languageJSON);
console.log(`${langURL} loaded successfully`);
} catch(e) {
console.error(`${langURL} Corrupted`, e);
}
});
} else {
console.error(`Language code ${JSON.stringify(SETTINGS.language)} not found in lang/index.json`);
}
}
}
var selectLang = document.getElementById("settings-lang");
console.log(languages);
languages.forEach(lang => {
selectLang.innerHTML += `<option value="${lang.code}" ${SETTINGS.language==lang.code?"selected":""}>${lang.name} (${lang.code})</option>`;
});
selectLang.addEventListener("change",event=>{
SETTINGS.language = event.target.value;
reloadLanguage();
saveSettings();
});
reloadLanguage();
});
});
function onAppJSONLoaded() {