Merge branch 'T0TProduction-master'

master
Gordon Williams 2022-02-28 13:12:31 +00:00
commit 27f5579070
33 changed files with 388 additions and 820 deletions

34
.github/workflows/nodejs.yml vendored Normal file
View File

@ -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

View File

@ -1,3 +0,0 @@
language: node_js
node_js:
- "node"

View File

@ -1 +0,0 @@
node_modules/

View File

@ -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

View File

@ -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.

View File

@ -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=="))

View File

@ -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

View File

@ -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>

View File

@ -1,6 +0,0 @@
{
"spec_dir": "test",
"spec_files": [
"**/*.spec.ts"
]
}

View File

@ -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}
]
}

View File

@ -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"
}
}

View File

@ -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(),
]
};

View File

@ -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 };

View File

@ -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' });

View File

@ -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,
};

View File

@ -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 };

View File

@ -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 };

View File

@ -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 };

View File

@ -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 };

View File

@ -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 };

View File

@ -1,10 +0,0 @@
{
"compilerOptions": {
"module": "es2015",
"noImplicitAny": true,
"target": "es2015"
},
"include": [
"src"
]
}

View File

@ -1,10 +0,0 @@
{
"compilerOptions": {
"module": "commonjs",
"noImplicitAny": true,
"target": "es2015"
},
"include": [
"test"
]
}

View File

@ -1 +1,2 @@
0.01: First release.
0.02: No functional changes, just moved codebase to Typescript.

View File

@ -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",

View File

@ -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;
function draw() {
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) => {
if (charging) {
Bangle.buzz();
WIDGETS.chargingStatus.width = iconWidth;
} else {
WIDGETS.chargingStatus.width = 0;
}
Bangle.drawWidgets(); // re-layout widgets
g.flip();
});
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.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();
}
});
})();

View File

@ -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();
}
});
})();

2
typescript/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
./node_modules
!package-lock.json

29
typescript/README.md Normal file
View File

@ -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.

36
typescript/package-lock.json generated Normal file
View File

@ -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
}
}
}

13
typescript/package.json Normal file
View File

@ -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"
}
}

17
typescript/tsconfig.json Normal file
View File

@ -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/**/*", "./**/*"],
}

184
typescript/types/globals.d.ts vendored Normal file
View File

@ -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 };