Merge remote-tracking branch 'upstream/master'
|
@ -12,3 +12,4 @@
|
|||
0.11: Setting to use "Today" and "Yesterday" instead of dates
|
||||
Added dynamic, short and range fields to clkinfo
|
||||
0.12: Added color field and updating clkinfo periodically (running events)
|
||||
0.13: Show day of the week in date
|
||||
|
|
|
@ -34,8 +34,9 @@ function getDate(timestamp) {
|
|||
return new Date(timestamp*1000);
|
||||
}
|
||||
function formatDay(date) {
|
||||
let formattedDate = Locale.dow(date,1) + " " + Locale.date(date).replace(/\d\d\d\d/,"");
|
||||
if (!settings.useToday) {
|
||||
return Locale.date(date);
|
||||
return formattedDate;
|
||||
}
|
||||
const dateformatted = date.toISOString().split('T')[0]; // yyyy-mm-dd
|
||||
const today = new Date(Date.now()).toISOString().split('T')[0]; // yyyy-mm-dd
|
||||
|
@ -46,7 +47,7 @@ function formatDay(date) {
|
|||
if (dateformatted == tomorrow) {
|
||||
return /*LANG*/"Tomorrow ";
|
||||
}
|
||||
return Locale.date(date);
|
||||
return formattedDate;
|
||||
}
|
||||
}
|
||||
function formatDateLong(date, includeDay, allDay) {
|
||||
|
@ -58,7 +59,7 @@ function formatDateLong(date, includeDay, allDay) {
|
|||
return shortTime;
|
||||
}
|
||||
function formatDateShort(date, allDay) {
|
||||
return formatDay(date).replace(/\d\d\d\d/,"")+(allDay?"":Locale.time(date,1)+Locale.meridian(date));
|
||||
return formatDay(date)+(allDay?"":Locale.time(date,1)+Locale.meridian(date));
|
||||
}
|
||||
|
||||
var lines = [];
|
||||
|
@ -75,25 +76,29 @@ function showEvent(ev) {
|
|||
if (titleCnt) lines.push(""); // add blank line after title
|
||||
if(start.getDay() == end.getDay() && start.getMonth() == end.getMonth())
|
||||
includeDay = false;
|
||||
if(includeDay || ev.allDay) {
|
||||
if(includeDay && ev.allDay) {
|
||||
//single day all day (average to avoid getting previous day)
|
||||
lines = lines.concat(
|
||||
/*LANG*/"Start:",
|
||||
g.wrapString(formatDateLong(new Date((start+end)/2), includeDay, ev.allDay), g.getWidth()-10));
|
||||
} else if(includeDay || ev.allDay) {
|
||||
lines = lines.concat(
|
||||
/*LANG*/"Start"+":",
|
||||
g.wrapString(formatDateLong(start, includeDay, ev.allDay), g.getWidth()-10),
|
||||
/*LANG*/"End:",
|
||||
/*LANG*/"End"+":",
|
||||
g.wrapString(formatDateLong(end, includeDay, ev.allDay), g.getWidth()-10));
|
||||
} else {
|
||||
lines = lines.concat(
|
||||
g.wrapString(Locale.date(start), g.getWidth()-10),
|
||||
g.wrapString(formatDateShort(start,true), g.getWidth()-10),
|
||||
g.wrapString(/*LANG*/"Start"+": "+formatDateLong(start, includeDay, ev.allDay), g.getWidth()-10),
|
||||
g.wrapString(/*LANG*/"End"+": "+formatDateLong(end, includeDay, ev.allDay), g.getWidth()-10));
|
||||
}
|
||||
if(ev.location)
|
||||
lines = lines.concat(/*LANG*/"Location"+": ", g.wrapString(ev.location, g.getWidth()-10));
|
||||
if(ev.description)
|
||||
lines = lines.concat("",/*LANG*/"Location"+": ", g.wrapString(ev.location, g.getWidth()-10));
|
||||
if(ev.description && ev.description.trim())
|
||||
lines = lines.concat("",g.wrapString(ev.description, g.getWidth()-10));
|
||||
if(ev.calName)
|
||||
lines = lines.concat(/*LANG*/"Calendar"+": ", g.wrapString(ev.calName, g.getWidth()-10));
|
||||
lines = lines.concat(["",/*LANG*/"< Back"]);
|
||||
lines = lines.concat("",/*LANG*/"Calendar"+": ", g.wrapString(ev.calName, g.getWidth()-10));
|
||||
lines = lines.concat("",/*LANG*/"< Back");
|
||||
E.showScroller({
|
||||
h : g.getFontHeight(), // height of each menu item in pixels
|
||||
c : lines.length, // number of menu items
|
||||
|
@ -120,7 +125,7 @@ function showList() {
|
|||
CALENDAR = CALENDAR.filter(ev=>ev.timestamp + ev.durationInSeconds > now/1000);
|
||||
}
|
||||
if(CALENDAR.length == 0) {
|
||||
E.showMessage("No events");
|
||||
E.showMessage(/*LANG*/"No events");
|
||||
return;
|
||||
}
|
||||
E.showScroller({
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "agenda",
|
||||
"name": "Agenda",
|
||||
"version": "0.12",
|
||||
"version": "0.13",
|
||||
"description": "Simple agenda",
|
||||
"icon": "agenda.png",
|
||||
"screenshots": [{"url":"screenshot_agenda_overview.png"}, {"url":"screenshot_agenda_event1.png"}, {"url":"screenshot_agenda_event2.png"}],
|
||||
|
|
|
@ -2,3 +2,4 @@
|
|||
0.02: Update to work with Bangle.js 2
|
||||
0.03: Select GNSS systems to use for Bangle.js 2
|
||||
0.04: Now turns GPS off after upload
|
||||
0.05: Fix regression in 0.04 that caused AGPS data not to get loaded
|
||||
|
|
|
@ -158,7 +158,7 @@
|
|||
var chunk = bin.substr(i,chunkSize);
|
||||
js += `\x10Serial1.write(atob("${btoa(chunk)}"))\n`;
|
||||
}
|
||||
js = "\x10setTimeout(() => Bangle.setGPSPower(0,'agps'), 1000);\n"; // turn GPS off after a delay
|
||||
js += "\x10setTimeout(() => Bangle.setGPSPower(0,'agps'), 1000);\n"; // turn GPS off after a delay
|
||||
return js;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "assistedgps",
|
||||
"name": "Assisted GPS Updater (AGPS)",
|
||||
"version": "0.04",
|
||||
"version": "0.05",
|
||||
"description": "Downloads assisted GPS (AGPS) data to Bangle.js for faster GPS startup and more accurate fixes. **No app will be installed**, this just uploads new data to the GPS chip.",
|
||||
"sortorder": -1,
|
||||
"icon": "app.png",
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
0.01: New App!
|
||||
0.02: Don't fire if the app uses swipes already.
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
Service that allows you to use an app's back button using left to right swipe gesture.
|
||||
|
||||
## Settings
|
||||
|
||||
Mode: Blacklist/Whitelist/Always On/Disabled
|
||||
App List: Black-/whitelisted apps
|
||||
Standard # of swipe handlers: 0-10 (Default: 0, must be changed for backswipe to work at all)
|
||||
Standard # of drag handlers: 0-10 (Default: 0, must be changed for backswipe to work at all)
|
||||
|
||||
|
||||
Standard # of handlers settings are used to fine tune when backswipe should trigger the back function. E.g. when using a keyboard that works on drags, we don't want the backswipe to trigger when we just wanted to select a letter. This might not be able to cover all cases however.
|
||||
|
||||
## Creator
|
||||
Kedlub
|
||||
|
||||
## Contributors
|
||||
thyttan
|
|
@ -15,18 +15,28 @@
|
|||
|
||||
var currentFile = global.__FILE__ || "";
|
||||
|
||||
if(global.BACK) delete global.BACK;
|
||||
if (global.BACK) delete global.BACK;
|
||||
if (options && options.back && enabledForApp(currentFile)) {
|
||||
global.BACK = options.back;
|
||||
}
|
||||
setUI(mode, cb);
|
||||
};
|
||||
|
||||
function goBack(lr, ud) {
|
||||
function countHandlers(eventType) {
|
||||
if (Bangle["#on"+eventType] === undefined) {
|
||||
return 0;
|
||||
} else if (Bangle["#on"+eventType] instanceof Array) {
|
||||
return Bangle["#on"+eventType].length;
|
||||
} else if (Bangle["#on"+eventType] !== undefined) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
function goBack(lr, _) {
|
||||
// if it is a left to right swipe
|
||||
if (lr === 1) {
|
||||
// if we're in an app that has a back button, run the callback for it
|
||||
if (global.BACK) {
|
||||
if (global.BACK && countHandlers("swipe")<=settings.standardNumSwipeHandlers && countHandlers("drag")<=settings.standardNumDragHandlers) {
|
||||
global.BACK();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{ "id": "backswipe",
|
||||
"name": "Back Swipe",
|
||||
"shortName":"BackSwipe",
|
||||
"version":"0.01",
|
||||
"version":"0.02",
|
||||
"description": "Service that allows you to use an app's back button using left to right swipe gesture",
|
||||
"icon": "app.png",
|
||||
"tags": "back,gesture,swipe",
|
||||
|
|
|
@ -4,18 +4,20 @@
|
|||
// Apps is an array of app info objects, where all the apps that are there are either blocked or allowed, depending on the mode
|
||||
var DEFAULTS = {
|
||||
'mode': 0,
|
||||
'apps': []
|
||||
'apps': [],
|
||||
'standardNumSwipeHandlers': 0,
|
||||
'standardNumDragHandlers': 0
|
||||
};
|
||||
|
||||
var settings = {};
|
||||
|
||||
var loadSettings = function() {
|
||||
settings = require('Storage').readJSON(FILE, 1) || DEFAULTS;
|
||||
}
|
||||
};
|
||||
|
||||
var saveSettings = function(settings) {
|
||||
require('Storage').write(FILE, settings);
|
||||
}
|
||||
};
|
||||
|
||||
// Get all app info files
|
||||
var getApps = function() {
|
||||
|
@ -35,7 +37,7 @@
|
|||
return 0;
|
||||
});
|
||||
return apps;
|
||||
}
|
||||
};
|
||||
|
||||
var showMenu = function() {
|
||||
var menu = {
|
||||
|
@ -55,11 +57,31 @@
|
|||
},
|
||||
'App List': () => {
|
||||
showAppSubMenu();
|
||||
},
|
||||
'Standard # of swipe handlers' : { // If more than this many handlers are present backswipe will not go back
|
||||
value: 0|settings.standardNumSwipeHandlers,
|
||||
min: 0,
|
||||
max: 10,
|
||||
format: v=>v,
|
||||
onchange: v => {
|
||||
settings.standardNumSwipeHandlers = v;
|
||||
saveSettings(settings);
|
||||
},
|
||||
},
|
||||
'Standard # of drag handlers' : { // If more than this many handlers are present backswipe will not go back
|
||||
value: 0|settings.standardNumDragHandlers,
|
||||
min: 0,
|
||||
max: 10,
|
||||
format: v=>v,
|
||||
onchange: v => {
|
||||
settings.standardNumDragHandlers = v;
|
||||
saveSettings(settings);
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
E.showMenu(menu);
|
||||
}
|
||||
};
|
||||
|
||||
var showAppSubMenu = function() {
|
||||
var menu = {
|
||||
|
|
|
@ -33,14 +33,11 @@ if (s.ble!==false) {
|
|||
}
|
||||
}
|
||||
// settings.log 0-off, 1-display, 2-log, 3-both
|
||||
if (s.log>=2) { // logging to file
|
||||
boot += `_DBGLOG=require("Storage").open("log.txt","a");
|
||||
`;
|
||||
} if (s.blerepl===false) { // If not programmable, force terminal off Bluetooth
|
||||
if (s.log>=2) boot += `_DBGLOG=require("Storage").open("log.txt","a");
|
||||
LoopbackB.on('data',function(d) {_DBGLOG.write(d);Terminal.write(d);});
|
||||
if (s.blerepl===false) { // If not programmable, force terminal off Bluetooth
|
||||
if (s.log>=2) { boot += `_DBGLOG=require("Storage").open("log.txt","a");
|
||||
LoopbackB.on('data',function(d) {_DBGLOG.write(d);${(s.log==3)?"Terminal.write(d);":""}});
|
||||
LoopbackA.setConsole(true);\n`;
|
||||
else if (s.log==1||s.log==3) boot += `Terminal.setConsole(true);\n`; // if showing debug, force REPL onto terminal
|
||||
} else if (s.log==1) boot += `Terminal.setConsole(true);\n`; // if showing debug, force REPL onto terminal
|
||||
else boot += `E.setConsole(null,{force:true});\n`; // on new (2v05+) firmware we have E.setConsole which allows a 'null' console
|
||||
/* If not programmable add our own handler for Bluetooth data
|
||||
to allow Gadgetbridge commands to be received*/
|
||||
|
@ -58,9 +55,9 @@ Bluetooth.on('line',function(l) {
|
|||
});\n`;
|
||||
} else {
|
||||
if (s.log>=2) boot += `_DBGLOG=require("Storage").open("log.txt","a");
|
||||
LoopbackB.on('data',function(d) {_DBGLOG.write(d);Terminal.write(d);});
|
||||
LoopbackB.on('data',function(d) {_DBGLOG.write(d);${(s.log==3)?"Terminal.write(d);":""}});
|
||||
if (!NRF.getSecurityStatus().connected) LoopbackA.setConsole();\n`;
|
||||
else if (s.log==1||s.log==3) boot += `if (!NRF.getSecurityStatus().connected) Terminal.setConsole();\n`; // if showing debug, put REPL on terminal (until connection)
|
||||
else if (s.log==1) boot += `if (!NRF.getSecurityStatus().connected) Terminal.setConsole();\n`; // if showing debug, put REPL on terminal (until connection)
|
||||
else boot += `Bluetooth.setConsole(true);\n`; // else if no debug, force REPL to Bluetooth
|
||||
}
|
||||
// we just reset, so BLE should be on.
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
0.01: New App!
|
|
@ -0,0 +1,28 @@
|
|||
# LEGO Remote control
|
||||
|
||||
This app allows you to control LEGO models from Bangle.js
|
||||
|
||||
Right now the only supported control device is the Mould King M-0006
|
||||
Bluetooth remote for LEGO Power Functions: http://www.espruino.com/LEGO+Power+Functions+Clone
|
||||
|
||||
LEGO Power Functions does not have an official Bluetooth remote controller. Hopefully
|
||||
in the future this app will be able to support other types of remote (see below).
|
||||
|
||||
## Usage
|
||||
|
||||
Run the app, and ensure you're not connected to your watch via Bluetooth
|
||||
(a warning will pop up if so).
|
||||
|
||||
Now press the arrow keys on the screen to control the robot.
|
||||
|
||||
It is expected that the robot is controlled by two motors, one on the left
|
||||
side (connected to the `A` output) and one on the right (connected to the `B` output).
|
||||
|
||||
## Future additions
|
||||
|
||||
In the future it would be great to add:
|
||||
|
||||
* Recording a series of movements and playing them back
|
||||
* Support for official LEGO bluetooth remotes (via [Pybricks](https://pybricks.com/))
|
||||
* Support for different robot styles and configurations
|
||||
* Using the Bangle's compass (or even GPS) to allow better robot control.
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEw4X/8H5A4NH/4ABJf4A/AFMC1Uq1QABhWqBYYJDAAegBYWqxWogWohQBCBYWpqsVqoABytVBYUC0u5rf5rf1q/xtQLBhWW/t//2/AYVKBYWX/u//z3B3//rRGCr/1/+H/v9/1/BYn2/lT+v6/oXDlF/4//9/78/5F4epv/X//f7/3+v9I4Wp38b/9v7//+4LD0P/HgN/7f/EgMoBYOlJ4IACDAP1O4QLH6ibCBYI7Br/+qf/iwLC1NIquhq2lquprWAWQVVoNVgtU0NVlQLCZQ7XDbgLWJEgOCdgLZBdwgA/AH4AaA"))
|
|
@ -0,0 +1,70 @@
|
|||
var lego = require("mouldking");
|
||||
lego.start();
|
||||
E.on('kill', () => {
|
||||
// return to normal Bluetooth advertising
|
||||
NRF.setAdvertising({},{showName:true});
|
||||
});
|
||||
// You must leave one second after 'start' to allow the remote to be paired
|
||||
|
||||
var arrowIcon = atob("IiiBAAAAwAAAAPwAAAB/gAAAP/AAAB/+AAAP/8AAB//4AAP//wAA///gAH///AA///8AH///4A////wH////gf////D////8f////5/////n////+f////4AP/8AAA//wAAD//AAAP/8AAA//wAAD//AAAH/8AAAf/wAAB//AAAH/8AAAf/gAAB/+AAAH/4AAAf/gAAB/+AAAH/4AAAP/gAAA/+AAAD/wAAAD8AA");
|
||||
var controlState = "";
|
||||
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
var R = Bangle.appRect;
|
||||
// we'll divide up into 3x3
|
||||
function getBoxCoords(x,y) {
|
||||
return {
|
||||
x : R.x + R.w*x/3,
|
||||
y : R.y + R.h*y/3
|
||||
};
|
||||
}
|
||||
|
||||
function draw() {
|
||||
g.reset().clearRect(R);
|
||||
var c, ninety = Math.PI/2;
|
||||
var colOn = "#f00", colOff = g.theme.fg;
|
||||
c = getBoxCoords(1.5, 0.5);
|
||||
g.setColor(controlState=="up"?colOn:colOff).drawImage(arrowIcon,c.x,c.y,{rotate:0});
|
||||
c = getBoxCoords(2.5, 1.5);
|
||||
g.setColor(controlState=="right"?colOn:colOff).drawImage(arrowIcon,c.x,c.y,{rotate:ninety});
|
||||
c = getBoxCoords(0.5, 1.5);
|
||||
g.setColor(controlState=="left"?colOn:colOff).drawImage(arrowIcon,c.x,c.y,{rotate:-ninety});
|
||||
c = getBoxCoords(1.5, 1.5);
|
||||
g.setColor(controlState=="down"?colOn:colOff).drawImage(arrowIcon,c.x,c.y,{rotate:ninety*2});
|
||||
if (NRF.getSecurityStatus().connected) {
|
||||
c = getBoxCoords(1.5, 2.5);
|
||||
g.setFontAlign(0,0).setFont("6x8").drawString("WARNING:\nBluetooth Connected\nYou must disconnect\nbefore LEGO will work",c.x,c.y);
|
||||
}
|
||||
}
|
||||
draw();
|
||||
NRF.on('connect', draw);
|
||||
NRF.on('disconnect', draw);
|
||||
|
||||
function setControlState(s) {
|
||||
controlState = s;
|
||||
var c = {};
|
||||
var speed = 3;
|
||||
if (s=="up") c={a:-speed,b:-speed};
|
||||
if (s=="down") c={a:speed,b:speed};
|
||||
if (s=="left") c={a:speed,b:-speed};
|
||||
if (s=="right") c={a:-speed,b:speed};
|
||||
draw();
|
||||
lego.set(c);
|
||||
}
|
||||
|
||||
Bangle.on('drag',e => {
|
||||
var x = Math.floor(E.clip((e.x - R.x) * 3 / R.w,0,2.99));
|
||||
var y = Math.floor(E.clip((e.y - R.y) * 3 / R.h,0,2.99));
|
||||
if (!e.b) {
|
||||
setControlState("");
|
||||
return;
|
||||
}
|
||||
if (y==0) { // top row
|
||||
if (x==1) setControlState("up");
|
||||
} else if (y==1) {
|
||||
if (x==0) setControlState("left");
|
||||
if (x==1) setControlState("down");
|
||||
if (x==2) setControlState("right");
|
||||
}
|
||||
});
|
After Width: | Height: | Size: 14 KiB |
|
@ -0,0 +1,14 @@
|
|||
{ "id": "legoremote",
|
||||
"name": "LEGO Remote control",
|
||||
"shortName":"LEGO Remote",
|
||||
"version":"0.01",
|
||||
"description": "Use your Bangle.js to control LEGO models. See the README for compatibility",
|
||||
"icon": "app.png",
|
||||
"tags": "toy,lego,bluetooth",
|
||||
"supports" : ["BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
"storage": [
|
||||
{"name":"legoremote.app.js","url":"app.js"},
|
||||
{"name":"legoremote.img","url":"app-icon.js","evaluate":true}
|
||||
]
|
||||
}
|
|
@ -8,6 +8,7 @@
|
|||
"tags": "xiaomi,mi,ble,bluetooth,thermometer,humidity",
|
||||
"readme": "README.md",
|
||||
"supports": ["BANGLEJS", "BANGLEJS2"],
|
||||
"dependencies": {"textinput":"type"},
|
||||
"storage": [
|
||||
{"name":"mitherm.app.js","url":"app.js"},
|
||||
{"name":"mitherm.img","url":"app-icon.js","evaluate":true}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
0.01: New App!
|
||||
0.02: Get weather from weather.json
|
||||
0.03: Address unexpected undefined when reading weather.json
|
|
@ -4,7 +4,7 @@ Based on the Pebble watchface Weather Land.
|
|||
|
||||
Mountain Pass Clock changes depending on time (day/night) and weather conditions.
|
||||
|
||||
This clock requires Gadgetbridge and an app that Gadgetbridge can use to get the current weather from OpenWeatherMap (e.g. Weather Notification). To set up Gadgetbridge and weather, see https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Weather.
|
||||
This clock requires Gadgetbridge and an app that Gadgetbridge can use to get the current weather from OpenWeatherMap (e.g. Weather Notification), or a Bangle app that will update weather.json such as OWM Weather. To set up Gadgetbridge and weather, see https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Weather.
|
||||
|
||||
The scene will change according to the following OpenWeatherMap conditions: clear, cloudy, overcast, lightning, drizzle, rain, fog and snow. Each weather condition has night/day scenes.
|
||||
|
||||
|
|
|
@ -323,11 +323,28 @@ function setWeather() {
|
|||
draw(a);
|
||||
}
|
||||
|
||||
function readWeather() {
|
||||
var weatherJson = require("Storage").readJSON('weather.json', 1);
|
||||
// save updated weather data if available and it has been an hour since last updated
|
||||
if (weatherJson && weatherJson.weather && weatherJson.weather.time && (data.time === undefined || (data.time + 3600000) < weatherJson.weather.time)) {
|
||||
data = {
|
||||
time: weatherJson.weather.time,
|
||||
temp: weatherJson.weather.temp,
|
||||
code: weatherJson.weather.code
|
||||
};
|
||||
require("Storage").writeJSON('mtnclock.json', data);
|
||||
}
|
||||
}
|
||||
|
||||
const _GB = global.GB;
|
||||
global.GB = (event) => {
|
||||
if (event.t==="weather") {
|
||||
data = event;
|
||||
require("Storage").write('mtnclock.json', event);
|
||||
data = {
|
||||
temp: event.temp,
|
||||
code: event.code,
|
||||
time: Date.now()
|
||||
};
|
||||
require("Storage").writeJSON('mtnclock.json', data);
|
||||
setWeather();
|
||||
}
|
||||
if (_GB) setTimeout(_GB, 0, event);
|
||||
|
@ -340,11 +357,13 @@ function queueDraw() {
|
|||
if (drawTimeout) clearTimeout(drawTimeout);
|
||||
drawTimeout = setTimeout(function() {
|
||||
drawTimeout = undefined;
|
||||
readWeather();
|
||||
setWeather();
|
||||
queueDraw();
|
||||
}, 60000 - (Date.now() % 60000));
|
||||
}
|
||||
|
||||
queueDraw();
|
||||
readWeather();
|
||||
setWeather();
|
||||
Bangle.setUI("clock");
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"id": "mtnclock",
|
||||
"name": "Mountain Pass Clock",
|
||||
"shortName": "Mtn Clock",
|
||||
"version": "0.01",
|
||||
"version": "0.03",
|
||||
"description": "A clock that changes scenery based on time and weather.",
|
||||
"readme":"README.md",
|
||||
"icon": "app.png",
|
||||
|
|
|
@ -3,3 +3,4 @@
|
|||
0.03: Use default Bangle formatter for booleans
|
||||
0.04: Remove calibration with current voltage (Calibrate->Auto) as it is now handled by settings app
|
||||
Allow automatic calibration on every charge longer than 3 hours
|
||||
0.05: Add back button to settings menu.
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"id": "powermanager",
|
||||
"name": "Power Manager",
|
||||
"shortName": "Power Manager",
|
||||
"version": "0.04",
|
||||
"version": "0.05",
|
||||
"description": "Allow configuration of warnings and thresholds for battery charging and display.",
|
||||
"icon": "app.png",
|
||||
"type": "bootloader",
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
'': {
|
||||
'title': 'Power Manager'
|
||||
},
|
||||
"< Back" : back,
|
||||
'Monotonic percentage': {
|
||||
value: !!settings.forceMonoPercentage,
|
||||
onchange: v => {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
0.01: New App!
|
||||
0.02: Fix fast loading on swipe to clock
|
||||
0.03: Adds a setting for going back to clock on a timeout
|
||||
0.04: Fix timeouts closing fast loaded apps
|
|
@ -111,6 +111,7 @@ let layout = new Layout({
|
|||
remove: ()=>{
|
||||
Bangle.removeListener("swipe", onSwipe);
|
||||
Bangle.removeListener("touch", updateTimeout);
|
||||
if (timeout) clearTimeout(timeout);
|
||||
delete Graphics.prototype.setFont8x12;
|
||||
}
|
||||
});
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"id": "qcenter",
|
||||
"name": "Quick Center",
|
||||
"shortName": "QCenter",
|
||||
"version": "0.03",
|
||||
"version": "0.04",
|
||||
"description": "An app for quickly launching your favourite apps, inspired by the control centres of other watches.",
|
||||
"icon": "app.png",
|
||||
"tags": "",
|
||||
|
|
|
@ -0,0 +1,223 @@
|
|||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="../../css/spectre.min.css">
|
||||
<link rel="stylesheet" href="../../css/spectre-icons.min.css">
|
||||
<script src="../../core/lib/interface.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/ical.js/0.0.3/ical.min.js"></script>
|
||||
<script>
|
||||
let dataElement = document.getElementById("data");
|
||||
let alarms;
|
||||
let schedSettings;
|
||||
|
||||
function readFile(input) {
|
||||
document.getElementById('upload').disabled = true;
|
||||
const offsetMinutes = document.getElementById("offsetMinutes").value;
|
||||
|
||||
for(let i=0; i<input.files.length; i++) {
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener("load", () => {
|
||||
const jCalData = ICAL.parse(reader.result);
|
||||
const comp = new ICAL.Component(jCalData[1]);
|
||||
// Fetch the VEVENT part
|
||||
comp.getAllSubcomponents('vevent').forEach(vevent => {
|
||||
event = new ICAL.Event(vevent);
|
||||
const exists = alarms.some(alarm => alarm.id === event.uid);
|
||||
|
||||
const alarm = eventToAlarm(event, offsetMinutes*60*1000);
|
||||
renderAlarm(alarm, exists);
|
||||
|
||||
if (exists) {
|
||||
alarms = alarms.filter(alarm => alarm.id !== event.uid); // remove if already exists
|
||||
const tr = document.querySelector(`.event-row[data-uid='${event.uid}']`);
|
||||
document.getElementById('events').removeChild(tr);
|
||||
}
|
||||
alarms.push(alarm);
|
||||
});
|
||||
}, false);
|
||||
|
||||
reader.readAsText(input.files[i], "UTF-8");
|
||||
}
|
||||
}
|
||||
|
||||
function dateToMsSinceMidnight(date) {
|
||||
const dateMidnight = new Date(date);
|
||||
dateMidnight.setHours(0,0,0,0);
|
||||
return date - dateMidnight;
|
||||
}
|
||||
|
||||
function dateFromAlarm(alarm) {
|
||||
const date = new Date(alarm.date);
|
||||
return new Date(date.getTime() + alarm.t);
|
||||
}
|
||||
|
||||
function getAlarmDefaults() {
|
||||
const date = new Date();
|
||||
return {
|
||||
on: true,
|
||||
t: dateToMsSinceMidnight(date),
|
||||
dow: 127,
|
||||
date: date.toISOString().substring(0,10),
|
||||
last: 0,
|
||||
rp: "defaultRepeat" in schedSettings ? schedSettings.defaultRepeat : false,
|
||||
vibrate: "defaultAlarmPattern" in schedSettings ? schedSettings.defaultAlarmPattern : "::",
|
||||
as: false,
|
||||
};
|
||||
}
|
||||
|
||||
function eventToAlarm(event, offsetMs) {
|
||||
const dateOrig = event.startDate.toJSDate();
|
||||
const date = offsetMs ? new Date(dateOrig - offsetMs) : dateOrig;
|
||||
|
||||
const alarm = {...getAlarmDefaults(), ...{
|
||||
id: event.uid,
|
||||
msg: event.summary,
|
||||
t: dateToMsSinceMidnight(date),
|
||||
date: date.toISOString().substring(0,10),
|
||||
data: {end: event.endDate.toJSDate().toISOString()}
|
||||
}};
|
||||
if (offsetMs) { // Alarm time is not real event time, so do a backup
|
||||
alarm.data.time = dateOrig.toISOString();
|
||||
}
|
||||
return alarm;
|
||||
}
|
||||
|
||||
function upload() {
|
||||
Util.showModal("Saving...");
|
||||
Util.writeStorage("sched.json", JSON.stringify(alarms), () => {
|
||||
location.reload(); // reload so we see current data
|
||||
});
|
||||
}
|
||||
|
||||
function renderAlarm(alarm, exists) {
|
||||
const localDate = dateFromAlarm(alarm);
|
||||
|
||||
const tr = document.createElement('tr');
|
||||
tr.classList.add('event-row');
|
||||
tr.dataset.uid = alarm.id;
|
||||
const tdTime = document.createElement('td');
|
||||
tr.appendChild(tdTime);
|
||||
const inputTime = document.createElement('input');
|
||||
inputTime.type = "datetime-local";
|
||||
inputTime.classList.add('event-date');
|
||||
inputTime.classList.add('form-input');
|
||||
inputTime.dataset.uid = alarm.id;
|
||||
inputTime.value = localDate.toISOString().slice(0,16);
|
||||
inputTime.onchange = (e => {
|
||||
const date = new Date(inputTime.value);
|
||||
alarm.t = dateToMsSinceMidnight(date);
|
||||
alarm.date = date.toISOString().substring(0,10);
|
||||
});
|
||||
tdTime.appendChild(inputTime);
|
||||
|
||||
const tdSummary = document.createElement('td');
|
||||
tr.appendChild(tdSummary);
|
||||
const inputSummary = document.createElement('input');
|
||||
inputSummary.type = "text";
|
||||
inputSummary.classList.add('event-summary');
|
||||
inputSummary.classList.add('form-input');
|
||||
inputSummary.dataset.uid = alarm.id;
|
||||
inputSummary.maxLength=40;
|
||||
const realHumanStartTime = alarm.data?.time ? ' ' + (new Date(alarm.data.time)).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}) : '';
|
||||
const summary = (alarm.msg?.substring(0, inputSummary.maxLength) || "");
|
||||
inputSummary.value = summary.endsWith(realHumanStartTime) ? summary : summary + realHumanStartTime;
|
||||
inputSummary.onchange = (e => {
|
||||
alarm.msg = inputSummary.value;
|
||||
});
|
||||
tdSummary.appendChild(inputSummary);
|
||||
inputSummary.onchange();
|
||||
|
||||
const tdInfo = document.createElement('td');
|
||||
tr.appendChild(tdInfo);
|
||||
|
||||
const buttonDelete = document.createElement('button');
|
||||
buttonDelete.classList.add('btn');
|
||||
buttonDelete.classList.add('btn-action');
|
||||
tdInfo.prepend(buttonDelete);
|
||||
const iconDelete = document.createElement('i');
|
||||
iconDelete.classList.add('icon');
|
||||
iconDelete.classList.add('icon-delete');
|
||||
buttonDelete.appendChild(iconDelete);
|
||||
buttonDelete.onclick = (e => {
|
||||
alarms = alarms.filter(a => a !== alarm);
|
||||
document.getElementById('events').removeChild(tr);
|
||||
});
|
||||
|
||||
document.getElementById('events').appendChild(tr);
|
||||
document.getElementById('upload').disabled = false;
|
||||
}
|
||||
|
||||
function addAlarm() {
|
||||
const alarm = getAlarmDefaults();
|
||||
renderAlarm(alarm);
|
||||
alarms.push(alarm);
|
||||
}
|
||||
|
||||
function getData() {
|
||||
Util.showModal("Loading...");
|
||||
Util.readStorage('sched.json',data=>{
|
||||
alarms = JSON.parse(data || "[]") || [];
|
||||
|
||||
Util.readStorage('sched.settings.json',data=>{
|
||||
schedSettings = JSON.parse(data || "{}") || {};
|
||||
Util.hideModal();
|
||||
alarms.forEach(alarm => {
|
||||
if (alarm.date) {
|
||||
renderAlarm(alarm, true);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Called when app starts
|
||||
function onInit() {
|
||||
getData();
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<h4>Manage dated events</h4>
|
||||
|
||||
<div class="float-right">
|
||||
<button class="btn" onclick="addAlarm()">
|
||||
<i class="icon icon-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Summary</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="events">
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="divider"></div>
|
||||
<div class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<div class="col-5 col-xs-12">
|
||||
<label class="form-label" for="fileinput">Add from iCalendar file</label>
|
||||
</div>
|
||||
<div class="col-7 col-xs-12">
|
||||
<input id="fileinput" class="form-input" type="file" onchange="readFile(this)" accept=".ics,.ifb,.ical,.ifbf" multiple/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-5 col-xs-12">
|
||||
<label class="form-label" for="fileinput">Minutes to alarm in advance</label>
|
||||
</div>
|
||||
<div class="col-7 col-xs-12">
|
||||
<input id="offsetMinutes" class="form-input" type="number" value="0" min="0" step="5"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
|
||||
<button id="upload" class="btn btn-primary" onClick="upload()" disabled>Upload</button>
|
||||
<button id="reload" class="btn" onClick="location.reload()">Reload</button>
|
||||
</body>
|
||||
</html>
|
|
@ -10,6 +10,7 @@
|
|||
"provides_modules" : ["sched"],
|
||||
"default" : true,
|
||||
"readme": "README.md",
|
||||
"interface": "interface.html",
|
||||
"storage": [
|
||||
{"name":"sched.boot.js","url":"boot.js"},
|
||||
{"name":"sched.js","url":"sched.js"},
|
||||
|
|
|
@ -14,3 +14,6 @@
|
|||
0.11: Minor tweaks
|
||||
0.12: Support javascript command to execute as defined in scheduler 'js' configuration
|
||||
0.13: Fix dated events alarm on wrong date
|
||||
0.14: Reduce update interval of current time when seconds are not shown
|
||||
Limit logging on Bangle.js 1 to one day due to low memory
|
||||
Add plot logged data to settings
|
||||
|
|
|
@ -23,6 +23,11 @@ Replacing the watch strap with a more comfortable one (e.g. made of nylon) is re
|
|||
## Logging
|
||||
|
||||
For each day of month (1..31) the ESS states are logged. An entry will be overwritten in the next month, e.g. an entry on the 4th May will overwrite an entry on the 4th April.
|
||||
The logs can be viewed with the download button:
|
||||
On Bangle.js 1 only one day is logged due to low memory.
|
||||
The logs can be plotted from the settings menu:
|
||||
|
||||

|
||||

|
||||
|
||||
The logs can also be viewed with the download button in the App Loader:
|
||||
|
||||

|
||||
|
|
|
@ -14,6 +14,7 @@ const active = alarms.filter(alarm => require("sched").getTimeToAlarm(alarm));
|
|||
const schedSettings = require("sched").getSettings();
|
||||
let buzzCount = schedSettings.buzzCount;
|
||||
let logs = [];
|
||||
let drawTimeTimeout;
|
||||
|
||||
// Sleep/Wake detection with Estimation of Stationary Sleep-segments (ESS):
|
||||
// Marko Borazio, Eugen Berlin, Nagihan Kücükyildiz, Philipp M. Scholl and Kristof Van Laerhoven, "Towards a Benchmark for Wearable Sleep Analysis with Inertial Wrist-worn Sensing Units", ICHI 2014, Verona, Italy, IEEE Press, 2014.
|
||||
|
@ -26,7 +27,7 @@ const nomothresh=0.023; // Original implementation: 6, resolution 11 bit, scale
|
|||
const sleepthresh=600;
|
||||
var ess_values = [];
|
||||
var slsnds = 0;
|
||||
function calc_ess(acc_magn) {
|
||||
function calc_ess(acc_magn) {"ram"
|
||||
ess_values.push(acc_magn);
|
||||
|
||||
if (ess_values.length == winwidth) {
|
||||
|
@ -90,10 +91,12 @@ function drawApp() {
|
|||
layout.alarm_date.label = `${LABEL_WAKEUP_TIME}: ${alarmHour}:${alarmMinute}`;
|
||||
layout.render();
|
||||
|
||||
function drawTime() {
|
||||
function drawTime() {"ram"
|
||||
const drawSeconds = !Bangle.isLocked();
|
||||
|
||||
if (Bangle.isLCDOn()) {
|
||||
const now = new Date();
|
||||
layout.date.label = locale.time(now, BANGLEJS2 && Bangle.isLocked() ? 1 : 0); // hide seconds on bangle 2
|
||||
layout.date.label = locale.time(now, !drawSeconds); // hide seconds on bangle 2
|
||||
const diff = nextAlarmDate - now;
|
||||
const diffHour = Math.floor((diff % 86400000) / 3600000).toString();
|
||||
const diffMinutes = Math.floor(((diff % 86400000) % 3600000) / 60000).toString();
|
||||
|
@ -101,11 +104,22 @@ function drawApp() {
|
|||
layout.render();
|
||||
}
|
||||
|
||||
setTimeout(()=>{
|
||||
const period = drawSeconds ? 1000 : 60000;
|
||||
if (this.drawTimeTimeout !== undefined) {
|
||||
clearTimeout(this.drawTimeTimeout);
|
||||
}
|
||||
drawTimeTimeout = setTimeout(()=>{
|
||||
drawTimeTimeout = undefined;
|
||||
drawTime();
|
||||
}, 1000 - (Date.now() % 1000));
|
||||
}, period - (Date.now() % period));
|
||||
}
|
||||
|
||||
Bangle.on('lock', function(on) {
|
||||
if (on === false) {
|
||||
drawTime();
|
||||
}
|
||||
});
|
||||
|
||||
drawTime();
|
||||
}
|
||||
|
||||
|
@ -132,8 +146,9 @@ function addLog(time, type) {
|
|||
var minAlarm = new Date();
|
||||
var measure = true;
|
||||
if (nextAlarmDate !== undefined) {
|
||||
config.logs[nextAlarmDate.getDate()] = []; // overwrite log on each day of month
|
||||
logs = config.logs[nextAlarmDate.getDate()];
|
||||
const logday = BANGLEJS2 ? nextAlarmDate.getDate() : 0;
|
||||
config.logs[logday] = []; // overwrite log on each day of month
|
||||
logs = config.logs[logday];
|
||||
g.clear();
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
|
@ -146,7 +161,7 @@ if (nextAlarmDate !== undefined) {
|
|||
layout.render();
|
||||
Bangle.setOptions({powerSave: false}); // do not dynamically change accelerometer poll interval
|
||||
Bangle.setPollInterval(80); // 12.5Hz
|
||||
Bangle.on('accel', (accelData) => {
|
||||
Bangle.on('accel', (accelData) => {"ram"
|
||||
const now = new Date();
|
||||
const acc = accelData.mag;
|
||||
const swest = calc_ess(acc);
|
||||
|
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 45 KiB |
|
@ -2,7 +2,7 @@
|
|||
"id": "sleepphasealarm",
|
||||
"name": "SleepPhaseAlarm",
|
||||
"shortName": "SleepPhaseAlarm",
|
||||
"version": "0.13",
|
||||
"version": "0.14",
|
||||
"description": "Uses the accelerometer to estimate sleep and wake states with the principle of Estimation of Stationary Sleep-segments (ESS, see https://ubicomp.eti.uni-siegen.de/home/datasets/ichi14/index.html.en). This app will read the next alarm from the alarm application and will wake you up to 30 minutes early at the best guessed time when you are almost already awake.",
|
||||
"icon": "app.png",
|
||||
"tags": "alarm",
|
||||
|
@ -15,5 +15,6 @@
|
|||
{"name":"sleepphasealarm.img","url":"app-icon.js","evaluate":true}
|
||||
],
|
||||
"data": [{"name":"sleepphasealarm.json"}],
|
||||
"interface": "interface.html"
|
||||
"interface": "interface.html",
|
||||
"screenshots": [ {"url":"screenshot.png"}, {"url":"screenshot_log.png"} ]
|
||||
}
|
||||
|
|
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 3.1 KiB |
|
@ -13,25 +13,102 @@
|
|||
require('Storage').writeJSON(CONFIGFILE, config);
|
||||
}
|
||||
|
||||
// Show the menu
|
||||
E.showMenu({
|
||||
"" : { "title" : "SleepPhaseAlarm" },
|
||||
'Keep alarm enabled': {
|
||||
value: !!config.settings.disableAlarm,
|
||||
format: v => v?"No":"Yes",
|
||||
onchange: v => {
|
||||
config.settings.disableAlarm = v;
|
||||
writeSettings();
|
||||
function draw(log) {
|
||||
const step = 10*60*1000; // resolution 10min
|
||||
const yTicks = ["sleep", "awake", "alarm"];
|
||||
const starttime = new Date(log[0].time);
|
||||
const endtime = new Date(log[log.length-1].time);
|
||||
|
||||
let logidx = 0;
|
||||
let curtime = starttime;
|
||||
const data = new Uint8Array(Math.ceil((endtime-curtime)/step) + 1);
|
||||
let curval;
|
||||
let logtime;
|
||||
let i=0;
|
||||
while(curtime < endtime) {
|
||||
if (logtime === undefined || curtime > logtime) {
|
||||
curval = yTicks.indexOf(log[logidx].type);
|
||||
logidx++;
|
||||
logtime = new Date(log[logidx].time);
|
||||
}
|
||||
}, "< Back" : () => back(),
|
||||
'Run before alarm': {
|
||||
format: v => v === 0 ? 'disabled' : v+'h',
|
||||
value: config.settings.startBeforeAlarm,
|
||||
min: 0, max: 23,
|
||||
onchange: v => {
|
||||
config.settings.startBeforeAlarm = v;
|
||||
writeSettings();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
data[i++] = curval;
|
||||
curtime = new Date(curtime + step);
|
||||
}
|
||||
data[i] = 1; // always end with awake
|
||||
|
||||
Bangle.setUI({
|
||||
mode: "custom",
|
||||
back: () => selectday(),
|
||||
});
|
||||
g.reset().setFont("6x8",1);
|
||||
|
||||
require("graph").drawLine(g, data, {
|
||||
axes: true,
|
||||
x: 4,
|
||||
y: Bangle.appRect.y+8,
|
||||
height: Bangle.appRect.h-20,
|
||||
gridx: 1,
|
||||
gridy: 1,
|
||||
miny: -1,
|
||||
maxy: 2,
|
||||
title: /*LANG*/"Wakeup " + require("locale").date(endtime, 1),
|
||||
ylabel: y => y >= 0 && y <= 1 ? yTicks[y] : "",
|
||||
xlabel: x => {
|
||||
if (x === Math.round(data.length/10)) {
|
||||
return require("locale").time(starttime, 1);
|
||||
} else if (x === (data.length-2)-Math.round(data.length/10)) {
|
||||
return require("locale").time(endtime, 1);
|
||||
}
|
||||
return "";
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function selectday() {
|
||||
E.showMessage(/*LANG*/"Loading...");
|
||||
|
||||
const logs = config.logs.filter(log => log != null && log.filter(entry => entry.type === "alarm").length > 0);
|
||||
logs.sort(function(a, b) { // sort by alarm date desc
|
||||
const adate = new Date(a.filter(entry => entry.type === "alarm")[0].time);
|
||||
const bdate = new Date(b.filter(entry => entry.type === "alarm")[0].time);
|
||||
return bdate - adate;
|
||||
});
|
||||
|
||||
const menu = {};
|
||||
menu[""] = { title: /*LANG*/"Select day" };
|
||||
menu["< Back"] = () => settingsmenu();
|
||||
logs.forEach((log, i) => {
|
||||
const date = new Date(log.filter(entry => entry.type === "alarm")[0].time);
|
||||
menu[require("locale").date(date, 1)] = () => { E.showMenu(); draw(log); };
|
||||
});
|
||||
E.showMenu(menu);
|
||||
}
|
||||
|
||||
function settingsmenu() {
|
||||
// Show the menu
|
||||
E.showMenu({
|
||||
"" : { "title" : "SleepPhaseAlarm" },
|
||||
'Keep alarm enabled': {
|
||||
value: !!config.settings.disableAlarm,
|
||||
format: v => v?"No":"Yes",
|
||||
onchange: v => {
|
||||
config.settings.disableAlarm = v;
|
||||
writeSettings();
|
||||
}
|
||||
}, "< Back" : () => back(),
|
||||
'Run before alarm': {
|
||||
format: v => v === 0 ? 'disabled' : v+'h',
|
||||
value: config.settings.startBeforeAlarm,
|
||||
min: 0, max: 23,
|
||||
onchange: v => {
|
||||
config.settings.startBeforeAlarm = v;
|
||||
writeSettings();
|
||||
}
|
||||
},
|
||||
/*LANG*/'Select day': () => selectday(),
|
||||
});
|
||||
}
|
||||
|
||||
settingsmenu();
|
||||
})
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
0.01: 3/Feb/2023 Added 'Temperature Graph' app to depository.
|
||||
0.02: 4/Feb/2023 Rewrote the widget handling after discovering there's a 'widget_utils' module to properly hide and show them.
|
||||
0.03: 4/Feb/2023 Fixed number error in timesData array.
|
|
@ -0,0 +1,36 @@
|
|||
# Temperature Graph
|
||||
|
||||
**Temperature Graph** (tempgraph) is a Bangle.js 2 app for recording graphs of the temperature for various time periods from 10 minutes to 7 days long. It samples the watch's temperature sensor 150 times while creating a graph, regardless of the time period selected.
|
||||
|
||||
### Menu Options
|
||||
* **Widgets** Toggles the watch's widgets on and off. With them off gives you a bigger graph when viewing it.
|
||||
|
||||
* **Duration** Select the time period for drawing the graph, from 10 minutes to 7 days long.
|
||||
|
||||
* **Draw Graph** Draws the graph.
|
||||
* Tapping the screen toggles the graph between Celsius (red) and Fahrenheit (blue).
|
||||
* Pressing the watch button takes you back to the menu. **Note:** While the graph can still be viewed after returning to the menu, you can't continue recording it if you had returned to the menu before the time period was up. The graph is saved in the watch though so it's still there the next time you start the app.
|
||||
|
||||
* **Show Graph** Shows the last drawn graph.
|
||||
* Tapping the screen toggles the graph between Celsius (red) and Fahrenheit (blue).
|
||||
* Pressing the watch button takes you back to the menu.
|
||||
|
||||
* **Save Graph** Sends a screengrab of the graph to the Espruino Web IDE from where you can save it as you would any image on a webpage.
|
||||
|
||||
* **Save Data** Sends a CSV file of the graph's temperature data to the Espruino Web IDE where you can save it for further use. I suggest you use the Espruino Web IDE's Terminal Logger (selected in the IDE's Settings/General) to record the data as it's sent. This is the easiest way to save it as a text file.
|
||||
|
||||
* **Show Temp** Shows the current temperature.
|
||||
|
||||
### Note
|
||||
Using the watch in a normal fashion can raise the temperature it's sensing to quite a few degrees above the surrounding temperature and it may take half an hour or so to drop to close to the surrounding temperature. After that it seems to give quite accurate readings, assuming the thermometer I've been comparing it to is itself reasonably accurate. So best to load the app then not touch the watch for half an hour before starting a recording. This is assuming you're not wearing the app and are just using it to record the temperature where you've put the watch. You could of course wear it and it'll still draw a graph, which might also be useful.
|
||||
|
||||
### Screenshots
|
||||

|
||||

|
||||

|
||||
|
||||
### Creator
|
||||
Carl Read ([mail](mailto:cread98@orcon.net.nz), [github](https://github.com/CarlR9))
|
||||
|
||||
#### License
|
||||
[MIT License](LICENSE)
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwxH+goA/AH4AgrHXABFYF0XXkYAK64utGENYFxoABSTxeHXYJglF+UAAIQvEBApfhE4UAF4IDBFwZf/X7hfsR4K/tL96/vRwpf/X/5fJGYK/tL9u02i/tF4KOFL/6/XF4ZftR4K/tL96/vRwpf/X/5fJGYK/tL96/vRwpf/X7gADF8ouBGA4v/F/6/urAmGABFYF7pgIL0owPF0KSC64AIRj4A/AH4ACA="))
|
|
@ -0,0 +1,394 @@
|
|||
// Temperature Graph
|
||||
// BangleJS Script
|
||||
|
||||
Bangle.setBarometerPower(true,"tempgraph");
|
||||
Bangle.loadWidgets();
|
||||
var widsOn=true;
|
||||
var rm=null;
|
||||
var gt=null;
|
||||
var dg=null;
|
||||
var Layout=require("Layout");
|
||||
var C=true;
|
||||
var temp,tempMode,readErrCnt,watchButton2;
|
||||
|
||||
var graph=require("Storage").readJSON("tempgraph.json",true);
|
||||
if(graph==undefined) {
|
||||
graph=[];
|
||||
}
|
||||
|
||||
var timesData=[
|
||||
// dur=duration, u=time units, d=divisions on graph, s=seconds per unit.
|
||||
{dur:10,u:"Mins",d:5,s:60},
|
||||
{dur:20,u:"Mins",d:4,s:60},
|
||||
{dur:30,u:"Mins",d:3,s:60},
|
||||
{dur:40,u:"Mins",d:4,s:60},
|
||||
{dur:1,u:"Hr",d:4,s:3600},
|
||||
{dur:2,u:"Hrs",d:4,s:3600},
|
||||
{dur:3,u:"Hrs",d:3,s:3600},
|
||||
{dur:4,u:"Hrs",d:4,s:3600},
|
||||
{dur:6,u:"Hrs",d:6,s:3600},
|
||||
{dur:8,u:"Hrs",d:4,s:3600},
|
||||
{dur:12,u:"Hrs",d:6,s:3600},
|
||||
{dur:16,u:"Hrs",d:4,s:3600},
|
||||
{dur:20,u:"Hrs",d:5,s:3600},
|
||||
{dur:1,u:"Day",d:4,s:86400},
|
||||
{dur:2,u:"Days",d:4,s:86400},
|
||||
{dur:3,u:"Days",d:3,s:86400},
|
||||
{dur:4,u:"Days",d:4,s:86400},
|
||||
{dur:5,u:"Days",d:5,s:86400},
|
||||
{dur:6,u:"Days",d:6,s:86400},
|
||||
{dur:7,u:"Days",d:7,s:86400}
|
||||
];
|
||||
var times=[];
|
||||
for(n=0;n<timesData.length;n++){
|
||||
times.push(timesData[n].dur+" "+timesData[n].u);
|
||||
}
|
||||
var durInd=0;
|
||||
var duration=times[durInd];
|
||||
|
||||
function drawWids(){
|
||||
g.clear();
|
||||
if(widsOn){
|
||||
Bangle.drawWidgets();
|
||||
require("widget_utils").show();
|
||||
} else {
|
||||
require("widget_utils").hide();
|
||||
}
|
||||
}
|
||||
|
||||
function openMenu(){
|
||||
drawWids();
|
||||
E.showMenu(menu);
|
||||
}
|
||||
|
||||
function redoMenu(){
|
||||
clearInterval(rm);
|
||||
E.showMenu();
|
||||
openMenu();
|
||||
}
|
||||
|
||||
function refreshMenu(){
|
||||
rm = setInterval(redoMenu,100);
|
||||
}
|
||||
function getF(c){
|
||||
// Get Fahrenheit temperature from Celsius.
|
||||
return c*1.8+32;
|
||||
}
|
||||
|
||||
function getT(){
|
||||
Bangle.getPressure().then(p=>{
|
||||
temp=p.temperature;
|
||||
if(tempMode=="drawGraph"&&graph.length>0&&Math.abs(graph[graph.length-1].temp-temp)>10&&readErrCnt<2){
|
||||
// A large change in temperature may be a reading error. ie. A 0C or less reading after
|
||||
// a 20C reading. So if this happens, the reading is repeated up to 2 times to hopefully
|
||||
// skip such errors.
|
||||
readErrCnt++;
|
||||
print("readErrCnt "+readErrCnt);
|
||||
return;
|
||||
}
|
||||
clearInterval(gt);
|
||||
readErrCnt=0;
|
||||
switch (tempMode){
|
||||
case "showTemp":
|
||||
showT();
|
||||
break;
|
||||
case "drawGraph":
|
||||
var date=new Date();
|
||||
var dateStr=require("locale").date(date).trim();
|
||||
var hrs=date.getHours();
|
||||
var mins=date.getMinutes();
|
||||
var secs=date.getSeconds();
|
||||
graph.push({
|
||||
temp:temp,
|
||||
date:dateStr,
|
||||
hrs:hrs,
|
||||
mins:mins,
|
||||
secs:secs
|
||||
});
|
||||
if(graph.length==1){
|
||||
graph[0].dur=durInd;
|
||||
}
|
||||
require("Storage").writeJSON("tempgraph.json", graph);
|
||||
if(graph.length==150){
|
||||
clearInterval(dg);
|
||||
}
|
||||
drawG();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getTemp(){
|
||||
readErrCnt=0;
|
||||
gt = setInterval(getT,800);
|
||||
}
|
||||
|
||||
function setButton(){
|
||||
var watchButton=setWatch(function(){
|
||||
clearInterval(gt);
|
||||
clearInterval(dg);
|
||||
clearWatch(watchButton);
|
||||
Bangle.removeListener("touch",screenTouch);
|
||||
openMenu();
|
||||
},BTN);
|
||||
Bangle.on('touch',screenTouch);
|
||||
}
|
||||
|
||||
function setButton2(){
|
||||
watchButton2=setWatch(function(){
|
||||
clearWatch(watchButton2);
|
||||
openMenu();
|
||||
},BTN);
|
||||
}
|
||||
|
||||
function zPad(n){
|
||||
return n.toString().padStart(2,0);
|
||||
}
|
||||
|
||||
function screenTouch(n,ev){
|
||||
if(ev.y>23&&ev.y<152){
|
||||
C=C==false;
|
||||
drawG(false);
|
||||
}
|
||||
}
|
||||
|
||||
function drawG(){
|
||||
function cf(t){
|
||||
if(C){
|
||||
return t;
|
||||
}
|
||||
return getF(t);
|
||||
}
|
||||
drawWids();
|
||||
var top=1;
|
||||
var bar=21;
|
||||
var barBot=175-22;
|
||||
if(widsOn){
|
||||
top=25;
|
||||
bar=bar+24;
|
||||
barBot=barBot-24;
|
||||
}
|
||||
var low=graph[0].temp;
|
||||
var hi=low;
|
||||
for(n=0;n<graph.length;n++){
|
||||
var t=graph[n].temp;
|
||||
if(low>t){
|
||||
low=t;
|
||||
}
|
||||
if(hi<t){
|
||||
hi=t;
|
||||
}
|
||||
}
|
||||
var tempHi=Math.ceil((cf(hi)+2)/10)*10;
|
||||
var tempLow=Math.floor((cf(low)-2)/10)*10;
|
||||
var div=2;
|
||||
if(tempHi-tempLow>10){
|
||||
div=5;
|
||||
}
|
||||
if(C){
|
||||
g.setColor(1,0,0);
|
||||
}else{
|
||||
g.setColor(0,0,1);
|
||||
}
|
||||
var step=(barBot-bar)/((tempHi-tempLow)/div);
|
||||
for(n=0;n<graph.length;n++){
|
||||
var pos=tempLow-cf(graph[n].temp);
|
||||
g.drawLine(n+3,pos*(step/div)+barBot,n+3,barBot+3);
|
||||
}
|
||||
g.fillRect(161,barBot+5,174,barBot+20);
|
||||
g.setColor(1,1,1);
|
||||
g.setFont("6x8:2");
|
||||
if(C){
|
||||
g.drawString("C",163,barBot+5);
|
||||
}else{
|
||||
g.drawString("F",163,barBot+5);
|
||||
}
|
||||
g.setColor(0,0,0);
|
||||
g.setFont6x15();
|
||||
g.drawString("Temperature Graph - "+times[graph[0].dur],1,top);
|
||||
g.drawRect(2,bar-4,153,barBot+4);
|
||||
g.setFont("6x8:1");
|
||||
var num=tempHi;
|
||||
for(n=bar;n<=barBot;n=n+step){
|
||||
g.drawLine(3,n,152,n);
|
||||
g.drawString(num.toString().padStart(3," "),155,n-4);
|
||||
num=num-div;
|
||||
}
|
||||
step=151/timesData[graph[0].dur].d;
|
||||
for(n=step+2;n<152;n=n+step){
|
||||
g.drawLine(n,bar-4,n,barBot+4);
|
||||
}
|
||||
grSt=graph[0];
|
||||
g.drawString("Start: "+grSt.date+" "+grSt.hrs+":"+zPad(grSt.mins),1,barBot+6);
|
||||
var lastT=graph[graph.length-1].temp;
|
||||
g.drawString("Last Reading:",1,barBot+14);
|
||||
g.setColor(1,0,0);
|
||||
g.drawString(lastT.toFixed(1)+"C",85,barBot+14);
|
||||
g.setColor(0,0,1);
|
||||
g.drawString(getF(lastT).toFixed(1)+"F",121,barBot+14);
|
||||
process.memory(true);
|
||||
}
|
||||
|
||||
function drawGraph(){
|
||||
setButton();
|
||||
tempMode="drawGraph";
|
||||
durInd=times.indexOf(duration);
|
||||
graph=[];
|
||||
getTemp();
|
||||
dg=setInterval(getTemp,1000*timesData[durInd].dur*timesData[durInd].s/150);
|
||||
}
|
||||
|
||||
function showGraph(){
|
||||
setButton();
|
||||
drawG();
|
||||
}
|
||||
|
||||
function noBluetooth(){
|
||||
if(NRF.getSecurityStatus().connected){
|
||||
return false;
|
||||
}else{
|
||||
message("Error! Your\nBangle Watch\ncurrently has\nno Bluetooth\nconnection.");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function saveGraph(){
|
||||
if(noBluetooth()){
|
||||
return;
|
||||
}
|
||||
drawG();
|
||||
g.flip();
|
||||
g.dump();
|
||||
message("Graph has\nbeen sent\nto Web IDE\nfor saving.\n");
|
||||
}
|
||||
|
||||
function saveData(){
|
||||
if(noBluetooth()){
|
||||
return;
|
||||
}
|
||||
drawG();
|
||||
g.flip();
|
||||
print("Temperature Graph - "+times[graph[0].dur]+"\n");
|
||||
print("\"Date\",\"Time\",\"Celsius\",\"Fahrenheit\"");
|
||||
for(n=0;n<graph.length;n++){
|
||||
var gr=graph[n];
|
||||
print("\""+gr.date+"\",\""+gr.hrs+":"+zPad(gr.mins)+":"+zPad(gr.secs)+"\","+gr.temp+","+getF(gr.temp));
|
||||
}
|
||||
message("Data has\nbeen sent\nto Web IDE\nfor saving.\n");
|
||||
}
|
||||
|
||||
function message(mes){
|
||||
setButton2();
|
||||
var messageLO=new Layout({
|
||||
type:"v",c:[
|
||||
{type:"txt",font:"6x8:2",width:171,label:mes,id:"label"},
|
||||
{type:"btn",font:"6x8:2",pad:3,label:"OK",cb:l=>exit()},
|
||||
],lazy:true
|
||||
});
|
||||
drawWids();
|
||||
messageLO.render();
|
||||
}
|
||||
|
||||
function showT(){
|
||||
tempLO.lab1.label=tempLO.lab3.label;
|
||||
tempLO.lab2.label=tempLO.lab4.label;
|
||||
tempLO.lab3.label=tempLO.lab5.label;
|
||||
tempLO.lab4.label=tempLO.lab6.label;
|
||||
tempLO.lab5.label=temp.toFixed(2)+"C";
|
||||
tempLO.lab6.label=getF(temp).toFixed(2)+"F";
|
||||
tempLO.render();
|
||||
}
|
||||
|
||||
function exit(){
|
||||
clearWatch(watchButton2);
|
||||
openMenu();
|
||||
}
|
||||
|
||||
function showTemp(){
|
||||
tempMode="showTemp";
|
||||
setButton2();
|
||||
tempLO=new Layout({
|
||||
type:"v",c:[
|
||||
{type:"h",c:[
|
||||
{type:"txt",pad:5,col:"#f77",font:"6x8:2",label:" ",id:"lab1"},
|
||||
{type:"txt",pad:5,col:"#77f",font:"6x8:2",label:" ",id:"lab2"}
|
||||
]},
|
||||
{type:"h",c:[
|
||||
{type:"txt",pad:5,col:"#f77",font:"6x8:2",label:" ",id:"lab3"},
|
||||
{type:"txt",pad:5,col:"#77f",font:"6x8:2",label:" ",id:"lab4"}
|
||||
]},
|
||||
{type:"h",c:[
|
||||
{type:"txt",pad:5,col:"#f00",font:"6x8:2",label:" ",id:"lab5"},
|
||||
{type:"txt",pad:5,col:"#00f",font:"6x8:2",label:" ",id:"lab6"}
|
||||
]},
|
||||
{type:"h",c:[
|
||||
{type:"btn",pad:2,font:"6x8:2",label:"Temp",cb:l=>getTemp()},
|
||||
{type:"btn",pad:2,font:"6x8:2",label:"Exit",cb:l=>exit()}
|
||||
]}
|
||||
]
|
||||
},{lazy:true});
|
||||
tempLO.render();
|
||||
getTemp();
|
||||
}
|
||||
|
||||
var menu={
|
||||
"":{
|
||||
"title":" Temp. Graph"
|
||||
},
|
||||
|
||||
"Widgets":{
|
||||
value:widsOn,
|
||||
format:vis=>vis?"Hide":"Show",
|
||||
onchange:vis=>{
|
||||
widsOn=vis;
|
||||
refreshMenu();
|
||||
}
|
||||
},
|
||||
|
||||
"Duration":{
|
||||
value:times.indexOf(duration),
|
||||
min:0,max:times.length-1,step:1,wrap:true,
|
||||
format:tim=>times[tim],
|
||||
onchange:(dur)=>{
|
||||
duration=times[dur];
|
||||
}
|
||||
},
|
||||
|
||||
"Draw Graph":function(){
|
||||
E.showMenu();
|
||||
drawGraph();
|
||||
},
|
||||
|
||||
"Show Graph" : function(){
|
||||
E.showMenu();
|
||||
if(graph.length>0){
|
||||
showGraph();
|
||||
}else{
|
||||
message("No graph to\nshow as no\ngraph has been\ndrawn yet.");
|
||||
}
|
||||
},
|
||||
|
||||
"Save Graph" : function(){
|
||||
E.showMenu();
|
||||
if(graph.length>0){
|
||||
saveGraph();
|
||||
}else{
|
||||
message("No graph to\nsave as no\ngraph has been\ndrawn yet.");
|
||||
}
|
||||
},
|
||||
|
||||
"Save Data" : function(){
|
||||
E.showMenu();
|
||||
if(graph.length>0){
|
||||
saveData();
|
||||
}else{
|
||||
message("No data to\nsave as no\ngraph has been\ndrawn yet.");
|
||||
}
|
||||
},
|
||||
|
||||
"Show Temp":function(){
|
||||
E.showMenu();
|
||||
showTemp();
|
||||
}
|
||||
};
|
||||
|
||||
openMenu();
|
After Width: | Height: | Size: 6.8 KiB |
|
@ -0,0 +1,19 @@
|
|||
{ "id": "tempgraph",
|
||||
"name": "Temperature Graph",
|
||||
"shortName":"Temp Graph",
|
||||
"version":"0.03",
|
||||
"description": "An app for recording the temperature for time periods ranging from 10 minutes to 7 days.",
|
||||
"icon": "app.png",
|
||||
"type": "app",
|
||||
"tags": "temperature,tempgraph,graph",
|
||||
"supports" : ["BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
"storage": [
|
||||
{"name":"tempgraph.app.js","url":"app.js"},
|
||||
{"name":"tempgraph.img","url":"app-icon.js","evaluate":true}
|
||||
],
|
||||
"data": [
|
||||
{"name":"tempgraph.json"}
|
||||
],
|
||||
"screenshots": [{"url":"screenshot_1.png"},{"url":"screenshot_2.png"},{"url":"screenshot_3.png"}]
|
||||
}
|
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 4.1 KiB |
After Width: | Height: | Size: 3.4 KiB |
|
@ -4,3 +4,4 @@
|
|||
Add option to show seconds
|
||||
0.03: Fix Bell not appearing on alarms > 24h and redrawing interval
|
||||
Update to match the default alarm widget, and not show itself when an alarm is hidden.
|
||||
0.04: Fix check for active alarm
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"id": "widalarmeta",
|
||||
"name": "Alarm & Timer ETA",
|
||||
"shortName": "Alarm ETA",
|
||||
"version": "0.03",
|
||||
"version": "0.04",
|
||||
"description": "A widget that displays the time to the next Alarm or Timer in hours and minutes, maximum 24h (configurable).",
|
||||
"icon": "widget.png",
|
||||
"type": "widget",
|
||||
|
|
|
@ -9,10 +9,10 @@
|
|||
|
||||
function draw() {
|
||||
const times = alarms
|
||||
.map(alarm => {
|
||||
.map(alarm =>
|
||||
alarm.hidden !== true
|
||||
&& require("sched").getTimeToAlarm(alarm)
|
||||
})
|
||||
)
|
||||
.filter(a => a !== undefined);
|
||||
const next = times.length > 0 ? Math.min.apply(null, times) : 0;
|
||||
let calcWidth = 0;
|
||||
|
|
|
@ -6,3 +6,4 @@
|
|||
0.07: Move CHARGING variable to more readable string
|
||||
0.08: Ensure battery updates every 60s even if LCD was on at boot and stays on
|
||||
0.09: Misc speed/memory tweaks
|
||||
0.10: Color changes due to the battery level
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "widbat",
|
||||
"name": "Battery Level Widget",
|
||||
"version": "0.09",
|
||||
"version": "0.10",
|
||||
"description": "Show the current battery level and charging status in the top right of the clock",
|
||||
"icon": "widget.png",
|
||||
"type": "widget",
|
||||
|
|
|
@ -31,7 +31,11 @@
|
|||
x+=16;
|
||||
}
|
||||
g.setColor(g.theme.fg).fillRect(x,y+2,x+s-4,y+21).clearRect(x+2,y+4,x+s-6,y+19).fillRect(x+s-3,y+10,x+s,y+14);
|
||||
g.setColor("#0f0").fillRect(x+4,y+6,x+4+E.getBattery()*(s-12)/100,y+17);
|
||||
var battery = E.getBattery();
|
||||
if(battery < 20) {g.setColor("#f00");}
|
||||
else if (battery < 50) {g.setColor("#ff0");}
|
||||
else {g.setColor("#0f0");}
|
||||
g.fillRect(x+4,y+6,x+4+battery*(s-12)/100,y+17);
|
||||
}};
|
||||
setWidth();
|
||||
})()
|
||||
|
|
|
@ -200,7 +200,15 @@
|
|||
"Green": "Verde",
|
||||
"Blue": "Blu",
|
||||
"Black": "Nero",
|
||||
"Show Week Number": "Mostra numero settimana"
|
||||
"Show Week Number": "Mostra numero settimana",
|
||||
"Calendar": "Calendario",
|
||||
"Start": "Inizio",
|
||||
"End": "Fine",
|
||||
"Location": "Posizione",
|
||||
"No location": "Nessuna posizione",
|
||||
"No events": "Nessun evento",
|
||||
"Today": "Oggi",
|
||||
"Tomorrow": "Domani"
|
||||
},
|
||||
"//2": "App-specific overrides",
|
||||
"alarm": {
|
||||
|
|