1
0
Fork 0
Andreas Rozek 2022-01-11 16:25:27 +01:00
commit 94c41d4dbc
113 changed files with 3764 additions and 805 deletions

182
apps.json
View File

@ -2,7 +2,7 @@
{
"id": "fwupdate",
"name": "Firmware Update",
"version": "0.02",
"version": "0.03",
"description": "[BETA] Uploads new Espruino firmwares to Bangle.js 2. For now, please use the instructions under https://www.espruino.com/Bangle.js2#firmware-updates",
"icon": "app.png",
"type": "RAM",
@ -16,7 +16,7 @@
{
"id": "boot",
"name": "Bootloader",
"version": "0.40",
"version": "0.41",
"description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings",
"icon": "bootloader.png",
"type": "bootloader",
@ -218,7 +218,7 @@
{
"id": "locale",
"name": "Languages",
"version": "0.14",
"version": "0.15",
"description": "Translations for different countries",
"icon": "locale.png",
"type": "locale",
@ -768,7 +768,7 @@
"id": "recorder",
"name": "Recorder (BETA)",
"shortName": "Recorder",
"version": "0.05",
"version": "0.06",
"description": "Record GPS position, heart rate and more in the background, then download to your PC.",
"icon": "app.png",
"tags": "tool,outdoors,gps,widget",
@ -936,7 +936,7 @@
"id": "widbatpc",
"name": "Battery Level Widget (with percentage)",
"shortName": "Battery Widget",
"version": "0.15",
"version": "0.16",
"description": "Show the current battery level and charging status in the top right of the clock, with charge percentage",
"icon": "widget.png",
"type": "widget",
@ -1040,16 +1040,19 @@
"id": "bthrm",
"name": "Bluetooth Heart Rate Monitor",
"shortName": "BT HRM",
"version": "0.01",
"version": "0.02",
"description": "Overrides Bangle.js's build in heart rate monitor with an external Bluetooth one.",
"icon": "app.png",
"type": "boot",
"type": "app",
"tags": "health,bluetooth",
"supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name":"bthrm.app.js","url":"bthrm.js"},
{"name":"bthrm.recorder.js","url":"recorder.js"},
{"name":"bthrm.boot.js","url":"boot.js"},
{"name":"bthrm.img","url":"app-icon.js","evaluate":true}
{"name":"bthrm.img","url":"app-icon.js","evaluate":true},
{"name":"bthrm.settings.js","url":"settings.js"}
]
},
{
@ -1348,6 +1351,22 @@
{"name":"pparrot.img","url":"party-parrot-icon.js","evaluate":true}
]
},
{
"id": "hralarm",
"name": "Heart rate alarm",
"shortName":"HR Alarm",
"version":"0.01",
"description": "This invisible widget vibrates whenever the heart rate gets close to the upper limit or goes over or under the configured limits",
"icon": "widget.png",
"type": "widget",
"tags": "widget",
"supports" : ["BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name":"hralarm.wid.js","url":"widget.js"},
{"name":"hralarm.settings.js","url":"settings.js"}
]
},
{
"id": "hrings",
"name": "Hypno Rings",
@ -1501,7 +1520,7 @@
{
"id": "gpsinfo",
"name": "GPS Info",
"version": "0.06",
"version": "0.09",
"description": "An application that displays information about altitude, lat/lon, satellites and time",
"icon": "gps-info.png",
"type": "app",
@ -1590,7 +1609,7 @@
{
"id": "widpedom",
"name": "Pedometer widget",
"version": "0.20",
"version": "0.22",
"description": "Daily pedometer widget",
"icon": "widget.png",
"type": "widget",
@ -1714,17 +1733,18 @@
{
"id": "wohrm",
"name": "Workout HRM",
"version": "0.08",
"version": "0.09",
"description": "Workout heart rate monitor notifies you with a buzz if your heart rate goes above or below the set limits.",
"icon": "app.png",
"type": "app",
"tags": "hrm,workout",
"supports": ["BANGLEJS"],
"supports": ["BANGLEJS", "BANGLEJS2"],
"readme": "README.md",
"allow_emulator": true,
"screenshots": [{"url":"bangle1-workout-HRM-screenshot.png"}],
"storage": [
{"name":"wohrm.app.js","url":"app.js"},
{"name":"wohrm.settings.js","url":"settings.js"},
{"name":"wohrm.img","url":"app-icon.js","evaluate":true}
]
},
@ -1890,13 +1910,15 @@
{
"id": "widhwt",
"name": "Hand Wash Timer",
"version": "0.01",
"description": "Swipe your wrist over the watch face to start your personal Bangle.js hand wash timer for 35 sec. Start washing after the short buzz and stop after the long buzz.",
"version": "0.02",
"description": "On Bangle.js 1 swipe your wrist over the watch face to start your personal Bangle.js 1 hand wash timer. On Bangle.js2 the Pattern Launcher is recommended to start the timer. Start washing after the short buzz and stop after the long buzz 35sec. later.",
"icon": "widget.png",
"type": "widget",
"tags": "widget,tool",
"supports": ["BANGLEJS"],
"allow_emulator": true,
"supports": ["BANGLEJS", "BANGLEJS2"],
"storage": [
{"name":"widhwt.app.js","url":"app.js"},
{"name":"widhwt.wid.js","url":"widget.js"}
]
},
@ -3791,7 +3813,7 @@
{
"id": "simplest",
"name": "Simplest Clock",
"version": "0.03",
"version": "0.05",
"description": "The simplest working clock, acts as a tutorial piece",
"icon": "simplest.png",
"screenshots": [{"url":"screenshot_simplest.png"}],
@ -4212,7 +4234,7 @@
"id": "pastel",
"name": "Pastel Clock",
"shortName": "Pastel",
"version": "0.10",
"version": "0.11",
"description": "A Configurable clock with custom fonts, background and weather display. Has a cyclic information line that includes, day, date, battery, sunrise and sunset times",
"icon": "pastel.png",
"dependencies": {"mylocation":"app", "widpedom":"app","weather":"app"},
@ -4239,8 +4261,9 @@
{
"id": "antonclk",
"name": "Anton Clock",
"version": "0.03",
"description": "A simple clock using the bold Anton font.",
"version": "0.05",
"description": "A clock using the bold Anton font, optionally showing seconds and date in ISO-8601 format.",
"readme":"README.md",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
"type": "clock",
@ -4249,8 +4272,10 @@
"allow_emulator": true,
"storage": [
{"name":"antonclk.app.js","url":"app.js"},
{"name":"antonclk.settings.js","url":"settings.js"},
{"name":"antonclk.img","url":"app-icon.js","evaluate":true}
]
],
"data": [{"name":"antonclk.json"}]
},
{
"id": "waveclk",
@ -4416,7 +4441,7 @@
"name": "Q Alarm and Timer",
"shortName": "Q Alarm",
"icon": "app.png",
"version": "0.03",
"version": "0.04",
"description": "Alarm and timer app with days of week and 'hard' option.",
"tags": "tool,alarm,widget",
"supports": ["BANGLEJS", "BANGLEJS2"],
@ -4474,7 +4499,7 @@
"name": "A Battery Widget (with percentage)",
"shortName":"A Battery Widget",
"icon": "widget.png",
"version":"1.02",
"version":"1.03",
"type": "widget",
"supports": ["BANGLEJS", "BANGLEJS2"],
"readme": "README.md",
@ -4489,7 +4514,7 @@
"name": "LCARS Clock",
"shortName":"LCARS",
"icon": "lcars.png",
"version":"0.09",
"version":"0.11",
"readme": "README.md",
"supports": ["BANGLEJS2"],
"description": "Library Computer Access Retrieval System (LCARS) clock.",
@ -4799,7 +4824,7 @@
{
"id": "menuwheel",
"name": "Wheel Menus",
"version": "0.01",
"version": "0.02",
"description": "Replace Bangle.js 2's menus with a version that contains variable-size text and a back button",
"readme": "README.md",
"icon": "icon.png",
@ -4984,7 +5009,7 @@
{ "id": "pooqround",
"name": "pooq Round watch face",
"shortName":"pooq Round",
"version":"0.01",
"version":"0.02",
"description": "A 24 hour analogue watchface with high legibility and a novel style.",
"icon": "app.png",
"type": "clock",
@ -5037,7 +5062,7 @@
{
"id": "lapcounter",
"name": "Lap Counter",
"version": "0.01",
"version": "0.02",
"description": "Click button to count laps. Shows count and total time snapshot (like a stopwatch, but laid back).",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
@ -5072,10 +5097,10 @@
{ "id": "circlesclock",
"name": "Circles clock",
"shortName":"Circles clock",
"version":"0.03",
"version":"0.05",
"description": "A clock with circles for different data at the bottom in a probably familiar style",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
"screenshots": [{"url":"screenshot-dark.png"}, {"url":"screenshot-light.png"}],
"dependencies": {"widpedom":"app"},
"type": "clock",
"tags": "clock",
@ -5124,6 +5149,43 @@
]
},
{
"id": "ftclock",
"name": "Four Twenty Clock",
"version": "0.01",
"description": "A clock that tells when and where it's going to be 4:20 next",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}, {"url":"screenshot1.png"}],
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS","BANGLEJS2"],
"allow_emulator": true,
"readme": "README.md",
"storage": [
{"name":"ftclock.app.js","url":"app.js"},
{"name":"fourTwenty","url":"fourTwenty.js"},
{"name":"fourTwentyTz","url":"fourTwentyTz.js"},
{"name":"ftclock.img","url":"app-icon.js","evaluate":true}
]
},
{
"id": "mmind",
"name": "Classic Mind Game",
"shortName":"Master Mind",
"icon": "mmind.png",
"version":"0.01",
"description": "This is the classic game for masterminds",
"screenshots": [{"url":"screenshot_mmind.png"}],
"type": "app",
"tags": "game",
"readme":"README.md",
"supports": ["BANGLEJS2"],
"allow_emulator": true,
"storage": [
{"name":"mmind.app.js","url":"mmind.app.js"},
{"name":"mmind.img","url":"mmind.icon.js","evaluate":true}
]
},
{
"id": "presentor",
"name": "Presentor",
"version": "3.0",
@ -5162,15 +5224,16 @@
{
"id": "promenu",
"name": "Pro Menu",
"version": "0.01",
"description": "Replace Bangle.js 1's built in menu function.",
"version": "0.02",
"description": "Replace the built in menu function. Supports Bangle.js 1 and Bangle.js 2.",
"icon": "icon.png",
"type": "boot",
"tags": "system",
"supports": ["BANGLEJS"],
"supports": ["BANGLEJS","BANGLEJS2"],
"screenshots": [{"url":"pro-menu-screenshot.png"}],
"storage": [
{"name":"promenu.boot.js","url":"boot.js"},
{"name":"promenu.boot.js","url":"boot.js","supports": ["BANGLEJS"]},
{"name":"promenu.boot.js","url":"bootb2.js","supports": ["BANGLEJS2"]},
{"name":"promenu.img","url":"promenuIcon.js","evaluate":true}
]
},
@ -5428,21 +5491,52 @@
],
"data": [{"name":"puzzle15.json"}]
},
{ "id": "configurable_clock",
"name": "Configurable Analog Clock",
"shortName":"Configurable Clock",
"version":"0.01",
"description": "an analog clock with several kinds of faces, hands and colors to choose from",
"icon": "app-icon.png",
{
"id": "flipper",
"name": "Flipper",
"version": "0.01",
"description": "Switch between dark and light theme and vice versa, combine with pattern launcher and swipe to flip.",
"readme":"README.md",
"screenshots": [{"url":"flipper.png"}],
"icon": "flipper.png",
"type": "app",
"tags": "game",
"supports": ["BANGLEJS2"],
"allow_emulator": true,
"storage": [
{"name":"flipper.app.js","url":"flipper.app.js"},
{"name":"flipper.img","url":"flipper.icon.js","evaluate":true}
]
},
{ "id": "ruuviwatch",
"name": "Ruuvi Watch",
"shortName":"Ruuvi Watch",
"icon": "ruuviwatch.png",
"version":"1.01",
"description": "Keep an eye on RuuviTag devices (https://ruuvi.com). Only shows RuuviTags using the v5 format.",
"readme":"README.md",
"tags": "bluetooth",
"supports": ["BANGLEJS"],
"storage": [
{"name":"ruuviwatch.app.js","url":"ruuviwatch.app.js"},
{"name":"ruuviwatch.img","url":"ruuviwatch.app-icon.js","evaluate":true}
]
},
{
"id": "limelight",
"name": "Limelight",
"version": "0.01",
"description": "Simple analogue clock (with configurable fonts) based on the work of @Andreas_Rozek (Simple_Clock)",
"icon": "limelight.png",
"readme":"README.md",
"screenshots": [{"url":"screenshot_limelight.png"}],
"type": "clock",
"tags": "clock",
"supports" : ["BANGLEJS2"],
"allow_emulator": true,
"screenshots": [{"url":"app-screenshot.png"}],
"readme": "README.md",
"supports": ["BANGLEJS","BANGLEJS2"],
"storage": [
{"name":"configurable_clock.app.js","url":"app.js"},
{"name":"configurable_clock.img","url":"app-icon.js","evaluate":true}
{"name":"limelight.app.js","url":"limelight.app.js"},
{"name":"limelight.settings.js","url":"limelight.settings.js"},
{"name":"limelight.img","url":"limelight.icon.js","evaluate":true}
]
}
]

View File

@ -1,3 +1,7 @@
0.01: New App!
0.02: Load widgets after setUI so widclk knows when to hide
0.03: Clock now shows day of week under date.
0.04: Clock can optionally show seconds, date optionally in ISO-8601 format, weekdays and uppercase configurable, too.
0.05: Clock can optionally show ISO-8601 calendar weeknumber (default: Off)
when weekday name "Off": week #:<num>
when weekday name "On": weekday name is cut at 6th position and .#<week num> is added

79
apps/antonclk/README.md Normal file
View File

@ -0,0 +1,79 @@
# Anton Clock - Large font digital watch with seconds and date
Anton clock uses the "Anton" bold font to show the time in a clear, easily readable manner. On the Bangle.js 2, the time can be read easily even if the screen is locked and unlit.
## Features
The basic time representation only shows hours and minutes of the current time. However, Anton clock can show additional information:
* Seconds can be shown, either always or only if the screen is unlocked.
* To help easy recognition, the seconds can be coloured in blue on the Bangle.js 2.
* Date can be shown in three different formats:
* ISO-8601: 2021-12-19
* short local format: 19/12/2021, 19.12.2021
* long local format: DEC 19 2021
* Weekday can be shown (on seconds screen only instead of year)
## Usage
Install Anton clock through the Bangle.js app loader.
Configure it through the default Bangle.js configuration mechanism
(Settings app, "Apps" menu, "Anton clock" submenu).
If you like it, make it your default watch face
(Settings app, "System" menu, "Clock" submenu, select "Anton clock").
## Configuration
Anton clock is configured by the standard settings mechanism of Bangle.js's operating system:
Open the "Settings" app, then the "Apps" submenu and below it the "Anton clock" menu.
You configure Anton clock through several "on/off" switches in two menus.
### The main menu
The main menu contains several settings covering Anton clock in general.
* **Seconds...** - Opens the submenu for configuring the presentation of the current time's seconds.
* **Date** - Format of the date representation. Possible values are
* **Long** - "Long" date format in the current locale. Usually with the month as name, not number.
* **Short** - "Short" date format in the current locale. Usually with the month as number.
* **ISO8601** - Show the date in ISO-8601 format (YYYY-MM-DD), irrespective of the current locale.
* **Show Weekday** - Weekday is shown in the time presentation without seconds.
Weekday name depends on the current locale.
If seconds are shown, the weekday is never shown as there is not enough space on the watch face.
* **Show Weeknumber** - Week-number (ISO-8601) is shown. (default: Off)
If "Show Weekday" is "Off" the week-number is displayed as "week #:<num>".
If "Show Weekday" is "On" the weekday name is cut at 6th position and suffixed with ".#<week num>".
If seconds are shown, the week number is never shown as there is not enough space on the watch face.
* **Vector font** - Use the built-in vector font for dates and weekday.
This can improve readability.
Otherwise, a scaled version of the built-in 6x8 pixels font is used.
### The "Seconds" submenu
The "Seconds" submenu configures how (and if) seconds are shown on the "Anton" watch face.
* **Show** - Configure when the seconds should be shown at all:
* **Never** - Seconds are never shown.
In this case, hour and minute are a bit more centered on the screen and the clock will always only update every minute.
This saves battery power.
* **Unlocked** - Seconds are shown if the display is unlocked.
On locked displays, only hour, minutes, date and optionally the weekday are shown.
_This option is highly recommended on the Bangle.js 2!_
* **Always** - Seconds are _always_ shown, irrespective of the display's unlock state.
_Enabling this option increases power consumption as the watch face will update once per second instead of once per minute._
* **With ":"** - If enabled, a colon ":" is prepended to the seconds.
This resembles the usual time representation "hh:mm:ss", even though the seconds are printed on a separate line.
* **Color** - If enabled, seconds are shown in blue instead of black.
If the date is shown on the seconds screen, it is colored read instead of black.
This make the visual orientation much easier on the watch face.
* **Date** - It is possible to show the date together with the seconds:
* **No** - Date is _not_ shown in the seconds screen.
In this case, the seconds are centered below hour and minute.
* **Year** - Date is shown with day, month, and year. If "Date" in the main settings is configured to _ISO8601_, this is used here, too. Otherwise, the short local format is used.
* **Weekday** - Date is shown with day, month, and weekday.
The date is coloured in red if the "Coloured" option is chosen.
## Compatibility
Anton clock makes use of core Bangle.js 2 features (coloured display, display lock state). It also runs on the Bangle.js 1 but these features are not available there due to hardware restrictions.

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 759 B

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 696 B

After

Width:  |  Height:  |  Size: 1.6 KiB

107
apps/antonclk/settings.js Normal file
View File

@ -0,0 +1,107 @@
// Settings menu for the enhanced Anton clock
(function(back) {
var FILE = "antonclk.json";
// Load settings
var settings = Object.assign({
secondsOnUnlock: false,
}, require('Storage').readJSON(FILE, true) || {});
function writeSettings() {
require('Storage').writeJSON(FILE, settings);
}
// Helper method which uses int-based menu item for set of string values
function stringItems(startvalue, writer, values) {
return {
value: (startvalue === undefined ? 0 : values.indexOf(startvalue)),
format: v => values[v],
min: 0,
max: values.length - 1,
wrap: true,
step: 1,
onchange: v => {
writer(values[v]);
writeSettings();
}
};
}
// Helper method which breaks string set settings down to local settings object
function stringInSettings(name, values) {
return stringItems(settings[name], v => settings[name] = v, values);
}
var mainmenu = {
"": {
"title": "Anton clock"
},
"< Back": () => back(),
"Seconds...": () => E.showMenu(secmenu),
"Date": stringInSettings("dateOnMain", ["Short", "Long", "ISO8601"]),
"Show Weekday": {
value: (settings.weekDay !== undefined ? settings.weekDay : true),
format: v => v ? "On" : "Off",
onchange: v => {
settings.weekDay = v;
writeSettings();
}
},
"Show Weeknumber": {
value: (settings.weekNum !== undefined ? settings.weekNum : true),
format: v => v ? "On" : "Off",
onchange: v => {
settings.weekNum = v;
writeSettings();
}
},
"Uppercase": {
value: (settings.upperCase !== undefined ? settings.upperCase : false),
format: v => v ? "On" : "Off",
onchange: v => {
settings.upperCase = v;
writeSettings();
}
},
"Vector font": {
value: (settings.vectorFont !== undefined ? settings.vectorFont : false),
format: v => v ? "On" : "Off",
onchange: v => {
settings.vectorFont = v;
writeSettings();
}
},
};
// Submenu
var secmenu = {
"": {
"title": "Show seconds..."
},
"< Back": () => E.showMenu(mainmenu),
"Show": stringInSettings("secondsMode", ["Never", "Unlocked", "Always"]),
"With \":\"": {
value: (settings.secondsWithColon !== undefined ? settings.secondsWithColon : false),
format: v => v ? "On" : "Off",
onchange: v => {
settings.secondsWithColon = v;
writeSettings();
}
},
"Color": {
value: (settings.secondsColoured !== undefined ? settings.secondsColoured : false),
format: v => v ? "On" : "Off",
onchange: v => {
settings.secondsColoured = v;
writeSettings();
}
},
"Date": stringInSettings("dateOnSecs", ["No", "Year", "Weekday"])
};
// Actually display the menu
E.showMenu(mainmenu);
});
// end of file

View File

@ -44,3 +44,4 @@
0.38: Option to log to file if settings.log==2
0.39: Fix passkey support (fix https://github.com/espruino/Espruino/issues/2035)
0.40: Bootloader now rebuilds for new firmware versions
0.41: Add Keyboard and Mouse Bluetooth HID option

View File

@ -18,6 +18,7 @@ boot += `var bleServices = {}, bleServiceOptions = { uart : true};\n`;
if (s.ble!==false) {
if (s.HID) { // Human interface device
if (s.HID=="joy") boot += `Bangle.HID = E.toUint8Array(atob("BQEJBKEBCQGhAAUJGQEpBRUAJQGVBXUBgQKVA3UBgQMFAQkwCTEVgSV/dQiVAoECwMA="));`;
else if (s.HID=="com") boot += `Bangle.HID = E.toUint8Array(atob("BQEJAqEBhQEJAaEABQkZASkFFQAlAZUFdQGBApUBdQOBAwUBCTAJMQk4FYElf3UIlQOBBgUMCjgCFYElf3UIlQGBBsDABQEJBqEBhQIFBxngKecVACUBdQGVCIECdQiVAYEBGQApcxUAJXOVBXUIgQDA"));`
else if (s.HID=="kb") boot += `Bangle.HID = E.toUint8Array(atob("BQEJBqEBBQcZ4CnnFQAlAXUBlQiBApUBdQiBAZUFdQEFCBkBKQWRApUBdQORAZUGdQgVACVzBQcZAClzgQAJBRUAJv8AdQiVArECwA=="));`
else /*kbmedia*/boot += `Bangle.HID = E.toUint8Array(atob("BQEJBqEBhQIFBxngKecVACUBdQGVCIEClQF1CIEBlQV1AQUIGQEpBZEClQF1A5EBlQZ1CBUAJXMFBxkAKXOBAAkFFQAm/wB1CJUCsQLABQwJAaEBhQEVACUBdQGVAQm1gQIJtoECCbeBAgm4gQIJzYECCeKBAgnpgQIJ6oECwA=="));`;
boot += `bleServiceOptions.hid=Bangle.HID;\n`;

View File

@ -1 +1,4 @@
0.01: New App!
0.02: Make overriding the HRM event optional
Emit BTHRM event for external sensor
Add recorder app plugin

View File

@ -2,24 +2,43 @@
var log = function() {};//print
var gatt;
var status;
Bangle.isHRMOn = function() {
var origIsHRMOn = Bangle.isHRMOn;
Bangle.isBTHRMOn = function(){
return (status=="searching" || status=="connecting") || (gatt!==undefined);
}
Bangle.setHRMPower = function(isOn, app) {
Bangle.isHRMOn = function() {
var settings = require('Storage').readJSON("bthrm.json", true) || {};
print(settings);
if (settings.enabled && !settings.replace){
return origIsHRMOn();
} else if (settings.enabled && settings.replace){
return Bangle.isBTHRMOn();
}
return origIsHRMOn() || Bangle.isBTHRMOn();
}
Bangle.setBTHRMPower = function(isOn, app) {
var settings = require('Storage').readJSON("bthrm.json", true) || {};
// Do app power handling
if (!app) app="?";
log("setHRMPower ->", isOn, app);
log("setBTHRMPower ->", isOn, app);
if (Bangle._PWR===undefined) Bangle._PWR={};
if (Bangle._PWR.HRM===undefined) Bangle._PWR.HRM=[];
if (isOn && !Bangle._PWR.HRM.includes(app)) Bangle._PWR.HRM.push(app);
if (!isOn && Bangle._PWR.HRM.includes(app)) Bangle._PWR.HRM = Bangle._PWR.HRM.filter(a=>a!=app);
isOn = Bangle._PWR.HRM.length;
if (Bangle._PWR.BTHRM===undefined) Bangle._PWR.BTHRM=[];
if (isOn && !Bangle._PWR.BTHRM.includes(app)) Bangle._PWR.BTHRM.push(app);
if (!isOn && Bangle._PWR.BTHRM.includes(app)) Bangle._PWR.BTHRM = Bangle._PWR.BTHRM.filter(a=>a!=app);
isOn = Bangle._PWR.BTHRM.length;
// so now we know if we're really on
if (isOn) {
log("setHRMPower on", app);
if (!Bangle.isHRMOn()) {
log("HRM not already on");
log("setBTHRMPower on", app);
if (!Bangle.isBTHRMOn()) {
log("BTHRM not already on");
status = "searching";
NRF.requestDevice({ filters: [{ services: ['180D'] }] }).then(function(device) {
log("Found device "+device.id);
@ -49,7 +68,11 @@
if (flags&16) {
var interval = dv.getUint16(idx,1); // in milliseconds
}*/
Bangle.emit('HRM',{
var eventName = settings.replace ? "HRM" : "BTHRM";
Bangle.emit(eventName, {
bpm:bpm,
confidence:100
});
@ -65,15 +88,27 @@
});
}
} else { // not on
log("setHRMPower off", app);
log("setBTHRMPower off", app);
if (gatt) {
log("HRM connected - disconnecting");
log("BTHRM connected - disconnecting");
status = undefined;
try {gatt.disconnect();}catch(e) {
log("HRM disconnect error", e);
log("BTHRM disconnect error", e);
}
gatt = undefined;
}
}
};
var origSetHRMPower = Bangle.setHRMPower;
Bangle.setHRMPower = function(isOn, app) {
var settings = require('Storage').readJSON("bthrm.json", true) || {};
if (settings.enabled || !isOn){
Bangle.setBTHRMPower(isOn, app);
}
if (settings.enabled && !settings.replace || !isOn){
origSetHRMPower(isOn, app);
}
}
})();

61
apps/bthrm/bthrm.js Normal file
View File

@ -0,0 +1,61 @@
var btm = g.getHeight()-1;
var eventInt = null;
var eventBt = null;
var counterInt = 0;
var counterBt = 0;
function draw(y, event, type, counter) {
var px = g.getWidth()/2;
g.reset();
g.setFontAlign(0,0);
g.clearRect(0,y,g.getWidth(),y+80);
if (type == null || event == null || counter == 0) return;
var str = event.bpm + "";
g.setFontVector(40).drawString(str,px,y+20);
str = "Confidence: " + event.confidence;
g.setFontVector(12).drawString(str,px,y+50);
str = "Event: " + type;
g.setFontVector(12).drawString(str,px,y+60);
}
function onBtHrm(e) {
print("Event for BT " + JSON.stringify(e));
counterBt += 5;
eventBt = e;
}
function onHrm(e) {
print("Event for Int " + JSON.stringify(e));
counterInt += 5;
eventInt = e;
}
Bangle.on('BTHRM', onBtHrm);
Bangle.on('HRM', onHrm);
Bangle.setHRMPower(1,'bthrm')
Bangle.setBTHRMPower(1,'bthrm')
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
g.reset().setFont("6x8",2).setFontAlign(0,0);
g.drawString("Please wait...",g.getWidth()/2,g.getHeight()/2 - 16);
function drawInt(){
counterInt--;
if (counterInt < 0) counterInt = 0;
if (counterInt > 5) counterInt = 5;
draw(24, eventInt, "HRM", counterInt);
}
function drawBt(){
counterBt--;
if (counterBt < 0) counterBt = 0;
if (counterBt > 5) counterBt = 5;
draw(100, eventBt, "BTHRM", counterBt);
}
var interval = setInterval(drawInt, 1000);
var interval = setInterval(drawBt, 1000);

27
apps/bthrm/recorder.js Normal file
View File

@ -0,0 +1,27 @@
(function(recorders) {
recorders.bthrm = function() {
var bpm = 0;
function onHRM(h) {
bpm = h.bpm;
}
return {
name : "BTHR",
fields : ["BT Heartrate"],
getValues : () => {
result = [bpm];
bpm = 0;
return result;
},
start : () => {
Bangle.on('BTHRM', onHRM);
Bangle.setBTHRMPower(1,"recorder");
},
stop : () => {
Bangle.removeListener('BTHRM', onHRM);
Bangle.setBTHRMPower(0,"recorder");
},
draw : (x,y) => g.setColor(Bangle.isBTHRMOn()?"#00f":"#88f").drawImage(atob("DAwBAAAAMMeef+f+f+P8H4DwBgAA"),x,y)
};
}
})

33
apps/bthrm/settings.js Normal file
View File

@ -0,0 +1,33 @@
(function(back) {
var FILE = "bthrm.json";
var settings = Object.assign({
enabled: true,
replace: true,
}, require('Storage').readJSON(FILE, true) || {});
function writeSettings() {
require('Storage').writeJSON(FILE, settings);
}
E.showMenu({
'': { 'title': 'Bluetooth HRM' },
'< Back': back,
'Use BT HRM': {
value: !!settings.enabled,
format: v => settings.enabled ? "On" : "Off",
onchange: v => {
settings.enabled = v;
writeSettings();
}
},
'Use HRM event': {
value: !!settings.replace,
format: v => settings.replace ? "On" : "Off",
onchange: v => {
settings.replace = v;
writeSettings();
}
}
});
})

View File

@ -1,3 +1,9 @@
0.01: New clock
0.02: Fix icon & add battery warn functionality
0.03: Theming support & minor fixes
0.04: Make configurable what to show in each circle
Add step distance and weather
Allow switching visibility of widgets
Make circles and text slightly bigger
0.05: Show correct percentage values in circles
Show humidity as weather circle data

View File

@ -2,19 +2,25 @@
A clock with circles for different data at the bottom in a probably familiar style
It shows besides time, date and day of week the following information:
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))
* Heart rate (when screen is on and unlocked)
* Battery (including charging and battery low)
* Steps distance (depending on steps)
* 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
* Temperature inside circle
* Condition as icon below circle
## Screenshot
## Screenshots
![Screenshot dark theme](screenshot-dark.png)
![Screenshot light theme](screenshot-light.png)
![Screenshot](screenshot.png)
## TODO
* Show weather information
* Configure which information to show in each circle
* Configure visibility of widgets
# 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,19 +1,37 @@
const locale = require("locale");
const heatshrink = require("heatshrink");
const storage = require("Storage");
const shoesIcon = heatshrink.decompress(atob("h0OwYJGgmAAgUBkgECgVJB4cSoAUDyEBkARDpADBhMAyQRBgVAkgmDhIUDAAuQAgY1DAAYA="));
const shoesIconGreen = heatshrink.decompress(atob("h0OwYJGhIEDgVIAgUEyQKDkmACgcggVACIeQAYMSgIRCgmApIbDiQUDAAkBkAFDGoYAD"));
const heartIcon = heatshrink.decompress(atob("h0OwYOLkmQhMkgACByVJgESpIFBpEEBAIFBCgIFCCgsABwcAgQOCAAMSpAwDyBNM"));
const powerIcon = heatshrink.decompress(atob("h0OwYQNsAED7AEDmwEDtu2AgUbtuABwXbBIUN23AAoYOCgEDFIgODABI"));
const powerIconGreen = heatshrink.decompress(atob("h0OwYQNkAEDpAEDiQEDkmSAgUJkmABwVJBIUEyVAAoYOCgEBFIgODABI"));
const powerIconRed = heatshrink.decompress(atob("h0OwYQNoAEDyAEDkgEDpIFDiVJBweSAgUJkmAAoYZDgQpEBwYAJA"));
const weatherCloudy = heatshrink.decompress(atob("iEQwYWTgP//+AAoMPAoPwAoN/AocfAgP//0AAgQAB/AFEABgdDAAMDDohMRA"));
const weatherSunny = heatshrink.decompress(atob("iEQwYLIg3AAgVgAQMMAo8Am3YAgUB23bAoUNAoIUBjYFCsOwBYoFDDpFgHYI1JI4gFGAAYA="));
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=="));
const weatherSnowy = heatshrink.decompress(atob("iEQwYROn/8AocH8AECuAFBh0Agf+CIN/4EDx/4j/x4EAgIIBwAXBAogRFDoopFGoxBGABIA="));
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=="));
let settings;
function loadSettings() {
settings = require("Storage").readJSON("circlesclock.json", 1) || {
settings = storage.readJSON("circlesclock.json", 1) || {
'minHR': 40,
'maxHR': 200,
'stepGoal': 10000,
'batteryWarn': 30
'stepDistanceGoal': 8000,
'stepLength': 0.8,
'batteryWarn': 30,
'showWidgets': false,
'circle1': 'hr',
'circle2': 'steps',
'circle3': 'battery'
};
// Load step goal from pedometer widget as fallback
if (settings.stepGoal == undefined) {
@ -21,122 +39,227 @@ function loadSettings() {
settings.stepGoal = d != undefined && d.settings != undefined ? d.settings.goal : 10000;
}
}
loadSettings();
const showWidgets = settings.showWidgets || false;
let hrtValue;
// layout values:
const colorFg = g.theme.dark ? '#fff' : '#000';
const colorBg = g.theme.dark ? '#000' : '#fff';
const colorGrey = '#808080';
const colorRed = '#ff0000';
const colorGreen = '#00ff00';
let hrtValue;
const h = g.getHeight();
const colorGreen = '#008000';
const colorBlue = '#0000ff';
const colorYellow = '#ffff00';
const widgetOffset = showWidgets ? 24 : 0;
const h = g.getHeight() - widgetOffset;
const w = g.getWidth();
const hOffset = 30;
const hOffset = 30 - widgetOffset;
const h1 = Math.round(1 * h / 5 - hOffset);
const h2 = Math.round(3 * h / 5 - hOffset);
const h3 = Math.round(8 * h / 8 - hOffset);
const w1 = Math.round(w / 6);
const w2 = Math.round(3 * w / 6);
const w3 = Math.round(5 * w / 6);
const radiusOuter = 22;
const radiusInner = 16;
const h3 = Math.round(8 * h / 8 - hOffset - 3); // circle y position
const circlePosX = [Math.round(w / 6), Math.round(3 * w / 6), Math.round(5 * w / 6)]; // cirle x positions
const radiusOuter = 25;
const radiusInner = 20;
const circleFont = "Vector:15";
const circleFontBig = "Vector:16";
const circleFontSmall = "Vector:13";
function draw() {
g.reset();
g.clear(true);
if (!showWidgets) {
/*
* we are not drawing the widgets as we are taking over the whole screen
* so we will blank out the draw() functions of each widget and change the
* area to the top bar doesn't get cleared.
*/
if (WIDGETS && typeof WIDGETS === "object") {
for (let wd of WIDGETS) {
wd.draw = () => {};
wd.area = "";
}
}
} else {
Bangle.drawWidgets();
}
g.setColor(colorBg);
g.fillRect(0, 0, w, h);
g.fillRect(0, widgetOffset, w, h);
// time
g.setFont("Vector:50");
g.setFontAlign(-1, -1);
g.setFontAlign(0, -1);
g.setColor(colorFg);
g.drawString(locale.time(new Date(), 1), w / 10, h1 + 8);
g.drawString(locale.time(new Date(), 1), w / 2, h1 + 8);
// date & dow
g.setFont("Vector:20");
g.setFont("Vector:21");
g.setFontAlign(-1, 0);
g.drawString(locale.date(new Date()), w / 10, h2);
g.drawString(locale.dow(new Date()), w / 10, h2 + 22);
g.drawString(locale.date(new Date()), w > 180 ? 2 * w / 10 : w / 10, h2);
g.drawString(locale.dow(new Date()), w > 180 ? 2 * w / 10 : w / 10, h2 + 22);
// Steps circle
drawSteps();
// Heart circle
drawHeartRate();
// Battery circle
drawBattery();
drawCircle(1);
drawCircle(2);
drawCircle(3);
}
const defaultCircleTypes = ["steps", "hr", "battery"];
function drawCircle(index) {
let type = settings['circle' + index];
if (!type) type = defaultCircleTypes[index - 1];
const w = getCirclePosition(type);
switch (type) {
case "steps":
drawSteps(w);
break;
case "stepsDist":
drawStepsDistance(w);
break;
case "hr":
drawHeartRate(w);
break;
case "battery":
drawBattery(w);
break;
case "weather":
drawWeather(w);
break;
}
}
function drawSteps() {
function getCirclePosition(type) {
for (let i = 1; i <= 3; i++) {
const setting = settings['circle' + i];
if (setting == type) return circlePosX[i - 1];
}
for (let i = 0; i < defaultCircleTypes.length; i++) {
if (type == defaultCircleTypes[i]) return circlePosX[i];
}
return undefined;
}
function isCircleEnabled(type) {
return getCirclePosition(type) != undefined;
}
function drawSteps(w) {
if (!w) w = getCirclePosition("steps");
const steps = getSteps();
const blue = '#0000ff';
// 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(w1, h3, radiusOuter);
g.fillCircle(w, h3, radiusOuter);
const stepGoal = settings.stepGoal || 10000;
if (stepGoal > 0) {
let percent = steps / stepGoal;
if (stepGoal < steps) percent = 1;
drawGauge(w1, h3, percent, blue);
drawGauge(w, h3, percent, colorBlue);
}
g.setColor(colorBg);
g.fillCircle(w1, h3, radiusInner);
g.fillCircle(w, h3, radiusInner);
g.fillPoly([w1, h3, w1 - 15, h3 + radiusOuter + 5, w1 + 15, h3 + radiusOuter + 5]);
g.fillPoly([w, h3, w - 15, h3 + radiusOuter + 5, w + 15, h3 + radiusOuter + 5]);
g.setFont("Vector:12");
g.setFont(circleFont);
g.setFontAlign(0, 0);
g.setColor(colorFg);
g.drawString(shortValue(steps), w1 + 2, h3);
g.drawString(shortValue(steps), w + 2, h3);
g.drawImage(shoesIcon, w1 - 6, h3 + radiusOuter - 6);
g.drawImage(shoesIcon, w - 6, h3 + radiusOuter - 6);
}
function drawHeartRate() {
function drawStepsDistance(w) {
if (!w) w = getCirclePosition("steps");
const steps = getSteps();
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(w2, h3, radiusOuter);
g.fillCircle(w, h3, radiusOuter);
const stepDistanceGoal = settings.stepDistanceGoal || 8000;
if (stepDistanceGoal > 0) {
let percent = stepsDistance / stepDistanceGoal;
if (stepDistanceGoal < stepsDistance) percent = 1;
drawGauge(w, h3, percent, colorGreen);
}
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);
g.setColor(colorFg);
g.drawString(shortValue(stepsDistance), w + 2, h3);
g.drawImage(shoesIconGreen, w - 6, h3 + radiusOuter - 6);
}
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);
g.setColor(colorGrey);
g.fillCircle(w, h3, radiusOuter);
if (hrtValue != undefined && hrtValue > 0) {
const minHR = 40;
const minHR = settings.minHR || 40;
const percent = (hrtValue - minHR) / (settings.maxHR - minHR);
drawGauge(w2, h3, percent, colorRed);
drawGauge(w, h3, percent, colorRed);
}
g.setColor(colorBg);
g.fillCircle(w2, h3, radiusInner);
g.fillCircle(w, h3, radiusInner);
g.fillPoly([w2, h3, w2 - 15, h3 + radiusOuter + 5, w2 + 15, h3 + radiusOuter + 5]);
g.fillPoly([w, h3, w - 15, h3 + radiusOuter + 5, w + 15, h3 + radiusOuter + 5]);
g.setFont("Vector:12");
g.setFont(circleFontBig);
g.setFontAlign(0, 0);
g.setColor(colorFg);
g.drawString(hrtValue != undefined ? hrtValue : "-", w2, h3);
g.drawString(hrtValue != undefined ? hrtValue : "-", w, h3);
g.drawImage(heartIcon, w2 - 6, h3 + radiusOuter - 6);
g.drawImage(heartIcon, w - 6, h3 + radiusOuter - 6);
}
function drawBattery() {
function drawBattery(w) {
if (!w) w = getCirclePosition("battery");
const battery = E.getBattery();
const yellow = '#ffff00';
// 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(w3, h3, radiusOuter);
g.fillCircle(w, h3, radiusOuter);
if (battery > 0) {
const percent = battery / 100;
drawGauge(w3, h3, percent, yellow);
drawGauge(w, h3, percent, colorYellow);
}
g.setColor(colorBg);
g.fillCircle(w3, h3, radiusInner);
g.fillCircle(w, h3, radiusInner);
g.fillPoly([w3, h3, w3 - 15, h3 + radiusOuter + 5, w3 + 15, h3 + radiusOuter + 5]);
g.fillPoly([w, h3, w - 15, h3 + radiusOuter + 5, w + 15, h3 + radiusOuter + 5]);
g.setFont("Vector:12");
g.setFont(circleFont);
g.setFontAlign(0, 0);
let icon = powerIcon;
@ -144,17 +267,100 @@ function drawBattery() {
if (Bangle.isCharging()) {
color = colorGreen;
icon = powerIconGreen;
}
else {
} else {
if (settings.batteryWarn != undefined && battery <= settings.batteryWarn) {
color = colorRed;
icon = powerIconRed;
}
}
g.setColor(color);
g.drawString(battery + '%', w3, h3);
g.drawString(battery + '%', w, h3);
g.drawImage(icon, w3 - 6, h3 + radiusOuter - 6);
g.drawImage(icon, w - 6, h3 + radiusOuter - 6);
}
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);
g.setColor(colorGrey);
g.fillCircle(w, h3, radiusOuter);
if (humidity >= 0) {
drawGauge(w, h3, humidity / 100, colorYellow);
}
g.setColor(colorBg);
g.fillCircle(w, h3, radiusInner);
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);
if (code > 0) {
const icon = getWeatherIconByCode(code);
if (icon) g.drawImage(icon, w - 6, h3 + radiusOuter - 10);
}
}
/*
* Choose weather icon to display based on weather conditition code
* https://openweathermap.org/weather-conditions#Weather-Condition-Codes-2
*/
function getWeatherIconByCode(code) {
const codeGroup = Math.round(code / 100);
switch (codeGroup) {
case 2:
return weatherStormy;
case 3:
return weatherCloudy;
case 5:
switch (code) {
case 511:
return weatherSnowy;
case 520:
return weatherPartlyRainy;
case 521:
return weatherPartlyRainy;
case 522:
return weatherPartlyRainy;
case 531:
return weatherPartlyRainy;
default:
return weatherRainy;
}
break;
case 6:
return weatherSnowy;
case 7:
return weatherFoggy;
case 8:
switch (code) {
case 800:
return weatherSunny;
case 801:
return weatherPartlyCloudy;
case 802:
return weatherPartlyCloudy;
default:
return weatherCloudy;
}
break;
default:
return undefined;
}
return undefined;
}
function radians(a) {
@ -162,22 +368,21 @@ function radians(a) {
}
function drawGauge(cx, cy, percent, color) {
let offset = 30;
let end = 300;
var i = 0;
var r = radiusInner + 3;
const offset = 15;
const end = 345;
const r = radiusInner + 3;
if (percent <= 0) return;
if (percent > 1) percent = 1;
var startrot = -offset;
var endrot = startrot - ((end - offset) * percent) - 15;
const startrot = -offset;
const endrot = startrot - ((end - offset) * percent);
g.setColor(color);
const size = 4;
const size = radiusOuter - radiusInner - 2;
// draw gauge
for (i = startrot; i > endrot - size; i -= size) {
for (let i = startrot; i > endrot - size; i -= size) {
x = cx + r * Math.sin(radians(i));
y = cy + r * Math.cos(radians(i));
g.fillCircle(x, y, size);
@ -198,54 +403,56 @@ function shortValue(v) {
}
function getSteps() {
if (WIDGETS.wpedom !== undefined) {
if (WIDGETS && WIDGETS.wpedom !== undefined) {
return WIDGETS.wpedom.getSteps();
}
return 0;
}
Bangle.on('lock', function(isLocked) {
if (!isLocked) {
Bangle.setHRMPower(1, "watch");
if (hrtValue == undefined) {
hrtValue = '...';
drawHeartRate();
}
} else {
Bangle.setHRMPower(0, "watch");
}
drawHeartRate();
drawSteps();
});
function getWeather() {
const jsonWeather = storage.readJSON('weather.json');
return jsonWeather && jsonWeather.weather ? jsonWeather.weather : undefined;
}
Bangle.on('HRM', function(hrm) {
//if(hrm.confidence > 90){
hrtValue = hrm.bpm;
if (Bangle.isLCDOn())
function enableHRMSensor() {
Bangle.setHRMPower(1, "circleclock");
if (hrtValue == undefined) {
hrtValue = '...';
drawHeartRate();
//} else {
// hrtValue = undefined;
//}
});
Bangle.on('charging', function(charging) {
drawBattery();
});
g.clear();
Bangle.loadWidgets();
/*
* we are not drawing the widgets as we are taking over the whole screen
* so we will blank out the draw() functions of each widget and change the
* area to the top bar doesn't get cleared.
*/
if (typeof WIDGETS === "object") {
for (let wd of WIDGETS) {
wd.draw = () => {};
wd.area = "";
}
}
loadSettings();
setInterval(draw, 60000);
draw();
Bangle.on('lock', function(isLocked) {
if (!isLocked) {
if (isCircleEnabled("hr")) {
enableHRMSensor();
}
draw();
} else {
Bangle.setHRMPower(0, "circleclock");
}
});
Bangle.on('HRM', function(hrm) {
if (isCircleEnabled("hr")) {
hrtValue = hrm.bpm;
if (Bangle.isLCDOn())
drawHeartRate();
}
});
Bangle.setUI("clock");
Bangle.loadWidgets();
draw();
setInterval(draw, 60000);
Bangle.on('charging', function(charging) {
if (isCircleEnabled("battery")) drawBattery();
});
if (isCircleEnabled("hr")) {
enableHRMSensor();
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -6,13 +6,26 @@
settings[key] = value;
storage.write(SETTINGS_FILE, settings);
}
var valuesCircleTypes = ["steps", "stepsDist", "hr", "battery", "weather"];
var namesCircleTypes = ["steps", "distance", "heart", "battery", "weather"];
E.showMenu({
'': { 'title': 'circlesclock' },
'< Back': back,
'min heartrate': {
value: "minHR" in settings ? settings.minHR : 40,
min: 0,
max : 250,
step: 5,
format: x => {
return x;
},
onchange: x => save('minHR', x),
},
'max heartrate': {
value: "maxHR" in settings ? settings.maxHR : 200,
min: 20,
max : 250,
step: 10,
step: 5,
format: x => {
return x;
},
@ -28,7 +41,27 @@
},
onchange: x => save('stepGoal', x),
},
'battery warn lvl': {
'step length': {
value: "stepLength" in settings ? settings.stepLength : 0.8,
min: 0.1,
max : 1.5,
step: 0.01,
format: x => {
return x;
},
onchange: x => save('stepLength', x),
},
'step dist goal': {
value: "stepDistanceGoal" in settings ? settings.stepDistanceGoal : 8000,
min: 2000,
max : 30000,
step: 1000,
format: x => {
return x;
},
onchange: x => save('stepDistanceGoal', x),
},
'battery warn': {
value: "batteryWarn" in settings ? settings.batteryWarn : 30,
min: 10,
max : 100,
@ -38,6 +71,28 @@
},
onchange: x => save('batteryWarn', x),
},
'< Back': back,
'show widgets': {
value: "showWidgets" in settings ? settings.showWidgets : false,
format: () => (settings.showWidgets ? 'Yes' : 'No'),
onchange: x => save('showWidgets', x),
},
'left': {
value: settings.circle1 ? valuesCircleTypes.indexOf(settings.circle1) : 0,
min: 0, max: 4,
format: v => namesCircleTypes[v],
onchange: x => save('circle1', valuesCircleTypes[x]),
},
'middle': {
value: settings.circle2 ? valuesCircleTypes.indexOf(settings.circle2) : 2,
min: 0, max: 4,
format: v => namesCircleTypes[v],
onchange: x => save('circle2', valuesCircleTypes[x]),
},
'right': {
value: settings.circle3 ? valuesCircleTypes.indexOf(settings.circle3) : 3,
min: 0, max: 4,
format: v => namesCircleTypes[v],
onchange: x => save('circle3', valuesCircleTypes[x]),
}
});
});

View File

@ -11,4 +11,4 @@ The year itself begins on the December solstice. Because that always happens, th
The epoch (year numbering) begins in the last year when the perihelion coincided with the June solstice, near the beginning of the Holocene era. That astronomical basis makes the calendar free from politics, religion, or geography.
While the year number remains cardinal, BTN5 toggles between cardinal and ordinal for the rest of the calendar segments. BTN4 adds or removes a quickly changing digit to or from the clock.
While the year number remains cardinal, tapping on the right side of the watch face toggles between cardinal and ordinal for the rest of the calendar segments. Tapping on the left adds or removes a quickly changing digit to or from the clock.

1
apps/flipper/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: first release

20
apps/flipper/README.md Normal file
View File

@ -0,0 +1,20 @@
# Flipper
![](flipper.png)
*A utility to switch from the dark to the light theme and vice versa*
* If the current theme is dark it will switch to the light theme
* If the current theme is light it will switch to the dark theme
* Combine with the awesome pattern launcher and it saves loads of time
## Demo Video
There's no screenshot but there is a [demo video.](https://espruino.microco.sm/api/v1/files/9caa1afef7e4cce1d9b518af2dd271f1a57c5ecc.mp4)
## Future Enhancements
* Nothing left to add
Written by: [Hugh Barney](https://github.com/hughbarney) For support and discussion please post in the [Bangle JS Forum](http://forum.espruino.com/microcosms/1424/)

View File

@ -0,0 +1,39 @@
const storage = require('Storage');
let settings = storage.readJSON('setting.json', 1);
function cl(x) { return g.setColor(x).getColor(); }
function upd(th) {
g.theme = th;
settings.theme = th;
storage.write('setting.json', settings);
delete g.reset;
g._reset = g.reset;
g.reset = function(n) { return g._reset().setColor(th.fg).setBgColor(th.bg); };
g.clear = function(n) { if (n) g.reset(); return g.clearRect(0,0,g.getWidth(),g.getHeight()); };
g.clear(1);
}
function flipTheme() {
if (!g.theme.dark) {
upd({
fg:cl("#fff"), bg:cl("#000"),
fg2:cl("#0ff"), bg2:cl("#000"),
fgH:cl("#fff"), bgH:cl("#00f"),
dark:true
});
} else {
upd({
fg:cl("#000"), bg:cl("#fff"),
fg2:cl("#000"), bg2:cl("#cff"),
fgH:cl("#000"), bgH:cl("#0ff"),
dark:false
});
}
}
Bangle.loadWidgets();
Bangle.drawWidgets();
flipTheme();
setTimeout(load, 20);

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEw4X/AAO/mMUzs975K+ggLKysUAYNVqoLFitUoAKBqtQBYkJBIQABqwLEgQLEqtABggJDqkVBaoNCBZQwEgILWgoJENYsVBIcVBYpDEgpSIBYMBKQg6CuogCBY1UgoLCXAQLDqAsDBYhSBqEJHAoLDoEBcQ4LBEwILIMooLdIg4LaVoyaGERLcFao4LIdRAACYYUQBY5RKAH4Ar"))

BIN
apps/flipper/flipper.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 644 B

4
apps/ftclock/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
timezonedb.csv.zip
country.csv
zone.csv
timezone.csv

1
apps/ftclock/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: first release

24
apps/ftclock/README.md Normal file
View File

@ -0,0 +1,24 @@
# Four Twenty Clock
A clock that tells when and where it's going to be [4:20](https://en.wikipedia.org/wiki/420_%28cannabis_culture%29) next
![screensot](screenshot.png) ![screenshot at 4:20](screenshot1.png)
## Generating `fourTwentyTz.js`
Once in a while we need to regenerate it for 2 reasons:
* One or more places got in or out of daylight saving time (DST) mode.
* The database saying _when_ places enter/exit DST mode got updated.
I'll do my best to release a new version every time this happens,
but if you ever need to do this yourself, here's how:
* `cd` to the `ftclock` folder
* If you haven't done so yet, run `npm install` there (this would create the `node_modules` folder).
* Get and unzip the latest `timezone.csv.zip` from https://timezonedb.com/download
* Run `npm run make`
## Creator
[Nimrod Kerrett](zzzen.com)

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

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwghC/AH4A/AH4A/AAMHu4ACuwHBs4HDsEGBIQLCsADBgwPDCAQGEuwXFBwI0GEAMHuAGCCoMHC4pMHEAIXEAgIGEBwI9BC4wSCC8IVCMAwIBs4XKUQJfITQgXCDwp8EHAqaECoLFEu4cDBIggBs6uFZozuGBAVmC4g+FMgZQEZQ5vGC4iRIC5IrDN4h5EC5J3BCoIKGgyaEC44VBC46yEDgoeDgxqLC5SCMAgoTFY47GFC4xFBdwwPBD4oWFAH4A/AH4A/AH4AjA=="))

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

@ -0,0 +1,52 @@
let getNextFourTwenty = require("fourTwenty").getNextFourTwenty;
require("FontTeletext10x18Ascii").add(Graphics);
let leaf_img = "\x17\x18\x81\x00\x00\x10\x00\x00 \x00\x00@\x00\x01\xc0\x00\x03\x80\x00\x0f\x80\x00\x1f\x00\x00>\x00\x00|\x00\xc0\xf8\x19\xe1\xf0\xf1\xe3\xe3\xc3\xf7\xdf\x83\xff\xfe\x03\xff\xf8\x03\xff\xe0\x03\xff\x80\x03\xfe\x00\x7f\xff\xc0\xff\xff\xc0\x06\xe0\x00\x18\xc0\x00 \x80\x00\x00\x00";
// timeout used to update every minute
let drawTimeout;
// schedule a draw for the next minute
function queueDraw() {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = setTimeout(function() {
drawTimeout = undefined;
draw();
}, 60000 - (Date.now() % 60000));
}
function draw() {
g.reset();
g.setBgColor("#ffffff");
let date = new Date();
let timeStr = require("locale").time(date,1);
let next420 = getNextFourTwenty();
g.clearRect(0,26,g.getWidth(),g.getHeight());
g.setColor("#00ff00").setFontAlign(0,-1).setFont("Teletext10x18Ascii",2);
g.drawString(next420.minutes? timeStr: `\0${leaf_img}${timeStr}\0${leaf_img}`, g.getWidth()/2, 28);
g.setColor("#000000");
g.setFontAlign(-1,-1).setFont("Teletext10x18Ascii");
g.drawString(g.wrapString(next420.text, g.getWidth()-8).join("\n"),4,60);
// queue draw in one minute
queueDraw();
}
// Clear the screen once, at startup
g.clear();
// Load widgets
Bangle.loadWidgets();
Bangle.drawWidgets();
// draw immediately at first, queue update
draw();
// Stop updates when LCD is off, restart when on
Bangle.on('lcdPower',on=>{
if (on) {
draw(); // draw immediately, queue redraw
} else { // stop draw timer
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
}
});
// Show launcher when middle button pressed
Bangle.setUI("clock");

BIN
apps/ftclock/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

@ -0,0 +1,45 @@
let timezones = require("fourTwentyTz").timezones;
function get420offset() {
let current_time = Math.floor((Date.now()%(24*3600*1000))/60000);
let current_min = current_time%60;
if (current_min>20 && current_min<25) {
current_time -= current_min-20; // 5 minutes grace period
}
let offset = 16*60+20-current_time;
if (offset<0) {
offset += 24*60;
}
return offset;
}
function makeFourTwentyText(minutes, places) {
//let plural = minutes==1? "": "s";
//let msgprefix = minutes? `${minutes} minute${plural} to`: "It is now";
let msgprefix = minutes? `${minutes}m to`: "It is now";
let msgsuffix = places.length>1? ", and other fine places": "";
let msgplace = places[Math.floor(Math.random()*places.length)];
return `${msgprefix} 4:20 at ${msgplace}${msgsuffix}.`;
}
function getNextFourTwenty() {
let offs = get420offset();
for (let i=0; i<timezones.length; i++) {
if (timezones[i][0]<=offs) {
let minutes = offs-timezones[i][0];
let places = timezones[i][1];
return {
minutes: minutes,
places: places,
text: makeFourTwentyText(minutes, places)
};
}
}
return {
minutes: 666,
places: ["Snafu (Yes. It's a bug)"],
text: "Snafu (Yes. It's a bug)"
};
}
exports.getNextFourTwenty = getNextFourTwenty;

View File

@ -0,0 +1,463 @@
// Generated by mkFourTwentyTz.js
// Data source: https://timezonedb.com/files/timezonedb.csv.zip
// Sun Jan 09 2022 13:21:47 GMT+0200 (Israel Standard Time)
exports.timezones = {
"0": [
"Troll, Antarctica",
"Ouagadougou, Burkina Faso",
"Abidjan, Côte d'Ivoire",
"Canary, Spain",
"Faroe, Faroe Islands",
"London, United Kingdom of Great Britain and Northern Ireland",
"Guernsey, Guernsey",
"Accra, Ghana",
"Danmarkshavn, Greenland",
"Banjul, Gambia",
"Conakry, Guinea",
"Bissau, Guinea-Bissau",
"Dublin, Ireland",
"Isle of_Man, Isle of Man",
"Reykjavik, Iceland",
"Jersey, Jersey",
"Monrovia, Liberia",
"Bamako, Mali",
"Nouakchott, Mauritania",
"Lisbon, Portugal",
"Madeira, Portugal",
"St Helena, Saint Helena, Ascension and Tristan da Cunha",
"Freetown, Sierra Leone",
"Dakar, Senegal",
"Sao Tome, Sao Tome and Principe",
"Lome, Togo"
],
"60": [
"Andorra, Andorra",
"Tirane, Albania",
"Luanda, Angola",
"Vienna, Austria",
"Sarajevo, Bosnia and Herzegovina",
"Brussels, Belgium",
"Porto-Novo, Benin",
"Kinshasa, Congo, Democratic Republic of the",
"Bangui, Central African Republic",
"Brazzaville, Congo",
"Zurich, Switzerland",
"Douala, Cameroon",
"Prague, Czechia",
"Berlin, Germany",
"Busingen, Germany",
"Copenhagen, Denmark",
"Algiers, Algeria",
"El Aaiun, Western Sahara",
"Madrid, Spain",
"Ceuta, Spain",
"Paris, France",
"Libreville, Gabon",
"Gibraltar, Gibraltar",
"Malabo, Equatorial Guinea",
"Zagreb, Croatia",
"Budapest, Hungary",
"Rome, Italy",
"Vaduz, Liechtenstein",
"Luxembourg, Luxembourg",
"Casablanca, Morocco",
"Monaco, Monaco",
"Podgorica, Montenegro",
"Skopje, North Macedonia",
"Malta, Malta",
"Niamey, Niger",
"Lagos, Nigeria",
"Amsterdam, Netherlands",
"Oslo, Norway",
"Warsaw, Poland",
"Belgrade, Serbia",
"Stockholm, Sweden",
"Ljubljana, Slovenia",
"Longyearbyen, Svalbard and Jan Mayen",
"Bratislava, Slovakia",
"San Marino, San Marino",
"Ndjamena, Chad",
"Tunis, Tunisia",
"Vatican, Holy See"
],
"120": [
"Mariehamn, Åland Islands",
"Sofia, Bulgaria",
"Bujumbura, Burundi",
"Gaborone, Botswana",
"Lubumbashi, Congo, Democratic Republic of the",
"Nicosia, Cyprus",
"Famagusta, Cyprus",
"Tallinn, Estonia",
"Cairo, Egypt",
"Helsinki, Finland",
"Athens, Greece",
"Jerusalem, Israel",
"Amman, Jordan",
"Beirut, Lebanon",
"Maseru, Lesotho",
"Vilnius, Lithuania",
"Riga, Latvia",
"Tripoli, Libya",
"Chisinau, Moldova, Republic of",
"Blantyre, Malawi",
"Maputo, Mozambique",
"Windhoek, Namibia",
"Gaza, Palestine, State of",
"Hebron, Palestine, State of",
"Bucharest, Romania",
"Kaliningrad, Russian Federation",
"Kigali, Rwanda",
"Khartoum, Sudan",
"Juba, South Sudan",
"Damascus, Syrian Arab Republic",
"Mbabane, Eswatini",
"Kiev, Ukraine",
"Uzhgorod, Ukraine",
"Zaporozhye, Ukraine",
"Johannesburg, South Africa",
"Lusaka, Zambia",
"Harare, Zimbabwe"
],
"180": [
"Syowa, Antarctica",
"Bahrain, Bahrain",
"Minsk, Belarus",
"Djibouti, Djibouti",
"Asmara, Eritrea",
"Addis Ababa, Ethiopia",
"Baghdad, Iraq",
"Nairobi, Kenya",
"Comoro, Comoros",
"Kuwait, Kuwait",
"Antananarivo, Madagascar",
"Qatar, Qatar",
"Moscow, Russian Federation",
"Simferopol, Ukraine",
"Kirov, Russian Federation",
"Volgograd, Russian Federation",
"Riyadh, Saudi Arabia",
"Mogadishu, Somalia",
"Istanbul, Turkey",
"Dar es_Salaam, Tanzania, United Republic of",
"Kampala, Uganda",
"Aden, Yemen",
"Mayotte, Mayotte"
],
"240": [
"Dubai, United Arab Emirates",
"Yerevan, Armenia",
"Baku, Azerbaijan",
"Tbilisi, Georgia",
"Mauritius, Mauritius",
"Muscat, Oman",
"Reunion, Réunion",
"Astrakhan, Russian Federation",
"Saratov, Russian Federation",
"Ulyanovsk, Russian Federation",
"Samara, Russian Federation",
"Mahe, Seychelles"
],
"300": [
"Mawson, Antarctica",
"Qyzylorda, Kazakhstan",
"Aqtobe, Kazakhstan",
"Aqtau, Kazakhstan",
"Atyrau, Kazakhstan",
"Oral, Kazakhstan",
"Maldives, Maldives",
"Karachi, Pakistan",
"Yekaterinburg, Russian Federation",
"Kerguelen, French Southern Territories",
"Dushanbe, Tajikistan",
"Ashgabat, Turkmenistan",
"Samarkand, Uzbekistan",
"Tashkent, Uzbekistan"
],
"360": [
"Vostok, Antarctica",
"Dhaka, Bangladesh",
"Thimphu, Bhutan",
"Urumqi, China",
"Chagos, British Indian Ocean Territory",
"Bishkek, Kyrgyzstan",
"Almaty, Kazakhstan",
"Qostanay, Kazakhstan",
"Omsk, Russian Federation"
],
"420": [
"Davis, Antarctica",
"Christmas, Christmas Island",
"Jakarta, Indonesia",
"Pontianak, Indonesia",
"Phnom Penh, Cambodia",
"Vientiane, Lao People's Democratic Republic",
"Hovd, Mongolia",
"Novosibirsk, Russian Federation",
"Barnaul, Russian Federation",
"Tomsk, Russian Federation",
"Novokuznetsk, Russian Federation",
"Krasnoyarsk, Russian Federation",
"Bangkok, Thailand",
"Ho Chi_Minh, Viet Nam"
],
"480": [
"Perth, Australia",
"Brunei, Brunei Darussalam",
"Shanghai, China",
"Hong Kong, Hong Kong",
"Makassar, Indonesia",
"Ulaanbaatar, Mongolia",
"Choibalsan, Mongolia",
"Macau, Macao",
"Kuala Lumpur, Malaysia",
"Kuching, Malaysia",
"Manila, Philippines",
"Irkutsk, Russian Federation",
"Singapore, Singapore",
"Taipei, Taiwan, Province of China"
],
"540": [
"Jayapura, Indonesia",
"Tokyo, Japan",
"Pyongyang, Korea (Democratic People's Republic of)",
"Seoul, Korea, Republic of",
"Palau, Palau",
"Chita, Russian Federation",
"Yakutsk, Russian Federation",
"Khandyga, Russian Federation",
"Dili, Timor-Leste"
],
"600": [
"DumontDUrville, Antarctica",
"Brisbane, Australia",
"Lindeman, Australia",
"Chuuk, Micronesia (Federated States of)",
"Guam, Guam",
"Saipan, Northern Mariana Islands",
"Port Moresby, Papua New Guinea",
"Vladivostok, Russian Federation",
"Ust-Nera, Russian Federation"
],
"660": [
"Casey, Antarctica",
"Lord Howe, Australia",
"Macquarie, Australia",
"Hobart, Australia",
"Melbourne, Australia",
"Sydney, Australia",
"Pohnpei, Micronesia (Federated States of)",
"Kosrae, Micronesia (Federated States of)",
"Noumea, New Caledonia",
"Bougainville, Papua New Guinea",
"Magadan, Russian Federation",
"Sakhalin, Russian Federation",
"Srednekolymsk, Russian Federation",
"Guadalcanal, Solomon Islands",
"Efate, Vanuatu"
],
"720": [
"Tarawa, Kiribati",
"Majuro, Marshall Islands",
"Kwajalein, Marshall Islands",
"Norfolk, Norfolk Island",
"Nauru, Nauru",
"Kamchatka, Russian Federation",
"Anadyr, Russian Federation",
"Funafuti, Tuvalu",
"Wake, United States Minor Outlying Islands",
"Wallis, Wallis and Futuna"
],
"780": [
"McMurdo, Antarctica",
"Pago Pago, American Samoa",
"Fiji, Fiji",
"Kanton, Kiribati",
"Niue, Niue",
"Auckland, New Zealand",
"Fakaofo, Tokelau",
"Tongatapu, Tonga",
"Midway, United States Minor Outlying Islands",
"Apia, Samoa"
],
"840": [
"Rarotonga, Cook Islands",
"Kiritimati, Kiribati",
"Tahiti, French Polynesia",
"Adak, United States of America",
"Honolulu, United States of America"
],
"900": [
"Gambier, French Polynesia",
"Anchorage, United States of America",
"Juneau, United States of America",
"Sitka, United States of America",
"Metlakatla, United States of America",
"Yakutat, United States of America",
"Nome, United States of America"
],
"960": [
"Vancouver, Canada",
"Tijuana, Mexico",
"Pitcairn, Pitcairn",
"Los Angeles, United States of America"
],
"1020": [
"Edmonton, Canada",
"Cambridge Bay, Canada",
"Yellowknife, Canada",
"Inuvik, Canada",
"Creston, Canada",
"Dawson Creek, Canada",
"Fort Nelson, Canada",
"Whitehorse, Canada",
"Dawson, Canada",
"Mazatlan, Mexico",
"Chihuahua, Mexico",
"Ojinaga, Mexico",
"Hermosillo, Mexico",
"Denver, United States of America",
"Boise, United States of America",
"Phoenix, United States of America"
],
"1080": [
"Belize, Belize",
"Winnipeg, Canada",
"Rainy River, Canada",
"Resolute, Canada",
"Rankin Inlet, Canada",
"Regina, Canada",
"Swift Current, Canada",
"Costa Rica, Costa Rica",
"Galapagos, Ecuador",
"Guatemala, Guatemala",
"Tegucigalpa, Honduras",
"Mexico City, Mexico",
"Merida, Mexico",
"Monterrey, Mexico",
"Matamoros, Mexico",
"Bahia Banderas, Mexico",
"Managua, Nicaragua",
"El Salvador, El Salvador",
"Chicago, United States of America",
"Tell City, Indiana",
"Knox, Indiana",
"Menominee, United States of America",
"Center, North Dakota",
"New_Salem, North Dakota",
"Beulah, North Dakota"
],
"1140": [
"Eirunepe, Brazil",
"Rio Branco, Brazil",
"Nassau, Bahamas",
"Toronto, Canada",
"Nipigon, Canada",
"Thunder Bay, Canada",
"Iqaluit, Canada",
"Pangnirtung, Canada",
"Atikokan, Canada",
"Easter, Chile",
"Bogota, Colombia",
"Havana, Cuba",
"Guayaquil, Ecuador",
"Port-au-Prince, Haiti",
"Jamaica, Jamaica",
"Cayman, Cayman Islands",
"Cancun, Mexico",
"Panama, Panama",
"Lima, Peru",
"Grand Turk, Turks and Caicos Islands",
"New York, United States of America",
"Detroit, United States of America",
"Louisville, Kentucky",
"Monticello, Kentucky",
"Indianapolis, Indiana",
"Vincennes, Indiana",
"Winamac, Indiana",
"Marengo, Indiana",
"Petersburg, Indiana",
"Vevay, Indiana"
],
"1200": [
"Antigua, Antigua and Barbuda",
"Anguilla, Anguilla",
"Aruba, Aruba",
"Barbados, Barbados",
"St Barthelemy, Saint Barthélemy",
"Bermuda, Bermuda",
"La Paz, Bolivia (Plurinational State of)",
"Kralendijk, Bonaire, Sint Eustatius and Saba",
"Campo Grande, Brazil",
"Cuiaba, Brazil",
"Porto Velho, Brazil",
"Boa Vista, Brazil",
"Manaus, Brazil",
"Halifax, Canada",
"Glace Bay, Canada",
"Moncton, Canada",
"Goose Bay, Canada",
"Blanc-Sablon, Canada",
"Curacao, Curaçao",
"Dominica, Dominica",
"Santo Domingo, Dominican Republic",
"Grenada, Grenada",
"Thule, Greenland",
"Guadeloupe, Guadeloupe",
"Guyana, Guyana",
"St Kitts, Saint Kitts and Nevis",
"St Lucia, Saint Lucia",
"Marigot, Saint Martin (French part)",
"Martinique, Martinique",
"Montserrat, Montserrat",
"Puerto Rico, Puerto Rico",
"Lower Princes, Sint Maarten (Dutch part)",
"Port of_Spain, Trinidad and Tobago",
"St Vincent, Saint Vincent and the Grenadines",
"Caracas, Venezuela (Bolivarian Republic of)",
"Tortola, Virgin Islands (British)",
"St Thomas, Virgin Islands (U.S.)"
],
"1260": [
"Palmer, Antarctica",
"Rothera, Antarctica",
"Buenos Aires, Argentina",
"Cordoba, Argentina",
"Salta, Argentina",
"Jujuy, Argentina",
"Tucuman, Argentina",
"Catamarca, Argentina",
"La Rioja, Argentina",
"San Juan, Argentina",
"Mendoza, Argentina",
"San Luis, Argentina",
"Rio Gallegos, Argentina",
"Ushuaia, Argentina",
"Belem, Brazil",
"Fortaleza, Brazil",
"Recife, Brazil",
"Araguaina, Brazil",
"Maceio, Brazil",
"Bahia, Brazil",
"Sao Paulo, Brazil",
"Santarem, Brazil",
"Santiago, Chile",
"Punta Arenas, Chile",
"Stanley, Falkland Islands (Malvinas)",
"Cayenne, French Guiana",
"Nuuk, Greenland",
"Miquelon, Saint Pierre and Miquelon",
"Asuncion, Paraguay",
"Paramaribo, Suriname",
"Montevideo, Uruguay"
],
"1320": [
"Noronha, Brazil",
"South Georgia, South Georgia and the South Sandwich Islands"
],
"1380": [
"Cape Verde, Cabo Verde",
"Scoresbysund, Greenland",
"Azores, Portugal"
]
}

View File

@ -0,0 +1,74 @@
let fs = require('fs');
let csv = require('csv');
let countries = {},
zones = {},
offsdict = {},
now = Date.now(); // we need this to find zone's current DST state
function handleWrite(err,bytes) {
if (err) {
console.log(`Error writing to file ${err}`);
}
}
console.log("Generating fourTwentyTz.js...");
fs.createReadStream(__dirname+'/country.csv')
.pipe(csv.parse())
.on('data', (r) => {
countries[r[0]] = r[1];
})
.on('end', () => {
fs.createReadStream(__dirname+'/zone.csv')
.pipe(csv.parse())
.on('data', (r) => {
let parts = r[2].replace('_',' ').split('/');
let city = parts[parts.length-1];
let country ='';
if (parts.length>2) { // e.g. America/North_Dakota/New_Salem
country = parts[1]; // e.g. North Dakota
} else {
country = countries[r[1]]; // e.g. United States
}
zones[parseInt(r[0])] = {"name": `${city}, ${country}`};
})
.on('end', () => {
fs.createReadStream(__dirname+'/timezone.csv')
.pipe(csv.parse())
.on('data', (r) => {
code = parseInt(r[0]);
if (!(code in zones)) return;
starttime = parseInt(r[2] || "0"); // Bugger. They're feeding us blanks for UTC now
offs = parseInt(r[3]);
if (offs<0) {
offs += 60*60*24;
}
zone = zones[code];
if (starttime<now && (!("starttime" in zone) || zone.starttime<starttime)) {
zone.starttime = starttime;
zone.offs = Math.floor(offs/60);
}
})
.on('end', () => {
for (z in zones) {
zone = zones[z];
if (zone.offs%60) continue; // One a dem funky timezones. Ignore.
zonelist = offsdict[zone.offs] || [];
zonelist.push(zone.name);
offsdict[zone.offs] = zonelist;
}
fs.open("fourTwentyTz.js","w", (err, fd) => {
if (err) {
console.log("Can't open output file");
return;
}
fs.write(fd, "// Generated by mkFourTwentyTz.js\n", handleWrite);
fs.write(fd, `// ${Date()}\n`, handleWrite);
fs.write(fd, "// Data source: https://timezonedb.com/files/timezonedb.csv.zip\n", handleWrite);
fs.write(fd, "exports.timezones = ", handleWrite);
fs.write(fd, JSON.stringify(offsdict, null, 4), handleWrite);
console.log('Done.');
});
})
})
});

15
apps/ftclock/package.json Normal file
View File

@ -0,0 +1,15 @@
{
"name": "mkfourtwentytz",
"version": "1.0.0",
"description": "Convert timezonedb.com CSV to fourTwentyTz.js for BangleJS ftclock app",
"main": "mkFourTwentyTz.js",
"scripts": {
"make": "node mkFourTwentyTz.js"
},
"keywords": [],
"author": "",
"license": "GPL-3.0",
"dependencies": {
"csv": "^6.0.5"
}
}

BIN
apps/ftclock/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -2,3 +2,5 @@
0.02: Add support for ZIPs
Find and download ZIPs direct from the Espruino website
Take 'beta' tag off
0.03: Improve bootloader update safety. Now sets unsafeFlash:1 to allow flash with 2v11 and later
Add CRC checks for common bootloaders that we know don't work

View File

@ -60,6 +60,7 @@ function onInit(device) {
document.getElementById("fw-unknown").style = "display:none";
document.getElementById("fw-ok").style = "";
}
}
function checkForFileOnServer() {
@ -264,6 +265,8 @@ function createJS_app(binary, startAddress, endAddress) {
bin32[3] = VERSION; // VERSION! Use this to test ourselves
console.log("CRC 0x"+bin32[2].toString(16));
hexJS = "";//`\x10if (E.CRC32(E.memoryArea(${startAddress},${endAddress-startAddress}))==${bin32[2]}) { print("FIRMWARE UP TO DATE!"); load();}\n`;
hexJS += `\x10if (E.CRC32(E.memoryArea(0xF7000,0x7000))==1339551013) { print("BOOTLOADER 2v10.219 needs update"); load();}\n`;
hexJS += `\x10if (E.CRC32(E.memoryArea(0xF7000,0x7000))==1207580954) { print("BOOTLOADER 2v10.236 needs update"); load();}\n`;
hexJS += '\x10var s = require("Storage");\n';
hexJS += '\x10s.erase(".firmware");\n';
var CHUNKSIZE = 2048;
@ -291,20 +294,14 @@ function createJS_bootloader(binary, startAddress, endAddress) {
var chunk = btoa(new Uint8Array(binary.buffer, binary.byteOffset+i, l));
hexJS += '\x10_fw.set(atob("'+chunk+'"), 0x'+(i).toString(16)+');\n';
}
// hexJS += `\x10(function() {
// if (E.CRC32(_fw)!=${crc}) throw "Invalid CRC!";
// var f = require("Flash");
// for (var i=${startAddress};i<${endAddress};i+=4096) f.erasePage(i);
// f.write(_fw,${startAddress});
// E.reboot();
// })();\n`;
hexJS += `\x10if (E.CRC32(_fw)!=${crc}) throw "Invalid CRC: 0x"+E.CRC32(_fw).toString(16);\n`;
hexJS += '\x10var f = require("Flash");\n';
hexJS += `\x10(function() { if (E.CRC32(_fw)!=${crc}) throw "Invalid CRC: 0x"+E.CRC32(_fw).toString(16);\n`;
hexJS += 'E.showMessage("Flashing Bootloader...")\n';
hexJS += 'E.setFlags({unsafeFlash:1})\n';
hexJS += 'var f = require("Flash");\n';
for (var i=startAddress;i<endAddress;i+=4096)
hexJS += '\x10f.erasePage(0x'+i.toString(16)+');\n';
hexJS += `\x10f.write(_fw,${startAddress});\n`;
// hexJS += '\x10setTimeout(()=>E.showMessage("Rebooting..."),50);\n';
// hexJS += '\x10setTimeout(()=>E.reboot(), 2000);\n';
hexJS += 'f.erasePage(0x'+i.toString(16)+');\n';
hexJS += `f.write(_fw,${startAddress});\n`;
hexJS += `})()\n`;
}
function fileLoaded() {

View File

@ -2,4 +2,7 @@
0.03: Show number of satellites while waiting for fix
0.04: Add Maidenhead readout of GPS location
0.05: Refactor to use 'layout' library for multi-device support
0.06: Added number of satellites in view and fixed crash with GPS time
0.06: Add number of satellites in view and fix crash with GPS time
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

View File

@ -4,7 +4,7 @@ function satelliteImage() {
var Layout = require("Layout");
var layout;
Bangle.setGPSPower(1, "app");
//Bangle.setGPSPower(1, "app");
E.showMessage("Loading..."); // avoid showing rubbish on screen
var lastFix = {
@ -19,6 +19,7 @@ var lastFix = {
var SATinView = 0;
var nofBD = 0;
var nofGP = 0;
var listenerGPSraw = 0;
function formatTime(now) {
if (now == undefined) {
@ -86,13 +87,17 @@ function onGPS(fix) {
{type:"txt", font:"6x8", pad:3, label:"Satellites used" }
]},
{type:"txt", font:"6x8", label:"", fillx:true, id:"progress" }
]},{lazy:true});
]},{lazy:false});
}
g.clearRect(0,24,g.getWidth(),g.getHeight());
layout.render();
}
lastFix = fix;
//lastFix = fix;
if (fix.fix) {
if (listenerGPSraw == 1) {
Bangle.removeListener('GPS-raw', onGPSraw);
listenerGPSraw = 0;
}
var locale = require("locale");
var satellites = fix.satellites;
var maidenhead = getMaidenHead(fix.lat,fix.lon);
@ -103,23 +108,49 @@ function onGPS(fix) {
layout.time.label = "Time: "+formatTime(fix.time);
layout.sat.label = "Satellites: "+satellites;
layout.maidenhead.label = "Maidenhead: "+maidenhead;
layout.render();
} else {
layout.sat.label = fix.satellites;
layout.progress.label = "in view: " + SATinView;
if (fix.satelites != lastFix.satelites) {
layout.clear(layout.sat);
layout.sat.label = fix.satellites;
layout.render(layout.sat);
}
if (SATinView != lastFix.SATinView) {
layout.clear(layout.progress);
layout.progress.label = "in view: " + SATinView;
layout.render(layout.progress);
}
}
layout.render();
//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(3,6) == "GSV") {
// console.log(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(0,7) == "$BDGSV,") nofBD = Number(nmea.slice(11,13));
if (nmea.slice(0,7) == "$GPGSV,") nofGP = Number(nmea.slice(11,13));
SATinView = nofBD + nofGP;
}
Bangle.loadWidgets();
Bangle.drawWidgets();
Bangle.on('GPS', onGPS);
Bangle.on('GPS-raw', onGPSraw);
//Bangle.on('GPS-raw', onGPSraw);
Bangle.setGPSPower(1, "app");
function exitApp() {
load();
}
setWatch(_=>exitApp(), BTN1);
if (global.BTN2) {
setWatch(_=>exitApp(), BTN2);
setWatch(_=>exitApp(), BTN3);
}

1
apps/hralarm/ChangeLog Normal file
View File

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

15
apps/hralarm/README.md Normal file
View File

@ -0,0 +1,15 @@
# Heart rate alarm
This invisible widget vibrates whenever the heart rate gets close to the upper limit or goes over or under the configured limits.
## Usage
Configure the heart rate limits in the apps settings. This widget uses both 'HRM' and 'BTHRM' events.
## Features
Long vibration every 10 seconds on reaching upper limit, short vibrations between upper limit and warning threshold and an single vibration when reaching the lower limit again.
## Requests/Creator
https://github.com/halemmerich

57
apps/hralarm/settings.js Normal file
View File

@ -0,0 +1,57 @@
(function(back) {
var FILE = "hralarm.json";
var settings = Object.assign({
enabled: false,
upper: 180,
warning: 170,
lower: 150,
}, require('Storage').readJSON(FILE, true) || {});
function writeSettings() {
require('Storage').writeJSON(FILE, settings);
}
E.showMenu({
'': { 'title': 'HR Alarm' },
'< Back': back,
'Enabled': {
value: !!settings.enabled,
format: v => settings.enabled ? "On" : "Off",
onchange: v => {
settings.enabled = v;
writeSettings();
}
},
'Upper limit': {
value: settings.upper,
min: 0,
step:5,
max: 300,
onchange: v => {
settings.upper = v;
writeSettings();
}
},
'Lower limit': {
value: settings.lower,
min: 0,
step:5,
max: 300,
onchange: v => {
settings.lower = v;
writeSettings();
}
},
'Warning at': {
value: settings.warning,
min: 0,
step:5,
max: 300,
onchange: v => {
settings.warning = v;
writeSettings();
}
}
});
})

27
apps/hralarm/widget.js Normal file
View File

@ -0,0 +1,27 @@
(() => {
var settings = require('Storage').readJSON("hralarm.json", true) || {};
if (!settings.enabled){ Bangle.setHRMPower(0, 'hralarm'); return; }
Bangle.setHRMPower(1, 'hralarm');
var hitLimit = 0;
var checkHr = function(hr){
if (hr.bpm > settings.warning && hr.bpm <= settings.upper){
Bangle.buzz(100, 1);
}
if (hitLimit < getTime() && hr.bpm > settings.upper){
hitLimit = getTime() + 10;
Bangle.buzz(2000, 1);
}
if (hitLimit > 0 && hr.bpm < settings.lower){
hitLimit = 0;
Bangle.buzz(500, 1);
}
};
Bangle.on("HRM", checkHr);
Bangle.on("BTHRM", checkHr);
WIDGETS["hralarm"]={
area:"tl",
width: 0,
draw: function(){}
};
})()

BIN
apps/hralarm/widget.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -1 +1,2 @@
0.01: first release
0.02: Themeable app icon

View File

@ -1 +1 @@
require("heatshrink").decompress(atob("mEwwkBiIA/AH4A/AAkQgEBAREAC6oABdZQXkI6wuKC5iPUFxoXIOpoX/C6QFCC6IsCC6ZEDC/4XcPooXOFgoXQIgwX/C7IUFC5wsIC5ouCC6hcJC5h1DF9YwBChCPOAH4A/AH4Ap"))
require("heatshrink").decompress(atob("mEwwI0xg+evPsAon+ApX8Aon4AonwAod78AFDv4FWvoFE/IFDz4FXvIFD3wFE/wFW7wFDh5xBAoUfAok/Aol/BZUXAogA6A="))

View File

@ -6,4 +6,5 @@
0.06: Fix - Alarm disabled, if clock was closed.
0.07: Added settings to adjust data that is shown for each row.
0.08: Support for multiple screens. 24h graph for steps + HRM. Fullscreen Mode.
0.09: Tab anywhere to open the launcher.
0.09: Tab anywhere to open the launcher.
0.11: Added getting the gadgetbridge weather

View File

@ -1,5 +1,6 @@
const SETTINGS_FILE = "lcars.setting.json";
const Storage = require("Storage");
const weather = require('weather');
// ...and overwrite them with any saved values
@ -145,6 +146,14 @@ function printData(key, y, c){
text = "VREF";
value = E.getAnalogVRef().toFixed(2) + "V";
} else if (key == "Weather"){
text = "TEMP";
const w = weather.get();
if (!w) {
value = "ERR";
} else {
value = require('locale').temp(w.temp-273.15); // applies conversion
}
}
g.setColor(c);

View File

@ -18,14 +18,14 @@
storage.write(SETTINGS_FILE, settings)
}
var data_options = ["Battery", "Steps", "Temp.", "HRM", "VREF"];
var data_options = ["Battery", "Steps", "Temp.", "HRM", "VREF", "Weather"];
E.showMenu({
'': { 'title': 'LCARS Clock' },
'< Back': back,
'Row 1': {
value: 0 | data_options.indexOf(settings.dataRow1),
min: 0, max: 4,
min: 0, max: 5,
format: v => data_options[v],
onchange: v => {
settings.dataRow1 = data_options[v];
@ -34,7 +34,7 @@
},
'Row 2': {
value: 0 | data_options.indexOf(settings.dataRow2),
min: 0, max: 4,
min: 0, max: 5,
format: v => data_options[v],
onchange: v => {
settings.dataRow2 = data_options[v];
@ -43,7 +43,7 @@
},
'Row 3': {
value: 0 | data_options.indexOf(settings.dataRow3),
min: 0, max: 4,
min: 0, max: 5,
format: v => data_options[v],
onchange: v => {
settings.dataRow3 = data_options[v];

1
apps/limelight/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: first release

19
apps/limelight/README.md Normal file
View File

@ -0,0 +1,19 @@
# Limelight
*Simple configurable analogue clock based on the work of @Andreas_Rozek [Simple_Clock](https://github.com/espruino/BangleApps/tree/master/apps/simple_clock)*
![](screenshot_limelight.png)
* Selection of different fonts
* Settings menu where you can select font, or switch to Vector font and try a range of sizes
* Reduction by 100 lines of code, demonstrating that there is no need for a custom widget draw method
* Full screen option (widgets are loaded but not displayed)
![](screenshot_gochihand.png)
![](screenshot_monoton.png)
![](screenshot_grenadier.png)
Many thanks for @Andreas_Rozek for his pioneering work on building an analogue clock toolkit for the Bangle 2.
Limelight Written by: [Hugh Barney](https://github.com/hughbarney) For support and discussion please post in the [Bangle JS
Forum](http://forum.espruino.com/microcosms/1424/)

View File

@ -0,0 +1,263 @@
/*
* Limelight analoguce clock with bolted hands
* Based on the work of @Andreas_Rozek
* [Simple_Clock](https://github.com/espruino/BangleApps/tree/master/apps/simple_clock)
*
* . Demonstrates simpler approach to establishing the available size of the appRect in relation
* to widgets, avoids having to take on the responsibility for managing the widget draw.
* . Demonstrates a settings menu and various configuration options
* . Demonstrates fullscreen verses, widgets and app area.
*
*/
g.clear();
const SETTINGS_FILE = "limelight.json";
var UPDATE_PERIOD;
var drawTimeout;
function loadSettings() {
settings = require("Storage").readJSON(SETTINGS_FILE,1)||{};
settings.secondhand = settings.secondhand||false;
settings.font = settings.font||"Limelight";
settings.vector = settings.vector||false;
settings.fullscreen = settings.fullscreen||false;
settings.vector_size = settings.vector_size||42;
UPDATE_PERIOD = (settings.secondhand ? 1000 : 60000);
}
loadSettings();
// if we are not full screen then load and draw the widgets so that Bangle.appRect gets set
if (!settings.fullscreen) {
Bangle.loadWidgets();
Bangle.drawWidgets();
}
// fonts.google.com
Graphics.prototype.setFontLimelight = function(scale) {
// Actual height 28 (28 - 1)
g.setFontCustom(atob("AAAAAAAAAAAAAAAAAeAAAAAD8AAAAAf4AAAAB/gAAAAH+AAAAAf4AAAAB/gAAAAD8AAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAAPwAAAAH8AAAAD+AAAAD+AAAAB/AAAAA/gAAAAfwAAAAD4AAAAAMAAAAAAAAAAAAAAAAAAAAA/gAAAA//wAAAP//wAAB///wAAP///gAA///+AAH///8AAf///4AD////gAP///+AA////4AD////gAMAAAGAAwAAAYADAAABgAMAAAGAAwAAAwABgAADAAHAAAYAAOAADgAAeAA8AAAfh/AAAAf/wAAAAHgAAAAAAAAAAGAAAAAAYAAAAABAAAAAAMAAAAAAwAAAAAD///+AAf///4AB////gAH///+AAf///4AD////gAP///+AA////4AH////gAf///+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgABAAAeAAOAAH4AAwAA/gAGAAP+AAYAB/4ADAAf/gAMAD/+AAwAf/4ADAH//gAMA//+AAwH//4ADB//9gAOP//GAA///wYAD//+BgAH//gGAAf/8AYAA//ABgAB/4AGAAD+AAYAADAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAAAAAHAAAwAAGAAHAAAMAAYAAAwADAEABgAMAwAGAAwDAAYADAMABgAMAwAGAAwDAAYAD////gAP///+AA////4AD////gAP///8AAf///wAB/7//AAD/H/4AAP4f/AAAPA/4AAAAA+AAAAAAAAAAAAAAAAAAAGAAAAAB8AAAAAPwAAAADzAAAAAcMAAAAHgwAAAA8DAAAAHAMAAAB4AwAAAOADAAADwAMAAAcAAwAAD///+AA////4AD////gAP///+AA////4AD////gAP///+AA////4AD////gAP///+AAAAAMAAAAAAwAAAAADAAAAAAcAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAHAAAH4AOAAP/gAcAA+GAAwADAwABgAMDAAGAAwMAAYADAwABgAMDAAGAAwMAAYADA///gAMD//+AAwP//4ADA///AAMB//8AAwH//wADAP/+AAIAf/wAAAA/+AAAAB/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABgAAAAD/8AAAA//8AAAH//4AAB///wAAH///gAA////AAH///8AAf///4AD////gAP///+AAwAAAYADAYABgAMBgAGAAwGAAYADAYABgAMBgAGAAwGAAwABgYADAAHAwAYAAMDgHAAAAHh4AAAAP/AAAAAHgAAAAAAAAAAAAAAAA+AAAAAD4AAAAAMAAAAAAwAAAAADAAAAAAMAA/+AAwB//4ADB///gAM///+AA////4AD////gAP///+AA////4AD////gAP///+AA////4AD//+AAAP/wAAAA/wAAAAD4AAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAA/g/wAAH/GDgAA/+wGAAD//AMAAf/4AwAB//gBgAP//AGAAx/+AYADD/4BgAMH/wGAAwP/AYACA/+BgAMB/8GAAwD/wYADAP/jgAMBf/+AAYH//wABgz//AADHH/4AAH4f/gAAOA/8AAAAB/AAAAAAwAAAAAAAAAAAAAAAAAeAAAAAH/AAAAB8eAAAAGAcBgAAwAwHAAGABgMAAYAGAYADAAIBgAMAAwGAAwADAYADAAYBgAMAAgGAAwAAAYAD////gAP///+AA////wAB////AAH///4AAP///AAA///8AAA///AAAB//4AAAB/+AAAAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfAeAAAD8D8AAAf4f4AAB/h/gAAH+H+AAAf4f4AAB/h/gAAD8D8AAAHgHgAAAAAAAAAAAAAAA="), 46, atob("DQ0aExgZHRkbGBsbDQ=="), 40+(scale<<8)+(1<<16));
}
// fonts.google.com
Graphics.prototype.setFontGochiHand = function(scale) {
// Actual height 29 (31 - 3)
g.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAAAAAB4AAAAAD4AAAAAB4AAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAAAH/gAAAD//gAAD///gAD///+AAH///AAAH//gAAAH/wAAAAHwAAAAAAAAwAAAAAP+AAAAA//gAAAB//wAAAD//4AAAD8P4AAAHwD8AAAHgB8AAAPgA8AAAPAA8AAAPAA8AAAPAA8AAAPgA8AAAPgA8AAAPgB8AAAHwB4AAAH4D4AAAD+PwAAAD//gAAAB//gAAAA/+AAAAAP8AAAAAAAAAAAAAAAAAAAcAAAAAA8AAAAAB8AAAAAD4AAAAAD4AAAAAHwAAAAAHgAAAAAPgAAAAAPgAAAAAf/AAAAAf//wAAAP//wAAAH//wAAAAf/wAAAAAPgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAA8AAAeAB8AAA+AD8AAB+AH8AAB4AP8AAD4AP8AADwAf8AADwB+8AAD4D88AAD4H48AAD//w+AAB//g+AAB//A+AAA/8A+AAAPwA+AAAAAA+AAAAAAcAAAAAAIAAAAAAAAAA8AAAAAB8AAAAAB8AAAAAB4AHgAAD4AHwAAD4AH4AADwPH8AADwfB8AADwfA8AAD4fA+AAD4fA+AAB//A+AAB//A+AAA//A8AAA//x8AAAPP/8AAAAH/4AAAAD/wAAAAB/gAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAAAH8AAAAAP+AAAAA/+AAAAB/+AAAAD8+AAAAP4+AAAB/weAAAD/weAAAD/8eAAAB///AAAA///AAAAH//8AAAAf//AAAAD//AAAAAf/AAAAAf+AAAAAfAAAAAAMAAAAAAAAAAAAAAAAAAPA/gAAAfh/wAAA/x/4AAA/x/4AAB/4/8AAB74B8AAB58A8AAB58A+AAB5+A+AAB4+A+AAB4+A+AAB4fA+AAB4fg+AAB4Pg8AAB4P58AAB4H/4AAB4D/4AAB4D/wAAAQA/gAAAAAAAAAAAAAAAAAAAAAAAAAf8AAAAB/+AAAAD//gAAAH//gAAAPwfwAAAPgf4AAAfAf4AAAeA/8AAAeA98AAAeA88AAAfB48AAAfB48AAAPB48AAAOB48AAAAB98AAAAB/8AAAAA/4AAAAA/wAAAAAfgAAA8AAAAAA8BAAAAA8HgAAAA8HgAAAA8HgAAAA8HgAAAA8HgAAAA+HgAAAA+HgAAAA+HgAAAAfHgAAAAf//+AAAf//+AAAP//+AAAH//8AAAAPwAAAAAHgAAAAAHwAAAAAHwAAAAAHwAAAAADwAAAAADgAAAAAAAAAAAAAAAAAAAB/AAAAP3/wAAAf//wAAA///4AAA//D8AAB9+B8AAB4+A8AAB4+A+AAB4+A+AAB4+A+AAB8+A+AAB8+A+AAA/+A8AAA//A8AAAf/x8AAAP//4AAAH//wAAAAD/gAAAAB/AAAAAAAAAAAAAAAAAAD/AAAAAD/gAAAAH/gAAAAP/wAAAAPHwAAAAPDwAAAAeDwAAAAeDwAAAAeDwAAAAeHwAAAAeHgAAAAePgAAAAefAAAAAf/AAAAAf///gAAf///wAAP///wAAP///gAAH8AAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8BwAAAA8B4AAAA+D4AAAA8B4AAAAcB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="), 46, atob("DQoYERUWFBYVFhcVDQ=="), 42+(scale<<8)+(1<<16));
}
// free for commercial use
// https://www.1001fonts.com/search.html?search=Grenadier+NF
Graphics.prototype.setFontGrenadierNF = function(scale) {
// Actual height 39 (39 - 1)
g.setFontCustom(atob("AAAAAAAAAAAAB4AAAAAAPAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAEAAAAAAPgAAAAA/8AAAAB//gAAAD//4AAAP//wAAAf//AAAB//+AAAD//8AAAB//wAAAAP/gAAAAB+AAAAAAMAAAAAAAAAAAAAAAAB4AAAAAD/8AAAAB//4AAAA///wAAAP8D/AAAD8AD8AAA/AAPwAAPgAAfAAD4AAB8AAeAAAHgAHwAAA+AA8AAADwAHgAAAeAB4AAAB4APAAAAPAB4AAAB4APAAAAPAB4AAAB4APAAAAPAB4AAAB4APAAAAPAB4AAAB4APgAAAfAA8AAADwAHwAAA+AAeAAAHgAD4AAB8AAPgAAfAAA+AAHwAAD8AD8AAAP8D/AAAA///wAAAD//8AAAAH/+AAAAAD8AAAAAAAAAAAAAAAAAAABAAAAAAAcAAAAAAHwAAAAAB8AAAAAAfgAAAAAH/////AB/////4AP/////AB/////4AAAAAAAAAAAAAAAAAAAAADAAAAAAA4APAAAAPAB4AAAD4APAAAA/AB4AAAP4APAAAD/AB8AAA/4AHgAAPvAA8AAD54ADwAB+PAAfAAfh4AB8AH4PAAPwD+B4AA///gPAAD//wB4AAH/4APAAAP8AAAAAAAAAAAAAAAAAAAPAAAAHAB4AAAB4APAAAAPAB4PAAB4APB4AAPAB+/gAB4AH/8AAfAAf/wADwAB/fAA+AABD8APgAAAPwH4AAAA//+AAAAD//gAAAAH/4AAAAAP8AAAAAAAAAAAAAAAAAAAAAAYAAAAAAPAAAAAAH4AAAAAD/AAAAAB/4AAAAA//AAAAAf94AAAAH+PAAAAD/B4AAAB/gPAAAA/wB4AAAf8APAAAP////4AH/////AD/////4AAAAAPAAAAAAA4AAAAAAAAAAAAAAAAAAAIAAPAAAPAAB4AAf4AAPAA//gAB4AP/8AAPAB/3gAB4APg8AAfAB4DwADwAPAfAA+AB4B8APgAPAP4H4AB4A//+AAPAB//gAB4AH/4AAAAAP8AAAAAAAAAAAAAB4AAAAAD/4AAAAA//wAAAAf//AAAAP+H8AAAH+AHwAAB/gAfAAA/4AB4AAf+AAPAAP/wAA8AH+eAAHgB/ngAA8APw8AAHgB4HgAA8AMA8AAHgAADwAA8AAAeAAPgAAD4AB4AAAPgAfAAAA+AHwAAAH4D8AAAAf//AAAAB//wAAAAD/8AAAAAH8AAAAAAAAAAAAAAAAAAAAAAAYAAAAAAfAB4AAAP4APAAAH/AB4AAH/gAPAAD/wAB4AD/wAAPAB/4AAB4A/8AAAPA/8AAAB4f+AAAAPP+AAAAB//AAAAAP/gAAAAB/gAAAAAPwAAAAAB4AAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAD/gAAAAB//AAAAAf/+AAAAH//4AAAB+AfgAA+fAB8AAP/wAHwAD/+AAeAA//gAB4APj8AAPAB4PAAB4APB4AAPAB4PAAB4APB4AAPAB8fgAB4AH/8AAPAAf/wADwAB/eAA+AADj4APgAAAPwD8AAAA///AAAAD//wAAAAP/4AAAAAf8AAAAAAAAAAAB4AAAAAB/4AAAAA//wAAAAP//AAAAD+H8AAAA/AHwAAAHgAfAAAB8AB4AAAPAAHgAADwAA8AAAeAAHgBADwAAeA4AeAADwfADwAAeP4AeAAD3+ADwAA9/gAfAAH/wAB4AB/4AAPgAf8AAA+AH+AAAD8B/gAAAP//wAAAA//4AAAAD/8AAAAAD+AAAAAAAAAAAAAAAAAAAAAAAeB4AAAADwPAAAAAeB4AAAAAAAAAAAAAAAA=="), 46, atob("Bg4kChURExEaFBoaBg=="), 45+(scale<<8)+(1<<16));
}
// fonts.google.com
Graphics.prototype.setFontMonoton = function(scale) {
// Actual height 38 (37 - 0)
g.setFontCustom(atob("AAAAAAAAAAEkAAAAAbYAAAABtgAAAAG2AAAAAbYAAAABtgAAAAG2AAAAASQAAAAAAAAAAAAAAAAAAAB4AAAAB/gAAAB/gAAAB/h4AAB/h/AAB/h/CAB/h/D4B/h/D/B/j/D/APj/D/AAD/D/AAB/D/AAAPD/AAAAD/AAAAB/AAAAAPAAAAAAAAAAAAAAAAAAAAAD/wAAAB//4AAAfAD4AAHj/x4AA5//5wAHfAB5gAzj/5zAHc//5mAbvDBzcDdz/zmwNu+HzZhs3ADu2G2YAHbYbbAANthtsAA2yG2wABtsbbAAG2xtsAA2yG2wADbYbZgAdthm3ADs2DZv/92wM3P/O7AbvAD3YB3P/87gDvP/HMAHPgD7gAOP/+cAAfH+HgAAfgH4AAAf/+AAAAD+AAAAAAAAAAAAAAAAbYAAAABtgAAAAG2AAAAAbYAAAABt////wG3////AbYAAAABt////wG3////AbYAAAABt////wG3////AbYAAAABt////wEn///+AAAAAAAAAAAAAADQAAAUgdoAAF7BtsAA3sG2wAOewbbAB17BtsAc3sG2wDnewbbAdx7BtsHO3sG2w7newbbPd57Btv3OXsGzc73ewZuPc57A2f3nHsDMc5wewG8POB7Ac/zgHsA4Y8AewB+fABSAB/wAAAAAAAAAAAAAAAAA0AAAAsHbAAAbYbbAANthtsAA22G2wADbYbbJJNthts22W2G2zbZpIbbNtm2xts22bbG2zbZJIbbNttthtv2322Gzfbu7YNuO3HZg3f7P5sDuf2OMwGeHPHmAO/2f8wAccOOOAA/PfvwAA/4f8AAAAAAAAAAAAAAAAABkgAAAA/bAAAAPtsAAAD52wAAA8fbAAAfH9sAAHz42wAB8+fbAAePn9sADjx82wAJ8ePbAAfPj9sADz482wAI+fDbAAfHwNsADx8A2wAM+D/b+APgP9v4D4AA2wAMAD/b+AAAP9v4AAAA2wAAAADSAAAAAAAAAAAAAAAAAAAAMAaf/8AwBt//wJgG2AAA3Abf/8JsBt//w2wG2AABtgbf/822BtgADbYG2NvNtgbY28SSBtjbxtsG2NvG2gbY28SSBtjbzbYG2Nv9tgbY23m2BtjNg2YG2Gz+bAbYZnzcBtgzg5gG2Dn/MASQHHzgAAAPg8AAAAP/gAAAAHwAAAAAAAAAAAAAAAAA//4AAAf//8AAHgAB4AA4//44AGf//5wAzgAB7AGY//52AbP//7MDZwABmwNu//zZhs3//m2G25LTbYbbN7Nthts3sSSG2zexpIbbN7G2xts3sSaG2zezbYbbN7Nthts3v22GbDbezYNsG+HbA3Qbf7sBsB3edgGQDPHuAMAGf9wAQAOOOAAAAfvwAAAAf8AAAAAAAAAAAAAABtgAAAAG2AAAAAbYAAAABtgAAAAG2AAAAAbYAAAMBtgAAPwG2AAP8AbYAf4cBtgf4fwG0f4f4Aaf4f4cAf4f4fwH4f4f4AYf4f4cA/4/w/wHw/w/wAQ/w/wAA/w/wAAHw/wAAAQ/wAAAA/wAAAAHwAAAAAAAAAAAAAAAAAAAA+AfAAAf/P/gADwPwHgA5/OfnAHP+f/OAZwO4HYDM+d/MwNn+3/bBuwZsM2G2e2+bYbb7b9thtskk2yG2zbZtobbNtm2xts22bbG2zbZtsbbNtm2xts22bbG2zbZNsbbNttshtv2322GzfZu7YNuO3HZg3f7v5sBud3OcwHfPvHmAOf3P8wAcAeAOAAf///wAA/4f8AAAAAAAAAAAAAAAAfwAAAAH/wAAAA4DwAIAGfzgAwAz/3AJgGYDsA2AzP2YDsDZz9g2wNszbDNhs3ns22G2zezbYbbN5NthtsTkSSG2xORtMbbE5G2xts3sySG2zezbYbbN7Nths2ABm2Cbf/+3YNmf/nbAzeAB7MBuf/+dgHcP/DuAO///9wAc///OAA8AADwAA///8AAA///AAAAAAAAAAAAAAAAAAAAAAA2xtgAADbG2AAANsbYAAA2xtgAADbG2AAANsbYAAAkhJAAAAAAAAAAAAAAAA"), 46, atob("ChIiERcYGRwfGSAfCw=="), 40+(scale<<8)+(1<<16));
}
/*
* If only 1 widget is loaded at the top, then Bangle.appRect changes
* to report as if widgets were loaded at the bottom as well. The
* other option would be for Bangle.appRect to adjust for different
* combinations EG: no widgets, wigets on top, widgets on bottom and
* widgets on top and bottom areas, but it does not at present.
*
* Example of Bangle.appRect with 3 widges on the top, note h = 152, not 176
* ={ x: 0, y: 24, w: 176, h: 152, x2: 175, y2: 175 }
*
* With the example below we are going assume that the bottom widget
* space is not used.
*
*/
const CenterX = g.getWidth()/2;
const CenterY = (g.getHeight()/2) + (Bangle.appRect.y/2);
const outerRadius = (g.getHeight() - Bangle.appRect.y)/2;
if (settings.fullscreen) {
Bangle.loadWidgets();
/*
* We load the widgets as some like widpedom accumualte the step count.
* we are not drawing the widgets as we are taking over the whole screen
* so we will blank out the draw() functions of each widget and change the
* widgets area to the top bar doesn't get cleared.
*/
for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";}
}
function debug(o) {
//console.log(o);
}
debug("limelight.app.js");
debug("CenterX=" + CenterX);
debug("CenterY=" + CenterY);
debug("outerRadius=" + outerRadius);
debug("y12=" + (CenterY - outerRadius));
debug("y6=" + (CenterY + outerRadius));
let HourHandLength = outerRadius * 0.5;
let HourHandWidth = 2*5, halfHourHandWidth = HourHandWidth/2;
let MinuteHandLength = outerRadius * 0.7;
let MinuteHandWidth = 2*3, halfMinuteHandWidth = MinuteHandWidth/2;
let SecondHandLength = outerRadius * 0.9;
let SecondHandOffset = halfHourHandWidth + 10;
let outerBoltRadius = halfHourHandWidth + 2, innerBoltRadius = outerBoltRadius - 4;
let HandOffset = outerBoltRadius + 4;
let twoPi = 2*Math.PI, deg2rad = Math.PI/180;
let Pi = Math.PI;
let halfPi = Math.PI/2;
let sin = Math.sin, cos = Math.cos;
let sine = [0, sin(30*deg2rad), sin(60*deg2rad), 1];
let HandPolygon = [
-sine[3],-sine[0], -sine[2],-sine[1], -sine[1],-sine[2], -sine[0],-sine[3],
sine[0],-sine[3], sine[1],-sine[2], sine[2],-sine[1], sine[3],-sine[0],
sine[3], sine[0], sine[2], sine[1], sine[1], sine[2], sine[0], sine[3],
-sine[0], sine[3], -sine[1], sine[2], -sine[2], sine[1], -sine[3], sine[0],
];
let HourHandPolygon = new Array(HandPolygon.length);
for (let i = 0, l = HandPolygon.length; i < l; i+=2) {
HourHandPolygon[i] = halfHourHandWidth*HandPolygon[i];
HourHandPolygon[i+1] = halfHourHandWidth*HandPolygon[i+1];
if (i < l/2) { HourHandPolygon[i+1] -= HourHandLength; }
if (i > l/2) { HourHandPolygon[i+1] += HandOffset; }
}
let MinuteHandPolygon = new Array(HandPolygon.length);
for (let i = 0, l = HandPolygon.length; i < l; i+=2) {
MinuteHandPolygon[i] = halfMinuteHandWidth*HandPolygon[i];
MinuteHandPolygon[i+1] = halfMinuteHandWidth*HandPolygon[i+1];
if (i < l/2) { MinuteHandPolygon[i+1] -= MinuteHandLength; }
if (i > l/2) { MinuteHandPolygon[i+1] += HandOffset; }
}
/**** transforme polygon ****/
let transformedPolygon = new Array(HandPolygon.length);
function transformPolygon (originalPolygon, OriginX,OriginY, Phi) {
let sPhi = sin(Phi), cPhi = cos(Phi), x,y;
for (let i = 0, l = originalPolygon.length; i < l; i+=2) {
x = originalPolygon[i];
y = originalPolygon[i+1];
transformedPolygon[i] = OriginX + x*cPhi + y*sPhi;
transformedPolygon[i+1] = OriginY + x*sPhi - y*cPhi;
}
}
/**** draw clock hands ****/
function drawClockHands () {
let now = new Date();
let Hours = now.getHours() % 12;
let Minutes = now.getMinutes();
let Seconds = now.getSeconds();
let HoursAngle = (Hours+(Minutes/60))/12 * twoPi - Pi;
let MinutesAngle = (Minutes/60) * twoPi - Pi;
let SecondsAngle = (Seconds/60) * twoPi - Pi;
g.setColor(g.theme.fg);
transformPolygon(HourHandPolygon, CenterX,CenterY, HoursAngle);
g.fillPoly(transformedPolygon);
transformPolygon(MinuteHandPolygon, CenterX,CenterY, MinutesAngle);
g.fillPoly(transformedPolygon);
let sPhi = Math.sin(SecondsAngle), cPhi = Math.cos(SecondsAngle);
if (settings.secondhand) {
g.setColor(g.theme.fg2);
g.drawLine(
CenterX + SecondHandOffset*sPhi,
CenterY - SecondHandOffset*cPhi,
CenterX - SecondHandLength*sPhi,
CenterY + SecondHandLength*cPhi
);
}
g.setColor(g.theme.fg);
g.fillCircle(CenterX,CenterY, outerBoltRadius);
g.setColor(g.theme.bg);
g.drawCircle(CenterX,CenterY, outerBoltRadius);
g.fillCircle(CenterX,CenterY, innerBoltRadius);
}
function setNumbersFont() {
if (settings.vector) {
g.setFont('Vector', settings.vector_size);
return;
}
if (settings.font == "GochiHand")
g.setFontGochiHand();
else if (settings.font == "Grenadier")
g.setFontGrenadierNF();
else if (settings.font == "Monoton")
g.setFontMonoton();
else
g.setFontLimelight();
}
function drawNumbers() {
g.setColor(g.theme.fg);
setNumbersFont();
g.setFontAlign(0,-1);
g.drawString('12', CenterX, CenterY - outerRadius);
g.setFontAlign(1,0);
g.drawString('3', CenterX + outerRadius, CenterY);
g.setFontAlign(0,1);
g.drawString('6', CenterX, CenterY + outerRadius);
g.setFontAlign(-1,0);
g.drawString('9', CenterX - outerRadius,CenterY);
}
function draw() {
g.setColor(g.theme.bg);
g.fillRect(Bangle.appRect);
drawClockHands();
drawNumbers();
queueDraw();
}
// schedule a draw for the next minute
function queueDraw() {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = setTimeout(function() {
drawTimeout = undefined;
draw();
}, UPDATE_PERIOD - (Date.now() % UPDATE_PERIOD));
}
// Stop updates when LCD is off, restart when on
Bangle.on('lcdPower',on=>{
if (on) {
draw(); // draw immediately, queue redraw
} else { // stop draw timer
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
}
});
Bangle.setUI('clock');
draw();

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("lksgIqngf/wAFC//+AgUch/4AgMBwAQEh/8Dgf/4AKOEAQKCAYUB//gAoU/DQkPBQYVBGx5SDBQIbDBR0GEAlgFYcHGwh4B+CDHRwL04"))

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 B

View File

@ -0,0 +1,78 @@
(function(back) {
const SETTINGS_FILE = "limelight.json";
// initialize with default settings...
let s = {
'vector_size': 42,
'vector': false,
'font': "Limelight",
'secondhand': false,
'fullscreen': false
}
// ...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 || {}
// copy settings into variable
for (const key in saved) {
s[key] = saved[key]
}
function save() {
settings = s
storage.write(SETTINGS_FILE, settings)
}
var font_options = ["Limelight","GochiHand","Grenadier","Monoton"];
E.showMenu({
'': { 'title': 'Limelight Clock' },
'< Back': back,
'Full Screen': {
value: s.fullscreen,
format: () => (s.fullscreen ? 'Yes' : 'No'),
onchange: () => {
s.fullscreen = !s.fullscreen;
save();
},
},
'Font': {
value: 0 | font_options.indexOf(s.font),
min: 0, max: 3,
format: v => font_options[v],
onchange: v => {
s.font = font_options[v];
save();
},
},
'Vector Font': {
value: s.vector,
format: () => (s.vector ? 'Yes' : 'No'),
onchange: () => {
s.vector = !s.vector;
save();
},
},
'Vector Size': {
value: s.vector_size,
min: 24,
max: 56,
step: 6,
onchange: v => {
s.vector_size = v;
save();
}
},
'Second Hand': {
value: s.secondhand,
format: () => (s.secondhand ? 'Yes' : 'No'),
onchange: () => {
s.secondhand = !s.secondhand;
save();
},
}
});
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -14,3 +14,5 @@
0.12: Fixed nl_NL formatting, because the full months won't fit on the Bangle.js2's screen
0.13: Now use shorter de_DE date format to more closely match other languages for size
0.14: Added some first translations for Messages in nl_NL
0.15: Fixed sv_SE formatting, long date does not work well for Bangle.js2
Added Swedish localisation with English text

View File

@ -276,13 +276,31 @@ var locales = {
temperature: "°C",
ampm: { 0: "fm", 1: "em" },
timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" },
datePattern: { 0: "%A %B %d %Y", "1": "%Y-%m-%d" }, // söndag 1 mars 2020 // 2020-03-01
datePattern: { 0: "%b %d %Y", "1": "%Y-%m-%d" }, // feb 1 2020 // 2020-03-01
abmonth: "jan,feb,mars,apr,maj,juni,juli,aug,sep,okt,nov,dec",
month: "januari,februari,mars,april,maj,juni,juli,augusti,september,oktober,november,december",
abday: "sön,mån,tis,ons,tors,fre,lör",
day: "söndag,måndag,tisdag,onsdag,torsdag,fredag,lördag",
trans: { yes: "ja", Yes: "Ja", no: "nej", No: "Nej", ok: "ok", on: "on", off: "off" }
},
"en_SE": { // Swedish localisation with English text
lang: "en_SE",
decimal_point: ",",
thousands_sep: ".",
currency_symbol: "kr",
int_curr_symbol: "SKR",
speed: 'kmh',
distance: { "0": "m", "1": "km" },
temperature: '°C',
ampm: { 0: "", 1: "" },
timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" },
datePattern: { 0: "%B %d %Y", "1": "%Y-%m-%d" }, // March 1 2020 // 2020-03-01
abmonth: "Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec",
month: "January,February,March,April,May,June,July,August,September,October,November,December",
abday: "Sun,Mon,Tue,Wed,Thu,Fri,Sat",
day: "Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday",
// No translation for english...
},
"en_NZ": {
lang: "en_NZ",
decimal_point: ".",

View File

@ -1 +1,2 @@
0.01: New menu!
0.02: Clean up touch handler in setUI

View File

@ -1,8 +1,5 @@
E.showMenu = function(items) {
g.clearRect(Bangle.appRect); // clear screen if no menu supplied
// clean up back button listener
if (Bangle.backHandler) Bangle.removeListener('touch', Bangle.backHandler)
delete Bangle.backHandler;
if (!items) {
Bangle.setUI();
return;
@ -206,8 +203,13 @@ E.showMenu = function(items) {
if (b===1) back();
}
}
// note: backHandler is cleaned up at the top of this file
Bangle.on('touch', Bangle.backHandler);
}
return l;
};
// setUI now also needs to clear up our back button touch handler
Bangle.setUI = (old => function() {
if (Bangle.backHandler) Bangle.removeListener("touch", Bangle.backHandler);
delete Bangle.backHandler;
return old.apply(this, arguments);
})(Bangle.setUI);

View File

@ -83,7 +83,7 @@ function getMessageImage(msg) {
if (s=="calendar") return atob("GBiBAAAAAAAAAAAAAA//8B//+BgAGBgAGBgAGB//+B//+B//+B9m2B//+B//+Btm2B//+B//+Btm+B//+B//+A//8AAAAAAAAAAAAA==");
if (s=="facebook") return getFBIcon();
if (s=="hangouts") return atob("FBaBAAH4AH/gD/8B//g//8P//H5n58Y+fGPnxj5+d+fmfj//4//8H//B//gH/4A/8AA+AAHAABgAAAA=");
if (s=="instagram") return atob("GBiBAf////////////////wAP/n/n/P/z/f/b/eB7/c87/d+7/d+7/d+7/d+7/c87/eB7/f/7/P/z/n/n/wAP////////////////w==");
if (s=="instagram") return atob("GBiBAAAAAAAAAAAAAAAAAAP/wAYAYAwAMAgAkAh+EAjDEAiBEAiBEAiBEAiBEAjDEAh+EAgAEAwAMAYAYAP/wAAAAAAAAAAAAAAAAA==");
if (s=="gmail") return getNotificationImage();
if (s=="google home") return atob("GBiCAAAAAAAAAAAAAAAAAAAAAoAAAAAACqAAAAAAKqwAAAAAqroAAAACquqAAAAKq+qgAAAqr/qoAACqv/6qAAKq//+qgA6r///qsAqr///6sAqv///6sAqv///6sAqv///6sA6v///6sA6v///qsA6qqqqqsA6qqqqqsA6qqqqqsAP7///vwAAAAAAAAAAAAAAAAA==");
if (s=="mail") return getNotificationImage();

1
apps/mmind/ChangeLog Normal file
View File

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

31
apps/mmind/README.md Normal file
View File

@ -0,0 +1,31 @@
# Mastermind
Play the classic mind game mastermind on your Bangle 2.
![](screenshot_mmind.png)
## Game
The game will start when run.
Four colors pins are randomly chosen and kept secret.
You need to find the secret by scoring your choice within 6 turns.
The game makes use of touch features.
## Play
Select one of the dots, the color menu will show, select a colour for the pin.
If all pins are chosen with a color the red button will turn green.
Hit the green button and your play will be scored and listed from the top.
The first digit shows the number of pins with the correct color and in the right place.
The second digit gives the number of pins with the correct color but in the wrong place.
There are six turns to get the correct secret.
The blue button will start a new game.
## Requests
This is the first version, things to add are:
Add a menu to change game options like the number of colors, allow double colors, 5 pins per row. Add feature to drag screen up and down to see more scores. Timer and high score.
Any other fearures or remarks, let me know @psbest.
## Creator
This game is created by Peter Slendebroek.

198
apps/mmind/mmind.app.js Normal file
View File

@ -0,0 +1,198 @@
//MMind
//set vars
const H = g.getWidth();
const W = g.getHeight();
var touch_actions = [];
var cols = ["#FF0000","#00FF00","#0000FF", "#FF00FF", "#FFFF00", "#00FFFF", "#000000","#FFFFFF"];
var turn = 0;
var col_menu = false;
//pinsRow = 6;
//pinsThick = 10;
//pinsRow = 5;
//pinsThick = 10;
var pinsRow = 4;
var pinsThick = 10;
var play = [-1, -1, -1, -1];
var pinsCol = 5;
var playx = -1;
var sx = (W - 30 )/pinsRow;
var sy = (H - 20 )/7;
var touch_actions = [];
var secret = [];
var secret_no_dub = true;
var endgame = false;
g.clear();
g.setColor("#FFFFFF");
g.fillRect(0, 0, H, W);
g.setFont("Vector12",45);
function draw() {
touch_actions = [];
g.clear();
g.setColor("#FFFFFF");
g.fillRect(0, 0, H, W);
g.setColor("#000000");
//draw scores
for (y=0;y<game.length;y+=1) {
pp = game[y][0];
ps = game[y][1];
g.setColor("#000000");
//g.fillRect(W-30,10, W-30, 15);
g.setColor("#000000");
g.setFont("Vector30",10);
g.drawString(ps[0],W-31,y*sy+8);
g.setColor("#000000");
g.drawString(ps[1],W-15,y*sy+8);
g.setColor("#000000");
for (x=0;x<pinsRow;x+=1) {
xx = sx*x + pinsThick + 5;
yy = sy*y+20;
xc = pp[x];
g.setColor(cols[xc]);
g.fillCircle(xx,yy , pinsThick );
g.setColor("#000000");
g.drawCircle(xx,yy,pinsThick+1);
g.drawCircle(xx,yy,pinsThick);
}
}
//draw play input
for (k=0; k<pinsRow; k+=1){
xx = sx*k + pinsThick + 5;
yy = sy*7;
touch_actions.push([[xx-pinsThick-5, yy-pinsThick-10, xx+pinsThick+5, yy+pinsThick+20],[1,k]]);
if (play[k] < 0) {
//col not choisen, draw small dot
g.setColor("#000000");
g.fillCircle(xx,yy , 3 );
} else {
g.setColor(cols[play[k]]);
g.fillCircle(xx,yy , pinsThick );
}
}
// draw action button
// score, men
if (!endgame) {
if (col_menu) {
draw_col_choice();
} else {
// check if all pins are set yet
if (Math.min.apply(null,play) < 0) {
g.setColor("#FF0000");
} else {
g.setColor("#00FF00");
touch_actions.push([[W-30, H-30,192, 190], [3,1]]);
}
g.fillRect(W-30, H-30, W-1, H-10);
}
} else {
g.setColor("#0000FF");
touch_actions.push([[W-30, H-30,192, 190], [4,1]]);
g.fillRect(W-30, H-30, W-1, H-10);
}
}
function get_secret() {
//secret=[];
for (i=0; i<pinsRow; i+=1) {
s = Math.round(Math.random()*pinsCol);
if (secret_no_dub)
while(secret.indexOf(s) >= 0) s = Math.round(Math.random()*pinsCol);
secret[i]= s;
}
}
function score() {
bScore = 0;
wScore = 0;
for (i=0; i<pinsRow; i+=1) {
if (secret[i] == play[i]) {
bScore +=1;
}
else {
for (s=0; s<pinsRow; s+=1) {
if (secret[i] == play[s]) {
wScore +=1;
break;
}
}
}
}
return([bScore, wScore]);
}
function draw_col_choice(){
var cc = g.getColor();
var boxw = 30;
var boxh = H/pinsRow-20;
for (i=0; i<=pinsCol; i+=1) {
g.setColor(cols[i]);
g.fillRect(W-boxw, i*boxh, W-1, i*boxh+boxh);
touch_actions.push([[150, i*boxh, 191, i*boxh+boxh], [2,i]]);
g.setColor(cc);
}
}
Bangle.on('touch', function(zone,e) {
//console.log(e.x, e.y);
// check touch actions array to see what to do
for(i=0; i<touch_actions.length; i+=1) {
if (e.x > touch_actions[i][0][0] && e.x < touch_actions[i][0][2] &&
e.y > touch_actions[i][0][1] && e.y < touch_actions[i][0][3]) {
// a action is hit, add acctions here, todo: start, stop, new, etc.
switch (touch_actions[i][1][0]) {
case 1:
//get pins col menu
col_menu = 1;
playx = touch_actions[i][1][1];
break;
case 2:
//copy choice col to play
play[playx] = touch_actions[i][1][1];
col_menu = 0;
break;
case 3:
//score play
var sc;
sc = score();
game.push([play, sc]);
play = [-1,-1,-1,-1];
turn+=1;
if (turn==6 || sc[0]==pinsRow) {
play = secret;
col_menu = 0;
endgame = true;
}
break;
case 4:
//new game
play = [-1,-1,-1,-1];
game = [];
endgame=false;
break;
}
}
}
//console.log(touch_actions[i][1][0], touch_actions[i][1][1]);
draw();
}
);
game = [];
get_secret();
draw();
//Bangle.loadWidgets();
//Bangle.drawWidgets();

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

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwxH+64A/AEOBq2sBAusqwJHCaQFDAYlP2m0yGBCIkSj0eiWHBIkDgsFgYTE01v3O5t4mC1krgAEBq0ACYQuCAANsHIcxFwIwCEocsFwIwCBIYuCAANQF4QwBOgQABAgNIF4ZgELwQvCHIcCF4cEKwYvEt45DF4QwCL5YvFL5ITDF6OstheCvTjEjAuBjDJFX4UEq4TEyguBygTEF4dWBIeskkkqwQDDgUGgwaEBIUBgITHkslCYeBd4MrqwDBAgIuBcwRVGNIVs0oJEv3S6V+CYmIisjkcVZAYpBgDyBAAJFBFwTlGZIolDqouBGAQJDFwQABmRfCFAICCGwXXhgvDMAheCfI1UF4eoKwYvEiovHSoJfLF4pfJCYYvN1gwBAYMSLwVcbQmQFwOQZIq/C1GACYkcFwMcCYQoCLYNWF4KPBDgNWmIkEBIVPp5TDBIdWqoTHmUyCYlWRQTwCD4wA/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AHmy2QJH6PRBI/Q6AkOCAIAFBINDjwABGInR3+53O/GIu72gABGJnQCAQAE69oFwQABCYfFFwIwCBIfCDIe7FIus1gvXLwQACLw4aCAAkAgAvcL4gvLq1WF5uyFwdoCYfLF4fLDpHCX6owBtFoxoUF6PF4ruFDwPC4XJFxbSCAAwVNAH4ARA"))

17
apps/mmind/mmind.info Normal file
View File

@ -0,0 +1,17 @@
{
"id": "mmind",
"name": "Classic Mind Game",
"shortName":"Master Mind",
"icon": "mmind.png",
"version":"0.01",
"description": "This is the classic game for masterminds",
"type": "game",
"tags": "mastermind, game, classic",
"readme":"README.md",
"supports": ["BANGLEJS2"],
"allow_emulator": true,
"storage": [
{"name":"mmind.app.js","url":"mmind.app.js"},
{"name":"mmind.img","url":"mmind.icon.js","evaluate":true}
]
}

BIN
apps/mmind/mmind.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 712 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -8,3 +8,4 @@
0.08: Added dependancy on MyLocation
0.09: Added dependancy on Pedometer Widget
0.10: Added Weather line, fixed issues on a Bangle 1, update every minute
0.11: Changed cycle on minute to prevInfo to avoid the 2nd one being the blank line

View File

@ -255,7 +255,7 @@ function queueDraw() {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = setTimeout(function() {
drawTimeout = undefined;
nextInfo();
prevInfo();
draw();
}, 60000 - (Date.now() % 60000));
}

View File

@ -1,2 +1,3 @@
0.00: Initial check-in.
0.01: Add tap-to-decorate feature. Bugfixes.
0.02: Add autorotate while charging. Remove autolight. Tweak fonts. Add some haptic feedback on touchscreen operations.

View File

@ -20,8 +20,7 @@ you can quickly alter the number of hands on the display. When the watch i
or down to remove the distraction. There's also a setting that displays the second hand, but only if the watch is perfectly face-to-the-sky,
in case you want the ability to check the _exact_ time, hands free, without the impact on battery life this usually entails.
In some versions of the Bangle.js firmware, the backlight doesn't come on automatically when you twist your wrist. There's currently a workaround
for this integrated into the watchface; you can disable it in the menu, if you prefer.
While charging the main display automatically rotates so that noon is up. This can be disabled.
## Limitations

View File

@ -25,8 +25,8 @@
//
// This only works for Bangle 2.
const isString = x => typeof x === 'string';
const imageWidth = i => isString(i) ? i.charCodeAt(0) : i.width;
const isString = x => typeof x === 'string',
imageWidth = i => isString(i) ? i.charCodeAt(0) : i.width;
//////////////////////////////////////////////////////////////////////////////
/* System integration */
@ -115,9 +115,9 @@ class RoundOptions extends Options {
onchange: x => this.calendric = x,
format: x => ['none', 'day', 'date', 'both', 'month', 'full'][x],
},
'Auto-Illum.': {
init: _ => this.autolight,
onchange: x => this.autolight = x
'Autorotate': {
init: _ => this.autorotate,
onchange: x => this.autorotate = x
},
Defaults: _ => {this.reset(); this.interact();}
});
@ -133,7 +133,7 @@ RoundOptions.defaults = {
calendric: 5,
dayFg: '#fff',
nightFg: '#000',
autolight: true,
autorotate: true,
};
//////////////////////////////////////////////////////////////////////////////
@ -144,29 +144,29 @@ const dec = x => E.toString(heatshrink.decompress(atob(x)));
const y10F = [
dec(
'g///EAh////AA4IIBgPwgE+gAOBg/AngXB+EPAYM8gfggEfgF8D4OAj4dB8EDAYI' +
'fBBAISBAAMOAYUB4AECnEAkAuBgEQBAPgIYX8IYX/wYDCEwIiMMgUYgECCIZlBAY' +
'N4CoRUBIoMP8AZBge8CgMB8+BCAPw+F/gf8jxDB/0D4BGBEQMPAYIeBoAfBnEwge' +
'Ah0cB4MDx4PBgHn4EB8E7LQM8h/eJ4MDBgIpB+H+g/wnE/WwMMO4P8LwM/XAJLBT' +
'gY7BAAN/wC9CQwV+jwDB/4pBgP/EQKYBBIIxBPQP+SATfCIYIiCO4I9BBwM//hlB' +
'PQJlCwYGBTAPgIgM4CYM8hwKBMoODegPA8F+gZlBewP4hz/BE4QrBGgM/LAV//4+' +
'BAYJyBPwM/KQMeGQMPFwM8H4UHBIPwGQNwn4yBnhxBGQJxBGQK5BGQKWDOwUACAM' +
'D/BDCNYPg///8E5HwR2BIwMDSgK0FSocMAYTLBAAYpBQAPnDwJGBEwK+B/hlB+F8' +
'TARABTAJABTAPBMoR+BMoKXBDoX5DwIuBMoUPS4THCGwJbBhAaBvh5B+EHwPAOwP' +
'guA1BvCcB4E8nxlBn1/VoIyBwDKBO4SGCgA='
'fBBAISBAAMOAYUB4AECnEAkAuBgEQBAPgIYX8IYX/wYDCEwIiMMgUcgECCIZlBAY' +
'N4CoRUBIoMP8AZBge8MoMB8+B8B4B+E/gf4jw/B/kD4ADBEQMPSYXgoAfBnEwgeA' +
'hw7BvEDx4PBgHn4EB8E7LQM8h/eJ4MDBgIpB+H+g/wnE/WwMMG4ReBn4zBJYKcDH' +
'4IABv+AXoSGCv0eAYP/FIMB/4iBTAIJBGIJ6B/yQCb4RDBEQTlBHoIOBn51BwC+B' +
'MoWDAwKYBRgKYBCYM8hwKBMoODegPA8F+gZlBewP4hz/BE4QrBGgM/LAV//4+BAY' +
'JyBPwM/KQMeGQMPFwM8H4UHBIPwGQNwZgPwnhxBGQJxBGQK5BGQKWDOwUACALlBI' +
'YRrB8H///gnI+COwJGBgaUBWgqVDhgDCZYIADFIKAB84eBIwImBXwP8MoPwviYCI' +
'AKYBIAKYB4JlCPwJlBS4IdC/IeBFwJlCh6XCY4Q2BLYMIDQN8PIPwg+B4B2B8FwG' +
'oN4TgPAnk+MoM+v6tBGQOAZQJ3CQwUAA'
), 48, dec('hgAI'), 34
];const y1F = [
dec(
'g//AAPggE/AoX8gF/AoX+gF8CoU+gHwAoUPgAZBEIQFGCIodFFIo1FIIoADnAFEj' +
'gFEh0AhA1EiAFCgeAFIf/4A1DFQIED/5MDGB6OEjAECHIIYDhkAuAFCjwFEj6DEn' +
'+AAod74AFD/PgvAtC+Hwv/wgZSBvEfLwc8RISOBGAJsBVAXgggEBE4PgIgJLC8E8' +
'I4fgXQS/B8IhBGwOA8YFCgfA9+eAoMB4H/j/ACIPA/kPCQJCB/DMDMoMBboYVBKo' +
'IDBSYeAAoYlCAATpEg/4Xwc/QIcPFoJcBQIP8GILXCDYLXBbId//BeCL4QwDgIwD' +
'AAIXBDAQfCEYSPBAoaPCPQKPCAoZgBAoYvBAoIXBBAIFB/ALDEoJHBAoaPDaQSPB' +
'AoKcBJgY9DTQX/EoKmCC4SyCYYJJB+CHBj+Aj8ASYJNBBINwIIOAM4ILDAYN/wAB' +
'BB4JBBI45vCRYgADApEHL4pHB8AECFIPhAYLCCAggFBAgaNCYwgFEbAkAwAFEc4S' +
'PCj/+LIKPBv6PEAoRnBFIMDFYLXCKoTLDa4YRDBYIdDh4FDMoQ1DK4ZBBMQIDBJY' +
'bWBFIMEIIQpBgxxBgZRBh8AAYN8AoQVBjgbBAoTZBvwRCvEBF4IdB+E/OIp9CJgZ' +
'BCQQUAA='
'g//AAPggE/AoX8gF/AoX+gF8CoU+gHwAoUPgAZBEIQFGCIodFFIo1FIIoADnEAgQ' +
'FCjkAgwFCh0Ahg1EBoIABgeAFIf/4A1DFQIED/5MDGAYADEQYwDRwgMDhAYEH4Nw' +
'AoUeAok/QYl/wAFD/fAHgUD+PgvAFBj/g+E/4EBLAN4j5SCgE8h4EB/AwCAoOAVA' +
'PgggeBFoPgQgRLB8E8I4fgXQS/B8KwBMgOA8YFCgfA9+eAoMB4H/j/ACIPA/kPCQ' +
'JIB/DMDMoJSBboQVBKoIDBSYZOBAAQlCAATpEg/4Xwc/QIZyBwBcBgf//gxBa4Qb' +
'Ba4LZDv/4LwRfCGAcBGAYABC4IYCD4QjCR4IFDR4R6BR4QFDMAIFDF4IFBC4IIBA' +
'oLEBBYQlBI4IFDR4ZrBR4QFBTgJMDHoaaCdQSmCC4SyCYYJJB+CHBj+Aj8ASYJNB' +
'BINwIIOAM4ILDAYN/wABBB4JBBI45vCRYgADApEHL4pHB8AECFIPhAYLCCAggFBA' +
'gaNCYwgFEbAkAwAFEc4SPCj/+LIKPBv6PEAoRnBFIMDFYLXCKoTLDa4YRDBYIdDh' +
'4FDMoQ1DK4ZBBMQIDBJYbWBFIMEIIQpBgxxBgZRBh8AAYN8AoQVBjgbBAoTZBvwR' +
'CvEBF4IdB+E/OIp9CJgZBCQQUAA='
), 48, dec('hgAI'), 48
];const y10sF = [
dec(
@ -194,20 +194,20 @@ const y10F = [
];const d1F = [
dec(
'AB1/+AECj///4FCAgP/8EAgf/4F//EAg4CBgf8gEPwAUBn0AhwaCAYMeAoUPgEcA' +
'oUHAowRFDoopFGopBFJopZGBgIKCABlAIIcA4AFDgIFEgZBCAoMHAohVBAoY6CHg' +
'U/Aol/AogADGoQFUABEMAQM/AQN8bIRZBRgJ5BLILhBgP3LIcD84rDg/HWYcPw4F' +
'Dj4PBAoU+Aol8Aon4PocB+CJDgfgAoXgh/ATYX4v+AU4X//w/DbYQFCCwJ3PvDIE' +
'NYQCCdoJ6CgfAiCGCI4NwgEeFwISCLoMeJwJdCnkfHYd4v4FD+f5AoUB9/BAoUD/' +
'4jCh8HG4IpCh5DBAIMeE4Q/BvjMCfoP8Z4Uf//wCgInB/5lCABs+AoicBAAUDAok' +
'P9wFDv+OCAjUCHQP4AoY5BAoUHEIIFCv5JBAoLQBLQYqEApQpDArIAJv5IBnBTCV' +
'4McJAQFBcYLvBB4IkBd4N4cYQBBeoLdBCYIFDngFECoIFDOwIdCc4QpCFwIZCjwu' +
'BEoU8FwIxCvAIBEIPB+AUBJIP/8AmBLYWAd4RnBdx4XCcYf/Dgn//AuEP4LjBXoJ' +
'AC//vQYT0BBIKDC+CZBOIM/wAFDVYIFCgIrBAoUDPoIdCO4QnBaQYnBGoQVBIIZI' +
'CJoTNCLIY4CAYIaDAAKRCAASRDAAIaEYAQtDYAI5DRgZFCAAYuCQoQuBAgIFBvEH' +
'AgIFB+CgBAAMB86lE76EBFwX/GocPNoYmBIwk/HQl8LpIAQRId/SoYDB4ZJCUoPn' +
'VoUHwP3Y4YYBY4k+Y4h5BdILhBd4YFFCIodFFIo1FIIpNFLIplGAArMFn6oBHYMA' +
'DYQFBgP5E4IFBgfgUgIFCwBZBEAL1BPYZbDA4Z7DLYRtCBYYlDBoIxCEYMBHoIvC' +
'HAI7Dh5PBI4X/LIX//7+Dn52Eh4QCA=='
'oUHAowRFDoopFGopBFJopZGBgIKCAB5BBgA1CAoMBAokDCIgTCAYRTDAoI6CHgU/' +
'Aol/Aog1GAqgAIhgCBn4CBvjZCLIKMBPIJZBcIMB+4lBMoMD84rDg/HL4cPw4FDj' +
'5rEnwFEvgFE/AFBaYMB+CJCwED8AFC8EP4CbC/F/wCnC//+H4bbCAoQWBO594EAI' +
'TBgBrCAQTtBPQUD4EQQwRHBuEAjwuBCQRdBjxOBLoU8j47DvF/Aofz/IFCgPv4IF' +
'Cgf/EYUPg43BFIUPIYIBBjwnCH4N8ZgT9B/jPCj//+AUBE4P/MoQANnwFETgIACg' +
'YFEh/uAod/xwQEagQ6B/AFDHIIFCg4hBAoV/JIIFBaAJaDFQgFKFIYFZABN/JAM4' +
'KYSvBjhICAoLjBd4IPBEgLvBvDjCAIL1BboITBAoc8AogVBAoZ2BDoTnCFIQuBDI' +
'UeFwIlCnguBGIV4BAIhB4PwCgJJB//gEwJbCwDvCM4LuPC4TjD/4cE//4Fwh/BcY' +
'K9BIAX/96DCegIJBQYXwTIJxBn+AAoarBAoUBFYIFCgZ9BDoR3CE4LSDE4I1CCoJ' +
'BDJARNCZoRZDHAQDBDQYABSIQACSIYABDQjACFobABHIaMDIoQADFwSFCFwIEBAo' +
'N4g4EBAoPwUAIABgPnUonfQgIuC/41Dh5tDEwJGEn46EvhdJACCJDv6VDAYPDJIS' +
'lB86tCg+B+7HDDALHEnzHEPILpBcILvDAooRFDoopFGopBFJopZFMowAFZgs/VAI' +
'7BgAbCAoMB/InBAoMD8CkBAoWALIIgBeoJ7DLYYHDPYZbCNoQLDEoYNBGIQjBgI9' +
'BF4Q4BHYcPJ4JHC/5ZC///fwc/OwkPCAQA=='
), 48, dec('ikPigAGA'), 48
];const dowF = [
dec(
@ -220,10 +220,10 @@ const y10F = [
'kDMIgeBFIQEBBYRTBCAZ3FAggAMg4zEj7LEn7LEv++AodzxwFD+ePAofjw4FVDoo' +
'pFv+eIImcJomYLImAAoZeEAtTyBAAQFEVYIFDSQIvhAojaCFwgABh4YEngFEuAqJ' +
'gPAAocDApYuEgP/fgl/+B9HAAv+Aon8HQMOIAkeAokcAohaDAoM4Aol4AohmDAoJ' +
'BDAoJsDAo7vhABbJDAo9/AojEFMYbKMArCBDFI41FWIYABggFEgbuCDYMPLIQbBj' +
'//wBdCn0H4DZCvEBb4YZBdYZBBAofgCIQFDDoIFFDoPggYFBF4IFBGoI7B+AFCE4' +
'NwCIIlCuAdBIYU4gPwn5VBjC7B/y0Dv/4YwcPCwMAjJlCAAM584FDufDCAUA8eBA' +
'p/zC4n5EYj1BAoc//4RDU4IFDA=='
'BDAoJsDAo7vhABZuBQYoFDv4FEYgpjDZRgFYGYYpHGoqxDAAMEAokDdwQbBh//DY' +
'cf/+ALoU+g/AbIV4gLfDDILrDIIIFD8ARCAoYdBAoodB8EDAoIvBAoI1BHYPwAoQ' +
'nBuARBEoVwDoJDCnEB+E/KoMYXYP+Wgd//DGDh4WBgEZMoQABnPnAodz4YQCgHjw' +
'IFP+YXE/IjEeoIFDn//CIanBAoY='
), 48, dec('kElkMljsljw='), 48
];const mF = [
dec(
@ -322,21 +322,20 @@ class Round {
this.r = this.xc - this.minR;
}
reset(clear) {this.state = {}; clear && this.g.clear(true);}
reset(clear) {this.state = {}; clear == null || this.g.clear(true).setRotation(clear);}
doIcons(which) {
this.state[which] = null;
this.render(new Date()); // Not quite right, I think.
}
enhanceUntil(t) {this.enhance = t;}
pie(f, a0, a1, invert) {
if (!invert) return this.pie(f, a1, a0 + 1, true);
let t0 = Math.tan(a0 * 2 * Math.PI), t1 = Math.tan(a1 * 2 * Math.PI);
const t0 = Math.tan(a0 * 2 * Math.PI), t1 = Math.tan(a1 * 2 * Math.PI);
let i0 = Math.floor(a0 * 4 + 0.5), i1 = Math.floor(a1 * 4 + 0.5);
let x = f.getWidth() / 2, y = f.getHeight() / 2;
let poly = [
const x = f.getWidth() / 2, y = f.getHeight() / 2;
const poly = [
x + (i1 & 2 ? -x : x) * (i1 & 1 ? 1 : t1),
y + (i1 & 2 ? y : -y) / (i1 & 1 ? t1 : 1),
x,
@ -348,16 +347,17 @@ class Round {
for (i0++; i0 <= i1; i0++) poly.push(
3 * i0 & 2 ? f.getWidth() : 0, i0 & 2 ? f.getHeight() : 0
);
f.setColor(0).fillPoly(poly);
return f.setColor(0).fillPoly(poly);
}
hand(t, d, c0, r0, c1, r1) {
const g = this.g;
t *= Math.PI / 30;
const r = this.r;
const z = 2 * r0 + 1;
const x = this.xc + r * Math.sin(t), y = this.yc - r * Math.cos(t);
const x0 = x - r0, y0 = y - r0;
d = d ? d[0] : Graphics.createArrayBuffer(z, z, 16, {msb: true});
const r = this.r,
z = 2 * r0 + 1,
x = this.xc + r * Math.sin(t), y = this.yc - r * Math.cos(t),
x0 = x - r0, y0 = y - r0;
d = d ? d[0] : Graphics.createArrayBuffer(z, z, 4, {msb: true});
for (let i = 0; i < z; i++) for (let j = 0; j < z; j++) {
d.setPixel(i, j, g.getPixel(x0 + i, y0 + j));
}
@ -366,24 +366,20 @@ class Round {
return [d, x0, y0];
}
render(d) {
const g = this.g;
const b = this.b, bI = this.bI;
const c = this.c, cI = this.cI;
const e = d < this.enhance;
const state = this.state;
const options = this.options;
const cal = options.calendric;
const res = options.resolution;
const dow = (e || cal == 1 || cal > 2) && d.getDay();
const ts = res < 2 && d.getSeconds();
const tm = (e || res < 3) && d.getMinutes() + ts / 60;
const th = d.getHours() + d.getMinutes() / 60;
const dd = (e || cal > 1) && d.getDate();
const dm = (e || cal > 3) && d.getMonth();
const dy = (e || cal > 4) && d.getFullYear();
const xc = this.xc, yc = this.yc, r = this.r;
const dlr = xc * 3/4, dlw = 8, dlhw = 4;
render(d, rate) {
const g = this.g, b = this.b, bI = this.bI, c = this.c, cI = this.cI,
e = d < this.enhance,
state = this.state, options = this.options,
cal = options.calendric, res = options.resolution,
dow = (e || cal === 1 || cal > 2) && d.getDay(),
ts = res < 2 && d.getSeconds(),
tm = (e || res < 3) && d.getMinutes() + ts / 60,
th = d.getHours() + d.getMinutes() / 60,
dd = (e || cal > 1) && d.getDate(),
dm = (e || cal > 3) && d.getMonth(),
dy = (e || cal > 4) && d.getFullYear();
const xc = this.xc, yc = this.yc, r = this.r,
dlr = xc * 3/4, dlw = 8, dlhw = 4;
// Restore saveunders for fast-moving, overdrawing indicators.
if (state.sd) g.drawImage.apply(g, state.sd);
@ -397,10 +393,10 @@ class Round {
state.dow = dow;
}
const locked = Bangle.isLocked();
const charging = Bangle.isCharging();
const battery = E.getBattery();
const HRMOn = Bangle.isHRMOn();
const locked = Bangle.isLocked(),
charging = Bangle.isCharging(),
battery = E.getBattery(),
HRMOn = Bangle.isHRMOn();
if (dy !== state.dy ||
locked !== state.locked ||
charging !== state.charging ||
@ -463,6 +459,7 @@ class Round {
this.hand(tm, state.md, g.theme.bg, this.minR, g.theme.fg, this.minR - 1) :
null;
state.sd = ts === +ts ?
rate > 1000 ? this.hand(ts, state.sd, g.theme.fg2, this.secR, g.theme.bg, 2) :
this.hand(ts, state.sd, g.theme.fg2, this.secR) :
null;
}
@ -482,13 +479,23 @@ class Clock {
this.listeners = {
lcdPower: on => on ? this.active() : this.inactive(),
charging: () => {face.doIcons('charging'); this.active();},
charging: on => {
face.doIcons('charging');
if (on) {
this.listeners.accel =
a => this.orientation(a) === this.attitude || this.active();
Bangle.on('accel', this.listeners.accel);
} else {
Bangle.removeListener('accel', this.listeners.accel);
delete this.listeners.accel;
}
this.active();
},
lock: () => {face.doIcons('locked'); this.active();},
faceUp: up => {
this.conservative = !up;
this.active();
},
twist: _ => this.options.autolight && Bangle.setLCDPower(true),
drag: e => {
if (this.t0) {
if (e.b) {
@ -498,20 +505,23 @@ class Clock {
if (e.y - this.e0.y < -50) {
this.options.resolution > 0 && this.options.resolution--;
this.rates.clock = this.timescales[this.options.resolution];
this.ack();
this.active();
} else if (e.y - this.e0.y > 50) {
this.options.resolution < this.timescales.length - 1 &&
this.options.resolution++;
this.rates.clock = this.timescales[this.options.resolution];
this.ack();
this.active();
} else if (this.yX - this.yN < 20) {
const now = new Date();
if (now - this.t0 < 250) {
this.ack();
face.enhanceUntil(now + 30000);
face.render(now);
this.active();
} else if (now - this.t0 > 500) {
this.stop();
this.options.interact();
this.ack().then(_ => this.options.interact());
}
}
this.t0 = null;
@ -524,9 +534,25 @@ class Clock {
};
}
ack() {
return Bangle.buzz(33);
}
orientation(a) {
return Math.abs(a.z) < 0.85 ?
Math.abs(a.y) > Math.abs(a.x) ? a.y < 0 ? 0 : 2 : a.x > 0 ? 1 : 3 :
0;
}
rotation() {
return this.options.autorotate && Bangle.isCharging() ?
this.orientation(Bangle.getAccel()) :
0;
}
redraw(rate) {
const now = this.updated = new Date();
if (this.refresh) this.face.reset(true);
if (this.refresh) this.face.reset(this.attitude = this.rotation());
this.refresh = false;
rate = this.face.render(now, rate);
if (rate !== this.rates.face) {
@ -541,13 +567,13 @@ class Clock {
this.exception && clearTimeout(this.exception);
this.interval && clearInterval(this.interval);
this.timeout = this.exception = this.interval = this.rate = null;
this.face.reset(false); // Cancel any ongoing background rendering
this.face.reset(); // Cancel any ongoing background rendering
return this;
}
active() {
const prev = this.rate;
const now = Date.now();
const prev = this.rate,
now = Date.now();
let rate = Infinity;
for (const k in this.rates) {
let r = this.rates[k];

View File

@ -1,6 +1,6 @@
// pooqRoman resource maker
//
// Copyright (c) 2021 Stephen P Spackman
// Copyright (c) 2021, 2022 Stephen P Spackman
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
@ -147,18 +147,18 @@ res += prepFont('y10', `
xxx
xxx
-2--------------------------------
x xx
x xxx
xx xxx
xxxx xxx
xxxxx xxx
xxxxxxx xxx
xxxx xxx xxx
xxxx xxxx xxx
xxxx xxxx xxx
xxxx xxxxxxxx xxxxxxx
xxxx xxxxxxxxxxxxxxxxxxx
xxxx xxxx xxxx
xxxx xxxxx xxxx
xxxx xxxxxxx xxxxxx
xxxx xxxxxxxxxxxxxxxxx
xxxx xxxxxxxxxxxxxx
xxxx xxxxxxxxxx
xxxx xxxxxxxxx
-3--------------------------------
xxx x xxx
xxx xx xxx
@ -270,10 +270,10 @@ res += prepFont('y1', `
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
-1----------------------------------------------
xxx
xxx
xxx
xxx x
xxx x
xxx x
xxx xx
xxx xx
xxx xxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
@ -282,18 +282,18 @@ res += prepFont('y1', `
xxx
xxx
-2----------------------------------------------
x xx
x xxx
xx xxx
xxxx xxx
xxxxx xxx
xxxxxxx xxx
xxxx xxxx xxx
xxxx xxxxx xxx
xxxx xxxxxxx xxxx
xxxx xxxxxxxxxxxxx xxxxxxxxxxx
xxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxx xxxxxxxxxxxxxxxxxxxxxxxxx
xxxx xxxxxxxxxxxxxx
xxxxxx xxx
xxxxxxxx xxx
xxxx xxxxx xxx
xxxx xxxxxx xxxx
xxxx xxxxxxxx xxxx
xxxx xxxxxxxxxxx xxxxxxxx
xxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxx xxxxxxxxxxxxxxxxxxxxxxxxx
xxxx xxxxxxxxxxxxxxxxx
-3----------------------------------------------
xxx x xxx
xxx xx xxx
@ -645,12 +645,12 @@ xxxx xxxx
-1----------------------------------------------
xxx x
xxx xx
xxx xxx
xxx xxx
xxx xxxx
xxx xxxx
xxx
xxx x
xxx xx
xxx xx
xxx xxx
xxx xxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
@ -993,9 +993,9 @@ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxx
xxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxx
xxxx
xxxx
xxx

View File

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

View File

@ -70,7 +70,7 @@ E.showMenu = function(items) {
if(g.theme.dark){
fillRectRnd(x+2,iy+1,x2,iy+options.fontHeight-3,7,hl ? g.theme.bgH : g.theme.bg+20);
}else{
fillRectRnd(x+2,iy+1,x2,iy+options.fontHeight-3,7,hl ? g.theme.bgH : g.theme.bg-10);
fillRectRnd(x+2,iy+1,x2,iy+options.fontHeight-3,7,hl ? g.theme.bgH : g.theme.bg-20);
}
g.setColor(hl ? g.theme.fgH : g.theme.fg);
g.setFontAlign(-1,-1);

142
apps/promenu/bootb2.js Normal file
View File

@ -0,0 +1,142 @@
E.showMenu = function(items) {
function RectRnd(x1,y1,x2,y2,r) {
pp = [];
pp.push.apply(pp,g.quadraticBezier([x2-r,y1, x2,y1,x2,y1+r]));
pp.push.apply(pp,g.quadraticBezier([x2,y2-r,x2,y2,x2-r,y2]));
pp.push.apply(pp,g.quadraticBezier([x1+r,y2,x1,y2,x1,y2-r]));
pp.push.apply(pp,g.quadraticBezier([x1,y1+r,x1,y1,x1+r,y1]));
return pp;
}
function fillRectRnd(x1,y1,x2,y2,r,c) {
g.setColor(c);
g.fillPoly(RectRnd(x1,y1,x2,y2,r),1);
g.setColor(255,255,255);
}
function drawRectRnd(x1,y1,x2,y2,r,c) {
g.setColor(c);
g.drawPoly(RectRnd(x1,y1,x2,y2,r),1);
g.setColor(255,255,255);
}
g.reset().clearRect(Bangle.appRect); // clear if no menu supplied
Bangle.setLCDPower(1); // ensure screen is on
if (!items) {
Bangle.setUI();
return;
}
var menuItems = Object.keys(items);
var options = items[""];
if (options) menuItems.splice(menuItems.indexOf(""),1);
if (!(options instanceof Object)) options = {};
options.fontHeight = options.fontHeight||25;
if (options.selected === undefined)
options.selected = 0;
var ar = Bangle.appRect;
var x = ar.x;
var x2 = ar.x2;
var y = ar.y;
var y2 = ar.y2 - 12; // padding at end for arrow
if (options.title)
y += 22;
var loc = require("locale");
var l = {
lastIdx : 0,
draw : function(rowmin,rowmax) {
var rows = 0|Math.min((y2-y) / options.fontHeight,menuItems.length);
var idx = E.clip(options.selected-( rows>>1),0,menuItems.length-rows);
if (idx!=l.lastIdx) rowmin=undefined; // redraw all if we scrolled
l.lastIdx = idx;
var iy = y;
g.reset().setFontAlign(0,-1,0).setFont('12x20');
if (options.predraw) options.predraw(g);
if (rowmin===undefined && options.title)
g.drawString(options.title,(x+x2)/2,y-21).drawLine(x,y-2,x2,y-2).
setColor(g.theme.fg).setBgColor(g.theme.bg);
iy += 4;
if (rowmin!==undefined) {
if (idx<rowmin) {
iy += options.fontHeight*(rowmin-idx);
idx=rowmin;
}
if (idx+rows>rowmax) {
rows = 1+rowmax-rowmin;
}
}
while (rows--) {
var name = menuItems[idx];
var item = items[name];
var hl = (idx==options.selected && !l.selectEdit);
if(g.theme.dark){
fillRectRnd(x,iy,x2,iy+options.fontHeight-3,7,hl ? g.theme.bgH : g.theme.bg+40);
}else{
fillRectRnd(x,iy,x2,iy+options.fontHeight-3,7,hl ? g.theme.bgH : g.theme.bg-20);
}
g.setColor(hl ? g.theme.fgH : g.theme.fg);
g.setFontAlign(-1,-1);
var v = item.value;
v = loc.translate(""+v);
if(loc.translate(name).length >= 17-v.length && "object" == typeof item){
if (item.format) v=item.format(v);
g.drawString(loc.translate(name).substring(0, 12-v.length)+"...",x+3.7,iy+2.7);
}else{
if(loc.translate(name).length >= 15){
g.drawString(loc.translate(name).substring(0, 15)+"...",x+3.7,iy+2.7);
}else{
g.drawString(loc.translate(name),x+3.7,iy+2.7);
}
}
if ("object" == typeof item) {
var xo = x2;
var v = item.value;
if (item.format) v=item.format(v);
v = loc.translate(""+v);
if (l.selectEdit && idx==options.selected) {
xo -= 24 + 1;
g.setColor(g.theme.fgH).drawImage("\x0c\x05\x81\x00 \x07\x00\xF9\xF0\x0E\x00@",xo,iy+(options.fontHeight-10)/2,{scale:2});
}
g.setFontAlign(1,-1);
g.drawString(v,xo-2,iy+1);
}
g.setColor(g.theme.fg);
iy += options.fontHeight;
idx++;
}
g.setFontAlign(-1,-1);
g.setColor((idx<menuItems.length)?g.theme.fg:g.theme.bg).fillPoly([72,166,104,166,88,174]);
g.flip();
},
select : function() {
var item = items[menuItems[options.selected]];
if ("function" == typeof item) item(l);
else if ("object" == typeof item) {
// if a number, go into 'edit mode'
if ("number" == typeof item.value)
l.selectEdit = l.selectEdit?undefined:item;
else { // else just toggle bools
if ("boolean" == typeof item.value) item.value=!item.value;
if (item.onchange) item.onchange(item.value);
}
l.draw();
}
},
move : function(dir) {
var item = l.selectEdit;
if (item) {
item.value -= (dir||1)*(item.step||1);
if (item.min!==undefined && item.value<item.min) item.value = item.wrap ? item.max : item.min;
if (item.max!==undefined && item.value>item.max) item.value = item.wrap ? item.min : item.max;
if (item.onchange) item.onchange(item.value);
l.draw(options.selected,options.selected);
} else {
var lastSelected=options.selected;
options.selected = (dir+options.selected+menuItems.length)%menuItems.length;
l.draw(Math.min(lastSelected,options.selected), Math.max(lastSelected,options.selected));
}
}
};
l.draw();
Bangle.setUI("updown",dir => {
if (dir) l.move(dir);
else l.select();
});
return l;
};

View File

@ -2,11 +2,19 @@
Directly launch apps from the clock screen with custom patterns.
## Usage
## Installation and Usage
Install Pattern Launcher alongside your main laucher app.
_Do not delete that launcher!_
Pattern Launcher is designed as an additional app launching utility, not as a replacement for the main launcher.
In the main launcher, start Pattern Launcher in the app menu to assign the pattern configuration (see below).
Note that this actually among the applications, _not_ in the application settings!
Create patterns and link them to apps in the Pattern Launcher app.
Then launch the linked apps directly from the clock screen by simply drawing the desired pattern.
Note that this does only work in the clock screen, not if other applications run.
## Add Pattern Screenshots
@ -28,7 +36,8 @@ Then launch the linked apps directly from the clock screen by simply drawing the
## Detailed Steps
From the main menu you can:
The main menu of Pattern Launcher is accessible from the _application_ starter of the main launcher.
From there you can:
- Add a new pattern and link it to an app (first entry)
- To create a new pattern first select "Add Pattern"
@ -60,6 +69,16 @@ Make sure the watch is unlocked before you start drawing. If this bothers you, y
Please note that drawing on the clock screen will not visually show the pattern you drew. It will start the app as soon as the pattern was recognized - this might take 1 or 2 seconds! If still nothing happens, that might be a bug, sorry!
4. Where can I configure the patterns?
You have to start the "Pattern Launcher" app from the main app launcher's app selection.
5. Do I have to delete my former app launcher so that Pattern Launcher is the only installed launcher?
No! Pattern Launcher works alongside another "main" launcher.
If you have deleted that one, you do not have a general purpose app launcher any more and cannot access Pattern Launcher's configuration.
If you already have deleted your main launcher accidentially, just reinstall it from the app loader.
## Authors
Initial creation: [crazysaem](https://github.com/crazysaem)
@ -67,3 +86,5 @@ Initial creation: [crazysaem](https://github.com/crazysaem)
Improve pattern detection code readability: [PaddeK](http://forum.espruino.com/profiles/117930/)
Improve pattern rendering: [HughB](http://forum.espruino.com/profiles/167235/)
Doc additions: [dirkhillbrecht](http://forum.espruino.com/profiles/182498/)

View File

@ -3,3 +3,4 @@
0.03: Fix unfreed memory, and clearInterval that disabled all clocks at midnight
Fix app icon
Change menu order so 'back' is at the top
0.04: Fix alarm not activating sometimes.

View File

@ -143,7 +143,7 @@ let alarms = require("Storage").readJSON("qalarm.json", 1) || [];
let active = alarms.filter(
(alarm) =>
alarm.on &&
alarm.t < t &&
alarm.t <= t &&
alarm.last != time.getDate() &&
(alarm.timer || alarm.daysOfWeek[time.getDay()])
);

View File

@ -4,3 +4,7 @@
0.03: Fix theme and maps/graphing if no GPS
0.04: Multiple bugfixes
0.05: Add recording for coresensor
0.06: Add recording for battery stats
Fix execution of other recorders (*.recorder.js)
Modified icons and colors for better visibility
Only show plotting speed if Latitude is available

View File

@ -16,7 +16,8 @@ You can record
* **Time** The current time
* **GPS** GPS Latitude, Longitude and Altitude
* **Steps** Steps counted by the step counter
* **HR** Heart rate
* **HR** Heart rate and confidence
* **BAT** Battery percentage and voltage
* **Core** CoreTemp body temperature
**Note:** It is possible for other apps to record information using this app
@ -25,4 +26,4 @@ function in `widget.js` for more information.
## Tips
When recording GPS, it usually takes several minutes for the watch to get a [GPS fix](https://en.wikipedia.org/wiki/Time_to_first_fix). There is a grey satellite symbol, which you will see turn red when you get an actual GPS Fix. You can [upload assistant files](https://banglejs.com/apps/#assisted%20gps%20update) to speed up the time spent on getting a GPS fix.
When recording GPS, it usually takes several minutes for the watch to get a [GPS fix](https://en.wikipedia.org/wiki/Time_to_first_fix). There is a red satellite symbol, which you will see turn green when you get an actual GPS Fix. You can [upload assistant files](https://banglejs.com/apps/#assisted%20gps%20update) to speed up the time spent on getting a GPS fix.

View File

@ -199,9 +199,10 @@ function viewTrack(filename, info) {
menu['Plot Alt.'] = function() {
plotGraph(info, "Altitude");
};
menu['Plot Speed'] = function() {
plotGraph(info, "Speed");
};
if (info.fields.includes("Latitude"))
menu['Plot Speed'] = function() {
plotGraph(info, "Speed");
};
// TODO: steps, heart rate?
menu['Erase'] = function() {
E.showPrompt("Delete Track?").then(function(v) {

View File

@ -48,41 +48,50 @@
Bangle.removeListener('GPS', onGPS);
Bangle.setGPSPower(0,"recorder");
},
draw : (x,y) => g.setColor(hasFix?"#0ff":"#888").drawImage(atob("DAyBAAACADgDuBOAeA4AzAHADgAAAA=="),x,y)
draw : (x,y) => g.setColor(hasFix?"#0f0":"#f88").drawImage(atob("DAwBEAKARAKQE4DwHkPqPRGKAEAA"),x,y)
};
},
hrm:function() {
var bpm = 0, bpmConfidence = 0;
var hasBPM = false;
function onHRM(h) {
if (h.confidence >= bpmConfidence) {
bpmConfidence = h.confidence;
bpm = h.bpm;
if (bpmConfidence) hasBPM = true;
}
}
return {
name : "HR",
fields : ["Heartrate"],
fields : ["Heartrate", "Confidence"],
getValues : () => {
var r = [bpmConfidence?bpm:""];
var r = [bpm,bpmConfidence];
bpm = 0; bpmConfidence = 0;
return r;
},
start : () => {
hasBPM = false;
Bangle.on('HRM', onHRM);
Bangle.setHRMPower(1,"recorder");
},
stop : () => {
hasBPM = false;
Bangle.removeListener('HRM', onHRM);
Bangle.setHRMPower(0,"recorder");
},
draw : (x,y) => g.setColor(hasBPM?"#f00":"#888").drawImage(atob("DAyBAAAAAD/H/n/n/j/D/B+AYAAAAA=="),x,y)
draw : (x,y) => g.setColor(Bangle.isHRMOn()?"#f00":"#f88").drawImage(atob("DAwBAAAAMMeef+f+f+P8H4DwBgAA"),x,y)
};
},
bat:function() {
return {
name : "BAT",
fields : ["Battery Percentage", "Battery Voltage", "Charging"],
getValues : () => {
return [E.getBattery(), NRF.getBattery(), Bangle.isCharging()];
},
start : () => {
},
stop : () => {
},
draw : (x,y) => g.setColor(Bangle.isCharging() ? "#0f0" : "#ff0").drawImage(atob("DAwBAABgH4G4EYG4H4H4H4GIH4AA"),x,y)
};
},
temp:function() {
var core = 0, skin = 0;
var hasCore = false;
@ -106,7 +115,7 @@
hasCore = false;
Bangle.removeListener('CoreTemp', onCore);
},
draw : (x,y) => g.setColor(hasCore?"#0f0":"#888").drawImage(atob("DAyBAAHh0js3EuDMA8A8AWBnDj9A8A=="),x,y)
draw : (x,y) => g.setColor(hasCore?"#0f0":"#8f8").drawImage(atob("DAwBAAAOAKPOfgZgZgZgZgfgPAAA"),x,y)
};
},
steps:function() {
@ -121,7 +130,7 @@
},
start : () => { lastSteps = Bangle.getStepCount(); },
stop : () => {},
draw : (x,y) => g.reset().drawImage(atob("DAyBAAADDHnnnnnnnnnnjDmDnDnAAA=="),x,y)
draw : (x,y) => g.reset().drawImage(atob("DAwBAAMMeeeeeeeecOMMAAMMMMAA"),x,y)
};
}
// TODO: recAltitude from pressure sensor
@ -138,7 +147,7 @@
}
})
*/
require("Storage").list(/^.*\.recorder\.js$/).forEach(fn=>eval(fn)(recorders));
require("Storage").list(/^.*\.recorder\.js$/).forEach(fn=>eval(require("Storage").read(fn))(recorders));
return recorders;
}

View File

@ -0,0 +1,2 @@
1.00: Hello Ruuvi Watch!
1.01: Clear gfx on startup.

25
apps/ruuviwatch/README.md Normal file
View File

@ -0,0 +1,25 @@
# Ruuvi Watch
Watch the status of [RuuviTags](https://ruuvi.com) in range.
- Id
- Temperature (°C)
- Humidity (%)
- Pressure (hPa)
- Battery voltage
Also shows how "fresh" the data is (age of reading).
## Usage
- Scans for devices when launched and every N seconds.
- Page trough devices with BTN1/BTN3.
- Trigger scan with BTN2.
## Todo / ideas
- Allow to "name" known devices
- Prevent flicker when updating
- Include more data
- Support older Ruuvi protocols

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwhC/AH4A/ABMP/4ACCyIVDAAXwCyoYPIggAFCx4oEDBw/JJJguCBhAwLBZYjKBQQeGCIYNHB45bIBw4gIRgw+NC4wwJJ5YRLC5DzFCJBGMEYoSEFxoMEBQIXEF4gVFF5QcEC553JC5QRITgy/NVxIXGf5QlFIwy4IGBQuFC5JhGCwpGGERZOEBQ4MEDAwJJGAzdJCxLVJFxoYLCxoYICx6/GCqAA/AH4A/ACA"))

View File

@ -0,0 +1,151 @@
require("Storage").write("ruuviwatch.info",{
"id":"ruuviwatch",
"name":"Ruuvi Watch",
"src":"ruuviwatch.app.js",
"icon":"ruuviwatch.img"
});
const lookup = {};
const ruuvis = [];
let current = 0;
function int2Hex (str) {
return ('0' + str.toString(16).toUpperCase()).slice(-2);
}
function p(data) {
const OFFSET = 7; // 0-4 header, 5-6 Ruuvi id
const robject = {};
robject.version = data[OFFSET];
let temperature = (data[OFFSET+1] << 8) | (data[OFFSET+2] & 0xff);
if (temperature > 32767) {
temperature -= 65534;
}
robject.temperature = temperature / 200.0;
robject.humidity = (((data[OFFSET+3] & 0xff) << 8) | (data[OFFSET+4] & 0xff)) / 400.0;
robject.pressure = ((((data[OFFSET+5] & 0xff) << 8) | (data[OFFSET+6] & 0xff)) + 50000) / 100.0;
let accelerationX = (data[OFFSET+7] << 8) | (data[OFFSET+8] & 0xff);
if (accelerationX > 32767) accelerationX -= 65536; // two's complement
robject.accelerationX = accelerationX / 1000.0;
let accelerationY = (data[OFFSET+9] << 8) | (data[OFFSET+10] & 0xff);
if (accelerationY > 32767) accelerationY -= 65536; // two's complement
robject.accelerationY = accelerationY / 1000.0;
let accelerationZ = (data[OFFSET+11] << 8) | (data[OFFSET+12] & 0xff);
if (accelerationZ > 32767) accelerationZ -= 65536; // two's complement
robject.accelerationZ = accelerationZ / 1000.0;
const powerInfo = ((data[OFFSET+13] & 0xff) << 8) | (data[OFFSET+14] & 0xff);
robject.battery = ((powerInfo >>> 5) + 1600) / 1000.0;
robject.txPower = (powerInfo & 0b11111) * 2 - 40;
robject.movementCounter = data[OFFSET+15] & 0xff;
robject.measurementSequenceNumber = ((data[OFFSET+16] & 0xff) << 8) | (data[OFFSET+17] & 0xff);
robject.mac = [
int2Hex(data[OFFSET+18]),
int2Hex(data[OFFSET+19]),
int2Hex(data[OFFSET+20]),
int2Hex(data[OFFSET+21]),
int2Hex(data[OFFSET+22]),
int2Hex(data[OFFSET+23])
].join(':');
robject.name = "Ruuvi " + int2Hex(data[OFFSET+22]) + int2Hex(data[OFFSET+23]);
return robject;
}
function getAge(created) {
const now = new Date().getTime();
const ago = ((now - created) / 1000).toFixed(0);
return ago > 0 ? ago + "s ago" : "now";
}
function redraw() {
if (ruuvis.length > 0 && ruuvis[current]) {
const ruuvi = ruuvis[current];
g.clear();
g.setFontAlign(0,0);
g.setFont("Vector",12);
g.drawString(" (" + (current+1) + "/" + ruuvis.length + ")", g.getWidth()/2, 10);
g.setFont("Vector",20);
g.drawString(ruuvi.name, g.getWidth()/2, 30);
g.setFont("Vector",12);
const age = getAge(ruuvi.time);
if(age > (5*60)) {
g.setColor("#ff0000");
} else if (age > 60) {
g.setColor("#f39c12");
} else {
g.setColor("#2ecc71");
}
g.drawString(age, g.getWidth()/2, 50);
g.setColor("#ffffff");
g.setFont("Vector",60);
g.drawString(ruuvi.temperature.toFixed(2) + "°c", g.getWidth()/2, g.getHeight()/2);
g.setFontAlign(0,1);
g.setFont("Vector",20);
g.drawString(ruuvi.humidity + "% " + ruuvi.pressure + "hPa ", g.getWidth()/2, g.getHeight()-30);
g.setFont("Vector",12);
g.drawString(ruuvi.battery + "v", g.getWidth()/2, g.getHeight()-10);
} else {
g.clear();
g.drawImage(require("Storage").read("ruuviwatch.img"), g.getWidth()/2-24, g.getHeight()/2-24);
g.setFontAlign(0,0);
g.setFont("Vector",16);
g.drawString("Looking for Ruuvi...", g.getWidth()/2, g.getHeight()/2 + 50);
}
}
function scan() {
NRF.findDevices(function(devices) {
let foundNew = false;
devices.forEach(device => {
const data = p(device.data);
data.time = new Date().getTime();
const idx = lookup[data.name];
if (idx !== undefined) {
ruuvis[idx] = data;
} else {
lookup[data.name] = ruuvis.push(data)-1;
foundNew = true;
}
});
redraw();
if (foundNew) {
Bangle.buzz();
g.flip();
}
}, {timeout : 2000, filters : [{ manufacturerData:{0x0499:{}} }] });
}
g.clear();
g.drawImage(require("Storage").read("ruuviwatch.img"), g.getWidth()/2-24, g.getHeight()/2-24);
var drawInterval = setInterval(redraw, 1000);
var scanInterval = setInterval(scan, 10000);
setWatch(() => {
current--;
if (current < 0) {
current = ruuvis.length-1;
}
redraw();
}, BTN1, {repeat:true});
setWatch(() => {
scan();
}, BTN2, {repeat:true});
setWatch(() => {
current++;
if (current >= ruuvis.length) {
current = 0;
}
redraw();
}, BTN3, {repeat:true});
scan();

Binary file not shown.

After

Width:  |  Height:  |  Size: 665 B

View File

@ -154,8 +154,8 @@ function showAlertsMenu() {
function showBLEMenu() {
var hidV = [false, "kbmedia", "kb", "joy"];
var hidN = ["Off", "Kbrd & Media", "Kbrd","Joystick"];
var hidV = [false, "kbmedia", "kb", "com", "joy"];
var hidN = ["Off", "Kbrd & Media", "Kbrd", "Kbrd & Mouse" ,"Joystick"];
E.showMenu({
'': { 'title': 'Bluetooth' },
'< Back': ()=>showMainMenu(),

View File

@ -1,3 +1,6 @@
0.01: Modified for use with new bootloader and firmware
0.02: Use Bangle.setUI for button/launcher handling
0.03: Fix display for Bangle 2
0.04: Use queueDraw(), update every minute, respect theme, use Lato font
0.05: Decided against custom font as it inceases the code size
minimalism is useful when narrowing down issues

View File

@ -1,28 +1,55 @@
const h = g.getHeight();
const w = g.getWidth();
function draw() {
var d = new Date();
var da = d.toString().split(" ");
var time = da[4].substr(0,5);
var date = new Date();
var timeStr = require("locale").time(date,1);
g.reset();
g.clearRect(0, 30, w, 99);
g.setFontAlign(0, -1);
g.setFont("Vector", w/3);
g.drawString(time, w/2, 40);
g.setColor(g.theme.bg);
g.fillRect(Bangle.appRect);
g.setFont('Vector', w/3);
g.setFontAlign(0, 0);
g.setColor(g.theme.fg);
g.drawString(timeStr, w/2, h/2);
queueDraw();
}
// handle switch display on by pressing BTN1
Bangle.on('lcdPower', function(on) {
if (on) draw();
// timeout used to update every minute
var drawTimeout;
// schedule a draw for the next minute
function queueDraw() {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = setTimeout(function() {
drawTimeout = undefined;
draw();
}, 60000 - (Date.now() % 60000));
}
// Stop updates when LCD is off, restart when on
Bangle.on('lcdPower',on=>{
if (on) {
draw(); // draw immediately, queue redraw
} else { // stop draw timer
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
}
});
g.clear();
// Show launcher when middle button pressed
//Bangle.setUI("clock");
// use clockupdown as it tests for issue #1249
Bangle.setUI("clockupdown", btn=> {
draw();
});
// Load widgets
Bangle.loadWidgets();
Bangle.drawWidgets();
setInterval(draw, 15000); // refresh every 15s
draw();
// Show launcher when button pressed
Bangle.setUI("clock");

View File

@ -1,3 +1,4 @@
1.00: Release for Bangle 2 (2021/11/18)
1.01: Internal id update to wid_* as per Gordon's request (2021/11/21)
1.02: Support dark themes
1.02: Support dark themes
1.03: Increase screen update rate when charging

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