pull/925/head
Ronin0000 2021-11-21 18:11:43 -08:00
parent 3bde4d2e48
commit e878e54787
300 changed files with 11197 additions and 3283 deletions

View File

@ -1,10 +1,10 @@
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 **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
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
* 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
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...
* `apps/7chname/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"
* `apps/myappid/app.png` should be a 48px icon
* 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:
```
{ "id": "7chname",
{ "id": "myappid",
"name": "My app's human readable name",
"shortName" : "Short Name",
"icon": "app.png",
"description": "A detailed description of my great app",
"tags": "",
"storage": [
{"name":"7chname.app.js","url":"app.js"},
{"name":"7chname.img","url":"app-icon.js","evaluate":true}
{"name":"myappid.app.js","url":"app.js"},
{"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/)
(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
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`
and then enter `7chname.app.js` as the name.
`myappid.app.js` (if you'd uploaded your app previously), or `New File`
and then enter `myappid.app.js` as the name.
Now, clicking the `Send to Espruino` icon will load the app directly into
Espruino **and** will automatically run it.
@ -115,10 +115,13 @@ and set it to `Load default application`.
## Example Applications
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`
or `apps/_example_widget` to `apps/7chname`, and add `apps/_example_X/add_to_apps.json` to
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/myappid`, and add `apps/_example_X/add_to_apps.json` to
`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
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
"icon":"*7chname", // for Bangle.js menu
"src":"-7chname", // source file
"icon":"*myappid", // for Bangle.js menu
"src":"-myappid", // source file
"type":"widget/clock/app/bootloader", // optional, default "app"
// 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
@ -217,8 +220,10 @@ and which gives information about the app for the Launcher.
{ "id": "appid", // 7 character app id
"name": "Readable name", // readable name
"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)
"icon": "icon.png", // icon in apps/
"screenshots" : [ { url:"screenshot.png" } ], // optional screenshot for app
"type":"...", // optional(if app) -
// 'app' - an application
// 'widget' - a widget
@ -226,7 +231,9 @@ and which gives information about the app for the Launcher.
// 'bootloader' - code that runs at startup only
// 'RAM' - code that runs and doesn't upload anything to storage
"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" : { "messages":"app" } // optional, depend on a specific app ID
// 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
// 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)
"noOverwrite":true // if supplied, this file will not be overwritten if it
// 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
@ -306,10 +316,10 @@ version of what's in `apps.json`:
<script>
document.getElementById("upload").addEventListener("click", function() {
sendCustomizedApp({
id : "7chname",
id : "myappid",
storage:[
{name:"7chname.app.js", url:"app.js", content:app_source_code},
{name:"7chname.img", content:'require("heatshrink").decompress(atob("mEwg...4"))', evaluate:true},
{name:"myappid.app.js", url:"app.js", content:app_source_code},
{name:"myappid.img", content:'require("heatshrink").decompress(atob("mEwg...4"))', evaluate:true},
]
});
});
@ -435,7 +445,7 @@ from the IDE.
### 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.

1
_config.yml Normal file
View File

@ -0,0 +1 @@
theme: jekyll-theme-minimal

3811
apps.json

File diff suppressed because it is too large Load Diff

View File

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

View File

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

@ -7,3 +7,5 @@
0.07: Pressing a button now exits immediately (fix #618)
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

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

File diff suppressed because one or more lines are too long

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

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

@ -33,6 +33,7 @@ function getCurrentHr() {
function showMainMenu() {
const menu = {
'': { 'title': 'Alarm/Timer' },
'< Back' : ()=>{load();},
'New Alarm': ()=>editAlarm(-1),
'New Timer': ()=>editTimer(-1)
};
@ -48,7 +49,7 @@ function showMainMenu() {
else editAlarm(idx);
};
});
menu['< Back'] = ()=>{load();};
if (WIDGETS["alarm"]) WIDGETS["alarm"].reload();
return E.showMenu(menu);
}
@ -70,6 +71,7 @@ function editAlarm(alarmIndex) {
}
const menu = {
'': { 'title': 'Alarm' },
'< Back' : showMainMenu,
'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'
@ -119,7 +121,6 @@ function editAlarm(alarmIndex) {
showMainMenu();
};
}
menu['< Back'] = showMainMenu;
return E.showMenu(menu);
}
@ -174,7 +175,6 @@ function editTimer(alarmIndex) {
showMainMenu();
};
}
menu['< Back'] = showMainMenu;
return E.showMenu(menu);
}

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,4 +1,6 @@
0.01: First version
0.02: Moved arrow image load to global scope
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 pal2color = new Uint16Array([0x0000,0xffff],0,1);
var pal1color = new Uint16Array([g.theme.bg,0xFFC0],0,1);
var pal2color = new Uint16Array([g.theme.bg,g.theme.fg],0,1);
var buf1 = Graphics.createArrayBuffer(128,128,1,{msb:true});
var buf2 = Graphics.createArrayBuffer(80,40,1,{msb:true});
var intervalRef;
@ -7,6 +7,7 @@ var bearing=0; // always point north
var heading = 0;
var oldHeading = 0;
var candraw = false;
var isCalibrating = false;
var CALIBDATA = require("Storage").readJSON("magnav.json",1)||null;
function flip1(x,y) {
@ -76,7 +77,7 @@ function tiltfixread(O,S){
return psi;
}
function reading() {
function reading(m) {
var d = tiltfixread(CALIBDATA.offset,CALIBDATA.scale);
heading = newHeading(d,heading);
var dir = bearing - heading;
@ -97,18 +98,19 @@ function reading() {
function calibrate(){
var max={x:-32000, y:-32000, z:-32000},
min={x:32000, y:32000, z:32000};
var ref = setInterval(()=>{
var m = Bangle.getCompass();
function onMag(m) {
max.x = m.x>max.x?m.x:max.x;
max.y = m.y>max.y?m.y:max.y;
max.z = m.z>max.z?m.z:max.z;
min.x = m.x<min.x?m.x:min.x;
min.y = m.y<min.y?m.y:min.y;
min.z = m.z<min.z?m.z:min.z;
}, 100);
}
Bangle.on('mag', onMag);
Bangle.setCompassPower(1, "app");
return new Promise((resolve) => {
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 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;
@ -132,6 +134,7 @@ function docalibrate(e,first){
flip1(56,56);
calibrate().then((r)=>{
isCalibrating = false;
require("Storage").write("magnav.json",r);
Bangle.buzz();
CALIBDATA = r;
@ -143,9 +146,13 @@ function docalibrate(e,first){
setTimeout(setButtons,1000);
}
}
if (first===undefined) first=false;
if (first === undefined) first = false;
stopdraw();
clearWatch();
isCalibrating = true;
if (first)
E.showAlert(msg,title).then(action.bind(null,true));
else
@ -153,16 +160,24 @@ function docalibrate(e,first){
}
function startdraw(){
Bangle.setCompassPower(1, "app");
g.clear();
g.setColor(1,1,1);
Bangle.drawWidgets();
candraw = true;
intervalRef = setInterval(reading,500);
if (intervalRef) clearInterval(intervalRef);
intervalRef = setInterval(reading,200);
}
function stopdraw() {
candraw=false;
if(intervalRef) {clearInterval(intervalRef);}
Bangle.setCompassPower(0, "app");
if (intervalRef) {
clearInterval(intervalRef);
intervalRef = undefined;
}
}
function setButtons(){
@ -172,6 +187,7 @@ function setButtons(){
}
Bangle.on('lcdPower',function(on) {
if (isCalibrating) return;
if (on) {
startdraw();
} else {
@ -179,9 +195,8 @@ Bangle.on('lcdPower',function(on) {
}
});
Bangle.on('kill',()=>{Bangle.setCompassPower(0);});
Bangle.loadWidgets();
Bangle.setCompassPower(1);
startdraw();
setButtons();
Bangle.setLCDPower(1);
if (CALIBDATA) startdraw(); else docalibrate({},true);

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

2
apps/binwatch/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.01: start of development
0.02: first running version for BangleJs2

10
apps/binwatch/README.md Normal file
View File

@ -0,0 +1,10 @@
# TheBinWatch
Binary watch to train Your brain
Inspired by the 80's LCD wrist watch from RALtec
## Usage
- swipe to left or right to change displayed text (date, time, ...)
- currently only available for BangeJs2
- Widgets will not be shown
- If bluetooth connection is not established an icon will show up

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("2GwwcCBQ0JkmSpICuoBMNIP4ABH14CCpBAMgRB/IOlJIJkSIOcgIP5BNH2ICDIJ3/AFvkIJsEIOuAIJKSCk4jQ7dt2wCJt4dP/hBc4EAgIOCIJl8EgPyv5BPyBAIgJBCn4GBg4cG/wKBhJBC/ZBLsATByRBM/5BCyRBM/5BMvMkIJ3gDoOSp5BM+RBLhJBCXIIABj4bF+AJBDoOfA4JBLCQMEMoRBPoBBHBZCnFwEAgfJIIftIJPfDYM8IJ3+ILkCCIOTIJnYDYKnCn5BPpBAGF4WSuEHCgRBF/gRBjxBE/pBJcYMBIJ//U4IRBILDjDBYJBJ25BBh4vCk5BXiRxC+BBJ8ARBDQIdBp4JBQZISBhJBQ/IRCkBBFF4WfIJkHII32II++EgN5F4UkHg/8JohBYCAMEIIdJIJWwCYIRDEwJBGkkn/1k85BDkhAEF4f/IJP4CIM8II3+II/wgEDeoZBH/MJQIPJyV/8hBZFgYCBn5BJwEAgSDEyZBF5DDB+f5yUfIIYZBAAQaCKQJBJ4EAgJBH/5BGtgkBiRBK/1CGAPyvhBB/gRCyBBUh5BFCgJBHvgkB+RBEXIJBEJwKDB8mE55BHOIZuBIJH+CIMJIIraB//7IItgCYIRFIIvyiVP8//kkk5//CIZBCF4YVBIJd5IJH/IIvggEHII1PIIfwAwX5kMkzJKBIKnwCIIsFAQOfII4SBghBGFIRBDAwPJGwJBFoAvELIRBIwEAgfJIJPtIIffEgM8IJ0mpAMBIIP+CIVIIKUCQY+TII3YgEB8hBHn5BFmVOIJIvDXgZBG/hSBiRBK/pBD4BBBCIxBIGQKoBIILLBCIQvEIJrdDAQoOBIIe3IIMPIJEnIIlMyfz/JBDAgJBFNYRBI8BBBFg7dEQYYSBhJBIkgmC/0SsmH+V/knPIIsgCgWfIJkHIJn2IIO+IIN5IJsTkknQYRBC/4UGIJYtBghBJpJBE2ATBCJIsD/nJkLMB5MmfYRBHBIRBH/AtBnhBM/xBBwEAgRBPhMn/1JmY2D8hBSgIUGAQk/IIVvIIMeIJWTE4RBC+VJXIZBGSIJBJ4BBBFhJBD//btiWBiRBOHAMnyVkA4aOBIJQnBAARBCh5BLDQXbvgWBOAIUKfwf/LggIFIJt+AQMJIJbgC/dgCYIRLyVPHQoAGIJP+IAcDDhgAkTwhBEAG5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5Bs+EAgFJAoP27dt2wCJB4P8z4DBCJdt34PB5ApBh5BTwEAgfJAoP+IJffIIvtIJYpC5PAIK8CpM/IKHkyZBQ/1JQYMBIKX8CwMSIIX/IJYWCkhBC/pBKt41DnBBXBwILCIJW3IIeSv5BMBoI1CkArBNYRBP8AVBBYMkA4P7IJn5EANPAoJBNGQQrBg5BTg5BE/5BJ35BH+xBJEAQmCFgRBRKwMEDQWfIJ3JEARBO/gmCwAtBIKH4CYM8IIvtIJAWCIIv+IJHfBgPkEwWcIKoLCkmTIJv+EAc/IJQpCIIeQFoMfIJ/AgEBIIeSv///pBHt4gGIIP/IJZoEYwJBSh5BG/5BHBQQgHII+3II2SQYMDIJ3+CQMJDQlPIJgRDAQIHB/ZBJ/ImEoAvBDwRBL+ARBvJBH+xBGCwRBH/5BG35BHpxBTFggCBIJf8IIufIJfJEwlIF4MPIJuAa4IaFIIX+IIvfBIPkIJHtIIocCNA3AIKMCDQ0/II4VCII2TII9vIJKDBgJBM/gQBiQaGBwRBICIpBD/pBHGQgCCnBBRDQ4OC/ZBD25BJyV/IIwHBIJEgGIKtBIJXgB4IsGAQJBJ/JBHp4LBII4mIGIMHIJYOCIJX/IIe/IJv2IIYaCExB0BIJ0EDRGfIJHJII9JIJH8ExGAGYJBK/ANBFhBBD9pBCAoP+CI5BD/xBC74GB8gmIzhBOgIaJyZBEt5BMn5BEGIQmJyBBBj5BJ4BBBQZOSv///pBEDogCFEYRBFExOTYwMDIJcPIJn/IIIECIJv7tu3IJmSQYJBJ/wMBhIaKp5BGCJICBII35ExVAGoIkBII3wBYN5IJv27ZvCIJv/tu/AYPJExVOIJosKAQJBF/hBLz5BCIoRBLpA1Bh5BHwEAgRBO/3fAYPkIJ3tCwQmM4BBZn5tCIJ2TB4PvIJ6DBgJBHAHRB/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5BmwEAgO27dtARIZCkASBCJfbt4SB+YCB/omM4AjBIJMDDRe3IIUngEAjZBLv5uCAYJBM25BBh5BH+AuBmxBN/MkCQMDIJ2Sp4DBQZgiBIJH+BYN2DRW/IIfggEHIJaWCIIf2ExW+GoJXBIJMGIJvJkjZBgBBN/gpBIJuwIJX/aIMADRQPB/wXBzgSB9pBJ74TB8hBD/wmKMYMDCAJBJgJBPyBBBhpBJYgRBCn5BLt5BBj5BJ/AuBjYaJC4oSBgJBMFIRBB/5BJtggBIJs7DRDcBC4lwUgJBI25BFFIRBJvgzBCoRBH/4NBgZBLCgIXBoATBmxBK/IpCkgGB/YmIsBBN8EAg4aIBwRBDpwhBuxBH35BI/4mIDwMHIJsAIJX8IIdICQMGIJXJIIefIJPfIJ38B4MNDQ4NB8hBDpISBhxBHCQP+CIZBD9omG7AeBn5BOjpBPnATBII1vII+TIJP4gEBG4RBJ/+ACAIaGBgQsDAQMgIIMbIJApEAQN///9Ew3AIJ/wgEDDQu3IJEnIIM7IIo3BIJP/EwxBBh5BPgE2II/5IIskCQMDIJARFyVPII+2DgJBO/wRBuwaE34LB5JBG8EAg5BFCQP8IJP2EwmwF4JXCIJ0GIJ+ACYJBE75BJpJBHWYRBO/7XBgAaEJgQsGkmQCQPtII3kIJP+EwhdBgY2EIJkBDQdvIJWTEwMNIIYdCIJE/IItvDQMfIJ/4OAMbIIoUEAQgSBgJBGCI4sDIIdsDQJBQ/4TBnYaCbgRBJuCqBIIW3IJ37EwV8FoI1FIJsDIIosHAQNACYM2IIn5IJEkIItgIKfgCgIaCA4P8IJNOCQN2IIO/CYPJIJf/EwQYBg5BU9pBOpASBgxBBDYRBKz5BD74YBn5BR/gVBhoaBA4PkIJNJCQMAIIf+CJJBDNAPYIK8d2wHCIJc4gEB7dvIJuTIIf4C4I0FIJn/wAWBIIYsJAQMgKoMbIIQmEAQ9///923AIKvwgED25BOk5BBnYxBIJ//2wWBh40GEwpBIgE3AoP5IJckCQMDGIQRLyVPB4O+IKwAz/hWFIP5B88hBFz4SK8EAn4HE4EAgJBjHwYCCyYSKjkAg///xEB/0AAAJKFABXwCYMPIKUSIJsDwEAFII7CQYnxSol/ILP5IIUgIIWSEZAABgFwgEfwBBBwIKC45KECIIdG4H8HwQREIJ0CIJ1+gCGBn/8QATIBF4LRB//4ILfJIISYBIIVPIJV/4BBp/w7CpBBR/BBwhKJCIJYDBINHyIIVAIIoXJIOcBIKAmBIIYyBIIgREIKw4BHYJABIIknChHjAwuPc4ovDCIxBS/hBGgBBMADfwFYJECIJuQIIcEBASDPABUfILHkHAWAIKD1DVQP8gK2DBAMHAoRBHYqJBIgAICz5BLwBBE/0AIL7+CkhAEIIeTIK4FBIMcSILT7BDI5BQ/JBCkBBFgRBByQ4CIKmAZARBW5JBCIApBb/kAGRBBbgBBCp5BM/+BBQXHIIXgJQRBW/w1CpBBKpJBKEwfAgA7CIIMAGoRBjhJBJgYbDEwLCBAAIFBQYTdHAAfwCYJQJ//yIIVAIJbgKOIiDFCZhBagJBRVAoUTAA4yBGoJAHAAJBCk4saACf8IIWQIP5BLggOCIN3kGQWAIJufIPkABwQCyIBUAiRBzkBB/IJsCIOZALIP4ADIOVIIJsJIONAHQwA=="))

381
apps/binwatch/app.js Normal file
View File

@ -0,0 +1,381 @@
/***************************************************
* BINARY WATCH
* for Bangle 1 / 2
* inspired by RAL tec binary wrist watch
*
* TODO:
* - vibrate on full hour
* -
****************************************************/
/* reuqirements */
require("Font7x11Numeric7Seg").add(Graphics);
require("Font5x7Numeric7Seg").add(Graphics);
/* constants and definitions */
/* Bangle 2: 176 x 176 */
/* month images */
var month = [
/* JAN */ {width : 53, height : 24, bpp : 1, buffer : require("heatshrink").decompress(atob("AAMf/0D8AFBkM/9EvwMAgcM/3B30YgE4uEOh354EB4eAuFz90Ah0cgeDx9wgFw8Ecjk7wEDw8A8AIBgEcnEHg4IBgFh4EYnEDHYMF/8AwBID/BODgN/4EgAoI0BgODwExGgkDzg0FAII0D88A8PAnAIBAIMOgPBBAPAiBpCgPAQIOAmFwg0P/B5BwcAiE/JYYAHA"))},
/* FEB */ {width : 51, height : 24, bpp : 1, buffer : require("heatshrink").decompress(atob("v/8n/+g/+if/hP/wM/8c/4Of8Ez/FwgE4gEHgFOAYUA8Ed4ADBgEcge4AYMAgeAu4DCgFwhwQBEIMOgPcAYMAgPAjN/4G/8EX/kf/EP/kB/+F/8C/+Ar/xGQkBGTE7wADBMIMHMotMgEGv+A7/hEYOf/EH/hvBh6FBIIKYFA"))},
/* MAR */ {width : 52, height : 23, bpp : 1, buffer : require("heatshrink").decompress(atob("v/4j/+gf/hP/gV/6FP/HP8kz/cGv8OgHDwFwuEE8McnEHg8A905BgcO8ecBiM4BgMwuEGoeEi/8gX/wE4gH/4Ef/AMFx0QDIcA8BADnEOgIzCufABgk+Bglx+AMEh+OBgdwvnghk4gcGgfsgFDgEQoEeSgvg"))},
/* APR */ {width : 52, height : 23, bpp : 1, buffer : require("heatshrink").decompress(atob("v/4j/4gf/hP/oV/4FP/HP9kz/EGv8OgPDwEguEE8EcnEHg8A9wMCuFwhwMTgAMBmFwg1f+EX/kC/+D/8A//AJIIMFxwZCgFwgAmCgEHnBNDgFz4AMEnwMEuPwBgkPxwMDuF88EMBgMGgfsgFDRgNAjyUF8A="))},
/* MAY */ {width : 52, height : 24, bpp : 1, buffer : require("heatshrink").decompress(atob("v/4j/+AoMJ/8Cv/QAwPP8kz/cEgEugHDwFwsEG8McnEHg8A905BgVwh3jzgMRnAMBmAMBoeEi/8BgNgnEA//Ah/4BgcB/+OiAZCBgPgIARTB90BGYUAhwMahk4gYMBpkAocAiEP+CSDIAOAAwYMB"))},
/* JUN */ {width : 53, height : 24, bpp : 1, buffer : require("heatshrink").decompress(atob("AAcD8ADBkMAhEvwIJBhkA4O+jEAnFwh0O/PAgPDwFwufugEOjkDwePuEAuHgjkcneAgeHgHgBAMAjk4g8HBAMAsPAjE4gY7BggCBwBPLkACBGgMBweAmI0EgecGgoBBGgfngHh4E4BAIBBh0B4IIB4EQmEEBAPA/0An5qBg0P/ED/xNBiAKBh6PCAAw="))},
/* JUL */ {width : 53, height : 24, bpp : 1, buffer : require("heatshrink").decompress(atob("AA8hgEImAFBgcMgHB4AGBnFwh0OAoMB4eAuFwAwMOjkDweAAwNw8EcjggCw8A8HgAwMcnEHg40CsPAjE4AwUEAQIgCABMgGgcBGgMBGgo/BGggKBGgYBB8PAnA0BBQMOgJpC4EQmEENIX+gE/wFn/EP/ED/0Cv/gBQMP8EP/5QGA"))},
/* AUG */ {width : 52, height : 24, bpp : 1, buffer : require("heatshrink").decompress(atob("n/8AocE/+gAgMP/1n+0QgGA//HgHhwEYl/wuEOjkDw8Ag4MB4E4uEABilhBgcv/EcgOCgEB/+AwBBB/AMBAgMCj/ngFgAwNw/wmCgImBBgIzDhwzFBikGhkBgUAs0AkEf4EH+A3Bgf+gBLBAwIMD"))},
/* SEP */ {width : 51, height : 23, bpp : 1, buffer : require("heatshrink").decompress(atob("h/4j//g/+gf/wn/4M/8ABB5/wmf4mEAjkAg8Ap0AgeAgHgjvAgFwBwMD3EAhwOCu8AgIOCh3ggE4BwMB7gjCBwMYv/Ar/wi/8j/8IYMB/+BIYIODDwIyCLIMHGQYGB8JBDB4IyCAoMDw5BDB4JBDgEEMoZ6Cn/A8A6B8FP/kYgEf/EH/4eCA"))},
/* OCT */ {width : 50, height : 23, bpp : 1, buffer : require("heatshrink").decompress(atob("h/gg/+j//w/8gf/h//+H+gF/wP//OAkHADAXgjlwAoU4g8cAgMYh0B44pCgeAuIYBgfADAnwnEDDAUcghCDgRMIsACBkAYFGKZKDngYFgJjBwAYCPgX4DAMHPgQYBgB8C8EGgAA="))},
/* NOV */ {width : 51, height : 24, bpp : 1, buffer : require("heatshrink").decompress(atob("vkAgf4AoMX4GA/+ABIN8mEP8EggP350MgMGgF+vvDwFw8Ef4+4uEOjgiBu8OgIOBv8A8PAnFwEQMcnEHBwP8gOHgFh4EdHYNAgEQgJLFggFEhPAjFwg0cg4jDGQPnGQk8GQkPI4IyB8PDKwYOB+BWBMoMHnkOgHAn+A98BwEIh/4jnAHgX+gaGBAAcggAA=="))},
/* DEC */ {width : 49, height : 23, bpp : 1, buffer : require("heatshrink").decompress(atob("v/gj//gP/5/4iYFC2f4hn/CAOcgMHgEBwEOgPDwEB4AJB8PAgHggeAuHggFwBoM4uEAnANBjgDBjgNBgwDBh0AiEAgowBAAQ6BwEAggFBv/BwAwBsIwWhwwDnEHAYIiBjhhDgEN/0Dn/Aj/hO4M/+Ef/JABv/8g/+A=="))},
/* MAI */ {width : 44, height : 23, bpp : 1, buffer : require("heatshrink").decompress(atob("v/4j/+gEJ/8Cv/QgnP8kz/cA50A4eAuEc8McnEHgPOnIKD8ecBR04BQMwhlDwkX/kAoE4gH/4EABQlOiAVD8A2EgIrDBS0MnEDgHMGQMAiEEPwo="))},
/* OKT */ {width : 51, height : 24, bpp : 1, buffer : require("heatshrink").decompress(atob("g/wAQMP//B/8DgPh//8j/AuF8n//jECh0fDAUA8PH4AGB8EcnIhBsEcgeHvkAj0DwFw98AgYjBh0dDAN4h0A4eAEgQDBl/4gFAE4MD/5OE3/ggIyBhk4gcAuAyCBIIyDIIIyDAgOAGQMBGQNwh8B4E4BwMB8BlCBIM8gF/AgMYg+Aj/wmA3B+EB/hBChiYGA"))},
/* DEZ */ {width : 51, height : 23, bpp : 1, buffer : require("heatshrink").decompress(atob("n/wh//w//xP/gV/8F//Of4Fn/EH/04gUODAUHgHh4AFBnHgjk4BYUcgeHAoMB8eAuHgAwN4uEOjgFBh4jB4eAgED4ADBl/4gFwB4MD/4DBgQCB3/gC4PghgyBgPAGQl4gYyDjwgBGQQrBh0BGQVwDQM4F4MMLIJlEg3/gOfPAPgn/gk/+j/+h/8IoPh//gA="))}
];
var imgSquid = {width : 88, height : 26, bpp : 1, buffer : require("heatshrink").decompress(atob("gE/AYUYgEH////0B//gBQM8BQgDB/AKHh/A/gKBvwKBAgMOj8AnwKHBAIMBgH/BQgmCAoPnBQl4AoOAgPnwAKDuEAgYKB4YKIgfD4AKDMAMB4EDwIKIg+B8AKIgAKIh8A+AKHh0AuAKHj0AvBMG4EcgE4K458Bnh4HnEAjiOHBwMeBQpKBEgMOXQ/wBwIKDaAZQBg4KDcwT0BAAOHfgoKHgE/wDaBAAL8DA="))};
var imgNoBT = {width : 20, height : 20, bpp : 3, transparent : 0, buffer : require("heatshrink").decompress(atob("///8mSpM/AoP/yUT/8yuYGB5AMB/1MyYUBkmT/P85MP+USBwOT8mQ/8JBwXyoVnyGSv8//Mhk14pMn//8BYNMwmSp/+pFJkgyBDoMkkgODpOSuQOE5M/KgIOCsmfz/JknPhMyof5n+Ss/wzMhn4OBk1+smQLoWTn/mHAM/+VJz4KBwhZBEYJ/CkM8yZVBAAQxBCgP/A="))};
const V2_X_STEP = 26;
const V2_Y_STEP = 34;
const V2_TIME_Y_OFFSET = 8;
const V2_HX = 36;
const V2_HY = 0 + V2_TIME_Y_OFFSET;
const V2_MX = 10;
const V2_MY = 51 + V2_TIME_Y_OFFSET;
const V2_SX = 10;
const V2_SY = 95 + V2_TIME_Y_OFFSET;
const V2_BT_X = 137; /* 145, 35 */
const V2_BT_Y = 20;
const V2_DX = 100;
const V2_DY = 141;
const V2_BAT_POS_X = 21;
const V2_BAT_POS_Y = 40;
const V2_BAT_SIZE_X = 13;
const V2_BAT_SIZE_Y = 2;
const V2_SCREEN_SIZE_X = 176;
const V2_SCREEN_SIZE_Y = 176;
const V2_BACKGROUND_IMAGE = "Background176_center.png";
const V2_BG_COLOR = 0;
const V2_FG_COLOR = 1;
/* Bangle 1: 240 x 240 */
const V1_X_STEP = 35;
const V1_Y_STEP = 46;
const V1_TIME_Y_OFFSET = 41;
const V1_HX = 48;
const V1_HY = 0 + V1_TIME_Y_OFFSET;
const V1_MX = 14;
const V1_MY = 55 + V1_TIME_Y_OFFSET;
const V1_SX = 14;
const V1_SY = 110 + V1_TIME_Y_OFFSET;
const V1_BT_X = 41;
const V1_BT_Y = 14;
//var BT_X = 20, BT_Y = 14;
const V1_DX = 160;
const V1_DY = 205;
const V1_BAT_POS_X = 175;
const V1_BAT_POS_Y = 21;
const V1_BAT_SIZE_X = 3;
const V1_BAT_SIZE_Y = 5;
const V1_SCREEN_SIZE_X = 240;
const V1_SCREEN_SIZE_Y = 240;
const V1_BACKGROUND_IMAGE = "Background240_center.png";
const V1_BG_COLOR = 1;
const V1_FG_COLOR = 0;
/* runtime settings */
var x_step = 0;
var y_step = 0;
var time_y_offset = 0;
var hx = 0, hy = 0;
var mx = 0, my = 0;
var sx = 0, sy = 0;
var bt_x = 0, bt_y = 0;
var dx = 0, dy = 0;
var bat_pos_x, bat_pos_y, bat_size_x, bat_size_y;
var backgroundImage = "";
var screen_size_x = 0;
var screen_size_y = 0;
var bg_color = 0;
var fg_color = 1;
/* global variables */
var showDateTime = 2; /* show noting, time or date */
var cg;
var cgimg;
/* local functions */
/**
* function drawSquare(...)
*
* go through all bits and draw a square if a bit
* is set. So we get the binary representation
* of the value
* used to draw block for hours, mintutes, seconds, date
*
* @param gfx: graphic object to use
* @param x: x-coordinate of 1st the square
* @param y: y-coordinate of 1st the square
* @param data: data conatining the bit information
* @param numOfBits: number of bits to draw
*/
function drawSquare(gfx, x, y, data, numOfBits) {
for(i = numOfBits; i > 0 ; i--) {
if( (data & 1) != 0) {
gfx.fillRect(x + (i - 1) * x_step, y,
x + i * x_step , y + y_step);
}
data >>= 1; /* shift one bit right */
}
}
/**
* function drawBinary(...)
* draw the time in binary format
* default display for geeks and real men
* @param h: hours
* @param m: minutes
* @param s: seconds
*/
function drawBinary(gfx, hour, minute, second) {
gfx.clear(0);
if(hour > 12) {
hour -= 12; /* we use for bit for hours so we only display 12 hours*/
}
drawSquare(gfx, hx, hy, hour, 4); /* set hour */
drawSquare(gfx, mx, my, minute, 6); /* set minute */
drawSquare(gfx, sx, sy, second, 6); /* set second */
}
/**
* function drawTime(...)
* show time under the graphic
* for wimps and commies
*
* @param h: hours
* @param m: minutes
* @param s: seconds
*/
function drawTime(gfx, h, m, s) {
var time = (" "+h).substr(-2) + ":" + ("0"+m).substr(-2)+ ":" + ("0"+s).substr(-2);
gfx.setFontAlign(0,-1); // align right bottom
gfx.setFont("7x11Numeric7Seg", 2);
gfx.drawString(time, gfx.getWidth() / 2, dy + 1, false /*clear background*/);
}
/**
* function drawDate(...)
* show date under the graphic
* (optionally)
*
* @param gfx: graphic object to use
* @param d: date object
*/
var vMonth = 0;
function drawDate(gfx, d) {
var dateString = ""
+ ("0" + d.getDate()).substr(-2)
// + " "
// + ("0" + d.getMonth()).substr(-2)
// + " "
// + ("0" + d.getFullYear()).substr(-2)
;
gfx.setFontAlign(-1,-1); // align right bottom
gfx.setFont("7x11Numeric7Seg",2); /* draw the current time font */
gfx.drawString(dateString, dx, dy + 1, false /* don't clear background*/);
gfx.drawImage(month[d.getMonth()], 40, dy);
}
function toggleDateTime() {
showDateTime++;
if(showDateTime > 2){
showDateTime = 0;
}
}
function updateVTime() {
vMonth++;
if(vMonth >= 12 + 3) {
vMonth = 0;
}
second++;
if(second > 59) {
second = 0;
minute++;
if(minute > 59) {
minute = 0;
hour++;
if(hour > 12) {
hour = 0;
}
}
}
}
/**
* function drawBattery(...)
* fill the battery symbol with blocks
* according to the battery level
*
* @param gfx: graphic object
* @param level: current battery level
*/
function drawBattery(gfx, level) {
var pos_y = bat_pos_y - 1;
var stepLevel = Math.round((level + 10) / 20);
for(i = 0; i < stepLevel; i++) {
pos_y -= bat_size_y + 2;
gfx.fillRect(bat_pos_x, pos_y,
bat_pos_x + bat_size_x, pos_y + bat_size_y);
}
}
/**
* function drawBattery(...)
* fill the battery symbol with blocks
* according to the battery level
*
* @param gfx: graphic object
* @param level: current battery level
*/
function drawBT(gfx, status) {
if(!status) {
gfx.drawImage(imgNoBT, bt_x, bt_y);
}
}
function setRuntimeValues(resolution) {
if(240 == resolution) {
x_step = V1_X_STEP;
y_step = V1_Y_STEP;
time_y_offset = V1_TIME_Y_OFFSET;
hx = V1_HX;
hy = V1_HY;
mx = V1_MX;
my = V1_MY;
sx = V1_SX;
sy = V1_SY;
bt_x = V1_BT_X;
bt_y = V1_BT_Y;
dx = V1_DX;
dy = V1_DY;
screen_size_x = V1_SCREEN_SIZE_X;
screen_size_y = V1_SCREEN_SIZE_Y;
backgroundImage = V1_BACKGROUND_IMAGE;
bat_pos_x = V1_BAT_POS_X;
bat_pos_y = V1_BAT_POS_Y;
bat_size_x = V1_BAT_SIZE_X;
bat_size_y = V1_BAT_SIZE_Y;
setWatch(toggleDateTime, BTN1, { repeat : true, edge: "falling"});
} else {
x_step = V2_X_STEP;
y_step = V2_Y_STEP;
time_y_offset = V2_TIME_Y_OFFSET;
hx = V2_HX;
hy = V2_HY;
mx = V2_MX;
my = V2_MY;
sx = V2_SX;
sy = V2_SY;
bt_x = V2_BT_X;
bt_y = V2_BT_Y;
dx = V2_DX;
dy = V2_DY;
screen_size_x = V2_SCREEN_SIZE_X;
screen_size_y = V2_SCREEN_SIZE_Y;
backgroundImage = V2_BACKGROUND_IMAGE;
bat_pos_x = V2_BAT_POS_X;
bat_pos_y = V2_BAT_POS_Y;
bat_size_x = V2_BAT_SIZE_X;
bat_size_y = V2_BAT_SIZE_Y;
Bangle.on('swipe', function(direction) { toggleDateTime(direction);});
}
cg = Graphics.createArrayBuffer(
screen_size_x,screen_size_y, 1, {msb:true});
cgimg = {width:screen_size_x, height:screen_size_y, bpp:1,
transparent:0, buffer:cg.buffer};
}
var hour = 0, minute = 1, second = 50;
var batVLevel = 20;
function draw() {
var d = new Date();
var h = d.getHours(), m = d.getMinutes(), s = d.getSeconds();
g.reset();
drawBinary(cg, h, m, s);
switch(showDateTime) {
case 1:
drawTime(cg, h, m, s);
break;
case 2:
drawDate(cg, d);
break;
default:
cg.drawImage(imgSquid, cg.getWidth() / 2 - 44, dy);
}
drawBattery(cg, /*batVLevel*/ E.getBattery());
batVLevel += 2;
if(batVLevel > 100) {
batVLevel = 0;
}
updateVTime();
g.clear();
g.drawImages([{image:cgimg},
{image:require("Storage").read(backgroundImage)},
// { x:bt_x, y:bt_y, rotate: 0, image:require("Storage").read("bt-icon.png")},
]);
drawBT(g, NRF.getSecurityStatus().connected);
// Bangle.drawWidgets();
const millis = d.getMilliseconds();
setTimeout(draw, 1000-millis);
// Bangle.loadWidgets();
}
// Show launcher when button pressed
Bangle.setUI("clock");
setRuntimeValues(g.getWidth());
g.reset().clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
draw();

BIN
apps/binwatch/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

BIN
apps/binwatch/bt-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 708 B

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

@ -31,3 +31,12 @@
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.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. */
E.showMessage("Updating boot0...");
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 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}) { eval(require('Storage').read('bootupdate.js')); throw "Storage Updated!"}\n`;
if (require('Storage').hash) { // new in 2v11 - helps ensure files haven't changed
var CRC = E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\.boot\.js/);
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 += `var bleServices = {}, bleServiceOptions = { uart : true};\n`;
if (s.ble!==false) {
if (s.HID) { // Human interface device
if (s.HID=="joy") boot += `Bangle.HID = E.toUint8Array(atob("BQEJBKEBCQGhAAUJGQEpBRUAJQGVBXUBgQKVA3UBgQMFAQkwCTEVgSV/dQiVAoECwMA="));`;
else if (s.HID=="kb") boot += `Bangle.HID = E.toUint8Array(atob("BQEJBqEBBQcZ4CnnFQAlAXUBlQiBApUBdQiBAZUFdQEFCBkBKQWRApUBdQORAZUGdQgVACVzBQcZAClzgQAJBRUAJv8AdQiVArECwA=="));`
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
@ -48,7 +55,7 @@ boot += `E.setTimeZone(${s.timezone});`;
if (!Bangle.F_BEEPSET) {
if (!s.vibrate) boot += `Bangle.buzz=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) {
if ((0|freq)<=0) freq=4000;
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.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
delete g.theme; // deleting stops us getting confused by our own decl. builtins can't be deleted
if (!g.theme) {
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
boot += `Bangle.setUI=function(mode, cb) {
if (Bangle.btnWatches) {
@ -94,7 +103,7 @@ if (Bangle.swipeHandler) {
Bangle.removeListener("swipe", Bangle.swipeHandler);
delete Bangle.swipeHandler;
}
if (Bangle.touchandler) {
if (Bangle.touchHandler) {
Bangle.removeListener("touch", Bangle.touchHandler);
delete Bangle.touchHandler;
}
@ -131,6 +140,12 @@ else if (mode=="updown") {
throw new Error("Unknown UI mode");
};\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]};
@ -141,15 +156,18 @@ if (!g.imageMetrics) { // added in 2v11 - this is a limited functionality polyfi
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) {
return {width:this.stringWidth(txt), height:this.getFontHeight()};
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")) {
for (var unwrappedLine of str.split("\\n")) {
var words = unwrappedLine.split(" ");
var line = words.shift();
for (var word of words) {
@ -165,14 +183,23 @@ if (!g.wrapString) { // added in 2v11 - this is a limited functionality polyfill
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
// These could change bleServices/bleServiceOptions if needed
require('Storage').list(/\.boot\.js/).forEach(bootFile=>{
// we add a semicolon so if the file is wrapped in (function(){ ... }()
// with no semicolon we don't end up with (function(){ ... }()(function(){ ... }()
// 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);
delete boot;
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

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.12: added support for different locales
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 functionMode = NONE_FN_MODE;
let textCol = g.theme.dark ? "#0f0" : "#080";
function drawAll(){
updateTime();
updateRest(new Date());
@ -45,9 +47,7 @@ function writeLineStart(line){
function writeLine(str,line){
var y = marginTop+line*fontheight;
g.setFont("6x8",fontsize);
//g.setColor(0,1,0);
g.setColor("#0f0");
g.setFontAlign(-1,-1);
g.setColor(textCol).setFontAlign(-1,-1);
g.clearRect(0,y,((str.length+1)*20),y+fontheight-1);
writeLineStart(line);
g.drawString(str,25,y);
@ -56,7 +56,7 @@ function writeLine(str,line){
function drawInfo(line) {
let val;
let str = "";
let col = "#0f0"; // green
let col = textCol; // green
//console.log("drawInfo(), infoMode=" + infoMode + " funcMode=" + functionMode);
@ -64,7 +64,7 @@ function drawInfo(line) {
case NONE_FN_MODE:
break;
case HRT_FN_MODE:
col = "#0ff"; // cyan
col = g.theme.dark ? "#0ff": "#088"; // cyan
str = "HRM: " + (hrtOn ? "ON" : "OFF");
drawModeLine(line,str,col);
return;
@ -72,7 +72,7 @@ function drawInfo(line) {
switch(infoMode) {
case NONE_MODE:
col = "#fff";
col = g.theme.bg;
str = "";
break;
case HRT_MODE:
@ -104,9 +104,8 @@ function drawModeLine(line, str, col) {
g.setColor(col);
var y = marginTop+line*fontheight;
g.fillRect(0, y, 239, y+fontheight-1);
g.setColor(0);
g.setFontAlign(0, -1);
g.drawString(str, g.getWidth()/2, y);
g.setColor(g.theme.bg).setFontAlign(0, 0);
g.drawString(str, g.getWidth()/2, y+fontheight/2);
}
function changeInfoMode() {
@ -193,7 +192,7 @@ Bangle.on('lcdPower',function(on) {
var click = setInterval(updateTime, 1000);
// Show launcher when button pressed
Bangle.setUI("clockupdown", btn=>{
if (btn==0) changeInfoMode();
if (btn==1) changeFunctionMode();
if (btn<0) changeInfoMode();
if (btn>0) changeFunctionMode();
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.02: Show text if uncalibrated
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 timg = {
width:tg.getWidth(),
height:tg.getHeight(),
bpp:1,
buffer:tg.buffer
};
var ag = Graphics.createArrayBuffer(160,160,2,{msb:true});
var W = g.getWidth();
var M = W/2; // middle of screen
// Angle buffer
var AGS = W > 200 ? 160 : 120; // buffer size
var AGM = AGS/2; // midpoint/radius
var AGH = AGM-10; // hand size
var ag = Graphics.createArrayBuffer(AGS,AGS,2,{msb:true});
var aimg = {
width:ag.getWidth(),
height:ag.getHeight(),
bpp:2,
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.fillCircle(80,80,79,79);
ag.setColor(0);
ag.fillCircle(80,80,69,69);
ag.setColor(1).fillCircle(AGM,AGM,AGM-1,AGM-1);
ag.setColor(0).fillCircle(AGM,AGM,AGM-11,AGM-11);
function arrow(r,c) {
r=r*Math.PI/180;
var p = Math.PI/2;
ag.setColor(c);
ag.fillPoly([
80+60*Math.sin(r), 80-60*Math.cos(r),
80+10*Math.sin(r+p), 80-10*Math.cos(r+p),
80+10*Math.sin(r-p), 80-10*Math.cos(r-p),
ag.setColor(c).fillPoly([
AGM+AGH*Math.sin(r), AGM-AGH*Math.cos(r),
AGM+10*Math.sin(r+p), AGM-10*Math.cos(r+p),
AGM+10*Math.sin(r-p), AGM-10*Math.cos(r-p),
]);
}
var wasUncalibrated = false;
var oldHeading = 0;
Bangle.on('mag', function(m) {
if (!Bangle.isLCDOn()) return;
tg.clear();
tg.setFont("6x8",1);
tg.setColor(1);
g.reset();
if (isNaN(m.heading)) {
tg.setFontAlign(0,-1);
tg.setFont("6x8",1);
tg.drawString("Uncalibrated",60,4);
tg.drawString("turn 360° around",60,12);
if (!wasUncalibrated) {
g.clearRect(0,24,W,48);
g.setFontAlign(0,-1).setFont("6x8");
g.drawString("Uncalibrated\nturn 360° around",M,24+4);
wasUncalibrated = true;
}
else {
tg.setFontAlign(0,0);
tg.setFont("6x8",2);
tg.drawString(Math.round(m.heading),60,12);
} else {
if (wasUncalibrated) {
g.clearRect(0,24,W,48);
wasUncalibrated = false;
}
g.drawImage(timg,0,0,{scale:2});
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);
}
ag.setColor(0);
arrow(oldHeading,0);
arrow(oldHeading+180,0);
arrow(m.heading,2);
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;
});
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
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

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

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

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

View File

@ -27,7 +27,7 @@ function draw() {
const hour = d02(now.getHours() - (is12Hour && now.getHours() > 12 ? 12 : 0));
const minutes = d02(now.getMinutes());
const day = d02(now.getDay());
const day = d02(now.getDate());
const month = d02(now.getMonth() + 1);
const year = now.getFullYear();

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

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

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

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

BIN
apps/fwupdate/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

286
apps/fwupdate/custom.html Normal file
View File

@ -0,0 +1,286 @@
<html>
<head>
<link rel="stylesheet" href="../../css/spectre.min.css">
</head>
<body>
<p><b>THIS IS CURRENTLY BETA - PLEASE USE THE NORMAL FIRMWARE UPDATE
INSTRUCTIONS FOR <a href="https://www.espruino.com/Bangle.js#firmware-updates" target="_blank">BANGLE.JS</a> 1 AND <a href="https://www.espruino.com/Bangle.js2#firmware-updates" target="_blank">BANGLE.JS 2</a></b></p>
<div id="fw-unknown">
<p>Firmware updates using the App Loader are only possible on
Bangle.js 2. For firmware updates on Bangle.js 1 please
<a href="https://www.espruino.com/Bangle.js#firmware-updates" target="_blank">see the Bangle.js 1 instructions</a></p>
</div>
<div id="fw-ok" style="display:none">
<p>Please upload a hex file here. This file should be the <code>.app_hex</code>
file, *not* the normal <code>.hex</code> (as that contains the bootloader as well).</p>
<input class="form-input" type="file" id="fileLoader" accept=".hex,.app_hex"/><br>
<p><button id="upload" class="btn btn-primary">Upload</button></p>
</div>
<pre id="log"></pre>
<script src="../../core/lib/customize.js"></script>
<script>
var hex;
var hexJS; // JS to upload hex
var HEADER_LEN = 16; // size of app flash header
var MAX_ADDRESS = 0x1000000; // discount anything in hex file above this
var VERSION = 0x12345678; // VERSION! Use this to test firmware in JS land
var DEBUG = false;
function log(t) {
document.getElementById('log').innerText += t+"\n";
console.log(t);
}
function onInit(device) {
console.log(device);
if (device && device.id=="BANGLEJS2") {
document.getElementById("fw-unknown").style = "display:none";
document.getElementById("fw-ok").style = "";
}
}
function checkForFileOnServer() {
/*function getURL(url, callback) {
var xhr = new XMLHttpRequest();
xhr.onload = callback;
baseURL = url;
xhr.open("GET", baseURL);
xhr.responseType = "document";
xhr.send();
}
function getFilesFromURL(url, regex, callback) {
getURL(url, function() {
var files = [];
var elements = this.responseXML.getElementsByTagName("a");
for (var i=0;i<elements.length;i++) {
var href = elements[i].href;
if (regex.exec(href)) {
files.push(href);
}
}
callback(files);
});
}
var regex = new RegExp("_bangle2");
var domFirmware = document.getElementById("latest-firmware");
getFilesFromURL("https://www.espruino.com/binaries/", regex, function(releaseFiles) {
releaseFiles.sort().reverse().forEach(function(f) {
var name = f.substr(f.substr(0,f.length-1).lastIndexOf('/')+1);
domFirmware.innerHTML += 'Release: <a href="'+f+'">'+name+'</a><br/>';
});
getFilesFromURL("https://www.espruino.com/binaries/travis/master/",regex, function(travisFiles) {
travisFiles.forEach(function(f) {
var name = f.substr(f.lastIndexOf('/')+1);
domFirmware.innerHTML += 'Cutting Edge build: <a href="'+f+'">'+name+'</a><br/>';
});
document.getElementById("checking-server").style = "display:none";
document.getElementById("main-ui").style = "";
});
});*/
}
function downloadFile() {
/*response = await fetch(APP_HEX_PATH+"readlink.php?link="+APP_HEX_FILE, {
method: 'GET',
cache: 'no-cache',
});
if (response.ok) {
blob = await response.blob();
data = await blob.text();
document.getElementById("latest-firmware").innerHTML="(<b>"+data.toString()+"</b>)";
}*/
}
function handleFileSelect(event) {
if (event.target.files.length!=1) {
log("More than one file selected!");
return;
}
var reader = new FileReader();
reader.onload = function(event) {
hex = event.target.result.split("\n");
document.getElementById("upload").style = ""; // show upload
fileLoaded();
};
reader.readAsText(event.target.files[0]);
};
function parseLines(dataCallback) {
var addrHi = 0;
hex.forEach(function(hexline) {
if (DEBUG) console.log(hexline);
var bytes = hexline.substr(1,2);
var addrLo = parseInt(hexline.substr(3,4),16);
var cmd = hexline.substr(7,2);
if (cmd=="02") addrHi = parseInt(hexline.substr(9,4),16) << 4; // Extended Segment Address
else if (cmd=="04") addrHi = parseInt(hexline.substr(9,4),16) << 16; // Extended Linear Address
else if (cmd=="00") {
var addr = addrHi + addrLo;
var data = [];
for (var i=0;i<16;i++) data.push(parseInt(hexline.substr(9+(i*2),2),16));
dataCallback(addr,data);
}
});
}
function CRC32(data) {
var crc = 0xFFFFFFFF;
data.forEach(function(d) {
crc^=d;
crc=(crc>>>1)^(0xEDB88320&-(crc&1));
crc=(crc>>>1)^(0xEDB88320&-(crc&1));
crc=(crc>>>1)^(0xEDB88320&-(crc&1));
crc=(crc>>>1)^(0xEDB88320&-(crc&1));
crc=(crc>>>1)^(0xEDB88320&-(crc&1));
crc=(crc>>>1)^(0xEDB88320&-(crc&1));
crc=(crc>>>1)^(0xEDB88320&-(crc&1));
crc=(crc>>>1)^(0xEDB88320&-(crc&1));
});
return (~crc)>>>0; // >>>0 converts to unsigned 32-bit integer
}
function btoa(input) {
var b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
var out = "";
var i=0;
while (i<input.length) {
var octet_a = 0|input[i++];
var octet_b = 0;
var octet_c = 0;
var padding = 0;
if (i<input.length) {
octet_b = 0|input[i++];
if (i<input.length) {
octet_c = 0|input[i++];
padding = 0;
} else
padding = 1;
} else
padding = 2;
var triple = (octet_a << 0x10) + (octet_b << 0x08) + octet_c;
out += b64[(triple >> 18) & 63] +
b64[(triple >> 12) & 63] +
((padding>1)?'=':b64[(triple >> 6) & 63]) +
((padding>0)?'=':b64[triple & 63]);
}
return out;
}
// To upload the app, we write to external flash
function createJS_app(binary, bin32, startAddress, endAddress, HEADER_LEN) {
/* typedef struct {
uint32_t address;
uint32_t size;
uint32_t CRC;
uint32_t version;
} FlashHeader; */
bin32[0] = startAddress;
bin32[1] = endAddress - startAddress;
bin32[2] = CRC32(new Uint8Array(binary.buffer, HEADER_LEN));
bin32[3] = VERSION; // VERSION! Use this to test ourselves
console.log("CRC 0x"+bin32[2].toString(16));
hexJS = "";//`\x10if (E.CRC32(E.memoryArea(${startAddress},${endAddress-startAddress}))==${bin32[2]}) { print("FIRMWARE UP TO DATE!"); load();}\n`;
hexJS += '\x10var s = require("Storage");\n';
var CHUNKSIZE = 1024;
for (var i=0;i<binary.length;i+=CHUNKSIZE) {
var l = binary.length-i;
if (l>CHUNKSIZE) l=CHUNKSIZE;
var chunk = btoa(new Uint8Array(binary.buffer, i, l));
hexJS += `\x10s.write('.firmware', atob("${chunk}"), 0x${i.toString(16)}, ${binary.length});\n`;
}
hexJS += '\x10setTimeout(()=>E.showMessage("Rebooting..."),50);\n';
hexJS += '\x10setTimeout(()=>E.reboot(), 1000);\n';
}
// To upload the bootloader, we write to internal flash, right over bootloader
function createJS_bootloader(binary, startAddress, endAddress) {
var crc = CRC32(binary);
console.log("CRC 0x"+crc.toString(16));
hexJS = `\x10if (E.CRC32(E.memoryArea(${startAddress},${endAddress-startAddress}))==${crc}) { print("BOOTLOADER UP TO DATE!"); load();}\n`;
hexJS += `\x10var _fw = new Uint8Array(${binary.length})\n`;
var CHUNKSIZE = 1024;
for (var i=0;i<binary.length;i+=CHUNKSIZE) {
var l = binary.length-i;
if (l>CHUNKSIZE) l=CHUNKSIZE;
var chunk = btoa(new Uint8Array(binary.buffer, binary.byteOffset+i, l));
hexJS += '\x10_fw.set(atob("'+chunk+'"), 0x'+(i).toString(16)+');\n';
}
// hexJS += `\x10(function() {
// if (E.CRC32(_fw)!=${crc}) throw "Invalid CRC!";
// var f = require("Flash");
// for (var i=${startAddress};i<${endAddress};i+=4096) f.erasePage(i);
// f.write(_fw,${startAddress});
// E.reboot();
// })();\n`;
hexJS += `\x10if (E.CRC32(_fw)!=${crc}) throw "Invalid CRC: 0x"+E.CRC32(_fw).toString(16);\n`;
hexJS += '\x10var f = require("Flash");\n';
for (var i=startAddress;i<endAddress;i+=4096)
hexJS += '\x10f.erasePage(0x'+i.toString(16)+');\n';
hexJS += `\x10f.write(_fw,${startAddress});\n`;
// hexJS += '\x10setTimeout(()=>E.showMessage("Rebooting..."),50);\n';
// hexJS += '\x10setTimeout(()=>E.reboot(), 2000);\n';
}
function fileLoaded() {
// Work out addresses
var startAddress, endAddress = 0;
parseLines(function(addr, data) {
if (addr>MAX_ADDRESS) return; // ignore data out of range
if (startAddress === undefined || addr<startAddress)
startAddress = addr;
var end = addr + data.length;
if (end > endAddress)
endAddress = end;
});
console.log(`// Data from 0x${startAddress.toString(16)} to 0x${endAddress.toString(16)} (${endAddress-startAddress} bytes)`);
// Work out data
var HEADER_LEN = 16;
var binary = new Uint8Array(HEADER_LEN + endAddress-startAddress);
binary.fill(0); // actually seems to assume a block is filled with 0 if not complete
var bin32 = new Uint32Array(binary.buffer);
parseLines(function(addr, data) {
if (addr>MAX_ADDRESS) return; // ignore data out of range
var binAddr = HEADER_LEN + addr - startAddress;
binary.set(data, binAddr);
if (DEBUG) console.log("i",addr.toString(16).padStart(8,0), data.map(x=>x.toString(16).padStart(2,0)).join(" "));
//console.log("o",new Uint8Array(binary.buffer, binAddr, data.length));
});
if (startAddress == 0xf7000) {
console.log("Bootloader - Writing to internal flash");
createJS_bootloader(new Uint8Array(binary.buffer, HEADER_LEN), startAddress, endAddress);
} else {
console.log("App - Writing to external flash");
createJS_app(binary, bin32, startAddress, endAddress);
}
}
function handleUpload() {
if (!hexJS) {
log("Hex file not loaded!");
return;
}
sendCustomizedApp({
storage:[
{name:"RAM", content:hexJS},
]
});
}
document.getElementById('fileLoader').addEventListener('change', handleFileSelect, false);
document.getElementById("upload").addEventListener("click", handleUpload);
checkForFileOnServer();
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -1,2 +1,3 @@
0.03: Fix time output on new firmwares when no GPS time set (fix #104)
0.04: Fix shown UTC time zone sign
0.05: Use new 'layout library for Bangle2, fix #764 by adding a back button

View File

@ -1 +1 @@
require("heatshrink").decompress(atob("mEwghC/AH8A1QWVhWq0AuVAAIuVAAIwT1WinQwTFwMzmQwTCYMjlUqGCIuBlWi0UzC6JdBIoMjC4UDmAuOkYXBPAWgmczLp2ilUiVAUDC4IwLFwIUBLoJ2BFwQwM1WjCgJ1DFwQwLFwJ1B0SQCkQWDGBQXBCgK9BDgKQBAAgwJOwUzRgIDBC54wCkZdGPBwACRgguDBIIwLFxEJBQIwLFxGaBYQwKFxQwLgAWGmQuBcAQwJC48ifYYwJgUidgsyC4L7DGBIXBdohnBCgL7BcYIXIGAqMCIoL7DL5IwERgIUBLoL7BO5QXBGAK7DkWiOxQXGFwOjFoUyFxZhDgBdCCgJ1CCxYxCgBABkcqOwIuNGAQXC0S9BLpgAFXoIwBmYuPAAYwCLp4wHFyYwDFyYwDFygwCCyoA/AFQA="))
require("heatshrink").decompress(atob("mEw4UA////G161hyd8Jf4ALlQLK1WABREC1WgBZEK32oFxPW1QuJ7QwIFwOqvQLHhW31NaBY8qy2rtUFoAuG3W61EVqALF1+qr2gqtUHQu11dawNVqo6F22q9XFBYIwEhWqz2r6oLBGAheBqwuBBYx2CFwQLGlWqgoLCMAsKLoILChR6EgQuDqkqYYsBFweqYYoLDoWnYYoLD/WVYYv8FwXqPoIwEn52BqGrPoILEh/1FwOl9SsBBYcD/pdB2uq/QvEh/8LoOu1xHFh8/gGp9WWL4oMBgWltXeO4owBgWt1ReFYYh2GYYmXEQzDD3wiHegYKIGAJRGAAguJAH4AC"))

View File

@ -1,68 +1,75 @@
var img = require("heatshrink").decompress(atob("mEwghC/AH8A1QWVhWq0AuVAAIuVAAIwT1WinQwTFwMzmQwTCYMjlUqGCIuBlWi0UzC6JdBIoMjC4UDmAuOkYXBPAWgmczLp2ilUiVAUDC4IwLFwIUBLoJ2BFwQwM1WjCgJ1DFwQwLFwJ1B0SQCkQWDGBQXBCgK9BDgKQBAAgwJOwUzRgIDBC54wCkZdGPBwACRgguDBIIwLFxEJBQIwLFxGaBYQwKFxQwLgAWGmQuBcAQwJC48ifYYwJgUidgsyC4L7DGBIXBdohnBCgL7BcYIXIGAqMCIoL7DL5IwERgIUBLoL7BO5QXBGAK7DkWiOxQXGFwOjFoUyFxZhDgBdCCgJ1CCxYxCgBABkcqOwIuNGAQXC0S9BLpgAFXoIwBmYuPAAYwCLp4wHFyYwDFyYwDFygwCCyoA/AFQA="));
function satelliteImage() {
return require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AH4AGnE4F1wvsF34wgFldcLdyMYsoACF1WJF4YxPFzOtF4wxNFzAvKSiIvU1ovIGAkJAAQucF5QxCFwYwbF4QwLrwvjYIVfrwABrtdq9Wqwvkq4oCAAtXmYvi1teE4NXrphCrxoCGAbvdSIoAHNQNeFzQvGeRQvCsowrYYNfF8YwHZQQFCF8QwGF4owjeYovBroHEMERhEF8IwNrtWryYFF8YwCq4vhGBeJF5AwaxIwKwVXFwwvandfMJeJF8M6nZiLGQIvdstfGAVlGBZkCxJeZJQIwCGIRjMFzYACGIc6r/+FsIvGGIYABEzYvPGQYvusovkAH4A/AH4A/ACo="));
}
var fix;
Bangle.setLCDPower(1);
Bangle.setLCDTimeout(0);
var Layout = require("Layout");
Bangle.setGPSPower(1, "app");
Bangle.loadWidgets();
Bangle.drawWidgets();
E.showMessage("Loading..."); // avoid showing rubbish on screen
g.clear();
var fix;
Bangle.setGPSPower(1);
Bangle.on('GPS',function(f) {
fix = f;
g.reset(1);
g.setFont("6x8",2);
g.setFontAlign(0,0);
g.clearRect(90,30,239,90);
if (fix.fix) {
g.drawString("GPS",170,40);
g.drawString("Acquired",170,60);
function setGPSTime() {
if (fix.time!==undefined) {
setTime(fix.time.getTime()/1000);
E.showMessage("System time set", {img:require("heatshrink").decompress(atob("lEo4UBvvv///vEFBYNVAAWq1QFDBAgKGrQJD0oJDtQJD1IICqwGBFoIDByocDwAJBgQeDtWoJwcqDwWq0EAgfAgEKHoQcCBIQeBGAQaBBIQzBytaEwQJDlWlrQmBBIkK0tqBI+ptRNCBIcCBKhECBIh6CAgUL8AJHl/4BI8+3gJRl/8GJH/BI8Ah6MDLIZQB+BjGAAIoBBI84BIaVCAAaVBVIYJEWYLkEXobRDAAbRBcoYACcoT5DEwYJCtQoElWpBINaDwYcB0oJBGQIzCAYIwBDwQGBAAIcCDwYACDgQACBIYIEBQYFDA="))});
} else {
g.drawString("Waiting for",170,40);
g.drawString("GPS Fix",170,60);
E.showMessage("No GPS time to set");
}
g.setFont("6x8");
g.drawString(fix.satellites+" satellites",170,80);
g.clearRect(0,100,239,239);
var t = ["","","","---",""];
if (fix.time!==undefined)
Bangle.removeListener('GPS',onGPS);
setTimeout(function() {
fix = undefined;
layout.forgetLazyState(); // redraw all next time
Bangle.on('GPS',onGPS);
}, 2000);
}
var layout = new Layout( {
type:"v", c: [
{type:"h", c:[
{type:"img", src:satelliteImage },
{ type:"v", fillx:1, c: [
{type:"txt", font:"6x8:2", label:"Waiting\nfor GPS", id:"status" },
{type:"txt", font:"6x8", label:"---", id:"sat" },
]},
]},
{type:"txt", fillx:1, filly:1, font:"6x8:2", label:"---", id:"gpstime" }
]},{lazy:true, btns: [
{ label : "Set", cb : setGPSTime},
{ label : "Back", cb : ()=>load() }
]});
function onGPS(f) {
if (fix===undefined) {
g.clear();
Bangle.drawWidgets();
}
fix = f;
if (fix.fix) {
layout.status.label = "GPS\nAcquired";
} else {
layout.status.label = "Waiting\nfor GPS";
}
layout.sat.label = fix.satellites+" satellites";
var t = ["","---",""];
if (fix.time!==undefined) {
t = fix.time.toString().split(" ");
/*
[
"Sun",
"Nov",
"10",
"2019",
"15:55:35",
"GMT+0100"
]
*/
//g.setFont("6x8",2);
//g.drawString(t[0],120,110); // day
g.setFont("6x8",3);
g.drawString(t[1]+" "+t[2],120,135); // date
g.setFont("6x8",2);
g.drawString(t[3],120,160); // year
g.setFont("6x8",3);
g.drawString(t[4],120,185); // time
if (fix.time) {
// timezone
var tz = (new Date()).getTimezoneOffset()/-60;
if (tz==0) tz="UTC";
else if (tz>0) tz="UTC+"+tz;
else tz="UTC"+tz;
g.setFont("6x8",2);
g.drawString(tz,120,210); // gmt
g.setFontAlign(0,0,3);
g.drawString("Set",230,120);
g.setFontAlign(0,0);
}
});
setInterval(function() {
g.drawImage(img,48,48,{scale:1.5,rotate:Math.sin(getTime()*2)/2});
},100);
setWatch(function() {
if (fix.time!==undefined)
setTime(fix.time.getTime()/1000);
}, BTN2, {repeat:true});
t = [t[1]+" "+t[2],t[3],t[4],t[5],tz];
}
layout.gpstime.label = t.join("\n");
layout.render();
}
Bangle.on('GPS',onGPS);

1
apps/gpstouch/Changelog Normal file
View File

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

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

@ -0,0 +1,16 @@
# GPS Touch
- A touch controlled GPS watch for Bangle JS 2
- Key feature is the conversion of Lat/Lon into Ordinance Servey Grid Reference
- Swipe left and right to change the display
- Select GPS and switch the GPS On or Off by touching twice in the top half of the display
- Select LOGGER and switch the GPS Recorder On or Off by touching twice in the top half of the display
- Displays the GPS time in the bottom half of the screen when the GPS is powered on, otherwise 00:00:00
- Select display of Course, Speed, Altitude, Longitude, Latitude, Ordinance Servey Grid Reference
## Screenshots
![](screenshot1.png)
![](screenshot2.png)
![](screenshot3.png)
![](screenshot4.png)

128
apps/gpstouch/geotools.js Normal file
View File

@ -0,0 +1,128 @@
/**
*
* A module of Geo functions for use with gps fixes
*
* let geo = require("geotools");
* let os = geo.gpsToOSGrid(fix);
* let ref = geo.gpsToOSMapRef(fix);
*
*/
Number.prototype.toRad = function() { return this*Math.PI/180; };
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
/* Ordnance Survey Grid Reference functions (c) Chris Veness 2005-2014 */
/* - www.movable-type.co.uk/scripts/gridref.js */
/* - www.movable-type.co.uk/scripts/latlon-gridref.html */
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
function OsGridRef(easting, northing) {
this.easting = 0|easting;
this.northing = 0|northing;
}
OsGridRef.latLongToOsGrid = function(point) {
var lat = point.lat.toRad();
var lon = point.lon.toRad();
var a = 6377563.396, b = 6356256.909; // Airy 1830 major & minor semi-axes
var F0 = 0.9996012717; // NatGrid scale factor on central meridian
var lat0 = (49).toRad(), lon0 = (-2).toRad(); // NatGrid true origin is 49�N,2�W
var N0 = -100000, E0 = 400000; // northing & easting of true origin, metres
var e2 = 1 - (b*b)/(a*a); // eccentricity squared
var n = (a-b)/(a+b), n2 = n*n, n3 = n*n*n;
var cosLat = Math.cos(lat), sinLat = Math.sin(lat);
var nu = a*F0/Math.sqrt(1-e2*sinLat*sinLat); // transverse radius of curvature
var rho = a*F0*(1-e2)/Math.pow(1-e2*sinLat*sinLat, 1.5); // meridional radius of curvature
var eta2 = nu/rho-1;
var Ma = (1 + n + (5/4)*n2 + (5/4)*n3) * (lat-lat0);
var Mb = (3*n + 3*n*n + (21/8)*n3) * Math.sin(lat-lat0) * Math.cos(lat+lat0);
var Mc = ((15/8)*n2 + (15/8)*n3) * Math.sin(2*(lat-lat0)) * Math.cos(2*(lat+lat0));
var Md = (35/24)*n3 * Math.sin(3*(lat-lat0)) * Math.cos(3*(lat+lat0));
var M = b * F0 * (Ma - Mb + Mc - Md); // meridional arc
var cos3lat = cosLat*cosLat*cosLat;
var cos5lat = cos3lat*cosLat*cosLat;
var tan2lat = Math.tan(lat)*Math.tan(lat);
var tan4lat = tan2lat*tan2lat;
var I = M + N0;
var II = (nu/2)*sinLat*cosLat;
var III = (nu/24)*sinLat*cos3lat*(5-tan2lat+9*eta2);
var IIIA = (nu/720)*sinLat*cos5lat*(61-58*tan2lat+tan4lat);
var IV = nu*cosLat;
var V = (nu/6)*cos3lat*(nu/rho-tan2lat);
var VI = (nu/120) * cos5lat * (5 - 18*tan2lat + tan4lat + 14*eta2 - 58*tan2lat*eta2);
var dLon = lon-lon0;
var dLon2 = dLon*dLon, dLon3 = dLon2*dLon, dLon4 = dLon3*dLon, dLon5 = dLon4*dLon, dLon6 = dLon5*dLon;
var N = I + II*dLon2 + III*dLon4 + IIIA*dLon6;
var E = E0 + IV*dLon + V*dLon3 + VI*dLon5;
return new OsGridRef(E, N);
};
/*
* converts northing, easting to standard OS grid reference.
*
* [digits=10] - precision (10 digits = metres)
* to_map_ref(8, 651409, 313177); => 'TG 5140 1317'
* to_map_ref(0, 651409, 313177); => '651409,313177'
*
*/
function to_map_ref(digits, easting, northing) {
if (![ 0,2,4,6,8,10,12,14,16 ].includes(Number(digits))) throw new RangeError(`invalid precision '${digits}'`); // eslint-disable-line comma-spacing
let e = easting;
let n = northing;
// use digits = 0 to return numeric format (in metres) - note northing may be >= 1e7
if (digits == 0) {
const format = { useGrouping: false, minimumIntegerDigits: 6, maximumFractionDigits: 3 };
const ePad = e.toLocaleString('en', format);
const nPad = n.toLocaleString('en', format);
return `${ePad},${nPad}`;
}
// get the 100km-grid indices
const e100km = Math.floor(e / 100000), n100km = Math.floor(n / 100000);
// translate those into numeric equivalents of the grid letters
let l1 = (19 - n100km) - (19 - n100km) % 5 + Math.floor((e100km + 10) / 5);
let l2 = (19 - n100km) * 5 % 25 + e100km % 5;
// compensate for skipped 'I' and build grid letter-pairs
if (l1 > 7) l1++;
if (l2 > 7) l2++;
const letterPair = String.fromCharCode(l1 + 'A'.charCodeAt(0), l2 + 'A'.charCodeAt(0));
// strip 100km-grid indices from easting & northing, and reduce precision
e = Math.floor((e % 100000) / Math.pow(10, 5 - digits / 2));
n = Math.floor((n % 100000) / Math.pow(10, 5 - digits / 2));
// pad eastings & northings with leading zeros
e = e.toString().padStart(digits/2, '0');
n = n.toString().padStart(digits/2, '0');
return `${letterPair} ${e} ${n}`;
}
/**
*
* Module exports section, example code below
*
* let geo = require("geotools");
* let os = geo.gpsToOSGrid(fix);
* let ref = geo.gpsToOSMapRef(fix);
*/
// get easting and northings
exports.gpsToOSGrid = function(gps_fix) {
return OsGridRef.latLongToOsGrid(gps_fix);
}
// string with an OS Map grid reference
exports.gpsToOSMapRef = function(gps_fix) {
let os = OsGridRef.latLongToOsGrid(last_fix);
return to_map_ref(6, os.easting, os.northing);
}

View File

@ -0,0 +1,246 @@
const h = g.getHeight();
const w = g.getWidth();
let geo = require("geotools");
let last_fix;
let listennerCount = 0;
function log_debug(o) {
//console.log(o);
}
function resetLastFix() {
last_fix = {
fix: 0,
alt: 0,
lat: 0,
lon: 0,
speed: 0,
time: 0,
course: 0,
satellites: 0
};
}
function processFix(fix) {
last_fix.time = fix.time;
log_debug(fix);
if (fix.fix) {
if (!last_fix.fix) {
// we dont need to suppress this in quiet mode as it is user initiated
Bangle.buzz(1500); // buzz on first position
}
last_fix = fix;
}
}
function draw() {
var d = new Date();
var da = d.toString().split(" ");
var time = da[4].substr(0,5);
var hh = da[4].substr(0,2);
var mm = da[4].substr(3,2);
g.reset();
drawTop(d,hh,mm);
drawInfo();
}
function drawTop(d,hh,mm) {
g.setFont("Vector", w/3);
g.setFontAlign(0, 0);
g.setColor(g.theme.bg);
g.fillRect(0, 24, w, ((h-24)/2) + 24);
g.setColor(g.theme.fg);
g.setFontAlign(1,0); // right aligned
g.drawString(hh, (w/2) - 6, ((h-24)/4) + 24);
g.setFontAlign(-1,0); // left aligned
g.drawString(mm, (w/2) + 6, ((h-24)/4) + 24);
// for the colon
g.setFontAlign(0,0); // centre aligned
if (d.getSeconds()&1) g.drawString(":", w/2, ((h-24)/4) + 24);
}
function drawInfo() {
if (infoData[infoMode] && infoData[infoMode].calc) {
g.setFont("Vector", w/7);
g.setFontAlign(0, 0);
if (infoData[infoMode].get_color)
g.setColor(infoData[infoMode].get_color());
else
g.setColor("#0ff");
g.fillRect(0, ((h-24)/2) + 24 + 1, w, h);
if (infoData[infoMode].is_control)
g.setColor("#fff");
else
g.setColor("#000");
g.drawString((infoData[infoMode].calc()), w/2, (3*(h-24)/4) + 24);
}
}
const infoData = {
ID_LAT: {
calc: () => 'Lat: ' + last_fix.lat.toFixed(4),
},
ID_LON: {
calc: () => 'Lon: ' + last_fix.lon.toFixed(4),
},
ID_SPEED: {
calc: () => 'Speed: ' + last_fix.speed.toFixed(1),
},
ID_ALT: {
calc: () => 'Alt: ' + last_fix.alt.toFixed(0),
},
ID_COURSE: {
calc: () => 'Course: '+ last_fix.course.toFixed(0),
},
ID_SATS: {
calc: () => 'Satelites: ' + last_fix.satellites,
},
ID_TIME: {
calc: () => formatTime(last_fix.time),
},
OS_REF: {
calc: () => !last_fix.fix ? "OO 000 000" : geo.gpsToOSMapRef(last_fix),
},
GPS_POWER: {
calc: () => (Bangle.isGPSOn()) ? 'GPS On' : 'GPS Off',
action: () => toggleGPS(),
get_color: () => Bangle.isGPSOn() ? '#f00' : '#00f',
is_control: true,
},
GPS_LOGGER: {
calc: () => 'Logger ' + loggerStatus(),
action: () => toggleLogger(),
get_color: () => loggerStatus() == "ON" ? '#f00' : '#00f',
is_control: true,
},
};
function toggleGPS() {
if (loggerStatus() == "ON")
return;
Bangle.setGPSPower(Bangle.isGPSOn() ? 0 : 1, 'gpstouch');
// add or remove listenner
if (Bangle.isGPSOn()) {
if (listennerCount == 0) {
Bangle.on('GPS', processFix);
listennerCount++;
log_debug("listennerCount=" + listennerCount);
}
} else {
if (listennerCount > 0) {
Bangle.removeListener("GPS", processFix);
listennerCount--;
log_debug("listennerCount=" + listennerCount);
}
}
resetLastFix();
}
function loggerStatus() {
var settings = require("Storage").readJSON("gpsrec.json",1)||{};
if (settings == {}) return "Install";
return settings.recording ? "ON" : "OFF";
}
function toggleLogger() {
var settings = require("Storage").readJSON("gpsrec.json",1)||{};
if (settings == {}) return;
settings.recording = !settings.recording;
require("Storage").write("gpsrec.json", settings);
if (WIDGETS["gpsrec"])
WIDGETS["gpsrec"].reload();
if (settings.recording && listennerCount == 0) {
Bangle.on('GPS', processFix);
listennerCount++;
log_debug("listennerCount=" + listennerCount);
}
}
function formatTime(now) {
try {
var fd = now.toUTCString().split(" ");
return fd[4];
} catch (e) {
return "00:00:00";
}
}
const infoList = Object.keys(infoData).sort();
let infoMode = infoList[0];
function nextInfo() {
let idx = infoList.indexOf(infoMode);
if (idx > -1) {
if (idx === infoList.length - 1) infoMode = infoList[0];
else infoMode = infoList[idx + 1];
}
}
function prevInfo() {
let idx = infoList.indexOf(infoMode);
if (idx > -1) {
if (idx === 0) infoMode = infoList[infoList.length - 1];
else infoMode = infoList[idx - 1];
}
}
Bangle.on('swipe', dir => {
if (dir == 1) prevInfo(); else nextInfo();
draw();
});
let prevTouch = 0;
Bangle.on('touch', function(button, xy) {
let dur = 1000*(getTime() - prevTouch);
prevTouch = getTime();
if (dur <= 1000 && xy.y < h/2 && infoData[infoMode].is_control) {
Bangle.buzz();
if (infoData[infoMode] && infoData[infoMode].action) {
infoData[infoMode].action();
draw();
}
}
});
// Stop updates when LCD is off, restart when on
Bangle.on('lcdPower', on => {
if (secondInterval)
clearInterval(secondInterval);
secondInterval = undefined;
if (on)
secondInterval = setInterval(draw, 1000);
draw();
});
resetLastFix();
// add listenner if already powered on, plus tag app
if (Bangle.isGPSOn() || loggerStatus() == "ON") {
Bangle.setGPSPower(1, 'gpstouch');
if (listennerCount == 0) {
Bangle.on('GPS', processFix);
listennerCount++;
log_debug("listennerCount=" + listennerCount);
}
}
g.clear();
var secondInterval = setInterval(draw, 1000);
draw();
// Show launcher when button pressed
Bangle.setUI("clock");
Bangle.loadWidgets();
Bangle.drawWidgets();

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEw4UA///j+EAYO/uYDB//wCYcPBA4AFh/ABZMDBbkX6gLIgtX6tQBY9VBYNVBY0BBYdABYsFqoACEgQLDitVtWpqtUBYtVq2q1WVGAQLErQLB0oLFHQNqBYIkBHgMDIwYKBAAJIDIweqz/2BYJtDBYI6Bv/9HgILHYwILGh4gBBYWfbooLF6AjPBYW//wLGL4Wv/RfGNZaDIBYibEBYizIBYjLDBYzXBd4TXCBZ60BBYRqEBZpUBBYRSFJAQLCA4b7BHgQLFgYLGIwYLEgoLBHQYLEgILBHQYLEgALBAoYLFi/UBZMHBZUD6ALKApQAFBbHwBZMP/4ABBwgIDA="))

BIN
apps/gpstouch/gpstouch.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

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