Merge branch 'Eveeeon-master'

pull/2176/head
Gordon Williams 2022-10-10 10:37:07 +01:00
commit b7a277d2fb
52 changed files with 2141 additions and 366 deletions

View File

@ -1,3 +1,4 @@
0.01: Initial version
0.02: More compact rendering & app icon
0.03: Tell clock widgets to hide.
0.04: Improve current time readability in light theme.

View File

@ -68,7 +68,7 @@ function drawEvent(event, y) {
var curEventHeight = 0;
function drawCurrentEvents(y) {
g.setColor("#0ff");
g.setColor(g.theme.dark ? "#0ff" : "#0000ff");
g.clearRect(5, y, g.getWidth() - 5, y + curEventHeight);
curEventHeight = y;

View File

@ -2,7 +2,7 @@
"id": "calclock",
"name": "Calendar Clock",
"shortName": "CalClock",
"version": "0.03",
"version": "0.04",
"description": "Show the current and upcoming events synchronized from Gadgetbridge",
"icon": "calclock.png",
"type": "clock",

1
apps/gpstrek/ChangeLog Normal file
View File

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

43
apps/gpstrek/README.md Normal file
View File

@ -0,0 +1,43 @@
# GPS Trekking
Helper for tracking the status/progress during hiking. Do NOT depend on this for navigation!
This app is inspired by and uses code from "GPS Navigation" and "Navigation compass".
## Usage
Tapping or button to switch to the next information display, swipe right for the menu.
Choose either a route or a waypoint as basis for the display.
After this selection and availability of a GPS fix the compass will show a blue dot for your destination and a green one for possibly available waypoints on the way.
Waypoints are shown with name if available and distance to waypoint.
### Route
Routes can be created from .gpx files containing "trkpt" elements with this script: [createRoute.sh](createRoute.sh)
The resulting file needs to be uploaded to the watch and will be shown in the file selection menu.
The route can be mirrored to switch start and destination.
If the GPS position is closer than 30m to the next waypoint, the route is automatically advanced to the next waypoint.
### Waypoints
You can select a waypoint from the "Waypoints" app as destination.
## Calibration
### Altitude
You can correct the barometric altitude display either by manually setting a known correct value or using the GPS fix elevation as reference. This will only affect the display of altitude values.
### Compass
If the compass fallback starts to show unreliable values, you can reset the calibration in the menu. It starts to show values again after turning 360°.
## Widget
The widget keeps the sensors alive and records some very basic statics when the app is not started.
This uses a lot of power so ensure to stop the app if you are not actively using it.

1
apps/gpstrek/app-icon.js Normal file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwIjggOAApMD4AFJg4FF8AFJh/wApMf/AFJn/8ApN//wFDvfeAof774FD+fPLwYFBMAUB8fHAoUDAoJaCgfD4YFIg+D4JgCAosPAoJgCh6DBAoUfAoJgCjwFBvAFBnwFBvgFBngFBngFBvh3BnwFBvH//8eMgQFBMwX//k//5eB//wh//wAFBAQcDRoU/4EDJQfAbYbfFACYA="))

828
apps/gpstrek/app.js Normal file
View File

@ -0,0 +1,828 @@
const STORAGE = require("Storage");
const showWidgets = true;
let numberOfSlices=4;
if (showWidgets){
Bangle.loadWidgets();
}
let state = WIDGETS["gpstrek"].getState();
WIDGETS["gpstrek"].start();
function parseNumber(toParse){
if (toParse.includes(".")) return parseFloat(toParse);
return parseFloat("" + toParse + ".0");
}
function parseWaypoint(filename, offset, result){
result.lat = parseNumber(STORAGE.read(filename, offset, 11));
result.lon = parseNumber(STORAGE.read(filename, offset += 11, 12));
return offset + 12;
}
function parseWaypointWithElevation(filename, offset, result){
offset = parseWaypoint(filename, offset, result);
result.alt = parseNumber(STORAGE.read(filename, offset, 6));
return offset + 6;
}
function parseWaypointWithName(filename, offset, result){
offset = parseWaypoint(filename, offset, result);
return parseName(filename, offset, result);
}
function parseName(filename, offset, result){
let nameLength = STORAGE.read(filename, offset, 2) - 0;
result.name = STORAGE.read(filename, offset += 2, nameLength);
return offset + nameLength;
}
function parseWaypointWithElevationAndName(filename, offset, result){
offset = parseWaypointWithElevation(filename, offset, result);
return parseName(filename, offset, result);
}
function getEntry(filename, offset, result){
result.fileOffset = offset;
let type = STORAGE.read(filename, offset++, 1);
if (type == "") return -1;
switch (type){
case "A":
offset = parseWaypoint(filename, offset, result);
break;
case "B":
offset = parseWaypointWithName(filename, offset, result);
break;
case "C":
offset = parseWaypointWithElevation(filename, offset, result);
break;
case "D":
offset = parseWaypointWithElevationAndName(filename, offset, result);
break;
default:
print("Unknown entry type", type);
return -1;
}
offset++;
result.fileLength = offset - result.fileOffset;
//print(result);
return offset;
}
const labels = ["N","NE","E","SE","S","SW","W","NW"];
const loc = require("locale");
function matchFontSize(graphics, text, height, width){
graphics.setFontVector(height);
let metrics;
let size = 1;
while (graphics.stringMetrics(text).width > 0.90 * width){
size -= 0.05;
graphics.setFont("Vector",Math.floor(height*size));
}
}
function getDoubleLineSlice(title1,title2,provider1,provider2,refreshTime){
let lastDrawn = Date.now() - Math.random()*refreshTime;
return {
refresh: function (){
return Date.now() - lastDrawn > (Bangle.isLocked()?(refreshTime?refreshTime:5000):(refreshTime?refreshTime*2:10000));
},
draw: function (graphics, x, y, height, width){
lastDrawn = Date.now();
if (typeof title1 == "function") title1 = title1();
if (typeof title2 == "function") title2 = title2();
graphics.clearRect(x,y,x+width,y+height);
let value = provider1();
matchFontSize(graphics, title1 + value, Math.floor(height*0.5), width);
graphics.setFontAlign(-1,-1);
graphics.drawString(title1, x+2, y);
graphics.setFontAlign(1,-1);
graphics.drawString(value, x+width, y);
value = provider2();
matchFontSize(graphics, title2 + value, Math.floor(height*0.5), width);
graphics.setFontAlign(-1,-1);
graphics.drawString(title2, x+2, y+(height*0.5));
graphics.setFontAlign(1,-1);
graphics.drawString(value, x+width, y+(height*0.5));
}
};
}
function getTargetSlice(targetDataSource){
let nameIndex = 0;
let lastDrawn = Date.now() - Math.random()*3000;
return {
refresh: function (){
return Date.now() - lastDrawn > (Bangle.isLocked()?10000:3000);
},
draw: function (graphics, x, y, height, width){
lastDrawn = Date.now();
graphics.clearRect(x,y,x+width,y+height);
if (targetDataSource.icon){
graphics.drawImage(targetDataSource.icon,x,y + (height - 16)/2);
x += 16;
width -= 16;
}
if (!targetDataSource.getTarget() || !targetDataSource.getStart()) return;
let dist = distance(targetDataSource.getStart(),targetDataSource.getTarget());
if (isNaN(dist)) dist = Infinity;
let bearingString = bearing(targetDataSource.getStart(),targetDataSource.getTarget()) + "°";
if (targetDataSource.getTarget().name) {
graphics.setFont("Vector",Math.floor(height*0.5));
let scrolledName = (targetDataSource.getTarget().name || "").substring(nameIndex);
if (graphics.stringMetrics(scrolledName).width > width){
nameIndex++;
} else {
nameIndex = 0;
}
graphics.drawString(scrolledName, x+2, y);
let distanceString = loc.distance(dist,2);
matchFontSize(graphics, distanceString + bearingString, height*0.5, width);
graphics.drawString(bearingString, x+2, y+(height*0.5));
graphics.setFontAlign(1,-1);
graphics.drawString(distanceString, x + width, y+(height*0.5));
} else {
graphics.setFont("Vector",Math.floor(height*1));
let bearingString = bearing(targetDataSource.getStart(),targetDataSource.getTarget()) + "°";
let formattedDist = loc.distance(dist,2);
let distNum = (formattedDist.match(/[0-9\.]+/) || [Infinity])[0];
let size = 0.8;
let distNumMetrics;
while (graphics.stringMetrics(bearingString).width + (distNumMetrics = graphics.stringMetrics(distNum)).width > 0.90 * width){
size -= 0.05;
graphics.setFont("Vector",Math.floor(height*size));
}
graphics.drawString(bearingString, x+2, y + (height - distNumMetrics.height)/2);
graphics.setFontAlign(1,-1);
graphics.drawString(distNum, x + width, y + (height - distNumMetrics.height)/2);
graphics.setFont("Vector",Math.floor(height*0.25));
graphics.setFontAlign(-1,1);
if (targetDataSource.getProgress){
graphics.drawString(targetDataSource.getProgress(), x + 2, y + height);
}
graphics.setFontAlign(1,1);
if (!isNaN(distNum) && distNum != Infinity)
graphics.drawString(formattedDist.match(/[a-zA-Z]+/), x + width, y + height);
}
}
};
}
function drawCompass(graphics, x, y, height, width, increment, start){
graphics.setFont12x20();
graphics.setFontAlign(0,-1);
graphics.setColor(graphics.theme.fg);
let frag = 0 - start%15;
if (frag>0) frag = 0;
let xpos = 0 + frag*increment;
for (let i=start;i<=720;i+=15){
var res = i + frag;
if (res%90==0) {
graphics.drawString(labels[Math.floor(res/45)%8],xpos,y+2);
graphics.fillRect(xpos-2,Math.floor(y+height*0.6),xpos+2,Math.floor(y+height));
} else if (res%45==0) {
graphics.drawString(labels[Math.floor(res/45)%8],xpos,y+2);
graphics.fillRect(xpos-2,Math.floor(y+height*0.75),xpos+2,Math.floor(y+height));
} else if (res%15==0) {
graphics.fillRect(xpos,Math.floor(y+height*0.9),xpos+1,Math.floor(y+height));
}
xpos+=increment*15;
if (xpos > width + 20) break;
}
}
function getCompassSlice(compassDataSource){
let lastDrawn = Date.now() - Math.random()*2000;
const buffers = 4;
let buf = [];
return {
refresh : function (){return Bangle.isLocked()?(Date.now() - lastDrawn > 2000):true;},
draw: function (graphics, x,y,height,width){
lastDrawn = Date.now();
const max = 180;
const increment=width/max;
graphics.clearRect(x,y,x+width,y+height);
var start = compassDataSource.getCourse() - 90;
if (isNaN(compassDataSource.getCourse())) start = -90;
if (start<0) start+=360;
start = start % 360;
if (state.acc && compassDataSource.getCourseType() == "MAG"){
drawCompass(graphics,0,y+width*0.05,height-width*0.05,width,increment,start);
} else {
drawCompass(graphics,0,y,height,width,increment,start);
}
if (compassDataSource.getPoints){
for (let p of compassDataSource.getPoints()){
var bpos = p.bearing - compassDataSource.getCourse();
if (bpos>180) bpos -=360;
if (bpos<-180) bpos +=360;
bpos+=120;
let min = 0;
let max = 180;
if (bpos<=min){
bpos = Math.floor(width*0.05);
} else if (bpos>=max) {
bpos = Math.ceil(width*0.95);
} else {
bpos=Math.round(bpos*increment);
}
graphics.setColor(p.color);
graphics.fillCircle(bpos,y+height-12,Math.floor(width*0.03));
}
}
if (compassDataSource.getMarkers){
for (let m of compassDataSource.getMarkers()){
g.setColor(m.fillcolor);
let mpos = m.xpos * width;
if (m.xpos < 0.05) mpos = Math.floor(width*0.05);
if (m.xpos > 0.95) mpos = Math.ceil(width*0.95);
g.fillPoly(triangle(mpos,y+height-m.height, m.height, m.width));
g.setColor(m.linecolor);
g.drawPoly(triangle(mpos,y+height-m.height, m.height, m.width),true);
}
}
graphics.setColor(g.theme.fg);
graphics.fillRect(x,y,Math.floor(width*0.05),y+height);
graphics.fillRect(Math.ceil(width*0.95),y,width,y+height);
if (state.acc && compassDataSource.getCourseType() == "MAG") {
let xh = E.clip(width*0.5-height/2+(((state.acc.x+1)/2)*height),width*0.5 - height/2, width*0.5 + height/2);
let yh = E.clip(y+(((state.acc.y+1)/2)*height),y,y+height);
graphics.fillRect(width*0.5 - height/2, y, width*0.5 + height/2, y + Math.floor(width*0.05));
graphics.setColor(g.theme.bg);
graphics.drawLine(width*0.5 - 5, y, width*0.5 - 5, y + Math.floor(width*0.05));
graphics.drawLine(width*0.5 + 5, y, width*0.5 + 5, y + Math.floor(width*0.05));
graphics.fillRect(xh-1,y,xh+1,y+Math.floor(width*0.05));
let left = Math.floor(width*0.05);
let right = Math.ceil(width*0.95);
graphics.drawLine(0,y+height/2-5,left,y+height/2-5);
graphics.drawLine(right,y+height/2-5,x+width,y+height/2-5);
graphics.drawLine(0,y+height/2+5,left,y+height/2+5);
graphics.drawLine(right,y+height/2+5,x+width,y+height/2+5);
graphics.fillRect(0,yh-1,left,yh+1);
graphics.fillRect(right,yh-1,x+width,yh+1);
}
graphics.setColor(g.theme.fg);
graphics.drawRect(Math.floor(width*0.05),y,Math.ceil(width*0.95),y+height);
}
};
}
function radians(a) {
return a*Math.PI/180;
}
function degrees(a) {
var d = a*180/Math.PI;
return (d+360)%360;
}
function bearing(a,b){
if (!a || !b || !a.lon || !a.lat || !b.lon || !b.lat) return Infinity;
var delta = radians(b.lon-a.lon);
var alat = radians(a.lat);
var blat = radians(b.lat);
var y = Math.sin(delta) * Math.cos(blat);
var x = Math.cos(alat)*Math.sin(blat) -
Math.sin(alat)*Math.cos(blat)*Math.cos(delta);
return Math.round(degrees(Math.atan2(y, x)));
}
function distance(a,b){
if (!a || !b || !a.lon || !a.lat || !b.lon || !b.lat) return Infinity;
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);
}
function triangle (x, y, width, height){
return [
Math.round(x),Math.round(y),
Math.round(x+width * 0.5), Math.round(y+height),
Math.round(x-width * 0.5), Math.round(y+height)
];
}
function setButtons(){
Bangle.setUI("leftright", (dir)=>{
if (dir < 0) {
nextScreen();
} else if (dir > 0) {
switchMenu();
} else {
nextScreen();
}
});
}
function getApproxFileSize(name){
let currentStart = STORAGE.getStats().totalBytes;
let currentSize = 0;
for (let i = currentStart; i > 500; i/=2){
let currentDiff = i;
//print("Searching", currentDiff);
while (STORAGE.read(name, currentSize+currentDiff, 1) == ""){
//print("Loop", currentDiff);
currentDiff = Math.ceil(currentDiff/2);
}
i = currentDiff*2;
currentSize += currentDiff;
}
return currentSize;
}
function parseRouteData(filename, progressMonitor){
let routeInfo = {};
routeInfo.filename = filename;
routeInfo.refs = [];
let c = {};
let scanOffset = 0;
routeInfo.length = 0;
routeInfo.count = 0;
routeInfo.mirror = false;
let lastSeenWaypoint;
let lastSeenAlt;
let waypoint = {};
routeInfo.up = 0;
routeInfo.down = 0;
let size = getApproxFileSize(filename);
while ((scanOffset = getEntry(filename, scanOffset, waypoint)) > 0) {
if (routeInfo.count % 5 == 0) progressMonitor(scanOffset, "Loading", size);
if (lastSeenWaypoint){
routeInfo.length += distance(lastSeenWaypoint, waypoint);
let diff = waypoint.alt - lastSeenAlt;
//print("Distance", routeInfo.length, "alt", lastSeenAlt, waypoint.alt, diff);
if (waypoint.alt && lastSeenAlt && diff > 3){
if (lastSeenAlt < waypoint.alt){
//print("Up", diff);
routeInfo.up += diff;
} else {
//print("Down", diff);
routeInfo.down += diff;
}
}
}
routeInfo.count++;
routeInfo.refs.push(waypoint.fileOffset);
lastSeenWaypoint = waypoint;
if (!isNaN(waypoint.alt)) lastSeenAlt = waypoint.alt;
waypoint = {};
}
set(routeInfo, 0);
return routeInfo;
}
function hasPrev(route){
if (route.mirror) return route.index < (route.count - 1);
return route.index > 0;
}
function hasNext(route){
if (route.mirror) return route.index > 0;
return route.index < (route.count - 1);
}
function next(route){
if (!hasNext(route)) return;
if (route.mirror) set(route, --route.index);
if (!route.mirror) set(route, ++route.index);
}
function set(route, index){
route.currentWaypoint = {};
route.index = index;
getEntry(route.filename, route.refs[index], route.currentWaypoint);
}
function prev(route){
if (!hasPrev(route)) return;
if (route.mirror) set(route, ++route.index);
if (!route.mirror) set(route, --route.index);
}
let lastMirror;
let cachedLast;
function getLast(route){
let wp = {};
if (lastMirror != route.mirror){
if (route.mirror) getEntry(route.filename, route.refs[0], wp);
if (!route.mirror) getEntry(route.filename, route.refs[route.count - 1], wp);
lastMirror = route.mirror;
cachedLast = wp;
}
return cachedLast;
}
function removeMenu(){
E.showMenu();
switchNav();
}
function showProgress(progress, title, max){
//print("Progress",progress,max)
let message = title? title: "Loading";
if (max){
message += " " + E.clip((progress/max*100),0,100).toFixed(0) +"%";
} else {
let dots = progress % 4;
for (let i = 0; i < dots; i++) message += ".";
for (let i = dots; i < 4; i++) message += " ";
}
E.showMessage(message);
}
function handleLoading(c){
E.showMenu();
state.route = parseRouteData(c, showProgress);
state.waypoint = null;
removeMenu();
state.route.mirror = false;
}
function showRouteSelector (){
var menu = {
"" : {
back : showRouteMenu,
}
};
for (let c of STORAGE.list((/\.trf$/))){
let file = c;
menu[file] = ()=>{handleLoading(file);};
}
E.showMenu(menu);
}
function showRouteMenu(){
var menu = {
"" : {
"title" : "Route",
back : showMenu,
},
"Select file" : showRouteSelector
};
if (state.route){
menu.Mirror = {
value: state && state.route && !!state.route.mirror || false,
onchange: v=>{
state.route.mirror = v;
}
};
menu['Select closest waypoint'] = function () {
if (state.currentPos && state.currentPos.lat){
setClosestWaypoint(state.route, null, showProgress); removeMenu();
} else {
E.showAlert("No position").then(()=>{E.showMenu(menu);});
}
};
menu['Select closest waypoint (not visited)'] = function () {
if (state.currentPos && state.currentPos.lat){
setClosestWaypoint(state.route, state.route.index, showProgress); removeMenu();
} else {
E.showAlert("No position").then(()=>{E.showMenu(menu);});
}
};
menu['Select waypoint'] = {
value : state.route.index,
min:1,max:state.route.count,step:1,
onchange : v => { set(state.route, v-1); }
};
menu['Select waypoint as current position'] = function (){
state.currentPos.lat = state.route.currentWaypoint.lat;
state.currentPos.lon = state.route.currentWaypoint.lon;
state.currentPos.alt = state.route.currentWaypoint.alt;
removeMenu();
};
}
if (state.route && hasPrev(state.route))
menu['Previous waypoint'] = function() { prev(state.route); removeMenu(); };
if (state.route && hasNext(state.route))
menu['Next waypoint'] = function() { next(state.route); removeMenu(); };
E.showMenu(menu);
}
function showWaypointSelector(){
let waypoints = require("waypoints").load();
var menu = {
"" : {
back : showWaypointMenu,
}
};
for (let c in waypoints){
menu[waypoints[c].name] = function (){
state.waypoint = waypoints[c];
state.waypointIndex = c;
state.route = null;
removeMenu();
};
}
E.showMenu(menu);
}
function showCalibrationMenu(){
let menu = {
"" : {
"title" : "Calibration",
back : showMenu,
},
"Barometer (GPS)" : ()=>{
if (!state.currentPos || isNaN(state.currentPos.alt)){
E.showAlert("No GPS altitude").then(()=>{E.showMenu(menu);});
} else {
state.calibAltDiff = state.altitude - state.currentPos.alt;
E.showAlert("Calibrated Altitude Difference: " + state.calibAltDiff.toFixed(0)).then(()=>{removeMenu();});
}
},
"Barometer (Manual)" : {
value : Math.round(state.currentPos && (state.currentPos.alt != undefined && !isNaN(state.currentPos.alt)) ? state.currentPos.alt: state.altitude),
min:-2000,max: 10000,step:1,
onchange : v => { state.calibAltDiff = state.altitude - v; }
},
"Reset Compass" : ()=>{ Bangle.resetCompass(); removeMenu();},
};
E.showMenu(menu);
}
function showWaypointMenu(){
let menu = {
"" : {
"title" : "Waypoint",
back : showMenu,
},
"Select waypoint" : showWaypointSelector,
};
E.showMenu(menu);
}
function showMenu(){
var mainmenu = {
"" : {
"title" : "Main",
back : removeMenu,
},
"Route" : showRouteMenu,
"Waypoint" : showWaypointMenu,
"Calibration": showCalibrationMenu,
"Start" : ()=>{ E.showPrompt("Start?").then((v)=>{ if (v) {state.active = true; removeMenu();} else {E.showMenu(mainmenu);}});},
"Stop" : ()=>{ E.showPrompt("Stop?").then((v)=>{ if (v) {WIDGETS["gpstrek"].stop(); removeMenu();} else {E.showMenu(mainmenu);}});},
"Reset" : ()=>{ E.showPrompt("Do Reset?").then((v)=>{ if (v) {WIDGETS["gpstrek"].resetState(); removeMenu();} else {E.showMenu(mainmenu);}});},
"Slices" : {
value : numberOfSlices,
min:1,max:6,step:1,
onchange : v => { setNumberOfSlices(v); }
},
};
E.showMenu(mainmenu);
}
let scheduleDraw = true;
function switchMenu(){
screen = 0;
scheduleDraw = false;
showMenu();
}
function drawInTimeout(){
setTimeout(()=>{
draw();
if (scheduleDraw)
setTimeout(drawInTimeout, 0);
},0);
}
function switchNav(){
if (!screen) screen = 1;
setButtons();
scheduleDraw = true;
drawInTimeout();
}
function nextScreen(){
screen++;
if (screen > maxScreens){
screen = 1;
}
}
function setClosestWaypoint(route, startindex, progress){
if (startindex >= state.route.count) startindex = state.route.count - 1;
if (!state.currentPos.lat){
set(route, startindex);
return;
}
let minDist = 100000000000000;
let minIndex = 0;
for (let i = startindex?startindex:0; i < route.count - 1; i++){
if (progress && (i % 5 == 0)) progress(i-(startindex?startindex:0), "Searching", route.count);
let wp = {};
getEntry(route.filename, route.refs[i], wp);
let curDist = distance(state.currentPos, wp);
if (curDist < minDist){
minDist = curDist;
minIndex = i;
} else {
if (startindex) break;
}
}
set(route, minIndex);
}
let screen = 1;
const compassSliceData = {
getCourseType: function(){
return (state.currentPos && state.currentPos.course) ? "GPS" : "MAG";
},
getCourse: function (){
if(compassSliceData.getCourseType() == "GPS") return state.currentPos.course;
return state.compassHeading?state.compassHeading:undefined;
},
getPoints: function (){
let points = [];
if (state.currentPos && state.currentPos.lon && state.route && state.route.currentWaypoint){
points.push({bearing:bearing(state.currentPos, state.route.currentWaypoint), color:"#0f0"});
}
if (state.currentPos && state.currentPos.lon && state.route){
points.push({bearing:bearing(state.currentPos, getLast(state.route)), color:"#00f"});
}
return points;
},
getMarkers: function (){
return [{xpos:0.5, width:10, height:10, linecolor:g.theme.fg, fillcolor:"#f00"}];
}
};
const waypointData = {
icon: atob("EBCBAAAAAAAAAAAAcIB+zg/uAe4AwACAAAAAAAAAAAAAAAAA"),
getProgress: function() {
return (state.route.index + 1) + "/" + state.route.count;
},
getTarget: function (){
if (distance(state.currentPos,state.route.currentWaypoint) < 30 && hasNext(state.route)){
next(state.route);
Bangle.buzz(1000);
}
return state.route.currentWaypoint;
},
getStart: function (){
return state.currentPos;
}
};
const finishData = {
icon: atob("EBABAAA/4DmgJmAmYDmgOaAmYD/gMAAwADAAMAAwAAAAAAA="),
getTarget: function (){
if (state.route) return getLast(state.route);
if (state.waypoint) return state.waypoint;
},
getStart: function (){
return state.currentPos;
}
};
let sliceHeight;
function setNumberOfSlices(number){
numberOfSlices = number;
sliceHeight = Math.floor((g.getHeight()-(showWidgets?24:0))/numberOfSlices);
}
let slices = [];
let maxScreens = 1;
setNumberOfSlices(3);
let compassSlice = getCompassSlice(compassSliceData);
let waypointSlice = getTargetSlice(waypointData);
let finishSlice = getTargetSlice(finishData);
let eleSlice = getDoubleLineSlice("Up","Down",()=>{
return loc.distance(state.up,3) + "/" + (state.route ? loc.distance(state.route.up,3):"---");
},()=>{
return loc.distance(state.down,3) + "/" + (state.route ? loc.distance(state.route.down,3): "---");
});
let statusSlice = getDoubleLineSlice("Speed","Alt",()=>{
let speed = 0;
if (state.currentPos && state.currentPos.speed) speed = state.currentPos.speed;
return loc.speed(speed,2);
},()=>{
let alt = Infinity;
if (!isNaN(state.altitude)){
alt = isNaN(state.calibAltDiff) ? state.altitude : (state.altitude - state.calibAltDiff);
}
if (state.currentPos && state.currentPos.alt) alt = state.currentPos.alt;
return loc.distance(alt,3);
});
let status2Slice = getDoubleLineSlice("Compass","GPS",()=>{
return (state.compassHeading?Math.round(state.compassHeading):"---") + "°";
},()=>{
let course = "---°";
if (state.currentPos && state.currentPos.course) course = state.currentPos.course + "°";
return course;
},200);
let healthSlice = getDoubleLineSlice("Heart","Steps",()=>{
return state.bpm;
},()=>{
return state.steps;
});
let system2Slice = getDoubleLineSlice("Bat","",()=>{
return (Bangle.isCharging()?"+":"") + E.getBattery().toFixed(0)+"% " + NRF.getBattery().toFixed(2) + "V";
},()=>{
return "";
});
let systemSlice = getDoubleLineSlice("RAM","Storage",()=>{
let ram = process.memory(false);
return ((ram.blocksize * ram.free)/1024).toFixed(0)+"kB";
},()=>{
return (STORAGE.getFree()/1024).toFixed(0)+"kB";
});
function updateSlices(){
slices = [];
slices.push(compassSlice);
if (state.currentPos && state.currentPos.lat && state.route && state.route.currentWaypoint && state.route.index < state.route.count - 1) {
slices.push(waypointSlice);
}
if (state.currentPos && state.currentPos.lat && (state.route || state.waypoint)) {
slices.push(finishSlice);
}
if ((state.route && state.route.down !== undefined) || state.down != undefined) {
slices.push(eleSlice);
}
slices.push(statusSlice);
slices.push(status2Slice);
slices.push(healthSlice);
slices.push(systemSlice);
slices.push(system2Slice);
maxScreens = Math.ceil(slices.length/numberOfSlices);
}
function clear() {
g.clearRect(0,(showWidgets ? 24 : 0), g.getWidth(),g.getHeight());
}
let lastDrawnScreen;
let firstDraw = true;
function draw(){
if (!screen) return;
let ypos = showWidgets ? 24 : 0;
let firstSlice = (screen-1)*numberOfSlices;
updateSlices();
let force = lastDrawnScreen != screen || firstDraw;
if (force){
clear();
if (showWidgets){
Bangle.drawWidgets();
}
}
lastDrawnScreen = screen;
for (let slice of slices.slice(firstSlice,firstSlice + numberOfSlices)) {
g.reset();
if (!slice.refresh || slice.refresh() || force) slice.draw(g,0,ypos,sliceHeight,g.getWidth());
ypos += sliceHeight+1;
g.drawLine(0,ypos-1,g.getWidth(),ypos-1);
}
firstDraw = false;
}
switchNav();
g.clear();

14
apps/gpstrek/createRoute.sh Executable file
View File

@ -0,0 +1,14 @@
#!/bin/bash
[ -z "$1" ] && echo Give gpx file name
xmlstarlet select -t -m '//_:trkpt' \
--if '_:name and _:ele' -o D \
--elif '_:ele and not(_:name)' -o C \
--elif 'not(_:ele) and _:name' -o B \
--else -o A -b \
-v 'format-number(@lat,"+00.0000000;-00.0000000")' \
-v 'format-number(@lon,"+000.0000000;-000.0000000")' \
--if '_:ele' -v 'format-number(_:ele,"+00000;-00000")' -b \
--if _:name -v 'format-number(string-length(_:name),"00")' -v '_:name' -b \
-n "$1" | iconv -f utf8 -t iso8859-1 > "$(basename "$1" | sed -e "s|.gpx||").trf"

BIN
apps/gpstrek/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 851 B

View File

@ -0,0 +1,17 @@
{
"id": "gpstrek",
"name": "GPS Trekking",
"version": "0.01",
"description": "Helper for tracking the status/progress during hiking. Do NOT depend on this for navigation!",
"icon": "icon.png",
"screenshots": [{"url":"screen1.png"},{"url":"screen2.png"},{"url":"screen3.png"},{"url":"screen4.png"}],
"tags": "tool,outdoors,gps",
"supports": ["BANGLEJS2"],
"readme": "README.md",
"dependencies" : { "waypoints":"type" },
"storage": [
{"name":"gpstrek.app.js","url":"app.js"},
{"name":"gpstrek.wid.js","url":"widget.js"},
{"name":"gpstrek.img","url":"app-icon.js","evaluate":true}
]
}

BIN
apps/gpstrek/screen1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

BIN
apps/gpstrek/screen2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

BIN
apps/gpstrek/screen3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

BIN
apps/gpstrek/screen4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

129
apps/gpstrek/widget.js Normal file
View File

@ -0,0 +1,129 @@
(() => {
const STORAGE=require('Storage');
let state = STORAGE.readJSON("gpstrek.state.json")||{};
function saveState(){
state.saved = Date.now();
STORAGE.writeJSON("gpstrek.state.json", state);
}
E.on("kill",()=>{
if (state.active){
saveState();
}
});
function onPulse(e){
state.bpm = e.bpm;
}
function onGPS(fix) {
if(fix.fix) state.currentPos = fix;
}
Bangle.on('accel', function(e) {
state.acc = e;
});
function onMag(e) {
if (!state.compassHeading) state.compassHeading = e.heading;
//if (a+180)mod 360 == b then
//return (a+b)/2 mod 360 and ((a+b)/2 mod 360) + 180 (they are both the solution, so you may choose one depending if you prefer counterclockwise or clockwise direction)
//else
//return arctan( (sin(a)+sin(b)) / (cos(a)+cos(b) )
/*
let average;
let a = radians(compassHeading);
let b = radians(e.heading);
if ((a+180) % 360 == b){
average = ((a+b)/2 % 360); //can add 180 depending on rotation
} else {
average = Math.atan( (Math.sin(a)+Math.sin(b))/(Math.cos(a)+Math.cos(b)) );
}
print("Angle",compassHeading,e.heading, average);
compassHeading = (compassHeading + degrees(average)) % 360;
*/
state.compassHeading = Math.round(e.heading);
}
function onStep(e) {
state.steps++;
}
function onPressure(e) {
state.pressure = e.pressure;
if (!state.altitude){
state.altitude = e.altitude;
state.up = 0;
state.down = 0;
}
let diff = state.altitude - e.altitude;
if (Math.abs(diff) > 3){
if (diff > 0){
state.up += diff;
} else {
state.down -= diff;
}
state.altitude = e.altitude;
}
}
function start(){
Bangle.on('GPS', onGPS);
Bangle.on("HRM", onPulse);
Bangle.on("mag", onMag);
Bangle.on("step", onStep);
Bangle.on("pressure", onPressure);
Bangle.setGPSPower(1, "gpstrek");
Bangle.setHRMPower(1, "gpstrek");
Bangle.setCompassPower(1, "gpstrek");
Bangle.setBarometerPower(1, "gpstrek");
state.active = true;
Bangle.drawWidgets();
}
function stop(){
state.active = false;
saveState();
Bangle.drawWidgets();
}
function initState(){
//cleanup volatile state here
state.currentPos={};
state.steps = Bangle.getStepCount();
state.calibAltDiff = 0;
state.up = 0;
state.down = 0;
}
if (state.saved && state.saved < Date.now() - 60000){
initState();
}
if (state.active){
start();
}
WIDGETS["gpstrek"]={
area:"tl",
width:state.active?24:0,
resetState: initState,
getState: function() {
return state;
},
start:start,
stop:stop,
draw:function() {
if (state.active){
g.reset();
g.drawImage(atob("GBiBAAAAAAAAAAAYAAAYAAAYAAA8AAA8AAB+AAB+AADbAADbAAGZgAGZgAMYwAMYwAcY4AYYYA5+cA3/sB/D+B4AeBAACAAAAAAAAA=="), this.x, this.y);
}
}
};
})();

View File

@ -9,3 +9,6 @@
Show/Hide widgets with swipe up or down
0.08: Use default Bangle formatter for booleans
0.09: Support new fast app switching
0.10: Fix clock not correctly refreshing when drawing in timeouts option is not on
0.11: Additional option in customizer to force drawing directly
Fix some problems in handling timeouts

View File

@ -1,8 +1,21 @@
let unlockedDrawInterval = [];
let lockedDrawInterval = [];
let showWidgets = false;
let firstDraw = true;
{
let x = g.getWidth()/2;
let y = g.getHeight()/2;
g.setColor(g.theme.bg);
g.fillRect(x-49, y-19, x+49, y+19);
g.setColor(g.theme.fg);
g.drawRect(x-50, y-20, x+50, y+20);
y -= 4;
x -= 4*6;
g.setFont("6x8");
g.setFontAlign(-1,-1);
g.drawString("Loading...", x, y);
let watchface = require("Storage").readJSON("imageclock.face.json");
let watchfaceResources = require("Storage").readJSON("imageclock.resources.json");
let precompiledJs = eval(require("Storage").read("imageclock.draw.js"));
@ -12,37 +25,37 @@ let showWidgets = false;
let startPerfLog = () => {};
let endPerfLog = () => {};
let printPerfLog = () => print("Deactivated");
let resetPerfLog = () => {performanceLog = {};};
Bangle.printPerfLog = () => {print("Deactivated");};
Bangle.resetPerfLog = () => {performanceLog = {};};
let colormap={
"#000":0,
"#00f":1,
"#0f0":2,
"#0ff":3,
"#f00":4,
"#f0f":5,
"#ff0":6,
"#fff":7
"#000":0,
"#00f":1,
"#0f0":2,
"#0ff":3,
"#f00":4,
"#f0f":5,
"#ff0":6,
"#fff":7
};
let palette = new Uint16Array([
0x0000, //black #000
0x001f, //blue #00f
0x07e0, //green #0f0
0x07ff, //cyan #0ff
0xf800, //red #f00
0xf81f, //magenta #f0f
0xffe0, //yellow #ff0
0xffff, //white #fff
0xffff, //white
0xffff, //white
0xffff, //white
0xffff, //white
0xffff, //white
0xffff, //white
0xffff, //white
0xffff, //white
0x0000, //black #000
0x001f, //blue #00f
0x07e0, //green #0f0
0x07ff, //cyan #0ff
0xf800, //red #f00
0xf81f, //magenta #f0f
0xffe0, //yellow #ff0
0xffff, //white #fff
0xffff, //white
0xffff, //white
0xffff, //white
0xffff, //white
0xffff, //white
0xffff, //white
0xffff, //white
0xffff, //white
]);
let p0 = g;
@ -67,7 +80,7 @@ let showWidgets = false;
performanceLog.count[name]++;
};
printPerfLog = function(){
Bangle.printPerfLog = function(){
let result = "";
let keys = [];
for (let c in performanceLog.cum){
@ -79,23 +92,33 @@ let showWidgets = false;
}
};
}
let delayTimeouts = [];
function delay(t) {
startPerfLog("loadFunctions");
let delayTimeouts = {};
let timeoutCount = 0;
let delay = function(t) {
return new Promise(function (resolve) {
delayTimeouts.push(setTimeout(resolve, t));
const i = timeoutCount++;
let timeout = setTimeout(()=>{
resolve();
delete delayTimeouts[i];
}, t);
delayTimeouts[i] = timeout;
//print("Add delay timeout", delayTimeouts);
});
}
};
function cleanupDelays(){
let cleanupDelays = function(){
//print("Cleanup delays", delayTimeouts);
for (let t of delayTimeouts){
clearTimeout(t);
}
delayTimeouts = [];
}
delayTimeouts = {};
};
function prepareImg(resource){
let prepareImg = function(resource){
startPerfLog("prepareImg");
//print("prepareImg: ", resource);
@ -104,15 +127,15 @@ let showWidgets = false;
delete resource.dataOffset;
delete resource.dataLength;
if (resource.paletteData){
result.palette = new Uint16Array(resource.paletteData);
resource.palette = new Uint16Array(resource.paletteData);
delete resource.paletteData;
}
}
endPerfLog("prepareImg");
return resource;
}
};
function getByPath(object, path, lastElem){
let getByPath = function(object, path, lastElem){
startPerfLog("getByPath");
//print("getByPath", path,lastElem);
let current = object;
@ -133,23 +156,24 @@ let showWidgets = false;
return undefined;
}
return current;
}
};
function splitNumberToDigits(num){
let splitNumberToDigits = function(num){
return String(num).split('').map(item => Number(item));
}
};
function isChangedNumber(element){
let isChangedNumber = function(element){
return element.lastDrawnValue != getValue(element.Value);
}
};
function isChangedMultistate(element){
let isChangedMultistate = function(element){
return element.lastDrawnValue != getMultistate(element.Value);
}
};
function drawNumber(graphics, resources, element){
let drawNumber = function(graphics, resources, element){
startPerfLog("drawNumber");
let number = getValue(element.Value);
//print("drawNumber: ", number, element);
let spacing = element.Spacing ? element.Spacing : 0;
let unit = element.Unit;
@ -158,7 +182,6 @@ let showWidgets = false;
let numberOfDigits = element.Digits;
//print("drawNumber: ", number, element);
if (number) number = number.toFixed(0);
let isNegative;
@ -245,7 +268,7 @@ let showWidgets = false;
} else {
currentDigit = 0;
}
//print("Digit " + currentDigit + " " + currentX);
//print("Digit", currentDigit, currentX);
drawElement(graphics, resources, {X:currentX,Y:firstDigitY}, element, currentDigit + imageIndex);
currentX += firstImage.width + spacing;
}
@ -258,9 +281,9 @@ let showWidgets = false;
element.lastDrawnValue = number;
endPerfLog("drawNumber");
}
};
function drawElement(graphics, resources, pos, element, lastElem){
let drawElement = function(graphics, resources, pos, element, lastElem){
startPerfLog("drawElement");
let cacheKey = "_"+(lastElem?lastElem:"nole");
if (!element.cachedImage) element.cachedImage={};
@ -282,11 +305,9 @@ let showWidgets = false;
}
}
//print("cache ",typeof element.cachedImage[cacheKey], element.ImagePath, lastElem);
//print("cache ", typeof element.cachedImage[cacheKey], element.ImagePath, lastElem);
if(element.cachedImage[cacheKey]){
//print("drawElement ",pos, path, lastElem);
//print("resource ", resource,pos, path, lastElem);
//print("drawImage from drawElement", image, pos);
//print("drawElement ", pos, element, lastElem);
let options={};
if (element.RotationValue){
options.rotate = radians(element);
@ -304,35 +325,36 @@ let showWidgets = false;
endPerfLog("drawElement_g.drawImage");
}
endPerfLog("drawElement");
}
};
function getValue(value, defaultValue){
let getValue = function(value, defaultValue){
startPerfLog("getValue");
if (typeof value == "string"){
return numbers[value]();
}
if (value == undefined) return defaultValue;
endPerfLog("getValue");
return value;
}
};
function getMultistate(name, defaultValue){
let getMultistate = function(name, defaultValue){
startPerfLog("getMultistate");
if (typeof name == "string"){
return multistates[name]();
} else {
if (name == undefined) return defaultValue;
}
endPerfLog("getMultistate");
return undefined;
}
};
function drawScale(graphics, resources, scale){
let drawScale = function(graphics, resources, scale){
startPerfLog("drawScale");
//print("drawScale", scale);
let segments = scale.Segments;
let imageIndex = scale.ImageIndex !== undefined ? scale.ImageIndex : 0;
let value = scaledown(scale.Value, scale.MinValue, scale.MaxValue);
//print("Value is ", value, "(", maxValue, ",", minValue, ")");
let segmentsToDraw = Math.ceil(value * segments.length);
for (let i = 0; i < segmentsToDraw; i++){
@ -341,9 +363,9 @@ let showWidgets = false;
scale.lastDrawnValue = segmentsToDraw;
endPerfLog("drawScale");
}
};
function drawImage(graphics, resources, image, name){
let drawImage = function(graphics, resources, image, name){
startPerfLog("drawImage");
//print("drawImage", image.X, image.Y, name);
if (image.Value && image.Steps){
@ -357,9 +379,9 @@ let showWidgets = false;
}
endPerfLog("drawImage");
}
};
function drawCodedImage(graphics, resources, image){
let drawCodedImage = function(graphics, resources, image){
startPerfLog("drawCodedImage");
let code = getValue(image.Value);
//print("drawCodedImage", image, code);
@ -386,9 +408,9 @@ let showWidgets = false;
image.lastDrawnValue = code;
startPerfLog("drawCodedImage");
}
};
function getWeatherCode(){
let getWeatherCode = function(){
let jsonWeather = require("Storage").readJSON('weather.json');
let weather = (jsonWeather && jsonWeather.weather) ? jsonWeather.weather : undefined;
@ -396,9 +418,9 @@ let showWidgets = false;
return weather.code;
}
return undefined;
}
};
function getWeatherTemperature(){
let getWeatherTemperature = function(){
let jsonWeather = require("Storage").readJSON('weather.json');
let weather = (jsonWeather && jsonWeather.weather) ? jsonWeather.weather : undefined;
@ -415,25 +437,25 @@ let showWidgets = false;
}
}
return result;
}
};
function scaledown(value, min, max){
let scaledown = function(value, min, max){
//print("scaledown", value, min, max);
let scaled = E.clip(getValue(value),getValue(min,0),getValue(max,1));
scaled -= getValue(min,0);
scaled /= getValue(max,1);
return scaled;
}
};
function radians(rotation){
let radians = function(rotation){
let value = scaledown(rotation.RotationValue, rotation.MinRotationValue, rotation.MaxRotationValue);
value -= rotation.RotationOffset ? rotation.RotationOffset : 0;
value *= 360;
value *= Math.PI / 180;
return value;
}
};
function drawPoly(graphics, resources, element){
let drawPoly = function(graphics, resources, element){
startPerfLog("drawPoly");
let vertices = [];
@ -463,9 +485,9 @@ let showWidgets = false;
}
endPerfLog("drawPoly");
}
};
function drawRect(graphics, resources, element){
let drawRect = function(graphics, resources, element){
startPerfLog("drawRect");
let vertices = [];
@ -479,9 +501,9 @@ let showWidgets = false;
endPerfLog("drawRect_g.fillRect");
}
endPerfLog("drawRect");
}
};
function drawCircle(graphics, resources, element){
let drawCircle = function(graphics, resources, element){
startPerfLog("drawCircle");
if (element.Filled){
@ -494,7 +516,7 @@ let showWidgets = false;
endPerfLog("drawCircle_g.drawCircle");
}
endPerfLog("drawCircle");
}
};
let numbers = {};
numbers.Hour = () => { return new Date().getHours(); };
@ -547,7 +569,7 @@ let showWidgets = false;
multistates.WeatherTemperatureUnit = () => { return getWeatherTemperature().unit; };
multistates.StepsGoal = () => { return (numbers.Steps() >= (settings.stepsgoal || 10000)) ? "on": "off"; };
function drawMultiState(graphics, resources, element){
let drawMultiState = function(graphics, resources, element){
startPerfLog("drawMultiState");
//print("drawMultiState", element);
let value = multistates[element.Value]();
@ -555,7 +577,7 @@ let showWidgets = false;
drawImage(graphics, resources, element, value);
element.lastDrawnValue = value;
endPerfLog("drawMultiState");
}
};
let pulse,alt,temp,press;
@ -563,25 +585,20 @@ let showWidgets = false;
let requestedDraws = 0;
let isDrawing = false;
let drawingTime;
let start;
let deferredTimout;
function initialDraw(resources, face){
let initialDraw = function(resources, face){
//print("Free memory", process.memory(false).free);
requestedDraws++;
if (!isDrawing){
cleanupDelays();
//print(new Date().toISOString(), "Can draw,", requestedDraws, "draws requested so far");
isDrawing = true;
resetPerfLog();
requestedDraws = 0;
//print(new Date().toISOString(), "Drawing start");
startPerfLog("initialDraw");
//start = Date.now();
drawingTime = 0;
//print("Precompiled");
let promise = precompiledJs(watchfaceResources, watchface);
@ -595,8 +612,6 @@ let showWidgets = false;
g.drawLine(0,24,g.getWidth(),24);
}
lastDrawTime = Date.now() - start;
drawingTime += Date.now() - currentDrawingTime;
//print(new Date().toISOString(), "Drawing done in", lastDrawTime.toFixed(0), "active:", drawingTime.toFixed(0));
isDrawing=false;
firstDraw=false;
requestRefresh = false;
@ -608,14 +623,16 @@ let showWidgets = false;
if (requestedDraws > 0){
//print(new Date().toISOString(), "Had deferred drawing left, drawing again");
requestedDraws = 0;
//print("Clear deferred timeout", deferredTimout);
clearTimeout(deferredTimeout);
deferredTimout = setTimeout(()=>{initialDraw(resources, face);}, 10);
}
} //else {
//print("queued draw");
//}
}
};
function handleHrm(e){
let handleHrm = function(e){
if (e.confidence > 70){
pulse = e.bpm;
if (!redrawEvents || redrawEvents.includes("HRM") && !Bangle.isLocked()){
@ -623,9 +640,9 @@ let showWidgets = false;
initialDraw(watchfaceResources, watchface);
}
}
}
};
function handlePressure(e){
let handlePressure = function(e){
alt = e.altitude;
temp = e.temperature;
press = e.pressure;
@ -633,42 +650,46 @@ let showWidgets = false;
//print("Redrawing on pressure");
initialDraw(watchfaceResources, watchface);
}
}
};
function handleCharging(e){
let handleCharging = function(e){
if (!redrawEvents || redrawEvents.includes("charging") && !Bangle.isLocked()){
//print("Redrawing on charging");
initialDraw(watchfaceResources, watchface);
}
}
};
function getMatchedWaitingTime(time){
let getMatchedWaitingTime = function(time){
let result = time - (Date.now() % time);
//print("Matched timeout", time, result);
//print("Matched wating time", time, result);
return result;
}
};
function setMatchedInterval(callable, time, intervalHandler, delay){
let setMatchedInterval = function(callable, time, intervalHandler, delay){
//print("Setting matched interval for", time, intervalHandler);
if (!delay) delay = 0;
let matchedTime = getMatchedWaitingTime(time + delay);
return setTimeout(()=>{
let interval = setInterval(callable, time);
//print("setMatchedInterval", interval);
if (intervalHandler) intervalHandler(interval);
callable();
}, matchedTime);
}
};
endPerfLog("loadFunctions");
let lastDrawTime = 0;
let firstDraw = true;
startPerfLog("loadProperties");
let lockedRedraw = getByPath(watchface, ["Properties","Redraw","Locked"]) || 60000;
let unlockedRedraw = getByPath(watchface, ["Properties","Redraw","Unlocked"]) || 1000;
let defaultRedraw = getByPath(watchface, ["Properties","Redraw","Default"]) || "Always";
let redrawEvents = getByPath(watchface, ["Properties","Redraw","Events"]);
let clearOnRedraw = getByPath(watchface, ["Properties","Redraw","Clear"]);
let events = getByPath(watchface, ["Properties","Events"]);
endPerfLog("loadProperties");
//print("events", events);
//print("redrawEvents", redrawEvents);
@ -676,7 +697,7 @@ let showWidgets = false;
let initialDrawTimeoutUnlocked;
let initialDrawTimeoutLocked;
function handleLock(isLocked, forceRedraw){
let handleLock = function(isLocked, forceRedraw){
//print("isLocked", Bangle.isLocked());
for (let i of unlockedDrawInterval){
//print("Clearing unlocked", i);
@ -694,6 +715,10 @@ let showWidgets = false;
//print("Redrawing on unlock", isLocked);
initialDraw(watchfaceResources, watchface);
}
if (initialDrawTimeoutUnlocked){
//print("clear initialDrawTimeUnlocked timet", initialDrawTimeoutUnlocked);
clearTimeout(initialDrawTimeoutUnlocked);
}
initialDrawTimeoutUnlocked = setMatchedInterval(()=>{
//print("Redrawing on unlocked interval");
initialDraw(watchfaceResources, watchface);
@ -708,6 +733,10 @@ let showWidgets = false;
//print("Redrawing on lock", isLocked);
initialDraw(watchfaceResources, watchface);
}
if (initialDrawTimeoutLocked){
clearTimeout(initialDrawTimeoutLocked);
//print("clear initialDrawTimeLocked timet", initialDrawTimeoutLocked);
}
initialDrawTimeoutLocked = setMatchedInterval(()=>{
//print("Redrawing on locked interval");
initialDraw(watchfaceResources, watchface);
@ -718,13 +747,13 @@ let showWidgets = false;
Bangle.setHRMPower(0, "imageclock");
Bangle.setBarometerPower(0, 'imageclock');
}
}
};
let showWidgetsChanged = false;
let currentDragDistance = 0;
function restoreWidgetDraw(){
let restoreWidgetDraw = function(){
if (global.WIDGETS) {
for (let w in global.WIDGETS) {
let wd = global.WIDGETS[w];
@ -732,9 +761,9 @@ let showWidgets = false;
wd.area = originalWidgetArea[w];
}
}
}
};
function handleDrag(e){
let handleDrag = function(e){
//print("handleDrag");
currentDragDistance += e.dy;
if (Math.abs(currentDragDistance) < 10) return;
@ -757,7 +786,7 @@ let showWidgets = false;
showWidgets = dragDown;
initialDraw();
}
}
};
Bangle.on('drag', handleDrag);
@ -766,7 +795,7 @@ let showWidgets = false;
try{
Bangle.setBarometerPower(1, 'imageclock');
} catch (e){
print("Error during barometer power up", e);
//print("Error during barometer power up", e);
}
}
if (!events || events.includes("HRM")) {
@ -783,7 +812,7 @@ let showWidgets = false;
let originalWidgetDraw = {};
let originalWidgetArea = {};
function clearWidgetsDraw(){
let clearWidgetsDraw = function(){
//print("Clear widget draw calls");
if (global.WIDGETS) {
originalWidgetDraw = {};
@ -798,10 +827,7 @@ let showWidgets = false;
}
}
if (!global.WIDGETS) Bangle.loadWidgets();
clearWidgetsDraw();
handleLock(Bangle.isLocked());
handleLock(Bangle.isLocked(), true);
Bangle.setUI({
mode : "clock",
@ -832,9 +858,19 @@ let showWidgets = false;
}
delete lockedDrawInterval;
delete showWidgets;
delete firstDraw;
delete Bangle.printPerfLog;
if (settings.perflog){
delete Bangle.resetPerfLog;
delete performanceLog;
}
cleanupDelays();
restoreWidgetDraw();
}
});
Bangle.loadWidgets();
clearWidgetsDraw();
}

