1
0
Fork 0

Merge branch 'espruino:master' into master

master
only-meeps 2024-08-02 17:12:55 -04:00 committed by GitHub
commit e17250b3b2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
80 changed files with 3195 additions and 237 deletions

1
apps/beeptest/ChangeLog Normal file
View File

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

27
apps/beeptest/README.md Normal file
View File

@ -0,0 +1,27 @@
# App Name
Beep Test
## Usage
Mark out a 20m space
Click the side button to start the test
Shuttle run between your markers when the watch buzzes
Push the button when you need to stop
## Features
Buzzing on each shuttle run
Results page with vO2max and total distance covered.
## Controls
Side button starts, stops and resets the app.
## Requests
bb0x88 on giuthub
## Creator
Blade

View File

@ -0,0 +1,2 @@
require("heatshrink").decompress(atob("mEw4UA///+f8lky6f8HFmqBRMK1WgBAtUBYUABYtVqtAgEoAIQACioLBqALHBQIABBZMFEgIjHgEBqtUHY4aDKZA+CoBrIBYJJBBZJuCAA3VBYkC1QABGoJhDBYxTBBYUFEQoLDoEVSgIADO4ILCUASdGqtRGIYLFKoY7CIwdUEwJtBBYY6CqADBFwoLDDYIuFIwQUBigLITJQLFHYKNEHAgLGXw6NDBZbKHTIYLLKg6lDBY4KDEY5EIIwahFHQoKIBYIrHIwYLLuALJHRTcHAAjcGAEwA=="))

274
apps/beeptest/beeptest.js Normal file
View File

@ -0,0 +1,274 @@
var Layout = require("Layout");
// Beep Test Data
const BEET_TEST_DATA = [
{ shuttles: 7, timePerShuttle: 9.0, totalTime: 63.0, distancePerLevel: 140 },
{ shuttles: 8, timePerShuttle: 8.0, totalTime: 64.0, distancePerLevel: 160 },
{ shuttles: 8, timePerShuttle: 7.58, totalTime: 60.6, distancePerLevel: 160 },
{ shuttles: 9, timePerShuttle: 7.2, totalTime: 64.8, distancePerLevel: 180 },
{ shuttles: 9, timePerShuttle: 6.86, totalTime: 61.7, distancePerLevel: 180 },
{
shuttles: 10,
timePerShuttle: 6.55,
totalTime: 65.5,
distancePerLevel: 200,
},
{
shuttles: 10,
timePerShuttle: 6.26,
totalTime: 62.6,
distancePerLevel: 200,
},
{ shuttles: 11, timePerShuttle: 6.0, totalTime: 66.0, distancePerLevel: 220 },
{
shuttles: 11,
timePerShuttle: 5.76,
totalTime: 63.4,
distancePerLevel: 220,
},
{
shuttles: 11,
timePerShuttle: 5.54,
totalTime: 60.9,
distancePerLevel: 220,
},
{
shuttles: 12,
timePerShuttle: 5.33,
totalTime: 64.0,
distancePerLevel: 240,
},
{
shuttles: 12,
timePerShuttle: 5.14,
totalTime: 61.7,
distancePerLevel: 240,
},
{
shuttles: 13,
timePerShuttle: 4.97,
totalTime: 64.6,
distancePerLevel: 260,
},
{ shuttles: 13, timePerShuttle: 4.8, totalTime: 62.4, distancePerLevel: 260 },
{
shuttles: 13,
timePerShuttle: 4.65,
totalTime: 60.4,
distancePerLevel: 260,
},
{ shuttles: 14, timePerShuttle: 4.5, totalTime: 63.0, distancePerLevel: 280 },
{
shuttles: 14,
timePerShuttle: 4.36,
totalTime: 61.1,
distancePerLevel: 280,
},
{
shuttles: 15,
timePerShuttle: 4.24,
totalTime: 63.5,
distancePerLevel: 300,
},
{
shuttles: 15,
timePerShuttle: 4.11,
totalTime: 61.7,
distancePerLevel: 300,
},
{ shuttles: 16, timePerShuttle: 4.0, totalTime: 64.0, distancePerLevel: 320 },
{
shuttles: 16,
timePerShuttle: 3.89,
totalTime: 62.3,
distancePerLevel: 320,
},
];
// VO2max Data
const VO2MAX_DATA = [
{ level: 1, vo2max: 16.7 },
{ level: 2, vo2max: 23.0 },
{ level: 3, vo2max: 26.2 },
{ level: 4, vo2max: 29.3 },
{ level: 5, vo2max: 32.5 },
{ level: 6, vo2max: 35.7 },
{ level: 7, vo2max: 38.8 },
{ level: 8, vo2max: 42.0 },
{ level: 9, vo2max: 45.1 },
{ level: 10, vo2max: 48.3 },
{ level: 11, vo2max: 51.5 },
{ level: 12, vo2max: 54.6 },
{ level: 13, vo2max: 57.8 },
{ level: 14, vo2max: 60.9 },
{ level: 15, vo2max: 64.1 },
{ level: 16, vo2max: 67.3 },
{ level: 17, vo2max: 70.4 },
{ level: 18, vo2max: 73.6 },
{ level: 19, vo2max: 76.7 },
{ level: 20, vo2max: 79.9 },
{ level: 21, vo2max: 83.0 },
];
let currentLevel = 0;
let currentShuttle = 0;
let timeRemaining = 0;
let intervalId;
let beepTestLayout;
let testState = "start"; // 'start' | 'running' | 'result'
function initBeepTestLayout() {
beepTestLayout = new Layout(
{
type: "v",
c: [
{ type: "txt", font: "30%", pad: 0, label: "Start Test", id: "status" },
{ type: "txt", font: "15%", pad: 0, label: "", id: "level" },
{ type: "txt", font: "10%", pad: 0, label: "", id: "vo2max" }, // Smaller font for VO2max
{ type: "txt", font: "10%", pad: 0, label: "", id: "distance" }, // Smaller font for Distance
],
},
{
btns: [
{
label: "Start/Stop",
cb: (l) => {
if (testState === "start") {
startTest();
} else if (testState === "running") {
stopTest();
} else {
showStartScreen();
}
},
},
],
},
);
}
function showStartScreen() {
testState = "start";
g.clear();
beepTestLayout.clear(beepTestLayout.status);
beepTestLayout.status.label = "Start\nTest";
beepTestLayout.clear(beepTestLayout.level);
beepTestLayout.level.label = "";
beepTestLayout.clear(beepTestLayout.vo2max); // Clear VO2max text
beepTestLayout.vo2max.label = "";
beepTestLayout.clear(beepTestLayout.distance); // Clear Distance text
beepTestLayout.distance.label = "";
beepTestLayout.render();
}
function startTest() {
testState = "running";
currentLevel = 0;
currentShuttle = 0;
Bangle.buzz(2000); // Buzz for 2 seconds at the start of the test
runLevel();
}
function runLevel() {
if (currentLevel >= BEET_TEST_DATA.length) {
stopTest();
return;
}
const levelData = BEET_TEST_DATA[currentLevel];
timeRemaining = levelData.timePerShuttle * 1000; // Convert to milliseconds
updateDisplay();
if (intervalId) clearInterval(intervalId);
intervalId = setInterval(() => {
if (timeRemaining <= 0) {
currentShuttle++;
Bangle.buzz(100); // Short buzz after each shuttle
if (currentShuttle >= levelData.shuttles) {
// Buzz longer or twice at the end of each level
Bangle.buzz(1000); // Buzz for 1 second at level end
setTimeout(() => Bangle.buzz(1000), 500); // Buzz again after 0.5 seconds
currentLevel++;
currentShuttle = 0;
runLevel();
return;
}
timeRemaining = levelData.timePerShuttle * 1000; // Reset to original time for the next shuttle
}
updateDisplay();
timeRemaining -= 100; // Decrement time by 100 milliseconds
}, 100); // Update every 100 milliseconds
}
function updateDisplay() {
g.clear(); // Clear the entire screen
beepTestLayout.status.label = formatTime(timeRemaining);
beepTestLayout.level.label = `Level: ${currentLevel + 1}.${currentShuttle + 1}`;
beepTestLayout.render();
}
function stopTest() {
g.clear(); // Clear the entire screen
testState = "result";
clearInterval(intervalId);
// Determine previous level and shuttle
let prevLevel = currentLevel;
let prevShuttle = currentShuttle;
if (prevShuttle === 0) {
if (prevLevel > 0) {
prevLevel--;
prevShuttle = BEET_TEST_DATA[prevLevel].shuttles - 1;
} else {
prevShuttle = 0;
}
} else {
prevShuttle--;
}
// Determine VO2max and total distance
const vo2max = getVO2max(prevLevel + 1);
const totalDistance = calculateTotalDistance(prevLevel + 1);
beepTestLayout.clear(beepTestLayout.status);
beepTestLayout.status.label = "Result";
beepTestLayout.clear(beepTestLayout.level);
beepTestLayout.level.label = `Level: ${prevLevel + 1}.${prevShuttle + 1}`;
beepTestLayout.clear(beepTestLayout.vo2max);
beepTestLayout.vo2max.label = `VO2max: ${vo2max}`;
beepTestLayout.clear(beepTestLayout.distance);
beepTestLayout.distance.label = `Distance: ${totalDistance} m`;
beepTestLayout.render();
}
function getVO2max(level) {
const result = VO2MAX_DATA.find((item) => item.level === level);
return result ? result.vo2max : "N/A";
}
function calculateTotalDistance(level) {
// Calculate the total number of shuttles completed
let totalShuttles = 0;
for (let i = 0; i < level - 1; i++) {
totalShuttles += BEET_TEST_DATA[i].shuttles;
}
const levelData = BEET_TEST_DATA[level - 1];
totalShuttles += levelData.shuttles; // Add the shuttles completed in the current level
const distancePerShuttle = 20; // Distance per shuttle in meters
return totalShuttles * distancePerShuttle; // Total distance
}
function formatTime(milliseconds) {
let seconds = Math.floor(milliseconds / 1000);
let tenths = Math.floor((milliseconds % 1000) / 100); // Get tenths of a second
return (seconds < 10 ? "" : "") + seconds + "." + tenths; // Display only the tenths digit
}
// Initialize the app
Bangle.setLCDPower(1); // Keep the watch LCD lit up
initBeepTestLayout();
showStartScreen();

BIN
apps/beeptest/beeptest.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,15 @@
{
"id": "beeptest",
"name": "Beep Test",
"shortName": "Beep Test",
"version": "0.01",
"description": "Aerobic fitness test created by Léger & Lambert",
"icon": "beeptest.png",
"tags": "Health",
"supports": ["BANGLEJS2"],
"readme": "README.md",
"storage": [
{ "name": "beeptest.app.js", "url": "beeptest.js" },
{ "name": "beeptest.img", "url": "app-icon.js", "evaluate": true }
]
}

View File

@ -11,3 +11,4 @@
0.11: Minor code improvements
0.12: Added setting to change Battery estimate to hours
0.13: Fixed Battery estimate Default to percentage and improved setting string
0.14: Use `power_usage` module

View File

