merge
52
README.md
|
@ -1,10 +1,10 @@
|
|||
Bangle.js App Loader (and Apps)
|
||||
================================
|
||||
|
||||
[](https://travis-ci.org/espruino/BangleApps)
|
||||
[](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.
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
theme: jekyll-theme-minimal
|
|
@ -2,13 +2,14 @@
|
|||
{ "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"},
|
||||
{"name":"7chname.img","url":"app-icon.js","evaluate":true}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
2021/11/18 | 1.0: Release for Bangle 2
|
|
@ -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
|
||||
|
||||

|
||||
|
||||
## Creator
|
||||
[@alainsaas](https://github.com/alainsaas)
|
After Width: | Height: | Size: 65 KiB |
|
@ -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};
|
||||
})();
|
After Width: | Height: | Size: 2.3 KiB |
|
@ -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
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
# Analogue Clock
|
||||
|
||||

|
||||
|
After Width: | Height: | Size: 3.0 KiB |
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
0.01: New App!
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEw4MA///xF9FstggwFDuEOAoc//gFJv/+AoZHBAgUB8/nwAFCBYIFCgYFB4AFHABdjCIPGAoPzAoPPAvpHFMpYFFPosAnk8NgYFdjEYfMo="))
|
|
@ -0,0 +1,2 @@
|
|||
// Config app not implemented yet
|
||||
setTimeout(()=>load("messages.app.js"),10);
|
After Width: | Height: | Size: 636 B |
|
@ -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 });
|
||||
}
|
||||
})();
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
@ -29,7 +30,7 @@ function drawCompass(hd) {
|
|||
if (Math.abs(hd - oldHeading) < 2) return 0;
|
||||
hd=hd*Math.PI/180;
|
||||
var p = [0, 1.1071, Math.PI/4, 2.8198, 3.4633, 7*Math.PI/4 , 5.1760];
|
||||
|
||||
|
||||
// using polar cordinates, 64,64 is the offset from the 0,0 origin
|
||||
var poly = [
|
||||
64+60*Math.sin(hd+p[0]), 64-60*Math.cos(hd+p[0]),
|
||||
|
@ -40,16 +41,16 @@ function drawCompass(hd) {
|
|||
64+28.2843*Math.sin(hd+p[5]), 64-28.2843*Math.cos(hd+p[5]),
|
||||
64+44.7214*Math.sin(hd+p[6]), 64-44.7214*Math.cos(hd+p[6])
|
||||
];
|
||||
|
||||
|
||||
buf1.fillPoly(poly);
|
||||
flip1(56, 56);
|
||||
}
|
||||
|
||||
// stops violent compass swings and wobbles, takes 3ms
|
||||
function newHeading(m,h){
|
||||
function newHeading(m,h){
|
||||
var s = Math.abs(m - h);
|
||||
var delta = (m>h)?1:-1;
|
||||
if (s>=180){s=360-s; delta = -delta;}
|
||||
if (s>=180){s=360-s; delta = -delta;}
|
||||
if (s<2) return h;
|
||||
var hd = h + delta*(1 + Math.round(s/5));
|
||||
if (hd<0) hd+=360;
|
||||
|
@ -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;
|
||||
|
@ -142,27 +145,39 @@ function docalibrate(e,first){
|
|||
startdraw();
|
||||
setTimeout(setButtons,1000);
|
||||
}
|
||||
}
|
||||
if (first===undefined) first=false;
|
||||
}
|
||||
|
||||
if (first === undefined) first = false;
|
||||
|
||||
stopdraw();
|
||||
clearWatch();
|
||||
if (first)
|
||||
isCalibrating = true;
|
||||
|
||||
if (first)
|
||||
E.showAlert(msg,title).then(action.bind(null,true));
|
||||
else
|
||||
else
|
||||
E.showPrompt(msg,{title:title,buttons:{"Start":true,"Cancel":false}}).then(action);
|
||||
}
|
||||
|
||||
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(){
|
||||
|
@ -170,8 +185,9 @@ function setButtons(){
|
|||
setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"});
|
||||
setWatch(docalibrate, BTN3, {repeat:false,edge:"falling"});
|
||||
}
|
||||
|
||||
|
||||
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);
|
||||
|
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
After Width: | Height: | Size: 4.4 KiB |
After Width: | Height: | Size: 6.3 KiB |
|
@ -0,0 +1,2 @@
|
|||
0.01: start of development
|
||||
0.02: first running version for BangleJs2
|
|
@ -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
|
|
@ -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=="))
|
|
@ -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();
|
After Width: | Height: | Size: 3.7 KiB |
After Width: | Height: | Size: 708 B |
|
@ -0,0 +1,4 @@
|
|||
# Bold Clock
|
||||
|
||||

