mirror of https://github.com/espruino/BangleApps
Merge remote-tracking branch 'upstream/master'
merged in upstream changespull/643/head
commit
63131d3133
30
apps.json
30
apps.json
|
@ -1553,7 +1553,7 @@
|
|||
"name": "BangleRun",
|
||||
"shortName": "BangleRun",
|
||||
"icon": "banglerun.png",
|
||||
"version": "0.06",
|
||||
"version": "0.07",
|
||||
"interface": "interface.html",
|
||||
"description": "An app for running sessions. Displays info and logs your run for later viewing.",
|
||||
"tags": "run,running,fitness,outdoors",
|
||||
|
@ -2585,7 +2585,7 @@
|
|||
{ "id": "astral",
|
||||
"name": "Astral Clock",
|
||||
"icon": "app-icon.png",
|
||||
"version":"0.01",
|
||||
"version":"0.02",
|
||||
"readme": "README.md",
|
||||
"description": "Clock that calculates and displays Alt Az positions of all planets, Sun as well as several other astronomy targets (customizable) and current Moon phase. Coordinates are calculated by GPS & time and onscreen compass assists orienting. See Readme before using.",
|
||||
"tags": "clock",
|
||||
|
@ -2595,6 +2595,18 @@
|
|||
{"name":"astral.img","url":"app-icon.js","evaluate":true}
|
||||
]
|
||||
},
|
||||
{ "id": "alpinenav",
|
||||
"name": "Alpine Nav",
|
||||
"icon": "app-icon.png",
|
||||
"version":"0.01",
|
||||
"readme": "README.md",
|
||||
"description": "App that performs GPS monitoring to track and display position relative to a given origin in realtime",
|
||||
"tags": "outdoors,gps",
|
||||
"storage": [
|
||||
{"name":"alpinenav.app.js","url":"app.js"},
|
||||
{"name":"alpinenav.img","url":"app-icon.js","evaluate":true}
|
||||
]
|
||||
},
|
||||
{ "id": "lifeclk",
|
||||
"name": "Game of Life Clock",
|
||||
"shortName":"Conway's Clock",
|
||||
|
@ -2635,5 +2647,19 @@
|
|||
{"name":"gpsservice.wid.js","url":"widget.js"},
|
||||
{"name":"gpsservice.img","url":"gpsservice-icon.js","evaluate":true}
|
||||
]
|
||||
},
|
||||
{ "id": "mclockplus",
|
||||
"name": "Morph Clock+",
|
||||
"shortName":"Morph Clock+",
|
||||
"icon": "mclockplus.png",
|
||||
"version":"1.0",
|
||||
"description": "Morphing Clock with more readable seconds and date and additional stopwatch",
|
||||
"tags": "clock",
|
||||
"type": "clock",
|
||||
"readme": "README.md",
|
||||
"storage": [
|
||||
{"name":"mclockplus.app.js","url":"mclockplus.app.js"},
|
||||
{"name":"mclockplus.img","url":"mclockplus-icon.js","evaluate":true}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
Alpine Navigator
|
||||
================
|
||||
App that performs GPS monitoring to track and display position relative to a given origin in realtime.
|
||||
|
||||

|
||||
|
||||
Functions
|
||||
---------
|
||||
Note if you've not used GPS yet I suggest using one of the GPS apps to get your first fix and confirm as I've found that helps initially.
|
||||
|
||||
The GPS and magnetometer will be turned on and after a few moments, when the watch buzzes and the dot turns from red to pink, that means the GPS is fixed. all your movements now will be displayed with a line drawn back to show your position relative to the start. New waypoints will be added based on checking every 10 seconds for at least 5 meters of movement. The map will scale to your distance travelled so the route will always remain within the window, the accelerometer/pedometer is not used - this is a purely GPS and compass solution so can be used for driving/cycling etc. A log file will be recorded that tracks upto 1000 waypoints, this isn't a big file and you could remove the limit but I've kept it fairly conservative here as it's not intended as a main feature, there's already good GPS recorders for the Bangle. The following other items are displayed:
|
||||
|
||||
1. altitude at origin, this is displayed left of the centre.
|
||||
2. current altitude, displayed centre right
|
||||
3. distance from origin, bottom left (meters)
|
||||
4. distance travelled, bottom right (meters)
|
||||
5. compass heading, at the top
|
||||
|
||||
For the display, the route is kept at a set resolution, so there's no risk of running into memory problems if you run this for long periods or any length of time because the waypoints will be reduced when it reaches a set threshold so you may see the path smooth out slightly at intervals.
|
||||
|
||||
If you get strange values or dashes for the compass, it just needs calibration so you need to move the watch around briefly for this each time, ideally 360 degrees around itself, which involves taking the watch off. If you don't want to do that you can also just wave your hand around for a few seconds like you're at a rave or Dr Strange making a Sling Ring but often just moving your wrist a bit is enough.
|
||||
|
||||
The buttons do the following:
|
||||
BTN1: this will display an 'X' in the bottom of the screen and lock all the buttons, this is to prevent you accidentally pressing either of the below. Remember to press this again to unlock it! soft and hard reset will both still work.
|
||||
BTN2: this removes all waypoints aside from the origin and your current location; sometimes during smaller journeys and walks, the GPS can give sporadic differences in locations because of the error margins of GPS and this can add noise to your route.
|
||||
BTN3: this will pause the GPS and magnetometer, useful for saving power for situations where you don't necessarily need to track parts of your route e.g. you're going indoors/shelter for some time. You'll know it's paused because the compass won't update it's reading and all the metrics will be blacked out on the screen.
|
||||
|
||||
Things to do next
|
||||
-----------------
|
||||
There's a GPS widget that's been developed to leverage low-power mode capability on the sensor, will look to incorporate that.
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mUywkEIf4A/AHUBiAYWgcwDC0v+IYW///C6sC+c/kAYUj/xj/wDCgvBgfyVihhBAQQASh6TCMikvYoRkU/73CMicD+ZnFViJFBj5MBMiU/+IuBJoJkRCoUvfIPy/5kQVgM//7gBC4KCDFxSsDgTHCl8QWgaRKmBJBFIzmDSJXzYBECWobbJAAKNIMhYlBOoK/IMhZXCmYMLABAkCS4RkSXZoNJRBo/CgK6UBwTWBBIs/SJBAGl7UFegIXMaogHEehAAHj/yIYsfehAAGMQISFMRxbCiEDU4ZiQZY5iQZYpiSbQ8/cwzLOCiQA/AH4A1A"))
|
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
|
@ -0,0 +1,237 @@
|
|||
var background_colour = "#000000";
|
||||
var foregound_colour = "#ccff99";
|
||||
var position_colour = "#ff3329";
|
||||
var log_limit = 1000;
|
||||
var max_array_size = 50;
|
||||
var pause_tracker = false;
|
||||
var temp;
|
||||
var file;
|
||||
var d;
|
||||
var origin_lat;
|
||||
var origin_lon;
|
||||
var current_lat;
|
||||
var current_lon;
|
||||
var current_speed;
|
||||
var distance_from_origin = 0;
|
||||
var distane_travelled = 0;
|
||||
var log_size;
|
||||
var waypoints = [];
|
||||
var start_alt = 0;
|
||||
var current_alt = 0;
|
||||
var button_lock = false;
|
||||
var display_waypoints = [];
|
||||
var waypoint = {
|
||||
lat: "",
|
||||
lon: ""
|
||||
};
|
||||
var compass_heading = "---";
|
||||
|
||||
function calcCrow(lat1, lon1, lat2, lon2) {
|
||||
var R = 6371e3;
|
||||
var dLat = toRad(lat2 - lat1);
|
||||
var dLon = toRad(lon2 - lon1);
|
||||
lat1 = toRad(lat1);
|
||||
lat2 = toRad(lat2);
|
||||
|
||||
var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||
Math.sin(dLon / 2) * Math.sin(dLon / 2) * Math.cos(lat1) * Math.cos(lat2);
|
||||
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
var d = R * c;
|
||||
return d;
|
||||
}
|
||||
|
||||
function toRad(Value) {
|
||||
return Value * Math.PI / 180;
|
||||
}
|
||||
|
||||
function draw() {
|
||||
if (pause_tracker)
|
||||
g.setColor(background_colour);
|
||||
else
|
||||
g.setColor(foregound_colour);
|
||||
g.setFont("6x8", 2);
|
||||
g.setFontAlign(0, 1);
|
||||
g.drawString(distance_from_origin.toFixed(0), 40, 220, true);
|
||||
g.drawString(distane_travelled.toFixed(0), 200, 220, true);
|
||||
g.setFont("6x8", 1);
|
||||
g.drawString(start_alt.toFixed(0), 40, 120, true);
|
||||
g.drawString(current_alt.toFixed(0), 200, 120, true);
|
||||
if (button_lock) {
|
||||
g.setFont("6x8", 2);
|
||||
g.setFontAlign(0, 0);
|
||||
g.drawString("X", 120, 220, true);
|
||||
g.setFont("6x8", 1);
|
||||
}
|
||||
}
|
||||
|
||||
function cull_array() {
|
||||
for (var i = 2; i <= waypoints.length; i += 2)
|
||||
waypoints.splice(i, 1);
|
||||
}
|
||||
|
||||
function process_and_display() {
|
||||
g.setColor(background_colour);
|
||||
g.fillRect(10, 65, 230, 230);
|
||||
g.setColor(foregound_colour);
|
||||
if (waypoints.length > max_array_size)
|
||||
cull_array();
|
||||
rescale();
|
||||
if (display_waypoints.length > 0) {
|
||||
for (let x = 0; x < display_waypoints.length - 1; x++) {
|
||||
g.reset();
|
||||
g.setColor(foregound_colour);
|
||||
g.drawLine(display_waypoints[x].lon, display_waypoints[x].lat, display_waypoints[x + 1].lon, display_waypoints[x + 1].lat);
|
||||
}
|
||||
g.reset();
|
||||
g.setColor(position_colour);
|
||||
g.fillCircle(display_waypoints[display_waypoints.length - 1].lon, display_waypoints[display_waypoints.length - 1].lat, 3);
|
||||
}
|
||||
}
|
||||
|
||||
function process_GPS() {
|
||||
if (waypoints.length > 0) {
|
||||
//check distance
|
||||
temp_distance = calcCrow(current_lat, current_lon, waypoints[waypoints.length - 1].lat, waypoints[waypoints.length - 1].lon);
|
||||
if (temp_distance > 5) {
|
||||
var temp = Object.create(waypoint);
|
||||
temp.lat = current_lat;
|
||||
temp.lon = current_lon;
|
||||
waypoints.push(temp);
|
||||
distane_travelled += temp_distance;
|
||||
distance_from_origin = calcCrow(current_lat, current_lon, waypoints[0].lat, waypoints[0].lon);
|
||||
process_and_display();
|
||||
if (log_size < log_limit) {
|
||||
var csv = [
|
||||
d,
|
||||
origin_lat - current_lat,
|
||||
current_lon - origin_lon,
|
||||
current_speed,
|
||||
current_alt
|
||||
];
|
||||
file.write(csv.join(",") + "\n");
|
||||
log_size += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
g.setColor(position_colour);
|
||||
g.fillCircle(120, 120, 3);
|
||||
}
|
||||
draw();
|
||||
}
|
||||
|
||||
function rescale() {
|
||||
var max_val = 0;
|
||||
display_waypoints = [];
|
||||
|
||||
for (let x = 0; x < waypoints.length; x++) {
|
||||
if (Math.abs(waypoints[x].lat) > max_val)
|
||||
max_val = Math.abs(waypoints[x].lat);
|
||||
if (Math.abs(waypoints[x].lon) > max_val)
|
||||
max_val = Math.abs(waypoints[x].lon);
|
||||
}
|
||||
|
||||
scaler = 60 / max_val;
|
||||
|
||||
for (let x = 0; x < waypoints.length; x++) {
|
||||
temp = Object.create(waypoint);
|
||||
temp.lat = 140 - Math.round(waypoints[x].lat * scaler);
|
||||
temp.lon = 120 - Math.round(waypoints[x].lon * scaler);
|
||||
display_waypoints.push(temp);
|
||||
}
|
||||
}
|
||||
|
||||
Bangle.setCompassPower(1);
|
||||
Bangle.setGPSPower(1);
|
||||
g.clear();
|
||||
process_GPS();
|
||||
var poll_GPS = setInterval(process_GPS, 9000);
|
||||
|
||||
setWatch(function () {
|
||||
if (!button_lock) {
|
||||
waypoints.splice(1);
|
||||
process_GPS();
|
||||
}
|
||||
}, BTN2, { repeat: true, edge: "falling" });
|
||||
|
||||
setWatch(function () {
|
||||
if (!button_lock) {
|
||||
if (!pause_tracker) {
|
||||
Bangle.setCompassPower(0);
|
||||
Bangle.setGPSPower(0);
|
||||
pause_tracker = true;
|
||||
}
|
||||
else {
|
||||
Bangle.setCompassPower(1);
|
||||
Bangle.setGPSPower(1);
|
||||
pause_tracker = false;
|
||||
}
|
||||
}
|
||||
}, BTN3, { repeat: true, edge: "falling" });
|
||||
|
||||
setWatch(function () {
|
||||
if (button_lock) {
|
||||
button_lock = false;
|
||||
g.setFontAlign(0, 0);
|
||||
g.drawString(" ", 120, 220, true);
|
||||
}
|
||||
else {
|
||||
button_lock = true;
|
||||
g.setFontAlign(0, 0);
|
||||
g.drawString("X", 120, 220, true);
|
||||
}
|
||||
}, BTN1, { repeat: true, edge: "falling" });
|
||||
|
||||
Bangle.on('GPS', function (g) {
|
||||
if (g.fix) {
|
||||
if (waypoints.length == 0) {
|
||||
file = require("Storage").open("alpine_log.csv", "w");
|
||||
file.write("");
|
||||
file = require("Storage").open("alpine_log.csv", "a");
|
||||
Bangle.buzz();
|
||||
position_colour = 0xF81F;
|
||||
origin_lat = g.lat;
|
||||
origin_lon = g.lon;
|
||||
start_alt = g.alt;
|
||||
current_speed = g.speed;
|
||||
sat_count = g.satellites;
|
||||
var csv = [
|
||||
d,
|
||||
origin_lat,
|
||||
origin_lon,
|
||||
current_speed,
|
||||
current_alt
|
||||
];
|
||||
file.write(csv.join(",") + "\n");
|
||||
var temp = Object.create(waypoint);
|
||||
temp.lat = 0;
|
||||
temp.lon = 0;
|
||||
process_GPS();
|
||||
waypoints.push(temp);
|
||||
}
|
||||
else {
|
||||
current_lat = g.lat - origin_lat;
|
||||
current_lon = origin_lon - g.lon;
|
||||
current_speed = g.speed;
|
||||
sat_count = g.satellites;
|
||||
current_alt = g.alt;
|
||||
gps_time = g.time;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Bangle.on('mag', function (m) {
|
||||
if (isNaN(m.heading))
|
||||
compass_heading = "---";
|
||||
else
|
||||
compass_heading = 360 - Math.round(m.heading);
|
||||
current_colour = g.getColor();
|
||||
g.reset();
|
||||
g.setColor(background_colour);
|
||||
g.fillRect(140, 30, 190, 55);
|
||||
g.setColor(foregound_colour);
|
||||
g.setFont("6x8", 2);
|
||||
if(compass_heading<100)
|
||||
compass_heading = " " + compass_heading.toString();
|
||||
g.drawString(compass_heading, 150, 15, true);
|
||||
});
|
Binary file not shown.
After Width: | Height: | Size: 706 KiB |
|
@ -0,0 +1,2 @@
|
|||
0.01: Create astral clock app
|
||||
0.02: Fixed Whirlpool galaxy RA/DA, larger compass display, fixed moonphase overlapping battery widget
|
|
@ -32,14 +32,14 @@ if (test_file !== undefined) {
|
|||
if (astral_settings === undefined) {
|
||||
astral_settings = {
|
||||
version: 1,
|
||||
lat: 51.9492,
|
||||
lon: 0.2834,
|
||||
lat: 51.5074,
|
||||
lon: 0.1278,
|
||||
astral_default: true,
|
||||
extras: [
|
||||
{ name: "Andromeda", ra: "004244", de: "411609", type: 3 },
|
||||
{ name: "Cigar", ra: "095552", de: "694047", type: 3 },
|
||||
{ name: "Pinwheel", ra: "140313", de: "542057", type: 3 },
|
||||
{ name: "Whirlpool", ra: "1329553", de: "471143", type: 3 },
|
||||
{ name: "Whirlpool", ra: "132953", de: "471143", type: 3 },
|
||||
{ name: "Orion", ra: "053517", de: "-052328", type: 2 },
|
||||
{ name: "Hercules", ra: "160515", de: "174455", type: 1 },
|
||||
{ name: "Beehive", ra: "084024", de: "195900", type: 1 },
|
||||
|
@ -662,12 +662,12 @@ function draw_moon(phase) {
|
|||
if (phase == 5) {
|
||||
g.fillCircle(200, Yaxis, 30);
|
||||
g.setColor("#000000");
|
||||
g.fillRect(220, 20, 240, 90);
|
||||
g.fillRect(220, 25, 240, 90);
|
||||
}
|
||||
else if (phase == 6) {
|
||||
g.fillCircle(200, Yaxis, 30);
|
||||
g.setColor("#000000");
|
||||
g.fillRect(200, 20, 240, 90);
|
||||
g.fillRect(200, 25, 240, 90);
|
||||
}
|
||||
else if (phase == 1) {
|
||||
g.fillCircle(200, Yaxis, 30);
|
||||
|
@ -679,12 +679,12 @@ function draw_moon(phase) {
|
|||
else if (phase == 3) {
|
||||
g.fillCircle(200, Yaxis, 30);
|
||||
g.setColor("#000000");
|
||||
g.fillRect(160, 20, 180, 90);
|
||||
g.fillRect(160, 25, 180, 90);
|
||||
}
|
||||
else if (phase == 2) {
|
||||
g.fillCircle(200, Yaxis, 30);
|
||||
g.setColor("#000000");
|
||||
g.fillRect(160, 20, 200, 90);
|
||||
g.fillRect(160, 25, 200, 90);
|
||||
}
|
||||
else if (phase == 7) {
|
||||
g.fillCircle(200, Yaxis, 30);
|
||||
|
@ -823,19 +823,23 @@ setWatch(function () {
|
|||
display_colour = default_colour;
|
||||
else
|
||||
display_colour = setupcomplete_colour;
|
||||
draw_moon(current_moonphase);
|
||||
}
|
||||
}, BTN4, { repeat: true });
|
||||
|
||||
//events
|
||||
Bangle.on('mag', function (m) {
|
||||
g.setFont("6x8",2);
|
||||
if (isNaN(m.heading))
|
||||
compass_heading = "--";
|
||||
compass_heading = "---";
|
||||
else
|
||||
compass_heading = 360 - Math.round(m.heading);
|
||||
g.setColor("#000000");
|
||||
g.fillRect(100, 10, 140, 20);
|
||||
// g.setColor("#000000");
|
||||
// g.fillRect(160, 10, 160, 20);
|
||||
g.setColor(display_colour);
|
||||
g.drawString(" " + (compass_heading) + " ", 130, 20, true /*clear background*/);
|
||||
if(compass_heading<100)
|
||||
compass_heading = " " + compass_heading;
|
||||
g.drawString(compass_heading, 150, 20, true /*clear background*/);
|
||||
});
|
||||
|
||||
Bangle.on('GPS', function (g) {
|
||||
|
|
|
@ -4,3 +4,4 @@
|
|||
0.04: Use offscreen buffer for flickerless updates
|
||||
0.05: Complete rewrite. New UI, GPS & HRM Kalman filters, activity logging
|
||||
0.06: Reading HDOP directly from the GPS event (needs Espruino 2v07 or above)
|
||||
0.07: Fixed GPS update, added guards against NaN values
|
||||
|
|
|
@ -1 +1 @@
|
|||
!function(){"use strict";const t={STOP:63488,PAUSE:65504,RUN:2016};function n(t,n,e){g.setColor(0),g.fillRect(n-60,e,n+60,e+30),g.setColor(65535),g.drawString(t,n,e)}function e(e){var r;g.setFontVector(30),g.setFontAlign(0,-1,0),n((e.distance/1e3).toFixed(2),60,55),n(function(t){const n=Math.round(t),e=Math.floor(n/3600),r=Math.floor(n/60)%60,a=n%60;return(e?e+":":"")+("0"+r).substr(-2)+":"+("0"+a).substr(-2)}(e.duration),180,55),n(function(t){if(t<.1667)return"__'__\"";const n=Math.round(1e3/t),e=Math.floor(n/60),r=n%60;return("0"+e).substr(-2)+"'"+("0"+r).substr(-2)+'"'}(e.speed),60,115),n(e.hr.toFixed(0),180,115),n(e.steps.toFixed(0),60,175),n(e.cadence.toFixed(0),180,175),g.setFont("6x8",2),g.setColor(e.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"+(r=new Date).getHours()).substr(-2)+":"+("0"+r.getMinutes()).substr(-2),120,220),g.setColor(t[e.status]),g.fillRect(160,216,240,240),g.setColor(0),g.drawString(e.status,200,220)}function r(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),e(t),Bangle.drawWidgets()}var a;function s(t){t.status===a.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.file.write(["timestamp","latitude","longitude","altitude","duration","distance","heartrate","steps"].join(",")+"\n")}(t),t.status===a.Running?t.status=a.Paused:t.status=a.Running,e(t)}!function(t){t.Stopped="STOP",t.Paused="PAUSE",t.Running="RUN"}(a||(a={}));const o={fix:NaN,lat:NaN,lon:NaN,alt:NaN,vel:NaN,dop:NaN,gpsValid:!1,x:NaN,y:NaN,z:NaN,v:NaN,t:NaN,dt:NaN,pError:NaN,vError:NaN,hr:60,hrError:100,file:null,drawing:!1,status:a.Stopped,duration:0,distance:0,speed:0,steps:0,cadence:0};var i;i=o,Bangle.on("GPS",t=>function(t,n){t.lat=n.lat,t.lon=n.lon,t.alt=n.alt,t.vel=n.speed/3.6,t.fix=n.fix,t.dop=n.hdop}(i,t)),Bangle.setGPSPower(1),function(t){Bangle.on("HRM",n=>function(t,n){if(0===n.confidence)return;const e=n.bpm-t.hr,r=Math.abs(e)+101-n.confidence,a=t.hrError/(t.hrError+r);t.hr+=e*a,t.hrError+=(r-t.hrError)*a}(t,n)),Bangle.setHRMPower(1)}(o),function(t){Bangle.on("step",()=>function(t){t.status===a.Running&&(t.steps+=1)}(t))}(o),function(t){Bangle.loadWidgets(),Bangle.on("lcdPower",n=>{t.drawing=n,n&&r(t)}),r(t)}(o),setWatch(()=>s(o),BTN1,{repeat:!0,edge:"falling"}),setWatch(()=>function(t){t.status===a.Paused&&function(t){t.duration=0,t.distance=0,t.speed=0,t.steps=0,t.cadence=0}(t),t.status===a.Running?t.status=a.Paused:t.status=a.Stopped,e(t)}(o),BTN3,{repeat:!0,edge:"falling"})}();
|
||||
!function(){"use strict";const t={STOP:63488,PAUSE:65504,RUN:2016};function n(t,n,r){g.setColor(0),g.fillRect(n-60,r,n+60,r+30),g.setColor(65535),g.drawString(t,n,r)}function r(r){var e;g.setFontVector(30),g.setFontAlign(0,-1,0),n((r.distance/1e3).toFixed(2),60,55),n(function(t){const n=Math.round(t),r=Math.floor(n/3600),e=Math.floor(n/60)%60,o=n%60;return(r?r+":":"")+("0"+e).substr(-2)+":"+("0"+o).substr(-2)}(r.duration),180,55),n(function(t){if(t<.1667)return"__'__\"";const n=Math.round(1e3/t),r=Math.floor(n/60),e=n%60;return("0"+r).substr(-2)+"'"+("0"+e).substr(-2)+'"'}(r.speed),60,115),n(r.hr.toFixed(0),180,115),n(r.steps.toFixed(0),60,175),n(r.cadence.toFixed(0),180,175),g.setFont("6x8",2),g.setColor(r.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"+(e=new Date).getHours()).substr(-2)+":"+("0"+e.getMinutes()).substr(-2),120,220),g.setColor(t[r.status]),g.fillRect(160,216,240,240),g.setColor(0),g.drawString(r.status,200,220)}function e(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),r(t),Bangle.drawWidgets()}var o;function a(t){t.status===o.Stopped&&function(t){const n=(new Date).toISOString().replace(/[-:]/g,""),r=`banglerun_${n.substr(2,6)}_${n.substr(9,6)}`;t.file=require("Storage").open(r,"w"),t.file.write(["timestamp","latitude","longitude","altitude","duration","distance","heartrate","steps"].join(",")+"\n")}(t),t.status===o.Running?t.status=o.Paused:t.status=o.Running,r(t)}!function(t){t.Stopped="STOP",t.Paused="PAUSE",t.Running="RUN"}(o||(o={}));const s={fix:NaN,lat:NaN,lon:NaN,alt:NaN,vel:NaN,dop:NaN,gpsValid:!1,x:NaN,y:NaN,z:NaN,v:NaN,t:NaN,dt:NaN,pError:NaN,vError:NaN,hr:60,hrError:100,file:null,drawing:!1,status:o.Stopped,duration:0,distance:0,speed:0,steps:0,cadence:0};var i;i=s,Bangle.on("GPS",t=>function(t,n){t.lat=n.lat,t.lon=n.lon,t.alt=n.alt,t.vel=n.speed/3.6,t.fix=n.fix,t.dop=n.hdop,t.gpsValid=t.fix>0&&t.dop<=5,function(t){const n=Date.now(),r=(n-t.t)/1e3;if(t.t=n,t.dt+=r,t.status===o.Running&&(t.duration+=r),!t.gpsValid)return;const e=6371008.8+t.alt,a=e*Math.cos(t.lat)*Math.cos(t.lon),s=e*Math.cos(t.lat)*Math.sin(t.lon),i=e*Math.sin(t.lat),d=t.vel;if(!t.x)return t.x=a,t.y=s,t.z=i,t.v=d,t.pError=2.5*t.dop,void(t.vError=.05*t.dop);const u=a-t.x,g=s-t.y,l=i-t.z,c=d-t.v,p=Math.sqrt(u*u+g*g+l*l),f=Math.abs(c);t.pError+=t.v*t.dt,t.dt=0;const N=p+2.5*t.dop,h=f+.05*t.dop,S=t.pError/(t.pError+N)||0,x=t.vError/(t.vError+h)||0;t.x+=u*S,t.y+=g*S,t.z+=l*S,t.v+=c*x,t.pError+=(N-t.pError)*S,t.vError+=(h-t.vError)*x;const E=Math.sqrt(t.x*t.x+t.y*t.y+t.z*t.z);t.lat=180*Math.asin(t.z/E)/Math.PI||0,t.lon=180*Math.atan2(t.y,t.x)/Math.PI||0,t.alt=E-6371008.8,t.status===o.Running&&(t.distance+=p*S,t.speed=t.distance/t.duration||0,t.cadence=60*t.steps/t.duration||0)}(t),r(t),t.gpsValid&&t.status===o.Running&&function(t){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")}(t)}(i,t)),Bangle.setGPSPower(1),function(t){Bangle.on("HRM",n=>function(t,n){if(0===n.confidence)return;const r=n.bpm-t.hr,e=Math.abs(r)+101-n.confidence,o=t.hrError/(t.hrError+e)||0;t.hr+=r*o,t.hrError+=(e-t.hrError)*o}(t,n)),Bangle.setHRMPower(1)}(s),function(t){Bangle.on("step",()=>function(t){t.status===o.Running&&(t.steps+=1)}(t))}(s),function(t){Bangle.loadWidgets(),Bangle.on("lcdPower",n=>{t.drawing=n,n&&e(t)}),e(t)}(s),setWatch(()=>a(s),BTN1,{repeat:!0,edge:"falling"}),setWatch(()=>function(t){t.status===o.Paused&&function(t){t.duration=0,t.distance=0,t.speed=0,t.steps=0,t.cadence=0}(t),t.status===o.Running?t.status=o.Paused:t.status=o.Stopped,r(t)}(s),BTN3,{repeat:!0,edge:"falling"})}();
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { draw } from './display';
|
||||
import { updateLog } from './log';
|
||||
import { ActivityStatus, AppState } from './state';
|
||||
|
||||
declare var Bangle: any;
|
||||
|
@ -34,6 +36,15 @@ function readGps(state: AppState, gps: GpsEvent): void {
|
|||
state.vel = gps.speed / 3.6;
|
||||
state.fix = gps.fix;
|
||||
state.dop = gps.hdop;
|
||||
|
||||
state.gpsValid = state.fix > 0 && state.dop <= 5;
|
||||
|
||||
updateGps(state);
|
||||
draw(state);
|
||||
|
||||
if (state.gpsValid && state.status === ActivityStatus.Running) {
|
||||
updateLog(state);
|
||||
}
|
||||
}
|
||||
|
||||
function updateGps(state: AppState): void {
|
||||
|
@ -80,8 +91,8 @@ function updateGps(state: AppState): void {
|
|||
const pError = dpMag + state.dop * POS_ACCURACY;
|
||||
const vError = dvMag + state.dop * VEL_ACCURACY;
|
||||
|
||||
const pGain = state.pError / (state.pError + pError);
|
||||
const vGain = state.vError / (state.vError + vError);
|
||||
const pGain = (state.pError / (state.pError + pError)) || 0;
|
||||
const vGain = (state.vError / (state.vError + vError)) || 0;
|
||||
|
||||
state.x += dx * pGain;
|
||||
state.y += dy * pGain;
|
||||
|
@ -92,7 +103,7 @@ function updateGps(state: AppState): void {
|
|||
|
||||
const pMag = Math.sqrt(state.x * state.x + state.y * state.y + state.z * state.z);
|
||||
|
||||
state.lat = Math.asin(state.z / pMag) * 180 / Math.PI;
|
||||
state.lat = (Math.asin(state.z / pMag) * 180 / Math.PI) || 0;
|
||||
state.lon = (Math.atan2(state.y, state.x) * 180 / Math.PI) || 0;
|
||||
state.alt = pMag - EARTH_RADIUS;
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ function updateHrm(state: AppState, hrm: HrmData) {
|
|||
|
||||
const dHr = hrm.bpm - state.hr;
|
||||
const hrError = Math.abs(dHr) + 101 - hrm.confidence;
|
||||
const hrGain = state.hrError / (state.hrError + hrError);
|
||||
const hrGain = (state.hrError / (state.hrError + hrError)) || 0;
|
||||
|
||||
state.hr += dHr * hrGain;
|
||||
state.hrError += (hrError - state.hrError) * hrGain;
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
1.0: Created app
|
|
@ -0,0 +1,21 @@
|
|||
# Morphing Clock Plus
|
||||
|
||||
Based on Morphing Clock with more readable seconds and date, and an additional simple stopwatch.
|
||||
|
||||

|
||||
|
||||
## Usage
|
||||
|
||||
In addition to the Morphing Clock, a simple stopwatch can be started in the lower part of the display.
|
||||
|
||||
BTN3 starts and stops the stopwatch.
|
||||
|
||||
BTN1 resets/clears the stopwatch.
|
||||
|
||||
## Requests
|
||||
|
||||
Please leave bug reports and requests by raising an issue [here](https://github.com/skauertz/BangleApps).
|
||||
|
||||
## Creator
|
||||
|
||||
Sebastian Kauertz https://github.com/skauertz
|
Binary file not shown.
After Width: | Height: | Size: 41 KiB |
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwkEogA/AFGIAAQVVDKQWHDB1IC5OECx8z///mYYOBoWDCoIADnBJLFwQWGDAgwIEYU/CQXwh4EC+YwKBIOPFQYXE//4C5BGCIQgXF/5IILo4XGMIQXHLoYXIMIRGMC45IHC4KkGC45IBC4yNEC5KRBC7h2HC5B4GC5EggQXOBwvygEAl6QHC4sikRGEhGAJAgNBC75HIgZHNO48AgIJER54xCiYXKa5AxCGAjvPGA4XIwYXHbQs4C46QGGAbZDB4IXEPBQAEOwwXDJBJGEC4xILIxQwDSJCNDFwwXDMIh0ELoQXIJARhDC4hdCIw4wEDAQXDCwQuIGAgABmYXBmYHDFxIYGAAoWLJIgAGCxgYJCxwZGCqIA/AC4A="))
|
|
@ -0,0 +1,318 @@
|
|||
// Morphing Clock +
|
||||
// Modifies original Morphing Clock to make seconds and date more readable, and adds a simple stopwatch
|
||||
// Icon by https://icons8.com
|
||||
var is12Hour = (require("Storage").readJSON("setting.json",1)||{})["12hour"];
|
||||
var locale = require("locale");
|
||||
var CHARW = 28; // how tall are digits?
|
||||
var CHARP = 2; // how chunky are digits?
|
||||
var Y = 50; // start height
|
||||
// Offscreen buffer
|
||||
var buf = Graphics.createArrayBuffer(CHARW+CHARP*2,CHARW*2 + CHARP*2,1,{msb:true});
|
||||
var bufimg = {width:buf.getWidth(),height:buf.getHeight(),buffer:buf.buffer};
|
||||
// The last time that we displayed
|
||||
var lastTime = "-----";
|
||||
// If animating, this is the interval's id
|
||||
var animInterval;
|
||||
var timeInterval;
|
||||
// Variables for the stopwatch
|
||||
var counter = -1; // Counts seconds
|
||||
var oldDate = new Date(2020,0,1); // Initialize to a past date
|
||||
var swInterval; // The interval's id
|
||||
var B3 = 0; // Flag to track BTN3's current function
|
||||
var w1; // watch id for BTN1
|
||||
var w3; // watch id for BTN3
|
||||
|
||||
/* Get array of lines from digit d to d+1.
|
||||
n is the amount (0..1)
|
||||
maxFive is true is this digit only counts 0..5 */
|
||||
const DIGITS = {
|
||||
" ":n=>[],
|
||||
"0":n=>[
|
||||
[n,0,1,0],
|
||||
[1,0,1,1],
|
||||
[1,1,1,2],
|
||||
[n,2,1,2],
|
||||
[n,1,n,2],
|
||||
[n,0,n,1]],
|
||||
"1":n=>[
|
||||
[1-n,0,1,0],
|
||||
[1,0,1,1],
|
||||
[1-n,1,1,1],
|
||||
[1-n,1,1-n,2],
|
||||
[1-n,2,1,2]],
|
||||
"2":n=>[
|
||||
[0,0,1,0],
|
||||
[1,0,1,1],
|
||||
[0,1,1,1],
|
||||
[0,1+n,0,2],
|
||||
[1,2-n,1,2],
|
||||
[0,2,1,2]],
|
||||
"3":n=>[
|
||||
[0,0,1-n,0],
|
||||
[0,0,0,n],
|
||||
[1,0,1,1],
|
||||
[0,1,1,1],
|
||||
[1,1,1,2],
|
||||
[n,2,1,2]],
|
||||
"4":n=>[
|
||||
[0,0,0,1],
|
||||
[1,0,1-n,0],
|
||||
[1,0,1,1-n],
|
||||
[0,1,1,1],
|
||||
[1,1,1,2],
|
||||
[1-n,2,1,2]],
|
||||
"5to0": n=>[ // 5 -> 0
|
||||
[0,0,0,1],
|
||||
[0,0,1,0],
|
||||
[n,1,1,1],
|
||||
[1,1,1,2],
|
||||
[0,2,1,2],
|
||||
[0,2,0,2],
|
||||
[1,1-n,1,1],
|
||||
[0,1,0,1+n]],
|
||||
"5to6": n=>[ // 5 -> 6
|
||||
[0,0,0,1],
|
||||
[0,0,1,0],
|
||||
[0,1,1,1],
|
||||
[1,1,1,2],
|
||||
[0,2,1,2],
|
||||
[0,2-n,0,2]],
|
||||
"6":n=>[
|
||||
[0,0,0,1-n],
|
||||
[0,0,1,0],
|
||||
[n,1,1,1],
|
||||
[1,1-n,1,1],
|
||||
[1,1,1,2],
|
||||
[n,2,1,2],
|
||||
[0,1-n,0,2-2*n]],
|
||||
"7":n=>[
|
||||
[0,0,0,n],
|
||||
[0,0,1,0],
|
||||
[1,0,1,1],
|
||||
[1-n,1,1,1],
|
||||
[1,1,1,2],
|
||||
[1-n,2,1,2],
|
||||
[1-n,1,1-n,2]],
|
||||
"8":n=>[
|
||||
[0,0,0,1],
|
||||
[0,0,1,0],
|
||||
[1,0,1,1],
|
||||
[0,1,1,1],
|
||||
[1,1,1,2],
|
||||
[0,2,1,2],
|
||||
[0,1,0,2-n]],
|
||||
"9":n=>[
|
||||
[0,0,0,1],
|
||||
[0,0,1,0],
|
||||
[1,0,1,1],
|
||||
[0,1,1-n,1],
|
||||
[0,1,0,1+n],
|
||||
[1,1,1,2],
|
||||
[0,2,1,2]],
|
||||
":":n=>[
|
||||
[0.4,0.4,0.6,0.4],
|
||||
[0.6,0.4,0.6,0.6],
|
||||
[0.6,0.6,0.4,0.6],
|
||||
[0.4,0.4,0.4,0.6],
|
||||
[0.4,1.4,0.6,1.4],
|
||||
[0.6,1.4,0.6,1.6],
|
||||
[0.6,1.6,0.4,1.6],
|
||||
[0.4,1.4,0.4,1.6]]
|
||||
};
|
||||
|
||||
/* Draw a transition between lastText and thisText.
|
||||
'n' is the amount - 0..1 */
|
||||
function drawDigits(lastText,thisText,n) {
|
||||
"ram"
|
||||
const p = CHARP; // padding around digits
|
||||
const s = CHARW; // character size
|
||||
var x = 16; // x offset
|
||||
g.reset();
|
||||
for (var i=0;i<lastText.length;i++) {
|
||||
var lastCh = lastText[i];
|
||||
var thisCh = thisText[i];
|
||||
if (thisCh==":") x-=4;
|
||||
if (lastCh!=thisCh) {
|
||||
var ch, chn = n;
|
||||
if ((thisCh-1==lastCh ||
|
||||
(thisCh==0 && lastCh==5) ||
|
||||
(thisCh==0 && lastCh==9)))
|
||||
ch = lastCh;
|
||||
else {
|
||||
ch = thisCh;
|
||||
chn = 0;
|
||||
}
|
||||
buf.clear();
|
||||
if (ch=="5") ch = (lastCh==5 && thisCh==0)?"5to0":"5to6";
|
||||
var l = DIGITS[ch](chn);
|
||||
l.forEach(c=>{
|
||||
if (c[0]!=c[2]) // horiz
|
||||
buf.fillRect(p+c[0]*s,c[1]*s,p+c[2]*s,2*p+c[3]*s);
|
||||
else if (c[1]!=c[3]) // vert
|
||||
buf.fillRect(c[0]*s,p+c[1]*s,2*p+c[2]*s,p+c[3]*s);
|
||||
});
|
||||
g.drawImage(bufimg,x,Y);
|
||||
}
|
||||
if (thisCh==":") x-=4;
|
||||
x+=s+p+7;
|
||||
}
|
||||
}
|
||||
function drawDate() {
|
||||
var x = (CHARW + CHARP + 8)*5;
|
||||
var y = Y + 2*CHARW + CHARP;
|
||||
var d = new Date();
|
||||
// meridian
|
||||
g.reset();
|
||||
g.setFont("6x8",2);
|
||||
g.setFontAlign(-1,-1);
|
||||
if (is12Hour) g.drawString((d.getHours() < 12) ? "AM" : "PM", x+8, Y+0, true);
|
||||
// date
|
||||
g.setFont("Vector16");
|
||||
g.setFontAlign(0,-1);
|
||||
// Only draw the date if it has changed:
|
||||
if ((d.getDate()!=oldDate.getDate())||(d.getMonth()!=oldDate.getMonth())||(d.getFullYear()!=oldDate.getFullYear())) {
|
||||
var date = locale.date(d,false);
|
||||
g.clearRect(1,y+8,g.getWidth(),y+24);
|
||||
g.drawString(date, g.getWidth()/2, y+8, true);
|
||||
oldDate = d;
|
||||
}
|
||||
}
|
||||
|
||||
function drawSeconds() {
|
||||
var x = (CHARW + CHARP + 8)*5;
|
||||
var y = Y + 2*CHARW + CHARP;
|
||||
var d = new Date();
|
||||
// seconds
|
||||
g.reset();
|
||||
g.setFont("6x8",2);
|
||||
g.setFontAlign(-1,-1);
|
||||
g.drawString(("0"+d.getSeconds()).substr(-2), x+8, y-12, true);
|
||||
}
|
||||
|
||||
/* Show the current time, and animate if needed */
|
||||
function showTime() {
|
||||
if (animInterval) return; // in animation - quit
|
||||
var d = new Date();
|
||||
var hours = d.getHours();
|
||||
if (is12Hour) hours = ((hours + 11) % 12) + 1;
|
||||
var t = (" "+hours).substr(-2)+":"+
|
||||
("0"+d.getMinutes()).substr(-2);
|
||||
var l = lastTime;
|
||||
// same - don't animate
|
||||
if (t==l || l=="-----") {
|
||||
drawDigits(l,t,0);
|
||||
drawDate();
|
||||
drawSeconds();
|
||||
lastTime = t;
|
||||
return;
|
||||
}
|
||||
var n = 0;
|
||||
animInterval = setInterval(function() {
|
||||
n += 1/10;
|
||||
if (n>=1) {
|
||||
n=1;
|
||||
clearInterval(animInterval);
|
||||
animInterval = undefined;
|
||||
}
|
||||
drawDigits(l,t,n);
|
||||
drawSeconds();
|
||||
}, 20);
|
||||
lastTime = t;
|
||||
}
|
||||
|
||||
function stopWatch() {
|
||||
|
||||
counter++;
|
||||
|
||||
var hrs = Math.floor(counter/3600);
|
||||
var mins = Math.floor((counter-hrs*3600)/60);
|
||||
var secs = counter - mins*60 - hrs*3600;
|
||||
|
||||
// When starting the stopwatch:
|
||||
if (B3) {
|
||||
// Set BTN3 to stop the stopwatch and bind itself to restart it:
|
||||
w3=setWatch(() => {clearInterval(swInterval);
|
||||
swInterval=undefined;
|
||||
if (w3) {clearWatch(w3);w3=undefined;}
|
||||
setWatch(() => {swInterval=setInterval(stopWatch, 1000);stopWatch();},
|
||||
BTN3, {repeat:false,edge:"falling"});
|
||||
B3 = 1;},
|
||||
BTN3, {repeat:false,edge:"falling"});
|
||||
B3 = 0; // BTN3 is bound to stop the stopwatch
|
||||
}
|
||||
|
||||
// Bind BTN1 to call the reset function:
|
||||
if (!w1) w1 = setWatch(resetStopWatch, BTN1, {repeat:false,edge:"falling"});
|
||||
|
||||
// Draw elapsed time:
|
||||
g.reset();
|
||||
g.setColor(0.0,0.5,1.0).setFontAlign(0,-1).setFont("Vector24");
|
||||
g.clearRect(1,180,g.getWidth(),210);
|
||||
if (hrs>0) {
|
||||
g.drawString(("0"+parseInt(hrs)).substr(-2), g.getWidth()/2 - 72, 180, true);
|
||||
g.drawString( ":", g.getWidth()/2 - 48, 180, true);
|
||||
}
|
||||
g.drawString(("0"+parseInt(mins)).substr(-2), g.getWidth()/2 - 24, 180, true);
|
||||
g.drawString( ":", g.getWidth()/2, 180, true);
|
||||
g.drawString(("0"+parseInt(secs)).substr(-2), g.getWidth()/2 + 24, 180, true);
|
||||
|
||||
}
|
||||
|
||||
function resetStopWatch() {
|
||||
|
||||
// Stop the interval if necessary:
|
||||
if (swInterval) {
|
||||
clearInterval(swInterval);
|
||||
swInterval=undefined;
|
||||
}
|
||||
|
||||
// Clear the stopwatch:
|
||||
g.clearRect(1,180,g.getWidth(),210);
|
||||
|
||||
// Reset the counter:
|
||||
counter = -1;
|
||||
|
||||
// Set BTN3 to start the stopwatch again:
|
||||
if (!B3) {
|
||||
// In case the stopwatch is reset while still running, the watch on BTN3 is still active, so we need to reset it manually:
|
||||
if (w3) {clearWatch(w3);w3=undefined;}
|
||||
// Set BTN3 to start the watch again:
|
||||
setWatch(() => {swInterval=setInterval(stopWatch, 1000);stopWatch();}, BTN3, {repeat:false,edge:"falling"});
|
||||
B3 = 1; // BTN3 is bound to start the stopwatch
|
||||
}
|
||||
|
||||
// Reset watch on BTN1:
|
||||
if (w1) {clearWatch(w1);w1=undefined;}
|
||||
}
|
||||
|
||||
|
||||
Bangle.on('lcdPower',function(on) {
|
||||
if (animInterval) {
|
||||
clearInterval(animInterval);
|
||||
animInterval = undefined;
|
||||
}
|
||||
if (timeInterval) {
|
||||
clearInterval(timeInterval);
|
||||
timeInterval = undefined;
|
||||
}
|
||||
if (on) {
|
||||
showTime();
|
||||
timeInterval = setInterval(showTime, 1000);
|
||||
} else {
|
||||
lastTime = "-----";
|
||||
}
|
||||
});
|
||||
|
||||
g.clear();
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
// Update time once a second
|
||||
timeInterval = setInterval(showTime, 1000);
|
||||
showTime();
|
||||
|
||||
// Show launcher when middle button pressed
|
||||
setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"});
|
||||
|
||||
// Start stopwatch when BTN3 is pressed
|
||||
setWatch(() => {swInterval=setInterval(stopWatch, 1000);stopWatch();}, BTN3, {repeat:false,edge:"falling"});
|
||||
B3 = 1; // BTN3 is bound to start the stopwatch
|
Binary file not shown.
After Width: | Height: | Size: 1.7 KiB |
Loading…
Reference in New Issue