Merge remote-tracking branch 'upstream/master'

pull/2572/head
Hugh Barney 2023-02-11 20:22:56 +00:00
commit 1a0820d28d
56 changed files with 10157 additions and 8880 deletions

View File

@ -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

View File

@ -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({

View File

@ -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"}],

View File

@ -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

View File

@ -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;
}

View File

@ -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",

View File

@ -1 +1,2 @@
0.01: New App!
0.02: Don't fire if the app uses swipes already.

17
apps/backswipe/README.md Normal file
View File

@ -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

View File

@ -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();
}
}

View File

@ -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",

View File

@ -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 = {

View File

@ -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.

View File

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

28
apps/legoremote/README.md Normal file
View File

@ -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.

View File

@ -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"))

70
apps/legoremote/app.js Normal file
View File

@ -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");
}
});

BIN
apps/legoremote/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -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}
]
}

View File

@ -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}

3
apps/mtnclock/ChangeLog Normal file
View File

@ -0,0 +1,3 @@
0.01: New App!
0.02: Get weather from weather.json
0.03: Address unexpected undefined when reading weather.json

View File

@ -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.

View File

@ -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");

View File

@ -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",

View File

@ -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.

View File

@ -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",

View File

@ -23,6 +23,7 @@
'': {
'title': 'Power Manager'
},
"< Back" : back,
'Monotonic percentage': {
value: !!settings.forceMonoPercentage,
onchange: v => {

View File

@ -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

View File

@ -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;
}
});

View File

@ -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": "",

223
apps/sched/interface.html Normal file
View File

@ -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>

View File

@ -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"},

View File

@ -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

View File

@ -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:
![](screenshot.jpg)
![](screenshot_log.png)
The logs can also be viewed with the download button in the App Loader:
![](interface.jpg)

View File

@ -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);

View File

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

View File

@ -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"} ]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -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();
})

3
apps/tempgraph/ChangeLog Normal file
View File

@ -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.

36
apps/tempgraph/README.md Normal file
View File

@ -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
![](screenshot_1.png)
![](screenshot_2.png)
![](screenshot_3.png)
### Creator
Carl Read ([mail](mailto:cread98@orcon.net.nz), [github](https://github.com/CarlR9))
#### License
[MIT License](LICENSE)

View File

@ -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="))

394
apps/tempgraph/app.js Normal file
View File

@ -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();

BIN
apps/tempgraph/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

@ -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"}]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -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

View File

@ -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",

View File

@ -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;

View File

@ -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

View File

@ -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",

View File

@ -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();
})()

View File

@ -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": {

File diff suppressed because it is too large Load Diff