diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml new file mode 100644 index 000000000..1eb009153 --- /dev/null +++ b/.github/workflows/nodejs.yml @@ -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 \ No newline at end of file diff --git a/apps/banglerun/.gitignore b/apps/banglerun/.gitignore deleted file mode 100644 index c2658d7d1..000000000 --- a/apps/banglerun/.gitignore +++ /dev/null @@ -1 +0,0 @@ -node_modules/ diff --git a/apps/banglerun/README.md b/apps/banglerun/README.md deleted file mode 100644 index 80e984bfa..000000000 --- a/apps/banglerun/README.md +++ /dev/null @@ -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. diff --git a/apps/banglerun/activity.js b/apps/banglerun/activity.js new file mode 100644 index 000000000..94512094c --- /dev/null +++ b/apps/banglerun/activity.js @@ -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; diff --git a/apps/banglerun/src/activity.ts b/apps/banglerun/activity.ts similarity index 100% rename from apps/banglerun/src/activity.ts rename to apps/banglerun/activity.ts diff --git a/apps/banglerun/app.js b/apps/banglerun/app.js index b79255171..84d03820a 100644 --- a/apps/banglerun/app.js +++ b/apps/banglerun/app.js @@ -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' }); diff --git a/apps/banglerun/src/app.ts b/apps/banglerun/app.ts similarity index 100% rename from apps/banglerun/src/app.ts rename to apps/banglerun/app.ts diff --git a/apps/banglerun/display.js b/apps/banglerun/display.js new file mode 100644 index 000000000..648482676 --- /dev/null +++ b/apps/banglerun/display.js @@ -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; diff --git a/apps/banglerun/src/display.ts b/apps/banglerun/display.ts similarity index 100% rename from apps/banglerun/src/display.ts rename to apps/banglerun/display.ts diff --git a/apps/banglerun/gps.js b/apps/banglerun/gps.js new file mode 100644 index 000000000..35e0f8769 --- /dev/null +++ b/apps/banglerun/gps.js @@ -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; diff --git a/apps/banglerun/src/gps.ts b/apps/banglerun/gps.ts similarity index 100% rename from apps/banglerun/src/gps.ts rename to apps/banglerun/gps.ts diff --git a/apps/banglerun/hrm.js b/apps/banglerun/hrm.js new file mode 100644 index 000000000..c002de2f6 --- /dev/null +++ b/apps/banglerun/hrm.js @@ -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; diff --git a/apps/banglerun/src/hrm.ts b/apps/banglerun/hrm.ts similarity index 100% rename from apps/banglerun/src/hrm.ts rename to apps/banglerun/hrm.ts diff --git a/apps/banglerun/jasmine.json b/apps/banglerun/jasmine.json deleted file mode 100644 index 813363b27..000000000 --- a/apps/banglerun/jasmine.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "spec_dir": "test", - "spec_files": [ - "**/*.spec.ts" - ] -} \ No newline at end of file diff --git a/apps/banglerun/log.js b/apps/banglerun/log.js new file mode 100644 index 000000000..0cdeaa964 --- /dev/null +++ b/apps/banglerun/log.js @@ -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; diff --git a/apps/banglerun/log.ts b/apps/banglerun/log.ts new file mode 100644 index 000000000..ba1c1f00e --- /dev/null +++ b/apps/banglerun/log.ts @@ -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 }; diff --git a/apps/banglerun/package.json b/apps/banglerun/package.json deleted file mode 100644 index 1f5cc677b..000000000 --- a/apps/banglerun/package.json +++ /dev/null @@ -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" - } -} diff --git a/apps/banglerun/rollup.config.js b/apps/banglerun/rollup.config.js deleted file mode 100644 index f7027eb2b..000000000 --- a/apps/banglerun/rollup.config.js +++ /dev/null @@ -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(), - ] -}; diff --git a/apps/banglerun/src/log.ts b/apps/banglerun/src/log.ts deleted file mode 100644 index b6714e407..000000000 --- a/apps/banglerun/src/log.ts +++ /dev/null @@ -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 }; diff --git a/apps/banglerun/state.js b/apps/banglerun/state.js new file mode 100644 index 000000000..349ef1954 --- /dev/null +++ b/apps/banglerun/state.js @@ -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; diff --git a/apps/banglerun/src/state.ts b/apps/banglerun/state.ts similarity index 80% rename from apps/banglerun/src/state.ts rename to apps/banglerun/state.ts index 14ef2dc5d..1ba9bca26 100644 --- a/apps/banglerun/src/state.ts +++ b/apps/banglerun/state.ts @@ -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 }; diff --git a/apps/banglerun/step.js b/apps/banglerun/step.js new file mode 100644 index 000000000..e4acfd66a --- /dev/null +++ b/apps/banglerun/step.js @@ -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; diff --git a/apps/banglerun/src/step.ts b/apps/banglerun/step.ts similarity index 100% rename from apps/banglerun/src/step.ts rename to apps/banglerun/step.ts diff --git a/apps/banglerun/tsconfig.json b/apps/banglerun/tsconfig.json deleted file mode 100644 index a341a5a5e..000000000 --- a/apps/banglerun/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "compilerOptions": { - "module": "es2015", - "noImplicitAny": true, - "target": "es2015" - }, - "include": [ - "src" - ] -} diff --git a/apps/banglerun/tsconfig.spec.json b/apps/banglerun/tsconfig.spec.json deleted file mode 100644 index 136ae137b..000000000 --- a/apps/banglerun/tsconfig.spec.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "compilerOptions": { - "module": "commonjs", - "noImplicitAny": true, - "target": "es2015" - }, - "include": [ - "test" - ] -} diff --git a/apps/widChargingStatus/ChangeLog b/apps/widChargingStatus/ChangeLog index 1033c0cd3..5a6db5cb7 100644 --- a/apps/widChargingStatus/ChangeLog +++ b/apps/widChargingStatus/ChangeLog @@ -1 +1,2 @@ 0.01: First release. +0.02: No functional changes, just moved codebase to Typescript. diff --git a/apps/widChargingStatus/metadata.json b/apps/widChargingStatus/metadata.json index f68ccf5b4..573c594e7 100644 --- a/apps/widChargingStatus/metadata.json +++ b/apps/widChargingStatus/metadata.json @@ -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", diff --git a/apps/widChargingStatus/widget.js b/apps/widChargingStatus/widget.js index 90f9199fa..5d9ea3837 100644 --- a/apps/widChargingStatus/widget.js +++ b/apps/widChargingStatus/widget.js @@ -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(); - }); -})(); \ No newline at end of file +"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(); + } + }); +})(); diff --git a/apps/widChargingStatus/widget.ts b/apps/widChargingStatus/widget.ts new file mode 100644 index 000000000..14b4df4a4 --- /dev/null +++ b/apps/widChargingStatus/widget.ts @@ -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(); + } + }); +})(); diff --git a/typescript/.gitignore b/typescript/.gitignore new file mode 100644 index 000000000..630f61ee5 --- /dev/null +++ b/typescript/.gitignore @@ -0,0 +1,2 @@ +./node_modules +!package-lock.json diff --git a/typescript/README.md b/typescript/README.md new file mode 100644 index 000000000..13800aeec --- /dev/null +++ b/typescript/README.md @@ -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. diff --git a/typescript/package-lock.json b/typescript/package-lock.json new file mode 100644 index 000000000..52be5f98a --- /dev/null +++ b/typescript/package-lock.json @@ -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 + } + } +} diff --git a/typescript/package.json b/typescript/package.json new file mode 100644 index 000000000..8cd38ce63 --- /dev/null +++ b/typescript/package.json @@ -0,0 +1,13 @@ +{ + "name": "Bangle.ts", + "description": "Bangle.js Typescript Project Setup and Types", + "author": "Sebastian Di Luzio (https://diluz.io)", + "version": "0.0.1", + "devDependencies": { + "typescript": "4.5.2" + }, + "scripts": { + "build": "tsc", + "build:types": "tsc ./types/globals.d.ts" + } +} diff --git a/typescript/tsconfig.json b/typescript/tsconfig.json new file mode 100644 index 000000000..d36465a01 --- /dev/null +++ b/typescript/tsconfig.json @@ -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/**/*", "./**/*"], +} diff --git a/typescript/types/globals.d.ts b/typescript/types/globals.d.ts new file mode 100644 index 000000000..2ef52dcdf --- /dev/null +++ b/typescript/types/globals.d.ts @@ -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 };