Merge branch 'upstream/master' into fix/improve-formatter-check

master
Rob Pilling 2024-02-05 22:30:31 +00:00
commit b2ff9dbb78
24 changed files with 461 additions and 281 deletions

View File

@ -1 +1 @@
0.01: New Widget!
0.01: New Clock Info!

View File

@ -0,0 +1 @@
0.01: New Clock!

View File

@ -0,0 +1,25 @@
# Clock Name
More info on making Clock Faces: https://www.espruino.com/Bangle.js+Clock
Describe the Clock...
## Usage
Describe how to use it
## Features
Name the function
## Controls
Name the buttons and what they are used for
## Requests
Name who should be contacted for support/update requests
## Creator
Your name

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwJC/AH4A/AH4AgA=="))

View File

@ -0,0 +1,44 @@
// timeout used to update every minute
var drawTimeout;
// schedule a draw for the next minute
function queueDraw() {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = setTimeout(function() {
drawTimeout = undefined;
draw();
}, 60000 - (Date.now() % 60000));
}
function draw() {
// queue next draw in one minute
queueDraw();
// Work out where to draw...
var x = g.getWidth()/2;
var y = g.getHeight()/2;
g.reset();
// work out locale-friendly date/time
var date = new Date();
var timeStr = require("locale").time(date,1);
var dateStr = require("locale").date(date);
// draw time
g.setFontAlign(0,0).setFont("Vector",48);
g.clearRect(0,y-15,g.getWidth(),y+25); // clear the background
g.drawString(timeStr,x,y);
// draw date
y += 35;
g.setFontAlign(0,0).setFont("6x8");
g.clearRect(0,y-4,g.getWidth(),y+4); // clear the background
g.drawString(dateStr,x,y);
}
// Clear the screen once, at startup
g.clear();
// draw immediately at first, queue update
draw();
// Show launcher when middle button pressed
Bangle.setUI("clock");
// Load widgets
Bangle.loadWidgets();
Bangle.drawWidgets();

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,15 @@
{ "id": "7chname",
"name": "My clock human readable name",
"shortName":"Short Name",
"version":"0.01",
"description": "A detailed description of my clock",
"icon": "icon.png",
"type": "clock",
"tags": "clock",
"supports" : ["BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name":"7chname.app.js","url":"app.js"},
{"name":"7chname.img","url":"app-icon.js","evaluate":true}
]
}

View File

@ -32,3 +32,4 @@
Allow alarm enable/disable
0.31: Implement API for activity fetching
0.32: Added support for loyalty cards from gadgetbridge
0.33: Fix alarms created in Gadgetbridge not repeating

View File

@ -81,7 +81,12 @@
for (var j = 0; j < event.d.length; j++) {
// prevents all alarms from going off at once??
var dow = event.d[j].rep;
if (!dow) dow = 127; //if no DOW selected, set alarm to all DOW
var rp = false;
if (!dow) {
dow = 127; //if no DOW selected, set alarm to all DOW
} else {
rp = true;
}
var last = (event.d[j].h * 3600000 + event.d[j].m * 60000 < currentTime) ? (new Date()).getDate() : 0;
var a = require("sched").newDefaultAlarm();
a.id = "gb"+j;
@ -89,6 +94,7 @@
a.on = event.d[j].on !== undefined ? event.d[j].on : true;
a.t = event.d[j].h * 3600000 + event.d[j].m * 60000;
a.dow = ((dow&63)<<1) | (dow>>6); // Gadgetbridge sends DOW in a different format
a.rp = rp;
a.last = last;
alarms.push(a);
}

View File

@ -2,7 +2,7 @@
"id": "android",
"name": "Android Integration",
"shortName": "Android",
"version": "0.32",
"version": "0.33",
"description": "Display notifications/music/etc sent from the Gadgetbridge app on Android. This replaces the old 'Gadgetbridge' Bangle.js widget.",
"icon": "app.png",
"tags": "tool,system,messages,notifications,gadgetbridge",

View File

@ -2,7 +2,7 @@
"name": "BTHome Temperature and Pressure",
"shortName":"BTHome T",
"version":"0.02",
"description": "Displays temperature and pressure, and advertises them over bluetooth using BTHome.io standard",
"description": "Displays temperature and pressure, and advertises them over bluetooth for Home Assistant using BTHome.io standard",
"icon": "app.png",
"tags": "bthome,bluetooth,temperature",
"supports" : ["BANGLEJS2"],

