1
0
Fork 0

Merge remote-tracking branch 'upstream/master'

master
Hugh Barney 2023-11-11 18:22:06 +00:00
commit e9ec30ddf0
153 changed files with 4344 additions and 1429 deletions

View File

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

1
apps/Tyreid/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: Change log created

18
apps/Tyreid/README.md Normal file
View File

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

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

@ -0,0 +1 @@
E.toArrayBuffer(atob("MDACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAtAAAAAAAAAAAAAAB/QAAAAAAAAAAAAAD/0AAAAAAAAAAAAAH+9AAAAAAAAAAAAAPtfQAAAAAAAAAAAAudH0AAAAAAAAAAAB8tB9AAAAAAAAAAAD0tAfQAAAAAAAAAAHgtAH0AAAAAAAAAAPAtAB9AAAAAAAAAAtAtAAfQAAAAAAAAB8AtAAH0AAAAAAAAD0AtAAB9AAAAAAAALgAtAAAfQAAAAAAAfAAtAAAH0AAAAAAA9AAtAAAB9AAAAAAC4AAtAAAAfQAAAAADwAAtAAAALwAAAAAAQAAtAAAAvgAAAAAAAAAsAAAC9AAAAAAAAAAsAAAP0AAAAAAAAAAsAAB/AAAAAAAAAAAsAAH4AAAAAAAAAAAsAAvQAAAAAAAAAAAsAD9AAAAAAAAAAAAsAD0AAAAAAAAAAAAsAB8AAAAAAAAAAAAsAAtAAAAAAAAAAAAsAAPQAAAAAAAAAAAsAAHwAAAAAAAAAAAsAAC4AAAAAAAAAAAsAAA9AAAAAAAAAAAsAAAPAAAAAAAAAAAsAAAHgAAAAAAAAAA8AAAC0AAAAAAAAAA8AAAA8AAAAAAAAAA8AAAAEAAAAAAAAAA8AAAAAAAAAAAAAAA8AAAAAAAAAAAAAAA8AAAAAAAAAAAAAAA8AAAAAAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"))

272
apps/Tyreid/app.js Normal file

File diff suppressed because one or more lines are too long

14
apps/Tyreid/metadata.json Normal file
View File

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

BIN
apps/Tyreid/small_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
atob("MDAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAArgVYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABW19cAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACsrNcrAACBVoGsVgAAgVaBrFYAAKzXK4GsKwAAgayBAAAAgVYAVoEAAAAAAAAAAADXVqyBAACs14Gs1ysArNeBrNcrAIHX14HXgQCB14HXrAAAVtdW11YAAAAAAAAAAFbXK4GsAACs1wAr11YArNcAK9dWAADXrABWVgDXgQCB1wAAAKzXgQAAAAAAAAAAAKzXrNfXKwCs1wAA14EArKwAK9dWAADXVgAAAADXgQBW1ysAAIHXVgAAAAAAAAAAANfXgYHXVgCs11aB11YArNcrgddWACvXgSsAAACsrCus1wAAK9es1ysAAAAAAAAAK9dWAACsrACsrKzXrAAArKzX16wAVtfX16wAAAAr19fXKwAArKwArKwAAAAAAAAAAAAAAAAAAACsrAArAAAArIEAKwAAAAAAAAAAAAAAACsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACsrAAAAAAArIEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABWVgAAAAAAVlYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAArVoErAAAAAAAAAAAAAAAAAAAAAAArVgAAAAAAAAAAAAAAAAAAAAAAAAArrNfXgQCBrNdWAAAAAAAAAAAAAAAAAAAAAABW1wAAAAAAAAAAAAAAAAAAAAAAACvXrCtWgQAAANdWAAAAAAArKwAAAAAAACsAAABW1wAAAAAAAAAAAAAAAAAAAAAAAFbXKwAAAAAAANdWAAAAAIHX16wAAACB19fXVgBW1wAA14EAAAAAAAAAAAAAAAAAAIGsAAAAAAAAANdWAAAAK9eBVteBACvXgSuBVgBW1wBW1ysAAAAAAAAAAAAAAAAAAIHXAAAAAAAAANdWAAAAVtcrAKyBAFbXKwAAAABW16zX1wAAAAAAAAAAAAAAAAAAAFbXVgAAAAAAANeBAAAAVtcrANeBAFbXKwAAAABW14HXrAAAAAAAAAAAAAAAAAAAAACs14GBrAAAAKzXrIEAK9esrNdWACvX14GBVgBW1wAr11YAAAAAAAAAAAAAAAAAAAAAVoGBgQAAACusrIEAACusrFYAAAArgaysVgBWgQAAgYEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")

156
apps/approxclock/app.js Normal file
View File

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

BIN
apps/approxclock/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

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

View File

@ -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.
![screenshot](./Example.PNG)
<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:

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1 +1,2 @@
0.01: Simple app to display loyalty cards
0.02: Hiding widgets while showing the code

View File

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

View File

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

1
apps/contacts/ChangeLog Normal file
View File

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

29
apps/contacts/README.md Normal file
View File

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

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwcBkmSpIC/AVsJCJ+AQaCZBCOeACKGQLKGQBA0ggARPJ4IRsYo0ggR9IoAIGiRiIpEECJsAiACBBYoRGpEAI4JBFI47CBLIRlDHYJrGYQIRCwQICL4MQOgx9GboUSeQ4RFwAFBiSGHCIo4CiVIWZyPICP4RaRIQROgARHdIwICoIIFkDpGBAKqHgGACI0AyVIggIDoEEMQ1ICINJCIj4CfwIREBwUgQYYOCfYoFDJQKDFCIopEO4RoDKAqJHRhAC/ATA="))

BIN
apps/contacts/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

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

View File

@ -0,0 +1,6 @@
[
{
"name":"EU emergency",
"number":"112"
}
]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEw4UA///x3ygHo8H1Jf8AgILLoALVgoLHggLCqAgJioLIqgLDGQsBqtAioOBqoYFqtUAIMVBY9VqwCBDIIAECYILCHowLBrWVBZFlrWWyptGgtq1WqJYI7GrQLCFxGrBYJHEBQNV1Wv9IEBEocFKIOq//qJAIZEAoNq3/+1QMBHoYYBrQLB1J4GitaEYZfGtfvBYJ3HtWr9WlNY0V1Nr1WlC4xIBrWmBZWVrJGFcYILBZY4LBoILIgoNBEILvHDIQ5BBY4IBBYMBMAwLBBA4LPBRMAKAoLRiALWAGw="))

59
apps/flashcount/app.js Normal file
View File

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

BIN
apps/flashcount/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

16
apps/hardalarm/README.md Normal file
View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

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

4
apps/hrrawexp/ChangeLog Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

18
apps/ios/settings.js Normal file
View File

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

View File

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

View File

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

View File

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

View File

@ -1 +1,2 @@
0.01: New App!
0.02: Keep advertising when connected

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 += `

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More