Merge branch 'espruino:master' into light_switch

master
stweedo 2023-06-12 05:29:38 -05:00 committed by GitHub
commit 2fd49e556d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
58 changed files with 3870 additions and 172 deletions

View File

@ -0,0 +1,2 @@
0.01: Initial release.
0.02: Added compatibility to OpenTracks and added HRM Location

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

70
apps/bootgatthrm/boot.js Normal file
View File

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

View File

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

View File

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

View File

@ -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.
![](a_battery_widget-pic.jpg)
## Creator

View File

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

View File

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

7
apps/kbmatry/ChangeLog Normal file
View File

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

119
apps/kbmatry/README.md Normal file
View File

@ -0,0 +1,119 @@
# Matryoshka Keyboard
![icon](icon.png)
![screenshot](screenshot.png) ![screenshot](screenshot6.png)
![screenshot](screenshot5.png) ![screenshot](screenshot2.png)
![screenshot](screenshot3.png) ![screenshot](screenshot4.png)
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.
![help](help.png)
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.

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

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

BIN
apps/kbmatry/help.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

BIN
apps/kbmatry/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 852 B

501
apps/kbmatry/lib.js Normal file
View File

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

View File

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

BIN
apps/kbmatry/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
{
"enabled": false,
"log": false,
"mag": {
"enabled": false,
"mode": "emulate",

View File

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

View File

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

View File

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

View File

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

View File

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

4
apps/sunrise/ChangeLog Normal file
View File

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

25
apps/sunrise/README.md Normal file
View File

@ -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
![sunrise](screenshot.png)

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

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwkEIf4A/AH4A/AH4ADgMiAAMgCyoABiAXQiUjkQBCkIXQFYMzAAIEBIyMjn8z+cyMKECFwM+93uGAJIPgUzn4WBC4IwBIx0jC4njmQvOkUSkQXTicQL4JHSgSFBgUyO6QkCU4YWBF4KnLFwTXGIwMQRZQ7EC4IxBAYRHKgQjEVIIXCkcgiQwJQQxhBAAJQCGBBdEABIwIfJwmHFxwnFAYQuOFAuIcAMwC54pE1WpWgURMKUxhUKzWqDYLOKVQh1FGoOTnQaKdAR1HhWqzWKkUykK7GkKkM1WZyRsCGAikPhWZ1EzGoKHBaZ5rEGoQWRNgoXVAH4A5"))

401
apps/sunrise/app.js Normal file
View File

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

BIN
apps/sunrise/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

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

BIN
apps/sunrise/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

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

View File

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

View File

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

View File

@ -1,2 +1,3 @@
0.01: New widget
0.02: Make color depend on level
0.03: Stop battery widget clearing too far down

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

2181
package-lock.json generated

File diff suppressed because it is too large Load Diff