@ -116,7 +116,7 @@ function updateSunRiseSunSet(now, lat, lon, line){
function batteryString(){
let stringToInsert;
if (settings.batt_hours) {
var batt_usage = 200000/E.getPowerUsage().total;
var batt_usage = require("power_usage").get().hrsLeft;
let rounded;
if (batt_usage > 24) {
var days = Math.floor(batt_usage/24);

View File

@ -1,6 +1,6 @@
{ "id": "daisy",
"name": "Daisy",
"version": "0.13",
"version": "0.14",
"dependencies": {"mylocation":"app"},
"description": "A beautiful digital clock with large ring guage, idle timer and a cyclic information line that includes, day, date, steps, battery, sunrise and sunset times",
"icon": "app.png",

1
apps/dedreckon/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: attempt to import

20
apps/dedreckon/README.md Normal file
View File

@ -0,0 +1,20 @@
# Ded Reckon
Dead Reckoning using compass and step counter.
This allows logging track using "dead reckoning" -- that's logging
angles from compass and distances from step counter. You need to mark
turns, and point watch to direction of the turn. Simultaneously, it
tries to log positions using GPS. You can use it to calibrate your
step length by comparing GPS and step counter data. It can also get
pretty accurate recording of track walked in right circumstances.
Tap bottom part of the screen to select display (text or map for
now). Point watch to new direction, then tap top left part of screen
to indicate a turn.
Map shows blue line for track from dead reckonging, and green line for
track from GPS.
You probably want magnav installed (and calibrated) for useful
results, as it provides library with better compass.

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwhHXAH4A/AH4A/AFsAFtoADF1wwqF4wwhEI5goGGIjFYN4wFF1KbHGUolIMc4lGSdIwJd9DstAH7FrBywwgad4veDwojJBIIvcFwIACGBYICGDYvEGBYvdFwqyLL8i+LF7oxFRxgveGAQ0EF5IwfMY4vpL5AFLAEYv/F8owoE44vrAY4vmAQIEEF85dGGE0AE4gvoFwpmHd0oINAH4A/AH4AvA"))

BIN
apps/dedreckon/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,442 @@
/* Ded Reckon */
/* eslint-disable no-unused-vars */
/* fmt library v0.1.3 */
let fmt = {
icon_alt : "\0\x08\x1a\1\x00\x00\x00\x20\x30\x78\x7C\xFE\xFF\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\x00\x00\x00\x00\x00\x00\x00",
icon_m : "\0\x08\x1a\1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\x00\x00\x00\x00\x00\x00\x00",
icon_km : "\0\x08\x1a\1\xC3\xC6\xCC\xD8\xF0\xD8\xCC\xC6\xC3\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\x00\x00\x00\x00\x00\x00\x00",
icon_kph : "\0\x08\x1a\1\xC3\xC6\xCC\xD8\xF0\xD8\xCC\xC6\xC3\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\xFF\x00\xC3\xC3\xFF\xC3\xC3",
icon_c : "\0\x08\x1a\1\x00\x00\x60\x90\x90\x60\x00\x7F\xFF\xC0\xC0\xC0\xC0\xC0\xFF\x7F\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
/* 0 .. DD.ddddd
1 .. DD MM.mmm'
2 .. DD MM'ss"
*/
geo_mode : 1,
init: function() {},
fmtDist: function(km) {
if (km >= 1.0) return km.toFixed(1) + this.icon_km;
return (km*1000).toFixed(0) + this.icon_m;
},
fmtSteps: function(n) { return this.fmtDist(0.001 * 0.719 * n); },
fmtAlt: function(m) { return m.toFixed(0) + this.icon_alt; },
draw_dot : 1,
add0: function(i) {
if (i > 9) {
return ""+i;
} else {
return "0"+i;
}
},
fmtTOD: function(now) {
this.draw_dot = !this.draw_dot;
let dot = ":";
if (!this.draw_dot)
dot = ".";
return now.getHours() + dot + this.add0(now.getMinutes());
},
fmtNow: function() { return this.fmtTOD(new Date()); },
fmtTimeDiff: function(d) {
if (d < 180)
return ""+d.toFixed(0);
d = d/60;
return ""+d.toFixed(0)+"m";
},
fmtAngle: function(x) {
switch (this.geo_mode) {
case 0:
return "" + x;
case 1: {
let d = Math.floor(x);
let m = x - d;
m = m*60;
return "" + d + " " + m.toFixed(3) + "'";
}
case 2: {
let d = Math.floor(x);
let m = x - d;
m = m*60;
let mf = Math.floor(m);
let s = m - mf;
s = s*60;
return "" + d + " " + mf + "'" + s.toFixed(0) + '"';
}
}
return "bad mode?";
},
fmtPos: function(pos) {
let x = pos.lat;
let c = "N";
if (x<0) {
c = "S";
x = -x;
}
let s = c+this.fmtAngle(x) + "\n";
c = "E";
if (x<0) {
c = "W";
x = -x;
}
return s + c + this.fmtAngle(x);
},
fmtFix: function(fix, t) {
if (fix && fix.fix && fix.lat) {
return this.fmtSpeed(fix.speed) + " " +
this.fmtAlt(fix.alt);
} else {
return "N/FIX " + this.fmtTimeDiff(t);
}
},
fmtSpeed: function(kph) {
return kph.toFixed(1) + this.icon_kph;
},
};
/* gps library v0.1.1 */
let gps = {
emulator: -1,
init: function(x) {
this.emulator = (process.env.BOARD=="EMSCRIPTEN"
|| process.env.BOARD=="EMSCRIPTEN2")?1:0;
},
state: {},
on_gps: function(f) {
let fix = this.getGPSFix();
f(fix);
/*
"lat": number, // Latitude in degrees
"lon": number, // Longitude in degrees
"alt": number, // altitude in M
"speed": number, // Speed in kph
"course": number, // Course in degrees
"time": Date, // Current Time (or undefined if not known)
"satellites": 7, // Number of satellites
"fix": 1 // NMEA Fix state - 0 is no fix
"hdop": number, // Horizontal Dilution of Precision
*/
this.state.timeout = setTimeout(this.on_gps, 1000, f);
},
off_gps: function() {
clearTimeout(this.state.timeout);
},
getGPSFix: function() {
if (!this.emulator)
return Bangle.getGPSFix();
let fix = {};
fix.fix = 1;
fix.lat = 50;
fix.lon = 14-(getTime()-this.gps_start) / 1000; /* Go West! */
fix.alt = 200;
fix.speed = 5;
fix.course = 30;
fix.time = Date();
fix.satellites = 5;
fix.hdop = 12;
return fix;
},
gps_start : -1,
start_gps: function() {
Bangle.setGPSPower(1, "libgps");
this.gps_start = getTime();
},
stop_gps: function() {
Bangle.setGPSPower(0, "libgps");
},
};
/* ui library 0.1 */
let ui = {
display: 0,
numScreens: 2,
drawMsg: function(msg) {
g.reset().setFont("Vector", 35)
.setColor(1,1,1)
.fillRect(0, this.wi, 176, 176)
.setColor(0,0,0)
.drawString(msg, 5, 30);
},
drawBusy: function() {
this.drawMsg("\n.oO busy");
},
nextScreen: function() {
print("nextS");
this.display = this.display + 1;
if (this.display == this.numScreens)
this.display = 0;
this.drawBusy();
},
prevScreen: function() {
print("prevS");
this.display = this.display - 1;
if (this.display < 0)
this.display = this.numScreens - 1;
this.drawBusy();
},
onSwipe: function(dir) {
this.nextScreen();
},
h: 176,
w: 176,
wi: 32,
last_b: 0,
touchHandler: function(d) {
let x = Math.floor(d.x);
let y = Math.floor(d.y);
if (d.b != 1 || this.last_b != 0) {
this.last_b = d.b;
return;
}
print("touch", x, y, this.h, this.w);
/*
if ((x<this.h/2) && (y<this.w/2)) {
}
if ((x>this.h/2) && (y<this.w/2)) {
}
*/
if ((x<this.h/2) && (y>this.w/2)) {
print("prev");
this.prevScreen();
}
if ((x>this.h/2) && (y>this.w/2)) {
print("next");
this.nextScreen();
}
},
init: function() {
}
};
var last_steps = Bangle.getStepCount(), last_time = getTime(), speed = 0, step_phase = 0;
var mpstep = 0.719 * 1.15;
function updateSteps() {
if (step_phase ++ > 9) {
step_phase =0;
let steps = Bangle.getStepCount();
let time = getTime();
speed = 3.6 * mpstep * ((steps-last_steps) / (time-last_time));
last_steps = steps;
last_time = time;
}
return "" + fmt.fmtSpeed(speed) + " " + step_phase + "\n" + fmt.fmtDist(log_dist/1000) + " " + fmt.fmtDist(log_last/1000);
}
/* compensated compass */
var CALIBDATA = require("Storage").readJSON("magnav.json",1)||null;
const tiltfixread = require("magnav").tiltfixread;
var heading;
var cancel_gps = false;
function drawStats() {
let fix = gps.getGPSFix();
let msg = fmt.fmtFix(fix, getTime() - gps.gps_start);
msg += "\n" + fmt.fmtDist(gps_dist/1000) + " " + fmt.fmtDist(gps_last/1000) + "\n" + updateSteps();
let c = Bangle.getCompass();
if (c) msg += "\n" + c.heading.toFixed(0) + "/" + heading.toFixed(0) + "deg " + log.length + "\n";
g.reset().clear().setFont("Vector", 31)
.setColor(1,1,1)
.fillRect(0, 24, 176, 100)
.setColor(0,0,0)
.drawString(msg, 3, 25);
}
function updateGps() {
if (cancel_gps)
return;
heading = tiltfixread(CALIBDATA.offset,CALIBDATA.scale);
if (ui.display == 0) {
setTimeout(updateGps, 1000);
drawLog();
drawStats();
}
if (ui.display == 1) {
setTimeout(updateGps, 1000);
drawLog();
}
}
function stopGps() {
cancel_gps=true;
gps.stop_gps();
}
var log = [], log_dist = 0, gps_dist = 0;
var log_last = 0, gps_last = 0;
function logEntry() {
let e = {};
e.time = getTime();
e.fix = gps.getGPSFix();
e.steps = Bangle.getStepCount();
if (0) {
let c = Bangle.getCompass();
if (c)
e.dir = c.heading;
else
e.dir = -1;
} else {
e.dir = heading;
}
return e;
}
function onTurn() {
let e = logEntry();
log.push(e);
}
function radians(a) { return a*Math.PI/180; }
function degrees(a) { return a*180/Math.PI; }
// distance between 2 lat and lons, in meters, Mean Earth Radius = 6371km
// https://www.movable-type.co.uk/scripts/latlong.html
// (Equirectangular approximation)
function calcDistance(a,b) {
var x = radians(b.lon-a.lon) * Math.cos(radians((a.lat+b.lat)/2));
var y = radians(b.lat-a.lat);
return Math.sqrt(x*x + y*y) * 6371000;
}
var dn, de;
function initConv(fix) {
let n = { lat: fix.lat+1, lon: fix.lon };
let e = { lat: fix.lat, lon: fix.lon+1 };
dn = calcDistance(fix, n);
de = calcDistance(fix, e);
print("conversion is ", dn, 108000, de, 50000);
}
function toM(start, fix) {
return { x: (fix.lon - start.lon) * de, y: (fix.lat - start.lat) * dn };
}
var mpp = 4;
function toPix(q) {
let p = { x: q.x, y: q.y };
p.x /= mpp; /* 10 m / pix */
p.y /= -mpp;
p.x += 85;
p.y += 85;
return p;
}
function drawLog() {
let here = logEntry();
if (!here.fix.lat) {
here.fix.lat = 50;
here.fix.lon = 14;
}
initConv(here.fix);
log.push(here);
let l = log;
log_dist = 0;
log_last = -1;
gps_last = -1;
g.reset().clear();
g.setColor(0, 0, 1);
let last = { x: 0, y: 0 };
for (let i = l.length - 2; i >= 0; i--) {
let next = {};
let m = (l[i+1].steps - l[i].steps) * mpstep;
let dir = radians(180 + l[i].dir);
next.x = last.x + m * Math.sin(dir);
next.y = last.y + m * Math.cos(dir);
print(dir, m, last, next);
let lp = toPix(last);
let np = toPix(next);
g.drawLine(lp.x, lp.y, np.x, np.y);
g.drawCircle(np.x, np.y, 3);
last = next;
if (log_last == -1)
log_last = m;
log_dist += m;
}
g.setColor(0, 1, 0);
last = { x: 0, y: 0 };
gps_dist = 0;
for (let i = l.length - 2; i >= 0; i--) {
let fix = l[i].fix;
if (fix.fix && fix.lat) {
let next = toM(here.fix, fix);
let lp = toPix(last);
let np = toPix(next);
let d = Math.sqrt((next.x-last.x)*(next.x-last.x)+(next.y-last.y)*(next.y-last.y));
if (gps_last == -1)
gps_last = d;
gps_dist += d;
g.drawLine(lp.x, lp.y, np.x, np.y);
g.drawCircle(np.x, np.y, 3);
last = next;
}
}
log.pop();
}
function testPaint() {
let pos = gps.getGPSFix();
log = [];
let e = { fix: pos, steps: 100, dir: 0 };
log.push(e);
e = { fix: pos, steps: 200, dir: 90 };
log.push(e);
e = { fix: pos, steps: 300, dir: 0 };
log.push(e);
print(log, log.length, log[0], log[1]);
drawLog();
}
function touchHandler(d) {
let x = Math.floor(d.x);
let y = Math.floor(d.y);
if (d.b != 1 || ui.last_b != 0) {
ui.last_b = d.b;
return;
}
if ((x<ui.h/2) && (y<ui.w/2)) {
ui.drawMsg("Turn");
onTurn();
}
if ((x>ui.h/2) && (y<ui.w/2)) {
ui.drawMsg("Writing");
require('Storage').writeJSON("speedstep."+getTime()+".json", log);
ui.drawMsg("Wrote");
}
ui.touchHandler(d);
}
fmt.init();
gps.init();
ui.init();
ui.drawBusy();
gps.start_gps();
Bangle.setCompassPower(1, "speedstep");
Bangle.on("drag", touchHandler);
Bangle.setUI({
mode : "custom",
swipe : (s) => ui.onSwipe(s),
clock : 0
});
if (0)
testPaint();
if (1) {
g.reset();
updateGps();
}

View File

@ -0,0 +1,13 @@
{ "id": "dedreckon",
"name": "Ded Reckon",
"version": "0.01",
"description": "Dead Reckoning using compass and step counter",
"icon": "app.png",
"readme": "README.md",
"supports" : ["BANGLEJS2"],
"tags": "outdoors",
"storage": [
{"name":"dedreckon.app.js","url":"dedreckon.app.js"},
{"name":"dedreckon.img","url":"app-icon.js","evaluate":true}
]
}

View File

@ -1,3 +1,4 @@
0.01: New Clock Nifty A ++ >> adding more information on the right side of the clock
0.02: Fix weather icon for languages other than English

View File

@ -1,5 +1,5 @@
const w = require("weather");
//const locale = require("locale");
const locale = require("locale");
// Weather icons from https://icons8.com/icon/set/weather/color
function getSun() {
@ -67,6 +67,33 @@ function chooseIcon(condition) {
return getPartSun;
} else return getErr;
}
/*
* Choose weather icon to display based on weather conditition code
* https://openweathermap.org/weather-conditions#Weather-Condition-Codes-2
*/
function chooseIconByCode(code) {
const codeGroup = Math.round(code / 100);
switch (codeGroup) {
case 2: return getStorm;
case 3: return getRain;
case 5:
switch (code) {
case 511: return getSnow;
default: return getRain;
}
case 6: return getSnow;
case 7: return getPartSun;
case 8:
switch (code) {
case 800: return getSun;
case 804: return getCloud;
default: return getPartSun;
}
default: return getCloud;
}
}
/*function condenseWeather(condition) {
condition = condition.toLowerCase();
if (condition.includes("thunderstorm") ||
@ -143,8 +170,17 @@ const clock = new ClockFace({
//let cWea =(curr === "no data" ? "no data" : curr.txt);
let cTemp= (curr === "no data" ? 273 : curr.temp);
// const temp = locale.temp(curr.temp - 273.15).match(/^(\D*\d*)(.*)$/);
let w_icon = chooseIcon(curr.txt === undefined ? "no data" : curr.txt );
//let w_icon = chooseIcon(curr.txt);
let w_icon = getErr;
if (locale.name === "en" || locale.name === "en_GB" || locale.name === "en_US") {
w_icon = chooseIcon(curr.txt === undefined ? "no data" : curr.txt);
} else {
// cannot use condition string to determine icon if language is not English; use weather code instead
const code = curr.code || -1;
if (code > 0) {
w_icon = chooseIconByCode(curr.code);
}
}
g.setFontAlign(1, 0).setFont("Vector", 90 * this.scale);
g.drawString(format(hour), this.centerTimeScaleX, this.center.y - 31 * this.scale);

View File

@ -1,7 +1,7 @@
{
"id": "ffcniftyapp",
"name": "Nifty-A Clock ++",
"version": "0.01",
"version": "0.02",
"description": "A nifty clock with time and date and more",
"dependencies": {"weather":"app"},
"icon": "app.png",

2
apps/iconbits/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.01: attempt to import
0.02: implement colors and lines

20
apps/iconbits/README.md Normal file
View File

@ -0,0 +1,20 @@
# Icon Bits ![](app.png)
Bitmap editor suitable for creating icons and fonts for BangleJS2.
You'll want to run a copy of this in simulator, and another one on
watch to view the results.
Draw using the provided tools, then press the button, and you'll get
result on the console; you can also use "dump();" on command
line. show_icon() takes same parameter as is used in app-icon.js
files, you can just copy&paste it to get an icon. By using
"for_screen();" command, then taking a screenshot, you can easily
generate app.png file.
It is also possible to load existing icon into editor, using
"load_icon("");" command. At the end of iconbits.app.js file there are
more utility functions.

View File

@ -0,0 +1,2 @@
require("heatshrink").decompress(atob("mEwhHXAH4A/AH4A/ACkAgAuuGFovuL2Qu/F2amDABRdgC5QKDHSIvdOZQuaApYuGJBYvPJYwvLPBgvOJY4hGRYwvYJZAFGXQy/mdRAvqZpguSF5waMF8IaNF74aPF6ghTF/4UFAApfuGoyPwABoufSEQv/BpowRF6i6IF8oEDHAwueF/4vwD6gvZbyA/VCwIXHL8hQZZ8CQSIRhAiGIbhEcxQvaGo4YCAgQAJF7pBUIQgvnVY4vrAHQ="))

BIN
apps/iconbits/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,400 @@
// Icon bits, thanks to tinydraw
/* eslint-disable no-unused-vars */
/* We have functions we expect user to call from command line, so they
* appear "unused" to lint */
// font, draw, icon
let mode;
let pen = 'pixel';
let discard = null;
let kule = [0, 0, 0]; // R, G, B
var font_height = 22, font_width = 8;
var zoom_x = 64, zoom_y = 24, zoom_f = 6;
var color = true;
let oldLock = false;
let sg = null;
const top_bar = 20;
function clear(m) {
sg.setColor(1,1,1).fillRect(0,0, font_width, font_height);
}
function __setup(m) {
mode = m;
switch (m) {
case 'font':
font_height = 22;
font_width = 8;
zoom_x = 64;
zoom_y = 24;
zoom_f = 6;
break;
case 'draw':
return;
case 'icon':
font_height = 48;
font_width = 48;
zoom_x = 56;
zoom_y = 24;
zoom_f = 2;
break;
}
}
function setup(m) {
__setup(m);
sg = Graphics.createArrayBuffer(font_width, font_height, 8, {});
clear();
}
function icon_big() {
zoom_x = 16;
zoom_y = 25;
zoom_f = 3;
}
function icon_small() {
__setup("icon");
}
function updateLock() {
if (oldLock) {
return;
}
g.setColor('#fff');
g.fillRect(0, 0, g.getWidth(), 20);
g.setFont('Vector', 22);
g.setColor('#000');
g.drawString('PLEASE\nUNLOCK', 10, 2);
oldLock = true;
}
Bangle.on("lock", function() {
if (Bangle.isLocked()) {
updateLock();
} else {
oldLock = false;
drawUtil();
}
});
function nextColor() {
kule[0] = Math.random();
kule[1] = Math.random();
kule[2] = Math.random();
}
function selectColor(x) {
if (color) {
let i = Math.floor((x - 32) / 4);
kule = toColor(i);
return;
}
let c = 255;
if (x < g.getWidth()/2) {
c = 0;
}
kule[0] = c;
kule[1] = c;
kule[2] = c;
}
function nextPen () {
switch (pen) {
case 'circle': pen = 'pixel'; break;
case 'pixel': pen = 'line'; break;
case 'line': pen = 'square'; break;
case 'square': pen = 'circle'; break;
default: pen = 'pixel'; break;
}
drawUtil();
discard = setTimeout(function () { oldX = -1; oldY = -1; console.log('timeout'); discard = null; }, 500);
}
var oldX = -1, oldY = -1;
var line_from = null;
function drawBrushIcon () {
const w = g.getWidth();
switch (pen) {
case 'circle':
g.fillCircle(w - 10, 10, 5);
break;
case 'square':
g.fillRect(w - 5, 5, w - 15, 15);
break;
case 'pixel':
g.setPixel(10, 10);
g.fillCircle(w - 10, 10, 2);
break;
case 'crayon':
g.drawLine(w - 10, 5, w - 10, 15);
g.drawLine(w - 14, 6, w - 10, 12);
g.drawLine(w - 6, 6, w - 10, 12);
break;
case 'line':
g.drawLine(w - 5, 5, w - 15, 15);
break;
}
}
function drawArea() {
g.clear();
if (mode == "draw")
return;
const w = g.getWidth;
g.setColor(0, 0, 0.5);
g.fillRect(0, 0, g.getWidth(), g.getHeight());
g.setColor(1, 1, 1);
g.fillRect(zoom_x, zoom_y, zoom_x+8*zoom_f, zoom_y+font_height*zoom_f);
g.setColor(1, 1, 0.75);
for (let y=0; y<font_height; y++)
g.drawLine(zoom_x, zoom_y+y*zoom_f, zoom_x+font_width*zoom_f, zoom_y+y*zoom_f);
for (let x=0; x<font_width; x++)
g.drawLine(zoom_x+x*zoom_f, zoom_y, zoom_x+x*zoom_f, zoom_y+font_height*zoom_f);
update();
}
function toColor(i) {
let r = [0, 0, 0];
r[0] = (i % 3) / 2;
i = Math.floor(i / 3);
r[1] = (i % 3) / 2;
i = Math.floor(i / 3);
r[2] = (i % 3) / 2;
return r;
}
function drawUtil() {
if (Bangle.isLocked()) {
updateLock();
}
// titlebar
g.setColor(kule[0], kule[1], kule[2]);
g.fillRect(0, 0, g.getWidth(), top_bar);
for (let i = 0; i < 3*3*3; i++) {
let r = toColor(i);
g.setColor(r[0], r[1], r[2]);
g.fillRect(32+4*i, 12, 32+4*i+3, top_bar);
}
// clear button
g.setColor('#000'); // black
g.fillCircle(10, 10, 8, 8);
g.setColor('#fff');
g.drawLine(8, 8, 13, 13);
g.drawLine(13, 8, 8, 13);
// tool button
g.setColor('#fff');
g.fillCircle(g.getWidth() - 10, 10, 8);
g.setColor('#000');
drawBrushIcon();
}
function transform(p) {
if (p.x < zoom_x || p.y < zoom_y)
return p;
p.x = ((p.x - zoom_x) / zoom_f);
if (false)
p.x = font_width - p.x;
p.y = ((p.y - zoom_y) / zoom_f);
return p;
}
function __draw(g, from, to) {
let XS = (to.x - from.x) / 32;
let YS = (to.y - from.y) / 32;
switch (pen) {
case 'line':
case 'pixel':
g.drawLine(from.x, from.y, to.x, to.y);
break;
case 'crayon':
g.drawLine(from.x, from.y, to.x, to.y);
g.drawLine(from.x + 1, from.y + 2, to.x, to.y - 2);
g.drawLine(from.x + 2, from.y + 2, to.x, to.y + 2);
break;
case 'circle':
for (let i = 0; i < 32; i++) {
g.fillCircle(from.x + (i * XS), from.y + (i * YS), 2, 2);
}
break;
case 'square':
for (let i = 0; i < 32; i++) {
const posX = from.x + (i * XS);
const posY = from.y + (i * YS);
g.fillRect(posX - 4, posY - 4, posX + 4, posY + 4);
}
break;
default:
print("Unkown pen ", pen);
}
}
function update() {
if (zoom_f < 3)
g.drawImage(sg, 4, 64, {});
g.drawImage(sg, zoom_x, zoom_y, { scale: zoom_f });
}
function do_draw(from, to) {
from = transform(from);
to = transform(to);
if (from && to) {
__draw(sg, from, to);
}
update();
}
let tapTimer = null;
let dragTimer = null;
function on_drag (tap) {
let from = { x: tap.x, y: tap.y };
const to = { x: tap.x + tap.dx, y: tap.y + tap.dy };
if (oldX != -1) {
from = { x: oldX, y: oldY };
}
if (tap.b === 0) {
if (tapTimer !== null) {
clearTimeout(tapTimer);
tapTimer = null;
}
}
if (dragTimer != null) {
clearTimeout(dragTimer);
dragTimer = null;
}
dragTimer = setTimeout(function () {
oldX = -1;
oldY = -1;
}, 100);
// tap and hold the clear button
if (tap.x < 32 && tap.y < top_bar) {
if (tap.b === 1) {
if (tapTimer === null) {
tapTimer = setTimeout(function () {
clear();
drawArea();
drawUtil();
tapTimer = null;
}, 800);
}
if (discard) {
clearTimeout(discard); discard = null;
return;
}
}
return;
}
if (tap.x > g.getWidth() - 32 && tap.y < top_bar) {
if (tap.b === 1) {
if (tapTimer === null) {
tapTimer = setTimeout(function () {
g.clear();
drawUtil();
oldX = -1; oldY = -1;
tapTimer = null;
}, 800);
}
if (discard) {
clearTimeout(discard);
discard = null;
return;
}
nextPen();
}
drawUtil();
return;
} else if (tap.y < top_bar) {
if (mode == "draw")
nextColor();
else
selectColor(tap.x);
drawUtil();
return;
}
sg.setColor(kule[0], kule[1], kule[2]);
g.setColor(kule[0], kule[1], kule[2]);
oldX = to.x;
oldY = to.y;
if (pen != "line") {
do_draw(from, to);
} else {
if (tap.b == 1) {
print(line_from);
if (!line_from) {
line_from = to;
} else {
do_draw(line_from, to);
line_from = null;
}
}
}
drawUtil();
}
function dump(n) {
function f(i) {
return "\\x" + i.toString(16).padStart(2, '0');
}
let s = f(0) + f(font_width) + f(font_height) + f(1);
// 0..black, 65535..white
for (let y = 0; y < font_height; y++) {
let v = 0;
for (let x = 0; x < font_width; x++) {
let p = sg.getPixel(x, y);
v = v << 1 | (p==0);
}
s += f(v);
}
if (mode == "font")
print('show_font("' + s + '");');
var im = sg.asImage("string");
//print('show_unc_icon("'+btoa(im)+'");');
print('show_icon("'+btoa(require('heatshrink').compress(im))+'");');
}
setup("icon");
drawArea();
Bangle.setUI({
"mode": "custom",
"drag": on_drag,
"btn": dump,
});
drawUtil();
function show_font(icon) {
g.reset().clear();
g.setFont("Vector", 26).drawString("Hellord" + icon, 0, 0);
}
function show_bin_icon(icon) {
g.reset().clear();
g.drawImage(icon, 40, 40);
}
function show_unc_icon(icon) {
show_bin_icon(atob(icon));
}
function show_icon(icon) {
let unc = require("heatshrink").decompress(atob(icon));
show_bin_icon(unc);
}
function load_bin_icon(i) {
sg.reset().clear();
sg.drawImage(i, 0, 0);
drawArea();
}
function load_icon(icon) {
let unc = require("heatshrink").decompress(atob(icon));
load_bin_icon(unc);
}
function for_screen() {
g.reset().clear();
icon_big();
update();
}

View File

@ -0,0 +1,14 @@
{ "id": "iconbits",
"name": "Icon bits",
"version": "0.02",
"description": "Bitmap editor suitable for creating icons",
"icon": "app.png",
"readme": "README.md",
"supports" : ["BANGLEJS2"],
"allow_emulator": true,
"tags": "tools",
"storage": [
{"name":"iconbits.app.js","url":"iconbits.app.js"},
{"name":"iconbits.img","url":"app-icon.js","evaluate":true}
]
}

View File

@ -196,12 +196,6 @@ module.exports = {
"no-undef"
]
},
"apps/sixths/sixths.app.js": {
"hash": "2a4676828bdf78df052df402de34e6f1abd1c847ebe0d193fc789cd6e9dd0e5c",
"rules": [
"no-undef"
]
},
"apps/scribble/app.js": {
"hash": "6d13abd27bab8009a6bdabe1df2df394bc14aac20c68f67e8f8b085fa6b427cd",
"rules": [
@ -1249,12 +1243,6 @@ module.exports = {
"no-undef"
]
},
"apps/accelrec/app.js": {
"hash": "b5369a60afc8f360f0b33f71080eb3f5d09a1bf3703acfcf07cd80dd19f1997d",
"rules": [
"no-undef"
]
},
"apps/BLEcontroller/app-joy.js": {
"hash": "e4f34bb1bc11b52c3d7a1c537a140b0e23ccef82694dcd602cb517a8ba342898",
"rules": [

View File

@ -1 +1,3 @@
0.1: Initial release
0.1: Initial release
0.2: Draw line for 3d effect, fix number alignment
0.3: Fix day-end overflowing hour calculation

View File

@ -1,7 +1,9 @@
# Measure Time
Measure time in a fancy way. Inspired by a Watchface I had on my first Pebble Watch.
Icon from [Flaticon created by Smartline]("https://www.flaticon.com/free-icons/scale")
Written by [prefectAtEarth](https://www.github.com/prefectAtEarth/)
Watchface written by [prefectAtEarth](https://www.github.com/prefectAtEarth/)
![](measuretime.png)
![](screenshot_light.png)
![](screenshot_dark.png)

View File

@ -1 +1 @@
require("heatshrink").decompress(atob("mEw4n/AAIHB/fe8EHrvv333xVS221jnnlFC7//9NP997zXWjHGn+EGJsu9wAC0AHBgugq99C5d0kUq1WtoAHBgnaw8nC5d9mdwgEN7QHBxvQ5nhGwQXNiQHB19A41xC5dy3YXCwAHBwkqx3tI5d3AAV8L4UIDYRkBogADpTOQhWqAAZOLAAuoxAABfyYXXI4pKRO4oACqBHl0QXWAC8IF4QABwpHRkUilALHgutvwvMBY8NoEHKakCqtHR5gAH1FY7wUFcYS/LI5Fwd4r7IqXuJ4uUAYMK1QABKhEKIAQAC1kW7SnDAAUlPxnBiN9xEnu93vx6KAAeHyMdI5wAGox3OS5GAU4oAEoAXJhTXGfigAWhAvWX6QvcT5nog5HJF5QXLX5AAC0levwXId5cNoAvJhWqAAILHgVAhxHMQaZfFwoXQI5YALO5ZHPC6bXDAAmADqYARhBHXkUilC/oA="))
require("heatshrink").decompress(atob("mEwgX/6AHCh////hAQIAB4ALCg4GBwF/BZH+gEfBYXBAYQLB/AbDBY3ggEBBZGAqFAGAQLE/0B6HgGAXB1WrBYP4g/QAQJHDgYLB/kf6H+BY//v/Q/+fEYwGBBYJHIBdJHBBcJTvR5anFBaKz/Wf6zhv4LBz4LG/kf6H+DgQjE/EH6ACBHY3+gPQ8EfI4+AqFAv4LH8EAgJfI/EAFwRHCAAIHB/0AFwQLGGAIuCOAILCh4GB8IKC"))

View File

@ -1,7 +1,5 @@
{
require("Font7x11Numeric7Seg").add(Graphics);
g.setFont("7x11Numeric7Seg");
g.setFontAlign(0, 0);
const centerY = g.getHeight() / 2; //88
const lineStart = 25;
@ -27,10 +25,10 @@
var steps = [0, 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1];
var stepsReversed = steps.slice();
stepsReversed.reverse();
var polyLeftTop = [];
var polyLeftBottom = [];
var polyRightTop = [];
var polyRightBottom = [];
var polyLeftTop = [0, 0];
var polyLeftBottom = [0, g.getHeight()];
var polyRightTop = [g.getWidth() - 1, 0];
var polyRightBottom = [g.getWidth() - 1, g.getHeight()];
let xL = 0;
let xR = g.getWidth() - 1;
let yT = centerY - 13;
@ -70,6 +68,29 @@
g.fillPolyAA(polyRightBottom, true);
};
let hourStringXOffset = function (hour) {
if (hour == 1) {
return lineEndFull - 5;
}
if (hour < 10 || hour >= 20) {
return lineEndFull + 5;
}
return lineEndFull - 5;
};
let drawHourString = function(hour, yLines) {
var hourForDrawing = 0;
if (hour < 0) {
// a negative hour => (+ and - = -)
hourForDrawing = 24 + hour;
} else if (hour >= 24) {
hourForDrawing = hour - 24;
} else {
hourForDrawing = hour;
}
g.drawString(hourForDrawing, hourStringXOffset(hourForDrawing), yLines, true);
};
let drawTime = function () {
g.clear();
var d = new Date();
@ -86,19 +107,19 @@
var lineEnd = lineEndDefault;
g.setFont("7x11Numeric7Seg", 2);
g.setFontAlign(0, 0);
g.setFontAlign(-1, 0);
// gone
do {
switch (yTopLines - 88 + mins) {
case -60:
lineEnd = lineEndFull;
g.drawString(d.getHours() - 1, lineEnd + 10, yTopLines, true);
drawHourString(d.getHours() - 1, yTopLines);
break;
case 0:
case 60:
lineEnd = lineEndFull;
g.drawString(d.getHours(), lineEnd + 10, yTopLines, true);
drawHourString(d.getHours(), yTopLines);
break;
case 45:
case -45:
@ -128,11 +149,11 @@
case 0:
case 60:
lineEnd = lineEndFull;
g.drawString(d.getHours() + 1, lineEnd + 10, yBottomLines, true);
drawHourString(d.getHours() + 1, yBottomLines);
break;
case 120:
lineEnd = lineEndFull;
g.drawString(d.getHours() + 2, lineEnd + 10, yBottomLines, true);
drawHourString(d.getHours() + 2, yBottomLines);
break;
case 15:
case 75:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -1,10 +1,13 @@
{
"id": "measuretime",
"name": "Measure Time",
"version": "0.1",
"version": "0.3",
"description": "Measure Time in a fancy way.",
"icon": "small_measuretime.png",
"screenshots": [{ "url": "measuretime.png" }],
"icon": "measuretime_icon.png",
"screenshots": [
{ "url": "screenshot_light.png" },
{ "url": "screenshot_dark.png" }
],
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS2"],

Binary file not shown.

After

Width:  |  Height:  |  Size: 896 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEw4X/AwX48AFCqoAEC4oL/Bf4L/Bf4LTAH4A/ADGqAAIL/Bf4LD"))

142
apps/quarterclock/app.js Normal file
View File

@ -0,0 +1,142 @@
{
const minute_boxes = [
{x:0.5, y:0},
{x:0.5, y:0.5},
{x:0, y:0.5},
{x:0, y:0},
];
const hour_boxes = [
{x:0.5, y:0},
{x:0.75, y:0},
{x:0.75, y:0.25},
{x:0.75, y:0.5},
{x:0.75, y:0.75},
{x:0.5, y:0.75},
{x:0.25, y:0.75},
{x:0, y:0.75},
{x:0, y:0.5},
{x:0, y:0.25},
{x:0, y:0},
{x:0.25, y:0},
];
let drawTimeout;
// schedule a draw for the next 15 minute period
let queueDraw = function queueDraw() {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = setTimeout(function() {
drawTimeout = undefined;
draw();
}, (60000 * 15) - (Date.now() % (60000 * 15)));
};
// Main draw function
let draw = function draw() {
var d = new Date();
var h = d.getHours(), m = d.getMinutes();
g.setBgColor(settings.backgroundColour);
g.clearRect(Bangle.appRect);
if (settings.showBattery) {
drawBattery();
}
// Draw minute box
drawBox(Math.floor(m/15), minute_boxes, Bangle.appRect.h/2, settings.minuteColour);
// Draw an hour box or write the number
if (settings.digital) {
g.setColor(settings.hourColour);
g.setFont("Vector:60");
g.setFontAlign(0,0);
g.drawString(h, Bangle.appRect.x + Bangle.appRect.w/2, Bangle.appRect.y + Bangle.appRect.h/2);
} else {
drawBox(h % 12, hour_boxes, Bangle.appRect.h/4, settings.hourColour);
}
queueDraw();
};
// Draw battery box
let drawBattery = function drawBattery() {
// Round battery up to 10% interval
let battery = Math.min((Math.floor(E.getBattery()/10)+1)/10, 1);
// Maximum battery box
let batterySize = 30;
// Draw outer box at full brightness
g.setColor(settings.batteryColour);
g.drawRect(
(Bangle.appRect.w / 2) - batterySize,
(Bangle.appRect.h / 2) - batterySize + Bangle.appRect.y,
(Bangle.appRect.w / 2) + batterySize,
(Bangle.appRect.h / 2) + batterySize + Bangle.appRect.y
);
// Fade battery colour and draw inner box
g.setColor(settings.batteryColour.split('').map((c) => {
return c=='f' ? Math.ceil(15 * battery).toString(16) : c;
}).join(''));
g.fillRect(
(Bangle.appRect.w / 2) - (batterySize * battery),
(Bangle.appRect.h / 2) - (batterySize * battery) + Bangle.appRect.y,
(Bangle.appRect.w / 2) + (batterySize * battery),
(Bangle.appRect.h / 2) + (batterySize * battery) + Bangle.appRect.y
);
};
// Draw hour or minute boxes
let drawBox = function drawBox(current, boxes, size, colour) {
let x1 = (boxes[current].x * Bangle.appRect.h) + (Bangle.appRect.y/2);
let y1 = (boxes[current].y * Bangle.appRect.h) + Bangle.appRect.y;
let x2 = x1 + size;
let y2 = y1 + size;
g.setColor(colour);
g.fillRect(x1, y1, x2, y2);
};
let settings = Object.assign({
// Default values
minuteColour: '#f00',
hourColour: '#ff0',
backgroundColour: 'theme',
showWidgets: true,
showBattery: true,
digital: false,
batteryColour: '#0f0'
}, require('Storage').readJSON('quarterclock.json', true) || {});
if (settings.backgroundColour == 'theme') {
settings.backgroundColour = g.theme.bg;
}
// Set minuteColour to a darker shade if same as hourColour
if (settings.minuteColour == settings.hourColour) {
settings.minuteColour = settings.minuteColour.split('').map((c) => {
return c=='f' ? '7' : c;
}).join('');
}
// Show launcher when middle button pressed
// Remove handler to allow fast loading
Bangle.setUI({mode:"clock", remove:function() {
if (drawTimeout) clearTimeout(drawTimeout);
require("widget_utils").show();
}});
// Load and display widgets
Bangle.loadWidgets();
if (settings.showWidgets) {
require("widget_utils").show();
} else {
require("widget_utils").hide();
}
// draw initial boxes and queue subsequent redraws
draw();
}

BIN
apps/quarterclock/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 B

View File

@ -0,0 +1,20 @@
{
"id": "quarterclock",
"name": "Quarter Clock",
"shortName":"Quarter Clock",
"icon": "app.png",
"screenshots" : [ { "url":"screenshot.png" } ],
"version":"0.01",
"description": "For those lazy days when the exact time doesn't matter. Small square shows the hour, large square shows the fifteen minute period, and centre square shows the battery level.",
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS2"],
"storage": [
{"name":"quarterclock.app.js","url":"app.js"},
{"name":"quarterclock.settings.js","url":"settings.js"},
{"name":"quarterclock.img","url":"app-icon.js","evaluate":true}
],
"data": [
{"name":"quarterclock.json"}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,66 @@
(function(back) {
var FILE = 'quarterclock.json';
// Load settings
var settings = Object.assign({
minuteColour: '#f00',
hourColour: '#ff0',
backgroundColour: 'theme',
showWidgets: true,
showBattery: true,
digital: false,
batteryColour: '#0f0',
}, require('Storage').readJSON(FILE, true) || {});
function setSetting(key,value) {
settings[key] = value;
require('Storage').writeJSON(FILE, settings);
}
// Helper method which uses int-based menu item for set of string values and their labels
function stringItems(key, startvalue, values, labels) {
return {
value: (startvalue === undefined ? 0 : values.indexOf(startvalue)),
format: v => labels[v],
min: 0,
max: values.length - 1,
wrap: true,
step: 1,
onchange: v => {
setSetting(key,values[v]);
}
};
}
// Helper method which breaks string set settings down to local settings object
function stringInSettings(name, values, labels) {
return stringItems(name,settings[name], values, labels);
}
// Show the menu
E.showMenu({
'' : { 'title' : 'Quarter Clock' },
'< Back' : () => back(),
'Hour Colour': stringInSettings('hourColour', ['#f00', '#0f0', '#00f', '#ff0', '#0ff', '#f0f'], ['Red', 'Green', 'Blue', 'Yellow', 'Cyan', 'Magenta']),
'Minute Colour': stringInSettings('minuteColour', ['#f00', '#0f0', '#00f', '#ff0', '#0ff', '#f0f'], ['Red', 'Green', 'Blue', 'Yellow', 'Cyan', 'Magenta']),
'Background Colour': stringInSettings('backgroundColour', ['theme', '#000', '#fff'],['theme', 'Black', 'White']),
'Digital': {
value: !!settings.digital, // !! converts undefined to false
onchange: v => {
setSetting('digital', v);
},
},
'Show Widgets': {
value: !!settings.showWidgets,
onchange: v => {
setSetting('showWidgets', v);
},
},
'Show Battery': {
value: !!settings.showBattery,
onchange: v => {
setSetting('showBattery', v);
},
},
'Battery Colour': stringInSettings('batteryColour', ['#f00', '#0f0', '#00f', '#ff0', '#0ff', '#f0f'], ['Red', 'Green', 'Blue', 'Yellow', 'Cyan', 'Magenta']),
});
})

View File

@ -651,11 +651,11 @@ function showUtilMenu() {
E.showMessage(/*LANG*/'Flattening battery - this can take hours.\nLong-press button to cancel.');
Bangle.setLCDTimeout(0);
Bangle.setLCDPower(1);
Bangle.setLCDBrightness(1);
if (Bangle.setGPSPower) Bangle.setGPSPower(1,"flat");
if (Bangle.setHRMPower) Bangle.setHRMPower(1,"flat");
if (Bangle.setCompassPower) Bangle.setCompassPower(1,"flat");
if (Bangle.setBarometerPower) Bangle.setBarometerPower(1,"flat");
if (Bangle.setHRMPower) Bangle.setGPSPower(1,"flat");
setInterval(function() {
var i=1000;while (i--);
}, 1);

View File

@ -2,3 +2,4 @@
0.02: better GPS support, adding altitude and temperature support
0.03: minor code improvements
0.04: make height auto-calibration useful and slow ticks to save power
0.05: add ability to navigate to waypoints, better documentation

View File

@ -24,17 +24,35 @@ minutes, real distance will be usually higher than approximation.
Useful gestures:
F -- disable GPS.
G -- enable GPS for 4 hours in low power mode.
N -- take a note and write it to the log.
S -- enable GPS for 30 minutes in high power mode.
B -- "Battery", show/buzz battery info
D -- "Down", previous waypoint
F -- "oFf", disable GPS.
G -- "Gps", enable GPS for 4 hours in low power mode.
I -- "Info", toggle info display
L -- "aLtimeter", load altimeter app
M -- "Mark", create mark from current position
N -- "Note", take a note and write it to the log.
O -- "Orloj", run orloj app
R -- "Run", run "runplus" app
S -- "Speed", enable GPS for 30 minutes in high power mode.
T -- "Time", buzz current time
U -- "Up", next waypoint
Y -- "compass", reset compass
When application detects watch is being worn, it will use vibrations
to communicate back to the user.
B -- battery low.
E -- acknowledge, gesture understood.
T -- start of new hour.
Three colored dots may appear on display. North is on the 12 o'clock
position (top of the display).
red: this is direction to the waypoint.
green: this is direction you are moving into, according to GPS.
blue: this is direction top of watch faces, according to the compass.
Written by: [Pavel Machek](https://github.com/pavelmachek)
## Future Development
@ -55,4 +73,27 @@ Todo:
*) only turn on compass when needed
*) adjust draw timeouts to save power
*) only warn about battery low when it crosses thresholds, update
battery low message
*) rename "show" to something else -- it collides with built-in
*) adjust clock according to GPS
*) show something more reasonable than (NOTEHERE).
*) hide messages after timeout.
*) show route lengths after the fact
*) implement longer recording than "G".
*) Probably T should be G.
*) sum gps distances for a day
*) allow setting up home altitude, or at least disable auto-calibration
*) show time-to-sunset / sunrise?
*) one-second updates when gps is active

View File

@ -1,6 +1,6 @@
{ "id": "sixths",
"name": "Sixth sense",
"version": "0.04",
"version": "0.05",
"description": "Clock for outdoor use with GPS support",
"icon": "app.png",
"readme": "README.md",

View File

@ -3,18 +3,16 @@
// Options you'll want to edit
const rest_altitude = 354;
const geoid_to_sea_level = 0; // Maybe BangleJS2 already compensates?
const W = g.getWidth();
const H = g.getHeight();
var cx = 100, cy = 105, sc = 70,
temp = 0, alt = 0, bpm = 0;
var cx = 100, cy = 105, sc = 70, temp = 0, alt = 0, bpm = 0;
var buzz = "", /* Set this to transmit morse via vibrations */
inm = "", l = "", /* For incoming morse handling */
in_str = "",
note = "(NOTEHERE)",
debug = "v1119", debug2 = "(otherdb)", debug3 = "(short)";
note = "",
debug = "v0.04.1", debug2 = "(otherdb)", debug3 = "(short)";
var mode = 0, mode_time = 0; // 0 .. normal, 1 .. note, 2.. mark name
var disp_mode = 0; // 0 .. normal, 1 .. small time
@ -45,15 +43,15 @@ var cur_mark = null;
// Icons
var icon_alt = "\0\x08\x1a\1\x00\x00\x00\x20\x30\x78\x7C\xFE\xFF\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\x00\x00\x00\x00\x00\x00\x00";
var icon_m = "\0\x08\x1a\1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\x00\x00\x00\x00\x00\x00\x00";
//var icon_m = "\0\x08\x1a\1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\x00\x00\x00\x00\x00\x00\x00";
var icon_km = "\0\x08\x1a\1\xC3\xC6\xCC\xD8\xF0\xD8\xCC\xC6\xC3\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\x00\x00\x00\x00\x00\x00\x00";
var icon_kph = "\0\x08\x1a\1\xC3\xC6\xCC\xD8\xF0\xD8\xCC\xC6\xC3\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\xFF\x00\xC3\xC3\xFF\xC3\xC3";
var icon_c = "\0\x08\x1a\1\x00\x00\x60\x90\x90\x60\x00\x7F\xFF\xC0\xC0\xC0\xC0\xC0\xFF\x7F\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00";
function toMorse(x) {
var r = "";
let r = "";
for (var i = 0; i < x.length; i++) {
var c = x[i];
let c = x[i];
if (c == " ") {
r += " ";
continue;
@ -113,20 +111,15 @@ function gpsHandleFix(fix) {
doBuzz(" .");
prev_fix = fix;
}
if (0) {
/* GPS altitude fluctuates a lot, not really usable */
alt_adjust = cur_altitude - (fix.alt + geoid_to_sea_level);
alt_adjust_mode = "g";
}
if (1) {
let now1 = Date();
let now2 = fix.time;
var n1 = now1.getMinutes() * 60 + now1.getSeconds();
var n2 = now2.getMinutes() * 60 + now2.getSeconds();
let n1 = now1.getMinutes() * 60 + now1.getSeconds();
let n2 = now2.getMinutes() * 60 + now2.getSeconds();
debug2 = "te "+(n2-n1)+"s";
}
loggps(fix);
var d = calcDistance(fix, prev_fix);
let d = calcDistance(fix, prev_fix);
if (d > 30) {
prev_fix = fix;
gps_dist += d/1000;
@ -135,7 +128,7 @@ function gpsHandleFix(fix) {
function gpsHandle() {
let msg = "";
if (!last_restart) {
var d = (getTime()-last_pause);
let d = (getTime()-last_pause);
if (last_fix)
msg = "PL"+ fmtTimeDiff(getTime()-last_fix);
else
@ -146,7 +139,7 @@ function gpsHandle() {
gpsRestart();
}
} else {
var fix = Bangle.getGPSFix();
let fix = Bangle.getGPSFix();
if (fix && fix.fix && fix.lat) {
gpsHandleFix(fix);
msg = fix.speed.toFixed(1) + icon_kph;
@ -167,8 +160,8 @@ function gpsHandle() {
}
}
var d = (getTime()-last_restart);
var d2 = (getTime()-last_fstart);
let d = (getTime()-last_restart);
let d2 = (getTime()-last_fstart);
print("gps on, restarted ", d, gps_needed, d2, fix.lat);
if (getTime() > gps_speed_limit &&
(d > gps_needed || (last_fstart && d2 > 10))) {
@ -192,8 +185,11 @@ function markNew() {
}
function markHandle() {
let m = cur_mark;
var msg = m.name + ">" + fmtTimeDiff(getTime()- m.time);
if (m.fix && m.fix.fix) {
let msg = m.name + ">";
if (m.time) {
msg += fmtTimeDiff(getTime()- m.time);
}
if (prev_fix && prev_fix.fix && m.fix && m.fix.fix) {
let s = fmtDist(calcDistance(m.fix, prev_fix)/1000) + icon_km;
msg += " " + s;
debug = "wp>" + s;
@ -214,6 +210,34 @@ function entryDone() {
in_str = 0;
mode = 0;
}
var waypoints = [], sel_wp = 0;
function loadWPs() {
waypoints = require("Storage").readJSON(`waypoints.json`)||[{}];
print("Have waypoints", waypoints);
}
function saveWPs() {
require("Storage").writeJSON(`waypoints.json`,waypoints);
}
function selectWP(i) {
sel_wp += i;
if (sel_wp < 0)
sel_wp = 0;
if (sel_wp >= waypoints.length)
sel_wp = waypoints.length - 1;
if (sel_wp < 0) {
show("No WPs", 60);
}
let wp = waypoints[sel_wp];
cur_mark = {};
cur_mark.name = wp.name;
cur_mark.gps_dist = 0; /* HACK */
cur_mark.fix = {};
cur_mark.fix.fix = 1;
cur_mark.fix.lat = wp.lat;
cur_mark.fix.lon = wp.lon;
show("WP:"+wp.name, 60);
print("Select waypoint: ", cur_mark);
}
function inputHandler(s) {
print("Ascii: ", s, s[0], s[1]);
if (s[0] == '^') {
@ -230,9 +254,9 @@ function inputHandler(s) {
return;
}
switch(s) {
case 'B':
case 'B': {
s = ' B';
var bat = E.getBattery();
let bat = E.getBattery();
if (bat > 45)
s += 'E';
else
@ -240,6 +264,8 @@ function inputHandler(s) {
doBuzz(toMorse(s));
show("Bat "+bat+"%", 60);
break;
}
case 'D': selectWP(1); break;
case 'F': gpsOff(); show("GPS off", 3); break;
case 'G': gpsOn(); gps_limit = getTime() + 60*60*4; show("GPS on", 3); break;
case 'I':
@ -252,15 +278,17 @@ function inputHandler(s) {
case 'M': mode = 2; show("M>", 10); cur_mark = markNew(); mode_time = getTime(); break;
case 'N': mode = 1; show(">", 10); mode_time = getTime(); break;
case 'O': aload("orloj.app.js"); break;
case 'R': aload("runplus.app.js"); break;
case 'S': gpsOn(); gps_limit = getTime() + 60*30; gps_speed_limit = gps_limit; show("GPS on", 3); break;
case 'T':
case 'T': {
s = ' T';
var d = new Date();
let d = new Date();
s += d.getHours() % 10;
s += add0(d.getMinutes());
doBuzz(toMorse(s));
break;
case 'R': aload("run.app.js"); break;
}
case 'U': selectWP(-1); break;
case 'Y': doBuzz(buzz); Bangle.resetCompass(); break;
}
}
@ -378,23 +406,22 @@ function loggps(fix) {
}
function hourly() {
print("hourly");
var s = ' T';
let s = ' T';
let bat = E.getBattery();
if (bat < 25) {
s = ' B';
show("Bat "+bat+"%", 60);
}
if (is_active)
doBuzz(toMorse(s));
logstamp("");
//logstamp("");
}
function show(msg, timeout) {
note = msg;
}
function fivemin() {
print("fivemin");
var s = ' B';
var bat = E.getBattery();
if (bat < 25) {
if (is_active)
doBuzz(toMorse(s));
show("Bat "+bat+"%", 60);
}
let s = ' B';
try {
Bangle.getPressure().then((x) => { cur_altitude = x.altitude;
cur_temperature = x.temperature; },
@ -470,8 +497,9 @@ function drawDot(h, d, s) {
g.fillCircle(x,y, 10);
}
function drawBackground() {
var acc = Bangle.getAccel();
let acc = Bangle.getAccel();
is_level = (acc.z < -0.95);
Bangle.setCompassPower(!!is_level, "sixths");
if (is_level) {
let obj = Bangle.getCompass();
if (obj) {
@ -502,13 +530,6 @@ function drawTime(now) {
dot = ".";
g.drawString(now.getHours() + dot + add0(now.getMinutes()), W, 90);
}
function adjPressure(a) {
var o = Bangle.getOptions();
print(o);
o.seaLevelPressure = o.seaLevelPressure * m + a;
Bangle.setOptions(o);
var avr = [];
}
function draw() {
if (disp_mode == 2) {
draw_all();
@ -525,7 +546,11 @@ function draw() {
if (gps_on) {
msg = gpsHandle();
} else {
msg = note;
let o = Bangle.getOptions();
msg = o.seaLevelPressure.toFixed(1) + "hPa";
if (note != "") {
msg = note;
}
}
drawBackground();
@ -551,7 +576,7 @@ function draw() {
let alt_adjust = cur_altitude - rest_altitude;
let abs = Math.abs(alt_adjust);
print("adj", alt_adjust);
var o = Bangle.getOptions();
let o = Bangle.getOptions();
if (abs > 10 && abs < 150) {
let a = 0.01;
// FIXME: draw is called often compared to alt reading
@ -588,16 +613,16 @@ function draw_all() {
let now = new Date();
g.drawString(now.getHours() + ":" + add0(now.getMinutes()) + ":" + add0(now.getSeconds()), 10, 40);
var acc = Bangle.getAccel();
let acc = Bangle.getAccel();
let ax = 0 + acc.x, ay = 0.75 + acc.y, az = 0.75 + acc.y;
let diff = ax * ax + ay * ay + az * az;
diff = diff * 3;
if (diff > 1)
diff = 1;
var co = Bangle.getCompass();
var step = Bangle.getStepCount();
var bat = E.getBattery();
let co = Bangle.getCompass();
let step = Bangle.getStepCount();
let bat = E.getBattery();
Bangle.getPressure().then((x) => { alt = x.altitude; temp = x.temperature; },
print);
@ -618,7 +643,7 @@ function draw_all() {
g.fillCircle(cx + sc * co.dx / 300, cy + sc * co.dz / 400, 5);
}
if (1) {
h = co.heading / 360 * 2 * Math.PI;
let h = co.heading / 360 * 2 * Math.PI;
g.setColor(0, 0, 0.5);
g.fillCircle(cx + sc * Math.sin(h), cy + sc * Math.cos(h), 5);
}
@ -635,10 +660,10 @@ function draw_all() {
queueDraw();
}
function accelTask() {
var tm = 100;
var acc = Bangle.getAccel();
var en = !Bangle.isLocked();
var msg;
let tm = 100;
let acc = Bangle.getAccel();
let en = !Bangle.isLocked();
let msg = "";
if (en && acc.z < -0.95) {
msg = "Level";
doBuzz(".-..");
@ -654,14 +679,14 @@ function accelTask() {
doBuzz("..-");
tm = 3000;
}
print(msg);
setTimeout(accelTask, tm);
}
function buzzTask() {
if (buzz != "") {
var now = buzz[0];
let now = buzz[0];
buzz = buzz.substring(1);
var dot = 100;
let dot = 100;
if (now == " ") {
setTimeout(buzzTask, 300);
} else if (now == ".") {
@ -675,13 +700,14 @@ function buzzTask() {
} else print("Unknown character -- ", now, buzz);
}
}
var last_acc;
function aliveTask() {
function cmp(s) {
let d = acc[s] - last_acc[s];
return d < -0.03 || d > 0.03;
}
// HRM seems to detect hand quite nicely
acc = Bangle.getAccel();
let acc = Bangle.getAccel();
is_active = false;
if (cmp("x") || cmp("y") || cmp("z")) {
print("active");
@ -700,7 +726,8 @@ function lockHandler(locked) {
}
function queueDraw() {
if (getTime() - last_unlocked > 5*60)
let next;
if (getTime() - last_unlocked > 3*60)
next = 60000;
else
next = 1000;
@ -716,8 +743,6 @@ function start() {
Bangle.on("drag", touchHandler);
Bangle.on("lock", lockHandler);
if (0)
Bangle.on("accel", accelHandler);
if (0) {
Bangle.setCompassPower(1, "sixths");
Bangle.setBarometerPower(1, "sixths");
@ -729,8 +754,10 @@ function start() {
}
draw();
loadWPs();
buzzTask();
//accelTask();
if (0)
accelTask();
if (1) {
last_acc = Bangle.getAccel();

View File

@ -1,2 +1,3 @@
0.01: attempt to import
0.02: Minor code improvements
0.03: big rewrite, adding time-adjust and altitude-adjust functionality

View File

@ -1,6 +1,6 @@
{ "id": "skyspy",
"name": "Sky Spy",
"version": "0.02",
"version": "0.03",
"description": "Application for debugging GPS problems",
"icon": "app.png",
"readme": "README.md",

View File

@ -1,20 +1,121 @@
/* Sky spy */
/* 0 .. DD.ddddd
1 .. DD MM.mmm'
2 .. DD MM'ss"
*/
var mode = 1;
/* fmt library v0.1 */
let fmt = {
icon_alt : "\0\x08\x1a\1\x00\x00\x00\x20\x30\x78\x7C\xFE\xFF\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\x00\x00\x00\x00\x00\x00\x00",
icon_m : "\0\x08\x1a\1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\x00\x00\x00\x00\x00\x00\x00",
icon_km : "\0\x08\x1a\1\xC3\xC6\xCC\xD8\xF0\xD8\xCC\xC6\xC3\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\x00\x00\x00\x00\x00\x00\x00",
icon_kph : "\0\x08\x1a\1\xC3\xC6\xCC\xD8\xF0\xD8\xCC\xC6\xC3\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\xFF\x00\xC3\xC3\xFF\xC3\xC3",
icon_c : "\0\x08\x1a\1\x00\x00\x60\x90\x90\x60\x00\x7F\xFF\xC0\xC0\xC0\xC0\xC0\xFF\x7F\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
/* 0 .. DD.ddddd
1 .. DD MM.mmm'
2 .. DD MM'ss"
*/
geo_mode : 1,
init: function() {},
fmtDist: function(km) { return km.toFixed(1) + this.icon_km; },
fmtSteps: function(n) { return this.fmtDist(0.001 * 0.719 * n); },
fmtAlt: function(m) { return m.toFixed(0) + this.icon_alt; },
fmtTimeDiff: function(d) {
if (d < 180)
return ""+d.toFixed(0);
d = d/60;
return ""+d.toFixed(0)+"m";
},
fmtAngle: function(x) {
switch (this.geo_mode) {
case 0:
return "" + x;
case 1: {
let d = Math.floor(x);
let m = x - d;
m = m*60;
return "" + d + " " + m.toFixed(3) + "'";
}
case 2: {
let d = Math.floor(x);
let m = x - d;
m = m*60;
let mf = Math.floor(m);
let s = m - mf;
s = s*60;
return "" + d + " " + mf + "'" + s.toFixed(0) + '"';
}
}
return "bad mode?";
},
fmtPos: function(pos) {
let x = pos.lat;
let c = "N";
if (x<0) {
c = "S";
x = -x;
}
let s = c+this.fmtAngle(pos.lat) + "\n";
c = "E";
if (x<0) {
c = "W";
x = -x;
}
return s + c + this.fmtAngle(pos.lon);
},
};
/* gps library v0.1 */
let gps = {
emulator: -1,
init: function(x) {
this.emulator = (process.env.BOARD=="EMSCRIPTEN"
|| process.env.BOARD=="EMSCRIPTEN2")?1:0;
},
state: {},
on_gps: function(f) {
let fix = this.getGPSFix();
f(fix);
/*
"lat": number, // Latitude in degrees
"lon": number, // Longitude in degrees
"alt": number, // altitude in M
"speed": number, // Speed in kph
"course": number, // Course in degrees
"time": Date, // Current Time (or undefined if not known)
"satellites": 7, // Number of satellites
"fix": 1 // NMEA Fix state - 0 is no fix
"hdop": number, // Horizontal Dilution of Precision
*/
this.state.timeout = setTimeout(this.on_gps, 1000, f);
},
off_gps: function() {
clearTimeout(this.state.timeout);
},
getGPSFix: function() {
if (!this.emulator)
return Bangle.getGPSFix();
let fix = {};
fix.fix = 1;
fix.lat = 50;
fix.lon = 14;
fix.alt = 200;
fix.speed = 5;
fix.course = 30;
fix.time = Date();
fix.satellites = 5;
fix.hdop = 12;
return fix;
}
};
var display = 0;
var debug = 0;
var cancel_gps, gps_start;
var gps_start;
var cur_altitude;
var wi = 24;
var h = 176-wi, w = 176;
var fix;
var adj_time = 0, adj_alt = 0;
function radA(p) { return p*(Math.PI*2); }
function radD(d) { return d*(h/2); }
@ -27,26 +128,7 @@ function radY(p, d) {
return h/2 - Math.cos(a)*radD(d) + wi;
}
function format(x) {
switch (mode) {
case 0:
return "" + x;
case 1:
d = Math.floor(x);
m = x - d;
m = m*60;
return "" + d + " " + m.toFixed(3) + "'";
case 2:
d = Math.floor(x);
m = x - d;
m = m*60;
mf = Math.floor(m);
s = m - mf;
s = s*60;
return "" + d + " " + mf + "'" + s.toFixed(0) + '"';
}
}
var qalt = -1;
var qalt = -1, min_dalt, max_dalt, step;
function resetAlt() {
min_dalt = 9999; max_dalt = -9999; step = 0;
}
@ -64,65 +146,96 @@ function calcAlt(alt, cur_altitude) {
return ddalt;
}
function updateGps() {
let /*have = false,*/ lat = "lat", lon = "lon", alt = "alt",
speed = "speed", hdop = "hdop"; // balt = "balt";
let lat = "lat ", alt = "?",
speed = "speed ", hdop = "?", adelta = "adelta ",
tdelta = "tdelta ";
if (cancel_gps)
return;
fix = Bangle.getGPSFix();
fix = gps.getGPSFix();
if (adj_time) {
print("Adjusting time");
setTime(fix.time.getTime()/1000);
adj_time = 0;
}
if (adj_alt) {
print("Adjust altitude");
if (qalt < 5) {
let rest_altitude = fix.alt;
let alt_adjust = cur_altitude - rest_altitude;
let abs = Math.abs(alt_adjust);
print("adj", alt_adjust);
let o = Bangle.getOptions();
if (abs > 10 && abs < 150) {
let a = 0.01;
// FIXME: draw is called often compared to alt reading
if (cur_altitude > rest_altitude)
a = -a;
o.seaLevelPressure = o.seaLevelPressure + a;
Bangle.setOptions(o);
}
msg = o.seaLevelPressure.toFixed(1) + "hPa";
print(msg);
}
}
try {
Bangle.getPressure().then((x) => {
cur_altitude = x.altitude;
}, print);
} catch (e) {
print("Altimeter error", e);
//print("Altimeter error", e);
}
speed = getTime() - gps_start;
//print(fix);
if (fix && fix.time) {
tdelta = "" + (getTime() - fix.time.getTime()/1000).toFixed(0);
}
if (fix && fix.fix && fix.lat) {
lat = "" + format(fix.lat);
lon = "" + format(fix.lon);
alt = "" + fix.alt.toFixed(1);
lat = "" + fmt.fmtPos(fix);
alt = "" + fix.alt.toFixed(0);
adelta = "" + (cur_altitude - fix.alt).toFixed(0);
speed = "" + fix.speed.toFixed(1);
hdop = "" + fix.hdop.toFixed(1);
//have = true;
hdop = "" + fix.hdop.toFixed(0);
} else {
lat = "NO FIX\n"
+ "" + (getTime() - gps_start).toFixed(0) + "s "
+ sats_used + "/" + snum;
if (cur_altitude)
adelta = "" + cur_altitude.toFixed(0);
}
let ddalt = calcAlt(alt, cur_altitude);
if (display == 1)
g.reset().setFont("Vector", 20)
.setColor(1,1,1)
.fillRect(0, wi, 176, 176)
.setColor(0,0,0)
.drawString("Acquiring GPS", 0, 30)
.drawString(lat, 0, 50)
.drawString(lon, 0, 70)
.drawString("alt "+alt, 0, 90)
.drawString("speed "+speed, 0, 110)
.drawString("hdop "+hdop, 0, 130)
.drawString("balt" + cur_altitude, 0, 150);
let msg = "";
if (display == 1) {
msg = lat +
"\ne" + hdop + "m "+tdelta+"s\n" +
speed + "km/h\n"+ alt + "m+" + adelta + "\nmsghere";
}
if (display == 2) {
g.reset().setFont("Vector", 20)
.setColor(1,1,1)
.fillRect(0, wi, 176, 176)
.setColor(0,0,0)
.drawString("GPS status", 0, 30)
.drawString("speed "+speed, 0, 50)
.drawString("hdop "+hdop, 0, 70)
.drawString("dd "+qalt.toFixed(0) + " (" + ddalt.toFixed(0) + ")", 0, 90)
.drawString("alt "+alt, 0, 110)
.drawString("balt " + cur_altitude, 0, 130)
.drawString(step, 0, 150);
/* qalt is altitude quality estimate -- over ten seconds,
computes differences between GPS and barometric altitude.
The lower the better.
ddalt is just a debugging -- same estimate, but without
waiting 10 seconds, so will be always optimistic at start
of the cycle */
msg = speed + "km/h\n" +
"e"+hdop + "m"
+"\ndd "+qalt.toFixed(0) + "\n(" + step + "/" + ddalt.toFixed(0) + ")" +
"\n"+alt + "m+" + adelta;
}
step++;
if (step == 10) {
qalt = max_dalt - min_dalt;
resetAlt();
}
if (display > 0) {
g.reset().setFont("Vector", 31)
.setColor(1,1,1)
.fillRect(0, wi, 176, 176)
.setColor(0,0,0)
.drawString(msg, 3, 25);
}
if (debug > 0)
print(fix);
setTimeout(updateGps, 1000);
@ -184,7 +297,7 @@ function drawSats(sats) {
var sats = [];
var snum = 0;
//var sats_receiving = 0;
var sats_used = 0;
function parseRaw(msg, lost) {
if (lost)
@ -199,6 +312,7 @@ function parseRaw(msg, lost) {
if (s[2] == "1") {
snum = 0;
sats = [];
sats_used = 0;
}
let view = 1 * s[3];
@ -217,6 +331,8 @@ function parseRaw(msg, lost) {
sat.ele = 1*s[i++];
sat.azi = 1*s[i++];
sat.snr = s[i++];
if (sat.snr != "")
sats_used++;
if (debug > 0)
print(" ", sat);
sats[snum++] = sat;
@ -231,30 +347,80 @@ function parseRaw(msg, lost) {
}
}
function stopGps() {
cancel_gps=true;
Bangle.setGPSPower(0, "skyspy");
}
function markGps() {
cancel_gps = false;
Bangle.setGPSPower(1, "skyspy");
Bangle.on('GPS-raw', parseRaw);
gps_start = getTime();
updateGps();
}
function onSwipe(dir) {
display = display + 1;
if (display == 3)
display = 0;
function drawMsg(msg) {
g.reset().setFont("Vector", 35)
.setColor(1,1,1)
.fillRect(0, wi, 176, 176)
.setColor(0,0,0)
.drawString(msg, 5, 30);
}
function drawBusy() {
drawMsg("\n.oO busy");
}
var numScreens = 3;
function nextScreen() {
display = display + 1;
if (display == numScreens)
display = 0;
drawBusy();
}
function prevScreen() {
display = display - 1;
if (display < 0)
display = numScreens - 1;
drawBusy();
}
function onSwipe(dir) {
nextScreen();
}
var last_b = 0;
function touchHandler(d) {
let x = Math.floor(d.x);
let y = Math.floor(d.y);
if (d.b != 1 || last_b != 0) {
last_b = d.b;
return;
}
last_b = d.b;
if ((x<h/2) && (y<w/2)) {
drawMsg("Clock\nadjust");
adj_time = 1;
}
if ((x>h/2) && (y<w/2)) {
drawMsg("Alt\nadjust");
adj_alt = 1;
}
if ((x<h/2) && (y>w/2))
prevScreen();
if ((x>h/2) && (y>w/2))
nextScreen();
}
gps.init();
fmt.init();
Bangle.on("drag", touchHandler);
Bangle.setUI({
mode : "custom",
swipe : onSwipe,
clock : 0
});
Bangle.loadWidgets();
Bangle.drawWidgets();
drawBusy();
markGps();

View File

@ -7,3 +7,4 @@
0.07: Update clock_info to avoid a redraw
0.08: Timer ClockInfo now updates once a minute
0.09: Timer ClockInfo resets to timer menu when blurred
0.10: Timer ClockInfo now uses +- icons, and changes timer from 'T-5 min' to just '5 min' to aid readability

View File

@ -28,7 +28,7 @@
var min = getAlarmMinutes();
if(min < 0)
return "OFF";
return "T-" + String(min)+ " min";
return min + " min";
}
function increaseAlarm(t){
@ -80,7 +80,7 @@
offsets.forEach((o, i) => {
smpltmrItems.items = smpltmrItems.items.concat({
name: null,
get: () => ({ text: (o > 0 ? "+" : "") + o + " min.", img: smpltmrItems.img }),
get: () => ({ text: (o > 0 ? "+" : "") + o + " min", img: (o>0)?atob("GBiBAAB+AAB+AAAYAAAYAAB+AA3/sA+B8A4AcAwAMBgYGBgYGDAYDDAYDDH/jDH/jDAYDDAYDBgYGBgYGAwAMA4AcAeB4AH/gAB+AA=="):atob("GBiBAAB+AAB+AAAYAAAYAAB+AA3/sA+B8A4AcAwAMBgAGBgAGDAADDAADDH/jDH/jDAADDAADBgAGBgAGAwAMA4AcAeB4AH/gAB+AA==") }),
show: function() { },
hide: function() { },
blur: restoreMainItem,

View File

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

1
apps/splitsw/ChangeLog Normal file
View File

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

30
apps/splitsw/README.md Normal file
View File

@ -0,0 +1,30 @@
# Stopwatch with split times
A basic stopwatch with support for split times.
![](screenshot.png)
## Features
Implemented:
- Start stopwatch
- Stop stopwatch
- Show split times
- Reset stopwatch
- Keep display unlocked
Future:
- Save state and restore running stopwatch when it reopens
- View all split times
- Duplicate Start/Stop and/or Reset/Split button on the physical button
- Settings, e.g. what the physical button does, and whether to keep the backlight on
## Creator
James Taylor ([jt-nti](https://github.com/jt-nti))
## Icons
The same icons as apps/stopwatch!

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

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEw4UA///vvvvEF/muH+cDHgPABf4AElWoKhILClALH1WqAQIWHBYIABwAKEgQKD1WgBYkK1X1r4XHlWtqtVvQLG1XVBYNXHYsC1YJBBoPqC4kKEQILCvQ7EhW1BYdeBYkqytVqwCCQwkqCgILCq4LFIoILCqoLEIwIsBGQJIBBZ+pA4Na0oDBtQLGvSFCBaYjIHYR3CI5AADBYhrCAAaDHAASDGQASGCBYizCAASzFZYQACZYrjCIwb7QHgIkCvQ6EGAWq+tf1QuEGAWqAAQuFEgQKBEQw9DHIwAuA="))

167
apps/splitsw/app.js Normal file
View File

@ -0,0 +1,167 @@
Bangle.loadWidgets();
g.clear(true);
Bangle.drawWidgets();
Bangle.setLCDTimeout(undefined);
let renderIntervalId;
let startTime;
let stopTime;
let subtotal;
let currentSplitNumber;
let splitStartTime;
let splitSubtotal;
var Layout = require("Layout");
var layout = new Layout( {
type:"v", c: [
{type:"txt", pad:4, font:"20%", label:"", id:"time", fillx:1},
{type:"h", c: [
{type:"btn", pad:4, font:"6x8:2", label:"Start", id:"startStop", cb: l=>startStop() },
{type:"btn", pad:4, font:"6x8:2", label:"Reset", id:"resetSplit", cb: l=>resetSplit() }
]},
{
type:"v", pad:4, c: [
{type:"txt", font:"6x8:2", label:"", id:"split", fillx:1},
{type:"txt", font:"6x8:2", label:"", id:"prevSplit", fillx:1},
]},
]
}, {
lazy: true,
back: load,
});
// TODO The code in this function appears in various apps so it might be
// nice to add something to the time_utils module. (There is already a
// formatDuration function but that doesn't quite work the same way.)
const getTime = function(milliseconds) {
let hrs = Math.floor(milliseconds/3600000);
let mins = Math.floor(milliseconds/60000)%60;
let secs = Math.floor(milliseconds/1000)%60;
let tnth = Math.floor(milliseconds/100)%10;
let text;
if (hrs === 0) {
text = ("0"+mins).slice(-2) + ":" + ("0"+secs).slice(-2) + "." + tnth;
} else {
text = ("0"+hrs) + ":" + ("0"+mins).slice(-2) + ":" + ("0"+secs).slice(-2);
}
return text;
};
const renderIntervalCallback = function() {
if (startTime === undefined) {
return;
}
updateStopwatch();
};
const buzz = function() {
Bangle.buzz(50, 0.5);
};
const startStop = function() {
buzz();
if (layout.startStop.label === "Start") {
start();
} else {
stop();
}
};
const start = function() {
if (stopTime === undefined) {
startTime = Date.now();
splitStartTime = startTime;
subtotal = 0;
splitSubtotal = 0;
currentSplitNumber = 1;
} else {
subtotal += stopTime - startTime;
splitSubtotal += stopTime - splitStartTime;
startTime = Date.now();
splitStartTime = startTime;
stopTime = undefined;
}
layout.startStop.label = "Stop";
layout.resetSplit.label = "Split";
updateStopwatch();
renderIntervalId = setInterval(renderIntervalCallback, 100);
};
const stop = function() {
stopTime = Date.now();
layout.startStop.label = "Start";
layout.resetSplit.label = "Reset";
updateStopwatch();
if (renderIntervalId !== undefined) {
clearInterval(renderIntervalId);
renderIntervalId = undefined;
}
};
const resetSplit = function() {
buzz();
if (layout.resetSplit.label === "Reset") {
reset();
} else {
split();
}
};
const reset = function() {
layout.startStop.label = "Start";
layout.resetSplit.label = "Reset";
layout.split.label = "";
layout.prevSplit.label = "";
startTime = undefined;
stopTime = undefined;
subtotal = 0;
currentSplitNumber = 1;
splitStartTime = undefined;
splitSubtotal = 0;
updateStopwatch();
};
const split = function() {
const splitTime = Date.now() - splitStartTime + splitSubtotal;
layout.prevSplit.label = "#" + currentSplitNumber + " " + getTime(splitTime);
splitStartTime = Date.now();
splitSubtotal = 0;
currentSplitNumber++;
updateStopwatch();
};
const updateStopwatch = function() {
let elapsedTime;
if (startTime === undefined) {
elapsedTime = 0;
} else {
elapsedTime = Date.now() - startTime + subtotal;
}
layout.time.label = getTime(elapsedTime);
if (splitStartTime !== undefined) {
const splitTime = Date.now() - splitStartTime + splitSubtotal;
layout.split.label = "#" + currentSplitNumber + " " + getTime(splitTime);
}
layout.render();
// layout.debug();
};
updateStopwatch();

BIN
apps/splitsw/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,16 @@
{ "id": "splitsw",
"name": "Stopwatch with split times",
"shortName":"Stopwatch",
"version":"0.01",
"description": "A basic stopwatch with support for split times",
"icon": "app.png",
"tags": "tool,outdoors,health",
"supports" : ["BANGLEJS2"],
"readme": "README.md",
"screenshots": [{ "url": "screenshot.png" }],
"allow_emulator": true,
"storage": [
{"name":"splitsw.app.js","url":"app.js"},
{"name":"splitsw.img","url":"app-icon.js","evaluate":true}
]
}

BIN
apps/splitsw/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -0,0 +1 @@
0.01: Initial version

View File

@ -0,0 +1,55 @@
# Timestamp Log
Timestamp Log provides a convenient way to record points in time for later reference. Each time a button is tapped a date/time-stamped marker is logged. By default up to 30 entries can be stored at once; this can be increased up to 100 in the settings menu.
![Timestamp Log screenshot](screenshot.png)
## Usage and controls
When the app starts you will see the log display. Initially the log is empty. The large button on the bottom left displays the current time and will add a date- and time-stamp when tapped. Each tap of the button adds a new entry to the bottom of the log. The small button on the bottom right opens the app settings menu.
If the log contains more entries than can be displayed at once, swiping up and down will move through the entries one screenfull at a time.
To delete an individual entry, display it on the screen and then tap on it. The entry's position in the list will be shown along with a Delete button. Tap this button to remove the entry. The Up and Down arrows on the right side of the screen can be used to move between log entries. Further deletions can be made. Finally, click the Back button in the upper-left to finish and return to the main log screen.
## Settings
The settings menu provides the following settings:
### Log
**Max entries:** Select the maximum number of entries that the log can hold.
**Auto-delete oldest:** If turned on, adding a log entry when the log is full will cause the oldest entry to automatically be deleted to make room. Otherwise, it is not possible to add another log entry until some entries are manually deleted or the “Max entries” setting is increased.
**Clear log:** Remove all log entries, leaving the log empty.
### Appearance
**Log font:** Select the font used to display log entries.
**Log font H size** and **Log font V size**: Select the horizontal and vertical sizes, respectively, of the font. Reasonable values for bitmapped fonts are 1 or 2 for either setting. For Vector, values around 15 to 25 work best. Setting both sizes the same will display the font with normal proportions; varying the values will change the relative height or width of the font.
### Button
You can choose the action that the physical button (Bangle.js v2) performs when the screen is unlocked.
**Log time:** Add a date/time stamp to the log. Same as tapping the large button on the touch screen.
**Open settings:** Open this app settings menu.
**Quit app:** Return to the main clock app.
**Do nothing:** Perform no action.
## Web interface
Currently the web interface displays the list of dates and times, which can be copied and pasted as desired. The log cannot currently be edited with this interface, only displayed.
## Support
Issues and questions may be posted at: https://github.com/espruino/BangleApps/issues
## Creator
Travis Evans

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEw4cB/4ACBQX48AFDAAUkyVJAQoDCBZACDymSoEAgVJkQRJkskGAuJCJEpBwYDCyIRHpVICI0SogRGqQyEAgdIBwIUEyAODAQkJiVJxBoDwARIgJuBiIUBCIKzKCIOCQYWkCJUkpNQoiMBkARKgmJxUIxMlOgIQIQwOJyNBiKeBCJeRyUokUoGZPYAQMRyVFgiwDAAsGCIlI0GIBYfAAgUB2wECCINJikRCIfbAgVt2DaDCIMiwR9DjdggEDtg5DgTECSQIJDtuAEwYRFSQOSBIUN2xWCgANBgVSAYKSBR4k2AgYRCxQDBSQTGKgVRCISSBoARKxUpCIKSFAA0SqNFCIKSFAA0RlUo0iSHCI0losUSRARFkmo0SSEwAPFeoORkmViiSEiARHxJXB0SSFAgQCByEAggRCqiSEilChEgwUIXgMkBgVKSQmiqFBgkQoMUoArESQdE6Y1FxIRESQco0WIEYkRiQRDSQWRmnTpojEwRhFSQOKEYOJEYkQogRESQNNEZEIPoQUCEYeKkIjEoLFBCIdTEYc0EYsiCIlKpQjCkojCNIYREpMpEYwCCEYoCB0gjBkmEEYImCgQRGyWTNYJECbQQjHJQIDBygjNpSHCEZ0QAYIjODoJHPEAgjDA=="))

523
apps/timestamplog/app.js Normal file
View File

@ -0,0 +1,523 @@
const Layout = require('Layout');
const locale = require('locale');
const tsl = require('timestamplog');
// Min number of pixels of movement to recognize a touchscreen drag/swipe
const DRAG_THRESHOLD = 30;
// Width of scroll indicators
const SCROLL_BAR_WIDTH = 12;
// Fetch a stringified image
function getIcon(id) {
if (id == 'add') {
// Graphics.createImage(`
// XX X X X X
// XX X X X X
// XXXXXX X X X X
// XXXXXX X X X X
// XX X X X X
// XX X X X X
// X XX X X
// X X X X
// X XX X
// X X X
// X X
// X XX
// XXX XX
// XXXXX
// XXXX
// XX
// `);
return "\0\x17\x10\x81\x000\t\x12`$K\xF0\x91'\xE2D\x83\t\x12\x06$H\x00\xB1 \x01$\x80\x042\x00\b(\x00 \x00A\x80\x01\xCC\x00\x03\xE0\x00\x0F\x00\x00\x18\x00\x00";
} else if (id == 'menu') {
// Graphics.createImage(`
//
//
//
//
// XXXXXXXXXXXXXXXX
// XXXXXXXXXXXXXXXX
//
//
// XXXXXXXXXXXXXXXX
// XXXXXXXXXXXXXXXX
//
//
// XXXXXXXXXXXXXXXX
// XXXXXXXXXXXXXXXX
//
//
// `);
return "\0\x10\x10\x81\0\0\0\0\0\0\0\0\0\xFF\xFF\xFF\xFF\0\0\0\0\xFF\xFF\xFF\xFF\0\0\0\0\xFF\xFF\xFF\xFF\0\0\0\0";
}
}
// UI layout render callback for log entries
function renderLogItem(elem) {
if (elem.item) {
g.setColor(g.theme.bg)
.fillRect(elem.x, elem.y, elem.x + elem.w - 1, elem.y + elem.h - 1)
.setFont(tsl.fontSpec(tsl.SETTINGS.logFont,
tsl.SETTINGS.logFontHSize, tsl.SETTINGS.logFontVSize))
.setFontAlign(-1, -1)
.setColor(g.theme.fg)
.drawLine(elem.x, elem.y, elem.x + elem.w - 1, elem.y)
.drawString(locale.date(elem.item.stamp, 1)
+ '\n'
+ locale.time(elem.item.stamp).trim(),
elem.x, elem.y + 1);
} else {
g.setColor(g.blendColor(g.theme.bg, g.theme.fg, 0.25))
.fillRect(elem.x, elem.y, elem.x + elem.w - 1, elem.y + elem.h - 1);
}
}
// Render a scroll indicator
// `scroll` format: {
// pos: int,
// min: int,
// max: int,
// itemsPerPage: int,
// }
function renderScrollBar(elem, scroll) {
const border = 1;
const boxArea = elem.h - 2 * border;
const boxSize = E.clip(
Math.round(
scroll.itemsPerPage / (scroll.max - scroll.min + 1) * (elem.h - 2)
),
3,
boxArea
);
const boxTop = (scroll.max - scroll.min) ?
Math.round(
(scroll.pos - scroll.min) / (scroll.max - scroll.min)
* (boxArea - boxSize) + elem.y + border
) : elem.y + border;
// Draw border
g.setColor(g.theme.fg)
.fillRect(elem.x, elem.y, elem.x + elem.w - 1, elem.y + elem.h - 1)
// Draw scroll box area
.setColor(g.theme.bg)
.fillRect(elem.x + border, elem.y + border,
elem.x + elem.w - border - 1, elem.y + elem.h - border - 1)
// Draw scroll box
.setColor(g.blendColor(g.theme.bg, g.theme.fg, 0.5))
.fillRect(elem.x + border, boxTop,
elem.x + elem.w - border - 1, boxTop + boxSize - 1);
}
// Main app screen interface, launched by calling start()
class MainScreen {
constructor() {
// Values set up by start()
this.itemsPerPage = null;
this.scrollPos = null;
this.layout = null;
// Handlers/listeners
this.buttonTimeoutId = null;
this.listeners = {};
}
// Launch this UI and make it live
start() {
this._initLayout();
this.layout.clear();
this.scroll('b');
this.render('buttons');
this._initTouch();
}
// Stop this UI, shut down all timers/listeners, and otherwise clean up
stop() {
if (this.buttonTimeoutId) {
clearTimeout(this.buttonTimeoutId);
this.buttonTimeoutId = null;
}
// Kill layout handlers
Bangle.removeListener('drag', this.listeners.drag);
Bangle.removeListener('touch', this.listeners.touch);
clearWatch(this.listeners.btnWatch);
Bangle.setUI();
}
_initLayout() {
let layout = new Layout(
{type: 'v',
c: [
// Placeholder to force bottom alignment when there is unused
// vertical screen space
{type: '', id: 'placeholder', fillx: 1, filly: 1},
{type: 'h',
c: [
{type: 'v',
id: 'logItems',
// To be filled in with log item elements once we
// determine how many will fit on screen
c: [],
},
{type: 'custom',
id: 'logScroll',
render: elem => { renderScrollBar(elem, this.scrollBarInfo()); }
},
],
},
{type: 'h',
id: 'buttons',
c: [
{type: 'btn', font: '6x8:2', fillx: 1, label: '+ XX:XX', id: 'addBtn',
cb: this.addTimestamp.bind(this)},
{type: 'btn', font: '6x8:2', label: getIcon('menu'), id: 'menuBtn',
cb: () => launchSettingsMenu(returnFromSettings)},
],
},
],
}
);
// Calculate how many log items per page we have space to display
layout.update();
let availableHeight = layout.placeholder.h;
g.setFont(tsl.fontSpec(tsl.SETTINGS.logFont,
tsl.SETTINGS.logFontHSize, tsl.SETTINGS.logFontVSize));
let logItemHeight = g.getFontHeight() * 2;
this.itemsPerPage = Math.max(1,
Math.floor(availableHeight / logItemHeight));
// Populate log items in layout
for (let i = 0; i < this.itemsPerPage; i++) {
layout.logItems.c.push(
{type: 'custom', render: renderLogItem, item: undefined, itemIdx: undefined,
fillx: 1, height: logItemHeight}
);
}
layout.logScroll.height = logItemHeight * this.itemsPerPage;
layout.logScroll.width = SCROLL_BAR_WIDTH;
layout.update();
this.layout = layout;
}
// Redraw a particular display `item`, or everything if `item` is falsey
render(item) {
if (!item || item == 'log') {
let layLogItems = this.layout.logItems;
let logIdx = this.scrollPos - this.itemsPerPage;
for (let elem of layLogItems.c) {
logIdx++;
elem.item = stampLog.log[logIdx];
elem.itemIdx = logIdx;
}
this.layout.render(layLogItems);
this.layout.render(this.layout.logScroll);
}
if (!item || item == 'buttons') {
let addBtn = this.layout.addBtn;
if (!tsl.SETTINGS.rotateLog && stampLog.isFull()) {
// Dimmed appearance for unselectable button
addBtn.btnFaceCol = g.blendColor(g.theme.bg2, g.theme.bg, 0.5);
addBtn.btnBorderCol = g.blendColor(g.theme.fg2, g.theme.bg, 0.5);
addBtn.label = 'Log full';
} else {
addBtn.btnFaceCol = g.theme.bg2;
addBtn.btnBorderCol = g.theme.fg2;
addBtn.label = getIcon('add') + ' ' + locale.time(new Date(), 1).trim();
}
this.layout.render(this.layout.buttons);
// Auto-update time of day indication on log-add button upon
// next minute
if (!this.buttonTimeoutId) {
this.buttonTimeoutId = setTimeout(
() => {
this.buttonTimeoutId = null;
this.render('buttons');
},
60000 - (Date.now() % 60000)
);
}
}
}
_initTouch() {
let distanceY = null;
function dragHandler(ev) {
// Handle up/down swipes for scrolling
if (ev.b) {
if (distanceY === null) {
// Drag started
distanceY = ev.dy;
} else {
// Drag in progress
distanceY += ev.dy;
}
} else {
// Drag released
distanceY = null;
}
if (Math.abs(distanceY) > DRAG_THRESHOLD) {
// Scroll threshold reached
Bangle.buzz(50, .5);
this.scroll(distanceY > 0 ? 'u' : 'd');
distanceY = null;
}
}
this.listeners.drag = dragHandler.bind(this);
Bangle.on('drag', this.listeners.drag);
function touchHandler(button, xy) {
// Handle taps on log entries
let logUIItems = this.layout.logItems.c;
for (var logUIObj of logUIItems) {
if (!xy.type &&
logUIObj.x <= xy.x && xy.x < logUIObj.x + logUIObj.w &&
logUIObj.y <= xy.y && xy.y < logUIObj.y + logUIObj.h &&
logUIObj.item) {
switchUI(new LogEntryScreen(stampLog, logUIObj.itemIdx));
break;
}
}
}
this.listeners.touch = touchHandler.bind(this);
Bangle.on('touch', this.listeners.touch);
function buttonHandler() {
let act = tsl.SETTINGS.buttonAction;
if (act == 'Log time') {
if (currentUI != mainUI) {
switchUI(mainUI);
}
mainUI.addTimestamp();
} else if (act == 'Open settings') {
launchSettingsMenu(returnFromSettings);
} else if (act == 'Quit app') {
Bangle.showClock();
}
}
this.listeners.btnWatch = setWatch(buttonHandler, BTN,
{edge: 'falling', debounce: 50, repeat: true});
}
// Add current timestamp to log if possible and update UI display
addTimestamp() {
if (tsl.SETTINGS.rotateLog || !stampLog.isFull()) {
stampLog.addEntry();
this.scroll('b');
this.render('buttons');
}
}
// Get scroll information for log display
scrollInfo() {
return {
pos: this.scrollPos,
min: (stampLog.log.length - 1) % this.itemsPerPage,
max: stampLog.log.length - 1,
itemsPerPage: this.itemsPerPage
};
}
// Like scrollInfo, but adjust the data so as to suggest scrollbar
// geometry that accurately reflects the nature of the scrolling
// (page by page rather than item by item)
scrollBarInfo() {
const info = this.scrollInfo();
function toPage(scrollPos) {
return Math.floor(scrollPos / info.itemsPerPage);
}
return {
// Define 1 "screenfull" as the unit here
itemsPerPage: 1,
pos: toPage(info.pos),
min: toPage(info.min),
max: toPage(info.max),
};
}
// Scroll display in given direction or to given position:
// 'u': up, 'd': down, 't': to top, 'b': to bottom
scroll(how) {
let scroll = this.scrollInfo();
if (how == 'u') {
this.scrollPos -= scroll.itemsPerPage;
} else if (how == 'd') {
this.scrollPos += scroll.itemsPerPage;
} else if (how == 't') {
this.scrollPos = scroll.min;
} else if (how == 'b') {
this.scrollPos = scroll.max;
}
this.scrollPos = E.clip(this.scrollPos, scroll.min, scroll.max);
this.render('log');
}
}
// Log entry screen interface, launched by calling start()
class LogEntryScreen {
constructor(stampLog, logIdx) {
this.logIdx = logIdx;
this.logItem = stampLog.log[logIdx];
this.defaultFont = tsl.fontSpec(
tsl.SETTINGS.logFont, tsl.SETTINGS.logFontHSize, tsl.SETTINGS.logFontVSize);
}
start() {
this._initLayout();
this.layout.clear();
this.refresh();
}
stop() {
Bangle.setUI();
}
back() {
this.stop();
switchUI(mainUI);
}
_initLayout() {
let layout = new Layout(
{type: 'v',
c: [
{type: 'txt', font: this.defaultFont, id: 'entryno', label: 'Entry ?/?'},
{type: 'txt', font: this.defaultFont, id: 'date', label: '?'},
{type: 'txt', font: this.defaultFont, id: 'time', label: '?'},
{type: '', id: 'placeholder', fillx: 1, filly: 1},
{type: 'btn', font: '12x20', label: 'Delete',
cb: this.delLogItem.bind(this)},
],
},
{
back: this.back.bind(this),
btns: [
{label: '<', cb: this.prevLogItem.bind(this)},
{label: '>', cb: this.nextLogItem.bind(this)},
],
}
);
layout.update();
this.layout = layout;
}
render(item) {
this.layout.clear();
this.layout.render();
}
refresh() {
this.logItem = stampLog.log[this.logIdx];
this.layout.entryno.label = 'Entry ' + (this.logIdx+1) + '/' + stampLog.log.length;
this.layout.date.label = locale.date(this.logItem.stamp, 1);
this.layout.time.label = locale.time(this.logItem.stamp).trim();
this.layout.update();
this.render();
}
prevLogItem() {
this.logIdx = this.logIdx ? this.logIdx-1 : stampLog.log.length-1;
this.refresh();
}
nextLogItem() {
this.logIdx = this.logIdx == stampLog.log.length-1 ? 0 : this.logIdx+1;
this.refresh();
}
delLogItem() {
stampLog.deleteEntries([this.logItem]);
if (!stampLog.log.length) {
this.back();
return;
} else if (this.logIdx > stampLog.log.length - 1) {
this.logIdx = stampLog.log.length - 1;
}
// Create a brief “blink” on the screen to provide user feedback
// that the deletion has been performed
this.layout.clear();
setTimeout(this.refresh.bind(this), 250);
}
}
function switchUI(newUI) {
currentUI.stop();
currentUI = newUI;
currentUI.start();
}
function saveErrorAlert() {
currentUI.stop();
// Not `showAlert` because the icon plus message don't fit the
// screen well
E.showPrompt(
'Trouble saving timestamp log; data may be lost!',
{title: "Can't save log",
buttons: {'Ok': true}}
).then(currentUI.start.bind(currentUI));
}
function launchSettingsMenu(backCb) {
currentUI.stop();
stampLog.save();
tsl.launchSettingsMenu(backCb);
}
function returnFromSettings() {
// Reload stampLog to pick up any changes made from settings UI
stampLog = loadStampLog();
currentUI.start();
}
function loadStampLog() {
// Create a StampLog object with its data loaded from storage
return new tsl.StampLog(tsl.LOG_FILENAME, tsl.SETTINGS.maxLogLength);
}
Bangle.loadWidgets();
Bangle.drawWidgets();
var stampLog = loadStampLog();
E.on('kill', stampLog.save.bind(stampLog));
stampLog.on('saveError', saveErrorAlert);
var currentUI = new MainScreen(stampLog);
var mainUI = currentUI;
currentUI.start();

BIN
apps/timestamplog/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 851 B

BIN
apps/timestamplog/app.xcf Normal file

Binary file not shown.

View File

@ -0,0 +1,31 @@
<html>
<head>
<link rel="stylesheet" href="../../css/spectre.min.css">
</head>
<body>
<script src="../../core/lib/interface.js"></script>
<div id="content">Loading...</div>
<script>
function onInit() {
Util.readStorageJSON('timestamplog.json', stampLog => {
document.getElementById("content").innerHTML = HTMLStampLog(stampLog);
});
}
function HTMLStampLog(stampLog) {
html = ''
if (stampLog !== undefined && stampLog.length) {
html += '<ol>';
for (const logItem of stampLog) {
html += ('<li>' + new Date(logItem.stamp).toLocaleString() + '</li>');
}
html += '</ol>';
} else {
html += '(empty)';
}
console.log(html);
return html;
}
</script>
</body>
</html>

248
apps/timestamplog/lib.js Normal file
View File

@ -0,0 +1,248 @@
const storage = require('Storage');
// Storage filenames
const LOG_FILENAME = 'timestamplog.json';
const SETTINGS_FILENAME = 'timestamplog.settings.json';
// Settings
const SETTINGS = Object.assign({
logFont: '12x20',
logFontHSize: 1,
logFontVSize: 1,
maxLogLength: 30,
rotateLog: false,
buttonAction: 'Log time',
}, storage.readJSON(SETTINGS_FILENAME, true) || {});
const SETTINGS_BUTTON_ACTION = [
'Log time',
'Open settings',
'Quit app',
'Do nothing',
];
function fontSpec(name, hsize, vsize) {
return name + ':' + hsize + 'x' + vsize;
}
//// Data models ////
// High-level timestamp log object that provides an interface to the
// UI for managing log entries and automatically loading/saving
// changes to flash storage.
class StampLog {
constructor(filename, maxLength) {
// Name of file to save log to
this.filename = filename;
// Maximum entries for log before old entries are overwritten with
// newer ones
this.maxLength = maxLength;
// `true` when we have changes that need to be saved
this.isDirty = false;
// Wait at most this many msec upon first data change before
// saving (this is to avoid excessive writes to flash if several
// changes happen quickly; we wait a little bit so they can be
// rolled into a single write)
this.saveTimeout = 30000;
// setTimeout ID for scheduled save job
this.saveId = null;
// Underlying raw log data object. Outside this class it's
// recommended to use only the class methods to change it rather
// than modifying the object directly to ensure that changes are
// recognized and saved to storage.
this.log = [];
this.load();
}
// Read in the log data that is currently in storage
load() {
let log = storage.readJSON(this.filename, true);
if (!log) log = [];
// Convert stringified datetimes back into Date objects
for (let logEntry of log) {
logEntry.stamp = new Date(logEntry.stamp);
}
this.log = log;
}
// Write current log data to storage if anything needs to be saved
save() {
// Cancel any pending scheduled calls to save()
if (this.saveId) {
clearTimeout(this.saveId);
this.saveId = null;
}
if (this.isDirty) {
let logToSave = [];
for (let logEntry of this.log) {
// Serialize each Date object into an ISO string before saving
let newEntry = Object.assign({}, logEntry);
newEntry.stamp = logEntry.stamp.toISOString();
logToSave.push(newEntry);
}
if (storage.writeJSON(this.filename, logToSave)) {
console.log('stamplog: save to storage completed');
this.isDirty = false;
} else {
console.log('stamplog: save to storage FAILED');
this.emit('saveError');
}
} else {
console.log('stamplog: skipping save to storage because no changes made');
}
}
// Mark log as needing to be (re)written to storage
setDirty() {
this.isDirty = true;
if (!this.saveId) {
this.saveId = setTimeout(this.save.bind(this), this.saveTimeout);
}
}
// Add a timestamp for the current time to the end of the log
addEntry() {
// If log full, purge an old entry to make room for new one
if (this.maxLength) {
while (this.log.length + 1 > this.maxLength) {
this.log.shift();
}
}
// Add new entry
this.log.push({
stamp: new Date()
});
this.setDirty();
}
// Delete the log objects given in the array `entries` from the log
deleteEntries(entries) {
this.log = this.log.filter(entry => !entries.includes(entry));
this.setDirty();
}
// Does the log currently contain the maximum possible number of entries?
isFull() {
return this.log.length >= this.maxLength;
}
}
function launchSettingsMenu(backCb) {
const fonts = g.getFonts();
const stampLog = new StampLog(LOG_FILENAME, SETTINGS.maxLogLength);
function saveSettings() {
console.log('Saving timestamp log and settings');
stampLog.save();
if (!storage.writeJSON(SETTINGS_FILENAME, SETTINGS)) {
E.showAlert('Trouble saving settings');
}
}
E.on('kill', saveSettings);
function endMenu() {
saveSettings();
E.removeListener('kill', saveSettings);
backCb();
}
function topMenu() {
E.showMenu({
'': {
title: 'Stamplog',
back: endMenu,
},
'Log': logMenu,
'Appearance': appearanceMenu,
'Button': {
value: SETTINGS_BUTTON_ACTION.indexOf(SETTINGS.buttonAction),
min: 0, max: SETTINGS_BUTTON_ACTION.length - 1,
format: v => SETTINGS_BUTTON_ACTION[v],
onchange: v => {
SETTINGS.buttonAction = SETTINGS_BUTTON_ACTION[v];
},
},
});
}
function logMenu() {
E.showMenu({
'': {
title: 'Log',
back: topMenu,
},
'Max entries': {
value: SETTINGS.maxLogLength,
min: 5, max: 100, step: 5,
onchange: v => {
SETTINGS.maxLogLength = v;
stampLog.maxLength = v;
}
},
'Auto-delete oldest': {
value: SETTINGS.rotateLog,
onchange: v => {
SETTINGS.rotateLog = !SETTINGS.rotateLog;
}
},
'Clear log': doClearLog,
});
}
function appearanceMenu() {
E.showMenu({
'': {
title: 'Appearance',
back: topMenu,
},
'Log font': {
value: fonts.indexOf(SETTINGS.logFont),
min: 0, max: fonts.length - 1,
format: v => fonts[v],
onchange: v => {
SETTINGS.logFont = fonts[v];
},
},
'Log font H size': {
value: SETTINGS.logFontHSize,
min: 1, max: 50,
onchange: v => {
SETTINGS.logFontHSize = v;
},
},
'Log font V size': {
value: SETTINGS.logFontVSize,
min: 1, max: 50,
onchange: v => {
SETTINGS.logFontVSize = v;
},
},
});
}
function doClearLog() {
E.showPrompt('Erase ALL log entries?', {
title: 'Clear log',
buttons: {'Erase':1, "Don't":0}
}).then((yes) => {
if (yes) {
stampLog.deleteEntries(stampLog.log);
}
logMenu();
});
}
topMenu();
}
exports = {LOG_FILENAME, SETTINGS_FILENAME, SETTINGS, SETTINGS_BUTTON_ACTION, fontSpec, StampLog,
launchSettingsMenu};

View File

@ -0,0 +1,23 @@
{
"id": "timestamplog",
"name": "Timestamp log",
"shortName":"Timestamp log",
"icon": "app.png",
"version": "0.01",
"description": "Conveniently record a series of date/time stamps",
"screenshots": [ {"url": "screenshot.png" } ],
"readme": "README.md",
"tags": "timestamp, log",
"supports": ["BANGLEJS2"],
"interface": "interface.html",
"storage": [
{"name": "timestamplog.app.js", "url": "app.js"},
{"name": "timestamplog.img", "url": "app-icon.js", "evaluate": true},
{"name": "timestamplog", "url": "lib.js"},
{"name": "timestamplog.settings.js", "url": "settings.js"}
],
"data": [
{"name": "timestamplog.settings"},
{"name": "timestamplog.json"}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -0,0 +1,7 @@
const tsl = require('timestamplog');
(
function(backCb) {
tsl.launchSettingsMenu(backCb);
}
);

View File

@ -1 +1,2 @@
0.01: New Clock!
0.02: Clockinfos now save under correct name, and wrap correctly to >1 line

View File

@ -134,8 +134,8 @@ for (var i=0;i<10;i++)
if (g.stringWidth(txt) > options.w) // if too big, smaller font
g.setFont("LECO1976Regular14");
if (g.stringWidth(txt) > options.w) {// if still too big, split to 2 lines
var l = g.wrapString(txt, options.w);
txt = l.slice(0,2).join("\n") + (l.length>2)?"...":"";
var l = g.wrapString(txt, options.w-4);
txt = l.slice(0,2).join("\n") + ((l.length>2)?"...":"");
}
var x = options.x+options.w/2, y = options.y+54;
g.setColor(g.theme.bg).drawString(txt, x-2, y). // draw the text background
@ -147,12 +147,12 @@ for (var i=0;i<10;i++)
};
clockInfoMenuA = require("clock_info").addInteractive(clockInfoItems, {
app:"pebblepp",
app:"twotwoclock",
x : g.getWidth()-clockInfoW, y: 0, w: clockInfoW, h:clockInfoH,
draw : clockInfoDraw
});
clockInfoMenuB = require("clock_info").addInteractive(clockInfoItems, {
app:"pebblepp",
app:"twotwoclock",
x : g.getWidth()-clockInfoW, y: clockInfoH, w: clockInfoW, h:clockInfoH,
draw : clockInfoDraw
});

View File

@ -1,7 +1,7 @@
{ "id": "twotwoclock",
"name": "TwoTwo Clock",
"shortName":"22 Clock",
"version":"0.01",
"version":"0.02",
"description": "A clock with the time split over two lines, with custom backgrounds and two ClockInfos",
"icon": "icon.png",
"type": "clock",

View File

@ -1,2 +1,3 @@
0.01: Initial fork from hwid_a_battery_widget
0.02: Show battery percentage (instead of power) if charging
0.03: Use `power_usage` module

View File

@ -3,7 +3,7 @@
"name": "Battery power and percentage widget",
"shortName": "Batt Pwr",
"icon": "widget.png",
"version": "0.02",
"version": "0.03",
"type": "widget",
"supports": ["BANGLEJS2"],
"readme": "README.md",

View File

@ -20,13 +20,7 @@
function draw() {
var x = this.x;
var y = this.y;
var batt = E.getBattery();
var pwr = E.getPowerUsage();
var usage = 0;
for (var key in pwr.device) {
if (!/^(LCD|LED)/.test(key))
usage += pwr.device[key];
}
var _a = require("power_usage").get(), usage = _a.usage, hrsLeft = _a.hrsLeft, batt = _a.batt;
var pwrColour = powerColour(usage);
g.reset()
.setBgColor(g.theme.bg)
@ -43,9 +37,8 @@
txt = "".concat(batt, "%");
}
else {
var hrs = 175000 * batt / (100 * usage);
var days = hrs / 24;
txt = days >= 1 ? "".concat(Math.round(Math.min(days, 99)), "d") : "".concat(Math.round(hrs), "h");
var days = hrsLeft / 24;
txt = days >= 1 ? "".concat(Math.round(Math.min(days, 99)), "d") : "".concat(Math.round(hrsLeft), "h");
}
var txth = 14;
g.setColor(g.theme.fg);

View File

@ -22,13 +22,7 @@
let x = this.x!;
let y = this.y!;
const batt = E.getBattery();
const pwr = E.getPowerUsage();
let usage = 0;
for(const key in pwr.device){
if(!/^(LCD|LED)/.test(key))
usage += pwr.device[key];
}
const { usage, hrsLeft, batt } = require("power_usage").get();
const pwrColour = powerColour(usage);
g.reset()
@ -47,10 +41,8 @@
if(showPct || Bangle.isCharging()){
txt = `${batt}%`;
}else{
// 175mAh, scaled based on battery (batt/100), scaled down based on usage
const hrs = 175000 * batt / (100 * usage);
const days = hrs / 24;
txt = days >= 1 ? `${Math.round(Math.min(days, 99))}d` : `${Math.round(hrs)}h`;
const days = hrsLeft / 24;
txt = days >= 1 ? `${Math.round(Math.min(days, 99))}d` : `${Math.round(hrsLeft)}h`;
}
// draw time remaining, then shade it based on batt %

15
modules/power_usage.js Normal file
View File

@ -0,0 +1,15 @@
exports.get = function () {
var pwr = E.getPowerUsage();
var batt = E.getBattery();
var usage = 0;
for (var key in pwr.device) {
if (!key.startsWith("LCD"))
usage += pwr.device[key];
}
var hrsLeft = 175000 * batt / (100 * usage);
return {
usage: usage,
hrsLeft: hrsLeft,
batt: batt,
};
};

29
modules/power_usage.ts Normal file
View File

@ -0,0 +1,29 @@
type Pwr = {
usage: number,
hrsLeft: number,
batt: number, // battery percentage
};
// eslint-disable-next-line no-unused-vars
type PowerUsageModule = {
get: () => Pwr,
};
exports.get = (): Pwr => {
const pwr = E.getPowerUsage();
const batt = E.getBattery();
let usage = 0;
for(const key in pwr.device){
if(!key.startsWith("LCD"))
usage += pwr.device[key as keyof typeof pwr.device]!;
}
// 175mAh, scaled based on battery (batt/100), scaled down based on usage
const hrsLeft = 175000 * batt / (100 * usage);
return {
usage,
hrsLeft,
batt,
};
};

View File

@ -316,6 +316,48 @@ type VariableSizeInformation = {
more?: VariableSizeInformation;
};
type PowerUsage = {
total: number,
device: {
CPU?: number,
UART?: number,
PWM?: number,
LED1?: number,
LED2?: number,
LED3?: number,
// bangle
LCD?: number,
LCD_backlight?: number,
LCD_touch?: number,
HRM?: number,
GPS?: number,
compass?: number,
baro?: number,
// nrf
BLE_periph?: number,
BLE_central?: number,
BLE_advertise?: number,
BLE_scan?: number,
// pixljs
//LCD?: number, // (see above)
// puck
mag?: number,
accel?: number,
// jolt
driver0?: number,
driver1?: number,
pin0_internal_resistance?: number,
pin2_internal_resistance?: number,
pin4_internal_resistance?: number,
pin6_internal_resistance?: number,
},
};
type PipeOptions = {
chunkSize?: number,
end?: boolean,
@ -1014,13 +1056,15 @@ declare class NRF {
static eraseBonds(callback?: any): void;
/**
* Get this device's default Bluetooth MAC address.
* Get this device's default or current Bluetooth MAC address.
* For Puck.js, the last 5 characters of this (e.g. `ee:ff`) are used in the
* device's advertised Bluetooth name.
*
* @param {boolean} current - If true, return the current address rather than the default
* @returns {any} MAC address - a string of the form 'aa:bb:cc:dd:ee:ff'
* @url http://www.espruino.com/Reference#l_NRF_getAddress
*/
static getAddress(): any;
static getAddress(current: boolean): any;
/**
* Set this device's default Bluetooth MAC address:
@ -4524,10 +4568,32 @@ declare class Bangle {
static dbg(): any;
/**
* Writes a register on the accelerometer
* Writes a register on the touch controller
*
* @param {number} reg
* @param {number} data
* @url http://www.espruino.com/Reference#l_Bangle_touchWr
*/
static touchWr(reg: number, data: number): void;
/**
* Reads a register from the touch controller
* **Note:** On Espruino 2v06 and before this function only returns a number (`cnt`
* is ignored).
*
* @param {number} reg - Register number to read
* @param {number} cnt - If specified, returns an array of the given length (max 128). If not (or 0) it returns a number
* @returns {any}
* @url http://www.espruino.com/Reference#l_Bangle_touchRd
*/
static touchRd(reg: number, cnt?: 0): number;
static touchRd(reg: number, cnt: number): number[];
/**
* Writes a register on the accelerometer
*
* @param {number} reg - Register number to write
* @param {number} data - An integer value to write to the register
* @url http://www.espruino.com/Reference#l_Bangle_accelWr
*/
static accelWr(reg: number, data: number): void;
@ -8343,7 +8409,7 @@ declare class E {
* E.showScroller({
* h : 40, c : 8,
* draw : (idx, r) => {
* g.setBgColor((idx&1)?"#666":"#999").clearRect(r.x,r.y,r.x+r.w-1,r.y+r.h-1);
* g.setBgColor((idx&1)?"#666":"#CCC").clearRect(r.x,r.y,r.x+r.w-1,r.y+r.h-1);
* g.setFont("6x8:2").drawString("Item Number\n"+idx,r.x+10,r.y+4);
* },
* select : (idx) => console.log("You selected ", idx)
@ -9482,7 +9548,7 @@ declare class E {
* @returns {any} An object detailing power usage in microamps
* @url http://www.espruino.com/Reference#l_E_getPowerUsage
*/
static getPowerUsage(): any;
static getPowerUsage(): PowerUsage;
/**
* Decode a UTF8 string.
@ -11137,21 +11203,21 @@ interface String {
* Return the index of substring in this string, or -1 if not found
*
* @param {any} substring - The string to search for
* @param {any} fromIndex - Index to search from
* @param {any} [fromIndex] - [optional] Index to search from
* @returns {number} The index of the string, or -1 if not found
* @url http://www.espruino.com/Reference#l_String_indexOf
*/
indexOf(substring: any, fromIndex: any): number;
indexOf(substring: any, fromIndex?: any): number;
/**
* Return the last index of substring in this string, or -1 if not found
*
* @param {any} substring - The string to search for
* @param {any} fromIndex - Index to search from
* @param {any} [fromIndex] - [optional] Index to search from
* @returns {number} The index of the string, or -1 if not found
* @url http://www.espruino.com/Reference#l_String_lastIndexOf
*/
lastIndexOf(substring: any, fromIndex: any): number;
lastIndexOf(substring: any, fromIndex?: any): number;
/**
* Matches an occurrence `subStr` in the string.
@ -11206,20 +11272,20 @@ interface String {
/**
*
* @param {number} start - The start character index (inclusive)
* @param {any} end - The end character index (exclusive)
* @param {any} [end] - [optional] The end character index (exclusive)
* @returns {any} The part of this string between start and end
* @url http://www.espruino.com/Reference#l_String_substring
*/
substring(start: number, end: any): any;
substring(start: number, end?: any): any;
/**
*
* @param {number} start - The start character index
* @param {any} len - The number of characters
* @param {any} [len] - [optional] The number of characters
* @returns {any} Part of this string from start for len characters
* @url http://www.espruino.com/Reference#l_String_substr
*/
substr(start: number, len: any): any;
substr(start: number, len?: any): any;
/**
*
@ -11286,29 +11352,29 @@ interface String {
/**
*
* @param {any} searchString - The string to search for
* @param {number} position - The start character index (or 0 if not defined)
* @param {number} [position] - [optional] The start character index (or 0 if not defined)
* @returns {boolean} `true` if the given characters are found at the beginning of the string, otherwise, `false`.
* @url http://www.espruino.com/Reference#l_String_startsWith
*/
startsWith(searchString: any, position: number): boolean;
startsWith(searchString: any, position?: number): boolean;
/**
*
* @param {any} searchString - The string to search for
* @param {any} length - The 'end' of the string - if left off the actual length of the string is used
* @param {any} [length] - [optional] The 'end' of the string - if left off the actual length of the string is used
* @returns {boolean} `true` if the given characters are found at the end of the string, otherwise, `false`.
* @url http://www.espruino.com/Reference#l_String_endsWith
*/
endsWith(searchString: any, length: any): boolean;
endsWith(searchString: any, length?: any): boolean;
/**
*
* @param {any} substring - The string to search for
* @param {any} fromIndex - The start character index (or 0 if not defined)
* @param {any} [fromIndex] - [optional] The start character index (or 0 if not defined)
* @returns {boolean} `true` if the given characters are in the string, otherwise, `false`.
* @url http://www.espruino.com/Reference#l_String_includes
*/
includes(substring: any, fromIndex: any): boolean;
includes(substring: any, fromIndex?: any): boolean;
/**
* Repeat this string the given number of times.
@ -11411,8 +11477,10 @@ interface RegExp {
/**
* The built-in class for handling Regular Expressions
* **Note:** Espruino's regular expression parser does not contain all the features
* present in a full ES6 JS engine. However it does contain support for the all the
* basics.
* present in a full ES6 JS engine. however some parts of the spec are not implemented:
* * [Assertions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Assertions) other than `^` and `$`
* * [Numeric quantifiers](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Quantifiers) (eg `x{3}`)
* There's a GitHub issue [concerning RegExp features here](https://github.com/espruino/Espruino/issues/1257)
* @url http://www.espruino.com/Reference#RegExp
*/
declare const RegExp: RegExpConstructor

View File

@ -5,3 +5,4 @@ declare function require(moduleName: "sched"): typeof Sched;
declare function require(moduleName: "ClockFace"): typeof ClockFace_.ClockFace;
declare function require(moduleName: "clock_info"): typeof ClockInfo;
declare function require(moduleName: "Layout"): typeof Layout.Layout;
declare function require(moduleName: "power_usage"): PowerUsageModule;