Merge branch 'master' of https://github.com/T0TProduction/BangleApps into T0TProduction-master
commit
a375853e46
|
|
@ -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 +0,0 @@
|
|||
node_modules/
|
||||
|
|
@ -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.
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.stopActivity = exports.startActivity = exports.clearActivity = void 0;
|
||||
const display_1 = require("./display");
|
||||
const log_1 = require("./log");
|
||||
const state_1 = require("./state");
|
||||
function startActivity(state) {
|
||||
if (state.status === state_1.ActivityStatus.Stopped) {
|
||||
(0, log_1.initLog)(state);
|
||||
}
|
||||
if (state.status === state_1.ActivityStatus.Running) {
|
||||
state.status = state_1.ActivityStatus.Paused;
|
||||
}
|
||||
else {
|
||||
state.status = state_1.ActivityStatus.Running;
|
||||
}
|
||||
(0, display_1.draw)(state);
|
||||
}
|
||||
exports.startActivity = startActivity;
|
||||
function stopActivity(state) {
|
||||
if (state.status === state_1.ActivityStatus.Paused) {
|
||||
clearActivity(state);
|
||||
}
|
||||
if (state.status === state_1.ActivityStatus.Running) {
|
||||
state.status = state_1.ActivityStatus.Paused;
|
||||
}
|
||||
else {
|
||||
state.status = state_1.ActivityStatus.Stopped;
|
||||
}
|
||||
(0, display_1.draw)(state);
|
||||
}
|
||||
exports.stopActivity = stopActivity;
|
||||
function clearActivity(state) {
|
||||
state.duration = 0;
|
||||
state.distance = 0;
|
||||
state.speed = 0;
|
||||
state.steps = 0;
|
||||
state.cadence = 0;
|
||||
}
|
||||
exports.clearActivity = clearActivity;
|
||||
|
|
@ -1 +1,15 @@
|
|||
!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"})}();
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const activity_1 = require("./activity");
|
||||
const display_1 = require("./display");
|
||||
const gps_1 = require("./gps");
|
||||
const hrm_1 = require("./hrm");
|
||||
const state_1 = require("./state");
|
||||
const step_1 = require("./step");
|
||||
const appState = (0, state_1.initState)();
|
||||
(0, gps_1.initGps)(appState);
|
||||
(0, hrm_1.initHrm)(appState);
|
||||
(0, step_1.initStep)(appState);
|
||||
(0, display_1.initDisplay)(appState);
|
||||
setWatch(() => (0, activity_1.startActivity)(appState), BTN1, { repeat: true, edge: 'falling' });
|
||||
setWatch(() => (0, activity_1.stopActivity)(appState), BTN3, { repeat: true, edge: 'falling' });
|
||||
|
|
|
|||
|
|
@ -0,0 +1,106 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.initDisplay = exports.formatTime = exports.formatPace = exports.formatDistance = exports.formatClock = exports.drawValue = exports.drawBackground = exports.drawAll = exports.draw = void 0;
|
||||
const state_1 = require("./state");
|
||||
const STATUS_COLORS = {
|
||||
'STOP': 0xF800,
|
||||
'PAUSE': 0xFFE0,
|
||||
'RUN': 0x07E0,
|
||||
};
|
||||
function initDisplay(state) {
|
||||
Bangle.loadWidgets();
|
||||
Bangle.on('lcdPower', (on) => {
|
||||
state.drawing = on;
|
||||
if (on) {
|
||||
drawAll(state);
|
||||
}
|
||||
});
|
||||
drawAll(state);
|
||||
}
|
||||
exports.initDisplay = initDisplay;
|
||||
function drawBackground() {
|
||||
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);
|
||||
}
|
||||
exports.drawBackground = drawBackground;
|
||||
function drawValue(value, x, y) {
|
||||
g.setColor(0x0000);
|
||||
g.fillRect(x - 60, y, x + 60, y + 30);
|
||||
g.setColor(0xFFFF);
|
||||
g.drawString(value, x, y);
|
||||
}
|
||||
exports.drawValue = drawValue;
|
||||
function draw(state) {
|
||||
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 === state_1.ActivityStatus.Paused) {
|
||||
g.drawString("START", 236, 60, 1).drawString(" CLEAR ", 236, 180, 1);
|
||||
}
|
||||
else if (state.status === state_1.ActivityStatus.Running) {
|
||||
g.drawString(" PAUSE ", 236, 60, 1).drawString(" PAUSE ", 236, 180, 1);
|
||||
}
|
||||
else {
|
||||
g.drawString("START", 236, 60, 1).drawString(" ", 236, 180, 1);
|
||||
}
|
||||
}
|
||||
exports.draw = draw;
|
||||
function drawAll(state) {
|
||||
drawBackground();
|
||||
draw(state);
|
||||
Bangle.drawWidgets();
|
||||
}
|
||||
exports.drawAll = drawAll;
|
||||
function formatClock(date) {
|
||||
return ('0' + date.getHours()).substr(-2) + ':' + ('0' + date.getMinutes()).substr(-2);
|
||||
}
|
||||
exports.formatClock = formatClock;
|
||||
function formatDistance(meters) {
|
||||
return (meters / 1000).toFixed(2);
|
||||
}
|
||||
exports.formatDistance = formatDistance;
|
||||
function formatPace(speed) {
|
||||
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) + `"`;
|
||||
}
|
||||
exports.formatPace = formatPace;
|
||||
function formatTime(time) {
|
||||
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);
|
||||
}
|
||||
exports.formatTime = formatTime;
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.updateGps = exports.readGps = exports.initGps = void 0;
|
||||
const display_1 = require("./display");
|
||||
const log_1 = require("./log");
|
||||
const state_1 = require("./state");
|
||||
const EARTH_RADIUS = 6371008.8;
|
||||
function initGps(state) {
|
||||
Bangle.on('GPS', (gps) => readGps(state, gps));
|
||||
Bangle.setGPSPower(1);
|
||||
}
|
||||
exports.initGps = initGps;
|
||||
function readGps(state, gps) {
|
||||
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);
|
||||
(0, display_1.draw)(state);
|
||||
/* Only log GPS data every 5 secs if we
|
||||
have a fix and we're running. */
|
||||
if (state.gpsValid &&
|
||||
state.status === state_1.ActivityStatus.Running &&
|
||||
state.timeSinceLog > 5) {
|
||||
state.timeSinceLog = 0;
|
||||
(0, log_1.updateLog)(state);
|
||||
}
|
||||
}
|
||||
exports.readGps = readGps;
|
||||
function updateGps(state) {
|
||||
const t = Date.now();
|
||||
let dt = (t - state.t) / 1000;
|
||||
if (!isFinite(dt))
|
||||
dt = 0;
|
||||
state.t = t;
|
||||
state.timeSinceLog += dt;
|
||||
if (state.status === state_1.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 === state_1.ActivityStatus.Running) {
|
||||
state.distance += dpMag;
|
||||
state.speed = (state.distance / state.duration) || 0;
|
||||
state.cadence = (60 * state.steps / state.duration) || 0;
|
||||
}
|
||||
}
|
||||
exports.updateGps = updateGps;
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.updateHrm = exports.initHrm = void 0;
|
||||
function initHrm(state) {
|
||||
Bangle.on('HRM', (hrm) => updateHrm(state, hrm));
|
||||
Bangle.setHRMPower(1);
|
||||
}
|
||||
exports.initHrm = initHrm;
|
||||
function updateHrm(state, hrm) {
|
||||
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;
|
||||
}
|
||||
exports.updateHrm = updateHrm;
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"spec_dir": "test",
|
||||
"spec_files": [
|
||||
"**/*.spec.ts"
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.updateLog = exports.initLog = void 0;
|
||||
function initLog(state) {
|
||||
const datetime = new Date().toISOString().replace(/[-:]/g, '');
|
||||
const date = datetime.substr(2, 6);
|
||||
const time = datetime.substr(9, 6);
|
||||
const filename = `banglerun_${date}_${time}`;
|
||||
return Object.assign(Object.assign({}, state), { file: require('Storage').open(filename, 'w'), fileWritten: false });
|
||||
}
|
||||
exports.initLog = initLog;
|
||||
function updateLog(state) {
|
||||
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');
|
||||
}
|
||||
exports.updateLog = updateLog;
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
import { AppState, AppStateWithLog } from './state';
|
||||
|
||||
declare var require: any;
|
||||
|
||||
function initLog(state: AppState): AppStateWithLog {
|
||||
const datetime = new Date().toISOString().replace(/[-:]/g, '');
|
||||
const date = datetime.substr(2, 6);
|
||||
const time = datetime.substr(9, 6);
|
||||
const filename = `banglerun_${date}_${time}`;
|
||||
return {
|
||||
...state,
|
||||
file: require('Storage').open(filename, 'w'),
|
||||
fileWritten: false,
|
||||
} as AppStateWithLog;
|
||||
}
|
||||
|
||||
function updateLog(state: AppStateWithLog): 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,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,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 };
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.initState = exports.ActivityStatus = void 0;
|
||||
var ActivityStatus;
|
||||
(function (ActivityStatus) {
|
||||
ActivityStatus["Stopped"] = "STOP";
|
||||
ActivityStatus["Paused"] = "PAUSE";
|
||||
ActivityStatus["Running"] = "RUN";
|
||||
})(ActivityStatus || (ActivityStatus = {}));
|
||||
exports.ActivityStatus = ActivityStatus;
|
||||
function initState() {
|
||||
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,
|
||||
fileWritten: false,
|
||||
drawing: false,
|
||||
status: ActivityStatus.Stopped,
|
||||
duration: 0,
|
||||
distance: 0,
|
||||
speed: 0,
|
||||
steps: 0,
|
||||
cadence: 0,
|
||||
};
|
||||
}
|
||||
exports.initState = initState;
|
||||
|
|
@ -4,7 +4,7 @@ enum ActivityStatus {
|
|||
Running = 'RUN',
|
||||
}
|
||||
|
||||
interface AppState {
|
||||
interface BasicAppState {
|
||||
// GPS NMEA data
|
||||
fix: number;
|
||||
lat: number;
|
||||
|
|
@ -28,14 +28,12 @@ interface AppState {
|
|||
hrError: number,
|
||||
|
||||
// Logger data
|
||||
file: File;
|
||||
fileWritten: boolean;
|
||||
|
||||
// Drawing data
|
||||
drawing: boolean;
|
||||
|
||||
// Activity data
|
||||
status: ActivityStatus;
|
||||
duration: number;
|
||||
distance: number;
|
||||
speed: number;
|
||||
|
|
@ -43,6 +41,17 @@ interface AppState {
|
|||
cadence: number;
|
||||
}
|
||||
|
||||
interface AppStateWithoutLog extends BasicAppState {
|
||||
status: 'STOP';
|
||||
}
|
||||
|
||||
interface AppStateWithLog extends BasicAppState {
|
||||
file: File;
|
||||
status: ActivityStatus;
|
||||
}
|
||||
|
||||
type AppState = AppStateWithLog | AppStateWithoutLog;
|
||||
|
||||
interface File {
|
||||
read: Function;
|
||||
write: Function;
|
||||
|
|
@ -68,7 +77,6 @@ function initState(): AppState {
|
|||
hr: 60,
|
||||
hrError: 100,
|
||||
|
||||
file: null,
|
||||
fileWritten: false,
|
||||
|
||||
drawing: false,
|
||||
|
|
@ -82,4 +90,4 @@ function initState(): AppState {
|
|||
}
|
||||
}
|
||||
|
||||
export { ActivityStatus, AppState, File, initState };
|
||||
export { ActivityStatus, AppState, AppStateWithLog, File, initState };
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.updateStep = exports.initStep = void 0;
|
||||
const state_1 = require("./state");
|
||||
function initStep(state) {
|
||||
Bangle.on('step', () => updateStep(state));
|
||||
}
|
||||
exports.initStep = initStep;
|
||||
function updateStep(state) {
|
||||
if (state.status === state_1.ActivityStatus.Running) {
|
||||
state.steps += 1;
|
||||
}
|
||||
}
|
||||
exports.updateStep = 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.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 @@
|
|||
(() => {
|
||||
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();
|
||||
});
|
||||
})();
|
||||
"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) => {
|
||||
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,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