Skip to content

Commit 30d0383

Browse files
committed
create xstats module and make run app configurable to display different stats
1 parent e0c1c63 commit 30d0383

File tree

4 files changed

+336
-168
lines changed

4 files changed

+336
-168
lines changed

apps/run/README.md

+9
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,15 @@ so if you have no GPS lock you just need to wait.
2828
However you can just install the `Recorder` app, turn recording on in
2929
that, and then start the `Run` app.
3030

31+
## Settings
32+
33+
Under `Settings` -> `App` -> `Run` you can change settings for this app.
34+
35+
* `Pace` is the distance that pace should be shown over - 1km, 1 mile, 1/2 Marathon or 1 Maraton
36+
* `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.
37+
If you set it to `-` nothing will be displayed, so you can display only 4 boxes of information
38+
if you wish by setting the last 2 boxes to `-`.
39+
3140
## TODO
3241

3342
* Allow this app to trigger the `Recorder` app on and off directly.

apps/run/app.js

+48-142
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,37 @@
1+
var ExStats = require("exstats");
12
var B2 = process.env.HWVERSION==2;
23
var Layout = require("Layout");
34
var locale = require("locale");
45
var fontHeading = "6x8:2";
56
var fontValue = B2 ? "6x15:2" : "6x8:3";
67
var headingCol = "#888";
7-
var running = false;
88
var fixCount = 0;
9-
var startTime;
10-
var startSteps;
11-
// This & previous GPS readings
12-
var lastGPS, thisGPS;
13-
var distance = 0; ///< distance in meters
14-
var startSteps = Bangle.getStepCount(); ///< number of steps when we started
15-
var lastStepCount = startSteps; // last time 'step' was called
16-
var stepHistory = new Uint8Array(60); // steps each second for the last minute (0 = current minute)
179

1810
g.clear();
1911
Bangle.loadWidgets();
2012
Bangle.drawWidgets();
2113

2214
// ---------------------------
23-
24-
function formatTime(ms) {
25-
let hrs = Math.floor(ms/3600000).toString();
26-
let mins = (Math.floor(ms/60000)%60).toString();
27-
let secs = (Math.floor(ms/1000)%60).toString();
28-
29-
if (hrs === '0')
30-
return mins.padStart(2,0)+":"+secs.padStart(2,0);
31-
else
32-
return hrs+":"+mins.padStart(2,0)+":"+secs.padStart(2,0); // dont pad hours
33-
}
34-
35-
// Format speed in meters/second
36-
function formatPace(speed) {
37-
if (speed < 0.1667) {
38-
return `__:__`;
39-
}
40-
const pace = Math.round(1000 / speed); // seconds for 1km
41-
const min = Math.floor(pace / 60); // minutes for 1km
42-
const sec = pace % 60;
43-
return ('0' + min).substr(-2) + `:` + ('0' + sec).substr(-2);
44-
}
45-
15+
let settings = Object.assign({
16+
B1 : "dist",
17+
B2 : "time",
18+
B3 : "pacea",
19+
B4 : "bpm",
20+
B5 : "step",
21+
B6 : "caden",
22+
paceLength : 1000
23+
}, require("Storage").readJSON("run.json", 1) || {});
24+
var statIDs = [settings.B1,settings.B2,settings.B3,settings.B4,settings.B5,settings.B6].filter(s=>s!="");
25+
var exs = ExStats.getStats(statIDs, settings);
4626
// ---------------------------
4727

48-
function clearState() {
49-
distance = 0;
50-
startSteps = Bangle.getStepCount();
51-
stepHistory.fill(0);
52-
layout.dist.label=locale.distance(distance);
53-
layout.time.label="00:00";
54-
layout.pace.label=formatPace(0);
55-
layout.hrm.label="--";
56-
layout.steps.label=0;
57-
layout.cadence.label= "0";
58-
layout.status.bgCol = "#f00";
59-
}
60-
28+
// Called to start/stop running
6129
function onStartStop() {
62-
running = !running;
30+
var running = !exs.state.active;
6331
if (running) {
64-
clearState();
65-
startTime = Date.now();
32+
exs.start();
33+
} else {
34+
exs.stop();
6635
}
6736
layout.button.label = running ? "STOP" : "START";
6837
layout.status.label = running ? "RUN" : "STOP";
@@ -72,107 +41,44 @@ function onStartStop() {
7241
layout.render();
7342
}
7443

44+
var lc = [];
45+
// Load stats in pair by pair
46+
for (var i=0;i<statIDs.length;i+=2) {
47+
var sa = exs.stats[statIDs[i+0]];
48+
var sb = exs.stats[statIDs[i+1]];
49+
lc.push({ type:"h", filly:1, c:[
50+
{type:"txt", font:fontHeading, label:sa.title.toUpperCase(), fillx:1, col:headingCol },
51+
{type:"txt", font:fontHeading, label:sb.title.toUpperCase(), fillx:1, col:headingCol }
52+
]}, { type:"h", filly:1, c:[
53+
{type:"txt", font:fontValue, label:sa.getString(), id:sa.id, fillx:1 },
54+
{type:"txt", font:fontValue, label:sb.getString(), id:sb.id, fillx:1 }
55+
]});
56+
sa.on('changed', e=>layout[e.id].label = e.getString());
57+
sb.on('changed', e=>layout[e.id].label = e.getString());
58+
}
59+
// At the bottom put time/GPS state/etc
60+
lc.push({ type:"h", filly:1, c:[
61+
{type:"txt", font:fontHeading, label:"GPS", id:"gps", fillx:1, bgCol:"#f00" },
62+
{type:"txt", font:fontHeading, label:"00:00", id:"clock", fillx:1, bgCol:g.theme.fg, col:g.theme.bg },
63+
{type:"txt", font:fontHeading, label:"STOP", id:"status", fillx:1 }
64+
]});
65+
// Now calculate the layout
7566
var layout = new Layout( {
76-
type:"v", c: [
77-
{ type:"h", filly:1, c:[
78-
{type:"txt", font:fontHeading, label:"DIST", fillx:1, col:headingCol },
79-
{type:"txt", font:fontHeading, label:"TIME", fillx:1, col:headingCol }
80-
]}, { type:"h", filly:1, c:[
81-
{type:"txt", font:fontValue, label:"0.00", id:"dist", fillx:1 },
82-
{type:"txt", font:fontValue, label:"00:00", id:"time", fillx:1 }
83-
]}, { type:"h", filly:1, c:[
84-
{type:"txt", font:fontHeading, label:"PACE", fillx:1, col:headingCol },
85-
{type:"txt", font:fontHeading, label:"HEART", fillx:1, col:headingCol }
86-
]}, { type:"h", filly:1, c:[
87-
{type:"txt", font:fontValue, label:`__'__"`, id:"pace", fillx:1 },
88-
{type:"txt", font:fontValue, label:"--", id:"hrm", fillx:1 }
89-
]}, { type:"h", filly:1, c:[
90-
{type:"txt", font:fontHeading, label:"STEPS", fillx:1, col:headingCol },
91-
{type:"txt", font:fontHeading, label:"CADENCE", fillx:1, col:headingCol }
92-
]}, { type:"h", filly:1, c:[
93-
{type:"txt", font:fontValue, label:"0", id:"steps", fillx:1 },
94-
{type:"txt", font:fontValue, label:"0", id:"cadence", fillx:1 }
95-
]}, { type:"h", filly:1, c:[
96-
{type:"txt", font:fontHeading, label:"GPS", id:"gps", fillx:1, bgCol:"#f00" },
97-
{type:"txt", font:fontHeading, label:"00:00", id:"clock", fillx:1, bgCol:g.theme.fg, col:g.theme.bg },
98-
{type:"txt", font:fontHeading, label:"STOP", id:"status", fillx:1 }
99-
]},
100-
101-
]
67+
type:"v", c: lc
10268
},{lazy:true, btns:[{ label:"START", cb: onStartStop, id:"button"}]});
103-
clearState();
69+
delete lc;
10470
layout.render();
10571