View File

@ -23,6 +23,8 @@
Options:</br>
<input type="checkbox" id="timeoutwrap" name="mode"/>
<label for="timeoutwrap">Wrap draw calls in timeouts (Slower, more RAM use, better interactivity)</label></br>
<input type="checkbox" id="forceOrigPlane" name="mode" disabled="true"/>
<label for="forceOrigPlane">Force use of direct drawing (Even faster, but will produce visible artifacts on not optimized watch faces)</label></br>
<input type="checkbox" id="debugprints" name="mode"/>
<label for="debugprints">Add debug prints to generated code</label></br>
</p>
@ -579,11 +581,8 @@
return result;
}
function convertToCode(elements, properties, wrapInTimeouts){
function convertToCode(elements, properties, wrapInTimeouts, forceUseOrigPlane){
var code = "(function (wr, wf) {\n";
if (!wrapInTimeouts){
code += "var ct=Date.now();\n";
}
code += "var lc;\n";
code += "var p = Promise.resolve();\n";
@ -595,7 +594,7 @@
var c = elements[i].value;
console.log("Check element", c);
var name = c.Layer;
var plane = wrapInTimeouts ? 1 : 0;
var plane = (wrapInTimeouts && !forceUseOrigPlane) ? 1 : 0;
if (typeof c.Plane == "number"){
plane = c.Plane;
}
@ -610,8 +609,6 @@
console.log("Found planes", planes, "with numbers", planeNumbers)
if (wrapInTimeouts && planes == 0) planes = 1;
code += "p0 = g;\n";
for (var planeIndex = 0; planeIndex < planeNumbers.length; planeIndex++){
@ -624,32 +621,25 @@
if (plane != 0) code += "if (!p" + plane + ") p" + plane + " = Graphics.createArrayBuffer(g.getWidth(),g.getHeight(),4,{msb:true});\n";
if (properties.Redraw && properties.Redraw.Clear){
if (wrapInTimeouts && plane != 0){
if (wrapInTimeouts && (plane != 0 || forceUseOrigPlane)){
code += "p = p.then(()=>delay(0)).then(()=>{\n";
} else {
code += "p = p.then(()=>{\n";
}
code += "var ct=Date.now();\n"
if (addDebug()) code += 'print("Clear for redraw of plane ' + p + '");'+"\n";
code += 'startPerfLog("initialDraw_g.clear");'+"\n";
code += "p" + plane + ".clear(true);\n";
code += 'endPerfLog("initialDraw_g.clear");'+ "\n";
code += "drawingTime += Date.now() - ct;\n";
code += "});\n";
}
var previousPlane = plane + 1;
if (previousPlane < planeNumbers.length){
code += "p = p.then(()=>{\n";
code += "var ct=Date.now();\n";
if (addDebug()) code += 'print("Copying of plane ' + previousPlane + ' to display");'+"\n";
//code += "g.drawImage(p" + i + ".asImage());";
code += "p0.drawImage({width: p" + previousPlane + ".getWidth(), height: p" + previousPlane + ".getHeight(), bpp: p" + previousPlane + ".getBPP(), buffer: p" + previousPlane + ".buffer, palette: palette});\n";
code += "drawingTime += Date.now() - ct;\n";
code += "});\n";
}
@ -660,12 +650,6 @@
console.log("Layer elements", layername, layerElements);
//code for whole layer
if (wrapInTimeouts && plane != 0){
code += "p = p.then(()=>delay(0)).then(()=>{\n";
} else {
code += "p = p.then(()=>{\n";
}
code += "var ct=Date.now();\n";
if (addDebug()) code += 'print("Starting layer ' + layername + '");' + "\n";
var checkForLayerChange = false;
@ -732,14 +716,17 @@
if (addDebug()) code += 'print("Element condition is ' + condition + '");' + "\n";
code += "" + colorsetting;
code += (condition.length > 0 ? "if (" + condition + "){\n" : "");
if (wrapInTimeouts && (plane != 0 || forceUseOrigPlane)){
code += "p = p.then(()=>delay(0)).then(()=>{\n";
} else {
code += "p = p.then(()=>{\n";
}
if (addDebug()) code += 'print("Drawing element ' + elementIndex + ' with type ' + c.type + ' on plane ' + planeName + '");' + "\n";
code += "draw" + c.type + "(" + planeName + ", wr, wf.Collapsed[" + elementIndex + "].value);\n";
code += "});\n";
code += (condition.length > 0 ? "}\n" : "");
}
code += "drawingTime += Date.now() - ct;\n";
code += "});\n";
}
console.log("Current plane is", plane);
@ -759,7 +746,7 @@
var properties = faceJson.Properties;
faceJson = { Properties: properties, Collapsed: collapseTree(faceJson,{X:0,Y:0})};
console.log("After collapsing", faceJson);
precompiledJs = convertToCode(faceJson.Collapsed, properties, document.getElementById('timeoutwrap').checked);
precompiledJs = convertToCode(faceJson.Collapsed, properties, document.getElementById('timeoutwrap').checked, document.getElementById('forceOrigPlane').checked);
console.log("After precompiling", precompiledJs);
}
@ -1011,6 +998,10 @@
}
}
document.getElementById("timeoutwrap").addEventListener("click", function() {
document.getElementById("forceOrigPlane").disabled = !document.getElementById("timeoutwrap").checked;
});
document.getElementById("btnSave").addEventListener("click", function() {
var h = document.createElement('a');
h.href = 'data:text/json;charset=utf-8,' + encodeURI(JSON.stringify(resultJson));

View File

@ -2,7 +2,7 @@
"id": "imageclock",
"name": "Imageclock",
"shortName": "Imageclock",
"version": "0.09",
"version": "0.11",
"type": "clock",
"description": "BETA!!! File formats still subject to change --- This app is a highly customizable watchface. To use it, you need to select a watchface. You can build the watchfaces yourself without programming anything. All you need to do is write some json and create image files.",
"icon": "app.png",

View File

@ -1 +1,2 @@
0.01: New App.
0.01: New App.
0.02: Performance improvements.

View File

@ -55,18 +55,18 @@ var H = g.getHeight();
show: function() { dateMenu.items[0].emit("redraw"); },
hide: function () {}
},
{ name: "day",
get: () => ({ text: getDay(), img: null}),
show: function() { dateMenu.items[2].emit("redraw"); },
hide: function () {}
},
{ name: "date",
get: () => ({ text: getDate(), img: null}),
show: function() { dateMenu.items[1].emit("redraw"); },
hide: function () {}
},
{ name: "steps",
get: () => ({ text: Bangle.getHealthStatus("day").steps, img: null}),
show: function() { dateMenu.items[2].emit("redraw"); },
hide: function () {}
},
{ name: "battery",
get: () => ({ text: E.getBattery() + (Bangle.isCharging() ? "%++" : "%"), img: null}),
{ name: "week",
get: () => ({ text: weekOfYear(), img: null}),
show: function() { dateMenu.items[3].emit("redraw"); },
hide: function () {}
},
@ -130,37 +130,53 @@ function getDate(){
return twoD(date.getDate()) + "." + twoD(date.getMonth());
}
function getDay(){
var date = new Date();
return locale.dow(date, true);
}
/************************************************
* Draw
*/
function draw() {
queueDraw();
g.clear();
Bangle.drawWidgets();
drawMainScreen();
}
function weekOfYear() {
var date = new Date();
date.setHours(0, 0, 0, 0);
// Thursday in current week decides the year.
date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7);
// January 4 is always in week 1.
var week1 = new Date(date.getFullYear(), 0, 4);
// Adjust to Thursday in week 1 and count number of weeks from date to week1.
return 1 + Math.round(((date.getTime() - week1.getTime()) / 86400000
- 3 + (week1.getDay() + 6) % 7) / 7);
}
function drawMainScreen(){
g.setFontUbuntuMono();
g.setFontAlign(-1, -1);
/************************************************
* Draw
*/
function draw() {
queueDraw();
// Get menu item based on x
var menuItem = menu[settings.menuPosX];
var cmd = menuItem.name.slice(0,5).toLowerCase();
drawCmd(cmd);
g.setFontUbuntuMono();
g.setFontAlign(-1, -1);
// Draw menu items depending on our y value
drawMenuItems(menuItem);
g.clearRect(0,24,W,H);
// And draw the cursor
drawCursor();
}
drawMainScreen();
}
function drawMainScreen(){
// Get menu item based on x
var menuItem = menu[settings.menuPosX];
var cmd = menuItem.name.slice(0,5).toLowerCase();
drawCmd(cmd);
// Draw menu items depending on our y value
drawMenuItems(menuItem);
// And draw the cursor
drawCursor();
}
function drawMenuItems(menuItem) {
var start = parseInt(settings.menuPosY / 4) * 4;
@ -174,194 +190,197 @@ function drawMenuItems(menuItem) {
}
function drawCursor(){
g.setFontUbuntuMono();
g.setFontAlign(-1, -1);
g.setColor(g.theme.fg);
g.clearRect(0, 27 + 28, 15, H);
if(!Bangle.isLocked()){
g.drawString(">", -2, ((settings.menuPosY % 4) + 1) * 27 + 28);
}
}
function drawText(key, value, line){
g.setFontUbuntuMono();
var x = 15;
var y = line * 27 + 28;
g.setColor(g.theme.fg);
function drawText(key, value, line){
var x = 15;
var y = line * 27 + 28;
if(key){
key = (key.toLowerCase() + " ").slice(0, 4) + "|";
} else {
key = ""
g.setFontUbuntuMono();
g.setFontAlign(-1, -1);
g.setColor(g.theme.fg);
if(key){
key = (key.toLowerCase() + " ").slice(0, 4) + "|";
} else {
key = ""
}
value = String(value).replace("\n", " ");
g.drawString(key + value, x, y);
lock_input -= 1;
}
function drawCmd(cmd){
var c = 0;
var x = 10;
var y = 28;
g.setColor("#0f0");
g.drawString("bjs", x+c, y);
c += g.stringWidth("bjs");
g.setColor(g.theme.fg);
g.drawString(":", x+c, y);
c += g.stringWidth(":");
g.setColor("#0ff");
g.drawString("$ ", x+c, y);
c += g.stringWidth("$ ");
g.setColor(g.theme.fg);
g.drawString(cmd, x+c, y);
}
function twoD(str){
return ("0" + str).slice(-2)
}
/************************************************
* Listener
*/
// 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));
}
// Stop updates when LCD is off, restart when on
Bangle.on('lcdPower',on=>{
if (on) {
draw(); // draw immediately, queue redraw
} else { // stop draw timer
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
}
});
Bangle.on('lock', function(isLocked) {
drawCursor();
});
Bangle.on('charging',function(charging) {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
settings.menuPosX=0;
settings.menuPosY=0;
draw();
});
var lock_input = 0;
Bangle.on('touch', function(btn, e){
if(lock_input > 0){
return;
}
lock_input = 0;
var left = parseInt(g.getWidth() * 0.22);
var right = g.getWidth() - left;
var upper = parseInt(g.getHeight() * 0.22) + 20;
var lower = g.getHeight() - upper;
var is_upper = e.y < upper;
var is_lower = e.y > lower;
var is_left = e.x < left && !is_upper && !is_lower;
var is_right = e.x > right && !is_upper && !is_lower;
var is_center = !is_upper && !is_lower && !is_left && !is_right;
var oldYScreen = parseInt(settings.menuPosY/4);
if(is_lower){
if(settings.menuPosY >= menu[settings.menuPosX].items.length-1){
return;
}
value = String(value).replace("\n", " ");
g.drawString(key + value, x, y);
Bangle.buzz(40, 0.6);
settings.menuPosY++;
if(parseInt(settings.menuPosY/4) == oldYScreen){
drawCursor();
return;
}
}
lock_input -= 1;
}
function drawCmd(cmd){
var c = 0;
var x = 10;
var y = 28;
g.setColor("#0f0");
g.drawString("bjs", x+c, y);
c += g.stringWidth("bjs");
g.setColor(g.theme.fg);
g.drawString(":", x+c, y);
c += g.stringWidth(":");
g.setColor("#0ff");
g.drawString("~", x+c, y);
c += g.stringWidth("~");
g.setColor(g.theme.fg);
g.drawString("$", x+c, y);
c += g.stringWidth("$ ");
g.drawString(cmd, x+c, y);
}
function twoD(str){
return ("0" + str).slice(-2)
}
/************************************************
* Listener
*/
// 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));
}
// Stop updates when LCD is off, restart when on
Bangle.on('lcdPower',on=>{
if (on) {
draw(); // draw immediately, queue redraw
} else { // stop draw timer
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
}
});
Bangle.on('lock', function(isLocked) {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
draw();
});
Bangle.on('charging',function(charging) {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
draw();
});
var lock_input = 0;
Bangle.on('touch', function(btn, e){
if(lock_input > 0){
return;
}
lock_input = 0;
var left = parseInt(g.getWidth() * 0.22);
var right = g.getWidth() - left;
var upper = parseInt(g.getHeight() * 0.22) + 20;
var lower = g.getHeight() - upper;
var is_upper = e.y < upper;
var is_lower = e.y > lower;
var is_left = e.x < left && !is_upper && !is_lower;
var is_right = e.x > right && !is_upper && !is_lower;
var is_center = !is_upper && !is_lower && !is_left && !is_right;
var oldYScreen = parseInt(settings.menuPosY/4);
if(is_lower){
if(settings.menuPosY >= menu[settings.menuPosX].items.length-1){
return;
}
Bangle.buzz(40, 0.6);
settings.menuPosY++;
if(parseInt(settings.menuPosY/4) == oldYScreen){
drawCursor();
return;
}
}
if(is_upper){
if(e.y < 20){ // Reserved for widget clicks
return;
}
if(settings.menuPosY <= 0){
return;
}
Bangle.buzz(40, 0.6);
settings.menuPosY--;
settings.menuPosY = settings.menuPosY < 0 ? 0 : settings.menuPosY;
if(parseInt(settings.menuPosY/4) == oldYScreen){
drawCursor();
return;
}
}
if(is_right){
Bangle.buzz(40, 0.6);
settings.menuPosX = (settings.menuPosX+1) % menu.length;
settings.menuPosY = 0;
}
if(is_left){
Bangle.buzz(40, 0.6);
settings.menuPosY = 0;
settings.menuPosX = settings.menuPosX-1;
settings.menuPosX = settings.menuPosX < 0 ? menu.length-1 : settings.menuPosX;
}
if(is_center){
if(!canRunMenuItem()){
if(is_upper){
if(e.y < 20){ // Reserved for widget clicks
return;
}
runMenuItem();
}
draw();
});
if(settings.menuPosY <= 0){
return;
}
Bangle.buzz(40, 0.6);
settings.menuPosY--;
settings.menuPosY = settings.menuPosY < 0 ? 0 : settings.menuPosY;
E.on("kill", function(){
try{
storage.write(SETTINGS_FILE, settings);
} catch(ex){
// If this fails, we still kill the app...
}
});
if(parseInt(settings.menuPosY/4) == oldYScreen){
drawCursor();
return;
}
}
if(is_right){
Bangle.buzz(40, 0.6);
settings.menuPosX = (settings.menuPosX+1) % menu.length;
settings.menuPosY = 0;
}
if(is_left){
Bangle.buzz(40, 0.6);
settings.menuPosY = 0;
settings.menuPosX = settings.menuPosX-1;
settings.menuPosX = settings.menuPosX < 0 ? menu.length-1 : settings.menuPosX;
}
if(is_center){
if(!canRunMenuItem()){
return;
}
runMenuItem();
}
draw();
});
E.on("kill", function(){
try{
storage.write(SETTINGS_FILE, settings);
} catch(ex){
// If this fails, we still kill the app...
}
});
/************************************************
* Startup Clock
*/
/************************************************
* Startup Clock
*/
// Show launcher when middle button pressed
Bangle.setUI("clock");
// Show launcher when middle button pressed
Bangle.setUI("clock");
// Load and draw widgets
Bangle.loadWidgets();
Bangle.drawWidgets();
// Load and draw widgets
Bangle.loadWidgets();
// Draw first time
draw();
// Draw first time
draw();

View File

@ -1,7 +1,7 @@
{
"id": "linuxclock",
"name": "Linux Clock",
"version": "0.01",
"version": "0.02",
"description": "A Linux inspired clock.",
"readme": "README.md",
"icon": "app.png",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -1,2 +1,3 @@
0.01: Initial release
0.02: Removed accelerometer poll interval adjustment, fixed a few issues with detecting the current app
0.02: Removed accelerometer poll interval adjustment, fixed a few issues with detecting the current app
0.03: Fix a couple of silly mistakes

View File

@ -12,7 +12,7 @@ E.on("init", () => {
Storage.write("powersave.json", {
app: __FILE__
});
}else{
}else if(!("__FILE__" in global)){
Storage.write("powersave.json", {
app: null
});

View File

@ -1,7 +1,7 @@
{
"id": "powersave",
"name": "Power Save",
"version": "0.02",
"version": "0.03",
"description": "Halts foreground app execution while screen is off while still allowing background processes.",
"readme": "README.md",
"icon": "powersave.png",
@ -10,7 +10,7 @@
"supports": ["BANGLEJS2"],
"storage": [
{"name":"powersave.boot.js","url":"boot.js"},
{"name":"powersave.screen.js","url":"boot.js"}
{"name":"powersave.screen.js","url":"screen.js"}
],
"data": [
{"name": "powersave.json"}

10
apps/primetime/README.md Normal file
View File

@ -0,0 +1,10 @@
# App Name
Watchface that displays time and the prime factors of the "military time" (i.e. 21:05 => 2105, shows prime factors of 2105 which are 5 & 421). Displays "Prime Time!" if prime.
![image](https://user-images.githubusercontent.com/115424919/194777279-7f5e4d2a-f475-4099-beaf-38db5b460714.png)
## Creator
Adapted from simplestclock by [Eve Bury](https://www.github.com/eveeeon)

BIN
apps/primetime/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

View File

@ -0,0 +1,15 @@
{ "id": "primetime",
"name": "Prime Time Clock",
"version": "0.01",
"type": "clock",
"description": "A clock that tells you the primes of the time",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
"tags": "clock",
"supports": ["BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name":"primetime.app.js","url":"primetime.js"},
{"name":"primetime.img","url":"primetime-icon.js","evaluate":true}
]
}

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwgVVABVADJMBBf4L/Bf4LMgtQgIHCitAqoHBoEv+EHwALBv/S//4BYO//svwELoP//X/+gLB2E93+Ah9B9f+//QBYMVvv3C4XvvwLDl/0q+AgsB998qt4F4XgHYIXB/1+6ALC//93/4F4I7CI4QLBAIMLoF/6ABBBYNVqgBBgprCAIKz0qkAooLHgP8gXvvALH/EL7e4BY+tz/+vovH3PR1++L9YL/BYdVABQ="))

View File

@ -0,0 +1,89 @@
const h = g.getHeight();
const w = g.getWidth();
// creates a list of prime factors of n and outputs them as a string, if n is prime outputs "Prime Time!"
function primeFactors(n) {
const factors = [];
let divisor = 2;
while (n >= 2) {
if (n % divisor == 0) {
factors.push(divisor);
n = n / divisor;
} else {
divisor++;
}
}
if (factors.length === 1) {
return "Prime Time!";
}
else
return factors.toString();
}
// converts time HR:MIN to integer HRMIN e.g. 15:35 => 1535
function timeToInt(t) {
var arr = t.split(':');
var intTime = parseInt(arr[0])*100+parseInt(arr[1]);
return intTime;
}
function draw() {
var date = new Date();
var timeStr = require("locale").time(date,1);
var primeStr = primeFactors(timeToInt(timeStr));
g.reset();
g.setColor(0,0,0);
g.fillRect(Bangle.appRect);
g.setFont("6x8", w/30);
g.setFontAlign(0, 0);
g.setColor(100,100,100);
g.drawString(timeStr, w/2, h/2);
g.setFont("6x8", w/60);
g.drawString(primeStr, w/2, 3*h/4);
queueDraw();
}
// 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));
}
// Stop updates when LCD is off, restart when on
Bangle.on('lcdPower',on=>{
if (on) {
draw(); // draw immediately, queue redraw
} else { // stop draw timer
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
}
});
g.clear();
// Show launcher when middle button pressed
// Bangle.setUI("clock");
// use clockupdown as it tests for issue #1249
Bangle.setUI("clockupdown", btn=> {
draw();
});
// Load widgets
Bangle.loadWidgets();
Bangle.drawWidgets();
draw();

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

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

View File

@ -0,0 +1,44 @@
# Sensor tools
This allows to simulate sensor behaviour for development purposes
## Per Sensor settings:
enabled:
true or false
mode:
emulate: Completely craft events for this sensor
modify: Take existing events from real sensor and modify their data
name:
name of the emulation or modification mode
power:
emulate: Simulate Bangle._PWR changes, but do not call real power function
nop: Do nothing, ignore all power calls for this sensor but return true
passthrough: Just pass all power calls unmodified
on: Do not allow switching the sensor off, all calls are switching the real sensor on
### HRM
Modes: modify, emulate
Modification:
bpmtrippled: Multiply the bpm value of the original HRM values with 3
Emulation:
sin: Calculate bpm changes by using sin
### GPS
Modes: emulate
Emulation:
staticfix: static complete fix with all values
route: A square route starting in the SW corner and moving SW->NW->NO->SW...
routeFuzzy: Roughly the same square as route, but with 100m seqments with some variaton in course
nofix: All values NaN but time,sattelites,fix and fix == 0
changingfix: A fix with randomly changing values
### Compass
Modes: emulate
Emulation:
static: All values but heading are 1, heading == 0
rotate: All values but heading are 1, heading rotates 360°

351
apps/sensortools/boot.js Normal file
View File

@ -0,0 +1,351 @@
(function() {
var settings = Object.assign(
require('Storage').readJSON("sensortools.default.json", true) || {},
require('Storage').readJSON("sensortools.json", true) || {}
);
var log = function(text, param) {
var logline = new Date().toISOString() + " - " + "Sensortools - " + text;
if (param) logline += ": " + JSON.stringify(param);
print(logline);
};
if (settings.enabled) {
log("Enabled");
const POWER_DELAY = 10000;
var onEvents = [];
Bangle.sensortoolsOrigOn = Bangle.on;
Bangle.sensortoolsOrigEmit = Bangle.emit;
Bangle.sensortoolsOrigRemoveListener = Bangle.removeListener;
Bangle.on = function(name, callback) {
if (onEvents[name]) {
log("Redirecting listener for", name, "to", name + "_mod");
Bangle.sensortoolsOrigOn(name + "_mod", callback);
Bangle.sensortoolsOrigOn(name, (e) => {
log("Redirected event for", name, "to", name + "_mod");
Bangle.sensortoolsOrigEmit(name + "_mod", onEvents[name](e));
});
} else {
log("Pass through on call for", name, callback);
Bangle.sensortoolsOrigOn(name, callback);
}
};
Bangle.removeListener = function(name, callback) {
if (onEvents[name]) {
log("Removing augmented listener for", name, onEvents[name]);
Bangle.sensortoolsOrigRemoveListener(name + "_mod", callback);
} else {
log("Pass through remove listener for", name);
Bangle.sensortoolsOrigRemoveListener(name, callback);
}
};
Bangle.emit = function(name, event) {
if (onEvents[name]) {
log("Augmenting emit call for", name, onEvents[name]);
Bangle.sensortoolsOrigEmit(name + "_mod", event);
} else {
log("Pass through emit call for", name);
Bangle.sensortoolsOrigEmit(name, event);
}
};
var createPowerFunction = function(type, name, origPower) {
return function(isOn, app) {
if (type == "nop") {
return true;
}else if (type == "delay") {
setTimeout(() => {
origPower(isOn, app);
}, POWER_DELAY);
} else if (type == "on") {
origPower(1, "sensortools_force_on");
} else if (type == "passthrough"){
origPower(isOn, "app");
} else if (type == "emulate"){
if (!Bangle._PWR) Bangle._PWR={};
if (!Bangle._PWR[name]) Bangle._PWR[name] = [];
if (!app) app="?";
if (isOn) {
Bangle._PWR[name].push(app);
return true;
} else {
Bangle._PWR[name] = Bangle._PWR[name].filter((v)=>{return v == app;});
return false;
}
}
};
};
if (settings.hrm && settings.hrm.enabled) {
log("HRM", settings.hrm);
if (settings.hrm.power) {
log("HRM power");
Bangle.sensortoolsOrigSetHRMPower = Bangle.setHRMPower;
Bangle.setHRMPower = createPowerFunction(settings.hrm.power, "HRM", Bangle.sensortoolsOrigSetHRMPower);
}
if (settings.hrm.mode == "modify") {
if (settings.hrm.name == "bpmtrippled") {
onEvents.HRM = (e) => {
return {
bpm: e.bpm * 3
};
};
}
} else if (settings.hrm.mode == "emulate") {
if (settings.hrm.name == "sin") {
setInterval(() => {
Bangle.sensortoolsOrigEmit(60 + 3 * Math.sin(Date.now() / 10000));
}, 1000);
}
}
}
if (settings.gps && settings.gps.enabled) {
log("GPS", settings.gps);
let modGps = function(dataProvider) {
Bangle.getGPSFix = dataProvider;
setInterval(() => {
Bangle.sensortoolsOrigEmit("GPS", dataProvider());
}, 1000);
};
if (settings.gps.power) {
Bangle.sensortoolsOrigSetGPSPower = Bangle.setGPSPower;
Bangle.setGPSPower = createPowerFunction(settings.gps.power, "GPS", Bangle.sensortoolsOrigSetGPSPower);
}
if (settings.gps.mode == "emulate") {
function radians(a) {
return a*Math.PI/180;
}
function degrees(a) {
var d = a*180/Math.PI;
return (d+360)%360;
}
function bearing(a,b){
if (!a || !b || !a.lon || !a.lat || !b.lon || !b.lat) return Infinity;
var delta = radians(b.lon-a.lon);
var alat = radians(a.lat);
var blat = radians(b.lat);
var y = Math.sin(delta) * Math.cos(blat);
var x = Math.cos(alat)*Math.sin(blat) -
Math.sin(alat)*Math.cos(blat)*Math.cos(delta);
return Math.round(degrees(Math.atan2(y, x)));
}
function interpolate(a,b,progress){
return {
lat: a.lat * progress + b.lat * (1-progress),
lon: a.lon * progress + b.lon * (1-progress),
ele: a.ele * progress + b.ele * (1-progress)
}
}
function getSquareRoute(){
return [
{lat:"47.2577411",lon:"11.9927442",ele:2273},
{lat:"47.266761",lon:"11.9926673",ele:2166},
{lat:"47.2667605",lon:"12.0059511",ele:2245},
{lat:"47.2577516",lon:"12.0059925",ele:1994}
];
}
function getSquareRouteFuzzy(){
return [
{lat:"47.2578455",lon:"11.9929891",ele:2265},
{lat:"47.258592",lon:"11.9923341",ele:2256},
{lat:"47.2594506",lon:"11.9927412",ele:2230},
{lat:"47.2603323",lon:"11.9924949",ele:2219},
{lat:"47.2612056",lon:"11.9928175",ele:2199},
{lat:"47.2621002",lon:"11.9929817",ele:2182},
{lat:"47.2629025",lon:"11.9923915",ele:2189},
{lat:"47.2637828",lon:"11.9926486",ele:2180},
{lat:"47.2646733",lon:"11.9928167",ele:2191},
{lat:"47.2655617",lon:"11.9930357",ele:2185},
{lat:"47.2662862",lon:"11.992252",ele:2186},
{lat:"47.2669305",lon:"11.993173",ele:2166},
{lat:"47.266666",lon:"11.9944419",ele:2171},
{lat:"47.2667579",lon:"11.99576",ele:2194},
{lat:"47.2669409",lon:"11.9970579",ele:2207},
{lat:"47.2666562",lon:"11.9983128",ele:2212},
{lat:"47.2666027",lon:"11.9996335",ele:2262},
{lat:"47.2667245",lon:"12.0009395",ele:2278},
{lat:"47.2668457",lon:"12.002256",ele:2297},
{lat:"47.2666126",lon:"12.0035373",ele:2303},
{lat:"47.2664554",lon:"12.004841",ele:2251},
{lat:"47.2669461",lon:"12.005948",ele:2245},
{lat:"47.2660877",lon:"12.006323",ele:2195},
{lat:"47.2652729",lon:"12.0057552",ele:2163},
{lat:"47.2643926",lon:"12.0060123",ele:2131},
{lat:"47.2634978",lon:"12.0058302",ele:2095},
{lat:"47.2626129",lon:"12.0060759",ele:2066},
{lat:"47.2617325",lon:"12.0058188",ele:2037},
{lat:"47.2608668",lon:"12.0061784",ele:1993},
{lat:"47.2600155",lon:"12.0057392",ele:1967},
{lat:"47.2591203",lon:"12.0058233",ele:1949},
{lat:"47.2582307",lon:"12.0059718",ele:1972},
{lat:"47.2578014",lon:"12.004804",ele:2011},
{lat:"47.2577232",lon:"12.0034834",ele:2044},
{lat:"47.257745",lon:"12.0021656",ele:2061},
{lat:"47.2578682",lon:"12.0008597",ele:2065},
{lat:"47.2577082",lon:"11.9995526",ele:2071},
{lat:"47.2575917",lon:"11.9982348",ele:2102},
{lat:"47.2577401",lon:"11.996924",ele:2147},
{lat:"47.257715",lon:"11.9956061",ele:2197},
{lat:"47.2578996",lon:"11.9943081",ele:2228}
];
}
if (settings.gps.name == "staticfix") {
modGps(() => { return {
"lat": 52,
"lon": 8,
"alt": 100,
"speed": 10,
"course": 12,
"time": Date.now(),
"satellites": 7,
"fix": 1,
"hdop": 1
};});
} else if (settings.gps.name.includes("route")) {
let route;
let interpSteps;
if (settings.gps.name == "routeFuzzy"){
route = getSquareRouteFuzzy();
interpSteps = 5;
} else {
route = getSquareRoute();
interpSteps = 50;
}
let step = 0;
let routeIndex = 0;
modGps(() => {
let newIndex = (routeIndex + 1)%route.length;
let result = {
"speed": Math.random() * 3 + 2,
"time": Date.now(),
"satellites": Math.floor(Math.random()*5)+3,
"fix": 1,
"hdop": Math.floor(Math.random(30)+1)
};
let oldPos = route[routeIndex];
if (step != 0){
oldPos = interpolate(route[routeIndex], route[newIndex], E.clip(0,1,step/interpSteps));
}
let newPos = route[newIndex];
if (step < interpSteps - 1){
newPos = interpolate(route[routeIndex], route[newIndex], E.clip(0,1,(step+1)%interpSteps/interpSteps));
}
if (step == interpSteps - 1){
let followingIndex = (routeIndex + 2)%route.length;
newPos = interpolate(route[newIndex], route[followingIndex], E.clip(0,1,1/interpSteps));
}
result.lat = oldPos.lat;
result.lon = oldPos.lon;
result.alt = oldPos.ele;
result.course = bearing(oldPos,newPos);
step++;
if (step == interpSteps){
routeIndex = (routeIndex + 1) % route.length;
step = 0;
}
return result;
});
} else if (settings.gps.name == "nofix") {
modGps(() => { return {
"lat": NaN,
"lon": NaN,
"alt": NaN,
"speed": NaN,
"course": NaN,
"time": Date.now(),
"satellites": 2,
"fix": 0,
"hdop": NaN
};});
} else if (settings.gps.name == "changingfix") {
let currentSpeed=1;
let currentLat=20;
let currentLon=10;
let currentCourse=10;
let currentAlt=-100;
let currentSats=5;
modGps(() => {
currentLat += 0.1;
if (currentLat > 50) currentLat = 20;
currentLon += 0.1;
if (currentLon > 20) currentLon = 10;
currentSpeed *= 10;
if (currentSpeed > 1000) currentSpeed = 1;
currentCourse += 12;
if (currentCourse > 360) currentCourse -= 360;
currentSats += 1;
if (currentSats > 10) currentSats = 5;
currentAlt *= 10;
if (currentAlt > 1000) currentAlt = -100;
return {
"lat": currentLat,
"lon": currentLon,
"alt": currentAlt,
"speed": currentSpeed,
"course": currentCourse,
"time": Date.now(),
"satellites": currentSats,
"fix": 1,
"hdop": 1
};});
}
}
}
if (settings.mag && settings.mag.enabled) {
log("MAG", settings.mag);
let modMag = function(data) {
setInterval(() => {
Bangle.getCompass = data;
Bangle.sensortoolsOrigEmit("mag", data());
}, 100);
};
if (settings.mag.power) {
Bangle.sensortoolsOrigSetCompassPower = Bangle.setCompassPower;
Bangle.setCompassPower = createPowerFunction(settings.mag.power, "Compass", Bangle.sensortoolsOrigSetCompassPower);
}
if (settings.mag.mode == "emulate") {
if (settings.mag.name == "static") {
modMag(()=>{return {
x: 1,
y: 1,
z: 1,
dx: 1,
dy: 1,
dz: 1,
heading: 0
};});
} else if (settings.mag.name == "rotate"){
let last = 0;
modMag(()=>{return {
x: 1,
y: 1,
z: 1,
dx: 1,
dy: 1,
dz: 1,
heading: last = (last+1)%360,
};});
}
}
}
}
})();

View File

@ -0,0 +1,18 @@
{
"enabled": false,
"mag": {
"enabled": false,
"mode": "emulate",
"name": "static"
},
"hrm": {
"enabled": false,
"mode": "modify",
"name": "bpmtrippled"
},
"gps": {
"enabled": false,
"mode": "emulate",
"name": "changingfix"
}
}

BIN
apps/sensortools/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 557 B

View File

@ -0,0 +1,17 @@
{
"id": "sensortools",
"name": "Sensor tools",
"shortName": "Sensor tools",
"version": "0.01",
"description": "Tools for testing and debugging apps that use sensor input",
"icon": "icon.png",
"type": "bootloader",
"tags": "tool,boot,debug",
"supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name":"sensortools.0.boot.js","url":"boot.js"},
{"name":"sensortools.settings.js","url":"settings.js"},
{"name":"sensortools.default.json","url":"default.json"}
]
}

View File

@ -0,0 +1,99 @@
(function(back) {
function writeSettings(key, value) {
var s = require('Storage').readJSON(FILE, true) || {};
s[key] = value;
require('Storage').writeJSON(FILE, s);
readSettings();
}
function writeSettingsParent(parent, key, value) {
var s = require('Storage').readJSON(FILE, true) || {};
if (!s[parent]) s[parent] = {};
s[parent][key] = value;
require('Storage').writeJSON(FILE, s);
readSettings();
}
function readSettings(){
settings = Object.assign(
require('Storage').readJSON("sensortools.default.json", true) || {},
require('Storage').readJSON(FILE, true) || {}
);
}
var FILE="sensortools.json";
var settings;
readSettings();
let modes = ["nop", "emulate", "modify"];
let modesPower = ["nop", "emulate", "passthrough", "delay", "on"];
function showSubMenu(name,key,typesEmulate,typesModify){
var menu = {
'': { 'title': name,
back: ()=>{E.showMenu(buildMainMenu());}},
'Enabled': {
value: !!settings[key].enabled,
onchange: v => {
writeSettingsParent(key, "enabled",v);
}
},
'Mode': {
value: modes.indexOf(settings[key].mode||"nop"),
min: 0, max: modes.length-1,
format: v => { return modes[v]; },
onchange: v => {
writeSettingsParent(key,"mode",modes[v]);
showSubMenu(name,key,typesEmulate,typesModify);
}
},
'Name': {},
'Power': {
value: modesPower.indexOf(settings[key].power||"nop"),
min: 0, max: modesPower.length-1,
format: v => { return modesPower[v]; },
onchange: v => {
writeSettingsParent(key,"power",modesPower[v]);
}
},
};
if (settings[key].mode != "nop"){
let types = typesEmulate;
if (settings[key].mode == "modify") types = typesModify;
menu.Name = {
value: types.indexOf(settings[key].name||"static"),
min: 0, max: types.length-1,
format: v => { return types[v]; },
onchange: v => {
writeSettingsParent(key,"name",types[v]);
}
};
} else {
delete menu.Name;
}
E.showMenu(menu);
}
function buildMainMenu(){
var mainmenu = {
'': { 'title': 'Sensor tools' },
'< Back': back,
'Enabled': {
value: !!settings.enabled,
onchange: v => {
writeSettings("enabled",v);
},
},
'GPS': ()=>{showSubMenu("GPS","gps",["nop", "staticfix", "nofix", "changingfix", "route", "routeFuzzy"],[]);},
'Compass': ()=>{showSubMenu("Compass","mag",["nop", "static", "rotate"],[]);},
'HRM': ()=>{showSubMenu("HRM","hrm",["nop", "static"],["bpmtrippled"],["sin"]);}
};
return mainmenu;
}
E.showMenu(buildMainMenu());
});

View File

@ -1,3 +1,4 @@
0.01: Release
0.02: Rewrite with new interface
0.03: Added clock infos to expose timer functionality to clocks.
0.03: Added clock infos to expose timer functionality to clocks.
0.04: Improvements of clock infos.

View File

@ -69,7 +69,7 @@
img: img,
items: [
{
name: "Timer",
name: null,
get: () => ({ text: getAlarmMinutesText() + (isAlarmEnabled() ? " min" : ""), img: null}),
show: function() { smpltmrItems.items[0].emit("redraw"); },
hide: function () {},
@ -78,17 +78,18 @@
]
};
var offsets = [+1,+5,-1,-5];
var offsets = [+5,-5];
offsets.forEach((o, i) => {
smpltmrItems.items = smpltmrItems.items.concat({
name: String(o),
get: () => ({ text: getAlarmMinutesText() + " (" + (o > 0 ? "+" : "") + o + ")", img: null}),
name: null,
get: () => ({ text: (o > 0 ? "+" : "") + o + " min.", img: null}),
show: function() { smpltmrItems.items[i+1].emit("redraw"); },
hide: function () {},
run: function() {
if(o > 0) increaseAlarm(o);
else decreaseAlarm(Math.abs(o));
this.show();
return true;
}
});
});

View File

@ -2,7 +2,7 @@
"id": "smpltmr",
"name": "Simple Timer",
"shortName": "Simple Timer",
"version": "0.03",
"version": "0.04",
"description": "A very simple app to start a timer.",
"icon": "app.png",
"tags": "tool,alarm,timer",

View File

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

View File

@ -0,0 +1,9 @@
# Close Button Launcher
Adds a ![X](preview.png) button to close the current app and go back to the launcher.
(Widget is not visible on the clock screen)
Copied from widclose by @rigrig and slightly modified.
![Light theme screenshot](screenshot_light.png)
![Dark theme screenshot](screenshot_dark.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,15 @@
{
"id": "widcloselaunch",
"name": "Close Button to launcher",
"version": "0.01",
"description": "A button to close the current app and go to launcher",
"readme": "README.md",
"icon": "icon.png",
"type": "widget",
"tags": "widget,tools",
"supports": ["BANGLEJS2"],
"screenshots": [{"url":"screenshot_light.png"},{"url":"screenshot_dark.png"}],
"storage": [
{"name":"widcloselaunch.wid.js","url":"widget.js"}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,14 @@
if (!Bangle.CLOCK) WIDGETS.close = {
area: "tr", width: 24, sortorder: 10, // we want the right-most spot please
draw: function() {
Bangle.removeListener("touch", this.touch);
Bangle.on("touch", this.touch);
g.reset().setColor("#f00").drawImage(atob( // hardcoded red to match setUI back button
// b/w version of preview.png, 24x24
"GBgBABgAAf+AB//gD//wH//4P//8P//8fn5+fjx+fxj+f4H+/8P//8P/f4H+fxj+fjx+fn5+P//8P//8H//4D//wB//gAf+AABgA"
), this.x, this.y);
}, touch: function(_, c) {
const w = WIDGETS.close;
if (w && c.x>=w.x && c.x<=w.x+24 && c.y>=w.y && c.y<=w.y+24) Bangle.showLauncher();
}
};

View File

@ -97,10 +97,14 @@ exports.load = function() {
// In case there exists already a menu object b with the same name as the next
// object a, we append the items. Otherwise we add the new object a to the list.
require("Storage").list(/clkinfo.js$/).forEach(fn => {
var a = eval(require("Storage").read(fn))();
var b = menu.find(x => x.name === a.name)
if(b) b.items = b.items.concat(a.items);
else menu = menu.concat(a);
try{
var a = eval(require("Storage").read(fn))();
var b = menu.find(x => x.name === a.name)
if(b) b.items = b.items.concat(a.items);
else menu = menu.concat(a);
} catch(e){
console.log("Could not load clock info.")
}
});
// return it all!