Merge branch 'espruino:master' into development
12
android.html
|
|
@ -135,15 +135,17 @@
|
|||
<h3>Utilities</h3>
|
||||
<p>
|
||||
<button class="btn tooltip" id="settime" data-tooltip="Set the Bangle's time to your Browser's time">Set Bangle.js Time</button>
|
||||
<button class="btn tooltip" id="screenshot" data-tooltip="Create screenshot">Screenshot</button>
|
||||
<button class="btn tooltip" id="downloadallapps" data-tooltip="Download all Bangle.js files to a ZIP file">Backup</button>
|
||||
<button class="btn tooltip" id="uploadallapps" data-tooltip="Restore Bangle.js from a ZIP file">Restore</button>
|
||||
</p><p>
|
||||
<button class="btn tooltip" id="removeall" data-tooltip="Delete everything, leave it blank">Remove all Apps</button>
|
||||
<button class="btn tooltip" id="reinstallall" data-tooltip="Re-install every app, leave all data">Reinstall apps</button>
|
||||
<button class="btn tooltip" id="installdefault" data-tooltip="Delete everything, install default apps">Install default apps</button>
|
||||
<button class="btn tooltip" id="installfavourite" data-tooltip="Delete everything, install your favourites">Install favourite apps</button>
|
||||
<button class="btn tooltip" id="defaultbanglesettings" data-tooltip="Reset your Bangle's settings to the defaults">Reset Settings</button>
|
||||
</p><p>
|
||||
<button class="btn tooltip" id="newGithubIssue" data-tooltip="Create a new issue on GitHub">New issue on GitHub</button>
|
||||
<button class="btn tooltip" id="downloadallapps" data-tooltip="Download all Bangle.js files to a ZIP file">Backup</button>
|
||||
<button class="btn tooltip" id="uploadallapps" data-tooltip="Restore Bangle.js from a ZIP file">Restore</button>
|
||||
<button class="btn tooltip" id="defaultbanglesettings" data-tooltip="Reset your Bangle's settings to the defaults">Reset Settings</button>
|
||||
<button class="btn tooltip" id="webideremote" data-tooltip="Enable the Web IDE remote server">Web IDE Remote</button>
|
||||
</p>
|
||||
<h3>Settings</h3>
|
||||
|
|
@ -172,6 +174,10 @@
|
|||
<input type="checkbox" id="settings-minify">
|
||||
<i class="form-icon"></i> Minify apps before upload (⚠️DANGER⚠️: Not recommended. Uploads smaller, faster apps but this <b>will</b> break many apps)
|
||||
</label>
|
||||
<label class="form-switch">
|
||||
<input type="checkbox" id="settings-alwaysAllowUpdate">
|
||||
<i class="form-icon"></i> Always allow to reinstall apps in place regardless of the version
|
||||
</label>
|
||||
<button class="btn" id="defaultsettings">Reset to default App Loader settings</button>
|
||||
</details>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -44,3 +44,4 @@
|
|||
0.39: Dated event repeat option
|
||||
0.40: Use substring of message when it's longer than fits the designated menu entry.
|
||||
0.41: Fix a menu bug affecting alarms with empty messages.
|
||||
0.42: Fix date not getting saved in event edit menu when tapping Confirm
|
||||
|
|
|
|||
|
|
@ -190,7 +190,7 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex, withDate) {
|
|||
},
|
||||
/*LANG*/"Cancel": () => showMainMenu(),
|
||||
/*LANG*/"Confirm": () => {
|
||||
prepareAlarmForSave(alarm, alarmIndex, time);
|
||||
prepareAlarmForSave(alarm, alarmIndex, time, date);
|
||||
saveAndReload();
|
||||
showMainMenu();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"id": "alarm",
|
||||
"name": "Alarms & Timers",
|
||||
"shortName": "Alarms",
|
||||
"version": "0.41",
|
||||
"version": "0.42",
|
||||
"description": "Set alarms and timers on your Bangle",
|
||||
"icon": "app.png",
|
||||
"tags": "tool,alarm",
|
||||
|
|
|
|||
|
|
@ -27,4 +27,7 @@
|
|||
0.26: Change handling of GPS status to depend on GPS events instead of connection events
|
||||
0.27: Issue newline before GB commands (solves issue with console.log and ignored commands)
|
||||
0.28: Navigation messages no longer launch the Maps view unless they're new
|
||||
0.29: Support for http request xpath return format
|
||||
0.29: Support for http request xpath return format
|
||||
0.30: Send firmware and hardware versions on connection
|
||||
Allow alarm enable/disable
|
||||
0.31: Implement API for activity fetching
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@
|
|||
var a = require("sched").newDefaultAlarm();
|
||||
a.id = "gb"+j;
|
||||
a.appid = "gbalarms";
|
||||
a.on = true;
|
||||
a.on = event.d[j].on !== undefined ? event.d[j].on : true;
|
||||
a.t = event.d[j].h * 3600000 + event.d[j].m * 60000;
|
||||
a.dow = ((dow&63)<<1) | (dow>>6); // Gadgetbridge sends DOW in a different format
|
||||
a.last = last;
|
||||
|
|
@ -193,10 +193,37 @@
|
|||
Bangle.on('HRM',actHRMHandler);
|
||||
actInterval = setInterval(function() {
|
||||
var steps = Bangle.getStepCount();
|
||||
gbSend({ t: "act", stp: steps-lastSteps, hrm: lastBPM });
|
||||
gbSend({ t: "act", stp: steps-lastSteps, hrm: lastBPM, rt:1 });
|
||||
lastSteps = steps;
|
||||
}, event.int*1000);
|
||||
},
|
||||
// {t:"actfetch", ts:long}
|
||||
"actfetch": function() {
|
||||
gbSend({t: "actfetch", state: "start"});
|
||||
var actCount = 0;
|
||||
var actCb = function(r) {
|
||||
// The health lib saves the samples at the start of the 10-minute block
|
||||
// However, GB expects them at the end of the block, so let's offset them
|
||||
// here to keep a consistent API in the health lib
|
||||
var sampleTs = r.date.getTime() + 600000;
|
||||
if (sampleTs >= event.ts) {
|
||||
gbSend({
|
||||
t: "act",
|
||||
ts: sampleTs,
|
||||
stp: r.steps,
|
||||
hrm: r.bpm,
|
||||
mov: r.movement
|
||||
});
|
||||
actCount++;
|
||||
}
|
||||
}
|
||||
if (event.ts != 0) {
|
||||
require("health").readAllRecordsSince(new Date(event.ts - 600000), actCb);
|
||||
} else {
|
||||
require("health").readFullDatabase(actCb);
|
||||
}
|
||||
gbSend({t: "actfetch", state: "end", count: actCount});
|
||||
},
|
||||
"nav": function() {
|
||||
event.id="nav";
|
||||
if (event.instr) {
|
||||
|
|
@ -253,6 +280,7 @@
|
|||
Bangle.on("charging", sendBattery);
|
||||
NRF.on("connect", () => setTimeout(function() {
|
||||
sendBattery();
|
||||
gbSend({t: "ver", fw: process.env.VERSION, hw: process.env.HWVERSION});
|
||||
GB({t:"force_calendar_sync_start"}); // send a list of our calendar entries to start off the sync process
|
||||
}, 2000));
|
||||
NRF.on("disconnect", () => {
|
||||
|
|
@ -264,10 +292,9 @@
|
|||
require("messages").clearAll();
|
||||
});
|
||||
setInterval(sendBattery, 10*60*1000);
|
||||
// Health tracking
|
||||
Bangle.on('health', health=>{
|
||||
if (actInterval===undefined) // if 'realtime' we do it differently
|
||||
gbSend({ t: "act", stp: health.steps, hrm: health.bpm });
|
||||
// Health tracking - if 'realtime' data is sent with 'rt:1', but let's still send our activity log every 10 mins
|
||||
Bangle.on('health', h=>{
|
||||
gbSend({ t: "act", stp: h.steps, hrm: h.bpm, mov: h.movement });
|
||||
});
|
||||
// Music control
|
||||
Bangle.musicControl = cmd => {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"id": "android",
|
||||
"name": "Android Integration",
|
||||
"shortName": "Android",
|
||||
"version": "0.29",
|
||||
"version": "0.31",
|
||||
"description": "Display notifications/music/etc sent from the Gadgetbridge app on Android. This replaces the old 'Gadgetbridge' Bangle.js widget.",
|
||||
"icon": "app.png",
|
||||
"tags": "tool,system,messages,notifications,gadgetbridge",
|
||||
|
|
|
|||
|
|
@ -2,3 +2,5 @@
|
|||
0.02: Store last GPS lock, can be used instead of waiting for new GPS on start
|
||||
0.03: Use 'modules/suncalc.js' to avoid it being copied 8 times for different apps
|
||||
0.04: Compatibility with Bangle.js 2, get location from My Location
|
||||
0.05: Enable widgets
|
||||
0.06: Fix azimuth (bug #2651), only show degrees
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@
|
|||
|
||||
const SunCalc = require("suncalc"); // from modules folder
|
||||
const storage = require("Storage");
|
||||
const BANGLEJS2 = process.env.HWVERSION == 2; // check for bangle 2
|
||||
|
||||
function drawMoon(phase, x, y) {
|
||||
const moonImgFiles = [
|
||||
|
|
@ -110,7 +109,7 @@ function drawPoints() {
|
|||
}
|
||||
|
||||
function drawData(title, obj, startX, startY) {
|
||||
g.clear();
|
||||
g.clearRect(Bangle.appRect);
|
||||
drawTitle(title);
|
||||
|
||||
let xPos, yPos;
|
||||
|
|
@ -141,22 +140,21 @@ function drawData(title, obj, startX, startY) {
|
|||
function drawMoonPositionPage(gps, title) {
|
||||
const pos = SunCalc.getMoonPosition(new Date(), gps.lat, gps.lon);
|
||||
const moonColor = g.theme.dark ? {r: 1, g: 1, b: 1} : {r: 0, g: 0, b: 0};
|
||||
const azimuth = pos.azimuth + Math.PI; // 0 is south, we want 0 to be north
|
||||
|
||||
const pageData = {
|
||||
Azimuth: pos.azimuth.toFixed(2),
|
||||
Altitude: pos.altitude.toFixed(2),
|
||||
Azimuth: parseInt(azimuth * 180 / Math.PI + 0.5) + '°',
|
||||
Altitude: parseInt(pos.altitude * 180 / Math.PI + 0.5) + '°',
|
||||
Distance: `${pos.distance.toFixed(0)} km`,
|
||||
"Parallactic Ang": pos.parallacticAngle.toFixed(2),
|
||||
"Parallactic Ang": parseInt(pos.parallacticAngle * 180 / Math.PI + 0.5) + '°',
|
||||
};
|
||||
const azimuthDegrees = parseInt(pos.azimuth * 180 / Math.PI);
|
||||
const azimuthDegrees = parseInt(azimuth * 180 / Math.PI + 0.5);
|
||||
|
||||
drawData(title, pageData, null, g.getHeight()/2 - Object.keys(pageData).length/2*20);
|
||||
drawPoints();
|
||||
drawPoint(azimuthDegrees, 8, moonColor);
|
||||
|
||||
let m = setWatch(() => {
|
||||
let m = moonIndexPageMenu(gps);
|
||||
}, BANGLEJS2 ? BTN : BTN3, {repeat: false, edge: "falling"});
|
||||
Bangle.setUI({mode: "custom", back: () => moonIndexPageMenu(gps)});
|
||||
}
|
||||
|
||||
function drawMoonIlluminationPage(gps, title) {
|
||||
|
|
@ -174,9 +172,7 @@ function drawMoonIlluminationPage(gps, title) {
|
|||
drawData(title, pageData, null, 35);
|
||||
drawMoon(phaseIdx, g.getWidth() / 2, g.getHeight() / 2);
|
||||
|
||||
let m = setWatch(() => {
|
||||
let m = moonIndexPageMenu(gps);
|
||||
}, BANGLEJS2 ? BTN : BTN3, {repease: false, edge: "falling"});
|
||||
Bangle.setUI({mode: "custom", back: () => moonIndexPageMenu(gps)});
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -194,17 +190,17 @@ function drawMoonTimesPage(gps, title) {
|
|||
|
||||
// Draw the moon rise position
|
||||
const risePos = SunCalc.getMoonPosition(times.rise, gps.lat, gps.lon);
|
||||
const riseAzimuthDegrees = parseInt(risePos.azimuth * 180 / Math.PI);
|
||||
const riseAzimuth = risePos.azimuth + Math.PI; // 0 is south, we want 0 to be north
|
||||
const riseAzimuthDegrees = parseInt(riseAzimuth * 180 / Math.PI);
|
||||
drawPoint(riseAzimuthDegrees, 8, moonColor);
|
||||
|
||||
// Draw the moon set position
|
||||
const setPos = SunCalc.getMoonPosition(times.set, gps.lat, gps.lon);
|
||||
const setAzimuthDegrees = parseInt(setPos.azimuth * 180 / Math.PI);
|
||||
const setAzimuth = setPos.azimuth + Math.PI; // 0 is south, we want 0 to be north
|
||||
const setAzimuthDegrees = parseInt(setAzimuth * 180 / Math.PI);
|
||||
drawPoint(setAzimuthDegrees, 8, moonColor);
|
||||
|
||||
let m = setWatch(() => {
|
||||
let m = moonIndexPageMenu(gps);
|
||||
}, BANGLEJS2 ? BTN : BTN3, {repease: false, edge: "falling"});
|
||||
Bangle.setUI({mode: "custom", back: () => moonIndexPageMenu(gps)});
|
||||
}
|
||||
|
||||
function drawSunShowPage(gps, key, date) {
|
||||
|
|
@ -214,16 +210,15 @@ function drawSunShowPage(gps, key, date) {
|
|||
const mins = ("0" + date.getMinutes()).substr(-2);
|
||||
const secs = ("0" + date.getMinutes()).substr(-2);
|
||||
const time = `${hrs}:${mins}:${secs}`;
|
||||
const azimuth = pos.azimuth + Math.PI; // 0 is south, we want 0 to be north
|
||||
|
||||
const azimuth = Number(pos.azimuth.toFixed(2));
|
||||
const azimuthDegrees = parseInt(pos.azimuth * 180 / Math.PI);
|
||||
const altitude = Number(pos.altitude.toFixed(2));
|
||||
const azimuthDegrees = parseInt(azimuth * 180 / Math.PI + 0.5) + '°';
|
||||
const altitude = parseInt(pos.altitude * 180 / Math.PI + 0.5) + '°';
|
||||
|
||||
const pageData = {
|
||||
Time: time,
|
||||
Altitude: altitude,
|
||||
Azimumth: azimuth,
|
||||
Degrees: azimuthDegrees
|
||||
Azimuth: azimuthDegrees,
|
||||
};
|
||||
|
||||
drawData(key, pageData, null, g.getHeight()/2 - Object.keys(pageData).length/2*20 + 5);
|
||||
|
|
@ -233,9 +228,7 @@ function drawSunShowPage(gps, key, date) {
|
|||
// Draw the suns position
|
||||
drawPoint(azimuthDegrees, 8, {r: 1, g: 1, b: 0});
|
||||
|
||||
m = setWatch(() => {
|
||||
m = sunIndexPageMenu(gps);
|
||||
}, BANGLEJS2 ? BTN : BTN3, {repeat: false, edge: "falling"});
|
||||
Bangle.setUI({mode: "custom", back: () => sunIndexPageMenu(gps)});
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
@ -314,7 +307,9 @@ function getCenterStringX(str) {
|
|||
|
||||
function init() {
|
||||
let location = require("Storage").readJSON("mylocation.json",1)||{"lat":51.5072,"lon":0.1276,"location":"London"};
|
||||
Bangle.loadWidgets();
|
||||
indexPageMenu(location);
|
||||
Bangle.drawWidgets();
|
||||
}
|
||||
|
||||
let m;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
"id": "astrocalc",
|
||||
"name": "Astrocalc",
|
||||
"version": "0.04",
|
||||
"version": "0.06",
|
||||
"description": "Calculates interesting information on the sun like sunset and sunrise and moon cycles for the current day based on your location from MyLocation app",
|
||||
"icon": "astrocalc.png",
|
||||
"tags": "app,sun,moon,cycles,tool",
|
||||
"tags": "app,sun,moon,cycles,tool,outdoors",
|
||||
"supports": ["BANGLEJS", "BANGLEJS2"],
|
||||
"allow_emulator": true,
|
||||
"dependencies": {"mylocation":"app"},
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
1.00: Initial release of Bangle Blobs Clock!
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
# Bangle Blobs Clock
|
||||
What if every time you checked the time, you could play a turn of a turn-based puzzle game?
|
||||
You check the time dozens, maybe hundreds of times per day, and Bangle Blobs Clock wants to add a splash of fun to each of these moments!
|
||||
Bangle Blobs Clock is a fully featured watch face with a turn-based puzzle game right next to the clock.
|
||||
|
||||

|
||||

|
||||
|
||||
## Clock Features
|
||||
- Hour and minute
|
||||
- Seconds (only while the screen is unlocked to save power)
|
||||
- Month, day, and day of week
|
||||
- Battery percentage. Blue while charging, red when low, green otherwise.
|
||||
- Respects your 24-hour/12-hour time setting in Locale
|
||||
- Press the pause button to access your Widgets
|
||||
- Supports Fast Loading
|
||||
|
||||
## The Game
|
||||
This is a turn-based puzzle game based on Puyo Puyo, an addictive puzzle game franchise by SEGA.
|
||||
Blobs arrive in pairs that you can move, rotate, and place. When at least four Blobs of the same color touch, they pop, causing Blobs above them to fall.
|
||||
If this causes another pop, it's called a chain! Build a massive chain reaction of popping Blobs!
|
||||
- Drag left and right to move the pair
|
||||
- Tap the left or right half of the screen to rotate the pair
|
||||
- Swipe down to place the pair
|
||||
|
||||
## More Info
|
||||
If you're confused about the functionality of the clock or want a better explanation of how to play the game, I wrote up a user manual here: https://docs.google.com/document/d/1watPzChawBu4iM0lXypreejs3wvf2_8C-x5V2MWJQBc/edit?usp=sharing
|
||||
|
||||
## Special Thanks
|
||||
I'm Pasta Rhythm, computer scientist and aspiring game developer. I would like to say thank you to the people who inspired me while I was making this app:
|
||||
- [nxdefiant, who made a Tetris game.](https://github.com/espruino/BangleApps/tree/master/apps/tetris) Bangle Blobs is my first Bangle app and my first time using JavaScript, so this was a daunting project. This Tetris game served as a great example that helped me get started.
|
||||
- [gfwilliams for Anton Clock](https://github.com/espruino/BangleApps/tree/master/apps/antonclk) and [Targor for Kanagawa Clock.](https://github.com/espruino/BangleApps/tree/master/apps/kanagsec) These were good examples for how to make a watch face for the Bangle.js 2.
|
||||
- Thanks to Gordon Williams and to everyone who contributes to Espruino and the Bangle.js 2 projects!
|
||||
- SEGA, owners of the Puyo Puyo franchise that Bangle Blobs is based on. Please check out official Puyo Puyo games!
|
||||
- Compile, the original creators of Puyo Puyo. The company went bankrupt long ago, but the people who worked for them continue to make games.
|
||||
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwxH+HGm56+5BQ4JBAJItXAAoMMCJQAPJ5pfhJApPQL65HHKIbTU2nXAAu0I5xQNBo4tC2gAFGIxHIL5oNGEoItGGIgwDL6oMGFxgwFL6oVFFxwwEL7YuPGARfVBYwvUL6YLGL84THL84KHL7YHCL6AeBFx+0JggAGLx4wQFwa3DAIwvHNJQwMFwhgIEQ7ILGAYxHBAQWJADUeFAIAEjwtnjwAFGMglBFowxEGA/XgrgICJouMGA4aBAIgvMB4ouOGAouGMZgNGFx4wCPQ5hMN44vTK44wLNo5fUcRwuHL67iOHAxfhFxYJBBooeBFx8ecRY4KBowwOFxDgHM5BtHGBguZfhIkBGI4ICFyILFAIxBHAAoOGXIgLHBowBGFo0FAAoxHFxhfPAoQAJCIguNGxRtGABYpDQB72LFxwwEcCJfJFx4wCL7gvTADYv/F/4APYoQuOaoYwpFz4wOF0IwDGI4ICF0IxFAAgtFA="))
|
||||
|
|
@ -0,0 +1,768 @@
|
|||
{
|
||||
// ~~ Variables for clock ~~
|
||||
let clockDrawTimeout;
|
||||
let twelveHourTime = require('Storage').readJSON('setting.json', 1)['12hour'];
|
||||
let updateSeconds = !Bangle.isLocked();
|
||||
let batteryLevel = E.getBattery();
|
||||
|
||||
// ~~ Variables for game logic ~~
|
||||
const NUM_COLORS = 6;
|
||||
const NUISANCE_COLOR = 7;
|
||||
let grid = [
|
||||
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||
new Uint8Array([0, 0, 0, 0, 0, 0])
|
||||
];
|
||||
let hiddenRow = new Uint8Array([0, 0, 0, 0, 0, 0]);
|
||||
let nextQueue = [{pivot: 1, leaf: 1}, {pivot: 1, leaf: 1}];
|
||||
let currentPair = {pivot: 0, leaf: 0};
|
||||
let dropCoordinates = {pivotX: 2, pivotY: 11, leafX: 2, leafY: 10};
|
||||
let pairX = 2;
|
||||
let pairOrientation = 0; //0 is up, 1 is right, 2 is down, 3 is left
|
||||
let slotsToCheck = [];
|
||||
let selectedColors;
|
||||
let lastChain = 0;
|
||||
let gameLost = false;
|
||||
let gamePaused = false;
|
||||
let midChain = false;
|
||||
|
||||
/*
|
||||
Sets up a new game.
|
||||
Must be called once before the first round.
|
||||
*/
|
||||
let restartGame = function() {
|
||||
grid = [
|
||||
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||
new Uint8Array([0, 0, 0, 0, 0, 0])
|
||||
];
|
||||
hiddenRow = new Uint8Array([0, 0, 0, 0, 0, 0]);
|
||||
currentPair = {pivot: 0, leaf: 0};
|
||||
pairX = 2;
|
||||
pairOrientation = 0; //0 is up, 1 is right, 2 is down, 3 is left
|
||||
slotsToCheck = [];
|
||||
gameLost = false;
|
||||
lastChain = 0;
|
||||
|
||||
//Set up random colors
|
||||
selectedColors = new Uint8Array([1, 2, 3, 4, 5, 6]);
|
||||
for (let i = NUM_COLORS - 1; i > 0; i--) {
|
||||
let swap = selectedColors[i];
|
||||
let swapIndex = Math.floor(Math.random() * (i + 1));
|
||||
selectedColors[i] = selectedColors[swapIndex];
|
||||
selectedColors[swapIndex] = swap;
|
||||
}
|
||||
|
||||
//Create the first two pairs (Always in the first three colors)
|
||||
nextQueue[0].pivot = selectedColors[Math.floor(Math.random() * 3)];
|
||||
nextQueue[0].leaf = selectedColors[Math.floor(Math.random() * 3)];
|
||||
nextQueue[1].pivot = selectedColors[Math.floor(Math.random() * 3)];
|
||||
nextQueue[1].leaf = selectedColors[Math.floor(Math.random() * 3)];
|
||||
};
|
||||
|
||||
/*
|
||||
Readies the next pair and generates a new one for the queue.
|
||||
*/
|
||||
let newPair = function() {
|
||||
currentPair.pivot = nextQueue[0].pivot;
|
||||
currentPair.leaf = nextQueue[0].leaf;
|
||||
|
||||
nextQueue[0].pivot = nextQueue[1].pivot;
|
||||
nextQueue[0].leaf = nextQueue[1].leaf;
|
||||
|
||||
nextQueue[1].pivot = selectedColors[Math.floor(Math.random() * 4)];
|
||||
nextQueue[1].leaf = selectedColors[Math.floor(Math.random() * 4)];
|
||||
|
||||
pairX = 2;
|
||||
pairOrientation = 0;
|
||||
|
||||
calcDropCoordinates();
|
||||
};
|
||||
|
||||
/*
|
||||
Calculates the coordinates at which the current pair will be placed when quick dropped.
|
||||
*/
|
||||
let calcDropCoordinates = function() {
|
||||
dropCoordinates.pivotX = pairX;
|
||||
|
||||
//Find Y coordinate of pivot
|
||||
dropCoordinates.pivotY = -2;
|
||||
for (let i = 11; i >= 0; i--) {
|
||||
if (grid[i][pairX] == 0) {
|
||||
dropCoordinates.pivotY = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (dropCoordinates.pivotY == -2 && hiddenRow[pairX] == 0)
|
||||
dropCoordinates.pivotY = -1;
|
||||
|
||||
//Find coordinates of leaf
|
||||
if (pairOrientation == 1) {
|
||||
dropCoordinates.leafX = pairX + 1;
|
||||
|
||||
dropCoordinates.leafY = -2;
|
||||
for (let i = 11; i >= 0; i--) {
|
||||
if (grid[i][pairX + 1] == 0) {
|
||||
dropCoordinates.leafY = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (dropCoordinates.leafY == -2 && hiddenRow[pairX + 1] == 0)
|
||||
dropCoordinates.leafY = -1;
|
||||
} else if (pairOrientation == 3) {
|
||||
dropCoordinates.leafX = pairX - 1;
|
||||
|
||||
dropCoordinates.leafY = -2;
|
||||
for (let i = 11; i >= 0; i--) {
|
||||
if (grid[i][pairX - 1] == 0) {
|
||||
dropCoordinates.leafY = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (dropCoordinates.leafY == -2 && hiddenRow[pairX - 1] == 0)
|
||||
dropCoordinates.leafY = -1;
|
||||
} else if (pairOrientation == 2) {
|
||||
dropCoordinates.leafX = pairX;
|
||||
dropCoordinates.leafY = dropCoordinates.pivotY;
|
||||
dropCoordinates.pivotY--;
|
||||
} else {
|
||||
dropCoordinates.leafX = pairX;
|
||||
dropCoordinates.leafY = dropCoordinates.pivotY - 1;
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
Moves the current pair a certain number of slots.
|
||||
*/
|
||||
let movePair = function(dx) {
|
||||
pairX += dx;
|
||||
|
||||
if (dx < 0) {
|
||||
if (pairX < (pairOrientation == 3 ? 1 : 0))
|
||||
pairX = (pairOrientation == 3 ? 1 : 0);
|
||||
}
|
||||
if (dx > 0) {
|
||||
if (pairX > (pairOrientation == 1 ? 4 : 5))
|
||||
pairX = (pairOrientation == 1 ? 4 : 5);
|
||||
}
|
||||
|
||||
calcDropCoordinates();
|
||||
};
|
||||
|
||||
/*
|
||||
Rotates the pair in the given direction around the pivot.
|
||||
*/
|
||||
let rotatePair = function(clockwise) {
|
||||
pairOrientation += (clockwise ? 1 : -1);
|
||||
if (pairOrientation > 3)
|
||||
pairOrientation = 0;
|
||||
if (pairOrientation < 0)
|
||||
pairOrientation = 3;
|
||||
|
||||
if (pairOrientation == 1 && pairX == 5)
|
||||
pairX = 4;
|
||||
if (pairOrientation == 3 && pairX == 0)
|
||||
pairX = 1;
|
||||
|
||||
calcDropCoordinates();
|
||||
};
|
||||
|
||||
/*
|
||||
Places the current pair at the drop coordinates.
|
||||
*/
|
||||
let quickDrop = function() {
|
||||
if (dropCoordinates.pivotY == -1) {
|
||||
hiddenRow[dropCoordinates.pivotX] = currentPair.pivot;
|
||||
} else if (dropCoordinates.pivotY > -1) {
|
||||
grid[dropCoordinates.pivotY][dropCoordinates.pivotX] = currentPair.pivot;
|
||||
}
|
||||
|
||||
if (dropCoordinates.leafY == -1) {
|
||||
hiddenRow[dropCoordinates.leafX] = currentPair.leaf;
|
||||
} else if (dropCoordinates.leafY > -1) {
|
||||
grid[dropCoordinates.leafY][dropCoordinates.leafX] = currentPair.leaf;
|
||||
}
|
||||
|
||||
currentPair.pivot = 0;
|
||||
currentPair.leaf = 0;
|
||||
};
|
||||
|
||||
/*
|
||||
Makes all blobs fall to the lowest available slot.
|
||||
All blobs that fall will be added to slotsToCheck.
|
||||
*/
|
||||
let settleBlobs = function() {
|
||||
for (let x = 0; x < 6; x++) {
|
||||
let lowestOpen = 11;
|
||||
for (let y = 11; y >= 0; y--) {
|
||||
if (grid[y][x] != 0) {
|
||||
if (y != lowestOpen) {
|
||||
grid[lowestOpen][x] = grid[y][x];
|
||||
grid[y][x] = 0;
|
||||
addSlotToCheck(x, lowestOpen);
|
||||
}
|
||||
lowestOpen--;
|
||||
}
|
||||
}
|
||||
|
||||
if (lowestOpen >= 0 && hiddenRow[x] != 0) {
|
||||
grid[lowestOpen][x] = hiddenRow[x];
|
||||
hiddenRow[x] = 0;
|
||||
addSlotToCheck(x, lowestOpen);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
Adds a slot to slotsToCheck. This slot will be checked for a pop
|
||||
next time popAll is called.
|
||||
*/
|
||||
let addSlotToCheck = function(x, y) {
|
||||
slotsToCheck.push({x: x, y: y});
|
||||
};
|
||||
|
||||
/*
|
||||
Checks for a pop at every slot in slotsToCheck.
|
||||
Pops at all locations.
|
||||
*/
|
||||
let popAll = function() {
|
||||
let result = {pops: 0};
|
||||
while(slotsToCheck.length > 0) {
|
||||
let coord = slotsToCheck.pop();
|
||||
if (grid[coord.y][coord.x] != 0 && grid[coord.y][coord.x] != NUISANCE_COLOR) {
|
||||
if (checkSlotForPop(coord.x, coord.y))
|
||||
result.pops += 1;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
/*
|
||||
Checks a specific slot for a pop.
|
||||
If there are four or more adjacent blobs of the same color, they are removed.
|
||||
*/
|
||||
let checkSlotForPop = function(x, y) {
|
||||
let toDelete = [
|
||||
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||
new Uint8Array([0, 0, 0, 0, 0, 0])
|
||||
];
|
||||
let blobsInClump = 0;
|
||||
let color = grid[y][x];
|
||||
let toCheck = [{x: x, y: y}];
|
||||
|
||||
//Count every blob in this clump
|
||||
while (toCheck.length > 0) {
|
||||
let coord = toCheck.pop();
|
||||
if (grid[coord.y][coord.x] == color && toDelete[coord.y][coord.x] == 0) {
|
||||
blobsInClump++;
|
||||
toDelete[coord.y][coord.x] = 1;
|
||||
if (coord.x > 0) toCheck.push({x: coord.x - 1, y: coord.y});
|
||||
if (coord.x < 5) toCheck.push({x: coord.x + 1, y: coord.y});
|
||||
if (coord.y > 0) toCheck.push({x: coord.x, y: coord.y - 1});
|
||||
if (coord.y < 11) toCheck.push({x: coord.x, y: coord.y + 1});
|
||||
}
|
||||
if (grid[coord.y][coord.x] == NUISANCE_COLOR && toDelete[coord.y][coord.x] == 0)
|
||||
toDelete[coord.y][coord.x] = 1; //For erasing garbage
|
||||
}
|
||||
|
||||
//If there are at least four blobs in this clump, remove them from the grid and draw a pop.
|
||||
if (blobsInClump >= 4) {
|
||||
for (let y = 0; y < 12; y++) {
|
||||
for (let x = 0; x < 6; x++) {
|
||||
if (toDelete[y][x] == 1) {
|
||||
grid[y][x] = 0;
|
||||
|
||||
//Clear the blob out of the slot
|
||||
g.setBgColor(0, 0, 0);
|
||||
g.clearRect((x*18)+34, (y*14)+7, (x*18)+52, (y*14)+21);
|
||||
|
||||
//Draw the pop
|
||||
let colorInfo = getColor(color);
|
||||
g.setColor(colorInfo.r, colorInfo.g, colorInfo.b);
|
||||
if (color < NUISANCE_COLOR) {
|
||||
//A fancy pop for popped colors!
|
||||
g.drawEllipse((x*18)+36, (y*14)+7, (x*18)+50, (y*14)+21);
|
||||
g.drawEllipse((x*18)+27, (y*14)-2, (x*18)+59, (y*14)+30);
|
||||
} else if (color == NUISANCE_COLOR) {
|
||||
//Nuisance Blobs are simply crossed out.
|
||||
//TODO: Nuisance Blobs are currently unusued, but also untested. Test before use.
|
||||
g.drawLine((x*18)+34, (y*14)+7, (x*18)+52, (y*14)+21);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// Variables for graphics
|
||||
let oldGhost = {pivotX: 0, pivotY: 0, leafX: 0, leafY: 0};
|
||||
|
||||
/*
|
||||
Draws the time on the side.
|
||||
*/
|
||||
let drawTime = function(scheduleNext) {
|
||||
//Change this to alter the y-coordinate of the top edge.
|
||||
let dy = 25;
|
||||
|
||||
g.setBgColor(0, 0, 0);
|
||||
g.clearRect(2, dy, 30, dy + 121);
|
||||
|
||||
//Draw the time
|
||||
let d = new Date();
|
||||
let h = d.getHours(), m = d.getMinutes();
|
||||
if (twelveHourTime) {
|
||||
let mer = 'A';
|
||||
if (h >= 12) mer = 'P';
|
||||
if (h >= 13) h -= 12;
|
||||
if (h == 0) h = 12;
|
||||
|
||||
g.setColor(1, 1, 1);
|
||||
g.setFont("Vector", 12);
|
||||
g.drawString(mer, 23, dy + 63);
|
||||
}
|
||||
let hs = h.toString().padStart(2, 0);
|
||||
let ms = m.toString().padStart(2, 0);
|
||||
g.setFont("Vector", 24);
|
||||
g.setColor(1, 0.2, 1);
|
||||
g.drawString(hs, 3, dy + 21);
|
||||
g.setColor(0.5, 0.5, 1);
|
||||
g.drawString(ms, 3, dy + 42);
|
||||
|
||||
//Draw seconds
|
||||
let s = d.getSeconds();
|
||||
if (updateSeconds) {
|
||||
let ss = s.toString().padStart(2, 0);
|
||||
g.setFont("Vector", 12);
|
||||
g.setColor(0.2, 1, 0.2);
|
||||
g.drawString(ss, 3, dy + 63);
|
||||
}
|
||||
|
||||
//Draw the date
|
||||
let dayString = d.getDate().toString();
|
||||
let dayNames = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"];
|
||||
let dayName = dayNames[d.getDay()];
|
||||
let monthNames = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JLY", "AUG", "SEP", "OCT", "NOV", "DEC"];
|
||||
let monthName = monthNames[d.getMonth()];
|
||||
g.setColor(1, 1, 1);
|
||||
g.setFont("Vector", 12);
|
||||
g.drawString(monthName, 3, dy + 84);
|
||||
g.drawString(dayString, 3, dy + 97);
|
||||
g.setColor(0.5, 0.5, 0.5);
|
||||
g.drawString(dayName, 3, dy + 110);
|
||||
|
||||
//Draw battery
|
||||
if (s == 0) batteryLevel = E.getBattery();
|
||||
if (Bangle.isCharging()) {
|
||||
g.setColor(0, 0, 1);
|
||||
} else if (batteryLevel <= 15) {
|
||||
g.setColor(1, 0, 0);
|
||||
} else {
|
||||
g.setColor(0, 1, 0);
|
||||
}
|
||||
g.drawString(batteryLevel + "%", 3, dy + 1);
|
||||
|
||||
//Schedule the next draw if requested.
|
||||
if (!scheduleNext) return;
|
||||
if (clockDrawTimeout) clearTimeout(clockDrawTimeout);
|
||||
let interval = updateSeconds ? 1000 : 60000;
|
||||
clockDrawTimeout = setTimeout(function() {
|
||||
clockDrawTimeout = undefined;
|
||||
drawTime(true);
|
||||
}, interval - (Date.now() % interval));
|
||||
};
|
||||
|
||||
/*
|
||||
Returns a tuple in the format {r, g, b} with the color
|
||||
of the blob with the given ID.
|
||||
This saves memory compared to having the colors stored in an array.
|
||||
*/
|
||||
let getColor = function(color) {
|
||||
if (color == 1)
|
||||
return {r: 1, g: 0, b: 0};
|
||||
if (color == 2)
|
||||
return {r: 0, g: 1, b: 0};
|
||||
if (color == 3)
|
||||
return {r: 0, g: 0, b: 1};
|
||||
if (color == 4)
|
||||
return {r: 1, g: 1, b: 0};
|
||||
if (color == 5)
|
||||
return {r: 1, g: 0, b: 1};
|
||||
if (color == 6)
|
||||
return {r: 0, g: 1, b: 1};
|
||||
if (color == 7)
|
||||
return {r: 0.5, g: 0.5, b: 0.5};
|
||||
return {r: 1, g: 1, b: 1};
|
||||
};
|
||||
|
||||
/*
|
||||
Clears the screen and draws the background.
|
||||
*/
|
||||
let drawBackground = function() {
|
||||
//Background
|
||||
g.setBgColor(0.5, 0.2, 0.1);
|
||||
g.clear();
|
||||
g.setBgColor(0, 0, 0);
|
||||
g.clearRect(33, 0, 142, 176);
|
||||
g.setBgColor(0.5, 0.5, 0.5);
|
||||
g.clearRect(33, 4, 142, 6);
|
||||
|
||||
//Reset button
|
||||
g.setBgColor(0.5, 0.5, 0.5);
|
||||
g.setColor(0, 0, 0);
|
||||
g.clearRect(143, 150, 175, 175);
|
||||
g.setFont("Vector", 30);
|
||||
g.drawString("R", 152, 150);
|
||||
|
||||
//Pause button
|
||||
g.clearRect(0, 150, 32, 175);
|
||||
g.fillRect(9, 154, 13, 171);
|
||||
g.fillRect(18, 154, 22, 171);
|
||||
};
|
||||
|
||||
/*
|
||||
Draws a box under the next queue that displays
|
||||
the current value of lastChain.
|
||||
*/
|
||||
let drawChainCount = function() {
|
||||
g.setBgColor(0, 0, 0);
|
||||
g.setColor(1, 0.2, 0.2);
|
||||
g.setFont("Vector", 23);
|
||||
g.clearRect(145, 42, 173, 64);
|
||||
|
||||
if (lastChain > 0) {
|
||||
if (lastChain < 10) g.drawString(lastChain, 154, 44);
|
||||
if (lastChain >= 10) g.drawString(lastChain, 147, 44);
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
Draws the blob at the given slot.
|
||||
*/
|
||||
let drawBlobAtSlot = function(x, y) {
|
||||
//If this blob is in the hidden row, clear it out and stop.
|
||||
if (y < 0) {
|
||||
g.setBgColor(0, 0, 0);
|
||||
g.clearRect((x*18)+34, 0, (x*18)+52, 3);
|
||||
return;
|
||||
}
|
||||
|
||||
//First, clear what was in that slot.
|
||||
g.setBgColor(0, 0, 0);
|
||||
g.clearRect((x*18)+34, (y*14)+7, (x*18)+52, (y*14)+21);
|
||||
|
||||
let color = grid[y][x];
|
||||
|
||||
if (color != 0) {
|
||||
let myColor = getColor(color);
|
||||
g.setColor(myColor.r, myColor.g, myColor.b);
|
||||
g.fillEllipse((x*18)+34, (y*14)+7, (x*18)+52, (y*14)+21);
|
||||
g.setColor(1, 1, 1);
|
||||
g.drawEllipse((x*18)+34, (y*14)+7, (x*18)+52, (y*14)+21);
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
Draws the ghost piece.
|
||||
clearOld: if the previous location of the ghost piece should be cleared.
|
||||
*/
|
||||
let drawGhostPiece = function(clearOld) {
|
||||
if (clearOld) {
|
||||
g.setColor(0, 0, 0);
|
||||
g.fillRect((oldGhost.pivotX*18)+38, (oldGhost.pivotY*14)+8, (oldGhost.pivotX*18)+47, (oldGhost.pivotY*14)+17);
|
||||
g.fillRect((oldGhost.leafX*18)+38, (oldGhost.leafY*14)+8, (oldGhost.leafX*18)+47, (oldGhost.leafY*14)+17);
|
||||
}
|
||||
|
||||
let pivotX = dropCoordinates.pivotX;
|
||||
let pivotY = dropCoordinates.pivotY;
|
||||
let leafX = dropCoordinates.leafX;
|
||||
let leafY = dropCoordinates.leafY;
|
||||
let pivotColor = getColor(currentPair.pivot);
|
||||
let leafColor = getColor(currentPair.leaf);
|
||||
|
||||
g.setColor(pivotColor.r, pivotColor.g, pivotColor.b);
|
||||
g.fillRect((pivotX*18)+40, (pivotY*14)+10, (pivotX*18)+45, (pivotY*14)+15);
|
||||
g.setColor(1, 1, 1);
|
||||
g.drawRect((pivotX*18)+38, (pivotY*14)+8, (pivotX*18)+47, (pivotY*14)+17);
|
||||
g.setColor(leafColor.r, leafColor.g, leafColor.b);
|
||||
g.fillRect((leafX*18)+40, (leafY*14)+10, (leafX*18)+45, (leafY*14)+15);
|
||||
|
||||
oldGhost = {pivotX: pivotX, pivotY: pivotY, leafX: leafX, leafY: leafY};
|
||||
};
|
||||
|
||||
/*
|
||||
Draws the next queue.
|
||||
*/
|
||||
let drawNextQueue = function() {
|
||||
g.setBgColor(0, 0, 0);
|
||||
g.clearRect(145, 4, 173, 28);
|
||||
|
||||
let p1 = nextQueue[0].pivot;
|
||||
let l1 = nextQueue[0].leaf;
|
||||
let p2 = nextQueue[1].pivot;
|
||||
let l2 = nextQueue[1].leaf;
|
||||
let p1C = getColor(p1);
|
||||
let l1C = getColor(l1);
|
||||
let p2C = getColor(p2);
|
||||
let l2C = getColor(l2);
|
||||
|
||||
g.setColor(p1C.r, p1C.g, p1C.b);
|
||||
g.fillEllipse(146, 17, 157, 28);
|
||||
g.setColor(l1C.r, l1C.g, l1C.b);
|
||||
g.fillEllipse(146, 5, 157, 16);
|
||||
g.setColor(p2C.r, p2C.g, p2C.b);
|
||||
g.fillEllipse(162, 17, 173, 28);
|
||||
g.setColor(l2C.r, l2C.g, l2C.b);
|
||||
g.fillEllipse(162, 5, 173, 16);
|
||||
|
||||
g.setColor(1, 1, 1);
|
||||
g.drawLine(159, 4, 159, 28);
|
||||
g.drawEllipse(146, 17, 157, 28);
|
||||
g.drawEllipse(146, 5, 157, 16);
|
||||
g.drawEllipse(162, 17, 173, 28);
|
||||
g.drawEllipse(162, 5, 173, 16);
|
||||
};
|
||||
|
||||
/*
|
||||
Redraws the screen, except for the ghost piece.
|
||||
*/
|
||||
let redrawBoard = function() {
|
||||
drawBackground();
|
||||
drawNextQueue();
|
||||
drawChainCount();
|
||||
drawTime(false);
|
||||
for (let y = 0; y < 12; y++) {
|
||||
for (let x = 0; x < 6; x++) {
|
||||
drawBlobAtSlot(x, y);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
Toggles the pause screen.
|
||||
*/
|
||||
let togglePause = function() {
|
||||
gamePaused = !gamePaused;
|
||||
|
||||
if (gamePaused) {
|
||||
g.setBgColor(0.5, 0.2, 0.1);
|
||||
g.clear();
|
||||
drawTime(false);
|
||||
|
||||
g.setBgColor(0, 0, 0);
|
||||
g.setColor(1, 1, 1);
|
||||
g.clearRect(48, 66, 157, 110);
|
||||
g.setFont("Vector", 20);
|
||||
g.drawString("Tap here\nto unpause", 50, 68);
|
||||
|
||||
require("widget_utils").show();
|
||||
Bangle.drawWidgets();
|
||||
} else {
|
||||
require("widget_utils").hide();
|
||||
|
||||
redrawBoard();
|
||||
drawGhostPiece(false);
|
||||
|
||||
//Display the loss text if the game is lost.
|
||||
if (gameLost) {
|
||||
g.setBgColor(0, 0, 0);
|
||||
g.setColor(1, 1, 1);
|
||||
g.clearRect(33, 73, 142, 103);
|
||||
g.setFont("Vector", 20);
|
||||
g.drawString("You Lose", 43, 80);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ~~ Events ~~
|
||||
let dragAmnt = 0;
|
||||
|
||||
let onTouch = (z, e) => {
|
||||
if (midChain) return;
|
||||
|
||||
if (gamePaused) {
|
||||
if (e.x >= 40 && e.y >= 58 && e.x <= 165 && e.y <= 118) {
|
||||
g.setBgColor(1, 1, 1);
|
||||
g.clearRect(48, 66, 157, 110);
|
||||
g.flip();
|
||||
togglePause();
|
||||
}
|
||||
} else {
|
||||
//Tap reset button
|
||||
if (e.x >= 143 && e.y >= 150) {
|
||||
restartGame();
|
||||
newPair();
|
||||
redrawBoard();
|
||||
drawGhostPiece(false);
|
||||
g.flip();
|
||||
return;
|
||||
}
|
||||
|
||||
//Tap pause button
|
||||
if (e.x <= 32 && e.y >= 150) {
|
||||
togglePause();
|
||||
return;
|
||||
}
|
||||
|
||||
//While playing, rotate pieces.
|
||||
if (!gameLost && !gamePaused) {
|
||||
if (e.x < 88) {
|
||||
rotatePair(false);
|
||||
drawGhostPiece(true);
|
||||
} else {
|
||||
rotatePair(true);
|
||||
drawGhostPiece(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Bangle.on("touch", onTouch);
|
||||
|
||||
let onDrag = (e) => {
|
||||
if (gameLost || gamePaused || midChain) return;
|
||||
|
||||
//Do nothing if the user is dragging down so that they don't accidentally move while dropping
|
||||
if (e.dy >= 5) {
|
||||
return;
|
||||
}
|
||||
|
||||
dragAmnt += e.dx;
|
||||
if (e.b == 0) {
|
||||
dragAmnt = 0;
|
||||
}
|
||||
if (dragAmnt >= 20) {
|
||||
movePair(Math.floor(dragAmnt / 20));
|
||||
drawGhostPiece(true);
|
||||
dragAmnt = dragAmnt % 20;
|
||||
}
|
||||
if (dragAmnt <= -20) {
|
||||
movePair(Math.ceil(dragAmnt / 20));
|
||||
drawGhostPiece(true);
|
||||
dragAmnt = dragAmnt % 20;
|
||||
}
|
||||
};
|
||||
|
||||
Bangle.on("drag", onDrag);
|
||||
|
||||
let onSwipe = (x, y) => {
|
||||
if (gameLost || gamePaused || midChain) return;
|
||||
|
||||
if (y > 0) {
|
||||
let pivotX = dropCoordinates.pivotX;
|
||||
let pivotY = dropCoordinates.pivotY;
|
||||
let leafX = dropCoordinates.leafX;
|
||||
let leafY = dropCoordinates.leafY;
|
||||
|
||||
if (pivotY < -1 && leafY < -1) return;
|
||||
|
||||
quickDrop();
|
||||
drawBlobAtSlot(pivotX, pivotY);
|
||||
drawBlobAtSlot(leafX, leafY);
|
||||
g.flip();
|
||||
|
||||
//Check for pops
|
||||
if (pivotY >= 0) addSlotToCheck(pivotX, pivotY);
|
||||
if (leafY >= 0) addSlotToCheck(leafX, leafY);
|
||||
midChain = true;
|
||||
let currentChain = 0;
|
||||
while (popAll().pops > 0) {
|
||||
currentChain++;
|
||||
lastChain = currentChain;
|
||||
drawChainCount();
|
||||
g.flip();
|
||||
settleBlobs();
|
||||
redrawBoard();
|
||||
g.flip();
|
||||
}
|
||||
|
||||
newPair();
|
||||
drawNextQueue();
|
||||
drawGhostPiece(false);
|
||||
|
||||
//If the top slot of the third column is taken, lose the game.
|
||||
if (grid[0][2] != 0) {
|
||||
gameLost = true;
|
||||
g.setBgColor(0, 0, 0);
|
||||
g.setColor(1, 1, 1);
|
||||
g.clearRect(33, 73, 142, 103);
|
||||
g.setFont("Vector", 20);
|
||||
g.drawString("You Lose", 43, 80);
|
||||
}
|
||||
|
||||
midChain = false;
|
||||
}
|
||||
};
|
||||
|
||||
Bangle.on("swipe", onSwipe);
|
||||
|
||||
let onLock = on => {
|
||||
updateSeconds = !on;
|
||||
drawTime(true);
|
||||
};
|
||||
|
||||
Bangle.on('lock', onLock);
|
||||
|
||||
let onCharging = charging => {
|
||||
drawTime(false);
|
||||
};
|
||||
|
||||
Bangle.on('charging', onCharging);
|
||||
|
||||
Bangle.setUI({mode:"clock", remove:function() {
|
||||
//Remove listeners
|
||||
Bangle.removeListener("touch", onTouch);
|
||||
Bangle.removeListener("drag", onDrag);
|
||||
Bangle.removeListener("swipe", onSwipe);
|
||||
Bangle.removeListener('lock', onLock);
|
||||
Bangle.removeListener('charging', onCharging);
|
||||
|
||||
if (clockDrawTimeout) clearTimeout(clockDrawTimeout);
|
||||
require("widget_utils").show();
|
||||
}});
|
||||
|
||||
g.reset();
|
||||
|
||||
Bangle.loadWidgets();
|
||||
require("widget_utils").hide();
|
||||
|
||||
drawBackground();
|
||||
drawTime(true);
|
||||
|
||||
restartGame();
|
||||
|
||||
newPair();
|
||||
drawGhostPiece(false);
|
||||
|
||||
drawNextQueue();
|
||||
drawChainCount();
|
||||
}
|
||||
|
After Width: | Height: | Size: 691 B |
|
|
@ -0,0 +1,15 @@
|
|||
{ "id": "bblobface",
|
||||
"name": "Bangle Blobs Clock",
|
||||
"shortName":"BBClock",
|
||||
"icon": "app.png",
|
||||
"version": "1.00",
|
||||
"description": "A fully featured watch face with a playable game on the side.",
|
||||
"readme":"README.md",
|
||||
"type": "clock",
|
||||
"tags": "clock, game",
|
||||
"supports" : ["BANGLEJS2"],
|
||||
"storage": [
|
||||
{"name":"bblobface.app.js","url":"app.js"},
|
||||
{"name":"bblobface.img","url":"app-icon.js","evaluate":true}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
|
@ -3,3 +3,4 @@
|
|||
0.03: Use default Bangle formatter for booleans
|
||||
0.04: Add options for units in locale and recording GPS
|
||||
0.05: Allow toggling of "max" values (screen tap) and recording (button press)
|
||||
0.06: Fix local unit setting
|
||||
|
|
|
|||
|
|
@ -420,7 +420,7 @@ function updateClock() {
|
|||
// Read settings.
|
||||
let cfg = require('Storage').readJSON('bikespeedo.json',1)||{};
|
||||
|
||||
cfg.spd = !cfg.localeUnits; // Multiplier for speed unit conversions. 0 = use the locale values for speed
|
||||
cfg.spd = cfg.localeUnits ? 0 : 1; // Multiplier for speed unit conversions. 0 = use the locale values for speed
|
||||
cfg.spd_unit = 'km/h'; // Displayed speed unit
|
||||
cfg.alt = 1; // Multiplier for altitude unit conversions. (feet:'0.3048')
|
||||
cfg.alt_unit = 'm'; // Displayed altitude units ('feet')
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"id": "bikespeedo",
|
||||
"name": "Bike Speedometer (beta)",
|
||||
"shortName": "Bike Speedometer",
|
||||
"version": "0.05",
|
||||
"version": "0.06",
|
||||
"description": "Shows GPS speed, GPS heading, Compass heading, GPS altitude and Barometer altitude from internal sources",
|
||||
"icon": "app.png",
|
||||
"screenshots": [{"url":"Screenshot.png"}],
|
||||
|
|
|
|||
|
|
@ -67,3 +67,4 @@
|
|||
0.56: Settings.log = 0,1,2,3 for off,display, log, both
|
||||
0.57: Handle the whitelist being disabled
|
||||
0.58: "Make Connectable" temporarily bypasses the whitelist
|
||||
0.59: Whitelist: Try to resolve peer addresses using NRF.resolveAddress() - for 2v19 or 2v18 cutting edge builds
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ if (global.save) boot += `global.save = function() { throw new Error("You can't
|
|||
if (s.options) boot+=`Bangle.setOptions(${E.toJS(s.options)});\n`;
|
||||
if (s.brightness && s.brightness!=1) boot+=`Bangle.setLCDBrightness(${s.brightness});\n`;
|
||||
if (s.passkey!==undefined && s.passkey.length==6) boot+=`NRF.setSecurity({passkey:${E.toJS(s.passkey.toString())}, mitm:1, display:1});\n`;
|
||||
if (s.whitelist && !s.whitelist_disabled) boot+=`NRF.on('connect', function(addr) { if (!NRF.ignoreWhitelist && !(require('Storage').readJSON('setting.json',1)||{}).whitelist.includes(addr)) NRF.disconnect(); });\n`;
|
||||
if (s.whitelist && !s.whitelist_disabled) boot+=`NRF.on('connect', function(addr) { if (!NRF.ignoreWhitelist) { let whitelist = (require('Storage').readJSON('setting.json',1)||{}).whitelist; if (NRF.resolveAddress !== undefined) { let resolvedAddr = NRF.resolveAddress(addr); if (resolvedAddr !== undefined) addr = resolvedAddr + " (resolved)"; } if (!whitelist.includes(addr)) NRF.disconnect(); }});\n`;
|
||||
if (s.rotate) boot+=`g.setRotation(${s.rotate&3},${s.rotate>>2});\n` // screen rotation
|
||||
// ================================================== FIXING OLDER FIRMWARES
|
||||
if (FWVERSION<215.068) // 2v15.68 and before had compass heading inverted.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "boot",
|
||||
"name": "Bootloader",
|
||||
"version": "0.58",
|
||||
"version": "0.59",
|
||||
"description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings",
|
||||
"icon": "bootloader.png",
|
||||
"type": "bootloader",
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
0.01: Initial release.
|
||||
0.02: Handle the case where other apps have set bleAdvert to an array
|
||||
|
|
|
|||
|
|
@ -1,6 +1,22 @@
|
|||
(() => {
|
||||
function advertiseBattery() {
|
||||
Bangle.bleAdvert[0x180F] = [E.getBattery()];
|
||||
if(Array.isArray(Bangle.bleAdvert)){
|
||||
// ensure we're in the cycle
|
||||
var found = false;
|
||||
for(var ad in Bangle.bleAdvert){
|
||||
if(ad[0x180F]){
|
||||
ad[0x180F] = [E.getBattery()];
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!found)
|
||||
Bangle.bleAdvert.push({ 0x180F: [E.getBattery()] });
|
||||
}else{
|
||||
// simple object
|
||||
Bangle.bleAdvert[0x180F] = [E.getBattery()];
|
||||
}
|
||||
|
||||
NRF.setAdvertising(Bangle.bleAdvert);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"id": "bootgattbat",
|
||||
"name": "BLE GATT Battery Service",
|
||||
"shortName": "BLE Battery Service",
|
||||
"version": "0.01",
|
||||
"version": "0.02",
|
||||
"description": "Adds the GATT Battery Service to advertise the percentage of battery currently remaining over Bluetooth.\n",
|
||||
"icon": "bluetooth.png",
|
||||
"type": "bootloader",
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
0.01: New app!
|
||||
0.02: Advertise accelerometer data and sensor location
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
var _a;
|
||||
{
|
||||
var __assign = Object.assign;
|
||||
var Layout_1 = require("Layout");
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
var HRM_MIN_CONFIDENCE_1 = 75;
|
||||
var services_1 = ["0x180d", "0x181a", "0x1819"];
|
||||
var services_1 = [
|
||||
"0x180d",
|
||||
"0x181a",
|
||||
"0x1819",
|
||||
"E95D0753251D470AA062FA1922DFA9A8",
|
||||
];
|
||||
var acc_1;
|
||||
var bar_1;
|
||||
var gps_1;
|
||||
|
|
@ -21,7 +27,6 @@
|
|||
mag: false,
|
||||
};
|
||||
var idToName = {
|
||||
acc: "Acceleration",
|
||||
bar: "Barometer",
|
||||
gps: "GPS",
|
||||
hrm: "HRM",
|
||||
|
|
@ -69,7 +74,6 @@
|
|||
{
|
||||
type: "h",
|
||||
c: [
|
||||
__assign(__assign({ type: "btn", label: idToName.acc, id: "acc", cb: function () { } }, btnStyle), { col: colour_1.on, btnBorder: colour_1.on }),
|
||||
__assign({ type: "btn", label: "Back", cb: function () {
|
||||
setBtnsShown_1(false);
|
||||
} }, btnStyle),
|
||||
|
|
@ -222,6 +226,13 @@
|
|||
return [x[0], x[1], y[0], y[1], z[0], z[1]];
|
||||
};
|
||||
encodeMag_1.maxLen = 6;
|
||||
var encodeAcc_1 = function (data) {
|
||||
var x = toByteArray_1(data.x * 1000, 2, true);
|
||||
var y = toByteArray_1(data.y * 1000, 2, true);
|
||||
var z = toByteArray_1(data.z * 1000, 2, true);
|
||||
return [x[0], x[1], y[0], y[1], z[0], z[1]];
|
||||
};
|
||||
encodeAcc_1.maxLen = 6;
|
||||
var toByteArray_1 = function (value, numberOfBytes, isSigned) {
|
||||
var byteArray = new Array(numberOfBytes);
|
||||
if (isSigned && (value < 0)) {
|
||||
|
|
@ -251,6 +262,7 @@
|
|||
case "0x180d": return !!hrm_1;
|
||||
case "0x181a": return !!(bar_1 || mag_1);
|
||||
case "0x1819": return !!(gps_1 && gps_1.lat && gps_1.lon || mag_1);
|
||||
case "E95D0753251D470AA062FA1922DFA9A8": return !!acc_1;
|
||||
}
|
||||
};
|
||||
var serviceToAdvert_1 = function (serv, initial) {
|
||||
|
|
@ -264,11 +276,20 @@
|
|||
readable: true,
|
||||
notify: true,
|
||||
};
|
||||
var os = {
|
||||
maxLen: 1,
|
||||
readable: true,
|
||||
notify: true,
|
||||
};
|
||||
if (hrm_1) {
|
||||
o.value = encodeHrm_1(hrm_1);
|
||||
os.value = [2];
|
||||
hrm_1 = undefined;
|
||||
}
|
||||
return _a = {}, _a["0x2a37"] = o, _a;
|
||||
return _a = {},
|
||||
_a["0x2a37"] = o,
|
||||
_a["0x2a38"] = os,
|
||||
_a;
|
||||
}
|
||||
return {};
|
||||
case "0x1819":
|
||||
|
|
@ -331,6 +352,21 @@
|
|||
}
|
||||
return o;
|
||||
}
|
||||
case "E95D0753251D470AA062FA1922DFA9A8": {
|
||||
var o = {};
|
||||
if (acc_1 || initial) {
|
||||
o["E95DCA4B251D470AA062FA1922DFA9A8"] = {
|
||||
maxLen: encodeAcc_1.maxLen,
|
||||
readable: true,
|
||||
notify: true,
|
||||
};
|
||||
if (acc_1) {
|
||||
o["E95DCA4B251D470AA062FA1922DFA9A8"].value = encodeAcc_1(acc_1);
|
||||
acc_1 = undefined;
|
||||
}
|
||||
}
|
||||
return o;
|
||||
}
|
||||
}
|
||||
};
|
||||
var getBleAdvert_1 = function (map, all) {
|
||||
|
|
@ -402,12 +438,23 @@
|
|||
enableSensors_1();
|
||||
{
|
||||
var ad = getBleAdvert_1(function (serv) { return serviceToAdvert_1(serv, true); }, true);
|
||||
var adServices = Object
|
||||
.keys(ad)
|
||||
.map(function (k) { return k.replace("0x", ""); });
|
||||
NRF.setServices(ad, {
|
||||
advertise: adServices,
|
||||
uart: false,
|
||||
});
|
||||
var bangle2 = Bangle;
|
||||
var cycle = Array.isArray(bangle2.bleAdvert) ? bangle2.bleAdvert : [];
|
||||
for (var id in ad) {
|
||||
var serv = ad[id];
|
||||
var value = void 0;
|
||||
for (var ch in serv) {
|
||||
value = serv[ch].value;
|
||||
break;
|
||||
}
|
||||
cycle.push((_a = {}, _a[id] = value || [], _a));
|
||||
}
|
||||
bangle2.bleAdvert = cycle;
|
||||
NRF.setAdvertising(cycle, {
|
||||
interval: 100,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,16 +33,27 @@ const enum BleServ {
|
|||
// contains: LocationAndSpeed
|
||||
LocationAndNavigation = "0x1819",
|
||||
|
||||
// Acc // none known for this
|
||||
// org.microbit.service.accelerometer
|
||||
// contains: Acc
|
||||
Acc = "E95D0753251D470AA062FA1922DFA9A8",
|
||||
}
|
||||
|
||||
const services = [BleServ.HRM, BleServ.EnvSensing, BleServ.LocationAndNavigation];
|
||||
const services = [
|
||||
BleServ.HRM,
|
||||
BleServ.EnvSensing,
|
||||
BleServ.LocationAndNavigation,
|
||||
BleServ.Acc,
|
||||
];
|
||||
|
||||
const enum BleChar {
|
||||
// org.bluetooth.characteristic.heart_rate_measurement
|
||||
// <see encode function>
|
||||
HRM = "0x2a37",
|
||||
|
||||
// org.bluetooth.characteristic.body_sensor_location
|
||||
// u8
|
||||
SensorLocation = "0x2a38",
|
||||
|
||||
// org.bluetooth.characteristic.elevation
|
||||
// s24, meters 0.01
|
||||
Elevation = "0x2a6c",
|
||||
|
|
@ -65,6 +76,11 @@ const enum BleChar {
|
|||
// org.bluetooth.characteristic.magnetic_flux_density_3d
|
||||
// s16: x, y, z, tesla (10^-7)
|
||||
MagneticFlux3D = "0x2aa1",
|
||||
|
||||
// org.microbit.characteristic.accelerometer_data
|
||||
// s16 x3, -1024 .. 1024
|
||||
// docs: https://lancaster-university.github.io/microbit-docs/ble/accelerometer-service/
|
||||
Acc = "E95DCA4B251D470AA062FA1922DFA9A8",
|
||||
}
|
||||
|
||||
type BleCharAdvert = {
|
||||
|
|
@ -84,6 +100,16 @@ type LenFunc<T> = {
|
|||
maxLen: number,
|
||||
}
|
||||
|
||||
const enum SensorLocations {
|
||||
Other = 0,
|
||||
Chest = 1,
|
||||
Wrist = 2,
|
||||
Finger = 3,
|
||||
Hand = 4,
|
||||
EarLobe = 5,
|
||||
Foot = 6,
|
||||
}
|
||||
|
||||
let acc: undefined | AccelData;
|
||||
let bar: undefined | PressureData;
|
||||
let gps: undefined | GPSFix;
|
||||
|
|
@ -104,8 +130,7 @@ const settings: BtAdvMap<boolean> = {
|
|||
mag: false,
|
||||
};
|
||||
|
||||
const idToName: BtAdvMap<string, true> = {
|
||||
acc: "Acceleration",
|
||||
const idToName: BtAdvMap<string> = {
|
||||
bar: "Barometer",
|
||||
gps: "GPS",
|
||||
hrm: "HRM",
|
||||
|
|
@ -197,15 +222,6 @@ const btnLayout = new Layout(
|
|||
{
|
||||
type: "h",
|
||||
c: [
|
||||
{
|
||||
type: "btn",
|
||||
label: idToName.acc,
|
||||
id: "acc",
|
||||
cb: () => {},
|
||||
...btnStyle,
|
||||
col: colour.on,
|
||||
btnBorder: colour.on,
|
||||
},
|
||||
{
|
||||
type: "btn",
|
||||
label: "Back",
|
||||
|
|
@ -464,6 +480,15 @@ const encodeMag: LenFunc<CompassData> = (data: CompassData) => {
|
|||
};
|
||||
encodeMag.maxLen = 6;
|
||||
|
||||
const encodeAcc: LenFunc<AccelData> = (data: AccelData) => {
|
||||
const x = toByteArray(data.x * 1000, 2, true);
|
||||
const y = toByteArray(data.y * 1000, 2, true);
|
||||
const z = toByteArray(data.z * 1000, 2, true);
|
||||
|
||||
return [ x[0]!, x[1]!, y[0]!, y[1]!, z[0]!, z[1]! ];
|
||||
};
|
||||
encodeAcc.maxLen = 6;
|
||||
|
||||
const toByteArray = (value: number, numberOfBytes: number, isSigned: boolean) => {
|
||||
const byteArray: Array<number> = new Array(numberOfBytes);
|
||||
|
||||
|
|
@ -503,6 +528,7 @@ const haveServiceData = (serv: BleServ): boolean => {
|
|||
case BleServ.HRM: return !!hrm;
|
||||
case BleServ.EnvSensing: return !!(bar || mag);
|
||||
case BleServ.LocationAndNavigation: return !!(gps && gps.lat && gps.lon || mag);
|
||||
case BleServ.Acc: return !!acc;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -515,12 +541,22 @@ const serviceToAdvert = (serv: BleServ, initial = false): BleServAdvert => {
|
|||
readable: true,
|
||||
notify: true,
|
||||
};
|
||||
const os: BleCharAdvert = {
|
||||
maxLen: 1,
|
||||
readable: true,
|
||||
notify: true,
|
||||
};
|
||||
|
||||
if (hrm) {
|
||||
o.value = encodeHrm(hrm);
|
||||
os.value = [SensorLocations.Wrist];
|
||||
hrm = undefined;
|
||||
}
|
||||
|
||||
return { [BleChar.HRM]: o };
|
||||
return {
|
||||
[BleChar.HRM]: o,
|
||||
[BleChar.SensorLocation]: os,
|
||||
};
|
||||
}
|
||||
return {};
|
||||
|
||||
|
|
@ -591,6 +627,25 @@ const serviceToAdvert = (serv: BleServ, initial = false): BleServAdvert => {
|
|||
|
||||
return o;
|
||||
}
|
||||
|
||||
case BleServ.Acc: {
|
||||
const o: BleServAdvert = {};
|
||||
|
||||
if (acc || initial) {
|
||||
o[BleChar.Acc] = {
|
||||
maxLen: encodeAcc.maxLen,
|
||||
readable: true,
|
||||
notify: true,
|
||||
};
|
||||
|
||||
if (acc) {
|
||||
o[BleChar.Acc]!.value = encodeAcc(acc);
|
||||
acc = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return o;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -702,16 +757,39 @@ enableSensors();
|
|||
// must have fixed services from the start:
|
||||
const ad = getBleAdvert(serv => serviceToAdvert(serv, true), /*all*/true);
|
||||
|
||||
const adServices = Object
|
||||
.keys(ad)
|
||||
.map((k: string) => k.replace("0x", ""));
|
||||
|
||||
NRF.setServices(
|
||||
ad,
|
||||
{
|
||||
advertise: adServices,
|
||||
uart: false,
|
||||
},
|
||||
);
|
||||
|
||||
type BleAdvert = { [key: string]: number[] };
|
||||
const bangle2 = Bangle as {
|
||||
bleAdvert?: BleAdvert | BleAdvert[];
|
||||
};
|
||||
const cycle = Array.isArray(bangle2.bleAdvert) ? bangle2.bleAdvert : [];
|
||||
|
||||
for(const id in ad){
|
||||
const serv = ad[id as BleServ];
|
||||
let value;
|
||||
|
||||
// pick the first characteristic to advertise
|
||||
for(const ch in serv){
|
||||
value = serv[ch as BleChar]!.value;
|
||||
break;
|
||||
}
|
||||
|
||||
cycle.push({ [id]: value || [] });
|
||||
}
|
||||
|
||||
bangle2.bleAdvert = cycle;
|
||||
|
||||
NRF.setAdvertising(
|
||||
cycle,
|
||||
{
|
||||
interval: 100,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"id": "btadv",
|
||||
"name": "btadv",
|
||||
"shortName": "btadv",
|
||||
"version": "0.01",
|
||||
"version": "0.02",
|
||||
"description": "Advertise & export live heart rate, accel, pressure, GPS & mag data over bluetooth",
|
||||
"icon": "icon.png",
|
||||
"tags": "health,tool,sensors,bluetooth",
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
0.01: New App!
|
||||
0.02: Handle the case where other apps have set bleAdvert to an array
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ function onTemperature(p) {
|
|||
var temp100 = Math.round(avrTemp*100);
|
||||
var pressure100 = Math.round(avrPressure*100);
|
||||
|
||||
Bangle.bleAdvert[0xFCD2] = [ 0x40, /* BTHome Device Information
|
||||
var advert = [ 0x40, /* BTHome Device Information
|
||||
bit 0: "Encryption flag"
|
||||
bit 1-4: "Reserved for future use"
|
||||
bit 5-7: "BTHome Version" */
|
||||
|
|
@ -37,6 +37,21 @@ function onTemperature(p) {
|
|||
0x04, // Pressure, 16 bit
|
||||
pressure100&255,(pressure100>>8)&255,pressure100>>16
|
||||
];
|
||||
|
||||
if(Array.isArray(Bangle.bleAdvert)){
|
||||
var found = false;
|
||||
for(var ad in Bangle.bleAdvert){
|
||||
if(ad[0xFCD2]){
|
||||
ad[0xFCD2] = advert;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!found)
|
||||
Bangle.bleAdvert.push({ 0xFCD2: advert });
|
||||
}else{
|
||||
Bangle.bleAdvert[0xFCD2] = advert;
|
||||
}
|
||||
NRF.setAdvertising(Bangle.bleAdvert);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{ "id": "bthometemp",
|
||||
"name": "BTHome Temperature and Pressure",
|
||||
"shortName":"BTHome T",
|
||||
"version":"0.01",
|
||||
"version":"0.02",
|
||||
"description": "Displays temperature and pressure, and advertises them over bluetooth using BTHome.io standard",
|
||||
"icon": "app.png",
|
||||
"tags": "bthome,bluetooth,temperature",
|
||||
|
|
|
|||
|
|
@ -14,3 +14,4 @@
|
|||
0.13: Switch to swipe left/right for month and up/down for year selection
|
||||
Display events for current month on touch
|
||||
0.14: Add support for holidays
|
||||
0.15: Edit holidays on device in settings
|
||||
|
|
|
|||
|
|
@ -75,11 +75,32 @@ function getDowLbls(locale) {
|
|||
}
|
||||
|
||||
function sameDay(d1, d2) {
|
||||
"jit";
|
||||
return d1.getFullYear() === d2.getFullYear() &&
|
||||
d1.getMonth() === d2.getMonth() &&
|
||||
d1.getDate() === d2.getDate();
|
||||
}
|
||||
|
||||
function drawEvent(ev, curDay, x1, y1, x2, y2) {
|
||||
"ram";
|
||||
switch(ev.type) {
|
||||
case "e": // alarm/event
|
||||
const hour = 0|ev.date.getHours() + 0|ev.date.getMinutes()/60.0;
|
||||
const slice = hour/24*(eventsPerDay-1); // slice 0 for 0:00 up to eventsPerDay for 23:59
|
||||
const height = (y2-2) - (y1+2); // height of a cell
|
||||
const sliceHeight = height/eventsPerDay;
|
||||
const ystart = (y1+2) + slice*sliceHeight;
|
||||
g.setColor(bgEvent).fillRect(x1+1, ystart, x2-2, ystart+sliceHeight);
|
||||
break;
|
||||
case "h": // holiday
|
||||
g.setColor(bgColorWeekend).fillRect(x1+1, y1+1, x2-1, y2-1);
|
||||
break;
|
||||
case "o": // other
|
||||
g.setColor(bgOtherEvent).fillRect(x1+1, y1+1, x2-1, y2-1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function drawCalendar(date) {
|
||||
g.setBgColor(bgColor);
|
||||
g.clearRect(0, 0, maxX, maxY);
|
||||
|
|
@ -118,7 +139,6 @@ function drawCalendar(date) {
|
|||
true
|
||||
);
|
||||
|
||||
g.setFont("6x8", fontSize);
|
||||
let dowLbls = getDowLbls(require('locale').name);
|
||||
dowLbls.forEach((lbl, i) => {
|
||||
g.drawString(lbl, i * colW + colW / 2, headerH + rowH / 2);
|
||||
|
|
@ -172,6 +192,7 @@ function drawCalendar(date) {
|
|||
const eventsThisMonth = events.filter(ev => ev.date > weekBeforeMonth && ev.date < week2AfterMonth);
|
||||
eventsThisMonth.sort((a,b) => a.date - b.date);
|
||||
let i = 0;
|
||||
g.setFont("8x12", fontSize);
|
||||
for (y = 0; y < rowN - 1; y++) {
|
||||
for (x = 0; x < colN; x++) {
|
||||
i++;
|
||||
|
|
@ -188,22 +209,7 @@ function drawCalendar(date) {
|
|||
// Display events for this day
|
||||
eventsThisMonth.forEach((ev, idx) => {
|
||||
if (sameDay(ev.date, curDay)) {
|
||||
switch(ev.type) {
|
||||
case "e": // alarm/event
|
||||
const hour = ev.date.getHours() + ev.date.getMinutes()/60.0;
|
||||
const slice = hour/24*(eventsPerDay-1); // slice 0 for 0:00 up to eventsPerDay for 23:59
|
||||
const height = (y2-2) - (y1+2); // height of a cell
|
||||
const sliceHeight = height/eventsPerDay;
|
||||
const ystart = (y1+2) + slice*sliceHeight;
|
||||
g.setColor(bgEvent).fillRect(x1+1, ystart, x2-2, ystart+sliceHeight);
|
||||
break;
|
||||
case "h": // holiday
|
||||
g.setColor(bgColorWeekend).fillRect(x1+1, y1+1, x2-1, y2-1);
|
||||
break;
|
||||
case "o": // other
|
||||
g.setColor(bgOtherEvent).fillRect(x1+1, y1+1, x2-1, y2-1);
|
||||
break;
|
||||
}
|
||||
drawEvent(ev, curDay, x1, y1, x2, y2);
|
||||
|
||||
eventsThisMonth.splice(idx, 1); // this event is no longer needed
|
||||
}
|
||||
|
|
@ -221,17 +227,15 @@ function drawCalendar(date) {
|
|||
);
|
||||
}
|
||||
|
||||
require("Font8x12").add(Graphics);
|
||||
g.setFont("8x12", fontSize);
|
||||
g.setColor(day < 50 ? fgOtherMonth : fgSameMonth);
|
||||
g.drawString(
|
||||
(day > 50 ? day - 50 : day).toString(),
|
||||
x * colW + colW / 2,
|
||||
headerH + rowH + y * rowH + rowH / 2
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} // end for (x = 0; x < colN; x++)
|
||||
} // end for (y = 0; y < rowN - 1; y++)
|
||||
} // end function drawCalendar
|
||||
|
||||
function setUI() {
|
||||
Bangle.setUI({
|
||||
|
|
@ -279,6 +283,7 @@ function setUI() {
|
|||
});
|
||||
}
|
||||
|
||||
require("Font8x12").add(Graphics);
|
||||
drawCalendar(date);
|
||||
setUI();
|
||||
// No space for widgets!
|
||||
|
|
|
|||
|
|
@ -28,11 +28,16 @@ function readFile(input) {
|
|||
for(let i=0; i<input.files.length; i++) {
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener("load", () => {
|
||||
const jCalData = ICAL.parse(reader.result);
|
||||
const icalText = reader.result.substring(reader.result.indexOf("BEGIN:VCALENDAR")); // remove html before data
|
||||
const jCalData = ICAL.parse(icalText);
|
||||
const comp = new ICAL.Component(jCalData);
|
||||
const vtz = comp.getFirstSubcomponent('vtimezone');
|
||||
const tz = new ICAL.Timezone(vtz);
|
||||
|
||||
// Fetch the VEVENT part
|
||||
comp.getAllSubcomponents('vevent').forEach(vevent => {
|
||||
event = new ICAL.Event(vevent);
|
||||
const event = new ICAL.Event(vevent);
|
||||
event.startDate.zone = tz;
|
||||
holidays = holidays.filter(holiday => !sameDay(new Date(holiday.date), event.startDate.toJSDate())); // remove if already exists
|
||||
|
||||
const holiday = eventToHoliday(event);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "calendar",
|
||||
"name": "Calendar",
|
||||
"version": "0.14",
|
||||
"version": "0.15",
|
||||
"description": "Simple calendar",
|
||||
"icon": "calendar.png",
|
||||
"screenshots": [{"url":"screenshot_calendar.png"}],
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
(function (back) {
|
||||
var FILE = "calendar.json";
|
||||
const HOLIDAY_FILE = "calendar.days.json";
|
||||
var settings = require('Storage').readJSON(FILE, true) || {};
|
||||
if (settings.ndColors === undefined)
|
||||
if (process.env.HWVERSION == 2) {
|
||||
|
|
@ -7,21 +8,147 @@
|
|||
} else {
|
||||
settings.ndColors = false;
|
||||
}
|
||||
const holidays = require("Storage").readJSON(HOLIDAY_FILE,1).sort((a,b) => new Date(a.date) - new Date(b.date)) || [];
|
||||
|
||||
function writeSettings() {
|
||||
require('Storage').writeJSON(FILE, settings);
|
||||
}
|
||||
|
||||
E.showMenu({
|
||||
"": { "title": "Calendar" },
|
||||
"< Back": () => back(),
|
||||
'B2 Colors': {
|
||||
value: settings.ndColors,
|
||||
onchange: v => {
|
||||
settings.ndColors = v;
|
||||
writeSettings();
|
||||
}
|
||||
},
|
||||
});
|
||||
})
|
||||
function writeHolidays() {
|
||||
holidays.sort((a,b) => new Date(a.date) - new Date(b.date));
|
||||
require('Storage').writeJSON(HOLIDAY_FILE, holidays);
|
||||
}
|
||||
|
||||
function formatDate(d) {
|
||||
return d.getFullYear() + "-" + (d.getMonth() + 1).toString().padStart(2, '0') + "-" + d.getDate().toString().padStart(2, '0');
|
||||
}
|
||||
|
||||
const editdate = (i) => {
|
||||
const holiday = holidays[i];
|
||||
const date = new Date(holiday.date);
|
||||
const dateStr = require("locale").date(date, 1);
|
||||
const menu = {
|
||||
"": { "title" : holiday.name},
|
||||
"< Back": () => {
|
||||
writeHolidays();
|
||||
editdates();
|
||||
},
|
||||
/*LANG*/"Day": {
|
||||
value: date ? date.getDate() : null,
|
||||
min: 1,
|
||||
max: 31,
|
||||
wrap: true,
|
||||
onchange: v => {
|
||||
date.setDate(v);
|
||||
holiday.date = formatDate(date);
|
||||
}
|
||||
},
|
||||
/*LANG*/"Month": {
|
||||
value: date ? date.getMonth() + 1 : null,
|
||||
format: v => require("date_utils").month(v),
|
||||
onchange: v => {
|
||||
date.setMonth((v+11)%12);
|
||||
holiday.date = formatDate(date);
|
||||
}
|
||||
},
|
||||
/*LANG*/"Year": {
|
||||
value: date ? date.getFullYear() : null,
|
||||
min: 1900,
|
||||
max: 2100,
|
||||
onchange: v => {
|
||||
date.setFullYear(v);
|
||||
holiday.date = formatDate(date);
|
||||
}
|
||||
},
|
||||
/*LANG*/"Name": () => {
|
||||
require("textinput").input({text:holiday.name}).then(result => {
|
||||
holiday.name = result;
|
||||
editdate(i);
|
||||
});
|
||||
},
|
||||
/*LANG*/"Type": {
|
||||
value: function() {
|
||||
switch(holiday.type) {
|
||||
case 'h': return 0;
|
||||
case 'o': return 1;
|
||||
}
|
||||
return 0;
|
||||
}(),
|
||||
min: 0, max: 1,
|
||||
format: v => [/*LANG*/"Holiday", /*LANG*/"Other"][v],
|
||||
onchange: v => {
|
||||
holiday.type = function() {
|
||||
switch(v) {
|
||||
case 0: return 'h';
|
||||
case 1: return 'o';
|
||||
}
|
||||
}();
|
||||
}
|
||||
},
|
||||
/*LANG*/"Repeat": {
|
||||
value: !!holiday.repeat,
|
||||
format: v => v ? /*LANG*/"Yearly" : /*LANG*/"Never",
|
||||
onchange: v => {
|
||||
holiday.repeat = v ? 'y' : undefined;
|
||||
}
|
||||
},
|
||||
/*LANG*/"Delete": () => E.showPrompt(/*LANG*/"Delete" + " " + menu[""].title + "?").then(function(v) {
|
||||
if (v) {
|
||||
holidays.splice(i, 1);
|
||||
writeHolidays();
|
||||
editdates();
|
||||
} else {
|
||||
editday(i);
|
||||
}
|
||||
}
|
||||
),
|
||||
};
|
||||
try {
|
||||
require("textinput");
|
||||
} catch(e) {
|
||||
// textinput not installed
|
||||
delete menu[/*LANG*/"Name"];
|
||||
}
|
||||
|
||||
E.showMenu(menu);
|
||||
};
|
||||
|
||||
const editdates = () => {
|
||||
const menu = holidays.map((holiday,i) => {
|
||||
const date = new Date(holiday.date);
|
||||
const dateStr = require("locale").date(date, 1);
|
||||
return {
|
||||
title: dateStr + ' ' + holiday.name,
|
||||
onchange: v => setTimeout(() => editdate(i), 10),
|
||||
};
|
||||
});
|
||||
|
||||
menu[''] = { 'title': 'Holidays' };
|
||||
menu['< Back'] = ()=>settingsmenu();
|
||||
E.showMenu(menu);
|
||||
};
|
||||
|
||||
const settingsmenu = () => {
|
||||
E.showMenu({
|
||||
"": { "title": "Calendar" },
|
||||
"< Back": () => back(),
|
||||
'B2 Colors': {
|
||||
value: settings.ndColors,
|
||||
onchange: v => {
|
||||
settings.ndColors = v;
|
||||
writeSettings();
|
||||
}
|
||||
},
|
||||
/*LANG*/"Edit Holidays": () => editdates(),
|
||||
/*LANG*/"Add Holiday": () => {
|
||||
holidays.push({
|
||||
"date":formatDate(new Date()),
|
||||
"name":/*LANG*/"New",
|
||||
"type":'h',
|
||||
});
|
||||
editdate(holidays.length-1);
|
||||
},
|
||||
});
|
||||
};
|
||||
settingsmenu();
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
0.01: New App!
|
||||
0.02: Handle missing settings (e.g. first-install)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
(() => {
|
||||
const chargingRotation = 0 | require('Storage').readJSON("chargerot.settings.json").rotate;
|
||||
const chargingRotation = 0 | (require('Storage').readJSON("chargerot.settings.json",1)||{}).rotate;
|
||||
const defaultRotation = 0 | require('Storage').readJSON("setting.json").rotate;
|
||||
if (Bangle.isCharging()) g.setRotation(chargingRotation&3,chargingRotation>>2).clear();
|
||||
Bangle.on('charging', (charging) => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "chargerot",
|
||||
"name": "Charge LCD rotation",
|
||||
"version": "0.01",
|
||||
"version": "0.02",
|
||||
"description": "When charging, this app can rotate your screen and revert it when unplugged. Made for all sort of cradles.",
|
||||
"icon": "icon.png",
|
||||
"tags": "battery",
|
||||
|
|
|
|||
|
|
@ -4,4 +4,5 @@
|
|||
0.04: On 2v18+ firmware, we can now stop swipe events from being handled by other apps
|
||||
eg. when a clockinfo is selected, swipes won't affect swipe-down widgets
|
||||
0.05: Reported image for battery is now transparent (2v18+)
|
||||
0.06: When >1 clockinfo, swiping one back tries to ensure they don't display the same thing
|
||||
0.06: When >1 clockinfo, swiping one back tries to ensure they don't display the same thing
|
||||
0.07: Developer tweak: clkinfo load errors are emitted
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ exports.load = function() {
|
|||
if(b) b.items = b.items.concat(a.items);
|
||||
else menu = menu.concat(a);
|
||||
} catch(e){
|
||||
console.log("Could not load clock info "+E.toJS(fn));
|
||||
console.log("Could not load clock info "+E.toJS(fn)+": "+e);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{ "id": "clock_info",
|
||||
"name": "Clock Info Module",
|
||||
"shortName": "Clock Info",
|
||||
"version":"0.06",
|
||||
"version":"0.07",
|
||||
"description": "A library used by clocks to provide extra information on the clock face (Altitude, BPM, etc)",
|
||||
"icon": "app.png",
|
||||
"type": "module",
|
||||
|
|
|
|||
|
|
@ -5,3 +5,5 @@
|
|||
0.05: Now scrolls text when string gets longer than screen width.
|
||||
0.06: The code is now more reliable and the input snappier. Widgets will be drawn if present.
|
||||
0.07: Settings for display colors
|
||||
0.08: Catch and discard swipe events on fw2v19 and up (as well as some cutting
|
||||
edge 2v18 ones), allowing compatability with the Back Swipe app.
|
||||
|
|
|
|||
|
|
@ -103,6 +103,133 @@ exports.input = function(options) {
|
|||
initDraw();
|
||||
//setTimeout(initDraw, 0); // So Bangle.appRect reads the correct environment. It would draw off to the side sometimes otherwise.
|
||||
|
||||
let dragHandlerDB = function(event) {
|
||||
"ram";
|
||||
// ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
||||
// Choose character by draging along red rectangle at bottom of screen
|
||||
if (event.y >= ( (R.y+R.h) - 12 )) {
|
||||
// Translate x-position to character
|
||||
if (event.x < ABCPADDING) { abcHL = 0; }
|
||||
else if (event.x >= 176-ABCPADDING) { abcHL = 25; }
|
||||
else { abcHL = Math.floor((event.x-ABCPADDING)/6); }
|
||||
|
||||
// Datastream for development purposes
|
||||
//print(event.x, event.y, event.b, ABC.charAt(abcHL), ABC.charAt(abcHLPrev));
|
||||
|
||||
// Unmark previous character and mark the current one...
|
||||
// Handling switching between letters and numbers/punctuation
|
||||
if (typePrev != 'abc') resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
|
||||
|
||||
if (abcHL != abcHLPrev) {
|
||||
resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
|
||||
showChars(ABC.charAt(abcHL), abcHL, ABCPADDING, 2);
|
||||
}
|
||||
// Print string at top of screen
|
||||
if (event.b == 0) {
|
||||
text = text + ABC.charAt(abcHL);
|
||||
updateTopString();
|
||||
|
||||
// Autoswitching letter case
|
||||
if (ABC.charAt(abcHL) == ABC.charAt(abcHL).toUpperCase()) changeCase(abcHL);
|
||||
}
|
||||
// Update previous character to current one
|
||||
abcHLPrev = abcHL;
|
||||
typePrev = 'abc';
|
||||
}
|
||||
|
||||
// 12345678901234567890
|
||||
// Choose number or puctuation by draging on green rectangle
|
||||
else if ((event.y < ( (R.y+R.h) - 12 )) && (event.y > ( (R.y+R.h) - 52 ))) {
|
||||
// Translate x-position to character
|
||||
if (event.x < NUMPADDING) { numHL = 0; }
|
||||
else if (event.x > 176-NUMPADDING) { numHL = NUM.length-1; }
|
||||
else { numHL = Math.floor((event.x-NUMPADDING)/6); }
|
||||
|
||||
// Datastream for development purposes
|
||||
//print(event.x, event.y, event.b, NUM.charAt(numHL), NUM.charAt(numHLPrev));
|
||||
|
||||
// Unmark previous character and mark the current one...
|
||||
// Handling switching between letters and numbers/punctuation
|
||||
if (typePrev != 'num') resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
|
||||
|
||||
if (numHL != numHLPrev) {
|
||||
resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
|
||||
showChars(NUM.charAt(numHL), numHL, NUMPADDING, 4);
|
||||
}
|
||||
// Print string at top of screen
|
||||
if (event.b == 0) {
|
||||
g.setColor(HLCOLOR);
|
||||
// Backspace if releasing before list of numbers/punctuation
|
||||
if (event.x < NUMPADDING) {
|
||||
// show delete sign
|
||||
showChars('del', 0, (R.x+R.w)/2 +6 -27 , 4);
|
||||
delSpaceLast = 1;
|
||||
text = text.slice(0, -1);
|
||||
updateTopString();
|
||||
//print(text);
|
||||
}
|
||||
// Append space if releasing after list of numbers/punctuation
|
||||
else if (event.x > (R.x+R.w)-NUMPADDING) {
|
||||
//show space sign
|
||||
showChars('space', 0, (R.x+R.w)/2 +6 -6*3*5/2 , 4);
|
||||
delSpaceLast = 1;
|
||||
text = text + ' ';
|
||||
updateTopString();
|
||||
//print(text);
|
||||
}
|
||||
// Append selected number/punctuation
|
||||
else {
|
||||
text = text + NUMHIDDEN.charAt(numHL);
|
||||
updateTopString();
|
||||
|
||||
// Autoswitching letter case
|
||||
if ((text.charAt(text.length-1) == '.') || (text.charAt(text.length-1) == '!')) changeCase();
|
||||
}
|
||||
}
|
||||
// Update previous character to current one
|
||||
numHLPrev = numHL;
|
||||
typePrev = 'num';
|
||||
}
|
||||
|
||||
// Make a space or backspace by swiping right or left on screen above green rectangle
|
||||
else if (event.y > 20+4) {
|
||||
if (event.b == 0) {
|
||||
g.setColor(HLCOLOR);
|
||||
if (event.x < (R.x+R.w)/2) {
|
||||
resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
|
||||
resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
|
||||
|
||||
// show delete sign
|
||||
showChars('del', 0, (R.x+R.w)/2 +6 -27 , 4);
|
||||
delSpaceLast = 1;
|
||||
|
||||
// Backspace and draw string upper right corner
|
||||
text = text.slice(0, -1);
|
||||
updateTopString();
|
||||
if (text.length==0) changeCase(abcHL);
|
||||
//print(text, 'undid');
|
||||
}
|
||||
else {
|
||||
resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
|
||||
resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
|
||||
|
||||
//show space sign
|
||||
showChars('space', 0, (R.x+R.w)/2 +6 -6*3*5/2 , 4);
|
||||
delSpaceLast = 1;
|
||||
|
||||
// Append space and draw string upper right corner
|
||||
text = text + NUMHIDDEN.charAt(0);
|
||||
updateTopString();
|
||||
//print(text, 'made space');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let catchSwipe = ()=>{
|
||||
E.stopEventPropagation&&E.stopEventPropagation();
|
||||
};
|
||||
|
||||
function changeCase(abcHL) {
|
||||
if (settings.uppercase) return;
|
||||
g.setColor(BGCOLOR);
|
||||
|
|
@ -119,131 +246,12 @@ exports.input = function(options) {
|
|||
mode: 'custom',
|
||||
back: ()=>{
|
||||
Bangle.setUI();
|
||||
Bangle.prependListener&&Bangle.removeListener('swipe', catchSwipe); // Remove swipe lister if it was added with `Bangle.prependListener()` (fw2v19 and up).
|
||||
g.clearRect(Bangle.appRect);
|
||||
resolve(text);
|
||||
},
|
||||
drag: function(event) {
|
||||
"ram";
|
||||
// ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
||||
// Choose character by draging along red rectangle at bottom of screen
|
||||
if (event.y >= ( (R.y+R.h) - 12 )) {
|
||||
// Translate x-position to character
|
||||
if (event.x < ABCPADDING) { abcHL = 0; }
|
||||
else if (event.x >= 176-ABCPADDING) { abcHL = 25; }
|
||||
else { abcHL = Math.floor((event.x-ABCPADDING)/6); }
|
||||
|
||||
// Datastream for development purposes
|
||||
//print(event.x, event.y, event.b, ABC.charAt(abcHL), ABC.charAt(abcHLPrev));
|
||||
|
||||
// Unmark previous character and mark the current one...
|
||||
// Handling switching between letters and numbers/punctuation
|
||||
if (typePrev != 'abc') resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
|
||||
|
||||
if (abcHL != abcHLPrev) {
|
||||
resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
|
||||
showChars(ABC.charAt(abcHL), abcHL, ABCPADDING, 2);
|
||||
}
|
||||
// Print string at top of screen
|
||||
if (event.b == 0) {
|
||||
text = text + ABC.charAt(abcHL);
|
||||
updateTopString();
|
||||
|
||||
// Autoswitching letter case
|
||||
if (ABC.charAt(abcHL) == ABC.charAt(abcHL).toUpperCase()) changeCase(abcHL);
|
||||
}
|
||||
// Update previous character to current one
|
||||
abcHLPrev = abcHL;
|
||||
typePrev = 'abc';
|
||||
}
|
||||
|
||||
// 12345678901234567890
|
||||
// Choose number or puctuation by draging on green rectangle
|
||||
else if ((event.y < ( (R.y+R.h) - 12 )) && (event.y > ( (R.y+R.h) - 52 ))) {
|
||||
// Translate x-position to character
|
||||
if (event.x < NUMPADDING) { numHL = 0; }
|
||||
else if (event.x > 176-NUMPADDING) { numHL = NUM.length-1; }
|
||||
else { numHL = Math.floor((event.x-NUMPADDING)/6); }
|
||||
|
||||
// Datastream for development purposes
|
||||
//print(event.x, event.y, event.b, NUM.charAt(numHL), NUM.charAt(numHLPrev));
|
||||
|
||||
// Unmark previous character and mark the current one...
|
||||
// Handling switching between letters and numbers/punctuation
|
||||
if (typePrev != 'num') resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
|
||||
|
||||
if (numHL != numHLPrev) {
|
||||
resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
|
||||
showChars(NUM.charAt(numHL), numHL, NUMPADDING, 4);
|
||||
}
|
||||
// Print string at top of screen
|
||||
if (event.b == 0) {
|
||||
g.setColor(HLCOLOR);
|
||||
// Backspace if releasing before list of numbers/punctuation
|
||||
if (event.x < NUMPADDING) {
|
||||
// show delete sign
|
||||
showChars('del', 0, (R.x+R.w)/2 +6 -27 , 4);
|
||||
delSpaceLast = 1;
|
||||
text = text.slice(0, -1);
|
||||
updateTopString();
|
||||
//print(text);
|
||||
}
|
||||
// Append space if releasing after list of numbers/punctuation
|
||||
else if (event.x > (R.x+R.w)-NUMPADDING) {
|
||||
//show space sign
|
||||
showChars('space', 0, (R.x+R.w)/2 +6 -6*3*5/2 , 4);
|
||||
delSpaceLast = 1;
|
||||
text = text + ' ';
|
||||
updateTopString();
|
||||
//print(text);
|
||||
}
|
||||
// Append selected number/punctuation
|
||||
else {
|
||||
text = text + NUMHIDDEN.charAt(numHL);
|
||||
updateTopString();
|
||||
|
||||
// Autoswitching letter case
|
||||
if ((text.charAt(text.length-1) == '.') || (text.charAt(text.length-1) == '!')) changeCase();
|
||||
}
|
||||
}
|
||||
// Update previous character to current one
|
||||
numHLPrev = numHL;
|
||||
typePrev = 'num';
|
||||
}
|
||||
|
||||
// Make a space or backspace by swiping right or left on screen above green rectangle
|
||||
else if (event.y > 20+4) {
|
||||
if (event.b == 0) {
|
||||
g.setColor(HLCOLOR);
|
||||
if (event.x < (R.x+R.w)/2) {
|
||||
resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
|
||||
resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
|
||||
|
||||
// show delete sign
|
||||
showChars('del', 0, (R.x+R.w)/2 +6 -27 , 4);
|
||||
delSpaceLast = 1;
|
||||
|
||||
// Backspace and draw string upper right corner
|
||||
text = text.slice(0, -1);
|
||||
updateTopString();
|
||||
if (text.length==0) changeCase(abcHL);
|
||||
//print(text, 'undid');
|
||||
}
|
||||
else {
|
||||
resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
|
||||
resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
|
||||
|
||||
//show space sign
|
||||
showChars('space', 0, (R.x+R.w)/2 +6 -6*3*5/2 , 4);
|
||||
delSpaceLast = 1;
|
||||
|
||||
// Append space and draw string upper right corner
|
||||
text = text + NUMHIDDEN.charAt(0);
|
||||
updateTopString();
|
||||
//print(text, 'made space');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
drag: dragHandlerDB,
|
||||
});
|
||||
Bangle.prependListener&&Bangle.prependListener('swipe', catchSwipe); // Intercept swipes on fw2v19 and later. Should not break on older firmwares.
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{ "id": "dragboard",
|
||||
"name": "Dragboard",
|
||||
"version":"0.07",
|
||||
"version":"0.08",
|
||||
"description": "A library for text input via swiping keyboard",
|
||||
"icon": "app.png",
|
||||
"type":"textinput",
|
||||
|
|
|
|||
|
|
@ -1 +1,3 @@
|
|||
0.01: New App based on dragboard, but with a U shaped drag area
|
||||
0.02: Catch and discard swipe events on fw2v19 and up (as well as some cutting
|
||||
edge 2v18 ones), allowing compatability with the Back Swipe app.
|
||||
|
|
|
|||
|
|
@ -104,45 +104,53 @@ exports.input = function(options) {
|
|||
}
|
||||
}
|
||||
|
||||
let dragHandlerUB = function(event) {
|
||||
"ram";
|
||||
|
||||
// drag on middle bottom rectangle
|
||||
if (event.x > MIDPADDING - 2 && event.x < (R.x2-MIDPADDING + 2) && event.y >= ( (R.y2) - 12 )) {
|
||||
moveCharPos(MIDDLE, event.b == 0, (event.x-middleStart)/(middleWidth/MIDDLE.length));
|
||||
}
|
||||
// drag on left or right rectangle
|
||||
else if (event.y > R.y && (event.x < MIDPADDING-2 || event.x > (R.x2-MIDPADDING + 2))) {
|
||||
moveCharPos(event.x<MIDPADDING-2 ? LEFT : RIGHT, event.b == 0, (event.y-topStart)/((R.y2 - topStart)/vLength));
|
||||
}
|
||||
// drag on top rectangle for number or punctuation
|
||||
else if ((event.y < ( (R.y2) - 12 )) && (event.y > ( (R.y2) - 52 ))) {
|
||||
moveCharPos(NUM, event.b == 0, (event.x-NUMPADDING)/6);
|
||||
}
|
||||
// Make a space or backspace by tapping right or left on screen above green rectangle
|
||||
else if (event.y > R.y && event.b == 0) {
|
||||
if (event.x < (R.x2)/2) {
|
||||
showChars('<-');
|
||||
text = text.slice(0, -1);
|
||||
} else {
|
||||
//show space sign
|
||||
showChars('->');
|
||||
text += ' ';
|
||||
}
|
||||
prevChar = null;
|
||||
updateTopString();
|
||||
}
|
||||
};
|
||||
|
||||
let catchSwipe = ()=>{
|
||||
E.stopEventPropagation&&E.stopEventPropagation();
|
||||
};
|
||||
|
||||
return new Promise((resolve,reject) => {
|
||||
// Interpret touch input
|
||||
Bangle.setUI({
|
||||
mode: 'custom',
|
||||
back: ()=>{
|
||||
Bangle.setUI();
|
||||
Bangle.prependListener&&Bangle.removeListener('swipe', catchSwipe); // Remove swipe lister if it was added with `Bangle.prependListener()` (fw2v19 and up).
|
||||
g.clearRect(Bangle.appRect);
|
||||
resolve(text);
|
||||
},
|
||||
drag: function(event) {
|
||||
"ram";
|
||||
|
||||
// drag on middle bottom rectangle
|
||||
if (event.x > MIDPADDING - 2 && event.x < (R.x2-MIDPADDING + 2) && event.y >= ( (R.y2) - 12 )) {
|
||||
moveCharPos(MIDDLE, event.b == 0, (event.x-middleStart)/(middleWidth/MIDDLE.length));
|
||||
}
|
||||
// drag on left or right rectangle
|
||||
else if (event.y > R.y && (event.x < MIDPADDING-2 || event.x > (R.x2-MIDPADDING + 2))) {
|
||||
moveCharPos(event.x<MIDPADDING-2 ? LEFT : RIGHT, event.b == 0, (event.y-topStart)/((R.y2 - topStart)/vLength));
|
||||
}
|
||||
// drag on top rectangle for number or punctuation
|
||||
else if ((event.y < ( (R.y2) - 12 )) && (event.y > ( (R.y2) - 52 ))) {
|
||||
moveCharPos(NUM, event.b == 0, (event.x-NUMPADDING)/6);
|
||||
}
|
||||
// Make a space or backspace by tapping right or left on screen above green rectangle
|
||||
else if (event.y > R.y && event.b == 0) {
|
||||
if (event.x < (R.x2)/2) {
|
||||
showChars('<-');
|
||||
text = text.slice(0, -1);
|
||||
} else {
|
||||
//show space sign
|
||||
showChars('->');
|
||||
text += ' ';
|
||||
}
|
||||
prevChar = null;
|
||||
updateTopString();
|
||||
}
|
||||
}
|
||||
drag: dragHandlerDB
|
||||
});
|
||||
Bangle.prependListener&&Bangle.prependListener('swipe', catchSwipe); // Intercept swipes on fw2v19 and later. Should not break on older firmwares.
|
||||
|
||||
R = Bangle.appRect;
|
||||
MIDPADDING = R.x + 35;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{ "id": "draguboard",
|
||||
"name": "DragUboard",
|
||||
"version":"0.01",
|
||||
"version":"0.02",
|
||||
"description": "A library for text input via swiping U-shaped keyboard.",
|
||||
"icon": "app.png",
|
||||
"type":"textinput",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
0.01: Initial release.
|
||||
0.02: Fix reset of progress bars on midnight. Fix display of 100k+ steps.
|
||||
0.03: Added option to display weather.
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
# Edge Clock
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
Tinxx presents you a clock with as many straight edges as possible to allow for a crisp look and perfect readability.
|
||||
It comes with a custom font to display weekday, date, time, and steps. Also displays battery percentage while charging.
|
||||
There are three progress bars that indicate day of the week, time of the day, and daily step goal.
|
||||
The watch face is monochrome and allows for applying your favorite color scheme.
|
||||
|
||||
The appearance is highly configurable. In the settings menu you can:
|
||||
- De-/activate a buzz when the charger is connected while the watch face is active.
|
||||
- Decide if month or day should be displayed first.
|
||||
- Switch between 24h and 12h clock.
|
||||
- Hide or display seconds.*
|
||||
- Show AM/PM in place of the seconds.
|
||||
- Show weather temperature and icon in place of the seconds.
|
||||
- Set the daily step goal.
|
||||
- En- or disable the individual progress bars.
|
||||
- Set if your week should start with Monday or Sunday (for week progress bar).
|
||||
|
||||
*) Hiding seconds should further reduce power consumption as the draw interval is prolonged as well.
|
||||
|
||||
The clock implements Fast Loading for faster switching to and fro.
|
||||
|
||||
## Contributors
|
||||
- [tinxx](https://github.com/tinxx)
|
||||
- [peerdavid](https://github.com/peerdavid)
|
||||
|
||||
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwgMgBAoFEuADCgP8sAFD/wLE/wXDCIIjFAAv/ABQRF5fegEPgfe5UbgEJgVS5ebBYMyr36BYdC7YXEGq4AFj8f/ED8f+ApHjAoMHjkA8HjxwFIgAFCC4IFJjk4AoodEAogXBAoI1BDoYFGL5Z3XmHv33whkfuAFE/Fgw0whuD/Fjz0wh/fuALCh/Y/Fv30wgOf7AFE"))
|
||||
|
|
@ -0,0 +1,342 @@
|
|||
{
|
||||
/* Configuration
|
||||
------------------------------------------------------------------------------*/
|
||||
|
||||
const settings = Object.assign({
|
||||
buzzOnCharge: true,
|
||||
monthFirst: true,
|
||||
twentyFourH: true,
|
||||
showAmPm: false,
|
||||
showSeconds: true,
|
||||
showWeather: false,
|
||||
stepGoal: 10000,
|
||||
stepBar: true,
|
||||
weekBar: true,
|
||||
mondayFirst: true,
|
||||
dayBar: true,
|
||||
}, require('Storage').readJSON('edgeclk.settings.json', true) || {});
|
||||
|
||||
/* Runtime Variables
|
||||
------------------------------------------------------------------------------*/
|
||||
|
||||
let startTimeout;
|
||||
let drawInterval;
|
||||
|
||||
let lcdPower = true;
|
||||
let charging = Bangle.isCharging();
|
||||
|
||||
const font = atob('AA////wDwDwDwD////AAAAAAAAwAwA////AAAAAAAA8/8/wzwzwzwz/z/zAAAA4H4HwDxjxjxj////AAAA/w/wAwAwD/D/AwAwAAAA/j/jxjxjxjxjx/x/AAAA////xjxjxjxjx/x/AAAAwAwAwAwAwA////AAAAAA////xjxjxjxj////AAAA/j/jxjxjxjxj////AAAAAAAAAAMMMMAAAAAAAAAAAAAAABMOMMAAAAAAAAAABgBgDwDwGYGYMMMMAAAAAAGYGYGYGYGYGYAAAAAAMMMMGYGYDwDwBgBgAAAA4A4Ax7x7xgxg/g/gAAAA//gBv9shshv9gF/7AAAA////wwwwwwww////AAAA////xjxjxjxj////AAAA////wDwDwDwD4H4HAAAA////wDwDwD4Hf+P8AAAA////xjxjxjxjwDwDAAAA////xgxgxgxgwAwAAAAA////wDwDwzwz4/4/AAAA////BgBgBgBg////AAAAAAwDwD////wDwDAAAAAAAAwPwPwDwD////AAAAAA////DwH4OccO4HwDAAAA////ADADADADADADAAAA////YAGAGAYA////AAAA////MADAAwAM////AAAA////wDwDwDwD////AAAA////xgxgxgxg/g/gAAAA/+/+wGwOwOwO////AAAA////xgxgxwx8/v/jAAAA/j/jxjxjxjxjx/x/AAAAwAwAwA////wAwAwAAAAA////ADADADAD////AAAA/w/8AOAHAHAO/8/wAAAA////AGAYAYAG////AAAAwD4PecH4H4ec4PwDAAAAwA4AeBH/H/eA4AwAAAAAwPwfw7xzzj3D+D8DAAAAAAAAAAAA////wDAAAAAAAAAABgBgBgBgAAAAAAAAAAwD////AAAAAAAAAAAAAwDwPA8A8APADwAwAAAAAAAAAAAAAAAAAAAAAA');
|
||||
|
||||
const iconSize = [19, 26];
|
||||
const plugIcon = atob('ExoBBxwA44AccAOOAHHAf/8P/+H//D//h//w//4P/4H/8B/8Af8ABwAA4AAcAAOAAHAADgABwAA4AAcAAOAAHAA=');
|
||||
const stepIcon1 = atob('ExoBAfAAPgAHwAD4AB8AAAAB/wD/8D//Bn9wz+cZ/HM/hmfwAP4AAAAD+AD/gBxwB48A4OA8HgcBwcAcOAOGADA=');
|
||||
const stepIcon2 = atob('ExoBAfAAPgMHwfD4dx8ccAcH/8B/8Af8AH8AD+AB/AA/gAfwAP4AAAAD+AD/gBxwB48A4OA8HgcBwcAcOAOGADA=');
|
||||
|
||||
|
||||
/* Draw Functions
|
||||
------------------------------------------------------------------------------*/
|
||||
|
||||
const drawAll = function () {
|
||||
const date = new Date();
|
||||
|
||||
drawDate(date);
|
||||
if (settings.showSeconds) drawSecs(date);
|
||||
drawTime(date);
|
||||
drawLower();
|
||||
};
|
||||
|
||||
const drawLower = function (stepsOnlyCount) {
|
||||
if (charging) {
|
||||
drawCharge();
|
||||
} else {
|
||||
drawSteps(stepsOnlyCount);
|
||||
}
|
||||
|
||||
drawWeather();
|
||||
};
|
||||
|
||||
const drawWeather = function () {
|
||||
if (!settings.showWeather){
|
||||
return;
|
||||
}
|
||||
|
||||
g.setFontCustom(font, 48, 10, 512 + 12); // double size (1<<9)
|
||||
g.setFontAlign(1, 1); // right bottom
|
||||
|
||||
try{
|
||||
const weather = require('weather');
|
||||
const w = weather.get();
|
||||
let temp = parseInt(w.temp-273.15);
|
||||
temp = temp < 0 ? '\\' + String(temp*-1) : String(temp);
|
||||
|
||||
g.drawString(temp, g.getWidth()-40, g.getHeight() - 1, true);
|
||||
|
||||
// clear icon area in case weather condition changed
|
||||
g.clearRect(g.getWidth()-40, g.getHeight()-30, g.getWidth(), g.getHeight());
|
||||
weather.drawIcon(w, g.getWidth()-20, g.getHeight()-15, 14);
|
||||
|
||||
} catch(e) {
|
||||
g.drawString("???", g.getWidth()-3, g.getHeight() - 1, true);
|
||||
}
|
||||
};
|
||||
|
||||
const drawDate = function (date) {
|
||||
const top = 30;
|
||||
g.reset();
|
||||
|
||||
// weekday
|
||||
g.setFontCustom(font, 48, 10, 512 + 12); // double size (1<<9)
|
||||
g.setFontAlign(-1, -1); // left top
|
||||
g.drawString(date.toString().slice(0,3).toUpperCase(), 0, top + 12, true);
|
||||
|
||||
// date
|
||||
g.setFontAlign(1, -1); // right top
|
||||
// Note: to save space first and last two lines of ASCII are left out.
|
||||
// That is why '-' is assigned to '\' and ' ' (space) to '_'.
|
||||
if (settings.monthFirst) {
|
||||
g.drawString((date.getMonth()+1).toString().padStart(2, '_')
|
||||
+ '\\'
|
||||
+ date.getDate().toString().padStart(2, 0),
|
||||
g.getWidth(), top + 12, true);
|
||||
} else {
|
||||
g.drawString('_'
|
||||
+ date.getDate().toString().padStart(2, 0)
|
||||
+ '\\'
|
||||
+ (date.getMonth()+1).toString(),
|
||||
g.getWidth(), top + 12, true);
|
||||
}
|
||||
|
||||
// line/progress bar
|
||||
if (settings.weekBar) {
|
||||
let weekday = date.getDay();
|
||||
if (settings.mondayFirst) {
|
||||
if (weekday === 0) { weekday = 7; }
|
||||
} else {
|
||||
weekday += 1;
|
||||
}
|
||||
drawBar(top, weekday/7);
|
||||
} else {
|
||||
drawLine(top);
|
||||
}
|
||||
};
|
||||
|
||||
const drawTime = function (date) {
|
||||
const top = 72;
|
||||
g.reset();
|
||||
|
||||
const h = date.getHours();
|
||||
g.setFontCustom(font, 48, 10, 1024 + 12); // triple size (2<<9)
|
||||
g.setFontAlign(-1, -1); // left top
|
||||
g.drawString((settings.twentyFourH ? h : (h % 12 || 12)).toString().padStart(2, 0),
|
||||
0, top+12, true);
|
||||
g.setFontAlign(0, -1); // center top
|
||||
g.drawString(':', g.getWidth()/2, top+12, false);
|
||||
const m = date.getMinutes();
|
||||
g.setFontAlign(1, -1); // right top
|
||||
g.drawString(m.toString().padStart(2, 0),
|
||||
g.getWidth(), top+12, true);
|
||||
|
||||
if (settings.showAmPm) {
|
||||
g.setFontCustom(font, 48, 10, 512 + 12); // double size (1<<9)
|
||||
g.setFontAlign(1, 1); // right bottom
|
||||
g.drawString(h < 12 ? 'AM' : 'PM', g.getWidth(), g.getHeight() - 1, true);
|
||||
}
|
||||
|
||||
if (settings.dayBar) {
|
||||
drawBar(top, (h*60+m)/1440);
|
||||
} else {
|
||||
drawLine(top);
|
||||
}
|
||||
};
|
||||
|
||||
const drawSecs = function (date) {
|
||||
g.reset();
|
||||
g.setFontCustom(font, 48, 10, 512 + 12); // double size (1<<9)
|
||||
g.setFontAlign(1, 1); // right bottom
|
||||
g.drawString(date.getSeconds().toString().padStart(2, 0), g.getWidth(), g.getHeight() - 1, true);
|
||||
};
|
||||
|
||||
const drawSteps = function (onlyCount) {
|
||||
g.reset();
|
||||
g.setFontCustom(font, 48, 10, 512 + 12); // double size (1<<9)
|
||||
g.setFontAlign(-1, 1); // left bottom
|
||||
|
||||
const steps = Bangle.getHealthStatus('day').steps;
|
||||
const toKSteps = settings.showWeather ? 1000 : 100000;
|
||||
g.drawString((steps < toKSteps ? steps.toString() : ((steps / 1000).toFixed(0) + 'K')).padEnd(5, '_'),
|
||||
iconSize[0] + 6, g.getHeight() - 1, true);
|
||||
|
||||
if (onlyCount === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
const progress = steps / settings.stepGoal;
|
||||
if (settings.stepBar) {
|
||||
drawBar(g.getHeight() - 38, progress);
|
||||
} else {
|
||||
drawLine(g.getHeight() - 38);
|
||||
}
|
||||
|
||||
// icon
|
||||
if (progress < 1) {
|
||||
g.drawImage(stepIcon1, 0, g.getHeight() - iconSize[1]);
|
||||
} else {
|
||||
g.drawImage(stepIcon2, 0, g.getHeight() - iconSize[1]);
|
||||
}
|
||||
};
|
||||
|
||||
const drawCharge = function () {
|
||||
g.reset();
|
||||
g.setFontCustom(font, 48, 10, 512 + 12); // double size (1<<9)
|
||||
g.setFontAlign(-1, 1); // left bottom
|
||||
|
||||
const charge = E.getBattery();
|
||||
g.drawString(charge.toString().padEnd(5, '_'), iconSize[0] + 6, g.getHeight() - 1, true);
|
||||
|
||||
drawBar(g.getHeight() - 38, charge / 100);
|
||||
g.drawImage(plugIcon, 0, g.getHeight() - 26);
|
||||
};
|
||||
|
||||
const drawBar = function (top, progress) {
|
||||
// draw frame
|
||||
g.drawRect(0, top, g.getWidth() - 1, top + 5);
|
||||
g.drawRect(1, top + 1, g.getWidth() - 2, top + 4);
|
||||
// clear bar area
|
||||
g.clearRect(2, top + 2, g.getWidth() - 3, top + 3);
|
||||
// draw bar
|
||||
const barLen = progress >= 1 ? g.getWidth() : (g.getWidth() - 4) * progress;
|
||||
if (barLen < 1) return;
|
||||
g.drawLine(2, top + 2, barLen + 2, top + 2);
|
||||
g.drawLine(2, top + 3, barLen + 2, top + 3);
|
||||
};
|
||||
|
||||
const drawLine = function (top) {
|
||||
const width = g.getWidth();
|
||||
g.drawLine(0, top + 2, width, top + 2);
|
||||
g.drawLine(0, top + 3, width, top + 3);
|
||||
};
|
||||
|
||||
|
||||
/* Event Handlers
|
||||
------------------------------------------------------------------------------*/
|
||||
|
||||
const onSecondInterval = function () {
|
||||
const date = new Date();
|
||||
drawSecs(date);
|
||||
if (date.getSeconds() === 0) {
|
||||
onMinuteInterval();
|
||||
}
|
||||
};
|
||||
|
||||
const onMinuteInterval = function () {
|
||||
const date = new Date();
|
||||
drawTime(date);
|
||||
drawLower(true);
|
||||
};
|
||||
|
||||
const onMinuteIntervalStarter = function () {
|
||||
drawInterval = setInterval(onMinuteInterval, 60000);
|
||||
startTimeout = null;
|
||||
onMinuteInterval();
|
||||
};
|
||||
|
||||
const onLcdPower = function (on) {
|
||||
lcdPower = on;
|
||||
if (on) {
|
||||
drawAll();
|
||||
startTimers();
|
||||
} else {
|
||||
stopTimers();
|
||||
}
|
||||
};
|
||||
|
||||
const onMidnight = function () {
|
||||
if (!lcdPower) return;
|
||||
drawDate(new Date());
|
||||
// Lower part (steps/charge) will be updated every minute.
|
||||
// However, to save power while on battery only step count will get updated.
|
||||
// This will update icon and progress bar as well:
|
||||
if (!charging) drawSteps();
|
||||
drawWeather();
|
||||
};
|
||||
|
||||
const onHealth = function () {
|
||||
if (!lcdPower || charging) return;
|
||||
// This will update progress bar and icon:
|
||||
drawSteps();
|
||||
drawWeather();
|
||||
};
|
||||
|
||||
const onLock = function (locked) {
|
||||
if (locked) return;
|
||||
drawLower();
|
||||
};
|
||||
|
||||
const onCharging = function (isCharging) {
|
||||
charging = isCharging;
|
||||
if (isCharging && settings.buzzOnCharge) Bangle.buzz();
|
||||
if (!lcdPower) return;
|
||||
drawLower();
|
||||
};
|
||||
|
||||
|
||||
/* Lifecycle Functions
|
||||
------------------------------------------------------------------------------*/
|
||||
|
||||
const registerEvents = function () {
|
||||
// This is for original Bangle.js; version two has always-on display:
|
||||
Bangle.on('lcdPower', onLcdPower);
|
||||
|
||||
// Midnight event is triggered when health data is reset and a new day begins:
|
||||
Bangle.on('midnight', onMidnight);
|
||||
|
||||
// Health data is published via 10 mins interval:
|
||||
Bangle.on('health', onHealth);
|
||||
|
||||
// Lock event signals screen (un)lock:
|
||||
Bangle.on('lock', onLock);
|
||||
|
||||
// Charging event signals when charging status changes:
|
||||
Bangle.on('charging', onCharging);
|
||||
};
|
||||
|
||||
const deregisterEvents = function () {
|
||||
Bangle.removeListener('lcdPower', onLcdPower);
|
||||
Bangle.removeListener('midnight', onMidnight);
|
||||
Bangle.removeListener('health', onHealth);
|
||||
Bangle.removeListener('lock', onLock);
|
||||
Bangle.removeListener('charging', onCharging);
|
||||
};
|
||||
|
||||
const startTimers = function () {
|
||||
if (drawInterval) return;
|
||||
if (settings.showSeconds) {
|
||||
drawInterval = setInterval( onSecondInterval, 1000);
|
||||
} else {
|
||||
startTimeout = setTimeout(onMinuteIntervalStarter, (60 - new Date().getSeconds()) * 1000);
|
||||
}
|
||||
};
|
||||
|
||||
const stopTimers = function () {
|
||||
if (startTimeout) clearTimeout(startTimeout);
|
||||
if (!drawInterval) return;
|
||||
clearInterval(drawInterval);
|
||||
drawInterval = null;
|
||||
};
|
||||
|
||||
|
||||
/* Startup Process
|
||||
------------------------------------------------------------------------------*/
|
||||
|
||||
g.clear();
|
||||
drawAll();
|
||||
startTimers();
|
||||
registerEvents();
|
||||
|
||||
Bangle.setUI({mode: 'clock', remove: function() {
|
||||
stopTimers();
|
||||
deregisterEvents();
|
||||
}});
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
}
|
||||
|
After Width: | Height: | Size: 4.5 KiB |
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"id": "edgeclk",
|
||||
"name": "Edge Clock",
|
||||
"shortName": "Edge Clock",
|
||||
"version": "0.03",
|
||||
"description": "Crisp clock with perfect readability.",
|
||||
"readme": "README.md",
|
||||
"icon": "app.png",
|
||||
"screenshots": [{"url":"screenshot.png"}, {"url":"screenshot2.png"}, {"url":"screenshot3.png"}, {"url":"screenshot4.png"}],
|
||||
"type": "clock",
|
||||
"tags": "clock",
|
||||
"supports": ["BANGLEJS2"],
|
||||
"allow_emulator": true,
|
||||
"storage": [
|
||||
{"name":"edgeclk.app.js", "url": "app.js"},
|
||||
{"name":"edgeclk.settings.js", "url": "settings.js"},
|
||||
{"name":"edgeclk.img", "url": "app-icon.js", "evaluate": true}
|
||||
],
|
||||
"data": [{"name":"edgeclk.settings.json"}]
|
||||
}
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
|
@ -0,0 +1,125 @@
|
|||
(function(back) {
|
||||
const SETTINGS_FILE = 'edgeclk.settings.json';
|
||||
const storage = require('Storage');
|
||||
|
||||
const settings = {
|
||||
buzzOnCharge: true,
|
||||
monthFirst: true,
|
||||
twentyFourH: true,
|
||||
showAmPm: false,
|
||||
showSeconds: true,
|
||||
showWeather: false,
|
||||
stepGoal: 10000,
|
||||
stepBar: true,
|
||||
weekBar: true,
|
||||
mondayFirst: true,
|
||||
dayBar: true,
|
||||
};
|
||||
|
||||
const saved_settings = storage.readJSON(SETTINGS_FILE, true);
|
||||
if (saved_settings) {
|
||||
for (const key in saved_settings) {
|
||||
if (!settings.hasOwnProperty(key)) continue;
|
||||
settings[key] = saved_settings[key];
|
||||
}
|
||||
}
|
||||
|
||||
let save = function() {
|
||||
storage.write(SETTINGS_FILE, settings);
|
||||
}
|
||||
|
||||
E.showMenu({
|
||||
'': { 'title': 'Edge Clock' },
|
||||
'< Back': back,
|
||||
'Charge Buzz': {
|
||||
value: settings.buzzOnCharge,
|
||||
onchange: () => {
|
||||
settings.buzzOnCharge = !settings.buzzOnCharge;
|
||||
save();
|
||||
},
|
||||
},
|
||||
'Month First': {
|
||||
value: settings.monthFirst,
|
||||
onchange: () => {
|
||||
settings.monthFirst = !settings.monthFirst;
|
||||
save();
|
||||
},
|
||||
},
|
||||
'24h Clock': {
|
||||
value: settings.twentyFourH,
|
||||
onchange: () => {
|
||||
settings.twentyFourH = !settings.twentyFourH;
|
||||
save();
|
||||
},
|
||||
},
|
||||
'Show AM/PM': {
|
||||
value: settings.showAmPm,
|
||||
onchange: () => {
|
||||
settings.showAmPm = !settings.showAmPm;
|
||||
// TODO can this be visually changed?
|
||||
if (settings.showAmPm && settings.showSeconds) settings.showSeconds = false;
|
||||
if (settings.showAmPm && settings.showWeather) settings.showWeather = false;
|
||||
save();
|
||||
},
|
||||
},
|
||||
'Show Seconds': {
|
||||
value: settings.showSeconds,
|
||||
onchange: () => {
|
||||
settings.showSeconds = !settings.showSeconds;
|
||||
// TODO can this be visually changed?
|
||||
if (settings.showSeconds && settings.showAmPm) settings.showAmPm = false;
|
||||
if (settings.showSeconds && settings.showWeather) settings.showWeather = false;
|
||||
save();
|
||||
},
|
||||
},
|
||||
'Show Weather': {
|
||||
value: settings.showWeather,
|
||||
onchange: () => {
|
||||
settings.showWeather = !settings.showWeather;
|
||||
// TODO can this be visually changed?
|
||||
if (settings.showWeather && settings.showAmPm) settings.showAmPm = false;
|
||||
if (settings.showWeather && settings.showSeconds) settings.showSeconds = false;
|
||||
save();
|
||||
},
|
||||
},
|
||||
'Step Goal': {
|
||||
value: settings.stepGoal,
|
||||
min: 250,
|
||||
max: 50000,
|
||||
step: 250,
|
||||
onchange: v => {
|
||||
settings.stepGoal = v;
|
||||
save();
|
||||
}
|
||||
},
|
||||
'Step Progress': {
|
||||
value: settings.stepBar,
|
||||
onchange: () => {
|
||||
settings.stepBar = !settings.stepBar;
|
||||
save();
|
||||
}
|
||||
},
|
||||
'Week Progress': {
|
||||
value: settings.weekBar,
|
||||
onchange: () => {
|
||||
settings.weekBar = !settings.weekBar;
|
||||
save();
|
||||
},
|
||||
},
|
||||
'Week Start': {
|
||||
value: settings.mondayFirst,
|
||||
format: () => settings.mondayFirst ? 'Monday' : 'Sunday',
|
||||
onchange: () => {
|
||||
settings.mondayFirst = !settings.mondayFirst;
|
||||
save();
|
||||
},
|
||||
},
|
||||
'Day Progress': {
|
||||
value: settings.dayBar,
|
||||
onchange: () => {
|
||||
settings.dayBar = !settings.dayBar;
|
||||
save();
|
||||
},
|
||||
},
|
||||
});
|
||||
})
|
||||
|
|
@ -2,3 +2,5 @@
|
|||
0.02: Allow redirection of loads to the launcher
|
||||
0.03: Allow hiding the fastloading info screen
|
||||
0.04: (WIP) Allow use of app history when going back (`load()` or `Bangle.load()` calls without specified app).
|
||||
0.05: Check for changes in setting.js and force real reload if needed
|
||||
0.06: Fix caching whether an app is fastloadable
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ This allows fast loading of all apps with two conditions:
|
|||
* If Quick Launch is installed it can be excluded from app history
|
||||
* Allows to redirect all loads usually loading the clock to the launcher instead
|
||||
* The "Fastloading..." screen can be switched off
|
||||
* Enable checking `setting.json` and force a complete load on changes
|
||||
|
||||
## App history
|
||||
|
||||
|
|
|
|||
|
|
@ -19,15 +19,24 @@ let loadingScreen = function(){
|
|||
|
||||
let cache = s.readJSON("fastload.cache") || {};
|
||||
|
||||
let checkApp = function(n){
|
||||
const SYS_SETTINGS="setting.json";
|
||||
|
||||
let appFastloadPossible = function(n){
|
||||
if(SETTINGS.detectSettingsChange && (!cache[SYS_SETTINGS] || s.hash(SYS_SETTINGS) != cache[SYS_SETTINGS])){
|
||||
cache[SYS_SETTINGS] = s.hash(SYS_SETTINGS);
|
||||
s.writeJSON("fastload.cache", cache);
|
||||
return false;
|
||||
}
|
||||
|
||||
// no widgets, no problem
|
||||
if (!global.WIDGETS) return true;
|
||||
let app = s.read(n);
|
||||
if (cache[n] && E.CRC32(app) == cache[n].crc)
|
||||
let hash = s.hash(n);
|
||||
if (cache[n] && hash == cache[n].hash)
|
||||
return cache[n].fast;
|
||||
let app = s.read(n);
|
||||
cache[n] = {};
|
||||
cache[n].fast = app.includes("Bangle.loadWidgets");
|
||||
cache[n].crc = E.CRC32(app);
|
||||
cache[n].hash = hash;
|
||||
s.writeJSON("fastload.cache", cache);
|
||||
return cache[n].fast;
|
||||
};
|
||||
|
|
@ -39,7 +48,7 @@ let slowload = function(n){
|
|||
};
|
||||
|
||||
let fastload = function(n){
|
||||
if (!n || checkApp(n)){
|
||||
if (!n || appFastloadPossible(n)){
|
||||
// Bangle.load can call load, to prevent recursion this must be the system load
|
||||
global.load = slowload;
|
||||
Bangle.load(n);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{ "id": "fastload",
|
||||
"name": "Fastload Utils",
|
||||
"shortName" : "Fastload Utils",
|
||||
"version": "0.04",
|
||||
"version": "0.06",
|
||||
"icon": "icon.png",
|
||||
"description": "Enable experimental fastloading for more apps",
|
||||
"type":"bootloader",
|
||||
|
|
|
|||
|
|
@ -59,6 +59,13 @@
|
|||
}
|
||||
};
|
||||
|
||||
mainmenu['Detect settings changes'] = {
|
||||
value: !!settings.detectSettingsChange,
|
||||
onchange: v => {
|
||||
writeSettings("detectSettingsChange",v);
|
||||
}
|
||||
};
|
||||
|
||||
return mainmenu;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,179 +1 @@
|
|||
# Watch -> Phone
|
||||
|
||||
## show toast
|
||||
|
||||
```
|
||||
{ "t": "info", "msg": "message" }
|
||||
```
|
||||
|
||||
t can be one of "info", "warn", "error"
|
||||
|
||||
## report battery level
|
||||
|
||||
```
|
||||
{ "t": "status", "bat": 30, "volt": 30, "chg": 0 }
|
||||
```
|
||||
|
||||
* bat is in range 0 to 100
|
||||
* volt is optional and should be greater than 0
|
||||
* chg is optional and should be either 0 or 1 to indicate the watch is charging
|
||||
|
||||
## find phone
|
||||
|
||||
```
|
||||
{ "t": "findPhone", "n": true }
|
||||
```
|
||||
|
||||
n is an boolean and toggles the find phone function
|
||||
|
||||
## control music player
|
||||
|
||||
```
|
||||
{ "t": "music", "n": "play" }
|
||||
```
|
||||
|
||||
n can be one of "play", "pause", "playpause", "next", "previous", "volumeup", "volumedown", "forward", "rewind"
|
||||
|
||||
## control phone call
|
||||
|
||||
```
|
||||
{ "t": "call", "n": "accept"}
|
||||
```
|
||||
|
||||
n can be one of "accept", "end", "incoming", "outcoming", "reject", "start", "ignore"
|
||||
|
||||
## react to notifications
|
||||
|
||||
Send a response to a notification from phone
|
||||
|
||||
```
|
||||
{
|
||||
"t": "notify",
|
||||
"n": "dismiss",
|
||||
"id": 2,
|
||||
"tel": "+491234",
|
||||
"msg": "message",
|
||||
}
|
||||
```
|
||||
|
||||
* n can be one of "dismiss", "dismiss all", "open", "mute", "reply"
|
||||
* id, tel and message are optional
|
||||
|
||||
# Phone -> Watch
|
||||
|
||||
## show notification
|
||||
|
||||
```
|
||||
{
|
||||
"t": "notify",
|
||||
"id": 2,
|
||||
"src": "app",
|
||||
"title": "titel",
|
||||
"subject": "subject",
|
||||
"body": "message body",
|
||||
"sender": "sender",
|
||||
"tel": "+491234"
|
||||
}
|
||||
```
|
||||
|
||||
## notification deleted
|
||||
|
||||
This event is send when the user skipped a notification
|
||||
|
||||
```
|
||||
{ "t": "notify-", "id": 2 }
|
||||
```
|
||||
|
||||
## set alarm
|
||||
|
||||
```
|
||||
{
|
||||
"t": "alarm",
|
||||
"d": [
|
||||
{ "h": 13, "m": 37 },
|
||||
{ "h": 8, "m": 0 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## call state changed
|
||||
|
||||
```
|
||||
{
|
||||
"t": "call",
|
||||
"cmd": "accept",
|
||||
"name": "name",
|
||||
"number": "+491234"
|
||||
}
|
||||
```
|
||||
|
||||
cmd can be one of "", "undefined", "accept", "incoming", "outgoing", "reject", "start", "end"
|
||||
|
||||
## music state changed
|
||||
|
||||
```
|
||||
{
|
||||
"t": "musicstate",
|
||||
"state": "play",
|
||||
"position": 40,
|
||||
"shuffle": 0,
|
||||
"repeat": 1
|
||||
}
|
||||
```
|
||||
|
||||
## set music info
|
||||
|
||||
```
|
||||
{
|
||||
"t": "musicinfo",
|
||||
"artist": "artist",
|
||||
"album": "album",
|
||||
"track": "track",
|
||||
"dur": 1,
|
||||
"c": 2,
|
||||
"n" 3
|
||||
}
|
||||
```
|
||||
|
||||
* dur is the duration of the track
|
||||
* c is the track count
|
||||
* n is the track number
|
||||
|
||||
## find device
|
||||
|
||||
```
|
||||
{
|
||||
"t": "find",
|
||||
"n": true
|
||||
}
|
||||
```
|
||||
|
||||
n toggles find device functionality
|
||||
|
||||
## set constant vibration
|
||||
|
||||
```
|
||||
{
|
||||
"t": "vibrate",
|
||||
"n": 2
|
||||
}
|
||||
```
|
||||
|
||||
n is the intensity
|
||||
|
||||
## send weather
|
||||
|
||||
```
|
||||
{
|
||||
"t": "weather",
|
||||
"temp": 10,
|
||||
"hum": 71,
|
||||
"txt": "condition",
|
||||
"wind": 13,
|
||||
"loc": "location"
|
||||
}
|
||||
```
|
||||
|
||||
* hum is the humidity
|
||||
* txt is the weather condition
|
||||
* loc is the location
|
||||
For up to date protocol info, please see http://www.espruino.com/Gadgetbridge
|
||||
|
|
|
|||
|
|
@ -263,7 +263,7 @@
|
|||
function sendActivity(hrm) {
|
||||
var steps = currentSteps - lastSentSteps;
|
||||
lastSentSteps = currentSteps;
|
||||
gbSend({ t: "act", stp: steps, hrm:hrm });
|
||||
gbSend({ t: "act", stp: steps, hrm:hrm, rt:1 });
|
||||
}
|
||||
|
||||
// Battery monitor
|
||||
|
|
|
|||
|
|
@ -87,3 +87,23 @@
|
|||
* Reduce framerate if locked
|
||||
* Stroke to move around in the map
|
||||
* Fix for missing paths in display
|
||||
|
||||
0.20:
|
||||
* Large display for instant speed
|
||||
* Bugfix for negative coordinates
|
||||
* Disable menu while the map is not loaded
|
||||
* Turn screen off while idling to save battery (with setting)
|
||||
* New setting : disable buzz on turns
|
||||
* New setting : turn bluetooth off to save battery
|
||||
* New setting : power screen off between points to save battery
|
||||
* Color change for lost direction (now purple)
|
||||
* Adaptive screen powersaving
|
||||
|
||||
0.21:
|
||||
* Jit is back for display functions (10% speed increase)
|
||||
* Store, parse and display elevation data
|
||||
* Removed 'lost' indicator (we now change position to purple when lost)
|
||||
* Powersaving fix : don't powersave when lost
|
||||
* Bugfix for negative remaining distance when going backwards
|
||||
* New settings for powersaving
|
||||
* Adjustments to powersaving algorithm
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ It provides the following features :
|
|||
- display the path with current position from gps
|
||||
- display a local map around you, downloaded from openstreetmap
|
||||
- detects and buzzes if you leave the path
|
||||
- buzzes before sharp turns
|
||||
- buzzes before waypoints
|
||||
- (optional) buzzes before sharp turns
|
||||
- (optional) buzzes before waypoints
|
||||
(for example when you need to turn in https://mapstogpx.com/)
|
||||
- display instant / average speed
|
||||
- display distance to next point
|
||||
|
|
@ -28,6 +28,7 @@ It provides the following features :
|
|||
- toilets
|
||||
- artwork
|
||||
- bakeries
|
||||
- display elevation data if available in the trace
|
||||
|
||||
## Usage
|
||||
|
||||
|
|
@ -41,7 +42,7 @@ also a nice open source option.
|
|||
Note that *mapstogpx* has a super nice feature in its advanced settings.
|
||||
You can turn on 'next turn info' and be warned by the watch when you need to turn.
|
||||
|
||||
Once you have your gpx file you need to convert it to *gpc* which is my custom file format.
|
||||
Once you have your gpx file you need to convert it to *gps* which is my custom file format.
|
||||
They are smaller than gpx and reduce the number of computations left to be done on the watch.
|
||||
|
||||
Just click the disk icon and select your gpx file.
|
||||
|
|
@ -51,8 +52,8 @@ Your path will be displayed in svg.
|
|||
### Starting Gipy
|
||||
|
||||
At start you will have a menu for selecting your trace (if more than one).
|
||||
Choose the one you want and you will reach the splash screen where you'll wait for the gps signal.
|
||||
Once you have a signal you will reach the main screen:
|
||||
Choose the one you want and you will reach the splash screen where you'll wait for the map.
|
||||
Once the map is loaded you will reach the main screen:
|
||||
|
||||

|
||||
|
||||
|
|
@ -78,28 +79,74 @@ On your screen you can see:
|
|||
* green: artwork
|
||||
- a *turn* indicator on the top right when you reach a turning point
|
||||
- a *gps* indicator (blinking) on the top right if you lose gps signal
|
||||
- a *lost* indicator on the top right if you stray too far away from path
|
||||
|
||||
### Lost
|
||||
|
||||
If you stray away from path we will rescale the display to continue displaying nearby segments and
|
||||
display the direction to follow as a black segment.
|
||||
If you stray away from path we will display the direction to follow as a purple segment. Your main position will also turn to purple.
|
||||
|
||||
Note that while lost, the app will slow down a lot since it will start scanning all possible points to figure out where you
|
||||
are. On path it just needed to scan a few points ahead and behind.
|
||||
|
||||
The distance to next point displayed corresponds to the length of the black segment.
|
||||
The distance to next point displayed corresponds to the length of the purple segment.
|
||||
|
||||
### Menu
|
||||
|
||||
If you click the button you'll reach a menu where you can currently zoom out to see more of the map
|
||||
(with a slower refresh rate) and reverse the path direction.
|
||||
(with a slower refresh rate), reverse the path direction and disable power saving (keeping backlight on).
|
||||
|
||||
### Elevation
|
||||
|
||||
If you touch the screen you will switch between display modes.
|
||||
The first one displays the map, the second one the nearby elevation and the last one the elevation
|
||||
for the whole path.
|
||||
|
||||

|
||||
|
||||
Colors correspond to slopes.
|
||||
Above 15% will be red, above 8% orange, above 3% yellow, between 3% and -3% is green and shades of blue
|
||||
are for descents.
|
||||
|
||||
You should note that the precision is not very good. The input data is not very precise and you only get the
|
||||
slopes between path points. Don't expect to see small bumps on the road.
|
||||
|
||||
### Settings
|
||||
|
||||
Few settings for now (feel free to suggest me more) :
|
||||
|
||||
- buzz on turns : should the watch buzz when reaching a waypoint ?
|
||||
- disable bluetooth : turn bluetooth off completely to try to save some power.
|
||||
- lost distance : at which distance from path are you considered to be lost ?
|
||||
- wake-up speed : if you drive below this speed powersaving will disable itself
|
||||
- active-time : how long (in seconds) the screen should be turned on if activated before going back to sleep.
|
||||
- brightness : how bright should screen be ? (by default 0.5, again saving power)
|
||||
- power lcd off (disabled by default): turn lcd off when inactive to save power. the watch will wake up when reaching points,
|
||||
when you touch the screen and when speed is below 13km/h.
|
||||
|
||||
### Powersaving
|
||||
|
||||
Starting with release 0.20 we experiment with power saving.
|
||||
|
||||
There are now two display modes :
|
||||
|
||||
- active : the screen is lit back (default at 50% light but can be configured with the *brightness* setting)
|
||||
- inactive : by default the screen is not lit but you can also power it off completely (with the *power lcd off* setting)
|
||||
|
||||
The algorithm works in the following ways :
|
||||
|
||||
- some events will *activate* : the display will turn *active*
|
||||
- if no activation event occur for at least 10 seconds (or *active-time* setting) we switch back to *inactive*
|
||||
|
||||
Activation events are the following :
|
||||
|
||||
- you are near (< 100m) the next point on path
|
||||
- you are slow (< *wake-up speed* setting (13 km/h by default))
|
||||
- you press the button / touch the screen
|
||||
|
||||
|
||||
Power saving has been tested on a very long trip with several benefits
|
||||
|
||||
- longer battery life
|
||||
- waking up near path points will attract your attention more easily when needed
|
||||
|
||||
### Caveats
|
||||
|
||||
|
|
@ -107,6 +154,7 @@ It is good to use but you should know :
|
|||
|
||||
- the gps might take a long time to start initially (see the assisted gps update app).
|
||||
- gps signal is noisy : there is therefore a small delay for instant speed. sometimes you may jump somewhere else.
|
||||
- if you adventure in gorges the gps signal will become garbage.
|
||||
- your gpx trace has been decimated and approximated : the **REAL PATH** might be **A FEW METERS AWAY**
|
||||
- sometimes the watch will tell you that you are lost but you are in fact on the path. It usually figures again
|
||||
the real gps position after a few minutes. It usually happens when the signal is acquired very fast.
|
||||
|
|
@ -119,4 +167,8 @@ I had to go back uphill by quite a distance.
|
|||
|
||||
Feel free to give me feedback : is it useful for you ? what other features would you like ?
|
||||
|
||||
If you want to raise issues the main repository is [https://github.com/wagnerf42/BangleApps](here) and
|
||||
the rust code doing the actual map computations is located [https://github.com/wagnerf42/gps](here).
|
||||
You can try the cutting edge version at [https://wagnerf42.github.io/BangleApps/](https://wagnerf42.github.io/BangleApps/)
|
||||
|
||||
frederic.wagner@imag.fr
|
||||
|
|
|
|||
|
|
@ -1,19 +1,57 @@
|
|||
*** thoughts on lcd power ***
|
||||
|
||||
so, i tried experimenting with turning the lcd off in order to save power.
|
||||
|
||||
the good news: this saves a lot. i did a 3h ride which usually depletes the battery and I still had
|
||||
around two more hours to go.
|
||||
|
||||
now the bad news:
|
||||
|
||||
- i had to de-activate the twist detection : you cannot raise your watch to the eyes to turn it on.
|
||||
that's because with twist detection on all road bumps turn the watch on constantly.
|
||||
- i tried manual detection like :
|
||||
|
||||
Bangle.on('accel', function(xyz) {
|
||||
|
||||
if (xyz.diff > 0.4 && xyz.mag > 1 && xyz.z < -1.4) {
|
||||
Bangle.setLCDPower(true);
|
||||
Bangle.setLocked(false);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
this works nicely when you sit on a chair with a simulated gps signal but does not work so nicely when on the bike.
|
||||
sometimes it is ok, sometimes you can try 10 times with no success.
|
||||
|
||||
- instead i use screen touch to turn it on. that's a bother since you need two hands but well it could be worth it.
|
||||
the problem is in the delay: between 1 and 5 seconds before the screen comes back on.
|
||||
|
||||
|
||||
my conclusion is that:
|
||||
|
||||
* we should not turn screen off unless we agree to have an unresponsive ui
|
||||
* we should maybe autowake near segments ends and when lost
|
||||
* we should play with backlight instead
|
||||
|
||||
|
||||
**************************
|
||||
|
||||
JIT: array declaration in jit is buggy
|
||||
(especially several declarations)
|
||||
|
||||
**************************
|
||||
|
||||
+ try disabling gps for more powersaving
|
||||
|
||||
+ when you walk the direction still has a tendency to shift
|
||||
|
||||
+ put back foot only ways
|
||||
+ put back street names
|
||||
+ put back shortest paths but with points cache this time and jit
|
||||
+ how to display paths from shortest path ?
|
||||
|
||||
|
||||
misc:
|
||||
+ use Bangle.project(latlong)
|
||||
|
||||
* additional features
|
||||
|
||||
- config screen
|
||||
- are we on foot (and should use compass)
|
||||
|
||||
- we need to buzz 200m before sharp turns (or even better, 30seconds)
|
||||
(and look at more than next point)
|
||||
|
||||
- display distance to next water/toilet ?
|
||||
- display scale (100m)
|
||||
|
||||
- compress path ?
|
||||
|
||||
* misc
|
||||
|
||||
- code is becoming messy
|
||||
|
|
|
|||
724
apps/gipy/app.js
|
After Width: | Height: | Size: 3.5 KiB |
|
|
@ -2,13 +2,13 @@
|
|||
"id": "gipy",
|
||||
"name": "Gipy",
|
||||
"shortName": "Gipy",
|
||||
"version": "0.19",
|
||||
"version": "0.21",
|
||||
"description": "Follow gpx files using the gps. Don't get lost in your bike trips and hikes.",
|
||||
"allow_emulator":false,
|
||||
"icon": "gipy.png",
|
||||
"type": "app",
|
||||
"tags": "tool,outdoors,gps",
|
||||
"screenshots": [{"url":"splash.png"}],
|
||||
"screenshots": [{"url":"splash.png"}, {"url":"heights.png"}, {"url":"shot.png"}],
|
||||
"supports": ["BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
"interface": "interface.html",
|
||||
|
|
|
|||
|
|
@ -12,6 +12,11 @@ export function get_gps_map_svg(gps: Gps): string;
|
|||
export function get_polygon(gps: Gps): Float64Array;
|
||||
/**
|
||||
* @param {Gps} gps
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function has_heights(gps: Gps): boolean;
|
||||
/**
|
||||
* @param {Gps} gps
|
||||
* @returns {Float64Array}
|
||||
*/
|
||||
export function get_polyline(gps: Gps): Float64Array;
|
||||
|
|
@ -59,6 +64,7 @@ export interface InitOutput {
|
|||
readonly __wbg_gps_free: (a: number) => void;
|
||||
readonly get_gps_map_svg: (a: number, b: number) => void;
|
||||
readonly get_polygon: (a: number, b: number) => void;
|
||||
readonly has_heights: (a: number) => number;
|
||||
readonly get_polyline: (a: number, b: number) => void;
|
||||
readonly get_gps_content: (a: number, b: number) => void;
|
||||
readonly request_map: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number, l: number, m: number, n: number, o: number, p: number, q: number) => number;
|
||||
|
|
@ -67,11 +73,11 @@ export interface InitOutput {
|
|||
readonly __wbindgen_malloc: (a: number) => number;
|
||||
readonly __wbindgen_realloc: (a: number, b: number, c: number) => number;
|
||||
readonly __wbindgen_export_2: WebAssembly.Table;
|
||||
readonly _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hab13c10d53cd1c5a: (a: number, b: number, c: number) => void;
|
||||
readonly _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hb15c13006e54cdd7: (a: number, b: number, c: number) => void;
|
||||
readonly __wbindgen_add_to_stack_pointer: (a: number) => number;
|
||||
readonly __wbindgen_free: (a: number, b: number) => void;
|
||||
readonly __wbindgen_exn_store: (a: number) => void;
|
||||
readonly wasm_bindgen__convert__closures__invoke2_mut__h26ce002f44a5439b: (a: number, b: number, c: number, d: number) => void;
|
||||
readonly wasm_bindgen__convert__closures__invoke2_mut__h4d77bafb1e69a027: (a: number, b: number, c: number, d: number) => void;
|
||||
}
|
||||
|
||||
export type SyncInitInput = BufferSource | WebAssembly.Module;
|
||||
|
|
|
|||
|
|
@ -205,7 +205,7 @@ function makeMutClosure(arg0, arg1, dtor, f) {
|
|||
return real;
|
||||
}
|
||||
function __wbg_adapter_24(arg0, arg1, arg2) {
|
||||
wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hab13c10d53cd1c5a(arg0, arg1, addHeapObject(arg2));
|
||||
wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hb15c13006e54cdd7(arg0, arg1, addHeapObject(arg2));
|
||||
}
|
||||
|
||||
function _assertClass(instance, klass) {
|
||||
|
|
@ -263,6 +263,16 @@ export function get_polygon(gps) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Gps} gps
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function has_heights(gps) {
|
||||
_assertClass(gps, Gps);
|
||||
const ret = wasm.has_heights(gps.ptr);
|
||||
return ret !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Gps} gps
|
||||
* @returns {Float64Array}
|
||||
|
|
@ -368,8 +378,8 @@ function handleError(f, args) {
|
|||
wasm.__wbindgen_exn_store(addHeapObject(e));
|
||||
}
|
||||
}
|
||||
function __wbg_adapter_84(arg0, arg1, arg2, arg3) {
|
||||
wasm.wasm_bindgen__convert__closures__invoke2_mut__h26ce002f44a5439b(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3));
|
||||
function __wbg_adapter_85(arg0, arg1, arg2, arg3) {
|
||||
wasm.wasm_bindgen__convert__closures__invoke2_mut__h4d77bafb1e69a027(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -430,9 +440,6 @@ async function load(module, imports) {
|
|||
function getImports() {
|
||||
const imports = {};
|
||||
imports.wbg = {};
|
||||
imports.wbg.__wbg_log_d04343b58be82b0f = function(arg0, arg1) {
|
||||
console.log(getStringFromWasm0(arg0, arg1));
|
||||
};
|
||||
imports.wbg.__wbindgen_string_get = function(arg0, arg1) {
|
||||
const obj = getObject(arg1);
|
||||
const ret = typeof(obj) === 'string' ? obj : undefined;
|
||||
|
|
@ -441,6 +448,9 @@ function getImports() {
|
|||
getInt32Memory0()[arg0 / 4 + 1] = len0;
|
||||
getInt32Memory0()[arg0 / 4 + 0] = ptr0;
|
||||
};
|
||||
imports.wbg.__wbg_log_d04343b58be82b0f = function(arg0, arg1) {
|
||||
console.log(getStringFromWasm0(arg0, arg1));
|
||||
};
|
||||
imports.wbg.__wbindgen_object_drop_ref = function(arg0) {
|
||||
takeObject(arg0);
|
||||
};
|
||||
|
|
@ -460,6 +470,21 @@ function getImports() {
|
|||
const ret = getObject(arg0).fetch(getObject(arg1));
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbg_newwithstrandinit_05d7180788420c40 = function() { return handleError(function (arg0, arg1, arg2) {
|
||||
const ret = new Request(getStringFromWasm0(arg0, arg1), getObject(arg2));
|
||||
return addHeapObject(ret);
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_signal_31753ac644b25fbb = function(arg0) {
|
||||
const ret = getObject(arg0).signal;
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbg_new_6396e586b56e1dff = function() { return handleError(function () {
|
||||
const ret = new AbortController();
|
||||
return addHeapObject(ret);
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_abort_064ae59cda5cd244 = function(arg0) {
|
||||
getObject(arg0).abort();
|
||||
};
|
||||
imports.wbg.__wbg_new_2d0053ee81e4dd2a = function() { return handleError(function () {
|
||||
const ret = new Headers();
|
||||
return addHeapObject(ret);
|
||||
|
|
@ -496,21 +521,6 @@ function getImports() {
|
|||
const ret = getObject(arg0).text();
|
||||
return addHeapObject(ret);
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_signal_31753ac644b25fbb = function(arg0) {
|
||||
const ret = getObject(arg0).signal;
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbg_new_6396e586b56e1dff = function() { return handleError(function () {
|
||||
const ret = new AbortController();
|
||||
return addHeapObject(ret);
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_abort_064ae59cda5cd244 = function(arg0) {
|
||||
getObject(arg0).abort();
|
||||
};
|
||||
imports.wbg.__wbg_newwithstrandinit_05d7180788420c40 = function() { return handleError(function (arg0, arg1, arg2) {
|
||||
const ret = new Request(getStringFromWasm0(arg0, arg1), getObject(arg2));
|
||||
return addHeapObject(ret);
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_new_abda76e883ba8a5f = function() {
|
||||
const ret = new Error();
|
||||
return addHeapObject(ret);
|
||||
|
|
@ -610,7 +620,7 @@ function getImports() {
|
|||
const a = state0.a;
|
||||
state0.a = 0;
|
||||
try {
|
||||
return __wbg_adapter_84(a, state0.b, arg0, arg1);
|
||||
return __wbg_adapter_85(a, state0.b, arg0, arg1);
|
||||
} finally {
|
||||
state0.a = a;
|
||||
}
|
||||
|
|
@ -675,8 +685,8 @@ function getImports() {
|
|||
const ret = wasm.memory;
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbindgen_closure_wrapper2298 = function(arg0, arg1, arg2) {
|
||||
const ret = makeMutClosure(arg0, arg1, 260, __wbg_adapter_24);
|
||||
imports.wbg.__wbindgen_closure_wrapper2214 = function(arg0, arg1, arg2) {
|
||||
const ret = makeMutClosure(arg0, arg1, 268, __wbg_adapter_24);
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ export const memory: WebAssembly.Memory;
|
|||
export function __wbg_gps_free(a: number): void;
|
||||
export function get_gps_map_svg(a: number, b: number): void;
|
||||
export function get_polygon(a: number, b: number): void;
|
||||
export function has_heights(a: number): number;
|
||||
export function get_polyline(a: number, b: number): void;
|
||||
export function get_gps_content(a: number, b: number): void;
|
||||
export function request_map(a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number, l: number, m: number, n: number, o: number, p: number, q: number): number;
|
||||
|
|
@ -12,8 +13,8 @@ export function gps_from_area(a: number, b: number, c: number, d: number): numbe
|
|||
export function __wbindgen_malloc(a: number): number;
|
||||
export function __wbindgen_realloc(a: number, b: number, c: number): number;
|
||||
export const __wbindgen_export_2: WebAssembly.Table;
|
||||
export function _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hab13c10d53cd1c5a(a: number, b: number, c: number): void;
|
||||
export function _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hb15c13006e54cdd7(a: number, b: number, c: number): void;
|
||||
export function __wbindgen_add_to_stack_pointer(a: number): number;
|
||||
export function __wbindgen_free(a: number, b: number): void;
|
||||
export function __wbindgen_exn_store(a: number): void;
|
||||
export function wasm_bindgen__convert__closures__invoke2_mut__h26ce002f44a5439b(a: number, b: number, c: number, d: number): void;
|
||||
export function wasm_bindgen__convert__closures__invoke2_mut__h4d77bafb1e69a027(a: number, b: number, c: number, d: number): void;
|
||||
|
|
|
|||
|
|
@ -1,29 +1,86 @@
|
|||
(function (back) {
|
||||
var FILE = "gipy.json";
|
||||
// Load settings
|
||||
var settings = Object.assign(
|
||||
{
|
||||
lost_distance: 50,
|
||||
},
|
||||
require("Storage").readJSON(FILE, true) || {}
|
||||
);
|
||||
(function(back) {
|
||||
var FILE = "gipy.json";
|
||||
// Load settings
|
||||
var settings = Object.assign({
|
||||
lost_distance: 50,
|
||||
wake_up_speed: 13,
|
||||
active_time: 10,
|
||||
buzz_on_turns: false,
|
||||
disable_bluetooth: true,
|
||||
brightness: 0.5,
|
||||
power_lcd_off: false,
|
||||
},
|
||||
require("Storage").readJSON(FILE, true) || {}
|
||||
);
|
||||
|
||||
function writeSettings() {
|
||||
require("Storage").writeJSON(FILE, settings);
|
||||
}
|
||||
function writeSettings() {
|
||||
require("Storage").writeJSON(FILE, settings);
|
||||
}
|
||||
|
||||
// Show the menu
|
||||
E.showMenu({
|
||||
"": { title: "Gipy" },
|
||||
"< Back": () => back(),
|
||||
"lost distance": {
|
||||
value: 50 | settings.lost_distance, // 0| converts undefined to 0
|
||||
min: 10,
|
||||
max: 500,
|
||||
onchange: (v) => {
|
||||
settings.max_speed = v;
|
||||
writeSettings();
|
||||
},
|
||||
},
|
||||
});
|
||||
// Show the menu
|
||||
E.showMenu({
|
||||
"": {
|
||||
title: "Gipy"
|
||||
},
|
||||
"< Back": () => back(),
|
||||
/*LANG*/"buzz on turns": {
|
||||
value: settings.buzz_on_turns == true,
|
||||
onchange: (v) => {
|
||||
settings.buzz_on_turns = v;
|
||||
writeSettings();
|
||||
}
|
||||
},
|
||||
/*LANG*/"disable bluetooth": {
|
||||
value: settings.disable_bluetooth == true,
|
||||
onchange: (v) => {
|
||||
settings.disable_bluetooth = v;
|
||||
writeSettings();
|
||||
}
|
||||
},
|
||||
"lost distance": {
|
||||
value: settings.lost_distance,
|
||||
min: 10,
|
||||
max: 500,
|
||||
onchange: (v) => {
|
||||
settings.lost_distance = v;
|
||||
writeSettings();
|
||||
},
|
||||
},
|
||||
"wake-up speed": {
|
||||
value: settings.wake_up_speed,
|
||||
min: 0,
|
||||
max: 30,
|
||||
onchange: (v) => {
|
||||
settings.wake_up_speed = v;
|
||||
writeSettings();
|
||||
},
|
||||
},
|
||||
"active time": {
|
||||
value: settings.active_time,
|
||||
min: 5,
|
||||
max: 60,
|
||||
onchange: (v) => {
|
||||
settings.active_time = v;
|
||||
writeSettings();
|
||||
},
|
||||
},
|
||||
"brightness": {
|
||||
value: settings.brightness,
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.1,
|
||||
onchange: (v) => {
|
||||
settings.brightness = v;
|
||||
writeSettings();
|
||||
},
|
||||
},
|
||||
|
||||
/*LANG*/"power lcd off": {
|
||||
value: settings.power_lcd_off == true,
|
||||
onchange: (v) => {
|
||||
settings.power_lcd_off = v;
|
||||
writeSettings();
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 4.6 KiB |
|
|
@ -15,4 +15,7 @@
|
|||
Save state if route or waypoint has been chosen
|
||||
0.09: Workaround a minifier issue allowing to install gpstrek with minification enabled
|
||||
0.10: Adds map view of loaded route
|
||||
Automatically search for new waypoint if moving away from current target
|
||||
Automatically search for new waypoint if moving away from current target
|
||||
0.11: Adds configuration
|
||||
Draws direction arrows on route
|
||||
Turn of compass when GPS fix is available
|
||||
|
|
@ -7,25 +7,12 @@ const MODE_SLICES = 2;
|
|||
|
||||
const STORAGE = require("Storage");
|
||||
const BAT_FULL = require("Storage").readJSON("setting.json").batFullVoltage || 0.3144;
|
||||
const SETTINGS = {
|
||||
mapCompass: true,
|
||||
mapScale:0.2, //initial value
|
||||
mapRefresh:1000, //minimum time in ms between refreshs of the map
|
||||
mapChunkSize: 5, //render this many waypoints at a time
|
||||
overviewScroll: 30, //scroll this amount on swipe in pixels
|
||||
overviewScale: 0.02, //initial value
|
||||
refresh:500, //general refresh interval in ms
|
||||
refreshLocked:3000, //general refresh interval when Bangle is locked
|
||||
cacheMinFreeMem:2000,
|
||||
cacheMaxEntries:0,
|
||||
minCourseChange: 5, //course change needed in degrees before redrawing the map
|
||||
minPosChange: 5, //position change needed in pixels before redrawing the map
|
||||
waypointChangeDist: 50, //distance in m to next waypoint before advancing automatically
|
||||
queueWaitingTime: 5, // waiting time during processing of task queue items when running with timeouts
|
||||
autosearch: true,
|
||||
maxDistForAutosearch: 300,
|
||||
autosearchLimit: 3
|
||||
};
|
||||
|
||||
|
||||
const SETTINGS = Object.assign(
|
||||
require('Storage').readJSON("gpstrek.default.json", true) || {},
|
||||
require('Storage').readJSON("gpstrek.json", true) || {}
|
||||
);
|
||||
|
||||
let init = function(){
|
||||
global.screen = 1;
|
||||
|
|
@ -38,7 +25,6 @@ let init = function(){
|
|||
|
||||
Bangle.loadWidgets();
|
||||
WIDGETS.gpstrek.start(false);
|
||||
if (!WIDGETS.gpstrek.getState().numberOfSlices) WIDGETS.gpstrek.getState().numberOfSlices = 2;
|
||||
if (!WIDGETS.gpstrek.getState().mode) WIDGETS.gpstrek.getState().mode = MODE_MENU;
|
||||
};
|
||||
|
||||
|
|
@ -184,11 +170,6 @@ let getDoubleLineSlice = function(title1,title2,provider1,provider2){
|
|||
};
|
||||
};
|
||||
|
||||
const dot = Graphics.createImage(`
|
||||
XX
|
||||
XX
|
||||
`);
|
||||
|
||||
const arrow = Graphics.createImage(`
|
||||
X
|
||||
XXX
|
||||
|
|
@ -198,6 +179,14 @@ const arrow = Graphics.createImage(`
|
|||
XXX XXX
|
||||
`);
|
||||
|
||||
const thinarrow = Graphics.createImage(`
|
||||
X
|
||||
XXX
|
||||
XX XX
|
||||
XX XX
|
||||
XX XX
|
||||
`);
|
||||
|
||||
const cross = Graphics.createImage(`
|
||||
XX XX
|
||||
XX XX
|
||||
|
|
@ -459,7 +448,7 @@ let getMapSlice = function(){
|
|||
if (!isMapOverview){
|
||||
drawCurrentPos();
|
||||
}
|
||||
if (!isMapOverview && renderInTimeouts){
|
||||
if (SETTINGS.mapCompass && !isMapOverview && renderInTimeouts){
|
||||
drawMapCompass();
|
||||
}
|
||||
if (renderInTimeouts) drawInterface();
|
||||
|
|
@ -472,7 +461,8 @@ let getMapSlice = function(){
|
|||
i:startingIndex,
|
||||
poly:[],
|
||||
maxWaypoints: maxWaypoints,
|
||||
breakLoop: false
|
||||
breakLoop: false,
|
||||
dist: 0
|
||||
};
|
||||
|
||||
let drawChunk = function(data){
|
||||
|
|
@ -483,6 +473,7 @@ let getMapSlice = function(){
|
|||
let last;
|
||||
let toDraw;
|
||||
let named = [];
|
||||
let dir = [];
|
||||
for (let j = 0; j < SETTINGS.mapChunkSize; j++){
|
||||
data.i = data.i + (reverse?-1:1);
|
||||
let p = get(route, data.i);
|
||||
|
|
@ -497,7 +488,17 @@ let getMapSlice = function(){
|
|||
break;
|
||||
}
|
||||
toDraw = Bangle.project(p);
|
||||
if (p.name) named.push({i:data.poly.length,n:p.name});
|
||||
|
||||
if (SETTINGS.mapDirection){
|
||||
let lastWp = get(route, data.i - (reverse?-1:1));
|
||||
if (lastWp) data.dist+=distance(lastWp,p);
|
||||
if (!isMapOverview && data.dist > 20/mapScale){
|
||||
dir.push({i:data.poly.length,b:require("graphics_utils").degreesToRadians(bearing(lastWp,p)-(reverse?0:180))});
|
||||
data.dist=0;
|
||||
}
|
||||
}
|
||||
if (p.name)
|
||||
named.push({i:data.poly.length,n:p.name});
|
||||
data.poly.push(startingPoint.x-toDraw.x);
|
||||
data.poly.push((startingPoint.y-toDraw.y)*-1);
|
||||
}
|
||||
|
|
@ -518,7 +519,11 @@ let getMapSlice = function(){
|
|||
}
|
||||
graphics.drawString(c.n, data.poly[c.i] + 10, data.poly[c.i+1]);
|
||||
}
|
||||
|
||||
|
||||
for (let c of dir){
|
||||
graphics.drawImage(thinarrow, data.poly[c.i], data.poly[c.i+1], {rotate: c.b});
|
||||
}
|
||||
|
||||
if (finish)
|
||||
graphics.drawImage(finishIcon, data.poly[data.poly.length - 2] -5, data.poly[data.poly.length - 1] - 4);
|
||||
else if (last) {
|
||||
|
|
@ -1254,11 +1259,6 @@ let showMenu = function(){
|
|||
"Background" : showBackgroundMenu,
|
||||
"Calibration": showCalibrationMenu,
|
||||
"Reset" : ()=>{ E.showPrompt("Do Reset?").then((v)=>{ if (v) {WIDGETS.gpstrek.resetState(); removeMenu();} else {E.showMenu(mainmenu);}}).catch(()=>{E.showMenu(mainmenu);});},
|
||||
"Info rows" : {
|
||||
value : WIDGETS.gpstrek.getState().numberOfSlices,
|
||||
min:1,max:6,step:1,
|
||||
onchange : v => { WIDGETS.gpstrek.getState().numberOfSlices = v; }
|
||||
},
|
||||
};
|
||||
|
||||
E.showMenu(mainmenu);
|
||||
|
|
@ -1374,7 +1374,7 @@ const finishData = {
|
|||
};
|
||||
|
||||
let getSliceHeight = function(number){
|
||||
return Math.floor(Bangle.appRect.h/WIDGETS.gpstrek.getState().numberOfSlices);
|
||||
return Math.floor(Bangle.appRect.h/SETTINGS.numberOfSlices);
|
||||
};
|
||||
|
||||
let compassSlice = getCompassSlice();
|
||||
|
|
@ -1455,7 +1455,6 @@ let updateRouting = function() {
|
|||
lastSearch = Date.now();
|
||||
autosearchCounter++;
|
||||
}
|
||||
let counter = 0;
|
||||
while (hasNext(s.route) && distance(s.currentPos,get(s.route)) < SETTINGS.waypointChangeDist) {
|
||||
next(s.route);
|
||||
minimumDistance = Number.MAX_VALUE;
|
||||
|
|
@ -1479,7 +1478,7 @@ let updateSlices = function(){
|
|||
slices.push(healthSlice);
|
||||
slices.push(systemSlice);
|
||||
slices.push(system2Slice);
|
||||
maxSlicePages = Math.ceil(slices.length/s.numberOfSlices);
|
||||
maxSlicePages = Math.ceil(slices.length/SETTINGS.numberOfSlices);
|
||||
};
|
||||
|
||||
let page_slices = 0;
|
||||
|
|
@ -1515,9 +1514,9 @@ let drawSlices = function(){
|
|||
if (force){
|
||||
clear();
|
||||
}
|
||||
let firstSlice = page_slices*s.numberOfSlices;
|
||||
let firstSlice = page_slices*SETTINGS.numberOfSlices;
|
||||
let sliceHeight = getSliceHeight();
|
||||
let slicesToDraw = slices.slice(firstSlice,firstSlice + s.numberOfSlices);
|
||||
let slicesToDraw = slices.slice(firstSlice,firstSlice + SETTINGS.numberOfSlices);
|
||||
for (let slice of slicesToDraw) {
|
||||
g.reset();
|
||||
if (!slice.refresh || slice.refresh() || force)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"mapCompass": true,
|
||||
"mapScale":0.5,
|
||||
"mapRefresh":1000,
|
||||
"mapChunkSize": 15,
|
||||
"mapDirection": true,
|
||||
"overviewScroll": 30,
|
||||
"overviewScale": 0.02,
|
||||
"refresh":500,
|
||||
"refreshLocked":3000,
|
||||
"cacheMinFreeMem":2000,
|
||||
"cacheMaxEntries":0,
|
||||
"minCourseChange": 5,
|
||||
"minPosChange": 5,
|
||||
"waypointChangeDist": 50,
|
||||
"queueWaitingTime": 5,
|
||||
"autosearch": true,
|
||||
"maxDistForAutosearch": 300,
|
||||
"autosearchLimit": 3,
|
||||
"numberOfSlices": 3
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "gpstrek",
|
||||
"name": "GPS Trekking",
|
||||
"version": "0.10",
|
||||
"version": "0.11",
|
||||
"description": "Helper for tracking the status/progress during hiking. Do NOT depend on this for navigation!",
|
||||
"icon": "icon.png",
|
||||
"screenshots": [{"url":"screenInit.png"},{"url":"screenMenu.png"},{"url":"screenMap.png"},{"url":"screenLost.png"},{"url":"screenOverview.png"},{"url":"screenOverviewScroll.png"},{"url":"screenSlices.png"},{"url":"screenSlices2.png"},{"url":"screenSlices3.png"}],
|
||||
|
|
@ -12,8 +12,13 @@
|
|||
"interface" : "interface.html",
|
||||
"storage": [
|
||||
{"name":"gpstrek.app.js","url":"app.js"},
|
||||
{"name":"gpstrek.settings.js","url":"settings.js"},
|
||||
{"name":"gpstrek.default.json","url":"default.json"},
|
||||
{"name":"gpstrek.wid.js","url":"widget.js"},
|
||||
{"name":"gpstrek.img","url":"app-icon.js","evaluate":true}
|
||||
],
|
||||
"data": [{"name":"gpstrek.state.json"}]
|
||||
"data": [
|
||||
{"name":"gpstrek.state.json"},
|
||||
{"name":"gpstrek.json"}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,162 @@
|
|||
(function(back) {
|
||||
const FILE="gpstrek.json";
|
||||
let settings;
|
||||
|
||||
function writeSettings(key, value) {
|
||||
var s = require('Storage').readJSON(FILE, true) || {};
|
||||
s[key] = value;
|
||||
require('Storage').writeJSON(FILE, s);
|
||||
readSettings();
|
||||
}
|
||||
|
||||
function readSettings(){
|
||||
settings = Object.assign(
|
||||
require('Storage').readJSON("gpstrek.default.json", true) || {},
|
||||
require('Storage').readJSON(FILE, true) || {}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function showMapMenu(){
|
||||
var menu = {
|
||||
'': { 'title': 'Map', back: showMainMenu },
|
||||
'Show compass on map': {
|
||||
value: !!settings.mapCompass,
|
||||
onchange: v => {
|
||||
writeSettings("mapCompass",v);
|
||||
},
|
||||
},
|
||||
'Initial map scale': {
|
||||
value: settings.mapScale,
|
||||
min: 0.01,max: 2, step:0.01,
|
||||
onchange: v => {
|
||||
writeSettings("mapScale",v);
|
||||
},
|
||||
},
|
||||
'Rendered waypoints': {
|
||||
value: settings.mapChunkSize,
|
||||
min: 5,max: 60, step:5,
|
||||
onchange: v => {
|
||||
writeSettings("mapChunkSize",v);
|
||||
}
|
||||
},
|
||||
'Overview scroll': {
|
||||
value: settings.overviewScroll,
|
||||
min: 10,max: 100, step:10,
|
||||
format: v => v + "px",
|
||||
onchange: v => {
|
||||
writeSettings("overviewScroll",v);
|
||||
}
|
||||
},
|
||||
'Initial overview scale': {
|
||||
value: settings.overviewScale,
|
||||
min: 0.005,max: 0.1, step:0.005,
|
||||
onchange: v => {
|
||||
writeSettings("overviewScale",v);
|
||||
}
|
||||
},
|
||||
'Show direction': {
|
||||
value: !!settings.mapDirection,
|
||||
onchange: v => {
|
||||
writeSettings("mapDirection",v);
|
||||
}
|
||||
}
|
||||
};
|
||||
E.showMenu(menu);
|
||||
}
|
||||
|
||||
function showRoutingMenu(){
|
||||
var menu = {
|
||||
'': { 'title': 'Routing', back: showMainMenu },
|
||||
'Auto search closest waypoint': {
|
||||
value: !!settings.autosearch,
|
||||
onchange: v => {
|
||||
writeSettings("autosearch",v);
|
||||
},
|
||||
},
|
||||
'Auto search limit': {
|
||||
value: settings.autosearchLimit,
|
||||
onchange: v => {
|
||||
writeSettings("autosearchLimit",v);
|
||||
},
|
||||
},
|
||||
'Waypoint change distance': {
|
||||
value: settings.waypointChangeDist,
|
||||
format: v => v + "m",
|
||||
min: 5,max: 200, step:5,
|
||||
onchange: v => {
|
||||
writeSettings("waypointChangeDist",v);
|
||||
},
|
||||
}
|
||||
};
|
||||
E.showMenu(menu);
|
||||
}
|
||||
|
||||
function showRefreshMenu(){
|
||||
var menu = {
|
||||
'': { 'title': 'Refresh', back: showMainMenu },
|
||||
'Unlocked refresh': {
|
||||
value: settings.refresh,
|
||||
format: v => v + "ms",
|
||||
min: 250,max: 5000, step:250,
|
||||
onchange: v => {
|
||||
writeSettings("refresh",v);
|
||||
}
|
||||
},
|
||||
'Locked refresh': {
|
||||
value: settings.refreshLocked,
|
||||
min: 1000,max: 60000, step:1000,
|
||||
format: v => v + "ms",
|
||||
onchange: v => {
|
||||
writeSettings("refreshLocked",v);
|
||||
}
|
||||
},
|
||||
'Minimum refresh': {
|
||||
value: settings.mapRefresh,
|
||||
format: v => v + "ms",
|
||||
min: 250,max: 5000, step:250,
|
||||
onchange: v => {
|
||||
writeSettings("mapRefresh",v);
|
||||
}
|
||||
},
|
||||
'Minimum course change': {
|
||||
value: settings.minCourseChange,
|
||||
min: 0,max: 180, step:1,
|
||||
format: v => v + "°",
|
||||
onchange: v => {
|
||||
writeSettings("minCourseChange",v);
|
||||
}
|
||||
},
|
||||
'Minimum position change': {
|
||||
value: settings.minPosChange,
|
||||
min: 0,max: 50, step:1,
|
||||
format: v => v + "px",
|
||||
onchange: v => {
|
||||
writeSettings("minPosChange",v);
|
||||
}
|
||||
}
|
||||
};
|
||||
E.showMenu(menu);
|
||||
}
|
||||
|
||||
|
||||
function showMainMenu(){
|
||||
var mainmenu = {
|
||||
'': { 'title': 'GPS Trekking', back: back },
|
||||
'Map': showMapMenu,
|
||||
'Routing': showRoutingMenu,
|
||||
'Refresh': showRefreshMenu,
|
||||
"Info rows" : {
|
||||
value : settings.numberOfSlices,
|
||||
min:1,max:6,step:1,
|
||||
onchange : v => {
|
||||
writeSettings("numberOfSlices",v);
|
||||
}
|
||||
},
|
||||
};
|
||||
E.showMenu(mainmenu);
|
||||
}
|
||||
|
||||
readSettings();
|
||||
showMainMenu();
|
||||
})
|
||||
|
|
@ -45,7 +45,15 @@ function onPulse(e){
|
|||
}
|
||||
|
||||
function onGPS(fix) {
|
||||
if(fix.fix) state.currentPos = fix;
|
||||
if(fix.fix) {
|
||||
state.currentPos = fix;
|
||||
if (Bangle.isCompassOn()){
|
||||
Bangle.setCompassPower(0, "gpstrek");
|
||||
state.compassSamples = new Array(SAMPLES).fill(0)
|
||||
}
|
||||
} else {
|
||||
Bangle.setCompassPower(1, "gpstrek");
|
||||
}
|
||||
}
|
||||
|
||||
let radians = function(a) {
|
||||
|
|
|
|||
|
|
@ -22,3 +22,10 @@
|
|||
0.21: Update boot.min.js.
|
||||
0.22: Fix timeout for heartrate sensor on 3 minute setting (#2435)
|
||||
0.23: Fix HRM logic
|
||||
0.24: Correct daily health summary for movement (some logic errors resulted in garbage data being written)
|
||||
0.25: lib.read* methods now return correctly scaled movement
|
||||
movement graph in app is now an average, not sum
|
||||
fix 11pm slot for daily HRM
|
||||
0.26: Implement API for activity fetching
|
||||
0.27: Fix typo in daily summary graph code causing graph not to load
|
||||
Fix daily summaries for 31st of the month
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ function stepsPerHour() {
|
|||
function stepsPerDay() {
|
||||
E.showMessage(/*LANG*/"Loading...");
|
||||
current_selection = "stepsPerDay";
|
||||
var data = new Uint16Array(31);
|
||||
var data = new Uint16Array(32);
|
||||
require("health").readDailySummaries(new Date(), h=>data[h.day]+=h.steps);
|
||||
setButton(menuStepCount);
|
||||
barChart(/*LANG*/"DAY", data);
|
||||
|
|
@ -59,7 +59,7 @@ function hrmPerHour() {
|
|||
E.showMessage(/*LANG*/"Loading...");
|
||||
current_selection = "hrmPerHour";
|
||||
var data = new Uint16Array(24);
|
||||
var cnt = new Uint8Array(23);
|
||||
var cnt = new Uint8Array(24);
|
||||
require("health").readDay(new Date(), h=>{
|
||||
data[h.hr]+=h.bpm;
|
||||
if (h.bpm) cnt[h.hr]++;
|
||||
|
|
@ -72,8 +72,8 @@ function hrmPerHour() {
|
|||
function hrmPerDay() {
|
||||
E.showMessage(/*LANG*/"Loading...");
|
||||
current_selection = "hrmPerDay";
|
||||
var data = new Uint16Array(31);
|
||||
var cnt = new Uint8Array(31);
|
||||
var data = new Uint16Array(32);
|
||||
var cnt = new Uint8Array(32);
|
||||
require("health").readDailySummaries(new Date(), h=>{
|
||||
data[h.day]+=h.bpm;
|
||||
if (h.bpm) cnt[h.day]++;
|
||||
|
|
@ -87,7 +87,12 @@ function movementPerHour() {
|
|||
E.showMessage(/*LANG*/"Loading...");
|
||||
current_selection = "movementPerHour";
|
||||
var data = new Uint16Array(24);
|
||||
require("health").readDay(new Date(), h=>data[h.hr]+=h.movement);
|
||||
var cnt = new Uint8Array(24);
|
||||
require("health").readDay(new Date(), h=>{
|
||||
data[h.hr]+=h.movement;
|
||||
cnt[h.hr]++;
|
||||
});
|
||||
data.forEach((d,i)=>data[i] = d/cnt[i]);
|
||||
setButton(menuMovement);
|
||||
barChart(/*LANG*/"HOUR", data);
|
||||
}
|
||||
|
|
@ -95,8 +100,13 @@ function movementPerHour() {
|
|||
function movementPerDay() {
|
||||
E.showMessage(/*LANG*/"Loading...");
|
||||
current_selection = "movementPerDay";
|
||||
var data = new Uint16Array(31);
|
||||
require("health").readDailySummaries(new Date(), h=>data[h.day]+=h.movement);
|
||||
var data = new Uint16Array(32);
|
||||
var cnt = new Uint8Array(32);
|
||||
require("health").readDailySummaries(new Date(), h=>{
|
||||
data[h.day]+=h.movement;
|
||||
cnt[h.day]++;
|
||||
});
|
||||
data.forEach((d,i)=>data[i] = d/cnt[i]);
|
||||
setButton(menuMovement);
|
||||
barChart(/*LANG*/"DAY", data);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ Bangle.on("health", health => {
|
|||
return String.fromCharCode(
|
||||
health.steps>>8,health.steps&255, // 16 bit steps
|
||||
health.bpm, // 8 bit bpm
|
||||
Math.min(health.movement / 8, 255)); // movement
|
||||
Math.min(health.movement, 255)); // movement
|
||||
}
|
||||
|
||||
var rec = getRecordIdx(d);
|
||||
|
|
@ -68,6 +68,12 @@ Bangle.on("health", health => {
|
|||
require("Storage").write(fn, "HEALTH1\0", 0, DB_FILE_LEN); // header
|
||||
}
|
||||
var recordPos = DB_HEADER_LEN+(rec*DB_RECORD_LEN);
|
||||
|
||||
// scale down reported movement value in order to fit it within a
|
||||
// uint8 DB field
|
||||
health = Object.assign({}, health);
|
||||
health.movement /= 8;
|
||||
|
||||
require("Storage").write(fn, getRecordData(health), recordPos, DB_FILE_LEN);
|
||||
if (rec%DB_RECORDS_PER_DAY != DB_RECORDS_PER_DAY-2) return;
|
||||
// we're at the end of the day. Read in all of the data for the day and sum it up
|
||||
|
|
@ -82,10 +88,10 @@ Bangle.on("health", health => {
|
|||
var dt = f.substr(recordPos, DB_RECORD_LEN);
|
||||
if (dt!="\xFF\xFF\xFF\xFF") {
|
||||
health.steps += (dt.charCodeAt(0)<<8)+dt.charCodeAt(1);
|
||||
health.movement += dt.charCodeAt(2);
|
||||
health.movCnt++;
|
||||
var bpm = dt.charCodeAt(2);
|
||||
health.bpm += bpm;
|
||||
health.movement += dt.charCodeAt(3);
|
||||
health.movCnt++;
|
||||
if (bpm) health.bpmCnt++;
|
||||
}
|
||||
recordPos -= DB_RECORD_LEN;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
function l(){var a=require("Storage").readJSON("health.json",1)||{},d=Bangle.getHealthStatus("day").steps;a.stepGoalNotification&&0<a.stepGoal&&d>=a.stepGoal&&(d=(new Date(Date.now())).toISOString().split("T")[0],!a.stepGoalNotificationDate||a.stepGoalNotificationDate<d)&&(Bangle.buzz(200,.5),require("notify").show({title:a.stepGoal+" steps",body:"You reached your step goal!",icon:atob("DAyBABmD6BaBMAsA8BCBCBCBCA8AAA==")}),a.stepGoalNotificationDate=d,require("Storage").writeJSON("health.json",
|
||||
a))}(function(){var a=0|(require("Storage").readJSON("health.json",1)||{}).hrm;if(1==a||2==a){var d=function(){Bangle.setHRMPower(1,"health");setTimeout(function(){return Bangle.setHRMPower(0,"health")},6E4*a);if(1==a){var b=function(){Bangle.setHRMPower(1,"health");setTimeout(function(){Bangle.setHRMPower(0,"health")},6E4)};setTimeout(b,2E5);setTimeout(b,4E5)}};Bangle.on("health",d);Bangle.on("HRM",function(b){90<b.confidence&&1>Math.abs(Bangle.getHealthStatus().bpm-b.bpm)&&Bangle.setHRMPower(0,
|
||||
"health")});90<Bangle.getHealthStatus().bpmConfidence||d()}else Bangle.setHRMPower(!!a,"health")})();Bangle.on("health",function(a){function d(c){return String.fromCharCode(c.steps>>8,c.steps&255,c.bpm,Math.min(c.movement/8,255))}var b=new Date(Date.now()-59E4);a&&0<a.steps&&l();var f=function(c){return 145*(c.getDate()-1)+6*c.getHours()+(0|6*c.getMinutes()/60)}(b);b=function(c){return"health-"+c.getFullYear()+"-"+(c.getMonth()+1)+".raw"}(b);var g=require("Storage").read(b);if(g){var e=g.substr(8+
|
||||
4*f,4);if("\u00ff\u00ff\u00ff\u00ff"!=e){print("HEALTH ERR: Already written!");return}}else require("Storage").write(b,"HEALTH1\x00",0,17988);var h=8+4*f;require("Storage").write(b,d(a),h,17988);if(143==f%145)if(f=h+4,"\u00ff\u00ff\u00ff\u00ff"!=g.substr(f,4))print("HEALTH ERR: Daily summary already written!");else{a={steps:0,bpm:0,movement:0,movCnt:0,bpmCnt:0};for(var k=0;144>k;k++)e=g.substr(h,4),"\u00ff\u00ff\u00ff\u00ff"!=e&&(a.steps+=(e.charCodeAt(0)<<8)+e.charCodeAt(1),a.movement+=e.charCodeAt(2),
|
||||
a.movCnt++,e=e.charCodeAt(2),a.bpm+=e,e&&a.bpmCnt++),h-=4;a.bpmCnt&&(a.bpm/=a.bpmCnt);a.movCnt&&(a.movement/=a.movCnt);require("Storage").write(b,d(a),f,17988)}})
|
||||
function m(){var a=require("Storage").readJSON("health.json",1)||{},d=Bangle.getHealthStatus("day").steps;a.stepGoalNotification&&0<a.stepGoal&&d>=a.stepGoal&&(d=(new Date(Date.now())).toISOString().split("T")[0],!a.stepGoalNotificationDate||a.stepGoalNotificationDate<d)&&(Bangle.buzz(200,.5),require("notify").show({title:a.stepGoal+" steps",body:"You reached your step goal!",icon:atob("DAyBABmD6BaBMAsA8BCBCBCBCA8AAA==")}),a.stepGoalNotificationDate=d,require("Storage").writeJSON("health.json",
|
||||
a))}(function(){var a=0|(require("Storage").readJSON("health.json",1)||{}).hrm;if(1==a||2==a){function d(){Bangle.setHRMPower(1,"health");setTimeout(()=>Bangle.setHRMPower(0,"health"),6E4*a);if(1==a){function b(){Bangle.setHRMPower(1,"health");setTimeout(()=>{Bangle.setHRMPower(0,"health")},6E4)}setTimeout(b,2E5);setTimeout(b,4E5)}}Bangle.on("health",d);Bangle.on("HRM",b=>{90<b.confidence&&1>Math.abs(Bangle.getHealthStatus().bpm-b.bpm)&&Bangle.setHRMPower(0,"health")});90<Bangle.getHealthStatus().bpmConfidence||
|
||||
d()}else Bangle.setHRMPower(!!a,"health")})();Bangle.on("health",a=>{function d(c){return String.fromCharCode(c.steps>>8,c.steps&255,c.bpm,Math.min(c.movement,255))}var b=new Date(Date.now()-59E4);a&&0<a.steps&&m();var f=function(c){return 145*(c.getDate()-1)+6*c.getHours()+(0|6*c.getMinutes()/60)}(b);b=function(c){return"health-"+c.getFullYear()+"-"+(c.getMonth()+1)+".raw"}(b);var g=require("Storage").read(b);if(g){var e=g.substr(8+4*f,4);if("\xff\xff\xff\xff"!=e){print("HEALTH ERR: Already written!");
|
||||
return}}else require("Storage").write(b,"HEALTH1\x00",0,17988);var h=8+4*f;a=Object.assign({},a);a.movement/=8;require("Storage").write(b,d(a),h,17988);if(143==f%145)if(f=h+4,"\xff\xff\xff\xff"!=g.substr(f,4))print("HEALTH ERR: Daily summary already written!");else{a={steps:0,bpm:0,movement:0,movCnt:0,bpmCnt:0};for(var k=0;144>k;k++){e=g.substr(h,4);if("\xff\xff\xff\xff"!=e){a.steps+=(e.charCodeAt(0)<<8)+e.charCodeAt(1);var l=e.charCodeAt(2);a.bpm+=l;a.movement+=e.charCodeAt(3);a.movCnt++;
|
||||
l&&a.bpmCnt++}h-=4}a.bpmCnt&&(a.bpm/=a.bpmCnt);a.movCnt&&(a.movement/=a.movCnt);require("Storage").write(b,d(a),f,17988)}})
|
||||
|
|
@ -29,7 +29,7 @@ exports.readAllRecords = function(d, cb) {
|
|||
day:day+1, hr : hr, min:m*10,
|
||||
steps : (h.charCodeAt(0)<<8) | h.charCodeAt(1),
|
||||
bpm : h.charCodeAt(2),
|
||||
movement : h.charCodeAt(3)
|
||||
movement : h.charCodeAt(3)*8
|
||||
});
|
||||
}
|
||||
idx += DB_RECORD_LEN;
|
||||
|
|
@ -37,7 +37,36 @@ exports.readAllRecords = function(d, cb) {
|
|||
}
|
||||
idx += DB_RECORD_LEN; // +1 because we have an extra record with totals for the end of the day
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Read the entire database. There is no guarantee that the months are read in order.
|
||||
exports.readFullDatabase = function(cb) {
|
||||
require("Storage").list(/health-[0-9]+-[0-9]+.raw/).forEach(val => {
|
||||
console.log(val);
|
||||
var parts = val.split('-');
|
||||
var y = parseInt(parts[1]);
|
||||
var mo = parseInt(parts[2].replace('.raw', ''));
|
||||
|
||||
exports.readAllRecords(new Date(y, mo, 1), (r) => {
|
||||
r.date = new Date(y, mo, r.day, r.hr, r.min);
|
||||
cb(r);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Read all records per day, until the current time.
|
||||
// There may be some records for the day of the timestamp previous to the timestamp
|
||||
exports.readAllRecordsSince = function(d, cb) {
|
||||
var currentDate = new Date().getTime();
|
||||
var di = d;
|
||||
while (di.getTime() <= currentDate) {
|
||||
exports.readDay(di, (r) => {
|
||||
r.date = new Date(di.getFullYear(), di.getMonth(), di.getDate(), r.hr, r.min);
|
||||
cb(r);
|
||||
});
|
||||
di.setDate(di.getDate() + 1);
|
||||
}
|
||||
};
|
||||
|
||||
// Read daily summaries from the given month
|
||||
exports.readDailySummaries = function(d, cb) {
|
||||
|
|
@ -53,7 +82,7 @@ exports.readDailySummaries = function(d, cb) {
|
|||
day:day+1,
|
||||
steps : (h.charCodeAt(0)<<8) | h.charCodeAt(1),
|
||||
bpm : h.charCodeAt(2),
|
||||
movement : h.charCodeAt(3)
|
||||
movement : h.charCodeAt(3)*8
|
||||
});
|
||||
}
|
||||
idx += DB_RECORDS_PER_DAY*DB_RECORD_LEN;
|
||||
|
|
@ -75,7 +104,7 @@ exports.readDay = function(d, cb) {
|
|||
hr : hr, min:m*10,
|
||||
steps : (h.charCodeAt(0)<<8) | h.charCodeAt(1),
|
||||
bpm : h.charCodeAt(2),
|
||||
movement : h.charCodeAt(3)
|
||||
movement : h.charCodeAt(3)*8
|
||||
});
|
||||
}
|
||||
idx += DB_RECORD_LEN;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
function h(a){return"health-"+a.getFullYear()+"-"+(a.getMonth()+1)+".raw"}function k(a){return 145*(a.getDate()-1)+6*a.getHours()+(0|6*a.getMinutes()/60)}exports.readAllRecords=function(a,f){a=h(a);a=require("Storage").read(a);if(void 0!==a)for(var c=8,d=0;31>d;d++){for(var b=0;24>b;b++)for(var e=0;6>e;e++){var g=a.substr(c,4);"\u00ff\u00ff\u00ff\u00ff"!=g&&f({day:d+1,hr:b,min:10*e,steps:g.charCodeAt(0)<<8|g.charCodeAt(1),bpm:g.charCodeAt(2),movement:g.charCodeAt(3)});c+=
|
||||
4}c+=4}};exports.readDailySummaries=function(a,f){k(a);a=h(a);a=require("Storage").read(a);if(void 0!==a)for(var c=584,d=0;31>d;d++){var b=a.substr(c,4);"\u00ff\u00ff\u00ff\u00ff"!=b&&f({day:d+1,steps:b.charCodeAt(0)<<8|b.charCodeAt(1),bpm:b.charCodeAt(2),movement:b.charCodeAt(3)});c+=580}};exports.readDay=function(a,f){k(a);var c=h(a);c=require("Storage").read(c);if(void 0!==c){a=8+580*(a.getDate()-1);for(var d=0;24>d;d++)for(var b=0;6>b;b++){var e=c.substr(a,4);"\u00ff\u00ff\u00ff\u00ff"!=e&&f({hr:d,
|
||||
min:10*b,steps:e.charCodeAt(0)<<8|e.charCodeAt(1),bpm:e.charCodeAt(2),movement:e.charCodeAt(3)});a+=4}}}
|
||||
function h(a){return"health-"+a.getFullYear()+"-"+(a.getMonth()+1)+".raw"}function k(a){return 145*(a.getDate()-1)+6*a.getHours()+(0|6*a.getMinutes()/60)}exports.readAllRecords=function(a,e){a=h(a);a=require("Storage").read(a);if(void 0!==a)for(var d=8,b=0;31>b;b++){for(var c=0;24>c;c++)for(var f=0;6>f;f++){var g=a.substr(d,4);"\xff\xff\xff\xff"!=g&&e({day:b+1,hr:c,min:10*f,steps:g.charCodeAt(0)<<8|g.charCodeAt(1),bpm:g.charCodeAt(2),movement:8*g.charCodeAt(3)});d+=
|
||||
4}d+=4}};exports.readFullDatabase=function(a){require("Storage").list(/health-[0-9]+-[0-9]+.raw/).forEach(e=>{console.log(e);e=e.split("-");var d=parseInt(e[1]),b=parseInt(e[2].replace(".raw",""));exports.readAllRecords(new Date(d,b,1),c=>{c.date=new Date(d,b,c.day,c.hr,c.min);a(c)})})};exports.readAllRecordsSince=function(a,e){for(var d=(new Date).getTime();a.getTime()<=d;)exports.readDay(a,b=>{b.date=new Date(a.getFullYear(),a.getMonth(),a.getDate(),b.hr,b.min);e(b)}),a.setDate(a.getDate()+1)};
|
||||
exports.readDailySummaries=function(a,e){k(a);a=h(a);a=require("Storage").read(a);if(void 0!==a)for(var d=584,b=0;31>b;b++){var c=a.substr(d,4);"\xff\xff\xff\xff"!=c&&e({day:b+1,steps:c.charCodeAt(0)<<8|c.charCodeAt(1),bpm:c.charCodeAt(2),movement:8*c.charCodeAt(3)});d+=580}};exports.readDay=function(a,e){k(a);var d=h(a);d=require("Storage").read(d);if(void 0!==d){a=8+580*(a.getDate()-1);for(var b=0;24>b;b++)for(var c=0;6>c;c++){var f=d.substr(a,4);"\xff\xff\xff\xff"!=f&&e({hr:b,min:10*
|
||||
c,steps:f.charCodeAt(0)<<8|f.charCodeAt(1),bpm:f.charCodeAt(2),movement:8*f.charCodeAt(3)});a+=4}}}
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
"id": "health",
|
||||
"name": "Health Tracking",
|
||||
"shortName": "Health",
|
||||
"version": "0.23",
|
||||
"version": "0.27",
|
||||
"description": "Logs health data and provides an app to view it",
|
||||
"icon": "app.png",
|
||||
"tags": "tool,system,health",
|
||||
|
|
|
|||
|
|
@ -7,3 +7,4 @@
|
|||
0.07: Fixed position after unlocking
|
||||
0.08: Handling exceptions
|
||||
0.09: Add option for showing battery high mark
|
||||
0.10: Fix background color
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"name": "A Battery Widget (with percentage) - Hanks Mod",
|
||||
"shortName":"H Battery Widget",
|
||||
"icon": "widget.png",
|
||||
"version":"0.09",
|
||||
"version":"0.10",
|
||||
"type": "widget",
|
||||
"supports": ["BANGLEJS", "BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@
|
|||
var y = this.y;
|
||||
if ((typeof x === 'undefined') || (typeof y === 'undefined')) {
|
||||
} else {
|
||||
g.setBgColor(COLORS.white);
|
||||
g.clearRect(old_x, old_y, old_x + width, old_y + height);
|
||||
|
||||
const l = E.getBattery(); // debug: Math.floor(Math.random() * 101);
|
||||
|
|
|
|||
|
|
@ -21,4 +21,6 @@
|
|||
0.15: Ensure that we hide widgets if in fullscreen mode
|
||||
(So that widgets are still hidden if launcher is fast-loaded)
|
||||
0.16: Use firmware provided E.showScroller method
|
||||
0.17: fix fullscreen with oneClickExit
|
||||
0.17: fix fullscreen with oneClickExit
|
||||
0.18: Better performance
|
||||
0.19: Remove 'jit' keyword as 'for(..of..)' is not supported (fix #2937)
|
||||
|
|
@ -9,7 +9,6 @@
|
|||
timeOut:"Off"
|
||||
}, s.readJSON("iconlaunch.json", true) || {});
|
||||
|
||||
|
||||
if (!settings.fullscreen) {
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
|
|
@ -19,9 +18,9 @@
|
|||
let launchCache = s.readJSON("iconlaunch.cache.json", true)||{};
|
||||
let launchHash = s.hash(/\.info/);
|
||||
if (launchCache.hash!=launchHash) {
|
||||
launchCache = {
|
||||
hash : launchHash,
|
||||
apps : s.list(/\.info$/)
|
||||
launchCache = {
|
||||
hash : launchHash,
|
||||
apps : s.list(/\.info$/)
|
||||
.map(app=>{let a=s.readJSON(app,1);return a&&{name:a.name,type:a.type,icon:a.icon,sortorder:a.sortorder,src:a.src};})
|
||||
.filter(app=>app && (app.type=="app" || (app.type=="clock" && settings.showClocks) || !app.type))
|
||||
.sort((a,b)=>{
|
||||
|
|
@ -34,42 +33,65 @@
|
|||
s.writeJSON("iconlaunch.cache.json", launchCache);
|
||||
}
|
||||
|
||||
// cache items
|
||||
const ICON_MISSING = s.read("iconlaunch.na.img");
|
||||
let count = 0;
|
||||
|
||||
let selectedItem = -1;
|
||||
const R = Bangle.appRect;
|
||||
const iconSize = 48;
|
||||
const appsN = Math.floor(R.w / iconSize);
|
||||
const whitespace = (R.w - appsN * iconSize) / (appsN + 1);
|
||||
const whitespace = Math.floor((R.w - appsN * iconSize) / (appsN + 1));
|
||||
const iconYoffset = Math.floor(whitespace/4)-1;
|
||||
const itemSize = iconSize + whitespace;
|
||||
|
||||
launchCache.items = {};
|
||||
for (let c of launchCache.apps){
|
||||
let i = Math.floor(count/appsN);
|
||||
if (!launchCache.items[i])
|
||||
launchCache.items[i] = {};
|
||||
launchCache.items[i][(count%3)] = c;
|
||||
count++;
|
||||
}
|
||||
|
||||
let texted;
|
||||
let drawItem = function(itemI, r) {
|
||||
g.clearRect(r.x, r.y, r.x + r.w - 1, r.y + r.h - 1);
|
||||
let x = 0;
|
||||
for (let i = itemI * appsN; i < appsN * (itemI + 1); i++) {
|
||||
if (!launchCache.apps[i]) break;
|
||||
x += whitespace;
|
||||
if (!launchCache.apps[i].icon) {
|
||||
g.setFontAlign(0, 0, 0).setFont("12x20:2").drawString("?", x + r.x + iconSize / 2, r.y + iconSize / 2);
|
||||
} else {
|
||||
if (!launchCache.apps[i].icondata) launchCache.apps[i].icondata = s.read(launchCache.apps[i].icon);
|
||||
g.drawImage(launchCache.apps[i].icondata, x + r.x, r.y);
|
||||
}
|
||||
if (selectedItem == i) {
|
||||
g.drawRect(
|
||||
x + r.x - 1,
|
||||
r.y - 1,
|
||||
x + r.x + iconSize + 1,
|
||||
r.y + iconSize + 1
|
||||
);
|
||||
}
|
||||
x += iconSize;
|
||||
let x = whitespace;
|
||||
let i = itemI * appsN - 1;
|
||||
let selectedApp;
|
||||
let c;
|
||||
let selectedRect;
|
||||
let item = launchCache.items[itemI];
|
||||
if (texted == itemI){
|
||||
g.clearRect(r.x, r.y, r.x + r.w - 1, r.y + r.h - 1);
|
||||
texted = undefined;
|
||||
}
|
||||
for (c of item) {
|
||||
i++;
|
||||
let id = c.icondata || (c.iconData = (c.icon ? s.read(c.icon) : ICON_MISSING));
|
||||
g.drawImage(id,x + r.x - 1, r.y + iconYoffset - 1, x + r.x + iconSize, r.y + iconYoffset + iconSize);
|
||||
if (selectedItem == i) {
|
||||
selectedApp = c;
|
||||
selectedRect = [
|
||||
x + r.x - 1,
|
||||
r.y + iconYoffset - 1,
|
||||
x + r.x + iconSize,
|
||||
r.y + iconYoffset + iconSize
|
||||
];
|
||||
}
|
||||
x += iconSize + whitespace;
|
||||
}
|
||||
if (selectedRect) {
|
||||
g.drawRect.apply(null, selectedRect);
|
||||
drawText(itemI, r.y, selectedApp);
|
||||
texted=itemI;
|
||||
}
|
||||
drawText(itemI, r.y);
|
||||
};
|
||||
|
||||
let drawText = function(i, appY) {
|
||||
const selectedApp = launchCache.apps[selectedItem];
|
||||
let drawText = function(i, appY, selectedApp) {
|
||||
"jit";
|
||||
const idy = (selectedItem - (selectedItem % 3)) / 3;
|
||||
if (!selectedApp || i != idy) return;
|
||||
if (i != idy) return;
|
||||
appY = appY + itemSize/2;
|
||||
g.setFontAlign(0, 0, 0);
|
||||
g.setFont("12x20");
|
||||
|
|
@ -122,21 +144,21 @@
|
|||
},
|
||||
btn:Bangle.showClock
|
||||
};
|
||||
|
||||
|
||||
//work both the fullscreen and the oneClickExit
|
||||
if( settings.fullscreen && settings.oneClickExit)
|
||||
{
|
||||
idWatch=setWatch(function(e) {
|
||||
idWatch=setWatch(function(e) {
|
||||
Bangle.showClock();
|
||||
}, BTN, {repeat:false, edge:'rising' });
|
||||
|
||||
|
||||
}
|
||||
else if( settings.oneClickExit )
|
||||
else if( settings.oneClickExit )
|
||||
{
|
||||
options.back=Bangle.showClock;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
let scroller = E.showScroller(options);
|
||||
|
|
@ -151,7 +173,7 @@
|
|||
};
|
||||
|
||||
let swipeHandler = (h,_) => { if(settings.swipeExit && h==1) { Bangle.showClock(); } };
|
||||
|
||||
|
||||
Bangle.on("swipe", swipeHandler)
|
||||
Bangle.on("drag", updateTimeout);
|
||||
Bangle.on("touch", updateTimeout);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"id": "iconlaunch",
|
||||
"name": "Icon Launcher",
|
||||
"shortName" : "Icon launcher",
|
||||
"version": "0.17",
|
||||
"version": "0.19",
|
||||
"icon": "app.png",
|
||||
"description": "A launcher inspired by smartphones, with an icon-only scrollable menu.",
|
||||
"tags": "tool,system,launcher",
|
||||
|
|
@ -10,7 +10,8 @@
|
|||
"supports": ["BANGLEJS2"],
|
||||
"storage": [
|
||||
{ "name": "iconlaunch.app.js", "url": "app.js" },
|
||||
{ "name": "iconlaunch.settings.js", "url": "settings.js" }
|
||||
{ "name": "iconlaunch.settings.js", "url": "settings.js" },
|
||||
{ "name": "iconlaunch.na.img", "url": "na.img" }
|
||||
],
|
||||
"data": [{"name":"iconlaunch.json"},{"name":"iconlaunch.cache.json"}],
|
||||
"screenshots": [{ "url": "screenshot1.png" }, { "url": "screenshot2.png" }],
|
||||
|
|
|
|||