106-
function onTimer() {
107-
layout.clock.label = locale.time(new Date(),1);
108-
if (!running) {
109-
layout.render();
110-
return;
111-
}
112-
// called once a second
113-
var duration = Date.now() - startTime; // in ms
114-
// set cadence based on steps over last minute
115-
var stepsInMinute = E.sum(stepHistory);
116-
var cadence = 60000 * stepsInMinute / Math.min(duration,60000);
117-
// update layout
118-
layout.time.label = formatTime(duration);
119-
layout.steps.label = Bangle.getStepCount()-startSteps;
120-
layout.cadence.label = Math.round(cadence);
121-
layout.render();
122-
// move step history onwards
123-
stepHistory.set(stepHistory,1);
124-
stepHistory[0]=0;
125-
}
126-
127-
function radians(a) {
128-
return a*Math.PI/180;
129-
}
130-
131-
// distance between 2 lat and lons, in meters, Mean Earth Radius = 6371km
132-
// https://www.movable-type.co.uk/scripts/latlong.html
133-
function calcDistance(a,b) {
134-
var x = radians(a.lon-b.lon) * Math.cos(radians((a.lat+b.lat)/2));
135-
var y = radians(b.lat-a.lat);
136-
return Math.round(Math.sqrt(x*x + y*y) * 6371000);
137-
}
138-
72+
// Handle GPS state change for icon
13973
Bangle.on("GPS", function(fix) {
14074
layout.gps.bgCol = fix.fix ? "#0f0" : "#f00";
141-
if (!fix.fix) { return; } // only process actual fixes
75+
if (!fix.fix) return; // only process actual fixes
14276
if (fixCount++ == 0) {
14377
Bangle.buzz(); // first fix, does not need to respect quiet mode
144-
lastGPS = fix; // initialise on first fix
145-
}
146-
147-
thisGPS = fix;
148-
149-
if (running) {
150-
var d = calcDistance(lastGPS, thisGPS);
151-
distance += d;
152-
layout.dist.label=locale.distance(distance);
153-
var duration = Date.now() - startTime; // in ms
154-
var speed = distance * 1000 / duration; // meters/sec
155-
layout.pace.label = formatPace(speed);
156-
lastGPS = fix;
157-
}
158-
});
159-
Bangle.on("HRM", function(h) {
160-
layout.hrm.label = h.bpm;
161-
});
162-
Bangle.on("step", function(steps) {
163-
if (running) {
164-
layout.steps.label = steps-Bangle.getStepCount();
165-
stepHistory[0] += steps-lastStepCount;
16678
}
167-
lastStepCount = steps;
16879
});
169-
170-
let settings = require("Storage").readJSON('run.json',1)||{"use_gps":true,"use_hrm":true};
171-
172-
// We always call ourselves once a second, if only to update the time
173-
setInterval(onTimer, 1000);
174-
175-
/* Turn GPS and HRM on right at the start to ensure
176-
we get the highest chance of a lock. */
177-
if (settings.use_hrm) Bangle.setHRMPower(true,"app");
178-
if (settings.use_gps) Bangle.setGPSPower(true,"app");
80+
// We always call ourselves once a second to update
81+
setInterval(function() {
82+
layout.clock.label = locale.time(new Date(),1);
83+
layout.render();
84+
}, 1000);

