fix merge
|
@ -2,4 +2,5 @@ apps/animclk/V29.LBM.js
|
|||
apps/banglerun/rollup.config.js
|
||||
apps/schoolCalendar/fullcalendar/main.js
|
||||
apps/authentiwatch/qr_packed.js
|
||||
apps/qrcode/qr-scanner.umd.min.js
|
||||
*.test.js
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
name: Node CI
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [16.x]
|
||||
|
||||
steps:
|
||||
- name: Checkout repository and submodules
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: install testing dependencies
|
||||
run: npm i
|
||||
- name: test all apps and widgets
|
||||
run: npm run test
|
||||
- name: install typescript dependencies
|
||||
working-directory: ./typescript
|
||||
run: npm ci
|
||||
- name: build types
|
||||
working-directory: ./typescript
|
||||
run: npm run build:types
|
||||
- name: build all TS apps and widgets
|
||||
working-directory: ./typescript
|
||||
run: npm run build
|
|
@ -1,3 +0,0 @@
|
|||
language: node_js
|
||||
node_js:
|
||||
- "node"
|
|
@ -3,3 +3,4 @@
|
|||
0.03: Add "Calculating" placeholder, update JSON save format
|
||||
0.04: Fix tapping at very bottom of list, exit on inactivity
|
||||
0.05: Add support for bulk importing and exporting tokens
|
||||
0.06: Add spaces to codes for improved readability (thanks @BartS23)
|
||||
|
|
|
@ -33,7 +33,7 @@ Keep those copies safe and secure.
|
|||
* Swipe right to exit to the app launcher.
|
||||
* Swipe left on selected counter token to advance the counter to the next value.
|
||||
|
||||

|
||||
   
