Merge branch 'espruino:master' into light_switch
|
|
@ -0,0 +1,2 @@
|
|||
0.01: Initial release.
|
||||
0.02: Added compatibility to OpenTracks and added HRM Location
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# BLE GATT HRM Service
|
||||
|
||||
Adds the GATT HRM Service to advertise the current HRM over Bluetooth.
|
||||
|
||||
## Usage
|
||||
|
||||
This boot code runs in the background and has no user interface.
|
||||
|
||||
## Creator
|
||||
|
||||
[Another Stranger](https://github.com/anotherstranger)
|
||||
|
||||
## Aknowledgements
|
||||
|
||||
Special thanks to [Jonathan Jefferies](https://github.com/jjok) for creating the
|
||||
bootgattbat app, which was the inspiration for this App!
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
|
|
@ -0,0 +1,70 @@
|
|||
(() => {
|
||||
function setupHRMAdvertising() {
|
||||
/*
|
||||
* This function prepares BLE heart rate Advertisement.
|
||||
*/
|
||||
|
||||
NRF.setAdvertising(
|
||||
{
|
||||
0x180d: undefined
|
||||
},
|
||||
{
|
||||
// We need custom Advertisement settings for Apps like OpenTracks
|
||||
connectable: true,
|
||||
discoverable: true,
|
||||
scannable: true,
|
||||
whenConnected: true,
|
||||
}
|
||||
);
|
||||
|
||||
NRF.setServices({
|
||||
0x180D: { // heart_rate
|
||||
0x2A37: { // heart_rate_measurement
|
||||
notify: true,
|
||||
value: [0x06, 0],
|
||||
},
|
||||
0x2A38: { // Sensor Location: Wrist
|
||||
value: 0x02,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
function updateBLEHeartRate(hrm) {
|
||||
/*
|
||||
* Send updated heart rate measurement via BLE
|
||||
*/
|
||||
if (hrm === undefined || hrm.confidence < 50) return;
|
||||
try {
|
||||
NRF.updateServices({
|
||||
0x180D: {
|
||||
0x2A37: {
|
||||
value: [0x06, hrm.bpm],
|
||||
notify: true
|
||||
},
|
||||
0x2A38: {
|
||||
value: 0x02,
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.message.includes("BLE restart")) {
|
||||
/*
|
||||
* BLE has to restart after service setup.
|
||||
*/
|
||||
NRF.disconnect();
|
||||
}
|
||||
else if (error.message.includes("UUID 0x2a37")) {
|
||||
/*
|
||||
* Setup service if it wasn't setup correctly for some reason
|
||||
*/
|
||||
setupHRMAdvertising();
|
||||
} else {
|
||||
console.log("[bootgatthrm]: Unexpected error occured while updating HRM over BLE! Error: " + error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setupHRMAdvertising();
|
||||
Bangle.on("HRM", function (hrm) { updateBLEHeartRate(hrm); });
|
||||
})();
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"id": "bootgatthrm",
|
||||
"name": "BLE GATT HRM Service",
|
||||
"shortName": "BLE HRM Service",
|
||||
"version": "0.02",
|
||||
"description": "Adds the GATT HRM Service to advertise the measured HRM over Bluetooth.\n",
|
||||
"icon": "bluetooth.png",
|
||||
"type": "bootloader",
|
||||
"tags": "hrm,health,ble,bluetooth,gatt",
|
||||
"supports": ["BANGLEJS","BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
"storage": [
|
||||
{"name":"gatthrm.boot.js","url":"boot.js"}
|
||||
]
|
||||
}
|
||||
|
|
@ -5,4 +5,5 @@
|
|||
0.05: Deleting Background - making Font larger
|
||||
0.06: Fixing refresh issues
|
||||
0.07: Fixed position after unlocking
|
||||
0.08: Handling exceptions
|
||||
0.08: Handling exceptions
|
||||
0.09: Add option for showing battery high mark
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ Show the current battery level and charging status in the top right of the clock
|
|||
* Blue when charging
|
||||
* 40 pixels wide
|
||||
|
||||
The high-level marker (a little bar at the 100% point) can be toggled in settings.
|
||||
|
||||

|
||||
|
||||
## Creator
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"name": "A Battery Widget (with percentage) - Hanks Mod",
|
||||
"shortName":"H Battery Widget",
|
||||
"icon": "widget.png",
|
||||
"version":"0.08",
|
||||
"version":"0.09",
|
||||
"type": "widget",
|
||||
"supports": ["BANGLEJS", "BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
(function(){
|
||||
const intervalLow = 60000; // update time when not charging
|
||||
const intervalHigh = 2000; // update time when charging
|
||||
var old_l;
|
||||
|
||||
var old_x = this.x;
|
||||
var old_y = this.y;
|
||||
|
|
@ -22,49 +21,36 @@
|
|||
};
|
||||
|
||||
function draw() {
|
||||
if (typeof old_x === 'undefined') old_x = this.x;
|
||||
if (typeof old_y === 'undefined') old_y = this.y;
|
||||
var s = 29;
|
||||
var s = width - 1;
|
||||
var x = this.x;
|
||||
var y = this.y;
|
||||
if ((typeof x === 'undefined') || (typeof y === 'undefined')) {
|
||||
} else {
|
||||
g.clearRect(old_x, old_y, old_x + width, old_y + height);
|
||||
|
||||
const l = E.getBattery(); // debug: Math.floor(Math.random() * 101);
|
||||
let xl = x+4+l*(s-12)/100;
|
||||
if ((l != old_l) && (typeof old_l != 'undefined') ){ // Delete the old value from screen
|
||||
let xl_old = x+4+old_l*(s-12)/100;
|
||||
g.setColor(COLORS.white);
|
||||
// g.fillRect(x+2,y+5,x+s-6,y+18);
|
||||
g.fillRect(x,y,xl+4,y+16+3); //Clear
|
||||
g.setFontAlign(0,0);
|
||||
g.setFont('Vector',16);
|
||||
//g.fillRect(old_x,old_y,old_x+4+l*(s-12)/100,old_y+16+3); // clear (lazy)
|
||||
g.drawString(old_l, old_x + 14, old_y + 10);
|
||||
g.fillRect(x+4,y+14+3,xl_old,y+16+3); // charging bar
|
||||
|
||||
}
|
||||
old_l = l;
|
||||
//console.log(old_x);
|
||||
|
||||
g.setColor(levelColor(l));
|
||||
g.fillRect(x+4,y+14+3,xl,y+16+3); // charging bar
|
||||
g.fillRect((x+4+100*(s-12)/100)-1,y+14+3,x+4+100*(s-12)/100,y+16+3); // charging bar "full mark"
|
||||
// Show percentage
|
||||
g.setColor(COLORS.black);
|
||||
g.setFontAlign(0,0);
|
||||
g.setFont('Vector',16);
|
||||
g.drawString(l, x + 14, y + 10);
|
||||
|
||||
|
||||
}
|
||||
old_x = this.x;
|
||||
old_y = this.y;
|
||||
|
||||
old_y = this.y;
|
||||
|
||||
if (Bangle.isCharging()) changeInterval(id, intervalHigh);
|
||||
else changeInterval(id, intervalLow);
|
||||
}
|
||||
|
||||
Bangle.on('charging',function(charging) { draw(); });
|
||||
var id = setInterval(()=>WIDGETS["hwid_a_battery_widget"].draw(), intervalLow);
|
||||
var width = 30;
|
||||
var height = 19;
|
||||
|
||||
WIDGETS["hwid_a_battery_widget"]={area:"tr",width:30,draw:draw};
|
||||
WIDGETS["hwid_a_battery_widget"]={area:"tr",width,draw:draw};
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
1.00: New keyboard
|
||||
1.01: Change swipe interface to taps, speed up responses (efficiency tweaks).
|
||||
1.02: Generalize drawing and letter scaling. Allow custom and auto-generated character sets. Improve documentation.
|
||||
1.03: Attempt to improve keyboard load time.
|
||||
1.04: Make code asynchronous and improve load time.
|
||||
1.05: Fix layout issue and rename library
|
||||
1.06: Touch up readme, prep for IPO, add screenshots
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
# Matryoshka Keyboard
|
||||
|
||||

|
||||
|
||||
 
|
||||
|
||||
 
|
||||
 
|
||||
|
||||
Nested key input utility.
|
||||
|
||||
## How to type
|
||||
|
||||
Press your finger down on the letter group that contains the character you would like to type, then tap the letter you
|
||||
want to enter. Once you are touching the letter you want, release your
|
||||
finger.
|
||||
|
||||

|
||||
|
||||
Press "shft" or "caps" to access alternative characters, including upper case letters, punctuation, and special
|
||||
characters.
|
||||
Pressing "shft" also reveals a cancel button if you would like to terminate input without saving.
|
||||
|
||||
Press "ok" to finish typing and send your text to whatever app called this keyboard.
|
||||
|
||||
Press "del" to delete the leftmost character.
|
||||
|
||||
## Themes and Colors
|
||||
|
||||
This keyboard will attempt to use whatever theme or colorscheme is being used by your Bangle device.
|
||||
|
||||
## How to use in a program
|
||||
|
||||
This was developed to match the interface implemented for kbtouch, kbswipe, etc.
|
||||
|
||||
In your app's metadata, add:
|
||||
|
||||
```json
|
||||
"dependencies": {"textinput": "type"}
|
||||
```
|
||||
|
||||
From inside your app, call:
|
||||
|
||||
```js
|
||||
const textInput = require("textinput");
|
||||
|
||||
textInput.input({text: ""})
|
||||
.then(result => {
|
||||
console.log("The user entered: ", result);
|
||||
});
|
||||
```
|
||||
|
||||
Alternatively, if you want to improve the load time of the keyboard, you can pre-generate the data the keyboard needs
|
||||
to function and render like so:
|
||||
|
||||
```js
|
||||
const textInput = require("textinput");
|
||||
|
||||
const defaultKeyboard = textInput.generateKeyboard(textInput.defaultCharSet);
|
||||
const defaultShiftKeyboard = textInput.generateKeyboard(textInput.defaultCharSetShift);
|
||||
// ...
|
||||
textInput.input({text: "", keyboardMain: defaultKeyboard, keyboardShift: defaultShiftKeyboard})
|
||||
.then(result => {
|
||||
console.log("The user entered: ", result);
|
||||
// And it was faster!
|
||||
});
|
||||
```
|
||||
|
||||
This isn't required, but if you are using a large character set, and the user is interacting with the keyboard a lot,
|
||||
it can really smooth the experience.
|
||||
|
||||
The default keyboard has a full set of alphanumeric characters as well as special characters and buttons in a
|
||||
pre-defined layout. If your application needs something different, or you want to have a custom layout, you can do so:
|
||||
|
||||
```js
|
||||
const textInput = require("textinput");
|
||||
|
||||
const customKeyboard = textInput.generateKeyboard([
|
||||
["1", "2", "3", "4"], ["5", "6", "7", "8"], ["9", "0", ".", "-"], "ok", "del", "cncl"
|
||||
]);
|
||||
// ...
|
||||
textInput.input({text: "", keyboardMain: customKeyboard})
|
||||
.then(result => {
|
||||
console.log("The user entered: ", result);
|
||||
// And they could only enter numbers, periods, and dashes!
|
||||
});
|
||||
```
|
||||
|
||||
This will give you a keyboard with six buttons. The first three buttons will open up a 2x2 keyboard. The second three
|
||||
buttons are special keys for submitting, deleting, and cancelling respectively.
|
||||
|
||||
Finally if you are like, super lazy, or have a dynamic set of keys you want to be using at any given time, you can
|
||||
generate keysets from strings like so:
|
||||
|
||||
```js
|
||||
const textInput = require("textinput");
|
||||
|
||||
const customKeyboard = textInput.generateKeyboard(createCharSet("ABCDEFGHIJKLMNOP", ["ok", "shft", "cncl"]));
|
||||
const customShiftKeyboard = textInput.generateKeyboard(createCharSet("abcdefghijklmnop", ["ok", "shft", "cncl"]));
|
||||
// ...
|
||||
textInput.input({text: "", keyboardMain: customKeyboard, keyboardShift: customShiftKeyboard})
|
||||
.then(result => {
|
||||
console.log("The user entered: ", result);
|
||||
// And the keyboard was automatically generated to include "ABCDEFGHIJKLMNOP" plus an OK button, a shift button, and a cancel button!
|
||||
});
|
||||
```
|
||||
|
||||
The promise resolves when the user hits "ok" on the input or if they cancel. If the user cancels, undefined is
|
||||
returned, although the user can hit "OK" with an empty string as well. If you define a custom character set and
|
||||
do not include the "ok" button your user will be soft-locked by the keyboard. Fair warning!
|
||||
|
||||
At some point I may add swipe-for-space and swipe-for-delete as well as swipe-for-submit and swipe-for-cancel
|
||||
however I want to have a good strategy for the touch screen
|
||||
[affordance](https://careerfoundry.com/en/blog/ux-design/affordances-ux-design/).
|
||||
|
||||
## Secret features
|
||||
|
||||
If you long press a key with characters on it, that will enable "Shift" mode.
|
||||
|
||||
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwcBkmSpICVz//ABARGCBIRByA/Dk+AAgUH8AECgP4kmRCwX4n+PAoXH8YEC+IRC4HguE4/+P/EfCIXwgARHn4RG+P/j4RDJwgRBGQIRIEYNxCIRECGpV/CIXAgY1P4/8v41JOgeOn4RDGo4jER5Y1FCJWQg4RDYpeSNIQAMkmTCBwRBz4IG9YRIyA8COgJHBhMgI4+QyVJAYJrC9Mkw5rHwFAkEQCImSCJvAhIRBpazFGo3HEYVJkIjGCIIUCAQu/CKGSGo4jPLIhHMNayPLYo6zBYozpH9MvdI+TfaGSv4KHCI+Qg4GDI4IABg5HGyIYENYIAB45rGyPACKIIDx/4gF/CIPx/8fCIY1F4H8CJPA8BtCa4I1DCJFxCIYXBCILXBGpXHGplwn5HPuE4NaH4n6PLyC6CgEnYpeSpICDdJYRFz4RQARQ"))
|
||||
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 852 B |
|
|
@ -0,0 +1,501 @@
|
|||
/**
|
||||
* Attempt to lay out a set of characters in a logical way to optimize the number of buttons with the number
|
||||
* of characters per button. Useful if you need to dynamically (or frequently) change your character set
|
||||
* and don't want to create a layout for ever possible combination.
|
||||
* @param text The text you want to parse into a character set.
|
||||
* @param specials Any special buttons you want to add to the keyboard (must match hardcoded special string values)
|
||||
* @returns {*[]}
|
||||
*/
|
||||
function createCharSet(text, specials) {
|
||||
specials = specials || [];
|
||||
const mandatoryExtraKeys = specials.length;
|
||||
const preferredNumChars = [1, 2, 4, 6, 9, 12];
|
||||
const preferredNumKeys = [4, 6, 9, 12].map(num => num - mandatoryExtraKeys);
|
||||
let keyIndex = 0, charIndex = 0;
|
||||
let keySpace = preferredNumChars[charIndex] * preferredNumKeys[keyIndex];
|
||||
while (keySpace < text.length) {
|
||||
const numKeys = preferredNumKeys[keyIndex];
|
||||
const numChars = preferredNumChars[charIndex];
|
||||
const nextNumKeys = preferredNumKeys[keyIndex];
|
||||
const nextNumChars = preferredNumChars[charIndex];
|
||||
if (numChars <= numKeys) {
|
||||
charIndex++;
|
||||
} else if ((text.length / nextNumChars) < nextNumKeys) {
|
||||
charIndex++;
|
||||
} else {
|
||||
keyIndex++;
|
||||
}
|
||||
keySpace = preferredNumChars[charIndex] * preferredNumKeys[keyIndex];
|
||||
}
|
||||
const charsPerKey = preferredNumChars[charIndex];
|
||||
let charSet = [];
|
||||
for (let i = 0; i < text.length; i += charsPerKey) {
|
||||
charSet.push(text.slice(i, i + charsPerKey)
|
||||
.split(""));
|
||||
}
|
||||
charSet = charSet.concat(specials);
|
||||
return charSet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the width, height, padding (between chars) and number of characters that need to fit horizontally /
|
||||
* vertically, this function attempts to select the largest font it can that will still fit within the bounds when
|
||||
* drawing a grid of characters. Does not handle multi-letter entries well, assumes we are laying out a grid of
|
||||
* single characters.
|
||||
* @param width The total width available for letters (px)
|
||||
* @param height The total height available for letters (px)
|
||||
* @param padding The amount of space required between characters (px)
|
||||
* @param gridWidth The number of characters wide the rendering is going to be
|
||||
* @param gridHeight The number of characters high the rendering is going to be
|
||||
* @returns {{w: number, h: number, font: string}}
|
||||
*/
|
||||
function getBestFont(width, height, padding, gridWidth, gridHeight) {
|
||||
let font = "4x6";
|
||||
let w = 4;
|
||||
let h = 6;
|
||||
const charMaxWidth = width / gridWidth - padding * gridWidth;
|
||||
const charMaxHeight = height / gridHeight - padding * gridHeight;
|
||||
if (charMaxWidth >= 6 && charMaxHeight >= 8) {
|
||||
w = 6;
|
||||
h = 8;
|
||||
font = "6x8";
|
||||
}
|
||||
if (charMaxWidth >= 12 && charMaxHeight >= 16) {
|
||||
w = 12;
|
||||
h = 16;
|
||||
font = "6x8:2";
|
||||
}
|
||||
if (charMaxWidth >= 12 && charMaxHeight >= 20) {
|
||||
w = 12;
|
||||
h = 20;
|
||||
font = "12x20";
|
||||
}
|
||||
if (charMaxWidth >= 20 && charMaxHeight >= 20) {
|
||||
font = "Vector" + Math.floor(Math.min(charMaxWidth, charMaxHeight));
|
||||
const dims = g.setFont(font)
|
||||
.stringMetrics("W");
|
||||
w = dims.width
|
||||
h = dims.height;
|
||||
}
|
||||
return {w, h, font};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generate a set of key objects given an array of arrays of characters to make available for typing.
|
||||
* @param characterArrays
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
function getKeys(characterArrays) {
|
||||
if (Array.isArray(characterArrays)) {
|
||||
return Promise.all(characterArrays.map((chars, i) => generateKeyFromChars(characterArrays, i)));
|
||||
} else {
|
||||
return generateKeyFromChars(characterArrays, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a set of characters, determine whether or not this needs to be a matryoshka key, a basic key, or a special key.
|
||||
* Then generate that key. If the key is a matryoshka key, we queue up the generation of its sub-keys for later to
|
||||
* improve load times.
|
||||
* @param chars
|
||||
* @param i
|
||||
* @returns {Promise<unknown>}
|
||||
*/
|
||||
function generateKeyFromChars(chars, i) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let special;
|
||||
if (!Array.isArray(chars[i]) && chars[i].length > 1) {
|
||||
// If it's not an array we assume it's a string. Fingers crossed I guess, lol. Be nice to my functions!
|
||||
special = chars[i];
|
||||
}
|
||||
const key = getKeyByIndex(chars, i, special);
|
||||
if (!special) {
|
||||
key.chars = chars[i];
|
||||
}
|
||||
if (key.chars.length > 1) {
|
||||
key.pendingSubKeys = true;
|
||||
key.getSubKeys = () => getKeys(key.chars);
|
||||
resolve(key)
|
||||
} else {
|
||||
resolve(key);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Given a set of characters (or sets of characters) get the position and dimensions of the i'th key in that set.
|
||||
* @param charSet An array where each element represents a key on the hypothetical keyboard.
|
||||
* @param i The index of the key in the set you want to get dimensions for.
|
||||
* @param special The special property of the key - for example "del" for a key used for deleting characters.
|
||||
* @returns {{special, bord: number, pad: number, w: number, x: number, h: number, y: number, chars: *[]}}
|
||||
*/
|
||||
function getKeyByIndex(charSet, i, special) {
|
||||
// Key dimensions
|
||||
const keyboardOffsetY = 40;
|
||||
const margin = 3;
|
||||
const padding = 4;
|
||||
const border = 2;
|
||||
const gridWidth = Math.ceil(Math.sqrt(charSet.length));
|
||||
const gridHeight = Math.ceil(charSet.length / gridWidth);
|
||||
const keyWidth = Math.floor((g.getWidth()) / gridWidth) - margin;
|
||||
const keyHeight = Math.floor((g.getHeight() - keyboardOffsetY) / gridHeight) - margin;
|
||||
const gridx = i % gridWidth;
|
||||
const gridy = Math.floor(i / gridWidth) % gridWidth;
|
||||
const x = gridx * (keyWidth + margin);
|
||||
const y = gridy * (keyHeight + margin) + keyboardOffsetY;
|
||||
const w = keyWidth;
|
||||
const h = keyHeight;
|
||||
// internal Character spacing
|
||||
const numChars = charSet[i].length;
|
||||
const subGridWidth = Math.ceil(Math.sqrt(numChars));
|
||||
const subGridHeight = Math.ceil(numChars / subGridWidth);
|
||||
const bestFont = getBestFont(w - padding, h - padding, 0, subGridWidth, subGridHeight);
|
||||
const letterWidth = bestFont.w;
|
||||
const letterHeight = bestFont.h;
|
||||
const totalWidth = (subGridWidth - 1) * (w / subGridWidth) + padding + letterWidth + 1;
|
||||
const totalHeight = (subGridHeight - 1) * (h / subGridHeight) + padding + letterHeight + 1;
|
||||
const extraPadH = (w - totalWidth) / 2;
|
||||
const extraPadV = (h - totalHeight) / 2;
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
w,
|
||||
h,
|
||||
pad : padding,
|
||||
bord : border,
|
||||
chars: [],
|
||||
special,
|
||||
subGridWidth,
|
||||
subGridHeight,
|
||||
extraPadH,
|
||||
extraPadV,
|
||||
font : bestFont.font
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This is probably the most intense part of this keyboard library. If you don't do it ahead of time, it will happen
|
||||
* when you call the keyboard, and it can take up to 0.5 seconds for a full alphanumeric keyboard. Depending on what
|
||||
* is an acceptable user experience for you, and how many keys you are actually generating, you may choose to do this
|
||||
* ahead of time and pass the result to the "input" function of this library. NOTE: This function would need to be
|
||||
* called once per key set - so if you have a keyboard with a "shift" key you'd need to run it once for your base
|
||||
* keyset and once for your shift keyset.
|
||||
* @param charSets
|
||||
* @returns {Promise<unknown>}
|
||||
*/
|
||||
function generateKeyboard(charSets) {
|
||||
if (!Array.isArray(charSets)) {
|
||||
// User passed a string. We will divvy it up into a real set of subdivided characters.
|
||||
charSets = createCharSet(charSets, ["ok", "del", "shft"]);
|
||||
}
|
||||
return getKeys(charSets);
|
||||
}
|
||||
|
||||
// Default layout
|
||||
const defaultCharSet = [
|
||||
["a", "b", "c", "d", "e", "f", "g", "h", "i"],
|
||||
["j", "k", "l", "m", "n", "o", "p", "q", "r"],
|
||||
["s", "t", "u", "v", "w", "x", "y", "z", "0"],
|
||||
["1", "2", "3", "4", "5", "6", "7", "8", "9"],
|
||||
[" ", "`", "-", "=", "[", "]", "\\", ";", "'"],
|
||||
[",", ".", "/"],
|
||||
"ok",
|
||||
"shft",
|
||||
"del"
|
||||
];
|
||||
|
||||
// Default layout with shift pressed
|
||||
const defaultCharSetShift = [
|
||||
["A", "B", "C", "D", "E", "F", "G", "H", "I"],
|
||||
["J", "K", "L", "M", "N", "O", "P", "Q", "R"],
|
||||
["S", "T", "U", "V", "W", "X", "Y", "Z", ")"],
|
||||
["!", "@", "#", "$", "%", "^", "&", "*", "("],
|
||||
["~", "_", "+", "{", "}", "|", ":", "\"", "<"],
|
||||
[">", "?"],
|
||||
"ok",
|
||||
"shft",
|
||||
"del"
|
||||
];
|
||||
|
||||
/**
|
||||
* Given initial options, allow the user to type a set of characters and return their entry in a promise. If you do not
|
||||
* submit your own character set, a default alphanumeric keyboard will display.
|
||||
* @param options The object containing initial options for the keyboard.
|
||||
* @param {string} options.text The initial text to display / edit in the keyboard
|
||||
* @param {array[]|string[]} [options.keyboardMain] The primary keyboard generated with generateKeyboard()
|
||||
* @param {array[]|string[]} [options.keyboardShift] Like keyboardMain, but displayed when shift / capslock is pressed.
|
||||
* @returns {Promise<unknown>}
|
||||
*/
|
||||
function input(options) {
|
||||
options = options || {};
|
||||
let typed = options.text || "";
|
||||
let resolveFunction = () => {};
|
||||
let shift = false;
|
||||
let caps = false;
|
||||
let activeKeySet;
|
||||
|
||||
const offsetX = 0;
|
||||
const offsetY = 40;
|
||||
|
||||
E.showMessage("Loading...");
|
||||
let keyboardPromise;
|
||||
if (options.keyboardMain) {
|
||||
keyboardPromise = Promise.all([options.keyboardMain, options.keyboardShift || Promise.resolve([])]);
|
||||
} else {
|
||||
keyboardPromise = Promise.all([generateKeyboard(defaultCharSet), generateKeyboard(defaultCharSetShift)])
|
||||
}
|
||||
|
||||
let mainKeys;
|
||||
let mainKeysShift;
|
||||
|
||||
/**
|
||||
* Draw an individual keyboard key - handles special formatting and the rectangle pad, followed by the character
|
||||
* rendering.
|
||||
* @param key
|
||||
*/
|
||||
function drawKey(key) {
|
||||
let bgColor = g.theme.bg;
|
||||
if (key.special) {
|
||||
if (key.special === "ok") bgColor = "#0F0";
|
||||
if (key.special === "cncl") bgColor = "#F00";
|
||||
if (key.special === "del") bgColor = g.theme.bg2;
|
||||
if (key.special === "spc") bgColor = g.theme.bg2;
|
||||
if (key.special === "shft") {
|
||||
bgColor = shift ? g.theme.bgH : g.theme.bg2;
|
||||
}
|
||||
if (key.special === "caps") {
|
||||
bgColor = caps ? g.theme.bgH : g.theme.bg2;
|
||||
}
|
||||
g.setColor(bgColor)
|
||||
.fillRect({x: key.x, y: key.y, w: key.w, h: key.h});
|
||||
}
|
||||
g.setColor(g.theme.fg)
|
||||
.drawRect({x: key.x, y: key.y, w: key.w, h: key.h});
|
||||
drawChars(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw the characters for a given key - this handles the layout of all characters needed for the key, whether the
|
||||
* key has 12 characters, 1, or if it represents a special key.
|
||||
* @param key
|
||||
*/
|
||||
function drawChars(key) {
|
||||
const numChars = key.chars.length;
|
||||
if (key.special) {
|
||||
g.setColor(g.theme.fg)
|
||||
.setFont("12x20")
|
||||
.setFontAlign(-1, -1)
|
||||
.drawString(key.special, key.x + key.w / 2 - g.stringWidth(key.special) / 2, key.y + key.h / 2 - 10, false);
|
||||
} else {
|
||||
g.setColor(g.theme.fg)
|
||||
.setFont(key.font)
|
||||
.setFontAlign(-1, -1);
|
||||
for (let i = 0; i < numChars; i++) {
|
||||
const gridX = i % key.subGridWidth;
|
||||
const gridY = Math.floor(i / key.subGridWidth) % key.subGridWidth;
|
||||
const charOffsetX = gridX * (key.w / key.subGridWidth);
|
||||
const charOffsetY = gridY * (key.h / key.subGridHeight);
|
||||
const posX = key.x + key.pad + charOffsetX + key.extraPadH;
|
||||
const posY = key.y + key.pad + charOffsetY + key.extraPadV;
|
||||
g.drawString(key.chars[i], posX, posY, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the key set corresponding to the indicated shift state. Allows easy switching between capital letters and
|
||||
* lower case by just switching the boolean passed here.
|
||||
* @param shift
|
||||
* @returns {*[]}
|
||||
*/
|
||||
function getMainKeySet(shift) {
|
||||
return shift ? mainKeysShift : mainKeys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw all the given keys on the screen.
|
||||
* @param keys
|
||||
*/
|
||||
function drawKeys(keys) {
|
||||
keys.forEach(key => {
|
||||
drawKey(key);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw the text that the user has typed so far, includes a cursor and automatic truncation when the string is too
|
||||
* long.
|
||||
* @param text
|
||||
* @param cursorChar
|
||||
*/
|
||||
function drawTyped(text, cursorChar) {
|
||||
let visibleText = text;
|
||||
let ellipsis = false;
|
||||
const maxWidth = 176 - 40;
|
||||
while (g.setFont("12x20")
|
||||
.stringWidth(visibleText) > maxWidth) {
|
||||
ellipsis = true;
|
||||
visibleText = visibleText.slice(1);
|
||||
}
|
||||
if (ellipsis) {
|
||||
visibleText = "..." + visibleText;
|
||||
}
|
||||
g.setColor(g.theme.bg2)
|
||||
.fillRect(5, 5, 171, 30);
|
||||
g.setColor(g.theme.fg2)
|
||||
.setFont("12x20")
|
||||
.drawString(visibleText + cursorChar, 15, 10, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the space on the screen that the keyboard occupies (not the text the user has written).
|
||||
*/
|
||||
function clearKeySpace() {
|
||||
g.setColor(g.theme.bg)
|
||||
.fillRect(offsetX, offsetY, 176, 176);
|
||||
}
|
||||
|
||||
/**
|
||||
* Based on a touch event, determine which key was pressed by the user.
|
||||
* @param touchEvent
|
||||
* @param keys
|
||||
* @returns {*}
|
||||
*/
|
||||
function getTouchedKey(touchEvent, keys) {
|
||||
return keys.find((key) => {
|
||||
let relX = touchEvent.x - key.x;
|
||||
let relY = touchEvent.y - key.y;
|
||||
return relX > 0 && relX < key.w && relY > 0 && relY < key.h;
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* On a touch event, determine whether a key is touched and take appropriate action if it is.
|
||||
* @param button
|
||||
* @param touchEvent
|
||||
*/
|
||||
function keyTouch(button, touchEvent) {
|
||||
const pressedKey = getTouchedKey(touchEvent, activeKeySet);
|
||||
if (pressedKey == null) {
|
||||
// User tapped empty space.
|
||||
swapKeySet(getMainKeySet(shift !== caps));
|
||||
return;
|
||||
}
|
||||
if (pressedKey.pendingSubKeys) {
|
||||
// We have to generate the subkeys for this key still, but we decided to wait until we needed it!
|
||||
pressedKey.pendingSubKeys = false;
|
||||
pressedKey.getSubKeys()
|
||||
.then(subkeys => {
|
||||
pressedKey.subKeys = subkeys;
|
||||
keyTouch(undefined, touchEvent);
|
||||
})
|
||||
return;
|
||||
}
|
||||
// Haptic feedback
|
||||
Bangle.buzz(25, 1);
|
||||
if (pressedKey.subKeys) {
|
||||
// Hold press for "shift!"
|
||||
if (touchEvent.type > 1) {
|
||||
shift = !shift;
|
||||
swapKeySet(getMainKeySet(shift !== caps));
|
||||
} else {
|
||||
swapKeySet(pressedKey.subKeys);
|
||||
}
|
||||
} else {
|
||||
if (pressedKey.special) {
|
||||
evaluateSpecialFunctions(pressedKey);
|
||||
} else {
|
||||
typed = typed + pressedKey.chars;
|
||||
shift = false;
|
||||
drawTyped(typed, "");
|
||||
swapKeySet(getMainKeySet(shift !== caps));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manage setting, generating, and rendering new keys when a key set is changed.
|
||||
* @param newKeys
|
||||
*/
|
||||
function swapKeySet(newKeys) {
|
||||
activeKeySet = newKeys;
|
||||
clearKeySpace();
|
||||
drawKeys(activeKeySet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the key contains any of the special strings that have their own special behaviour when pressed.
|
||||
* @param key
|
||||
*/
|
||||
function evaluateSpecialFunctions(key) {
|
||||
switch (key.special) {
|
||||
case "ok":
|
||||
setTimeout(() => resolveFunction(typed), 50);
|
||||
break;
|
||||
case "del":
|
||||
typed = typed.slice(0, -1);
|
||||
drawTyped(typed, "");
|
||||
break;
|
||||
case "shft":
|
||||
shift = !shift;
|
||||
swapKeySet(getMainKeySet(shift !== caps));
|
||||
break;
|
||||
case "caps":
|
||||
caps = !caps;
|
||||
swapKeySet(getMainKeySet(shift !== caps));
|
||||
break;
|
||||
case "cncl":
|
||||
setTimeout(() => resolveFunction(), 50);
|
||||
break;
|
||||
case "spc":
|
||||
typed = typed + " ";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let isCursorVisible = true;
|
||||
|
||||
const blinkInterval = setInterval(() => {
|
||||
if (!activeKeySet) return;
|
||||
isCursorVisible = !isCursorVisible;
|
||||
if (isCursorVisible) {
|
||||
drawTyped(typed, "_");
|
||||
} else {
|
||||
drawTyped(typed, "");
|
||||
}
|
||||
}, 200);
|
||||
|
||||
|
||||
/**
|
||||
* We return a promise but the resolve function is assigned to a variable in the higher function scope. That allows
|
||||
* us to return the promise and resolve it after we are done typing without having to return the entire scope of the
|
||||
* application within the promise.
|
||||
*/
|
||||
return new Promise((resolve, reject) => {
|
||||
g.clear(true);
|
||||
resolveFunction = resolve;
|
||||
keyboardPromise.then((result) => {
|
||||
mainKeys = result[0];
|
||||
mainKeysShift = result[1];
|
||||
swapKeySet(getMainKeySet(shift !== caps));
|
||||
Bangle.setUI({
|
||||
mode: "custom", touch: keyTouch
|
||||
});
|
||||
Bangle.setLocked(false);
|
||||
})
|
||||
}).then((result) => {
|
||||
g.clearRect(Bangle.appRect);
|
||||
clearInterval(blinkInterval);
|
||||
Bangle.setUI();
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
exports.input = input;
|
||||
exports.generateKeyboard = generateKeyboard;
|
||||
exports.defaultCharSet = defaultCharSet;
|
||||
exports.defaultCharSetShift = defaultCharSetShift;
|
||||
exports.createCharSet = createCharSet;
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
{ "id": "kbmatry",
|
||||
"name": "Matryoshka Keyboard",
|
||||
"version":"1.06",
|
||||
"description": "A library for text input via onscreen keyboard. Easily enter characters with nested keyboards.",
|
||||
"icon": "icon.png",
|
||||
"type":"textinput",
|
||||
"tags": "keyboard",
|
||||
"supports" : ["BANGLEJS2"],
|
||||
"screenshots": [{"url":"screenshot.png"},{"url":"screenshot6.png"},{"url":"screenshot2.png"},{"url":"screenshot3.png"},{"url":"screenshot4.png"},{"url":"screenshot5.png"},{"url": "help.png"}],
|
||||
"readme": "README.md",
|
||||
"storage": [
|
||||
{"name":"textinput","url":"lib.js"}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
|
@ -91,4 +91,6 @@
|
|||
0.66: Updated Navigation handling to work with new Gadgetbridge release
|
||||
0.67: Support for 'Ignore' for messages from Gadgetbridge
|
||||
Message view is now taller, and we use swipe left/right to dismiss messages rather than buttons
|
||||
0.68: More navigation icons (for roundabouts)
|
||||
0.68: More navigation icons (for roundabouts)
|
||||
0.69: More navigation icons (keep/uturn left/right)
|
||||
Nav messages with '/' now get split on newlines
|
||||
|
|
@ -17,6 +17,7 @@ require("messages").pushMessage({"t":"add","id":1575479849,"src":"Skype","title"
|
|||
// maps
|
||||
GB({t:"nav",src:"maps",title:"Navigation",instr:"High St towards Tollgate Rd",distance:966,action:"continue",eta:"08:39"})
|
||||
GB({t:"nav",src:"maps",title:"Navigation",instr:"High St",distance:12345,action:"left_slight",eta:"08:39"})
|
||||
GB({t:"nav",src:"maps",title:"Navigation",instr:"Main St / I-29 ALT / Centerpoint Dr",distance:12345,action:"left_slight",eta:"08:39"})
|
||||
// call
|
||||
require("messages").pushMessage({"t":"add","id":"call","src":"Phone","title":"Bob","body":"12421312",positive:true,negative:true})
|
||||
*/
|
||||
|
|
@ -84,12 +85,13 @@ function showMapMessage(msg) {
|
|||
if (msg.distance!==undefined)
|
||||
distance = require("locale").distance(msg.distance);
|
||||
if (msg.instr) {
|
||||
if (msg.instr.includes("towards") || msg.instr.includes("toward")) {
|
||||
m = msg.instr.split(/towards|toward/);
|
||||
var instr = msg.instr.replace(/\s*\/\s*/g," \/\n"); // convert slashes to newlines
|
||||
if (instr.includes("towards") || instr.includes("toward")) {
|
||||
m = instr.split(/towards|toward/);
|
||||
target = m[0].trim();
|
||||
street = m[1].trim();
|
||||
}else
|
||||
target = msg.instr;
|
||||
target = instr;
|
||||
}
|
||||
switch (msg.action) {
|
||||
case "continue": img = "EBgBAIABwAPgD/Af+D/8f/773/PPY8cDwAPAA8ADwAPAA8AAAAPAA8ADwAAAA8ADwAPA";break;
|
||||
|
|
@ -97,10 +99,15 @@ function showMapMessage(msg) {
|
|||
case "right": img = "GhcBAABgAAA8AAAPgAAB8AAAPgAAB8D///j///9///+/AAPPAAHjgAD44AB8OAA+DgAPA4ABAOAAADgAAA4AAAOAAADgAAA4AAAOAAAA";break;
|
||||
case "left_slight": img = "ERgB//B/+D/8H4AP4Af4A74Bz4Dj4HD4OD4cD4AD4ADwADwADgAHgAPAAOAAcAA4ABwADgAH";break;
|
||||
case "right_slight": img = "ERgBB/+D/8H/4APwA/gD/APuA+cD44Phw+Dj4HPgAeAB4ADgAPAAeAA4ABwADgAHAAOAAcAA";break;
|
||||
case "keep_left": img = "ERmBAACAAOAB+AD+AP+B/+H3+PO+8c8w4wBwADgAHgAPAAfAAfAAfAAfAAeAAeAAcAA8AA4ABwADgA==";break;
|
||||
case "keep_right": img = "ERmBAACAAOAA/AD+AP+A//D/fPueeceY4YBwADgAPAAeAB8AHwAfAB8ADwAPAAcAB4ADgAHAAOAAAA==";break;
|
||||
case "uturn_left": img = "GRiBAAAH4AAP/AAP/wAPj8APAfAPAHgHgB4DgA8BwAOA4AHAcADsOMB/HPA7zvgd9/gOf/gHH/gDh/gBwfgA4DgAcBgAOAAAHAAADgAABw==";break;
|
||||
case "uturn_right": img = "GRiBAAPwAAf+AAf/gAfj4AfAeAPAHgPADwHgA4DgAcBwAOA4AHAcBjhuB5x/A+57gP99wD/84A/8cAP8OAD8HAA4DgAMBwAAA4AAAcAAAA==";break;
|
||||
case "finish": img = "HhsBAcAAAD/AAAH/wAAPB4AAeA4AAcAcAAYIcAA4cMAA48MAA4cMAAYAcAAcAcAAcA4AAOA4AAOBxjwHBzjwHjj/4Dnn/4B3P/4B+Pj4A8fj8Acfj8AI//8AA//+AA/j+AB/j+AB/j/A";break;
|
||||
case "roundabout_left": img = "HhcCAAAAAAAAAAAADwAAAEAAAAP8AAGqkAAA/8ABqqqAAD/wAGqqqgAP/AAKpAakA/8AAakAGoD/////QACpP/////gABpP/////gABpD/////wACpA/8AAv4AGoAP/AAf/QakAD/wAL//qgAA/8AC//qAAAP8AAf/kAAADwAAB/AAAAAAAAB/AAAAAAAAB/AAAAAAAAB/AAAAAAAAB/AAAAAAAAB/AAAAAAAAB/AAA=";break;
|
||||
case "roundabout_right": img = "HhcCAAAAAAAAAAAAZAAADwAAAf/9AAP8AAB///gAP/AAH///4AD/wAP/Rv9AA/8Af8AH/AAP/AvwAD/////w/wAC/////8/wAC/////8vwAB/////wf8AGpAAP/AP/QaoAA/8AH//qkAD/wAB//6QAP/AAAf/0AAP8AAAA/QAADwAAAA/QAAAAAAAA/QAAAAAAAA/QAAAAAAAA/QAAAAAAAA/QAAAAAAAA/QAAAAAAA=";break;
|
||||
case "roundabout_straight": img = "EhwCAABQAAAAH0AAAAf9AAAB//QAAH//0AAf//9AB////QH/v+/0P+P8v8L4P8L4CQP8BgAC/+QAAP/+kAA//+pAC/4GqQD/AAqgH+AAagL8AAKkL8AAKkH+AAagD/AAqgC/4GqQA//+pAAP/+kAAC/+QAAAP8AAAAP8AAAAP8AA";break;
|
||||
case "roundabout_left": img = "HBaCAAADwAAAAAAAD/AAAVUAAD/wABVVUAD/wABVVVQD/wAAVABUD/wAAVAAFT/////wABX/////8AAF//////AABT/////wABUP/AAD/AAVA/8AA/8AVAD/wAD//VQAP/AAP/1QAA/wAA/9AAADwAAD/AAAAAAAA/wAAAAAAAP8AAAAAAAD/AAAAAAAA/wAAAAAAAP8AAAAAAAD/AA=";break;
|
||||
case "roundabout_right": img = "HRaCAAAAAAAA8AAAP/8AAP8AAD///AA/8AA////AA/8AP/A/8AA/8A/wAP8AA/8P8AA/////8/wAD///////AAD//////8AAP////8P8ABUAAP/A/8AVQAD/wA//1UAA/8AA//VAAP/AAA/9AAA/wAAAPwAAA8AAAA/AAAAAAAAD8AAAAAAAAPwAAAAAAAA/AAAAAAAAD8AAAAAAAAPwAAAAAAA=";break;
|
||||
case "roundabout_straight": img = "EBuCAAADwAAAD/AAAD/8AAD//wAD///AD///8D/P8/z/D/D//A/wPzAP8AwA//UAA//1QA//9VA/8AFUP8AAVD8AAFQ/AABUPwAAVD8AAFQ/wABUP/ABVA//9VAD//VAAP/1AAAP8AAAD/AAAA/wAA==";break;
|
||||
case "roundabout_uturn": img = "ICCBAAAAAAAAAAAAAAAAAAAP4AAAH/AAAD/4AAB4fAAA8DwAAPAcAADgHgAA4B4AAPAcAADwPAAAeHwAADz4AAAc8AAABPAAAADwAAAY8YAAPPPAAD73gAAf/4AAD/8AABf8AAAb+AAAHfAAABzwAAAcYAAAAAAAAAAAAAAAAAAAAAAA";break;
|
||||
}
|
||||
//FIXME: what about countries where we drive on the right? How will we know to flip the icons?
|
||||
|
||||
|
|
@ -208,7 +215,7 @@ function showMessageScroller(msg) {
|
|||
var bodyFont = fontBig;
|
||||
g.setFont(bodyFont);
|
||||
var lines = [];
|
||||
if (msg.title) lines = g.wrapString(msg.title, g.getWidth()-10)
|
||||
if (msg.title) lines = g.wrapString(msg.title, g.getWidth()-10);
|
||||
var titleCnt = lines.length;
|
||||
if (titleCnt) lines.push(""); // add blank line after title
|
||||
lines = lines.concat(g.wrapString(msg.body, g.getWidth()-10),["",/*LANG*/"< Back"]);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"id": "messagegui",
|
||||
"name": "Message UI",
|
||||
"shortName": "Messages",
|
||||
"version": "0.68",
|
||||
"version": "0.69",
|
||||
"description": "Default app to display notifications from iOS and Gadgetbridge/Android",
|
||||
"icon": "app.png",
|
||||
"type": "app",
|
||||
|
|
|
|||
|
|
@ -32,3 +32,4 @@
|
|||
0.24: Can now specify `setRecording(true, {force:...` to not show a menu
|
||||
0.25: Widget now has `isRecording()` for retrieving recording status.
|
||||
0.26: Now record filename based on date
|
||||
0.27: Fix first ever recorded filename being log0 (now all are dated)
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
"id": "recorder",
|
||||
"name": "Recorder",
|
||||
"shortName": "Recorder",
|
||||
"version": "0.26",
|
||||
"version": "0.27",
|
||||
"description": "Record GPS position, heart rate and more in the background, then download to your PC.",
|
||||
"icon": "app.png",
|
||||
"tags": "tool,outdoors,gps,widget",
|
||||
|
|
|
|||
|
|
@ -240,8 +240,9 @@
|
|||
var settings = loadSettings();
|
||||
options = options||{};
|
||||
if (isOn && !settings.recording) {
|
||||
var date=(new Date()).toISOString().substr(0,10).replace(/-/g,""), trackNo=10;
|
||||
if (!settings.file) { // if no filename set
|
||||
settings.file = "recorder.log0.csv";
|
||||
settings.file = "recorder.log" + date + trackNo.toString(36) + ".csv";
|
||||
} else if (require("Storage").list(settings.file).length){ // if file exists
|
||||
if (!options.force) { // if not forced, ask the question
|
||||
g.reset(); // work around bug in 2v17 and earlier where bg color wasn't reset
|
||||
|
|
@ -263,7 +264,6 @@
|
|||
require("Storage").open(settings.file,"r").erase();
|
||||
} else if (options.force=="new") {
|
||||
// new file - use the current date
|
||||
var date=(new Date()).toISOString().substr(0,10).replace(/-/g,""), trackNo=10;
|
||||
var newFileName;
|
||||
do { // while a file exists, add one to the letter after the date
|
||||
newFileName = "recorder.log" + date + trackNo.toString(36) + ".csv";
|
||||
|
|
|
|||
|
|
@ -86,31 +86,74 @@ function eventToAlarm(event, offsetMs) {
|
|||
}
|
||||
|
||||
function upload() {
|
||||
// kick off all the (active) timers
|
||||
const now = new Date();
|
||||
const currentTime = now.getHours()*3600000
|
||||
+ now.getMinutes()*60000
|
||||
+ now.getSeconds()*1000;
|
||||
|
||||
for (const alarm of alarms)
|
||||
if (alarm.timer != undefined && alarm.on)
|
||||
alarm.t = currentTime + alarm.timer;
|
||||
|
||||
Util.showModal("Saving...");
|
||||
Util.writeStorage("sched.json", JSON.stringify(alarms), () => {
|
||||
location.reload(); // reload so we see current data
|
||||
Puck.write(`\x10require("sched").reload();\n`, () => {
|
||||
location.reload(); // reload so we see current data
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function renderAlarm(alarm, exists) {
|
||||
const localDate = dateFromAlarm(alarm);
|
||||
const localDate = alarm.date ? dateFromAlarm(alarm) : null;
|
||||
|
||||
const tr = document.createElement('tr');
|
||||
tr.classList.add('event-row');
|
||||
tr.dataset.uid = alarm.id;
|
||||
const tdTime = document.createElement('td');
|
||||
tr.appendChild(tdTime);
|
||||
const tdType = document.createElement('td');
|
||||
tdType.type = "text";
|
||||
tdType.classList.add('event-summary');
|
||||
tr.appendChild(tdType);
|
||||
const inputTime = document.createElement('input');
|
||||
inputTime.type = "datetime-local";
|
||||
if (localDate) {
|
||||
tdType.textContent = "Event";
|
||||
inputTime.type = "datetime-local";
|
||||
inputTime.value = localDate.toISOString().slice(0,16);
|
||||
inputTime.onchange = (e => {
|
||||
const date = new Date(inputTime.value);
|
||||
alarm.t = dateToMsSinceMidnight(date);
|
||||
alarm.date = formatDate(date);
|
||||
});
|
||||
} else {
|
||||
const [hours, mins, secs] = msToHMS(alarm.timer || alarm.t);
|
||||
|
||||
inputTime.type = "time";
|
||||
inputTime.step = 1; // display seconds
|
||||
inputTime.value = `${hours}:${mins}:${secs}`;
|
||||
|
||||
if (alarm.timer) {
|
||||
tdType.textContent = "Timer";
|
||||
inputTime.onchange = e => {
|
||||
alarm.timer = hmsToMs(inputTime.value);
|
||||
// alarm.t is set on upload
|
||||
};
|
||||
} else {
|
||||
tdType.textContent = "Alarm";
|
||||
inputTime.onchange = e => {
|
||||
alarm.t = hmsToMs(inputTime.value);
|
||||
};
|
||||
}
|
||||
}
|
||||
if (!exists) {
|
||||
const asterisk = document.createElement('sup');
|
||||
asterisk.textContent = '*';
|
||||
tdType.appendChild(asterisk);
|
||||
}
|
||||
inputTime.classList.add('event-date');
|
||||
inputTime.classList.add('form-input');
|
||||
inputTime.dataset.uid = alarm.id;
|
||||
inputTime.value = localDate.toISOString().slice(0,16);
|
||||
inputTime.onchange = (e => {
|
||||
const date = new Date(inputTime.value);
|
||||
alarm.t = dateToMsSinceMidnight(date);
|
||||
alarm.date = formatDate(date);
|
||||
});
|
||||
const tdTime = document.createElement('td');
|
||||
tr.appendChild(tdTime);
|
||||
tdTime.appendChild(inputTime);
|
||||
|
||||
const tdSummary = document.createElement('td');
|
||||
|
|
@ -130,13 +173,31 @@ function renderAlarm(alarm, exists) {
|
|||
tdSummary.appendChild(inputSummary);
|
||||
inputSummary.onchange();
|
||||
|
||||
const tdOptions = document.createElement('td');
|
||||
tr.appendChild(tdOptions);
|
||||
|
||||
const onOffCheck = document.createElement('input');
|
||||
onOffCheck.type = 'checkbox';
|
||||
onOffCheck.checked = alarm.on;
|
||||
onOffCheck.onchange = e => {
|
||||
alarm.on = !alarm.on;
|
||||
if (alarm.on) delete alarm.last;
|
||||
};
|
||||
const onOffIcon = document.createElement('i');
|
||||
onOffIcon.classList.add('form-icon');
|
||||
const onOff = document.createElement('label');
|
||||
onOff.classList.add('form-switch');
|
||||
onOff.appendChild(onOffCheck);
|
||||
onOff.appendChild(onOffIcon);
|
||||
tdOptions.appendChild(onOff);
|
||||
|
||||
const tdInfo = document.createElement('td');
|
||||
tr.appendChild(tdInfo);
|
||||
|
||||
const buttonDelete = document.createElement('button');
|
||||
buttonDelete.classList.add('btn');
|
||||
buttonDelete.classList.add('btn-action');
|
||||
tdInfo.prepend(buttonDelete);
|
||||
tdInfo.appendChild(buttonDelete);
|
||||
const iconDelete = document.createElement('i');
|
||||
iconDelete.classList.add('icon');
|
||||
iconDelete.classList.add('icon-delete');
|
||||
|
|
@ -150,12 +211,53 @@ function renderAlarm(alarm, exists) {
|
|||
document.getElementById('upload').disabled = false;
|
||||
}
|
||||
|
||||
function msToHMS(ms) {
|
||||
let secs = Math.floor(ms / 1000) % 60;
|
||||
let mins = Math.floor(ms / 1000 / 60) % 60;
|
||||
let hours = Math.floor(ms / 1000 / 60 / 60);
|
||||
if (secs < 10) secs = "0" + secs;
|
||||
if (mins < 10) mins = "0" + mins;
|
||||
if (hours < 10) hours = "0" + hours;
|
||||
return [hours, mins, secs];
|
||||
}
|
||||
|
||||
function hmsToMs(hms) {
|
||||
let [hours, mins, secs] = hms.split(":");
|
||||
hours = Number(hours);
|
||||
mins = Number(mins);
|
||||
secs = Number(secs);
|
||||
return ((hours * 60 + mins) * 60 + secs) * 1000;
|
||||
}
|
||||
|
||||
function addEvent() {
|
||||
const event = getAlarmDefaults();
|
||||
renderAlarm(event);
|
||||
alarms.push(event);
|
||||
}
|
||||
|
||||
function addAlarm() {
|
||||
const alarm = getAlarmDefaults();
|
||||
delete alarm.date;
|
||||
renderAlarm(alarm);
|
||||
alarms.push(alarm);
|
||||
}
|
||||
|
||||
function addTimer() {
|
||||
const alarmDefaults = getAlarmDefaults();
|
||||
const timer = {
|
||||
timer: hmsToMs("00:00:30"),
|
||||
t: 0,
|
||||
on: alarmDefaults.on,
|
||||
dow: alarmDefaults.dow,
|
||||
last: alarmDefaults.last,
|
||||
rp: alarmDefaults.rp,
|
||||
vibrate: alarmDefaults.vibrate,
|
||||
as: alarmDefaults.as,
|
||||
};;
|
||||
renderAlarm(timer);
|
||||
alarms.push(timer);
|
||||
}
|
||||
|
||||
function getData() {
|
||||
Util.showModal("Loading...");
|
||||
Util.readStorage('sched.json',data=>{
|
||||
|
|
@ -164,10 +266,19 @@ function getData() {
|
|||
Util.readStorage('sched.settings.json',data=>{
|
||||
schedSettings = JSON.parse(data || "{}") || {};
|
||||
Util.hideModal();
|
||||
alarms.sort((a, b) => {
|
||||
let x;
|
||||
|
||||
x = !!b.date - !!a.date;
|
||||
if(x) return x;
|
||||
|
||||
x = !!a.timer - !!b.timer;
|
||||
if(x) return x;
|
||||
|
||||
return a.t - b.t;
|
||||
});
|
||||
alarms.forEach(alarm => {
|
||||
if (alarm.date) {
|
||||
renderAlarm(alarm, true);
|
||||
}
|
||||
renderAlarm(alarm, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -183,16 +294,27 @@ function onInit() {
|
|||
<h4>Manage dated events</h4>
|
||||
|
||||
<div class="float-right">
|
||||
<button class="btn" onclick="addEvent()">
|
||||
<i class="icon icon-plus"></i>
|
||||
Event
|
||||
</button>
|
||||
<button class="btn" onclick="addAlarm()">
|
||||
<i class="icon icon-plus"></i>
|
||||
Alarm
|
||||
</button>
|
||||
<button class="btn" onclick="addTimer()">
|
||||
<i class="icon icon-plus"></i>
|
||||
Timer
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Type</th>
|
||||
<th>Date/Time</th>
|
||||
<th>Summary</th>
|
||||
<th>On?</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
|
|
|||
|
|
@ -2,3 +2,5 @@
|
|||
0.02: Less time used during boot if disabled
|
||||
0.03: Fixed some test data
|
||||
0.04: Correct type of time attribute in gps to Date
|
||||
0.05: Fix gps emulation interpolation
|
||||
Add setting for log output
|
||||
|
|
|
|||
|
|
@ -5,40 +5,56 @@ This allows to simulate sensor behaviour for development purposes
|
|||
|
||||
## Per Sensor settings:
|
||||
|
||||
enabled:
|
||||
true or false
|
||||
mode:
|
||||
emulate: Completely craft events for this sensor
|
||||
modify: Take existing events from real sensor and modify their data
|
||||
name:
|
||||
name of the emulation or modification mode
|
||||
power:
|
||||
emulate: Simulate Bangle._PWR changes, but do not call real power function
|
||||
nop: Do nothing, ignore all power calls for this sensor but return true
|
||||
passthrough: Just pass all power calls unmodified
|
||||
on: Do not allow switching the sensor off, all calls are switching the real sensor on
|
||||
Enabled:
|
||||
* **true**
|
||||
* **false**
|
||||
|
||||
Mode:
|
||||
* **emulate**: Completely craft events for this sensor
|
||||
* **modify**: Take existing events from real sensor and modify their data
|
||||
|
||||
Name:
|
||||
* name of the emulation or modification mode
|
||||
|
||||
Power:
|
||||
* **emulate**: Simulate Bangle._PWR changes, but do not call real power function
|
||||
* **nop**: Do nothing, ignore all power calls for this sensor but return true
|
||||
* **passthrough**: Just pass all power calls unmodified
|
||||
* **on**: Do not allow switching the sensor off, all calls are switching the real sensor on
|
||||
|
||||
### HRM
|
||||
|
||||
Modes: modify, emulate
|
||||
Modes:
|
||||
* **modify**: Modify the original events from this sensor
|
||||
* **emulate**: Create events simulating sensor activity
|
||||
|
||||
Modification:
|
||||
bpmtrippled: Multiply the bpm value of the original HRM values with 3
|
||||
* **bpmtrippled**: Multiply the bpm value of the original HRM values with 3
|
||||
|
||||
Emulation:
|
||||
sin: Calculate bpm changes by using sin
|
||||
* **sin**: Calculate bpm changes by using sin
|
||||
|
||||
### GPS
|
||||
|
||||
Modes: emulate
|
||||
Modes:
|
||||
* **emulate**
|
||||
|
||||
Emulation:
|
||||
staticfix: static complete fix with all values
|
||||
route: A square route starting in the SW corner and moving SW->NW->NO->SW...
|
||||
routeFuzzy: Roughly the same square as route, but with 100m seqments with some variaton in course
|
||||
nofix: All values NaN but time,sattelites,fix and fix == 0
|
||||
changingfix: A fix with randomly changing values
|
||||
* **staticfix**: static complete fix with all values
|
||||
* **route**: A square route starting in the SW corner and moving SW->NW->NO->SW... [Download as gpx](square.gpx)
|
||||
* **routeFuzzy**: Roughly the same square as route, but with 100m seqments with some variaton in course [Download as gpx](squareFuzzy.gpx)
|
||||
* **nofix**: All values NaN but time,sattelites,fix and fix == 0
|
||||
* **changingfix**: A fix with randomly changing values
|
||||
|
||||
### Compass
|
||||
|
||||
Modes: emulate
|
||||
Modes:
|
||||
* **emulate**
|
||||
|
||||
Emulation:
|
||||
static: All values but heading are 1, heading == 0
|
||||
rotate: All values but heading are 1, heading rotates 360°
|
||||
* **static**: All values but heading are 1, heading == 0
|
||||
* **rotate**: All values but heading are 1, heading rotates 360°
|
||||
|
||||
# Creator
|
||||
|
||||
[halemmerich](https://github.com/halemmerich)
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"enabled": false,
|
||||
"log": false,
|
||||
"mag": {
|
||||
"enabled": false,
|
||||
"mode": "emulate",
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ exports.enable = () => {
|
|||
);
|
||||
|
||||
let log = function(text, param) {
|
||||
if (!settings.log) return;
|
||||
let logline = new Date().toISOString() + " - " + "Sensortools - " + text;
|
||||
if (param) logline += ": " + JSON.stringify(param);
|
||||
print(logline);
|
||||
|
|
@ -138,63 +139,63 @@ exports.enable = () => {
|
|||
|
||||
function interpolate(a,b,progress){
|
||||
return {
|
||||
lat: a.lat * progress + b.lat * (1-progress),
|
||||
lon: a.lon * progress + b.lon * (1-progress),
|
||||
ele: a.ele * progress + b.ele * (1-progress)
|
||||
lat: b.lat * progress + a.lat * (1-progress),
|
||||
lon: b.lon * progress + a.lon * (1-progress),
|
||||
alt: b.alt * progress + a.alt * (1-progress)
|
||||
}
|
||||
}
|
||||
|
||||
function getSquareRoute(){
|
||||
return [
|
||||
{lat:"47.2577411",lon:"11.9927442",ele:2273},
|
||||
{lat:"47.266761",lon:"11.9926673",ele:2166},
|
||||
{lat:"47.2667605",lon:"12.0059511",ele:2245},
|
||||
{lat:"47.2577516",lon:"12.0059925",ele:1994}
|
||||
{lat:47.2577411,lon:11.9927442,alt:2273},
|
||||
{lat:47.266761,lon:11.9926673,alt:2166},
|
||||
{lat:47.2667605,lon:12.0059511,alt:2245},
|
||||
{lat:47.2577516,lon:12.0059925,alt:1994}
|
||||
];
|
||||
}
|
||||
function getSquareRouteFuzzy(){
|
||||
return [
|
||||
{lat:"47.2578455",lon:"11.9929891",ele:2265},
|
||||
{lat:"47.258592",lon:"11.9923341",ele:2256},
|
||||
{lat:"47.2594506",lon:"11.9927412",ele:2230},
|
||||
{lat:"47.2603323",lon:"11.9924949",ele:2219},
|
||||
{lat:"47.2612056",lon:"11.9928175",ele:2199},
|
||||
{lat:"47.2621002",lon:"11.9929817",ele:2182},
|
||||
{lat:"47.2629025",lon:"11.9923915",ele:2189},
|
||||
{lat:"47.2637828",lon:"11.9926486",ele:2180},
|
||||
{lat:"47.2646733",lon:"11.9928167",ele:2191},
|
||||
{lat:"47.2655617",lon:"11.9930357",ele:2185},
|
||||
{lat:"47.2662862",lon:"11.992252",ele:2186},
|
||||
{lat:"47.2669305",lon:"11.993173",ele:2166},
|
||||
{lat:"47.266666",lon:"11.9944419",ele:2171},
|
||||
{lat:"47.2667579",lon:"11.99576",ele:2194},
|
||||
{lat:"47.2669409",lon:"11.9970579",ele:2207},
|
||||
{lat:"47.2666562",lon:"11.9983128",ele:2212},
|
||||
{lat:"47.2666027",lon:"11.9996335",ele:2262},
|
||||
{lat:"47.2667245",lon:"12.0009395",ele:2278},
|
||||
{lat:"47.2668457",lon:"12.002256",ele:2297},
|
||||
{lat:"47.2666126",lon:"12.0035373",ele:2303},
|
||||
{lat:"47.2664554",lon:"12.004841",ele:2251},
|
||||
{lat:"47.2669461",lon:"12.005948",ele:2245},
|
||||
{lat:"47.2660877",lon:"12.006323",ele:2195},
|
||||
{lat:"47.2652729",lon:"12.0057552",ele:2163},
|
||||
{lat:"47.2643926",lon:"12.0060123",ele:2131},
|
||||
{lat:"47.2634978",lon:"12.0058302",ele:2095},
|
||||
{lat:"47.2626129",lon:"12.0060759",ele:2066},
|
||||
{lat:"47.2617325",lon:"12.0058188",ele:2037},
|
||||
{lat:"47.2608668",lon:"12.0061784",ele:1993},
|
||||
{lat:"47.2600155",lon:"12.0057392",ele:1967},
|
||||
{lat:"47.2591203",lon:"12.0058233",ele:1949},
|
||||
{lat:"47.2582307",lon:"12.0059718",ele:1972},
|
||||
{lat:"47.2578014",lon:"12.004804",ele:2011},
|
||||
{lat:"47.2577232",lon:"12.0034834",ele:2044},
|
||||
{lat:"47.257745",lon:"12.0021656",ele:2061},
|
||||
{lat:"47.2578682",lon:"12.0008597",ele:2065},
|
||||
{lat:"47.2577082",lon:"11.9995526",ele:2071},
|
||||
{lat:"47.2575917",lon:"11.9982348",ele:2102},
|
||||
{lat:"47.2577401",lon:"11.996924",ele:2147},
|
||||
{lat:"47.257715",lon:"11.9956061",ele:2197},
|
||||
{lat:"47.2578996",lon:"11.9943081",ele:2228}
|
||||
{lat:47.2578455,lon:11.9929891,alt:2265},
|
||||
{lat:47.258592,lon:11.9923341,alt:2256},
|
||||
{lat:47.2594506,lon:11.9927412,alt:2230},
|
||||
{lat:47.2603323,lon:11.9924949,alt:2219},
|
||||
{lat:47.2612056,lon:11.9928175,alt:2199},
|
||||
{lat:47.2621002,lon:11.9929817,alt:2182},
|
||||
{lat:47.2629025,lon:11.9923915,alt:2189},
|
||||
{lat:47.2637828,lon:11.9926486,alt:2180},
|
||||
{lat:47.2646733,lon:11.9928167,alt:2191},
|
||||
{lat:47.2655617,lon:11.9930357,alt:2185},
|
||||
{lat:47.2662862,lon:11.992252,alt:2186},
|
||||
{lat:47.2669305,lon:11.993173,alt:2166},
|
||||
{lat:47.266666,lon:11.9944419,alt:2171},
|
||||
{lat:47.2667579,lon:11.99576,alt:2194},
|
||||
{lat:47.2669409,lon:11.9970579,alt:2207},
|
||||
{lat:47.2666562,lon:11.9983128,alt:2212},
|
||||
{lat:47.2666027,lon:11.9996335,alt:2262},
|
||||
{lat:47.2667245,lon:12.0009395,alt:2278},
|
||||
{lat:47.2668457,lon:12.002256,alt:2297},
|
||||
{lat:47.2666126,lon:12.0035373,alt:2303},
|
||||
{lat:47.2664554,lon:12.004841,alt:2251},
|
||||
{lat:47.2669461,lon:12.005948,alt:2245},
|
||||
{lat:47.2660877,lon:12.006323,alt:2195},
|
||||
{lat:47.2652729,lon:12.0057552,alt:2163},
|
||||
{lat:47.2643926,lon:12.0060123,alt:2131},
|
||||
{lat:47.2634978,lon:12.0058302,alt:2095},
|
||||
{lat:47.2626129,lon:12.0060759,alt:2066},
|
||||
{lat:47.2617325,lon:12.0058188,alt:2037},
|
||||
{lat:47.2608668,lon:12.0061784,alt:1993},
|
||||
{lat:47.2600155,lon:12.0057392,alt:1967},
|
||||
{lat:47.2591203,lon:12.0058233,alt:1949},
|
||||
{lat:47.2582307,lon:12.0059718,alt:1972},
|
||||
{lat:47.2578014,lon:12.004804,alt:2011},
|
||||
{lat:47.2577232,lon:12.0034834,alt:2044},
|
||||
{lat:47.257745,lon:12.0021656,alt:2061},
|
||||
{lat:47.2578682,lon:12.0008597,alt:2065},
|
||||
{lat:47.2577082,lon:11.9995526,alt:2071},
|
||||
{lat:47.2575917,lon:11.9982348,alt:2102},
|
||||
{lat:47.2577401,lon:11.996924,alt:2147},
|
||||
{lat:47.257715,lon:11.9956061,alt:2197},
|
||||
{lat:47.2578996,lon:11.9943081,alt:2228}
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -215,51 +216,43 @@ exports.enable = () => {
|
|||
let interpSteps;
|
||||
if (settings.gps.name == "routeFuzzy"){
|
||||
route = getSquareRouteFuzzy();
|
||||
interpSteps = 5;
|
||||
interpSteps = 74;
|
||||
} else {
|
||||
route = getSquareRoute();
|
||||
interpSteps = 50;
|
||||
interpSteps = 740;
|
||||
}
|
||||
|
||||
let step = 0;
|
||||
let routeIndex = 0;
|
||||
modGps(() => {
|
||||
let newIndex = (routeIndex + 1)%route.length;
|
||||
|
||||
let followingIndex = (routeIndex + 2)%route.length;
|
||||
|
||||
let result = {
|
||||
"speed": Math.random() * 3 + 2,
|
||||
"speed": Math.random()*1 + 4.5,
|
||||
"time": new Date(),
|
||||
"satellites": Math.floor(Math.random()*5)+3,
|
||||
"fix": 1,
|
||||
"hdop": Math.floor(Math.random(30)+1)
|
||||
};
|
||||
|
||||
|
||||
let oldPos = route[routeIndex];
|
||||
if (step != 0){
|
||||
oldPos = interpolate(route[routeIndex], route[newIndex], E.clip(0,1,step/interpSteps));
|
||||
}
|
||||
let newPos = route[newIndex];
|
||||
if (step < interpSteps - 1){
|
||||
newPos = interpolate(route[routeIndex], route[newIndex], E.clip(0,1,(step+1)%interpSteps/interpSteps));
|
||||
let followingPos = route[followingIndex];
|
||||
let interpPos = interpolate(oldPos, newPos, E.clip(0,1,step/interpSteps));
|
||||
|
||||
if (step > 0.5* interpSteps) {
|
||||
result.course = bearing(interpPos, interpolate(newPos, followingPos, E.clip(0,1,(step-0.5*interpSteps)/interpSteps)));
|
||||
} else {
|
||||
result.course = bearing(oldPos, newPos);
|
||||
}
|
||||
|
||||
if (step == interpSteps - 1){
|
||||
let followingIndex = (routeIndex + 2)%route.length;
|
||||
newPos = interpolate(route[newIndex], route[followingIndex], E.clip(0,1,1/interpSteps));
|
||||
}
|
||||
|
||||
result.lat = oldPos.lat;
|
||||
result.lon = oldPos.lon;
|
||||
result.alt = oldPos.ele;
|
||||
|
||||
result.course = bearing(oldPos,newPos);
|
||||
|
||||
step++;
|
||||
if (step == interpSteps){
|
||||
routeIndex = (routeIndex + 1) % route.length;
|
||||
step = 0;
|
||||
}
|
||||
return result;
|
||||
return Object.assign(result, interpPos);
|
||||
});
|
||||
} else if (settings.gps.name == "nofix") {
|
||||
modGps(() => { return {
|
||||
|
|
@ -281,6 +274,7 @@ exports.enable = () => {
|
|||
let currentDir=1000;
|
||||
let currentAlt=500;
|
||||
let currentSats=5;
|
||||
|
||||
modGps(() => {
|
||||
currentLat += 0.01;
|
||||
if (currentLat > 50) currentLat = 20;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"id": "sensortools",
|
||||
"name": "Sensor tools",
|
||||
"shortName": "Sensor tools",
|
||||
"version": "0.04",
|
||||
"version": "0.05",
|
||||
"description": "Tools for testing and debugging apps that use sensor input",
|
||||
"icon": "icon.png",
|
||||
"type": "bootloader",
|
||||
|
|
|
|||
|
|
@ -88,6 +88,12 @@
|
|||
writeSettings("enabled",v);
|
||||
},
|
||||
},
|
||||
'Log': {
|
||||
value: !!settings.log,
|
||||
onchange: v => {
|
||||
writeSettings("log",v);
|
||||
},
|
||||
},
|
||||
'GPS': ()=>{showSubMenu("GPS","gps",["nop", "staticfix", "nofix", "changingfix", "route", "routeFuzzy"],[]);},
|
||||
'Compass': ()=>{showSubMenu("Compass","mag",["nop", "static", "rotate"],[]);},
|
||||
'HRM': ()=>{showSubMenu("HRM","hrm",["nop", "static"],["bpmtrippled"],["sin"]);}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<gpx version="1.1" creator="OsmAndRouterV2" xmlns="http://www.topografix.com/GPX/1/1" xmlns:osmand="https://osmand.net" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">
|
||||
<metadata>
|
||||
<name>1kmsquare</name>
|
||||
<desc>Export from GpsPrune</desc>
|
||||
</metadata>
|
||||
<trk>
|
||||
<name>1kmsquare</name>
|
||||
<number>1</number>
|
||||
<trkseg>
|
||||
<trkpt lat="47.2577411" lon="11.9927442">
|
||||
<ele>2273</ele>
|
||||
<name>Lower left</name>
|
||||
</trkpt>
|
||||
<trkpt lat="47.266761" lon="11.9926673">
|
||||
<ele>2166</ele>
|
||||
<name>Top left</name>
|
||||
</trkpt>
|
||||
<trkpt lat="47.2667605" lon="12.0059511">
|
||||
<ele>2245</ele>
|
||||
<name>Top right</name>
|
||||
</trkpt>
|
||||
<trkpt lat="47.2577516" lon="12.0059925">
|
||||
<ele>1994</ele>
|
||||
<name>Lower right</name>
|
||||
</trkpt>
|
||||
<trkpt lat="47.2577412" lon="11.9927442">
|
||||
<ele>2273</ele>
|
||||
<name>Destination</name>
|
||||
</trkpt>
|
||||
</trkseg>
|
||||
</trk>
|
||||
</gpx>
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<gpx version="1.1" creator="OsmAndRouterV2" xmlns="http://www.topografix.com/GPX/1/1" xmlns:osmand="https://osmand.net" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">
|
||||
<metadata>
|
||||
<name>1kmsquare100</name>
|
||||
<desc>Export from GpsPrune</desc>
|
||||
</metadata>
|
||||
<trk>
|
||||
<name>1kmsquare100</name>
|
||||
<number>1</number>
|
||||
<trkseg>
|
||||
<trkpt lat="47.2578455" lon="11.9929891">
|
||||
<ele>2265</ele>
|
||||
<name>Lower left</name>
|
||||
</trkpt>
|
||||
<trkpt lat="47.258592" lon="11.9923341">
|
||||
<ele>2256</ele>
|
||||
</trkpt>
|
||||
<trkpt lat="47.2594506" lon="11.9927412">
|
||||
<ele>2230</ele>
|
||||
</trkpt>
|
||||
<trkpt lat="47.2603323" lon="11.9924949">
|
||||
<ele>2219</ele>
|
||||
</trkpt>
|
||||
<trkpt lat="47.2612056" lon="11.9928175">
|
||||
<ele>2199</ele>
|
||||
</trkpt>
|
||||
<trkpt lat="47.2621002" lon="11.9929817">
|
||||
<ele>2182</ele>
|
||||
</trkpt>
|
||||
<trkpt lat="47.2629025" lon="11.9923915">
|
||||
<ele>2189</ele>
|
||||
</trkpt>
|
||||
<trkpt lat="47.2637828" lon="11.9926486">
|
||||
<ele>2180</ele>
|
||||
</trkpt>
|
||||
<trkpt lat="47.2646733" lon="11.9928167">
|
||||
<ele>2191</ele>
|
||||
</trkpt>
|
||||
<trkpt lat="47.2655617" lon="11.9930357">
|
||||
<ele>2185</ele>
|
||||
</trkpt>
|
||||
<trkpt lat="47.2662862" lon="11.992252">
|
||||
<ele>2186</ele>
|
||||
</trkpt>
|
||||
<trkpt lat="47.2669305" lon="11.993173">
|
||||
<ele>2166</ele>
|
||||
<name>Top left</name>
|
||||
</trkpt>
|
||||
<trkpt lat="47.266666" lon="11.9944419">
|
||||
<ele>2171</ele>
|
||||
</trkpt>
|
||||
<trkpt lat="47.2667579" lon="11.99576">
|
||||
<ele>2194</ele>
|
||||
</trkpt>
|
||||
<trkpt lat="47.2669409" lon="11.9970579">
|
||||
<ele>2207</ele>
|
||||
</trkpt>
|
||||
<trkpt lat="47.2666562" lon="11.9983128">
|
||||
<ele>2212</ele>
|
||||
</trkpt>
|
||||
<trkpt lat="47.2666027" lon="11.9996335">
|
||||
<ele>2262</ele>
|
||||
</trkpt>
|
||||
<trkpt lat="47.2667245" lon="12.0009395">
|
||||
<ele>2278</ele>
|
||||
</trkpt>
|
||||
<trkpt lat="47.2668457" lon="12.002256">
|
||||
<ele>2297</ele>
|
||||
</trkpt>
|
||||
<trkpt lat="47.2666126" lon="12.0035373">
|
||||
<ele>2303</ele>
|
||||
</trkpt>
|
||||
<trkpt lat="47.2664554" lon="12.004841">
|
||||
<ele>2251</ele>
|
||||
</trkpt>
|
||||
<trkpt lat="47.2669461" lon="12.005948">
|
||||
<ele>2245</ele>
|
||||
<name>Top right</name>
|
||||
</trkpt>
|
||||
<trkpt lat="47.2660877" lon="12.006323">
|
||||
<ele>2195</ele>
|
||||
</trkpt>
|
||||
<trkpt lat="47.2652729" lon="12.0057552">
|
||||
<ele>2163</ele>
|
||||
</trkpt>
|
||||
<trkpt lat="47.2643926" lon="12.0060123">
|
||||
<ele>2131</ele>
|
||||
</trkpt>
|
||||
<trkpt lat="47.2634978" lon="12.0058302">
|
||||
<ele>2095</ele>
|
||||
</trkpt>
|
||||
<trkpt lat="47.2626129" lon="12.0060759">
|
||||
<ele>2066</ele>
|
||||
</trkpt>
|
||||
<trkpt lat="47.2617325" lon="12.0058188">
|
||||
<ele>2037</ele>
|
||||
</trkpt>
|
||||
<trkpt lat="47.2608668" lon="12.0061784">
|
||||
<ele>1993</ele>
|
||||
</trkpt>
|
||||
<trkpt lat="47.2600155" lon="12.0057392">
|
||||
<ele>1967</ele>
|
||||
</trkpt>
|
||||
<trkpt lat="47.2591203" lon="12.0058233">
|
||||
<ele>1949</ele>
|
||||
</trkpt>
|
||||
<trkpt lat="47.2582307" lon="12.0059718">
|
||||
<ele>1972</ele>
|
||||
</trkpt>
|
||||
<trkpt lat="47.2578014" lon="12.004804">
|
||||
<ele>2011</ele>
|
||||
<name>Lower right</name>
|
||||
</trkpt>
|
||||
<trkpt lat="47.2577232" lon="12.0034834">
|
||||
<ele>2044</ele>
|
||||
</trkpt>
|
||||
<trkpt lat="47.257745" lon="12.0021656">
|
||||
<ele>2061</ele>
|
||||
</trkpt>
|
||||
<trkpt lat="47.2578682" lon="12.0008597">
|
||||
<ele>2065</ele>
|
||||
</trkpt>
|
||||
<trkpt lat="47.2577082" lon="11.9995526">
|
||||
<ele>2071</ele>
|
||||
</trkpt>
|
||||
<trkpt lat="47.2575917" lon="11.9982348">
|
||||
<ele>2102</ele>
|
||||
</trkpt>
|
||||
<trkpt lat="47.2577401" lon="11.996924">
|
||||
<ele>2147</ele>
|
||||
</trkpt>
|
||||
<trkpt lat="47.257715" lon="11.9956061">
|
||||
<ele>2197</ele>
|
||||
</trkpt>
|
||||
<trkpt lat="47.2578996" lon="11.9943081">
|
||||
<ele>2228</ele>
|
||||
</trkpt>
|
||||
<trkpt lat="47.2578436" lon="11.992992">
|
||||
<ele>2265</ele>
|
||||
<name>Destination</name>
|
||||
</trkpt>
|
||||
</trkseg>
|
||||
</trk>
|
||||
</gpx>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
0.01: First release
|
||||
0.02: Faster sinus line and fix button to open menu
|
||||
0.03: Show day/month, add animations, fix !mylocation and text glitch
|
||||
0.04: Always show the widgets, swifter animations and lighter sea line
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
# sunrise watchface
|
||||
|
||||
This app mimics the Apple Watch watchface that shows the sunrise and sunset time.
|
||||
|
||||
This is a work-in-progress app, so you may expect missfeatures, bugs and heavy
|
||||
battery draining. There's still a lot of things to optimize and improve, so take
|
||||
this into account before complaining :-)
|
||||
|
||||
* Requires to configure the location in Settings -> Apps -> My Location
|
||||
* Shows sea level and make the sun/moon glow depending on the x position
|
||||
* The sinus is fixed, so the sea level is curved to match the sunrise/sunset positions)
|
||||
|
||||
## TODO
|
||||
|
||||
* Improved gradients and add support for banglejs1
|
||||
* Faster rendering, by reducing sinus stepsize, only refreshing whats needed, etc
|
||||
* Show red vertical lines or dots inside the sinus if there are alarms
|
||||
|
||||
## Author
|
||||
|
||||
Written by pancake in 2023
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwkEIf4A/AH4A/AH4ADgMiAAMgCyoABiAXQiUjkQBCkIXQFYMzAAIEBIyMjn8z+cyMKECFwM+93uGAJIPgUzn4WBC4IwBIx0jC4njmQvOkUSkQXTicQL4JHSgSFBgUyO6QkCU4YWBF4KnLFwTXGIwMQRZQ7EC4IxBAYRHKgQjEVIIXCkcgiQwJQQxhBAAJQCGBBdEABIwIfJwmHFxwnFAYQuOFAuIcAMwC54pE1WpWgURMKUxhUKzWqDYLOKVQh1FGoOTnQaKdAR1HhWqzWKkUykK7GkKkM1WZyRsCGAikPhWZ1EzGoKHBaZ5rEGoQWRNgoXVAH4A5"))
|
||||
|
|
@ -0,0 +1,401 @@
|
|||
// banglejs app made by pancake
|
||||
// sunrise/sunset script by Matt Kane from https://github.com/Triggertrap/sun-js
|
||||
|
||||
const LOCATION_FILE = 'mylocation.json';
|
||||
let location;
|
||||
|
||||
Bangle.setUI('clock');
|
||||
Bangle.loadWidgets();
|
||||
// requires the myLocation app
|
||||
function loadLocation () {
|
||||
try {
|
||||
return require('Storage').readJSON(LOCATION_FILE, 1);
|
||||
} catch (e) {
|
||||
return { lat: 41.38, lon: 2.168 };
|
||||
}
|
||||
}
|
||||
let frames = 0; // amount of pending frames to render (0 if none)
|
||||
let curPos = 0; // x position of the sun
|
||||
let realPos = 0; // x position of the sun depending on currentime
|
||||
const latlon = loadLocation() || {};
|
||||
const lat = latlon.lat || 41.38;
|
||||
const lon = latlon.lon || 2.168;
|
||||
|
||||
/**
|
||||
* Sunrise/sunset script. By Matt Kane.
|
||||
*
|
||||
* Based loosely and indirectly on Kevin Boone's SunTimes Java implementation
|
||||
* of the US Naval Observatory's algorithm.
|
||||
*
|
||||
* Copyright © 2012 Triggertrap Ltd. All rights reserved.
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General
|
||||
* Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,but WITHOUT ANY WARRANTY; without even the implied
|
||||
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
|
||||
* details.
|
||||
* You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA,
|
||||
* or connect to: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
|
||||
*/
|
||||
|
||||
Date.prototype.sunrise = function (latitude, longitude, zenith) {
|
||||
return this.sunriseSet(latitude, longitude, true, zenith);
|
||||
};
|
||||
|
||||
Date.prototype.sunset = function (latitude, longitude, zenith) {
|
||||
return this.sunriseSet(latitude, longitude, false, zenith);
|
||||
};
|
||||
|
||||
Date.prototype.sunriseSet = function (latitude, longitude, sunrise, zenith) {
|
||||
if (!zenith) {
|
||||
zenith = 90.8333;
|
||||
}
|
||||
|
||||
const hoursFromMeridian = longitude / Date.DEGREES_PER_HOUR;
|
||||
const dayOfYear = this.getDayOfYear();
|
||||
let approxTimeOfEventInDays;
|
||||
let sunMeanAnomaly;
|
||||
let sunTrueLongitude;
|
||||
let ascension;
|
||||
let rightAscension;
|
||||
let lQuadrant;
|
||||
let raQuadrant;
|
||||
let sinDec;
|
||||
let cosDec;
|
||||
let localHourAngle;
|
||||
let localHour;
|
||||
let localMeanTime;
|
||||
let time;
|
||||
|
||||
if (sunrise) {
|
||||
approxTimeOfEventInDays = dayOfYear + ((6 - hoursFromMeridian) / 24);
|
||||
} else {
|
||||
approxTimeOfEventInDays = dayOfYear + ((18.0 - hoursFromMeridian) / 24);
|
||||
}
|
||||
|
||||
sunMeanAnomaly = (0.9856 * approxTimeOfEventInDays) - 3.289;
|
||||
|
||||
sunTrueLongitude = sunMeanAnomaly + (1.916 * Math.sinDeg(sunMeanAnomaly)) + (0.020 * Math.sinDeg(2 * sunMeanAnomaly)) + 282.634;
|
||||
sunTrueLongitude = Math.mod(sunTrueLongitude, 360);
|
||||
|
||||
ascension = 0.91764 * Math.tanDeg(sunTrueLongitude);
|
||||
rightAscension = 360 / (2 * Math.PI) * Math.atan(ascension);
|
||||
rightAscension = Math.mod(rightAscension, 360);
|
||||
|
||||
lQuadrant = Math.floor(sunTrueLongitude / 90) * 90;
|
||||
raQuadrant = Math.floor(rightAscension / 90) * 90;
|
||||
rightAscension = rightAscension + (lQuadrant - raQuadrant);
|
||||
rightAscension /= Date.DEGREES_PER_HOUR;
|
||||
|
||||
sinDec = 0.39782 * Math.sinDeg(sunTrueLongitude);
|
||||
cosDec = Math.cosDeg(Math.asinDeg(sinDec));
|
||||
cosLocalHourAngle = ((Math.cosDeg(zenith)) - (sinDec * (Math.sinDeg(latitude)))) / (cosDec * (Math.cosDeg(latitude)));
|
||||
|
||||
localHourAngle = Math.acosDeg(cosLocalHourAngle);
|
||||
|
||||
if (sunrise) {
|
||||
localHourAngle = 360 - localHourAngle;
|
||||
}
|
||||
|
||||
localHour = localHourAngle / Date.DEGREES_PER_HOUR;
|
||||
|
||||
localMeanTime = localHour + rightAscension - (0.06571 * approxTimeOfEventInDays) - 6.622;
|
||||
|
||||
time = localMeanTime - (longitude / Date.DEGREES_PER_HOUR);
|
||||
time = Math.mod(time, 24);
|
||||
|
||||
const midnight = new Date(0);
|
||||
// midnight.setUTCFullYear(this.getUTCFullYear());
|
||||
// midnight.setUTCMonth(this.getUTCMonth());
|
||||
// midnight.setUTCDate(this.getUTCDate());
|
||||
|
||||
const milli = midnight.getTime() + (time * 60 * 60 * 1000);
|
||||
|
||||
return new Date(milli);
|
||||
};
|
||||
|
||||
Date.DEGREES_PER_HOUR = 360 / 24;
|
||||
|
||||
// Utility functions
|
||||
|
||||
Date.prototype.getDayOfYear = function () {
|
||||
const onejan = new Date(this.getFullYear(), 0, 1);
|
||||
return Math.ceil((this - onejan) / 86400000);
|
||||
};
|
||||
|
||||
Math.degToRad = function (num) {
|
||||
return num * Math.PI / 180;
|
||||
};
|
||||
|
||||
Math.radToDeg = function (radians) {
|
||||
return radians * 180.0 / Math.PI;
|
||||
};
|
||||
|
||||
Math.sinDeg = function (deg) {
|
||||
return Math.sin(deg * 2.0 * Math.PI / 360.0);
|
||||
};
|
||||
|
||||
Math.acosDeg = function (x) {
|
||||
return Math.acos(x) * 360.0 / (2 * Math.PI);
|
||||
};
|
||||
|
||||
Math.asinDeg = function (x) {
|
||||
return Math.asin(x) * 360.0 / (2 * Math.PI);
|
||||
};
|
||||
|
||||
Math.tanDeg = function (deg) {
|
||||
return Math.tan(deg * 2.0 * Math.PI / 360.0);
|
||||
};
|
||||
|
||||
Math.cosDeg = function (deg) {
|
||||
return Math.cos(deg * 2.0 * Math.PI / 360.0);
|
||||
};
|
||||
|
||||
Math.mod = function (a, b) {
|
||||
let result = a % b;
|
||||
if (result < 0) {
|
||||
result += b;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const delta = 2;
|
||||
const sunrise = new Date().sunrise(lat, lon);
|
||||
const sr = sunrise.getHours() + ':' + sunrise.getMinutes();
|
||||
console.log('sunrise', sunrise);
|
||||
const sunset = new Date().sunset(lat, lon);
|
||||
const ss = sunset.getHours() + ':' + sunset.getMinutes();
|
||||
console.log('sunset', sunset);
|
||||
|
||||
const w = g.getWidth();
|
||||
const h = g.getHeight();
|
||||
const oy = h / 1.7;
|
||||
|
||||
let sunRiseX = 0;
|
||||
let sunSetX = 0;
|
||||
const sinStep = 12;
|
||||
|
||||
function drawSinuses () {
|
||||
let x = 0;
|
||||
|
||||
g.setColor(0, 0, 0);
|
||||
// g.fillRect(0,oy,w, h);
|
||||
g.setColor(1, 1, 1);
|
||||
let y = oy;
|
||||
for (i = 0; i < w; i++) {
|
||||
x = i;
|
||||
x2 = x + sinStep + 1;
|
||||
y2 = ypos(i);
|
||||
if (x == 0) {
|
||||
y = y2;
|
||||
}
|
||||
g.drawLine(x, y, x2, y2);
|
||||
y = y2;
|
||||
i += sinStep; // no need to draw all steps
|
||||
}
|
||||
|
||||
// sea level line
|
||||
const hh0 = sunrise.getHours();
|
||||
const hh1 = sunset.getHours();
|
||||
const sl0 = seaLevel(hh0);
|
||||
const sl1 = seaLevel(hh1);
|
||||
sunRiseX = xfromTime(hh0) + (r / 2);
|
||||
sunSetX = xfromTime(hh1) + (r / 2);
|
||||
g.setColor(0, 0.5, 1);
|
||||
g.drawLine(0, sl0, w, sl1);
|
||||
g.setColor(0, 0.5, 1);
|
||||
g.drawLine(0, sl0 + 1, w, sl1 + 1);
|
||||
/*
|
||||
g.setColor(0, 0, 1);
|
||||
g.drawLine(0, sl0 + 1, w, sl1 + 1);
|
||||
g.setColor(0, 0, 0.5);
|
||||
g.drawLine(0, sl0 + 2, w, sl1 + 2);
|
||||
*/
|
||||
}
|
||||
|
||||
function drawTimes () {
|
||||
g.setColor(1, 1, 1);
|
||||
g.setFont('6x8', 2);
|
||||
g.drawString(sr, 10, h - 20);
|
||||
g.drawString(ss, w - 60, h - 20);
|
||||
}
|
||||
|
||||
let pos = 0;
|
||||
let realTime = true;
|
||||
const r = 10;
|
||||
|
||||
function drawGlow () {
|
||||
const now = new Date();
|
||||
if (frames < 1 && realTime) {
|
||||
pos = xfromTime(now.getHours());
|
||||
}
|
||||
const rh = r / 2;
|
||||
const x = pos;
|
||||
const y = ypos(x - r);
|
||||
const r2 = 0;
|
||||
if (x > sunRiseX && x < sunSetX) {
|
||||
g.setColor(0.2, 0.2, 0);
|
||||
g.fillCircle(x, y, r + 20);
|
||||
g.setColor(0.5, 0.5, 0);
|
||||
// wide glow
|
||||
} else {
|
||||
g.setColor(0.2, 0.2, 0);
|
||||
}
|
||||
// smol glow
|
||||
g.fillCircle(x, y, r + 8);
|
||||
}
|
||||
|
||||
function seaLevel (hour) {
|
||||
// hour goes from 0 to 24
|
||||
// to get the X we divide the screen in 24
|
||||
return ypos(xfromTime(hour));
|
||||
}
|
||||
|
||||
function ypos (x) {
|
||||
const pc = (x * 100 / w);
|
||||
return oy + (32 * Math.sin(1.7 + (pc / 16)));
|
||||
}
|
||||
|
||||
function xfromTime (t) {
|
||||
return (w / 24) * t;
|
||||
}
|
||||
|
||||
function drawBall () {
|
||||
let x = pos;
|
||||
const now = new Date();
|
||||
if (frames < 1 && realTime) {
|
||||
x = xfromTime(now.getHours());
|
||||
}
|
||||
const y = ypos(x - r);
|
||||
|
||||
// glow
|
||||
if (x < sunRiseX || x > sunSetX) {
|
||||
g.setColor(0.5, 0.5, 0);
|
||||
} else {
|
||||
g.setColor(1, 1, 1);
|
||||
}
|
||||
const rh = r / 2;
|
||||
g.fillCircle(x, y, r);
|
||||
g.setColor(1, 1, 0);
|
||||
g.drawCircle(x, y, r);
|
||||
}
|
||||
function drawClock () {
|
||||
const now = new Date();
|
||||
|
||||
let curTime = '';
|
||||
let fhours = 0.0;
|
||||
let fmins = 0.0;
|
||||
let ypos = 32;
|
||||
if (realTime) {
|
||||
fhours = now.getHours();
|
||||
fmins = now.getMinutes();
|
||||
} else {
|
||||
ypos = 32;
|
||||
fhours = 24 * (pos / w);
|
||||
if (fhours > 23) {
|
||||
fhours = 0;
|
||||
}
|
||||
const nexth = 24 * 60 * (pos / w);
|
||||
fmins = 59 - ((24 * 60) - nexth) % 60;
|
||||
if (fmins < 0) {
|
||||
fmins = 0;
|
||||
}
|
||||
}
|
||||
if (fmins > 59) {
|
||||
fmins = 59;
|
||||
}
|
||||
const hours = ((fhours < 10) ? '0' : '') + (0 | fhours);
|
||||
const mins = ((fmins < 10) ? '0' : '') + (0 | fmins);
|
||||
curTime = hours + ':' + mins;
|
||||
g.setFont('Vector', 30);
|
||||
if (realTime) {
|
||||
g.setColor(1, 1, 1);
|
||||
} else {
|
||||
g.setColor(0, 1, 1);
|
||||
}
|
||||
g.drawString(curTime, w / 1.9, ypos);
|
||||
// day-month
|
||||
if (realTime) {
|
||||
const mo = now.getMonth() + 1;
|
||||
const da = now.getDate();
|
||||
const daymonth = '' + da + '/' + mo;
|
||||
g.setFont('6x8', 2);
|
||||
g.drawString(daymonth, 5, 30);
|
||||
}
|
||||
}
|
||||
|
||||
function renderScreen () {
|
||||
g.setColor(0, 0, 0);
|
||||
g.fillRect(0, 30, w, h);
|
||||
realPos = xfromTime((new Date()).getHours());
|
||||
g.setFontAlign(-1, -1, 0);
|
||||
|
||||
Bangle.drawWidgets();
|
||||
|
||||
drawGlow();
|
||||
drawSinuses();
|
||||
drawTimes();
|
||||
drawClock();
|
||||
drawBall();
|
||||
}
|
||||
|
||||
Bangle.on('drag', function (tap, top) {
|
||||
if (tap.y < h / 3) {
|
||||
curPos = pos;
|
||||
initialAnimation();
|
||||
} else {
|
||||
pos = tap.x - 5;
|
||||
realTime = false;
|
||||
}
|
||||
renderScreen();
|
||||
});
|
||||
|
||||
Bangle.on('lock', () => {
|
||||
// TODO: render animation here
|
||||
realTime = Bangle.isLocked();
|
||||
renderScreen();
|
||||
});
|
||||
|
||||
renderScreen();
|
||||
|
||||
realPos = xfromTime((new Date()).getHours());
|
||||
|
||||
function initialAnimationFrame () {
|
||||
let distance = (realPos - curPos) / 4;
|
||||
if (distance > 20) {
|
||||
distance = 20;
|
||||
}
|
||||
curPos += distance;
|
||||
pos = curPos;
|
||||
renderScreen();
|
||||
if (curPos >= realPos) {
|
||||
frame = 0;
|
||||
}
|
||||
frames--;
|
||||
if (frames-- > 0) {
|
||||
setTimeout(initialAnimationFrame, 50);
|
||||
} else {
|
||||
realTime = true;
|
||||
renderScreen();
|
||||
}
|
||||
}
|
||||
|
||||
function initialAnimation () {
|
||||
const distance = Math.abs(realPos - pos);
|
||||
frames = distance / 4;
|
||||
realTime = false;
|
||||
initialAnimationFrame();
|
||||
}
|
||||
|
||||
function main () {
|
||||
g.setBgColor(0, 0, 0);
|
||||
g.clear();
|
||||
setInterval(renderScreen, 60 * 1000);
|
||||
pos = 0;
|
||||
initialAnimation();
|
||||
}
|
||||
|
||||
main();
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"id": "sunrise",
|
||||
"name": "Sunrise",
|
||||
"shortName": "Sunrise",
|
||||
"version": "0.04",
|
||||
"type": "clock",
|
||||
"description": "Show sunrise and sunset times",
|
||||
"icon": "app.png",
|
||||
"allow_emulator": true,
|
||||
"tags": "clock",
|
||||
"supports": [
|
||||
"BANGLEJS2"
|
||||
],
|
||||
"readme": "README.md",
|
||||
"storage": [
|
||||
{
|
||||
"name": "sunrise.app.js",
|
||||
"url": "app.js"
|
||||
},
|
||||
{
|
||||
"name": "sunrise.img",
|
||||
"url": "app-icon.js",
|
||||
"evaluate": true
|
||||
}
|
||||
],
|
||||
"screenshots": [
|
||||
{
|
||||
"url": "screenshot.png"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
|
|
@ -2,3 +2,4 @@
|
|||
0.02: Fix issue with mode being undefined
|
||||
0.03: Update setUI to work with new Bangle.js 2v13 menu style
|
||||
0.04: Update to work with new 'fast switch' clock->launcher functionality
|
||||
0.05: Keep track of event listeners we "overwrite", and remove them at the start of setUI
|
||||
|
|
|
|||
|
|
@ -1,7 +1,13 @@
|
|||
(function() {
|
||||
var sui = Bangle.setUI;
|
||||
var oldSwipe;
|
||||
|
||||
Bangle.setUI = function(mode, cb) {
|
||||
if (oldSwipe && oldSwipe !== Bangle.swipeHandler)
|
||||
Bangle.removeListener("swipe", oldSwipe);
|
||||
sui(mode,cb);
|
||||
oldSwipe = Bangle.swipeHandler;
|
||||
|
||||
if(!mode) return;
|
||||
if ("object"==typeof mode) mode = mode.mode;
|
||||
if (mode.startsWith("clock")) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "swiperclocklaunch",
|
||||
"name": "Swiper Clock Launch",
|
||||
"version": "0.04",
|
||||
"version": "0.05",
|
||||
"description": "Navigate between clock and launcher with Swipe action",
|
||||
"icon": "swiperclocklaunch.png",
|
||||
"type": "bootloader",
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
0.01: New widget
|
||||
0.02: Make color depend on level
|
||||
0.03: Stop battery widget clearing too far down
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "widbatv",
|
||||
"name": "Battery Level Widget (Vertical)",
|
||||
"version": "0.02",
|
||||
"version": "0.03",
|
||||
"description": "Slim, vertical battery widget that only takes up 14px",
|
||||
"icon": "widget.png",
|
||||
"type": "widget",
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ WIDGETS["batv"]={area:"tr",width:14,draw:function() {
|
|||
if (Bangle.isCharging()) {
|
||||
g.setColor("#0f0").drawImage(atob("DhgBHOBzgc4HOP////////////////////3/4HgB4AeAHgB4AeAHgB4AeAHg"),x,y);
|
||||
} else {
|
||||
g.clearRect(x,y,x+14,y+24);
|
||||
g.clearRect(x,y,x+14,y+23);
|
||||
g.setColor(g.theme.fg).fillRect(x+2,y+2,x+12,y+22).clearRect(x+4,y+4,x+10,y+20).fillRect(x+5,y+1,x+9,y+2);
|
||||
var battery = E.getBattery();
|
||||
if (battery < 20) {g.setColor("#f00");}
|
||||
|
|
|
|||
|
|
@ -5,3 +5,4 @@
|
|||
0.06: Tweaking colors for dark/light themes and low bpp screens
|
||||
0.07: Memory usage improvements
|
||||
0.08: Disable LCD on, on bluetooth status change
|
||||
0.09: Fix widget not showing on blue background
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "widbt",
|
||||
"name": "Bluetooth Widget",
|
||||
"version": "0.08",
|
||||
"version": "0.09",
|
||||
"description": "Show the current Bluetooth connection status in the top right of the clock",
|
||||
"icon": "widget.png",
|
||||
"type": "widget",
|
||||
|
|
|
|||
|
|
@ -1,9 +1,14 @@
|
|||
WIDGETS["bluetooth"]={area:"tr",width:15,draw:function() {
|
||||
g.reset();
|
||||
if (NRF.getSecurityStatus().connected)
|
||||
g.setColor((g.getBPP()>8) ? "#07f" : (g.theme.dark ? "#0ff" : "#00f"));
|
||||
else
|
||||
if (NRF.getSecurityStatus().connected) {
|
||||
if (g.getBgColor() === 31) { // If background color is blue use cyan instead
|
||||
g.setColor("#0ff");
|
||||
} else {
|
||||
g.setColor((g.getBPP()>8) ? "#07f" : (g.theme.dark ? "#0ff" : "#00f"));
|
||||
}
|
||||
} else {
|
||||
g.setColor(g.theme.dark ? "#666" : "#999");
|
||||
}
|
||||
g.drawImage(atob("CxQBBgDgFgJgR4jZMawfAcA4D4NYybEYIwTAsBwDAA=="),2+this.x,2+this.y);
|
||||
},changed:function() {
|
||||
WIDGETS["bluetooth"].draw();
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
0.01: New Widget!
|
||||
0.02: Now use an app ID (to avoid conflicts with clocks that also use ClockInfo)
|
||||
0.02: Now use an app ID (to avoid conflicts with clocks that also use ClockInfo)
|
||||
0.03: Fix widget clearing too far down
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{ "id": "widclkinfo",
|
||||
"name": "Clock Info Widget",
|
||||
"version":"0.02",
|
||||
"version":"0.03",
|
||||
"description": "Use 'Clock Info' in the Widget bar. Tap on the widget to select, then drag up/down/left/right to choose what information is displayed.",
|
||||
"icon": "widget.png",
|
||||
"screenshots" : [ { "url":"screenshot.png" }],
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ if (!require("clock_info").loadCount) { // don't load if a clock_info was alread
|
|||
// indicate focus - make background reddish
|
||||
//if (clockInfoMenu.focus) g.setBgColor(g.blendColor(g.theme.bg, "#f00", 0.25));
|
||||
if (clockInfoMenu.focus) g.setColor("#f00");
|
||||
g.clearRect(o.x, o.y, o.x+o.w-1, o.y+o.h);
|
||||
g.clearRect(o.x, o.y, o.x+o.w-1, o.y+o.h-1);
|
||||
if (clockInfoInfo) {
|
||||
var x = o.x;
|
||||
if (clockInfoInfo.img) {
|
||||
|
|
|
|||
|
|
@ -70,13 +70,13 @@ exports.swipeOn = function(autohide) {
|
|||
// force app rect to be fullscreen
|
||||
Bangle.appRect = { x: 0, y: 0, w: g.getWidth(), h: g.getHeight(), x2: g.getWidth()-1, y2: g.getHeight()-1 };
|
||||
// setup offscreen graphics for widgets
|
||||
let og = Graphics.createArrayBuffer(g.getWidth(),24,16,{msb:true});
|
||||
let og = Graphics.createArrayBuffer(g.getWidth(),26,16,{msb:true});
|
||||
og.theme = g.theme;
|
||||
og._reset = og.reset;
|
||||
og.reset = function() {
|
||||
return this._reset().setColor(g.theme.fg).setBgColor(g.theme.bg);
|
||||
};
|
||||
og.reset().clearRect(0,0,og.getWidth(),og.getHeight());
|
||||
og.reset().clearRect(0,0,og.getWidth(),23).fillRect(0,24,og.getWidth(),25);
|
||||
let _g = g;
|
||||
let offset = -24; // where on the screen are we? -24=hidden, 0=full visible
|
||||
|
||||
|
|
@ -146,4 +146,4 @@ exports.swipeOn = function(autohide) {
|
|||
};
|
||||
Bangle.on("swipe", exports.swipeHandler);
|
||||
Bangle.drawWidgets();
|
||||
};
|
||||
};
|
||||