|
||||
|
After Width: | Height: | Size: 3.5 KiB |
|
@ -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
|
||||
|
|
|
@ -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...");
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
0.01: New App!
|
|
@ -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
|
|
@ -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=="))
|
After Width: | Height: | Size: 2.7 KiB |
|
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
})();
|
After Width: | Height: | Size: 2.7 KiB |
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 29 KiB |
After Width: | Height: | Size: 60 KiB |
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwkBiIA/AH4A/AH4A8gAAKC8gKUC7Rf/C/PM5gDBjnBC6EcC4PBDIIbCC5/BAIIXVA4YXXAoRHUC6R3EC6KnEMAbv/C6oAKC8YA/AH4A/AH4Ax"))
|
|
@ -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();
|
||||
});
|
After Width: | Height: | Size: 6.0 KiB |
After Width: | Height: | Size: 421 B |
After Width: | Height: | Size: 2.3 KiB |
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
After Width: | Height: | Size: 2.1 KiB |
|
@ -0,0 +1,4 @@
|
|||
# 2x3 Pixel Clock
|
||||
|
||||

|
||||
|
After Width: | Height: | Size: 1.9 KiB |
|
@ -1,3 +1,4 @@
|
|||
0.01: New App!
|
||||
0.02: Show text if uncalibrated
|
||||
0.03: Eliminate flickering
|
||||
0.03: Eliminate flickering
|
||||
0.04: Fix for Bangle.js 2 and themes
|
||||
|
|
|
@ -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 {
|
||||
if (wasUncalibrated) {
|
||||
g.clearRect(0,24,W,48);
|
||||
wasUncalibrated = false;
|
||||
}
|
||||
g.setFontAlign(0,0).setFont("6x8",3);
|
||||
var y = 36;
|
||||
g.clearRect(M-40,y,M+40,y+24);
|
||||
g.drawString(Math.round(m.heading),M,y,true);
|
||||
}
|
||||
else {
|
||||
tg.setFontAlign(0,0);
|
||||
tg.setFont("6x8",2);
|
||||
tg.drawString(Math.round(m.heading),60,12);
|
||||
}
|
||||
g.drawImage(timg,0,0,{scale:2});
|
||||
|
||||
|
||||
ag.setColor(0);
|
||||
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);
|
||||
|
|
After Width: | Height: | Size: 2.6 KiB |
|
@ -0,0 +1,2 @@
|
|||
0.01: Initial Release
|
||||
0.02: Replace icon with one found on https://icons8.com
|
|
@ -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)
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("AH4A6iIAQCwkBC6MQC4kYxAACwMT/4ACmMe9wAC6IXLj4XD+IXE8IX/C9/zR4oXOmYABC6UTCwQXZjrXKf5IAHC713AAURindAAVBiatDmIXFi4XDuMdC4fRYooX/5nBC6Xc5gABC6UcCwQXF+aPMC471DC6MTCwQXHa4V2szXBC4bXBC5YAQC7se9wAC6MYxAACwJTCAAIXL8IXFQYoX/C/4XtjrXNu1ma4z/JAA4XEgAXRCwgA/AGo"))
|
|
@ -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();
|
After Width: | Height: | Size: 1.3 KiB |
|
@ -0,0 +1 @@
|
|||
0.01: New App!
|
|
@ -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)
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwkBiIAHkUoxGIwUiBxAAGiQVCAAeCkIWNCooADDBYWKDBYWEkc////+cyDBhxDCoQAD+YLDCw0YBQQVFAAYYCwIXFHQRDElGCJYgOCFw8vBwPyOgoJFGAg4BIoQWGDAhJCIwoLBHgYAGJQIjCIwguCnCRFRoeDGAZICAgOPFwaRGDAQfB/AwDBAYuCX44wDAgTrDBoIDBGYP/manBmYFBFYQPDwJeBD4iRGRoQ/FC4QqBEYIbERooTBCAeBNAIjBBQIDDAAggBG4IDDwQXBEQIDDUAgcCHASaBAYQTFMQpcFDYp+EEII9DAARRDFIIfDHIwXBVISlDC4YzD9wA0osFpwIF8lQqgWK8kAgEEBItABIIhGAAfgBoMABIoIChwX0jwED8oNBgoXFqAJBrwHD8IXEBwQNEEIYgFC4wAQ8MRC6sRC+BgULwIwHSINVpwuLC43kaAQABqgaHC4bZHAAkFqhGHGAovFAAYyDCwgwFL4IwGFxAwNFxIwG8lVCoSTEFw7bPCxAYNCxT0LIpIxMCpoyHFhI"))
|
|
@ -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);
|
After Width: | Height: | Size: 1.9 KiB |
|
@ -0,0 +1,4 @@
|
|||
# Nifty-A Clock
|
||||
|
||||

|
||||
|
|
@ -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();
|
||||
|
||||
|
|
After Width: | Height: | Size: 3.4 KiB |
|
@ -0,0 +1,2 @@
|
|||
0.01: New Clock Nifty B
|
||||
0.02: Added configuration
|
|
@ -0,0 +1,9 @@
|
|||
# Nifty Series B Clock
|
||||
|
||||
- Display Time and Date
|
||||
- Color Configuration
|
||||
|
||||
##
|
||||
|
||||

