1
0
Fork 0
Vingelar 2021-11-19 21:33:08 +01:00
commit 51501b740d
366 changed files with 9893 additions and 1466 deletions

View File

@ -1,10 +1,10 @@
Bangle.js App Loader (and Apps) Bangle.js App Loader (and Apps)
================================ ================================
[![Build Status](https://travis-ci.org/espruino/BangleApps.svg?branch=master)](https://travis-ci.org/espruino/BangleApps) [![Build Status](https://app.travis-ci.com/espruino/BangleApps.svg?branch=master)](https://app.travis-ci.com/github/espruino/BangleApps)
* 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 [github.io](https://espruino.github.io/BangleApps/) * Try the **development version** at [espruino.github.io](https://espruino.github.io/BangleApps/)
**All software (including apps) in this repository is MIT Licensed - see [LICENSE](LICENSE)** By **All software (including apps) in this repository is MIT Licensed - see [LICENSE](LICENSE)** By
submitting code to this repository you confirm that you are happy with it being MIT licensed, submitting code to this repository you confirm that you are happy with it being MIT licensed,
@ -49,25 +49,25 @@ easily distinguish between file types, we use the following:
## Adding your app to the menu ## Adding your app to the menu
* Come up with a unique (all lowercase, no spaces) name, we'll assume `7chname`. Bangle.js * Come up with a unique (all lowercase, no spaces) name, we'll assume `myappid`. Bangle.js
is limited to 28 char filenames and appends a file extension (eg `.js`) so please is limited to 28 char filenames and appends a file extension (eg `.js`) so please
try and keep filenames short to avoid overflowing the buffer. try and keep filenames short to avoid overflowing the buffer.
* Create a folder called `apps/<id>`, lets assume `apps/7chname` * Create a folder called `apps/<id>`, lets assume `apps/myappid`
* We'd recommend that you copy files from 'Example Applications' (below) as a base, or... * We'd recommend that you copy files from 'Example Applications' (below) as a base, or...
* `apps/7chname/app.png` should be a 48px icon * `apps/myappid/app.png` should be a 48px icon
* Use http://www.espruino.com/Image+Converter to create `apps/7chname/app-icon.js`, using a 1 bit, 4 bit or 8 bit Web Palette "Image String" * Use http://www.espruino.com/Image+Converter to create `apps/myappid/app-icon.js`, using a 1 bit, 4 bit or 8 bit Web Palette "Image String"
* Create an entry in `apps.json` as follows: * Create an entry in `apps.json` as follows:
``` ```
{ "id": "7chname", { "id": "myappid",
"name": "My app's human readable name", "name": "My app's human readable name",
"shortName" : "Short Name", "shortName" : "Short Name",
"icon": "app.png", "icon": "app.png",
"description": "A detailed description of my great app", "description": "A detailed description of my great app",
"tags": "", "tags": "",
"storage": [ "storage": [
{"name":"7chname.app.js","url":"app.js"}, {"name":"myappid.app.js","url":"app.js"},
{"name":"7chname.img","url":"app-icon.js","evaluate":true} {"name":"myappid.img","url":"app-icon.js","evaluate":true}
], ],
}, },
``` ```
@ -95,12 +95,12 @@ Be aware of the delay between commits and updates on github.io - it can take a f
Using the 'Storage' icon in [the Web IDE](https://www.espruino.com/ide/) Using the 'Storage' icon in [the Web IDE](https://www.espruino.com/ide/)
(4 discs), upload your files into the places described in your JSON: (4 discs), upload your files into the places described in your JSON:
* `app-icon.js` -> `7chname.img` * `app-icon.js` -> `myappid.img`
Now load `app.js` up in the editor, and click the down-arrow to the bottom Now load `app.js` up in the editor, and click the down-arrow to the bottom
right of the `Send to Espruino` icon. Click `Storage` and then either choose right of the `Send to Espruino` icon. Click `Storage` and then either choose
`7chname.app.js` (if you'd uploaded your app previously), or `New File` `myappid.app.js` (if you'd uploaded your app previously), or `New File`
and then enter `7chname.app.js` as the name. and then enter `myappid.app.js` as the name.
Now, clicking the `Send to Espruino` icon will load the app directly into Now, clicking the `Send to Espruino` icon will load the app directly into
Espruino **and** will automatically run it. Espruino **and** will automatically run it.
@ -115,10 +115,13 @@ and set it to `Load default application`.
## Example Applications ## Example Applications
To make the process easier we've come up with some example applications that you can use as a base To make the process easier we've come up with some example applications that you can use as a base
when creating your own. Just come up with a unique 7 character name, copy `apps/_example_app` when creating your own. Just come up with a unique name (ideally lowercase, under 20 chars), copy `apps/_example_app`
or `apps/_example_widget` to `apps/7chname`, and add `apps/_example_X/add_to_apps.json` to or `apps/_example_widget` to `apps/myappid`, and add `apps/_example_X/add_to_apps.json` to
`apps.json`. `apps.json`.
**Note:** the max filename length is 28 chars, so we suggest an app ID of under
20 so that when `.app.js`/etc gets added to the end the filename isn't cropped.
**If you're making a widget** please start the name with `wid` to make **If you're making a widget** please start the name with `wid` to make
it easy to find! it easy to find!
@ -192,8 +195,8 @@ and which gives information about the app for the Launcher.
``` ```
{ {
"name":"Short Name", // for Bangle.js menu "name":"Short Name", // for Bangle.js menu
"icon":"*7chname", // for Bangle.js menu "icon":"*myappid", // for Bangle.js menu
"src":"-7chname", // source file "src":"-myappid", // source file
"type":"widget/clock/app/bootloader", // optional, default "app" "type":"widget/clock/app/bootloader", // optional, default "app"
// if this is 'widget' then it's not displayed in the menu // if this is 'widget' then it's not displayed in the menu
// if it's 'clock' then it'll be loaded by default at boot time // if it's 'clock' then it'll be loaded by default at boot time
@ -217,8 +220,10 @@ and which gives information about the app for the Launcher.
{ "id": "appid", // 7 character app id { "id": "appid", // 7 character app id
"name": "Readable name", // readable name "name": "Readable name", // readable name
"shortName": "Short name", // short name for launcher "shortName": "Short name", // short name for launcher
"icon": "icon.png", // icon in apps/ "version": "0v01", // the version of this app
"description": "...", // long description (can contain markdown) "description": "...", // long description (can contain markdown)
"icon": "icon.png", // icon in apps/
"screenshots" : [ { url:"screenshot.png" } ], // optional screenshot for app
"type":"...", // optional(if app) - "type":"...", // optional(if app) -
// 'app' - an application // 'app' - an application
// 'widget' - a widget // 'widget' - a widget
@ -226,7 +231,9 @@ and which gives information about the app for the Launcher.
// 'bootloader' - code that runs at startup only // 'bootloader' - code that runs at startup only
// 'RAM' - code that runs and doesn't upload anything to storage // 'RAM' - code that runs and doesn't upload anything to storage
"tags": "", // comma separated tag list for searching "tags": "", // comma separated tag list for searching
"supports": ["BANGLEJS2"], // List of device IDs supported, either BANGLEJS or BANGLEJS2
"dependencies" : { "notify":"type" } // optional, app 'types' we depend on "dependencies" : { "notify":"type" } // optional, app 'types' we depend on
"dependencies" : { "messages":"app" } // optional, depend on a specific app ID
// for instance this will use notify/notifyfs is they exist, or will pull in 'notify' // for instance this will use notify/notifyfs is they exist, or will pull in 'notify'
"readme": "README.md", // if supplied, a link to a markdown-style text file "readme": "README.md", // if supplied, a link to a markdown-style text file
// that contains more information about this app (usage, etc) // that contains more information about this app (usage, etc)
@ -259,6 +266,9 @@ and which gives information about the app for the Launcher.
// (eg it's evaluated as JS) // (eg it's evaluated as JS)
"noOverwrite":true // if supplied, this file will not be overwritten if it "noOverwrite":true // if supplied, this file will not be overwritten if it
// already exists // already exists
"supports": ["BANGLEJS2"]// if supplied, this file will ONLY be uploaded to the device
// types named in the array. This allows different versions of
// the app to be uploaded for different platforms
}, },
] ]
"data": [ // list of files the app writes to "data": [ // list of files the app writes to
@ -306,10 +316,10 @@ version of what's in `apps.json`:
<script> <script>
document.getElementById("upload").addEventListener("click", function() { document.getElementById("upload").addEventListener("click", function() {
sendCustomizedApp({ sendCustomizedApp({
id : "7chname", id : "myappid",
storage:[ storage:[
{name:"7chname.app.js", url:"app.js", content:app_source_code}, {name:"myappid.app.js", url:"app.js", content:app_source_code},
{name:"7chname.img", content:'require("heatshrink").decompress(atob("mEwg...4"))', evaluate:true}, {name:"myappid.img", content:'require("heatshrink").decompress(atob("mEwg...4"))', evaluate:true},
] ]
}); });
}); });
@ -435,7 +445,7 @@ from the IDE.
### Misc Notes ### Misc Notes
- Need to save state? Use the `E.on('kill',...)` event to save JSON to a file called `7chname.json`, then load it at startup. - Need to save state? Use the `E.on('kill',...)` event to save JSON to a file called `myappid.json`, then load it at startup.
- 'Alarm' apps define a file called `alarm.js` which handles the actual alarm window. - 'Alarm' apps define a file called `alarm.js` which handles the actual alarm window.

View File

@ -2,13 +2,14 @@
{ "id": "7chname", { "id": "7chname",
"name": "My app's human readable name", "name": "My app's human readable name",
"shortName":"Short Name", "shortName":"Short Name",
"icon": "app.png",
"version":"0.01", "version":"0.01",
"description": "A detailed description of my great app", "description": "A detailed description of my great app",
"icon": "app.png",
"tags": "", "tags": "",
"supports" : ["BANGLEJS2"],
"readme": "README.md", "readme": "README.md",
"storage": [ "storage": [
{"name":"7chname.app.js","url":"app.js"}, {"name":"7chname.app.js","url":"app.js"},
{"name":"7chname.img","url":"app-icon.js","evaluate":true} {"name":"7chname.img","url":"app-icon.js","evaluate":true}
] ]
} }

View File

@ -2,11 +2,12 @@
{ "id": "7chname", { "id": "7chname",
"name": "My widget's human readable name", "name": "My widget's human readable name",
"shortName":"Short Name", "shortName":"Short Name",
"icon": "widget.png",
"version":"0.01", "version":"0.01",
"description": "A detailed description of my great widget", "description": "A detailed description of my great widget",
"tags": "widget", "icon": "widget.png",
"type": "widget", "type": "widget",
"tags": "widget",
"supports" : ["BANGLEJS2"],
"readme": "README.md", "readme": "README.md",
"storage": [ "storage": [
{"name":"7chname.wid.js","url":"widget.js"} {"name":"7chname.wid.js","url":"widget.js"}

View File

@ -0,0 +1 @@
2021/11/18 | 1.0: Release for Bangle 2

View File

@ -0,0 +1,14 @@
# A Battery Widget (with percentage)
Show the current battery level and charging status in the top right of the clock, with charge percentage
* Works with Bangle 2
* Simple design, no settings
* Red when the batterly level is below 30%
* Blue when charging
* 40 pixels wide
![](a_battery_widget-pic.jpg)
## Creator
[@alainsaas](https://github.com/alainsaas)

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

View File

@ -0,0 +1,45 @@
(function(){
let COLORS = {
'white': "#fff",
'black': "#000",
'charging': "#08f",
'high': "#000",
'low': "#f00",
};
const levelColor = (l) => {
if (Bangle.isCharging()) return COLORS.charging;
if (l >= 30) return COLORS.high;
return COLORS.low;
};
function draw() {
var s = 29;
var x = this.x, y = this.y;
const l = E.getBattery();
let xl = x+4+l*(s-12)/100;
g.setColor(COLORS.white);
g.fillRect(x+2,y+5,x+s-6,y+18);
g.setColor(levelColor(l));
g.fillRect(x+1,y+3,x+s-5,y+4);
g.fillRect(x+1,y+19,x+s-5,y+20);
g.fillRect(x,y+4,x+1,y+19);
g.fillRect(x+s-5,y+4,x+s-4,y+19);
g.fillRect(x+s-3,y+8,x+s-2,y+16); // tip of the battery
g.fillRect(x+4,y+15,xl,y+16); // charging bar
g.setColor(COLORS.black);
g.setFontAlign(0,0);
g.setFont('6x8');
g.drawString(l, x + 14, y + 10);
}
Bangle.on('charging',function(charging) { draw(); });
setInterval(()=>WIDGETS["a_battery_widget"].draw(), 60000);
WIDGETS["a_battery_widget"]={area:"tr",width:30,draw:draw};
})();

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -6,3 +6,6 @@
0.06: Actual pixels as of 12 Jun 2020 0.06: Actual pixels as of 12 Jun 2020
0.07: Pressing a button now exits immediately (fix #618) 0.07: Pressing a button now exits immediately (fix #618)
0.08: Make about (mostly) work on non-240px screens 0.08: Make about (mostly) work on non-240px screens
0.09: Actual Bangle.js 1 pixels as of 13 Oct 2021
0.10: Added separate Bangle.js 2 file with Bangle.js 2 kickstarter pixels (as of 28 Oct 2021)
0.11: Bangle.js2: New pixels, btn1 to exit

43
apps/about/app-bangle1.js Normal file

File diff suppressed because one or more lines are too long

72
apps/about/app-bangle2.js Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,2 +1,3 @@
0.01: New App! 0.01: New App!
0.02: Use the new multiplatform 'Layout' library 0.02: Use the new multiplatform 'Layout' library
0.03: Exit as first menu option, dont show decimal places for seconds

View File

@ -8,6 +8,9 @@ function getFileName(n) {
function showMenu() { function showMenu() {
var menu = { var menu = {
"" : { title : "Accel Logger" }, "" : { title : "Accel Logger" },
"Exit" : function() {
load();
},
"File No" : { "File No" : {
value : fileNumber, value : fileNumber,
min : 0, min : 0,
@ -21,9 +24,6 @@ function showMenu() {
"View Logs" : function() { "View Logs" : function() {
viewLogs(); viewLogs();
}, },
"Exit" : function() {
load();
},
}; };
E.showMenu(menu); E.showMenu(menu);
} }
@ -34,7 +34,7 @@ function viewLog(n) {
var records = 0, l = "", ll=""; var records = 0, l = "", ll="";
while ((l=f.readLine())!==undefined) {records++;ll=l;} while ((l=f.readLine())!==undefined) {records++;ll=l;}
var length = 0; var length = 0;
if (ll) length = (ll.split(",")[0]|0)/1000; if (ll) length = Math.round( (ll.split(",")[0]|0)/1000 );
var menu = { var menu = {
"" : { title : "Log "+n } "" : { title : "Log "+n }
@ -99,12 +99,12 @@ function startRecord(force) {
{type:"txt", id:"time", font:"6x8:2", label:" - ", pad:5, bgCol:g.theme.bg}, {type:"txt", id:"time", font:"6x8:2", label:" - ", pad:5, bgCol:g.theme.bg},
{type:"txt", font:"6x8:2", label:"RECORDING", bgCol:"#f00", pad:5, fillx:1}, {type:"txt", font:"6x8:2", label:"RECORDING", bgCol:"#f00", pad:5, fillx:1},
] ]
},[ // Buttons... },{btns:[ // Buttons...
{label:"STOP", cb:()=>{ {label:"STOP", cb:()=>{
Bangle.removeListener('accel', accelHandler); Bangle.removeListener('accel', accelHandler);
showMenu(); showMenu();
}} }}
]); ]});
layout.render(); layout.render();
// now start writing // now start writing

4
apps/aclock/README.md Normal file
View File

@ -0,0 +1,4 @@
# Analogue Clock
![](screenshot_analog.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -10,3 +10,6 @@
0.10: Fix auto-snooze option (this stopped new alarms being added) (fix #506) 0.10: Fix auto-snooze option (this stopped new alarms being added) (fix #506)
0.11: Respect Quiet Mode 0.11: Respect Quiet Mode
0.12: Fix widget for bangle 2, now uses theme 0.12: Fix widget for bangle 2, now uses theme
Widgets now shown on Alarm screen
0.13: Alarm widget state now updates when setting/resetting an alarm
0.14: Order of 'back' menu item

View File

@ -18,8 +18,10 @@ function showAlarm(alarm) {
var buzzCount = 10; var buzzCount = 10;
if (alarm.msg) if (alarm.msg)
msg += "\n"+alarm.msg; msg += "\n"+alarm.msg;
Bangle.loadWidgets();
Bangle.drawWidgets();
E.showPrompt(msg,{ E.showPrompt(msg,{
title:"ALARM!", title:alarm.timer ? "TIMER!" : "ALARM!",
buttons : {"Sleep":true,"Ok":false} // default is sleep so it'll come back in 10 mins buttons : {"Sleep":true,"Ok":false} // default is sleep so it'll come back in 10 mins
}).then(function(sleep) { }).then(function(sleep) {
buzzCount = 0; buzzCount = 0;

View File

@ -9,6 +9,7 @@ var alarms = require("Storage").readJSON("alarm.json",1)||[];
last : 0, // last day of the month we alarmed on - so we don't alarm twice in one day! last : 0, // last day of the month we alarmed on - so we don't alarm twice in one day!
rp : true, // repeat rp : true, // repeat
as : false, // auto snooze as : false, // auto snooze
timer : 5, // OPTIONAL - if set, this is a timer and it's the time in minutes
} }
];*/ ];*/
@ -18,6 +19,12 @@ function formatTime(t) {
return hrs+":"+("0"+mins).substr(-2); return hrs+":"+("0"+mins).substr(-2);
} }
function formatMins(t) {
mins = (0|t)%60;
hrs = 0|(t/60);
return hrs+":"+("0"+mins).substr(-2);
}
function getCurrentHr() { function getCurrentHr() {
var time = new Date(); var time = new Date();
return time.getHours()+(time.getMinutes()/60)+(time.getSeconds()/3600); return time.getHours()+(time.getMinutes()/60)+(time.getSeconds()/3600);
@ -25,17 +32,25 @@ function getCurrentHr() {
function showMainMenu() { function showMainMenu() {
const menu = { const menu = {
'': { 'title': 'Alarms' }, '': { 'title': 'Alarm/Timer' },
'New Alarm': ()=>editAlarm(-1) '< Back' : ()=>{load();},
'New Alarm': ()=>editAlarm(-1),
'New Timer': ()=>editTimer(-1)
}; };
alarms.forEach((alarm,idx)=>{ alarms.forEach((alarm,idx)=>{
txt = (alarm.on?"on ":"off ")+formatTime(alarm.hr); if (alarm.timer) {
if (alarm.rp) txt += " (repeat)"; txt = "TIMER "+(alarm.on?"on ":"off ")+formatMins(alarm.timer);
} else {
txt = "ALARM "+(alarm.on?"on ":"off ")+formatTime(alarm.hr);
if (alarm.rp) txt += " (repeat)";
}
menu[txt] = function() { menu[txt] = function() {
editAlarm(idx); if (alarm.timer) editTimer(idx);
else editAlarm(idx);
}; };
}); });
menu['< Back'] = ()=>{load();};
if (WIDGETS["alarm"]) WIDGETS["alarm"].reload();
return E.showMenu(menu); return E.showMenu(menu);
} }
@ -55,7 +70,8 @@ function editAlarm(alarmIndex) {
as = a.as; as = a.as;
} }
const menu = { const menu = {
'': { 'title': 'Alarms' }, '': { 'title': 'Alarm' },
'< Back' : showMainMenu,
'Hours': { 'Hours': {
value: hrs, value: hrs,
onchange: function(v){if (v<0)v=23;if (v>23)v=0;hrs=v;this.value=v;} // no arrow fn -> preserve 'this' onchange: function(v){if (v<0)v=23;if (v>23)v=0;hrs=v;this.value=v;} // no arrow fn -> preserve 'this'
@ -105,7 +121,60 @@ function editAlarm(alarmIndex) {
showMainMenu(); showMainMenu();
}; };
} }
menu['< Back'] = showMainMenu; return E.showMenu(menu);
}
function editTimer(alarmIndex) {
var newAlarm = alarmIndex<0;
var hrs = 0;
var mins = 5;
var en = true;
if (!newAlarm) {
var a = alarms[alarmIndex];
mins = (0|a.timer)%60;
hrs = 0|(a.timer/60);
en = a.on;
}
const menu = {
'': { 'title': 'Timer' },
'Hours': {
value: hrs,
onchange: function(v){if (v<0)v=23;if (v>23)v=0;hrs=v;this.value=v;} // no arrow fn -> preserve 'this'
},
'Minutes': {
value: mins,
onchange: function(v){if (v<0)v=59;if (v>59)v=0;mins=v;this.value=v;} // no arrow fn -> preserve 'this'
},
'Enabled': {
value: en,
format: v=>v?"On":"Off",
onchange: v=>en=v
}
};
function getTimer() {
var d = new Date(Date.now() + ((hrs*60)+mins)*60000);
var hr = d.getHours() + (d.getMinutes()/60) + (d.getSeconds()/3600);
// Save alarm
return {
on : en,
timer : (hrs*60)+mins,
hr : hr,
rp : false, as: false
};
}
menu["> Save"] = function() {
if (newAlarm) alarms.push(getTimer());
else alarms[alarmIndex] = getTimer();
require("Storage").write("alarm.json",JSON.stringify(alarms));
showMainMenu();
};
if (!newAlarm) {
menu["> Delete"] = function() {
alarms.splice(alarmIndex,1);
require("Storage").write("alarm.json",JSON.stringify(alarms));
showMainMenu();
};
}
return E.showMenu(menu); return E.showMenu(menu);
} }

View File

@ -1,11 +1,7 @@
(() => { WIDGETS["alarm"]={area:"tl",width:0,draw:function() {
var alarms = require('Storage').readJSON('alarm.json',1)||[]; 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);
alarms = alarms.filter(alarm=>alarm.on); },reload:function() {
if (!alarms.length) return; // no alarms, no widget! WIDGETS["alarm"].width = (require('Storage').readJSON('alarm.json',1)||[]).some(alarm=>alarm.on) ? 24 : 0;
delete alarms; }
// add the widget };
WIDGETS["alarm"]={area:"tl",width:24,draw:function() { WIDGETS["alarm"].reload();
g.setColor(g.theme.fg);
g.drawImage(atob("GBgBAAAAAAAAABgADhhwDDwwGP8YGf+YMf+MM//MM//MA//AA//AA//AA//AA//AA//AB//gD//wD//wAAAAADwAABgAAAAAAAAA"),this.x,this.y);
}};
})()

1
apps/android/ChangeLog Normal file
View File

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

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

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEw4MA///xF9FstggwFDuEOAoc//gFJv/+AoZHBAgUB8/nwAFCBYIFCgYFB4AFHABdjCIPGAoPzAoPPAvpHFMpYFFPosAnk8NgYFdjEYfMo="))

2
apps/android/app.js Normal file
View File

@ -0,0 +1,2 @@
// Config app not implemented yet
setTimeout(()=>load("messages.app.js"),10);

BIN
apps/android/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 636 B

55
apps/android/boot.js Normal file
View File

@ -0,0 +1,55 @@
(function() {
function gbSend(message) {
Bluetooth.println("");
Bluetooth.println(JSON.stringify(message));
}
var _GB = global.GB;
global.GB = (event) => {
// feed a copy to other handlers if there were any
if (_GB) setTimeout(_GB,0,Object.assign({},event));
/* TODO: Call handling, fitness */
var HANDLERS = {
// {t:"notify",id:int, src,title,subject,body,sender,tel:string} add
"notify" : function() { event.t="add";require("messages").pushMessage(event); },
// {t:"notify~",id:int, title:string} // modified
"notify~" : function() { event.t="modify";require("messages").pushMessage(event); },
// {t:"notify-",id:int} // remove
"notify-" : function() { event.t="remove";require("messages").pushMessage(event); },
// {t:"find", n:bool} // find my phone
"find" : function() {
if (Bangle.findDeviceInterval) {
clearInterval(Bangle.findDeviceInterval);
delete Bangle.findDeviceInterval;
}
if (event.n) // Ignore quiet mode: we always want to find our watch
Bangle.findDeviceInterval = setInterval(_=>Bangle.buzz(),1000);
},
// {t:"musicstate", state:"play/pause",position,shuffle,repeat}
"musicstate" : function() {
require("messages").pushMessage({t:"modify",id:"music",title:"Music",state:event.state});
},
// {t:"musicinfo", artist,album,track,dur,c(track count),n(track num}
"musicinfo" : function() {
require("messages").pushMessage(Object.assign(event, {t:"modify",id:"music",title:"Music"}));
}
};
var h = HANDLERS[event.t];
if (h) h(); else console.log("GB Unknown",event);
};
// Battery monitor
function sendBattery() { gbSend({ t: "status", bat: E.getBattery() }); }
NRF.on("connect", () => setTimeout(sendBattery, 2000));
setInterval(sendBattery, 10*60*1000);
// Health tracking
Bangle.on('health', health=>{
gbSend({ t: "act", stp: health.steps, hrm: health.bpm });
});
// Music control
Bangle.musicControl = cmd => {
// play/pause/next/previous/volumeup/volumedown
gbSend({ t: "music", m:cmd });
}
})();

View File

@ -1 +1,2 @@
0.01: New App! 0.01: New App!
0.02: Load widgets after setUI so widclk knows when to hide

View File

@ -19,7 +19,7 @@ function queueDraw() {
function draw() { function draw() {
var x = g.getWidth()/2; var x = g.getWidth()/2;
var y = g.getHeight()/2; var y = g.getHeight()/2;
g.reset(); g.reset();
var date = new Date(); var date = new Date();
var timeStr = require("locale").time(date,1); var timeStr = require("locale").time(date,1);
var dateStr = require("locale").date(date).toUpperCase(); var dateStr = require("locale").date(date).toUpperCase();
@ -33,7 +33,7 @@ function draw() {
g.clearRect(0,y-8,g.getWidth(),y+8); // clear the background g.clearRect(0,y-8,g.getWidth(),y+8); // clear the background
g.drawString(dateStr,x,y); g.drawString(dateStr,x,y);
// queue draw in one minute // queue draw in one minute
queueDraw(); queueDraw();
} }
// Clear the screen once, at startup // Clear the screen once, at startup
@ -49,9 +49,8 @@ Bangle.on('lcdPower',on=>{
drawTimeout = undefined; drawTimeout = undefined;
} }
}); });
// Show launcher when middle button pressed
Bangle.setUI("clock");
// Load widgets // Load widgets
Bangle.loadWidgets(); Bangle.loadWidgets();
Bangle.drawWidgets(); Bangle.drawWidgets();
// Show launcher when middle button pressed
Bangle.setUI("clock");

View File

@ -1,4 +1,6 @@
0.01: First version 0.01: First version
0.02: Moved arrow image load to global scope 0.02: Moved arrow image load to global scope
0.03: faster drawCompass() function, does not cause buttons to become unresponsive 0.03: faster drawCompass() function, does not cause buttons to become unresponsive
0.04: removed LCD1.write() as it was keeping LCD on 0.04: removed LED1.write() as it was keeping LCD on
0.05: Turn compass off when screen off
Calibrate at start if no info

View File

@ -1,5 +1,5 @@
var pal1color = new Uint16Array([0x0000,0xFFC0],0,1); var pal1color = new Uint16Array([g.theme.bg,0xFFC0],0,1);
var pal2color = new Uint16Array([0x0000,0xffff],0,1); var pal2color = new Uint16Array([g.theme.bg,g.theme.fg],0,1);
var buf1 = Graphics.createArrayBuffer(128,128,1,{msb:true}); var buf1 = Graphics.createArrayBuffer(128,128,1,{msb:true});
var buf2 = Graphics.createArrayBuffer(80,40,1,{msb:true}); var buf2 = Graphics.createArrayBuffer(80,40,1,{msb:true});
var intervalRef; var intervalRef;
@ -7,6 +7,7 @@ var bearing=0; // always point north
var heading = 0; var heading = 0;
var oldHeading = 0; var oldHeading = 0;
var candraw = false; var candraw = false;
var isCalibrating = false;
var CALIBDATA = require("Storage").readJSON("magnav.json",1)||null; var CALIBDATA = require("Storage").readJSON("magnav.json",1)||null;
function flip1(x,y) { function flip1(x,y) {
@ -29,7 +30,7 @@ function drawCompass(hd) {
if (Math.abs(hd - oldHeading) < 2) return 0; if (Math.abs(hd - oldHeading) < 2) return 0;
hd=hd*Math.PI/180; hd=hd*Math.PI/180;
var p = [0, 1.1071, Math.PI/4, 2.8198, 3.4633, 7*Math.PI/4 , 5.1760]; var p = [0, 1.1071, Math.PI/4, 2.8198, 3.4633, 7*Math.PI/4 , 5.1760];
// using polar cordinates, 64,64 is the offset from the 0,0 origin // using polar cordinates, 64,64 is the offset from the 0,0 origin
var poly = [ var poly = [
64+60*Math.sin(hd+p[0]), 64-60*Math.cos(hd+p[0]), 64+60*Math.sin(hd+p[0]), 64-60*Math.cos(hd+p[0]),
@ -40,16 +41,16 @@ function drawCompass(hd) {
64+28.2843*Math.sin(hd+p[5]), 64-28.2843*Math.cos(hd+p[5]), 64+28.2843*Math.sin(hd+p[5]), 64-28.2843*Math.cos(hd+p[5]),
64+44.7214*Math.sin(hd+p[6]), 64-44.7214*Math.cos(hd+p[6]) 64+44.7214*Math.sin(hd+p[6]), 64-44.7214*Math.cos(hd+p[6])
]; ];
buf1.fillPoly(poly); buf1.fillPoly(poly);
flip1(56, 56); flip1(56, 56);
} }
// stops violent compass swings and wobbles, takes 3ms // stops violent compass swings and wobbles, takes 3ms
function newHeading(m,h){ function newHeading(m,h){
var s = Math.abs(m - h); var s = Math.abs(m - h);
var delta = (m>h)?1:-1; var delta = (m>h)?1:-1;
if (s>=180){s=360-s; delta = -delta;} if (s>=180){s=360-s; delta = -delta;}
if (s<2) return h; if (s<2) return h;
var hd = h + delta*(1 + Math.round(s/5)); var hd = h + delta*(1 + Math.round(s/5));
if (hd<0) hd+=360; if (hd<0) hd+=360;
@ -76,7 +77,7 @@ function tiltfixread(O,S){
return psi; return psi;
} }
function reading() { function reading(m) {
var d = tiltfixread(CALIBDATA.offset,CALIBDATA.scale); var d = tiltfixread(CALIBDATA.offset,CALIBDATA.scale);
heading = newHeading(d,heading); heading = newHeading(d,heading);
var dir = bearing - heading; var dir = bearing - heading;
@ -97,18 +98,19 @@ function reading() {
function calibrate(){ function calibrate(){
var max={x:-32000, y:-32000, z:-32000}, var max={x:-32000, y:-32000, z:-32000},
min={x:32000, y:32000, z:32000}; min={x:32000, y:32000, z:32000};
var ref = setInterval(()=>{ function onMag(m) {
var m = Bangle.getCompass();
max.x = m.x>max.x?m.x:max.x; max.x = m.x>max.x?m.x:max.x;
max.y = m.y>max.y?m.y:max.y; max.y = m.y>max.y?m.y:max.y;
max.z = m.z>max.z?m.z:max.z; max.z = m.z>max.z?m.z:max.z;
min.x = m.x<min.x?m.x:min.x; min.x = m.x<min.x?m.x:min.x;
min.y = m.y<min.y?m.y:min.y; min.y = m.y<min.y?m.y:min.y;
min.z = m.z<min.z?m.z:min.z; min.z = m.z<min.z?m.z:min.z;
}, 100); }
Bangle.on('mag', onMag);
Bangle.setCompassPower(1, "app");
return new Promise((resolve) => { return new Promise((resolve) => {
setTimeout(()=>{ setTimeout(()=>{
if(ref) clearInterval(ref); Bangle.removeListener('mag', onMag);
var offset = {x:(max.x+min.x)/2,y:(max.y+min.y)/2,z:(max.z+min.z)/2}; var offset = {x:(max.x+min.x)/2,y:(max.y+min.y)/2,z:(max.z+min.z)/2};
var delta = {x:(max.x-min.x)/2,y:(max.y-min.y)/2,z:(max.z-min.z)/2}; var delta = {x:(max.x-min.x)/2,y:(max.y-min.y)/2,z:(max.z-min.z)/2};
var avg = (delta.x+delta.y+delta.z)/3; var avg = (delta.x+delta.y+delta.z)/3;
@ -132,6 +134,7 @@ function docalibrate(e,first){
flip1(56,56); flip1(56,56);
calibrate().then((r)=>{ calibrate().then((r)=>{
isCalibrating = false;
require("Storage").write("magnav.json",r); require("Storage").write("magnav.json",r);
Bangle.buzz(); Bangle.buzz();
CALIBDATA = r; CALIBDATA = r;
@ -142,27 +145,39 @@ function docalibrate(e,first){
startdraw(); startdraw();
setTimeout(setButtons,1000); setTimeout(setButtons,1000);
} }
} }
if (first===undefined) first=false;
if (first === undefined) first = false;
stopdraw(); stopdraw();
clearWatch(); clearWatch();
if (first) isCalibrating = true;
if (first)
E.showAlert(msg,title).then(action.bind(null,true)); E.showAlert(msg,title).then(action.bind(null,true));
else else
E.showPrompt(msg,{title:title,buttons:{"Start":true,"Cancel":false}}).then(action); E.showPrompt(msg,{title:title,buttons:{"Start":true,"Cancel":false}}).then(action);
} }
function startdraw(){ function startdraw(){
Bangle.setCompassPower(1, "app");
g.clear(); g.clear();
g.setColor(1,1,1); g.setColor(1,1,1);
Bangle.drawWidgets(); Bangle.drawWidgets();
candraw = true; candraw = true;
intervalRef = setInterval(reading,500); if (intervalRef) clearInterval(intervalRef);
intervalRef = setInterval(reading,200);
} }
function stopdraw() { function stopdraw() {
candraw=false; candraw=false;
if(intervalRef) {clearInterval(intervalRef);}
Bangle.setCompassPower(0, "app");
if (intervalRef) {
clearInterval(intervalRef);
intervalRef = undefined;
}
} }
function setButtons(){ function setButtons(){
@ -170,8 +185,9 @@ function setButtons(){
setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"}); setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"});
setWatch(docalibrate, BTN3, {repeat:false,edge:"falling"}); setWatch(docalibrate, BTN3, {repeat:false,edge:"falling"});
} }
Bangle.on('lcdPower',function(on) { Bangle.on('lcdPower',function(on) {
if (isCalibrating) return;
if (on) { if (on) {
startdraw(); startdraw();
} else { } else {
@ -179,9 +195,8 @@ Bangle.on('lcdPower',function(on) {
} }
}); });
Bangle.on('kill',()=>{Bangle.setCompassPower(0);});
Bangle.loadWidgets(); Bangle.loadWidgets();
Bangle.setCompassPower(1);
startdraw();
setButtons(); setButtons();
Bangle.setLCDPower(1);
if (CALIBDATA) startdraw(); else docalibrate({},true);

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -40,7 +40,7 @@ const layout = new Layout({
{height: 40}, {height: 40},
{id: "date", type: "txt", font: "10%", valign: 1}, {id: "date", type: "txt", font: "10%", valign: 1},
], ],
}, false, {lazy: true}); }, {lazy: true});
// adjustments based on screen size and whether we display am/pm // adjustments based on screen size and whether we display am/pm
let thickness; // bar thickness, same as time font "pixel block" size let thickness; // bar thickness, same as time font "pixel block" size
if (is12Hour) { if (is12Hour) {

View File

@ -1,3 +1,6 @@
0.02: Modified for use with new bootloader and firmware 0.02: Modified for use with new bootloader and firmware
0.03: Shrinked size to avoid cut-off edges on the physical device. BTN3: show date. BTN1: show time in decimal. 0.03: Shrinked size to avoid cut-off edges on the physical device. BTN3: show date. BTN1: show time in decimal.
0.04: Update to use Bangle.setUI instead of setWatch 0.04: Update to use Bangle.setUI instead of setWatch
0.05: Update *on* the minute rather than every 15 secs
Now show widgets
Make compatible with themes, and Bangle.js 2

View File

@ -1,7 +1,7 @@
// Berlin Clock see https://en.wikipedia.org/wiki/Mengenlehreuhr // Berlin Clock see https://en.wikipedia.org/wiki/Mengenlehreuhr
// https://github.com/eska-muc/BangleApps // https://github.com/eska-muc/BangleApps
const fields = [4, 4, 11, 4]; const fields = [4, 4, 11, 4];
const offset = 20; const offset = 24;
const width = g.getWidth() - 2 * offset; const width = g.getWidth() - 2 * offset;
const height = g.getHeight() - 2 * offset; const height = g.getHeight() - 2 * offset;
const rowHeight = height / 4; const rowHeight = height / 4;
@ -10,11 +10,23 @@ var show_date = false;
var show_time = false; var show_time = false;
var yy = 0; var yy = 0;
rowlights = []; var rowlights = [];
time_digit = []; var time_digit = [];
function drawBerlinClock() { // timeout used to update every minute
g.clear(); var drawTimeout;
// schedule a draw for the next minute
function queueDraw() {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = setTimeout(function() {
drawTimeout = undefined;
draw();
}, 60000 - (Date.now() % 60000));
}
function draw() {
g.reset().clearRect(0,24,g.getWidth(),g.getHeight());
var now = new Date(); var now = new Date();
// show date below the clock // show date below the clock
@ -24,8 +36,7 @@ function drawBerlinClock() {
var day = now.getDate(); var day = now.getDate();
var dateString = `${yr}-${month < 10 ? '0' : ''}${month}-${day < 10 ? '0' : ''}${day}`; var dateString = `${yr}-${month < 10 ? '0' : ''}${month}-${day < 10 ? '0' : ''}${day}`;
var strWidth = g.stringWidth(dateString); var strWidth = g.stringWidth(dateString);
g.setColor(1, 1, 1); g.setColor(g.theme.fg).setFontAlign(-1,-1);
g.setFontAlign(-1,-1);
g.drawString(dateString, ( g.getWidth() - strWidth ) / 2, height + offset + 4); g.drawString(dateString, ( g.getWidth() - strWidth ) / 2, height + offset + 4);
} }
@ -50,8 +61,7 @@ function drawBerlinClock() {
x2 = (col + 1) * boxWidth + offset; x2 = (col + 1) * boxWidth + offset;
y2 = (row + 1) * rowHeight + offset; y2 = (row + 1) * rowHeight + offset;
g.setColor(1, 1, 1); g.setColor(g.theme.fg).drawRect(x1, y1, x2, y2);
g.drawRect(x1, y1, x2, y2);
if (col < rowlights[row]) { if (col < rowlights[row]) {
if (row === 2) { if (row === 2) {
if (((col + 1) % 3) === 0) { if (((col + 1) % 3) === 0) {
@ -65,46 +75,42 @@ function drawBerlinClock() {
g.fillRect(x1 + 2, y1 + 2, x2 - 2, y2 - 2); g.fillRect(x1 + 2, y1 + 2, x2 - 2, y2 - 2);
} }
if (row == 3 && show_time) { if (row == 3 && show_time) {
g.setColor(1,1,1); g.setColor(g.theme.fg).setFontAlign(0,0);
g.setFontAlign(0,0);
g.drawString(time_digit[col],(x1+x2)/2,(y1+y2)/2); g.drawString(time_digit[col],(x1+x2)/2,(y1+y2)/2);
} }
} }
} }
queueDraw();
} }
function toggleDate() { function toggleDate() {
show_date = ! show_date; show_date = ! show_date;
drawBerlinClock(); draw();
} }
function toggleTime() { function toggleTime() {
show_time = ! show_time; show_time = ! show_time;
drawBerlinClock(); draw();
} }
// special function to handle display switch on // Stop updates when LCD is off, restart when on
Bangle.on('lcdPower', (on) => { Bangle.on('lcdPower',on=>{
g.clear();
if (on) { if (on) {
Bangle.drawWidgets(); draw(); // draw immediately, queue redraw
// call your app function here } else { // stop draw timer
drawBerlinClock(); if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
} }
}); });
// refesh every 15 sec // Show launcher when button pressed, handle up/down
setInterval(drawBerlinClock, 15E3); Bangle.setUI("clockupdown", dir=> {
if (dir<0) toggleTime();
if (dir>0) toggleDate();
});
g.clear(); g.clear();
Bangle.loadWidgets(); Bangle.loadWidgets();
Bangle.drawWidgets(); Bangle.drawWidgets();
drawBerlinClock(); draw();
if (BTN3) {
// Toggle date display, when BTN3 is pressed
setWatch(toggleTime,BTN1, { repeat : true, edge: "falling"});
// Toggle date display, when BTN3 is pressed
setWatch(toggleDate,BTN3, { repeat : true, edge: "falling"});
}
// Show launcher when button pressed
Bangle.setUI("clock");

View File

@ -2,43 +2,9 @@
Binary watch to train Your brain Binary watch to train Your brain
Inspired by the 80's LCD wrist watch from RALtec Inspired by the 80's LCD wrist watch from RALtec
![](screenshot.png)
## Usage ## Usage
- swipe to left or right to change displayed text (date, time, ...) - swipe to left or right to change displayed text (date, time, ...)
- currently only available for BangeJs2 - currently only available for BangeJs2
- Widgets will not be shown - Widgets will not be shown
- If bluetooth connection is not established an icon will show up - If bluetooth connection is not established an icon will show up
## How it works
Binary means that every digit can represent 2 states: 0 or 1, displayed by a black bar.
The principle is the same like in out well known and daily used decimal system with values from 0 to 9:
We start from the most right position with the least significant bit (binary digit) which can have the value 0 or 1
The 2nd bit from the right can have the value 0 or 2 (sum of all bits to the right set to 1 plus 1).
This principle is valid for all the remaining bits.
Mathematically spoken: the value of a digit is the base number of the system (10 for decimal or 2 for binary)
to the power of the position (from the right, starting with 0).
That means in numbers: 2^5 = 32, 2^4 = 16, 2^3 = 8, 2^2 = 4, 2^1 = 2, 2^0 = 1
The upper row represents the hours with 4 bit (2^4 = 16 possible values in total, 12 are used: 1 to 12),
the 2nd row represents the minutes with 6 bit (2^6 = 64 possible values in total, 60 are used: 0 to 59).
Same holds for the thrid row: 0-59 seconds
To read the values of a row we summ up the vaules of set bits (black bars).
E.g. the picture above, 3rd row (seconds):
101001
is 1 * 32 + 0 * 16 + 1 * 8 + 0 * 4 + 0 * 2 + 1 * 1
is (only the '1' bit): 32 + 8 + 1 = 41
for the minutes we do the same: 32 + 1 = 33
and the hours: 8 + 2 = 10
So the time is 10:33:41 (that's all)
## TRAIN YOUR BRAIN

View File

@ -1 +1 @@
require("heatshrink").decompress(atob("mEwwcCgEBkmSpICKCwQRRhMn/4AK+VACIU4A4PAz+27dt20ECI1IgEDCIOT+wRB2EkCIX+BwMCpE/8f+gmSvwRB2Mkz///v/5IRBpwRHwIRC5PzCIMSCIXwMQNP7dshMkyf/p+G/MgiV+CIPxCJFM8gRByf+CIIvBRIP7sCMCv/h8//C4P+g6ABCIdiCIVP/M///kFIPAj6iLCIYAOCPH4ibUC2zABdgW/8ARFUgILB2/8fwf/kB3BPobUD3/kz4pCTwMDCIrCBCIWTCINv/IREfAVJDoYpCv/JkmAv4RCYQYRM+ARCn4vCHYX+bQOQh4RBfAYRJyUBCI3/F4IFB/4RGdP4RHwDmC7/gmzaC//tbQWBR4UbfAWQgzIDfwVsR4QRCfAIRM/0DCIWSgDaDz4RBsDXDCIIdByVAfAb+CCIf/4AREjYRFgZ9D/D4DpEDfAT+Cj4REhoRJ7ARE/8PfAVJgbmDp/YWZHgv6zIkkSBYWB44sB/4CB/AREkESp4EBx4RBx0/CIPACAf5kECCIQAHPQIAB5MAgVJEYs4AwIjECIMACI0ACIv+pARCn5rDvwFDGoQRDhILDABHyoARBgKeCARQQBCKIA==")) require("heatshrink").decompress(atob("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"))

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

4
apps/boldclk/README.md Normal file
View File

@ -0,0 +1,4 @@
# Bold Clock
![](screenshot_bold.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -30,3 +30,13 @@
0.29: Update boot0 to avoid code block (faster execution) 0.29: Update boot0 to avoid code block (faster execution)
Fix issues where 'Uncaught Error: Function not found' could happen with multiple .boot.js Fix issues where 'Uncaught Error: Function not found' could happen with multiple .boot.js
0.30: Remove 'Get GPS time' at boot. Latest firmwares keep time through reboots, so this is not needed now 0.30: Remove 'Get GPS time' at boot. Latest firmwares keep time through reboots, so this is not needed now
0.31: Add polyfills for g.wrapString, g.imageMetrics, g.stringMetrics
0.32: Fix single quote error in g.wrapString polyfill
improve g.stringMetrics polyfill
Fix issue where re-running bootupdate could disable existing polyfills
0.33: Add E.showScroller polyfill
0.34: Use Storage.hash if available
Rearrange NRF.setServices to allow .boot.js files to add services (eg ANCS)
0.35: Add Bangle.appRect polyfill
Don't set beep vibration up on Bangle.js 2 (built in)
0.36: Add comments to .boot0 to make debugging a bit easier

View File

@ -3,17 +3,24 @@ recalculates, but this avoids us doing a whole bunch of reconfiguration most
of the time. */ of the time. */
E.showMessage("Updating boot0..."); E.showMessage("Updating boot0...");
var s = require('Storage').readJSON('setting.json',1)||{}; var s = require('Storage').readJSON('setting.json',1)||{};
var isB2 = process.env.HWVERSION; // Is Bangle.js 2 var BANGLEJS2 = process.env.HWVERSION==2; // Is Bangle.js 2
var boot = ""; var boot = "";
var CRC = E.CRC32(require('Storage').read('setting.json'))+E.CRC32(require('Storage').list(/\.boot\.js/)); if (require('Storage').hash) { // new in 2v11 - helps ensure files haven't changed
boot += `if (E.CRC32(require('Storage').read('setting.json'))+E.CRC32(require('Storage').list(/\.boot\.js/))!=${CRC}) { eval(require('Storage').read('bootupdate.js')); throw "Storage Updated!"}\n`; var CRC = E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\.boot\.js/);
boot += `if (E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\\.boot\\.js/)!=${CRC})`;
} else {
var CRC = E.CRC32(require('Storage').read('setting.json'))+E.CRC32(require('Storage').list(/\.boot\.js/));
boot += `if (E.CRC32(require('Storage').read('setting.json'))+E.CRC32(require('Storage').list(/\\.boot\\.js/))!=${CRC})`;
}
boot += ` { eval(require('Storage').read('bootupdate.js')); throw "Storage Updated!"}\n`;
boot += `E.setFlags({pretokenise:1});\n`; boot += `E.setFlags({pretokenise:1});\n`;
boot += `var bleServices = {}, bleServiceOptions = { uart : true};\n`;
if (s.ble!==false) { if (s.ble!==false) {
if (s.HID) { // Human interface device if (s.HID) { // Human interface device
if (s.HID=="joy") boot += `Bangle.HID = E.toUint8Array(atob("BQEJBKEBCQGhAAUJGQEpBRUAJQGVBXUBgQKVA3UBgQMFAQkwCTEVgSV/dQiVAoECwMA="));`; if (s.HID=="joy") boot += `Bangle.HID = E.toUint8Array(atob("BQEJBKEBCQGhAAUJGQEpBRUAJQGVBXUBgQKVA3UBgQMFAQkwCTEVgSV/dQiVAoECwMA="));`;
else if (s.HID=="kb") boot += `Bangle.HID = E.toUint8Array(atob("BQEJBqEBBQcZ4CnnFQAlAXUBlQiBApUBdQiBAZUFdQEFCBkBKQWRApUBdQORAZUGdQgVACVzBQcZAClzgQAJBRUAJv8AdQiVArECwA=="));` else if (s.HID=="kb") boot += `Bangle.HID = E.toUint8Array(atob("BQEJBqEBBQcZ4CnnFQAlAXUBlQiBApUBdQiBAZUFdQEFCBkBKQWRApUBdQORAZUGdQgVACVzBQcZAClzgQAJBRUAJv8AdQiVArECwA=="));`
else /*kbmedia*/boot += `Bangle.HID = E.toUint8Array(atob("BQEJBqEBhQIFBxngKecVACUBdQGVCIEClQF1CIEBlQV1AQUIGQEpBZEClQF1A5EBlQZ1CBUAJXMFBxkAKXOBAAkFFQAm/wB1CJUCsQLABQwJAaEBhQEVACUBdQGVAQm1gQIJtoECCbeBAgm4gQIJzYECCeKBAgnpgQIJ6oECwA=="));`; else /*kbmedia*/boot += `Bangle.HID = E.toUint8Array(atob("BQEJBqEBhQIFBxngKecVACUBdQGVCIEClQF1CIEBlQV1AQUIGQEpBZEClQF1A5EBlQZ1CBUAJXMFBxkAKXOBAAkFFQAm/wB1CJUCsQLABQwJAaEBhQEVACUBdQGVAQm1gQIJtoECCbeBAgm4gQIJzYECCeKBAgnpgQIJ6oECwA=="));`;
boot += `NRF.setServices({}, {uart:true, hid:Bangle.HID});\n`; boot += `bleServiceOptions.hid=Bangle.HID;\n`;
} }
} }
if (s.blerepl===false) { // If not programmable, force terminal off Bluetooth if (s.blerepl===false) { // If not programmable, force terminal off Bluetooth
@ -48,7 +55,7 @@ boot += `E.setTimeZone(${s.timezone});`;
if (!Bangle.F_BEEPSET) { if (!Bangle.F_BEEPSET) {
if (!s.vibrate) boot += `Bangle.buzz=Promise.resolve;\n` if (!s.vibrate) boot += `Bangle.buzz=Promise.resolve;\n`
if (s.beep===false) boot += `Bangle.beep=Promise.resolve;\n` if (s.beep===false) boot += `Bangle.beep=Promise.resolve;\n`
else if (s.beep=="vib") boot += `Bangle.beep = function (time, freq) { else if (s.beep=="vib" && !BANGLEJS2) boot += `Bangle.beep = function (time, freq) {
return new Promise(function(resolve) { return new Promise(function(resolve) {
if ((0|freq)<=0) freq=4000; if ((0|freq)<=0) freq=4000;
if ((0|time)<=0) time=200; if ((0|time)<=0) time=200;
@ -81,9 +88,11 @@ if (s.quiet && s.qmTimeout) boot+=`Bangle.setLCDTimeout(${s.qmTimeout});\n`;
if (s.passkey!==undefined && s.passkey.length==6) boot+=`NRF.setSecurity({passkey:${s.passkey}, mitm:1, display:1});\n`; if (s.passkey!==undefined && s.passkey.length==6) boot+=`NRF.setSecurity({passkey:${s.passkey}, mitm:1, display:1});\n`;
if (s.whitelist) boot+=`NRF.on('connect', function(addr) { if (!(require('Storage').readJSON('setting.json',1)||{}).whitelist.includes(addr)) NRF.disconnect(); });\n`; if (s.whitelist) boot+=`NRF.on('connect', function(addr) { if (!(require('Storage').readJSON('setting.json',1)||{}).whitelist.includes(addr)) NRF.disconnect(); });\n`;
// Pre-2v10 firmwares without a theme/setUI // Pre-2v10 firmwares without a theme/setUI
delete g.theme; // deleting stops us getting confused by our own decl. builtins can't be deleted
if (!g.theme) { if (!g.theme) {
boot += `g.theme={fg:-1,bg:0,fg2:-1,bg2:7,fgH:-1,bgH:0x02F7,dark:true};\n`; boot += `g.theme={fg:-1,bg:0,fg2:-1,bg2:7,fgH:-1,bgH:0x02F7,dark:true};\n`;
} }
delete Bangle.setUI; // deleting stops us getting confused by our own decl. builtins can't be deleted
if (!Bangle.setUI) { // assume this is just for F18 - Q3 should already have it if (!Bangle.setUI) { // assume this is just for F18 - Q3 should already have it
boot += `Bangle.setUI=function(mode, cb) { boot += `Bangle.setUI=function(mode, cb) {
if (Bangle.btnWatches) { if (Bangle.btnWatches) {
@ -94,7 +103,7 @@ if (Bangle.swipeHandler) {
Bangle.removeListener("swipe", Bangle.swipeHandler); Bangle.removeListener("swipe", Bangle.swipeHandler);
delete Bangle.swipeHandler; delete Bangle.swipeHandler;
} }
if (Bangle.touchandler) { if (Bangle.touchHandler) {
Bangle.removeListener("touch", Bangle.touchHandler); Bangle.removeListener("touch", Bangle.touchHandler);
delete Bangle.touchHandler; delete Bangle.touchHandler;
} }
@ -131,13 +140,66 @@ else if (mode=="updown") {
throw new Error("Unknown UI mode"); throw new Error("Unknown UI mode");
};\n`; };\n`;
} }
delete E.showScroller; // deleting stops us getting confused by our own decl. builtins can't be deleted
if (!E.showScroller) { // added in 2v11 - this is a limited functionality polyfill
boot += `E.showScroller = (function(a){function n(){g.reset();b>=l+c&&(c=1+b-l);b<c&&(c=b);g.setColor(g.theme.fg);for(var d=0;d<l;d++){var m=d+c;if(0>m||m>=a.c)break;var f=24+d*a.h;a.draw(m,{x:0,y:f,w:h,h:a.h});d+c==b&&g.setColor(g.theme.fg).drawRect(0,f,h-1,f+a.h-1).drawRect(1,f+1,h-2,f+a.h-2)}g.setColor(c?g.theme.fg:g.theme.bg);g.fillPoly([e,6,e-14,20,e+14,20]);g.setColor(a.c>l+c?g.theme.fg:g.theme.bg);g.fillPoly([e,k-7,e-14,k-21,e+14,k-21])}if(!a)return Bangle.setUI();var b=0,c=0,h=g.getWidth(),
k=g.getHeight(),e=h/2,l=Math.floor((k-48)/a.h);g.reset().clearRect(0,24,h-1,k-1);n();Bangle.setUI("updown",d=>{d?(b+=d,0>b&&(b=a.c-1),b>=a.c&&(b=0),n()):a.select(b)})});\n`;
}
delete g.imageMetrics; // deleting stops us getting confused by our own decl. builtins can't be deleted
if (!g.imageMetrics) { // added in 2v11 - this is a limited functionality polyfill
boot += `Graphics.prototype.imageMetrics=function(src) {
if (src[0]) return {width:src[0],height:src[1]};
else if ('object'==typeof src) return {
width:("width" in src) ? src.width : src.getWidth(),
height:("height" in src) ? src.height : src.getHeight()};
var im = E.toString(src);
return {width:im.charCodeAt(0), height:im.charCodeAt(1)};
};\n`;
}
delete g.stringMetrics; // deleting stops us getting confused by our own decl. builtins can't be deleted
if (!g.stringMetrics) { // added in 2v11 - this is a limited functionality polyfill
boot += `Graphics.prototype.stringMetrics=function(txt) {
txt = txt.toString().split("\\n");
return {width:Math.max.apply(null,txt.map(x=>g.stringWidth(x))), height:this.getFontHeight()*txt.length};
};\n`;
}
delete g.wrapString; // deleting stops us getting confused by our own decl. builtins can't be deleted
if (!g.wrapString) { // added in 2v11 - this is a limited functionality polyfill
boot += `Graphics.prototype.wrapString=function(str, maxWidth) {
var lines = [];
for (var unwrappedLine of str.split("\\n")) {
var words = unwrappedLine.split(" ");
var line = words.shift();
for (var word of words) {
if (g.stringWidth(line + " " + word) > maxWidth) {
lines.push(line);
line = word;
} else {
line += " " + word;
}
}
lines.push(line);
}
return lines;
};\n`;
}
delete Bangle.appRect; // deleting stops us getting confused by our own decl. builtins can't be deleted
if (!Bangle.appRect) { // added in 2v11 - polyfill for older firmwares
boot += `Bangle.appRect = ((y,w,h)=>({x:0,y:0,w:w,h:h,x2:w-1,y2:h-1}))(g.getWidth(),g.getHeight());
(lw=>{ Bangle.loadWidgets = () => { lw(); Bangle.appRect = ((y,w,h)=>({x:0,y:y,w:w,h:h-y,x2:w-1,y2:h-(1+h)}))(global.WIDGETS?24:0,g.getWidth(),g.getHeight()); }; })(Bangle.loadWidgets);\n`;
}
// Append *.boot.js files // Append *.boot.js files
// These could change bleServices/bleServiceOptions if needed
require('Storage').list(/\.boot\.js/).forEach(bootFile=>{ require('Storage').list(/\.boot\.js/).forEach(bootFile=>{
// we add a semicolon so if the file is wrapped in (function(){ ... }() // we add a semicolon so if the file is wrapped in (function(){ ... }()
// with no semicolon we don't end up with (function(){ ... }()(function(){ ... }() // with no semicolon we don't end up with (function(){ ... }()(function(){ ... }()
// which would cause an error! // which would cause an error!
boot += require('Storage').read(bootFile)+";\n"; boot += "//"+bootFile+"\n"+require('Storage').read(bootFile)+";\n";
}); });
// update ble
boot += `NRF.setServices(bleServices, bleServiceOptions);delete bleServices,bleServiceOptions;\n`;
// write file
require('Storage').write('.boot0',boot); require('Storage').write('.boot0',boot);
delete boot; delete boot;
E.showMessage("Reloading..."); E.showMessage("Reloading...");

1
apps/bthrm/ChangeLog Normal file
View File

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

45
apps/bthrm/README.md Normal file
View File

@ -0,0 +1,45 @@
# Bluetooth Heart Rate Monitor
When this app is installed it overrides Bangle.js's build in heart rate monitor with an external Bluetooth one.
HRM is requested it searches on Bluetooth for a heart rate monitor, connects, and sends data back using the `Bangle.on('HRM'` event as if it came from the on board monitor.
This means it's compatible with many Bangle.js apps including:
* [Heart Rate Widget](https://banglejs.com/apps/#widhrt)
* [Heart Rate Recorder](https://banglejs.com/apps/#heart)
It it NOT COMPATIBLE with [Heart Rate Monitor](https://banglejs.com/apps/#hrm)
as that requires live sensor data (rather than just BPM readings).
## Usage
Just install the app, then install an app that uses the heart rate monitor.
Once installed it'll automatically try and connect to the first bluetooth
heart rate monitor it finds.
**To disable this and return to normal HRM, uninstall the app**
## Compatible Heart Rate Monitors
This works with any heart rate monitor providing the standard Bluetooth
Heart Rate Service (`180D`) and characteristic (`2A37`).
So far it has been tested on:
* CooSpo Bluetooth Heart Rate Monitor
## Internals
This replaces `Bangle.setHRMPower` with its own implementation.
## TODO
* Maybe a `bthrm.settings.js` and app (that calls it) to enable it to be turned on and off
* A widget to show connection state?
* Specify a specific device by address?
## Creator
Gordon Williams

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

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEw4UA///g3yy06AoIZNitUAg8AgtVqtQAgoRCAwITBAggABAoIABAgsAgIGDoIEDoApDAAwwBFIV1BYo1E+oLTAgQLGJon9BZNXBatdBYRVFBYN/r9fHoxTBBYYlEL4QLFq/a1WUgE///fr4xBv/+1Wq1EAh/3/tX6/fv/6BYOqwCzBBYf9tWq9QLF79X+oLBDIOgKgILEEIIxBGAMVNAP/BYf/BYUFBYJSB6wLC9QLBeAQLBqwLCGAL9BBYmr9X+GAILBbIIlBBYP6/wwBBYMFBYZGB/4XDGAILD34vEcwYLB15HBBYYkBBYWrFwILDKoRTCVIQLCEgQXIEgVaF44YCoRHHAAMUgQuBNgILFgECO4W/BZCPFBYinGBY6/CAArXFBY7vDAAsq1QuB0ALIOwOABY0KEgJGGGAguHDAYDBA=="))

BIN
apps/bthrm/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

79
apps/bthrm/boot.js Normal file
View File

@ -0,0 +1,79 @@
(function() {
var log = function() {};//print
var gatt;
var status;
Bangle.isHRMOn = function() {
return (status=="searching" || status=="connecting") || (gatt!==undefined);
}
Bangle.setHRMPower = function(isOn, app) {
// Do app power handling
if (!app) app="?";
log("setHRMPower ->", isOn, app);
if (Bangle._PWR===undefined) Bangle._PWR={};
if (Bangle._PWR.HRM===undefined) Bangle._PWR.HRM=[];
if (isOn && !Bangle._PWR.HRM.includes(app)) Bangle._PWR.HRM.push(app);
if (!isOn && Bangle._PWR.HRM.includes(app)) Bangle._PWR.HRM = Bangle._PWR.HRM.filter(a=>a!=app);
isOn = Bangle._PWR.HRM.length;
// so now we know if we're really on
if (isOn) {
log("setHRMPower on", app);
if (!Bangle.isHRMOn()) {
log("HRM not already on");
status = "searching";
NRF.requestDevice({ filters: [{ services: ['180D'] }] }).then(function(device) {
log("Found device "+device.id);
status = "connecting";
device.on('gattserverdisconnected', function(reason) {
gatt = undefined;
});
return device.gatt.connect();
}).then(function(g) {
log("Connected");
gatt = g;
return gatt.getPrimaryService(0x180D);
}).then(function(service) {
return service.getCharacteristic(0x2A37);
}).then(function(characteristic) {
log("Got characteristic");
characteristic.on('characteristicvaluechanged', function(event) {
var dv = event.target.value;
var flags = dv.getUint8(0);
// 0 = 8 or 16 bit
// 1,2 = sensor contact
// 3 = energy expended shown
// 4 = RR interval
var bpm = (flags&1) ? (dv.getUint16(1)/100/* ? */) : dv.getUint8(1); // 8 or 16 bit
/* var idx = 2 + (flags&1); // index of next field
if (flags&8) idx += 2; // energy expended
if (flags&16) {
var interval = dv.getUint16(idx,1); // in milliseconds
}*/
Bangle.emit('HRM',{
bpm:bpm,
confidence:100
});
});
return characteristic.startNotifications();
}).then(function() {
log("Ready");
status = "ok";
}).catch(function(err) {
log("Error",err);
gatt = undefined;
status = "error";
});
}
} else { // not on
log("setHRMPower off", app);
if (gatt) {
log("HRM connected - disconnecting");
status = undefined;
try {gatt.disconnect();}catch(e) {
log("HRM disconnect error", e);
}
gatt = undefined;
}
}
};
})();

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -5,11 +5,15 @@ The advantage is, that you can still see your normal watchface and other widgets
The widget is always active, but only shown when the timer is on. The widget is always active, but only shown when the timer is on.
Hours, minutes, seconds and timer status can be set with an app. Hours, minutes, seconds and timer status can be set with an app.
Depending on when you start the timer, it may alert up to 0,999 seconds early. This is because it checks only for full seconds. When there is less than one seconds left, it buzzes. This cannot be avoided without checking more than every second, which I would like to avoid. When there is less than one seconds left on the timer it buzzes.
The widget has been tested on Bangle 1 and Bangle 2
## Screenshots ## Screenshots
TBD ![](chrono_with_wave.jpg)
![](chrono_with_pastel.jpg)
## Features ## Features

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwkBiIA/AH4A/AH4A8gAAKC8gKUC7Rf/C/PM5gDBjnBC6EcC4PBDIIbCC5/BAIIXVA4YXXAoRHUC6R3EC6KnEMAbv/C6oAKC8YA/AH4A/AH4Ax"))

View File

@ -0,0 +1,216 @@
var fontsize = g.getWidth()>200 ? 3 : 2;
var fontsizeTime = g.getWidth()>200 ? 4 : 4;
var fontheight = 10*fontsize;
var fontheightTime = 10*fontsizeTime;
var locale = require("locale");
var marginTop = 40;
var flag = false;
var hrtOn = false;
var hrtStr = "Hrt: ??? bpm";
const NONE_MODE = "none";
const ID_MODE = "id";
const VER_MODE = "ver";
const BATT_MODE = "batt";
const MEM_MODE = "mem";
const STEPS_MODE = "step";
const HRT_MODE = "hrt";
const NONE_FN_MODE = "no_fn";
const HRT_FN_MODE = "fn_hrt";
let infoMode = NONE_MODE;
let functionMode = NONE_FN_MODE;
let textCol = g.theme.dark ? "#0f0" : "#080";
function drawAll(){
updateTime();
updateRest(new Date());
}
function updateRest(now){
writeLine(locale.dow(now),1);
writeLine(locale.date(now,1),2);
drawInfo(5);
}
function updateTime(){
if (!Bangle.isLCDOn()) return;
let now = new Date();
writeLine(locale.time(now,1),0);
writeLine(flag?" ":"_",3);
flag = !flag;
if(now.getMinutes() == 0)
updateRest(now);
}
function writeLineStart(line){
if (line==0){
g.drawString(">",0,marginTop+(line)*fontheight);
} else {
g.drawString(">",4,marginTop+(line-1)*fontheight + fontheightTime);
}
}
function writeLine(str,line){
if (line == 0){
var y = marginTop+line*fontheightTime;
g.setFont("6x8",fontsizeTime);
g.setColor(textCol).setFontAlign(-1,-1);
g.clearRect(0,y,((str.length+1)*40),y+fontheightTime-1);
writeLineStart(line);
g.drawString(str,25,y);
} else {
var y = marginTop+(line-1)*fontheight+fontheightTime;
g.setFont("6x8",fontsize);
g.setColor(textCol).setFontAlign(-1,-1);
g.clearRect(0,y,((str.length+1)*20),y+fontheight-1);
writeLineStart(line);
g.drawString(str,25,y);
}
}
function drawInfo(line) {
let val;
let str = "";
let col = textCol; // green
//console.log("drawInfo(), infoMode=" + infoMode + " funcMode=" + functionMode);
switch(functionMode) {
case NONE_FN_MODE:
break;
case HRT_FN_MODE:
col = g.theme.dark ? "#0ff": "#088"; // cyan
str = "HRM: " + (hrtOn ? "ON" : "OFF");
drawModeLine(line,str,col);
return;
}
switch(infoMode) {
case NONE_MODE:
col = g.theme.bg;
str = "";
break;
case HRT_MODE:
str = hrtStr;
break;
case STEPS_MODE:
str = "Steps: " + stepsWidget().getSteps();
break;
case ID_MODE:
val = NRF.getAddress().split(":");
str = "Id: " + val[4] + val[5];
break;
case VER_MODE:
str = "Fw: " + process.env.VERSION;
break;
case MEM_MODE:
val = process.memory();
str = "Memory: " + Math.round(val.usage*100/val.total) + "%";
break;
case BATT_MODE:
default:
str = "Battery: " + E.getBattery() + "%";
}
drawModeLine(line,str,col);
}
function drawModeLine(line, str, col) {
g.setColor(col);
var y = marginTop+line*fontheight;
g.fillRect(0, y, 239, y+fontheight-1);
g.setColor(g.theme.bg).setFontAlign(0, 0);
g.drawString(str, g.getWidth()/2, y+fontheight/2);
}
function changeInfoMode() {
switch(functionMode) {
case NONE_FN_MODE:
break;
case HRT_FN_MODE:
hrtOn = !hrtOn;
Bangle.buzz();
Bangle.setHRMPower(hrtOn ? 1 : 0);
if (hrtOn) infoMode = HRT_MODE;
return;
}
switch(infoMode) {
case NONE_MODE:
if (stepsWidget() !== undefined)
infoMode = hrtOn ? HRT_MODE : STEPS_MODE;
else
infoMode = VER_MODE;
break;
case HRT_MODE:
if (stepsWidget() !== undefined)
infoMode = STEPS_MODE;
else
infoMode = VER_MODE;
break;
case STEPS_MODE:
infoMode = ID_MODE;
break;
case ID_MODE:
infoMode = VER_MODE;
break;
case VER_MODE:
infoMode = BATT_MODE;
break;
case BATT_MODE:
infoMode = MEM_MODE;
break;
case MEM_MODE:
default:
infoMode = NONE_MODE;
}
}
function changeFunctionMode() {
//console.log("changeFunctionMode()");
switch(functionMode) {
case NONE_FN_MODE:
functionMode = HRT_FN_MODE;
break;
case HRT_FN_MODE:
default:
functionMode = NONE_FN_MODE;
}
//console.log(functionMode);
}
function stepsWidget() {
if (WIDGETS.activepedom !== undefined) {
return WIDGETS.activepedom;
} else if (WIDGETS.wpedom !== undefined) {
return WIDGETS.wpedom;
}
return undefined;
}
Bangle.on('HRM', function(hrm) {
if(hrm.confidence > 90){
hrtStr = "Hrt: " + hrm.bpm + " bpm";
} else {
hrtStr = "Hrt: ??? bpm";
}
});
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
drawAll();
Bangle.on('lcdPower',function(on) {
if (on) drawAll();
});
var click = setInterval(updateTime, 1000);
// Show launcher when button pressed
Bangle.setUI("clockupdown", btn=>{
if (btn<0) changeInfoMode();
drawAll();
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -5,3 +5,5 @@
0.11: added Heart Rate Monitor status and ability to turn on/off 0.11: added Heart Rate Monitor status and ability to turn on/off
0.12: added support for different locales 0.12: added support for different locales
0.13: Use setUI, work with smaller screens and themes 0.13: Use setUI, work with smaller screens and themes
0.14: Fix BTN1 (fix #853)
Add light/dark theme support

View File

@ -20,6 +20,8 @@ const HRT_FN_MODE = "fn_hrt";
let infoMode = NONE_MODE; let infoMode = NONE_MODE;
let functionMode = NONE_FN_MODE; let functionMode = NONE_FN_MODE;
let textCol = g.theme.dark ? "#0f0" : "#080";
function drawAll(){ function drawAll(){
updateTime(); updateTime();
updateRest(new Date()); updateRest(new Date());
@ -45,9 +47,7 @@ function writeLineStart(line){
function writeLine(str,line){ function writeLine(str,line){
var y = marginTop+line*fontheight; var y = marginTop+line*fontheight;
g.setFont("6x8",fontsize); g.setFont("6x8",fontsize);
//g.setColor(0,1,0); g.setColor(textCol).setFontAlign(-1,-1);
g.setColor("#0f0");
g.setFontAlign(-1,-1);
g.clearRect(0,y,((str.length+1)*20),y+fontheight-1); g.clearRect(0,y,((str.length+1)*20),y+fontheight-1);
writeLineStart(line); writeLineStart(line);
g.drawString(str,25,y); g.drawString(str,25,y);
@ -56,7 +56,7 @@ function writeLine(str,line){
function drawInfo(line) { function drawInfo(line) {
let val; let val;
let str = ""; let str = "";
let col = "#0f0"; // green let col = textCol; // green
//console.log("drawInfo(), infoMode=" + infoMode + " funcMode=" + functionMode); //console.log("drawInfo(), infoMode=" + infoMode + " funcMode=" + functionMode);
@ -64,7 +64,7 @@ function drawInfo(line) {
case NONE_FN_MODE: case NONE_FN_MODE:
break; break;
case HRT_FN_MODE: case HRT_FN_MODE:
col = "#0ff"; // cyan col = g.theme.dark ? "#0ff": "#088"; // cyan
str = "HRM: " + (hrtOn ? "ON" : "OFF"); str = "HRM: " + (hrtOn ? "ON" : "OFF");
drawModeLine(line,str,col); drawModeLine(line,str,col);
return; return;
@ -72,7 +72,7 @@ function drawInfo(line) {
switch(infoMode) { switch(infoMode) {
case NONE_MODE: case NONE_MODE:
col = "#fff"; col = g.theme.bg;
str = ""; str = "";
break; break;
case HRT_MODE: case HRT_MODE:
@ -104,9 +104,8 @@ function drawModeLine(line, str, col) {
g.setColor(col); g.setColor(col);
var y = marginTop+line*fontheight; var y = marginTop+line*fontheight;
g.fillRect(0, y, 239, y+fontheight-1); g.fillRect(0, y, 239, y+fontheight-1);
g.setColor(0); g.setColor(g.theme.bg).setFontAlign(0, 0);
g.setFontAlign(0, -1); g.drawString(str, g.getWidth()/2, y+fontheight/2);
g.drawString(str, g.getWidth()/2, y);
} }
function changeInfoMode() { function changeInfoMode() {
@ -193,7 +192,7 @@ Bangle.on('lcdPower',function(on) {
var click = setInterval(updateTime, 1000); var click = setInterval(updateTime, 1000);
// Show launcher when button pressed // Show launcher when button pressed
Bangle.setUI("clockupdown", btn=>{ Bangle.setUI("clockupdown", btn=>{
if (btn==0) changeInfoMode(); if (btn<0) changeInfoMode();
if (btn==1) changeFunctionMode(); if (btn>0) changeFunctionMode();
drawAll(); drawAll();
}); });

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

4
apps/clock2x3/README.md Normal file
View File

@ -0,0 +1,4 @@
# 2x3 Pixel Clock
![](screenshot_pixel.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -1,3 +1,4 @@
0.01: New App! 0.01: New App!
0.02: Show text if uncalibrated 0.02: Show text if uncalibrated
0.03: Eliminate flickering 0.03: Eliminate flickering
0.04: Fix for Bangle.js 2 and themes

View File

@ -1,60 +1,72 @@
var tg = Graphics.createArrayBuffer(120,20,1,{msb:true}); var W = g.getWidth();
var timg = { var M = W/2; // middle of screen
width:tg.getWidth(), // Angle buffer
height:tg.getHeight(), var AGS = W > 200 ? 160 : 120; // buffer size
bpp:1, var AGM = AGS/2; // midpoint/radius
buffer:tg.buffer var AGH = AGM-10; // hand size
}; var ag = Graphics.createArrayBuffer(AGS,AGS,2,{msb:true});
var ag = Graphics.createArrayBuffer(160,160,2,{msb:true});
var aimg = { var aimg = {
width:ag.getWidth(), width:ag.getWidth(),
height:ag.getHeight(), height:ag.getHeight(),
bpp:2, bpp:2,
buffer:ag.buffer, buffer:ag.buffer,
palette:new Uint16Array([0,0x03FF,0xF800,0x001F]) palette:new Uint16Array([
g.theme.bg,
g.toColor("#07f"),
g.toColor("#f00"),
g.toColor("#00f")])
}; };
ag.setColor(1); ag.setColor(1).fillCircle(AGM,AGM,AGM-1,AGM-1);
ag.fillCircle(80,80,79,79); ag.setColor(0).fillCircle(AGM,AGM,AGM-11,AGM-11);
ag.setColor(0);
ag.fillCircle(80,80,69,69);
function arrow(r,c) { function arrow(r,c) {
r=r*Math.PI/180; r=r*Math.PI/180;
var p = Math.PI/2; var p = Math.PI/2;
ag.setColor(c); ag.setColor(c).fillPoly([
ag.fillPoly([ AGM+AGH*Math.sin(r), AGM-AGH*Math.cos(r),
80+60*Math.sin(r), 80-60*Math.cos(r), AGM+10*Math.sin(r+p), AGM-10*Math.cos(r+p),
80+10*Math.sin(r+p), 80-10*Math.cos(r+p), AGM+10*Math.sin(r-p), AGM-10*Math.cos(r-p),
80+10*Math.sin(r-p), 80-10*Math.cos(r-p),
]); ]);
} }
var wasUncalibrated = false;
var oldHeading = 0; var oldHeading = 0;
Bangle.on('mag', function(m) { Bangle.on('mag', function(m) {
if (!Bangle.isLCDOn()) return; if (!Bangle.isLCDOn()) return;
tg.clear(); g.reset();
tg.setFont("6x8",1);
tg.setColor(1);
if (isNaN(m.heading)) { if (isNaN(m.heading)) {
tg.setFontAlign(0,-1); if (!wasUncalibrated) {
tg.setFont("6x8",1); g.clearRect(0,24,W,48);
tg.drawString("Uncalibrated",60,4); g.setFontAlign(0,-1).setFont("6x8");
tg.drawString("turn 360° around",60,12); g.drawString("Uncalibrated\nturn 360° around",M,24+4);
wasUncalibrated = true;
}
} else {
if (wasUncalibrated) {
g.clearRect(0,24,W,48);
wasUncalibrated = false;
}
g.setFontAlign(0,0).setFont("6x8",3);
var y = 36;
g.clearRect(M-40,y,M+40,y+24);
g.drawString(Math.round(m.heading),M,y,true);
} }
else {
tg.setFontAlign(0,0);
tg.setFont("6x8",2);
tg.drawString(Math.round(m.heading),60,12);
}
g.drawImage(timg,0,0,{scale:2});
ag.setColor(0); ag.setColor(0);
arrow(oldHeading,0); arrow(oldHeading,0);
arrow(oldHeading+180,0); arrow(oldHeading+180,0);
arrow(m.heading,2); arrow(m.heading,2);
arrow(m.heading+180,3); arrow(m.heading+180,3);
g.drawImage(aimg,40,50); g.drawImage(aimg,
(W-ag.getWidth())/2,
g.getHeight()-(ag.getHeight()+4));
oldHeading = m.heading; oldHeading = m.heading;
}); });
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
Bangle.setCompassPower(1); Bangle.setCompassPower(1);
Bangle.setLCDPower(1);
Bangle.setLCDTimeout(0);

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -0,0 +1,2 @@
0.01: Initial Release
0.02: Replace icon with one found on https://icons8.com

View File

@ -0,0 +1,18 @@
# Cube Scramble
A random scramble generator for the 3x3 Rubik's cube
## Future features
I'm keen to complete this project with
* Add a timer
* Add the ability for times to be stored and exported
## Requests
Please reach out if you have feature requests or notice bugs.
## Creator
Made by [Nathan Lisgo](https://github.com/nlisgo)

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("AH4A6iIAQCwkBC6MQC4kYxAACwMT/4ACmMe9wAC6IXLj4XD+IXE8IX/C9/zR4oXOmYABC6UTCwQXZjrXKf5IAHC713AAURindAAVBiatDmIXFi4XDuMdC4fRYooX/5nBC6Xc5gABC6UcCwQXF+aPMC471DC6MTCwQXHa4V2szXBC4bXBC5YAQC7se9wAC6MYxAACwJTCAAIXL8IXFQYoX/C/4XtjrXNu1ma4z/JAA4XEgAXRCwgA/AGo"))

View File

@ -0,0 +1,74 @@
// Scramble code from: https://raw.githubusercontent.com/bjcarlson42/blog-post-sample-code/master/Rubik's%20Cube%20JavaScript%20Scrambler/part_two.js
const makeScramble = () => {
const options = ["F", "F2", "F'", "R", "R2", "R'", "U", "U2", "U'", "B", "B2", "B'", "L", "L2", "L'", "D", "D2", "D'"];
const numOptions = [0, 1, 2, 3, 4, 5]; // 0 = F, 1 = R, 2 = U, 3 = B, 4 = L, 5 = D
const scrambleMoves = [];
let bad = true;
while (bad) {
let scramble = [];
for (let i = 0; i < 20; i++) {
scramble.push(numOptions[getRandomInt(6)]);
}
// check if moves directly next to each other involve the same letter
for (let i = 0; i < 20 - 1; i++) {
if (scramble[i] == scramble[i + 1]) {
bad = true;
break;
} else {
bad = false;
}
}
}
// switch numbers to letters
let move;
for (let i = 0; i < 20; i++) {
switch (scramble[i]) {
case 0:
move = options[getRandomInt(3)]; // 0,1,2
scrambleMoves.push(move);
break;
case 1:
move = options[getRandomIntBetween(3, 6)]; // 3,4,5
scrambleMoves.push(move);
break;
case 2:
move = options[getRandomIntBetween(6, 9)]; // 6,7,8
scrambleMoves.push(move);
break;
case 3:
move = options[getRandomIntBetween(9, 12)]; // 9,10,11
scrambleMoves.push(move);
break;
case 4:
move = options[getRandomIntBetween(12, 15)]; // 12,13,14
scrambleMoves.push(move);
break;
case 5:
move = options[getRandomIntBetween(15, 18)]; // 15,16,17
scrambleMoves.push(move);
break;
}
}
return scrambleMoves;
};
const getRandomInt = max => Math.floor(Math.random() * Math.floor(max)); // returns up to max - 1
const getRandomIntBetween = (min, max) => Math.floor(Math.random() * (max - min) + min);
const presentScramble = () => {
g.clear();
E.showMessage(makeScramble().join(" "));
};
const init = () => {
presentScramble();
setWatch(() => {
presentScramble();
}, BTN1, {repeat:true});
};
init();

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -1 +1,2 @@
0.01: New App! 0.01: New App!
0.02: Minor adjustment to fix out of memory errors

View File

@ -60,8 +60,8 @@ var scenes = [
}; };
}, },
function() { function() {
Bangle.setLCDMode("120x120");
var img = require("heatshrink").decompress(atob("oNBxH+5wA/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AHGpAAoQKv4ADCBQAeqsrAAejBw9/B4oABqt/IGepHw5CEQspALH5hBC5pAvv4/MAALFkIBWpPI6IHqpAu0Z3GfYOpRYdPQEhALYIp2FBYNVI4JAvvL4LH0yBYAFJAQQQ5Ay1JAFftBAQBYxCDv+qIGiCHIQiGnIBfOv5BJIQRAyIJkrvKEkIBrFBB4qEGIGRCNYsZAQIQV/IZDEiICRCDQVJAUIQVPC4lVIF6yJQYpAZ5t/FYvNIBepqtVIJGjIDoqBDY2pdYo3DfAhBIQLmpvIcDvIrC5oJEIAhTCGQmj5qgEC4t5e7YrBqt5BI6UFBg15v4XHbQwAQb4oAKv7NKABdVRoYATUAwnICqjZFIMdVE4+jXI4XGYCxBFFZN/M5OpCxUrvJ/ZFYmjvNVAAY+KCwpDBC6YAV5vNC9oA/AH4A/AHYA==")); var img = require("heatshrink").decompress(atob("oNBxH+5wA/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AHGpAAoQKv4ADCBQAeqsrAAejBw9/B4oABqt/IGepHw5CEQspALH5hBC5pAvv4/MAALFkIBWpPI6IHqpAu0Z3GfYOpRYdPQEhALYIp2FBYNVI4JAvvL4LH0yBYAFJAQQQ5Ay1JAFftBAQBYxCDv+qIGiCHIQiGnIBfOv5BJIQRAyIJkrvKEkIBrFBB4qEGIGRCNYsZAQIQV/IZDEiICRCDQVJAUIQVPC4lVIF6yJQYpAZ5t/FYvNIBepqtVIJGjIDoqBDY2pdYo3DfAhBIQLmpvIcDvIrC5oJEIAhTCGQmj5qgEC4t5e7YrBqt5BI6UFBg15v4XHbQwAQb4oAKv7NKABdVRoYATUAwnICqjZFIMdVE4+jXI4XGYCxBFFZN/M5OpCxUrvJ/ZFYmjvNVAAY+KCwpDBC6YAV5vNC9oA/AH4A/AHYA=="));
g.clear(); g.clear();
y = 0; y = 0;
var step = 4; var step = 4;
@ -70,8 +70,7 @@ var scenes = [
g.clear(); g.clear();
g.drawImage(img,60,60,{rotate:Math.sin(y*0.03)*0.5}); g.drawImage(img,60,60,{rotate:Math.sin(y*0.03)*0.5});
g.flip(); g.flip();
}, 20); }, 20);
Bangle.setLCDMode("120x120");
return function() { return function() {
if (i) clearInterval(i); if (i) clearInterval(i);
}; };

1
apps/emojuino/ChangeLog Normal file
View File

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

28
apps/emojuino/README.md Normal file
View File

@ -0,0 +1,28 @@
# Emojuino
Emojis & Espruino!
## Usage
Select an emoji and then tap to transmit! The emoji will be recognised by [Pareto Anywhere](https://www.reelyactive.com/pareto/anywhere/) open source middleware and any other program which observes the [InteroperaBLE Identifier](https://reelyactive.github.io/interoperable-identifier/) open standard.
## Features
Currently implements a tiny subset of possible [Unicode emojis](https://unicode.org/emoji/charts/full-emoji-list.html) which are advertised as an [InteroperaBLE Identifier](https://reelyactive.github.io/interoperable-identifier/) encapsulated as Eddystone UID.
## Controls
Swipe left/right to select the emoji to broadcast. Tap the screen to initiate the broadcast. Emoji will flash while broadcasting, which lasts for 5 seconds.
## Requests
[Contact reelyActive](https://www.reelyactive.com/contact/) for support/updates.
## Creator
Developed by [jeffyactive](https://github.com/jeffyactive) of [reelyActive](https://www.reelyactive.com)

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwkBiIAHkUoxGIwUiBxAAGiQVCAAeCkIWNCooADDBYWKDBYWEkc////+cyDBhxDCoQAD+YLDCw0YBQQVFAAYYCwIXFHQRDElGCJYgOCFw8vBwPyOgoJFGAg4BIoQWGDAhJCIwoLBHgYAGJQIjCIwguCnCRFRoeDGAZICAgOPFwaRGDAQfB/AwDBAYuCX44wDAgTrDBoIDBGYP/manBmYFBFYQPDwJeBD4iRGRoQ/FC4QqBEYIbERooTBCAeBNAIjBBQIDDAAggBG4IDDwQXBEQIDDUAgcCHASaBAYQTFMQpcFDYp+EEII9DAARRDFIIfDHIwXBVISlDC4YzD9wA0osFpwIF8lQqgWK8kAgEEBItABIIhGAAfgBoMABIoIChwX0jwED8oNBgoXFqAJBrwHD8IXEBwQNEEIYgFC4wAQ8MRC6sRC+BgULwIwHSINVpwuLC43kaAQABqgaHC4bZHAAkFqhGHGAovFAAYyDCwgwFL4IwGFxAwNFxIwG8lVCoSTEFw7bPCxAYNCxT0LIpIxMCpoyHFhI"))

145
apps/emojuino/emojuino.js Normal file
View File

@ -0,0 +1,145 @@
/**
* Copyright reelyActive 2021
* We believe in an open Internet of Things
*/
// Emojis are integer pairs with the form [ image, Unicode code point ]
// For code points see https://unicode.org/emoji/charts/emoji-list.html
const EMOJIS = [
[ ':)', 0x1f642 ], // Slightly smiling
[ ':|', 0x1f610 ], // Neutral
[ ':(', 0x1f641 ], // Slightly frowning
[ '+1', 0x1f44d ], // Thumbs up
[ '-1', 0x1f44e ], // Thumbs down
[ '<3', 0x02764 ], // Heart
];
const EMOJI_TRANSMISSION_MILLISECONDS = 5000;
const BLINK_PERIOD_MILLISECONDS = 500;
const TRANSMIT_BUZZ_MILLISECONDS = 200;
const CYCLE_BUZZ_MILLISECONDS = 50;
// Non-user-configurable constants
const IMAGE_INDEX = 0;
const CODE_POINT_INDEX = 1;
const BTN_WATCH_OPTIONS = { repeat: true, debounce: 20, edge: "falling" };
const UNICODE_CODE_POINT_ELIDED_UUID = [ 0x49, 0x6f, 0x49, 0x44, 0x55,
0x54, 0x46, 0x2d, 0x33, 0x32 ];
// Global variables
let emojiIndex = 0;
let isToggleOn = false;
let isTransmitting = false;
let lastDragX = 0;
let lastDragY = 0;
// Cycle through emojis
function cycleEmoji(isForward) {
if(isTransmitting) { return; }
if(isForward) {
emojiIndex = (emojiIndex + 1) % EMOJIS.length;
}
else if(--emojiIndex < 0) {
emojiIndex = EMOJIS.length - 1;
}
drawImage(EMOJIS[emojiIndex][IMAGE_INDEX]);
Bangle.buzz(CYCLE_BUZZ_MILLISECONDS);
}
// Handle a touch: transmit displayed emoji
function handleTouch(zone, event) {
if(isTransmitting) { return; }
let emoji = EMOJIS[emojiIndex];
transmitEmoji(emoji[IMAGE_INDEX], emoji[CODE_POINT_INDEX],
EMOJI_TRANSMISSION_MILLISECONDS);
Bangle.buzz(TRANSMIT_BUZZ_MILLISECONDS);
}
// Transmit the given code point for the given duration in milliseconds,
// blinking the image once per second.
function transmitEmoji(image, codePoint, duration) {
let instance = [ 0x00, 0x00, (codePoint >> 24) & 0xff,
(codePoint >> 16) & 0xff, (codePoint >> 8) & 0xff,
codePoint & 0xff ];
require('ble_eddystone_uid').advertise(UNICODE_CODE_POINT_ELIDED_UUID,
instance);
isTransmitting = true;
let displayIntervalId = setInterval(toggleImage, BLINK_PERIOD_MILLISECONDS,
image);
setTimeout(terminateEmoji, duration, displayIntervalId);
}
// Terminate the emoji transmission
function terminateEmoji(displayIntervalId) {
NRF.setAdvertising({ });
isTransmitting = false;
clearInterval(displayIntervalId);
drawImage(EMOJIS[emojiIndex][IMAGE_INDEX]);
}
// Toggle the display between image/off
function toggleImage(image) {
if(isToggleOn) {
drawImage(EMOJIS[emojiIndex][IMAGE_INDEX]);
}
else {
g.clear();
}
isToggleOn = !isToggleOn;
}
// Draw the given emoji
function drawImage(image) {
g.clear();
g.drawString(image, g.getWidth() / 2, g.getHeight() / 2);
g.flip();
}
// Handle a drag event
function handleDrag(event) {
let isFingerReleased = (event.b === 0);
if(isFingerReleased) {
let isHorizontalDrag = (Math.abs(lastDragX) >= Math.abs(lastDragY)) &&
(lastDragX !== 0);
if(isHorizontalDrag) {
cycleEmoji(lastDragX > 0);
}
}
else {
lastDragX = event.dx;
lastDragY = event.dy;
}
}
// Special function to handle display switch on
Bangle.on('lcdPower', (on) => {
if(on) {
drawImage(EMOJIS[emojiIndex][IMAGE_INDEX]);
}
});
// On start: display the first emoji and handle drag and touch events
g.clear();
g.setFont('Vector', 80);
g.setFontAlign(0, 0);
drawImage(EMOJIS[emojiIndex][IMAGE_INDEX]);
Bangle.on('touch', handleTouch);
Bangle.on('drag', handleDrag);

BIN
apps/emojuino/emojuino.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

1
apps/ffcniftya/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: New Clock Nifty A

4
apps/ffcniftya/README.md Normal file
View File

@ -0,0 +1,4 @@
# Nifty-A Clock
![](screenshot_nifty.png)

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwkEIf4A5gX/+AGEn//mIWLgP/C4gGCAAMgC5UvC4sDC4YICkIhBgMQiEBE4Uxn4XDj//iEAn/yA4ICBgUikEikYXBBAIXEn/xJYURAYMygERkQHBiYLBKYIXF+AVDC4czgUSmIXBCQgED+ZeBR4YXBLYICDC5CPGC4IAIC40zmaPDC4MSLQQXK+ayCR4QXCiRoEC44ECh4bCC4MTiTDBC6ZHOC5B3NLYcvC4kBgL5BAAUikT+BfIIrB/8ykf/eYQXBkUTI4cBW4YQCgQGDmAXDkJfEC46GBAoJKCR4geCAAMRAAZRDAoIODO4UBPRIAJR5QXWgKNCTApNDC5Mv/6/DAwR3GAAyHCC4anJIo3/+bvEa4Uia4oXHkEvC4cvIgUf+YXKHYIvEAgcPC5QSGC5UBSwYXJLYQXFkUhgABBC5Ef/4mBl4XEmETmIXKgaXBmYCBC4cTkMxiQXJS4IACL4p3MgESCwJHFR5oxCiB3FkERC5cSToQXFmUyiAZFR48Bn7zCAQMjkfykQkBN4n/XgKPBAAQgCUQIfBUwYXHFgIGCdI4XDmYADmIIEkAWJAH4A4A=="))

95
apps/ffcniftya/app.js Normal file
View File

@ -0,0 +1,95 @@
const locale = require("locale");
const is12Hour = (require("Storage").readJSON("setting.json", 1) || {})["12hour"];
/* Clock *********************************************/
const scale = g.getWidth() / 176;
const widget = 24;
const viewport = {
width: g.getWidth(),
height: g.getHeight(),
}
const center = {
x: viewport.width / 2,
y: Math.round(((viewport.height - widget) / 2) + widget),
}
function d02(value) {
return ('0' + value).substr(-2);
}
function draw() {
g.reset();
g.clearRect(0, widget, viewport.width, viewport.height);
const now = new Date();
const hour = d02(now.getHours() - (is12Hour && now.getHours() > 12 ? 12 : 0));
const minutes = d02(now.getMinutes());
const day = d02(now.getDate());
const month = d02(now.getMonth() + 1);
const year = now.getFullYear();
const month2 = locale.month(now, 3);
const day2 = locale.dow(now, 3);
g.setFontAlign(1, 0).setFont("Vector", 90 * scale);
g.drawString(hour, center.x + 32 * scale, center.y - 31 * scale);
g.drawString(minutes, center.x + 32 * scale, center.y + 46 * scale);
g.fillRect(center.x + 30 * scale, center.y - 72 * scale, center.x + 32 * scale, center.y + 74 * scale);
g.setFontAlign(-1, 0).setFont("Vector", 16 * scale);
g.drawString(year, center.x + 40 * scale, center.y - 62 * scale);
g.drawString(month, center.x + 40 * scale, center.y - 44 * scale);
g.drawString(day, center.x + 40 * scale, center.y - 26 * scale);
g.drawString(month2, center.x + 40 * scale, center.y + 48 * scale);
g.drawString(day2, center.x + 40 * scale, center.y + 66 * scale);
}
/* Minute Ticker *************************************/
let tickTimer;
function clearTickTimer() {
if (tickTimer) {
clearTimeout(tickTimer);
tickTimer = undefined;
}
}
function queueNextTick() {
clearTickTimer();
tickTimer = setTimeout(tick, 60000 - (Date.now() % 60000));
// tickTimer = setTimeout(tick, 3000);
}
function tick() {
draw();
queueNextTick();
}
/* Init **********************************************/
// Clear the screen once, at startup
g.clear();
// Start ticking
tick();
// Stop updates when LCD is off, restart when on
Bangle.on('lcdPower', (on) => {
if (on) {
tick(); // Start ticking
} else {
clearTickTimer(); // stop ticking
}
});
// Load widgets
Bangle.loadWidgets();
Bangle.drawWidgets();
// Show launcher when middle button pressed
Bangle.setUI("clock");

BIN
apps/ffcniftya/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

2
apps/ffcniftyb/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.01: New Clock Nifty B
0.02: Added configuration

9
apps/ffcniftyb/README.md Normal file
View File

@ -0,0 +1,9 @@
# Nifty Series B Clock
- Display Time and Date
- Color Configuration
##
![](screenshot.png)

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwkB/4A/AH4ARgMRBA3xBBIJCAYIFDAAYHGCAYJBDYQABj4PD+AXFCwgXGCAg9ECwwJBJQooGCxAXCIYQpBAgg9IC5yPCCw4XKBYIsFPwUBXQQXHAYREIF5ZEC+MfWQYXODQYTGC5ZDEOw0QMAIXMPggvSC44vRL5b8EAYIACC5i0FCwaOBC5C0DA4ZLCC5hfC/4DBIwwXKCInwgAWEKIwXJAA4XXCxYXCEwR2EgJeLR5LbCGRYXIAgzvKh7zGZg4XGIYisBA4JJCC6B5DAoYXWF6xfRC4fwAgMBC6cBU5I6CC5AECCo0QJwQXJaZJHMEYR1JC5QKBXo8QC4oCBAZAwHgKXBTQwSDBIKmGgJ3DEYheEA4ZfJKgkPdJQXHDAQWBC44eIC4QAMDA4A=="))

118
apps/ffcniftyb/app.js Normal file
View File

@ -0,0 +1,118 @@
const locale = require("locale");
const storage = require('Storage');
const is12Hour = (storage.readJSON("setting.json", 1) || {})["12hour"];
const color = (storage.readJSON("ffcniftyb.json", 1) || {})["color"] || 63488 /* red */;
/* Clock *********************************************/
const scale = g.getWidth() / 176;
const screen = {
width: g.getWidth(),
height: g.getHeight() - 24,
};
const center = {
x: screen.width / 2,
y: screen.height / 2,
};
function d02(value) {
return ('0' + value).substr(-2);
}
function renderEllipse(g) {
g.fillEllipse(center.x - 5 * scale, center.y - 70 * scale, center.x + 160 * scale, center.y + 90 * scale);
}
function renderText(g) {
const now = new Date();
const hour = d02(now.getHours() - (is12Hour && now.getHours() > 12 ? 12 : 0));
const minutes = d02(now.getMinutes());
const day = d02(now.getDate());
const month = d02(now.getMonth() + 1);
const year = now.getFullYear();
const month2 = locale.month(now, 3);
const day2 = locale.dow(now, 3);
g.setFontAlign(1, 0).setFont("Vector", 90 * scale);
g.drawString(hour, center.x + 32 * scale, center.y - 31 * scale);
g.drawString(minutes, center.x + 32 * scale, center.y + 46 * scale);
g.setFontAlign(1, 0).setFont("Vector", 16 * scale);
g.drawString(year, center.x + 80 * scale, center.y - 42 * scale);
g.drawString(month, center.x + 80 * scale, center.y - 26 * scale);
g.drawString(day, center.x + 80 * scale, center.y - 10 * scale);
g.drawString(month2, center.x + 80 * scale, center.y + 44 * scale);
g.drawString(day2, center.x + 80 * scale, center.y + 60 * scale);
}
const buf = Graphics.createArrayBuffer(screen.width, screen.height, 1, {
msb: true
});
function draw() {
const img = {
width: screen.width,
height: screen.height,
transparent: 0,
bpp: 1,
buffer: buf.buffer
};
// cleat screen area
g.clearRect(0, 24, g.getWidth(), g.getHeight());
// render outside text with ellipse
buf.clear();
renderText(buf.setColor(1));
renderEllipse(buf.setColor(0));
g.setColor(color).drawImage(img, 0, 24);
// render ellipse with inside text
buf.clear();
renderEllipse(buf.setColor(1));
renderText(buf.setColor(0));
g.setColor(color).drawImage(img, 0, 24);
}
/* Minute Ticker *************************************/
let ticker;
function stopTick() {
if (ticker) {
clearTimeout(ticker);
ticker = undefined;
}
}
function startTick(run) {
stopTick();
run();
ticker = setTimeout(() => startTick(run), 60000 - (Date.now() % 60000));
// ticker = setTimeout(() => startTick(run), 3000);
}
/* Init **********************************************/
g.clear();
startTick(draw);
Bangle.on('lcdPower', (on) => {
if (on) {
startTick(draw);
} else {
stopTick();
}
});
Bangle.loadWidgets();
Bangle.drawWidgets();
Bangle.setUI("clock");

BIN
apps/ffcniftyb/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -0,0 +1,49 @@
(function (back) {
const storage = require('Storage');
const SETTINGS_FILE = "ffcniftyb.json";
const colors = {
65535: 'White',
63488: 'Red',
65504: 'Yellow',
2047: 'Cyan',
2016: 'Green',
31: 'Blue',
0: 'Black',
}
function load(settings) {
return Object.assign(settings, storage.readJSON(SETTINGS_FILE, 1) || {});
}
function save(settings) {
storage.write(SETTINGS_FILE, settings)
}
const settings = load({
color: 63488 /* red */,
});
const saveColor = (color) => () => {
settings.color = color;
save(settings);
back();
};
function showMenu(items, opt) {
items[''] = opt || {};
items['< Back'] = back;
E.showMenu(items);
}
showMenu(
Object.keys(colors).reduce((menu, color) => {
menu[colors[color]] = saveColor(color);
return menu;
}, {}),
{
title: 'Color',
selected: Object.keys(colors).indexOf(settings.color)
}
);
});

View File

@ -7,13 +7,9 @@ function showMainMenu() {
'': { '': {
'title': 'App Manager', 'title': 'App Manager',
}, },
'Free': { '< Back': ()=> {load();},
value: undefined, 'Sort Apps': () => showSortAppsMenu(),
format: (v) => { 'Manage Apps': ()=> showApps(),
return store.getFree();
},
onchange: () => {}
},
'Compact': () => { 'Compact': () => {
E.showMessage('Compacting...'); E.showMessage('Compacting...');
try { try {
@ -22,9 +18,13 @@ function showMainMenu() {
} }
showMainMenu(); showMainMenu();
}, },
'Apps': ()=> showApps(), 'Free': {
'Sort Apps': () => showSortAppsMenu(), value: undefined,
'< Back': ()=> {load();} format: (v) => {
return store.getFree();
},
onchange: () => {}
},
}; };
E.showMenu(mainmenu); E.showMenu(mainmenu);
} }

5
apps/flappy/README.md Normal file
View File

@ -0,0 +1,5 @@
# Flappy Bird
![](screenshot1_flappy.png)
![](screenshot2_flappy.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

1
apps/floralclk/ChangeLog Normal file
View File

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

4
apps/floralclk/README.md Normal file
View File

@ -0,0 +1,4 @@
# Floral Clock
![](screenshot_floral.png)

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwhE+sVin0/tVjsdim84sdro1GAQNrAAlHAYVqABk/FINosc/AoNpF4cTGoMTnIhBo1qEgIvFABJACoAvEFwJaDGoNjnFpn5eCik/DYQwBAAQwOFIMUDYKBBLwQwBnwoBBAM3GIIEBhs5E4RLBPYqMKFwU4AAM+nwCBF4SJCAwMxXII2BnBeDJogAGNQIAFBIJMBRIYvCLQK+Bow7BhsNCINjm45BXwZgHF5ITBigoBF4NpQoIwBLwLJBn8Oh0NBoU4F4J6CF4RiKR47iCtIrBiaGBEgdknMOnBABiYKBtNkKoaUIdo5hCQoKvBYgKGBGAIJBMANjhqfBHgLQBNgKcBEpAxBBA9HHoiwBF4S3BcoM4Nwdim83sVEGAINBMQIfBEASYGLII4ECISFBnEyFgKHBGwRsDHYKfBaQOGrifCXw4qBNgIEBXoQHBCQZXBnArCAQNpnBWBFwUTBINiwGkwFcsbzDEwJcFG4pcCAAMUik/EIKJBn6JBMYNpnzABsY0BwGeAAN6wLnCEQQACF4ztCF4UUJ4QNDGAKTCtMTnASBHwOezAwCveIP4ReEeQzNDFwgvDDQU/oDlDJYVkF4e8z2Hx2Px2IAAKKEGo1qnAuBtLYBBwRJCAoIuCbgVqxAsCAQWB1mBwN6AANWmSwBJwRcCDIIuDnxAGAYU5HQmPF4W84QBBvWlGoOY4TIBmMxnJGCYYc5ik+coNjn8UhsUiqRDGQQUBQQJUBz3CzAxBYYYADvWGsTZBDoNHcQUTXgU+n1pB4LmBFQSUCAoNkw4uCF4QBBF4QFBAAIFBF4IjBoBMCn84nwtCMAIABm8TnLREXYd7KQSLBzoBDAQJlBBQN5w1osU/VAQuBnCLBGQNpGYM4R4LRCAATTBRgJcCy4kBz3I5HO5HHy4JBYwRfBcYIfBoE4m5YCho0BA4M4FwzzCxCMBEIO73guB5wAC5BgBSoWAF4KQBsdkKoKNChoACik4nIuHF4i0BdQOdF4XNAQK8Cz2lqzjCnCIBFwTnBS4IrBTQQuBdoLuCBAWOdoOYWgRfC5ovDy4vDriDCAAS8CFQYADdQgvCowvDSAK0CF4SPC4QwCvVcmUymMxFwSUBFwQoDFQToBAoIsBBoSQBMASRBy6QDXwIJCSIWAFwjzDWowAKGYOHwIhBYIOezooC3YuBF4d6GAK/BYAVkF6CUExBiBYQQCBFQIvCzAvCAYM3LoUTLwIeDF5pBDSQRgCLQYDBZQZrBz1cw9iiZeCWoQvWeYQuBfIIxCLwd6w1inEULyQvFtYvFEwOB0uBz4zCX4QuBnDsCDortNCQNHL4mYFwN7ZAOIfgN6AANcsc+m6NBDoRgQFwNGo5FBx2HKoZeBHYNqsg7BqtVsS8BRoReCL6AgBSYQ1CwJWBveHZYlkmMxLwM4h05sgADPwRRCF5ouDMIKKCxwPDsk4mM4XgMTXwLvBikOYYQvOBoQOBoE/JA4DBn8UFwNim8NF4QABhsNnIvQUgVAnMOVoQ4CAANqscUidiRoMNm4zBAAQHBF6CLDO4JIBGAVHXgYiBn1jn0NGYVoAAIvBIwYvOBgM/hyxBAAQXBHYU5RANjscTLwNjLgIuBny+FF5xeBhtcPYU+DYJeDRog0CCIYSBoAvRGAMUmOHJgcbF4QuBFIUNmIBBeYItCIIRNBd54ABisUVgNED4QJBn69Dm4uBh0OnIsBoArCFBoPDHgNqoAvBL4YvCb4JeBnxiCslkDogvRNQVGGALrBVobwBfAMNXoMTigsHDINHAAIvJGIdGn9ro4FBscNMANpF4LoBm4DChq1BFAJDBDobmMMIgvDA4UULwKHBMoLlBG4MynBeBCYQfFF56MBoAbDMAKzBnETm7oBGoM4hxeCQoJfCcJC/KAgIvFMAMNAASNBsQ1BLwVqFwIeELppCBF4dq"))

76
apps/floralclk/app.js Normal file
View File

@ -0,0 +1,76 @@
function getImg() {
return require("heatshrink").decompress(atob("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"));
}
var IMAGEWIDTH = 176;
var IMAGEHEIGHT = 109;
Graphics.prototype.setFontDancingScript = function() {
// Actual height 44 (44 - 1)
var widths = atob("DBIhFB4bGRoeFhweDQ==");
var font = atob("AAAAAAAAAAAAAAMAAAAAAAHgAAAAAAD4AAAAAAA+AAAAAAAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4AAAAAAD8AAAAAAD+AAAAAAD/AAAAAAD/gAAAAAH/gAAAAAH/wAAAAAH/gAAAAAP/gAAAAAP/gAAAAAf/AAAAAAf/AAAAAA/+AAAAAA/+AAAAAB/+AAAAAB/8AAAAAB/8AAAAAA/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/8AAAAAf//4AAAAf///AAAAf///4AAAf////AAAf/8Af4AAP/gAA/AAP/AAADwAH+AAAAcAD+AAAAHAA+AAAABwAfAAAAAcAPAAAAAHADgAAAABwB4AAAAA8AcAAAAAOAHAAAAAHgBgAAAAHwAYAAAAD4AGAAAAD+ABwAAAD/AAcAAAD/gAHgAAH/wAA+AAP/wAAH+H//4AAB////4AAAP///4AAAA///4AAAAD//gAAAAAD8AAAAAAAAAAAGAAAAAAABwAAAAAAAcAAAAAAAHAAAAAAABwAAAAAAAcAAGAAAAPAADwAAA/wAA4AAB/8AAeAAP//AAPAD//7wAHwf//w8AB////gPAA///+ABwAf//4AAcAP/+AAADAH/wAAAAQB8AAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAAAAAB8AAAAAAAfAAAAAAAGwAAAAAABsAAAAAAAfAAAAAAAHwAAP4AAB4AAP/AAAeAAH/wAAHAAD/+AAD4AB+AgAB+AA8AAAA/gAOAAAAf8AHAAAAPvABwAAAPzwAYAAAH4eAGAAAH8HgBgAAD+B8AcAAD/AfAHAAH/AHwB4AH/gB8AP4//gAfAD///wAHwAf//wAB8AB//gAAeAAP/gAAHAAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfwAAAAAAP+AAAAAAH/wAAAAAB/+AAAAAAcfgAAAAAEB8AB/AAAAPAA/4AAABwAf+AAAAcAP/gAAAHADwQAAABwAwABAAAcAcAAYAAHAHAAOAADwBgAHwAB4AYAD+AB+AHAB/4B/ABwB+///wAeB/P//4AH//h//8AA//4P/+AAH/4B/+AAA/8AH/AAAH8AAEAAAAAAAAAAAAAAAAAAAAAAABgAAAAAAA4AAAAAAAeAAAAAAAPAAAAAAAHwAAAAAAD8AAAAAAB/AAAAAAA/wAAAAAAe8AAAAAAPPAAAAAAHjwAAAAADw8AAAAAB4PAAwAAA8DwP+AAAeA///wAAPAP//8AAHh////AAH3////wAD////AAAB///wAAAA//88AAAAf/gPAAAAf8ADgAAAHgAA4AAAAAAAMAAAAAAAAAfwAAAAAAP+AAAAAAH/wAAAAAB/+AAAAAA8PgAAAAwEA8AAAH8AAHAAAP/AABwAA//gAAcAD/4YAAHAH/gGAABwB/ABgAAcAfgAYAAPAD4AHAADwA+ABwAB4APgAeAB+AD4AH4B/AA+AA///wAPgAP//4AD4AB//8AB+AAP/+AAfgAB//AAHwAAH/AAB8AAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4AAAAAAf/4AAAAA///gAAAB///+AAAB////wAAB////+AAA//+B/gAA//8AD8AAf+OAAPAAP8HAABwAH8BgAAcAD8A4AAHAB+AOAABwAeADgAAcAPAA4AAHADgAOAADwBwADgAA8AcAA8AA+AGAAPgAfgBgAD+A/wAYAAf//4AGAAH//8ABwAA//+AAfgAH//AAD4AAf/AAAcAAA+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAAAHAA/AAAABwB/wAwAA8B/8AeAAPB//AHwADx//gB8AA9//wAfAAP/4IAHwAD/wAAB8AB/wAAAPAB/gAAADwB/wAAAA8A/8AAAAPA/PAAAADw/DwAAAA8/A8AAAAP/AOAAAAD/ADgAAAB/gAwAAAAAAAAAAAAAAAAABAAAAAAAH/AAAAAAD/4AAAAAB//AAAAAA//4AAOAAfA+AAf8APgHwAP/wHgA8AH/+DwAHAD//48ABwA///eAAcAeAf/AAHAHAB/gABwBwAP4AA8AYAB/AAPAGAAf4AHgBgAP/AD4AcAHv8B8AHgD5///AA8H8H//gAP/+A//wAB/+AH/4AAP/AAf8AAA/AAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAAAAAP/gAAeAAP/+AAPgAH//wAD8AD//+AAPAB///gAAwAfwD8AAMAPwAfAADADwADwAAwB4AAeAAcAcAAHgAHAHAAA4ADgBwAAOAB4AcAADAB8AHAAAwA/ABwAAcA/gAeAAGB/wAHwADh/4AA+AB//8AAP+D//8AAB////+AAAP///+AAAB///+AAAAP//8AAAAAf/4AAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAA4AAAAAAAeAAAAADAHgAAAAB4BwAAAAAeAIAAAAAHgAAAAAABwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
var scale = 1; // size multiplier for this font
g.setFontCustom(font, 46, widths, 50+(scale<<8)+(1<<16));
}
// timeout used to update every minute
var drawTimeout;
// schedule a draw for the next minute
function queueDraw() {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = setTimeout(function() {
drawTimeout = undefined;
draw();
}, 60000 - (Date.now() % 60000));
}
function draw() {
var x = g.getWidth()/2;
var y = 50;
g.reset().clearRect(0,24,g.getWidth(),g.getHeight()-IMAGEHEIGHT);
if (g.getWidth() == IMAGEWIDTH)
g.drawImage(getImg(),0,g.getHeight()-IMAGEHEIGHT);
else {
let scale = g.getWidth()/IMAGEWIDTH;
y *= scale;
g.drawImage(getImg(),0,g.getHeight()-IMAGEHEIGHT*scale,{scale:scale});
}
var date = new Date();
var dateStr = require("locale").date(date);
// draw time
g.setFont("DancingScript").setFontAlign(0,0).setColor("#f00");
g.drawString(date.getHours(), x,y);
y += 43;
g.drawString(date.getMinutes().toString().padStart(2,0), x,y);
// draw date
y += 22;
g.setFontAlign(0,0).setFont("6x8");
var p = g.getWidth()-60;
g.clearRect(p,y-4,g.getWidth()-p,y+3); // clear the background
g.drawString(dateStr,x,y);
// queue draw in one minute
queueDraw();
}
// Stop updates when LCD is off, restart when on
Bangle.on('lcdPower',on=>{
if (on) {
draw(); // draw immediately, queue redraw
} else { // stop draw timer
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
}
});
// set background colour
g.setTheme({bg:"#0ff"});
// Clear the screen once, at startup
g.clear();
// draw immediately at first, queue update
draw();
// Show launcher when middle button pressed
Bangle.setUI("clock");
// Load widgets
Bangle.loadWidgets();
Bangle.drawWidgets();

BIN
apps/floralclk/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

1
apps/fwupdate/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: Initial version

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