Merge branch 'T0TProduction-master'
commit
27f5579070
|
|
@ -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"
|
|
||||||
|
|
@ -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"})}();
|
|
||||||
Binary file not shown.
|
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 +1,2 @@
|
||||||
0.01: First release.
|
0.01: First release.
|
||||||
|
0.02: No functional changes, just moved codebase to Typescript.
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
"name": "Charging Status",
|
"name": "Charging Status",
|
||||||
"shortName":"ChargingStatus",
|
"shortName":"ChargingStatus",
|
||||||
"icon": "widget.png",
|
"icon": "widget.png",
|
||||||
"version":"0.01",
|
"version":"0.02",
|
||||||
"type": "widget",
|
"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.",
|
"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",
|
"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 iconWidth = 18;
|
const icon = require('heatshrink').decompress(atob('ikggMAiEAgYIBmEAg4EB+EAh0AgPggEeCAIEBnwQBAgP+gEP//x///j//8f//k///H//4BYOP/4lBv4bDvwEB4EAvAEBwEAuA7DCAI7BgAQBhEAA'));
|
||||||
|
const iconWidth = 18;
|
||||||
function draw() {
|
function draw() {
|
||||||
g.reset();
|
g.reset();
|
||||||
if (Bangle.isCharging()) {
|
if (Bangle.isCharging()) {
|
||||||
g.setColor("#FD0");
|
g.setColor('#FD0');
|
||||||
g.drawImage(icon, this.x + 1, this.y + 1, {
|
g.drawImage(icon, this.x + 1, this.y + 1, {
|
||||||
scale: 0.6875
|
scale: 0.6875,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
WIDGETS.chargingStatus = {
|
||||||
WIDGETS.chargingStatus = {
|
area: 'tr',
|
||||||
area: 'tr',
|
width: Bangle.isCharging() ? iconWidth : 0,
|
||||||
width: Bangle.isCharging() ? iconWidth : 0,
|
draw: draw,
|
||||||
draw: draw,
|
};
|
||||||
};
|
Bangle.on('charging', (charging) => {
|
||||||
|
const widget = WIDGETS.chargingStatus;
|
||||||
Bangle.on('charging', (charging) => {
|
if (widget) {
|
||||||
if (charging) {
|
if (charging) {
|
||||||
Bangle.buzz();
|
Bangle.buzz();
|
||||||
WIDGETS.chargingStatus.width = iconWidth;
|
widget.width = iconWidth;
|
||||||
} else {
|
}
|
||||||
WIDGETS.chargingStatus.width = 0;
|
else {
|
||||||
}
|
widget.width = 0;
|
||||||
Bangle.drawWidgets(); // re-layout widgets
|
}
|
||||||
g.flip();
|
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,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 };
|
||||||
Loading…
Reference in New Issue