Merge remote-tracking branch 'upstream/master'
|
@ -176,9 +176,13 @@
|
|||
</label>
|
||||
<label class="form-switch">
|
||||
<input type="checkbox" id="settings-alwaysAllowUpdate">
|
||||
<i class="form-icon"></i> Always allow to reinstall apps in place regardless of the version
|
||||
<i class="form-icon"></i> Always show "reinstall app" button <i class="icon icon-refresh"></i> regardless of the version
|
||||
</label>
|
||||
<button class="btn" id="defaultsettings">Reset to default App Loader settings</button>
|
||||
<label class="form-switch">
|
||||
<input type="checkbox" id="settings-autoReload">
|
||||
<i class="form-icon"></i> Automatically reload watch after app App Loader actions (removes "Hold button" prompt)
|
||||
</label>
|
||||
<button class="btn" id="defaultsettings">Reset App Loader settings to defaults</button>
|
||||
</details>
|
||||
</div>
|
||||
<div id="more-deviceinfo" style="display:none">
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
0.01: Change log created
|
|
@ -0,0 +1,18 @@
|
|||
Tyreid
|
||||
|
||||
Tyreid is a Bluetooth war-driving app for the Bangle.js 2.
|
||||
|
||||
Menu options:
|
||||
- Start: This turns on the Bluetooth and starts logging Bluetooth packets with time, latitude, and longitude information to a CSV file.
|
||||
- Pause/Continue: These functions pause the capture and then allow it to resume.
|
||||
- Devices: When paused this menu option will display the MAC addresses of discovered Bluetooth devices. Selecting a device will then display the MAC, Manufacturer code, the time it was first seen, and the RSSI of the first sighting.
|
||||
- Marker: This command adds a 'marker' to the CSV log, which consists of the time and location information, but the Bluetooth packet information is replaced with the word MARKER. Markers can also be added by pressing the watch's button.
|
||||
- Exit: This exits the app.
|
||||
|
||||
The current number of discovered devices is displayed in the top left corner.
|
||||
This value is displayed in green when the GPS has a fix, or red otherwise.
|
||||
|
||||
To retrieve the CSV file, connect to the watch through the Espruino web IDE (https://www.espruino.com/ide/). From there the files stored on the watch can be downloaded by clicking the storage icon in the IDE's central column.
|
||||
|
||||
|
||||
|
|
@ -0,0 +1 @@
|
|||
E.toArrayBuffer(atob("MDACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAtAAAAAAAAAAAAAAB/QAAAAAAAAAAAAAD/0AAAAAAAAAAAAAH+9AAAAAAAAAAAAAPtfQAAAAAAAAAAAAudH0AAAAAAAAAAAB8tB9AAAAAAAAAAAD0tAfQAAAAAAAAAAHgtAH0AAAAAAAAAAPAtAB9AAAAAAAAAAtAtAAfQAAAAAAAAB8AtAAH0AAAAAAAAD0AtAAB9AAAAAAAALgAtAAAfQAAAAAAAfAAtAAAH0AAAAAAA9AAtAAAB9AAAAAAC4AAtAAAAfQAAAAADwAAtAAAALwAAAAAAQAAtAAAAvgAAAAAAAAAsAAAC9AAAAAAAAAAsAAAP0AAAAAAAAAAsAAB/AAAAAAAAAAAsAAH4AAAAAAAAAAAsAAvQAAAAAAAAAAAsAD9AAAAAAAAAAAAsAD0AAAAAAAAAAAAsAB8AAAAAAAAAAAAsAAtAAAAAAAAAAAAsAAPQAAAAAAAAAAAsAAHwAAAAAAAAAAAsAAC4AAAAAAAAAAAsAAA9AAAAAAAAAAAsAAAPAAAAAAAAAAAsAAAHgAAAAAAAAAA8AAAC0AAAAAAAAAA8AAAA8AAAAAAAAAA8AAAAEAAAAAAAAAA8AAAAAAAAAAAAAAA8AAAAAAAAAAAAAAA8AAAAAAAAAAAAAAA8AAAAAAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"))
|
|
@ -0,0 +1,14 @@
|
|||
{ "id": "Tyreid",
|
||||
"name": "Tyreid",
|
||||
"shortName":"Tyreid",
|
||||
"version":"0.01",
|
||||
"description": "Bluetooth war-driving app for Bangle.js 2",
|
||||
"icon": "small_logo.png",
|
||||
"tags": "",
|
||||
"supports" : ["BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
"storage": [
|
||||
{"name":"Tyreid.app.js","url":"app.js"},
|
||||
{"name":"Tyreid.img","url":"app-icon.js","evaluate":true}
|
||||
]
|
||||
}
|
After Width: | Height: | Size: 8.2 KiB |
|
@ -46,3 +46,4 @@
|
|||
0.41: Fix a menu bug affecting alarms with empty messages.
|
||||
0.42: Fix date not getting saved in event edit menu when tapping Confirm
|
||||
0.43: New settings: Show confirm, Show Overflow, Show Type.
|
||||
0.44: Add "delete timer after expiration" setting to events.
|
||||
|
|
|
@ -106,6 +106,9 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex, withDate) {
|
|||
var isNew = alarmIndex === undefined;
|
||||
|
||||
var alarm = require("sched").newDefaultAlarm();
|
||||
if (withDate || selectedAlarm.date) {
|
||||
alarm.del = require("sched").getSettings().defaultDeleteExpiredTimers;
|
||||
}
|
||||
alarm.dow = handleFirstDayOfWeek(alarm.dow);
|
||||
|
||||
if (selectedAlarm) {
|
||||
|
@ -193,6 +196,9 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex, withDate) {
|
|||
/*LANG*/"Repeat": {
|
||||
value: decodeRepeat(alarm),
|
||||
onchange: () => setTimeout(showEditRepeatMenu, 100, alarm.rp, date || alarm.dow, (repeat, dow) => {
|
||||
if (repeat) {
|
||||
alarm.del = false; // do not auto delete a repeated alarm
|
||||
}
|
||||
alarm.rp = repeat;
|
||||
alarm.dow = dow;
|
||||
prepareAlarmForSave(alarm, alarmIndex, time, date, true);
|
||||
|
@ -204,6 +210,10 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex, withDate) {
|
|||
value: alarm.as,
|
||||
onchange: v => alarm.as = v
|
||||
},
|
||||
/*LANG*/"Delete After Expiration": {
|
||||
value: alarm.del,
|
||||
onchange: v => alarm.del = v
|
||||
},
|
||||
/*LANG*/"Hidden": {
|
||||
value: alarm.hidden || false,
|
||||
onchange: v => alarm.hidden = v
|
||||
|
@ -225,6 +235,7 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex, withDate) {
|
|||
delete menu[/*LANG*/"Day"];
|
||||
delete menu[/*LANG*/"Month"];
|
||||
delete menu[/*LANG*/"Year"];
|
||||
delete menu[/*LANG*/"Delete After Expiration"];
|
||||
}
|
||||
|
||||
if (!isNew) {
|
||||
|
@ -283,7 +294,6 @@ function decodeRepeat(alarm) {
|
|||
}
|
||||
|
||||
function showEditRepeatMenu(repeat, day, dowChangeCallback) {
|
||||
var originalRepeat = repeat;
|
||||
var dow;
|
||||
|
||||
const menu = {
|
||||
|
@ -316,26 +326,32 @@ function showEditRepeatMenu(repeat, day, dowChangeCallback) {
|
|||
},
|
||||
/*LANG*/"Custom": {
|
||||
value: isCustom ? decodeRepeat({ rp: true, dow: dow }) : false,
|
||||
onchange: () => setTimeout(showCustomDaysMenu, 10, dow, dowChangeCallback, originalRepeat, originalDow)
|
||||
onchange: () => setTimeout(showCustomDaysMenu, 10, dow, dowChangeCallback, repeat, originalDow)
|
||||
}
|
||||
};
|
||||
} else {
|
||||
// var date = day; // eventually: detect day of date and configure a repeat e.g. 3rd Monday of Month
|
||||
dow = EVERY_DAY;
|
||||
repeat = repeat || {interval: "month", num: 1};
|
||||
const repeatObj = repeat || {interval: "month", num: 1};
|
||||
|
||||
restOfMenu = {
|
||||
/*LANG*/"Every": {
|
||||
value: repeat.num,
|
||||
value: repeatObj.num,
|
||||
min: 1,
|
||||
onchange: v => repeat.num = v
|
||||
onchange: v => {
|
||||
repeat = repeatObj;
|
||||
repeat.num = v;
|
||||
}
|
||||
},
|
||||
/*LANG*/"Interval": {
|
||||
value: INTERVALS.indexOf(repeat.interval),
|
||||
value: INTERVALS.indexOf(repeatObj.interval),
|
||||
format: v => INTERVAL_LABELS[v],
|
||||
min: 0,
|
||||
max: INTERVALS.length - 1,
|
||||
onchange: v => repeat.interval = INTERVALS[v]
|
||||
onchange: v => {
|
||||
repeat = repeatObj;
|
||||
repeat.interval = INTERVALS[v];
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"id": "alarm",
|
||||
"name": "Alarms & Timers",
|
||||
"shortName": "Alarms",
|
||||
"version": "0.43",
|
||||
"version": "0.44",
|
||||
"description": "Set alarms and timers on your Bangle",
|
||||
"icon": "app.png",
|
||||
"tags": "tool,alarm",
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
0.1: Initial release
|
||||
0.2: Added more descriptive approximations
|
||||
0.2f: Bug fixes: Incorrect hour drawn after 50 mins, incorrect quarter minute drawn after 50 mins
|
||||
0.3: Added touch interaction to display exact time and date.
|
|
@ -0,0 +1 @@
|
|||
atob("MDAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAArgVYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABW19cAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACsrNcrAACBVoGsVgAAgVaBrFYAAKzXK4GsKwAAgayBAAAAgVYAVoEAAAAAAAAAAADXVqyBAACs14Gs1ysArNeBrNcrAIHX14HXgQCB14HXrAAAVtdW11YAAAAAAAAAAFbXK4GsAACs1wAr11YArNcAK9dWAADXrABWVgDXgQCB1wAAAKzXgQAAAAAAAAAAAKzXrNfXKwCs1wAA14EArKwAK9dWAADXVgAAAADXgQBW1ysAAIHXVgAAAAAAAAAAANfXgYHXVgCs11aB11YArNcrgddWACvXgSsAAACsrCus1wAAK9es1ysAAAAAAAAAK9dWAACsrACsrKzXrAAArKzX16wAVtfX16wAAAAr19fXKwAArKwArKwAAAAAAAAAAAAAAAAAAACsrAArAAAArIEAKwAAAAAAAAAAAAAAACsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACsrAAAAAAArIEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABWVgAAAAAAVlYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAArVoErAAAAAAAAAAAAAAAAAAAAAAArVgAAAAAAAAAAAAAAAAAAAAAAAAArrNfXgQCBrNdWAAAAAAAAAAAAAAAAAAAAAABW1wAAAAAAAAAAAAAAAAAAAAAAACvXrCtWgQAAANdWAAAAAAArKwAAAAAAACsAAABW1wAAAAAAAAAAAAAAAAAAAAAAAFbXKwAAAAAAANdWAAAAAIHX16wAAACB19fXVgBW1wAA14EAAAAAAAAAAAAAAAAAAIGsAAAAAAAAANdWAAAAK9eBVteBACvXgSuBVgBW1wBW1ysAAAAAAAAAAAAAAAAAAIHXAAAAAAAAANdWAAAAVtcrAKyBAFbXKwAAAABW16zX1wAAAAAAAAAAAAAAAAAAAFbXVgAAAAAAANeBAAAAVtcrANeBAFbXKwAAAABW14HXrAAAAAAAAAAAAAAAAAAAAACs14GBrAAAAKzXrIEAK9esrNdWACvX14GBVgBW1wAr11YAAAAAAAAAAAAAAAAAAAAAVoGBgQAAACusrIEAACusrFYAAAArgaysVgBWgQAAgYEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
|
|
@ -0,0 +1,156 @@
|
|||
//load fonts
|
||||
require("FontSinclair").add(Graphics);
|
||||
require("FontTeletext5x9Ascii").add(Graphics);
|
||||
|
||||
//const
|
||||
|
||||
const numbers = {
|
||||
"0": "Twelve",
|
||||
"1": "One",
|
||||
"2": "Two",
|
||||
"3": "Three",
|
||||
"4": "Four",
|
||||
"5": "Five",
|
||||
"6": "Six",
|
||||
"7": "Seven",
|
||||
"8": "Eight",
|
||||
"9": "Nine",
|
||||
"10": "Ten",
|
||||
"11": "Eleven",
|
||||
"12": "Twelve",
|
||||
"13": "One",
|
||||
"14": "Two",
|
||||
"15": "Three",
|
||||
"16": "Four",
|
||||
"17": "Five",
|
||||
"18": "Six",
|
||||
"19": "Seven",
|
||||
"20": "Eight",
|
||||
"21": "Nine",
|
||||
"22": "Ten",
|
||||
"23": "Eleven",
|
||||
"24": "Twelve",
|
||||
};
|
||||
|
||||
const minutesByQuarterString = {
|
||||
0: "O'Clock",
|
||||
15: "Fifteen",
|
||||
30: "Thirty",
|
||||
45: "Fourty-Five"
|
||||
};
|
||||
|
||||
const width = g.getWidth();
|
||||
const height = g.getHeight();
|
||||
let drawTimeout;
|
||||
|
||||
const getNearestHour = (hours, minutes) => {
|
||||
if (minutes > 54) {
|
||||
return hours + 1;
|
||||
}
|
||||
return hours;
|
||||
};
|
||||
|
||||
const getApproximatePrefix = (minutes, minutesByQuarter) => {
|
||||
if (minutes === minutesByQuarter) {
|
||||
return " exactly";
|
||||
} else if (minutesByQuarter - minutes < -54) {
|
||||
return " nearly";
|
||||
} else if (minutesByQuarter - minutes < -5) {
|
||||
return " after";
|
||||
} else if (minutesByQuarter - minutes < 0) {
|
||||
return " just after";
|
||||
} else if (minutesByQuarter - minutes > 5) {
|
||||
return " before";
|
||||
} else {
|
||||
return " nearly";
|
||||
}
|
||||
};
|
||||
|
||||
const getMinutesByQuarter = minutes => {
|
||||
if (minutes < 10) {
|
||||
return 0;
|
||||
} else if (minutes < 20) {
|
||||
return 15;
|
||||
} else if (minutes < 40) {
|
||||
return 30;
|
||||
} else if (minutes < 55) {
|
||||
return 45;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
// schedule a draw for the next minute
|
||||
function queueDraw() {
|
||||
if (drawTimeout) clearTimeout(drawTimeout);
|
||||
drawTimeout = setTimeout(function () {
|
||||
drawTimeout = undefined;
|
||||
drawTime();
|
||||
}, 60000 - (Date.now() % 60000));
|
||||
}
|
||||
|
||||
const drawTimeExact = () => {
|
||||
var dateTime = Date();
|
||||
var hours = dateTime.getHours();
|
||||
var minutes = dateTime.getMinutes().toString().padStart(2,0);
|
||||
var day = dateTime.getDay();
|
||||
var date = dateTime.getDate();
|
||||
var month = dateTime.getMonth();
|
||||
var year = dateTime.getFullYear();
|
||||
g.clear();
|
||||
g.setBgColor(0,0,0);
|
||||
g.clearRect(0,0,width, height);
|
||||
g.setColor(1,1,1);
|
||||
g.setFont("Vector", 30);
|
||||
g.drawString(hours + ":" + minutes, (width - g.stringWidth(hours + ":" + minutes))/2, height * 0.3, false);
|
||||
g.setFont("Vector", 26);
|
||||
g.drawString(month + 1 + "/" + date + "/" + year, (width - g.stringWidth(month + 1 + "/" + date + "/" + year))/2, height * 0.6, false);
|
||||
};
|
||||
|
||||
const drawTime = () => {
|
||||
//Grab time vars
|
||||
var date = Date();
|
||||
var hour = date.getHours();
|
||||
var minutes = date.getMinutes();
|
||||
var minutesByQuarter = getMinutesByQuarter(minutes);
|
||||
|
||||
//reset graphics
|
||||
g.clear();
|
||||
g.reset();
|
||||
|
||||
//Build watch face
|
||||
g.setBgColor(0, 0, 0);
|
||||
g.clearRect(0, 0, width, height);
|
||||
g.setFont("Vector", 22);
|
||||
g.setColor(1, 1, 1);
|
||||
g.drawString("It's" + getApproximatePrefix(minutes, minutesByQuarter), (width - g.stringWidth("It's" + getApproximatePrefix(minutes, minutesByQuarter))) / 2, height * 0.25, false);
|
||||
g.setFont("Vector", 30);
|
||||
g.drawString(numbers[getNearestHour(hour, minutes)], (width - g.stringWidth(numbers[getNearestHour(hour, minutes)])) / 2, height * 0.45, false);
|
||||
g.setFont("Vector", 22);
|
||||
g.drawString(minutesByQuarterString[minutesByQuarter], (width - g.stringWidth(minutesByQuarterString[minutesByQuarter])) / 2, height * 0.7, false);
|
||||
|
||||
queueDraw();
|
||||
};
|
||||
|
||||
g.clear();
|
||||
drawTime();
|
||||
|
||||
Bangle.on('lcdPower', function (on) {
|
||||
if (on) {
|
||||
drawTime();
|
||||
} else {
|
||||
if (idTimeout) {
|
||||
clearTimeout(idTimeout);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Bangle.on('touch', function(button, xy){
|
||||
drawTimeExact();
|
||||
setTimeout(drawTime, 7000);
|
||||
});
|
||||
|
||||
// Show launcher when button pressed
|
||||
Bangle.setUI("clock");
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
After Width: | Height: | Size: 2.0 KiB |
|
@ -0,0 +1,18 @@
|
|||
{ "id": "approxclock",
|
||||
"name": "Approximate Clock",
|
||||
"shortName" : "Approx Clock",
|
||||
"version": "0.3",
|
||||
"icon": "app.png",
|
||||
"description": "A really basic spelled out time display for people looking for the vague time at a glance.",
|
||||
"readme": "readme.md",
|
||||
"type": "clock",
|
||||
"tags": "clock",
|
||||
"supports": ["BANGLEJS2"],
|
||||
"storage": [
|
||||
{"name":"approxclock.app.js","url":"app.js"},
|
||||
{"name":"approxclock.img","url":"app-icon.js","evaluate":true}
|
||||
],
|
||||
"screenshots": [
|
||||
{"url": "screenshot.png"}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
## Approximate Clock
|
||||
|
||||
### Description
|
||||
|
||||
Get a rough idea of the time at a quick glance, mostly made for myself based on a similar watchface on pebble. I find this keeps me from checking my watch too often and also saves me from moments of severe brainfart staring at these mysterious symbols we call numbers.
|
||||
|
||||
Exact time and date can be viewed temporarily by touching the screen.
|
After Width: | Height: | Size: 1.9 KiB |
|
@ -3,3 +3,6 @@
|
|||
0.03: Update to use Bangle.setUI instead of setWatch
|
||||
0.04: Tell clock widgets to hide.
|
||||
0.05: Added adjustment for Bangle.js magnetometer heading fix
|
||||
0.06: optimized to update much faster
|
||||
0.07: added support for bangle.js 2
|
||||
0.08: call setUI before loading widgets to indicate we're a clock
|
||||
|
|
|
@ -1,29 +1,29 @@
|
|||
Astral Clock
|
||||
============
|
||||
NOTE FOR THE BANGLE 2 THIS APP ONLY SUPPORTS USING THE BLACK BACKGROUND CURRENTLY
|
||||
|
||||
Clock that calculates and displays Alt Az positions of all planets, Sun as well as several other astronomy targets (customizable) and current Moon phase. Coordinates are calculated by GPS & time and onscreen compass assists orienting.
|
||||
|
||||

|
||||
<sup>(The clock does have Pluto now - felt bad for leaving it out)</sup>
|
||||
|
||||
Functions
|
||||
---------
|
||||
**BTN1**: Refreshes Alt/Az readings. The coordinates are NOT continually updated, this is to save resources and battery usage plus it avoids you having to wait for calculations to finish before you can do anything else on the watch - it doesn't take long but it could still be annoying.
|
||||
**BTN2**: Load side-menu as standard for clocks.
|
||||
**BTN3**: Changes between planet mode and extra/other targets - discussed below (will still need to press button 1 after switching to update calcs).
|
||||
**BTN4**: This is the left touchscreen, and when the LCD is on you can use this to change the font between red/white. This will only work after the GPS location has been set initially.
|
||||
---------
|
||||
**BTN2**: Load side-menu as standard for clocks.
|
||||
Swiping left or right will alternate between planets and other astronomy targets, see below for how to change these addtional ones.
|
||||
|
||||
The text will turn blue during calculation and then back again once complete.
|
||||
The data is refreshed automatically every 2 minutes. You can force a refresh as well by swiping up or, on Bangle 1, pressing Button 3.
|
||||
|
||||
When you first install it, all positions will be estimated from UK as the default location and all the text will be white; from the moment you get your first GPS lock with the clock, it will save your location, recalculate accordingly and change the text to red, ideal for maintaining night vision, the calculations will also now be relevant to your location and time. If you have not used the GPS yet, I suggest using it outside briefly to get your first fix as the initial one can take a bit longer, although it should still just be a minute or 2 max normally.
|
||||
Swiping down will disable/enable the compass and GPS.
|
||||
|
||||
When you first install it, all positions will be estimated from UK as the default location and all the text will be white; from the moment you get your first GPS lock with the clock, it will save your location, recalculate accordingly and change the text to red, ideal for maintaining night vision. One the Bangle.JS 2, the colour will be a light blue rather than red because the colours are not as vibrant. The calculations will also now be relevant to your location and time. If you have not used the GPS yet, I suggest using it outside briefly to get your first fix as the initial one can take a bit longer, although it should still just be a minute or 2 max normally.
|
||||
Lat and Lon are saved in a file called **astral.config**. You can review this file if you want to confirm current coordinates or even hard set different values \- although be careful doing the latter as there is no error handling to manage bad values here so you would have to delete the file and have the app generate a new one if that happens, also the GPS functionality will overwrite anything you put in here once it picks up your location.
|
||||
|
||||
There can currently be a slight error mainly to the Az at times due to a firmware issue for acos (arccosine) that affect spherical calculations but I have used an estimator function that gives a good enough accuracy for general observation so shouldn't noticeably be too far off. I\'ll be implementing acos for better accuracy when the fix is in a standard release and the update will still include the current estimate function to support a level of backward compatibility.
|
||||
|
||||
The moon phases are split into the 8 phases with an image for each - new moon would show no image.
|
||||
|
||||
The compass is displayed above the minute digits, if you get strange values or dashes the compass needs calibration but you just need to move the watch around a bit for this each time - ideally 360 degrees around itself, which involves taking the watch off. If you don't want to do that you can also just wave your hand around for a few seconds like you're at a rave or doing Wing Chun Kuen.
|
||||
The compass is displayed on the left.
|
||||
|
||||
Also the compass isn\’t tilt compensated so try and keep the face parallel when taking a reading.
|
||||
Also the compass isn\’t tilt compensated so try and keep the face parallel when taking a reading. It's more of an indicator, for a more accurate compass reading, you can use one of the many great apps in the apploader that compensated for movement and angles of the watch etc.
|
||||
|
||||
Additional Astronomy Targets
|
||||
----------------------------
|
||||
|
@ -37,7 +37,7 @@ The type property is not utilised as yet but relates to whether the object is (i
|
|||
|
||||
Updates & Feedback
|
||||
------------------
|
||||
Put together, initially at least, by \"Ben Jabituya\", https://jabituyaben.wixsite.com/majorinput, jabituyaben@gmail.com. Feel free to get in touch for any feature request. Also I\'m not precious at all - if you know of efficiencies or improvements you could make, just put the changes in. One thing that would probably be ideal is to change some of the functions to inline C to make it faster.
|
||||
Put together, initially at least, by \"Ben Jabituya\", https://majorinput.co.uk, jabituyaben@gmail.com.
|
||||
|
||||
Credit to various sources from which I have literally taken source code and shoehorned to fit on the Bangle:
|
||||
|
||||
|
|
1264
apps/astral/app.js
|
@ -1,13 +1,13 @@
|
|||
{
|
||||
"id": "astral",
|
||||
"name": "Astral Clock",
|
||||
"version": "0.05",
|
||||
"version": "0.08",
|
||||
"description": "Clock that calculates and displays Alt Az positions of all planets, Sun as well as several other astronomy targets (customizable) and current Moon phase. Coordinates are calculated by GPS & time and onscreen compass assists orienting. See Readme before using.",
|
||||
"icon": "app-icon.png",
|
||||
"type": "clock",
|
||||
"tags": "clock",
|
||||
"supports": ["BANGLEJS"],
|
||||
"readme": "README.md",
|
||||
"supports": ["BANGLEJS","BANGLEJS2"],
|
||||
"storage": [
|
||||
{"name":"astral.app.js","url":"app.js"},
|
||||
{"name":"astral.img","url":"app-icon.js","evaluate":true}
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
0.01: New App!
|
||||
0.02: Don't fire if the app uses swipes already.
|
||||
0.03: Only count defined handlers in the handler array.
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
if (Bangle["#on"+eventType] === undefined) {
|
||||
return 0;
|
||||
} else if (Bangle["#on"+eventType] instanceof Array) {
|
||||
return Bangle["#on"+eventType].length;
|
||||
return Bangle["#on"+eventType].filter(x=>x).length;
|
||||
} else if (Bangle["#on"+eventType] !== undefined) {
|
||||
return 1;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{ "id": "backswipe",
|
||||
"name": "Back Swipe",
|
||||
"shortName":"BackSwipe",
|
||||
"version":"0.02",
|
||||
"version":"0.03",
|
||||
"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,3 +4,4 @@
|
|||
0.04: Improve current time readability in light theme.
|
||||
0.05: Show calendar colors & improved all day events.
|
||||
0.06: Improved multi-line locations & titles
|
||||
0.07: Buzz 30, 15 and 1 minute before an event
|
||||
|
|
|
@ -117,6 +117,17 @@ function fullRedraw() {
|
|||
drawFutureEvents(y);
|
||||
}
|
||||
|
||||
function buzzForEvents() {
|
||||
let nextEvent = next[0]; if (!nextEvent) return;
|
||||
if (nextEvent.allDay) return;
|
||||
let minToEvent = Math.round((nextEvent.timestamp - getTime()) / 60.0);
|
||||
switch (minToEvent) {
|
||||
case 30: require("buzz").pattern(","); break;
|
||||
case 15: require("buzz").pattern(", ,"); break;
|
||||
case 1: require("buzz").pattern(": : :"); break;
|
||||
}
|
||||
}
|
||||
|
||||
function redraw() {
|
||||
g.reset();
|
||||
if (current.find(e=>!isActive(e)) || next.find(isActive)) {
|
||||
|
@ -124,10 +135,12 @@ function redraw() {
|
|||
} else {
|
||||
drawCurrentEvents(30);
|
||||
}
|
||||
buzzForEvents();
|
||||
}
|
||||
|
||||
g.clear();
|
||||
fullRedraw();
|
||||
buzzForEvents();
|
||||
var minuteInterval = setInterval(redraw, 60 * 1000);
|
||||
|
||||
Bangle.setUI("clock");
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"id": "calclock",
|
||||
"name": "Calendar Clock",
|
||||
"shortName": "CalClock",
|
||||
"version": "0.06",
|
||||
"version": "0.07",
|
||||
"description": "Show the current and upcoming events synchronized from Gadgetbridge",
|
||||
"icon": "calclock.png",
|
||||
"type": "clock",
|
||||
|
|
|
@ -15,3 +15,5 @@
|
|||
Display events for current month on touch
|
||||
0.14: Add support for holidays
|
||||
0.15: Edit holidays on device in settings
|
||||
0.16: Add menu to fast open settings to edit holidays
|
||||
Display Widgets in menus
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# Calendar
|
||||
|
||||
Basic calendar
|
||||
Monthly calendar, displays holidays uploaded from the web interface and scheduled events.
|
||||
|
||||
## Usage
|
||||
|
||||
|
@ -14,4 +14,4 @@ Basic calendar
|
|||
|
||||
## Settings
|
||||
|
||||
- B2 Colors: use non-dithering colors (default, recommended for Bangle 2) or the original color scheme.
|
||||
B2 Colors: use non-dithering colors (default, recommended for Bangle 2) or the original color scheme.
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
{
|
||||
const maxX = g.getWidth();
|
||||
const maxY = g.getHeight();
|
||||
const fontSize = g.getWidth() > 200 ? 2 : 1;
|
||||
|
@ -17,71 +18,85 @@ const red = "#d41706";
|
|||
const blue = "#0000ff";
|
||||
const yellow = "#ffff00";
|
||||
const cyan = "#00ffff";
|
||||
let bgColor = color4;
|
||||
let bgColorMonth = color1;
|
||||
let bgColorDow = color2;
|
||||
let bgColorWeekend = color3;
|
||||
let fgOtherMonth = gray1;
|
||||
let fgSameMonth = white;
|
||||
let bgEvent = blue;
|
||||
let bgOtherEvent = "#ff8800";
|
||||
let bgColor;
|
||||
let bgColorMonth;
|
||||
let bgColorDow;
|
||||
let bgColorWeekend;
|
||||
let fgOtherMonth;
|
||||
let fgSameMonth;
|
||||
let bgEvent;
|
||||
let bgOtherEvent;
|
||||
const eventsPerDay=6; // how much different events per day we can display
|
||||
const date = new Date();
|
||||
|
||||
const timeutils = require("time_utils");
|
||||
let settings = require('Storage').readJSON("calendar.json", true) || {};
|
||||
let startOnSun = ((require("Storage").readJSON("setting.json", true) || {}).firstDayOfWeek || 0) === 0;
|
||||
// all alarms that run on a specific date
|
||||
const events = (require("Storage").readJSON("sched.json",1) || []).filter(a => a.on && a.date).map(a => {
|
||||
const date = new Date(a.date);
|
||||
const time = timeutils.decodeTime(a.t);
|
||||
date.setHours(time.h);
|
||||
date.setMinutes(time.m);
|
||||
date.setSeconds(time.s);
|
||||
return {date: date, msg: a.msg, type: "e"};
|
||||
});
|
||||
// add holidays & other events
|
||||
(require("Storage").readJSON("calendar.days.json",1) || []).forEach(d => {
|
||||
const date = new Date(d.date);
|
||||
const o = {date: date, msg: d.name, type: d.type};
|
||||
if (d.repeat) {
|
||||
o.repeat = d.repeat;
|
||||
}
|
||||
events.push(o);
|
||||
});
|
||||
|
||||
if (settings.ndColors === undefined) {
|
||||
settings.ndColors = !g.theme.dark;
|
||||
}
|
||||
|
||||
if (settings.ndColors === true) {
|
||||
bgColor = white;
|
||||
bgColorMonth = blue;
|
||||
bgColorDow = black;
|
||||
bgColorWeekend = yellow;
|
||||
fgOtherMonth = blue;
|
||||
fgSameMonth = black;
|
||||
bgEvent = color2;
|
||||
bgOtherEvent = cyan;
|
||||
}
|
||||
|
||||
function getDowLbls(locale) {
|
||||
let days = startOnSun ? [0, 1, 2, 3, 4, 5, 6] : [1, 2, 3, 4, 5, 6, 0];
|
||||
let events;
|
||||
const dowLbls = function() {
|
||||
const locale = require('locale').name;
|
||||
const days = startOnSun ? [0, 1, 2, 3, 4, 5, 6] : [1, 2, 3, 4, 5, 6, 0];
|
||||
const d = new Date();
|
||||
return days.map(i => {
|
||||
d.setDate(d.getDate() + (i + 7 - d.getDay()) % 7);
|
||||
return require("locale").dow(d, 1);
|
||||
});
|
||||
}
|
||||
}();
|
||||
|
||||
function sameDay(d1, d2) {
|
||||
const loadEvents = () => {
|
||||
// all alarms that run on a specific date
|
||||
events = (require("Storage").readJSON("sched.json",1) || []).filter(a => a.on && a.date).map(a => {
|
||||
const date = new Date(a.date);
|
||||
const time = timeutils.decodeTime(a.t);
|
||||
date.setHours(time.h);
|
||||
date.setMinutes(time.m);
|
||||
date.setSeconds(time.s);
|
||||
return {date: date, msg: a.msg, type: "e"};
|
||||
});
|
||||
// add holidays & other events
|
||||
(require("Storage").readJSON("calendar.days.json",1) || []).forEach(d => {
|
||||
const date = new Date(d.date);
|
||||
const o = {date: date, msg: d.name, type: d.type};
|
||||
if (d.repeat) {
|
||||
o.repeat = d.repeat;
|
||||
}
|
||||
events.push(o);
|
||||
});
|
||||
};
|
||||
|
||||
const loadSettings = () => {
|
||||
let settings = require('Storage').readJSON("calendar.json", true) || {};
|
||||
if (settings.ndColors === undefined) {
|
||||
settings.ndColors = !g.theme.dark;
|
||||
}
|
||||
if (settings.ndColors === true) {
|
||||
bgColor = white;
|
||||
bgColorMonth = blue;
|
||||
bgColorDow = black;
|
||||
bgColorWeekend = yellow;
|
||||
fgOtherMonth = blue;
|
||||
fgSameMonth = black;
|
||||
bgEvent = color2;
|
||||
bgOtherEvent = cyan;
|
||||
} else {
|
||||
bgColor = color4;
|
||||
bgColorMonth = color1;
|
||||
bgColorDow = color2;
|
||||
bgColorWeekend = color3;
|
||||
fgOtherMonth = gray1;
|
||||
fgSameMonth = white;
|
||||
bgEvent = blue;
|
||||
bgOtherEvent = "#ff8800";
|
||||
}
|
||||
};
|
||||
|
||||
const sameDay = function(d1, d2) {
|
||||
"jit";
|
||||
return d1.getFullYear() === d2.getFullYear() &&
|
||||
d1.getMonth() === d2.getMonth() &&
|
||||
d1.getDate() === d2.getDate();
|
||||
}
|
||||
};
|
||||
|
||||
function drawEvent(ev, curDay, x1, y1, x2, y2) {
|
||||
const drawEvent = function(ev, curDay, x1, y1, x2, y2) {
|
||||
"ram";
|
||||
switch(ev.type) {
|
||||
case "e": // alarm/event
|
||||
|
@ -99,9 +114,33 @@ function drawEvent(ev, curDay, x1, y1, x2, y2) {
|
|||
g.setColor(bgOtherEvent).fillRect(x1+1, y1+1, x2-1, y2-1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function drawCalendar(date) {
|
||||
const calcDays = (month, monthMaxDayMap, dowNorm) => {
|
||||
"jit";
|
||||
const maxDay = colN * (rowN - 1) + 1;
|
||||
const days = [];
|
||||
let nextMonthDay = 1;
|
||||
let thisMonthDay = 51;
|
||||
const month2 = month;
|
||||
let prevMonthDay = monthMaxDayMap[month > 0 ? month - 1 : 11] - dowNorm + 1;
|
||||
|
||||
for (let i = 0; i < maxDay; i++) {
|
||||
if (i < dowNorm) {
|
||||
days.push(prevMonthDay);
|
||||
prevMonthDay++;
|
||||
} else if (thisMonthDay <= monthMaxDayMap[month] + 50) {
|
||||
days.push(thisMonthDay);
|
||||
thisMonthDay++;
|
||||
} else {
|
||||
days.push(nextMonthDay);
|
||||
nextMonthDay++;
|
||||
}
|
||||
}
|
||||
return days;
|
||||
};
|
||||
|
||||
const drawCalendar = function(date) {
|
||||
g.setBgColor(bgColor);
|
||||
g.clearRect(0, 0, maxX, maxY);
|
||||
g.setBgColor(bgColorMonth);
|
||||
|
@ -139,7 +178,6 @@ function drawCalendar(date) {
|
|||
true
|
||||
);
|
||||
|
||||
let dowLbls = getDowLbls(require('locale').name);
|
||||
dowLbls.forEach((lbl, i) => {
|
||||
g.drawString(lbl, i * colW + colW / 2, headerH + rowH / 2);
|
||||
});
|
||||
|
@ -163,23 +201,7 @@ function drawCalendar(date) {
|
|||
11: 31
|
||||
};
|
||||
|
||||
let days = [];
|
||||
let nextMonthDay = 1;
|
||||
let thisMonthDay = 51;
|
||||
let prevMonthDay = monthMaxDayMap[month > 0 ? month - 1 : 11] - dowNorm + 1;
|
||||
for (let i = 0; i < colN * (rowN - 1) + 1; i++) {
|
||||
if (i < dowNorm) {
|
||||
days.push(prevMonthDay);
|
||||
prevMonthDay++;
|
||||
} else if (thisMonthDay <= monthMaxDayMap[month] + 50) {
|
||||
days.push(thisMonthDay);
|
||||
thisMonthDay++;
|
||||
} else {
|
||||
days.push(nextMonthDay);
|
||||
nextMonthDay++;
|
||||
}
|
||||
}
|
||||
|
||||
const days = calcDays(month, monthMaxDayMap, dowNorm);
|
||||
const weekBeforeMonth = new Date(date.getTime());
|
||||
weekBeforeMonth.setDate(weekBeforeMonth.getDate() - 7);
|
||||
const week2AfterMonth = new Date(date.getFullYear(), date.getMonth()+1, 0);
|
||||
|
@ -189,8 +211,15 @@ function drawCalendar(date) {
|
|||
ev.date.setFullYear(ev.date.getMonth() < 6 ? week2AfterMonth.getFullYear() : weekBeforeMonth.getFullYear());
|
||||
}
|
||||
});
|
||||
const eventsThisMonth = events.filter(ev => ev.date > weekBeforeMonth && ev.date < week2AfterMonth);
|
||||
eventsThisMonth.sort((a,b) => a.date - b.date);
|
||||
|
||||
const eventsThisMonthPerDay = events.filter(ev => ev.date > weekBeforeMonth && ev.date < week2AfterMonth).reduce((acc, ev) => {
|
||||
const day = ev.date.getDate();
|
||||
if (!acc[day]) {
|
||||
acc[day] = [];
|
||||
}
|
||||
acc[day].push(ev);
|
||||
return acc;
|
||||
}, []);
|
||||
let i = 0;
|
||||
g.setFont("8x12", fontSize);
|
||||
for (y = 0; y < rowN - 1; y++) {
|
||||
|
@ -205,13 +234,13 @@ function drawCalendar(date) {
|
|||
const x2 = x * colW + colW;
|
||||
const y2 = y * rowH + headerH + rowH + rowH;
|
||||
|
||||
if (eventsThisMonth.length > 0) {
|
||||
const eventsThisDay = eventsThisMonthPerDay[curDay.getDate()];
|
||||
if (eventsThisDay && eventsThisDay.length > 0) {
|
||||
// Display events for this day
|
||||
eventsThisMonth.forEach((ev, idx) => {
|
||||
eventsThisDay.forEach((ev, idx) => {
|
||||
if (sameDay(ev.date, curDay)) {
|
||||
drawEvent(ev, curDay, x1, y1, x2, y2);
|
||||
|
||||
eventsThisMonth.splice(idx, 1); // this event is no longer needed
|
||||
eventsThisDay.splice(idx, 1); // this event is no longer needed
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -235,9 +264,44 @@ function drawCalendar(date) {
|
|||
);
|
||||
} // end for (x = 0; x < colN; x++)
|
||||
} // end for (y = 0; y < rowN - 1; y++)
|
||||
} // end function drawCalendar
|
||||
}; // end function drawCalendar
|
||||
|
||||
const showMenu = function() {
|
||||
const menu = {
|
||||
"" : {
|
||||
title : "Calendar",
|
||||
remove: () => {
|
||||
require("widget_utils").show();
|
||||
}
|
||||
},
|
||||
"< Back": () => {
|
||||
require("widget_utils").hide();
|
||||
E.showMenu();
|
||||
setUI();
|
||||
},
|
||||
/*LANG*/"Exit": () => load(),
|
||||
/*LANG*/"Settings": () => {
|
||||
const appSettings = eval(require('Storage').read('calendar.settings.js'));
|
||||
appSettings(() => {
|
||||
loadSettings();
|
||||
loadEvents();
|
||||
showMenu();
|
||||
});
|
||||
},
|
||||
};
|
||||
if (require("Storage").read("alarm.app.js")) {
|
||||
menu[/*LANG*/"Launch Alarms"] = () => {
|
||||
load("alarm.app.js");
|
||||
};
|
||||
}
|
||||
require("widget_utils").show();
|
||||
E.showMenu(menu);
|
||||
};
|
||||
|
||||
const setUI = function() {
|
||||
require("widget_utils").hide(); // No space for widgets!
|
||||
drawCalendar(date);
|
||||
|
||||
function setUI() {
|
||||
Bangle.setUI({
|
||||
mode : "custom",
|
||||
swipe: (dirLR, dirUD) => {
|
||||
|
@ -261,7 +325,14 @@ function setUI() {
|
|||
drawCalendar(date);
|
||||
}
|
||||
},
|
||||
btn: (n) => n === (process.env.HWVERSION === 2 ? 1 : 3) && load(),
|
||||
btn: (n) => {
|
||||
if (process.env.HWVERSION === 2 || n === 2) {
|
||||
showMenu();
|
||||
} else if (n === 3) {
|
||||
// directly exit only on Bangle.js 1
|
||||
load();
|
||||
}
|
||||
},
|
||||
touch: (n,e) => {
|
||||
events.sort((a,b) => a.date - b.date);
|
||||
const menu = events.filter(ev => ev.date.getFullYear() === date.getFullYear() && ev.date.getMonth() === date.getMonth()).map(e => {
|
||||
|
@ -274,16 +345,19 @@ function setUI() {
|
|||
}
|
||||
menu[""] = { title: require("locale").month(date) + " " + date.getFullYear() };
|
||||
menu["< Back"] = () => {
|
||||
require("widget_utils").hide();
|
||||
E.showMenu();
|
||||
drawCalendar(date);
|
||||
setUI();
|
||||
};
|
||||
require("widget_utils").show();
|
||||
E.showMenu(menu);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
loadSettings();
|
||||
loadEvents();
|
||||
Bangle.loadWidgets();
|
||||
require("Font8x12").add(Graphics);
|
||||
drawCalendar(date);
|
||||
setUI();
|
||||
// No space for widgets!
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"id": "calendar",
|
||||
"name": "Calendar",
|
||||
"version": "0.15",
|
||||
"description": "Simple calendar",
|
||||
"version": "0.16",
|
||||
"description": "Monthly calendar, displays holidays uploaded from the web interface and scheduled events.",
|
||||
"icon": "calendar.png",
|
||||
"screenshots": [{"url":"screenshot_calendar.png"}],
|
||||
"tags": "calendar,tool",
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
(function (back) {
|
||||
var FILE = "calendar.json";
|
||||
const FILE = "calendar.json";
|
||||
const HOLIDAY_FILE = "calendar.days.json";
|
||||
var settings = require('Storage').readJSON(FILE, true) || {};
|
||||
if (settings.ndColors === undefined)
|
||||
const settings = require('Storage').readJSON(FILE, true) || {};
|
||||
if (settings.ndColors === undefined) {
|
||||
if (process.env.HWVERSION == 2) {
|
||||
settings.ndColors = true;
|
||||
} else {
|
||||
settings.ndColors = false;
|
||||
}
|
||||
const holidays = require("Storage").readJSON(HOLIDAY_FILE,1).sort((a,b) => new Date(a.date) - new Date(b.date)) || [];
|
||||
}
|
||||
const holidays = (require("Storage").readJSON(HOLIDAY_FILE,1)||[]).sort((a,b) => new Date(a.date) - new Date(b.date)) || [];
|
||||
|
||||
function writeSettings() {
|
||||
require('Storage').writeJSON(FILE, settings);
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
0.01: Simple app to display loyalty cards
|
||||
0.02: Hiding widgets while showing the code
|
||||
|
|
|
@ -18,9 +18,8 @@ Bangle.drawWidgets();
|
|||
const WHITE=-1
|
||||
const BLACK=0
|
||||
|
||||
var FILE = "android.cards.json";
|
||||
|
||||
var Locale = require("locale");
|
||||
const Locale = require("locale");
|
||||
const widget_utils = require('widget_utils');
|
||||
|
||||
var fontSmall = "6x8";
|
||||
var fontMedium = g.getFonts().includes("6x15")?"6x15":"6x8:2";
|
||||
|
@ -90,6 +89,7 @@ function printLinearCode(binary) {
|
|||
}
|
||||
|
||||
function showCode(card) {
|
||||
widget_utils.hide();
|
||||
E.showScroller();
|
||||
// keeping it on rising edge would come back twice..
|
||||
setWatch(()=>showCard(card), BTN, {edge:"falling"});
|
||||
|
@ -151,6 +151,7 @@ function showCard(card) {
|
|||
var titleColor = g.theme.fg2;
|
||||
if (card.color)
|
||||
titleColor = isLight(titleBgColor) ? BLACK : WHITE;
|
||||
widget_utils.show();
|
||||
E.showScroller({
|
||||
h : g.getFontHeight(), // height of each menu item in pixels
|
||||
c : lines.length, // number of menu items
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "cards",
|
||||
"name": "Cards",
|
||||
"version": "0.01",
|
||||
"version": "0.02",
|
||||
"description": "Display loyalty cards",
|
||||
"icon": "app.png",
|
||||
"screenshots": [{"url":"screenshot_cards_overview.png"}, {"url":"screenshot_cards_card1.png"}, {"url":"screenshot_cards_card2.png"}, {"url":"screenshot_cards_barcode.png"}, {"url":"screenshot_cards_qrcode.png"}],
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
0.01: New App!
|
|
@ -0,0 +1,29 @@
|
|||
# Contacts
|
||||
|
||||
This app provides a common way to set up the `contacts.json` file.
|
||||
|
||||
## Contacts JSON file
|
||||
|
||||
When the app is loaded from the app loader, a file named
|
||||
`contacts.json` is loaded along with the javascript etc. The file
|
||||
has the following contents:
|
||||
|
||||
```
|
||||
[
|
||||
{
|
||||
"name":"NONE"
|
||||
},
|
||||
{
|
||||
"name":"First Last",
|
||||
"number":"123456789",
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Contacts Editor
|
||||
|
||||
Clicking on the download icon of `Contents` in the app loader invokes
|
||||
the contact editor. The editor downloads and displays the current
|
||||
`contacts.json` file. Clicking the `Edit` button beside an entry
|
||||
causes the entry to be deleted from the list and displayed in the edit
|
||||
boxes. It can be restored - by clicking the `Add` button.
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwcBkmSpIC/AVsJCJ+AQaCZBCOeACKGQLKGQBA0ggARPJ4IRsYo0ggR9IoAIGiRiIpEECJsAiACBBYoRGpEAI4JBFI47CBLIRlDHYJrGYQIRCwQICL4MQOgx9GboUSeQ4RFwAFBiSGHCIo4CiVIWZyPICP4RaRIQROgARHdIwICoIIFkDpGBAKqHgGACI0AyVIggIDoEEMQ1ICINJCIj4CfwIREBwUgQYYOCfYoFDJQKDFCIopEO4RoDKAqJHRhAC/ATA="))
|
After Width: | Height: | Size: 12 KiB |
|
@ -0,0 +1,189 @@
|
|||
/* contacts.js */
|
||||
|
||||
var Layout = require("Layout");
|
||||
|
||||
const W = g.getWidth();
|
||||
const H = g.getHeight();
|
||||
|
||||
var wp = require('Storage').readJSON("contacts.json", true) || [];
|
||||
// Use this with corrupted contacts
|
||||
//var wp = [];
|
||||
|
||||
var key; /* Shared between functions, typically wp name */
|
||||
|
||||
function writeContact() {
|
||||
require('Storage').writeJSON("contacts.json", wp);
|
||||
}
|
||||
|
||||
function mainMenu() {
|
||||
var menu = {
|
||||
"< Back" : Bangle.load
|
||||
};
|
||||
if (Object.keys(wp).length==0) Object.assign(menu, {"NO Contacts":""});
|
||||
else for (let id in wp) {
|
||||
let i = id;
|
||||
menu[wp[id]["name"]]=()=>{ decode(i); };
|
||||
}
|
||||
menu["Add"]=addCard;
|
||||
menu["Remove"]=removeCard;
|
||||
g.clear();
|
||||
E.showMenu(menu);
|
||||
}
|
||||
|
||||
function decode(pin) {
|
||||
var i = wp[pin];
|
||||
var l = i["name"] + "\n" + i["number"];
|
||||
var la = new Layout ({
|
||||
type:"v", c: [
|
||||
{type:"txt", font:"10%", pad:1, fillx:1, filly:1, label: l},
|
||||
{type:"btn", font:"10%", pad:1, fillx:1, filly:1, label:"OK", cb:l=>{mainMenu();}}
|
||||
], lazy:true});
|
||||
g.clear();
|
||||
la.render();
|
||||
}
|
||||
|
||||
function showNumpad(text, key_, callback) {
|
||||
key = key_;
|
||||
E.showMenu();
|
||||
function addDigit(digit) {
|
||||
key+=digit;
|
||||
if (1) {
|
||||
l = text[key.length];
|
||||
switch (l) {
|
||||
case '.': case ' ': case "'":
|
||||
key+=l;
|
||||
break;
|
||||
case 'd': case 'D': default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
Bangle.buzz(20);
|
||||
update();
|
||||
}
|
||||
function update() {
|
||||
g.reset();
|
||||
g.clearRect(0,0,g.getWidth(),23);
|
||||
s = key + text.substr(key.length, 999);
|
||||
g.setFont("Vector:24").setFontAlign(1,0).drawString(s,g.getWidth(),12);
|
||||
}
|
||||
ds="12%";
|
||||
var numPad = new Layout ({
|
||||
type:"v", c: [{
|
||||
type:"v", c: [
|
||||
{type:"", height:24},
|
||||
{type:"h",filly:1, c: [
|
||||
{type:"btn", font:ds, width:58, label:"7", cb:l=>{addDigit("7");}},
|
||||
{type:"btn", font:ds, width:58, label:"8", cb:l=>{addDigit("8");}},
|
||||
{type:"btn", font:ds, width:58, label:"9", cb:l=>{addDigit("9");}}
|
||||
]},
|
||||
{type:"h",filly:1, c: [
|
||||
{type:"btn", font:ds, width:58, label:"4", cb:l=>{addDigit("4");}},
|
||||
{type:"btn", font:ds, width:58, label:"5", cb:l=>{addDigit("5");}},
|
||||
{type:"btn", font:ds, width:58, label:"6", cb:l=>{addDigit("6");}}
|
||||
]},
|
||||
{type:"h",filly:1, c: [
|
||||
{type:"btn", font:ds, width:58, label:"1", cb:l=>{addDigit("1");}},
|
||||
{type:"btn", font:ds, width:58, label:"2", cb:l=>{addDigit("2");}},
|
||||
{type:"btn", font:ds, width:58, label:"3", cb:l=>{addDigit("3");}}
|
||||
]},
|
||||
{type:"h",filly:1, c: [
|
||||
{type:"btn", font:ds, width:58, label:"0", cb:l=>{addDigit("0");}},
|
||||
{type:"btn", font:ds, width:58, label:"C", cb:l=>{key=key.slice(0,-1); update();}},
|
||||
{type:"btn", font:ds, width:58, id:"OK", label:"OK", cb:callback}
|
||||
]}
|
||||
]}
|
||||
], lazy:true});
|
||||
g.clear();
|
||||
numPad.render();
|
||||
update();
|
||||
}
|
||||
|
||||
function removeCard() {
|
||||
var menu = {
|
||||
"" : {title : "Select Contact"},
|
||||
"< Back" : mainMenu
|
||||
};
|
||||
if (Object.keys(wp).length==0) Object.assign(menu, {"No Contacts":""});
|
||||
else {
|
||||
wp.forEach((val, card) => {
|
||||
const name = wp[card].name;
|
||||
menu[name]=()=>{
|
||||
E.showMenu();
|
||||
var confirmRemove = new Layout (
|
||||
{type:"v", c: [
|
||||
{type:"txt", font:"15%", pad:1, fillx:1, filly:1, label:"Delete"},
|
||||
{type:"txt", font:"15%", pad:1, fillx:1, filly:1, label:name},
|
||||
{type:"h", c: [
|
||||
{type:"btn", font:"15%", pad:1, fillx:1, filly:1, label: "YES", cb:l=>{
|
||||
wp.splice(card, 1);
|
||||
writeContact();
|
||||
mainMenu();
|
||||
}},
|
||||
{type:"btn", font:"15%", pad:1, fillx:1, filly:1, label: " NO", cb:l=>{mainMenu();}}
|
||||
]}
|
||||
], lazy:true});
|
||||
g.clear();
|
||||
confirmRemove.render();
|
||||
};
|
||||
});
|
||||
}
|
||||
E.showMenu(menu);
|
||||
}
|
||||
|
||||
function askPosition(callback) {
|
||||
let full = "";
|
||||
showNumpad("dddDDDddd", "", function() {
|
||||
callback(key, "");
|
||||
});
|
||||
}
|
||||
|
||||
function createContact(lat, name) {
|
||||
let n = {};
|
||||
n["name"] = name;
|
||||
n["number"] = lat;
|
||||
wp.push(n);
|
||||
print("add -- contacts", wp);
|
||||
writeContact();
|
||||
}
|
||||
|
||||
function addCardName2(key) {
|
||||
g.clear();
|
||||
askPosition(function(lat, lon) {
|
||||
print("position -- ", lat, lon);
|
||||
createContact(lat, result);
|
||||
mainMenu();
|
||||
});
|
||||
}
|
||||
|
||||
function addCardName(key) {
|
||||
result = key;
|
||||
if (wp[result]!=undefined) {
|
||||
E.showMenu();
|
||||
var alreadyExists = new Layout (
|
||||
{type:"v", c: [
|
||||
{type:"txt", font:Math.min(15,100/result.length)+"%", pad:1, fillx:1, filly:1, label:result},
|
||||
{type:"txt", font:"12%", pad:1, fillx:1, filly:1, label:"already exists."},
|
||||
{type:"h", c: [
|
||||
{type:"btn", font:"10%", pad:1, fillx:1, filly:1, label: "REPLACE", cb:l=>{ addCardName2(key); }},
|
||||
{type:"btn", font:"10%", pad:1, fillx:1, filly:1, label: "CANCEL", cb:l=>{mainMenu();}}
|
||||
]}
|
||||
], lazy:true});
|
||||
g.clear();
|
||||
alreadyExists.render();
|
||||
return;
|
||||
}
|
||||
addCardName2(key);
|
||||
}
|
||||
|
||||
function addCard() {
|
||||
require("textinput").input({text:""}).then(result => {
|
||||
if (result != "") {
|
||||
addCardName(result);
|
||||
} else
|
||||
mainMenu();
|
||||
});
|
||||
}
|
||||
|
||||
g.reset();
|
||||
Bangle.setUI();
|
||||
mainMenu();
|
|
@ -0,0 +1,6 @@
|
|||
[
|
||||
{
|
||||
"name":"EU emergency",
|
||||
"number":"112"
|
||||
}
|
||||
]
|
|
@ -0,0 +1,249 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" href="../../css/spectre.min.css">
|
||||
<link rel="stylesheet" href="../../css/spectre-icons.min.css">
|
||||
<link rel="stylesheet" href="../../css/spectre-icons.min.css">
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A==" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet-control-geocoder/dist/Control.Geocoder.css">
|
||||
|
||||
<style type="text/css">
|
||||
html, body { height: 100% }
|
||||
.flex-col { display:flex; flex-direction:column; height:100% }
|
||||
#map { width:100%; height:100% }
|
||||
#tab-list { width:100%; height:100% }
|
||||
|
||||
/* https://stackoverflow.com/a/58686215 */
|
||||
.arrow-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
.arrow-icon > div {
|
||||
margin-left: -1px;
|
||||
margin-top: -3px;
|
||||
transform-origin: center center;
|
||||
font: 12px "Helvetica Neue", Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Contacts v.2</h1>
|
||||
<div class="flex-col">
|
||||
<div id="statusarea">
|
||||
<button id="download" class="btn btn-error">Reload</button> <button id="upload" class="btn btn-primary">Upload</button>
|
||||
<span id="status"></span>
|
||||
<span id="routestatus"></span>
|
||||
</div>
|
||||
<div>
|
||||
<ul class="tab tab-block">
|
||||
<li class="tab-item active" id="tabitem-map">
|
||||
<a href="#">Map</a>
|
||||
</li>
|
||||
<li class="tab-item" id="tabitem-list">
|
||||
<a href="#">List</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div style="flex: 1">
|
||||
<div id="tab-list">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Number</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="contacts">
|
||||
</tbody>
|
||||
</table>
|
||||
<br>
|
||||
<h4>Add a new contact</h4>
|
||||
<form id="add_contact_form">
|
||||
<div class="columns">
|
||||
<div class="column col-3 col-xs-8">
|
||||
<input class="form-input input-sm" type="text" id="add_contact_name" placeholder="Name">
|
||||
</div>
|
||||
<div class="column col-3 col-xs-8">
|
||||
<input class="form-input input-sm" value="123456789" type="text" id="add_number" placeholder="Number">
|
||||
</div>
|
||||
</div>
|
||||
<div class="columns">
|
||||
<div class="column col-3 col-xs-8">
|
||||
<button id="add_contact_button" class="btn btn-primary btn-sm">Add Contact</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://unpkg.com/sweetalert/dist/sweetalert.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
|
||||
<script src="../../core/lib/interface.js"></script>
|
||||
|
||||
<script>
|
||||
var contacts = [];
|
||||
|
||||
// ==========================================================================
|
||||
/*** status ***/
|
||||
|
||||
function clean() {
|
||||
$('#status').html('<i class="icon icon-check"></i> No pending changes.');
|
||||
}
|
||||
|
||||
function dirty() {
|
||||
$('#status').html('<b><i class="icon icon-edit"></i> Changes have not been sent to the watch.</b>');
|
||||
}
|
||||
|
||||
/*** contacts ***/
|
||||
|
||||
function addContact(arr, lat, lon, name) {
|
||||
arr.push({number:lat, name:name});
|
||||
renderAllContacts();
|
||||
dirty();
|
||||
}
|
||||
|
||||
function deleteContact(arr, i) {
|
||||
arr.splice(i, 1);
|
||||
renderAllContacts();
|
||||
dirty();
|
||||
}
|
||||
|
||||
function renameContact(arr, i) {
|
||||
var name = prompt("Enter new name for the contact:", arr[i].name);
|
||||
if (name == null || name == "" || name == arr[i].name)
|
||||
return;
|
||||
arr[i].name = name;
|
||||
renderAllContacts();
|
||||
dirty();
|
||||
}
|
||||
|
||||
/*** util ***/
|
||||
|
||||
// https://stackoverflow.com/a/22706073
|
||||
function escapeHTML(str){
|
||||
return new Option(str).innerHTML;
|
||||
}
|
||||
|
||||
/*** Bangle.js ***/
|
||||
|
||||
function gotStored(pts) {
|
||||
contacts = pts;
|
||||
renderAllContacts();
|
||||
}
|
||||
|
||||
// ========================================================================== LIST
|
||||
|
||||
var $name = document.getElementById('add_contact_name')
|
||||
var $form = document.getElementById('add_contact_form')
|
||||
var $button = document.getElementById('add_contact_button')
|
||||
var $number = document.getElementById('add_number')
|
||||
var $list = document.getElementById('contacts')
|
||||
|
||||
function compare(a, b){
|
||||
var x = a.name.toLowerCase();
|
||||
var y = b.name.toLowerCase();
|
||||
if (x=="none") {return -1};
|
||||
if (y=="none") {return 1};
|
||||
if (x < y) {return -1;}
|
||||
if (x > y) {return 1;}
|
||||
return 0;
|
||||
}
|
||||
|
||||
$button.addEventListener('click', event => {
|
||||
event.preventDefault()
|
||||
var name = $name.value.trim()
|
||||
if(!name) return;
|
||||
var number = $number.value.trim();
|
||||
|
||||
contacts.push({
|
||||
name, number,
|
||||
});
|
||||
|
||||
contacts.sort(compare);
|
||||
|
||||
renderAllContacts()
|
||||
$name.value = ''
|
||||
$number.value = (0);
|
||||
|
||||
dirty();
|
||||
});
|
||||
|
||||
function removeContact(index){
|
||||
$name.value = contacts[index].name
|
||||
$number.value = contacts[index].number
|
||||
contacts = contacts.filter((p,i) => i!==index)
|
||||
renderAllContacts()
|
||||
}
|
||||
|
||||
function renderContactsList(){
|
||||
$list.innerHTML = ''
|
||||
contacts.forEach((contact,index) => {
|
||||
var $contact = document.createElement('tr')
|
||||
if(contact.number==undefined){
|
||||
$contact.innerHTML = `<td>${contact.name}</td><td>(no number)</td>`;
|
||||
} else {
|
||||
$contact.innerHTML = `<td>${contact.name}</td><td><a href="tel:${contact.number}">${contact.number}</a></td>`;
|
||||
}
|
||||
$contact.innerHTML += `<td><button class="btn btn-action btn-primary" onclick="removeContact(${index})"><i class="icon icon-delete"></i></button></td>`;
|
||||
$list.appendChild($contact)
|
||||
})
|
||||
$name.focus()
|
||||
}
|
||||
|
||||
function renderContacts() {
|
||||
renderContactsList();
|
||||
}
|
||||
|
||||
function renderAllContacts() {
|
||||
renderContactsList();
|
||||
}
|
||||
|
||||
// ========================================================================== UPLOAD/DOWNLOAD
|
||||
|
||||
function downloadJSONfile(fileid, callback) {
|
||||
// TODO: use interface.js-provided stuff?
|
||||
Puck.write(`\x10(function() {
|
||||
var pts = require("Storage").readJSON("${fileid}")||[{name:"NONE"}];
|
||||
Bluetooth.print(JSON.stringify(pts));
|
||||
})()\n`, contents => {
|
||||
if (contents=='[{name:"NONE"}]') contents="[]";
|
||||
var storedpts = JSON.parse(contents);
|
||||
callback(storedpts);
|
||||
clean();
|
||||
});
|
||||
}
|
||||
|
||||
function uploadFile(fileid, contents) {
|
||||
// TODO: use interface.js-provided stuff?
|
||||
Puck.write(`\x10(function() {
|
||||
require("Storage").write("${fileid}",'${contents}');
|
||||
Bluetooth.print("OK");
|
||||
})()\n`, ret => {
|
||||
console.log("uploadFile", ret);
|
||||
if (ret == "OK")
|
||||
clean();
|
||||
});
|
||||
}
|
||||
|
||||
function onInit() {
|
||||
downloadJSONfile("contacts.json", gotStored);
|
||||
}
|
||||
|
||||
$('#download').on('click', function() {
|
||||
downloadJSONfile("contacts.json", gotStored);
|
||||
});
|
||||
|
||||
$('#upload').click(function() {
|
||||
var data = JSON.stringify(contacts);
|
||||
uploadFile("contacts.json",data);
|
||||
});
|
||||
|
||||
// ========================================================================== FINALLY...
|
||||
clean();
|
||||
renderAllContacts();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,19 @@
|
|||
{ "id": "contacts",
|
||||
"name": "contacts",
|
||||
"version":"0.01",
|
||||
"description": "Provides means of storing user contacts, viewing/editing them on device and from the App loader",
|
||||
"icon": "app.png",
|
||||
"tags": "tool",
|
||||
"supports" : ["BANGLEJS2"],
|
||||
"allow_emulator": true,
|
||||
"readme": "README.md",
|
||||
"interface": "interface.html",
|
||||
"dependencies": {"textinput":"type"},
|
||||
"storage": [
|
||||
{"name":"contacts.app.js","url":"contacts.app.js"},
|
||||
{"name":"contacts.img","url":"app-icon.js","evaluate":true}
|
||||
],
|
||||
"data": [
|
||||
{"name":"contacts.json","url":"contacts.json"}
|
||||
]
|
||||
}
|
|
@ -128,38 +128,36 @@ LED.set();NRF.sleep();`);
|
|||
posteditor.on("change", editorChanged);
|
||||
|
||||
document.getElementById("upload").addEventListener("click", function() {
|
||||
if (!hasWarnings()) {
|
||||
var precode = preeditor.getValue();
|
||||
var jscode = jseditor.getValue();
|
||||
var postcode = posteditor.getValue();
|
||||
var namePrefix = document.getElementById("nameprefix").value;
|
||||
localStorage.setItem(LS_PRECODE, precode);
|
||||
localStorage.setItem(LS_JSCODE, jscode);
|
||||
localStorage.setItem(LS_POSTCODE, postcode);
|
||||
localStorage.setItem(LS_NAMEPREFIX, namePrefix);
|
||||
var precode = preeditor.getValue();
|
||||
var jscode = jseditor.getValue();
|
||||
var postcode = posteditor.getValue();
|
||||
var namePrefix = document.getElementById("nameprefix").value;
|
||||
localStorage.setItem(LS_PRECODE, precode);
|
||||
localStorage.setItem(LS_JSCODE, jscode);
|
||||
localStorage.setItem(LS_POSTCODE, postcode);
|
||||
localStorage.setItem(LS_NAMEPREFIX, namePrefix);
|
||||
|
||||
// force version - as long as we're above 1v96 we get the ability to upload to different storage files
|
||||
var ENV = Espruino.Core.Env.getData();
|
||||
ENV.VERSION_MAJOR = 2;
|
||||
ENV.VERSION_MINOR = 0;
|
||||
// Now compile
|
||||
Espruino.transform(jscode, {
|
||||
SET_TIME_ON_WRITE : false, // time would just be out of date
|
||||
SAVE_ON_SEND : 1, // save to flash
|
||||
LOAD_STORAGE_FILE : 0, // do not load from storage after saving
|
||||
// PRETOKENISE : true,
|
||||
// MINIFICATION_LEVEL : "ESPRIMA", // maybe?
|
||||
}).then(content => {
|
||||
sendCustomizedApp({
|
||||
storage: [{ name: "espruinoprog.json", content: JSON.stringify({
|
||||
namePrefix : namePrefix,
|
||||
pre : Espruino.Core.CodeWriter.reformatCode(precode),
|
||||
code : Espruino.Core.CodeWriter.reformatCode(content),
|
||||
post : Espruino.Core.CodeWriter.reformatCode(postcode)
|
||||
})}]
|
||||
});
|
||||
// force version - as long as we're above 1v96 we get the ability to upload to different storage files
|
||||
var ENV = Espruino.Core.Env.getData();
|
||||
ENV.VERSION_MAJOR = 2;
|
||||
ENV.VERSION_MINOR = 0;
|
||||
// Now compile
|
||||
Espruino.transform(jscode, {
|
||||
SET_TIME_ON_WRITE : false, // time would just be out of date
|
||||
SAVE_ON_SEND : 1, // save to flash
|
||||
LOAD_STORAGE_FILE : 0, // do not load from storage after saving
|
||||
// PRETOKENISE : true,
|
||||
// MINIFICATION_LEVEL : "ESPRIMA", // maybe?
|
||||
}).then(content => {
|
||||
sendCustomizedApp({
|
||||
storage: [{ name: "espruinoprog.json", content: JSON.stringify({
|
||||
namePrefix : namePrefix,
|
||||
pre : Espruino.Core.CodeWriter.reformatCode(precode),
|
||||
code : Espruino.Core.CodeWriter.reformatCode(content),
|
||||
post : Espruino.Core.CodeWriter.reformatCode(postcode)
|
||||
})}]
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
document.getElementById("setdefault").addEventListener("click", function(e) {
|
||||
e.preventDefault();
|
||||
|
|
|
@ -1,2 +1,5 @@
|
|||
0.01: New App!
|
||||
0.02: Shorten the timeout before executing to 250 ms.
|
||||
0.03: Add inner timeout of 150 ms so user has more time to release the button
|
||||
before clock ui is initialized and adds it's button watch for going to
|
||||
launcher.
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{let buzzTimeout;
|
||||
setWatch((e)=>{
|
||||
if (e.state) buzzTimeout = setTimeout(()=>{Bangle.buzz(80,0.40);Bangle.showClock();}, 250);
|
||||
if (e.state) buzzTimeout = setTimeout(()=>{Bangle.buzz(80,0.40);setTimeout(Bangle.showClock,150);}, 250);
|
||||
if (!e.state && buzzTimeout) clearTimeout(buzzTimeout);},
|
||||
BTN,{repeat:true, edge:'both' });}
|
||||
BTN,{repeat:true,edge:'both'});}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{ "id": "fastreset",
|
||||
"name": "Fast Reset",
|
||||
"shortName":"Fast Reset",
|
||||
"version":"0.02",
|
||||
"version":"0.03",
|
||||
"description": "Reset the watch by pressing the hardware button just a little bit longer than a click. If 'Fastload Utils' is installed this will typically be done with fastloading. A buzz acts as indicator.",
|
||||
"icon": "app.png",
|
||||
"type": "bootloader",
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
0.01: New App!
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEw4UA///x3ygHo8H1Jf8AgILLoALVgoLHggLCqAgJioLIqgLDGQsBqtAioOBqoYFqtUAIMVBY9VqwCBDIIAECYILCHowLBrWVBZFlrWWyptGgtq1WqJYI7GrQLCFxGrBYJHEBQNV1Wv9IEBEocFKIOq//qJAIZEAoNq3/+1QMBHoYYBrQLB1J4GitaEYZfGtfvBYJ3HtWr9WlNY0V1Nr1WlC4xIBrWmBZWVrJGFcYILBZY4LBoILIgoNBEILvHDIQ5BBY4IBBYMBMAwLBBA4LPBRMAKAoLRiALWAGw="))
|
|
@ -0,0 +1,59 @@
|
|||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
E.showMessage("Loading...");
|
||||
Bangle.setOptions({hrmPollInterval:5});
|
||||
Bangle.setHRMPower(1);
|
||||
|
||||
function drawCounter() {
|
||||
g.reset().clearRect(0,24,175,90);
|
||||
//g.drawRect(0,24,175,90);
|
||||
g.setFontAlign(0,0).setFontVector(60);
|
||||
g.drawString(count, 88, 60);
|
||||
}
|
||||
|
||||
function hadPulse() {
|
||||
count++;
|
||||
drawCounter();
|
||||
g.setColor("#f00").fillCircle(156,156,20);
|
||||
setTimeout(function() {
|
||||
g.setColor(g.theme.bg).fillCircle(156,156,20);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
if (parseFloat(process.env.VERSION.replace("v","0"))<2019) {
|
||||
E.showMessage("You need at least firmware 2v19","Error");
|
||||
} else if (Bangle.hrmRd(0)!=33) { // wrong sensor - probably VC31 from original bangle.js 2
|
||||
E.showMessage("This Bangle.js doesn't have a VC31B HRM sensor","Error");
|
||||
} else {
|
||||
Bangle.setOptions({hrmGreenAdjust:false, hrmWearDetect:false, hrmPushEnv:true});
|
||||
Bangle.hrmWr(0x10, 197&0xF8 | 4); // just SLOT2
|
||||
Bangle.hrmWr(0x16, 0); // force env to be used as fast as possible
|
||||
|
||||
var samples = 0, samplesHi = 0;
|
||||
var count = 0;
|
||||
{
|
||||
let last = 0;
|
||||
Bangle.on('HRM-env',v => {
|
||||
if (v) {
|
||||
if (!last) hadPulse();
|
||||
samplesHi++;
|
||||
}
|
||||
last = v;
|
||||
samples++;
|
||||
});
|
||||
}
|
||||
|
||||
drawCounter();
|
||||
setInterval(function() {
|
||||
g.reset().clearRect(0,90,175,130);
|
||||
g.setFontAlign(0,0).setFont("6x8:2");
|
||||
g.drawString(samples+" sps", 88, 100);
|
||||
if (samplesHi*5 > samples) {
|
||||
g.setBgColor("#f00").setColor("#fff");
|
||||
g.clearRect(0,110,175,130).drawString("TOO LIGHT",88,120);
|
||||
}
|
||||
samples=0;
|
||||
samplesHi=0;
|
||||
Bangle.setLCDPower(1); // force LCD on!
|
||||
}, 1000);
|
||||
}
|
After Width: | Height: | Size: 1.2 KiB |
|
@ -0,0 +1,13 @@
|
|||
{ "id": "flashcount",
|
||||
"name": "Flash Counter",
|
||||
"shortName":"FlashCount",
|
||||
"version":"0.01",
|
||||
"description": "Count flashes/pulses of light using the heart rate monitor. Requires a VC31B HRM sensor, which should be in most watches except those produced for the original KickStarter campaign.",
|
||||
"icon": "app.png",
|
||||
"tags": "",
|
||||
"supports" : ["BANGLEJS2"],
|
||||
"storage": [
|
||||
{"name":"flashcount.app.js","url":"app.js"},
|
||||
{"name":"flashcount.img","url":"app-icon.js","evaluate":true}
|
||||
]
|
||||
}
|
|
@ -12,7 +12,8 @@
|
|||
<a href="https://www.espruino.com/Bangle.js#firmware-updates" target="_blank">see the Bangle.js 1 instructions</a></b></p>
|
||||
</div>
|
||||
<ul>
|
||||
<p>Your current firmware version is <span id="fw-version" style="font-weight:bold">unknown</span> and DFU is <span id="boot-version" style="font-weight:bold">unknown</span></p>
|
||||
<p>Your current firmware version is <span id="fw-version" style="font-weight:bold">unknown</span> and DFU is <span id="boot-version" style="font-weight:bold">unknown</span>.
|
||||
The DFU (bootloader) rarely changes, so it does not have to be the same version as your main firmware.</p>
|
||||
</ul>
|
||||
<div id="fw-ok" style="display:none">
|
||||
<p id="fw-old-bootloader-msg">If you have an early (KickStarter or developer) Bangle.js device and still have the old 2v10.x DFU, the Firmware Update
|
||||
|
|
|
@ -2,3 +2,4 @@
|
|||
0.02: Refactor code to store grocery list in separate file
|
||||
0.03: Sort selected items to bottom and enable Widgets
|
||||
0.04: Add settings to edit list on device
|
||||
0.05: Drop app customiser as it is redundant with download interface
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
var filename = 'grocery_list.json';
|
||||
var settings = require("Storage").readJSON(filename,1)|| { products: [] };
|
||||
{
|
||||
const filename = 'grocery_list.json';
|
||||
const settings = require("Storage").readJSON(filename,1)|| { products: [] };
|
||||
let menu;
|
||||
|
||||
function updateSettings() {
|
||||
const updateSettings = function() {
|
||||
require("Storage").writeJSON(filename, settings);
|
||||
Bangle.buzz();
|
||||
}
|
||||
};
|
||||
|
||||
function twoChat(n){
|
||||
const twoChat = function(n) {
|
||||
if(n<10) return '0'+n;
|
||||
return ''+n;
|
||||
}
|
||||
};
|
||||
|
||||
function sortMenu() {
|
||||
const sortMenu = function() {
|
||||
mainMenu.sort((a,b) => {
|
||||
const byValue = a.value-b.value;
|
||||
return byValue !== 0 ? byValue : a.index-b.index;
|
||||
|
@ -20,7 +21,7 @@ function sortMenu() {
|
|||
if (menu) {
|
||||
menu.draw();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const mainMenu = settings.products.map((p,i) => ({
|
||||
title: twoChat(p.quantity)+' '+p.name,
|
||||
|
@ -35,9 +36,14 @@ const mainMenu = settings.products.map((p,i) => ({
|
|||
}));
|
||||
sortMenu();
|
||||
|
||||
mainMenu[''] = { 'title': 'Grocery list' };
|
||||
mainMenu[''] = {
|
||||
'title': 'Grocery list',
|
||||
remove: () => {
|
||||
},
|
||||
};
|
||||
mainMenu['< Back'] = ()=>{load();};
|
||||
|
||||
Bangle.loadWidgets();
|
||||
menu = E.showMenu(mainMenu);
|
||||
Bangle.drawWidgets();
|
||||
}
|
||||
|
|
|
@ -1,116 +0,0 @@
|
|||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="../../css/spectre.min.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h4>List of products</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>name</th>
|
||||
<th>quantity</th>
|
||||
<th>actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="products">
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<br><br>
|
||||
<h4>Add a new product</h4>
|
||||
<form id="add_product_form">
|
||||
<div class="columns">
|
||||
<div class="column col-4 col-xs-12">
|
||||
<input class="form-input input-sm" type="text" id="add_product_name" placeholder="Name">
|
||||
</div>
|
||||
<div class="column col-4 col-xs-12">
|
||||
<input class="form-input input-sm" value="1" type="number" id="add_product_quantity" placeholder="Quantity">
|
||||
</div>
|
||||
<div class="column col-4 col-xs-12">
|
||||
<button id="add_product_button" class="btn btn-primary btn-sm">Add</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<br><br>
|
||||
<button id="reset" class="btn btn-error">Reset</button> <button id="upload" class="btn btn-primary">Upload</button>
|
||||
|
||||
<script src="../../core/lib/customize.js"></script>
|
||||
|
||||
<script>
|
||||
|
||||
var products = []
|
||||
try{
|
||||
var stored = localStorage.getItem('grocery-product-list')
|
||||
if(stored) products = JSON.parse(stored);
|
||||
}catch(e){}
|
||||
|
||||
var $name = document.getElementById('add_product_name')
|
||||
var $form = document.getElementById('add_product_form')
|
||||
var $button = document.getElementById('add_product_button')
|
||||
var $quantity = document.getElementById('add_product_quantity')
|
||||
var $list = document.getElementById('products')
|
||||
var $reset = document.getElementById('reset')
|
||||
|
||||
renderProducts()
|
||||
|
||||
$reset.addEventListener('click', reset)
|
||||
|
||||
$form.addEventListener('submit', event => {
|
||||
event.preventDefault()
|
||||
|
||||
var name = $name.value.trim()
|
||||
if(!name) return;
|
||||
|
||||
var quantity = parseInt($quantity.value)
|
||||
|
||||
products.push({
|
||||
name, quantity,
|
||||
ok: false
|
||||
})
|
||||
|
||||
renderProducts()
|
||||
$name.value = ''
|
||||
$quantity.value = 1
|
||||
save()
|
||||
})
|
||||
|
||||
function save(){
|
||||
localStorage.setItem('grocery-product-list',JSON.stringify(products));
|
||||
}
|
||||
|
||||
function reset(){
|
||||
products = []
|
||||
save()
|
||||
renderProducts()
|
||||
}
|
||||
|
||||
function removeProduct(index){
|
||||
products = products.filter((p,i) => i!==index)
|
||||
save()
|
||||
renderProducts()
|
||||
}
|
||||
|
||||
function renderProducts(){
|
||||
$list.innerHTML = ''
|
||||
products.forEach((product,index) => {
|
||||
var $product = document.createElement('tr')
|
||||
|
||||
$product.innerHTML = `<td>${product.name}</td><td>${product.quantity}</td><td><button class="btn btn-error" onclick="removeProduct(${index})">remove</button></td>`
|
||||
$list.appendChild($product)
|
||||
})
|
||||
|
||||
$name.focus()
|
||||
|
||||
}
|
||||
|
||||
document.getElementById("upload").addEventListener("click", function() {
|
||||
sendCustomizedApp({
|
||||
storage:[
|
||||
{ name:"grocery_list.json", content: JSON.stringify({products: products}) }
|
||||
]
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -80,7 +80,7 @@
|
|||
// remove window
|
||||
Util.hideModal();
|
||||
|
||||
settings = JSON.parse(data || "{products: []}");
|
||||
settings = JSON.parse(data || '{"products": []}');
|
||||
products = settings.products;
|
||||
renderProducts();
|
||||
});
|
||||
|
@ -89,7 +89,6 @@
|
|||
function save(){
|
||||
settings.products = products;
|
||||
Util.showModal("Saving...");
|
||||
localStorage.setItem('grocery-product-list',JSON.stringify(products));
|
||||
Util.writeStorage("grocery_list.json", JSON.stringify(settings), () => {
|
||||
Util.hideModal();
|
||||
});
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
{
|
||||
"id": "grocery",
|
||||
"name": "Grocery",
|
||||
"version": "0.04",
|
||||
"version": "0.05",
|
||||
"description": "Simple grocery (shopping) list - Display a list of product and track if you already put them in your cart.",
|
||||
"icon": "grocery.png",
|
||||
"type": "app",
|
||||
"tags": "tool,shopping,list",
|
||||
"supports": ["BANGLEJS", "BANGLEJS2"],
|
||||
"custom": "grocery.html",
|
||||
"interface": "interface.html",
|
||||
"allow_emulator": true,
|
||||
"dependencies": {"textinput":"type"},
|
||||
|
|
|
@ -118,7 +118,7 @@
|
|||
/*LANG*/"Edit List": () => editlist(),
|
||||
/*LANG*/"Add item": () => {
|
||||
settings.products.push({
|
||||
"name":/*LANG*/"New item",
|
||||
"name":/*LANG*/"New",
|
||||
"quantity":1,
|
||||
"ok":false
|
||||
});
|
||||
|
|
|
@ -2,3 +2,4 @@
|
|||
0.02: Respect Quiet Mode
|
||||
0.03: Fix hour/minute wrapping code for new menu system
|
||||
0.04: Use default Bangle formatter for booleans
|
||||
0.05: Support BangleJS 2
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
Hard Alarm
|
||||
========
|
||||
|
||||
The Hard Alarm will is vibration only alarm clock that will not stop vibrating until you are awake enough to match the random number on the screen.
|
||||
|
||||
To set a hard alarm open the app and in the settings screen:
|
||||
1. Press the top half of the screen add 1 and press the bottom half of the screen to subtract 1.
|
||||
2. Press the center number itself to save your selection
|
||||
3. Exit the app or it may not run the alarm.
|
||||
|
||||
When the alarm goes off:
|
||||
1. Press the top half of the screen add 1 and press the bottom half of the screen to subtract 1.
|
||||
2. Press the side button to confirm your selection and turn off the alarm
|
||||
3. You now have the touchscreen option to Snooze the alarm for 10 more minutes or end the alarm.
|
||||
|
||||
Made by https://dare.fail
|
|
@ -8,7 +8,6 @@ var alarms = require("Storage").readJSON("hardalarm.json",1)||[];
|
|||
msg : "Eat chocolate",
|
||||
last : 0, // last day of the month we alarmed on - so we don't alarm twice in one day!
|
||||
rp : true, // repeat
|
||||
as : false, // auto snooze
|
||||
}
|
||||
];*/
|
||||
|
||||
|
@ -45,14 +44,12 @@ function editAlarm(alarmIndex) {
|
|||
var mins = 0;
|
||||
var en = true;
|
||||
var repeat = true;
|
||||
var as = false;
|
||||
if (!newAlarm) {
|
||||
var a = alarms[alarmIndex];
|
||||
hrs = 0|a.hr;
|
||||
mins = Math.round((a.hr-hrs)*60);
|
||||
en = a.on;
|
||||
repeat = a.rp;
|
||||
as = a.as;
|
||||
}
|
||||
const menu = {
|
||||
'': { 'title': 'Alarms' },
|
||||
|
@ -72,10 +69,6 @@ function editAlarm(alarmIndex) {
|
|||
value: en,
|
||||
onchange: v=>repeat=v
|
||||
},
|
||||
/*LANG*/'Auto snooze': {
|
||||
value: as,
|
||||
onchange: v=>as=v
|
||||
}
|
||||
};
|
||||
function getAlarm() {
|
||||
var hr = hrs+(mins/60);
|
||||
|
@ -86,7 +79,7 @@ function editAlarm(alarmIndex) {
|
|||
// Save alarm
|
||||
return {
|
||||
on : en, hr : hr,
|
||||
last : day, rp : repeat, as: as
|
||||
last : day, rp : repeat
|
||||
};
|
||||
}
|
||||
menu["> Save"] = function() {
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
// 'load(hardalarm.js)' - so let's remove it first!
|
||||
clearInterval();
|
||||
|
||||
var okClicked = false;
|
||||
|
||||
function formatTime(t) {
|
||||
var hrs = 0|t;
|
||||
var mins = Math.round((t-hrs)*60);
|
||||
|
@ -61,58 +63,63 @@ function showPrompt(msg, buzzCount, alarm) {
|
|||
});
|
||||
}
|
||||
|
||||
function buzz() {
|
||||
Bangle.buzz(500).then(()=>{
|
||||
setTimeout(()=>{
|
||||
Bangle.buzz(500).then(function() {
|
||||
setTimeout(()=>{
|
||||
Bangle.buzz(2000).then(function() {
|
||||
if(!okClicked) {
|
||||
buzz();
|
||||
}
|
||||
});
|
||||
},100);
|
||||
});
|
||||
},100);
|
||||
});
|
||||
}
|
||||
|
||||
function showAlarm(alarm) {
|
||||
if ((require('Storage').readJSON('setting.json',1)||{}).quiet>1) return; // total silence
|
||||
var msg = formatTime(alarm.hr);
|
||||
var buzzCount = 20;
|
||||
if (alarm.msg)
|
||||
msg += "\n"+alarm.msg;
|
||||
var okClicked = false;
|
||||
okClicked = false;
|
||||
var currentGuess = 10;
|
||||
var randomNum = getRandomFromRange(0, 7, 13, 20);
|
||||
showNumberPicker(currentGuess, randomNum)
|
||||
setWatch(o => {
|
||||
if(!okClicked && currentGuess < 20) {
|
||||
currentGuess = currentGuess + 1;
|
||||
showNumberPicker(currentGuess, randomNum);
|
||||
|
||||
let onSwipe = (x, y) => {
|
||||
if (y == 1) {
|
||||
if(!okClicked && currentGuess < 20) {
|
||||
currentGuess = currentGuess + 1;
|
||||
showNumberPicker(currentGuess, randomNum);
|
||||
}
|
||||
}
|
||||
}, BTN1, {repeat: true, edge: 'rising'});
|
||||
else if (y == -1) {
|
||||
if(!okClicked && currentGuess > 0) {
|
||||
currentGuess = currentGuess - 1;
|
||||
showNumberPicker(currentGuess, randomNum);
|
||||
}
|
||||
}
|
||||
};
|
||||
Bangle.on("swipe", onSwipe);
|
||||
|
||||
|
||||
setWatch(o => {
|
||||
if(currentGuess == randomNum) {
|
||||
okClicked = true;
|
||||
showPrompt(msg, buzzCount, alarm);
|
||||
}
|
||||
}, BTN2, {repeat: true, edge: 'rising'});
|
||||
}, BTN, {repeat: true, edge: 'rising'});
|
||||
|
||||
setWatch(o => {
|
||||
if(!okClicked && currentGuess > 0) {
|
||||
currentGuess = currentGuess - 1;
|
||||
showNumberPicker(currentGuess, randomNum);
|
||||
}
|
||||
}, BTN3, {repeat: true, edge: 'rising'});
|
||||
|
||||
function buzz() {
|
||||
Bangle.buzz(500).then(()=>{
|
||||
setTimeout(()=>{
|
||||
Bangle.buzz(500).then(function() {
|
||||
setTimeout(()=>{
|
||||
Bangle.buzz(2000).then(function() {
|
||||
if (buzzCount--)
|
||||
setTimeout(buzz, 2000);
|
||||
else if(alarm.as) { // auto-snooze
|
||||
buzzCount = 20;
|
||||
setTimeout(buzz, 600000); // 10 minutes
|
||||
}
|
||||
});
|
||||
},100);
|
||||
});
|
||||
},100);
|
||||
});
|
||||
}
|
||||
buzz();
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Check for alarms
|
||||
var day = (new Date()).getDate();
|
||||
var hr = getCurrentHr()+10000; // get current time - 10s in future to ensure we alarm if we've started the app a tad early
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
"id": "hardalarm",
|
||||
"name": "Hard Alarm",
|
||||
"shortName": "HardAlarm",
|
||||
"version": "0.04",
|
||||
"version": "0.05",
|
||||
"description": "Make sure you wake up! Count to the right number to turn off the alarm",
|
||||
"icon": "app.png",
|
||||
"tags": "tool,alarm,widget",
|
||||
"supports": ["BANGLEJS"],
|
||||
"supports": ["BANGLEJS", "BANGLEJS2"],
|
||||
"storage": [
|
||||
{"name":"hardalarm.app.js","url":"app.js"},
|
||||
{"name":"hardalarm.boot.js","url":"boot.js"},
|
||||
|
@ -14,5 +14,7 @@
|
|||
{"name":"hardalarm.img","url":"app-icon.js","evaluate":true},
|
||||
{"name":"hardalarm.wid.js","url":"widget.js"}
|
||||
],
|
||||
"data": [{"name":"hardalarm.json"}]
|
||||
}
|
||||
"data": [{"name":"hardalarm.json"}],
|
||||
"screenshots": [{"url":"screenshot.png"},{"url":"screenshot2.png"},{"url":"screenshot3.png"}],
|
||||
"readme": "README.md"
|
||||
}
|
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 2.2 KiB |
|
@ -5,7 +5,6 @@
|
|||
</head>
|
||||
<body>
|
||||
<script src="../../core/lib/customize.js"></script>
|
||||
<script src="https://www.puck-js.com/puck.js"></script>
|
||||
<p>
|
||||
<div class="form-group">
|
||||
<label class="form-switch">
|
||||
|
@ -15,7 +14,7 @@
|
|||
</div>
|
||||
</p>
|
||||
<p>
|
||||
<button id="btnConnect" class="btn btn-primary">Start</button>
|
||||
<button id="btnStart" class="btn btn-primary">Start</button>
|
||||
<button id="btnStop" class="btn btn-secondary">Stop</button>
|
||||
<button id="btnReset" class="btn btn-secondary">Reset</button>
|
||||
<button id="btnSave" class="btn btn-primary">Save CSV</button>
|
||||
|
@ -181,15 +180,11 @@ function createCode(){
|
|||
`;
|
||||
}
|
||||
|
||||
var connection;
|
||||
var isConnected = false;
|
||||
var lineCount=-1;
|
||||
|
||||
function stop (){
|
||||
connection.reconnect((c)=>{
|
||||
c.write("load();\n");
|
||||
c.close();
|
||||
connection = undefined;
|
||||
});
|
||||
Puck.write("load();\n");
|
||||
}
|
||||
|
||||
function updateButtons(){
|
||||
|
@ -206,24 +201,15 @@ document.getElementById("chkLocal").addEventListener("click", function() {
|
|||
updateButtons();
|
||||
});
|
||||
|
||||
window.addEventListener("message", function(event) {
|
||||
let msg = event.data;
|
||||
if (msg.type=="readstoragefilersp") {
|
||||
Util.saveCSV("log", msg.data);
|
||||
}
|
||||
}, false);
|
||||
|
||||
document.getElementById("btnDownload").addEventListener("click", function() {
|
||||
if (connection) {
|
||||
stop();
|
||||
}
|
||||
stop();
|
||||
console.log("Loading data from BangleJs...");
|
||||
try {
|
||||
window.postMessage({type:"readstoragefile",data:"log.csv",id:0});
|
||||
} catch(ex) {
|
||||
console.log("(Warning) Could not load apikey from BangleJs.");
|
||||
console.log(ex);
|
||||
}
|
||||
isConnected = false;
|
||||
setTimeout(function() {
|
||||
Util.readStorageFile("log.csv",function(data) {
|
||||
Util.saveCSV("log", data);
|
||||
});
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
document.getElementById("btnSave").addEventListener("click", function() {
|
||||
|
@ -235,63 +221,57 @@ function reset(){
|
|||
}
|
||||
|
||||
document.getElementById("btnReset").addEventListener("click", function() {
|
||||
if (connection) {
|
||||
stop();
|
||||
}
|
||||
Puck.write("load();\n");
|
||||
isConnected = false;
|
||||
lineCount=-1;
|
||||
localStorage.removeItem("data");
|
||||
reset();
|
||||
});
|
||||
|
||||
document.getElementById("btnStop").addEventListener("click", function() {
|
||||
if (connection) {
|
||||
stop();
|
||||
}
|
||||
stop();
|
||||
isConnected = false;
|
||||
});
|
||||
|
||||
function connect(connectionHandler){
|
||||
Puck.connect(function(c) {
|
||||
if (!c) {
|
||||
console.log("Couldn't connect!\n");
|
||||
return;
|
||||
}
|
||||
connection = c;
|
||||
connectionHandler(c);
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById("btnConnect").addEventListener("click", function() {
|
||||
document.getElementById("btnStart").addEventListener("click", function() {
|
||||
localStorage.setItem("data", "");
|
||||
lineCount=-1;
|
||||
if (connection) {
|
||||
stop();
|
||||
document.getElementById("result").innerText="0";
|
||||
}
|
||||
connect(function(connection) {
|
||||
var buf = "";
|
||||
connection.on("data", function(d) {
|
||||
buf += d;
|
||||
var l = buf.split("\n");
|
||||
buf = l.pop();
|
||||
l.forEach(onLine);
|
||||
});
|
||||
connection.write("reset();\n", function() {
|
||||
setTimeout(function() {
|
||||
connection.write("\x03\x10if(1){"+createCode()+"}\n",
|
||||
function() { console.log("Ready..."); });
|
||||
}, 1500);
|
||||
});
|
||||
document.getElementById("result").innerText="0";
|
||||
|
||||
Puck.write("reset();\n", function() {
|
||||
setTimeout(function() {
|
||||
Puck.write("\x03\x10if(1){"+createCode()+"}\n",
|
||||
function() {
|
||||
console.log("Ready...");
|
||||
isConnected = true;
|
||||
}
|
||||
);
|
||||
}, 1500);
|
||||
});
|
||||
});
|
||||
|
||||
function onLine(line) {
|
||||
console.log("RECEIVED:"+line);
|
||||
if (!isConnected) return;
|
||||
if (line.startsWith("DATA:")){
|
||||
localStorage.setItem("data", localStorage.getItem("data") + line.substr(5) + "\n");
|
||||
lineCount++;
|
||||
document.getElementById("result").innerText="Captured events: " + lineCount;
|
||||
}
|
||||
}
|
||||
|
||||
var buf = "";
|
||||
Puck.on("data", function(d) {
|
||||
buf += d;
|
||||
var l = buf.split("\n");
|
||||
buf = l.pop();
|
||||
l.forEach(onLine);
|
||||
});
|
||||
|
||||
|
||||
function onInit(info) {
|
||||
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
0.01: New App!
|
||||
0.02: Fixes
|
||||
0.03: updated to work with new API and additional features added such as longer recording time and additional filtered data
|
||||
0.04: added support for bangle.js 2
|
|
@ -1,13 +1,20 @@
|
|||
Extract hrm raw signal data to CSV file
|
||||
=======================================
|
||||
|
||||
Simple app that will run the heart rate monitor for a defined period of time you set at the start.
|
||||
Simple app that will run the heart rate monitor for a defined period of time you set at the start and record data to a csv file.
|
||||
|
||||
-The app creates a csv file (it's actually just 1 column) and you can download this via My Apps in the App Loader.
|
||||
Updated to work with new API and includes support for Bangle.js 2. Additional capability includes:
|
||||
|
||||
-The max time value is 60 minutes.
|
||||
1. Now also records upto 2 hours - if you cancel at any time the CSV file will still be there, the timer you set at the start is more so that you get an alert when it's complete.
|
||||
2. Along with raw PPG readings, it also records bandpassed filtered data in a second column, available in the new API.
|
||||
3. Rather than overwriting 1 data file, the app will record upto 5 files before recording to a generic data file as a fallback if all 5 allocated files remain on the watch storage. The limit is in place to avoid going over storage limits as these files can get large over time.
|
||||
|
||||
-The first item holds the data/time when the readings were taken and the file is reset each time the app is run.
|
||||
-The hrm sensor is sampled @50Hz on the Bangle.JS 1 and 25Hz on the Bangle 2 by default. At least on the Bangle 2 you can change the sample rate by using the 'custom boot code' app and running this line:
|
||||
Bangle.setOptions({hrmPollInterval:20});
|
||||
|
||||
-The hrm sensor is sampled @50Hz and this app does not do any processing on it other than clip overly high/extreme values, the array is written as-is. There is an example Python script that can process this signal, smooth it and also extract a myriad of heart rate variability metrics using the hrvanalysis library:
|
||||
the 20 in the boot code means the hrm will poll every 20ms (50Hz) instead of the default 40.
|
||||
|
||||
4. On the bangle.JS 2 you can swipe up to begin recording, and on the Bangle.JS 1 you just use the top button.
|
||||
|
||||
For Bangle 1, there is an example Python script that can process this signal, smooth it and also extract a myriad of heart rate variability metrics using the hrvanalysis library. I will be working on a method for Bangle 2 because the data seems more noisy so will need a different processing method:
|
||||
https://github.com/jabituyaben/BangleJS-HRM-Signal-Processing
|
||||
|
|
|
@ -1,97 +1,157 @@
|
|||
var counter = 1;
|
||||
var counter = 15;
|
||||
var logging_started;
|
||||
var interval;
|
||||
var value;
|
||||
var filt;
|
||||
|
||||
var file = require("Storage").open("hrm_log.csv", "w");
|
||||
file.write("");
|
||||
var fileClosed = 0;
|
||||
var Storage = require("Storage");
|
||||
var file;
|
||||
|
||||
file = require("Storage").open("hrm_log.csv", "a");
|
||||
var screenSize = g.getHeight();
|
||||
|
||||
|
||||
function exists(name){
|
||||
s = require('Storage');
|
||||
var fileList = s.list();
|
||||
var fileExists = false;
|
||||
for (let i = 0; i < fileList.length; i++) {
|
||||
fileExists = fileList[i].includes(name);
|
||||
if(fileExists){
|
||||
break;
|
||||
}
|
||||
}
|
||||
return fileExists;
|
||||
}
|
||||
|
||||
function update_timer() {
|
||||
g.clear();
|
||||
g.setColor("#00ff7f");
|
||||
g.setColor("#CC00CC");
|
||||
g.setFont("6x8", 4);
|
||||
g.setFontAlign(0, 0); // center font
|
||||
|
||||
g.drawString(counter, 120, 120);
|
||||
g.drawString(counter, screenSize/2, screenSize/2);
|
||||
g.setFont("6x8", 2);
|
||||
g.setFontAlign(-1, -1);
|
||||
g.drawString("-", 220, 200);
|
||||
g.drawString("+", 220, 40);
|
||||
g.drawString("GO", 210, 120);
|
||||
|
||||
g.setColor("#ffffff");
|
||||
g.setFontAlign(0, 0); // center font
|
||||
g.drawString("Timer (minutes)", 120, 90);
|
||||
|
||||
g.setFont("6x8", 4); // bitmap font, 8x magnified
|
||||
//g.setFontAlign(-1, -1);
|
||||
g.drawString("+", screenSize-10, screenSize/2);
|
||||
g.drawString("-", 10, screenSize/2);
|
||||
g.drawString("GO",screenSize/2 , (screenSize/2)+(screenSize/5));
|
||||
//g.setColor("#ffffff");
|
||||
//g.setFontAlign(0, 0); // center font
|
||||
g.drawString("Timer(minutes)", screenSize/2+5,screenSize/4 );
|
||||
g.setFont("6x8", 4);
|
||||
g.drawString("^",screenSize/2 , 150);
|
||||
|
||||
if (!logging_started)
|
||||
g.flip();
|
||||
}
|
||||
|
||||
function btn1Pressed() {
|
||||
function btn2Pressed() {
|
||||
if (!logging_started) {
|
||||
if (counter < 60)
|
||||
counter += 1;
|
||||
if (counter < 120)
|
||||
counter += 15;
|
||||
else
|
||||
counter = 1;
|
||||
counter = 15;
|
||||
update_timer();
|
||||
}
|
||||
}
|
||||
|
||||
function btn3Pressed() {
|
||||
if (!logging_started) {
|
||||
if (counter > 1)
|
||||
counter -= 1;
|
||||
if (counter > 15)
|
||||
counter -= 15;
|
||||
else
|
||||
counter = 60;
|
||||
counter = 120;
|
||||
update_timer();
|
||||
}
|
||||
}
|
||||
|
||||
function btn2Pressed() {
|
||||
launchtime = 0 | getTime();
|
||||
file.write(launchtime + "," + "\n");
|
||||
logging_started = true;
|
||||
counter = counter * 60;
|
||||
interval = setInterval(countDown, 1000);
|
||||
Bangle.setHRMPower(1);
|
||||
function btn1Pressed() {
|
||||
if (!logging_started) {
|
||||
var filename = "";
|
||||
var fileset = false;
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
filename = "HRM_data" + i.toString() + ".csv";
|
||||
if(exists(filename) == 0){
|
||||
file = require("Storage").open(filename,"w");
|
||||
console.log("creating new file " + filename);
|
||||
fileset = true;
|
||||
}
|
||||
if(fileset){
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!fileset){
|
||||
console.log("overwiting file");
|
||||
file = require("Storage").open("HRM_data.csv","w");
|
||||
}
|
||||
|
||||
file.write("");
|
||||
file = require("Storage").open(filename,"a");
|
||||
|
||||
//launchtime = 0 | getTime();
|
||||
//file.write(launchtime + "," + "\n");
|
||||
logging_started = true;
|
||||
counter = counter * 60;
|
||||
interval = setInterval(countDown, 1000);
|
||||
Bangle.setHRMPower(1);
|
||||
}
|
||||
}
|
||||
|
||||
function fmtMSS(e) {
|
||||
var m = Math.floor(e % 3600 / 60).toString().padStart(2, '0'),
|
||||
s = Math.floor(e % 60).toString().padStart(2, '0');
|
||||
return m + ':' + s;
|
||||
h = Math.floor(e / 3600);
|
||||
e %= 3600;
|
||||
m = Math.floor(e / 60);
|
||||
s = e % 60;
|
||||
return h + ":" + m + ':' + s;
|
||||
}
|
||||
|
||||
function countDown() {
|
||||
g.clear();
|
||||
counter--;
|
||||
if (counter == 0) {
|
||||
if (counter <= 0 && fileClosed == 0) {
|
||||
Bangle.setHRMPower(0);
|
||||
clearInterval(interval);
|
||||
g.drawString("Finished", g.getWidth() / 2, g.getHeight() / 2);
|
||||
g.drawString("Done", g.getWidth() / 2, g.getHeight() / 2);
|
||||
Bangle.buzz(500, 1);
|
||||
fileClosed = 1;
|
||||
}
|
||||
else
|
||||
g.drawString(fmtMSS(counter), g.getWidth() / 2, g.getHeight() / 2);
|
||||
}
|
||||
|
||||
var HRVal = 0;
|
||||
var HRConfidence = 0;
|
||||
|
||||
update_timer();
|
||||
|
||||
setWatch(btn1Pressed, BTN1, { repeat: true });
|
||||
setWatch(btn2Pressed, BTN2, { repeat: true });
|
||||
setWatch(btn3Pressed, BTN3, { repeat: true });
|
||||
//setWatch(btn2Pressed, BTN2, { repeat: true });
|
||||
//setWatch(btn3Pressed, BTN3, { repeat: true });
|
||||
|
||||
Bangle.on('HRM', function (hrm) {
|
||||
for (let i = 0; i < hrm.raw.length; i++) {
|
||||
value = hrm.raw[i];
|
||||
if (value < -2)
|
||||
value = -2;
|
||||
if (value > 6)
|
||||
value = 6;
|
||||
file.write(value + "," + "\n");
|
||||
Bangle.on("swipe",function(directionLR, directionUD){
|
||||
if (1==directionLR){
|
||||
btn1Pressed();
|
||||
}
|
||||
else if (-1==directionUD || directionUD==1){
|
||||
btn2Pressed();
|
||||
}
|
||||
else if(directionLR == -1){
|
||||
btn3Pressed();
|
||||
}
|
||||
});
|
||||
|
||||
Bangle.on('HRM-raw', function (hrm) {
|
||||
value = hrm.raw;
|
||||
filt = hrm.filt;
|
||||
//var dataArray = [value,filt,HRVal,HRConfidence];
|
||||
file.write(value + "," + filt + "\n");
|
||||
});
|
||||
/*
|
||||
Bangle.on('HRM', function (hrmB) {
|
||||
HRVal = hrmB.bpm;
|
||||
HRConfidence = hrmB.confidence;
|
||||
});
|
||||
*/
|
||||
|
|
|
@ -2,13 +2,13 @@
|
|||
"id": "hrrawexp",
|
||||
"name": "HRM Data Exporter",
|
||||
"shortName": "HRM Data Exporter",
|
||||
"version": "0.01",
|
||||
"version": "0.04",
|
||||
"description": "export raw hrm signal data to a csv file",
|
||||
"icon": "app-icon.png",
|
||||
"tags": "",
|
||||
"supports": ["BANGLEJS"],
|
||||
"readme": "README.md",
|
||||
"interface": "interface.html",
|
||||
"supports": ["BANGLEJS","BANGLEJS2"],
|
||||
"storage": [
|
||||
{"name":"hrrawexp.app.js","url":"app.js"},
|
||||
{"name":"hrrawexp.img","url":"app-icon.js","evaluate":true}
|
||||
|
|
|
@ -11,3 +11,5 @@
|
|||
0.11: Added letters with caron to unicodeRemap, to properly display messages in Czech language
|
||||
0.12: Use new message library
|
||||
0.13: Making ANCS message receive more resilient (#2402)
|
||||
0.14: Add settings page, allow time sync
|
||||
Allow negative/positive actions to pass through to message GUI
|
|
@ -1,27 +1,35 @@
|
|||
# iOS integration app
|
||||
|
||||
This is the iOS integration app for Bangle.js. This app allows you to receive
|
||||
notifications from your iPhone. The Apple Notification Center Service (ANCS)
|
||||
sends all the messages to your watch.
|
||||
notifications from your iPhone. The Apple Notification Center Service (ANCS)
|
||||
sends all the messages to your watch.
|
||||
|
||||
You can allow this if you connect your Bangle to your iPhone. It will be
|
||||
prompted for immediatly after you connect the Bangle to the iPhone.
|
||||
You can allow this if you connect your Bangle to your iPhone. It will be
|
||||
prompted for immediatly after you connect the Bangle to the iPhone.
|
||||
|
||||
### Connecting your Bangle(2).js to your iPhone
|
||||
The Bangle watches are Bluetooth Low Energy (BLE) devices. Sometimes they
|
||||
will not be seen/detected by the Bluetooth scanner in your iPhone settings
|
||||
### Setting
|
||||
|
||||
Under `Settings -> Apps -> iOS Integration` there is
|
||||
a `Time Sync` setting. This will enable syncing between the
|
||||
watch and iOS.
|
||||
|
||||
### Connecting your Bangle.js to your iPhone
|
||||
|
||||
The Bangle watches are Bluetooth Low Energy (BLE) devices. Sometimes they
|
||||
will not be seen/detected by the Bluetooth scanner in your iPhone settings
|
||||
menu.
|
||||
|
||||
To resolve this, you can download numerous apps who can actually scan
|
||||
To resolve this, you can download an app that can actually scan
|
||||
for BLE devices. There are great ones out there, free and paid.
|
||||
|
||||
We really like WebBLE, which we also recommend to load apps on your
|
||||
watch with your iOS device, as Safari does not support WebBluetooth
|
||||
We really like WebBLE, which we also recommend to load apps on your
|
||||
watch with your iOS device, as Safari does not support WebBluetooth
|
||||
for now. It's just a few bucks/pounds/euro's.
|
||||
|
||||
If you like to try a free app first, you can always use NRF Toolbox or
|
||||
Bluetooth BLE Device Finder to find and connect your Bangle.
|
||||
|
||||
|
||||
## Requests
|
||||
|
||||
Please file any issues on https://github.com/espruino/BangleApps/issues/new?title=ios%20app
|
||||
|
|
|
@ -27,13 +27,11 @@ E.on('ANCS',msg=>{
|
|||
function ancsHandler() {
|
||||
var msg = Bangle.ancsMessageQueue[0];
|
||||
NRF.ancsGetNotificationInfo( msg.uid ).then( info => { // success
|
||||
|
||||
if(msg.preExisting === true){
|
||||
info.new = false;
|
||||
} else {
|
||||
info.new = true;
|
||||
}
|
||||
|
||||
E.emit("notify", Object.assign(msg, info));
|
||||
Bangle.ancsMessageQueue.shift();
|
||||
if (Bangle.ancsMessageQueue.length)
|
||||
|
@ -169,7 +167,9 @@ E.on('notify',msg=>{
|
|||
new : msg.new,
|
||||
title : msg.title&&E.decodeUTF8(msg.title, unicodeRemap, replacer),
|
||||
subject : msg.subtitle&&E.decodeUTF8(msg.subtitle, unicodeRemap, replacer),
|
||||
body : msg.message&&E.decodeUTF8(msg.message, unicodeRemap, replacer) || "Cannot display"
|
||||
body : msg.message&&E.decodeUTF8(msg.message, unicodeRemap, replacer) || "Cannot display",
|
||||
positive : msg.positive,
|
||||
negative : msg.negative
|
||||
});
|
||||
// TODO: posaction/negaction?
|
||||
});
|
||||
|
@ -196,7 +196,7 @@ Bangle.musicControl = cmd => {
|
|||
};
|
||||
// Message response
|
||||
Bangle.messageResponse = (msg,response) => {
|
||||
if (isFinite(msg.id)) return NRF.sendANCSAction(msg.id, response);//true/false
|
||||
if (isFinite(msg.id)) return NRF.ancsAction(msg.id, response);//true/false
|
||||
// error/warn here?
|
||||
};
|
||||
// remove all messages on disconnect
|
||||
|
@ -225,6 +225,8 @@ NRF.ancsGetNotificationInfo = function(uid) {
|
|||
});
|
||||
};
|
||||
|
||||
E.on("notify", n => print("NOTIFY", n));
|
||||
|
||||
E.emit("ANCS", {
|
||||
event:"add",
|
||||
uid:42,
|
||||
|
@ -232,9 +234,32 @@ E.emit("ANCS", {
|
|||
categoryCnt:42,
|
||||
silent:true,
|
||||
important:false,
|
||||
preExisting:true,
|
||||
preExisting:false,
|
||||
positive:false,
|
||||
negative:true
|
||||
});
|
||||
|
||||
|
||||
*/
|
||||
|
||||
{
|
||||
let settings = require("Storage").readJSON("ios.settings.json",1)||{};
|
||||
let ctsUpdate = e=>{
|
||||
if (process.env.VERSION=="2v19")
|
||||
e.date.setMonth(e.date.getMonth()-1); // fix for bug in 2v19 firmware
|
||||
var tz = 0;
|
||||
if (e.timezone!==undefined) {
|
||||
E.setTimeZone(e.timezone);
|
||||
tz = e.timezone*3600;
|
||||
var settings = require('Storage').readJSON('setting.json',1)||{};
|
||||
settings.timezone = e.timezone;
|
||||
require('Storage').writeJSON('setting.json',settings);
|
||||
}
|
||||
setTime((e.date.getTime()/1000) - tz);
|
||||
};
|
||||
if (settings.timeSync && NRF.ctsGetTime) {
|
||||
if (NRF.ctsIsActive())
|
||||
NRF.ctsGetTime().then(ctsUpdate, function(){ /* */ })
|
||||
E.on('CTS',ctsUpdate);
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "ios",
|
||||
"name": "iOS Integration",
|
||||
"version": "0.13",
|
||||
"version": "0.14",
|
||||
"description": "Display notifications/music/etc from iOS devices",
|
||||
"icon": "app.png",
|
||||
"tags": "tool,system,ios,apple,messages,notifications",
|
||||
|
@ -11,7 +11,9 @@
|
|||
"storage": [
|
||||
{"name":"ios.app.js","url":"app.js"},
|
||||
{"name":"ios.img","url":"app-icon.js","evaluate":true},
|
||||
{"name":"ios.boot.js","url":"boot.js"}
|
||||
{"name":"ios.boot.js","url":"boot.js"},
|
||||
{"name":"ios.settings.js","url":"settings.js"}
|
||||
],
|
||||
"data": [{"name":"ios.settings.json"}],
|
||||
"sortorder": -8
|
||||
}
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
(function(back) {
|
||||
var settings = require("Storage").readJSON("ios.settings.json",1)||{};
|
||||
function updateSettings() {
|
||||
require("Storage").writeJSON("ios.settings.json", settings);
|
||||
}
|
||||
var mainmenu = {
|
||||
"" : { "title" : "iOS" },
|
||||
"< Back" : back,
|
||||
/*LANG*/"Time Sync" : {
|
||||
value : !!settings.timeSync,
|
||||
onchange: v => {
|
||||
settings.timeSync = v;
|
||||
updateSettings();
|
||||
}
|
||||
}
|
||||
};
|
||||
E.showMenu(mainmenu);
|
||||
})
|
|
@ -7,6 +7,5 @@
|
|||
0.07: compatible with Bang;e.js 2
|
||||
0.08: fix minute tick bug
|
||||
0.09: use setUI clockupdown for controls + fix small display bug in nifty face
|
||||
|
||||
|
||||
0.10: stop widget field from flashing when moving to the dk clock face.
|
||||
|
||||
|
|
|
@ -27,7 +27,6 @@
|
|||
d[0] = locale.dow(now,3);
|
||||
var dt=d[0]+" "+d[1]+" "+d[2];//+" "+d[3];
|
||||
g.drawString(dt,W/2,H/2+24);
|
||||
g.flip();
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "multiclock",
|
||||
"name": "Multi Clock",
|
||||
"version": "0.09",
|
||||
"version": "0.10",
|
||||
"description": "Clock with multiple faces. Switch between faces with BTN1 & BTN3 (Bangle 2 touch top-right, bottom right). For best display set theme Background 2 to cyan or some other bright colour in settings.",
|
||||
"screenshots": [{"url":"screen-ana.png"},{"url":"screen-big.png"},{"url":"screen-td.png"},{"url":"screen-nifty.png"},{"url":"screen-word.png"},{"url":"screen-sec.png"}],
|
||||
"icon": "multiclock.png",
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
0.01: New App!
|
||||
0.02: Keep advertising when connected
|
||||
|
|
|
@ -40,7 +40,7 @@ const key = E.toUint8Array(atob(${JSON.stringify(keyValue)})); // public key
|
|||
const mac = [ key[0] | 0b11000000, key[1], key[2], key[3], key[4], key[5] ].map(x => x.toString(16).padStart(2, '0')).join(':'); // mac address
|
||||
const adv = [ 0x1e, 0xff, 0x4c, 0x00, 0x12, 0x19, 0x00, key[6], key[7], key[8], key[9], key[10], key[11], key[12], key[13], key[14], key[15], key[16], key[17], key[18], key[19], key[20], key[21], key[22], key[23], key[24], key[25], key[26], key[27], key[0] >> 6, 0x00 ]; // advertising packet
|
||||
NRF.setAddress(mac);
|
||||
NRF.setAdvertising([adv,{}]); // advertise AirTag *and* normal device name (to remain connectable)
|
||||
NRF.setAdvertising([adv,{}],{whenConnected: true, interval: 1000}); // advertise AirTag *and* normal device name (to remain connectable)
|
||||
}
|
||||
`;
|
||||
// send finished app
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{ "id": "openhaystack",
|
||||
"name": "OpenHaystack (AirTag)",
|
||||
"icon": "icon.png",
|
||||
"version":"0.01",
|
||||
"version":"0.02",
|
||||
"description": "Copy a base64 key from https://github.com/seemoo-lab/openhaystack and make your Bangle.js trackable as if it's an AirTag",
|
||||
"tags": "openhaystack,bluetooth,ble,tracking,airtag",
|
||||
"type": "bootloader",
|
||||
|
|
|
@ -29,4 +29,5 @@
|
|||
0.22: Replace position marker with direction arrow
|
||||
0.23: Bugfix: Enable Compass if needed
|
||||
0.24: Allow zooming by clicking the screen
|
||||
0.25: Enable scaled image filtering on 2v19+ firmware
|
||||
0.25: Enable scaled image filtering on 2v19+ firmware
|
||||
0.26: Ensure that when redrawing, we always cancel any in-progress track draw
|
|
@ -30,6 +30,10 @@ if (settings.dirSrc === undefined) {
|
|||
|
||||
// Redraw the whole page
|
||||
function redraw() {
|
||||
// ensure we do cancel track drawing
|
||||
if (plotTrack && plotTrack.stop)
|
||||
plotTrack.stop();
|
||||
// set clip rect so we don't overwrite widgets
|
||||
g.setClipRect(R.x,R.y,R.x2,R.y2);
|
||||
const count = m.draw();
|
||||
if (checkMapPos && count === 0) {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"id": "openstmap",
|
||||
"name": "OpenStreetMap",
|
||||
"shortName": "OpenStMap",
|
||||
"version": "0.25",
|
||||
"version": "0.26",
|
||||
"description": "Loads map tiles from OpenStreetMap onto your Bangle.js and displays a map of where you are. Once installed this also adds map functionality to `GPS Recorder` and `Recorder` apps",
|
||||
"readme": "README.md",
|
||||
"icon": "app.png",
|
||||
|
|
|
@ -3,3 +3,4 @@
|
|||
0.03: Swipe down to see widgets
|
||||
Support for fast loading
|
||||
0.04: Localisation request: added Miles and AM/PM
|
||||
0.05: Prevent exceptions from halting the draw cycle
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"id": "pebbled",
|
||||
"name": "Pebble Clock with distance",
|
||||
"shortName": "Pebble + distance",
|
||||
"version": "0.04",
|
||||
"version": "0.05",
|
||||
"description": "Fork of Pebble Clock with distance in KM. Both step count and the distance are on the main screen. Default step length = 0.75m (can be changed in settings).",
|
||||
"readme": "README.md",
|
||||
"icon": "pebbled.png",
|
||||
|
|
|
@ -37,6 +37,13 @@ const h3 = 7*h/8 - 10;
|
|||
let batteryWarning = false;
|
||||
|
||||
let draw = function() {
|
||||
// queue next draw
|
||||
if (drawTimeout) clearTimeout(drawTimeout);
|
||||
drawTimeout = setTimeout(function() {
|
||||
drawTimeout = undefined;
|
||||
draw();
|
||||
}, 60000 - (Date.now() % 60000));
|
||||
|
||||
let date = new Date();
|
||||
let da = date.toString().split(" ");
|
||||
let timeStr = settings.localization === "US" ? tConv24(da[4].substr(0,5)) : da[4].substr(0,5);
|
||||
|
@ -101,13 +108,6 @@ let draw = function() {
|
|||
else
|
||||
g.setColor('#000'); // otherwise black regardless of theme
|
||||
g.drawString(distanceStr, w/2, ha + 107);
|
||||
|
||||
// queue next draw
|
||||
if (drawTimeout) clearTimeout(drawTimeout);
|
||||
drawTimeout = setTimeout(function() {
|
||||
drawTimeout = undefined;
|
||||
draw();
|
||||
}, 60000 - (Date.now() % 60000));
|
||||
};
|
||||
|
||||
// at x,y width:wi thicknes:th
|
||||
|
|
|
@ -36,3 +36,7 @@
|
|||
0.28: Automatically create new track if the filename is different
|
||||
0.29: When plotting with OpenStMap scale map to track width & height
|
||||
0.30: Add clock info for showing and toggling recording state
|
||||
0.31: Ensure that background-drawn tracks can get cancelled, and draw less at a time to make updates smoother
|
||||
plotTrack now draws the current track even if you're not actively recording
|
||||
0.32: Add cadence data to output files
|
||||
0.33: Ensure that a new file is always created if the stuff that's being recorded has changed (fix #3081)
|
|
@ -95,17 +95,27 @@ function saveGPX(track, title) {
|
|||
<trk>
|
||||
<name>${title}</name>
|
||||
<trkseg>`;
|
||||
let lastTime = 0;
|
||||
track.forEach(pt=>{
|
||||
let cadence;
|
||||
if (pt.Steps && lastTime != 0){
|
||||
cadence = pt.Steps * 60000 / (pt.Time.getTime() - lastTime);
|
||||
cadence = cadence / 2; /*Convert from rpm to spm (one cycle is two steps), see https://github.com/espruino/BangleApps/pull/3068#issuecomment-1790041058*/
|
||||
}
|
||||
lastTime = pt.Time.getTime();
|
||||
|
||||
gpx += `
|
||||
<trkpt lat="${pt.Latitude}" lon="${pt.Longitude}">
|
||||
<ele>${pt.Altitude}</ele>
|
||||
<time>${pt.Time.toISOString()}</time>
|
||||
<extensions>
|
||||
<gpxtpx:TrackPointExtension>
|
||||
${pt.Heartrate ? `<gpxtpx:hr>${pt.Heartrate}</gpxtpx:hr>`:``}${""/*<gpxtpx:distance>...</gpxtpx:distance><gpxtpx:cad>65</gpxtpx:cad>*/}
|
||||
${pt.Heartrate ? `<gpxtpx:hr>${pt.Heartrate}</gpxtpx:hr>`:``}
|
||||
${cadence ? `<gpxtpx:cad>${cadence}</gpxtpx:cad>`:``} ${""/*<gpxtpx:distance>...</gpxtpx:distance><gpxtpx:cad>65</gpxtpx:cad>*/}
|
||||
</gpxtpx:TrackPointExtension>
|
||||
</extensions>
|
||||
</trkpt>`;
|
||||
|
||||
});
|
||||
// https://www8.garmin.com/xmlschemas/TrackPointExtensionv1.xsd
|
||||
gpx += `
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"id": "recorder",
|
||||
"name": "Recorder",
|
||||
"shortName": "Recorder",
|
||||
"version": "0.30",
|
||||
"version": "0.33",
|
||||
"description": "Record GPS position, heart rate and more in the background, then download to your PC.",
|
||||
"icon": "app.png",
|
||||
"tags": "tool,outdoors,gps,widget,clkinfo",
|
||||
|
|
|
@ -159,6 +159,21 @@
|
|||
return recorders;
|
||||
}
|
||||
|
||||
let getActiveRecorders = function() {
|
||||
let activeRecorders = [];
|
||||
let recorders = getRecorders();
|
||||
settings.record.forEach(r => {
|
||||
var recorder = recorders[r];
|
||||
if (!recorder) {
|
||||
console.log(/*LANG*/"Recorder for "+E.toJS(r)+/*LANG*/"+not found");
|
||||
return;
|
||||
}
|
||||
activeRecorders.push(recorder());
|
||||
});
|
||||
return activeRecorders;
|
||||
};
|
||||
let getCSVHeaders = activeRecorders => ["Time"].concat(activeRecorders.map(r=>r.fields));
|
||||
|
||||
let writeLog = function() {
|
||||
entriesWritten++;
|
||||
WIDGETS["recorder"].draw();
|
||||
|
@ -189,17 +204,9 @@
|
|||
|
||||
if (settings.recording) {
|
||||
// set up recorders
|
||||
var recorders = getRecorders(); // TODO: order??
|
||||
settings.record.forEach(r => {
|
||||
var recorder = recorders[r];
|
||||
if (!recorder) {
|
||||
console.log(/*LANG*/"Recorder for "+E.toJS(r)+/*LANG*/"+not found");
|
||||
return;
|
||||
}
|
||||
var activeRecorder = recorder();
|
||||
activeRecorders = getActiveRecorders();
|
||||
activeRecorders.forEach(activeRecorder => {
|
||||
activeRecorder.start();
|
||||
activeRecorders.push(activeRecorder);
|
||||
// TODO: write field names?
|
||||
});
|
||||
WIDGETS["recorder"].width = 15 + ((activeRecorders.length+1)>>1)*12; // 12px per recorder
|
||||
// open/create file
|
||||
|
@ -209,9 +216,7 @@
|
|||
} else {
|
||||
storageFile = require("Storage").open(settings.file,"w");
|
||||
// New file - write headers
|
||||
var fields = ["Time"];
|
||||
activeRecorders.forEach(recorder => fields.push.apply(fields,recorder.fields));
|
||||
storageFile.write(fields.join(",")+"\n");
|
||||
storageFile.write(getCSVHeaders(activeRecorders).join(",")+"\n");
|
||||
}
|
||||
// start recording...
|
||||
WIDGETS["recorder"].draw();
|
||||
|
@ -246,7 +251,8 @@
|
|||
// if no filename set or date different, set up a new filename
|
||||
settings.file = getTrackFilename();
|
||||
}
|
||||
if (require("Storage").list(settings.file).length){ // if file exists
|
||||
var headers = require("Storage").open(settings.file,"r").readLine();
|
||||
if (headers && headers.trim()==getCSVHeaders(getActiveRecorders()).join(",")){ // if file exists AND the headers match (#3081)
|
||||
if (!options.force) { // if not forced, ask the question
|
||||
g.reset(); // work around bug in 2v17 and earlier where bg color wasn't reset
|
||||
return E.showPrompt(
|
||||
|
@ -288,8 +294,8 @@
|
|||
}
|
||||
*/
|
||||
options = options||{};
|
||||
if (!activeRecorders.length) return; // not recording
|
||||
var settings = loadSettings();
|
||||
if (!settings.file) return; // no file specified
|
||||
// keep function to draw track in RAM
|
||||
function plot(g) { "ram";
|
||||
var f = require("Storage").open(settings.file,"r");
|
||||
|
@ -311,7 +317,7 @@
|
|||
mp = m.latLonToXY(+c[la], +c[lo]);
|
||||
g.moveTo(mp.x,mp.y).setColor(color);
|
||||
l = f.readLine(f);
|
||||
var n = options.async ? 20 : 200; // only plot first 200 points to keep things fast(ish)
|
||||
var n = options.async ? 10 : 200; // only plot first 200 points to keep things fast(ish)
|
||||
while(l && n--) {
|
||||
c = l.split(",");
|
||||
if (c[la]) {
|
||||
|
@ -333,7 +339,7 @@
|
|||
}
|
||||
};
|
||||
}
|
||||
plot(g);
|
||||
return plot(g);
|
||||
}};
|
||||
// load settings, set correct widget width
|
||||
reload();
|
||||
|
|
|
@ -15,3 +15,4 @@
|
|||
0.14: Fix Bangle.js 1 issue where after the 'overwrite track' menu, the start/stop button stopped working
|
||||
0.15: Keep run state between runs (allowing you to exit and restart the app)
|
||||
0.16: Added ability to resume a run that was stopped previously (fix #1907)
|
||||
0.17: Ensure screen redraws after "Resume run?" menu (#3044)
|
|
@ -60,7 +60,11 @@ function onStartStop() {
|
|||
isMenuDisplayed = true;
|
||||
return E.showPrompt("Resume run?",{title:"Run"});
|
||||
}).then(r => {
|
||||
isMenuDisplayed=false;shouldResume=r;
|
||||
isMenuDisplayed = false;
|
||||
layout.setUI(); // grab our input handling again
|
||||
layout.forgetLazyState();
|
||||
layout.render();
|
||||
shouldResume = r;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{ "id": "run",
|
||||
"name": "Run",
|
||||
"version":"0.16",
|
||||
"version":"0.17",
|
||||
"description": "Displays distance, time, steps, cadence, pace and more for runners.",
|
||||
"icon": "app.png",
|
||||
"tags": "run,running,fitness,outdoors,gps",
|
||||
|
|
|
@ -22,3 +22,4 @@ Write to correct settings file, fixing settings not working.
|
|||
0.20: Tweak HRM min/max defaults. Extend min/max intervals in settings. Fix
|
||||
another typo.
|
||||
0.21: Rebase on "Run" app ver. 0.16.
|
||||
0.22: Ensure screen redraws after "Resume run?" menu (#3044)
|
|
@ -71,7 +71,11 @@ function onStartStop() {
|
|||
isMenuDisplayed = true;
|
||||
return E.showPrompt("Resume run?",{title:"Run"});
|
||||
}).then(r => {
|
||||
isMenuDisplayed=false;shouldResume=r;
|
||||
isMenuDisplayed=false;
|
||||
layout.setUI(); // grab our input handling again
|
||||
layout.forgetLazyState();
|
||||
layout.render();
|
||||
shouldResume=r;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "runplus",
|
||||
"name": "Run+",
|
||||
"version": "0.21",
|
||||
"version": "0.22",
|
||||
"description": "Displays distance, time, steps, cadence, pace and more for runners. Based on the Run app, but extended with additional screen for heart rate interval training.",
|
||||
"icon": "app.png",
|
||||
"tags": "run,running,fitness,outdoors,gps,karvonen,karvonnen",
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
0.01: New App!
|
||||
0.02: Hide widgets instead of not loading them at all
|
||||
0.02: Hide widgets instead of not loading them at all
|
||||
0.03: Bangle.js 2: add "monochrome" setting
|
||||
|
|