apps/run/settings.js

+40-26
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,58 @@
11
(function(back) {
22
const SETTINGS_FILE = "run.json";
3-
4-
// initialize with default settings...
5-
let s = {
6-
'use_gps': true,
7-
'use_hrm': true
8-
}
3+
var ExStats = require("exstats");
4+
var statsList = ExStats.getList();
5+
statsList.unshift({name:"-",id:""}); // add blank menu item
6+
var statsIDs = statsList.map(s=>s.id);
97

108
// ...and overwrite them with any saved values
119
// This way saved values are preserved if a new version adds more settings
1210
const storage = require('Storage')
13-
let settings = storage.readJSON(SETTINGS_FILE, 1) || {}
14-
const saved = settings || {}
15-
for (const key in saved) {
16-
s[key] = saved[key]
17-
}
18-
11+
let settings = Object.assign({
12+
B1 : "dist",
13+
B2 : "time",
14+
B3 : "pacea",
15+
B4 : "bpm",
16+
B5 : "step",
17+
B6 : "caden",
18+
paceLength : 1000
19+
}, storage.readJSON(SETTINGS_FILE, 1) || {});
1920
function save() {
20-
settings = s
2121
storage.write(SETTINGS_FILE, settings)
2222
}
2323

24+
function getBoxChooser(boxID) {
25+
return {
26+
min :0, max: statsIDs.length-1,
27+
value: Math.max(statsIDs.indexOf(settings[boxID]),0),
28+
format: v => statsList[v].name,
29+
onchange: v => {
30+
settings[boxID] = statsIDs[v];
31+
save();
32+
},
33+
}
34+
}
35+
36+
var paceNames = ["1000m","1 mile","1/2 Mthn", "Marathon",];
37+
var paceAmts = [1000,1609,21098,42195];
2438
E.showMenu({
2539
'': { 'title': 'Run' },
2640
'< Back': back,
27-
'Use GPS': {
28-
value: s.use_gps,
29-
format: () => (s.use_gps ? 'Yes' : 'No'),
30-
onchange: () => {
31-
s.use_gps = !s.use_gps;
41+
'Pace': {
42+
min :0, max: paceNames.length-1,
43+
value: Math.max(paceAmts.indexOf(settings.paceLength),0),
44+
format: v => paceNames[v],
45+
onchange: v => {
46+
settings.paceLength = paceAmts[v];
47+
print(settings);
3248
save();
3349
},
3450
},
35-
'Use HRM': {
36-
value: s.use_hrm,
37-
format: () => (s.use_hrm ? 'Yes' : 'No'),
38-
onchange: () => {
39-
s.use_hrm = !s.use_hrm;
40-
save();
41-
},
42-
}
51+
'Box 1': getBoxChooser("B1"),
52+
'Box 2': getBoxChooser("B2"),
53+
'Box 3': getBoxChooser("B3"),
54+
'Box 4': getBoxChooser("B4"),
55+
'Box 5': getBoxChooser("B5"),
56+
'Box 6': getBoxChooser("B6"),
4357
})
4458
})

0 commit comments

Comments
 (0)