|
||||
|
||||
## Creator
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
const tokenextraheight = 16;
|
||||
var tokendigitsheight = 30;
|
||||
var tokenheight = tokendigitsheight + tokenextraheight;
|
||||
// Hash functions
|
||||
const crypto = require("crypto");
|
||||
const algos = {
|
||||
|
@ -93,6 +94,9 @@ function hotp(d, token, dohmac) {
|
|||
while (ret.length < token.digits) {
|
||||
ret = "0" + ret;
|
||||
}
|
||||
// add a space after every 3rd or 4th digit
|
||||
var re = (token.digits % 3 == 0 || (token.digits % 3 >= token.digits % 4 && token.digits % 4 != 0)) ? "" : ".";
|
||||
ret = ret.replace(new RegExp("(..." + re + ")", "g"), "$1 ").trim();
|
||||
} catch(err) {
|
||||
ret = notsupported;
|
||||
}
|
||||
|
@ -121,15 +125,15 @@ function drawToken(id, r) {
|
|||
lbl = tokens[id].label.substr(0, 10);
|
||||
if (id == state.curtoken) {
|
||||
// current token
|
||||
g.setColor(g.theme.fgH);
|
||||
g.setBgColor(g.theme.bgH);
|
||||
g.setFont("Vector", tokenextraheight);
|
||||
g.setColor(g.theme.fgH)
|
||||
.setBgColor(g.theme.bgH)
|
||||
.setFont("Vector", tokenextraheight)
|
||||
// center just below top line
|
||||
g.setFontAlign(0, -1, 0);
|
||||
.setFontAlign(0, -1, 0);
|
||||
adj = y1;
|
||||
} else {
|
||||
g.setColor(g.theme.fg);
|
||||
g.setBgColor(g.theme.bg);
|
||||
g.setColor(g.theme.fg)
|
||||
.setBgColor(g.theme.bg);
|
||||
sz = tokendigitsheight;
|
||||
do {
|
||||
g.setFont("Vector", sz--);
|
||||
|
@ -138,8 +142,8 @@ function drawToken(id, r) {
|
|||
g.setFontAlign(0, 0, 0);
|
||||
adj = (y1 + y2) / 2;
|
||||
}
|
||||
g.clearRect(x1, y1, x2, y2);
|
||||
g.drawString(lbl, (x1 + x2) / 2, adj, false);
|
||||
g.clearRect(x1, y1, x2, y2)
|
||||
.drawString(lbl, (x1 + x2) / 2, adj, false);
|
||||
if (id == state.curtoken) {
|
||||
if (tokens[id].period > 0) {
|
||||
// timed - draw progress bar
|
||||
|
@ -160,10 +164,10 @@ function drawToken(id, r) {
|
|||
g.drawString(state.otp, (x1 + adj + x2) / 2, y1 + tokenextraheight, false);
|
||||
}
|
||||
// shaded lines top and bottom
|
||||
g.setColor(0.5, 0.5, 0.5);
|
||||
g.drawLine(x1, y1, x2, y1);
|
||||
g.drawLine(x1, y2, x2, y2);
|
||||
g.setClipRect(0, 0, g.getWidth(), g.getHeight());
|
||||
g.setColor(0.5, 0.5, 0.5)
|
||||
.drawLine(x1, y1, x2, y1)
|
||||
.drawLine(x1, y2, x2, y2)
|
||||
.setClipRect(0, 0, g.getWidth(), g.getHeight());
|
||||
}
|
||||
|
||||
function draw() {
|
||||
|
@ -198,15 +202,15 @@ function draw() {
|
|||
}
|
||||
if (tokens.length > 0) {
|
||||
var drewcur = false;
|
||||
var id = Math.floor(state.listy / (tokendigitsheight + tokenextraheight));
|
||||
var y = id * (tokendigitsheight + tokenextraheight) + Bangle.appRect.y - state.listy;
|
||||
var id = Math.floor(state.listy / tokenheight);
|
||||
var y = id * tokenheight + Bangle.appRect.y - state.listy;
|
||||
while (id < tokens.length && y < Bangle.appRect.y2) {
|
||||
drawToken(id, {x:Bangle.appRect.x, y:y, w:Bangle.appRect.w, h:(tokendigitsheight + tokenextraheight)});
|
||||
drawToken(id, {x:Bangle.appRect.x, y:y, w:Bangle.appRect.w, h:tokenheight});
|
||||
if (id == state.curtoken && (tokens[id].period <= 0 || state.nextTime != 0)) {
|
||||
drewcur = true;
|
||||
}
|
||||
id += 1;
|
||||
y += (tokendigitsheight + tokenextraheight);
|
||||
y += tokenheight;
|
||||
}
|
||||
if (drewcur) {
|
||||
// the current token has been drawn - schedule a redraw
|
||||
|
@ -228,9 +232,9 @@ function draw() {
|
|||
state.nexttime = 0;
|
||||
}
|
||||
} else {
|
||||
g.setFont("Vector", tokendigitsheight);
|
||||
g.setFontAlign(0, 0, 0);
|
||||
g.drawString(notokens, Bangle.appRect.x + Bangle.appRect.w / 2, Bangle.appRect.y + Bangle.appRect.h / 2, false);
|
||||
g.setFont("Vector", tokendigitsheight)
|
||||
.setFontAlign(0, 0, 0)
|
||||
.drawString(notokens, Bangle.appRect.x + Bangle.appRect.w / 2, Bangle.appRect.y + Bangle.appRect.h / 2, false);
|
||||
}
|
||||
if (state.drawtimer) {
|
||||
clearTimeout(state.drawtimer);
|
||||
|
@ -240,18 +244,18 @@ function draw() {
|
|||
|
||||
function onTouch(zone, e) {
|
||||
if (e) {
|
||||
var id = Math.floor((state.listy + (e.y - Bangle.appRect.y)) / (tokendigitsheight + tokenextraheight));
|
||||
var id = Math.floor((state.listy + (e.y - Bangle.appRect.y)) / tokenheight);
|
||||
if (id == state.curtoken || tokens.length == 0 || id >= tokens.length) {
|
||||
id = -1;
|
||||
}
|
||||
if (state.curtoken != id) {
|
||||
if (id != -1) {
|
||||
var y = id * (tokendigitsheight + tokenextraheight) - state.listy;
|
||||
var y = id * tokenheight - state.listy;
|
||||
if (y < 0) {
|
||||
state.listy += y;
|
||||
y = 0;
|
||||
}
|
||||
y += (tokendigitsheight + tokenextraheight);
|
||||
y += tokenheight;
|
||||
if (y > Bangle.appRect.h) {
|
||||
state.listy += (y - Bangle.appRect.h);
|
||||
}
|
||||
|
@ -268,7 +272,7 @@ function onTouch(zone, e) {
|
|||
function onDrag(e) {
|
||||
if (e.x > g.getWidth() || e.y > g.getHeight()) return;
|
||||
if (e.dx == 0 && e.dy == 0) return;
|
||||
var newy = Math.min(state.listy - e.dy, tokens.length * (tokendigitsheight + tokenextraheight) - Bangle.appRect.h);
|
||||
var newy = Math.min(state.listy - e.dy, tokens.length * tokenheight - Bangle.appRect.h);
|
||||
state.listy = Math.max(0, newy);
|
||||
draw();
|
||||
}
|
||||
|
@ -300,8 +304,12 @@ function bangle1Btn(e) {
|
|||
}
|
||||
state.curtoken = Math.max(state.curtoken, 0);
|
||||
state.curtoken = Math.min(state.curtoken, tokens.length - 1);
|
||||
state.listy = state.curtoken * tokenheight;
|
||||
state.listy -= (Bangle.appRect.h - tokenheight) / 2;
|
||||
state.listy = Math.min(state.listy, tokens.length * tokenheight - Bangle.appRect.h);
|
||||
state.listy = Math.max(state.listy, 0);
|
||||
var fakee = {};
|
||||
fakee.y = state.curtoken * (tokendigitsheight + tokenextraheight) - state.listy + Bangle.appRect.y;
|
||||
fakee.y = state.curtoken * tokenheight - state.listy + Bangle.appRect.y;
|
||||
state.curtoken = -1;
|
||||
state.nextTime = 0;
|
||||
onTouch(0, fakee);
|
||||
|
@ -319,7 +327,7 @@ Bangle.on('drag' , onDrag );
|
|||
Bangle.on('swipe', onSwipe);
|
||||
if (typeof BTN2 == 'number') {
|
||||
setWatch(function(){bangle1Btn(-1);}, BTN1, {edge:"rising" , debounce:50, repeat:true});
|
||||
setWatch(function(){exitApp(); }, BTN2, {edge:"rising", debounce:50, repeat:true});
|
||||
setWatch(function(){exitApp(); }, BTN2, {edge:"falling", debounce:50});
|
||||
setWatch(function(){bangle1Btn( 1);}, BTN3, {edge:"rising" , debounce:50, repeat:true});
|
||||
}
|
||||
Bangle.loadWidgets();
|
||||
|
|
|
@ -56,6 +56,7 @@ function base32clean(val, nows) {
|
|||
var ret = val.replaceAll(/\s+/g, ' ');
|
||||
ret = ret.replaceAll(/0/g, 'O');
|
||||
ret = ret.replaceAll(/1/g, 'I');
|
||||
ret = ret.replaceAll(/8/g, 'B');
|
||||
ret = ret.replaceAll(/[^A-Za-z2-7 ]/g, '');
|
||||
if (nows) {
|
||||
ret = ret.replaceAll(/\s+/g, '');
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
"name": "2FA Authenticator",
|
||||
"shortName": "AuthWatch",
|
||||
"icon": "app.png",
|
||||
"screenshots": [{"url":"screenshot.png"}],
|
||||
"version": "0.05",
|
||||
"screenshots": [{"url":"screenshot1.png"},{"url":"screenshot2.png"},{"url":"screenshot3.png"},{"url":"screenshot4.png"}],
|
||||
"version": "0.06",
|
||||
"description": "Google Authenticator compatible tool.",
|
||||
"tags": "tool",
|
||||
"interface": "interface.html",
|
||||
|
|
Before Width: | Height: | Size: 2.9 KiB |
After Width: | Height: | Size: 2.6 KiB |
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 2.6 KiB |
After Width: | Height: | Size: 2.9 KiB |
|
@ -1 +0,0 @@
|
|||
node_modules/
|
|
@ -1,13 +0,0 @@
|
|||
0.01: First release
|
||||
0.02: Bugfix time: Reset minutes to 0 when hitting 60
|
||||
0.03: Fix distance >=10 km (fix #529)
|
||||
0.04: Use offscreen buffer for flickerless updates
|
||||
0.05: Complete rewrite. New UI, GPS & HRM Kalman filters, activity logging
|
||||
0.06: Reading HDOP directly from the GPS event (needs Espruino 2v07 or above)
|
||||
0.07: Fixed GPS update, added guards against NaN values
|
||||
0.08: Fix issue with GPS coordinates being wrong after the first one
|
||||
0.09: Another GPS fix (log raw coordinates - not filtered ones)
|
||||
0.10: Removed kalman filtering to allow distance log to work
|
||||
Only log data every 5 seconds (not 1 sec)
|
||||
Don't create a file until the first log entry is ready
|
||||
Add labels for buttons
|
|
@ -1,25 +0,0 @@
|
|||
# BangleRun
|
||||
|
||||
An app for running sessions. Displays info and logs your run for later viewing.
|
||||
|
||||
## Compilation
|
||||
|
||||
The app is written in Typescript, and needs to be transpiled in order to be
|
||||
run on the BangleJS. The easiest way to perform this step is by using the
|
||||
ubiquitous [NPM package manager](https://www.npmjs.com/get-npm).
|
||||
|
||||
After having installed NPM for your platform, checkout the `BangleApps` repo,
|
||||
open a terminal, and navigate into the `apps/banglerun` folder. Then issue:
|
||||
|
||||
```
|
||||
npm i
|
||||
```
|
||||
|
||||
to install the project's build tools, and:
|
||||
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
To build the app. The last command will generate the `app.js` file, containing
|
||||
the transpiled code for the BangleJS.
|
|
@ -1 +0,0 @@
|
|||
require("heatshrink").decompress(atob("mEwwIHEuAEDgP8ApMDAqAXBjAGD/E8AgUcgF8CAX/BgIFBn//wAFCv//8PwAoP///5Aon/8AcB+IFB4AFB8P/34FBgfj/8fwAFB4f+g4cBg/H/w/Cg+HKQcPx4FEh4/CAoMfAocOj4/CKYRwELIIFDLII6BAoZSBLIYeCgP+v4FD/k/GAQFBHgcD/ABBIIX4gIFBSYPwAoUPAog/B8AFEwAFDDQQCBQoQFCZYYFigCKEgFwgAA=="))
|
|
@ -1 +0,0 @@
|
|||
!function(){"use strict";var t;!function(t){t.Stopped="STOP",t.Paused="PAUSE",t.Running="RUN"}(t||(t={}));const n={STOP:63488,PAUSE:65504,RUN:2016};function e(t,n,e){g.setColor(0),g.fillRect(n-60,e,n+60,e+30),g.setColor(65535),g.drawString(t,n,e)}function i(i){var s;g.setFontVector(30),g.setFontAlign(0,-1,0),e((i.distance/1e3).toFixed(2),60,55),e(function(t){const n=Math.round(t),e=Math.floor(n/3600),i=Math.floor(n/60)%60,s=n%60;return(e?e+":":"")+("0"+i).substr(-2)+":"+("0"+s).substr(-2)}(i.duration),172,55),e(function(t){if(t<.1667)return"__'__\"";const n=Math.round(1e3/t),e=Math.floor(n/60),i=n%60;return("0"+e).substr(-2)+"'"+("0"+i).substr(-2)+'"'}(i.speed),60,115),e(i.hr.toFixed(0),172,115),e(i.steps.toFixed(0),60,175),e(i.cadence.toFixed(0),172,175),g.setFont("6x8",2),g.setColor(i.gpsValid?2016:63488),g.fillRect(0,216,80,240),g.setColor(0),g.drawString("GPS",40,220),g.setColor(65535),g.fillRect(80,216,160,240),g.setColor(0),g.drawString(("0"+(s=new Date).getHours()).substr(-2)+":"+("0"+s.getMinutes()).substr(-2),120,220),g.setColor(n[i.status]),g.fillRect(160,216,230,240),g.setColor(0),g.drawString(i.status,200,220),g.setFont("6x8").setFontAlign(0,0,1).setColor(-1),i.status===t.Paused?g.drawString("START",236,60,1).drawString(" CLEAR ",236,180,1):i.status===t.Running?g.drawString(" PAUSE ",236,60,1).drawString(" PAUSE ",236,180,1):g.drawString("START",236,60,1).drawString(" ",236,180,1)}function s(t){g.clear(),g.setColor(50712),g.setFont("6x8",2),g.setFontAlign(0,-1,0),g.drawString("DIST (KM)",60,32),g.drawString("TIME",180,32),g.drawString("PACE",60,92),g.drawString("HEART",180,92),g.drawString("STEPS",60,152),g.drawString("CADENCE",180,152),i(t),Bangle.drawWidgets()}function a(n){n.status===t.Stopped&&function(t){const n=(new Date).toISOString().replace(/[-:]/g,""),e=`banglerun_${n.substr(2,6)}_${n.substr(9,6)}`;t.file=require("Storage").open(e,"w"),t.fileWritten=!1}(n),n.status===t.Running?n.status=t.Paused:n.status=t.Running,i(n)}const r={fix:NaN,lat:NaN,lon:NaN,alt:NaN,vel:NaN,dop:NaN,gpsValid:!1,x:NaN,y:NaN,z:NaN,t:NaN,timeSinceLog:0,hr:60,hrError:100,file:null,fileWritten:!1,drawing:!1,status:t.Stopped,duration:0,distance:0,speed:0,steps:0,cadence:0};var o;o=r,Bangle.on("GPS",n=>function(n,e){n.lat=e.lat,n.lon=e.lon,n.alt=e.alt,n.vel=e.speed/3.6,n.fix=e.fix,n.dop=e.hdop,n.gpsValid=n.fix>0,function(n){const e=Date.now();let i=(e-n.t)/1e3;if(isFinite(i)||(i=0),n.t=e,n.timeSinceLog+=i,n.status===t.Running&&(n.duration+=i),!n.gpsValid)return;const s=6371008.8+n.alt,a=n.lat*Math.PI/180,r=n.lon*Math.PI/180,o=s*Math.cos(a)*Math.cos(r),g=s*Math.cos(a)*Math.sin(r),d=s*Math.sin(a);if(!n.x)return n.x=o,n.y=g,void(n.z=d);const u=o-n.x,l=g-n.y,c=d-n.z,f=Math.sqrt(u*u+l*l+c*c);n.x=o,n.y=g,n.z=d,n.status===t.Running&&(n.distance+=f,n.speed=n.distance/n.duration||0,n.cadence=60*n.steps/n.duration||0)}(n),i(n),n.gpsValid&&n.status===t.Running&&n.timeSinceLog>5&&(n.timeSinceLog=0,function(t){t.fileWritten||(t.file.write(["timestamp","latitude","longitude","altitude","duration","distance","heartrate","steps"].join(",")+"\n"),t.fileWritten=!0),t.file.write([Date.now().toFixed(0),t.lat.toFixed(6),t.lon.toFixed(6),t.alt.toFixed(2),t.duration.toFixed(0),t.distance.toFixed(2),t.hr.toFixed(0),t.steps.toFixed(0)].join(",")+"\n")}(n))}(o,n)),Bangle.setGPSPower(1),function(t){Bangle.on("HRM",n=>function(t,n){if(0===n.confidence)return;const e=n.bpm-t.hr,i=Math.abs(e)+101-n.confidence,s=t.hrError/(t.hrError+i)||0;t.hr+=e*s,t.hrError+=(i-t.hrError)*s}(t,n)),Bangle.setHRMPower(1)}(r),function(n){Bangle.on("step",()=>function(n){n.status===t.Running&&(n.steps+=1)}(n))}(r),function(t){Bangle.loadWidgets(),Bangle.on("lcdPower",n=>{t.drawing=n,n&&s(t)}),s(t)}(r),setWatch(()=>a(r),BTN1,{repeat:!0,edge:"falling"}),setWatch(()=>function(n){n.status===t.Paused&&function(t){t.duration=0,t.distance=0,t.speed=0,t.steps=0,t.cadence=0}(n),n.status===t.Running?n.status=t.Paused:n.status=t.Stopped,i(n)}(r),BTN3,{repeat:!0,edge:"falling"})}();
|
Before Width: | Height: | Size: 10 KiB |
|
@ -1,217 +0,0 @@
|
|||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="../../css/spectre.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="tracks"></div>
|
||||
|
||||
<script src="../../core/lib/interface.js"></script>
|
||||
<script>
|
||||
/* TODO: Calculate cadence from step count */
|
||||
var domTracks = document.getElementById("tracks");
|
||||
|
||||
function saveKML(track,title) {
|
||||
var kml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<kml xmlns="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2">
|
||||
<Document>
|
||||
<Schema id="schema">
|
||||
<gx:SimpleArrayField name="heartrate" type="int">
|
||||
<displayName>Heart Rate</displayName>
|
||||
</gx:SimpleArrayField>
|
||||
<gx:SimpleArrayField name="steps" type="int">
|
||||
<displayName>Step Count</displayName>
|
||||
</gx:SimpleArrayField>
|
||||
<gx:SimpleArrayField name="distance" type="float">
|
||||
<displayName>Distance</displayName>
|
||||
</gx:SimpleArrayField>
|
||||
<gx:SimpleArrayField name="cadence" type="int">
|
||||
<displayName>Cadence</displayName>
|
||||
</gx:SimpleArrayField>
|
||||
</Schema>
|
||||
<Folder>
|
||||
<name>Tracks</name>
|
||||
<Placemark>
|
||||
<name>${title}</name>
|
||||
<gx:Track>
|
||||
${track.map(pt=>` <when>${pt.date.toISOString()}</when>\n`).join("")}
|
||||
${track.map(pt=>` <gx:coord>${pt.lon} ${pt.lat} ${pt.alt}</gx:coord>\n`).join("")}
|
||||
<ExtendedData>
|
||||
<SchemaData schemaUrl="#schema">
|
||||
<gx:SimpleArrayData name="heartrate">
|
||||
${track.map(pt=>` <gx:value>${pt.heartrate}</gx:value>\n`).join("")}
|
||||
</gx:SimpleArrayData>
|
||||
<gx:SimpleArrayData name="steps">
|
||||
${track.map(pt=>` <gx:value>${pt.steps}</gx:value>\n`).join("")}
|
||||
</gx:SimpleArrayData>
|
||||
<gx:SimpleArrayData name="distance">
|
||||
${track.map(pt=>` <gx:value>${pt.distance}</gx:value>\n`).join("")}
|
||||
</gx:SimpleArrayData>
|
||||
</SchemaData>
|
||||
</ExtendedData>
|
||||
</gx:Track>
|
||||
</Placemark>
|
||||
</Folder>
|
||||
</Document>
|
||||
</kml>`;
|
||||
var a = document.createElement("a"),
|
||||
file = new Blob([kml], {type: "application/vnd.google-earth.kml+xml"});
|
||||
var url = URL.createObjectURL(file);
|
||||
a.href = url;
|
||||
a.download = title+".kml";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
setTimeout(function() {
|
||||
document.body.removeChild(a);
|
||||
window.URL.revokeObjectURL(url);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function saveGPX(track, title) {
|
||||
var gpx = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<gpx creator="Bangle.js" version="1.1" xmlns="http://www.topografix.com/GPX/1/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v1 http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd" xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1" xmlns:gpxx="http://www.garmin.com/xmlschemas/GpxExtensions/v3">
|
||||
<metadata>
|
||||
<time>${track[0].date.toISOString()}</time>
|
||||
</metadata>
|
||||
<trk>
|
||||
<name>${title}</name>
|
||||
<trkseg>`;
|
||||
track.forEach(pt=>{
|
||||
gpx += `
|
||||
<trkpt lat="${pt.lat}" lon="${pt.lon}">
|
||||
<ele>${pt.alt}</ele>
|
||||
<time>${pt.date.toISOString()}</time>
|
||||
<extensions>
|
||||
<gpxtpx:TrackPointExtension>
|
||||
<gpxtpx:hr>${pt.heartrate}</gpxtpx:hr>
|
||||
<gpxtpx:distance>${pt.distance}</gpxtpx:distance>
|
||||
${/* <gpxtpx:cad>65</gpxtpx:cad> */""}
|
||||
</gpxtpx:TrackPointExtension>
|
||||
</extensions>
|
||||
</trkpt>`;
|
||||
});
|
||||
gpx += `
|
||||
</trkseg>
|
||||
</trk>
|
||||
</gpx>`;
|
||||
var a = document.createElement("a"),
|
||||
file = new Blob([gpx], {type: "application/gpx+xml"});
|
||||
var url = URL.createObjectURL(file);
|
||||
a.href = url;
|
||||
a.download = title+".gpx";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
setTimeout(function() {
|
||||
document.body.removeChild(a);
|
||||
window.URL.revokeObjectURL(url);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function trackLineToObject(l, hasFileName) {
|
||||
// "timestamp,latitude,longitude,altitude,duration,distance,heartrate,steps\n"
|
||||
var t = l.trim().split(",");
|
||||
var n = hasFileName ? 1 : 0;
|
||||
var o = {
|
||||
invalid : t.length < 8,
|
||||
date : new Date(parseInt(t[n+0])),
|
||||
lat : parseFloat(t[n+1]),
|
||||
lon : parseFloat(t[n+2]),
|
||||
alt : parseFloat(t[n+3]),
|
||||
duration : parseFloat(t[n+4]),
|
||||
distance : parseFloat(t[n+5]),
|
||||
heartrate : parseInt(t[n+6]),
|
||||
steps : parseInt(t[n+7]),
|
||||
};
|
||||
if (hasFileName)
|
||||
o.filename = t[0];
|
||||
return o;
|
||||
}
|
||||
|
||||
function downloadTrack(trackid, callback) {
|
||||
Util.showModal("Downloading Track...");
|
||||
Util.readStorageFile(trackid, data=>{
|
||||
Util.hideModal();
|
||||
var trackLines = data.trim().split("\n");
|
||||
trackLines.shift(); // remove first line, which is column header
|
||||
// should be:
|
||||
// "timestamp,latitude,longitude,altitude,duration,distance,heartrate,steps\n"
|
||||
var track = trackLines.map(l=>trackLineToObject(l,false));
|
||||
callback(track);
|
||||
});
|
||||
}
|
||||
function getTrackList() {
|
||||
Util.showModal("Loading Tracks...");
|
||||
domTracks.innerHTML = "";
|
||||
Puck.eval(`require("Storage").list(/banglerun_.*\\x01/).map(fn=>{fn=fn.slice(0,-1);var f=require("Storage").open(fn,"r");f.readLine();return fn+","+f.readLine()})`,trackLines=>{
|
||||
var html = `<div class="container">
|
||||
<div class="columns">\n`;
|
||||
trackLines.forEach(l => {
|
||||
var track = trackLineToObject(l, true /*has filename*/);
|
||||
html += `
|
||||
<div class="column col-12">
|
||||
<div class="card-header">
|
||||
<div class="card-title h5">Track ${track.filename}</div>
|
||||
<div class="card-subtitle text-gray">${track.invalid ? "No Data":track.date.toString().substr(0,24)}</div>
|
||||
</div>
|
||||
${track.invalid?``:`<div class="card-image">
|
||||
<iframe
|
||||
width="100%"
|
||||
height="250"
|
||||
frameborder="0" style="border:0"
|
||||
src="https://www.google.com/maps/embed/v1/place?key=AIzaSyBxTcwrrVOh2piz7EmIs1Xn4FsRxJWeVH4&q=${track.lat},${track.lon}&zoom=10" allowfullscreen>
|
||||
</iframe>
|
||||
</div>
|
||||
<div class="card-body"></div>`}
|
||||
<div class="card-footer">${track.invalid?``:`
|
||||
<button class="btn btn-primary" trackid="${track.filename}" task="downloadkml">Download KML</button>
|
||||
<button class="btn btn-primary" trackid="${track.filename}" task="downloadgpx">Download GPX</button>`}
|
||||
<button class="btn btn-default" trackid="${track.filename}" task="delete">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
if (trackLines.length==0) {
|
||||
html += `
|
||||
<div class="column col-12">
|
||||
<div class="card-header">
|
||||
<div class="card-title h5">No tracks</div>
|
||||
<div class="card-subtitle text-gray">No GPS tracks found</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
html += `
|
||||
</div>
|
||||
</div>`;
|
||||
domTracks.innerHTML = html;
|
||||
Util.hideModal();
|
||||
var buttons = domTracks.querySelectorAll("button");
|
||||
for (var i=0;i<buttons.length;i++) {
|
||||
buttons[i].addEventListener("click",event => {
|
||||
var button = event.currentTarget;
|
||||
var trackid = button.getAttribute("trackid");
|
||||
var task = button.getAttribute("task");
|
||||
if (task=="delete") {
|
||||
Util.showModal("Deleting Track...");
|
||||
Util.eraseStorageFile(trackid,()=>{
|
||||
Util.hideModal();
|
||||
getTrackList();
|
||||
});
|
||||
}
|
||||
if (task=="downloadkml") {
|
||||
downloadTrack(trackid, track => saveKML(track, `Bangle.js Track ${trackid}`));
|
||||
}
|
||||
if (task=="downloadgpx") {
|
||||
downloadTrack(trackid, track => saveGPX(track, `Bangle.js Track ${trackid}`));
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function onInit() {
|
||||
getTrackList();
|
||||
}
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"spec_dir": "test",
|
||||
"spec_files": [
|
||||
"**/*.spec.ts"
|
||||
]
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
{
|
||||
"id": "banglerun",
|
||||
"name": "BangleRun",
|
||||
"shortName": "BangleRun",
|
||||
"version": "0.10",
|
||||
"description": "An app for running sessions. Displays info and logs your run for later viewing.",
|
||||
"icon": "banglerun.png",
|
||||
"tags": "run,running,fitness,outdoors",
|
||||
"supports": ["BANGLEJS"],
|
||||
"interface": "interface.html",
|
||||
"allow_emulator": false,
|
||||
"storage": [
|
||||
{"name":"banglerun.app.js","url":"app.js"},
|
||||
{"name":"banglerun.img","url":"app-icon.js","evaluate":true}
|
||||
]
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
{
|
||||
"name": "banglerun",
|
||||
"version": "0.5.0",
|
||||
"description": "Bangle.js app for running sessions",
|
||||
"main": "app.js",
|
||||
"types": "app.d.ts",
|
||||
"scripts": {
|
||||
"build": "rollup -c",
|
||||
"test": "ts-node -P tsconfig.spec.json node_modules/jasmine/bin/jasmine --config=jasmine.json"
|
||||
},
|
||||
"author": {
|
||||
"name": "Stefano Baldan",
|
||||
"email": "singintime@gmail.com"
|
||||
},
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-typescript": "^4.1.1",
|
||||
"@types/jasmine": "^3.5.10",
|
||||
"jasmine": "^3.5.0",
|
||||
"rollup": "^2.10.2",
|
||||
"rollup-plugin-terser": "^5.3.0",
|
||||
"terser": "^4.7.0",
|
||||
"ts-node": "^8.10.2",
|
||||
"tslib": "^2.0.0",
|
||||
"typescript": "^3.9.2"
|
||||
}
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
import typescript from '@rollup/plugin-typescript';
|
||||
import { terser } from 'rollup-plugin-terser';
|
||||
|
||||
export default {
|
||||
input: './src/app.ts',
|
||||
output: {
|
||||
dir: '.',
|
||||
format: 'iife',
|
||||
name: 'banglerun'
|
||||
},
|
||||
plugins: [
|
||||
typescript(),
|
||||
terser(),
|
||||
]
|
||||
};
|
|
@ -1,41 +0,0 @@
|
|||
import { draw } from './display';
|
||||
import { initLog } from './log';
|
||||
import { ActivityStatus, AppState } from './state';
|
||||
|
||||
function startActivity(state: AppState): void {
|
||||
if (state.status === ActivityStatus.Stopped) {
|
||||
initLog(state);
|
||||
}
|
||||
|
||||
if (state.status === ActivityStatus.Running) {
|
||||
state.status = ActivityStatus.Paused;
|
||||
} else {
|
||||
state.status = ActivityStatus.Running;
|
||||
}
|
||||
|
||||
draw(state);
|
||||
}
|
||||
|
||||
function stopActivity(state: AppState): void {
|
||||
if (state.status === ActivityStatus.Paused) {
|
||||
clearActivity(state);
|
||||
}
|
||||
|
||||
if (state.status === ActivityStatus.Running) {
|
||||
state.status = ActivityStatus.Paused;
|
||||
} else {
|
||||
state.status = ActivityStatus.Stopped;
|
||||
}
|
||||
|
||||
draw(state);
|
||||
}
|
||||
|
||||
function clearActivity(state: AppState): void {
|
||||
state.duration = 0;
|
||||
state.distance = 0;
|
||||
state.speed = 0;
|
||||
state.steps = 0;
|
||||
state.cadence = 0;
|
||||
}
|
||||
|
||||
export { clearActivity, startActivity, stopActivity };
|
|
@ -1,20 +0,0 @@
|
|||
import { startActivity, stopActivity } from './activity';
|
||||
import { initDisplay } from './display';
|
||||
import { initGps } from './gps';
|
||||
import { initHrm } from './hrm';
|
||||
import { initState } from './state';
|
||||
import { initStep } from './step';
|
||||
|
||||
declare var BTN1: any;
|
||||
declare var BTN3: any;
|
||||
declare var setWatch: any;
|
||||
|
||||
const appState = initState();
|
||||
|
||||
initGps(appState);
|
||||
initHrm(appState);
|
||||
initStep(appState);
|
||||
initDisplay(appState);
|
||||
|
||||
setWatch(() => startActivity(appState), BTN1, { repeat: true, edge: 'falling' });
|
||||
setWatch(() => stopActivity(appState), BTN3, { repeat: true, edge: 'falling' });
|
|
@ -1,123 +0,0 @@
|
|||
import { ActivityStatus, AppState } from './state';
|
||||
|
||||
declare var Bangle: any;
|
||||
declare var g: any;
|
||||
|
||||
const STATUS_COLORS = {
|
||||
'STOP': 0xF800,
|
||||
'PAUSE': 0xFFE0,
|
||||
'RUN': 0x07E0,
|
||||
}
|
||||
|
||||
function initDisplay(state: AppState): void {
|
||||
Bangle.loadWidgets();
|
||||
Bangle.on('lcdPower', (on: boolean) => {
|
||||
state.drawing = on;
|
||||
if (on) {
|
||||
drawAll(state);
|
||||
}
|
||||
});
|
||||
drawAll(state);
|
||||
}
|
||||
|
||||
function drawBackground(): void {
|
||||
g.clear();
|
||||
g.setColor(0xC618);
|
||||
g.setFont('6x8', 2);
|
||||
g.setFontAlign(0, -1, 0);
|
||||
g.drawString('DIST (KM)', 60, 32);
|
||||
g.drawString('TIME', 172, 32);
|
||||
g.drawString('PACE', 60, 92);
|
||||
g.drawString('HEART', 172, 92);
|
||||
g.drawString('STEPS', 60, 152);
|
||||
g.drawString('CADENCE', 172, 152);
|
||||
}
|
||||
|
||||
function drawValue(value: string, x: number, y: number) {
|
||||
g.setColor(0x0000);
|
||||
g.fillRect(x - 60, y, x + 60, y + 30);
|
||||
g.setColor(0xFFFF);
|
||||
g.drawString(value, x, y);
|
||||
}
|
||||
|
||||
function draw(state: AppState): void {
|
||||
g.setFontVector(30);
|
||||
g.setFontAlign(0, -1, 0);
|
||||
|
||||
drawValue(formatDistance(state.distance), 60, 55);
|
||||
drawValue(formatTime(state.duration), 172, 55);
|
||||
drawValue(formatPace(state.speed), 60, 115);
|
||||
drawValue(state.hr.toFixed(0), 172, 115);
|
||||
drawValue(state.steps.toFixed(0), 60, 175);
|
||||
drawValue(state.cadence.toFixed(0), 172, 175);
|
||||
|
||||
g.setFont('6x8', 2);
|
||||
|
||||
g.setColor(state.gpsValid ? 0x07E0 : 0xF800);
|
||||
g.fillRect(0, 216, 80, 240);
|
||||
g.setColor(0x0000);
|
||||
g.drawString('GPS', 40, 220);
|
||||
|
||||
g.setColor(0xFFFF);
|
||||
g.fillRect(80, 216, 160, 240);
|
||||
g.setColor(0x0000);
|
||||
g.drawString(formatClock(new Date()), 120, 220);
|
||||
|
||||
g.setColor(STATUS_COLORS[state.status]);
|
||||
g.fillRect(160, 216, 230, 240);
|
||||
g.setColor(0x0000);
|
||||
g.drawString(state.status, 200, 220);
|
||||
|
||||
g.setFont("6x8").setFontAlign(0,0,1).setColor(-1);
|
||||
if (state.status === ActivityStatus.Paused) {
|
||||
g.drawString("START",236,60,1).drawString(" CLEAR ",236,180,1);
|
||||
} else if (state.status === ActivityStatus.Running) {
|
||||
g.drawString(" PAUSE ",236,60,1).drawString(" PAUSE ",236,180,1);
|
||||
} else {
|
||||
g.drawString("START",236,60,1).drawString(" ",236,180,1);
|
||||
}
|
||||
}
|
||||
|
||||
function drawAll(state: AppState) {
|
||||
drawBackground();
|
||||
draw(state);
|
||||
Bangle.drawWidgets();
|
||||
}
|
||||
|
||||
function formatClock(date: Date): string {
|
||||
return ('0' + date.getHours()).substr(-2) + ':' + ('0' + date.getMinutes()).substr(-2);
|
||||
}
|
||||
|
||||
function formatDistance(meters: number): string {
|
||||
return (meters / 1000).toFixed(2);
|
||||
}
|
||||
|
||||
function formatPace(speed: number): string {
|
||||
if (speed < 0.1667) {
|
||||
return `__'__"`;
|
||||
}
|
||||
const pace = Math.round(1000 / speed);
|
||||
const min = Math.floor(pace / 60);
|
||||
const sec = pace % 60;
|
||||
return ('0' + min).substr(-2) + `'` + ('0' + sec).substr(-2) + `"`;
|
||||
}
|
||||
|
||||
function formatTime(time: number): string {
|
||||
const seconds = Math.round(time);
|
||||
const hrs = Math.floor(seconds / 3600);
|
||||
const min = Math.floor(seconds / 60) % 60;
|
||||
const sec = seconds % 60;
|
||||
return (hrs ? hrs + ':' : '') + ('0' + min).substr(-2) + `:` + ('0' + sec).substr(-2);
|
||||
}
|
||||
|
||||
export {
|
||||
draw,
|
||||
drawAll,
|
||||
drawBackground,
|
||||
drawValue,
|
||||
formatClock,
|
||||
formatDistance,
|
||||
formatPace,
|
||||
formatTime,
|
||||
initDisplay,
|
||||
};
|
|
@ -1,90 +0,0 @@
|
|||
import { draw } from './display';
|
||||
import { updateLog } from './log';
|
||||
import { ActivityStatus, AppState } from './state';
|
||||
|
||||
declare var Bangle: any;
|
||||
|
||||
interface GpsEvent {
|
||||
lat: number;
|
||||
lon: number;
|
||||
alt: number;
|
||||
speed: number;
|
||||
hdop: number;
|
||||
fix: number;
|
||||
}
|
||||
|
||||
const EARTH_RADIUS = 6371008.8;
|
||||
|
||||
function initGps(state: AppState): void {
|
||||
Bangle.on('GPS', (gps: GpsEvent) => readGps(state, gps));
|
||||
Bangle.setGPSPower(1);
|
||||
}
|
||||
|
||||
function readGps(state: AppState, gps: GpsEvent): void {
|
||||
state.lat = gps.lat;
|
||||
state.lon = gps.lon;
|
||||
state.alt = gps.alt;
|
||||
state.vel = gps.speed / 3.6;
|
||||
state.fix = gps.fix;
|
||||
state.dop = gps.hdop;
|
||||
state.gpsValid = state.fix > 0;
|
||||
|
||||
updateGps(state);
|
||||
draw(state);
|
||||
|
||||
/* Only log GPS data every 5 secs if we
|
||||
have a fix and we're running. */
|
||||
if (state.gpsValid &&
|
||||
state.status === ActivityStatus.Running &&
|
||||
state.timeSinceLog > 5) {
|
||||
state.timeSinceLog = 0;
|
||||
updateLog(state);
|
||||
}
|
||||
}
|
||||
|
||||
function updateGps(state: AppState): void {
|
||||
const t = Date.now();
|
||||
let dt = (t - state.t) / 1000;
|
||||
if (!isFinite(dt)) dt=0;
|
||||
state.t = t;
|
||||
state.timeSinceLog += dt;
|
||||
|
||||
if (state.status === ActivityStatus.Running) {
|
||||
state.duration += dt;
|
||||
}
|
||||
|
||||
if (!state.gpsValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const r = EARTH_RADIUS + state.alt;
|
||||
const lat = state.lat * Math.PI / 180;
|
||||
const lon = state.lon * Math.PI / 180;
|
||||
const x = r * Math.cos(lat) * Math.cos(lon);
|
||||
const y = r * Math.cos(lat) * Math.sin(lon);
|
||||
const z = r * Math.sin(lat);
|
||||
|
||||
if (!state.x) {
|
||||
state.x = x;
|
||||
state.y = y;
|
||||
state.z = z;
|
||||
return;
|
||||
}
|
||||
|
||||
const dx = x - state.x;
|
||||
const dy = y - state.y;
|
||||
const dz = z - state.z;
|
||||
const dpMag = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
|
||||
state.x = x;
|
||||
state.y = y;
|
||||
state.z = z;
|
||||
|
||||
if (state.status === ActivityStatus.Running) {
|
||||
state.distance += dpMag;
|
||||
state.speed = (state.distance / state.duration) || 0;
|
||||
state.cadence = (60 * state.steps / state.duration) || 0;
|
||||
}
|
||||
}
|
||||
|
||||
export { initGps, readGps, updateGps };
|
|
@ -1,29 +0,0 @@
|
|||
import { AppState } from './state';
|
||||
|
||||
interface HrmData {
|
||||
bpm: number;
|
||||
confidence: number;
|
||||
raw: string;
|
||||
}
|
||||
|
||||
declare var Bangle: any;
|
||||
|
||||
function initHrm(state: AppState) {
|
||||
Bangle.on('HRM', (hrm: HrmData) => updateHrm(state, hrm));
|
||||
Bangle.setHRMPower(1);
|
||||
}
|
||||
|
||||
function updateHrm(state: AppState, hrm: HrmData) {
|
||||
if (hrm.confidence === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dHr = hrm.bpm - state.hr;
|
||||
const hrError = Math.abs(dHr) + 101 - hrm.confidence;
|
||||
const hrGain = (state.hrError / (state.hrError + hrError)) || 0;
|
||||
|
||||
state.hr += dHr * hrGain;
|
||||
state.hrError += (hrError - state.hrError) * hrGain;
|
||||
}
|
||||
|
||||
export { initHrm, updateHrm };
|
|
@ -1,40 +0,0 @@
|
|||
import { AppState } from './state';
|
||||
|
||||
declare var require: any;
|
||||
|
||||
function initLog(state: AppState): void {
|
||||
const datetime = new Date().toISOString().replace(/[-:]/g, '');
|
||||
const date = datetime.substr(2, 6);
|
||||
const time = datetime.substr(9, 6);
|
||||
const filename = `banglerun_${date}_${time}`;
|
||||
state.file = require('Storage').open(filename, 'w');
|
||||
state.fileWritten = false;
|
||||
}
|
||||
|
||||
function updateLog(state: AppState): void {
|
||||
if (!state.fileWritten) {
|
||||
state.file.write([
|
||||
'timestamp',
|
||||
'latitude',
|
||||
'longitude',
|
||||
'altitude',
|
||||
'duration',
|
||||
'distance',
|
||||
'heartrate',
|
||||
'steps',
|
||||
].join(',') + '\n');
|
||||
state.fileWritten = true;
|
||||
}
|
||||
state.file.write([
|
||||
Date.now().toFixed(0),
|
||||
state.lat.toFixed(6),
|
||||
state.lon.toFixed(6),
|
||||
state.alt.toFixed(2),
|
||||
state.duration.toFixed(0),
|
||||
state.distance.toFixed(2),
|
||||
state.hr.toFixed(0),
|
||||
state.steps.toFixed(0),
|
||||
].join(',') + '\n');
|
||||
}
|
||||
|
||||
export { initLog, updateLog };
|
|
@ -1,85 +0,0 @@
|
|||
enum ActivityStatus {
|
||||
Stopped = 'STOP',
|
||||
Paused = 'PAUSE',
|
||||
Running = 'RUN',
|
||||
}
|
||||
|
||||
interface AppState {
|
||||
// GPS NMEA data
|
||||
fix: number;
|
||||
lat: number;
|
||||
lon: number;
|
||||
alt: number;
|
||||
vel: number;
|
||||
dop: number;
|
||||
gpsValid: boolean;
|
||||
|
||||
// Absolute position data
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
// Last fix time
|
||||
t: number;
|
||||
// Last time we saved log info
|
||||
timeSinceLog : number;
|
||||
|
||||
// HRM data
|
||||
hr: number,
|
||||
hrError: number,
|
||||
|
||||
// Logger data
|
||||
file: File;
|
||||
fileWritten: boolean;
|
||||
|
||||
// Drawing data
|
||||
drawing: boolean;
|
||||
|
||||
// Activity data
|
||||
status: ActivityStatus;
|
||||
duration: number;
|
||||
distance: number;
|
||||
speed: number;
|
||||
steps: number;
|
||||
cadence: number;
|
||||
}
|
||||
|
||||
interface File {
|
||||
read: Function;
|
||||
write: Function;
|
||||
erase: Function;
|
||||
}
|
||||
|
||||
function initState(): AppState {
|
||||
return {
|
||||
fix: NaN,
|
||||
lat: NaN,
|
||||
lon: NaN,
|
||||
alt: NaN,
|
||||
vel: NaN,
|
||||
dop: NaN,
|
||||
gpsValid: false,
|
||||
|
||||
x: NaN,
|
||||
y: NaN,
|
||||
z: NaN,
|
||||
t: NaN,
|
||||
timeSinceLog : 0,
|
||||
|
||||
hr: 60,
|
||||
hrError: 100,
|
||||
|
||||
file: null,
|
||||
fileWritten: false,
|
||||
|
||||
drawing: false,
|
||||
|
||||
status: ActivityStatus.Stopped,
|
||||
duration: 0,
|
||||
distance: 0,
|
||||
speed: 0,
|
||||
steps: 0,
|
||||
cadence: 0,
|
||||
}
|
||||
}
|
||||
|
||||
export { ActivityStatus, AppState, File, initState };
|
|
@ -1,15 +0,0 @@
|
|||
import { ActivityStatus, AppState } from './state';
|
||||
|
||||
declare var Bangle: any;
|
||||
|
||||
function initStep(state: AppState) {
|
||||
Bangle.on('step', () => updateStep(state));
|
||||
}
|
||||
|
||||
function updateStep(state: AppState) {
|
||||
if (state.status === ActivityStatus.Running) {
|
||||
state.steps += 1;
|
||||
}
|
||||
}
|
||||
|
||||
export { initStep, updateStep };
|
|
@ -1,10 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "es2015",
|
||||
"noImplicitAny": true,
|
||||
"target": "es2015"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"noImplicitAny": true,
|
||||
"target": "es2015"
|
||||
},
|
||||
"include": [
|
||||
"test"
|
||||
]
|
||||
}
|
|
@ -1,2 +1,3 @@
|
|||
0.01: New App!
|
||||
0.02: Write available data on reset or kill
|
||||
0.03: Buzz short on every finished measurement and longer if all are done
|
||||
|
|
|
@ -75,7 +75,6 @@ function write(){
|
|||
data += "," + rrMax + "," + rrMin + ","+rrNumberOfValues;
|
||||
data += "\n";
|
||||
file.write(data);
|
||||
Bangle.buzz(500);
|
||||
}
|
||||
|
||||
function onBtHrm(e) {
|
||||
|
@ -87,6 +86,11 @@ function onBtHrm(e) {
|
|||
if (currentSlot <= hrvSlots.length && (Date.now() - startingTime) > (hrvSlots[currentSlot] * 1000) && !hrvValues[hrvSlots[currentSlot]]){
|
||||
hrvValues[hrvSlots[currentSlot]] = hrv;
|
||||
currentSlot++;
|
||||
if (currentSlot == hrvSlots.length){
|
||||
Bangle.buzz(500)
|
||||
} else {
|
||||
Bangle.buzz(50);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"id": "bthrv",
|
||||
"name": "Bluetooth Heart Rate variance calculator",
|
||||
"shortName": "BT HRV",
|
||||
"version": "0.02",
|
||||
"version": "0.03",
|
||||
"description": "Calculates HRV from a a BT HRM with interval data",
|
||||
"icon": "app.png",
|
||||
"type": "app",
|
||||
|
|
|
@ -20,3 +20,6 @@
|
|||
Color depending on value (green -> red, red -> green) option
|
||||
Good HRM value will not be overwritten so fast anymore
|
||||
0.10: Use roboto font for time, date and day of week and center align them
|
||||
0.11: New color option: foreground color
|
||||
Improve performance, reduce memory usage
|
||||
Small optical adjustments
|
||||
|
|
|
@ -3,23 +3,8 @@ const storage = require("Storage");
|
|||
const SunCalc = require("https://raw.githubusercontent.com/mourner/suncalc/master/suncalc.js");
|
||||
|
||||
const shoesIcon = atob("EBCBAAAACAAcAB4AHgAeABwwADgGeAZ4AHgAMAAAAHAAIAAA");
|
||||
const heartIcon = atob("EBCBAAAAAAAeeD/8P/x//n/+P/w//B/4D/AH4APAAYAAAAAA");
|
||||
const powerIcon = atob("EBCBAAAAA8ADwA/wD/AP8A/wD/AP8A/wD/AP8A/wD/AH4AAA");
|
||||
const temperatureIcon = atob("EBCBAAAAAYADwAJAAkADwAPAA8ADwAfgB+AH4AfgA8ABgAAA");
|
||||
|
||||
const weatherCloudy = atob("EBCBAAAAAAAAAAfgD/Af8H/4//7///////9//z/+AAAAAAAA");
|
||||
const weatherSunny = atob("EBCBAAAAAYAQCBAIA8AH4A/wb/YP8A/gB+ARiBAIAYABgAAA");
|
||||
const weatherMoon = atob("EBCBAAAAAYAP8B/4P/w//D/8f/5//j/8P/w//B/4D/ABgAAA");
|
||||
const weatherPartlyCloudy = atob("EBCBAAAAAAAYQAMAD8AIQBhoW+AOYBwwOBBgHGAGP/wf+AAA");
|
||||
const weatherRainy = atob("EBCBAAAAAYAH4AwwOBBgGEAOQAJBgjPOEkgGYAZgA8ABgAAA");
|
||||
const weatherPartlyRainy = atob("EBCBAAAAEEAQAAeADMAYaFvoTmAMMDgQIBxhhiGGG9wDwAGA");
|
||||
const weatherSnowy = atob("EBCBAAAAAAADwAGAEYg73C50BCAEIC50O9wRiAGAA8AAAAAA");
|
||||
const weatherFoggy = atob("EBCBAAAAAAADwAZgDDA4EGAcQAZAAgAAf74AAAAAd/4AAAAA");
|
||||
const weatherStormy = atob("EBCBAAAAAYAH4AwwOBBgGEAOQMJAgjmOGcgAgACAAAAAAAAA");
|
||||
|
||||
const sunSetDown = atob("EBCBAAAAAAABgAAAAAATyAZoBCB//gAAAAAGYAPAAYAAAAAA");
|
||||
const sunSetUp = atob("EBCBAAAAAAABgAAAAAATyAZoBCB//gAAAAABgAPABmAAAAAA");
|
||||
|
||||
Graphics.prototype.setFontRobotoRegular50NumericOnly = function(scale) {
|
||||
// Actual height 39 (40 - 2)
|
||||
this.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAAAB8AAAAAAAfAAAAAAAPwAAAAAAB8AAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAA4AAAAAAB+AAAAAAD/gAAAAAD/4AAAAAH/4AAAAAP/wAAAAAP/gAAAAAf/gAAAAAf/AAAAAA/+AAAAAB/+AAAAAB/8AAAAAD/4AAAAAH/4AAAAAD/wAAAAAA/wAAAAAAPgAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA///wAAAB////gAAA////8AAA/////gAAP////8AAH8AAA/gAB8AAAD4AA+AAAAfAAPAAAADwADwAAAA8AA8AAAAPAAPAAAADwADwAAAA8AA8AAAAPAAPgAAAHwAB8AAAD4AAfwAAD+AAD/////AAA/////wAAH////4AAAf///4AAAB///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAAAAPgAAAAAADwAAAAAAB8AAAAAAAfAAAAAAAHgAAAAAAD4AAAAAAA+AAAAAAAPAAAAAAAH/////wAB/////8AA//////AAP/////wAD/////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAAAfgAADwAAP4AAB8AAH+AAA/AAD/gAAfwAB/AAAf8AAfAAAP/AAPgAAH7wAD4AAD88AA8AAB+PAAPAAA/DwADwAAfg8AA8AAPwPAAPAAH4DwADwAH8A8AA+AD+APAAPwB/ADwAB/D/gA8AAf//gAPAAD//wADwAAf/wAA8AAD/4AAPAAAHwAADwAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAADgAAAHwAA+AAAD8AAP4AAB/AAD/AAA/wAA/wAAf4AAD+AAHwAAAPgAD4APAB8AA+ADwAPAAPAA8ADwADwAPAA8AA8ADwAPAAPAA8ADwADwAfAA8AA8AH4APAAPgD+AHwAB8B/wD4AAf7/+B+AAD//v//AAA//x//wAAD/4P/4AAAf8B/4AAAAYAH4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAAAAAHwAAAAAAH8AAAAAAD/AAAAAAD/wAAAAAD/8AAAAAB/vAAAAAB/jwAAAAA/g8AAAAA/wPAAAAAfwDwAAAAf4A8AAAAf4APAAAAP8ADwAAAP8AA8AAAH8AAPAAAD/////8AA//////AAP/////wAD/////8AA//////AAAAAAPAAAAAAADwAAAAAAA8AAAAAAAPAAAAAAADwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAAB/APwAAH//wD+AAD//8A/wAA///AH+AAP//wAPgAD/B4AB8AA8A+AAfAAPAPAADwADwDwAA8AA8A8AAPAAPAPAADwADwD4AA8AA8A+AAPAAPAPwAHwADwD8AD4AA8AfwD+AAPAH///AADwA///wAA8AH//4AAPAAf/4AAAAAB/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//AAAAAD//+AAAAD///4AAAD////AAAB////4AAA/78D/AAAfw8AH4AAPweAA+AAD4PgAHwAB8DwAA8AAfA8AAPAAHgPAADwAD4DwAA8AA+A8AAPAAPAPgAHwADwD4AB8AA8AfgA+AAPAH+B/gAAAA///wAAAAH//4AAAAA//8AAAAAH/8AAAAAAP4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAAAAAA8AAAAAAAPAAAAAAADwAAAAAAA8AAAABAAPAAAABwADwAAAB8AA8AAAB/AAPAAAB/wADwAAD/8AA8AAD/8AAPAAD/4AADwAD/4AAA8AD/4AAAPAH/wAAADwH/wAAAA8H/wAAAAPH/wAAAAD3/gAAAAA//gAAAAAP/gAAAAAD/gAAAAAA/AAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwA/4AAAH/Af/AAAH/8P/4AAD//n//AAA//7//4AAfx/+A+AAHwD+AHwAD4AfgB8AA8AHwAPAAPAA8ADwADwAPAA8AA8ADwAPAAPAA8ADwADwAfAA8AA+AH4AfAAHwD+AHwAB/D/4D4AAP/+/n+AAD//n//AAAf/w//gAAB/wH/wAAAHwA/4AAAAAABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAAAAD/8AAAAAD//wAAAAB//+AAAAA///wAAAAf4H+APAAH4AfgDwAD8AB8A8AA+AAfAPAAPAADwDwADwAA8B8AA8AAPAfAAPAADwHgADwAA8D4AA+AAeB+AAHwAHg/AAB+ADwfgAAP8D4/4AAD////8AAAf///8AAAB///+AAAAP//+AAAAAP/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAOAAAB8AAHwAAAfgAD8AAAH4AA/AAAB8AAHwAAAOAAA4AAAAAAAAAAAAAAAAAAAAAAAAAA"), 46, atob("DRUcHBwcHBwcHBwcDA=="), 50+(scale<<8)+(1<<16));
|
||||
|
@ -67,7 +52,7 @@ const colorGreen = '#008000';
|
|||
const colorBlue = '#0000ff';
|
||||
const colorYellow = '#ffff00';
|
||||
const widgetOffset = showWidgets ? 24 : 0;
|
||||
const dowOffset = circleCount == 3 ? 22 : 24; // dow offset relative to date
|
||||
const dowOffset = circleCount == 3 ? 20 : 22; // dow offset relative to date
|
||||
const h = g.getHeight() - widgetOffset;
|
||||
const w = g.getWidth();
|
||||
const hOffset = (circleCount == 3 ? 34 : 30) - widgetOffset;
|
||||
|
@ -103,10 +88,7 @@ const circleFontBig = circleCount == 3 ? "Vector:16" : "Vector:12";
|
|||
const iconOffset = circleCount == 3 ? 6 : 8;
|
||||
const defaultCircleTypes = ["steps", "hr", "battery", "weather"];
|
||||
|
||||
|
||||
function draw() {
|
||||
g.clear(true);
|
||||
if (!showWidgets) {
|
||||
function hideWidgets() {
|
||||
/*
|
||||
* we are not drawing the widgets as we are taking over the whole screen
|
||||
* so we will blank out the draw() functions of each widget and change the
|
||||
|
@ -118,6 +100,12 @@ function draw() {
|
|||
wd.area = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function draw() {
|
||||
g.clear(true);
|
||||
if (!showWidgets) {
|
||||
hideWidgets();
|
||||
} else {
|
||||
Bangle.drawWidgets();
|
||||
}
|
||||
|
@ -129,7 +117,7 @@ function draw() {
|
|||
g.setFontRobotoRegular50NumericOnly();
|
||||
g.setFontAlign(0, -1);
|
||||
g.setColor(colorFg);
|
||||
g.drawString(locale.time(new Date(), 1), w / 2, h1 + 8);
|
||||
g.drawString(locale.time(new Date(), 1), w / 2, h1 + 6);
|
||||
now = Math.round(new Date().getTime() / 1000);
|
||||
|
||||
// date & dow
|
||||
|
@ -138,10 +126,19 @@ function draw() {
|
|||
g.drawString(locale.date(new Date()), w / 2, h2);
|
||||
g.drawString(locale.dow(new Date()), w / 2, h2 + dowOffset);
|
||||
|
||||
// draw the circles a little bit delayed so we decrease the blocking time
|
||||
setTimeout(function() {
|
||||
drawCircle(1);
|
||||
}, 1);
|
||||
setTimeout(function() {
|
||||
drawCircle(2);
|
||||
}, 1);
|
||||
setTimeout(function() {
|
||||
drawCircle(3);
|
||||
}, 1);
|
||||
setTimeout(function() {
|
||||
if (circleCount >= 4) drawCircle(4);
|
||||
}, 1);
|
||||
}
|
||||
|
||||
function drawCircle(index) {
|
||||
|
@ -248,6 +245,9 @@ function getGradientColor(color, percent) {
|
|||
const colorList = [
|
||||
'#00FF00', '#80FF00', '#FFFF00', '#FF8000', '#FF0000'
|
||||
];
|
||||
if (color == "fg") {
|
||||
color = colorFg;
|
||||
}
|
||||
if (color == "green-red") {
|
||||
const colorIndex = Math.round(colorList.length * percent);
|
||||
return colorList[Math.min(colorIndex, colorList.length) - 1] || "#00ff00";
|
||||
|
@ -325,6 +325,8 @@ function drawStepsDistance(w) {
|
|||
function drawHeartRate(w) {
|
||||
if (!w) w = getCircleXPosition("hr");
|
||||
|
||||
const heartIcon = atob("EBCBAAAAAAAeeD/8P/x//n/+P/w//B/4D/AH4APAAYAAAAAA");
|
||||
|
||||
drawCircleBackground(w);
|
||||
|
||||
const color = getCircleColor("hr");
|
||||
|
@ -349,6 +351,8 @@ function drawBattery(w) {
|
|||
if (!w) w = getCircleXPosition("battery");
|
||||
const battery = E.getBattery();
|
||||
|
||||
const powerIcon = atob("EBCBAAAAA8ADwA/wD/AP8A/wD/AP8A/wD/AP8A/wD/AH4AAA");
|
||||
|
||||
drawCircleBackground(w);
|
||||
|
||||
let color = getCircleColor("battery");
|
||||
|
@ -426,6 +430,10 @@ function drawSunProgress(w) {
|
|||
if (!w) w = getCircleXPosition("sunprogress");
|
||||
const percent = getSunProgress();
|
||||
|
||||
// sunset icons:
|
||||
const sunSetDown = atob("EBCBAAAAAAABgAAAAAATyAZoBCB//gAAAAAGYAPAAYAAAAAA");
|
||||
const sunSetUp = atob("EBCBAAAAAAABgAAAAAATyAZoBCB//gAAAAABgAPABmAAAAAA");
|
||||
|
||||
drawCircleBackground(w);
|
||||
|
||||
const color = getCircleColor("sunprogress");
|
||||
|
@ -559,6 +567,18 @@ function windAsBeaufort(windInKmh) {
|
|||
*/
|
||||
function getWeatherIconByCode(code) {
|
||||
const codeGroup = Math.round(code / 100);
|
||||
|
||||
// weather icons:
|
||||
const weatherCloudy = atob("EBCBAAAAAAAAAAfgD/Af8H/4//7///////9//z/+AAAAAAAA");
|
||||
const weatherSunny = atob("EBCBAAAAAYAQCBAIA8AH4A/wb/YP8A/gB+ARiBAIAYABgAAA");
|
||||
const weatherMoon = atob("EBCBAAAAAYAP8B/4P/w//D/8f/5//j/8P/w//B/4D/ABgAAA");
|
||||
const weatherPartlyCloudy = atob("EBCBAAAAAAAYQAMAD8AIQBhoW+AOYBwwOBBgHGAGP/wf+AAA");
|
||||
const weatherRainy = atob("EBCBAAAAAYAH4AwwOBBgGEAOQAJBgjPOEkgGYAZgA8ABgAAA");
|
||||
const weatherPartlyRainy = atob("EBCBAAAAEEAQAAeADMAYaFvoTmAMMDgQIBxhhiGGG9wDwAGA");
|
||||
const weatherSnowy = atob("EBCBAAAAAAADwAGAEYg73C50BCAEIC50O9wRiAGAA8AAAAAA");
|
||||
const weatherFoggy = atob("EBCBAAAAAAADwAZgDDA4EGAcQAZAAgAAf74AAAAAd/4AAAAA");
|
||||
const weatherStormy = atob("EBCBAAAAAYAH4AwwOBBgGEAOQMJAgjmOGcgAgACAAAAAAAAA");
|
||||
|
||||
switch (codeGroup) {
|
||||
case 2:
|
||||
return weatherStormy;
|
||||
|
@ -823,7 +843,6 @@ if (isCircleEnabled("hr")) {
|
|||
enableHRMSensor();
|
||||
}
|
||||
|
||||
|
||||
Bangle.setUI("clock");
|
||||
Bangle.loadWidgets();
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{ "id": "circlesclock",
|
||||
"name": "Circles clock",
|
||||
"shortName":"Circles clock",
|
||||
"version":"0.10",
|
||||
"version":"0.11",
|
||||
"description": "A clock with three or four circles for different data at the bottom in a probably familiar style",
|
||||
"icon": "app.png",
|
||||
"screenshots": [{"url":"screenshot-dark.png"}, {"url":"screenshot-light.png"}, {"url":"screenshot-dark-4.png"}, {"url":"screenshot-light-4.png"}],
|
||||
|
|
|
@ -14,8 +14,10 @@
|
|||
const valuesCircleTypes = ["empty", "steps", "stepsDist", "hr", "battery", "weather", "sunprogress", "temperature", "pressure", "altitude"];
|
||||
const namesCircleTypes = ["empty", "steps", "distance", "heart", "battery", "weather", "sun", "temperature", "pressure", "altitude"];
|
||||
|
||||
const valuesColors = ["", "#ff0000", "#00ff00", "#0000ff", "#ffff00", "#ff00ff", "#00ffff", "#fff", "#000", "green-red", "red-green"];
|
||||
const namesColors = ["default", "red", "green", "blue", "yellow", "magenta", "cyan", "white", "black", "green->red", "red->green"];
|
||||
const valuesColors = ["", "#ff0000", "#00ff00", "#0000ff", "#ffff00", "#ff00ff",
|
||||
"#00ffff", "#fff", "#000", "green-red", "red-green", "fg"];
|
||||
const namesColors = ["default", "red", "green", "blue", "yellow", "magenta",
|
||||
"cyan", "white", "black", "green->red", "red->green", "foreground"];
|
||||
|
||||
const weatherData = ["empty", "humidity", "wind"];
|
||||
|
||||
|
|
|
@ -5,3 +5,4 @@
|
|||
0.23: Customizer! Unused fonts no longer take up precious memory.
|
||||
0.24: Added previews to the customizer.
|
||||
0.25: Fixed a bug that would let widgets change the color of the clock.
|
||||
0.26: Time formatted to locale
|
||||
|
|
|
@ -1,3 +1,11 @@
|
|||
var is12;
|
||||
function getHours(d) {
|
||||
var h = d.getHours();
|
||||
if (is12===undefined) is12 = (require('Storage').readJSON('setting.json',1)||{})["12hour"];
|
||||
if (!is12) return h;
|
||||
return (h%12==0) ? 12 : h%12;
|
||||
}
|
||||
|
||||
exports.drawClock = function(fontIndex) {
|
||||
var digits = [];
|
||||
fontFile=require("Storage").read("contourclock-"+Math.abs(parseInt(fontIndex+0.5))+".json");
|
||||
|
@ -15,8 +23,8 @@ exports.drawClock = function(fontIndex) {
|
|||
var y = g.getHeight()/2-digits[0].height/2;
|
||||
var date = new Date();
|
||||
g.clearRect(0,38,g.getWidth()-1,138);
|
||||
d1=parseInt(date.getHours()/10);
|
||||
d2=parseInt(date.getHours()%10);
|
||||
d1=parseInt(getHours(date)/10);
|
||||
d2=parseInt(getHours(date)%10);
|
||||
d3=10;
|
||||
d4=parseInt(date.getMinutes()/10);
|
||||
d5=parseInt(date.getMinutes()%10);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{ "id": "contourclock",
|
||||
"name": "Contour Clock",
|
||||
"shortName" : "Contour Clock",
|
||||
"version":"0.25",
|
||||
"version":"0.26",
|
||||
"icon": "app.png",
|
||||
"description": "A Minimalist clockface with large Digits. Now with more fonts!",
|
||||
"screenshots" : [{"url":"cc-screenshot-1.png"},{"url":"cc-screenshot-2.png"}],
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
0.01: first release
|
||||
0.02: added settings menu to change color
|
||||
0.03: added heart rate which is switched on when cycled to it through up/down touch on rhs
|
||||
0.03: fix metadata.json to allow setting as clock
|
||||
0.04: added heart rate which is switched on when cycled to it through up/down touch on rhs
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
{ "id": "daisy",
|
||||
"name": "Daisy",
|
||||
"version":"0.03",
|
||||
"version":"0.04",
|
||||
"dependencies": {"mylocation":"app"},
|
||||
"description": "A clock based on the Pastel clock with large ring guage for steps",
|
||||
"icon": "app.png",
|
||||
"type": "clock",
|
||||
"tags": "clock",
|
||||
"supports" : ["BANGLEJS2"],
|
||||
"screenshots": [{"url":"screenshot_daisy2.jpg"}],
|
||||
|
|
|
@ -6,3 +6,5 @@
|
|||
0.06: Adds settings page (hide clocks or launchers)
|
||||
0.07: Adds setting for directly launching app on touch for Bangle 2
|
||||
0.08: Optimize line wrapping for Bangle 2
|
||||
0.09: fix the trasparent widget bar if there are no widgets for Bangle 2
|
||||
0.10: added "one click exit" setting for Bangle 2
|
||||
|
|
|
@ -6,8 +6,12 @@ var settings = Object.assign({
|
|||
showClocks: true,
|
||||
showLaunchers: true,
|
||||
direct: false,
|
||||
oneClickExit:false
|
||||
}, require('Storage').readJSON("dtlaunch.json", true) || {});
|
||||
|
||||
if( settings.oneClickExit)
|
||||
setWatch(_=> load(), BTN1);
|
||||
|
||||
var s = require("Storage");
|
||||
var apps = s.list(/\.info$/).map(app=>{
|
||||
var a=s.readJSON(app,1);
|
||||
|
@ -125,5 +129,6 @@ Bangle.on("touch",(_,p)=>{
|
|||
});
|
||||
|
||||
Bangle.loadWidgets();
|
||||
g.clear();
|
||||
Bangle.drawWidgets();
|
||||
drawPage(0);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "dtlaunch",
|
||||
"name": "Desktop Launcher",
|
||||
"version": "0.08",
|
||||
"version": "0.10",
|
||||
"description": "Desktop style App Launcher with six (four for Bangle 2) apps per page - fast access if you have lots of apps installed.",
|
||||
"screenshots": [{"url":"shot1.png"},{"url":"shot2.png"},{"url":"shot3.png"}],
|
||||
"icon": "icon.png",
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
var settings = Object.assign({
|
||||
showClocks: true,
|
||||
showLaunchers: true,
|
||||
direct: false
|
||||
direct: false,
|
||||
oneClickExit:false
|
||||
}, require('Storage').readJSON(FILE, true) || {});
|
||||
|
||||
function writeSettings() {
|
||||
|
@ -37,6 +38,14 @@
|
|||
settings.direct = v;
|
||||
writeSettings();
|
||||
}
|
||||
},
|
||||
'One click exit': {
|
||||
value: settings.oneClickExit,
|
||||
format: v => v?"On":"Off",
|
||||
onchange: v => {
|
||||
settings.oneClickExit = v;
|
||||
writeSettings();
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
0.01: Initial version
|
||||
0.02: Added BangleJS Two
|
|
@ -1,4 +1,4 @@
|
|||
The application is based on a technique that Thomas Edison used to prevent falling asleep using a steel ball. Essentially the app starts with a display that shows the current HR value that the watch alarm is set to and this can be adjusted with buttons 1 and 3. This HR settng should be the approximate value you want the alarm to trigger and so you should ideally know both what your HR is currently and what your heartrate normally is during sleep. For your current HR according to the watch, you can simply use the HR monitor available in the Espruino app loader, and then from that you can choose a lower value as the target for the alarm and adjust as required.
|
||||
The application is based on a technique that Thomas Edison used to prevent falling asleep using a steel ball. Essentially the app starts with a display that shows the current HR value that the watch alarm is set to and this can be adjusted with buttons 1 and 3 (Mapped to top touch and bottom touch on Bangle 2). This HR settng should be the approximate value you want the alarm to trigger and so you should ideally know both what your HR is currently and what your heartrate normally is during sleep. For your current HR according to the watch, you can simply use the HR monitor available in the Espruino app loader, and then from that you can choose a lower value as the target for the alarm and adjust as required.
|
||||
|
||||
When you press the middle button on the side, the HR monitor starts, the alarm will trigger when your heart rate average drops to the limit you’ve set and has a certain level of steadiness that is determined by a assessing the variance over several readings - the sensitivity of this variance can be adjusted in a variable in the app's code under 'ADVANCED SETTINGS' if needed. The code also has a basic logging function which shows, in a CSV file, when you started the HR tracker and when the alarm was triggered.
|
||||
|
||||
|
|
|
@ -3,6 +3,8 @@ var lower_limit_BPM = 49;
|
|||
var upper_limit_BPM = 140;
|
||||
var deviation_threshold = 3;
|
||||
|
||||
var ISBANGLEJS1 = process.env.HWVERSION==1;
|
||||
|
||||
var target_heartrate = 70;
|
||||
var heartrate_set;
|
||||
|
||||
|
@ -33,8 +35,8 @@ function btn2Pressed() {
|
|||
}
|
||||
|
||||
function update_target_HR(){
|
||||
|
||||
g.clear();
|
||||
if (process.env.HWVERSION==1) {
|
||||
g.setColor("#00ff7f");
|
||||
g.setFont("6x8", 4);
|
||||
g.setFontAlign(0,0); // center font
|
||||
|
@ -52,6 +54,20 @@ function update_target_HR(){
|
|||
|
||||
g.setFont("6x8", 1);
|
||||
g.drawString("if unsure, start with 7-10%\n less than waking average and\n adjust as required", 120,170);
|
||||
} else {
|
||||
g.setFont("6x8", 4);
|
||||
g.setFontAlign(0,0); // center font
|
||||
g.drawString(target_heartrate, 88,88);
|
||||
g.setFont("6x8", 2);
|
||||
g.setFontAlign(-1,-1);
|
||||
g.drawString("-", 160, 160);
|
||||
g.drawString("+", 160, 10);
|
||||
g.drawString("GO", 150, 88);
|
||||
g.setFontAlign(0,0); // center font
|
||||
g.drawString("target HR", 88,65);
|
||||
g.setFont("6x8", 1);
|
||||
g.drawString("if unsure, start with 7-10%\n less than waking average and\n adjust as required", 88,150);
|
||||
}
|
||||
|
||||
g.setFont("6x8",3);
|
||||
g.flip();
|
||||
|
@ -105,8 +121,13 @@ function checkHR() {
|
|||
average_HR = average(HR_samples).toFixed(0);
|
||||
stdev_HR = getStandardDeviation (HR_samples).toFixed(1);
|
||||
|
||||
if (ISBANGLEJS1) {
|
||||
g.drawString("HR: " + average_HR, 120,100);
|
||||
g.drawString("STDEV: " + stdev_HR, 120,160);
|
||||
} else {
|
||||
g.drawString("HR: " + average_HR, 88,60);
|
||||
g.drawString("STDEV: " + stdev_HR, 88,90);
|
||||
}
|
||||
HR_samples = [];
|
||||
if(average_HR < target_heartrate && stdev_HR < deviation_threshold){
|
||||
|
||||
|
@ -131,12 +152,26 @@ function checkHR() {
|
|||
|
||||
update_target_HR();
|
||||
|
||||
if (ISBANGLEJS1) {
|
||||
// Bangle 1
|
||||
setWatch(btn1Pressed, BTN1, {repeat:true});
|
||||
setWatch(btn2Pressed, BTN2, {repeat:true});
|
||||
setWatch(btn3Pressed, BTN3, {repeat:true});
|
||||
} else {
|
||||
// Bangle 2
|
||||
setWatch(btn2Pressed, BTN1, { repeat: true });
|
||||
Bangle.on('touch', function(zone, e) {
|
||||
if (e.y < g.getHeight() / 2) {
|
||||
btn1Pressed();
|
||||
}
|
||||
if (e.y > g.getHeight() / 2) {
|
||||
btn3Pressed();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Bangle.on('HRM',function(hrm) {
|
||||
|
||||
if(trigger_count < 2){
|
||||
if (firstBPM)
|
||||
firstBPM=false; // ignore the first one as it's usually rubbish
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
"id": "edisonsball",
|
||||
"name": "Edison's Ball",
|
||||
"shortName": "Edison's Ball",
|
||||
"version": "0.01",
|
||||
"version": "0.02",
|
||||
"description": "Hypnagogia/Micro-Sleep alarm for experimental use in exploring sleep transition and combating drowsiness",
|
||||
"icon": "app-icon.png",
|
||||
"tags": "",
|
||||
"supports": ["BANGLEJS"],
|
||||
"tags": "sleep,hyponagogia,quick,nap",
|
||||
"supports": ["BANGLEJS", "BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
"storage": [
|
||||
{"name":"edisonsball.app.js","url":"app.js"},
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
0.01: New App!
|
||||
0.02: Set Bangle.js 2 compatible
|
||||
|
|
|
@ -2,12 +2,12 @@
|
|||
"id": "gpsautotime",
|
||||
"name": "GPS auto time",
|
||||
"shortName": "GPS auto time",
|
||||
"version": "0.01",
|
||||
"version": "0.02",
|
||||
"description": "A widget that automatically updates the Bangle.js time to the GPS time whenever there is a valid GPS fix.",
|
||||
"icon": "widget.png",
|
||||
"type": "widget",
|
||||
"tags": "widget,gps",
|
||||
"supports": ["BANGLEJS"],
|
||||
"supports": ["BANGLEJS","BANGLEJS2"],
|
||||
"storage": [
|
||||
{"name":"gpsautotime.wid.js","url":"widget.js"}
|
||||
]
|
||||
|
|
|
@ -75,9 +75,13 @@ function getPosImage() {
|
|||
function getNegImage() {
|
||||
return atob("FhaBADAAMeAB78AP/4B/fwP4/h/B/P4D//AH/4AP/AAf4AB/gAP/AB/+AP/8B/P4P4fx/A/v4B//AD94AHjAAMA=");
|
||||
}
|
||||
/*
|
||||
* icons should be 24x24px with 1bpp colors and transparancy
|
||||
*/
|
||||
function getMessageImage(msg) {
|
||||
if (msg.img) return atob(msg.img);
|
||||
var s = (msg.src||"").toLowerCase();
|
||||
if (s=="alarm" || s =="alarmclockreceiver") return atob("GBjBAP////8AAAAAAAACAEAHAOAefng5/5wTgcgHAOAOGHAMGDAYGBgYGBgYGBgYGBgYDhgYBxgMATAOAHAHAOADgcAB/4AAfgAAAAAAAAA=");
|
||||
if (s=="calendar") return atob("GBiBAAAAAAAAAAAAAA//8B//+BgAGBgAGBgAGB//+B//+B//+B9m2B//+B//+Btm2B//+B//+Btm+B//+B//+A//8AAAAAAAAAAAAA==");
|
||||
if (s=="facebook") return getFBIcon();
|
||||
if (s=="hangouts") return atob("FBaBAAH4AH/gD/8B//g//8P//H5n58Y+fGPnxj5+d+fmfj//4//8H//B//gH/4A/8AA+AAHAABgAAAA=");
|
||||
|
@ -104,6 +108,7 @@ function getMessageImage(msg) {
|
|||
function getMessageImageCol(msg,def) {
|
||||
return {
|
||||
// generic colors, using B2-safe colors
|
||||
"alarm": "#fff",
|
||||
"calendar": "#f00",
|
||||
"mail": "#ff0",
|
||||
"music": "#f0f",
|
||||
|
|
|
@ -353,13 +353,15 @@ const events = {
|
|||
let c, p, i, l = from - o, h = to - o;
|
||||
for (i = 0; (c = this.wall[i]).time < l; i++) ;
|
||||
for (; (c = this.wall[i]).time < h; i++) {
|
||||
if ((p = c.time < t) ? c.past : c.future)
|
||||
p = c.time < t;
|
||||
if (p ? c.past : c.future)
|
||||
result = Math.min(result, f(c, new Date(c.time + o), p));
|
||||
}
|
||||
l += o; h += o; t += o;
|
||||
for (i = 0; (c = this.fixed[i]).time < l; i++) ;
|
||||
for (; (c = this.fixed[i]).time < h; i++) {
|
||||
if ((p = c.time < t) ? c.past : c.future)
|
||||
p = c.time < t;
|
||||
if (p ? c.past : c.future)
|
||||
result = Math.min(f(c, new Date(c.time), p));
|
||||
}
|
||||
return result;
|
||||
|
|
|
@ -60,7 +60,8 @@ const prepFont = (name, data) => {
|
|||
let width = m[2] == '*' ? null : +m[2];
|
||||
let c = null, o = 0;
|
||||
lines.forEach((line, l) => {
|
||||
if (m = /^(<*)(=)([*\d]*)(=*)(>*)$/.exec(line) || /^(<*)(-)(.)(-*)(>*)$/.exec(line)) {
|
||||
m = /^(<*)(=)([*\d]*)(=*)(>*)$/.exec(line) || /^(<*)(-)(.)(-*)(>*)$/.exec(line);
|
||||
if (m) {
|
||||
const h = m[2] == '=';
|
||||
if (m[1].length > desc || h && m[1].length != desc)
|
||||
throw new Error('Invalid descender height at ' + l);
|
||||
|
|
|
@ -60,7 +60,8 @@ const prepFont = (name, data) => {
|
|||
let width = m[2] == '*' ? null : +m[2];
|
||||
let c = null, o = 0;
|
||||
lines.forEach((line, l) => {
|
||||
if (m = /^(<*)(=)([*\d]*)(=*)(>*)$/.exec(line) || /^(<*)(-)(.)(-*)(>*)$/.exec(line)) {
|
||||
m = /^(<*)(=)([*\d]*)(=*)(>*)$/.exec(line) || /^(<*)(-)(.)(-*)(>*)$/.exec(line);
|
||||
if (m) {
|
||||
const h = m[2] == '=';
|
||||
if (m[1].length > desc || h && m[1].length != desc)
|
||||
throw new Error('Invalid descender height at ' + l);
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
0.01: Initial Release
|
||||
0.02: Minor tweaks for light theme
|
||||
0.03: Made images 2 bit and fixed theme honoring
|
||||
0.04: Fixed date font alignment and changed date font to match a real Rolex
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
# Rolex
|
||||
|
||||

|
||||
 
|
||||
|
||||
Created with the aid of the Espruino documentation and looking through many of the wonderful exising watchfaces that have been made.
|
||||
This has not been tested on a watch yet as I haven't aquired one but has been tested in the emulator.
|
||||
The hands don't rotate dead on center but they're as close as I could get them to.
|
||||
|
||||
Colour switches based on watch theme so if you want a white on black change your watch theme to dark, and for black on white change it to light.
|
||||
|
||||
Special thanks to:
|
||||
* rozek (for his updated widget draw code for utilization with background images)
|
||||
* Gordon Williams (Bangle.js, watchapps for reference code and documentation)
|
||||
|
|
|
@ -28,6 +28,14 @@ var imgSec = {
|
|||
buffer : E.toArrayBuffer(atob("v/q//r/+v/qv+q/qq+qr6qvqq+qr6qvqq+qr6qvqq+qr6qvqq+qr6qvqq+qr6qvqq+qr6qvqq+qv+v/6/D/wD8PDw8PwD/w///6v+qvqq+qr6qvqq+qr6qvqq+qr6qvqq+qr6qvqq+qr6qvqq+qr6qvqq+qr6qvqq+qr6qvqq+qr6qvqq+qr6qvqq+qr6qvqq+qr6qvqr+r//v///X/1X/Vf9V/1X/1///+//q/qq+qr6qvqq+qr6qvqq+qr6qvqq+qr6qvqq+qr6qvqq+qr6qvqq+qr6qvqq+qr6qvqq+qr6qvqq6qrqg=="))
|
||||
};
|
||||
|
||||
/* use font closer to Rolex */
|
||||
|
||||
Graphics.prototype.setFontRolexFont = function(scale) {
|
||||
// Actual height 12 (12 - 1)
|
||||
this.setFontCustom(atob("AAAABAACAAAAAYAHgA4AOABgAAAAA/gD/gMBgQBAwGA/4A/gAAAAAAIBAQCB/8D/4AAQAAAAAAAAAwEDAYEBQIEgYxA/CA4MAAAAAAAAgIBAhCBCEDOYHvgCOAAAAAAABgANAAyAHEAf/A/+AAgABAAAAAAADBAcCBsECYIEYgIeAAAAAAAHwAfwB5wGggZBAjGBH4CDgAAACAAYAAgABAMCDwE+APgAYAAAAAAABxwH3wJwgRhA3iB54AhgAAAAAAPhA/iBDMCCQGHgH+AHwAAAAAAAQIAgQAAAAAA="), 46, atob("BAUJCQkJCQkJCQkJBQ=="), 17+(scale<<8)+(1<<16));
|
||||
return this;
|
||||
};
|
||||
|
||||
/* Set variables to get screen width, height and center points */
|
||||
|
||||
let W = g.getWidth();
|
||||
|
@ -36,10 +44,6 @@ let cx = W/2;
|
|||
let cy = H/2;
|
||||
let Timeout;
|
||||
|
||||
/* set font */
|
||||
|
||||
require("Font4x5Numeric").add(Graphics);
|
||||
|
||||
Bangle.loadWidgets();
|
||||
|
||||
/* Custom version of Bangle.drawWidgets (does not clear the widget areas) Thanks to rozek */
|
||||
|
@ -106,9 +110,10 @@ function drawHands() {
|
|||
g.drawImage(imgHour,cx-22*hourSin,cy+22*hourCos,{rotate:hourAngle});
|
||||
g.drawImage(imgMin,cx-34*minSin,cy+34*minCos,{rotate:minAngle});
|
||||
g.drawImage(imgSec,cx-25*secSin,cy+25*secCos,{rotate:secAngle});
|
||||
g.setFont("4x5Numeric:3");
|
||||
g.setFontRolexFont();
|
||||
g.setColor(g.theme.bg);
|
||||
g.drawString(d.getDate(),157,81);
|
||||
g.setFontAlign(0,0,0);
|
||||
g.drawString(d.getDate(),165,89);
|
||||
}
|
||||
|
||||
function drawBackground() {
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
"name": "rolex",
|
||||
"shortName":"rolex",
|
||||
"icon": "rolex.png",
|
||||
"screenshots": [{"url":"screenshot.png"}],
|
||||
"version":"0.03",
|
||||
"screenshots": [{"url":"screenshot1.png"}],
|
||||
"version":"0.04",
|
||||
"description": "A rolex like watch face",
|
||||
"tags": "clock",
|
||||
"type": "clock",
|
||||
|
|
After Width: | Height: | Size: 3.8 KiB |
|
@ -46,8 +46,8 @@ record GPS/HRM/etc data every time you start a run?
|
|||
|
||||
## Development
|
||||
|
||||
This app uses the [`exstats` module](/modules/exstats.js). When uploaded via the
|
||||
This app uses the [`exstats` module](https://github.com/espruino/BangleApps/blob/master/modules/exstats.js). When uploaded via the
|
||||
app loader, the module is automatically included in the app's source. However
|
||||
when developing via the IDE the module won't get pulled in by default.
|
||||
|
||||
There are some options to fix this easily - please check out the [modules README.md file](/modules/README.md)
|
||||
There are some options to fix this easily - please check out the [modules README.md file](https://github.com/espruino/BangleApps/blob/master/modules/README.md)
|
||||
|
|
|
@ -1 +1 @@
|
|||
require("heatshrink").decompress(atob("oFA4X/AAOJksvr2rmokYgWqB7sq/2AB5krgYPMgW8ioPc1X9i/oLplVqv+1BdK1OV//q9QPMv4PL1eqy/q1SRK3tVu+AgWCFxP96t+Vhn9qoPLgWr/+//wFBSBEq3/qlW+JwJ/I3eXDQIOBB5OrB5sC3xMD1WAH4+r6xsOtSpKLoYPN1fV1bpKTYf+RJAeDytXFxoPOdQYPNPpkCy1VtQPc6wvO62Vu+CbhfVN4P//+q//uMgwPH9QPH3tqqtpqoABv4wHfoOpBoP/6tVUg7uBFwIvB3xlIB4v+OpJsC1WA1fVQpiGCB52+uzlMB58A31XB5sqy4PNlYPfH50rywPN3++BxgPPgW9V5kCZ4L/HBwmq/tX1APM/4PMBwNVvxuKgW/tP/HxUq1X+1eqFxQPRAAKsLB4KqNAFY="))
|
||||
require("heatshrink").decompress(atob("mEwwcBkmSpICZqVECJ+SCJ+UxIRP0lIggRNlckxFICJlKrYGCsmJNBfZsmSpdkyIRL7YRBAwIRLAYQyLNAQRPkoGDCJlLBwQmBAoZ6HBYI4Cy3ZCJVZCITpLymSCIhWMF4IRMlMky3JkTRMAYTjNqREBCJ4DCX5gRD2IRO5MlCKAjQRgOSkslBIRrMUoO2fZVSpdkyQ4BBIWRUJNtBIzpHWYYCCpYRJa4RWDEZQCH0oR0yuyCJ+UCKI1QEaOkCKI1PAYQgMyQDDNBwDBxIRLdgOydgQRKqVJloROyVLthWOpQONAUIA="))
|
||||
|
|
|
@ -179,7 +179,8 @@ var buf = Graphics.createArrayBuffer(240,160,2,{msb:true});
|
|||
let LED = // LED as minimal and only definition (as instance / singleton)
|
||||
{ isOn: false // status on / off, not needed if you don't need to ask for it
|
||||
, set: function(v) { // turn on w/ no arg or truey, else off
|
||||
g.setColor((this.isOn=(v===undefined||!!v))?1:0,0,0).fillCircle(120,10,10); }
|
||||
this.isOn = v===undefined||!!v;
|
||||
g.setColor(this.isOn?1:0,0,0).fillCircle(120,10,10); }
|
||||
, reset: function() { this.set(false); } // turn off
|
||||
, write: function(v) { this.set(v); } // turn on w/ no arg or truey, else off
|
||||
, toggle: function() { this.set( ! this.isOn); } // toggle the LED
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
0.01: New App!
|
||||
0.02: Rename "Activity" in "Motion" and display the true values for it
|
||||
|
|
|
@ -5,5 +5,5 @@ It can display :
|
|||
- time
|
||||
- date
|
||||
- hrm
|
||||
- activity
|
||||
- motion
|
||||
- steps
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
var locale = require("locale");
|
||||
var fontColor = g.theme.dark ? "#0f0" : "#000";
|
||||
var startY = 24;
|
||||
var paddingY = 2;
|
||||
var font6x8At4Size = 32;
|
||||
var font6x8At2Size = 18;
|
||||
|
@ -15,25 +14,25 @@ function setFontSize(pos){
|
|||
}
|
||||
|
||||
function clearField(pos){
|
||||
var yStartPos = startY +
|
||||
var yStartPos = Bangle.appRect.y +
|
||||
paddingY * (pos - 1) +
|
||||
font6x8At4Size * Math.min(1, pos-1) +
|
||||
font6x8At2Size * Math.max(0, pos-2);
|
||||
var yEndPos = startY +
|
||||
var yEndPos = Bangle.appRect.y +
|
||||
paddingY * (pos - 1) +
|
||||
font6x8At4Size * Math.min(1, pos) +
|
||||
font6x8At2Size * Math.max(0, pos-1);
|
||||
g.clearRect(0, yStartPos, 240, yEndPos);
|
||||
g.clearRect(Bangle.appRect.x, yStartPos, Bangle.appRect.x2, yEndPos);
|
||||
}
|
||||
|
||||
function clearWatchIfNeeded(now){
|
||||
if(now.getMinutes() % 10 == 0)
|
||||
g.clearRect(0, startY, 240, 240);
|
||||
g.clearRect(Bangle.appRect.x, Bangle.appRect.y, Bangle.appRect.x2, Bangle.appRect.y2);
|
||||
}
|
||||
|
||||
function drawLine(line, pos){
|
||||
setFontSize(pos);
|
||||
var yPos = startY +
|
||||
var yPos = Bangle.appRect.y +
|
||||
paddingY * (pos - 1) +
|
||||
font6x8At4Size * Math.min(1, pos-1) +
|
||||
font6x8At2Size * Math.max(0, pos-2);
|
||||
|
@ -76,11 +75,10 @@ function drawHRM(pos){
|
|||
function drawActivity(pos){
|
||||
clearField(pos);
|
||||
var health = Bangle.getHealthStatus('last');
|
||||
var steps_formated = ">Activity: " + parseInt(health.movement/10);
|
||||
var steps_formated = ">Motion: " + parseInt(health.movement);
|
||||
drawLine(steps_formated, pos);
|
||||
}
|
||||
|
||||
|
||||
function draw(){
|
||||
var curPos = 1;
|
||||
g.reset();
|
||||
|
@ -109,7 +107,6 @@ function draw(){
|
|||
drawInput(now, curPos);
|
||||
}
|
||||
|
||||
|
||||
Bangle.on('HRM',function(hrmInfo) {
|
||||
if(hrmInfo.confidence >= settings.HRMinConfidence)
|
||||
heartRate = hrmInfo.bpm;
|
||||
|
@ -127,12 +124,12 @@ var settings = Object.assign({
|
|||
showActivity: true,
|
||||
showStepCount: true,
|
||||
}, require('Storage').readJSON("terminalclock.json", true) || {});
|
||||
// draw immediately at first
|
||||
draw();
|
||||
// Show launcher when middle button pressed
|
||||
Bangle.setUI("clock");
|
||||
// Load widgets
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
// draw immediately at first
|
||||
draw();
|
||||
|
||||
var secondInterval = setInterval(draw, 10000);
|
||||
|
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 2.9 KiB |
|
@ -1,18 +1,26 @@
|
|||
{
|
||||
"id": "timeandlife",
|
||||
"name": "Time and Life",
|
||||
"shortName":"Time and Lfie",
|
||||
"shortName": "Time and Life",
|
||||
"icon": "app.png",
|
||||
"version": "0.01",
|
||||
"description": "A simple watchface which displays the time when the screen is tapped and decay according to the rules of Conway's game of life.",
|
||||
"type": "clock",
|
||||
"tags": "clock",
|
||||
"supports": ["BANGLEJS2"],
|
||||
"supports": [
|
||||
"BANGLEJS2"
|
||||
],
|
||||
"allow_emulator": true,
|
||||
"readme": "README.md",
|
||||
"storage": [
|
||||
{"name":"timeandlife.app.js","url":"app.js"},
|
||||
{"name":"timeandlife.img","url":"app-icon.js","evaluate":true}
|
||||
{
|
||||
"name": "timeandlife.app.js",
|
||||
"url": "app.js"
|
||||
},
|
||||
{
|
||||
"name": "timeandlife.img",
|
||||
"url": "app-icon.js",
|
||||
"evaluate": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
@ -71,7 +71,6 @@ function chooseIconByCode(code) {
|
|||
case 801: return partSunIcon;
|
||||
default: return cloudIcon;
|
||||
}
|
||||
break;
|
||||
default: return cloudIcon;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
0.01: First release.
|
||||
0.02: No functional changes, just moved codebase to Typescript.
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"name": "Charging Status",
|
||||
"shortName":"ChargingStatus",
|
||||
"icon": "widget.png",
|
||||
"version":"0.01",
|
||||
"version":"0.02",
|
||||
"type": "widget",
|
||||
"description": "A simple widget that shows a yellow lightning icon to indicate whenever the watch is charging. This way one can see the charging status at a glance, no matter which battery widget is being used.",
|
||||
"tags": "widget",
|
||||
|
|
|
@ -1,31 +1,33 @@
|
|||
"use strict";
|
||||
(() => {
|
||||
const icon = require("heatshrink").decompress(atob("ikggMAiEAgYIBmEAg4EB+EAh0AgPggEeCAIEBnwQBAgP+gEP//x///j//8f//k///H//4BYOP/4lBv4bDvwEB4EAvAEBwEAuA7DCAI7BgAQBhEAA"));
|
||||
const icon = require('heatshrink').decompress(atob('ikggMAiEAgYIBmEAg4EB+EAh0AgPggEeCAIEBnwQBAgP+gEP//x///j//8f//k///H//4BYOP/4lBv4bDvwEB4EAvAEBwEAuA7DCAI7BgAQBhEAA'));
|
||||
const iconWidth = 18;
|
||||
|
||||
function draw() {
|
||||
g.reset();
|
||||
if (Bangle.isCharging()) {
|
||||
g.setColor("#FD0");
|
||||
g.setColor('#FD0');
|
||||
g.drawImage(icon, this.x + 1, this.y + 1, {
|
||||
scale: 0.6875
|
||||
scale: 0.6875,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
WIDGETS.chargingStatus = {
|
||||
area: 'tr',
|
||||
width: Bangle.isCharging() ? iconWidth : 0,
|
||||
draw: draw,
|
||||
};
|
||||
|
||||
Bangle.on('charging', (charging) => {
|
||||
const widget = WIDGETS.chargingStatus;
|
||||
if (widget) {
|
||||
if (charging) {
|
||||
Bangle.buzz();
|
||||
WIDGETS.chargingStatus.width = iconWidth;
|
||||
} else {
|
||||
WIDGETS.chargingStatus.width = 0;
|
||||
widget.width = iconWidth;
|
||||
}
|
||||
else {
|
||||
widget.width = 0;
|
||||
}
|
||||
Bangle.drawWidgets(); // re-layout widgets
|
||||
g.flip();
|
||||
}
|
||||
});
|
||||
})();
|
|
@ -0,0 +1,38 @@
|
|||
(() => {
|
||||
const icon = require('heatshrink').decompress(
|
||||
atob(
|
||||
'ikggMAiEAgYIBmEAg4EB+EAh0AgPggEeCAIEBnwQBAgP+gEP//x///j//8f//k///H//4BYOP/4lBv4bDvwEB4EAvAEBwEAuA7DCAI7BgAQBhEAA'
|
||||
)
|
||||
);
|
||||
const iconWidth = 18;
|
||||
|
||||
function draw(this: { x: number; y: number }) {
|
||||
g.reset();
|
||||
if (Bangle.isCharging()) {
|
||||
g.setColor('#FD0');
|
||||
g.drawImage(icon, this.x + 1, this.y + 1, {
|
||||
scale: 0.6875,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
WIDGETS.chargingStatus = {
|
||||
area: 'tr',
|
||||
width: Bangle.isCharging() ? iconWidth : 0,
|
||||
draw: draw,
|
||||
};
|
||||
|
||||
Bangle.on('charging', (charging) => {
|
||||
const widget = WIDGETS.chargingStatus;
|
||||
if (widget) {
|
||||
if (charging) {
|
||||
Bangle.buzz();
|
||||
widget.width = iconWidth;
|
||||
} else {
|
||||
widget.width = 0;
|
||||
}
|
||||
Bangle.drawWidgets(); // re-layout widgets
|
||||
g.flip();
|
||||
}
|
||||
});
|
||||
})();
|
|
@ -0,0 +1,57 @@
|
|||
# Adjust Clock
|
||||
|
||||
Adjusts clock continually in the background to counter clock drift.
|
||||
|
||||
## Usage
|
||||
|
||||
First you need to determine the clock drift of your watch in PPM (parts per million).
|
||||
|
||||
For example if you measure that your watch clock is too fast by 5 seconds in 24 hours,
|
||||
then PPM is `5 / (24*60*60) * 1000000 = 57.9`.
|
||||
|
||||
Then set PPM in settings and this widget will continually adjust the clock by that amount.
|
||||
|
||||
## Settings
|
||||
|
||||
See **Basic logic** below for more details.
|
||||
|
||||
- **PPM x 10** - change PPM in steps of 10
|
||||
- **PPM x 1** - change PPM in steps of 1
|
||||
- **PPM x 0.1** - change PPM in steps of 0.1
|
||||
- **Update Interval** - How often to update widget and clock error.
|
||||
- **Threshold** - Threshold for adjusting clock.
|
||||
When clock error exceeds this threshold, clock is adjusted with `setTime`.
|
||||
- **Save State** - If `On` clock error state is saved to file when widget exits, if needed.
|
||||
That is recommended and default setting.
|
||||
If `Off` clock error state is forgotten and reset to 0 whenever widget is restarted,
|
||||
for example when going to Launcher. This can cause significant inaccuracy especially
|
||||
with large **Update Interval** or **Threshold**.
|
||||
- **Debug Log** - If `On` some debug information is logged to file `widadjust.log`.
|
||||
|
||||
## Display
|
||||
|
||||
Widget shows clock error in milliseconds and PPM.
|
||||
|
||||
## Basic logic
|
||||
|
||||
- When widget starts, clock error state is loaded from file `widadjust.state`.
|
||||
- While widget is running, widget display and clock error is updated
|
||||
periodically (**Update Interval**) according to **PPM**.
|
||||
- When clock error exceeds **Threshold** clock is adjusted with `setTime`.
|
||||
- When widget exists, clock error state is saved to file `widadjust.state` if needed.
|
||||
|
||||
## Services
|
||||
|
||||
Other apps/widgets can use `WIDGETS.adjust.now()` to request current adjusted time.
|
||||
To support also case where this widget isn't present, the following code can be used:
|
||||
|
||||
```
|
||||
function adjustedNow() {
|
||||
return WIDGETS.adjust ? WIDGETS.adjust.now() : Date.now();
|
||||
}
|
||||
```
|
||||
|
||||
## Acknowledgment
|
||||
|
||||
Uses [Clock Settings](https://icons8.com/icon/tQvI71EfIWy3/clock-settings)
|
||||
icon by [Icons8](https://icons8.com).
|
After Width: | Height: | Size: 1.4 KiB |
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"id": "widadjust",
|
||||
"name": "Adjust Clock",
|
||||
"icon": "icon.png",
|
||||
"version": "0.01",
|
||||
"description": "Adjusts clock continually in the background to counter clock drift",
|
||||
"type": "widget",
|
||||
"tags": "widget",
|
||||
"supports": [ "BANGLEJS", "BANGLEJS2" ],
|
||||
"readme": "README.md",
|
||||
"storage": [
|
||||
{ "name": "widadjust.wid.js", "url": "widget.js" },
|
||||
{ "name": "widadjust.settings.js", "url": "settings.js" }
|
||||
],
|
||||
"data": [
|
||||
{ "name": "widadjust.json" },
|
||||
{ "name": "widadjust.state" }
|
||||
]
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
(function(back) {
|
||||
const SETTINGS_FILE = 'widadjust.json';
|
||||
const STATE_FILE = 'widadjust.state';
|
||||
|
||||
const DEFAULT_ADJUST_THRESHOLD = 100;
|
||||
let thresholdV = [ 10, 25, 50, 100, 250, 500, 1000 ];
|
||||
|
||||
const DEFAULT_UPDATE_INTERVAL = 60000;
|
||||
let intervalV = [ 10000, 30000, 60000, 180000, 600000, 1800000, 3600000 ];
|
||||
let intervalN = [ "10 s", "30 s", "1 m", "3 m", "10 m", "30 m", "1 h" ];
|
||||
|
||||
let stateFileErased = false;
|
||||
|
||||
let settings = Object.assign({
|
||||
advanced: false,
|
||||
saveState: true,
|
||||
debugLog: false,
|
||||
ppm: 0,
|
||||
adjustThreshold: DEFAULT_ADJUST_THRESHOLD,
|
||||
updateInterval: DEFAULT_UPDATE_INTERVAL,
|
||||
}, require('Storage').readJSON(SETTINGS_FILE, true) || {});
|
||||
|
||||
if (thresholdV.indexOf(settings.adjustThreshold) == -1) {
|
||||
settings.adjustThreshold = DEFAULT_ADJUST_THRESHOLD;
|
||||
}
|
||||
|
||||
if (intervalV.indexOf(settings.updateInterval) == -1) {
|
||||
settings.updateInterval = DEFAULT_UPDATE_INTERVAL;
|
||||
}
|
||||
|
||||
function onPpmChange(v) {
|
||||
settings.ppm = v;
|
||||
mainMenu['PPM x 10' ].value = v;
|
||||
mainMenu['PPM x 1' ].value = v;
|
||||
mainMenu['PPM x 0.1'].value = v;
|
||||
}
|
||||
|
||||
let mainMenu = {
|
||||
'': { 'title' : 'Adjust Clock' },
|
||||
|
||||
'< Back': () => {
|
||||
require('Storage').writeJSON(SETTINGS_FILE, settings);
|
||||
back();
|
||||
},
|
||||
|
||||
/*
|
||||
// NOT FULLY WORKING YET
|
||||
'Mode': {
|
||||
value: settings.advanced,
|
||||
format: v => v ? 'Advanced' : 'Basic',
|
||||
onchange: () => {
|
||||
settings.advanced = !settings.advanced;
|
||||
}
|
||||
},
|
||||
*/
|
||||
|
||||
'PPM x 10' : {
|
||||
value: settings.ppm,
|
||||
format: v => v.toFixed(1),
|
||||
step: 10,
|
||||
onchange : onPpmChange,
|
||||
},
|
||||
|
||||
'PPM x 1' : {
|
||||
value: settings.ppm,
|
||||
format: v => v.toFixed(1),
|
||||
step: 1,
|
||||
onchange : onPpmChange,
|
||||
},
|
||||
|
||||
'PPM x 0.1' : {
|
||||
value: settings.ppm,
|
||||
format: v => v.toFixed(1),
|
||||
step: 0.1,
|
||||
onchange : onPpmChange,
|
||||
},
|
||||
|
||||
'Update Interval': {
|
||||
value: intervalV.indexOf(settings.updateInterval),
|
||||
min: 0,
|
||||
max: intervalV.length - 1,
|
||||
format: v => intervalN[v],
|
||||
onchange: v => {
|
||||
settings.updateInterval = intervalV[v];
|
||||
},
|
||||
},
|
||||
|
||||
'Threshold': {
|
||||
value: thresholdV.indexOf(settings.adjustThreshold),
|
||||
min: 0,
|
||||
max: thresholdV.length - 1,
|
||||
format: v => thresholdV[v] + " ms",
|
||||
onchange: v => {
|
||||
settings.adjustThreshold = thresholdV[v];
|
||||
},
|
||||
},
|
||||
|
||||
'Save State': {
|
||||
value: settings.saveState,
|
||||
format: v => v ? 'On' : 'Off',
|
||||
onchange: () => {
|
||||
settings.saveState = !settings.saveState;
|
||||
if (!settings.saveState && !stateFileErased) {
|
||||
stateFileErased = true;
|
||||
require("Storage").erase(STATE_FILE);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
'Debug Log': {
|
||||
value: settings.debugLog,
|
||||
format: v => v ? 'On' : 'Off',
|
||||
onchange: () => {
|
||||
settings.debugLog = !settings.debugLog;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
E.showMenu(mainMenu);
|
||||
})
|
|
@ -0,0 +1,244 @@
|
|||
(() => {
|
||||
// ======================================================================
|
||||
// CONST
|
||||
|
||||
const DEBUG_LOG_FILE = 'widadjust.log';
|
||||
const SETTINGS_FILE = 'widadjust.json';
|
||||
const STATE_FILE = 'widadjust.state';
|
||||
|
||||
const DEFAULT_ADJUST_THRESHOLD = 100;
|
||||
const DEFAULT_UPDATE_INTERVAL = 60 * 1000;
|
||||
const MIN_INTERVAL = 10 * 1000;
|
||||
|
||||
const MAX_CLOCK_ERROR_FROM_SAVED_STATE = 2000;
|
||||
|
||||
const SAVE_STATE_CLOCK_ERROR_DELTA_THRESHOLD = 1;
|
||||
const SAVE_STATE_CLOCK_ERROR_DELTA_IN_PPM_THRESHOLD = 1;
|
||||
const SAVE_STATE_PPM_DELTA_THRESHOLD = 1;
|
||||
|
||||
// Widget width.
|
||||
const WIDTH = 22;
|
||||
|
||||
// ======================================================================
|
||||
// VARIABLES
|
||||
|
||||
let settings;
|
||||
let saved;
|
||||
|
||||
let lastClockCheckTime = Date.now();
|
||||
let lastClockErrorUpdateTime;
|
||||
|
||||
let clockError;
|
||||
let currentUpdateInterval;
|
||||
let lastPpm = null;
|
||||
|
||||
let debugLogFile = null;
|
||||
|
||||
// ======================================================================
|
||||
// FUNCTIONS
|
||||
|
||||
function clockCheck() {
|
||||
let now = Date.now();
|
||||
let elapsed = now - lastClockCheckTime;
|
||||
lastClockCheckTime = now;
|
||||
|
||||
let prevUpdateInterval = currentUpdateInterval;
|
||||
currentUpdateInterval = settings.updateInterval;
|
||||
setTimeout(clockCheck, lastClockCheckTime + currentUpdateInterval - Date.now());
|
||||
|
||||
// If elapsed time differs a lot from expected,
|
||||
// some other app probably used setTime to change clock significantly.
|
||||
// -> reset clock error since elapsed time can't be trusted
|
||||
if (Math.abs(elapsed - prevUpdateInterval) > 10 * 1000) {
|
||||
// RESET CLOCK ERROR
|
||||
|
||||
clockError = 0;
|
||||
lastClockErrorUpdateTime = now;
|
||||
|
||||
debug(
|
||||
'Looks like some other app used setTime, so reset clockError. (elapsed = ' +
|
||||
elapsed.toFixed(0) + ')'
|
||||
);
|
||||
WIDGETS.adjust.draw();
|
||||
|
||||
} else if (!settings.advanced) {
|
||||
// UPDATE CLOCK ERROR WITHOUT TEMPERATURE COMPENSATION
|
||||
|
||||
updateClockError(settings.ppm);
|
||||
} else {
|
||||
// UPDATE CLOCK ERROR WITH TEMPERATURE COMPENSATION
|
||||
|
||||
Bangle.getPressure().then(d => {
|
||||
let temp = d.temperature;
|
||||
updateClockError(settings.ppm0 + settings.ppm1 * temp + settings.ppm2 * temp * temp);
|
||||
}).catch(e => {
|
||||
WIDGETS.adjust.draw();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function debug(line) {
|
||||
console.log(line);
|
||||
if (debugLogFile !== null) {
|
||||
debugLogFile.write(line + '\n');
|
||||
}
|
||||
}
|
||||
|
||||
function draw() {
|
||||
g.reset().setFont('6x8').setFontAlign(0, 0);
|
||||
g.clearRect(this.x, this.y, this.x + WIDTH - 1, this.y + 23);
|
||||
g.drawString(Math.round(clockError), this.x + WIDTH/2, this.y + 9);
|
||||
|
||||
if (lastPpm !== null) {
|
||||
g.setFont('4x6').setFontAlign(0, 1);
|
||||
g.drawString(lastPpm.toFixed(1), this.x + WIDTH/2, this.y + 23);
|
||||
}
|
||||
}
|
||||
|
||||
function loadSettings() {
|
||||
settings = Object.assign({
|
||||
advanced: false,
|
||||
saveState: true,
|
||||
debugLog: false,
|
||||
ppm: 0,
|
||||
ppm0: 0,
|
||||
ppm1: 0,
|
||||
ppm2: 0,
|
||||
adjustThreshold: DEFAULT_ADJUST_THRESHOLD,
|
||||
updateInterval: DEFAULT_UPDATE_INTERVAL,
|
||||
}, require('Storage').readJSON(SETTINGS_FILE, true) || {});
|
||||
|
||||
if (settings.debugLog) {
|
||||
if (debugLogFile === null) {
|
||||
debugLogFile = require('Storage').open(DEBUG_LOG_FILE, 'a');
|
||||
}
|
||||
} else {
|
||||
debugLogFile = null;
|
||||
}
|
||||
|
||||
settings.updateInterval = Math.max(settings.updateInterval, MIN_INTERVAL);
|
||||
}
|
||||
|
||||
function onQuit() {
|
||||
let now = Date.now();
|
||||
// WIP
|
||||
let ppm = (lastPpm !== null) ? lastPpm : settings.ppm;
|
||||
let updatedClockError = clockError + (now - lastClockErrorUpdateTime) * ppm / 1000000;
|
||||
let save = false;
|
||||
|
||||
if (! settings.saveState) {
|
||||
debug(new Date(now).toISOString() + ' QUIT');
|
||||
|
||||
} else if (saved === undefined) {
|
||||
save = true;
|
||||
debug(new Date(now).toISOString() + ' QUIT & SAVE STATE');
|
||||
|
||||
} else {
|
||||
let elapsedSaved = now - saved.time;
|
||||
let estimatedClockError = saved.clockError + elapsedSaved * saved.ppm / 1000000;
|
||||
|
||||
let clockErrorDelta = updatedClockError - estimatedClockError;
|
||||
let clockErrorDeltaInPpm = clockErrorDelta / elapsedSaved * 1000000;
|
||||
let ppmDelta = ppm - saved.ppm;
|
||||
|
||||
let debugA = new Date(now).toISOString() + ' QUIT';
|
||||
let debugB =
|
||||
'\n> ' + updatedClockError.toFixed(2) + ' - ' + estimatedClockError.toFixed(2) + ' = ' +
|
||||
clockErrorDelta.toFixed(2) + ' (' +
|
||||
clockErrorDeltaInPpm.toFixed(1) + ' PPM) ; ' +
|
||||
ppm.toFixed(1) + ' - ' + saved.ppm.toFixed(1) + ' = ' + ppmDelta.toFixed(1);
|
||||
|
||||
if ((Math.abs(clockErrorDelta) >= SAVE_STATE_CLOCK_ERROR_DELTA_THRESHOLD
|
||||
&& Math.abs(clockErrorDeltaInPpm) >= SAVE_STATE_CLOCK_ERROR_DELTA_IN_PPM_THRESHOLD
|
||||
) || Math.abs(ppmDelta) >= SAVE_STATE_PPM_DELTA_THRESHOLD
|
||||
)
|
||||
{
|
||||
save = true;
|
||||
debug(debugA + ' & SAVE STATE' + debugB);
|
||||
} else {
|
||||
debug(debugA + debugB);
|
||||
}
|
||||
}
|
||||
|
||||
if (save) {
|
||||
require('Storage').writeJSON(STATE_FILE, {
|
||||
counter: (saved === undefined) ? 1 : saved.counter + 1,
|
||||
time: Math.round(now),
|
||||
clockError: Math.round(updatedClockError * 1000) / 1000,
|
||||
ppm: Math.round(ppm * 1000) / 1000,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function updateClockError(ppm) {
|
||||
let now = Date.now();
|
||||
let elapsed = now - lastClockErrorUpdateTime;
|
||||
let drift = elapsed * ppm / 1000000;
|
||||
clockError += drift;
|
||||
lastClockErrorUpdateTime = now;
|
||||
lastPpm = ppm;
|
||||
|
||||
if (Math.abs(clockError) >= settings.adjustThreshold) {
|
||||
let now = Date.now();
|
||||
// Shorter variables are faster to look up and this part is time sensitive.
|
||||
let e = clockError / 1000;
|
||||
setTime(getTime() - e);
|
||||
debug(
|
||||
new Date(now).toISOString() + ' -> ' + ((now / 1000 - e) % 60).toFixed(3) +
|
||||
' SET TIME (' + clockError.toFixed(2) + ')'
|
||||
);
|
||||
clockError = 0;
|
||||
}
|
||||
|
||||
WIDGETS.adjust.draw();
|
||||
}
|
||||
|
||||
// ======================================================================
|
||||
// MAIN
|
||||
|
||||
loadSettings();
|
||||
|
||||
WIDGETS.adjust = {
|
||||
area: 'tr',
|
||||
draw: draw,
|
||||
now: () => {
|
||||
let now = Date.now();
|
||||
// WIP
|
||||
let ppm = (lastPpm !== null) ? lastPpm : settings.ppm;
|
||||
let updatedClockError = clockError + (now - lastClockErrorUpdateTime) * ppm / 1000000;
|
||||
return now - updatedClockError;
|
||||
},
|
||||
width: WIDTH,
|
||||
};
|
||||
|
||||
if (settings.saveState) {
|
||||
saved = require('Storage').readJSON(STATE_FILE, true);
|
||||
}
|
||||
|
||||
let now = Date.now();
|
||||
lastClockErrorUpdateTime = now;
|
||||
if (saved === undefined) {
|
||||
clockError = 0;
|
||||
debug(new Date().toISOString() + ' START');
|
||||
} else {
|
||||
clockError = saved.clockError + (now - saved.time) * saved.ppm / 1000000;
|
||||
|
||||
if (Math.abs(clockError) <= MAX_CLOCK_ERROR_FROM_SAVED_STATE) {
|
||||
debug(
|
||||
new Date().toISOString() + ' START & LOAD STATE (' +
|
||||
clockError.toFixed(2) + ')'
|
||||
);
|
||||
} else {
|
||||
debug(
|
||||
new Date().toISOString() + ' START & IGNORE STATE (' +
|
||||
clockError.toFixed(2) + ')'
|
||||
);
|
||||
clockError = 0;
|
||||
}
|
||||
}
|
||||
|
||||
clockCheck();
|
||||
|
||||
E.on('kill', onQuit);
|
||||
|
||||
})()
|
|
@ -0,0 +1,2 @@
|
|||
./node_modules
|
||||
!package-lock.json
|
|
@ -0,0 +1,29 @@
|
|||
# BangleTS
|
||||
|
||||
A generic project setup for compiling apps from Typescript to Bangle.js ready, readable Javascript.
|
||||
It includes types for _some_ of the modules and globals that are exposed for apps to use.
|
||||
The goal is to have types for everything, but that will take some time. Feel free to help out by contributing!
|
||||
|
||||
## Using the types
|
||||
|
||||
All currently typed modules can be found in `/typescript/types.globals.d.ts`.
|
||||
The typing is an ongoing process. If anything is still missing, you can add it! It will automatically be available in your TS files.
|
||||
|
||||
## Compilation
|
||||
|
||||
Install [npm](https://www.npmjs.com/get-npm) and node.js if you haven't already. We recommend using a version manager like nvm, which is also referenced in the linked documentation.
|
||||
Make sure you are using node version 16 by running `nvm use 16` and npm version ^8 by running `npm -v`. If the latter version is incorrect, run `npm i -g npm@^8`.
|
||||
|
||||
After having installed npm for your platform, open a terminal, and navigate into the `/typescript` folder. Then run:
|
||||
|
||||
```
|
||||
npm ci
|
||||
```
|
||||
|
||||
to install the project's build tools, and:
|
||||
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
To build all Typescript apps and widgets. The last command will generate the `app.js` files containing the transpiled code for the BangleJS.
|
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"name": "Bangle.ts",
|
||||
"version": "0.0.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "Bangle.ts",
|
||||
"version": "0.0.1",
|
||||
"devDependencies": {
|
||||
"typescript": "4.5.2"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "4.5.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.2.tgz",
|
||||
"integrity": "sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.2.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"typescript": {
|
||||
"version": "4.5.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.2.tgz",
|
||||
"integrity": "sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"name": "Bangle.ts",
|
||||
"description": "Bangle.js Typescript Project Setup and Types",
|
||||
"author": "Sebastian Di Luzio <sebastian@diluz.io> (https://diluz.io)",
|
||||
"version": "0.0.1",
|
||||
"devDependencies": {
|
||||
"typescript": "4.5.2"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"build:types": "tsc ./types/globals.d.ts"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"noImplicitAny": true,
|
||||
"target": "es6",
|
||||
"allowUnreachableCode": false,
|
||||
"allowUnusedLabels": false,
|
||||
"noImplicitOverride": true,
|
||||
"noImplicitReturns": true,
|
||||
"noImplicitThis": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"strict": true
|
||||
},
|
||||
"include": ["../apps/**/*", "./**/*"],
|
||||
}
|
|
@ -0,0 +1,184 @@
|
|||
// TODO all of these globals (copied from eslintrc) need to be typed at some point
|
||||
/* The typing status is listed on the left of the attribute, e.g.:
|
||||
status "Attribute"
|
||||
|
||||
// Methods and Fields at https://banglejs.com/reference
|
||||
"Array": "readonly",
|
||||
"ArrayBuffer": "readonly",
|
||||
"ArrayBufferView": "readonly",
|
||||
started "Bangle": "readonly",
|
||||
"BluetoothDevice": "readonly",
|
||||
"BluetoothRemoteGATTCharacteristic": "readonly",
|
||||
"BluetoothRemoteGATTServer": "readonly",
|
||||
"BluetoothRemoteGATTService": "readonly",
|
||||
"Boolean": "readonly",
|
||||
"console": "readonly",
|
||||
"DataView": "readonly",
|
||||
"Date": "readonly",
|
||||
"E": "readonly",
|
||||
"Error": "readonly",
|
||||
"Flash": "readonly",
|
||||
"Float32Array": "readonly",
|
||||
"Float64Array": "readonly",
|
||||
"fs": "readonly",
|
||||
"Function": "readonly",
|
||||
started "Graphics": "readonly",
|
||||
done "heatshrink": "readonly",
|
||||
"I2C": "readonly",
|
||||
"Int16Array": "readonly",
|
||||
"Int32Array": "readonly",
|
||||
"Int8Array": "readonly",
|
||||
"InternalError": "readonly",
|
||||
"JSON": "readonly",
|
||||
"Math": "readonly",
|
||||
"Modules": "readonly",
|
||||
"NRF": "readonly",
|
||||
"Number": "readonly",
|
||||
"Object": "readonly",
|
||||
"OneWire": "readonly",
|
||||
"Pin": "readonly",
|
||||
"process": "readonly",
|
||||
"Promise": "readonly",
|
||||
"ReferenceError": "readonly",
|
||||
"RegExp": "readonly",
|
||||
"Serial": "readonly",
|
||||
"SPI": "readonly",
|
||||
"Storage": "readonly",
|
||||
"StorageFile": "readonly",
|
||||
"String": "readonly",
|
||||
"SyntaxError": "readonly",
|
||||
"tensorflow": "readonly",
|
||||
"TFMicroInterpreter": "readonly",
|
||||
"TypeError": "readonly",
|
||||
"Uint16Array": "readonly",
|
||||
"Uint24Array": "readonly",
|
||||
"Uint32Array": "readonly",
|
||||
"Uint8Array": "readonly",
|
||||
"Uint8ClampedArray": "readonly",
|
||||
"Waveform": "readonly",
|
||||
// Methods and Fields at https://banglejs.com/reference
|
||||
"analogRead": "readonly",
|
||||
"analogWrite": "readonly",
|
||||
"arguments": "readonly",
|
||||
"atob": "readonly",
|
||||
"Bluetooth": "readonly",
|
||||
"BTN": "readonly",
|
||||
"BTN1": "readonly",
|
||||
"BTN2": "readonly",
|
||||
"BTN3": "readonly",
|
||||
"BTN4": "readonly",
|
||||
"BTN5": "readonly",
|
||||
"btoa": "readonly",
|
||||
"changeInterval": "readonly",
|
||||
"clearInterval": "readonly",
|
||||
"clearTimeout": "readonly",
|
||||
"clearWatch": "readonly",
|
||||
"decodeURIComponent": "readonly",
|
||||
"digitalPulse": "readonly",
|
||||
"digitalRead": "readonly",
|
||||
"digitalWrite": "readonly",
|
||||
"dump": "readonly",
|
||||
"echo": "readonly",
|
||||
"edit": "readonly",
|
||||
"encodeURIComponent": "readonly",
|
||||
"eval": "readonly",
|
||||
"getPinMode": "readonly",
|
||||
"getSerial": "readonly",
|
||||
"getTime": "readonly",
|
||||
"global": "readonly",
|
||||
"HIGH": "readonly",
|
||||
"I2C1": "readonly",
|
||||
"Infinity": "readonly",
|
||||
"isFinite": "readonly",
|
||||
"isNaN": "readonly",
|
||||
"LED": "readonly",
|
||||
"LED1": "readonly",
|
||||
"LED2": "readonly",
|
||||
"load": "readonly",
|
||||
"LoopbackA": "readonly",
|
||||
"LoopbackB": "readonly",
|
||||
"LOW": "readonly",
|
||||
"NaN": "readonly",
|
||||
"parseFloat": "readonly",
|
||||
"parseInt": "readonly",
|
||||
"peek16": "readonly",
|
||||
"peek32": "readonly",
|
||||
"peek8": "readonly",
|
||||
"pinMode": "readonly",
|
||||
"poke16": "readonly",
|
||||
"poke32": "readonly",
|
||||
"poke8": "readonly",
|
||||
"print": "readonly",
|
||||
started "require": "readonly",
|
||||
"reset": "readonly",
|
||||
"save": "readonly",
|
||||
"Serial1": "readonly",
|
||||
"setBusyIndicator": "readonly",
|
||||
"setInterval": "readonly",
|
||||
"setSleepIndicator": "readonly",
|
||||
"setTime": "readonly",
|
||||
"setTimeout": "readonly",
|
||||
"setWatch": "readonly",
|
||||
"shiftOut": "readonly",
|
||||
"SPI1": "readonly",
|
||||
"Terminal": "readonly",
|
||||
"trace": "readonly",
|
||||
"VIBRATE": "readonly",
|
||||
// Aliases and not defined at https://banglejs.com/reference
|
||||
done "g": "readonly",
|
||||
done "WIDGETS": "readonly"
|
||||
*/
|
||||
|
||||
// ambient JS definitions
|
||||
|
||||
declare const require: ((module: 'heatshrink') => {
|
||||
decompress: (compressedString: string) => string;
|
||||
}) & // TODO add more
|
||||
((module: 'otherString') => {});
|
||||
|
||||
// ambient bangle.js definitions
|
||||
|
||||
declare const Bangle: {
|
||||
// functions
|
||||
buzz: () => void;
|
||||
drawWidgets: () => void;
|
||||
isCharging: () => boolean;
|
||||
// events
|
||||
on(event: 'charging', listener: (charging: boolean) => void): void;
|
||||
// TODO add more
|
||||
};
|
||||
|
||||
declare type Image = {
|
||||
width: number;
|
||||
height: number;
|
||||
bpp?: number;
|
||||
buffer: ArrayBuffer | string;
|
||||
transparent?: number;
|
||||
palette?: Uint16Array;
|
||||
};
|
||||
|
||||
declare type GraphicsApi = {
|
||||
reset: () => void;
|
||||
flip: () => void;
|
||||
setColor: (color: string) => void; // TODO we can most likely type color more usefully than this
|
||||
drawImage: (
|
||||
image: string | Image | ArrayBuffer,
|
||||
xOffset: number,
|
||||
yOffset: number,
|
||||
options?: {
|
||||
rotate?: number;
|
||||
scale?: number;
|
||||
}
|
||||
) => void;
|
||||
// TODO add more
|
||||
};
|
||||
|
||||
declare const Graphics: GraphicsApi;
|
||||
declare const g: GraphicsApi;
|
||||
|
||||
declare type Widget = {
|
||||
area: 'tr' | 'tl';
|
||||
width: number;
|
||||
draw: (this: { x: number; y: number }) => void;
|
||||
};
|
||||
declare const WIDGETS: { [key: string]: Widget };
|