mirror of https://github.com/espruino/BangleApps
Merge branch 'run_module'
commit
2f3a9c8c87
|
@ -2,3 +2,4 @@
|
|||
0.02: Set pace format to mm:ss, time format to h:mm:ss,
|
||||
added settings to opt out of GPS and HRM
|
||||
0.03: Fixed distance calculation, tested against Garmin Etrex, Amazfit GTS 2
|
||||
0.04: Use the exstats module, and make what is displayed configurable
|
||||
|
|
|
@ -28,7 +28,24 @@ so if you have no GPS lock you just need to wait.
|
|||
However you can just install the `Recorder` app, turn recording on in
|
||||
that, and then start the `Run` app.
|
||||
|
||||
## Settings
|
||||
|
||||
Under `Settings` -> `App` -> `Run` you can change settings for this app.
|
||||
|
||||
* `Pace` is the distance that pace should be shown over - 1km, 1 mile, 1/2 Marathon or 1 Marathon
|
||||
* `Box 1/2/3/4/5/6` are what should be shown in each of the 6 boxes on the display. From the top left, down.
|
||||
If you set it to `-` nothing will be displayed, so you can display only 4 boxes of information
|
||||
if you wish by setting the last 2 boxes to `-`.
|
||||
|
||||
## TODO
|
||||
|
||||
* Allow this app to trigger the `Recorder` app on and off directly.
|
||||
* Keep a log of each run's stats (distance/steps/etc)
|
||||
|
||||
## Development
|
||||
|
||||
This app uses the [`exstats` module](/modules/exstats.js). When uploaded via the
|
||||
app loader, the module is automatically included in the app's source. However
|
||||
when developing via the IDE the module won't get pulled in by default.
|
||||
|
||||
There are some options to fix this easily - please check out the [modules README.md file](/modules/README.md)
|
||||
|
|
184
apps/run/app.js
184
apps/run/app.js
|
@ -1,68 +1,37 @@
|
|||
var ExStats = require("exstats");
|
||||
var B2 = process.env.HWVERSION==2;
|
||||
var Layout = require("Layout");
|
||||
var locale = require("locale");
|
||||
var fontHeading = "6x8:2";
|
||||
var fontValue = B2 ? "6x15:2" : "6x8:3";
|
||||
var headingCol = "#888";
|
||||
var running = false;
|
||||
var fixCount = 0;
|
||||
var startTime;
|
||||
var startSteps;
|
||||
// This & previous GPS readings
|
||||
var lastGPS, thisGPS;
|
||||
var distance = 0; ///< distance in meters
|
||||
var startSteps = Bangle.getStepCount(); ///< number of steps when we started
|
||||
var lastStepCount = startSteps; // last time 'step' was called
|
||||
var stepHistory = new Uint8Array(60); // steps each second for the last minute (0 = current minute)
|
||||
|
||||
g.clear();
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
|
||||
// ---------------------------
|
||||
|
||||
function formatTime(ms) {
|
||||
let hrs = Math.floor(ms/3600000).toString();
|
||||
let mins = (Math.floor(ms/60000)%60).toString();
|
||||
let secs = (Math.floor(ms/1000)%60).toString();
|
||||
|
||||
if (hrs === '0')
|
||||
return mins.padStart(2,0)+":"+secs.padStart(2,0);
|
||||
else
|
||||
return hrs+":"+mins.padStart(2,0)+":"+secs.padStart(2,0); // dont pad hours
|
||||
}
|
||||
|
||||
// Format speed in meters/second
|
||||
function formatPace(speed) {
|
||||
if (speed < 0.1667) {
|
||||
return `__:__`;
|
||||
}
|
||||
const pace = Math.round(1000 / speed); // seconds for 1km
|
||||
const min = Math.floor(pace / 60); // minutes for 1km
|
||||
const sec = pace % 60;
|
||||
return ('0' + min).substr(-2) + `:` + ('0' + sec).substr(-2);
|
||||
}
|
||||
|
||||
let settings = Object.assign({
|
||||
B1 : "dist",
|
||||
B2 : "time",
|
||||
B3 : "pacea",
|
||||
B4 : "bpm",
|
||||
B5 : "step",
|
||||
B6 : "caden",
|
||||
paceLength : 1000
|
||||
}, require("Storage").readJSON("run.json", 1) || {});
|
||||
var statIDs = [settings.B1,settings.B2,settings.B3,settings.B4,settings.B5,settings.B6].filter(s=>s!="");
|
||||
var exs = ExStats.getStats(statIDs, settings);
|
||||
// ---------------------------
|
||||
|
||||
function clearState() {
|
||||
distance = 0;
|
||||
startSteps = Bangle.getStepCount();
|
||||
stepHistory.fill(0);
|
||||
layout.dist.label=locale.distance(distance);
|
||||
layout.time.label="00:00";
|
||||
layout.pace.label=formatPace(0);
|
||||
layout.hrm.label="--";
|
||||
layout.steps.label=0;
|
||||
layout.cadence.label= "0";
|
||||
layout.status.bgCol = "#f00";
|
||||
}
|
||||
|
||||
// Called to start/stop running
|
||||
function onStartStop() {
|
||||
running = !running;
|
||||
var running = !exs.state.active;
|
||||
if (running) {
|
||||
clearState();
|
||||
startTime = Date.now();
|
||||
exs.start();
|
||||
} else {
|
||||
exs.stop();
|
||||
}
|
||||
layout.button.label = running ? "STOP" : "START";
|
||||
layout.status.label = running ? "RUN" : "STOP";
|
||||
|
@ -72,107 +41,44 @@ function onStartStop() {
|
|||
layout.render();
|
||||
}
|
||||
|
||||
var layout = new Layout( {
|
||||
type:"v", c: [
|
||||
{ type:"h", filly:1, c:[
|
||||
{type:"txt", font:fontHeading, label:"DIST", fillx:1, col:headingCol },
|
||||
{type:"txt", font:fontHeading, label:"TIME", fillx:1, col:headingCol }
|
||||
]}, { type:"h", filly:1, c:[
|
||||
{type:"txt", font:fontValue, label:"0.00", id:"dist", fillx:1 },
|
||||
{type:"txt", font:fontValue, label:"00:00", id:"time", fillx:1 }
|
||||
]}, { type:"h", filly:1, c:[
|
||||
{type:"txt", font:fontHeading, label:"PACE", fillx:1, col:headingCol },
|
||||
{type:"txt", font:fontHeading, label:"HEART", fillx:1, col:headingCol }
|
||||
]}, { type:"h", filly:1, c:[
|
||||
{type:"txt", font:fontValue, label:`__'__"`, id:"pace", fillx:1 },
|
||||
{type:"txt", font:fontValue, label:"--", id:"hrm", fillx:1 }
|
||||
]}, { type:"h", filly:1, c:[
|
||||
{type:"txt", font:fontHeading, label:"STEPS", fillx:1, col:headingCol },
|
||||
{type:"txt", font:fontHeading, label:"CADENCE", fillx:1, col:headingCol }
|
||||
]}, { type:"h", filly:1, c:[
|
||||
{type:"txt", font:fontValue, label:"0", id:"steps", fillx:1 },
|
||||
{type:"txt", font:fontValue, label:"0", id:"cadence", fillx:1 }
|
||||
var lc = [];
|
||||
// Load stats in pair by pair
|
||||
for (var i=0;i<statIDs.length;i+=2) {
|
||||
var sa = exs.stats[statIDs[i+0]];
|
||||
var sb = exs.stats[statIDs[i+1]];
|
||||
lc.push({ type:"h", filly:1, c:[
|
||||
{type:"txt", font:fontHeading, label:sa.title.toUpperCase(), fillx:1, col:headingCol },
|
||||
{type:"txt", font:fontHeading, label:sb.title.toUpperCase(), fillx:1, col:headingCol }
|
||||
]}, { type:"h", filly:1, c:[
|
||||
{type:"txt", font:fontValue, label:sa.getString(), id:sa.id, fillx:1 },
|
||||
{type:"txt", font:fontValue, label:sb.getString(), id:sb.id, fillx:1 }
|
||||
]});
|
||||
sa.on('changed', e=>layout[e.id].label = e.getString());
|
||||
sb.on('changed', e=>layout[e.id].label = e.getString());
|
||||
}
|
||||
// At the bottom put time/GPS state/etc
|
||||
lc.push({ type:"h", filly:1, c:[
|
||||
{type:"txt", font:fontHeading, label:"GPS", id:"gps", fillx:1, bgCol:"#f00" },
|
||||
{type:"txt", font:fontHeading, label:"00:00", id:"clock", fillx:1, bgCol:g.theme.fg, col:g.theme.bg },
|
||||
{type:"txt", font:fontHeading, label:"STOP", id:"status", fillx:1 }
|
||||
]},
|
||||
|
||||
]
|
||||
]});
|
||||
// Now calculate the layout
|
||||
var layout = new Layout( {
|
||||
type:"v", c: lc
|
||||
},{lazy:true, btns:[{ label:"START", cb: onStartStop, id:"button"}]});
|
||||
clearState();
|
||||
delete lc;
|
||||
layout.render();
|
||||
|
||||
function onTimer() {
|
||||
layout.clock.label = locale.time(new Date(),1);
|
||||
if (!running) {
|
||||
layout.render();
|
||||
return;
|
||||
}
|
||||
// called once a second
|
||||
var duration = Date.now() - startTime; // in ms
|
||||
// set cadence based on steps over last minute
|
||||
var stepsInMinute = E.sum(stepHistory);
|
||||
var cadence = 60000 * stepsInMinute / Math.min(duration,60000);
|
||||
// update layout
|
||||
layout.time.label = formatTime(duration);
|
||||
layout.steps.label = Bangle.getStepCount()-startSteps;
|
||||
layout.cadence.label = Math.round(cadence);
|
||||
layout.render();
|
||||
// move step history onwards
|
||||
stepHistory.set(stepHistory,1);
|
||||
stepHistory[0]=0;
|
||||
}
|
||||
|
||||
function radians(a) {
|
||||
return a*Math.PI/180;
|
||||
}
|
||||
|
||||
// distance between 2 lat and lons, in meters, Mean Earth Radius = 6371km
|
||||
// https://www.movable-type.co.uk/scripts/latlong.html
|
||||
function calcDistance(a,b) {
|
||||
var x = radians(a.lon-b.lon) * Math.cos(radians((a.lat+b.lat)/2));
|
||||
var y = radians(b.lat-a.lat);
|
||||
return Math.round(Math.sqrt(x*x + y*y) * 6371000);
|
||||
}
|
||||
|
||||
// Handle GPS state change for icon
|
||||
Bangle.on("GPS", function(fix) {
|
||||
layout.gps.bgCol = fix.fix ? "#0f0" : "#f00";
|
||||
if (!fix.fix) { return; } // only process actual fixes
|
||||
if (!fix.fix) return; // only process actual fixes
|
||||
if (fixCount++ == 0) {
|
||||
Bangle.buzz(); // first fix, does not need to respect quiet mode
|
||||
lastGPS = fix; // initialise on first fix
|
||||
}
|
||||
|
||||
thisGPS = fix;
|
||||
|
||||
if (running) {
|
||||
var d = calcDistance(lastGPS, thisGPS);
|
||||
distance += d;
|
||||
layout.dist.label=locale.distance(distance);
|
||||
var duration = Date.now() - startTime; // in ms
|
||||
var speed = distance * 1000 / duration; // meters/sec
|
||||
layout.pace.label = formatPace(speed);
|
||||
lastGPS = fix;
|
||||
}
|
||||
});
|
||||
Bangle.on("HRM", function(h) {
|
||||
layout.hrm.label = h.bpm;
|
||||
});
|
||||
Bangle.on("step", function(steps) {
|
||||
if (running) {
|
||||
layout.steps.label = steps-Bangle.getStepCount();
|
||||
stepHistory[0] += steps-lastStepCount;
|
||||
}
|
||||
lastStepCount = steps;
|
||||
});
|
||||
|
||||
let settings = require("Storage").readJSON('run.json',1)||{"use_gps":true,"use_hrm":true};
|
||||
|
||||
// We always call ourselves once a second, if only to update the time
|
||||
setInterval(onTimer, 1000);
|
||||
|
||||
/* Turn GPS and HRM on right at the start to ensure
|
||||
we get the highest chance of a lock. */
|
||||
if (settings.use_hrm) Bangle.setHRMPower(true,"app");
|
||||
if (settings.use_gps) Bangle.setGPSPower(true,"app");
|
||||
// We always call ourselves once a second to update
|
||||
setInterval(function() {
|
||||
layout.clock.label = locale.time(new Date(),1);
|
||||
layout.render();
|
||||
}, 1000);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{ "id": "run",
|
||||
"name": "Run",
|
||||
"version":"0.03",
|
||||
"version":"0.04",
|
||||
"description": "Displays distance, time, steps, cadence, pace and more for runners.",
|
||||
"icon": "app.png",
|
||||
"tags": "run,running,fitness,outdoors,gps",
|
||||
|
|
|
@ -1,44 +1,50 @@
|
|||
(function(back) {
|
||||
const SETTINGS_FILE = "run.json";
|
||||
|
||||
// initialize with default settings...
|
||||
let s = {
|
||||
'use_gps': true,
|
||||
'use_hrm': true
|
||||
}
|
||||
var ExStats = require("exstats");
|
||||
var statsList = ExStats.getList();
|
||||
statsList.unshift({name:"-",id:""}); // add blank menu item
|
||||
var statsIDs = statsList.map(s=>s.id);
|
||||
|
||||
// ...and overwrite them with any saved values
|
||||
// This way saved values are preserved if a new version adds more settings
|
||||
const storage = require('Storage')
|
||||
let settings = storage.readJSON(SETTINGS_FILE, 1) || {}
|
||||
const saved = settings || {}
|
||||
for (const key in saved) {
|
||||
s[key] = saved[key]
|
||||
}
|
||||
|
||||
function save() {
|
||||
settings = s
|
||||
let settings = Object.assign({
|
||||
B1 : "dist",
|
||||
B2 : "time",
|
||||
B3 : "pacea",
|
||||
B4 : "bpm",
|
||||
B5 : "step",
|
||||
B6 : "caden",
|
||||
paceLength : 1000
|
||||
}, storage.readJSON(SETTINGS_FILE, 1) || {});
|
||||
function saveSettings() {
|
||||
storage.write(SETTINGS_FILE, settings)
|
||||
}
|
||||
|
||||
E.showMenu({
|
||||
'': { 'title': 'Run' },
|
||||
'< Back': back,
|
||||
'Use GPS': {
|
||||
value: s.use_gps,
|
||||
format: () => (s.use_gps ? 'Yes' : 'No'),
|
||||
onchange: () => {
|
||||
s.use_gps = !s.use_gps;
|
||||
save();
|
||||
},
|
||||
},
|
||||
'Use HRM': {
|
||||
value: s.use_hrm,
|
||||
format: () => (s.use_hrm ? 'Yes' : 'No'),
|
||||
onchange: () => {
|
||||
s.use_hrm = !s.use_hrm;
|
||||
save();
|
||||
function getBoxChooser(boxID) {
|
||||
return {
|
||||
min :0, max: statsIDs.length-1,
|
||||
value: Math.max(statsIDs.indexOf(settings[boxID]),0),
|
||||
format: v => statsList[v].name,
|
||||
onchange: v => {
|
||||
settings[boxID] = statsIDs[v];
|
||||
saveSettings();
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var menu = {
|
||||
'': { 'title': 'Run' },
|
||||
'< Back': back
|
||||
};
|
||||
ExStats.appendMenuItems(menu, settings, saveSettings);
|
||||
Object.assign(menu,{
|
||||
'Box 1': getBoxChooser("B1"),
|
||||
'Box 2': getBoxChooser("B2"),
|
||||
'Box 3': getBoxChooser("B3"),
|
||||
'Box 4': getBoxChooser("B4"),
|
||||
'Box 5': getBoxChooser("B5"),
|
||||
'Box 6': getBoxChooser("B6"),
|
||||
});
|
||||
E.showMenu(menu);
|
||||
})
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
/* Copyright (c) 2022 Bangle.js contibutors. See the file LICENSE for copying permission. */
|
||||
/*
|
||||
|
||||
Take a look at README.md for hints on developing with this library.
|
||||
|
||||
Usage:
|
||||
|
||||
```
|
||||
|
|
|
@ -1,9 +1,54 @@
|
|||
App Modules
|
||||
===========
|
||||
|
||||
These are modules used by apps - you can use them with:
|
||||
These are modules used by apps - you can use them from a Bangle.js app with:
|
||||
|
||||
```
|
||||
var testmodule = require("testmodule");
|
||||
testmodule.test()
|
||||
```
|
||||
|
||||
Development
|
||||
-----------
|
||||
|
||||
When apps that use these modules are uploaded via the
|
||||
app loader, the module is automatically included in the app's source. However
|
||||
when developing via the IDE the module won't get pulled in by default.
|
||||
|
||||
To fix this you have three options:
|
||||
|
||||
### Host your own App Loader and upload from that
|
||||
|
||||
This is reasonably easy to set up, but it's more difficult to make changes and upload:
|
||||
|
||||
* Follow the steps here to set up your own App Loader: https://www.espruino.com/Bangle.js+App+Loader
|
||||
* Make changes to that repository
|
||||
* Refresh and upload your app from the app loader (you can have the IDE connected
|
||||
at the same time so you can see any error messages)
|
||||
|
||||
### Upload the module to the Bangle's internal storage
|
||||
|
||||
This allows you to develop both the app and module very quickly, but the app is
|
||||
uploaded in a slightly different way to what you'd get when you use the App Loader
|
||||
or the method below:
|
||||
|
||||
* Load the module's source file in the Web IDE
|
||||
* Click the down-arrow below the upload button, then `Storage`
|
||||
* Click `New File`, type `your_module_name` as the name (with no `.js` extension), click `Ok`
|
||||
* Now Click the `Upload` icon.
|
||||
|
||||
You can now upload the app direct from the IDE. You can even leave a second Web IDE window open
|
||||
(one for the app, one for the module) to allow you to change the module.
|
||||
|
||||
### Change the Web IDE search path to include Bangle.js modules
|
||||
|
||||
This is nice and easy (and the results are the same as if the app was
|
||||
uploaded via the app loader), however you cannot then make/test changes
|
||||
to the module.
|
||||
|
||||
* In the IDE, Click the `Settings` icon in the top right
|
||||
* Click `Communications` and scroll down to `Module URL`
|
||||
* Now change the module URL from the default of `https://www.espruino.com/modules`
|
||||
to `https://banglejs.com/apps/modules|https://www.espruino.com/modules`
|
||||
|
||||
The next time you upload your app, the module will automatically be included.
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
/* Copyright (c) 2020 OmegaRogue. See the file LICENSE for copying permission. */
|
||||
/*
|
||||
Graphics Functions based on the React Sci-Fi UI Framework Arwes
|
||||
|
||||
Take a look at README.md for hints on developing with this library.
|
||||
*/
|
||||
|
||||
var C = {
|
||||
|
|
|
@ -0,0 +1,262 @@
|
|||
/* Copyright (c) 2022 Bangle.js contibutors. See the file LICENSE for copying permission. */
|
||||
/* Exercise Stats module
|
||||
|
||||
Take a look at README.md for hints on developing with this library.
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
var ExStats = require("exstats");
|
||||
// Get a list of available types of run statistic
|
||||
print(ExStats.getList());
|
||||
// returns list of available stat IDs like
|
||||
[
|
||||
{name: "Time", id:"time"},
|
||||
{name: "Distance", id:"dist"},
|
||||
{name: "Steps", id:"step"},
|
||||
{name: "Heart (BPM)", id:"bpm"},
|
||||
{name: "Pace (avr)", id:"pacea"},
|
||||
{name: "Pace (current)", id:"pacec"},
|
||||
{name: "Cadence", id:"caden"},
|
||||
]
|
||||
|
||||
// Setup and load all statistic types
|
||||
var exs = ExStats.getStats(["dist", "time", "pacea","bpm","step","caden"], options);
|
||||
// exs contains
|
||||
{
|
||||
stats : { time : {
|
||||
id : "time"
|
||||
title : "Time" // title to use when rendering
|
||||
getValue : function // get a floating point value for this stat
|
||||
getString : function // get a formatted string for this stat
|
||||
// also fires a 'changed' event
|
||||
},
|
||||
dist : { ... },
|
||||
pacea : { ... },
|
||||
...
|
||||
},
|
||||
state : { active : bool,
|
||||
.. other internal-ish state info
|
||||
},
|
||||
start : function, // call to start exercise and reset state
|
||||
stop : function, // call to stop exercise
|
||||
}
|
||||
|
||||
/// Or you can display a menu where the settings can be configured - these are passed as the 'options' argument of getStats
|
||||
|
||||
var menu = { ... };
|
||||
ExStats.appendMenuItems(menu, settings, saveSettingsFunction);
|
||||
E.showMenu(menu);
|
||||
|
||||
*/
|
||||
var state = {
|
||||
active : false, // are we working or not?
|
||||
// startTime, // time exercise started
|
||||
lastGPS:{}, thisGPS:{}, // This & previous GPS readings
|
||||
// distance : 0, ///< distance in meters
|
||||
// avrSpeed : 0, ///< in m/sec
|
||||
startSteps : Bangle.getStepCount(), ///< number of steps when we started
|
||||
lastSteps : Bangle.getStepCount(), // last time 'step' was called
|
||||
stepHistory : new Uint8Array(60), // steps each second for the last minute (0 = current minute)
|
||||
// stepsInMinute // steps over the last minute
|
||||
// cadence // steps per minute adjusted if <1 minute
|
||||
// BPM // beats per minute
|
||||
// BPMage // how many seconds was BPM set?
|
||||
};
|
||||
// list of active stats (indexed by ID)
|
||||
var stats = {};
|
||||
|
||||
// distance between 2 lat and lons, in meters, Mean Earth Radius = 6371km
|
||||
// https://www.movable-type.co.uk/scripts/latlong.html
|
||||
function calcDistance(a,b) {
|
||||
function radians(a) { return a*Math.PI/180; }
|
||||
var x = radians(a.lon-b.lon) * Math.cos(radians((a.lat+b.lat)/2));
|
||||
var y = radians(b.lat-a.lat);
|
||||
return Math.round(Math.sqrt(x*x + y*y) * 6371000);
|
||||
}
|
||||
|
||||
// Given milliseconds, return a time
|
||||
function formatTime(ms) {
|
||||
let hrs = Math.floor(ms/3600000).toString();
|
||||
let mins = (Math.floor(ms/60000)%60).toString();
|
||||
let secs = (Math.floor(ms/1000)%60).toString();
|
||||
|
||||
if (hrs === '0')
|
||||
return mins.padStart(2,0)+":"+secs.padStart(2,0);
|
||||
else
|
||||
return hrs+":"+mins.padStart(2,0)+":"+secs.padStart(2,0); // dont pad hours
|
||||
}
|
||||
|
||||
// Format speed in meters/second, paceLength=length in m for pace over
|
||||
function formatPace(speed, paceLength) {
|
||||
if (speed < 0.1667) {
|
||||
return `__:__`;
|
||||
}
|
||||
const pace = Math.round(paceLength / speed); // seconds for paceLength (1000=1km)
|
||||
const min = Math.floor(pace / 60); // minutes for paceLength
|
||||
const sec = pace % 60;
|
||||
return ('0' + min).substr(-2) + `:` + ('0' + sec).substr(-2);
|
||||
}
|
||||
|
||||
Bangle.on("GPS", function(fix) {
|
||||
if (!fix.fix) return; // only process actual fixes
|
||||
|
||||
if (!state.active) return;
|
||||
if( state.lastGPS.fix)
|
||||
state.distance += calcDistance(state.lastGPS, fix);
|
||||
var duration = Date.now() - state.startTime; // in ms
|
||||
state.avrSpeed = state.distance * 1000 / duration; // meters/sec
|
||||
if (stats["pacea"]) stats["pacea"].emit("changed",stats["pacea"]);
|
||||
state.lastGPS = state.thisGPS;
|
||||
state.thisGPS = fix;
|
||||
if (stats["pacec"]) stats["pacec"].emit("changed",stats["pacec"]);
|
||||
});
|
||||
|
||||
Bangle.on("step", function(steps) {
|
||||
if (!state.active) return;
|
||||
if (stats["step"]) stats["step"].emit("changed",stats["step"]);
|
||||
state.lastStepCount = steps;
|
||||
});
|
||||
Bangle.on("HRM", function(h) {
|
||||
if (h.confidence>=60) {
|
||||
state.BPM = h.bpm;
|
||||
state.BPMage = 0;
|
||||
stats["bpm"].emit("changed",stats["bpm"]);
|
||||
}
|
||||
});
|
||||
|
||||
/** Get list of available statistic types */
|
||||
exports.getList = function() {
|
||||
return [
|
||||
{name: "Time", id:"time"},
|
||||
{name: "Distance", id:"dist"},
|
||||
{name: "Steps", id:"step"},
|
||||
{name: "Heart (BPM)", id:"bpm"},
|
||||
{name: "Pace (avr)", id:"pacea"},
|
||||
{name: "Pace (current)", id:"pacec"},
|
||||
{name: "Cadence", id:"caden"},
|
||||
];
|
||||
};
|
||||
/** Instatiate the given list of statistic IDs (see comments at top)
|
||||
options = {
|
||||
paceLength : meters to measure pace over
|
||||
}
|
||||
*/
|
||||
exports.getStats = function(statIDs, options) {
|
||||
options = options||{};
|
||||
options.paceLength = options.paceLength||1000;
|
||||
var needGPS,needHRM;
|
||||
// ======================
|
||||
if (statIDs.includes("time")) {
|
||||
stats["time"]={
|
||||
title : "Time",
|
||||
getValue : function() { return Date.now()-state.startTime; },
|
||||
getString : function() { return formatTime(this.getValue()) },
|
||||
};
|
||||
};
|
||||
if (statIDs.includes("dist")) {
|
||||
needGPS = true;
|
||||
stats["dist"]={
|
||||
title : "Dist",
|
||||
getValue : function() { return state.distance; },
|
||||
getString : function() { return require("locale").distance(state.distance); },
|
||||
};
|
||||
}
|
||||
if (statIDs.includes("step")) {
|
||||
stats["step"]={
|
||||
title : "Steps",
|
||||
getValue : function() { return Bangle.getStepCount() - state.startSteps; },
|
||||
getString : function() { return this.getValue().toString() },
|
||||
};
|
||||
}
|
||||
if (statIDs.includes("bpm")) {
|
||||
needHRM = true;
|
||||
stats["bpm"]={
|
||||
title : "BPM",
|
||||
getValue : function() { return state.BPM; },
|
||||
getString : function() { return state.BPM||"--" },
|
||||
};
|
||||
}
|
||||
if (statIDs.includes("pacea")) {
|
||||
needGPS = true;
|
||||
stats["pacea"]={
|
||||
title : "Pace(avr)",
|
||||
getValue : function() { return state.avrSpeed; }, // in m/sec
|
||||
getString : function() { return formatPace(state.avrSpeed, options.paceLength); },
|
||||
};
|
||||
}
|
||||
if (statIDs.includes("pacec")) {
|
||||
needGPS = true;
|
||||
stats["pacec"]={
|
||||
title : "Pace(now)",
|
||||
getValue : function() { return (state.thisGPS.speed||0)/3.6; }, // in m/sec
|
||||
getString : function() { return formatPace(this.getValue(), options.paceLength); },
|
||||
};
|
||||
}
|
||||
if (statIDs.includes("caden")) {
|
||||
needGPS = true;
|
||||
stats["caden"]={
|
||||
title : "Cadence",
|
||||
getValue : function() { return state.stepsPerMin; },
|
||||
getString : function() { return state.stepsPerMin; },
|
||||
};
|
||||
}
|
||||
// ======================
|
||||
for (var i in stats) stats[i].id=i; // set up ID field
|
||||
if (needGPS) Bangle.setGPSPower(true,"exs");
|
||||
if (needHRM) Bangle.setHRMPower(true,"exs");
|
||||
setInterval(function() { // run once a second....
|
||||
if (!state.active) return;
|
||||
// called once a second
|
||||
var duration = Date.now() - state.startTime; // in ms
|
||||
// set cadence -> steps over last minute
|
||||
state.stepsPerMin = Math.round(60000 * E.sum(state.stepHistory) / Math.min(duration,60000));
|
||||
if (stats["caden"]) stats["caden"].emit("changed",stats["caden"]);
|
||||
// move step history onwards
|
||||
state.stepHistory.set(state.stepHistory,1);
|
||||
state.stepHistory[0]=0;
|
||||
if (stats["time"]) stats["time"].emit("changed",stats["time"]);
|
||||
// update BPM - if nothing valid in 60s remove the reading
|
||||
state.BPMage++;
|
||||
if (state.BPM && state.BPMage>60) {
|
||||
state.BPM = 0;
|
||||
if (stats["bpm"]) stats["bpm"].emit("changed",stats["bpm"]);
|
||||
}
|
||||
}, 1000);
|
||||
function reset() {
|
||||
state.startTime = Date.now();
|
||||
state.startSteps = state.lastSteps = Bangle.getStepCount();
|
||||
state.lastSteps = 0;
|
||||
state.stepHistory.fill(0);
|
||||
state.stepsPerMin = 0;
|
||||
state.distance = 0;
|
||||
state.avrSpeed = 0;
|
||||
state.BPM = 0;
|
||||
state.BPMage = 0;
|
||||
}
|
||||
reset();
|
||||
return {
|
||||
stats : stats, state : state,
|
||||
start : function() {
|
||||
state.active = true;
|
||||
reset();
|
||||
},
|
||||
stop : function() {
|
||||
state.active = false;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
exports.appendMenuItems = function(menu, settings, saveSettings) {
|
||||
var paceNames = ["1000m","1 mile","1/2 Mthn", "Marathon",];
|
||||
var paceAmts = [1000,1609,21098,42195];
|
||||
menu['Pace'] = {
|
||||
min :0, max: paceNames.length-1,
|
||||
value: Math.max(paceAmts.indexOf(settings.paceLength),0),
|
||||
format: v => paceNames[v],
|
||||
onchange: v => {
|
||||
settings.paceLength = paceAmts[v];
|
||||
saveSettings();
|
||||
},
|
||||
};
|
||||
};
|
Loading…
Reference in New Issue