diff --git a/apps/_example_clkinfo/ChangeLog b/apps/_example_clkinfo/ChangeLog
index 4c21f3ace..78ba28f3b 100644
--- a/apps/_example_clkinfo/ChangeLog
+++ b/apps/_example_clkinfo/ChangeLog
@@ -1 +1 @@
-0.01: New Widget!
+0.01: New Clock Info!
diff --git a/apps/_example_clock/ChangeLog b/apps/_example_clock/ChangeLog
new file mode 100644
index 000000000..09953593e
--- /dev/null
+++ b/apps/_example_clock/ChangeLog
@@ -0,0 +1 @@
+0.01: New Clock!
diff --git a/apps/_example_clock/README.md b/apps/_example_clock/README.md
new file mode 100644
index 000000000..5d750a965
--- /dev/null
+++ b/apps/_example_clock/README.md
@@ -0,0 +1,25 @@
+# Clock Name
+
+More info on making Clock Faces: https://www.espruino.com/Bangle.js+Clock
+
+Describe the Clock...
+
+## Usage
+
+Describe how to use it
+
+## Features
+
+Name the function
+
+## Controls
+
+Name the buttons and what they are used for
+
+## Requests
+
+Name who should be contacted for support/update requests
+
+## Creator
+
+Your name
diff --git a/apps/_example_clock/app-icon.js b/apps/_example_clock/app-icon.js
new file mode 100644
index 000000000..49232b838
--- /dev/null
+++ b/apps/_example_clock/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwwJC/AH4A/AH4AgA=="))
diff --git a/apps/_example_clock/clock.js b/apps/_example_clock/clock.js
new file mode 100644
index 000000000..7e97cf758
--- /dev/null
+++ b/apps/_example_clock/clock.js
@@ -0,0 +1,44 @@
+// timeout used to update every minute
+var drawTimeout;
+
+// schedule a draw for the next minute
+function queueDraw() {
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = setTimeout(function() {
+ drawTimeout = undefined;
+ draw();
+ }, 60000 - (Date.now() % 60000));
+}
+
+function draw() {
+ // queue next draw in one minute
+ queueDraw();
+ // Work out where to draw...
+ var x = g.getWidth()/2;
+ var y = g.getHeight()/2;
+ g.reset();
+ // work out locale-friendly date/time
+ var date = new Date();
+ var timeStr = require("locale").time(date,1);
+ var dateStr = require("locale").date(date);
+ // draw time
+ g.setFontAlign(0,0).setFont("Vector",48);
+ g.clearRect(0,y-15,g.getWidth(),y+25); // clear the background
+ g.drawString(timeStr,x,y);
+ // draw date
+ y += 35;
+ g.setFontAlign(0,0).setFont("6x8");
+ g.clearRect(0,y-4,g.getWidth(),y+4); // clear the background
+ g.drawString(dateStr,x,y);
+}
+
+// Clear the screen once, at startup
+g.clear();
+// draw immediately at first, queue update
+draw();
+
+// Show launcher when middle button pressed
+Bangle.setUI("clock");
+// Load widgets
+Bangle.loadWidgets();
+Bangle.drawWidgets();
\ No newline at end of file
diff --git a/apps/_example_clock/icon.png b/apps/_example_clock/icon.png
new file mode 100644
index 000000000..582cb2e08
Binary files /dev/null and b/apps/_example_clock/icon.png differ
diff --git a/apps/_example_clock/metadata.json b/apps/_example_clock/metadata.json
new file mode 100644
index 000000000..c6e1256d3
--- /dev/null
+++ b/apps/_example_clock/metadata.json
@@ -0,0 +1,15 @@
+{ "id": "7chname",
+ "name": "My clock human readable name",
+ "shortName":"Short Name",
+ "version":"0.01",
+ "description": "A detailed description of my clock",
+ "icon": "icon.png",
+ "type": "clock",
+ "tags": "clock",
+ "supports" : ["BANGLEJS2"],
+ "readme": "README.md",
+ "storage": [
+ {"name":"7chname.app.js","url":"app.js"},
+ {"name":"7chname.img","url":"app-icon.js","evaluate":true}
+ ]
+}
diff --git a/apps/android/ChangeLog b/apps/android/ChangeLog
index d531e43a9..108242825 100644
--- a/apps/android/ChangeLog
+++ b/apps/android/ChangeLog
@@ -32,3 +32,4 @@
Allow alarm enable/disable
0.31: Implement API for activity fetching
0.32: Added support for loyalty cards from gadgetbridge
+0.33: Fix alarms created in Gadgetbridge not repeating
diff --git a/apps/android/boot.js b/apps/android/boot.js
index 846fc40a8..63f9b2883 100644
--- a/apps/android/boot.js
+++ b/apps/android/boot.js
@@ -81,7 +81,12 @@
for (var j = 0; j < event.d.length; j++) {
// prevents all alarms from going off at once??
var dow = event.d[j].rep;
- if (!dow) dow = 127; //if no DOW selected, set alarm to all DOW
+ var rp = false;
+ if (!dow) {
+ dow = 127; //if no DOW selected, set alarm to all DOW
+ } else {
+ rp = true;
+ }
var last = (event.d[j].h * 3600000 + event.d[j].m * 60000 < currentTime) ? (new Date()).getDate() : 0;
var a = require("sched").newDefaultAlarm();
a.id = "gb"+j;
@@ -89,6 +94,7 @@
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.rp = rp;
a.last = last;
alarms.push(a);
}
diff --git a/apps/android/metadata.json b/apps/android/metadata.json
index 68bd946c5..5babc520b 100644
--- a/apps/android/metadata.json
+++ b/apps/android/metadata.json
@@ -2,7 +2,7 @@
"id": "android",
"name": "Android Integration",
"shortName": "Android",
- "version": "0.32",
+ "version": "0.33",
"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",
diff --git a/apps/aviatorclk/ChangeLog b/apps/aviatorclk/ChangeLog
index 971e5b97e..929ee8387 100644
--- a/apps/aviatorclk/ChangeLog
+++ b/apps/aviatorclk/ChangeLog
@@ -1 +1,2 @@
1.00: initial release
+1.01: added tap event to scroll METAR and toggle seconds display
diff --git a/apps/aviatorclk/README.md b/apps/aviatorclk/README.md
index fe7376b5d..ac27b80d3 100644
--- a/apps/aviatorclk/README.md
+++ b/apps/aviatorclk/README.md
@@ -18,15 +18,20 @@ module after installing this app.
- Latest METAR for the nearest airport (scrollable)
Tap the screen in the top or bottom half to scroll the METAR text (in case not
-the whole report fits on the screen).
+the whole report fits on the screen). You can also tap the watch from the top
+or bottom to scroll, which works even with the screen locked.
The colour of the METAR text will change to orange if the report is more than
1h old, and red if it's older than 1.5h.
+To toggle the seconds display, double tap the watch from either the left or
+right. This only changes the display "temporarily" (ie. it doesn't change the
+default configured through the settings).
+
## Settings
-- **Show Seconds**: to conserve battery power, you can turn the seconds display off
+- **Show Seconds**: to conserve battery power, you can turn the seconds display off (as the default)
- **Invert Scrolling**: swaps the METAR scrolling direction of the top and bottom taps
diff --git a/apps/aviatorclk/aviatorclk.app.js b/apps/aviatorclk/aviatorclk.app.js
index 1d99fdbde..33d671bc7 100644
--- a/apps/aviatorclk/aviatorclk.app.js
+++ b/apps/aviatorclk/aviatorclk.app.js
@@ -169,6 +169,7 @@ function drawSeconds() {
let seconds = now.getSeconds().toString();
if (seconds.length == 1) seconds = '0' + seconds;
let y = Bangle.appRect.y + mainTimeHeight - 3;
+ g.setBgColor(g.theme.bg);
g.setFontAlign(-1, 1).setFont("Vector", secondaryFontHeight).setColor(COLOUR_GREY);
g.drawString(seconds, horizontalCenter + 54, y, true);
}
@@ -232,10 +233,10 @@ function draw() {
// initialise
g.clear(true);
-// scroll METAR lines on taps
-Bangle.setUI("clockupdown", action => {
+// scroll METAR lines (either by touch or tap)
+function scrollAVWX(action) {
switch (action) {
- case -1: // top tap
+ case -1: // top touch/tap
if (settings.invertScrolling) {
if (METARscollLines > 0)
METARscollLines--;
@@ -244,7 +245,7 @@ Bangle.setUI("clockupdown", action => {
METARscollLines++;
}
break;
- case 1: // bottom tap
+ case 1: // bottom touch/tap
if (settings.invertScrolling) {
if (METARscollLines < METARlinesCount - 4)
METARscollLines++;
@@ -254,11 +255,41 @@ Bangle.setUI("clockupdown", action => {
}
break;
default:
- // ignore
+ // ignore other actions
}
drawAVWX();
+}
+
+Bangle.on('tap', data => {
+ switch (data.dir) {
+ case 'top':
+ scrollAVWX(-1);
+ break;
+ case 'bottom':
+ scrollAVWX(1);
+ break;
+ case 'left':
+ case 'right':
+ // toggle seconds display on double taps left or right
+ if (data.double) {
+ if (settings.showSeconds) {
+ clearInterval(secondsInterval);
+ let y = Bangle.appRect.y + mainTimeHeight - 3;
+ g.clearRect(horizontalCenter + 54, y - secondaryFontHeight, g.getWidth(), y);
+ settings.showSeconds = false;
+ } else {
+ settings.showSeconds = true;
+ syncSecondsUpdate();
+ }
+ }
+ break;
+ default:
+ // ignore other taps
+ }
});
+Bangle.setUI("clockupdown", scrollAVWX);
+
// load widgets
Bangle.loadWidgets();
Bangle.drawWidgets();
@@ -276,8 +307,8 @@ updateAVWX();
// TMP for debugging:
-//METAR = 'YAAA 011100Z 21014KT CAVOK 23/08 Q1018 RMK RF000/0000';
-//METAR = 'YAAA 150900Z 14012KT 9999 SCT045 BKN064 26/14 Q1012 RMK RF000/0000 DL-W/DL-NW';
-//METAR = 'YAAA 020030Z VRB CAVOK';
+//METAR = 'YAAA 011100Z 21014KT CAVOK 23/08 Q1018 RMK RF000/0000'; drawAVWX();
+//METAR = 'YAAA 150900Z 14012KT 9999 SCT045 BKN064 26/14 Q1012 RMK RF000/0000 DL-W/DL-NW'; drawAVWX();
+//METAR = 'YAAA 020030Z VRB CAVOK'; drawAVWX();
//METARts = new Date(Date.now() - 61 * 60000); // 61 to trigger warning, 91 to trigger alert
diff --git a/apps/aviatorclk/aviatorclk.settings.js b/apps/aviatorclk/aviatorclk.settings.js
index 6db212ef1..d3ffbaad2 100644
--- a/apps/aviatorclk/aviatorclk.settings.js
+++ b/apps/aviatorclk/aviatorclk.settings.js
@@ -17,7 +17,6 @@
"< Back" : () => back(),
'Show Seconds': {
value: !!settings.showSeconds, // !! converts undefined to false
- format: v => v ? "On" : "Off",
onchange: v => {
settings.showSeconds = v;
writeSettings();
@@ -25,7 +24,6 @@
},
'Invert Scrolling': {
value: !!settings.invertScrolling, // !! converts undefined to false
- format: v => v ? "On" : "Off",
onchange: v => {
settings.invertScrolling = v;
writeSettings();
diff --git a/apps/aviatorclk/metadata.json b/apps/aviatorclk/metadata.json
index 6ae8c4a18..9d2b0beef 100644
--- a/apps/aviatorclk/metadata.json
+++ b/apps/aviatorclk/metadata.json
@@ -2,7 +2,7 @@
"id": "aviatorclk",
"name": "Aviator Clock",
"shortName":"AV8R Clock",
- "version":"1.00",
+ "version":"1.01",
"description": "A clock for aviators, with local time and UTC - and the latest METAR for the nearest airport",
"icon": "aviatorclk.png",
"screenshots": [{ "url": "screenshot.png" }, { "url": "screenshot2.png" }],
diff --git a/apps/bthome/ChangeLog b/apps/bthome/ChangeLog
new file mode 100644
index 000000000..5560f00bc
--- /dev/null
+++ b/apps/bthome/ChangeLog
@@ -0,0 +1 @@
+0.01: New App!
diff --git a/apps/bthome/README.md b/apps/bthome/README.md
new file mode 100644
index 000000000..d232e8d64
--- /dev/null
+++ b/apps/bthome/README.md
@@ -0,0 +1,26 @@
+# BTHome
+
+This uses BTHome (https://bthome.io/) to allow easy control of [Home Assistant](https://www.home-assistant.io/) via Bluetooth advertisements.
+
+Other apps like [the Home Assistant app](https://banglejs.com/apps/?id=ha) communicate with Home Assistant
+via your phone so work from anywhere, but require being in range of your phone.
+
+## Usage
+
+When the app is installed, go to the `BTHome` app and click Settings.
+
+Here, you can choose if you want to advertise your Battery status, but can also click `Add Button`.
+
+You can then add a custom button event:
+
+* `Icon` - the picture for the button
+* `Name` - the name associated with the button
+* `Action` - the action that Home Assistant will see when this button is pressed
+* `Button #` - the button event 'number' - keep this at 0 for now
+
+Once you've saved, you will then get your button shown in the BTHome app. Tapping it will make Bangle.js advertise via BTHome that the button has been pressed.
+
+## ClockInfo
+
+When you've added one or more buttons, they will appear in a ClockInfo under the main `Bangle.js` heading. You can just tap to select the ClockInfo, scroll down until a BTHome one is visible and then tap again. It will immediately send the Advertisement.
+
diff --git a/apps/bthome/app-icon.js b/apps/bthome/app-icon.js
new file mode 100644
index 000000000..ecdc205bc
--- /dev/null
+++ b/apps/bthome/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEw4X/AAIHBy06nnnDiHwBRMDrgLJhtXBZM1qvABZHVqtwFxFVqowIhoLBGBE1q35GBHVrkDytyrAuGHIPVroLFFwODrklqoLFLoOCrALHLoIXILoVw+APBBYhdCsEAyphFFwITBgQDBMIgeBqtUgILCSQQuBrflBYW+SQYuBuENBYItB6owCXYUDBYIUBYYYuBh2wBYNQ9cFGAWlq0JsGUgNgy0J1WsEgMWhtwBYXXhWq1YLBkvD4HUgNwnk61Wq2ALBwEAkkBAYPq14kCktsgEMgZmBBIILDqoMBBQOWBIM61ALCrYLBh1WBYMKHgILBqxlBnILC2eqBYVVIAPlrWj1mg9QLDtkDyta1ns2AXEX4Va1c84YLEWYVa1XAhwLJ2B5BBZA6BBZOAC5UA5xHI1E8NYQAFh2g9hrCBY2vQYYAFgSPBF4QAFX4U6cgQLH9S/BAA2qcYYAG9WuPIILHOoKdBBY8D9WvgA"))
\ No newline at end of file
diff --git a/apps/bthome/app.js b/apps/bthome/app.js
new file mode 100644
index 000000000..7e9a39531
--- /dev/null
+++ b/apps/bthome/app.js
@@ -0,0 +1,27 @@
+Bangle.loadWidgets();
+Bangle.drawWidgets();
+
+function showMenu() {
+ var settings = require("Storage").readJSON("bthome.json",1)||{};
+ if (!(settings.buttons instanceof Array))
+ settings.buttons = [];
+ var menu = { "": {title:"BTHome", back:load} };
+ settings.buttons.forEach((button,idx) => {
+ var img = require("icons").getIcon(button.icon);
+ menu[/*LANG*/"\0"+img+" "+button.name] = function() {
+ Bangle.btHome([{type:"button_event",v:button.v,n:button.n}],{event:true});
+ E.showMenu();
+ E.showMessage("Sending Event");
+ Bangle.buzz();
+ setTimeout(showMenu, 500);
+ };
+ });
+ menu[/*LANG*/"Settings"] = function() {
+ eval(require("Storage").read("bthome.settings.js"))(()=>showMenu());
+ };
+ E.showMenu(menu);
+}
+
+showMenu();
+
+
diff --git a/apps/bthome/boot.js b/apps/bthome/boot.js
new file mode 100644
index 000000000..9c02581fe
--- /dev/null
+++ b/apps/bthome/boot.js
@@ -0,0 +1,68 @@
+// Ensure we have the bleAdvert global (to play well with other stuff)
+if (!Bangle.bleAdvert) Bangle.bleAdvert = {};
+Bangle.btHomeData = [];
+{
+ require("BTHome").packetId = 0|(Math.random()*256); // random packet id so new packets show up
+ let settings = require("Storage").readJSON("bthome.json",1)||{};
+ if (settings.showBattery)
+ Bangle.btHomeData.push({
+ type : "battery",
+ v : E.getBattery()
+ });
+ // If buttons defined, add events for them
+ if (settings.buttons instanceof Array) {
+ let n = settings.buttons.reduce((n,b)=>b.n>n?b.n:n,-1);
+ for (var i=0;i<=n;i++)
+ Bangle.btHomeData.push({type:"button_event",v:"none",n:n});
+ }
+}
+
+/* Global function to allow advertising BTHome adverts
+ extras = array of extra data, see require("BTHome").getAdvertisement - can add {n:0/1/2} for different instances
+ options = { event : an event - advertise fast, and when connected
+ }
+*/
+Bangle.btHome = function(extras, options) {
+ options = options||{};
+ if(extras) { // update with extras
+ extras.forEach(extra => {
+ var n = Bangle.btHomeData.find(b=>b.type==extra.type && b.n==extra.n);
+ if (n) Object.assign(n, extra);
+ else Bangle.btHomeData.push(extra);
+ });
+ }
+ var bat = Bangle.btHomeData.find(b=>b.type=="battery");
+ if (bat) bat.v = E.getBattery();
+ var advert = require("BTHome").getAdvertisement(Bangle.btHomeData)[0xFCD2];
+ // Add to the list of available advertising
+ 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;
+ }
+ var advOptions = {};
+ var updateTimeout = 10*60*1000; // update every 10 minutes
+ if (options.event) { // if it's an event...
+ advOptions.interval = 50;
+ advOptions.whenConnected = true;
+ updateTimeout = 30000; // slow down in 30 seconds
+ }
+ NRF.setAdvertising(Bangle.bleAdvert, advOptions);
+ if (Bangle.btHomeTimeout) clearTimeout(Bangle.btHomeTimeout);
+ Bangle.btHomeTimeout = setTimeout(function() {
+ delete Bangle.btHomeTimeout;
+ // clear events
+ Bangle.btHomeData.forEach(d => {if (d.type=="button_event") d.v="none";});
+ // update
+ Bangle.btHome();
+ },updateTimeout);
+};
diff --git a/apps/bthome/clkinfo.js b/apps/bthome/clkinfo.js
new file mode 100644
index 000000000..8698c9828
--- /dev/null
+++ b/apps/bthome/clkinfo.js
@@ -0,0 +1,17 @@
+(function() {
+ var settings = require("Storage").readJSON("bthome.json",1)||{};
+ if (!(settings.buttons instanceof Array))
+ settings.buttons = [];
+ return {
+ name: "Bangle",
+ items: settings.buttons.map(button => {
+ return { name : button.name,
+ get : function() { return { text : button.name,
+ img : require("icons").getIcon(button.icon) }},
+ show : function() {},
+ hide : function() {},
+ run : function() { Bangle.btHome([{type:"button_event",v:button.v,n:button.n}],{event:true}); }
+ }
+ })
+ };
+}) // must not have a semi-colon!
\ No newline at end of file
diff --git a/apps/bthome/icon.png b/apps/bthome/icon.png
new file mode 100644
index 000000000..091784477
Binary files /dev/null and b/apps/bthome/icon.png differ
diff --git a/apps/bthome/metadata.json b/apps/bthome/metadata.json
new file mode 100644
index 000000000..c2767b9cf
--- /dev/null
+++ b/apps/bthome/metadata.json
@@ -0,0 +1,20 @@
+{ "id": "bthome",
+ "name": "BTHome",
+ "shortName":"BTHome",
+ "version":"0.01",
+ "description": "Allow your Bangle to advertise with BTHome and send events to Home Assistant via Bluetooth",
+ "icon": "icon.png",
+ "type": "app",
+ "tags": "clkinfo,bthome,bluetooth",
+ "supports" : ["BANGLEJS2"],
+ "dependencies": {"textinput":"type", "icons":"module"},
+ "readme": "README.md",
+ "storage": [
+ {"name":"bthome.img","url":"app-icon.js","evaluate":true},
+ {"name":"bthome.clkinfo.js","url":"clkinfo.js"},
+ {"name":"bthome.boot.js","url":"boot.js"},
+ {"name":"bthome.app.js","url":"app.js"},
+ {"name":"bthome.settings.js","url":"settings.js"}
+ ],
+ "data":[{"name":"bthome.json"}]
+}
diff --git a/apps/bthome/settings.js b/apps/bthome/settings.js
new file mode 100644
index 000000000..70f50f2ac
--- /dev/null
+++ b/apps/bthome/settings.js
@@ -0,0 +1,91 @@
+(function(back) {
+ var settings = require("Storage").readJSON("bthome.json",1)||{};
+ if (!(settings.buttons instanceof Array))
+ settings.buttons = [];
+
+ function saveSettings() {
+ require("Storage").writeJSON("bthome.json",settings)
+ }
+
+ function showButtonMenu(button, isNew) {
+ var isNew = false;
+ if (!button) {
+ button = {name:"home", icon:"home", n:0, v:"press"};
+ isNew = true;
+ }
+ var actions = ["press","double_press","triple_press","long_press","long_double_press","long_triple_press"];
+ var menu = {
+ "":{title:isNew ? /*LANG*/"New Button" : /*LANG*/"Edit Button", back:showMenu},
+ /*LANG*/"Icon" : {
+ value : "\0"+require("icons").getIcon(button.icon),
+ onchange : () => {
+ require("icons").showIconChooser().then(function(iconName) {
+ button.icon = iconName;
+ button.name = iconName;
+ showButtonMenu(button, isNew);
+ }, function() {
+ showButtonMenu(button, isNew);
+ });
+ }
+ },
+ /*LANG*/"Name" : {
+ value : button.name,
+ onchange : () => {
+ require("textinput").input({text:button.name}).then(function(name) {
+ button.name = name;
+ showButtonMenu(button, isNew);
+ }, function() {
+ showButtonMenu(button, isNew);
+ });
+ }
+ },
+ /*LANG*/"Action" : {
+ value : Math.max(0,actions.indexOf(button.v)), min:0, max:actions.length-1,
+ format : v => actions[v],
+ onchange : v => button.v=actions[v]
+ },
+ /*LANG*/"Button #" : {
+ value : button.n, min:0, max:3,
+ onchange : v => button.n=v
+ },
+ /*LANG*/"Save" : () => {
+ settings.buttons.push(button);
+ saveSettings();
+ showMenu();
+ }
+ };
+ if (!isNew) menu[/*LANG*/"Delete"] = function() {
+ E.showPrompt("Delete Button?").then(function(yes) {
+ if (yes) {
+ settings.buttons.splice(settings.buttons.indexOf(button),1);
+ saveSettings();
+ }
+ showMenu();
+ });
+ }
+ E.showMenu(menu);
+ }
+
+ function showMenu() {
+ var menu = { "": {title:"BTHome", back:back},
+ /*LANG*/"Show Battery" : {
+ value : !!settings.showBattery,
+ onchange : v=>{
+ settings.showBattery = v;
+ saveSettings();
+ }
+ }
+ };
+ settings.buttons.forEach((button,idx) => {
+ var img = require("icons").getIcon(button.icon);
+ menu[/*LANG*/"Button"+(img ? " \0"+img : (idx+1))] = function() {
+ showButtonMenu(button, false);
+ };
+ });
+ menu[/*LANG*/"Add Button"] = function() {
+ showButtonMenu(undefined, true);
+ };
+ E.showMenu(menu);
+ }
+ showMenu();
+})
\ No newline at end of file
diff --git a/apps/bthometemp/metadata.json b/apps/bthometemp/metadata.json
index 8ffb22c83..fc6804f17 100644
--- a/apps/bthometemp/metadata.json
+++ b/apps/bthometemp/metadata.json
@@ -2,7 +2,7 @@
"name": "BTHome Temperature and Pressure",
"shortName":"BTHome T",
"version":"0.02",
- "description": "Displays temperature and pressure, and advertises them over bluetooth using BTHome.io standard",
+ "description": "Displays temperature and pressure, and advertises them over bluetooth for Home Assistant using BTHome.io standard",
"icon": "app.png",
"tags": "bthome,bluetooth,temperature",
"supports" : ["BANGLEJS2"],
diff --git a/apps/espruinoctrl/ChangeLog b/apps/espruinoctrl/ChangeLog
index 819ae56cb..522cba63e 100644
--- a/apps/espruinoctrl/ChangeLog
+++ b/apps/espruinoctrl/ChangeLog
@@ -1,2 +1,3 @@
0.01: New App!
0.02: Disable not existing BTN3 on Bangle.js 2, set maximum transmit power
+0.03: Now use BTN2 on Bangle.js 1, and on Bangle.js 2 use the middle button to return to the menu
\ No newline at end of file
diff --git a/apps/espruinoctrl/README.md b/apps/espruinoctrl/README.md
index 7b2e434e7..59c96b0de 100644
--- a/apps/espruinoctrl/README.md
+++ b/apps/espruinoctrl/README.md
@@ -14,7 +14,8 @@ with 4 options:
with this address will be connected to directly. If not specified a menu
showing available Espruino devices is popped up.
* **RX** - If checked, the app will display any data received from the
-device being connected to. Use this if you want to print data - eg: `print(E.getBattery())`
+device being connected to (waiting 500ms after the last data before disconnecting).
+Use this if you want to print data - eg: `print(E.getBattery())`
When done, click 'Upload'. Your changes will be saved to local storage
so they'll be remembered next time you upload from the same device.
@@ -25,4 +26,9 @@ Simply load the app and you'll see a menu with the menu items
you defined. Select one and you'll be able to connect to the device
and send the command.
-If a command should wait for a response then
+The Bangle will connect to the device, send the command, and if:
+
+* `RX` isn't set it will disconnect immediately and return to the menu
+* `RX` is set it will listen for a response and write it to the screen, before
+disconnecting after 500ms of inactivity. To return to the menu after this, press the button.
+
diff --git a/apps/espruinoctrl/custom.html b/apps/espruinoctrl/custom.html
index 2329ad214..27ef1eb53 100644
--- a/apps/espruinoctrl/custom.html
+++ b/apps/espruinoctrl/custom.html
@@ -194,16 +194,14 @@ function sendCommandRX(device, text, callback) {
function done() {
Terminal.println("\\n============\\n Disconnected");
device.disconnect();
- if (global.BTN3 !== undefined) {
- setTimeout(function() {
- setWatch(function() {
- if (callback) callback();
- resolve();
- }, BTN3);
- g.reset().setFont("6x8",2).setFontAlign(0,0,1);
- g.drawString("Back", g.getWidth()-10, g.getHeight()-50);
- }, 200);
- }
+ setTimeout(function() {
+ setWatch(function() {
+ if (callback) callback();
+ resolve();
+ }, (process.env.HWVERSION==2) ? BTN1 : BTN2);
+ g.reset().setFont("6x8",2).setFontAlign(0,0,1);
+ g.drawString("Back", g.getWidth()-10, g.getHeight()/2);
+ }, 200);
}
device.getPrimaryService("6e400001-b5a3-f393-e0a9-e50e24dcca9e").then(function(s) {
service = s;
diff --git a/apps/espruinoctrl/metadata.json b/apps/espruinoctrl/metadata.json
index 9308b4a46..4f5fa01c8 100644
--- a/apps/espruinoctrl/metadata.json
+++ b/apps/espruinoctrl/metadata.json
@@ -2,7 +2,7 @@
"id": "espruinoctrl",
"name": "Espruino Control",
"shortName": "Espruino Ctrl",
- "version": "0.02",
+ "version": "0.03",
"description": "Send commands to other Espruino devices via the Bluetooth UART interface. Customisable commands!",
"icon": "app.png",
"tags": "tool,bluetooth",
diff --git a/apps/fwupdate/custom.html b/apps/fwupdate/custom.html
index b917da87b..b2e5b37fe 100644
--- a/apps/fwupdate/custom.html
+++ b/apps/fwupdate/custom.html
@@ -3,7 +3,7 @@
- This tool allows you to update the firmware on Bangle.js 2 devices
+
This tool allows you to update the firmware on Bangle.js 2 devices
from within the App Loader.
@@ -32,7 +32,7 @@
bit of code that runs when Bangle.js starts, and it is able to update the
Bangle.js firmware. Normally you would update firmware via this Firmware
Updater app, but if for some reason Bangle.js will not boot, you can
-
always use DFU to do the update manually .
+
always use DFU to do the update manually .
On DFU 2v19 and earlier, iOS devices could have issues updating firmware - 2v20 fixes this.
DFU is itself a bootloader, but here we're calling it DFU to avoid confusion
with the Bootloader app in the app loader (which prepares Bangle.js for running apps).
@@ -42,7 +42,7 @@
Advanced
Firmware updates via this tool work differently to the NRF Connect method mentioned on
- the Bangle.js 2 page . Firmware
+ the Bangle.js 2 page . Firmware
is uploaded to a file on the Bangle. Once complete the Bangle reboots and DFU copies
the new firmware into internal Storage.
In addition to the links above, you can upload a hex or zip file directly below. This file should be an .app_hex
@@ -58,6 +58,15 @@
+
Firmware ChangeLog ▼
+
+
@@ -93,9 +102,9 @@ function onInit(device) {
console.log("DFU CRC (7 pages) = "+crcs[1]);
var version = `unknown (CRC ${crcs[1]})`;
var ok = true;
- if (crcs[0] == 1787004733) { // check 6 page CRC - the 7th page isn't used in 2v20
- version = "2v20";
- } else { // for other versions all 7 pages are used, check those
+ if (crcs[0] == 1787004733) version = "2v20"; // check 6 page CRCs - the 7th page isn't used in 2v20+
+ else if (crcs[0] == 3816337552) version = "2v21";
+ else { // for other versions all 7 pages are used, check those
var crc = crcs[1];
if (crc==1339551013) { version = "2v10.219"; ok = false; }
if (crc==1207580954) { version = "2v10.236"; ok = false; }
@@ -436,13 +445,20 @@ function handleUpload() {
document.getElementById('fileLoader').addEventListener('change', handleFileSelect, false);
document.getElementById("upload").addEventListener("click", handleUpload);
-document.getElementById("info-btn").addEventListener("click", function() {
+document.getElementById("info-btn").addEventListener("click", function(e) {
document.getElementById("info-btn").style = "display:none";
document.getElementById("info-div").style = "";
+ e.preventDefault();
});
-document.getElementById("advanced-btn").addEventListener("click", function() {
+document.getElementById("advanced-btn").addEventListener("click", function(e) {
document.getElementById("advanced-btn").style = "display:none";
document.getElementById("advanced-div").style = "";
+ e.preventDefault();
+});
+document.getElementById("changelog-btn").addEventListener("click", function(e) {
+ document.getElementById("changelog-btn").style = "display:none";
+ document.getElementById("changelog-div").style = "";
+ e.preventDefault();
});
setTimeout(checkForFileOnServer, 10);
diff --git a/apps/gassist/settings.js b/apps/gassist/settings.js
index 987c3fdfc..20634ed5e 100644
--- a/apps/gassist/settings.js
+++ b/apps/gassist/settings.js
@@ -20,7 +20,6 @@
"< Back": () => back(),
'Front Tap:': {
value: (appSettings.enableTap === true),
- format: v => v ? "On" : "Off",
onchange: v => {
appSettings.enableTap = v;
writeSettings();
diff --git a/apps/ha/metadata.json b/apps/ha/metadata.json
index a34621b04..c58e5e486 100644
--- a/apps/ha/metadata.json
+++ b/apps/ha/metadata.json
@@ -2,7 +2,7 @@
"id": "ha",
"name": "Home Assistant",
"version": "0.10",
- "description": "Integrates your Bangle.js into Home Assistant.",
+ "description": "Integrates your Bangle.js into Home Assistant using Android Integration/Gadgetbridge",
"icon": "ha.png",
"type": "app",
"tags": "tool,clkinfo,bluetooth",
diff --git a/apps/hasensors/metadata.json b/apps/hasensors/metadata.json
index 106f11407..5764c6100 100644
--- a/apps/hasensors/metadata.json
+++ b/apps/hasensors/metadata.json
@@ -3,7 +3,7 @@
"name": "Home Assistant Sensors",
"shortName": "HA sensors",
"version": "0.02",
- "description": "Send sensor values to Home Assistant using the Android Integration.",
+ "description": "Send sensor values to Home Assistant using Android Integration/Gadgetbridge",
"icon": "ha.png",
"type": "bootloader",
"tags": "tool,sensors",
diff --git a/apps/icons/ChangeLog b/apps/icons/ChangeLog
new file mode 100644
index 000000000..73fce0974
--- /dev/null
+++ b/apps/icons/ChangeLog
@@ -0,0 +1 @@
+0.01: New library
diff --git a/apps/icons/README.md b/apps/icons/README.md
new file mode 100644
index 000000000..3eb420b62
--- /dev/null
+++ b/apps/icons/README.md
@@ -0,0 +1,21 @@
+# Icons Library
+
+This library contains a set of icons that might be useful in your application, as well as a chooser for those icons:
+
+```JS
+// get a list of available icons
+require("icons").getIconNames()
+
+// draw an icon
+g.drawImage(require("icons").getIcon("light"),0,0);
+
+// Allow the user to request an icon
+require("icons").showIconChooser().then(function(iconName) {
+ console.log("User chose "+iconName);
+}, function() {
+ console.log("User Cancelled");
+});
+```
+
+To ensure the app loader auto-installs this module along with your app, just add the line
+```"dependencies" : { "messageicons":"module" },``` to your `metadata.json` file.
\ No newline at end of file
diff --git a/apps/icons/app.png b/apps/icons/app.png
new file mode 100644
index 000000000..1afe0b05b
Binary files /dev/null and b/apps/icons/app.png differ
diff --git a/apps/icons/gen/bike.png b/apps/icons/gen/bike.png
new file mode 100644
index 000000000..65c3d6eaa
Binary files /dev/null and b/apps/icons/gen/bike.png differ
diff --git a/apps/icons/gen/car.png b/apps/icons/gen/car.png
new file mode 100644
index 000000000..4e28abea5
Binary files /dev/null and b/apps/icons/gen/car.png differ
diff --git a/apps/icons/gen/close.png b/apps/icons/gen/close.png
new file mode 100644
index 000000000..472c74cf7
Binary files /dev/null and b/apps/icons/gen/close.png differ
diff --git a/apps/icons/gen/down.png b/apps/icons/gen/down.png
new file mode 100644
index 000000000..05b04e617
Binary files /dev/null and b/apps/icons/gen/down.png differ
diff --git a/apps/icons/gen/fan.png b/apps/icons/gen/fan.png
new file mode 100644
index 000000000..bff383de5
Binary files /dev/null and b/apps/icons/gen/fan.png differ
diff --git a/apps/icons/gen/generate.js b/apps/icons/gen/generate.js
new file mode 100755
index 000000000..387826903
--- /dev/null
+++ b/apps/icons/gen/generate.js
@@ -0,0 +1,92 @@
+#!/usr/bin/node
+
+// Creates lib.js from icons
+// npm install png-js
+
+// Icons from https://fonts.google.com/icons
+
+var imageconverter = require("../../../webtools/imageconverter.js").imageconverter;
+var icons = JSON.parse(require("fs").readFileSync(__dirname+"/icon_names.json"));
+const imgOptions = {
+ mode : "1bit",
+ inverted : true,
+ transparent : true,
+ output: "raw"
+};
+var PNG = require('png-js');
+var IMAGE_BYTES = 76;
+
+var iconTests = [];
+
+var promises = [];
+
+icons.forEach((icon,iconIndex) => {
+ // create image
+ console.log("Loading "+icon.name);
+ var fn = __dirname+"/"+icon.name+".png";
+ console.log(fn);
+ var png = new PNG(require("fs").readFileSync(fn));
+ if (png.width!=24 || png.height!=24) {
+ console.warn(icon.name+" should be 24x24px");
+ }
+
+ promises.push(new Promise(r => {
+ png.decode(function (pixels) {
+ var rgba = new Uint8Array(pixels);
+ var isTransparent = false;
+ for (var i=0;i
{
+ iconData.set(Array.prototype.slice.call(Buffer.from(icon.img,"binary")), idx*IMAGE_BYTES)
+ });
+
+ console.log("Saving images");
+ require("fs").writeFileSync(__dirname+"/../icons.img", Buffer.from(iconData,"binary"));
+
+ console.log("Saving library");
+ require("fs").writeFileSync(__dirname+"/../lib.js", `
+// Auto-generated by apps/icons/gen/generate.js
+
+/// Get an icon based on a name from getIconNames that can be drawn with g.drawImage
+exports.getIcon = function(name) {
+ let match = ${JSON.stringify(","+icons.map(icon=>icon.name+"|"+icon.index).join(",")+",")}.match(new RegExp(\`,\${name.toLowerCase()}\\\\|(\\\\d+)\`))
+ return require("Storage").read("icons.img", (match===null)?0:match[1]*${IMAGE_BYTES}, ${IMAGE_BYTES});
+};
+
+/// Get a list of available icon names
+exports.getIconNames = function() {
+ return ${JSON.stringify(icons.map(i=>i.name))};
+};
+
+/// Show a menu to allow an icon to be chosen - its name is returned
+exports.showIconChooser = function() {
+ return new Promise((resolve,reject) => {
+ var menu = { "" : { title : /*LANG*/"Icons", back : ()=>{E.showMenu();reject();}}}
+ exports.getIconNames().forEach(name => {
+ menu[\`\\0\${exports.getIcon(name)} \${name}\`] = ()=>{E.showMenu();resolve(name);};
+ });
+ E.showMenu(menu);
+ });
+};
+ `);
+});
diff --git a/apps/icons/gen/home.png b/apps/icons/gen/home.png
new file mode 100644
index 000000000..3125c0749
Binary files /dev/null and b/apps/icons/gen/home.png differ
diff --git a/apps/icons/gen/icon_names.json b/apps/icons/gen/icon_names.json
new file mode 100644
index 000000000..d0b3f540c
--- /dev/null
+++ b/apps/icons/gen/icon_names.json
@@ -0,0 +1,16 @@
+[
+ {"name":"home"},
+ {"name":"bike"},
+ {"name":"car"},
+ {"name":"fan"},
+ {"name":"light"},
+ {"name":"plug"},
+ {"name":"rocket"},
+ {"name":"switch"},
+ {"name":"sync"},
+ {"name":"up"},
+ {"name":"down"},
+ {"name":"left"},
+ {"name":"right"},
+ {"name":"close"}
+]
diff --git a/apps/icons/gen/left.png b/apps/icons/gen/left.png
new file mode 100644
index 000000000..d602f1076
Binary files /dev/null and b/apps/icons/gen/left.png differ
diff --git a/apps/icons/gen/light.png b/apps/icons/gen/light.png
new file mode 100644
index 000000000..15b255dc7
Binary files /dev/null and b/apps/icons/gen/light.png differ
diff --git a/apps/icons/gen/plug.png b/apps/icons/gen/plug.png
new file mode 100644
index 000000000..0b4be69af
Binary files /dev/null and b/apps/icons/gen/plug.png differ
diff --git a/apps/icons/gen/right.png b/apps/icons/gen/right.png
new file mode 100644
index 000000000..069e91437
Binary files /dev/null and b/apps/icons/gen/right.png differ
diff --git a/apps/icons/gen/rocket.png b/apps/icons/gen/rocket.png
new file mode 100644
index 000000000..414f67182
Binary files /dev/null and b/apps/icons/gen/rocket.png differ
diff --git a/apps/icons/gen/switch.png b/apps/icons/gen/switch.png
new file mode 100644
index 000000000..9d6f8a74b
Binary files /dev/null and b/apps/icons/gen/switch.png differ
diff --git a/apps/icons/gen/sync.png b/apps/icons/gen/sync.png
new file mode 100644
index 000000000..7966646b4
Binary files /dev/null and b/apps/icons/gen/sync.png differ
diff --git a/apps/icons/gen/up.png b/apps/icons/gen/up.png
new file mode 100644
index 000000000..e6955fe84
Binary files /dev/null and b/apps/icons/gen/up.png differ
diff --git a/apps/icons/icons.img b/apps/icons/icons.img
new file mode 100644
index 000000000..9bc112c97
Binary files /dev/null and b/apps/icons/icons.img differ
diff --git a/apps/icons/lib.js b/apps/icons/lib.js
new file mode 100644
index 000000000..7889654f1
--- /dev/null
+++ b/apps/icons/lib.js
@@ -0,0 +1,25 @@
+
+// Auto-generated by apps/icons/gen/generate.js
+
+/// Get an icon based on a name from getIconNames that can be drawn with g.drawImage
+exports.getIcon = function(name) {
+ let match = ",home|0,bike|1,car|2,fan|3,light|4,plug|5,rocket|6,switch|7,sync|8,up|9,down|10,left|11,right|12,close|13,".match(new RegExp(`,${name.toLowerCase()}\\|(\\d+)`))
+ return require("Storage").read("icons.img", (match===null)?0:match[1]*76, 76);
+};
+
+/// Get a list of available icon names
+exports.getIconNames = function() {
+ return ["home","bike","car","fan","light","plug","rocket","switch","sync","up","down","left","right","close"];
+};
+
+/// Show a menu to allow an icon to be chosen - its name is returned
+exports.showIconChooser = function() {
+ return new Promise((resolve,reject) => {
+ var menu = { "" : { title : /*LANG*/"Icons", back : ()=>{E.showMenu();reject();}}}
+ exports.getIconNames().forEach(name => {
+ menu[`\0${exports.getIcon(name)} ${name}`] = ()=>{E.showMenu();resolve(name);};
+ });
+ E.showMenu(menu);
+ });
+};
+
\ No newline at end of file
diff --git a/apps/icons/metadata.json b/apps/icons/metadata.json
new file mode 100644
index 000000000..77b24d98c
--- /dev/null
+++ b/apps/icons/metadata.json
@@ -0,0 +1,17 @@
+{
+ "id": "icons",
+ "name": "Icons",
+ "version": "0.01",
+ "description": "Library containing useful icons for apps",
+ "icon": "app.png",
+ "type": "module",
+ "tags": "tool,system",
+ "supports": ["BANGLEJS","BANGLEJS2"],
+ "provides_modules" : ["icons"],
+ "default": true,
+ "readme": "README.md",
+ "storage": [
+ {"name":"icons","url":"lib.js"},
+ {"name":"icons.img","url":"icons.img"}
+ ]
+}
diff --git a/apps/kbtouch/metadata.json b/apps/kbtouch/metadata.json
index 349726498..10d63a99e 100644
--- a/apps/kbtouch/metadata.json
+++ b/apps/kbtouch/metadata.json
@@ -4,6 +4,7 @@
"description": "A library for text input via onscreen keyboard",
"icon": "app.png",
"type":"textinput",
+ "default": true,
"tags": "keyboard",
"supports" : ["BANGLEJS2"],
"screenshots": [{"url":"screenshot.png"}],
diff --git a/apps/legoremote/ChangeLog b/apps/legoremote/ChangeLog
index 5560f00bc..b86638553 100644
--- a/apps/legoremote/ChangeLog
+++ b/apps/legoremote/ChangeLog
@@ -1 +1,2 @@
0.01: New App!
+0.02: Add more control styles
diff --git a/apps/legoremote/README.md b/apps/legoremote/README.md
index a95b7b154..b6cd82979 100644
--- a/apps/legoremote/README.md
+++ b/apps/legoremote/README.md
@@ -10,8 +10,21 @@ in the future this app will be able to support other types of remote (see below)
## Usage
-Run the app, and ensure you're not connected to your watch via Bluetooth
-(a warning will pop up if so).
+Run the app, then choose the type of controls you want and ensure you're not connected
+to your watch via Bluetooth (a warning will pop up if so).
+
+Linear mode controls A/B axes individually, and allows you to vary the speed of the
+motors based on the distance you drag from the centre. Other modes just use on/off
+buttons.
+
+| Mode | up | down | left | right |
+|------------|------|------|------|-------|
+| **Linear** | +A | -A | -B | +B |
+| **Normal** | +A | -A | -B | +B |
+| **Tank** | -A+B | +A-B | +A+B | -A-B |
+| **Merged** | -A-B | +A+B | +A-B | -A+B |
+
+In all cases pressing the C/D buttons will turn on C/D outputs
Now press the arrow keys on the screen to control the robot.
diff --git a/apps/legoremote/app.js b/apps/legoremote/app.js
index 1c76a54a8..40935cabf 100644
--- a/apps/legoremote/app.js
+++ b/apps/legoremote/app.js
@@ -1,5 +1,4 @@
var lego = require("mouldking");
-lego.start();
E.on('kill', () => {
// return to normal Bluetooth advertising
NRF.setAdvertising({},{showName:true});
@@ -12,59 +11,133 @@ var controlState = "";
Bangle.loadWidgets();
Bangle.drawWidgets();
var R = Bangle.appRect;
-// we'll divide up into 3x3
-function getBoxCoords(x,y) {
- return {
- x : R.x + R.w*x/3,
- y : R.y + R.h*y/3
- };
-}
-function draw() {
- g.reset().clearRect(R);
- var c, ninety = Math.PI/2;
- var colOn = "#f00", colOff = g.theme.fg;
- c = getBoxCoords(1.5, 0.5);
- g.setColor(controlState=="up"?colOn:colOff).drawImage(arrowIcon,c.x,c.y,{rotate:0});
- c = getBoxCoords(2.5, 1.5);
- g.setColor(controlState=="right"?colOn:colOff).drawImage(arrowIcon,c.x,c.y,{rotate:ninety});
- c = getBoxCoords(0.5, 1.5);
- g.setColor(controlState=="left"?colOn:colOff).drawImage(arrowIcon,c.x,c.y,{rotate:-ninety});
- c = getBoxCoords(1.5, 1.5);
- g.setColor(controlState=="down"?colOn:colOff).drawImage(arrowIcon,c.x,c.y,{rotate:ninety*2});
- if (NRF.getSecurityStatus().connected) {
- c = getBoxCoords(1.5, 2.5);
- g.setFontAlign(0,0).setFont("6x8").drawString("WARNING:\nBluetooth Connected\nYou must disconnect\nbefore LEGO will work",c.x,c.y);
+function startLegoButtons(controls) {
+ // we'll divide up into 3x3
+ function getBoxCoords(x,y) {
+ return {
+ x : R.x + R.w*x/3,
+ y : R.y + R.h*y/3
+ };
}
-}
-draw();
-NRF.on('connect', draw);
-NRF.on('disconnect', draw);
-function setControlState(s) {
- controlState = s;
- var c = {};
- var speed = 3;
- if (s=="up") c={a:-speed,b:-speed};
- if (s=="down") c={a:speed,b:speed};
- if (s=="left") c={a:speed,b:-speed};
- if (s=="right") c={a:-speed,b:speed};
+ function draw() {
+ g.reset().clearRect(R);
+ var c, ninety = Math.PI/2;
+ var colOn = "#f00", colOff = g.theme.fg;
+ c = getBoxCoords(1.5, 0.5);
+ g.setColor(controlState=="up"?colOn:colOff).drawImage(arrowIcon,c.x,c.y,{rotate:0});
+ c = getBoxCoords(2.5, 1.5);
+ g.setColor(controlState=="right"?colOn:colOff).drawImage(arrowIcon,c.x,c.y,{rotate:ninety});
+ c = getBoxCoords(0.5, 1.5);
+ g.setColor(controlState=="left"?colOn:colOff).drawImage(arrowIcon,c.x,c.y,{rotate:-ninety});
+ c = getBoxCoords(1.5, 1.5);
+ g.setColor(controlState=="down"?colOn:colOff).drawImage(arrowIcon,c.x,c.y,{rotate:ninety*2});
+ if (NRF.getSecurityStatus().connected) {
+ c = getBoxCoords(1.5, 2.5);
+ g.setFontAlign(0,0).setFont("6x8").drawString("WARNING:\nBluetooth Connected\nYou must disconnect\nbefore LEGO will work",c.x,c.y);
+ }
+ g.setFont("6x8:3").setFontAlign(0,0);
+ c = getBoxCoords(0.5, 0.5);
+ g.setColor(controlState=="c"?colOn:colOff).drawString("C",c.x,c.y);
+ c = getBoxCoords(2.5, 0.5);
+ g.setColor(controlState=="d"?colOn:colOff).drawString("D",c.x,c.y);
+ }
+
+ function setControlState(s) {
+ controlState = s;
+ var c = {};
+ if (s in controls)
+ c = controls[s];
+ draw();
+ lego.set(c);
+ }
+
+ lego.start();
+ Bangle.setUI({mode:"custom", drag : e => {
+ var x = Math.floor(E.clip((e.x - R.x) * 3 / R.w,0,2.99));
+ var y = Math.floor(E.clip((e.y - R.y) * 3 / R.h,0,2.99));
+ if (!e.b) {
+ setControlState("");
+ return;
+ }
+ if (y==0) { // top row
+ if (x==0) setControlState("c");
+ if (x==1) setControlState("up");
+ if (x==2) setControlState("d");
+ } else if (y==1) {
+ if (x==0) setControlState("left");
+ if (x==1) setControlState("down");
+ if (x==2) setControlState("right");
+ }
+ }});
+
draw();
- lego.set(c);
+ NRF.on('connect', draw);
+ NRF.on('disconnect', draw);
}
-Bangle.on('drag',e => {
- var x = Math.floor(E.clip((e.x - R.x) * 3 / R.w,0,2.99));
- var y = Math.floor(E.clip((e.y - R.y) * 3 / R.h,0,2.99));
- if (!e.b) {
- setControlState("");
- return;
- }
- if (y==0) { // top row
- if (x==1) setControlState("up");
- } else if (y==1) {
- if (x==0) setControlState("left");
- if (x==1) setControlState("down");
- if (x==2) setControlState("right");
+function startLegoLinear() {
+ var mx = R.x+R.w/2;
+ var my = R.y+R.h/2;
+ var x=0,y=0;
+ var scale = 10;
+
+ function draw() {
+ g.reset().clearRect(R);
+ for (var i=3;i<60;i+=10)
+ g.drawCircle(mx,my,i);
+ g.setColor("#F00");
+ var px = E.clip(mx + x*scale, R.x+20, R.x2-20);
+ var py = E.clip(my + y*scale, R.y+20, R.y2-20);
+ g.fillCircle(px, py, 20);
}
+
+ lego.start();
+ Bangle.setUI({mode:"custom", drag : e => {
+ x = Math.round((e.x - mx) / scale);
+ y = Math.round((e.y - my) / scale);
+ if (!e.b) {
+ x=0; y=0;
+ }
+ lego.set({a:x, b:y});
+ draw();
+ }});
+
+ draw();
+ NRF.on('connect', draw);
+ NRF.on('disconnect', draw);
+}
+
+// Mappings of button to output
+const CONTROLS = {
+ normal : {
+ up : {a: 7,b: 0},
+ down : {a:-7,b: 0},
+ left : {a: 0,b:-7},
+ right: {a: 0,b: 7},
+ c : {c:7},
+ d : {d:7}
+ }, tank : {
+ up : {a:-7,b:7},
+ down : {a: 7,b:-7},
+ left : {a: 7,b:7},
+ right: {a:-7,b:-7},
+ c : {c:7},
+ d : {d:7}
+ }, merged : {
+ up : {a: 7,b: 7},
+ down : {a:-7,b:-7},
+ left : {a: 7,b:-7},
+ right: {a:-7,b: 7},
+ c : {c:7},
+ d : {d:7}
+ }
+ };
+
+E.showMenu({ "" : {title:"LEGO Remote", back:()=>load()},
+ "Linear" : () => startLegoLinear(),
+ "Normal" : () => startLego(CONTROLS.normal),
+ "Tank" : () => startLego(CONTROLS.tank),
+ "Marged" : () => startLego(CONTROLS.merged),
});
diff --git a/apps/legoremote/metadata.json b/apps/legoremote/metadata.json
index c86251860..8fe4c4b44 100644
--- a/apps/legoremote/metadata.json
+++ b/apps/legoremote/metadata.json
@@ -1,7 +1,7 @@
{ "id": "legoremote",
"name": "LEGO Remote control",
"shortName":"LEGO Remote",
- "version":"0.01",
+ "version":"0.02",
"description": "Use your Bangle.js to control LEGO models. See the README for compatibility",
"icon": "app.png",
"tags": "toy,lego,bluetooth",
diff --git a/apps/mywelcome/settings.js b/apps/mywelcome/settings.js
index cf7208d65..e8d294cd4 100644
--- a/apps/mywelcome/settings.js
+++ b/apps/mywelcome/settings.js
@@ -5,7 +5,6 @@
'': { 'title': 'Welcome App' },
'Run next boot': {
value: !settings.welcomed,
- format: v => v ? 'Yes' : 'No',
onchange: v => require('Storage').write('mywelcome.json', {welcomed: !v}),
},
'Run Now': () => load('mywelcome.app.js'),
diff --git a/apps/openstmap/ChangeLog b/apps/openstmap/ChangeLog
index 98d01d5e9..f0a1e5c5a 100644
--- a/apps/openstmap/ChangeLog
+++ b/apps/openstmap/ChangeLog
@@ -33,4 +33,5 @@
0.26: Ensure that when redrawing, we always cancel any in-progress track draw
0.27: Display message if no map is installed
0.28: Fix rounding errors
-0.29: move exit to bottom of menu
+0.29: Keep exit at bottom of menu
+ Speed up latLonToXY for track rendering
\ No newline at end of file
diff --git a/apps/openstmap/openstmap.js b/apps/openstmap/openstmap.js
index 7e84f66e5..eb1aeaf3c 100644
--- a/apps/openstmap/openstmap.js
+++ b/apps/openstmap/openstmap.js
@@ -38,17 +38,17 @@ if (m.map) {
m.lat = m.map.lat; // position of middle of screen
m.lon = m.map.lon; // position of middle of screen
}
+var CX = g.getWidth()/2;
+var CY = g.getHeight()/2;
// return number of tiles drawn
exports.draw = function() {
- var cx = g.getWidth()/2;
- var cy = g.getHeight()/2;
var p = Bangle.project({lat:m.lat,lon:m.lon});
let count = 0;
m.maps.forEach((map,idx) => {
var d = map.scale/m.scale;
- var ix = (p.x-map.center.x)/m.scale + (map.imgx*d/2) - cx;
- var iy = (map.center.y-p.y)/m.scale + (map.imgy*d/2) - cy;
+ var ix = (p.x-map.center.x)/m.scale + (map.imgx*d/2) - CX;
+ var iy = (map.center.y-p.y)/m.scale + (map.imgy*d/2) - CY;
var o = {};
var s = map.tilesize;
if (d!=1) { // if the two are different, add scaling
@@ -85,14 +85,12 @@ exports.draw = function() {
};
/// Convert lat/lon to pixels on the screen
-exports.latLonToXY = function(lat, lon) {
- var p = Bangle.project({lat:m.lat,lon:m.lon});
- var q = Bangle.project({lat:lat, lon:lon});
- var cx = g.getWidth()/2;
- var cy = g.getHeight()/2;
+exports.latLonToXY = function(lat, lon) { "ram"
+ var p = Bangle.project({lat:m.lat,lon:m.lon}),
+ q = Bangle.project({lat:lat, lon:lon});
return {
- x : Math.round((q.x-p.x)/m.scale + cx),
- y : Math.round(cy - (q.y-p.y)/m.scale)
+ x : Math.round((q.x-p.x)/m.scale + CX),
+ y : Math.round(CY - (q.y-p.y)/m.scale)
};
};
@@ -102,4 +100,4 @@ exports.scroll = function(x,y) {
var b = Bangle.project({lat:m.lat+1,lon:m.lon+1});
this.lon += x * m.scale / (a.x-b.x);
this.lat -= y * m.scale / (a.y-b.y);
-};
+};
\ No newline at end of file
diff --git a/apps/recorder/ChangeLog b/apps/recorder/ChangeLog
index dadd3fbcb..0e20a13fc 100644
--- a/apps/recorder/ChangeLog
+++ b/apps/recorder/ChangeLog
@@ -45,3 +45,4 @@
0.36: When recording with 1 second periods, log time with one decimal.
0.37: 1 second periods + gps log => log when gps event is received, not with
setInterval.
+0.38: Tweaks to speed up track rendering
\ No newline at end of file
diff --git a/apps/recorder/app.js b/apps/recorder/app.js
index a2218420a..c02a800d0 100644
--- a/apps/recorder/app.js
+++ b/apps/recorder/app.js
@@ -213,230 +213,230 @@ function viewTrack(filename, info) {
});
};
menu['< Back'] = () => { viewTracks(); };
+ return E.showMenu(menu);
+}
- function plotTrack(info) { "ram"
- function distance(lat1,long1,lat2,long2) { "ram"
- var x = (long1-long2) * Math.cos((lat1+lat2)*Math.PI/360);
- var y = lat2 - lat1;
- return Math.sqrt(x*x + y*y) * 6371000 * Math.PI / 180;
- }
+function plotTrack(info) { "ram"
+ function distance(lat1,long1,lat2,long2) { "ram"
+ var x = (long1-long2) * Math.cos((lat1+lat2)*Math.PI/360);
+ var y = lat2 - lat1;
+ return Math.sqrt(x*x + y*y) * 6371000 * Math.PI / 180;
+ }
- // Function to convert lat/lon to XY
- var getMapXY;
- if (info.qOSTM) {
- // scale map to view full track
- const max = Bangle.project({lat: info.maxLat, lon: info.maxLong});
- const min = Bangle.project({lat: info.minLat, lon: info.minLong});
- const scaleX = (max.x-min.x)/Bangle.appRect.w;
- const scaleY = (max.y-min.y)/Bangle.appRect.h;
- osm.scale = Math.ceil((scaleX > scaleY ? scaleX : scaleY)*1.1); // add 10% margin
- getMapXY = osm.latLonToXY.bind(osm);
- } else {
- getMapXY = function(lat, lon) { "ram"
- return {x:cx + Math.round((long - info.lon)*info.lfactor*info.scale),
- y:cy + Math.round((info.lat - lat)*info.scale)};
- };
- }
+ // Function to convert lat/lon to XY
+ var XY;
+ if (info.qOSTM) {
+ // scale map to view full track
+ const max = Bangle.project({lat: info.maxLat, lon: info.maxLong});
+ const min = Bangle.project({lat: info.minLat, lon: info.minLong});
+ const scaleX = (max.x-min.x)/Bangle.appRect.w;
+ const scaleY = (max.y-min.y)/Bangle.appRect.h;
+ osm.scale = Math.ceil((scaleX > scaleY ? scaleX : scaleY)*1.1); // add 10% margin
+ XY = osm.latLonToXY.bind(osm);
+ } else {
+ XY = function(lat, lon) { "ram"
+ return {x:cx + Math.round((long - info.lon)*info.lfactor*info.scale),
+ y:cy + Math.round((info.lat - lat)*info.scale)};
+ };
+ }
- E.showMenu(); // remove menu
- E.showMessage(/*LANG*/"Drawing...",/*LANG*/"Track "+info.fn);
- g.flip(); // on buffered screens, draw a not saying we're busy
- g.clear(1);
- var s = require("Storage");
- var W = g.getWidth();
- var H = g.getHeight();
- var cx = W/2;
- var cy = 24 + (H-24)/2;
- if (!info.qOSTM) {
- g.setColor("#f00").fillRect(9,80,11,120).fillPoly([9,60,19,80,0,80]);
- g.setColor(g.theme.fg).setFont("6x8").setFontAlign(0,0).drawString("N",10,50);
- } else {
- osm.lat = info.lat;
- osm.lon = info.lon;
- osm.draw();
- g.setColor("#000");
+ E.showMenu(); // remove menu
+ E.showMessage(/*LANG*/"Drawing...",/*LANG*/"Track "+info.fn);
+ g.flip(); // on buffered screens, draw a not saying we're busy
+ g.clear(1);
+ var s = require("Storage");
+ var G = g;
+ var W = g.getWidth();
+ var H = g.getHeight();
+ var cx = W/2;
+ var cy = 24 + (H-24)/2;
+ if (!info.qOSTM) {
+ g.setColor("#f00").fillRect(9,80,11,120).fillPoly([9,60,19,80,0,80]);
+ g.setColor(g.theme.fg).setFont("6x8").setFontAlign(0,0).drawString("N",10,50);
+ } else {
+ osm.lat = info.lat;
+ osm.lon = info.lon;
+ osm.draw();
+ g.setColor("#000");
+ }
+ var latIdx = info.fields.indexOf("Latitude");
+ var lonIdx = info.fields.indexOf("Longitude");
+ g.drawString(asTime(info.duration),10,220);
+ var f = require("Storage").open(info.filename,"r");
+ if (f===undefined) return;
+ var l = f.readLine(f);
+ l = f.readLine(f); // skip headers
+ var ox=0;
+ var oy=0;
+ var olat,olong,dist=0;
+ var i=0, c = l.split(",");
+ // skip until we find our first data
+ while(l!==undefined && c[latIdx]=="") {
+ c = l.split(",");
+ l = f.readLine(f);
+ }
+ // now start plotting
+ var lat = +c[latIdx];
+ var long = +c[lonIdx];
+ var mp = XY(lat, long);
+ g.moveTo(mp.x,mp.y);
+ g.setColor("#0f0");
+ g.fillCircle(mp.x,mp.y,5);
+ if (info.qOSTM) g.setColor("#f09");
+ else g.setColor(g.theme.fg);
+ l = f.readLine(f);
+ g.flip(); // force update
+ while(l!==undefined) {
+ c = l.split(",");l = f.readLine(f);
+ if (c[latIdx]=="")continue;
+ lat = +c[latIdx];
+ long = +c[lonIdx];
+ mp = XY(lat, long);
+ G.lineTo(mp.x,mp.y);
+ if (info.qOSTM) G.fillCircle(mp.x,mp.y,2); // make the track more visible
+ var d = distance(olat,olong,lat,long);
+ if (!isNaN(d)) dist+=d;
+ olat = lat;
+ olong = long;
+ ox = mp.x;
+ oy = mp.y;
+ if (++i > 100) { G.flip();i=0; }
+ }
+ g.setColor("#f00");
+ g.fillCircle(ox,oy,5);
+ if (info.qOSTM) g.setColor("#000");
+ else g.setColor(g.theme.fg);
+ g.drawString(require("locale").distance(dist,2),g.getWidth() / 2, g.getHeight() - 20);
+ g.setFont("6x8",2);
+ g.setFontAlign(0,0,3);
+ var isBTN3 = "BTN3" in global;
+ g.drawString(/*LANG*/"Back",g.getWidth() - 10, isBTN3 ? (g.getHeight() - 40) : (g.getHeight()/2));
+ setWatch(function() {
+ viewTrack(info.fn, info);
+ }, isBTN3?BTN3:BTN1);
+ Bangle.drawWidgets();
+ g.flip();
+}
+
+function plotGraph(info, style) { "ram"
+ E.showMenu(); // remove menu
+ E.showMessage(/*LANG*/"Calculating...",/*LANG*/"Track "+info.fn);
+ var filename = info.filename;
+ var infn = new Float32Array(80);
+ var infc = new Uint16Array(80);
+ var title;
+ var lt = 0; // last time
+ var tn = 0; // count for each time period
+ var strt, dur = info.duration;
+ var f = require("Storage").open(filename,"r");
+ if (f===undefined) return;
+ var l = f.readLine(f);
+ l = f.readLine(f); // skip headers
+ var nl = 0, c, i;
+ var factor = 1; // multiplier used for values when graphing
+ var timeIdx = info.fields.indexOf("Time");
+ if (l!==undefined) {
+ c = l.split(",");
+ strt = c[timeIdx];
+ }
+ if (style=="Heartrate") {
+ title = /*LANG*/"Heartrate (bpm)";
+ var hrmIdx = info.fields.indexOf("Heartrate");
+ while(l!==undefined) {
+ ++nl;c=l.split(",");l = f.readLine(f);
+ if (c[hrmIdx]=="") continue;
+ i = Math.round(80*(c[timeIdx] - strt)/dur);
+ infn[i]+=+c[hrmIdx];
+ infc[i]++;
}
+ } else if (style=="Altitude") {
+ title = /*LANG*/"Altitude (m)";
+ var altIdx = info.fields.indexOf("Barometer Altitude");
+ if (altIdx<0) altIdx = info.fields.indexOf("Altitude");
+ while(l!==undefined) {
+ ++nl;c=l.split(",");l = f.readLine(f);
+ if (c[altIdx]=="") continue;
+ i = Math.round(80*(c[timeIdx] - strt)/dur);
+ infn[i]+=+c[altIdx];
+ infc[i]++;
+ }
+ } else if (style=="Speed") {
+ // use locate to work out units
+ var localeStr = require("locale").speed(1,5); // get what 1kph equates to
+ let units = localeStr.replace(/[0-9.]*/,"");
+ factor = parseFloat(localeStr)*3.6; // m/sec to whatever out units are
+ // title
+ title = /*LANG*/"Speed"+` (${units})`;
var latIdx = info.fields.indexOf("Latitude");
var lonIdx = info.fields.indexOf("Longitude");
- g.drawString(asTime(info.duration),10,220);
- var f = require("Storage").open(info.filename,"r");
- if (f===undefined) return;
- var l = f.readLine(f);
- l = f.readLine(f); // skip headers
- var ox=0;
- var oy=0;
- var olat,olong,dist=0;
- var i=0, c = l.split(",");
// skip until we find our first data
while(l!==undefined && c[latIdx]=="") {
c = l.split(",");
l = f.readLine(f);
}
- // now start plotting
- var lat = +c[latIdx];
- var long = +c[lonIdx];
- var mp = getMapXY(lat, long);
- g.moveTo(mp.x,mp.y);
- g.setColor("#0f0");
- g.fillCircle(mp.x,mp.y,5);
- if (info.qOSTM) g.setColor("#f09");
- else g.setColor(g.theme.fg);
- l = f.readLine(f);
- g.flip(); // force update
+ // now iterate
+ var p,lp = Bangle.project({lat:c[1],lon:c[2]});
+ var t,dx,dy,d,lt = c[timeIdx];
while(l!==undefined) {
- c = l.split(",");l = f.readLine(f);
- if (c[latIdx]=="")continue;
- lat = +c[latIdx];
- long = +c[lonIdx];
- mp = getMapXY(lat, long);
- g.lineTo(mp.x,mp.y);
- if (info.qOSTM) g.fillCircle(mp.x,mp.y,2); // make the track more visible
- var d = distance(olat,olong,lat,long);
- if (!isNaN(d)) dist+=d;
- olat = lat;
- olong = long;
- ox = mp.x;
- oy = mp.y;
- if (++i > 100) { g.flip();i=0; }
- }
- g.setColor("#f00");
- g.fillCircle(ox,oy,5);
- if (info.qOSTM) g.setColor("#000");
- else g.setColor(g.theme.fg);
- g.drawString(require("locale").distance(dist,2),g.getWidth() / 2, g.getHeight() - 20);
- g.setFont("6x8",2);
- g.setFontAlign(0,0,3);
- var isBTN3 = "BTN3" in global;
- g.drawString(/*LANG*/"Back",g.getWidth() - 10, isBTN3 ? (g.getHeight() - 40) : (g.getHeight()/2));
- setWatch(function() {
- viewTrack(info.fn, info);
- }, isBTN3?BTN3:BTN1);
- Bangle.drawWidgets();
- g.flip();
- }
-
- function plotGraph(info, style) { "ram"
- E.showMenu(); // remove menu
- E.showMessage(/*LANG*/"Calculating...",/*LANG*/"Track "+info.fn);
- var filename = info.filename;
- var infn = new Float32Array(80);
- var infc = new Uint16Array(80);
- var title;
- var lt = 0; // last time
- var tn = 0; // count for each time period
- var strt, dur = info.duration;
- var f = require("Storage").open(filename,"r");
- if (f===undefined) return;
- var l = f.readLine(f);
- l = f.readLine(f); // skip headers
- var nl = 0, c, i;
- var factor = 1; // multiplier used for values when graphing
- var timeIdx = info.fields.indexOf("Time");
- if (l!==undefined) {
- c = l.split(",");
- strt = c[timeIdx];
- }
- if (style=="Heartrate") {
- title = /*LANG*/"Heartrate (bpm)";
- var hrmIdx = info.fields.indexOf("Heartrate");
- while(l!==undefined) {
- ++nl;c=l.split(",");l = f.readLine(f);
- if (c[hrmIdx]=="") continue;
- i = Math.round(80*(c[timeIdx] - strt)/dur);
- infn[i]+=+c[hrmIdx];
+ ++nl;c=l.split(",");
+ l = f.readLine(f);
+ if (c[latIdx] == "") {
+ continue;
+ }
+ t = c[timeIdx];
+ i = Math.round(80*(t - strt)/dur);
+ p = Bangle.project({lat:c[latIdx],lon:c[lonIdx]});
+ dx = p.x-lp.x;
+ dy = p.y-lp.y;
+ d = Math.sqrt(dx*dx+dy*dy);
+ if (t!=lt) {
+ infn[i]+=d / (t-lt); // speed
infc[i]++;
}
- } else if (style=="Altitude") {
- title = /*LANG*/"Altitude (m)";
- var altIdx = info.fields.indexOf("Barometer Altitude");
- if (altIdx<0) altIdx = info.fields.indexOf("Altitude");
- while(l!==undefined) {
- ++nl;c=l.split(",");l = f.readLine(f);
- if (c[altIdx]=="") continue;
- i = Math.round(80*(c[timeIdx] - strt)/dur);
- infn[i]+=+c[altIdx];
- infc[i]++;
- }
- } else if (style=="Speed") {
- // use locate to work out units
- var localeStr = require("locale").speed(1,5); // get what 1kph equates to
- let units = localeStr.replace(/[0-9.]*/,"");
- factor = parseFloat(localeStr)*3.6; // m/sec to whatever out units are
- // title
- title = /*LANG*/"Speed"+` (${units})`;
- var latIdx = info.fields.indexOf("Latitude");
- var lonIdx = info.fields.indexOf("Longitude");
- // skip until we find our first data
- while(l!==undefined && c[latIdx]=="") {
- c = l.split(",");
- l = f.readLine(f);
- }
- // now iterate
- var p,lp = Bangle.project({lat:c[1],lon:c[2]});
- var t,dx,dy,d,lt = c[timeIdx];
- while(l!==undefined) {
- ++nl;c=l.split(",");
- l = f.readLine(f);
- if (c[latIdx] == "") {
- continue;
- }
- t = c[timeIdx];
- i = Math.round(80*(t - strt)/dur);
- p = Bangle.project({lat:c[latIdx],lon:c[lonIdx]});
- dx = p.x-lp.x;
- dy = p.y-lp.y;
- d = Math.sqrt(dx*dx+dy*dy);
- if (t!=lt) {
- infn[i]+=d / (t-lt); // speed
- infc[i]++;
- }
- lp = p;
- lt = t;
- }
- } else throw new Error("Unknown type "+style);
- var min=100000,max=-100000;
- for (var i=0;i0) infn[i]=factor*infn[i]/infc[i];
- else { // no data - search back and see if we can find something
- for (var j=i-1;j>=0;j--)
- if (infc[j]) { infn[i]=infn[j]; break; }
- }
- var n = infn[i];
- if (n>max) max=n;
- if (n 8) {
- grid*=2;
+ } else throw new Error("Unknown type "+style);
+ var min=100000,max=-100000;
+ for (var i=0;i0) infn[i]=factor*infn[i]/infc[i];
+ else { // no data - search back and see if we can find something
+ for (var j=i-1;j>=0;j--)
+ if (infc[j]) { infn[i]=infn[j]; break; }
}
- // draw
- g.clear(1).setFont("6x8",1);
- var r = require("graph").drawLine(g, infn, {
- x:4,y:24,
- width: g.getWidth()-24,
- height: g.getHeight()-(24+8),
- axes : true,
- gridy : grid,
- gridx : infn.length / 3,
- title: title,
- miny: min,
- maxy: max,
- xlabel : x=>Math.round(x*dur/(60*infn.length))+/*LANG*/" min" // minutes
- });
- g.setFont("6x8",2);
- g.setFontAlign(0,0,3);
- var isBTN3 = "BTN3" in global;
- g.drawString(/*LANG*/"Back",g.getWidth() - 10, isBTN3 ? (g.getHeight() - 40) : (g.getHeight()/2));
- setWatch(function() {
- viewTrack(info.filename, info);
- }, isBTN3?BTN3:BTN1);
- g.flip();
+ var n = infn[i];
+ if (n>max) max=n;
+ if (n 8) {
+ grid*=2;
+ }
+ // draw
+ g.clear(1).setFont("6x8",1);
+ var r = require("graph").drawLine(g, infn, {
+ x:4,y:24,
+ width: g.getWidth()-24,
+ height: g.getHeight()-(24+8),
+ axes : true,
+ gridy : grid,
+ gridx : infn.length / 3,
+ title: title,
+ miny: min,
+ maxy: max,
+ xlabel : x=>Math.round(x*dur/(60*infn.length))+/*LANG*/" min" // minutes
+ });
+ g.setFont("6x8",2);
+ g.setFontAlign(0,0,3);
+ var isBTN3 = "BTN3" in global;
+ g.drawString(/*LANG*/"Back",g.getWidth() - 10, isBTN3 ? (g.getHeight() - 40) : (g.getHeight()/2));
+ setWatch(function() {
+ viewTrack(info.filename, info);
+ }, isBTN3?BTN3:BTN1);
+ g.flip();
}
-showMainMenu();
+showMainMenu();
\ No newline at end of file
diff --git a/apps/recorder/metadata.json b/apps/recorder/metadata.json
index b0f42e1b4..a231a98e9 100644
--- a/apps/recorder/metadata.json
+++ b/apps/recorder/metadata.json
@@ -2,7 +2,7 @@
"id": "recorder",
"name": "Recorder",
"shortName": "Recorder",
- "version": "0.37",
+ "version": "0.38",
"description": "Record GPS position, heart rate and more in the background, then download to your PC.",
"icon": "app.png",
"tags": "tool,outdoors,gps,widget,clkinfo",
diff --git a/apps/setting/ChangeLog b/apps/setting/ChangeLog
index a8285c91f..2e6c76997 100644
--- a/apps/setting/ChangeLog
+++ b/apps/setting/ChangeLog
@@ -77,3 +77,4 @@ of 'Select Clock'
calibration was done.
0.67: Rename 'Wake on BTN1/Touch' to 'Wake on Button/Tap' on Bangle.js 2
0.68: Fix syntax error
+0.69: Add option to wake on double tap
\ No newline at end of file
diff --git a/apps/setting/metadata.json b/apps/setting/metadata.json
index d7c442fb6..1a850af83 100644
--- a/apps/setting/metadata.json
+++ b/apps/setting/metadata.json
@@ -1,7 +1,7 @@
{
"id": "setting",
"name": "Settings",
- "version": "0.68",
+ "version": "0.69",
"description": "A menu for setting up Bangle.js",
"icon": "settings.png",
"tags": "tool,system",
diff --git a/apps/setting/settings.js b/apps/setting/settings.js
index a259a7862..7726822f4 100644
--- a/apps/setting/settings.js
+++ b/apps/setting/settings.js
@@ -14,7 +14,7 @@ function updateOptions() {
var o = settings.options;
// Check to make sure nobody disabled all wakeups and locked themselves out!
if (BANGLEJS2) {
- if (!(o.wakeOnBTN1||o.wakeOnFaceUp||o.wakeOnTouch||o.wakeOnTwist)) {
+ if (!(o.wakeOnBTN1||o.wakeOnFaceUp||o.wakeOnTouch||o.wakeOnDoubleTap||o.wakeOnTwist)) {
o.wakeOnBTN1 = true;
}
} else {
@@ -451,48 +451,58 @@ function showLCDMenu() {
}
});
- if (BANGLEJS2)
+ if (BANGLEJS2) {
Object.assign(lcdMenu, {
/*LANG*/'Wake on Button': {
- value: settings.options.wakeOnBTN1,
+ value: !!settings.options.wakeOnBTN1,
onchange: () => {
settings.options.wakeOnBTN1 = !settings.options.wakeOnBTN1;
updateOptions();
}
},
/*LANG*/'Wake on Tap': {
- value: settings.options.wakeOnTouch,
+ value: !!settings.options.wakeOnTouch,
onchange: () => {
settings.options.wakeOnTouch = !settings.options.wakeOnTouch;
updateOptions();
}
}
});
- else
+ if (process.env.VERSION.replace("v",0)>=2020)
+ Object.assign(lcdMenu, {
+ /*LANG*/'Wake on Double Tap': {
+ value: !!settings.options.wakeOnDoubleTap,
+ onchange: () => {
+ settings.options.wakeOnDoubleTap = !settings.options.wakeOnDoubleTap;
+ updateOptions();
+ }
+ }
+ });
+ } else
Object.assign(lcdMenu, {
/*LANG*/'Wake on BTN1': {
- value: settings.options.wakeOnBTN1,
+ value: !!settings.options.wakeOnBTN1,
onchange: () => {
settings.options.wakeOnBTN1 = !settings.options.wakeOnBTN1;
updateOptions();
}
},
/*LANG*/'Wake on BTN2': {
- value: settings.options.wakeOnBTN2,
+ value: !!settings.options.wakeOnBTN2,
onchange: () => {
settings.options.wakeOnBTN2 = !settings.options.wakeOnBTN2;
updateOptions();
}
},
/*LANG*/'Wake on BTN3': {
- value: settings.options.wakeOnBTN3,
+ value: !!settings.options.wakeOnBTN3,
onchange: () => {
settings.options.wakeOnBTN3 = !settings.options.wakeOnBTN3;
updateOptions();
}
},
/*LANG*/'Wake on Touch': {
- value: settings.options.wakeOnTouch,
+ value: !!settings.options.wakeOnTouch,
onchange: () => {
settings.options.wakeOnTouch = !settings.options.wakeOnTouch;
updateOptions();
@@ -500,14 +510,14 @@ function showLCDMenu() {
}});
Object.assign(lcdMenu, {
/*LANG*/'Wake on FaceUp': {
- value: settings.options.wakeOnFaceUp,
+ value: !!settings.options.wakeOnFaceUp,
onchange: () => {
settings.options.wakeOnFaceUp = !settings.options.wakeOnFaceUp;
updateOptions();
}
},
/*LANG*/'Wake on Twist': {
- value: settings.options.wakeOnTwist,
+ value: !!settings.options.wakeOnTwist,
onchange: () => {
settings.options.wakeOnTwist = !settings.options.wakeOnTwist;
updateOptions();
diff --git a/apps/shadowclk/settings.js b/apps/shadowclk/settings.js
index 3fb774892..1472cb099 100644
--- a/apps/shadowclk/settings.js
+++ b/apps/shadowclk/settings.js
@@ -130,7 +130,6 @@
},
'Date Suffix:': {
value: appSettings.enableSuffix,
- format: v => v ? 'Yes' : 'No',
onchange: v => {
appSettings.enableSuffix = v;
writeSettings();
@@ -138,7 +137,6 @@
},
'Lead Zero:': {
value: appSettings.enableLeadingZero,
- format: v => v ? 'Yes' : 'No',
onchange: v => {
appSettings.enableLeadingZero = v;
writeSettings();
diff --git a/apps/synthwave/README.md b/apps/synthwave/README.md
new file mode 100644
index 000000000..9f92de33f
--- /dev/null
+++ b/apps/synthwave/README.md
@@ -0,0 +1,9 @@
+# Synthwave Watch
+
+Fly towards the sunset in a 3D jet, cruising to the sound of futuristic synthesizers*.
+
+  
+
+Theme colors and widgets supported. Widgets only appear when the screen is locked.
+
+* synthesizers not included
diff --git a/apps/synthwave/app-icon.js b/apps/synthwave/app-icon.js
new file mode 100644
index 000000000..5b46e62cf
--- /dev/null
+++ b/apps/synthwave/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEw4f/AoP///NjHvlGf/e4yMVzFf/cUqNHAQNFAQnVBAvPtu27fv2Vp1Wr12r3Vp00YsMEiEd/v3jv3798uIcBuMFEYtvEYP79++926AIPq9eq02atOGiEUqvf799+8RoscuPFikQEYlp0+uEYW6AQIFB12u3WqJQOaCgQeBAgPNmnTpoHBosEEYZlC3YmCSYP/3369YLBEYVBEoYgBEYQCBTQVBoo7BMoJEBEAPnz1//5QD3XrMoMUuYdCpl548dEYUc6P3+4TBMoX//1DhmSvIoBEYP69/+uPHIIhnB48cuhKE+47CMwXyEYMyvPnEYQOCWAIjD48avOGjFxBALhBu/XC4f//kwyVJkmTk4gCAQVfEYV9mOmjARB7lxcAQCCEAJoBvOMkgjBk88pMw/JKDCgP06dBoOECIMluFJkxuCEYQ7C/EaEYlK7OT8wOCNAcfeYIjCua9BqPx44OBaIdDzfACIOf82YtOmiFDEYcx48eot0ic161044jBocHEYI4B/3/+cY7MP1M/wEYEYMMz1//zLDpl9ilcq/sqHDg0evPHjpHC//tgkQpVI9EDmEwknDk4NB3/TrojC4sQEAJEBpOHzzaCI4IkC/XQgEGmFByZECBgX/CgXcqKJC4sWjBEBKYRrD73//P4tf2zEwIgOfEQYjDa4PFqtOuPHjhEDEYgVB9XyIAPnEAYLBa4vV6r+BDogCC6IqBDAP+12vDwX+XgQgCAQOu79Nj8UEA4CBJYIjBC4m/IIO/BAgCB9euuPNm5ELEYW6DQoCKqPHj/fo4aB5v0KAMcqP3EYf6LgP/HYJoC3Xr3369er3YjBEAQmB7l94/3793ml9osXuPFigaBEAW+34gBRIRoC1wFBC4PXudHfQNHj9d+v3JoPfI4MX3fu9w7BDQO/1eu3eu/XqEgPq91zoogB/vXrt3i9duvXqYvBqN17/7JAO7JQROC3XuEAIFB33u7oXBJQVR+omBEYIgBFgXX/Q+B9wdBxwjDAQPo9f6AoIA="))
diff --git a/apps/synthwave/app.js b/apps/synthwave/app.js
new file mode 100644
index 000000000..61a0a1e79
--- /dev/null
+++ b/apps/synthwave/app.js
@@ -0,0 +1,760 @@
+const gfx = E.compiledC(`
+// void init(int, int, int)
+// void tick(int)
+// void render(int)
+// void setCamera(int, int, int)
+// void bubble(int, int, int, int)
+
+unsigned char* fb;
+int stride;
+unsigned char* sint;
+
+const int near = 5 << 8;
+int f = 0;
+
+typedef struct {
+ int x, y, z;
+} Point;
+
+Point camera;
+Point rotation;
+Point scale;
+Point position;
+Point speed;
+
+const unsigned char ship[] = {
+0,38,25,10,3,8,6,10,7,3,6,13,3,11,5,13,1,12,3,15,3,5,8,15,1,3,7,13,12,11,3,15,5,6,8,15,6,1,7,10,5,0,6,10,0,1,6,12,5,11,4,12,12,1,2,12,2,11,12,12,10,5,4,13,5,10,0,12,2,1,9,13,9,1,0,12,4,11,2,10,19,22,21,12,4,2,10,12,10,2,9,10,13,16,15,13,10,9,0,15,21,20,19,15,15,14,13,15,19,20,22,15,13,14,16,15,21,23,20,15,15,17,14,15,22,20,23,10,22,24,21,15,16,14,17,10,16,18,15,15,24,23,21,15,18,17,15,15,22,23,24,15,16,17,18,0,0,62,236,243,244,247,0,234,0,229,194,11,0,234,21,243,246,0,234,33,193,250,20,63,249,19,249,4,3,9,4,3,7,247,222,250,247,222,240,0,22,238,13,22,226,1,20,229,7,62,225,11,20,208,27,62,19,0,20,22,12,20,33,0,18,30,5,60,34,10,18,52,26,60
+};
+
+const unsigned int terrainLength = 12;
+const unsigned int terrainWidth = 12;
+unsigned char terrain[terrainLength][terrainWidth];
+unsigned int _rngState;
+unsigned int rng() {
+ _rngState ^= _rngState << 17;
+ _rngState ^= _rngState >> 13;
+ _rngState ^= _rngState << 5;
+ return _rngState;
+}
+
+void shiftTerrain() {
+ for (int i = terrainLength - 1; i > 0; --i) {
+ for (int x = 0; x < terrainWidth; ++x)
+ terrain[i][x] = terrain[i-1][x];
+ }
+
+ for (int x = 0; x < terrainWidth; ++x)
+ terrain[0][x] = (int(terrain[0][x]) + ((rng() & 0x7F) + 0x7)) >> 1;
+ int mid = terrainWidth >> 1;
+ terrain[0][mid-1] >>= 1;
+ terrain[0][mid ] = 0;
+ terrain[0][mid+1] = 0;
+ terrain[0][mid+2] = 0;
+ terrain[0][mid+3] >>= 1;
+}
+
+void init(unsigned char* _fb, int _stride, unsigned char* _sint) {
+ fb = _fb;
+ stride = _stride;
+ sint = _sint;
+ _rngState = 1013904223;
+ for (int i = 0; i < terrainLength; ++i)
+ shiftTerrain();
+ speed.x = 0;
+ speed.y = 0;
+ speed.z = 0;
+ position.x = 100 << 8;
+ position.y = -150 << 8;
+ position.z = 100 << 8;
+ rotation.x = 0;
+ rotation.y = 256 << 8;
+ rotation.z = 0;
+ scale.x = 1 << 8;
+ scale.y = 1 << 8;
+ scale.z = 1 << 8;
+}
+
+int sin(int angle) {
+ int a = (angle >> 7) & 0xFF;
+ if (angle & (1 << 15))
+ a = 0xFF - a;
+ int v = sint[a];
+ if (angle & (1 << 16))
+ v = -v;
+ return v;
+}
+
+int cos(int angle) {
+ return sin(angle + 0x8000);
+}
+
+void setCamera(int x, int y, int z) {
+ camera.x = x;
+ camera.y = y;
+ camera.z = z;
+}
+
+unsigned int solid(unsigned int c) {
+ c &= 7;
+ c |= c << 3;
+ c |= c << 6;
+ c |= c << 12;
+ c |= c << 24;
+ return c;
+}
+
+unsigned int alternate(unsigned int a, unsigned int b) {
+ unsigned int c = (a & 7) | ((b & 7) << 3);
+ c |= c << 6;
+ c |= c << 12;
+ c |= c << 24;
+ return c;
+}
+
+void drawHLine(int x, unsigned int y, int l, unsigned int c) {
+ if (x < 0) {
+ l += x;
+ x = 0;
+ }
+ if (x + l >= 176) {
+ l = 176 - x;
+ }
+ if (l <= 0 || y >= 176)
+ return;
+
+ if (y & 1)
+ c = alternate(c >> 3, c);
+
+ int bitstart = x * 3;
+ int bitend = (x + l) * 3;
+ int wstart = bitstart >> 5;
+ int wend = bitend >> 5;
+ int padstart = bitstart & 31;
+ int padend = bitend & 31;
+ int maskstart = -1 << padstart;
+ int maskend = unsigned(-1) >> (32 - padend);
+ if (wstart == wend) {
+ maskstart &= maskend;
+ maskend = 0;
+ }
+
+ int* row = (int*) &fb[y * stride];
+ if (maskstart) {
+ row[wstart] = (row[wstart] & ~maskstart) | ((c << padstart) & maskstart);
+ while (bitstart >> 5 == wstart)
+ bitstart += 3;
+ }
+ if (maskend)
+ row[wend] = (row[wend] & ~maskend) |
+ (((c >> (30 - padend)) | (c >> (36 - padend))) & maskend);
+ bitend -= padend;
+ for (int x = bitstart; x < bitend; x += 10 * 3) {
+ unsigned int R = x & 31;
+ row[x >> 5] = (c << R) | (c >> (36 - R)) | (c >> (30 - R)) | (c << (R - 6));
+ }
+}
+
+void fillRect(int x, unsigned int y, int w, int h, unsigned int c) {
+ if (x < 0) {
+ w += x;
+ x = 0;
+ }
+ if (x + w >= 176) {
+ w = 176 - x;
+ }
+ if (w <= 0 || y >= 176)
+ return;
+
+ if (y < 0) {
+ h += y;
+ y = 0;
+ }
+ if (y + h >= 176) {
+ h = 176 - y;
+ }
+ if (h <= 0 || y >= 176)
+ return;
+
+ int bitstart = x * 3;
+ int bitend = (x + w) * 3;
+ int wstart = bitstart >> 5;
+ int wend = bitend >> 5;
+ int padstart = bitstart & 31;
+ int padend = bitend & 31;
+ int maskstart = -1 << padstart;
+ int maskend = unsigned(-1) >> (32 - padend);
+ if (wstart == wend) {
+ maskstart &= maskend;
+ maskend = 0;
+ }
+
+ int* row = (int*) &fb[y * stride];
+ if (maskstart) {
+ for (int i = 0; i < h; ++i)
+ row[wstart + (i*stride>>2)] = (row[wstart + (i*stride>>2)] & ~maskstart) | ((c << padstart) & maskstart);
+ while (bitstart >> 5 == wstart)
+ bitstart += 3;
+ }
+ if (maskend) {
+ for (int i = 0; i < h; ++i)
+ row[wend + (i*stride>>2)] = (row[wend + (i*stride>>2)] & ~maskend) |
+ (((c >> (30 - padend)) | (c >> (36 - padend))) & maskend);
+ }
+ bitend -= padend;
+ for (int x = bitstart; x < bitend; x += 10 * 3) {
+ unsigned int R = x & 31;
+ R = (c << R) | (c >> (36 - R)) | (c >> (30 - R)) | (c << (R - 6));
+ for (int i = 0; i < h; ++i)
+ row[(x >> 5) + (i*stride>>2)] = R;
+ }
+}
+
+void fillTriangle( int x0, int y0,
+ int x1, int y1,
+ int x2, int y2,
+ unsigned int col) {
+ int a, b, y, last, tmp;
+
+ a = 176;
+ b = 176;
+ if( x0 < 0 && x1 < 0 && x2 < 0 ) return;
+ if( x0 >= a && x1 > a && x2 > a ) return;
+ if( y0 < 0 && y1 < 0 && y2 < 0 ) return;
+ if( y0 >= b && y1 > b && y2 > b ) return;
+
+ // Sort coordinates by Y order (y2 >= y1 >= y0)
+ if (y0 > y1) {
+ tmp = y0; y0 = y1; y1 = tmp;
+ tmp = x0; x0 = x1; x1 = tmp;
+ }
+ if (y1 > y2) {
+ tmp = y2; y2 = y1; y1 = tmp;
+ tmp = x2; x2 = x1; x1 = tmp;
+ }
+ if (y0 > y1) {
+ tmp = y0; y0 = y1; y1 = tmp;
+ tmp = x0; x0 = x1; x1 = tmp;
+ }
+
+ if (y0 == y2) { // Handle awkward all-on-same-line case as its own thing
+ a = b = x0;
+ if (x1 < a) a = x1;
+ else if (x1 > b) b = x1;
+ if (x2 < a) a = x2;
+ else if (x2 > b) b = x2;
+ drawHLine(a, y0, b - a + 1, col);
+ return;
+ }
+
+ int dx01 = x1 - x0,
+ dx02 = x2 - x0,
+ dy02 = (1<<16) / (y2 - y0),
+ dx12 = x2 - x1,
+ sa = 0,
+ sb = 0;
+
+ // For upper part of triangle, find scanline crossings for segments
+ // 0-1 and 0-2. If y1=y2 (flat-bottomed triangle), the scanline y1
+ // is included here (and second loop will be skipped, avoiding a /0
+ // error there), otherwise scanline y1 is skipped here and handled
+ // in the second loop...which also avoids a /0 error here if y0=y1
+ // (flat-topped triangle).
+ if (y1 == y2) last = y1; // Include y1 scanline
+ else last = y1 - 1; // Skip it
+
+ y = y0;
+
+ if( y0 != y1 ){
+ int dy01 = (1<<16) / (y1 - y0);
+ for (y = y0; y <= last; y++) {
+ a = x0 + ((sa * dy01) >> 16);
+ b = x0 + ((sb * dy02) >> 16);
+ sa += dx01;
+ sb += dx02;
+ /* longhand:
+ a = x0 + (x1 - x0) * (y - y0) / (y1 - y0);
+ b = x0 + (x2 - x0) * (y - y0) / (y2 - y0);
+ */
+ if (a > b){
+ tmp = a;
+ a = b;
+ b = tmp;
+ }
+ drawHLine(a, y, b - a + 1, col);
+ }
+ }
+
+ // For lower part of triangle, find scanline crossings for segments
+ // 0-2 and 1-2. This loop is skipped if y1=y2.
+ if( y1 != y2 ){
+ int dy12 = (1<<16) / (y2 - y1);
+ sa = dx12 * (y - y1);
+ sb = dx02 * (y - y0);
+ for (; y <= y2; y++) {
+ a = x1 + ((sa * dy12) >> 16);
+ b = x0 + ((sb * dy02) >> 16);
+ sa += dx12;
+ sb += dx02;
+ if (a > b){
+ tmp = a;
+ a = b;
+ b = tmp;
+ }
+ drawHLine(a, y, b - a + 1, col);
+ }
+ }
+}
+
+void v_project(Point* p){
+ int fovz = ((90 << 16) / ((90 << 8) + p->z)); // 16:16 / 16:8 -> 16:8
+ p->x = (p->x * fovz >> 8) + (176/2 << 8); // 16:8 * 16:8 = 16:16 -> 16:8
+ p->y = (176/2 << 8) - (p->y * fovz >> 8);
+ p->z = fovz;
+}
+
+void drawTerrain() {
+ const int tileSize = 40 << 8;
+ camera.x = (terrainWidth + 2) * tileSize / 2;
+ camera.y = 60 << 8;
+ camera.z += 6 << 8;
+ if (camera.z > tileSize * 3) {
+ camera.z -= tileSize;
+ shiftTerrain();
+ }
+
+ int dist[] = {
+ solid(7),
+ solid(7),
+ alternate(5, 7),
+ alternate(5, 7),
+ solid(5),
+ solid(5),
+ alternate(5, 0),
+ solid(0)
+ };
+ int line = solid(5);
+
+ int fovz, fz;
+ int pz = (terrainLength) * tileSize - camera.z;
+ int prvz = ((90 << 16) / ((90 << 8) + pz)); // 16:16 / 16:8 = 16:8
+ for (int i = 0; i < terrainLength - 1; ++i, prvz = fovz, pz = fz) {
+ fz = (terrainLength - (i + 1)) * tileSize - camera.z;
+ fovz = ((90 << 16) / ((90 << 8) + fz)); // 16:16 / 16:8 = 16:8
+ int lum = i < 7 ? i : 7;
+ for (int x = 0; x < terrainWidth - 1; ++x) {
+ int ax = ((x ) * tileSize - camera.x) >> 8;
+ int bx = ((x + 1) * tileSize - camera.x) >> 8;
+ int cx = ((x ) * tileSize - camera.x) >> 8;
+ int dx = ((x + 1) * tileSize - camera.x) >> 8;
+
+ int ay = ((terrain[i ][x ] << 8) - camera.y) >> 8;
+ int by = ((terrain[i ][x + 1] << 8) - camera.y) >> 8;
+ int cy = ((terrain[i + 1][x ] << 8) - camera.y) >> 8;
+ int dy = ((terrain[i + 1][x + 1] << 8) - camera.y) >> 8;
+
+ int na = ((ax - bx)*(ay - cy) - (ay - by)*(ax - cx)) >> 8;
+ int nb = ((bx - dx)*(by - cy) - (by - dy)*(bx - cx)) >> 8;
+ int ca = lum - na;
+ int cb = lum - nb;
+
+ ax = 88 + (ax * prvz >> 8);
+ bx = 88 + (bx * prvz >> 8);
+ cx = 88 + (cx * fovz >> 8);
+ dx = 88 + (dx * fovz >> 8);
+ ay = 88 - (ay * prvz >> 8);
+ by = 88 - (by * prvz >> 8);
+ cy = 88 - (cy * fovz >> 8);
+ dy = 88 - (dy * fovz >> 8);
+
+ int av = (ax - bx)*(ay - cy) - (ay - by)*(ax - cx);
+ int bv = (bx - dx)*(by - cy) - (by - dy)*(bx - cx);
+
+ if (av > 0) {
+ if (ca < 0) ca = 0;
+ else if (ca >= 7) ca = 7;
+ if (ca >= 6 && x >= terrainWidth/2 && x < terrainWidth/2+2) ca = 6;
+ ca = dist[ca];
+ fillTriangle(ax, ay, bx, by, cx, cy, ca);
+ }
+
+ if (bv > 0) {
+ int hasLine = false;
+ if (cb < 0) cb = 0;
+ else if (cb >= 7) {
+ cb = 7;
+ hasLine = true;
+ }
+ if (cb >= 6 && x >= terrainWidth/2 && x < terrainWidth/2+2) {
+ cb = 6;
+ hasLine = true;
+ }
+ cb = dist[cb];
+ fillTriangle(bx, by, cx, cy, dx, dy, cb);
+ if (hasLine) {
+ fillTriangle(ax, ay, bx, by, bx, by - 1, line);
+ fillTriangle(ax, ay, cx, cy, cx, cy - 1, line);
+ }
+ }
+ }
+ }
+}
+
+void transform(Point* p) {
+ int x = p->x;
+ int y = p->y;
+ int z = p->z;
+ int s, c;
+
+ if (rotation.x) {
+ s = sin(rotation.x);
+ c = cos(rotation.x);
+ p->y = (y*c>>8) - (z*s>>8);
+ p->z = (y*s>>8) + (z*c>>8);
+ y = p->y;
+ z = p->z;
+ }
+
+ if (rotation.z) {
+ s = sin(rotation.z);
+ c = cos(rotation.z);
+ p->x = (x*c>>8) - (y*s>>8);
+ p->y = (x*s>>8) + (y*c>>8);
+ x = p->x;
+ y = p->y;
+ }
+
+ if (rotation.y) {
+ s = sin(rotation.y);
+ c = cos(rotation.y);
+ p->x = (x*c>>8) - (z*s>>8);
+ p->z = (x*s>>8) + (z*c>>8);
+ }
+
+// Scale
+ p->x = p->x * scale.x >> 8;
+ p->y = p->y * scale.y >> 8;
+ p->z = p->z * scale.z >> 8;
+
+// Translate
+ p->x += position.x;
+ p->y += position.y;
+ p->z += position.z;
+}
+
+void fillCircleInternal(int xc, int yc, int x, int y, int c) {
+ drawHLine(xc - x, yc - y, x * 2, c);
+ drawHLine(xc - x, yc + y, x * 2, c);
+ drawHLine(xc - y, yc - x, y * 2, c);
+ drawHLine(xc - y, yc + x, y * 2, c);
+}
+
+void fillCircle(int xc, int yc, int r, int color) {
+ if (r < 1 || xc + r < 0 || xc - r >= 176 || yc + r < 0 || yc - r >= 176)
+ return;
+ int x = 0, y = r;
+ int d = 3 - 2 * r;
+ fillCircleInternal(xc, yc, x, y, color);
+ while (y >= x) {
+ x++;
+ if (d > 0) {
+ y--;
+ d = d + 4 * (x - y) + 10;
+ } else {
+ d = d + 4 * x + 6;
+ }
+ fillCircleInternal(xc, yc, x, y, color);
+ }
+}
+
+void bubble(int x, int y, int r, int c) {
+ fillCircle(x, y, r + 3, alternate(7, 4));
+ fillCircle(x, y, r, alternate(c, 0));
+ int rs = r * 0xE666 >> 16;
+ int off = (r - rs) * 0x9696 >> 16;
+ fillCircle(x + off, y - off, rs, solid(c));
+ rs = r * 0x4CCC >> 16;
+ off = (r - rs) * 0x9696 >> 16;
+ fillCircle(x + off, y - off, rs, alternate(c, 7));
+ rs = r * 0x1999 >> 16;
+ off = (r - rs) * 0x8E38 >> 16;
+ fillCircle(x + off, y - off, rs, solid(7));
+}
+
+void render(const unsigned char* m){
+ if (position.z < near)
+ return;
+
+ if (!m)
+ m = ship;
+
+ int faceCount = (((int)m[0]) << 8) + (int)m[1];
+ const unsigned char* faceOffset = m + 3;
+ const unsigned char* vtxOffset = faceOffset + faceCount*4;
+
+ Point pointA, pointB, pointC;
+ Point* A = &pointA;
+ unsigned char* Ai = 0;
+ Point* B = &pointB;
+ unsigned char* Bi = 0;
+ Point* C = &pointC;
+ unsigned char* Ci = 0;
+ bool Ab, Bb, Cb;
+
+ for (int face = 0; face> 2) & 1;
+
+ const unsigned char* indexA = vtxOffset + ((int)*faceOffset++) * 3;
+ const unsigned char* indexB = vtxOffset + ((int)*faceOffset++) * 3;
+ const unsigned char* indexC = vtxOffset + ((int)*faceOffset++) * 3;
+
+ if( indexA == Ai ){ Ab = true; }
+ else if( indexA == Bi ){ A = &pointB; Bb = true; }
+ else if( indexA == Ci ){ A = &pointC; Cb = true; }
+ else A = 0;
+
+ if (indexB == Bi) { Bb = true; }
+ else if (indexB == Ai) { B = &pointA; Ab = true; }
+ else if (indexB == Ci) { B = &pointC; Cb = true; }
+ else B = 0;
+
+ if (indexC == Ci) { Cb = true; }
+ else if (indexC == Bi) { C = &pointB; Bb = true; }
+ else if (indexC == Ai) { C = &pointA; Ab = true; }
+ else C = 0;
+
+ if (!A) {
+ if (!Ab) { A = &pointA; Ab = true; }
+ else if (!Bb) { A = &pointB; Bb = true; }
+ else if (!Cb) { A = &pointC; Cb = true; }
+ A->x = ((signed char)*indexA++) << 8;
+ A->y = ((signed char)*indexA++) << 8;
+ A->z = ((signed char)*indexA) << 8;
+ transform(A);
+ if(A->z <= near) continue;
+ v_project(A);
+ }
+
+ if (!B) {
+ if (!Ab) { B = &pointA; Ab = true; }
+ else if (!Bb) { B = &pointB; Bb = true; }
+ else if (!Cb) { B = &pointC; Cb = true; }
+ B->x = ((signed char)*indexB++) << 8;
+ B->y = ((signed char)*indexB++) << 8;
+ B->z = ((signed char)*indexB) << 8;
+ transform(B);
+ if(B->z <= near) continue;
+ v_project(B);
+ }
+
+ if (!C) {
+ if (!Ab) { C = &pointA; Ab = true; }
+ else if (!Bb) { C = &pointB; Bb = true; }
+ else if (!Cb) { C = &pointC; Cb = true; }
+ C->x = ((signed char)*indexC++) << 8;
+ C->y = ((signed char)*indexC++) << 8;
+ C->z = ((signed char)*indexC) << 8;
+ transform(C);
+ if(C->z <= near) continue;
+ v_project(C);
+ }
+
+ int cross = (A->x - B->x)*(A->y - C->y) - (A->y - B->y)*(A->x - C->x);
+ if (cross < 0)
+ continue;
+
+ cross >>= 8;
+ int light = cross > (20000 << 3);
+ int dark = cross < (5000 << 2);
+
+ fillTriangle(
+ A->x >> 8, A->y >> 8,
+ B->x >> 8, B->y >> 8,
+ C->x >> 8, C->y >> 8,
+ light ? alternate(color, 7) :
+ dark ? alternate(color, 0) :
+ solid(color)
+ );
+ }
+}
+
+void tick(int c) {
+ c &= 7;
+ if (!c || c==7) {
+ c = solid(c);
+ unsigned short* cursor = (unsigned short*) fb;
+ for (int y = 0; y < 176; ++y) {
+ for (int x = 0; x < 66/2; ++x)
+ *cursor++ = c;
+ cursor++;
+ }
+ } else {
+ fillRect(0, 0, 176, 176, solid(c));
+ }
+
+
+ fillCircle(88, 110, 35, alternate(5,0));
+ fillCircle(88, 110, 27, alternate(5,7));
+ fillCircle(88, 110, 20, solid(7));
+ drawTerrain();
+
+ speed.x += ((position.x < 0) ? 1 : -1) << 8;
+ speed.y += ((position.y < (-80 << 8)) ? 1 : -1) << 8;
+ rotation.x = speed.y;
+ rotation.z = speed.x;
+ position.y += speed.y >> 1;
+ position.x += speed.x >> 1;
+
+ render(ship);
+}
+
+
+`);
+
+// font from 93dub
+var fontNum = atob("AAAAAAAAAAAAAA//8D//g//8P/+I//8//44//w//j4//A/+P4/8A/4/4AAAAD/4AAAAP/wAAAAf/gAAAA//AAAAB/+AAAAD/8AAAAH/4AAAAP/wAAAAf/gAAAA//AAAAB/+AAAAD/8AAAAH/wAAAAH/H/gH/H8f/gf/Hx//h//HH//n//Ef/+H//B//4H//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/wB/4AP/4H/4A//4f/4D//5//4P//h//4//+B//4AAAAAAAAAAAAAAAAAf/+AAAB//4gAAD//jgAAD/+PgABj/4/gAHj/j/gAfgAP/gA/AA//AB+AB/+AD8AD/8AH4AH/4APwAP/wAfgAf/gA/AA//AB+AB/+AD8AD/8AH4AH/4APwAP/wAfgAf/AA/AAf8f88AAfx/8wAAfH/8AAAcf/8AAAR//4AAAH//gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAA4AAAAAD4AAYAAP4AD8AA/4AH4AD/4APwAP/wAfgAf/gA/AA//AB+AB/+AD8AD/8AH4AH/4APwAP/wAfgAf/gA/AA//AB+AB/+AD8AD/8AH4AH/wAHgAH/H/GH/H8f/gf/Hx//h//HH//n//Ef/+H//B//4H//AAAAAAAAAAAAAAP//AAAAP//AAAAP//AAAAP/8AAAAP/2AAAAP/eAAAAAB+AAAAAD8AAAAAH4AAAAAPwAAAAAfgAAAAA/AAAAAB+AAAAAD8AAAAAH4AAAAAPwAAAAAfgAAAAA/AAAAAB+AAAAAD8AAAB/7x/4AH/7H/4Af/4f/4B//5//4H//h//4f/+B//4AAAAAAAAAAAAAD//wAAAD//wAAAj//gAADj/+AAAPj/5gAA/j/ngAD/gAfgAP/gA/AA//AB+AB/+AD8AD/8AH4AH/4APwAP/wAfgAf/gA/AA//AB+AB/+AD8AD/8AH4AH/4APwAP/wAfgAf/AA/AAf8AA8f8fwAAx/8fAAAH/8cAAAf/8QAAA//8AAAA//8AAAAAAAAAAAAAA//8D//g//8P/+I//8//44//0//j4//Y/+P4/94/4/4AH4AD/4APwAP/wAfgAf/gA/AA//AB+AB/+AD8AD/8AH4AH/4APwAP/wAfgAf/gA/AA//AB+AB/+AD8AD/8AH4AH/wAPwAH/AAPH/H8AAMf/HwAAB//HAAAH//EAAAH//AAAAH//AAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAGAAAAAAOAAAAAAeAAAAAA+AAAAAB+AAAAAD8AAAAAH4AAAAAPwAAAAAfgAAAAA/AAAAAB+AAAAAD8AAAAAH4AAAAAPwAAAAAfgAAAAA/AAAAAB8AAAAADx/4B/4HH/4H/4Mf/4f/4R//5//4H//h//4f/+B//4AAAAAAAAAAAAAD//wP/+D//w//4j//z//jj//T/+Pj/9j/4/j/3j/j/gAfgAP/gA/AA//AB+AB/+AD8AD/8AH4AH/4APwAP/wAfgAf/gA/AA//AB+AB/+AD8AD/8AH4AH/4APwAP/wAfgAf/AA/AAf8f+8f8fx/+x/8fH/+H/8cf/+f/8R//4f/8H//gf/8AAAAAAAAAAAAAA//8AAAA//8AAAI//8AAA4//0AAD4//YAAP4/94AA/4AH4AD/4APwAP/wAfgAf/gA/AA//AB+AB/+AD8AD/8AH4AH/4APwAP/wAfgAf/gA/AA//AB+AB/+AD8AD/8AH4AH/wAPwAH/H/vH/H8f/sf/Hx//h//HH//n//Ef/+H//B//4H//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
+
+const sintable = new Uint8Array(256);
+let bgColor = 0;
+const BLACK = g.setColor.bind(g, 0);
+const WHITE = g.setColor.bind(g, 0xFFFF);
+let lcdBuffer = 0,
+ start = 0;
+
+let locked = false;
+let charging = false;
+let stopped = true;
+let interval = 30;
+let timeout;
+
+function setupInterval(force) {
+ if (timeout)
+ clearTimeout(timeout);
+ let stop = locked && !charging;
+ timeout = setTimeout(setupInterval, stop ? 60000 : 60);
+ tick(stop && !force);
+ if (stop != stopped) {
+ stopped = stop;
+ let widget_utils = require("widget_utils");
+ if (stop) widget_utils.show();
+ else if (widget_utils.hide) widget_utils.hide();
+ }
+}
+
+function test(addr, y) {
+ BLACK().fillRect(0, y, 176, y);
+ if (peek8(addr)) return false;
+ WHITE().fillRect(0, y, 176, y);
+ let b = peek8(addr);
+ BLACK().fillRect(0, y, 176, y);
+ if (!b) return false;
+ return !peek8(addr);
+}
+
+function probe() {
+ if (!start) {
+ start = 0x20000000;
+ if (test(Bangle.getOptions().lcdBufferPtr, 0))
+ start = Bangle.getOptions().lcdBufferPtr; // FW=2v21
+ else if (test(0x2002d3fe, 0)) // try to skip loading if possible
+ start = 0x2002d3fe; // FW=2v20
+ }
+ const end = Math.min(start + 0x800, 0x20038000);
+
+ if (start >= end) {
+ print("Could not find framebuffer");
+ return;
+ }
+
+ BLACK().fillRect(0, 0, 176, 0);
+ // sampling every 64 bytes since a 176-pixel row is 66 bytes at 3bpp
+ for (; start < end; start += 64) {
+ if (peek8(start)) continue;
+ WHITE().fillRect(0, 0, 176, 0);
+ let b = peek8(start);
+ BLACK().fillRect(0, 0, 176, 0);
+ if (!b) continue;
+ if (!peek8(start)) break;
+ }
+
+ if (start >= end) {
+ setTimeout(probe, 1);
+ return;
+ }
+
+ // find the beginning of the row
+ while (test(start - 1, 0))
+ start--;
+
+ /*
+ let stride = (176 * 3 + 7) >> 3,
+ padding = 0;
+ for (let i = 0; i < 20; ++i, ++padding) {
+ if (test(start + stride + padding, 1)) {
+ break;
+ }
+ }
+
+ stride += padding;
+ if (padding == 20) {
+ print("Warning: Could not calculate padding");
+ stride = 68;
+ }
+ */
+ stride = 68;
+
+ lcdBuffer = start;
+ print('Found lcdBuffer at ' + lcdBuffer.toString(16) + ' stride=' + stride);
+ gfx.init(start, stride, E.getAddressOf(sintable, true));
+ gfx.setCamera(0, 0, 0);
+ setupInterval(true);
+}
+
+function init() {
+ require("Font5x9Numeric7Seg").add(Graphics);
+ g.setFont("5x9Numeric7Seg");
+ bgColor = g.theme.bg & 0x8410;
+ bgColor = ((bgColor >> 15) | (bgColor >> 9) | (bgColor >> 2));
+
+ g.clear();
+ g.setFontAlign(0, 0.5);
+ g.drawString("[LOADING]", 90, 66);
+
+ // setup sin/cos table
+ for (let i = 0; i < sintable.length; ++i)
+ sintable[i] = Math.sin((i * Math.PI * 0.5) / sintable.length) * ((1 << 8) - 1);
+ setTimeout(probe, 1);
+}
+
+function tick(locked) {
+ g.reset();
+
+ if (lcdBuffer && !locked) {
+ BLACK().drawRect(-1, -1, 0, 177); // dirty all the rows
+ gfx.tick(bgColor);
+ }
+
+ var d = new Date();
+ var h = d.getHours(), m = d.getMinutes();
+ g.setColor(locked ? g.theme.fg : g.toColor(1,0,1))
+ .setBgColor(g.theme.bg)
+ .setFontCustom(fontNum, 48, 28, 41)
+ .setFontAlign(-1, -1)
+ .drawString(("0" + h).substr(-2) + ("0"+m).substr(-2), 30, 30, true);
+}
+
+Bangle.setUI("clock");
+Bangle.loadWidgets();
+
+Bangle.on("lock", l => {
+ locked = l;
+ setupInterval();
+});
+
+Bangle.on('charging', c => {
+ charging = c;
+ setupInterval();
+});
+
+init();
diff --git a/apps/synthwave/app.png b/apps/synthwave/app.png
new file mode 100644
index 000000000..b85c1ac62
Binary files /dev/null and b/apps/synthwave/app.png differ
diff --git a/apps/synthwave/metadata.json b/apps/synthwave/metadata.json
new file mode 100644
index 000000000..8972e3faa
--- /dev/null
+++ b/apps/synthwave/metadata.json
@@ -0,0 +1,17 @@
+{
+ "id": "synthwave",
+ "name": "synthwave clock",
+ "version": "0.01",
+ "description": "A watchface with an animated 3D scene.",
+ "readme": "README.md",
+ "icon": "app.png",
+ "screenshots": [{"url":"screenshot.png"}, {"url":"theme.png"}, {"url":"widgets.png"}],
+ "type": "clock",
+ "tags": "clock",
+ "supports": ["BANGLEJS2"],
+ "allow_emulator": false,
+ "storage": [
+ {"name":"synthwave.app.js","url":"app.js"},
+ {"name":"synthwave.img","url":"app-icon.js","evaluate":true}
+ ]
+}
diff --git a/apps/synthwave/screenshot.png b/apps/synthwave/screenshot.png
new file mode 100644
index 000000000..e8697bd0a
Binary files /dev/null and b/apps/synthwave/screenshot.png differ
diff --git a/apps/synthwave/theme.png b/apps/synthwave/theme.png
new file mode 100644
index 000000000..1012af3ec
Binary files /dev/null and b/apps/synthwave/theme.png differ
diff --git a/apps/synthwave/widgets.png b/apps/synthwave/widgets.png
new file mode 100644
index 000000000..0b41c92ec
Binary files /dev/null and b/apps/synthwave/widgets.png differ
diff --git a/apps/warpdrive/README.md b/apps/warpdrive/README.md
new file mode 100644
index 000000000..0f596a500
--- /dev/null
+++ b/apps/warpdrive/README.md
@@ -0,0 +1,9 @@
+# WarpDrive
+
+An animated watchface featuring 3D spaceships traveling just shy of ludicrous speed.
+
+
+
+WE BREAK FOR NOBODY.
+
+Theme colors and widgets supported. Widgets only appear when the screen is locked.
\ No newline at end of file
diff --git a/apps/warpdrive/app-icon.js b/apps/warpdrive/app-icon.js
new file mode 100644
index 000000000..1ac6aa67d
--- /dev/null
+++ b/apps/warpdrive/app-icon.js
@@ -0,0 +1 @@
+atob("MDDD/wAA//+nMRF8r3PznG1znfdmmccUUUUUMMQQQQQQQQUdFFc8/66rKKLBBBBBBBBBBB6zyy+mnFcUUUUUMMQQQQMQMddFFdk3266rLKLLBBBBBBBBK6y6x/7FddcUUUUUMMMQQQQMcdUdd9e66q6rLJqLBBBBBBBSpyqze65dcUccUUUUUMMMMMNdccdd9ce66pqqppqLBBBBBBOp6qu/6qpcccccccUUUMMOsOccEdFlcUW5q66ppprLJBBDDWyr64866rIcUcccUUUUUMMUXfEGkGlddUWpqJ6pppqLLhCjO26u4867LLIcdFlFl0UUUXEMVdEEGndcUUVpjKs30330303ra1Ng8327LLIUW0VlnF9FEXcEEQUOnHdcUUVppppA0033u77sxBBA036pqrIUUUUUWHHHFHFHUYQUdENcEcVpppppBL373swzBBLB3rK4irAUUUUUYYGHGHEMMYUUcMMEdcVp5pprBDBWc2hBBBBLbC7Ba6ocUUUUMUYGmEUYQMUUMUUMFddpppprK5A82rDDBDBKLLDDK64UUcefEQGlcMMMQMQUcUMMVFdppp/2pA36BBBBDJDKpppjDKsUcedHccYQQQYUUUUFccUUVcdpr+38rBBBBBBDJBDBJ3KJqK4UEVelcYUYUQUQUMYYcdcdcUVoprc6rK6hJLLBDBBBBCqqqKIEUWkUUenUUVdUMYUUQQQcUUWpqfrDK22rK4+rBDDJBC5BJpIUW8UUUddcfGnUFFEUMdccUUVqeBLKKbrLY82q6yuBCa66ppAW0UUUXcUUGnddddUUVcdFUUWeLDDLdrDI+7a66ypLJpyyrLC0UMUV8UUe8VccddUUUUdcUUOLBBCdrBDfC66J6rJJqKKyBBAUMMV0UMW0cccUcccUUUVUUMNBBCdqBBKKKprLJ1pqLDLDBBAMMeUUMMUUccUUW0UUUUMUMMRBD1qJBDDKJrLLfLLLBBDLDLAUHUUMMUUUUUUW0UUUUMUUUUNAqKJBBBDKLLLXLLDJBDBBDLAEUUUMQMUUUcVUUUUUMUMMMUQqLKBBBDLLK6rLDBBBBDDBBBAUUUMQMUUUfcUMMMQMMUUQQQSKLBBBBDLL2rBBBBBBBBBBBBAUUMMMMUUfcUMMMMQMMMMQQQTLBBBBBDL2rBBBBBBBhBBBBBA=")
diff --git a/apps/warpdrive/app.js b/apps/warpdrive/app.js
new file mode 100644
index 000000000..855b86c78
--- /dev/null
+++ b/apps/warpdrive/app.js
@@ -0,0 +1,702 @@
+const gfx = E.compiledC(`
+// void init(int, int, int)
+// void clear(int)
+// void render(int, int)
+// void setCamera(int, int, int)
+// void stars()
+
+unsigned char* fb;
+int stride;
+unsigned char* sint;
+
+const int near = 5 << 8;
+int f = 0;
+
+typedef struct {
+ int x, y, z;
+} Point;
+
+Point camera;
+Point rotation;
+Point scale;
+Point position;
+
+const unsigned char ship[] = {
+0,38,25,10,3,8,6,10,7,3,6,13,3,11,5,13,1,12,3,15,3,5,8,15,1,3,7,13,12,11,3,15,5,6,8,15,6,1,7,10,5,0,6,10,0,1,6,12,5,11,4,12,12,1,2,12,2,11,12,12,10,5,4,13,5,10,0,12,2,1,9,13,9,1,0,12,4,11,2,10,19,22,21,12,4,2,10,12,10,2,9,10,13,16,15,13,10,9,0,15,21,20,19,15,15,14,13,15,19,20,22,15,13,14,16,15,21,23,20,15,15,17,14,15,22,20,23,10,22,24,21,15,16,14,17,10,16,18,15,15,24,23,21,15,18,17,15,15,22,23,24,15,16,17,18,0,0,62,236,243,244,247,0,234,0,229,194,11,0,234,21,243,246,0,234,33,193,250,20,63,249,19,249,4,3,9,4,3,7,247,222,250,247,222,240,0,22,238,13,22,226,1,20,229,7,62,225,11,20,208,27,62,19,0,20,22,12,20,33,0,18,30,5,60,34,10,18,52,26,60
+};
+
+unsigned int _rngState;
+unsigned int rng() {
+ _rngState ^= _rngState << 17;
+ _rngState ^= _rngState >> 13;
+ _rngState ^= _rngState << 5;
+ return _rngState;
+}
+
+void init(unsigned char* _fb, int _stride, unsigned char* _sint) {
+ fb = _fb;
+ stride = _stride;
+ sint = _sint;
+}
+
+int sin(int angle) {
+ int a = (angle >> 7) & 0xFF;
+ if (angle & (1 << 15))
+ a = 0xFF - a;
+ int v = sint[a];
+ if (angle & (1 << 16))
+ v = -v;
+ return v;
+}
+
+int cos(int angle) {
+ return sin(angle + 0x8000);
+}
+
+void setCamera(int x, int y, int z) {
+ camera.x = x;
+ camera.y = y;
+ camera.z = z;
+}
+
+unsigned int solid(unsigned int c) {
+ c &= 7;
+ c |= c << 3;
+ c |= c << 6;
+ c |= c << 12;
+ c |= c << 24;
+ return c;
+}
+
+unsigned int alternate(unsigned int a, unsigned int b) {
+ unsigned int c = (a & 7) | ((b & 7) << 3);
+ c |= c << 6;
+ c |= c << 12;
+ c |= c << 24;
+ return c;
+}
+
+void drawHLine(int x, unsigned int y, int l, unsigned int c) {
+ if (x < 0) {
+ l += x;
+ x = 0;
+ }
+ if (x + l >= 176) {
+ l = 176 - x;
+ }
+ if (l <= 0 || y >= 176)
+ return;
+
+ if (y & 1)
+ c = alternate(c >> 3, c);
+
+ int bitstart = x * 3;
+ int bitend = (x + l) * 3;
+ int wstart = bitstart >> 5;
+ int wend = bitend >> 5;
+ int padstart = bitstart & 31;
+ int padend = bitend & 31;
+ int maskstart = -1 << padstart;
+ int maskend = unsigned(-1) >> (32 - padend);
+ if (wstart == wend) {
+ maskstart &= maskend;
+ maskend = 0;
+ }
+
+ int* row = (int*) &fb[y * stride];
+ if (maskstart) {
+ row[wstart] = (row[wstart] & ~maskstart) | ((c << padstart) & maskstart);
+ while (bitstart >> 5 == wstart)
+ bitstart += 3;
+ }
+ if (maskend)
+ row[wend] = (row[wend] & ~maskend) |
+ (((c >> (30 - padend)) | (c >> (36 - padend))) & maskend);
+ bitend -= padend;
+ for (int x = bitstart; x < bitend; x += 10 * 3) {
+ unsigned int R = x & 31;
+ row[x >> 5] = (c << R) | (c >> (36 - R)) | (c >> (30 - R)) | (c << (R - 6));
+ }
+}
+
+void fillRect(int x, unsigned int y, int w, int h, unsigned int c) {
+ if (x < 0) {
+ w += x;
+ x = 0;
+ }
+ if (x + w >= 176) {
+ w = 176 - x;
+ }
+ if (w <= 0 || y >= 176)
+ return;
+
+ if (y < 0) {
+ h += y;
+ y = 0;
+ }
+ if (y + h >= 176) {
+ h = 176 - y;
+ }
+ if (h <= 0 || y >= 176)
+ return;
+
+ int bitstart = x * 3;
+ int bitend = (x + w) * 3;
+ int wstart = bitstart >> 5;
+ int wend = bitend >> 5;
+ int padstart = bitstart & 31;
+ int padend = bitend & 31;
+ int maskstart = -1 << padstart;
+ int maskend = unsigned(-1) >> (32 - padend);
+ if (wstart == wend) {
+ maskstart &= maskend;
+ maskend = 0;
+ }
+
+ int* row = (int*) &fb[y * stride];
+ if (maskstart) {
+ for (int i = 0; i < h; ++i)
+ row[wstart + (i*stride>>2)] = (row[wstart + (i*stride>>2)] & ~maskstart) | ((c << padstart) & maskstart);
+ while (bitstart >> 5 == wstart)
+ bitstart += 3;
+ }
+ if (maskend) {
+ for (int i = 0; i < h; ++i)
+ row[wend + (i*stride>>2)] = (row[wend + (i*stride>>2)] & ~maskend) |
+ (((c >> (30 - padend)) | (c >> (36 - padend))) & maskend);
+ }
+ bitend -= padend;
+ for (int x = bitstart; x < bitend; x += 10 * 3) {
+ unsigned int R = x & 31;
+ R = (c << R) | (c >> (36 - R)) | (c >> (30 - R)) | (c << (R - 6));
+ for (int i = 0; i < h; ++i)
+ row[(x >> 5) + (i*stride>>2)] = R;
+ }
+}
+
+void clear(int c) {
+ c &= 7;
+ if (!c || c==7) {
+ c = solid(c);
+ unsigned short* cursor = (unsigned short*) fb;
+ for (int y = 0; y < 176; ++y) {
+ for (int x = 0; x < 66/2; ++x)
+ *cursor++ = c;
+ cursor++;
+ }
+ } else {
+ fillRect(0, 0, 176, 176, solid(c));
+ }
+}
+
+void fillTriangle( int x0, int y0,
+ int x1, int y1,
+ int x2, int y2,
+ unsigned int col) {
+ int a, b, y, last, tmp;
+
+ a = 176;
+ b = 176;
+ if( x0 < 0 && x1 < 0 && x2 < 0 ) return;
+ if( x0 >= a && x1 > a && x2 > a ) return;
+ if( y0 < 0 && y1 < 0 && y2 < 0 ) return;
+ if( y0 >= b && y1 > b && y2 > b ) return;
+
+ // Sort coordinates by Y order (y2 >= y1 >= y0)
+ if (y0 > y1) {
+ tmp = y0; y0 = y1; y1 = tmp;
+ tmp = x0; x0 = x1; x1 = tmp;
+ }
+ if (y1 > y2) {
+ tmp = y2; y2 = y1; y1 = tmp;
+ tmp = x2; x2 = x1; x1 = tmp;
+ }
+ if (y0 > y1) {
+ tmp = y0; y0 = y1; y1 = tmp;
+ tmp = x0; x0 = x1; x1 = tmp;
+ }
+
+ if (y0 == y2) { // Handle awkward all-on-same-line case as its own thing
+ a = b = x0;
+ if (x1 < a) a = x1;
+ else if (x1 > b) b = x1;
+ if (x2 < a) a = x2;
+ else if (x2 > b) b = x2;
+ drawHLine(a, y0, b - a + 1, col);
+ return;
+ }
+
+ int dx01 = x1 - x0,
+ dx02 = x2 - x0,
+ dy02 = (1<<16) / (y2 - y0),
+ dx12 = x2 - x1,
+ sa = 0,
+ sb = 0;
+
+ // For upper part of triangle, find scanline crossings for segments
+ // 0-1 and 0-2. If y1=y2 (flat-bottomed triangle), the scanline y1
+ // is included here (and second loop will be skipped, avoiding a /0
+ // error there), otherwise scanline y1 is skipped here and handled
+ // in the second loop...which also avoids a /0 error here if y0=y1
+ // (flat-topped triangle).
+ if (y1 == y2) last = y1; // Include y1 scanline
+ else last = y1 - 1; // Skip it
+
+ y = y0;
+
+ if( y0 != y1 ){
+ int dy01 = (1<<16) / (y1 - y0);
+ for (y = y0; y <= last; y++) {
+ a = x0 + ((sa * dy01) >> 16);
+ b = x0 + ((sb * dy02) >> 16);
+ sa += dx01;
+ sb += dx02;
+ /* longhand:
+ a = x0 + (x1 - x0) * (y - y0) / (y1 - y0);
+ b = x0 + (x2 - x0) * (y - y0) / (y2 - y0);
+ */
+ if (a > b){
+ tmp = a;
+ a = b;
+ b = tmp;
+ }
+ drawHLine(a, y, b - a + 1, col);
+ }
+ }
+
+ // For lower part of triangle, find scanline crossings for segments
+ // 0-2 and 1-2. This loop is skipped if y1=y2.
+ if( y1 != y2 ){
+ int dy12 = (1<<16) / (y2 - y1);
+ sa = dx12 * (y - y1);
+ sb = dx02 * (y - y0);
+ for (; y <= y2; y++) {
+ a = x1 + ((sa * dy12) >> 16);
+ b = x0 + ((sb * dy02) >> 16);
+ sa += dx12;
+ sb += dx02;
+ if (a > b){
+ tmp = a;
+ a = b;
+ b = tmp;
+ }
+ drawHLine(a, y, b - a + 1, col);
+ }
+ }
+}
+
+void v_project(Point* p){
+ int fovz = ((90 << 16) / ((90 << 8) + p->z)); // 16:8 / 16:8 -> 16:8
+ p->x = (p->x * fovz >> 8) + (176/2 << 8); // 16:8 * 16:8 = 16:16 -> 16:8
+ p->y = (176/2 << 8) - (p->y * fovz >> 8);
+ p->z = fovz;
+}
+
+void stars() {
+ f += 7;
+ _rngState = 1013904223;
+
+ for (int i = 0; i < 100; ++i) {
+ int a = rng() + ((i & 1 ? f : -f) << 7);
+ int ca = cos(a);
+ int sa = sin(a);
+ int r = ((rng() & 0xFF) + 0xFF);
+ position.x = r*ca;
+ position.y = r*sa;
+ position.z = 0xFF - ((rng() + f) & 0xFF);
+ position.z <<= 12;
+ position.z -= 100 << 8;
+ int light = position.z < (800 << 8);
+ int dark = position.z > ((800 + 500) << 8);
+ scale = position;
+
+ v_project(&position);
+ int s = 32 * position.z >> 8;
+ if (!s)
+ continue;
+
+ scale.z += 30 << 10;
+ v_project(&scale);
+ int rx = s*sa >> 8;
+ int ry = s*ca >> 8;
+
+ position.x >>= 8;
+ position.y >>= 8;
+ scale.x >>= 8;
+ scale.y >>= 8;
+
+ if (position.x < - 100 || position.x > 276) continue;
+ if (position.y < - 100 || position.y > 276) continue;
+ int color = 4 | (i & 1);
+ fillTriangle(
+ scale.x, scale.y,
+ position.x - rx, position.y - ry,
+ position.x + rx, position.y + ry,
+ light ? alternate(color, 7) :
+ dark ? alternate(color, 0) :
+ solid(color)
+ );
+ }
+}
+
+void transform(Point* p) {
+ int x = p->x;
+ int y = p->y;
+ int z = p->z;
+ int s, c;
+ if (rotation.z) {
+ s = sin(rotation.z);
+ c = cos(rotation.z);
+ p->x = (x*c>>8) - (y*s>>8);
+ p->y = (x*s>>8) + (y*c>>8);
+ x = p->x;
+ y = p->y;
+ }
+
+ if (rotation.y) {
+ s = sin(rotation.y);
+ c = cos(rotation.y);
+ p->x = (x*c>>8) - (z*s>>8);
+ p->z = (x*s>>8) + (z*c>>8);
+ }
+
+// Scale
+ p->x = p->x * scale.x >> 8;
+ p->y = p->y * scale.y >> 8;
+ p->z = p->z * scale.z >> 8;
+
+// Translate
+ p->x += position.x;
+ p->y += position.y;
+ p->z += position.z;
+}
+
+void render(int* n, const unsigned char* m){
+ rotation.x = n[0];
+ rotation.y = n[1];
+ rotation.z = n[2];
+ scale.x = n[3];
+ scale.y = n[4];
+ scale.z = n[5];
+ position.x = n[6] - camera.x;
+ position.y = n[7] - camera.y;
+ position.z = n[8] - camera.z;
+ unsigned char tint = n[9];
+
+ if (position.z < near)
+ return;
+
+ if (!m)
+ m = ship;
+
+ int light = position.z < (800 << 8);
+ int dark = position.z > ((800 + 500) << 8);
+
+ int faceCount = (((int)m[0]) << 8) + (int)m[1];
+ // int vtxCount = m[2];
+ const unsigned char* faceOffset = m + 3;
+ const unsigned char* vtxOffset = faceOffset + faceCount*4;
+
+ Point pointA, pointB, pointC;
+ Point* A = &pointA;
+ unsigned char* Ai = 0;
+ Point* B = &pointB;
+ unsigned char* Bi = 0;
+ Point* C = &pointC;
+ unsigned char* Ci = 0;
+ bool Ab, Bb, Cb;
+
+ for (int face = 0; facex = ((signed char)*indexA++) << 8;
+ A->y = ((signed char)*indexA++) << 8;
+ A->z = ((signed char)*indexA) << 8;
+ transform(A);
+ if(A->z <= near) continue;
+ v_project(A);
+ }
+
+ if (!B) {
+ if (!Ab) { B = &pointA; Ab = true; }
+ else if (!Bb) { B = &pointB; Bb = true; }
+ else if (!Cb) { B = &pointC; Cb = true; }
+ B->x = ((signed char)*indexB++) << 8;
+ B->y = ((signed char)*indexB++) << 8;
+ B->z = ((signed char)*indexB) << 8;
+ transform(B);
+ if(B->z <= near) continue;
+ v_project(B);
+ }
+
+ if (!C) {
+ if (!Ab) { C = &pointA; Ab = true; }
+ else if (!Bb) { C = &pointB; Bb = true; }
+ else if (!Cb) { C = &pointC; Cb = true; }
+ C->x = ((signed char)*indexC++) << 8;
+ C->y = ((signed char)*indexC++) << 8;
+ C->z = ((signed char)*indexC) << 8;
+ transform(C);
+ if(C->z <= near) continue;
+ v_project(C);
+ }
+
+ if (((A->x - B->x) >> 8)*((A->y - C->y) >> 8) -
+ ((A->y - B->y) >> 8)*((A->x - C->x) >> 8) < 0)
+ continue;
+
+ fillTriangle(
+ A->x >> 8, A->y >> 8,
+ B->x >> 8, B->y >> 8,
+ C->x >> 8, C->y >> 8,
+ light ? alternate(color, 7) :
+ dark ? alternate(color, 0) :
+ solid(color)
+ );
+ }
+}
+
+`);
+
+const nodeCount = 4;
+const nodes = new Array(nodeCount);
+const sintable = new Uint8Array(256);
+const translation = new Uint32Array(10);
+let bgColor = 0;
+const BLACK = g.setColor.bind(g, 0);
+const WHITE = g.setColor.bind(g, 0xFFFF);
+let lcdBuffer = 0,
+ start = 0;
+
+let locked = false;
+let charging = false;
+let stopped = true;
+let interval = 30;
+let timeout;
+
+function setupInterval(force) {
+ if (timeout)
+ clearTimeout(timeout);
+ let stop = locked && !charging;
+ timeout = setTimeout(setupInterval, stop ? 60000 : 60);
+ tick(stop && !force);
+ if (stop != stopped) {
+ stopped = stop;
+ let widget_utils = require("widget_utils");
+ if (stop) widget_utils.show();
+ else if (widget_utils.hide) widget_utils.hide();
+ }
+}
+
+function test(addr, y) {
+ BLACK().fillRect(0, y, 176, y);
+ if (peek8(addr)) return false;
+ WHITE().fillRect(0, y, 176, y);
+ let b = peek8(addr);
+ BLACK().fillRect(0, y, 176, y);
+ if (!b) return false;
+ return !peek8(addr);
+}
+
+function probe() {
+ if (!start) {
+ start = 0x20000000;
+ if (test(Bangle.getOptions().lcdBufferPtr, 0))
+ start = Bangle.getOptions().lcdBufferPtr; // FW=2v21
+ else if (test(0x2002d3fe, 0)) // try to skip loading if possible
+ start = 0x2002d3fe; // FW=2v20
+ }
+ const end = Math.min(start + 0x800, 0x20038000);
+
+ if (start >= end) {
+ print("Could not find framebuffer");
+ return;
+ }
+
+ BLACK().fillRect(0, 0, 176, 0);
+ // sampling every 64 bytes since a 176-pixel row is 66 bytes at 3bpp
+ for (; start < end; start += 64) {
+ if (peek8(start)) continue;
+ WHITE().fillRect(0, 0, 176, 0);
+ let b = peek8(start);
+ BLACK().fillRect(0, 0, 176, 0);
+ if (!b) continue;
+ if (!peek8(start)) break;
+ }
+
+ if (start >= end) {
+ setTimeout(probe, 1);
+ return;
+ }
+
+ // find the beginning of the row
+ while (test(start - 1, 0))
+ start--;
+
+ /*
+ let stride = (176 * 3 + 7) >> 3,
+ padding = 0;
+ for (let i = 0; i < 20; ++i, ++padding) {
+ if (test(start + stride + padding, 1)) {
+ break;
+ }
+ }
+
+ stride += padding;
+ if (padding == 20) {
+ print("Warning: Could not calculate padding");
+ stride = 68;
+ }
+ */
+ stride = 68;
+
+ lcdBuffer = start;
+ print('Found lcdBuffer at ' + lcdBuffer.toString(16) + ' stride=' + stride);
+ gfx.init(start, stride, E.getAddressOf(sintable, true));
+ gfx.setCamera(0, 0, -300 << 8);
+ setupInterval(true);
+}
+
+function init() {
+ bgColor = g.theme.bg & 0x8410;
+ bgColor = ((bgColor >> 15) | (bgColor >> 9) | (bgColor >> 2));
+
+ g.clear();
+ g.setFont('6x8', 2);
+ g.setFontAlign(0, 0.5);
+ g.drawString("[LOADING]", 90, 66);
+
+ // setup sin/cos table
+ for (let i = 0; i < sintable.length; ++i)
+ sintable[i] = Math.sin((i * Math.PI * 0.5) / sintable.length) * ((1 << 8) - 1);
+
+ // setup nodes
+ let o = 0;
+ for (let i = 0; i < nodeCount; ++i) {
+ nodes[i] = {
+ rx: 0,
+ ry: 256,
+ rz: 0,
+ sx: 4,
+ sy: 4,
+ sz: 4,
+ vx: Math.random() * 20 - 10,
+ vy: Math.random() * 20 - 10,
+ vz: Math.random() * 5 - 2.5,
+ x: Math.random() * 2000 - 1000,
+ y: Math.random() * 2000 - 1000,
+ z: i * 500 + 500,
+ c: i
+ };
+ }
+ setTimeout(probe, 1);
+}
+
+function updateNode(index) {
+ let o = nodes[index];
+ let x = o.x;
+ let y = o.y;
+ let z = o.z;
+ let tz = index * 500 + 500;
+ o.vx += (x < 0) * 10 - 5;
+ o.vy += (y < 0) * 10 - 5;
+ o.vz += (z < tz) * 1 - 0.5;
+ // lean into the curve
+ o.rz = o.vx * 0.5;
+
+ x += o.vx;
+ y += o.vy;
+ z += o.vz;
+
+ o.x = x;
+ o.y = y;
+ o.z = z;
+
+ // iterative bubble sort
+ let p = nodes[index - 1];
+ if (p && z > p.z) {
+ nodes[index - 1] = o;
+ nodes[index] = p;
+ }
+}
+
+function drawNode(index) {
+ let o = nodes[index];
+ let i = 0;
+ // float to 23.8 fixed
+ translation[i++] = o.rx * 256;
+ translation[i++] = o.ry * 256;
+ translation[i++] = o.rz * 256;
+ translation[i++] = o.sx * 256;
+ translation[i++] = o.sy * 256;
+ translation[i++] = o.sz * 256;
+ translation[i++] = o.x * 256;
+ translation[i++] = o.y * 256;
+ translation[i++] = o.z * 256;
+ translation[i++] = o.c;
+ gfx.render(E.getAddressOf(translation, true));
+}
+
+function tick(locked) {
+ g.reset();
+
+ if (lcdBuffer && !locked) {
+ BLACK().drawRect(-1, -1, 0, 177); // dirty all the rows
+ gfx.clear(bgColor);
+ gfx.stars();
+ for (let i = 0; i < nodeCount; ++i)
+ updateNode(i);
+ for (let i = 0; i < nodeCount; ++i)
+ drawNode(i);
+ }
+
+ var d = new Date();
+ var h = d.getHours(),
+ m = d.getMinutes();
+ var time = (" " + h).substr(-2) + ":" + m.toString().padStart(2, 0);
+ g.setColor(g.theme.fg)
+ .setBgColor(g.theme.bg)
+ .setFontAlign(0, 0.5)
+ .setFont('6x8', 2)
+ .drawString(time, 176 / 2, 176 - 16, true);
+}
+
+Bangle.setUI("clock");
+Bangle.loadWidgets();
+
+Bangle.on("lock", l => {
+ locked = l;
+ setupInterval();
+});
+
+Bangle.on('charging', c => {
+ charging = c;
+ setupInterval();
+});
+
+init();
diff --git a/apps/warpdrive/app.png b/apps/warpdrive/app.png
new file mode 100644
index 000000000..ceef96912
Binary files /dev/null and b/apps/warpdrive/app.png differ
diff --git a/apps/warpdrive/metadata.json b/apps/warpdrive/metadata.json
new file mode 100644
index 000000000..c4fa1277e
--- /dev/null
+++ b/apps/warpdrive/metadata.json
@@ -0,0 +1,17 @@
+{
+ "id": "warpdrive",
+ "name": "warpdrive clock",
+ "version": "0.01",
+ "description": "A watchface with an animated 3D scene.",
+ "readme": "README.md",
+ "icon": "app.png",
+ "screenshots": [{"url":"screenshot.png"}],
+ "type": "clock",
+ "tags": "clock",
+ "supports": ["BANGLEJS2"],
+ "allow_emulator": false,
+ "storage": [
+ {"name":"warpdrive.app.js","url":"app.js"},
+ {"name":"warpdrive.img","url":"app-icon.js","evaluate":true}
+ ]
+}
diff --git a/apps/warpdrive/screenshot.png b/apps/warpdrive/screenshot.png
new file mode 100644
index 000000000..110642499
Binary files /dev/null and b/apps/warpdrive/screenshot.png differ
diff --git a/apps/warpdrive/warpdrive.gif b/apps/warpdrive/warpdrive.gif
new file mode 100644
index 000000000..514edb249
Binary files /dev/null and b/apps/warpdrive/warpdrive.gif differ
diff --git a/apps/welcome/settings.js b/apps/welcome/settings.js
index 27a322c7f..aa374694d 100644
--- a/apps/welcome/settings.js
+++ b/apps/welcome/settings.js
@@ -5,7 +5,6 @@
'': { 'title': 'Welcome App' },
'Run next boot': {
value: !settings.welcomed,
- format: v => v ? 'Yes' : 'No',
onchange: v => require('Storage').write('welcome.json', {welcomed: !v}),
},
'Run Now': () => load('welcome.app.js'),
diff --git a/bin/sanitycheck.js b/bin/sanitycheck.js
index 4e6662e4a..78cadc34c 100755
--- a/bin/sanitycheck.js
+++ b/bin/sanitycheck.js
@@ -263,7 +263,7 @@ apps.forEach((app,appIdx) => {
WARN(`App ${app.id} has a setting file but no corresponding data entry (add \`"data":[{"name":"${app.id}.settings.json"}]\`)`, {file:appDirRelative+file.url});
}
// check for manual boolean formatter
- const m = fileContents.match(/format: *\(\) *=>.*["'](yes|on)["']/i);
+ const m = fileContents.match(/format: *\(?\w*\)? *=>.*["'](yes|on)["']/i);
if (m) {
WARN(`Settings for ${app.id} has a boolean formatter - this is handled automatically, the line can be removed`, {file:appDirRelative+file.url, line: fileContents.substr(0, m.index).split("\n").length});
}
diff --git a/core b/core
index 364b2c1b0..bd301be33 160000
--- a/core
+++ b/core
@@ -1 +1 @@
-Subproject commit 364b2c1b00de17ffbbee87fb1d91e79b513b9127
+Subproject commit bd301be3324775a8f464328ba9e34f750d503a2b
diff --git a/loader.js b/loader.js
index 28ff540eb..81e3433d4 100644
--- a/loader.js
+++ b/loader.js
@@ -16,7 +16,7 @@ if (window.location.host=="banglejs.com") {
'This is not the official Bangle.js App Loader - you can try the Official Version here.';
}
-var RECOMMENDED_VERSION = "2v20";
+var RECOMMENDED_VERSION = "2v21";
// could check http://www.espruino.com/json/BANGLEJS.json for this
// We're only interested in Bangles