Merge branch 'espruino:master' into sleeplog_v0.10_beta

pull/2110/head
storm64 2022-08-17 23:32:03 +02:00 committed by GitHub
commit 6dd4b965ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
647 changed files with 29971 additions and 3784 deletions

12
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1,12 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: espruino
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: ['http://www.espruino.com/Donate']# Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

View File

@ -1,4 +1,4 @@
name: Node CI name: build
on: [push, pull_request] on: [push, pull_request]
@ -6,29 +6,22 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x]
steps: steps:
- name: Checkout repository and submodules - name: Checkout repository and submodules
uses: actions/checkout@v2 uses: actions/checkout@v3
with: with:
submodules: recursive submodules: recursive
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js 16.x
uses: actions/setup-node@v1 uses: actions/setup-node@v3
with: with:
node-version: ${{ matrix.node-version }} node-version: 16.x
- name: install testing dependencies - name: Install testing dependencies
run: npm i run: npm ci
- name: test all apps and widgets - name: Test all apps and widgets
run: npm run test run: npm test
- name: install typescript dependencies - name: Install typescript dependencies
working-directory: ./typescript working-directory: ./typescript
run: npm ci run: npm ci
- name: build types - name: Build all TS apps and widgets
working-directory: ./typescript working-directory: ./typescript
run: npm run build:types run: npm run build
- name: build all TS apps and widgets
working-directory: ./typescript
run: npm run build

1
.gitignore vendored
View File

@ -1,6 +1,5 @@
.htaccess .htaccess
node_modules node_modules
package-lock.json
.DS_Store .DS_Store
*.js.bak *.js.bak
appdates.csv appdates.csv

View File