|
||||
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwkB/4A/AH4ARgMRBA3xBBIJCAYIFDAAYHGCAYJBDYQABj4PD+AXFCwgXGCAg9ECwwJBJQooGCxAXCIYQpBAgg9IC5yPCCw4XKBYIsFPwUBXQQXHAYREIF5ZEC+MfWQYXODQYTGC5ZDEOw0QMAIXMPggvSC44vRL5b8EAYIACC5i0FCwaOBC5C0DA4ZLCC5hfC/4DBIwwXKCInwgAWEKIwXJAA4XXCxYXCEwR2EgJeLR5LbCGRYXIAgzvKh7zGZg4XGIYisBA4JJCC6B5DAoYXWF6xfRC4fwAgMBC6cBU5I6CC5AECCo0QJwQXJaZJHMEYR1JC5QKBXo8QC4oCBAZAwHgKXBTQwSDBIKmGgJ3DEYheEA4ZfJKgkPdJQXHDAQWBC44eIC4QAMDA4A=="))
|
|
@ -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");
|
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 4.2 KiB |
|
@ -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)
|
||||
}
|
||||
);
|
||||
});
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
# Flappy Bird
|
||||
|
||||

|
||||

|
||||
|
After Width: | Height: | Size: 2.5 KiB |
After Width: | Height: | Size: 2.7 KiB |
|
@ -0,0 +1,4 @@
|
|||
# Floral Clock
|
||||
|
||||

|
||||
|
After Width: | Height: | Size: 5.9 KiB |
|
@ -0,0 +1 @@
|
|||
0.01: Initial version
|
After Width: | Height: | Size: 1.0 KiB |
|
@ -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>
|
After Width: | Height: | Size: 3.7 KiB |
|
@ -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.04: Fix shown UTC time zone sign
|
||||
0.05: Use new 'layout library for Bangle2, fix #764 by adding a back button
|
||||
|
|
|
@ -1 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwghC/AH8A1QWVhWq0AuVAAIuVAAIwT1WinQwTFwMzmQwTCYMjlUqGCIuBlWi0UzC6JdBIoMjC4UDmAuOkYXBPAWgmczLp2ilUiVAUDC4IwLFwIUBLoJ2BFwQwM1WjCgJ1DFwQwLFwJ1B0SQCkQWDGBQXBCgK9BDgKQBAAgwJOwUzRgIDBC54wCkZdGPBwACRgguDBIIwLFxEJBQIwLFxGaBYQwKFxQwLgAWGmQuBcAQwJC48ifYYwJgUidgsyC4L7DGBIXBdohnBCgL7BcYIXIGAqMCIoL7DL5IwERgIUBLoL7BO5QXBGAK7DkWiOxQXGFwOjFoUyFxZhDgBdCCgJ1CCxYxCgBABkcqOwIuNGAQXC0S9BLpgAFXoIwBmYuPAAYwCLp4wHFyYwDFyYwDFygwCCyoA/AFQA="))
|
||||
require("heatshrink").decompress(atob("mEw4UA////G161hyd8Jf4ALlQLK1WABREC1WgBZEK32oFxPW1QuJ7QwIFwOqvQLHhW31NaBY8qy2rtUFoAuG3W61EVqALF1+qr2gqtUHQu11dawNVqo6F22q9XFBYIwEhWqz2r6oLBGAheBqwuBBYx2CFwQLGlWqgoLCMAsKLoILChR6EgQuDqkqYYsBFweqYYoLDoWnYYoLD/WVYYv8FwXqPoIwEn52BqGrPoILEh/1FwOl9SsBBYcD/pdB2uq/QvEh/8LoOu1xHFh8/gGp9WWL4oMBgWltXeO4owBgWt1ReFYYh2GYYmXEQzDD3wiHegYKIGAJRGAAguJAH4AC"))
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
0.01: First version
|
|
@ -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
|
||||
|
||||

|
||||

|
||||

|
||||

|
|
@ -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);
|
||||
}
|
|
@ -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();
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEw4UA///j+EAYO/uYDB//wCYcPBA4AFh/ABZMDBbkX6gLIgtX6tQBY9VBYNVBY0BBYdABYsFqoACEgQLDitVtWpqtUBYtVq2q1WVGAQLErQLB0oLFHQNqBYIkBHgMDIwYKBAAJIDIweqz/2BYJtDBYI6Bv/9HgILHYwILGh4gBBYWfbooLF6AjPBYW//wLGL4Wv/RfGNZaDIBYibEBYizIBYjLDBYzXBd4TXCBZ60BBYRqEBZpUBBYRSFJAQLCA4b7BHgQLFgYLGIwYLEgoLBHQYLEgILBHQYLEgALBAoYLFi/UBZMHBZUD6ALKApQAFBbHwBZMP/4ABBwgIDA="))
|
After Width: | Height: | Size: 1.5 KiB |