View File

@ -2,7 +2,7 @@
"id": "ha",
"name": "Home Assistant",
"version": "0.10",
"description": "Integrates your Bangle.js into Home Assistant.",
"description": "Integrates your Bangle.js into Home Assistant using Android Integration/Gadgetbridge",
"icon": "ha.png",
"type": "app",
"tags": "tool,clkinfo,bluetooth",

View File

@ -3,7 +3,7 @@
"name": "Home Assistant Sensors",
"shortName": "HA sensors",
"version": "0.02",
"description": "Send sensor values to Home Assistant using the Android Integration.",
"description": "Send sensor values to Home Assistant using Android Integration/Gadgetbridge",
"icon": "ha.png",
"type": "bootloader",
"tags": "tool,sensors",

View File

@ -1 +1,2 @@
0.01: New App!
0.02: Add more control styles

View File

@ -10,8 +10,21 @@ in the future this app will be able to support other types of remote (see below)
## Usage
Run the app, and ensure you're not connected to your watch via Bluetooth
(a warning will pop up if so).
Run the app, then choose the type of controls you want and ensure you're not connected
to your watch via Bluetooth (a warning will pop up if so).
Linear mode controls A/B axes individually, and allows you to vary the speed of the
motors based on the distance you drag from the centre. Other modes just use on/off
buttons.
| Mode | up | down | left | right |
|------------|------|------|------|-------|
| **Linear** | +A | -A | -B | +B |
| **Normal** | +A | -A | -B | +B |
| **Tank** | -A+B | +A-B | +A+B | -A-B |
| **Merged** | -A-B | +A+B | +A-B | -A+B |
In all cases pressing the C/D buttons will turn on C/D outputs
Now press the arrow keys on the screen to control the robot.

View File