@ -1,7 +1,7 @@
Bangle.js App Loader (and Apps) Bangle.js App Loader (and Apps)
================================ ================================
[![Build Status](https://app.travis-ci.com/espruino/BangleApps.svg?branch=master)](https://app.travis-ci.com/github/espruino/BangleApps) [![Build Status](https://github.com/espruino/BangleApps/actions/workflows/nodejs.yml/badge.svg)](https://github.com/espruino/BangleApps/actions/workflows/nodejs.yml)
* Try the **release version** at [banglejs.com/apps](https://banglejs.com/apps) * Try the **release version** at [banglejs.com/apps](https://banglejs.com/apps)
* Try the **development version** at [espruino.github.io](https://espruino.github.io/BangleApps/) * Try the **development version** at [espruino.github.io](https://espruino.github.io/BangleApps/)
@ -191,7 +191,7 @@ widget bar at the top of the screen they can add themselves to the global
``` ```
WIDGETS["mywidget"]={ WIDGETS["mywidget"]={
area:"tl", // tl (top left), tr (top right) area:"tl", // tl (top left), tr (top right), bl (bottom left), br (bottom right)
sortorder:0, // (Optional) determines order of widgets in the same corner sortorder:0, // (Optional) determines order of widgets in the same corner
width: 24, // how wide is the widget? You can change this and call Bangle.drawWidgets() to re-layout width: 24, // how wide is the widget? You can change this and call Bangle.drawWidgets() to re-layout
draw:draw // called to draw the widget draw:draw // called to draw the widget
@ -324,7 +324,7 @@ and which gives information about the app for the Launcher.
``` ```
* name, icon and description present the app in the app loader. * name, icon and description present the app in the app loader.
* tags is used for grouping apps in the library, separate multiple entries by comma. Known tags are `tool`, `system`, `clock`, `game`, `sound`, `gps`, `widget`, `launcher` or empty. * tags is used for grouping apps in the library, separate multiple entries by comma. Known tags are `tool`, `system`, `clock`, `game`, `sound`, `gps`, `widget`, `launcher`, `bluetooth` or empty.
* storage is used to identify the app files and how to handle them * storage is used to identify the app files and how to handle them
* data is used to clean up files when the app is uninstalled * data is used to clean up files when the app is uninstalled

View File

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

11
apps/2ofthemclk/README.md Normal file
View File

@ -0,0 +1,11 @@
# two of them clock
You can now wear teh memez on your wrist.
![](screenshot.png)
Also serves as an example of displaying seconds only when unlocked or charging and only refreshing on the minute otherwise.
Widgets not supported
## Creator
- [Kilrah](https://github.com/kilrah)

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwgZC/AH4ADkAPOgVJkgEBAQQAJiQRByEJgmQCJWSpMEAQMkyQJCpASHhAOBpAmBJJgjBCIUJCRg4CCIJxFMQ2SoARCkmACI0EBAJHCCIMLj4RFiUBskAgIXBEAU5A4P34CtCiEJsEJ/AHBCgOBAoQAEi0H////HciQsBwywICIXWzkG4A+BEY0gif46dt6/cgnIgkWnHfLIP/MoUWwHbpvC/kAjEEj0HNYQCCkEfGgP/64RB2EAifHLwMAjg1CCIMD/0H/0B8EAh+HgeAkARCE4IjC/4jBYIMPLIcIAYUPB4OBCIQABhu/AoShCHYIRBx6QBDgUw2//8OHPwcJ39//ILBCIU9LgMBSQgsBJAYRBkE/CIIABgRHD3wRFkk/2zBDAYU//3b/oRB8ARBj6ABgEE7YREEYf+oMkSwINCyClCn//z//+4RBgMkgU3EgUcwFJgEeboOXCIP2EYJCDAAVJkkGWoIuBgf2EYQPDkECCIOGd4ffyEJkgFBAAcSoEkwQCBhw+BwQaByVAkGAKwIFBBANLkEQgAyBCIVIkBpBgmSBYOQoApBgcgiQRCAQIyCCgsSjIFBCIcgRgJNCCgQyBpAgDAQT2BCgIOBBAQUCCIpfBCIwCKP4QRNpCSDCLyJBCIbjBTwYRLboJ0BCI4QD"))

90
apps/2ofthemclk/app.js Normal file

File diff suppressed because one or more lines are too long

BIN
apps/2ofthemclk/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

BIN
apps/2ofthemclk/bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -0,0 +1,17 @@
{
"id": "2ofthemclk",
"name": "two of them clock",
"version": "0.01",
"description": "You can now wear teh memez on your wrist.",
"readme": "README.md",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS2"],
"allow_emulator": true,
"storage": [
{"name":"2ofthemclk.app.js","url":"app.js"},
{"name":"2ofthemclk.img","url":"app-icon.js","evaluate":true}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@ -9,7 +9,7 @@ currently-running apps */
// add your widget // add your widget
WIDGETS["mywidget"]={ WIDGETS["mywidget"]={
area:"tl", // tl (top left), tr (top right), bl (bottom left), br (bottom right) area:"tl", // tl (top left), tr (top right), bl (bottom left), br (bottom right), be aware that not all apps support widgets at the bottom of the screen
width: 28, // how wide is the widget? You can change this and call Bangle.drawWidgets() to re-layout width: 28, // how wide is the widget? You can change this and call Bangle.drawWidgets() to re-layout
draw:draw // called to draw the widget draw:draw // called to draw the widget
}; };

View File

@ -4,3 +4,5 @@
0.04: Obey system quiet mode 0.04: Obey system quiet mode
0.05: Battery optimisation, add the pause option, bug fixes 0.05: Battery optimisation, add the pause option, bug fixes
0.06: Add a temperature threshold to detect (and not alert) if the BJS isn't worn. Better support for the peoples using the app at night 0.06: Add a temperature threshold to detect (and not alert) if the BJS isn't worn. Better support for the peoples using the app at night
0.07: Fix bug on the cutting edge firmware
0.08: Use default Bangle formatter for booleans

View File

@ -1,42 +1,46 @@
function drawAlert() { (function () {
E.showPrompt("Inactivity detected", { // load variable before defining functions cause it can trigger a ReferenceError
title: "Activity reminder", const activityreminder = require("activityreminder");
buttons: { "Ok": 1, "Dismiss": 2, "Pause": 3 } const storage = require("Storage");
}).then(function (v) { const activityreminder_settings = activityreminder.loadSettings();
if (v == 1) { let activityreminder_data = activityreminder.loadData();
activityreminder_data.okDate = new Date();
function drawAlert() {
E.showPrompt("Inactivity detected", {
title: "Activity reminder",
buttons: { "Ok": 1, "Dismiss": 2, "Pause": 3 }
}).then(function (v) {
if (v == 1) {
activityreminder_data.okDate = new Date();
}
if (v == 2) {
activityreminder_data.dismissDate = new Date();
}
if (v == 3) {
activityreminder_data.pauseDate = new Date();
}
activityreminder.saveData(activityreminder_data);
load();
});
// Obey system quiet mode:
if (!(storage.readJSON('setting.json', 1) || {}).quiet) {
Bangle.buzz(400);
}
setTimeout(load, 20000);
} }
if (v == 2) {
activityreminder_data.dismissDate = new Date(); function run() {
if (activityreminder.mustAlert(activityreminder_data, activityreminder_settings)) {
drawAlert();
} else {
eval(storage.read("activityreminder.settings.js"))(() => load());
}
} }
if (v == 3) {
activityreminder_data.pauseDate = new Date();
}
activityreminder.saveData(activityreminder_data);
load();
});
// Obey system quiet mode: g.clear();
if (!(storage.readJSON('setting.json', 1) || {}).quiet) { Bangle.loadWidgets();
Bangle.buzz(400); Bangle.drawWidgets();
} run();
setTimeout(load, 20000);
} })();
function run() {
if (activityreminder.mustAlert(activityreminder_data, activityreminder_settings)) {
drawAlert();
} else {
eval(storage.read("activityreminder.settings.js"))(() => load());
}
}
const activityreminder = require("activityreminder");
const storage = require("Storage");
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
const activityreminder_settings = activityreminder.loadSettings();
const activityreminder_data = activityreminder.loadData();
run();

View File

@ -1,65 +1,70 @@
function run() { (function () {
if (isNotWorn()) return; // load variable before defining functions cause it can trigger a ReferenceError
let now = new Date(); const activityreminder = require("activityreminder");
let h = now.getHours(); const activityreminder_settings = activityreminder.loadSettings();
let activityreminder_data = activityreminder.loadData();
if (isDuringAlertHours(h)) {
let health = Bangle.getHealthStatus("day"); if (activityreminder_data.firstLoad) {
if (health.steps - activityreminder_data.stepsOnDate >= activityreminder_settings.minSteps // more steps made than needed
|| health.steps < activityreminder_data.stepsOnDate) { // new day or reboot of the watch
activityreminder_data.stepsOnDate = health.steps;
activityreminder_data.stepsDate = now;
activityreminder.saveData(activityreminder_data);
/* todo in a futur release
Add settimer to trigger like 30 secs after going in this part cause the person have been walking
(pass some argument to run() to handle long walks and not triggering so often)
*/
}
if(activityreminder.mustAlert(activityreminder_data, activityreminder_settings)){
load('activityreminder.app.js');
}
}
}
function isNotWorn() {
return (Bangle.isCharging() || activityreminder_settings.tempThreshold >= E.getTemperature());
}
function isDuringAlertHours(h) {
if(activityreminder_settings.startHour < activityreminder_settings.endHour){ // not passing through midnight
return (h >= activityreminder_settings.startHour && h < activityreminder_settings.endHour)
} else{ // passing through midnight
return (h >= activityreminder_settings.startHour || h < activityreminder_settings.endHour)
}
}
Bangle.on('midnight', function() {
/*
Usefull trick to have the app working smothly for people using it at night
*/
let now = new Date();
let h = now.getHours();
if (activityreminder_settings.enabled && isDuringAlertHours(h)){
// updating only the steps and keeping the original stepsDate on purpose
activityreminder_data.stepsOnDate = 0;
activityreminder.saveData(activityreminder_data);
}
});
const activityreminder = require("activityreminder");
const activityreminder_settings = activityreminder.loadSettings();
if (activityreminder_settings.enabled) {
const activityreminder_data = activityreminder.loadData();
if(activityreminder_data.firstLoad){
activityreminder_data.firstLoad = false; activityreminder_data.firstLoad = false;
activityreminder.saveData(activityreminder_data); activityreminder.saveData(activityreminder_data);
} }
setInterval(run, 60000);
/* todo in a futur release
increase setInterval time to something that is still sensible (5 mins ?)
when we added a settimer
*/
}
function run() {
if (isNotWorn()) return;
let now = new Date();
let h = now.getHours();
if (isDuringAlertHours(h)) {
let health = Bangle.getHealthStatus("day");
if (health.steps - activityreminder_data.stepsOnDate >= activityreminder_settings.minSteps // more steps made than needed
|| health.steps < activityreminder_data.stepsOnDate) { // new day or reboot of the watch
activityreminder_data.stepsOnDate = health.steps;
activityreminder_data.stepsDate = now;
activityreminder.saveData(activityreminder_data);
/* todo in a futur release
Add settimer to trigger like 30 secs after going in this part cause the person have been walking
(pass some argument to run() to handle long walks and not triggering so often)
*/
}
if (activityreminder.mustAlert(activityreminder_data, activityreminder_settings)) {
load('activityreminder.app.js');
}
}
}
function isNotWorn() {
return (Bangle.isCharging() || activityreminder_settings.tempThreshold >= E.getTemperature());
}
function isDuringAlertHours(h) {
if (activityreminder_settings.startHour < activityreminder_settings.endHour) { // not passing through midnight
return (h >= activityreminder_settings.startHour && h < activityreminder_settings.endHour);
} else { // passing through midnight
return (h >= activityreminder_settings.startHour || h < activityreminder_settings.endHour);
}
}
Bangle.on('midnight', function () {
/*
Usefull trick to have the app working smothly for people using it at night
*/
let now = new Date();
let h = now.getHours();
if (activityreminder_settings.enabled && isDuringAlertHours(h)) {
// updating only the steps and keeping the original stepsDate on purpose
activityreminder_data.stepsOnDate = 0;
activityreminder.saveData(activityreminder_data);
}
});
if (activityreminder_settings.enabled) {
setInterval(run, 60000);
/* todo in a futur release
increase setInterval time to something that is still sensible (5 mins ?)
when we added a settimer
*/
}
})();

View File

@ -1,5 +1,3 @@
const storage = require("Storage");
exports.loadSettings = function () { exports.loadSettings = function () {
return Object.assign({ return Object.assign({
enabled: true, enabled: true,
@ -10,20 +8,20 @@ exports.loadSettings = function () {
pauseDelayMin: 120, pauseDelayMin: 120,
minSteps: 50, minSteps: 50,
tempThreshold: 27 tempThreshold: 27
}, storage.readJSON("activityreminder.s.json", true) || {}); }, require("Storage").readJSON("activityreminder.s.json", true) || {});
}; };
exports.writeSettings = function (settings) { exports.writeSettings = function (settings) {
storage.writeJSON("activityreminder.s.json", settings); require("Storage").writeJSON("activityreminder.s.json", settings);
}; };
exports.saveData = function (data) { exports.saveData = function (data) {
storage.writeJSON("activityreminder.data.json", data); require("Storage").writeJSON("activityreminder.data.json", data);
}; };
exports.loadData = function () { exports.loadData = function () {
let health = Bangle.getHealthStatus("day"); let health = Bangle.getHealthStatus("day");
const data = Object.assign({ let data = Object.assign({
firstLoad: true, firstLoad: true,
stepsDate: new Date(), stepsDate: new Date(),
stepsOnDate: health.steps, stepsOnDate: health.steps,
@ -31,7 +29,7 @@ exports.loadData = function () {
dismissDate: new Date(1970), dismissDate: new Date(1970),
pauseDate: new Date(1970), pauseDate: new Date(1970),
}, },
storage.readJSON("activityreminder.data.json") || {}); require("Storage").readJSON("activityreminder.data.json") || {});
if(typeof(data.stepsDate) == "string") if(typeof(data.stepsDate) == "string")
data.stepsDate = new Date(data.stepsDate); data.stepsDate = new Date(data.stepsDate);

View File

@ -3,7 +3,7 @@
"name": "Activity Reminder", "name": "Activity Reminder",
"shortName":"Activity Reminder", "shortName":"Activity Reminder",
"description": "A reminder to take short walks for the ones with a sedentary lifestyle", "description": "A reminder to take short walks for the ones with a sedentary lifestyle",
"version":"0.06", "version":"0.08",
"icon": "app.png", "icon": "app.png",
"type": "app", "type": "app",
"tags": "tool,activity", "tags": "tool,activity",

View File

@ -1,85 +1,80 @@
(function (back) { (function (back) {
// Load settings // Load settings
const activityreminder = require("activityreminder"); const activityreminder = require("activityreminder");
const settings = activityreminder.loadSettings(); let settings = activityreminder.loadSettings();
// Show the menu // Show the menu
E.showMenu({ E.showMenu({
"": { "title": "Activity Reminder" }, "": { "title": "Activity Reminder" },
"< Back": () => back(), "< Back": () => back(),
'Enable': { 'Enable': {
value: settings.enabled, value: settings.enabled,
format: v => v ? "Yes" : "No", onchange: v => {
onchange: v => { settings.enabled = v;
settings.enabled = v; activityreminder.writeSettings(settings);
activityreminder.writeSettings(settings); }
} },
}, 'Start hour': {
'Start hour': { value: settings.startHour,
value: settings.startHour, min: 0, max: 24,
min: 0, max: 24, onchange: v => {
onchange: v => { settings.startHour = v;
settings.startHour = v; activityreminder.writeSettings(settings);
activityreminder.writeSettings(settings); }
} },
}, 'End hour': {
'End hour': { value: settings.endHour,
value: settings.endHour, min: 0, max: 24,
min: 0, max: 24, onchange: v => {
onchange: v => { settings.endHour = v;
settings.endHour = v; activityreminder.writeSettings(settings);
activityreminder.writeSettings(settings); }
} },
}, 'Max inactivity': {
'Max inactivity': { value: settings.maxInnactivityMin,
value: settings.maxInnactivityMin, min: 15, max: 120,
min: 15, max: 120, onchange: v => {
onchange: v => { settings.maxInnactivityMin = v;
settings.maxInnactivityMin = v; activityreminder.writeSettings(settings);
activityreminder.writeSettings(settings); },
}, format: x => x + "m"
format: x => { },
return x + " min"; 'Dismiss delay': {
} value: settings.dismissDelayMin,
}, min: 5, max: 60,
'Dismiss delay': { onchange: v => {
value: settings.dismissDelayMin, settings.dismissDelayMin = v;
min: 5, max: 60, activityreminder.writeSettings(settings);
onchange: v => { },
settings.dismissDelayMin = v; format: x => x + "m"
activityreminder.writeSettings(settings); },
}, 'Pause delay': {
format: x => { value: settings.pauseDelayMin,
return x + " min"; min: 30, max: 240, step: 5,
} onchange: v => {
}, settings.pauseDelayMin = v;
'Pause delay': { activityreminder.writeSettings(settings);
value: settings.pauseDelayMin, },
min: 30, max: 240, step: 5, format: x => {
onchange: v => { return x + "m";
settings.pauseDelayMin = v; }
activityreminder.writeSettings(settings); },
}, 'Min steps': {
format: x => { value: settings.minSteps,
return x + " min"; min: 10, max: 500, step: 10,
} onchange: v => {
}, settings.minSteps = v;
'Min steps': { activityreminder.writeSettings(settings);
value: settings.minSteps, }
min: 10, max: 500, step: 10, },
onchange: v => { 'Temp Threshold': {
settings.minSteps = v; value: settings.tempThreshold,
activityreminder.writeSettings(settings); min: 20, max: 40, step: 0.5,
} format: v => v + "°C",
}, onchange: v => {
'Temp Threshold': { settings.tempThreshold = v;
value: settings.tempThreshold, activityreminder.writeSettings(settings);
min: 20, max: 40, step: 0.5, }
format: v => v + "°C", }
onchange: v => { });
settings.tempThreshold = v;
activityreminder.writeSettings(settings);
}
}
});
}) })

2
apps/advcasio/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.01: AdvCasio first version
0.02: Remove un-needed fonts to improve memory usage

62
apps/advcasio/README.md Normal file
View File

@ -0,0 +1,62 @@
# Adv Casio Clock
<img src="https://user-images.githubusercontent.com/2981891/175355586-1dfc0d66-6555-4385-b124-1605fdb71a11.jpg" width="250" />
An over-engineered clock inspired by Casio watches.<br/>
It has a dedicated timer, a scratchpad and can display the weather condition 4 days ahead.<br/>
It uses a <a target="_blank" href="https://dotgreg.github.io/advCasioBangleClock/">custom web app</a> to update its content.<br/>
Forked from the awesome Cassio Watch.<br/>
## Todo
- Improving quality of the background images, right now it is quite blurry.
- Improving screenshots quality.
- Improving web app look.
- Improving bangle app performances (using functions for images and specialized array).
## Functionalities
- Current time
- Current day and month
- Footsteps
- Battery
- Simple Timer embedded
- Weather forecast (7 days)
- Scratchpad
## Screenshots
Clock:<br/>
<img src="https://user-images.githubusercontent.com/2981891/175519126-049caf93-73d0-4472-9650-33b28f80843c.jpg" width="250" />
<img src="https://user-images.githubusercontent.com/2981891/175519128-96926e32-2165-4c61-9364-843440304bb9.jpg" width="250" />
<img src="https://user-images.githubusercontent.com/2981891/175519130-4921073c-48fc-4c29-932d-d42acc3b395c.jpg" width="250" />
Web interface to update weather & scratchpad <br/>
<a target="_blank" ref="https://dotgreg.github.io/advCasioBangleClock/">https://dotgreg.github.io/advCasioBangleClock</a>
<img src="https://user-images.githubusercontent.com/2981891/175519121-851bb209-7192-40db-a014-490c344f7597.jpg" width="250" />
## Usage
### How to update the tasks list / weather
- you will need a <a target="_blank" href="https://openweathermap.org/price#weather">free openweathermap.org api key</a>.
- go to https://dotgreg.github.io/advCasioBangleClock/
- Alternatively you can install it on your own server/heroku/service/github pages, the web-app code is <a target="_blank" href="https://github.com/dotgreg/advCasioBangleClock/tree/master/web-app">here</a>
- fill the location and the api key (it will be saved on your browser, no need to do it each time)
- edit the scratchpad with what you want
- click on sync
- reload your clock!
### How to start/stop the timer
- swipe up : add time (+5min)
- swipe down : remove time (-5min)
- swipe right : start timer
- swipe left : stop timer
## Links
### Issues, suggestions and bugtracker
<a target="_blank" href="https://github.com/dotgreg/advCasioBangleClock/issues">https://github.com/dotgreg/advCasioBangleClock/issues</a>
### Code repository (bangle app and web app)
<a target="_blank" href="https://github.com/dotgreg/advCasioBangleClock">https://github.com/dotgreg/advCasioBangleClock</a>
### Creator
<a target="_blank" href="https://github.com/dotgreg">https://github.com/dotgreg</a>

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwghC/AH4A/AGsCmUQC6kf/8wC6k///wgEv//zD4PxAQIJBABP//4QBC4IcBh/yEQIXKgP/l4rBl/yGAMP/4iBKJUC/5gBIAQVBBAMR/8gC5IQBAAMQC4IVBFoMjAYIXNmAXBgYXCPgQAJl/xHwPwj/yn5kC/55BUxSlC+JiBVgQ5BUxiDBUIIXBIQQXBcCoA/AH4ADXAUgUAUQBAkPeoTDFgIHBAALQEA4XwC4IOEAAQRBbAQBBCAIgBEYMQC4TnEC4XyeQgBDAAMwC4pIDC4kDAgJLD//xC5QIBNQISCFYIZCC4aEBAQRCDAAPyl4hBOIh3Cn53GNgMRiKxGBAR5BAoYA/AH4A/AH4A5A"))

303
apps/advcasio/app.js Normal file
View File

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

BIN
apps/advcasio/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

1
apps/advcasio/data.json Normal file
View File

@ -0,0 +1 @@
{"tasks":"", "weather":[]};

View File

@ -0,0 +1,25 @@
{ "id": "advcasio",
"name": "Advanced Casio Clock",
"shortName":"advcasio",
"version":"0.02",
"description": "An over-engineered clock inspired by Casio watches. It has a 4 days weather, a timer using swipe and a scratchpad. Can be updated using a dedicated webapp.",
"icon": "app.png",
"tags": "clock",
"type": "clock",
"screenshots": [
{ "url": "screenshot-clock-1.jpg" },
{ "url": "screenshot-clock-2.jpg" },
{ "url": "screenshot-clock-3.jpg" },
{ "url": "screenshot-webapp.jpg" }
],
"supports" : ["BANGLEJS", "BANGLEJS2"],
"readme": "README.md",
"allow_emulator":true,
"storage": [
{"name":"advcasio.app.js","url":"app.js"},
{"name":"advcasio.img","url":"app-icon.js","evaluate":true}
],
"data": [
{ "name": "advcasio.data.json", "url": "data.json", "storageFile": true }
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

3
apps/agenda/ChangeLog Normal file
View File

@ -0,0 +1,3 @@
0.01: Basic agenda with events from GB
0.02: Added settings page to force calendar sync
0.03: Disable past events display from settings

3
apps/agenda/README.md Normal file
View File

@ -0,0 +1,3 @@
# Agenda
Basic agenda reading the events synchronised from GadgetBridge

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwg1yhGIxAPMBwIPFhH//GAC5n/C4oHBC5/IGwoXBHQQAKC4OIFAWOxHv9GO9wAKI4XoC4foEIIWLC4IABC4gIBFxnuE4IqBC4gARC4ZzNAAwXaxe7ACO4C625C4m4xIJBzAeCxGbCAOIFgQOBC4pOBxe4AYIPBAYQKCAYYXE3GL/ADBx/oxb3BC4X+xG4xwOBC4uP/YDB54MBf4Po3eM/4XBx/+C4pTBGIIkBLgOYAYIvB9GJBwI6BL45zCL4aCCL4h3GU64ALdYS1CI55bBAAgXFO4mMO4QDBDIO/////YxBU53IxIVB/GfDAWYa5wtC/GPAYWIL4wXBL4oSBC4jcBC4m4QIWYSwWIIQIAG/CnMMAIAC/JLCMIIvMIwZHFJAJfLC5yPHAYIRDAoy/KCIi7BMon4d4+Od4IXBxAZBEQLtB/+YxIXDL4SLCL4WPzAXCNgRFBLIKnKLIrcEI4gXNAAp3CxGZAAzCBC5KnCKAIAICxBlBC4IAJxG/C4/4wAXLhBgD/IcD3AXMGAIqDDgRGNGAoXDFxxhEI4W4FxwwCaoYWBFx4YDAAQWRAEQ"))

132
apps/agenda/agenda.js Normal file
View File

@ -0,0 +1,132 @@
/* CALENDAR is a list of:
{id:int,
type,
timestamp,
durationInSeconds,
title,
description,
location,
allDay: bool,
}
*/
Bangle.loadWidgets();
Bangle.drawWidgets();
var FILE = "android.calendar.json";
var Locale = require("locale");
var fontSmall = "6x8";
var fontMedium = g.getFonts().includes("6x15")?"6x15":"6x8:2";
var fontBig = g.getFonts().includes("12x20")?"12x20":"6x8:2";
var fontLarge = g.getFonts().includes("6x15")?"6x15:2":"6x8:4";
//FIXME maybe write the end from GB already? Not durationInSeconds here (or do while receiving?)
var CALENDAR = require("Storage").readJSON("android.calendar.json",true)||[];
var settings = require("Storage").readJSON("agenda.settings.json",true)||{};
CALENDAR=CALENDAR.sort((a,b)=>a.timestamp - b.timestamp)
function getDate(timestamp) {
return new Date(timestamp*1000);
}
function formatDateLong(date, includeDay) {
if(includeDay)
return Locale.date(date)+" "+Locale.time(date,1);
return Locale.time(date,1);
}
function formatDateShort(date) {
return Locale.date(date).replace(/\d\d\d\d/,"")+Locale.time(date,1);
}
var lines = [];
function showEvent(ev) {
var bodyFont = fontBig;
if(!ev) return;
g.setFont(bodyFont);
//var lines = [];
if (ev.title) lines = g.wrapString(ev.title, g.getWidth()-10)
var titleCnt = lines.length;
var start = getDate(ev.timestamp);
var end = getDate((+ev.timestamp) + (+ev.durationInSeconds));
var includeDay = true;
if (titleCnt) lines.push(""); // add blank line after title
if(start.getDay() == end.getDay() && start.getMonth() == end.getMonth())
includeDay = false;
if(includeDay) {
lines = lines.concat(
/*LANG*/"Start:",
g.wrapString(formatDateLong(start, includeDay), g.getWidth()-10),
/*LANG*/"End:",
g.wrapString(formatDateLong(end, includeDay), g.getWidth()-10));
} else {
lines = lines.concat(
g.wrapString(Locale.date(start), g.getWidth()-10),
g.wrapString(/*LANG*/"Start"+": "+formatDateLong(start, includeDay), g.getWidth()-10),
g.wrapString(/*LANG*/"End"+": "+formatDateLong(end, includeDay), g.getWidth()-10));
}
if(ev.location)
lines = lines.concat(/*LANG*/"Location"+": ", g.wrapString(ev.location, g.getWidth()-10));
if(ev.description)
lines = lines.concat("",g.wrapString(ev.description, g.getWidth()-10));
lines = lines.concat(["",/*LANG*/"< Back"]);
E.showScroller({
h : g.getFontHeight(), // height of each menu item in pixels
c : lines.length, // number of menu items
// a function to draw a menu item
draw : function(idx, r) {
// FIXME: in 2v13 onwards, clearRect(r) will work fine. There's a bug in 2v12
g.setBgColor(idx<titleCnt ? g.theme.bg2 : g.theme.bg).
setColor(idx<titleCnt ? g.theme.fg2 : g.theme.fg).
clearRect(r.x,r.y,r.x+r.w, r.y+r.h);
g.setFont(bodyFont).drawString(lines[idx], r.x, r.y);
}, select : function(idx) {
if (idx>=lines.length-2)
showList();
},
back : () => showList()
});
}
function showList() {
//it might take time for GB to delete old events, decide whether to show them grayed out or hide entirely
if(!settings.pastEvents) {
let now = new Date();
//TODO add threshold here?
CALENDAR = CALENDAR.filter(ev=>ev.timestamp + ev.durationInSeconds > now/1000);
}
if(CALENDAR.length == 0) {
E.showMessage("No events");
return;
}
E.showScroller({
h : 52,
c : Math.max(CALENDAR.length,3), // workaround for 2v10.219 firmware (min 3 not needed for 2v11)
draw : function(idx, r) {"ram"
var ev = CALENDAR[idx];
g.setColor(g.theme.fg);
g.clearRect(r.x,r.y,r.x+r.w, r.y+r.h);
if (!ev) return;
var isPast = false;
var x = r.x+2, title = ev.title;
var body = formatDateShort(getDate(ev.timestamp))+"\n"+(ev.location?ev.location:/*LANG*/"No location");
if(settings.pastEvents) isPast = ev.timestamp + ev.durationInSeconds < (new Date())/1000;
if (title) g.setFontAlign(-1,-1).setFont(fontBig)
.setColor(isPast ? "#888" : g.theme.fg).drawString(title, x,r.y+2);
if (body) {
g.setFontAlign(-1,-1).setFont(fontMedium).setColor(isPast ? "#888" : g.theme.fg);
var l = g.wrapString(body, r.w-(x+14));
if (l.length>3) {
l = l.slice(0,3);
l[l.length-1]+="...";
}
g.drawString(l.join("\n"), x+10,r.y+20);
}
g.setColor("#888").fillRect(r.x,r.y+r.h-1,r.x+r.w-1,r.y+r.h-1); // dividing line between items
},
select : idx => showEvent(CALENDAR[idx]),
back : () => load()
});
}
showList();

BIN
apps/agenda/agenda.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

18
apps/agenda/metadata.json Normal file
View File

@ -0,0 +1,18 @@
{
"id": "agenda",
"name": "Agenda",
"version": "0.03",
"description": "Simple agenda",
"icon": "agenda.png",
"screenshots": [{"url":"screenshot_agenda_overview.png"}, {"url":"screenshot_agenda_event1.png"}, {"url":"screenshot_agenda_event2.png"}],
"tags": "agenda",
"supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
"allow_emulator": true,
"storage": [
{"name":"agenda.app.js","url":"agenda.js"},
{"name":"agenda.settings.js","url":"settings.js"},
{"name":"agenda.img","url":"agenda-icon.js","evaluate":true}
],
"data": [{"name":"agenda.settings.json"}]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

48
apps/agenda/settings.js Normal file
View File

@ -0,0 +1,48 @@
(function(back) {
function gbSend(message) {
Bluetooth.println("");
Bluetooth.println(JSON.stringify(message));
}
var settings = require("Storage").readJSON("agenda.settings.json",1)||{};
function updateSettings() {
require("Storage").writeJSON("agenda.settings.json", settings);
}
var CALENDAR = require("Storage").readJSON("android.calendar.json",true)||[];
var mainmenu = {
"" : { "title" : "Agenda" },
"< Back" : back,
/*LANG*/"Connected" : { value : NRF.getSecurityStatus().connected?/*LANG*/"Yes":/*LANG*/"No" },
/*LANG*/"Force calendar sync" : () => {
if(NRF.getSecurityStatus().connected) {
E.showPrompt(/*LANG*/"Do you want to also clear the internal database first?", {
buttons: {/*LANG*/"Yes": 1, /*LANG*/"No": 2, /*LANG*/"Cancel": 3}
}).then((v)=>{
switch(v) {
case 1:
require("Storage").writeJSON("android.calendar.json",[]);
CALENDAR = [];
/* falls through */
case 2:
gbSend({t:"force_calendar_sync", ids: CALENDAR.map(e=>e.id)});
E.showAlert(/*LANG*/"Request sent to the phone").then(()=>E.showMenu(mainmenu));
break;
case 3:
default:
E.showMenu(mainmenu);
return;
}
});
} else {
E.showAlert(/*LANG*/"You are not connected").then(()=>E.showMenu(mainmenu));
}
},
/*LANG*/"Show past events" : {
value : !!settings.pastEvents,
onchange: v => {
settings.pastEvents = v;
updateSettings();
}
},
};
E.showMenu(mainmenu);
})

2
apps/agpsdata/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.01: First, proof of concept
0.02: Load AGPS data on app start and automatically in background

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

@ -0,0 +1,19 @@
# A-GPS Data
Load assisted GPS (A-GPS) data directly to your Bangle.js using the new http requests on Android GadgetBridge.
Will download A-GPS data in background (if enabled in settings).
The GNSS type can be configured in the settings.
Make sure:
* your GadgetBridge version supports http requests
* turn on internet access in GadgetBridge settings
Currently proof of concept on Bangle.js 2 only.
## Creator
[@pidajo](https://github.com/pidajo)
## Contributor
[@myxor](https://github.com/myxor)

View File

@ -0,0 +1 @@
atob("MDCEAAAAAAAAAAAAAAAAiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAIiIiAAAAAAAAAAAAAAAAAAAAAAAAAAAAIiIiAAAAAAAAAAAAAAAAAAAAAAAAAAACIiIiIAAAAAAAAAAAAAAAAAAAAAAAAAACIiIiIAAAAAAAAAAAAAAAAAAAAAAAAAACIiIiIAAAAAAAAAAAAAAAAAAAAAAAAAAiIiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAiIiIiIgAAAAAAAAAAAAAAAAAAAAAAAAIiIOIiIiAAAAAAAAAAAAAAAAAAAAAAAAIiDOIiIiAAAAAAAAAAAAAAAAAAAAAAAAIiDOIiIiIAAAAAAAAAAAAAAAAAAAAAACIiPOIiIiIAAAAAAAAAAAAAAAAAAAAAAiIj/OIiIiIgAAAAAAAAAAAAAAAAAAAAAiI//OIiIiIgAAAAAAAAAAAAAAAAAAAAAiI//OIiIiIiAAAAAAAAAAAAAAAAAAAAAiD//OIiIiIiAAAAAAAAAAAAAAAAAAAAIiP//OIiIiIiAAAAAAAAAAAAAAAAAAAAIg///OIiIiIiIAAAAAAAAAAAAAAAAAACIj///OIiIiIiIAAAAAAAAAAAAAAAAAACIP///OIiIiIiIgAAAAAAAAAAAAAAAAACI////OIiIiIiIgAAAAAAAAAAAAAAAAAiD////OIiIiIiIiAAAAAAAAAAAAAAAAAiP////OIiIiIiIiAAAAAAAAAAAAAAAAIiP////OIiIiIiIiIAAAAAAAAAAAAAAAIj/////OIiIiIiIiIAAAAAAAAAAAAAACIj/////OIiIiIiIiIgAAAAAAAAAAAAACI//////OIiIiIiIiIgAAAAAAAAAAAAAiI//////OIiIiIiIiIgAAAAAAAAAAAAAiIiIiIiIgzMzMzMziIiAAAAAAAAAAAAAiIiIiIiIj///////+IiAAAAAAAAAAAAIiIiIiIiIj////////4iIAAAAAAAAAAAIiIiIiIiIgzMzMzMzM4iIAAAAAAAAAACIP///////OIiIiIiIiIiIgAAAAAAAAACI////////OIiIiIiIiIiIgAAAAAAAAAiI////////OIiIiIiIiIiIiAAAAAAAAAiP////////OIiIiIiIiIiIiAAAAAAAAIiP////////OIiIiIiIiIiIiIAAAAAAAIj/////////OIiIiIiIiIiIiIAAAAAACIj////////ziIiIiIiIiIiIiIgAAAAACIP///////+IiIiIiIiIiIiIiIgAAAAACI//////84gYiIiIiIiIiIiIiIgAAAAAiD////84iIiAAAiIiIiIiIiIiIiAAAAAiP///ziIiIAAAAAIiIiIiIiIiIiAAAAIg/8ziIiIAAAAAAAAAIiIiIiIiIiIAAAIj/iIiIAAAAAAAAAAAAAIiIiIiIiIAACIiIiBgAAAAAAAAAAAAAAACIiIiIiIgACIiIgAAAAAAAAAAAAAAAAAAACIiIiIgACIgAAAAAAAAAAAAAAAAAAAAAAAiIiIgA==")

BIN
apps/agpsdata/agpsdata.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

54
apps/agpsdata/app.js Normal file
View File

@ -0,0 +1,54 @@
function display(text1, text2) {
g.reset();
g.clear();
var img = require("Storage").read("agpsdata.img");
if (img) {
g.drawImage(img, g.getWidth() - 48, g.getHeight() - 48 - 24);
}
g.setFont("Vector", 18);
g.setFontAlign(0, 1);
g.drawString(text1, g.getWidth() / 2, g.getHeight() / 3 + 24);
if (text2 != undefined) {
g.setFont("Vector", 12);
g.setFontAlign(-1, -1);
g.drawString(text2, 5, g.getHeight() / 3 + 29);
}
Bangle.drawWidgets();
}
// Show launcher when middle button pressed
// Load widgets
Bangle.loadWidgets();
Bangle.drawWidgets();
let waiting = false;
function start() {
g.reset();
g.clear();
waiting = false;
display("Retry?", "touch to retry");
Bangle.on("touch", () => { updateAgps(); });
}
function updateAgps() {
g.reset();
g.clear();
if (!waiting) {
waiting = true;
display("Updating A-GPS...");
require("agpsdata").pull(function() {
waiting = false;
display("A-GPS updated.", "touch to close");
Bangle.on("touch", () => { load(); });
},
function(error) {
waiting = false;
E.showAlert(error, "Error")
.then(() => { start(); });
});
} else {
display("Waiting...");
}
}
updateAgps();

33
apps/agpsdata/boot.js Normal file
View File

@ -0,0 +1,33 @@
(function() {
let waiting = false;
let settings = require("Storage").readJSON("agpsdata.settings.json", 1) || {
enabled: true,
refresh: 1440
};
if (settings.refresh == undefined) settings.refresh = 1440;
function successCallback(){
waiting = false;
}
function errorCallback(){
waiting = false;
}
if (settings.enabled) {
let lastUpdate = settings.lastUpdate;
if (!lastUpdate || lastUpdate + settings.refresh * 1000 * 60 < Date.now()){
if (!waiting){
waiting = true;
require("agpsdata").pull(successCallback, errorCallback);
}
}
setInterval(() => {
if (!waiting && NRF.getSecurityStatus().connected){
waiting = true;
require("agpsdata").pull(successCallback, errorCallback);
}
}, settings.refresh * 1000 * 60);
}
})();

View File

@ -0,0 +1 @@
{"enabled":true,"refresh":1440,"gnsstype":1}

75
apps/agpsdata/lib.js Normal file
View File

@ -0,0 +1,75 @@
function readSettings() {
settings = Object.assign(
require('Storage').readJSON("agpsdata.default.json", true) || {},
require('Storage').readJSON(FILE, true) || {});
}
var FILE = "agpsdata.settings.json";
var settings;
readSettings();
function setAGPS(data) {
var js = jsFromBase64(data);
try {
eval(js);
return true;
}
catch(e) {
console.log("error:", e);
}
return false;
}
function jsFromBase64(b64) {
var bin = atob(b64);
var chunkSize = 128;
var js = "Bangle.setGPSPower(1);\n"; // turn GPS on
var gnsstype = settings.gnsstype || 1; // default GPS
js += `Serial1.println("${CASIC_CHECKSUM("$PCAS04,"+gnsstype)}")\n`; // set GNSS mode
// What about:
// NAV-TIMEUTC (0x01 0x10)
// NAV-PV (0x01 0x03)
// or AGPS.zip uses AID-INI (0x0B 0x01)
for (var i=0;i<bin.length;i+=chunkSize) {
var chunk = bin.substr(i,chunkSize);
js += `Serial1.write(atob("${btoa(chunk)}"))\n`;
}
return js;
}
function CASIC_CHECKSUM(cmd) {
var cs = 0;
for (var i=1;i<cmd.length;i++)
cs = cs ^ cmd.charCodeAt(i);
return cmd+"*"+cs.toString(16).toUpperCase().padStart(2, '0');
}
function updateLastUpdate() {
const file = "agpsdata.json";
let data = require("Storage").readJSON(file, 1) || {};
data.lastUpdate = Math.round(Date.now());
require("Storage").writeJSON(file, data);
}
exports.pull = function(successCallback, failureCallback) {
let uri = "https://www.espruino.com/agps/casic.base64";
if (Bangle.http){
Bangle.http(uri, {timeout:10000}).then(event => {
let result = setAGPS(event.resp);
if (result) {
updateLastUpdate();
if (successCallback) successCallback();
} else {
console.log("error applying AGPS data");
if (failureCallback) failureCallback("Error applying AGPS data");
}
}).catch((e)=>{
console.log("error", e);
if (failureCallback) failureCallback(e);
});
} else {
console.log("error: No http method found");
if (failureCallback) failureCallback(/*LANG*/"No http method");
}
};

View File

@ -0,0 +1,24 @@
{ "id": "agpsdata",
"name": "A-GPS Data",
"shortName":"A-GPS Data",
"icon": "agpsdata.png",
"version":"0.02",
"description": "Download assisted GPS (A-GPS) data directly to your Bangle.js **using Gadgetbridge**",
"tags": "boot,tool,assisted,gps,agps,http",
"allow_emulator":true,
"supports": ["BANGLEJS2"],
"readme":"README.md",
"screenshots" : [ { "url":"screenshot.png" }, { "url":"screenshot2.png" } ],
"storage": [
{"name":"agpsdata.app.js","url":"app.js"},
{"name":"agpsdata.img","url":"agpsdata-icon.js","evaluate":true},
{"name":"agpsdata.default.json","url":"default.json"},
{"name":"agpsdata.boot.js","url":"boot.js"},
{"name":"agpsdata","url":"lib.js"},
{"name":"agpsdata.settings.js","url":"settings.js"}
],
"data": [
{"name": "agpsdata.json"},
{"name": "agpsdata.settings.json"}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

71
apps/agpsdata/settings.js Normal file
View File

@ -0,0 +1,71 @@
(function(back) {
function writeSettings(key, value) {
var s = Object.assign(
require('Storage').readJSON(settingsDefaultFile, true) || {},
require('Storage').readJSON(settingsFile, true) || {});
s[key] = value;
require('Storage').writeJSON(settingsFile, s);
readSettings();
}
function readSettings() {
settings = Object.assign(
require('Storage').readJSON(settingsDefaultFile, true) || {},
require('Storage').readJSON(settingsFile, true) || {});
}
var settingsFile = "agpsdata.settings.json";
var settingsDefaultFile = "agpsdata.default.json";
var settings;
readSettings();
const gnsstypes = [
"", "GPS", "BDS", "GPS+BDS", "GLONASS", "GPS+GLONASS", "BDS+GLONASS",
"GPS+BDS+GLON."
];
function buildMainMenu() {
var mainmenu = {
'' : {'title' : 'AGPS download'},
'< Back' : back,
"Enabled" : {
value : !!settings.enabled,
onchange : v => { writeSettings("enabled", v); }
},
"Refresh every" : {
value : settings.refresh / 60,
min : 1,
max : 168,
step : 1,
format : v => v + "h",
onchange : v => { writeSettings("refresh", Math.round(v * 60)); }
},
"GNSS type" : {
value : settings.gnsstype,
min : 1,
max : 7,
step : 1,
format : v => gnsstypes[v],
onchange : x => writeSettings('gnsstype', x)
},
"Force refresh" : () => {
E.showMessage("Loading A-GPS data");
require("agpsdata")
.pull(
function() {
E.showAlert("Success").then(
() => { E.showMenu(buildMainMenu()); });
},
function(error) {
E.showAlert(error, "Error")
.then(() => { E.showMenu(buildMainMenu()); });
});
}
};
return mainmenu;
}
E.showMenu(buildMainMenu());
});

View File

@ -30,3 +30,8 @@
0.28: Fix bug with alarms not firing when configured to fire only once 0.28: Fix bug with alarms not firing when configured to fire only once
0.29: Fix wrong 'dow' handling in new timer if first day of week is Monday 0.29: Fix wrong 'dow' handling in new timer if first day of week is Monday
0.30: Fix "Enable All" 0.30: Fix "Enable All"
0.31: Add seconds to timers
0.32: Fix wrong hidden filter
Add option for auto-delete a timer after it expires
0.33: Allow hiding timers&alarms

View File

@ -124,6 +124,10 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex) {
value: alarm.as, value: alarm.as,
onchange: v => alarm.as = v onchange: v => alarm.as = v
}, },
/*LANG*/"Hidden": {
value: alarm.hidden || false,
onchange: v => alarm.hidden = v
},
/*LANG*/"Cancel": () => showMainMenu() /*LANG*/"Cancel": () => showMainMenu()
}; };
@ -268,11 +272,28 @@ function showEditTimerMenu(selectedTimer, timerIndex) {
wrap: true, wrap: true,
onchange: v => time.m = v onchange: v => time.m = v
}, },
/*LANG*/"Seconds": {
value: time.s,
min: 0,
max: 59,
step: 1,
wrap: true,
onchange: v => time.s = v
},
/*LANG*/"Enabled": { /*LANG*/"Enabled": {
value: timer.on, value: timer.on,
onchange: v => timer.on = v onchange: v => timer.on = v
}, },
/*LANG*/"Delete After Expiration": {
value: timer.del,
onchange: v => timer.del = v
},
/*LANG*/"Hidden": {
value: timer.hidden || false,
onchange: v => timer.hidden = v
},
/*LANG*/"Vibrate": require("buzz_menu").pattern(timer.vibrate, v => timer.vibrate = v), /*LANG*/"Vibrate": require("buzz_menu").pattern(timer.vibrate, v => timer.vibrate = v),
/*LANG*/"Cancel": () => showMainMenu()
}; };
if (!isNew) { if (!isNew) {

View File

@ -2,7 +2,7 @@
"id": "alarm", "id": "alarm",
"name": "Alarms & Timers", "name": "Alarms & Timers",
"shortName": "Alarms", "shortName": "Alarms",
"version": "0.30", "version": "0.33",
"description": "Set alarms and timers on your Bangle", "description": "Set alarms and timers on your Bangle",
"icon": "app.png", "icon": "app.png",
"tags": "tool,alarm,widget", "tags": "tool,alarm,widget",

View File

@ -2,7 +2,7 @@ WIDGETS["alarm"]={area:"tl",width:0,draw:function() {
if (this.width) g.reset().drawImage(atob("GBgBAAAAAAAAABgADhhwDDwwGP8YGf+YMf+MM//MM//MA//AA//AA//AA//AA//AA//AB//gD//wD//wAAAAADwAABgAAAAAAAAA"),this.x,this.y); if (this.width) g.reset().drawImage(atob("GBgBAAAAAAAAABgADhhwDDwwGP8YGf+YMf+MM//MM//MA//AA//AA//AA//AA//AA//AB//gD//wD//wAAAAADwAABgAAAAAAAAA"),this.x,this.y);
},reload:function() { },reload:function() {
// don't include library here as we're trying to use as little RAM as possible // don't include library here as we're trying to use as little RAM as possible
WIDGETS["alarm"].width = (require('Storage').readJSON('sched.json',1)||[]).some(alarm=>alarm.on&&(alarm.hidden!==false)) ? 24 : 0; WIDGETS["alarm"].width = (require('Storage').readJSON('sched.json',1)||[]).some(alarm=>alarm.on&&(alarm.hidden!==true)) ? 24 : 0;
} }
}; };
WIDGETS["alarm"].reload(); WIDGETS["alarm"].reload();

View File

@ -9,3 +9,7 @@
0.08: Handling of alarms 0.08: Handling of alarms
0.09: Alarm vibration, repeat, and auto-snooze now handled by sched 0.09: Alarm vibration, repeat, and auto-snooze now handled by sched
0.10: Fix SMS bug 0.10: Fix SMS bug
0.12: Use default Bangle formatter for booleans
0.13: Added Bangle.http function (see Readme file for more info)
0.14: Fix timeout of http function not being cleaned up
0.15: Allow method/body/headers to be specified for `http` (needs Gadgetbridge 0.68.0b or later)

View File

@ -32,6 +32,25 @@ Responses are sent back to Gadgetbridge simply as one line of JSON.
More info on message formats on http://www.espruino.com/Gadgetbridge More info on message formats on http://www.espruino.com/Gadgetbridge
## Functions provided
The boot code also provides some useful functions:
* `Bangle.messageResponse = function(msg,response)` - send a yes/no response to a message. `msg` is a message object, and `response` is a boolean.
* `Bangle.musicControl = function(cmd)` - control music, cmd = `play/pause/next/previous/volumeup/volumedown`
* `Bangle.http = function(url,options)` - make an HTTPS request to a URL and return a promise with the data. Requires the [internet enabled `Bangle.js Gadgetbridge` app](http://www.espruino.com/Gadgetbridge#http-requests). `options` can contain:
* `id` - a custom (string) ID
* `timeout` - a timeout for the request in milliseconds (default 30000ms)
* `xpath` an xPath query to run on the request (but right now the URL requested must be XML - HTML is rarely XML compliant)
eg:
```
Bangle.http("https://pur3.co.uk/hello.txt").then(data=>{
console.log("Got ",data);
});
```
## Testing ## Testing
Bangle.js can only hold one connection open at a time, so it's hard to see Bangle.js can only hold one connection open at a time, so it's hard to see

View File

@ -90,10 +90,81 @@
sched.setAlarms(alarms); sched.setAlarms(alarms);
sched.reload(); sched.reload();
}, },
//TODO perhaps move those in a library (like messages), used also for viewing events?
//simple package with events all together
"calendarevents" : function() {
require("Storage").writeJSON("android.calendar.json", event.events);
},
//add and remove events based on activity on phone (pebble-like)
"calendar" : function() {
var cal = require("Storage").readJSON("android.calendar.json",true);
if (!cal || !Array.isArray(cal)) cal = [];
var i = cal.findIndex(e=>e.id==event.id);
if(i<0)
cal.push(event);
else
cal[i] = event;
require("Storage").writeJSON("android.calendar.json", cal);
},
"calendar-" : function() {
var cal = require("Storage").readJSON("android.calendar.json",true);
//if any of those happen we are out of sync!
if (!cal || !Array.isArray(cal)) return;
cal = cal.filter(e=>e.id!=event.id);
require("Storage").writeJSON("android.calendar.json", cal);
},
//triggered by GB, send all ids
"force_calendar_sync_start" : function() {
var cal = require("Storage").readJSON("android.calendar.json",true);
if (!cal || !Array.isArray(cal)) cal = [];
gbSend({t:"force_calendar_sync", ids: cal.map(e=>e.id)});
},
"http":function() {
//get the promise and call the promise resolve
if (Bangle.httpRequest === undefined) return;
var request=Bangle.httpRequest[event.id];
if (request === undefined) return; //already timedout or wrong id
delete Bangle.httpRequest[event.id];
clearTimeout(request.t); //t = timeout variable
if(event.err!==undefined) //if is error
request.j(event.err); //r = reJect function
else
request.r(event); //r = resolve function
}
}; };
var h = HANDLERS[event.t]; var h = HANDLERS[event.t];
if (h) h(); else console.log("GB Unknown",event); if (h) h(); else console.log("GB Unknown",event);
}; };
// HTTP request handling - see the readme
// options = {id,timeout,xpath}
Bangle.http = (url,options)=>{
options = options||{};
if (Bangle.httpRequest === undefined)
Bangle.httpRequest={};
if (options.id === undefined) {
// try and create a unique ID
do {
options.id = Math.random().toString().substr(2);
} while( Bangle.httpRequest[options.id]!==undefined);
}
//send the request
var req = {t: "http", url:url, id:options.id};
if (options.xpath) req.xpath = options.xpath;
if (options.method) req.method = options.method;
if (options.body) req.body = options.body;
if (options.headers) req.headers = options.headers;
gbSend(req);
//create the promise
var promise = new Promise(function(resolve,reject) {
//save the resolve function in the dictionary and create a timeout (30 seconds default)
Bangle.httpRequest[options.id]={r:resolve,j:reject,t:setTimeout(()=>{
//if after "timeoutMillisec" it still hasn't answered -> reject
delete Bangle.httpRequest[options.id];
reject("Timeout");
},options.timeout||30000)};
});
return promise;
}
// Battery monitor // Battery monitor
function sendBattery() { gbSend({ t: "status", bat: E.getBattery(), chg: Bangle.isCharging()?1:0 }); } function sendBattery() { gbSend({ t: "status", bat: E.getBattery(), chg: Bangle.isCharging()?1:0 }); }

View File

@ -2,7 +2,7 @@
"id": "android", "id": "android",
"name": "Android Integration", "name": "Android Integration",
"shortName": "Android", "shortName": "Android",
"version": "0.10", "version": "0.15",
"description": "Display notifications/music/etc sent from the Gadgetbridge app on Android. This replaces the old 'Gadgetbridge' Bangle.js widget.", "description": "Display notifications/music/etc sent from the Gadgetbridge app on Android. This replaces the old 'Gadgetbridge' Bangle.js widget.",
"icon": "app.png", "icon": "app.png",
"tags": "tool,system,messages,notifications,gadgetbridge", "tags": "tool,system,messages,notifications,gadgetbridge",
@ -15,6 +15,6 @@
{"name":"android.img","url":"app-icon.js","evaluate":true}, {"name":"android.img","url":"app-icon.js","evaluate":true},
{"name":"android.boot.js","url":"boot.js"} {"name":"android.boot.js","url":"boot.js"}
], ],
"data": [{"name":"android.settings.json"}], "data": [{"name":"android.settings.json"}, {"name":"android.calendar.json"}],
"sortorder": -8 "sortorder": -8
} }

View File

@ -18,7 +18,6 @@
}), }),
/*LANG*/"Keep Msgs" : { /*LANG*/"Keep Msgs" : {
value : !!settings.keep, value : !!settings.keep,
format : v=>v?/*LANG*/"Yes":/*LANG*/"No",
onchange: v => { onchange: v => {
settings.keep = v; settings.keep = v;
updateSettings(); updateSettings();

View File

@ -9,4 +9,5 @@
when weekday name and calendar weeknumber are on then display is <weekday short> #<calweek> when weekday name and calendar weeknumber are on then display is <weekday short> #<calweek>
week is buffered until date or timezone changes week is buffered until date or timezone changes
0.07: align default settings with app.js (otherwise the initial displayed settings will be confusing to users) 0.07: align default settings with app.js (otherwise the initial displayed settings will be confusing to users)
0.08: fixed calendar weeknumber not shortened to two digits 0.08: fixed calendar weeknumber not shortened to two digits
0.09: Use default Bangle formatter for booleans

View File

@ -1,7 +1,7 @@
{ {
"id": "antonclk", "id": "antonclk",
"name": "Anton Clock", "name": "Anton Clock",
"version": "0.08", "version": "0.09",
"description": "A clock using the bold Anton font, optionally showing seconds and date in ISO-8601 format.", "description": "A clock using the bold Anton font, optionally showing seconds and date in ISO-8601 format.",
"readme":"README.md", "readme":"README.md",
"icon": "app.png", "icon": "app.png",

View File

@ -2,7 +2,6 @@
(function(back) { (function(back) {
var FILE = "antonclk.json"; var FILE = "antonclk.json";
// Load settings
var settings = Object.assign({ var settings = Object.assign({
secondsOnUnlock: false, secondsOnUnlock: false,
}, require('Storage').readJSON(FILE, true) || {}); }, require('Storage').readJSON(FILE, true) || {});
@ -41,7 +40,6 @@
"Date": stringInSettings("dateOnMain", ["Long", "Short", "ISO8601"]), "Date": stringInSettings("dateOnMain", ["Long", "Short", "ISO8601"]),
"Show Weekday": { "Show Weekday": {
value: (settings.weekDay !== undefined ? settings.weekDay : true), value: (settings.weekDay !== undefined ? settings.weekDay : true),
format: v => v ? "On" : "Off",
onchange: v => { onchange: v => {
settings.weekDay = v; settings.weekDay = v;
writeSettings(); writeSettings();
@ -49,7 +47,6 @@
}, },
"Show CalWeek": { "Show CalWeek": {
value: (settings.calWeek !== undefined ? settings.calWeek : false), value: (settings.calWeek !== undefined ? settings.calWeek : false),
format: v => v ? "On" : "Off",
onchange: v => { onchange: v => {
settings.calWeek = v; settings.calWeek = v;
writeSettings(); writeSettings();
@ -57,7 +54,6 @@
}, },
"Uppercase": { "Uppercase": {
value: (settings.upperCase !== undefined ? settings.upperCase : true), value: (settings.upperCase !== undefined ? settings.upperCase : true),
format: v => v ? "On" : "Off",
onchange: v => { onchange: v => {
settings.upperCase = v; settings.upperCase = v;
writeSettings(); writeSettings();
@ -65,7 +61,6 @@
}, },
"Vector font": { "Vector font": {
value: (settings.vectorFont !== undefined ? settings.vectorFont : false), value: (settings.vectorFont !== undefined ? settings.vectorFont : false),
format: v => v ? "On" : "Off",
onchange: v => { onchange: v => {
settings.vectorFont = v; settings.vectorFont = v;
writeSettings(); writeSettings();
@ -82,7 +77,6 @@
"Show": stringInSettings("secondsMode", ["Never", "Unlocked", "Always"]), "Show": stringInSettings("secondsMode", ["Never", "Unlocked", "Always"]),
"With \":\"": { "With \":\"": {
value: (settings.secondsWithColon !== undefined ? settings.secondsWithColon : true), value: (settings.secondsWithColon !== undefined ? settings.secondsWithColon : true),
format: v => v ? "On" : "Off",
onchange: v => { onchange: v => {
settings.secondsWithColon = v; settings.secondsWithColon = v;
writeSettings(); writeSettings();
@ -90,7 +84,6 @@
}, },
"Color": { "Color": {
value: (settings.secondsColoured !== undefined ? settings.secondsColoured : true), value: (settings.secondsColoured !== undefined ? settings.secondsColoured : true),
format: v => v ? "On" : "Off",
onchange: v => { onchange: v => {
settings.secondsColoured = v; settings.secondsColoured = v;
writeSettings(); writeSettings();
@ -99,9 +92,6 @@
"Date": stringInSettings("dateOnSecs", ["Year", "Weekday", "No"]) "Date": stringInSettings("dateOnSecs", ["Year", "Weekday", "No"])
}; };
// Actually display the menu
E.showMenu(mainmenu); E.showMenu(mainmenu);
}); });
// end of file

View File

@ -11,3 +11,5 @@
0.11: Use ClockFace.is12Hour 0.11: Use ClockFace.is12Hour
0.12: Add settings to hide date,widgets 0.12: Add settings to hide date,widgets
0.13: Add font setting 0.13: Add font setting
0.14: Use ClockFace_menu.addItems
0.15: Add Power saving option

View File

@ -7,4 +7,5 @@ A simple digital clock showing seconds as a horizontal bar.
## Settings ## Settings
* `Show date`: display date at the bottom of screen * `Show date`: display date at the bottom of screen
* `Font`: choose between bitmap or vector fonts * `Font`: choose between bitmap or vector fonts
* `Power saving`: (Bangle.js 2 only) don't draw the seconds bar while the watch is locked

View File

@ -13,16 +13,20 @@ let locale = require("locale");
locale.hasMeridian = (locale.meridian(date)!==""); locale.hasMeridian = (locale.meridian(date)!=="");
} }
let barW = 0, prevX = 0;
function renderBar(l) { function renderBar(l) {
if (!this.fraction) { "ram";
// zero-size fillRect stills draws one line of pixels, we don't want that if (l) prevX = 0; // called from Layout: drawing area was cleared
return; else l = clock.layout.bar;
} let x2 = l.x+barW;
const width = this.fraction*l.w; if (clock.powerSave && Bangle.isLocked()) x2 = 0; // hide bar
g.fillRect(l.x, l.y, l.x+width-1, l.y+l.height-1); if (x2===prevX) return; // nothing to do
if (x2===0) x2--; // don't leave 1px line
if (x2<Math.max(0, prevX)) g.setBgColor(l.bgCol || g.theme.bg).clearRect(x2+1, l.y, prevX, l.y2);
else g.setColor(l.col || g.theme.fg).fillRect(prevX+1, l.y, x2, l.y2);
prevX = x2;
} }
function timeText(date) { function timeText(date) {
if (!clock.is12Hour) { if (!clock.is12Hour) {
return locale.time(date, true); return locale.time(date, true);
@ -47,22 +51,21 @@ function dateText(date) {
return `${dayName} ${dayMonth}`; return `${dayName} ${dayMonth}`;
} }
const ClockFace = require("ClockFace"), const ClockFace = require("ClockFace"),
clock = new ClockFace({ clock = new ClockFace({
precision:1, precision: 1,
settingsFile:'barclock.settings.json', settingsFile: "barclock.settings.json",
init: function() { init: function() {
const Layout = require("Layout"); const Layout = require("Layout");
this.layout = new Layout({ this.layout = new Layout({
type: "v", c: [ type: "v", c: [
{ {
type: "h", c: [ type: "h", c: [
{id: "time", label: "88:88", type: "txt", font: "6x8:5", col:g.theme.fg, bgCol: g.theme.bg}, // updated below {id: "time", label: "88:88", type: "txt", font: "6x8:5", col: g.theme.fg, bgCol: g.theme.bg}, // updated below
{id: "ampm", label: " ", type: "txt", font: "6x8:2", col:g.theme.fg, bgCol: g.theme.bg}, {id: "ampm", label: " ", type: "txt", font: "6x8:2", col: g.theme.fg, bgCol: g.theme.bg},
], ],
}, },
{id: "bar", type: "custom", fraction: 0, fillx: 1, height: 6, col: g.theme.fg2, render: renderBar}, {id: "bar", type: "custom", fillx: 1, height: 6, col: g.theme.fg2, render: renderBar},
this.showDate ? {height: 40} : {}, this.showDate ? {height: 40} : {},
this.showDate ? {id: "date", type: "txt", font: "10%", valign: 1} : {}, this.showDate ? {id: "date", type: "txt", font: "10%", valign: 1} : {},
], ],
@ -76,7 +79,8 @@ const ClockFace = require("ClockFace"),
this.layout.ampm.label = ""; this.layout.ampm.label = "";
thickness = Math.floor(Bangle.appRect.w/(5*6)); thickness = Math.floor(Bangle.appRect.w/(5*6));
} }
this.layout.bar.height = thickness+1; let bar = this.layout.bar;
bar.height = thickness+1;
if (this.font===1) { // vector if (this.font===1) { // vector
const B2 = process.env.HWVERSION>1; const B2 = process.env.HWVERSION>1;
if (this.is12Hour && locale.hasMeridian) { if (this.is12Hour && locale.hasMeridian) {
@ -89,17 +93,32 @@ const ClockFace = require("ClockFace"),
this.layout.time.font = "6x8:"+thickness; this.layout.time.font = "6x8:"+thickness;
} }
this.layout.update(); this.layout.update();
bar.y2 = bar.y+bar.height-1;
}, },
update: function(date, c) { update: function(date, c) {
"ram";
if (c.m) this.layout.time.label = timeText(date); if (c.m) this.layout.time.label = timeText(date);
if (c.h) this.layout.ampm.label = ampmText(date); if (c.h) this.layout.ampm.label = ampmText(date);
if (c.d && this.showDate) this.layout.date.label = dateText(date); if (c.d && this.showDate) this.layout.date.label = dateText(date);
const SECONDS_PER_MINUTE = 60; if (c.m) this.layout.render();
if (c.s) this.layout.bar.fraction = date.getSeconds()/SECONDS_PER_MINUTE; if (c.s) {
this.layout.render(); barW = Math.round(date.getSeconds()/60*this.layout.bar.w);
renderBar();
}
}, },
resume: function() { resume: function() {
prevX = 0; // force redraw of bar
this.layout.forgetLazyState(); this.layout.forgetLazyState();
}, },
}); });
// power saving: only update once a minute while locked, hide bar
if (clock.powerSave) {
Bangle.on("lock", lock => {
clock.precision = lock ? 60 : 1;
clock.tick();
renderBar(); // hide/redraw bar right away
});
}
clock.start(); clock.start();

View File

@ -1,7 +1,7 @@
{ {
"id": "barclock", "id": "barclock",
"name": "Bar Clock", "name": "Bar Clock",
"version": "0.13", "version": "0.15",
"description": "A simple digital clock showing seconds as a bar", "description": "A simple digital clock showing seconds as a bar",
"icon": "clock-bar.png", "icon": "clock-bar.png",
"screenshots": [{"url":"screenshot.png"},{"url":"screenshot_pm.png"}], "screenshots": [{"url":"screenshot.png"},{"url":"screenshot_pm.png"}],

View File

@ -1,26 +1,30 @@
(function(back) { (function(back) {
let s = require('Storage').readJSON("barclock.settings.json", true) || {}; let s = require("Storage").readJSON("barclock.settings.json", true) || {};
function saver(key) { function save(key, value) {
return value => { s[key] = value;
s[key] = value; require("Storage").writeJSON("barclock.settings.json", s);
require('Storage').writeJSON("barclock.settings.json", s);
}
} }
const fonts = [/*LANG*/"Bitmap",/*LANG*/"Vector"]; const fonts = [/*LANG*/"Bitmap",/*LANG*/"Vector"];
const menu = { let menu = {
"": {"title": /*LANG*/"Bar Clock"}, "": {"title": /*LANG*/"Bar Clock"},
/*LANG*/"< Back": back, /*LANG*/"< Back": back,
/*LANG*/"Show date": require("ClockFace_menu").showDate(s.showDate, saver('showDate')),
/*LANG*/"Load widgets": require("ClockFace_menu").loadWidgets(s.loadWidgets, saver('loadWidgets')),
/*LANG*/"Font": { /*LANG*/"Font": {
value: s.font|0, value: s.font|0,
min:0,max:1,wrap:true, min: 0, max: 1, wrap: true,
format:v=>fonts[v], format: v => fonts[v],
onchange:saver('font'), onchange: v => save("font", v),
}, },
}; };
let items = {
showDate: s.showDate,
loadWidgets: s.loadWidgets,
};
// Power saving for Bangle.js 1 doesn't make sense (no updates while screen is off anyway)
if (process.env.HWVERSION>1) {
items.powerSave = s.powerSave;
}
require("ClockFace_menu").addItems(menu, save, items);
E.showMenu(menu); E.showMenu(menu);
}); });

View File

@ -1,3 +1,5 @@
0.01: Initial version 0.01: Initial version
0.02: setTimeout bug fix; no leading zero on date; lightmode; 12 hour format; cleanup 0.02: setTimeout bug fix; no leading zero on date; lightmode; 12 hour format; cleanup
0.03: Internationalisation; bug fix - battery icon responds promptly to charging state 0.03: Internationalisation; bug fix - battery icon responds promptly to charging state
0.04: bug fix
0.05: proper fix for the race condition in queueDraw()

View File

@ -12,12 +12,12 @@ Graphics.prototype.setFontOpenSans = function(scale) {
var drawTimeout; var drawTimeout;
// schedule a draw for the next minute function queueDraw(millis_now) {
function queueDraw() {
if (drawTimeout) clearTimeout(drawTimeout); if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = setTimeout(function () { drawTimeout = setTimeout(function () {
drawTimeout = undefined;
draw(); draw();
}, 60300 - (Date.now() % 60000)); // We aim for 300ms into the next minute to ensure we make it! }, 60000 - (millis_now % 60000));
} }
function draw() { function draw() {
@ -69,7 +69,7 @@ function draw() {
// widget redraw // widget redraw
Bangle.drawWidgets(); Bangle.drawWidgets();
queueDraw(); queueDraw(date.getTime());
} }
Bangle.on('lcdPower', on => { Bangle.on('lcdPower', on => {

View File

@ -1,7 +1,7 @@
{ "id": "bigdclock", { "id": "bigdclock",
"name": "Big digit clock containing just the essentials", "name": "Big digit clock containing just the essentials",
"shortName":"Big digit clk", "shortName":"Big digit clk",
"version":"0.03", "version":"0.05",
"description": "A clock containing just the essentials, made as easy to read as possible for those of us that need glasses. It contains the time, the day-of-week, the day-of-month, and the current battery state-of-charge.", "description": "A clock containing just the essentials, made as easy to read as possible for those of us that need glasses. It contains the time, the day-of-week, the day-of-month, and the current battery state-of-charge.",
"icon": "bigdclock.png", "icon": "bigdclock.png",
"type": "clock", "type": "clock",

View File

@ -1,2 +1,3 @@
0.01: New App! 0.01: New App!
0.02: Barometer altitude adjustment setting 0.02: Barometer altitude adjustment setting
0.03: Use default Bangle formatter for booleans

View File

@ -2,7 +2,7 @@
"id": "bikespeedo", "id": "bikespeedo",
"name": "Bike Speedometer (beta)", "name": "Bike Speedometer (beta)",
"shortName": "Bike Speedometer", "shortName": "Bike Speedometer",
"version": "0.02", "version": "0.03",
"description": "Shows GPS speed, GPS heading, Compass heading, GPS altitude and Barometer altitude from internal sources", "description": "Shows GPS speed, GPS heading, Compass heading, GPS altitude and Barometer altitude from internal sources",
"icon": "app.png", "icon": "app.png",
"screenshots": [{"url":"Screenshot.png"}], "screenshots": [{"url":"Screenshot.png"}],

View File

@ -33,12 +33,10 @@
'< Back': function() { E.showMenu(appMenu); }, '< Back': function() { E.showMenu(appMenu); },
'Speed' : { 'Speed' : {
value : settings.spdFilt, value : settings.spdFilt,
format : v => v?"On":"Off",
onchange : () => { settings.spdFilt = !settings.spdFilt; writeSettings(); } onchange : () => { settings.spdFilt = !settings.spdFilt; writeSettings(); }
}, },
'Altitude' : { 'Altitude' : {
value : settings.altFilt, value : settings.altFilt,
format : v => v?"On":"Off",
onchange : () => { settings.altFilt = !settings.altFilt; writeSettings(); } onchange : () => { settings.altFilt = !settings.altFilt; writeSettings(); }
} }
}; };

3
apps/bowserWF/ChangeLog Normal file
View File

@ -0,0 +1,3 @@
...
0.02: First update with ChangeLog Added
0.03: updated watch face to use the ClockFace library

View File

@ -1,102 +1,233 @@
var sprite = { var sprite = {
width : 47, height : 47, bpp : 3, width: 47,
transparent : 1, height: 47,
buffer : require("heatshrink").decompress(atob("kmSpICFn/+BAwCImV//VICJuT//SogRMpmT/2SCJtSyQDB/4RMymRkmX/gRLygDC3/piVhCJElAYf/pNIkgRIlIDCl/6pVBkIRIGwWJEYPypMJCI9KGwQRBLANIPRI2CGoPkyVCBwmeyVLTYNJom8yImBz4gEqV/6Vf+g2BPwf/IIq8C/+kyVRkgDBp/5CIX/+mkz/+y/9BIOf0v6///5LdCz+kCIOk34RBYQMSp5XBGQVk/pNBAQP/9IyBxGSv4yCk/1OIK8EC4QgEpM/JgJ+EGoIRBTApQCEYvplLOFXIIdBO4SqBeQJABGoeTDQMlk5WCAAPSYQLgEz4aBlM/9IgB/7CCcAvP/QsBiVfUwOJBgUiCIcmpAVCy/+pMAKwMkRgIRCp6VBAwW6qVOgmSgPkwgRDv53E6WSuEkyEPRgmf2VJv5HBl2SgAKBwEJRgnJiVKp/Sr/0y/yBQOQv56DKwVSv2STwO/DgWD/BADmaDByRoBYoQRCgFCCIf/+jgDNwOUAwMg/kSPQbODX4IJBAwUH8B6DsmRl5oBl7OBklMyV+gBoDycSxMpiVLZwS8EAQeYyjaByR6BBIJBDAQnEIgbFCogOFRgQDBr//I4L0EAQsxAYP//5WCGQ6MCAAKbCpKYEAQiMB//kIQOUyf+CJF/CIIEBTYOfcgQRHBQv/CJKnBpP8GRTCDJIPkGRQCB5I3C/n/EZUgA")) bpp: 3,
transparent: 1,
buffer: require("heatshrink").decompress(
atob(
"kmSpICFn/+BAwCImV//VICJuT//SogRMpmT/2SCJtSyQDB/4RMymRkmX/gRLygDC3/piVhCJElAYf/pNIkgRIlIDCl/6pVBkIRIGwWJEYPypMJCI9KGwQRBLANIPRI2CGoPkyVCBwmeyVLTYNJom8yImBz4gEqV/6Vf+g2BPwf/IIq8C/+kyVRkgDBp/5CIX/+mkz/+y/9BIOf0v6///5LdCz+kCIOk34RBYQMSp5XBGQVk/pNBAQP/9IyBxGSv4yCk/1OIK8EC4QgEpM/JgJ+EGoIRBTApQCEYvplLOFXIIdBO4SqBeQJABGoeTDQMlk5WCAAPSYQLgEz4aBlM/9IgB/7CCcAvP/QsBiVfUwOJBgUiCIcmpAVCy/+pMAKwMkRgIRCp6VBAwW6qVOgmSgPkwgRDv53E6WSuEkyEPRgmf2VJv5HBl2SgAKBwEJRgnJiVKp/Sr/0y/yBQOQv56DKwVSv2STwO/DgWD/BADmaDByRoBYoQRCgFCCIf/+jgDNwOUAwMg/kSPQbODX4IJBAwUH8B6DsmRl5oBl7OBklMyV+gBoDycSxMpiVLZwS8EAQeYyjaByR6BBIJBDAQnEIgbFCogOFRgQDBr//I4L0EAQsxAYP//5WCGQ6MCAAKbCpKYEAQiMB//kIQOUyf+CJF/CIIEBTYOfcgQRHBQv/CJKnBpP8GRTCDJIPkGRQCB5I3C/n/EZUgA"
)
),
}; };
const boxes = { const boxes = {
width : 122, height : 56, bpp : 3, width: 122,
transparent : 1, height: 56,
buffer : require("heatshrink").decompress(atob("kmZkmSpICPwgDBmQUQAQMJAYNkFiOSiQDB5JESAYQsSpADByYsSyBZBydt23bAR+wgFJkwUQAQNggGSposR23AgMkzZESwECpM2IiUAgmSFiW2gDlBFiVsgDlBFiXYgDNBL4MDWZy2FgEGWZy2FgENWZy2EL4MbWZpTBWwZfBXJpTCWwZiCWZpTBWwZiCWZsbWwhiCWZpWCWwTORWwgXRWwgXRWwZESWwZESWwZESWwYXRWwgXRW362/W362/W362/W362/W362/W362/W362/W362/W362/W362/WwuAgazOWwsAgyzOWwsAhqzOWwhfBjazNKYK2DL4K5NKYS2DMQSzNKYK2DMQSzNja2EMQSzNKwS2CZyK2EC6K2EC6K2DIiS2DIiS2DIiUAFoMAAFTkBFtckyAtrLgWSpICnLIIsqyVAgAsqpIA=")) bpp: 3,
transparent: 1,
buffer: require("heatshrink").decompress(
atob(
"kmZkmSpICPwgDBmQUQAQMJAYNkFiOSiQDB5JESAYQsSpADByYsSyBZBydt23bAR+wgFJkwUQAQNggGSposR23AgMkzZESwECpM2IiUAgmSFiW2gDlBFiVsgDlBFiXYgDNBL4MDWZy2FgEGWZy2FgENWZy2EL4MbWZpTBWwZfBXJpTCWwZiCWZpTBWwZiCWZsbWwhiCWZpWCWwTORWwgXRWwgXRWwZESWwZESWwZESWwYXRWwgXRW362/W362/W362/W362/W362/W362/W362/W362/W362/W362/WwuAgazOWwsAgyzOWwsAhqzOWwhfBjazNKYK2DL4K5NKYS2DMQSzNKYK2DMQSzNja2EMQSzNKwS2CZyK2EC6K2EC6K2DIiS2DIiS2DIiUAFoMAAFTkBFtckyAtrLgWSpICnLIIsqyVAgAsqpIA="
)
),
}; };
const background = { const background = {
width : 176, height : 176, bpp : 3, width: 176,
transparent : 5, height: 176,
buffer : require("heatshrink").decompress(atob("kmSpIC/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/ATWAgEAIP1///8iRB8gf/AAOCIPdIIARBBoJB/+E4IP4ABghB9v4CB8BB5g/92//9pB7wP/97FEIO9IgDACAAn8iVBIOlHH4xBDnA+wyY9IAAmB/BB//5B/IOQ/OAARBup5B/yV/IP5B/IP5BRt5B7/wDC7aD8/w+B+3bBgP7IP5B7HYNt23/AQPfIPX/9oCC24IDINwCBIRAAHIOACBHI3+g4EC/l/4BByAQkA//wpED//4gGAhJB3pMAgQFBgEBH3AC/AX4C/AX4C/AX4C/AX4C/AUOAgBB/v//ghB9gf///gH3UgiVIIAJBBwRB5j+CIIf8uBB5//wIIXb//+hJB6o/92/7v5B7/0/97GCIPYAG4MgIP/BjkSIP34/hB//5B/AAQ+0IP5B/IP5BN7ZB97///wCBIPX93yAB2wCB+5B5tv//dt24CB35B5v/+n/t+P/I4PH8ESIO38gFA/+CgH/+EIgiD3gACCPoMAgQ+2AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/ASVIgAACgRB/IPY8GkAHBiRB/IPBLKgJB/IP5B/AQUAkmQghB/IP2AgEAyVAiRB/IP5BBpMAIP5B/IIUkgBB/IP5BpoAsBgJBOgEEIIoIBIP5BlyE27dt2EEIJ4CBBAlIgRBgpEAhu2IIO24ESQwxB/IJQhGkEJIL8GHwQCDgOweQpB/IKMkwAKJILVgAofYeQhBzsEAIKICLoESILmBQARBBtuwgZB3kA4B4ENIgJBcpMAIMYCDIOcAgEbHYgCGsEJkhEBE6cBIP5BZfYQ+JIIkDsEBIP5BVyEAIKtAHxgCDwBEBINk2IKCGCIKmSpECIP5BUkEBHyACD2BBUFoMJIP5BSpEbHyQCDIP5BXkmAIP5B/AQcAbKJB/ILH/AAP8hM/AgWSv4KCAAP+gmfAoXJk4ME//gpIEC8mTBgvwkgEC+QRDAAX4gVPAgP5kgsCLwWQh/kMIUf5LuFg4jBAoMBKAJ5EwF/AoUA/yFFoE/CI6RDgY+BCIQsDIP5B/IP5B/IP5B/IJ/AIJfghJBKv0EIJcAIJfwIP5BMhMAAAMEz5BGgmABoVJII9IBgUkII8kBgUSII8CoAMBhJB/IIsQoMAYoP/AAP4YpAMC/+BII9/BgXAYpAMC8DFIBgXwIIcCIP6DCgkQh/kCIRBIbQcBIJAFCgBBICI5BE/IRDFgQA=")) bpp: 3,
transparent: 5,
buffer: require("heatshrink").decompress(
atob(
"kmSpIC/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/ATWAgEAIP1///8iRB8gf/AAOCIPdIIARBBoJB/+E4IP4ABghB9v4CB8BB5g/92//9pB7wP/97FEIO9IgDACAAn8iVBIOlHH4xBDnA+wyY9IAAmB/BB//5B/IOQ/OAARBup5B/yV/IP5B/IP5BRt5B7/wDC7aD8/w+B+3bBgP7IP5B7HYNt23/AQPfIPX/9oCC24IDINwCBIRAAHIOACBHI3+g4EC/l/4BByAQkA//wpED//4gGAhJB3pMAgQFBgEBH3AC/AX4C/AX4C/AX4C/AX4C/AUOAgBB/v//ghB9gf///gH3UgiVIIAJBBwRB5j+CIIf8uBB5//wIIXb//+hJB6o/92/7v5B7/0/97GCIPYAG4MgIP/BjkSIP34/hB//5B/AAQ+0IP5B/IP5BN7ZB97///wCBIPX93yAB2wCB+5B5tv//dt24CB35B5v/+n/t+P/I4PH8ESIO38gFA/+CgH/+EIgiD3gACCPoMAgQ+2AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/ASVIgAACgRB/IPY8GkAHBiRB/IPBLKgJB/IP5B/AQUAkmQghB/IP2AgEAyVAiRB/IP5BBpMAIP5B/IIUkgBB/IP5BpoAsBgJBOgEEIIoIBIP5BlyE27dt2EEIJ4CBBAlIgRBgpEAhu2IIO24ESQwxB/IJQhGkEJIL8GHwQCDgOweQpB/IKMkwAKJILVgAofYeQhBzsEAIKICLoESILmBQARBBtuwgZB3kA4B4ENIgJBcpMAIMYCDIOcAgEbHYgCGsEJkhEBE6cBIP5BZfYQ+JIIkDsEBIP5BVyEAIKtAHxgCDwBEBINk2IKCGCIKmSpECIP5BUkEBHyACD2BBUFoMJIP5BSpEbHyQCDIP5BXkmAIP5B/AQcAbKJB/ILH/AAP8hM/AgWSv4KCAAP+gmfAoXJk4ME//gpIEC8mTBgvwkgEC+QRDAAX4gVPAgP5kgsCLwWQh/kMIUf5LuFg4jBAoMBKAJ5EwF/AoUA/yFFoE/CI6RDgY+BCIQsDIP5B/IP5B/IP5B/IJ/AIJfghJBKv0EIJcAIJfwIP5BMhMAAAMEz5BGgmABoVJII9IBgUkII8kBgUSII8CoAMBhJB/IIsQoMAYoP/AAP4YpAMC/+BII9/BgXAYpAMC8DFIBgXwIIcCIP6DCgkQh/kCIRBIbQcBIJAFCgBBICI5BE/IRDFgQA="
)
),
}; };
numbersDims = { numbersDims = {
width: 20, width: 20,
height: 44 height: 44,
}; };
const numbers = [ const numbers = [
require("heatshrink").decompress(atob("ikswcBkmSpIC/ARGQKYQIDAwUEBxMAAQNAgECpMgAQMkB4IOIAQQLCgEQBwQaBgEBB1oCBBwYCCiRWDCIRWEO5wOHAX4CnA=")), require("heatshrink").decompress(
require("heatshrink").decompress(atob("ikswcBkmSpIC/ARNIKYIIEwEAggOKNIQODyAHCBxQsWB3TUFgMgA4sSBwzU/AVA=")), atob(
require("heatshrink").decompress(atob("ikswcBkmSpIC/AQ8gKggIBAwkCBw+QCIQLCgIRCDQcQBwwyDDwUSCgVAAwIOBEwI7EpI7FBw4FDghZGHwgOEF4Y+CEYQ+DBxQADNAIAFNAIOFa/4CoA=")), "ikswcBkmSpIC/ARGQKYQIDAwUEBxMAAQNAgECpMgAQMkB4IOIAQQLCgEQBwQaBgEBB1oCBBwYCCiRWDCIRWEO5wOHAX4CnA="
require("heatshrink").decompress(atob("ikswcBkmSpIC/AQ8gKosSAwsBBw4aCoEAgQjEBoIpEBwtIBoIUEwEAggUDBwwyDDoWQA4ZWHhIIEJQoOCgI+EBwMQEAYOJO4oLBO4oRDJQrX/AU4")), )
require("heatshrink").decompress(atob("ikswcBkmSpIC/ARNIKgQIDwAGBgQOJNQYOCyAHDBxEggB6BBwYDBiVABxIjBCIIODF4YOEAAkBV40QBwxiDNAosEB0IC/AUg")), ),
require("heatshrink").decompress(atob("ikswcBkmSpIC/AQ5UFkmQAwkCBxIdGCIIIDBxAsTgAaEkEASooOBiQOVJQgOBiBKDBxMSJQwRBLIgRCBwjX/AVA=")), require("heatshrink").decompress(
require("heatshrink").decompress(atob("ikswcBkmSpIC/ARGQKgYICAwcCBxADBiQdDkEANYoOGEAYyEHYoOIHYqfFBxIdDBAMQFgZHCBysSFgwRBO46GFa/4CnA")), atob("ikswcBkmSpIC/ARNIKYIIEwEAggOKNIQODyAHCBxQsWB3TUFgMgA4sSBwzU/AVA=")
require("heatshrink").decompress(atob("ikswcBkmSpIC/AQ5VGiAGFgIOIDQUgBwUCEYQOJGQYNBHAlADQgOHwEAggUDpANBCgYpBBwmQAwJiGhIjDB1gC/AU4A=")), ),
require("heatshrink").decompress(atob("ikswcBkmSpIC/AQ8gKYYICAwcEBxGQgAaDgVJgACBDQQOJgB6CBwcAiQODHa4AEhIRBpAHDiARBwAGCgIgCFIYOCFIYOHiQrEJQxlCBwzX/AVAA=")), require("heatshrink").decompress(
require("heatshrink").decompress(atob("ikswcBkmSpIC/AQ8gKggIBAwkCBw+QCIQLCgIRCDQcQBzkSTAsBHYoOIL4gOCMooOENAYOCoA4EBwoqDgiGGF4gOEa/4CoA=")), atob(
"ikswcBkmSpIC/AQ8gKggIBAwkCBw+QCIQLCgIRCDQcQBwwyDDwUSCgVAAwIOBEwI7EpI7FBw4FDghZGHwgOEF4Y+CEYQ+DBxQADNAIAFNAIOFa/4CoA="
)
),
require("heatshrink").decompress(
atob(
"ikswcBkmSpIC/AQ8gKosSAwsBBw4aCoEAgQjEBoIpEBwtIBoIUEwEAggUDBwwyDDoWQA4ZWHhIIEJQoOCgI+EBwMQEAYOJO4oLBO4oRDJQrX/AU4"
)
),
require("heatshrink").decompress(
atob(
"ikswcBkmSpIC/ARNIKgQIDwAGBgQOJNQYOCyAHDBxEggB6BBwYDBiVABxIjBCIIODF4YOEAAkBV40QBwxiDNAosEB0IC/AUg"
)
),
require("heatshrink").decompress(
atob(
"ikswcBkmSpIC/AQ5UFkmQAwkCBxIdGCIIIDBxAsTgAaEkEASooOBiQOVJQgOBiBKDBxMSJQwRBLIgRCBwjX/AVA="
)
),
require("heatshrink").decompress(
atob(
"ikswcBkmSpIC/ARGQKgYICAwcCBxADBiQdDkEANYoOGEAYyEHYoOIHYqfFBxIdDBAMQFgZHCBysSFgwRBO46GFa/4CnA"
)
),
require("heatshrink").decompress(
atob(
"ikswcBkmSpIC/AQ5VGiAGFgIOIDQUgBwUCEYQOJGQYNBHAlADQgOHwEAggUDpANBCgYpBBwmQAwJiGhIjDB1gC/AU4A="
)
),
require("heatshrink").decompress(
atob(
"ikswcBkmSpIC/AQ8gKYYICAwcEBxGQgAaDgVJgACBDQQOJgB6CBwcAiQODHa4AEhIRBpAHDiARBwAGCgIgCFIYOCFIYOHiQrEJQxlCBwzX/AVAA="
)
),
require("heatshrink").decompress(
atob(
"ikswcBkmSpIC/AQ8gKggIBAwkCBw+QCIQLCgIRCDQcQBzkSTAsBHYoOIL4gOCMooOENAYOCoA4EBwoqDgiGGF4gOEa/4CoA="
)
),
]; ];
digitPositions = [ // relative to the box digitPositions = [
{x:13, y:6}, {x:32, y:6}, // relative to the box
{x:74, y:6}, {x:93, y:6}, { x: 13, y: 6 },
{ x: 32, y: 6 },
{ x: 74, y: 6 },
{ x: 93, y: 6 },
]; ];
var drawTimeout;
const animation_duration = 1; // seconds const animation_duration = 1; // seconds
const animation_steps = 20; const animation_steps = 20;
const jump_height = 45; // top coordinate of the jump const jump_height = 45; // top coordinate of the jump
const seconds_per_minute = 60; const seconds_per_minute = 60;
function draw() { const ClockFace = require("ClockFace");
const now = new Date(); const clock = new ClockFace({
g.drawImage(background, 0, 0); precision: 60, // just once a minute
var boxTL_x = 27; var boxTL_y = 29;
var sprite_TL_x = 72; var sprite_TL_y = 161 - sprite.height;
const seconds = now.getSeconds()%seconds_per_minute + now.getMilliseconds()/1000;
const hours = now.getHours();
const minutes = now.getMinutes();
var time_advance = seconds / animation_duration;
if (time_advance < 0.5) {
sprite_TL_y += (jump_height - sprite_TL_y) * time_advance * 2;
} else if (time_advance < 1) {
sprite_TL_y = jump_height + (sprite_TL_y-jump_height) * (time_advance-0.5) * 2;
}
const box_penetration = boxTL_y + boxes.height - sprite_TL_y;
if (box_penetration > 0) {
boxTL_y -= box_penetration;
}
g.drawImage(boxes, boxTL_x, boxTL_y);
g.drawImage(numbers[(hours / 10) >> 0], boxTL_x+digitPositions[0].x, boxTL_y+digitPositions[0].y);
g.drawImage(numbers[(hours % 10) >> 0], boxTL_x+digitPositions[1].x, boxTL_y+digitPositions[1].y);
g.drawImage(numbers[(minutes / 10) >> 0], boxTL_x+digitPositions[2].x, boxTL_y+digitPositions[2].y);
g.drawImage(numbers[(minutes % 10) >> 0], boxTL_x+digitPositions[3].x, boxTL_y+digitPositions[3].y);
g.drawImage(sprite, sprite_TL_x, sprite_TL_y);
Bangle.drawWidgets();
const timeout = time_advance <= 1?
animation_duration / animation_steps
: (seconds_per_minute - seconds);
setTimeout( _=>{
drawTimeout = undefined;
draw();
}, timeout * 1000);
}
// Clear the screen once, at startup init: function() {
g.setTheme({bg:"#00f",fg:"#fff",dark:true}).clear(); // Clear the screen once, at startup
g.setTheme({ bg: "#00f", fg: "#fff", dark: true }).clear();
Bangle.on('lcdPower',on=>{ this.drawing = true;
if (on) {
draw(); // draw immediately, queue redraw this.simpleDraw = function(now) {
} else { // stop draw timer var boxTL_x = 27;
if (drawTimeout) { var boxTL_y = 29;
clearTimeout(drawTimeout); var sprite_TL_x = 72;
} var sprite_TL_y = 161 - sprite.height;
drawTimeout = undefined; const seconds =
} (now.getSeconds() % seconds_per_minute) + now.getMilliseconds() / 1000;
const hours =
this.is12Hour && now.getHours() > 12
? now.getHours() - 12
: now.getHours();
const minutes = now.getMinutes();
g.drawImage(boxes, boxTL_x, boxTL_y);
g.drawImage(
numbers[(hours / 10) >> 0],
boxTL_x + digitPositions[0].x,
boxTL_y + digitPositions[0].y
);
g.drawImage(
numbers[hours % 10 >> 0],
boxTL_x + digitPositions[1].x,
boxTL_y + digitPositions[1].y
);
g.drawImage(
numbers[(minutes / 10) >> 0],
boxTL_x + digitPositions[2].x,
boxTL_y + digitPositions[2].y
);
g.drawImage(
numbers[minutes % 10 >> 0],
boxTL_x + digitPositions[3].x,
boxTL_y + digitPositions[3].y
);
};
},
pause: function() {
this.drawing = false;
},
resume: function() {
this.drawing = true;
},
draw: function(now) {
if (!this.drawing) {
this.simpleDraw(now);
return;
}
g.drawImage(background, 0, 0);
var boxTL_x = 27;
var boxTL_y = 29;
var sprite_TL_x = 72;
var sprite_TL_y = 161 - sprite.height;
const seconds =
(now.getSeconds() % seconds_per_minute) + now.getMilliseconds() / 1000;
const hours =
this.is12Hour && now.getHours() > 12
? now.getHours() - 12
: now.getHours();
const minutes = now.getMinutes();
var time_advance = seconds / animation_duration;
if (time_advance < 0.5) {
sprite_TL_y += (jump_height - sprite_TL_y) * time_advance * 2;
} else if (time_advance < 1) {
sprite_TL_y =
jump_height + (sprite_TL_y - jump_height) * (time_advance - 0.5) * 2;
}
const box_penetration = boxTL_y + boxes.height - sprite_TL_y;
if (box_penetration > 0) {
boxTL_y -= box_penetration;
}
g.drawImage(boxes, boxTL_x, boxTL_y);
g.drawImage(
numbers[(hours / 10) >> 0],
boxTL_x + digitPositions[0].x,
boxTL_y + digitPositions[0].y
);
g.drawImage(
numbers[hours % 10 >> 0],
boxTL_x + digitPositions[1].x,
boxTL_y + digitPositions[1].y
);
g.drawImage(
numbers[(minutes / 10) >> 0],
boxTL_x + digitPositions[2].x,
boxTL_y + digitPositions[2].y
);
g.drawImage(
numbers[minutes % 10 >> 0],
boxTL_x + digitPositions[3].x,
boxTL_y + digitPositions[3].y
);
g.drawImage(sprite, sprite_TL_x, sprite_TL_y);
// Bangle.drawWidgets();
if (this.drawing) {
const timeout =
time_advance <= 1 ? animation_duration / animation_steps : -999;
if (timeout > 0) {
setTimeout((_) => {
this.draw(new Date());
}, timeout * 1000);
}
}
},
update: function(date, changed) {
if (this.drawing && changed.m) {
this.draw(date);
}
},
}); });
// Show launcher when middle button pressed clock.start();
Bangle.setUI("clock");
// Load widgets
Bangle.loadWidgets();
draw();

View File

@ -1,18 +1,18 @@
{ {
"id": "bowserWF", "id": "bowserWF",
"name": "Bowser Watchface", "name": "Bowser Watchface",
"shortName":"Bowser Watchface", "shortName": "Bowser Watchface",
"version":"0.02", "version": "0.03",
"description": "Let bowser show you the time", "description": "Let bowser show you the time",
"icon": "app.png", "icon": "app.png",
"type": "clock", "type": "clock",
"tags": "clock", "tags": "clock",
"supports" : ["BANGLEJS2"], "supports": ["BANGLEJS2"],
"allow_emulator": true, "allow_emulator": true,
"readme": "README.md", "readme": "README.md",
"storage": [ "storage": [
{"name":"bowserWF.app.js","url":"app.js"}, { "name": "bowserWF.app.js", "url": "app.js" },
{"name":"bowserWF.img","url":"app-icon.js","evaluate":true} { "name": "bowserWF.img", "url": "app-icon.js", "evaluate": true }
], ],
"data": [{"name":"bowserWF.json"}] "data": [{ "name": "bowserWF.json" }]
} }

View File

@ -22,3 +22,10 @@
Restructure the settings menu Restructure the settings menu
0.08: Allow scanning for devices in settings 0.08: Allow scanning for devices in settings
0.09: Misc Fixes and improvements (https://github.com/espruino/BangleApps/pull/1655) 0.09: Misc Fixes and improvements (https://github.com/espruino/BangleApps/pull/1655)
0.10: Use default Bangle formatter for booleans
0.11: App now shows status info while connecting
Fixes to allow cached BluetoothRemoteGATTCharacteristic to work with 2v14.14 onwards (>1 central)
0.12: Fix HRM fallback handling
Use default boolean formatter in custom menu and directly apply config if useful
Allow recording unmodified internal HR
Better connection retry handling

View File

@ -19,7 +19,14 @@ Just install the app, then install an app that uses the heart rate monitor.
Once installed you will have to go into this app's settings while your heart rate monitor Once installed you will have to go into this app's settings while your heart rate monitor
is available for bluetooth pairing and scan for devices. is available for bluetooth pairing and scan for devices.
**To disable this and return to normal HRM, uninstall the app** **To disable this and return to normal HRM, uninstall the app or change the settings**
### Modes
* Off - Internal HRM is used, no attempt on connecting to BT HRM.
* Default - Replaces internal HRM with BT HRM and falls back to internal HRM if no valid measurements received.
* Both - The BT HRM needs to be started explicitly by an app that wants to use it. BT HRM has its own event and is completely separated from the internal HRM. Apps not supporting the BT HRM will not see the BT HRM measurements.
* Custom - Combine low level settings as you see fit.
## Compatible Heart Rate Monitors ## Compatible Heart Rate Monitors
@ -35,6 +42,10 @@ So far it has been tested on:
* Polar OH1 * Polar OH1
* Wahoo TICKR X 2 * Wahoo TICKR X 2
## Recorder plugin
The recorder plugin can record the BT HRM event (blue) and the original unchanged HRM event (green). This is mainly useful for debugging purposes or comparing the BT with the internal HRM, as the resulting "merged" HRM can be recordet using the default HRM recorder.
## Internals ## Internals
This replaces `Bangle.setHRMPower` with its own implementation. This replaces `Bangle.setHRMPower` with its own implementation.

View File

@ -5,11 +5,11 @@
); );
var log = function(text, param){ var log = function(text, param){
if (global.showStatusInfo)
showStatusInfo(text);
if (settings.debuglog){ if (settings.debuglog){
var logline = new Date().toISOString() + " - " + text; var logline = new Date().toISOString() + " - " + text;
if (param){ if (param) logline += ": " + JSON.stringify(param);
logline += " " + JSON.stringify(param);
}
print(logline); print(logline);
} }
}; };
@ -30,7 +30,7 @@
}; };
var addNotificationHandler = function(characteristic) { var addNotificationHandler = function(characteristic) {
log("Setting notification handler: " + supportedCharacteristics[characteristic.uuid].handler); log("Setting notification handler"/*supportedCharacteristics[characteristic.uuid].handler*/);
characteristic.on('characteristicvaluechanged', (ev) => supportedCharacteristics[characteristic.uuid].handler(ev.target.value)); characteristic.on('characteristicvaluechanged', (ev) => supportedCharacteristics[characteristic.uuid].handler(ev.target.value));
}; };
@ -61,7 +61,8 @@
writeCache(cache); writeCache(cache);
}; };
var characteristicsFromCache = function() { var characteristicsFromCache = function(device) {
var service = { device : device }; // fake a BluetoothRemoteGATTService
log("Read cached characteristics"); log("Read cached characteristics");
var cache = getCache(); var cache = getCache();
if (!cache.characteristics) return []; if (!cache.characteristics) return [];
@ -75,6 +76,7 @@
r.properties = {}; r.properties = {};
r.properties.notify = cached.notify; r.properties.notify = cached.notify;
r.properties.read = cached.read; r.properties.read = cached.read;
r.service = service;
addNotificationHandler(r); addNotificationHandler(r);
log("Restored characteristic: ", r); log("Restored characteristic: ", r);
restored.push(r); restored.push(r);
@ -92,13 +94,24 @@
"0x180f", // Battery "0x180f", // Battery
]; ];
var bpmTimeout;
var supportedCharacteristics = { var supportedCharacteristics = {
"0x2a37": { "0x2a37": {
//Heart rate measurement //Heart rate measurement
active: false,
handler: function (dv){ handler: function (dv){
var flags = dv.getUint8(0); var flags = dv.getUint8(0);
var bpm = (flags & 1) ? (dv.getUint16(1) / 100 /* ? */ ) : dv.getUint8(1); // 8 or 16 bit var bpm = (flags & 1) ? (dv.getUint16(1) / 100 /* ? */ ) : dv.getUint8(1); // 8 or 16 bit
supportedCharacteristics["0x2a37"].active = bpm > 0;
log("BTHRM BPM " + supportedCharacteristics["0x2a37"].active);
if (supportedCharacteristics["0x2a37"].active) stopFallback();
if (bpmTimeout) clearTimeout(bpmTimeout);
bpmTimeout = setTimeout(()=>{
supportedCharacteristics["0x2a37"].active = false;
startFallback();
}, 3000);
var sensorContact; var sensorContact;
@ -141,8 +154,8 @@
src: "bthrm" src: "bthrm"
}; };
log("Emitting HRM: ", repEvent); log("Emitting HRM", repEvent);
Bangle.emit("HRM", repEvent); Bangle.emit("HRM_int", repEvent);
} }
var newEvent = { var newEvent = {
@ -155,7 +168,7 @@
if (battery) newEvent.battery = battery; if (battery) newEvent.battery = battery;
if (sensorContact) newEvent.contact = sensorContact; if (sensorContact) newEvent.contact = sensorContact;
log("Emitting BTHRM: ", newEvent); log("Emitting BTHRM", newEvent);
Bangle.emit("BTHRM", newEvent); Bangle.emit("BTHRM", newEvent);
} }
}, },
@ -200,6 +213,10 @@
}; };
if (settings.enabled){ if (settings.enabled){
Bangle.isBTHRMActive = function (){
return supportedCharacteristics["0x2a37"].active;
};
Bangle.isBTHRMOn = function(){ Bangle.isBTHRMOn = function(){
return (Bangle._PWR && Bangle._PWR.BTHRM && Bangle._PWR.BTHRM.length > 0); return (Bangle._PWR && Bangle._PWR.BTHRM && Bangle._PWR.BTHRM.length > 0);
}; };
@ -210,24 +227,28 @@
} }
if (settings.replace){ if (settings.replace){
var origIsHRMOn = Bangle.isHRMOn; Bangle.origIsHRMOn = Bangle.isHRMOn;
Bangle.isHRMOn = function() { Bangle.isHRMOn = function() {
if (settings.enabled && !settings.replace){ if (settings.enabled && !settings.replace){
return origIsHRMOn(); return Bangle.origIsHRMOn();
} else if (settings.enabled && settings.replace){ } else if (settings.enabled && settings.replace){
return Bangle.isBTHRMOn(); return Bangle.isBTHRMOn();
} }
return origIsHRMOn() || Bangle.isBTHRMOn(); return Bangle.origIsHRMOn() || Bangle.isBTHRMOn();
}; };
} }
var clearRetryTimeout = function() { var clearRetryTimeout = function(resetTime) {
if (currentRetryTimeout){ if (currentRetryTimeout){
log("Clearing timeout " + currentRetryTimeout); log("Clearing timeout " + currentRetryTimeout);
clearTimeout(currentRetryTimeout); clearTimeout(currentRetryTimeout);
currentRetryTimeout = undefined; currentRetryTimeout = undefined;
} }
if (resetTime) {
log("Resetting retry time");
retryTime = initialRetryTime;
}
}; };
var retry = function() { var retry = function() {
@ -236,8 +257,8 @@
if (!currentRetryTimeout){ if (!currentRetryTimeout){
var clampedTime = retryTime < 100 ? 100 : retryTime; var clampedTime = retryTime < 100 ? 100 : retryTime;
log("Set timeout for retry as " + clampedTime); log("Set timeout for retry as " + clampedTime);
clearRetryTimeout(); clearRetryTimeout();
currentRetryTimeout = setTimeout(() => { currentRetryTimeout = setTimeout(() => {
log("Retrying"); log("Retrying");
@ -257,11 +278,11 @@
var buzzing = false; var buzzing = false;
var onDisconnect = function(reason) { var onDisconnect = function(reason) {
log("Disconnect: " + reason); log("Disconnect: " + reason);
log("GATT: ", gatt); log("GATT", gatt);
log("Characteristics: ", characteristics); log("Characteristics", characteristics);
retryTime = initialRetryTime; clearRetryTimeout(reason != "Connection Timeout");
clearRetryTimeout(); supportedCharacteristics["0x2a37"].active = false;
switchInternalHrm(); startFallback();
blockInit = false; blockInit = false;
if (settings.warnDisconnect && !buzzing){ if (settings.warnDisconnect && !buzzing){
buzzing = true; buzzing = true;
@ -273,13 +294,13 @@
}; };
var createCharacteristicPromise = function(newCharacteristic) { var createCharacteristicPromise = function(newCharacteristic) {
log("Create characteristic promise: ", newCharacteristic); log("Create characteristic promise", newCharacteristic);
var result = Promise.resolve(); var result = Promise.resolve();
// For values that can be read, go ahead and read them, even if we might be notified in the future // For values that can be read, go ahead and read them, even if we might be notified in the future
// Allows for getting initial state of infrequently updating characteristics, like battery // Allows for getting initial state of infrequently updating characteristics, like battery
if (newCharacteristic.readValue){ if (newCharacteristic.readValue){
result = result.then(()=>{ result = result.then(()=>{
log("Reading data for " + JSON.stringify(newCharacteristic)); log("Reading data", newCharacteristic);
return newCharacteristic.readValue().then((data)=>{ return newCharacteristic.readValue().then((data)=>{
if (supportedCharacteristics[newCharacteristic.uuid] && supportedCharacteristics[newCharacteristic.uuid].handler) { if (supportedCharacteristics[newCharacteristic.uuid] && supportedCharacteristics[newCharacteristic.uuid].handler) {
supportedCharacteristics[newCharacteristic.uuid].handler(data); supportedCharacteristics[newCharacteristic.uuid].handler(data);
@ -289,8 +310,8 @@
} }
if (newCharacteristic.properties.notify){ if (newCharacteristic.properties.notify){
result = result.then(()=>{ result = result.then(()=>{
log("Starting notifications for: ", newCharacteristic); log("Starting notifications", newCharacteristic);
var startPromise = newCharacteristic.startNotifications().then(()=>log("Notifications started for ", newCharacteristic)); var startPromise = newCharacteristic.startNotifications().then(()=>log("Notifications started", newCharacteristic));
if (settings.gracePeriodNotification > 0){ if (settings.gracePeriodNotification > 0){
log("Add " + settings.gracePeriodNotification + "ms grace period after starting notifications"); log("Add " + settings.gracePeriodNotification + "ms grace period after starting notifications");
startPromise = startPromise.then(()=>{ startPromise = startPromise.then(()=>{
@ -301,7 +322,7 @@
return startPromise; return startPromise;
}); });
} }
return result.then(()=>log("Handled characteristic: ", newCharacteristic)); return result.then(()=>log("Handled characteristic", newCharacteristic));
}; };
var attachCharacteristicPromise = function(promise, characteristic) { var attachCharacteristicPromise = function(promise, characteristic) {
@ -312,11 +333,11 @@
}; };
var createCharacteristicsPromise = function(newCharacteristics) { var createCharacteristicsPromise = function(newCharacteristics) {
log("Create characteristics promise: ", newCharacteristics); log("Create characteristics promis ", newCharacteristics);
var result = Promise.resolve(); var result = Promise.resolve();
for (var c of newCharacteristics){ for (var c of newCharacteristics){
if (!supportedCharacteristics[c.uuid]) continue; if (!supportedCharacteristics[c.uuid]) continue;
log("Supporting characteristic: ", c); log("Supporting characteristic", c);
characteristics.push(c); characteristics.push(c);
if (c.properties.notify){ if (c.properties.notify){
addNotificationHandler(c); addNotificationHandler(c);
@ -328,10 +349,10 @@
}; };
var createServicePromise = function(service) { var createServicePromise = function(service) {
log("Create service promise: ", service); log("Create service promise", service);
var result = Promise.resolve(); var result = Promise.resolve();
result = result.then(()=>{ result = result.then(()=>{
log("Handling service: " + service.uuid); log("Handling service" + service.uuid);
return service.getCharacteristics().then((c)=>createCharacteristicsPromise(c)); return service.getCharacteristics().then((c)=>createCharacteristicsPromise(c));
}); });
return result.then(()=>log("Handled service" + service.uuid)); return result.then(()=>log("Handled service" + service.uuid));
@ -368,7 +389,7 @@
} }
promise = promise.then((d)=>{ promise = promise.then((d)=>{
log("Got device: ", d); log("Got device", d);
d.on('gattserverdisconnected', onDisconnect); d.on('gattserverdisconnected', onDisconnect);
device = d; device = d;
}); });
@ -379,14 +400,14 @@
}); });
} else { } else {
promise = Promise.resolve(); promise = Promise.resolve();
log("Reuse device: ", device); log("Reuse device", device);
} }
promise = promise.then(()=>{ promise = promise.then(()=>{
if (gatt){ if (gatt){
log("Reuse GATT: ", gatt); log("Reuse GATT", gatt);
} else { } else {
log("GATT is new: ", gatt); log("GATT is new", gatt);
characteristics = []; characteristics = [];
var cachedId = getCache().id; var cachedId = getCache().id;
if (device.id !== cachedId){ if (device.id !== cachedId){
@ -404,7 +425,10 @@
promise = promise.then((gatt)=>{ promise = promise.then((gatt)=>{
if (!gatt.connected){ if (!gatt.connected){
var connectPromise = gatt.connect(connectSettings); log("Connecting...");
var connectPromise = gatt.connect(connectSettings).then(function() {
log("Connected.");
});
if (settings.gracePeriodConnect > 0){ if (settings.gracePeriodConnect > 0){
log("Add " + settings.gracePeriodConnect + "ms grace period after connecting"); log("Add " + settings.gracePeriodConnect + "ms grace period after connecting");
connectPromise = connectPromise.then(()=>{ connectPromise = connectPromise.then(()=>{
@ -432,7 +456,7 @@
promise = promise.then(()=>{ promise = promise.then(()=>{
if (!characteristics || characteristics.length === 0){ if (!characteristics || characteristics.length === 0){
characteristics = characteristicsFromCache(); characteristics = characteristicsFromCache(device);
} }
}); });
@ -445,11 +469,11 @@
}); });
characteristicsPromise = characteristicsPromise.then((services)=>{ characteristicsPromise = characteristicsPromise.then((services)=>{
log("Got services:", services); log("Got services", services);
var result = Promise.resolve(); var result = Promise.resolve();
for (var service of services){ for (var service of services){
if (!(supportedServices.includes(service.uuid))) continue; if (!(supportedServices.includes(service.uuid))) continue;
log("Supporting service: ", service.uuid); log("Supporting service", service.uuid);
result = attachServicePromise(result, service); result = attachServicePromise(result, service);
} }
if (settings.gracePeriodService > 0) { if (settings.gracePeriodService > 0) {
@ -473,7 +497,7 @@
return promise.then(()=>{ return promise.then(()=>{
log("Connection established, waiting for notifications"); log("Connection established, waiting for notifications");
characteristicsToCache(characteristics); characteristicsToCache(characteristics);
clearRetryTimeout(); clearRetryTimeout(true);
}).catch((e) => { }).catch((e) => {
characteristics = []; characteristics = [];
log("Error:", e); log("Error:", e);
@ -491,12 +515,14 @@
isOn = Bangle._PWR.BTHRM.length; isOn = Bangle._PWR.BTHRM.length;
// so now we know if we're really on // so now we know if we're really on
if (isOn) { if (isOn) {
switchFallback();
if (!Bangle.isBTHRMConnected()) initBt(); if (!Bangle.isBTHRMConnected()) initBt();
} else { // not on } else { // not on
log("Power off for " + app); log("Power off for " + app);
clearRetryTimeout(true);
if (gatt) { if (gatt) {
if (gatt.connected){ if (gatt.connected){
log("Disconnect with gatt: ", gatt); log("Disconnect with gatt", gatt);
try{ try{
gatt.disconnect().then(()=>{ gatt.disconnect().then(()=>{
log("Successful disconnect"); log("Successful disconnect");
@ -511,7 +537,33 @@
} }
}; };
var origSetHRMPower = Bangle.setHRMPower; if (settings.replace){
Bangle.on("HRM", (e) => {
e.modified = true;
Bangle.emit("HRM_int", e);
});
Bangle.origOn = Bangle.on;
Bangle.on = function(name, callback) {
if (name == "HRM") {
Bangle.origOn("HRM_int", callback);
} else {
Bangle.origOn(name, callback);
}
};
Bangle.origRemoveListener = Bangle.removeListener;
Bangle.removeListener = function(name, callback) {
if (name == "HRM") {
Bangle.origRemoveListener("HRM_int", callback);
} else {
Bangle.origRemoveListener(name, callback);
}
};
}
Bangle.origSetHRMPower = Bangle.setHRMPower;
if (settings.startWithHrm){ if (settings.startWithHrm){
@ -521,40 +573,54 @@
Bangle.setBTHRMPower(isOn, app); Bangle.setBTHRMPower(isOn, app);
} }
if ((settings.enabled && !settings.replace) || !settings.enabled){ if ((settings.enabled && !settings.replace) || !settings.enabled){
origSetHRMPower(isOn, app); Bangle.origSetHRMPower(isOn, app);
} }
}; };
} }
var fallbackInterval; var fallbackActive = false;
var inSwitch = false;
var switchInternalHrm = function() { var stopFallback = function(){
if (settings.allowFallback && !fallbackInterval){ if (fallbackActive){
log("Fallback to HRM enabled"); Bangle.origSetHRMPower(0, "bthrm_fallback");
origSetHRMPower(1, "bthrm_fallback"); fallbackActive = false;
fallbackInterval = setInterval(()=>{ log("Fallback to HRM disabled");
if (Bangle.isBTHRMConnected()){
origSetHRMPower(0, "bthrm_fallback");
clearInterval(fallbackInterval);
fallbackInterval = undefined;
log("Fallback to HRM disabled");
}
}, settings.fallbackTimeout);
} }
}; };
var startFallback = function(){
if (!fallbackActive && settings.allowFallback) {
fallbackActive = true;
Bangle.origSetHRMPower(1, "bthrm_fallback");
log("Fallback to HRM enabled");
}
};
var switchFallback = function() {
log("Check falling back to HRM");
if (!inSwitch){
inSwitch = true;
if (Bangle.isBTHRMActive()){
stopFallback();
} else {
startFallback();
}
}
inSwitch = false;
};
if (settings.replace){ if (settings.replace){
log("Replace HRM event"); log("Replace HRM event");
if (Bangle._PWR && Bangle._PWR.HRM){ if (Bangle._PWR && Bangle._PWR.HRM){
for (var i = 0; i < Bangle._PWR.HRM.length; i++){ for (var i = 0; i < Bangle._PWR.HRM.length; i++){
var app = Bangle._PWR.HRM[i]; var app = Bangle._PWR.HRM[i];
log("Moving app " + app); log("Moving app " + app);
origSetHRMPower(0, app); Bangle.origSetHRMPower(0, app);
Bangle.setBTHRMPower(1, app); Bangle.setBTHRMPower(1, app);
if (Bangle._PWR.HRM===undefined) break; if (Bangle._PWR.HRM===undefined) break;
} }
} }
switchInternalHrm();
} }
E.on("kill", ()=>{ E.on("kill", ()=>{

View File

@ -42,12 +42,20 @@ function draw(y, type, event) {
if (event.energy) str += " kJoule: " + event.energy.toFixed(0); if (event.energy) str += " kJoule: " + event.energy.toFixed(0);
g.setFontVector(12).drawString(str,px,y+60); g.setFontVector(12).drawString(str,px,y+60);
} }
} }
var firstEventBt = true; var firstEventBt = true;
var firstEventInt = true; var firstEventInt = true;
// This can get called for the boot code to show what's happening
function showStatusInfo(txt) {
var R = Bangle.appRect;
g.reset().clearRect(R.x,R.y2-8,R.x2,R.y2).setFont("6x8");
txt = g.wrapString(txt, R.w)[0];
g.setFontAlign(0,1).drawString(txt, (R.x+R.x2)/2, R.y2);
}
function onBtHrm(e) { function onBtHrm(e) {
if (firstEventBt){ if (firstEventBt){
clear(24); clear(24);

View File

@ -2,7 +2,7 @@
"id": "bthrm", "id": "bthrm",
"name": "Bluetooth Heart Rate Monitor", "name": "Bluetooth Heart Rate Monitor",
"shortName": "BT HRM", "shortName": "BT HRM",
"version": "0.09", "version": "0.12",
"description": "Overrides Bangle.js's build in heart rate monitor with an external Bluetooth one.", "description": "Overrides Bangle.js's build in heart rate monitor with an external Bluetooth one.",
"icon": "app.png", "icon": "app.png",
"type": "app", "type": "app",

View File

@ -32,8 +32,45 @@
Bangle.removeListener('BTHRM', onHRM); Bangle.removeListener('BTHRM', onHRM);
if (Bangle.setBTRHMPower) Bangle.setBTHRMPower(0,"recorder"); if (Bangle.setBTRHMPower) Bangle.setBTHRMPower(0,"recorder");
}, },
draw : (x,y) => g.setColor((bpm != "")?"#00f":"#88f").drawImage(atob("DAwBAAAAMMeef+f+f+P8H4DwBgAA"),x,y) draw : (x,y) => g.setColor((Bangle.isBTHRMActive && Bangle.isBTHRMActive())?"#00f":"#88f").drawImage(atob("DAwBAAAAMMeef+f+f+P8H4DwBgAA"),x,y)
}; };
} };
recorders.hrmint = function() {
var active = false;
var bpmTimeout;
var bpm = "", bpmConfidence = "", src="";
function onHRM(h) {
bpmConfidence = h.confidence;
bpm = h.bpm;
srv = h.src;
if (h.bpm > 0){
active = true;
print("active" + h.bpm);
if (bpmTimeout) clearTimeout(bpmTimeout);
bpmTimeout = setTimeout(()=>{
print("inactive");
active = false;
},3000);
}
}
return {
name : "HR int",
fields : ["Heartrate", "Confidence"],
getValues : () => {
var r = [bpm,bpmConfidence,src];
bpm = ""; bpmConfidence = ""; src="";
return r;
},
start : () => {
Bangle.origOn('HRM', onHRM);
if (Bangle.origSetHRMPower) Bangle.origSetHRMPower(1,"recorder");
},
stop : () => {
Bangle.removeListener('HRM', onHRM);
if (Bangle.origSetHRMPower) Bangle.origSetHRMPower(0,"recorder");
},
draw : (x,y) => g.setColor(( Bangle.origIsHRMOn && Bangle.origIsHRMOn() && active)?"#0f0":"#8f8").drawImage(atob("DAwBAAAAMMeef+f+f+P8H4DwBgAA"),x,y)
};
};
}) })

View File

@ -17,6 +17,14 @@
var settings; var settings;
readSettings(); readSettings();
function applyCustomSettings(){
writeSettings("enabled",true);
writeSettings("replace",settings.custom_replace);
writeSettings("startWithHrm",settings.custom_startWithHrm);
writeSettings("allowFallback",settings.custom_allowFallback);
writeSettings("fallbackTimeout",settings.custom_fallbackTimeout);
}
function buildMainMenu(){ function buildMainMenu(){
var mainmenu = { var mainmenu = {
'': { 'title': 'Bluetooth HRM' }, '': { 'title': 'Bluetooth HRM' },
@ -35,7 +43,6 @@
case 1: case 1:
writeSettings("enabled",true); writeSettings("enabled",true);
writeSettings("replace",true); writeSettings("replace",true);
writeSettings("debuglog",false);
writeSettings("startWithHrm",true); writeSettings("startWithHrm",true);
writeSettings("allowFallback",true); writeSettings("allowFallback",true);
writeSettings("fallbackTimeout",10); writeSettings("fallbackTimeout",10);
@ -43,17 +50,11 @@
case 2: case 2:
writeSettings("enabled",true); writeSettings("enabled",true);
writeSettings("replace",false); writeSettings("replace",false);
writeSettings("debuglog",false);
writeSettings("startWithHrm",false); writeSettings("startWithHrm",false);
writeSettings("allowFallback",false); writeSettings("allowFallback",false);
break; break;
case 3: case 3:
writeSettings("enabled",true); applyCustomSettings();
writeSettings("replace",settings.custom_replace);
writeSettings("debuglog",settings.custom_debuglog);
writeSettings("startWithHrm",settings.custom_startWithHrm);
writeSettings("allowFallback",settings.custom_allowFallback);
writeSettings("fallbackTimeout",settings.custom_fallbackTimeout);
break; break;
} }
writeSettings("mode",v); writeSettings("mode",v);
@ -85,14 +86,12 @@
'< Back': function() { E.showMenu(buildMainMenu()); }, '< Back': function() { E.showMenu(buildMainMenu()); },
'Alert on disconnect': { 'Alert on disconnect': {
value: !!settings.warnDisconnect, value: !!settings.warnDisconnect,
format: v => settings.warnDisconnect ? "On" : "Off",
onchange: v => { onchange: v => {
writeSettings("warnDisconnect",v); writeSettings("warnDisconnect",v);
} }
}, },
'Debug log': { 'Debug log': {
value: !!settings.debuglog, value: !!settings.debuglog,
format: v => settings.debuglog ? "On" : "Off",
onchange: v => { onchange: v => {
writeSettings("debuglog",v); writeSettings("debuglog",v);
} }
@ -140,23 +139,23 @@
'< Back': function() { E.showMenu(buildMainMenu()); }, '< Back': function() { E.showMenu(buildMainMenu()); },
'Replace HRM': { 'Replace HRM': {
value: !!settings.custom_replace, value: !!settings.custom_replace,
format: v => settings.custom_replace ? "On" : "Off",
onchange: v => { onchange: v => {
writeSettings("custom_replace",v); writeSettings("custom_replace",v);
if (settings.mode == 3) applyCustomSettings();
} }
}, },
'Start w. HRM': { 'Start w. HRM': {
value: !!settings.custom_startWithHrm, value: !!settings.custom_startWithHrm,
format: v => settings.custom_startWithHrm ? "On" : "Off",
onchange: v => { onchange: v => {
writeSettings("custom_startWithHrm",v); writeSettings("custom_startWithHrm",v);
if (settings.mode == 3) applyCustomSettings();
} }
}, },
'HRM Fallback': { 'HRM Fallback': {
value: !!settings.custom_allowFallback, value: !!settings.custom_allowFallback,
format: v => settings.custom_allowFallback ? "On" : "Off",
onchange: v => { onchange: v => {
writeSettings("custom_allowFallback",v); writeSettings("custom_allowFallback",v);
if (settings.mode == 3) applyCustomSettings();
} }
}, },
'Fallback Timeout': { 'Fallback Timeout': {
@ -167,6 +166,7 @@
format: v=>v+"s", format: v=>v+"s",
onchange: v => { onchange: v => {
writeSettings("custom_fallbackTimout",v*1000); writeSettings("custom_fallbackTimout",v*1000);
if (settings.mode == 3) applyCustomSettings();
} }
}, },
}; };

View File

@ -6,4 +6,10 @@
0.06: Design and usability improvements. 0.06: Design and usability improvements.
0.07: Improved positioning. 0.07: Improved positioning.
0.08: Select the color of widgets correctly. Additional settings to hide colon. 0.08: Select the color of widgets correctly. Additional settings to hide colon.
0.09: Larger font size if colon is hidden to improve readability further. 0.09: Larger font size if colon is hidden to improve readability further.
0.10: HomeAssistant integration if HomeAssistant is installed.
0.11: Performance improvements.
0.12: Implements a 2D menu.
0.13: Clicks < 24px are for widgets, if fullscreen mode is disabled.
0.14: Adds humidity to weather data.
0.15: Added option for a dynamic mode to show widgets only if unlocked.

View File

@ -1,17 +1,45 @@
# BW Clock # BW Clock
A very minimalistic clock to mainly show date and time.
![](screenshot.png) ![](screenshot.png)
## Features ## Features
- Fullscreen on/off The BW clock provides many features and also 3rd party integrations:
- Tab left/right of screen to show steps, temperature etc. - Bangle data such as steps, heart rate, battery or charging state.
- Enable / disable lock icon in the settings. - A timer can be set directly. *Requirement: Scheduler library*
- If the "sched" app is installed tab top / bottom of the screen to set the timer. - Weather temperature as well as the wind speed can be shown. *Requirement: Weather app*
- The design is adapted to the theme of your bangle. - HomeAssistant triggers can be executed directly. *Requirement: HomeAssistant app*
- The colon (e.g. 7:35 = 735) can be hidden now in the settings.
Note: If some apps are not installed (e.gt. weather app), then this menu item is hidden.
## Settings
- Screen: Normal (widgets shown), Dynamic (widgets shown if unlocked) or Full (widgets are hidden).
- Enable/disable lock icon in the settings. Useful if fullscreen mode is on.
- The colon (e.g. 7:35 = 735) can be hidden in the settings for an even larger time font to improve readability further.
- Your bangle uses the sys color settings so you can change the color too.
## Menu structure
2D menu allows you to display lots of different data including data from 3rd party apps and it's also possible to control things e.g. to set a timer or send a HomeAssistant trigger.
Simply click left / right to go through the menu entries such as Bangle, Timer etc.
and click up/down to move into this sub-menu. You can then click in the middle of the screen
to e.g. send a trigger via HomeAssistant once you selected it.
```
+5min
|
Bangle -- Timer[Optional] -- Weather[Optional] -- HomeAssistant [Optional]
| | | |
Bpm -5min Temperature Trigger1
| | |
Steps ... ...
|
Battery
```
## Thanks to ## Thanks to
<a href="https://www.flaticon.com/free-icons/" title="Icons">Icons created by Flaticon</a> <a href="https://www.flaticon.com/free-icons/" title="Icons">Icons created by Flaticon</a>
## Creator ## Creator
- [David Peer](https://github.com/peerdavid) [David Peer](https://github.com/peerdavid)

File diff suppressed because one or more lines are too long

View File

@ -1,11 +1,11 @@
{ {
"id": "bwclk", "id": "bwclk",
"name": "BW Clock", "name": "BW Clock",
"version": "0.09", "version": "0.15",
"description": "BW Clock.", "description": "A very minimalistic clock to mainly show date and time.",
"readme": "README.md", "readme": "README.md",
"icon": "app.png", "icon": "app.png",
"screenshots": [{"url":"screenshot.png"}, {"url":"screenshot_2.png"}, {"url":"screenshot_3.png"}], "screenshots": [{"url":"screenshot.png"}, {"url":"screenshot_2.png"}, {"url":"screenshot_3.png"}, {"url":"screenshot_4.png"}],
"type": "clock", "type": "clock",
"tags": "clock", "tags": "clock",
"supports": ["BANGLEJS2"], "supports": ["BANGLEJS2"],

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
apps/bwclk/screenshot_4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -4,7 +4,7 @@
// initialize with default settings... // initialize with default settings...
const storage = require('Storage') const storage = require('Storage')
let settings = { let settings = {
fullscreen: false, screen: "Normal",
showLock: true, showLock: true,
hideColon: false, hideColon: false,
}; };
@ -17,15 +17,16 @@
storage.write(SETTINGS_FILE, settings) storage.write(SETTINGS_FILE, settings)
} }
var screenOptions = ["Normal", "Dynamic", "Full"];
E.showMenu({ E.showMenu({
'': { 'title': 'BW Clock' }, '': { 'title': 'BW Clock' },
'< Back': back, '< Back': back,
'Fullscreen': { 'Screen': {
value: settings.fullscreen, value: 0 | screenOptions.indexOf(settings.screen),
format: () => (settings.fullscreen ? 'Yes' : 'No'), min: 0, max: 2,
onchange: () => { format: v => screenOptions[v],
settings.fullscreen = !settings.fullscreen; onchange: v => {
settings.screen = screenOptions[v];
save(); save();
}, },
}, },

View File

@ -8,3 +8,4 @@
0.08: Do not register as watch, manually start clock on button 0.08: Do not register as watch, manually start clock on button
read start of week from system settings read start of week from system settings
0.09: Fix scope of let variables 0.09: Fix scope of let variables
0.10: Use default Bangle formatter for booleans

View File

@ -1,7 +1,7 @@
{ {
"id": "calendar", "id": "calendar",
"name": "Calendar", "name": "Calendar",
"version": "0.09", "version": "0.10",
"description": "Simple calendar", "description": "Simple calendar",
"icon": "calendar.png", "icon": "calendar.png",
"screenshots": [{"url":"screenshot_calendar.png"}], "screenshots": [{"url":"screenshot_calendar.png"}],

View File

@ -17,7 +17,6 @@
"< Back": () => back(), "< Back": () => back(),
'B2 Colors': { 'B2 Colors': {
value: settings.ndColors, value: settings.ndColors,
format: v => v ? "Yes" : "No",
onchange: v => { onchange: v => {
settings.ndColors = v; settings.ndColors = v;
writeSettings(); writeSettings();

View File

@ -1,2 +1,3 @@
1.00: New App! 0.01: New App!
1.01: Use fractional numbers and scale the points to keep working consistently on whole screen 0.02: Use fractional numbers and scale the points to keep working consistently on whole screen
0.03: Use default Bangle formatter for booleans

View File

@ -2,7 +2,7 @@
"name": "Touchscreen Calibration", "name": "Touchscreen Calibration",
"shortName":"Calibration", "shortName":"Calibration",
"icon": "calibration.png", "icon": "calibration.png",
"version":"1.01", "version":"0.03",
"description": "A simple calibration app for the touchscreen", "description": "A simple calibration app for the touchscreen",
"supports": ["BANGLEJS","BANGLEJS2"], "supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md", "readme": "README.md",

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