/
widget.js
357 lines (348 loc) · 12.6 KB
/
widget.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
{
let storageFile; // file for GPS track
let activeRecorders = [];
let writeSetup; // the interval for writing, or 'true' if using GPS
let writeSubSecs; // true if we should write .1s for time, otherwise round to nearest second
let loadSettings = function() {
var settings = require("Storage").readJSON("recorder.json",1)||{};
settings.period = settings.period||10;
if (!settings.file || !settings.file.startsWith("recorder.log"))
settings.recording = false;
if (!settings.record)
settings.record = ["gps"];
return settings;
}
let updateSettings = function(settings) {
require("Storage").writeJSON("recorder.json", settings);
if (WIDGETS["recorder"]) WIDGETS["recorder"].reload();
}
let getRecorders = function() {
var recorders = {
gps:function() {
var lat = 0;
var lon = 0;
var alt = 0;
var samples = 0;
var hasFix = 0;
function onGPS(f) {
hasFix = f.fix;
if (!hasFix) return;
lat += f.lat;
lon += f.lon;
alt += f.alt;
samples++;
}
return {
name : "GPS",
fields : ["Latitude","Longitude","Altitude"],
getValues : () => {
var r = ["","",""];
if (samples)
r = [(lat/samples).toFixed(6),(lon/samples).toFixed(6),Math.round(alt/samples)];
samples = 0; lat = 0; lon = 0; alt = 0;
return r;
},
start : () => {
hasFix = false;
Bangle.on('GPS', onGPS);
Bangle.setGPSPower(1,"recorder");
},
stop : () => {
hasFix = false;
Bangle.removeListener('GPS', onGPS);
Bangle.setGPSPower(0,"recorder");
},
draw : (x,y) => g.setColor(hasFix?"#0f0":"#f88").drawImage(atob("DAwBEAKARAKQE4DwHkPqPRGKAEAA"),x,y)
};
},
hrm:function() {
var bpm = "", bpmConfidence = "", src="";
function onHRM(h) {
bpmConfidence = h.confidence;
bpm = h.bpm;
src = h.src;
}
return {
name : "HR",
fields : ["Heartrate", "Confidence", "Source"],
getValues : () => {
var r = [bpm,bpmConfidence,src];
bpm = ""; bpmConfidence = ""; src="";
return r;
},
start : () => {
Bangle.on('HRM', onHRM);
Bangle.setHRMPower(1,"recorder");
},
stop : () => {
Bangle.removeListener('HRM', onHRM);
Bangle.setHRMPower(0,"recorder");
},
draw : (x,y) => g.setColor(Bangle.isHRMOn()?"#f00":"#f88").drawImage(atob("DAwBAAAAMMeef+f+f+P8H4DwBgAA"),x,y)
};
},
bat:function() {
return {
name : "BAT",
fields : ["Battery Percentage", "Battery Voltage", "Charging"],
getValues : () => {
return [E.getBattery(), NRF.getBattery(), Bangle.isCharging()];
},
start : () => {
},
stop : () => {
},
draw : (x,y) => g.setColor(Bangle.isCharging() ? "#0f0" : "#ff0").drawImage(atob("DAwBAABgH4G4EYG4H4H4H4GIH4AA"),x,y)
};
},
steps:function() {
var lastSteps = 0;
return {
name : "Steps",
fields : ["Steps"],
getValues : () => {
var c = Bangle.getStepCount(), r=[c-lastSteps];
lastSteps = c;
return r;
},
start : () => { lastSteps = Bangle.getStepCount(); },
stop : () => {},
draw : (x,y) => g.reset().drawImage(atob("DAwBAAMMeeeeeeeecOMMAAMMMMAA"),x,y)
};
}
};
if (Bangle.getPressure){
recorders['baro'] = function() {
var temp="",press="",alt="";
function onPress(c) {
temp=c.temperature;
press=c.pressure;
alt=c.altitude;
}
return {
name : "Baro",
fields : ["Barometer Temperature", "Barometer Pressure", "Barometer Altitude"],
getValues : () => {
var r = [temp,press,alt];
temp="";
press="";
alt="";
return r;
},
start : () => {
Bangle.setBarometerPower(1,"recorder");
Bangle.on('pressure', onPress);
},
stop : () => {
Bangle.setBarometerPower(0,"recorder");
Bangle.removeListener('pressure', onPress);
},
draw : (x,y) => g.setColor("#0f0").drawImage(atob("DAwBAAH4EIHIEIHIEIHIEIEIH4AA"),x,y)
};
}
}
/* eg. foobar.recorder.js
(function(recorders) {
recorders.foobar = {
name : "Foobar",
fields : ["foobar"],
getValues : () => [123],
start : () => {},
stop : () => {},
draw (x,y) => {} // draw 12x12px status image
}
})
*/
require("Storage").list(/^.*\.recorder\.js$/).forEach(fn=>eval(require("Storage").read(fn))(recorders));
return recorders;
}
let getActiveRecorders = function(settings) {
let activeRecorders = [];
let recorders = getRecorders();
settings.record.forEach(r => {
var recorder = recorders[r];
if (!recorder) {
console.log(/*LANG*/"Recorder for "+E.toJS(r)+/*LANG*/"+not found");
return;
}
activeRecorders.push(recorder());
});
return activeRecorders;
};
let getCSVHeaders = activeRecorders => ["Time"].concat(activeRecorders.map(r=>r.fields));
let writeLog = function() {
WIDGETS["recorder"].draw();
try {
var fields = [writeSubSecs?getTime().toFixed(1):Math.round(getTime())];
activeRecorders.forEach(recorder => fields.push.apply(fields,recorder.getValues()));
if (storageFile) storageFile.write(fields.join(",")+"\n");
} catch(e) {
// If storage.write caused an error, disable
// GPS recording so we don't keep getting errors!
console.log("recorder: error", e);
var settings = loadSettings();
settings.recording = false;
require("Storage").write("recorder.json", settings);
reload();
}
}
// Called by the GPS app to reload settings and decide what to do
let reload = function() {
var settings = loadSettings();
if (typeof writeSetup === "number") clearInterval(writeSetup);
writeSetup = undefined;
Bangle.removeListener('GPS', writeLog);
activeRecorders.forEach(rec => rec.stop());
activeRecorders = [];
if (settings.recording) {
// set up recorders
activeRecorders = getActiveRecorders(settings);
activeRecorders.forEach(activeRecorder => {
activeRecorder.start();
});
WIDGETS["recorder"].width = 15 + ((activeRecorders.length+1)>>1)*12; // 12px per recorder
// open/create file
if (require("Storage").list(settings.file).length) { // Append
storageFile = require("Storage").open(settings.file,"a");
// TODO: what if loaded modules are different??
} else {
storageFile = require("Storage").open(settings.file,"w");
// New file - write headers
storageFile.write(getCSVHeaders(activeRecorders).join(",")+"\n");
}
// start recording...
WIDGETS["recorder"].draw();
writeSubSecs = settings.period===1;
if (settings.period===1 && settings.record.includes("gps")) {
Bangle.on('GPS', writeLog);
writeSetup = true;
} else {
writeSetup = setInterval(writeLog, settings.period*1000, settings.period);
}
} else {
WIDGETS["recorder"].width = 0;
storageFile = undefined;
}
}
// add the widget
WIDGETS["recorder"]={area:"tl",width:0,draw:function() {
if (!writeSetup) return;
g.reset().drawImage(atob("DRSBAAGAHgDwAwAAA8B/D/hvx38zzh4w8A+AbgMwGYDMDGBjAA=="),this.x+1,this.y+2);
activeRecorders.forEach((recorder,i)=>{
recorder.draw(this.x+15+(i>>1)*12, this.y+(i&1)*12);
});
},getRecorders:getRecorders,reload:function() {
reload();
Bangle.drawWidgets(); // relayout all widgets
},isRecording:function() {
return !!writeSetup;
},setRecording:function(isOn, options) {
/* options = {
force : [optional] "append"/"new"/"overwrite" - don't ask, just do what's requested
} */
var settings = loadSettings();
options = options||{};
if (isOn && !settings.recording) {
var date=(new Date()).toISOString().substr(0,10).replace(/-/g,""), trackNo=10;
function getTrackFilename() { return "recorder.log" + date + trackNo.toString(36) + ".csv"; }
if (!settings.file || !settings.file.startsWith("recorder.log" + date)) {
// if no filename set or date different, set up a new filename
settings.file = getTrackFilename();
}
var headers = require("Storage").open(settings.file,"r").readLine();
if (headers){ // if file exists
if(headers.trim()!==getCSVHeaders(getActiveRecorders(settings)).join(",")){
// headers don't match, reset (#3081)
options.force = "new";
}
if (!options.force) { // if not forced, ask the question
g.reset(); // work around bug in 2v17 and earlier where bg color wasn't reset
return E.showPrompt(
/*LANG*/"Overwrite\nLog " + settings.file.match(/^recorder\.log(.*)\.csv$/)[1] + "?",
{ title:/*LANG*/"Recorder",
buttons:{/*LANG*/"Yes":"overwrite",/*LANG*/"No":"cancel",/*LANG*/"New":"new",/*LANG*/"Append":"append"}
}).then(selection=>{
if (selection==="cancel") return false; // just cancel
if (selection==="overwrite") return WIDGETS["recorder"].setRecording(1,{force:"overwrite"});
if (selection==="new") return WIDGETS["recorder"].setRecording(1,{force:"new"});
if (selection==="append") return WIDGETS["recorder"].setRecording(1,{force:"append"});
throw new Error("Unknown response!");
});
} else if (options.force=="append") {
// do nothing, filename is the same - we are good
} else if (options.force=="overwrite") {
// wipe the file
require("Storage").open(settings.file,"r").erase();
} else if (options.force=="new") {
// new file - use the current date
var newFileName;
do { // while a file exists, add one to the letter after the date
newFileName = getTrackFilename();
trackNo++;
} while (require("Storage").list(newFileName).length);
settings.file = newFileName;
} else throw new Error("Unknown options.force, "+options.force);
}
}
settings.recording = isOn;
updateSettings(settings);
WIDGETS["recorder"].reload();
return Promise.resolve(settings.recording);
},plotTrack:function(m, options) { // m=instance of openstmap module
/* Plots the current track in the currently set color.
options can be {
async: if true, plots the path a bit at a time - returns an object with a 'stop' function to stop
callback: a function to call back when plotting is finished
}
*/
options = options||{};
var settings = loadSettings();
if (!settings.file) return; // no file specified
// keep function to draw track in RAM
function plot(g) { "ram";
var f = require("Storage").open(settings.file,"r");
var l = f.readLine();
if (l===undefined) return; // empty file?
var mp, c = l.split(",");
var la=c.indexOf("Latitude"),lo=c.indexOf("Longitude");
if (la<0 || lo<0) return; // no GPS!
l = f.readLine();c=[];
while (l && !c[la]) {
c = l.split(",");
l = f.readLine(f);
}
var asyncTimeout;
var color = g.getColor();
function plotPartial() {
asyncTimeout = undefined;
if (l===undefined) return; // empty file?
mp = m.latLonToXY(+c[la], +c[lo]);
g.moveTo(mp.x,mp.y).setColor(color);
l = f.readLine(f);
var n = options.async ? 10 : 200; // only plot first 200 points to keep things fast(ish)
while(l && n--) {
c = l.split(",");
if (c[la]) {
mp = m.latLonToXY(+c[la], +c[lo]);
g.lineTo(mp.x,mp.y);
}
l = f.readLine(f);
}
if (options.async && n<0)
asyncTimeout = setTimeout(plotPartial, 20);
else if (options.callback) options.callback();
}
plotPartial();
if (options.async) return {
stop : function() {
if (asyncTimeout) clearTimeout(asyncTimeout);
asyncTimeout = undefined;
if (options.callback) options.callback();
}
};
}
return plot(g);
}};
// load settings, set correct widget width
reload();
}