@ -1,5 +1,4 @@
var lego = require("mouldking");
lego.start();
E.on('kill', () => {
// return to normal Bluetooth advertising
NRF.setAdvertising({},{showName:true});
@ -12,59 +11,133 @@ var controlState = "";
Bangle.loadWidgets();
Bangle.drawWidgets();
var R = Bangle.appRect;
// we'll divide up into 3x3
function getBoxCoords(x,y) {
return {
x : R.x + R.w*x/3,
y : R.y + R.h*y/3
};
}
function draw() {
g.reset().clearRect(R);
var c, ninety = Math.PI/2;
var colOn = "#f00", colOff = g.theme.fg;
c = getBoxCoords(1.5, 0.5);
g.setColor(controlState=="up"?colOn:colOff).drawImage(arrowIcon,c.x,c.y,{rotate:0});
c = getBoxCoords(2.5, 1.5);
g.setColor(controlState=="right"?colOn:colOff).drawImage(arrowIcon,c.x,c.y,{rotate:ninety});
c = getBoxCoords(0.5, 1.5);
g.setColor(controlState=="left"?colOn:colOff).drawImage(arrowIcon,c.x,c.y,{rotate:-ninety});
c = getBoxCoords(1.5, 1.5);
g.setColor(controlState=="down"?colOn:colOff).drawImage(arrowIcon,c.x,c.y,{rotate:ninety*2});
if (NRF.getSecurityStatus().connected) {
c = getBoxCoords(1.5, 2.5);
g.setFontAlign(0,0).setFont("6x8").drawString("WARNING:\nBluetooth Connected\nYou must disconnect\nbefore LEGO will work",c.x,c.y);
function startLegoButtons(controls) {
// we'll divide up into 3x3
function getBoxCoords(x,y) {
return {
x : R.x + R.w*x/3,
y : R.y + R.h*y/3
};
}
}
draw();
NRF.on('connect', draw);
NRF.on('disconnect', draw);
function setControlState(s) {
controlState = s;
var c = {};
var speed = 3;
if (s=="up") c={a:-speed,b:-speed};
if (s=="down") c={a:speed,b:speed};
if (s=="left") c={a:speed,b:-speed};
if (s=="right") c={a:-speed,b:speed};
function draw() {
g.reset().clearRect(R);
var c, ninety = Math.PI/2;
var colOn = "#f00", colOff = g.theme.fg;
c = getBoxCoords(1.5, 0.5);
g.setColor(controlState=="up"?colOn:colOff).drawImage(arrowIcon,c.x,c.y,{rotate:0});
c = getBoxCoords(2.5, 1.5);
g.setColor(controlState=="right"?colOn:colOff).drawImage(arrowIcon,c.x,c.y,{rotate:ninety});
c = getBoxCoords(0.5, 1.5);
g.setColor(controlState=="left"?colOn:colOff).drawImage(arrowIcon,c.x,c.y,{rotate:-ninety});
c = getBoxCoords(1.5, 1.5);
g.setColor(controlState=="down"?colOn:colOff).drawImage(arrowIcon,c.x,c.y,{rotate:ninety*2});
if (NRF.getSecurityStatus().connected) {
c = getBoxCoords(1.5, 2.5);
g.setFontAlign(0,0).setFont("6x8").drawString("WARNING:\nBluetooth Connected\nYou must disconnect\nbefore LEGO will work",c.x,c.y);
}
g.setFont("6x8:3").setFontAlign(0,0);
c = getBoxCoords(0.5, 0.5);
g.setColor(controlState=="c"?colOn:colOff).drawString("C",c.x,c.y);
c = getBoxCoords(2.5, 0.5);
g.setColor(controlState=="d"?colOn:colOff).drawString("D",c.x,c.y);
}
function setControlState(s) {
controlState = s;
var c = {};
if (s in controls)
c = controls[s];
draw();
lego.set(c);
}
lego.start();
Bangle.setUI({mode:"custom", drag : e => {
var x = Math.floor(E.clip((e.x - R.x) * 3 / R.w,0,2.99));
var y = Math.floor(E.clip((e.y - R.y) * 3 / R.h,0,2.99));
if (!e.b) {
setControlState("");
return;
}
if (y==0) { // top row
if (x==0) setControlState("c");
if (x==1) setControlState("up");
if (x==2) setControlState("d");
} else if (y==1) {
if (x==0) setControlState("left");
if (x==1) setControlState("down");
if (x==2) setControlState("right");
}
}});
draw();
lego.set(c);
NRF.on('connect', draw);
NRF.on('disconnect', draw);
}
Bangle.on('drag',e => {
var x = Math.floor(E.clip((e.x - R.x) * 3 / R.w,0,2.99));
var y = Math.floor(E.clip((e.y - R.y) * 3 / R.h,0,2.99));
if (!e.b) {
setControlState("");
return;
}
if (y==0) { // top row
if (x==1) setControlState("up");
} else if (y==1) {
if (x==0) setControlState("left");
if (x==1) setControlState("down");
if (x==2) setControlState("right");
function startLegoLinear() {
var mx = R.x+R.w/2;
var my = R.y+R.h/2;
var x=0,y=0;
var scale = 10;
function draw() {
g.reset().clearRect(R);
for (var i=3;i<60;i+=10)
g.drawCircle(mx,my,i);
g.setColor("#F00");
var px = E.clip(mx + x*scale, R.x+20, R.x2-20);
var py = E.clip(my + y*scale, R.y+20, R.y2-20);
g.fillCircle(px, py, 20);
}
lego.start();
Bangle.setUI({mode:"custom", drag : e => {
x = Math.round((e.x - mx) / scale);
y = Math.round((e.y - my) / scale);
if (!e.b) {
x=0; y=0;
}
lego.set({a:x, b:y});
draw();
}});
draw();
NRF.on('connect', draw);
NRF.on('disconnect', draw);
}
// Mappings of button to output
const CONTROLS = {
normal : {
up : {a: 7,b: 0},
down : {a:-7,b: 0},
left : {a: 0,b:-7},
right: {a: 0,b: 7},
c : {c:7},
d : {d:7}
}, tank : {
up : {a:-7,b:7},
down : {a: 7,b:-7},
left : {a: 7,b:7},
right: {a:-7,b:-7},
c : {c:7},
d : {d:7}
}, merged : {
up : {a: 7,b: 7},
down : {a:-7,b:-7},
left : {a: 7,b:-7},
right: {a:-7,b: 7},
c : {c:7},
d : {d:7}
}
};
E.showMenu({ "" : {title:"LEGO Remote", back:()=>load()},
"Linear" : () => startLegoLinear(),
"Normal" : () => startLego(CONTROLS.normal),
"Tank" : () => startLego(CONTROLS.tank),
"Marged" : () => startLego(CONTROLS.merged),
});

View File

@ -1,7 +1,7 @@
{ "id": "legoremote",
"name": "LEGO Remote control",
"shortName":"LEGO Remote",
"version":"0.01",
"version":"0.02",
"description": "Use your Bangle.js to control LEGO models. See the README for compatibility",
"icon": "app.png",
"tags": "toy,lego,bluetooth",

View File

@ -33,3 +33,5 @@
0.26: Ensure that when redrawing, we always cancel any in-progress track draw
0.27: Display message if no map is installed
0.28: Fix rounding errors
0.29: Keep exit at bottom of menu
Speed up latLonToXY for track rendering

View File

@ -172,7 +172,6 @@ function showMenu() {
var menu = {
"":{title:/*LANG*/"Map"},
"< Back": ()=> showMap(),
/*LANG*/"Exit": () => load(),
};
// If we have a GPS fix, add a menu item to center it
if (fix.fix) menu[/*LANG*/"Center GPS"]=() =>{
@ -180,7 +179,6 @@ function showMenu() {
m.lon = fix.lon;
showMap();
};
menu = Object.assign(menu, {
/*LANG*/"Zoom In": () =>{
m.scale /= 2;
@ -234,6 +232,7 @@ function showMenu() {
}
};
}
menu[/*LANG*/"Exit"] = () => load();
E.showMenu(menu);
}

View File

@ -2,7 +2,7 @@
"id": "openstmap",
"name": "OpenStreetMap",
"shortName": "OpenStMap",
"version": "0.28",
"version": "0.29",
"description": "Loads map tiles from OpenStreetMap onto your Bangle.js and displays a map of where you are. Once installed this also adds map functionality to `GPS Recorder` and `Recorder` apps",
"readme": "README.md",
"icon": "app.png",

View File

@ -38,17 +38,17 @@ if (m.map) {
m.lat = m.map.lat; // position of middle of screen
m.lon = m.map.lon; // position of middle of screen
}
var CX = g.getWidth()/2;
var CY = g.getHeight()/2;
// return number of tiles drawn
exports.draw = function() {
var cx = g.getWidth()/2;
var cy = g.getHeight()/2;
var p = Bangle.project({lat:m.lat,lon:m.lon});
let count = 0;
m.maps.forEach((map,idx) => {
var d = map.scale/m.scale;
var ix = (p.x-map.center.x)/m.scale + (map.imgx*d/2) - cx;
var iy = (map.center.y-p.y)/m.scale + (map.imgy*d/2) - cy;
var ix = (p.x-map.center.x)/m.scale + (map.imgx*d/2) - CX;
var iy = (map.center.y-p.y)/m.scale + (map.imgy*d/2) - CY;
var o = {};
var s = map.tilesize;
if (d!=1) { // if the two are different, add scaling
@ -85,14 +85,12 @@ exports.draw = function() {
};
/// Convert lat/lon to pixels on the screen
exports.latLonToXY = function(lat, lon) {
var p = Bangle.project({lat:m.lat,lon:m.lon});
var q = Bangle.project({lat:lat, lon:lon});
var cx = g.getWidth()/2;
var cy = g.getHeight()/2;
exports.latLonToXY = function(lat, lon) { "ram"
var p = Bangle.project({lat:m.lat,lon:m.lon}),
q = Bangle.project({lat:lat, lon:lon});
return {
x : Math.round((q.x-p.x)/m.scale + cx),
y : Math.round(cy - (q.y-p.y)/m.scale)
x : Math.round((q.x-p.x)/m.scale + CX),
y : Math.round(CY - (q.y-p.y)/m.scale)
};
};

View File

@ -45,3 +45,4 @@
0.36: When recording with 1 second periods, log time with one decimal.
0.37: 1 second periods + gps log => log when gps event is received, not with
setInterval.
0.38: Tweaks to speed up track rendering

View File

@ -213,229 +213,229 @@ function viewTrack(filename, info) {
});
};
menu['< Back'] = () => { viewTracks(); };
return E.showMenu(menu);
}
function plotTrack(info) { "ram"
function distance(lat1,long1,lat2,long2) { "ram"
var x = (long1-long2) * Math.cos((lat1+lat2)*Math.PI/360);
var y = lat2 - lat1;
return Math.sqrt(x*x + y*y) * 6371000 * Math.PI / 180;
}
function plotTrack(info) { "ram"
function distance(lat1,long1,lat2,long2) { "ram"
var x = (long1-long2) * Math.cos((lat1+lat2)*Math.PI/360);
var y = lat2 - lat1;
return Math.sqrt(x*x + y*y) * 6371000 * Math.PI / 180;
}
// Function to convert lat/lon to XY
var getMapXY;
if (info.qOSTM) {
// scale map to view full track
const max = Bangle.project({lat: info.maxLat, lon: info.maxLong});
const min = Bangle.project({lat: info.minLat, lon: info.minLong});
const scaleX = (max.x-min.x)/Bangle.appRect.w;
const scaleY = (max.y-min.y)/Bangle.appRect.h;
osm.scale = Math.ceil((scaleX > scaleY ? scaleX : scaleY)*1.1); // add 10% margin
getMapXY = osm.latLonToXY.bind(osm);
} else {
getMapXY = function(lat, lon) { "ram"
return {x:cx + Math.round((long - info.lon)*info.lfactor*info.scale),
y:cy + Math.round((info.lat - lat)*info.scale)};
};
}
// Function to convert lat/lon to XY
var XY;
if (info.qOSTM) {
// scale map to view full track
const max = Bangle.project({lat: info.maxLat, lon: info.maxLong});
const min = Bangle.project({lat: info.minLat, lon: info.minLong});
const scaleX = (max.x-min.x)/Bangle.appRect.w;
const scaleY = (max.y-min.y)/Bangle.appRect.h;
osm.scale = Math.ceil((scaleX > scaleY ? scaleX : scaleY)*1.1); // add 10% margin
XY = osm.latLonToXY.bind(osm);
} else {
XY = function(lat, lon) { "ram"
return {x:cx + Math.round((long - info.lon)*info.lfactor*info.scale),
y:cy + Math.round((info.lat - lat)*info.scale)};
};
}
E.showMenu(); // remove menu
E.showMessage(/*LANG*/"Drawing...",/*LANG*/"Track "+info.fn);
g.flip(); // on buffered screens, draw a not saying we're busy
g.clear(1);
var s = require("Storage");
var W = g.getWidth();
var H = g.getHeight();
var cx = W/2;
var cy = 24 + (H-24)/2;
if (!info.qOSTM) {
g.setColor("#f00").fillRect(9,80,11,120).fillPoly([9,60,19,80,0,80]);
g.setColor(g.theme.fg).setFont("6x8").setFontAlign(0,0).drawString("N",10,50);
} else {
osm.lat = info.lat;
osm.lon = info.lon;
osm.draw();
g.setColor("#000");
E.showMenu(); // remove menu
E.showMessage(/*LANG*/"Drawing...",/*LANG*/"Track "+info.fn);
g.flip(); // on buffered screens, draw a not saying we're busy
g.clear(1);
var s = require("Storage");
var G = g;
var W = g.getWidth();
var H = g.getHeight();
var cx = W/2;
var cy = 24 + (H-24)/2;
if (!info.qOSTM) {
g.setColor("#f00").fillRect(9,80,11,120).fillPoly([9,60,19,80,0,80]);
g.setColor(g.theme.fg).setFont("6x8").setFontAlign(0,0).drawString("N",10,50);
} else {
osm.lat = info.lat;
osm.lon = info.lon;
osm.draw();
g.setColor("#000");
}
var latIdx = info.fields.indexOf("Latitude");
var lonIdx = info.fields.indexOf("Longitude");
g.drawString(asTime(info.duration),10,220);
var f = require("Storage").open(info.filename,"r");
if (f===undefined) return;
var l = f.readLine(f);
l = f.readLine(f); // skip headers
var ox=0;
var oy=0;
var olat,olong,dist=0;
var i=0, c = l.split(",");
// skip until we find our first data
while(l!==undefined && c[latIdx]=="") {
c = l.split(",");
l = f.readLine(f);
}
// now start plotting
var lat = +c[latIdx];
var long = +c[lonIdx];
var mp = XY(lat, long);
g.moveTo(mp.x,mp.y);
g.setColor("#0f0");
g.fillCircle(mp.x,mp.y,5);
if (info.qOSTM) g.setColor("#f09");
else g.setColor(g.theme.fg);
l = f.readLine(f);
g.flip(); // force update
while(l!==undefined) {
c = l.split(",");l = f.readLine(f);
if (c[latIdx]=="")continue;
lat = +c[latIdx];
long = +c[lonIdx];
mp = XY(lat, long);
G.lineTo(mp.x,mp.y);
if (info.qOSTM) G.fillCircle(mp.x,mp.y,2); // make the track more visible
var d = distance(olat,olong,lat,long);
if (!isNaN(d)) dist+=d;
olat = lat;
olong = long;
ox = mp.x;
oy = mp.y;
if (++i > 100) { G.flip();i=0; }
}
g.setColor("#f00");
g.fillCircle(ox,oy,5);
if (info.qOSTM) g.setColor("#000");
else g.setColor(g.theme.fg);
g.drawString(require("locale").distance(dist,2),g.getWidth() / 2, g.getHeight() - 20);
g.setFont("6x8",2);
g.setFontAlign(0,0,3);
var isBTN3 = "BTN3" in global;
g.drawString(/*LANG*/"Back",g.getWidth() - 10, isBTN3 ? (g.getHeight() - 40) : (g.getHeight()/2));
setWatch(function() {
viewTrack(info.fn, info);
}, isBTN3?BTN3:BTN1);
Bangle.drawWidgets();
g.flip();
}
function plotGraph(info, style) { "ram"
E.showMenu(); // remove menu
E.showMessage(/*LANG*/"Calculating...",/*LANG*/"Track "+info.fn);
var filename = info.filename;
var infn = new Float32Array(80);
var infc = new Uint16Array(80);
var title;
var lt = 0; // last time
var tn = 0; // count for each time period
var strt, dur = info.duration;
var f = require("Storage").open(filename,"r");
if (f===undefined) return;
var l = f.readLine(f);
l = f.readLine(f); // skip headers
var nl = 0, c, i;
var factor = 1; // multiplier used for values when graphing
var timeIdx = info.fields.indexOf("Time");
if (l!==undefined) {
c = l.split(",");
strt = c[timeIdx];
}
if (style=="Heartrate") {
title = /*LANG*/"Heartrate (bpm)";
var hrmIdx = info.fields.indexOf("Heartrate");
while(l!==undefined) {
++nl;c=l.split(",");l = f.readLine(f);
if (c[hrmIdx]=="") continue;
i = Math.round(80*(c[timeIdx] - strt)/dur);
infn[i]+=+c[hrmIdx];
infc[i]++;
}
} else if (style=="Altitude") {
title = /*LANG*/"Altitude (m)";
var altIdx = info.fields.indexOf("Barometer Altitude");
if (altIdx<0) altIdx = info.fields.indexOf("Altitude");
while(l!==undefined) {
++nl;c=l.split(",");l = f.readLine(f);
if (c[altIdx]=="") continue;
i = Math.round(80*(c[timeIdx] - strt)/dur);
infn[i]+=+c[altIdx];
infc[i]++;
}
} else if (style=="Speed") {
// use locate to work out units
var localeStr = require("locale").speed(1,5); // get what 1kph equates to
let units = localeStr.replace(/[0-9.]*/,"");
factor = parseFloat(localeStr)*3.6; // m/sec to whatever out units are
// title
title = /*LANG*/"Speed"+` (${units})`;
var latIdx = info.fields.indexOf("Latitude");
var lonIdx = info.fields.indexOf("Longitude");
g.drawString(asTime(info.duration),10,220);
var f = require("Storage").open(info.filename,"r");
if (f===undefined) return;
var l = f.readLine(f);
l = f.readLine(f); // skip headers
var ox=0;
var oy=0;
var olat,olong,dist=0;
var i=0, c = l.split(",");
// skip until we find our first data
while(l!==undefined && c[latIdx]=="") {
c = l.split(",");
l = f.readLine(f);
}
// now start plotting
var lat = +c[latIdx];
var long = +c[lonIdx];
var mp = getMapXY(lat, long);
g.moveTo(mp.x,mp.y);
g.setColor("#0f0");
g.fillCircle(mp.x,mp.y,5);
if (info.qOSTM) g.setColor("#f09");
else g.setColor(g.theme.fg);
l = f.readLine(f);
g.flip(); // force update
// now iterate
var p,lp = Bangle.project({lat:c[1],lon:c[2]});
var t,dx,dy,d,lt = c[timeIdx];
while(l!==undefined) {
c = l.split(",");l = f.readLine(f);
if (c[latIdx]=="")continue;
lat = +c[latIdx];
long = +c[lonIdx];
mp = getMapXY(lat, long);
g.lineTo(mp.x,mp.y);
if (info.qOSTM) g.fillCircle(mp.x,mp.y,2); // make the track more visible
var d = distance(olat,olong,lat,long);
if (!isNaN(d)) dist+=d;
olat = lat;
olong = long;
ox = mp.x;
oy = mp.y;
if (++i > 100) { g.flip();i=0; }
}
g.setColor("#f00");
g.fillCircle(ox,oy,5);
if (info.qOSTM) g.setColor("#000");
else g.setColor(g.theme.fg);
g.drawString(require("locale").distance(dist,2),g.getWidth() / 2, g.getHeight() - 20);
g.setFont("6x8",2);
g.setFontAlign(0,0,3);
var isBTN3 = "BTN3" in global;
g.drawString(/*LANG*/"Back",g.getWidth() - 10, isBTN3 ? (g.getHeight() - 40) : (g.getHeight()/2));
setWatch(function() {
viewTrack(info.fn, info);
}, isBTN3?BTN3:BTN1);
Bangle.drawWidgets();
g.flip();
}
function plotGraph(info, style) { "ram"
E.showMenu(); // remove menu
E.showMessage(/*LANG*/"Calculating...",/*LANG*/"Track "+info.fn);
var filename = info.filename;
var infn = new Float32Array(80);
var infc = new Uint16Array(80);
var title;
var lt = 0; // last time
var tn = 0; // count for each time period
var strt, dur = info.duration;
var f = require("Storage").open(filename,"r");
if (f===undefined) return;
var l = f.readLine(f);
l = f.readLine(f); // skip headers
var nl = 0, c, i;
var factor = 1; // multiplier used for values when graphing
var timeIdx = info.fields.indexOf("Time");
if (l!==undefined) {
c = l.split(",");
strt = c[timeIdx];
}
if (style=="Heartrate") {
title = /*LANG*/"Heartrate (bpm)";
var hrmIdx = info.fields.indexOf("Heartrate");
while(l!==undefined) {
++nl;c=l.split(",");l = f.readLine(f);
if (c[hrmIdx]=="") continue;
i = Math.round(80*(c[timeIdx] - strt)/dur);
infn[i]+=+c[hrmIdx];
++nl;c=l.split(",");
l = f.readLine(f);
if (c[latIdx] == "") {
continue;
}
t = c[timeIdx];
i = Math.round(80*(t - strt)/dur);
p = Bangle.project({lat:c[latIdx],lon:c[lonIdx]});
dx = p.x-lp.x;
dy = p.y-lp.y;
d = Math.sqrt(dx*dx+dy*dy);
if (t!=lt) {
infn[i]+=d / (t-lt); // speed
infc[i]++;
}
} else if (style=="Altitude") {
title = /*LANG*/"Altitude (m)";
var altIdx = info.fields.indexOf("Barometer Altitude");
if (altIdx<0) altIdx = info.fields.indexOf("Altitude");
while(l!==undefined) {
++nl;c=l.split(",");l = f.readLine(f);
if (c[altIdx]=="") continue;
i = Math.round(80*(c[timeIdx] - strt)/dur);
infn[i]+=+c[altIdx];
infc[i]++;
}
} else if (style=="Speed") {
// use locate to work out units
var localeStr = require("locale").speed(1,5); // get what 1kph equates to
let units = localeStr.replace(/[0-9.]*/,"");
factor = parseFloat(localeStr)*3.6; // m/sec to whatever out units are
// title
title = /*LANG*/"Speed"+` (${units})`;
var latIdx = info.fields.indexOf("Latitude");
var lonIdx = info.fields.indexOf("Longitude");
// skip until we find our first data
while(l!==undefined && c[latIdx]=="") {
c = l.split(",");
l = f.readLine(f);
}
// now iterate
var p,lp = Bangle.project({lat:c[1],lon:c[2]});
var t,dx,dy,d,lt = c[timeIdx];
while(l!==undefined) {
++nl;c=l.split(",");
l = f.readLine(f);
if (c[latIdx] == "") {
continue;
}
t = c[timeIdx];
i = Math.round(80*(t - strt)/dur);
p = Bangle.project({lat:c[latIdx],lon:c[lonIdx]});
dx = p.x-lp.x;
dy = p.y-lp.y;
d = Math.sqrt(dx*dx+dy*dy);
if (t!=lt) {
infn[i]+=d / (t-lt); // speed
infc[i]++;
}
lp = p;
lt = t;
}
} else throw new Error("Unknown type "+style);
var min=100000,max=-100000;
for (var i=0;i<infn.length;i++) {
if (infc[i]>0) infn[i]=factor*infn[i]/infc[i];
else { // no data - search back and see if we can find something
for (var j=i-1;j>=0;j--)
if (infc[j]) { infn[i]=infn[j]; break; }
}
var n = infn[i];
if (n>max) max=n;
if (n<min) min=n;
lp = p;
lt = t;
}
// work out a nice grid value
var heightDiff = max-min;
var grid = 1;
while (heightDiff/grid > 8) {
grid*=2;
} else throw new Error("Unknown type "+style);
var min=100000,max=-100000;
for (var i=0;i<infn.length;i++) {
if (infc[i]>0) infn[i]=factor*infn[i]/infc[i];
else { // no data - search back and see if we can find something
for (var j=i-1;j>=0;j--)
if (infc[j]) { infn[i]=infn[j]; break; }
}
// draw
g.clear(1).setFont("6x8",1);
var r = require("graph").drawLine(g, infn, {
x:4,y:24,
width: g.getWidth()-24,
height: g.getHeight()-(24+8),
axes : true,
gridy : grid,
gridx : infn.length / 3,
title: title,
miny: min,
maxy: max,
xlabel : x=>Math.round(x*dur/(60*infn.length))+/*LANG*/" min" // minutes
});
g.setFont("6x8",2);
g.setFontAlign(0,0,3);
var isBTN3 = "BTN3" in global;
g.drawString(/*LANG*/"Back",g.getWidth() - 10, isBTN3 ? (g.getHeight() - 40) : (g.getHeight()/2));
setWatch(function() {
viewTrack(info.filename, info);
}, isBTN3?BTN3:BTN1);
g.flip();
var n = infn[i];
if (n>max) max=n;
if (n<min) min=n;
}
return E.showMenu(menu);
// work out a nice grid value
var heightDiff = max-min;
var grid = 1;
while (heightDiff/grid > 8) {
grid*=2;
}
// draw
g.clear(1).setFont("6x8",1);
var r = require("graph").drawLine(g, infn, {
x:4,y:24,
width: g.getWidth()-24,
height: g.getHeight()-(24+8),
axes : true,
gridy : grid,
gridx : infn.length / 3,
title: title,
miny: min,
maxy: max,
xlabel : x=>Math.round(x*dur/(60*infn.length))+/*LANG*/" min" // minutes
});
g.setFont("6x8",2);
g.setFontAlign(0,0,3);
var isBTN3 = "BTN3" in global;
g.drawString(/*LANG*/"Back",g.getWidth() - 10, isBTN3 ? (g.getHeight() - 40) : (g.getHeight()/2));
setWatch(function() {
viewTrack(info.filename, info);
}, isBTN3?BTN3:BTN1);
g.flip();
}

View File

@ -2,7 +2,7 @@
"id": "recorder",
"name": "Recorder",
"shortName": "Recorder",
"version": "0.37",
"version": "0.38",
"description": "Record GPS position, heart rate and more in the background, then download to your PC.",
"icon": "app.png",
"tags": "tool,outdoors,gps,widget,clkinfo",