Merge branch 'espruino:master' into master
|
|
@ -1 +1 @@
|
||||||
0.01: New Widget!
|
0.01: New Clock Info!
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
0.01: New Clock!
|
||||||
|
|
@ -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
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
require("heatshrink").decompress(atob("mEwwJC/AH4A/AH4AgA=="))
|
||||||
|
|
@ -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();
|
||||||
|
After Width: | Height: | Size: 1.6 KiB |
|
|
@ -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}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -32,3 +32,4 @@
|
||||||
Allow alarm enable/disable
|
Allow alarm enable/disable
|
||||||
0.31: Implement API for activity fetching
|
0.31: Implement API for activity fetching
|
||||||
0.32: Added support for loyalty cards from gadgetbridge
|
0.32: Added support for loyalty cards from gadgetbridge
|
||||||
|
0.33: Fix alarms created in Gadgetbridge not repeating
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,12 @@
|
||||||
for (var j = 0; j < event.d.length; j++) {
|
for (var j = 0; j < event.d.length; j++) {
|
||||||
// prevents all alarms from going off at once??
|
// prevents all alarms from going off at once??
|
||||||
var dow = event.d[j].rep;
|
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 last = (event.d[j].h * 3600000 + event.d[j].m * 60000 < currentTime) ? (new Date()).getDate() : 0;
|
||||||
var a = require("sched").newDefaultAlarm();
|
var a = require("sched").newDefaultAlarm();
|
||||||
a.id = "gb"+j;
|
a.id = "gb"+j;
|
||||||
|
|
@ -89,6 +94,7 @@
|
||||||
a.on = event.d[j].on !== undefined ? event.d[j].on : true;
|
a.on = event.d[j].on !== undefined ? event.d[j].on : true;
|
||||||
a.t = event.d[j].h * 3600000 + event.d[j].m * 60000;
|
a.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.dow = ((dow&63)<<1) | (dow>>6); // Gadgetbridge sends DOW in a different format
|
||||||
|
a.rp = rp;
|
||||||
a.last = last;
|
a.last = last;
|
||||||
alarms.push(a);
|
alarms.push(a);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
"id": "android",
|
"id": "android",
|
||||||
"name": "Android Integration",
|
"name": "Android Integration",
|
||||||
"shortName": "Android",
|
"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.",
|
"description": "Display notifications/music/etc sent from the Gadgetbridge app on Android. This replaces the old 'Gadgetbridge' Bangle.js widget.",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"tags": "tool,system,messages,notifications,gadgetbridge",
|
"tags": "tool,system,messages,notifications,gadgetbridge",
|
||||||
|
|
|
||||||
|
|
@ -1 +1,2 @@
|
||||||
1.00: initial release
|
1.00: initial release
|
||||||
|
1.01: added tap event to scroll METAR and toggle seconds display
|
||||||
|
|
|
||||||
|
|
@ -18,15 +18,20 @@ module after installing this app.
|
||||||
- Latest METAR for the nearest airport (scrollable)
|
- Latest METAR for the nearest airport (scrollable)
|
||||||
|
|
||||||
Tap the screen in the top or bottom half to scroll the METAR text (in case not
|
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
|
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.
|
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
|
## 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
|
- **Invert Scrolling**: swaps the METAR scrolling direction of the top and bottom taps
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -169,6 +169,7 @@ function drawSeconds() {
|
||||||
let seconds = now.getSeconds().toString();
|
let seconds = now.getSeconds().toString();
|
||||||
if (seconds.length == 1) seconds = '0' + seconds;
|
if (seconds.length == 1) seconds = '0' + seconds;
|
||||||
let y = Bangle.appRect.y + mainTimeHeight - 3;
|
let y = Bangle.appRect.y + mainTimeHeight - 3;
|
||||||
|
g.setBgColor(g.theme.bg);
|
||||||
g.setFontAlign(-1, 1).setFont("Vector", secondaryFontHeight).setColor(COLOUR_GREY);
|
g.setFontAlign(-1, 1).setFont("Vector", secondaryFontHeight).setColor(COLOUR_GREY);
|
||||||
g.drawString(seconds, horizontalCenter + 54, y, true);
|
g.drawString(seconds, horizontalCenter + 54, y, true);
|
||||||
}
|
}
|
||||||
|
|
@ -232,10 +233,10 @@ function draw() {
|
||||||
// initialise
|
// initialise
|
||||||
g.clear(true);
|
g.clear(true);
|
||||||
|
|
||||||
// scroll METAR lines on taps
|
// scroll METAR lines (either by touch or tap)
|
||||||
Bangle.setUI("clockupdown", action => {
|
function scrollAVWX(action) {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case -1: // top tap
|
case -1: // top touch/tap
|
||||||
if (settings.invertScrolling) {
|
if (settings.invertScrolling) {
|
||||||
if (METARscollLines > 0)
|
if (METARscollLines > 0)
|
||||||
METARscollLines--;
|
METARscollLines--;
|
||||||
|
|
@ -244,7 +245,7 @@ Bangle.setUI("clockupdown", action => {
|
||||||
METARscollLines++;
|
METARscollLines++;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 1: // bottom tap
|
case 1: // bottom touch/tap
|
||||||
if (settings.invertScrolling) {
|
if (settings.invertScrolling) {
|
||||||
if (METARscollLines < METARlinesCount - 4)
|
if (METARscollLines < METARlinesCount - 4)
|
||||||
METARscollLines++;
|
METARscollLines++;
|
||||||
|
|
@ -254,11 +255,41 @@ Bangle.setUI("clockupdown", action => {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
// ignore
|
// ignore other actions
|
||||||
}
|
}
|
||||||
drawAVWX();
|
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
|
// load widgets
|
||||||
Bangle.loadWidgets();
|
Bangle.loadWidgets();
|
||||||
Bangle.drawWidgets();
|
Bangle.drawWidgets();
|
||||||
|
|
@ -276,8 +307,8 @@ updateAVWX();
|
||||||
|
|
||||||
|
|
||||||
// TMP for debugging:
|
// TMP for debugging:
|
||||||
//METAR = 'YAAA 011100Z 21014KT CAVOK 23/08 Q1018 RMK RF000/0000';
|
//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';
|
//METAR = 'YAAA 150900Z 14012KT 9999 SCT045 BKN064 26/14 Q1012 RMK RF000/0000 DL-W/DL-NW'; drawAVWX();
|
||||||
//METAR = 'YAAA 020030Z VRB CAVOK';
|
//METAR = 'YAAA 020030Z VRB CAVOK'; drawAVWX();
|
||||||
//METARts = new Date(Date.now() - 61 * 60000); // 61 to trigger warning, 91 to trigger alert
|
//METARts = new Date(Date.now() - 61 * 60000); // 61 to trigger warning, 91 to trigger alert
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@
|
||||||
"< Back" : () => back(),
|
"< Back" : () => back(),
|
||||||
'Show Seconds': {
|
'Show Seconds': {
|
||||||
value: !!settings.showSeconds, // !! converts undefined to false
|
value: !!settings.showSeconds, // !! converts undefined to false
|
||||||
format: v => v ? "On" : "Off",
|
|
||||||
onchange: v => {
|
onchange: v => {
|
||||||
settings.showSeconds = v;
|
settings.showSeconds = v;
|
||||||
writeSettings();
|
writeSettings();
|
||||||
|
|
@ -25,7 +24,6 @@
|
||||||
},
|
},
|
||||||
'Invert Scrolling': {
|
'Invert Scrolling': {
|
||||||
value: !!settings.invertScrolling, // !! converts undefined to false
|
value: !!settings.invertScrolling, // !! converts undefined to false
|
||||||
format: v => v ? "On" : "Off",
|
|
||||||
onchange: v => {
|
onchange: v => {
|
||||||
settings.invertScrolling = v;
|
settings.invertScrolling = v;
|
||||||
writeSettings();
|
writeSettings();
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
"id": "aviatorclk",
|
"id": "aviatorclk",
|
||||||
"name": "Aviator Clock",
|
"name": "Aviator Clock",
|
||||||
"shortName":"AV8R 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",
|
"description": "A clock for aviators, with local time and UTC - and the latest METAR for the nearest airport",
|
||||||
"icon": "aviatorclk.png",
|
"icon": "aviatorclk.png",
|
||||||
"screenshots": [{ "url": "screenshot.png" }, { "url": "screenshot2.png" }],
|
"screenshots": [{ "url": "screenshot.png" }, { "url": "screenshot2.png" }],
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
0.01: New App!
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
require("heatshrink").decompress(atob("mEw4X/AAIHBy06nnnDiHwBRMDrgLJhtXBZM1qvABZHVqtwFxFVqowIhoLBGBE1q35GBHVrkDytyrAuGHIPVroLFFwODrklqoLFLoOCrALHLoIXILoVw+APBBYhdCsEAyphFFwITBgQDBMIgeBqtUgILCSQQuBrflBYW+SQYuBuENBYItB6owCXYUDBYIUBYYYuBh2wBYNQ9cFGAWlq0JsGUgNgy0J1WsEgMWhtwBYXXhWq1YLBkvD4HUgNwnk61Wq2ALBwEAkkBAYPq14kCktsgEMgZmBBIILDqoMBBQOWBIM61ALCrYLBh1WBYMKHgILBqxlBnILC2eqBYVVIAPlrWj1mg9QLDtkDyta1ns2AXEX4Va1c84YLEWYVa1XAhwLJ2B5BBZA6BBZOAC5UA5xHI1E8NYQAFh2g9hrCBY2vQYYAFgSPBF4QAFX4U6cgQLH9S/BAA2qcYYAG9WuPIILHOoKdBBY8D9WvgA"))
|
||||||
|
|
@ -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();
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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);
|
||||||
|
};
|
||||||
|
|
@ -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!
|
||||||
|
After Width: | Height: | Size: 15 KiB |
|
|
@ -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"}]
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
})
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
"name": "BTHome Temperature and Pressure",
|
"name": "BTHome Temperature and Pressure",
|
||||||
"shortName":"BTHome T",
|
"shortName":"BTHome T",
|
||||||
"version":"0.02",
|
"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",
|
"icon": "app.png",
|
||||||
"tags": "bthome,bluetooth,temperature",
|
"tags": "bthome,bluetooth,temperature",
|
||||||
"supports" : ["BANGLEJS2"],
|
"supports" : ["BANGLEJS2"],
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
0.01: New App!
|
0.01: New App!
|
||||||
0.02: Disable not existing BTN3 on Bangle.js 2, set maximum transmit power
|
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
|
||||||
|
|
@ -14,7 +14,8 @@ with 4 options:
|
||||||
with this address will be connected to directly. If not specified a menu
|
with this address will be connected to directly. If not specified a menu
|
||||||
showing available Espruino devices is popped up.
|
showing available Espruino devices is popped up.
|
||||||
* **RX** - If checked, the app will display any data received from the
|
* **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
|
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.
|
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
|
you defined. Select one and you'll be able to connect to the device
|
||||||
and send the command.
|
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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -194,16 +194,14 @@ function sendCommandRX(device, text, callback) {
|
||||||
function done() {
|
function done() {
|
||||||
Terminal.println("\\n============\\n Disconnected");
|
Terminal.println("\\n============\\n Disconnected");
|
||||||
device.disconnect();
|
device.disconnect();
|
||||||
if (global.BTN3 !== undefined) {
|
setTimeout(function() {
|
||||||
setTimeout(function() {
|
setWatch(function() {
|
||||||
setWatch(function() {
|
if (callback) callback();
|
||||||
if (callback) callback();
|
resolve();
|
||||||
resolve();
|
}, (process.env.HWVERSION==2) ? BTN1 : BTN2);
|
||||||
}, BTN3);
|
g.reset().setFont("6x8",2).setFontAlign(0,0,1);
|
||||||
g.reset().setFont("6x8",2).setFontAlign(0,0,1);
|
g.drawString("Back", g.getWidth()-10, g.getHeight()/2);
|
||||||
g.drawString("Back", g.getWidth()-10, g.getHeight()-50);
|
}, 200);
|
||||||
}, 200);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
device.getPrimaryService("6e400001-b5a3-f393-e0a9-e50e24dcca9e").then(function(s) {
|
device.getPrimaryService("6e400001-b5a3-f393-e0a9-e50e24dcca9e").then(function(s) {
|
||||||
service = s;
|
service = s;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
"id": "espruinoctrl",
|
"id": "espruinoctrl",
|
||||||
"name": "Espruino Control",
|
"name": "Espruino Control",
|
||||||
"shortName": "Espruino Ctrl",
|
"shortName": "Espruino Ctrl",
|
||||||
"version": "0.02",
|
"version": "0.03",
|
||||||
"description": "Send commands to other Espruino devices via the Bluetooth UART interface. Customisable commands!",
|
"description": "Send commands to other Espruino devices via the Bluetooth UART interface. Customisable commands!",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"tags": "tool,bluetooth",
|
"tags": "tool,bluetooth",
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
<link rel="stylesheet" href="../../css/spectre.min.css">
|
<link rel="stylesheet" href="../../css/spectre.min.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<p>This tool allows you to update the firmware on <a href="https://www.espruino.com/Bangle.js2">Bangle.js 2</a> devices
|
<p>This tool allows you to update the firmware on <a href="https://www.espruino.com/Bangle.js2" target="_blank">Bangle.js 2</a> devices
|
||||||
from within the App Loader.</p>
|
from within the App Loader.</p>
|
||||||
|
|
||||||
<div id="fw-unknown">
|
<div id="fw-unknown">
|
||||||
|
|
@ -32,7 +32,7 @@
|
||||||
bit of code that runs when Bangle.js starts, and it is able to update the
|
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
|
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
|
Updater app, but if for some reason Bangle.js will not boot, you can
|
||||||
<a href="https://www.espruino.com/Bangle.js2#firmware-updates">always use DFU to do the update manually</a>.
|
<a href="https://www.espruino.com/Bangle.js2#firmware-updates" target="_blank">always use DFU to do the update manually</a>.
|
||||||
On DFU 2v19 and earlier, iOS devices could have issues updating firmware - 2v20 fixes this.</p>
|
On DFU 2v19 and earlier, iOS devices could have issues updating firmware - 2v20 fixes this.</p>
|
||||||
<p>DFU is itself a bootloader, but here we're calling it DFU to avoid confusion
|
<p>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).</p>
|
with the Bootloader app in the app loader (which prepares Bangle.js for running apps).</p>
|
||||||
|
|
@ -42,7 +42,7 @@
|
||||||
<div id="advanced-div" style="display:none">
|
<div id="advanced-div" style="display:none">
|
||||||
<p><b>Advanced</b></p>
|
<p><b>Advanced</b></p>
|
||||||
<p>Firmware updates via this tool work differently to the NRF Connect method mentioned on
|
<p>Firmware updates via this tool work differently to the NRF Connect method mentioned on
|
||||||
<a href="https://www.espruino.com/Bangle.js2#firmware-updates">the Bangle.js 2 page</a>. Firmware
|
<a href="https://www.espruino.com/Bangle.js2#firmware-updates" target="_blank">the Bangle.js 2 page</a>. Firmware
|
||||||
is uploaded to a file on the Bangle. Once complete the Bangle reboots and DFU copies
|
is uploaded to a file on the Bangle. Once complete the Bangle reboots and DFU copies
|
||||||
the new firmware into internal Storage.</p>
|
the new firmware into internal Storage.</p>
|
||||||
<p>In addition to the links above, you can upload a hex or zip file directly below. This file should be an <code>.app_hex</code>
|
<p>In addition to the links above, you can upload a hex or zip file directly below. This file should be an <code>.app_hex</code>
|
||||||
|
|
@ -58,6 +58,15 @@
|
||||||
|
|
||||||
<pre id="log"></pre>
|
<pre id="log"></pre>
|
||||||
|
|
||||||
|
<p><a href="#" id="changelog-btn">Firmware ChangeLog ▼</a></p>
|
||||||
|
<div id="changelog-div" style="display:none">
|
||||||
|
<p><b>Firmware ChangeLog</b></p>
|
||||||
|
<ul>
|
||||||
|
<li><a href="https://www.espruino.com/ChangeLog" target="_blank">Released</a></li>
|
||||||
|
<li><a href="https://github.com/espruino/Espruino/blob/master/ChangeLog" target="_blank">Cutting Edge</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="../../core/lib/customize.js"></script>
|
<script src="../../core/lib/customize.js"></script>
|
||||||
<script src="../../core/lib/espruinotools.js"></script>
|
<script src="../../core/lib/espruinotools.js"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.js"></script>
|
||||||
|
|
@ -93,9 +102,9 @@ function onInit(device) {
|
||||||
console.log("DFU CRC (7 pages) = "+crcs[1]);
|
console.log("DFU CRC (7 pages) = "+crcs[1]);
|
||||||
var version = `unknown (CRC ${crcs[1]})`;
|
var version = `unknown (CRC ${crcs[1]})`;
|
||||||
var ok = true;
|
var ok = true;
|
||||||
if (crcs[0] == 1787004733) { // check 6 page CRC - the 7th page isn't used in 2v20
|
if (crcs[0] == 1787004733) version = "2v20"; // check 6 page CRCs - the 7th page isn't used in 2v20+
|
||||||
version = "2v20";
|
else if (crcs[0] == 3816337552) version = "2v21";
|
||||||
} else { // for other versions all 7 pages are used, check those
|
else { // for other versions all 7 pages are used, check those
|
||||||
var crc = crcs[1];
|
var crc = crcs[1];
|
||||||
if (crc==1339551013) { version = "2v10.219"; ok = false; }
|
if (crc==1339551013) { version = "2v10.219"; ok = false; }
|
||||||
if (crc==1207580954) { version = "2v10.236"; 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('fileLoader').addEventListener('change', handleFileSelect, false);
|
||||||
document.getElementById("upload").addEventListener("click", handleUpload);
|
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-btn").style = "display:none";
|
||||||
document.getElementById("info-div").style = "";
|
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-btn").style = "display:none";
|
||||||
document.getElementById("advanced-div").style = "";
|
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);
|
setTimeout(checkForFileOnServer, 10);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@
|
||||||
"< Back": () => back(),
|
"< Back": () => back(),
|
||||||
'Front Tap:': {
|
'Front Tap:': {
|
||||||
value: (appSettings.enableTap === true),
|
value: (appSettings.enableTap === true),
|
||||||
format: v => v ? "On" : "Off",
|
|
||||||
onchange: v => {
|
onchange: v => {
|
||||||
appSettings.enableTap = v;
|
appSettings.enableTap = v;
|
||||||
writeSettings();
|
writeSettings();
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
"id": "ha",
|
"id": "ha",
|
||||||
"name": "Home Assistant",
|
"name": "Home Assistant",
|
||||||
"version": "0.10",
|
"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",
|
"icon": "ha.png",
|
||||||
"type": "app",
|
"type": "app",
|
||||||
"tags": "tool,clkinfo,bluetooth",
|
"tags": "tool,clkinfo,bluetooth",
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
"name": "Home Assistant Sensors",
|
"name": "Home Assistant Sensors",
|
||||||
"shortName": "HA sensors",
|
"shortName": "HA sensors",
|
||||||
"version": "0.02",
|
"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",
|
"icon": "ha.png",
|
||||||
"type": "bootloader",
|
"type": "bootloader",
|
||||||
"tags": "tool,sensors",
|
"tags": "tool,sensors",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
0.01: New library
|
||||||
|
|
@ -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.
|
||||||
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 510 B |
|
After Width: | Height: | Size: 350 B |
|
After Width: | Height: | Size: 228 B |
|
After Width: | Height: | Size: 268 B |
|
After Width: | Height: | Size: 646 B |
|
|
@ -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<rgba.length;i+=4)
|
||||||
|
if (rgba[i+3]<255) isTransparent=true;
|
||||||
|
if (!isTransparent) { // make it transparent
|
||||||
|
for (var i=0;i<rgba.length;i+=4)
|
||||||
|
rgba[i+3] = 255-rgba[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
imgOptions.width = png.width;
|
||||||
|
imgOptions.height = png.height;
|
||||||
|
var img = imageconverter.RGBAtoString(rgba, imgOptions);
|
||||||
|
icon.index = iconIndex;
|
||||||
|
icon.img = img;
|
||||||
|
console.log("Loaded "+icon.name);
|
||||||
|
if (img.length != IMAGE_BYTES) throw new Error("Image size should be 76 bytes");
|
||||||
|
r(); // done
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
Promise.all(promises).then(function() {
|
||||||
|
// Allocate a big array of icons
|
||||||
|
var iconData = new Uint8Array(IMAGE_BYTES * icons.length);
|
||||||
|
icons.forEach((icon,idx) => {
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
After Width: | Height: | Size: 326 B |
|
|
@ -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"}
|
||||||
|
]
|
||||||
|
After Width: | Height: | Size: 256 B |
|
After Width: | Height: | Size: 452 B |
|
After Width: | Height: | Size: 264 B |
|
After Width: | Height: | Size: 247 B |
|
After Width: | Height: | Size: 675 B |
|
After Width: | Height: | Size: 344 B |
|
After Width: | Height: | Size: 445 B |
|
After Width: | Height: | Size: 271 B |
|
|
@ -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);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
@ -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"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
"description": "A library for text input via onscreen keyboard",
|
"description": "A library for text input via onscreen keyboard",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"type":"textinput",
|
"type":"textinput",
|
||||||
|
"default": true,
|
||||||
"tags": "keyboard",
|
"tags": "keyboard",
|
||||||
"supports" : ["BANGLEJS2"],
|
"supports" : ["BANGLEJS2"],
|
||||||
"screenshots": [{"url":"screenshot.png"}],
|
"screenshots": [{"url":"screenshot.png"}],
|
||||||
|
|
|
||||||
|
|
@ -1 +1,2 @@
|
||||||
0.01: New App!
|
0.01: New App!
|
||||||
|
0.02: Add more control styles
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,21 @@ in the future this app will be able to support other types of remote (see below)
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
Run the app, and ensure you're not connected to your watch via Bluetooth
|
Run the app, then choose the type of controls you want and ensure you're not connected
|
||||||
(a warning will pop up if so).
|
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.
|
Now press the arrow keys on the screen to control the robot.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
var lego = require("mouldking");
|
var lego = require("mouldking");
|
||||||
lego.start();
|
|
||||||
E.on('kill', () => {
|
E.on('kill', () => {
|
||||||
// return to normal Bluetooth advertising
|
// return to normal Bluetooth advertising
|
||||||
NRF.setAdvertising({},{showName:true});
|
NRF.setAdvertising({},{showName:true});
|
||||||
|
|
@ -12,59 +11,133 @@ var controlState = "";
|
||||||
Bangle.loadWidgets();
|
Bangle.loadWidgets();
|
||||||
Bangle.drawWidgets();
|
Bangle.drawWidgets();
|
||||||
var R = Bangle.appRect;
|
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() {
|
function startLegoButtons(controls) {
|
||||||
g.reset().clearRect(R);
|
// we'll divide up into 3x3
|
||||||
var c, ninety = Math.PI/2;
|
function getBoxCoords(x,y) {
|
||||||
var colOn = "#f00", colOff = g.theme.fg;
|
return {
|
||||||
c = getBoxCoords(1.5, 0.5);
|
x : R.x + R.w*x/3,
|
||||||
g.setColor(controlState=="up"?colOn:colOff).drawImage(arrowIcon,c.x,c.y,{rotate:0});
|
y : R.y + R.h*y/3
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
draw();
|
|
||||||
NRF.on('connect', draw);
|
|
||||||
NRF.on('disconnect', draw);
|
|
||||||
|
|
||||||
function setControlState(s) {
|
function draw() {
|
||||||
controlState = s;
|
g.reset().clearRect(R);
|
||||||
var c = {};
|
var c, ninety = Math.PI/2;
|
||||||
var speed = 3;
|
var colOn = "#f00", colOff = g.theme.fg;
|
||||||
if (s=="up") c={a:-speed,b:-speed};
|
c = getBoxCoords(1.5, 0.5);
|
||||||
if (s=="down") c={a:speed,b:speed};
|
g.setColor(controlState=="up"?colOn:colOff).drawImage(arrowIcon,c.x,c.y,{rotate:0});
|
||||||
if (s=="left") c={a:speed,b:-speed};
|
c = getBoxCoords(2.5, 1.5);
|
||||||
if (s=="right") c={a:-speed,b:speed};
|
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();
|
draw();
|
||||||
lego.set(c);
|
NRF.on('connect', draw);
|
||||||
|
NRF.on('disconnect', draw);
|
||||||
}
|
}
|
||||||
|
|
||||||
Bangle.on('drag',e => {
|
function startLegoLinear() {
|
||||||
var x = Math.floor(E.clip((e.x - R.x) * 3 / R.w,0,2.99));
|
var mx = R.x+R.w/2;
|
||||||
var y = Math.floor(E.clip((e.y - R.y) * 3 / R.h,0,2.99));
|
var my = R.y+R.h/2;
|
||||||
if (!e.b) {
|
var x=0,y=0;
|
||||||
setControlState("");
|
var scale = 10;
|
||||||
return;
|
|
||||||
}
|
function draw() {
|
||||||
if (y==0) { // top row
|
g.reset().clearRect(R);
|
||||||
if (x==1) setControlState("up");
|
for (var i=3;i<60;i+=10)
|
||||||
} else if (y==1) {
|
g.drawCircle(mx,my,i);
|
||||||
if (x==0) setControlState("left");
|
g.setColor("#F00");
|
||||||
if (x==1) setControlState("down");
|
var px = E.clip(mx + x*scale, R.x+20, R.x2-20);
|
||||||
if (x==2) setControlState("right");
|
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),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{ "id": "legoremote",
|
{ "id": "legoremote",
|
||||||
"name": "LEGO Remote control",
|
"name": "LEGO Remote control",
|
||||||
"shortName":"LEGO Remote",
|
"shortName":"LEGO Remote",
|
||||||
"version":"0.01",
|
"version":"0.02",
|
||||||
"description": "Use your Bangle.js to control LEGO models. See the README for compatibility",
|
"description": "Use your Bangle.js to control LEGO models. See the README for compatibility",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"tags": "toy,lego,bluetooth",
|
"tags": "toy,lego,bluetooth",
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@
|
||||||
'': { 'title': 'Welcome App' },
|
'': { 'title': 'Welcome App' },
|
||||||
'Run next boot': {
|
'Run next boot': {
|
||||||
value: !settings.welcomed,
|
value: !settings.welcomed,
|
||||||
format: v => v ? 'Yes' : 'No',
|
|
||||||
onchange: v => require('Storage').write('mywelcome.json', {welcomed: !v}),
|
onchange: v => require('Storage').write('mywelcome.json', {welcomed: !v}),
|
||||||
},
|
},
|
||||||
'Run Now': () => load('mywelcome.app.js'),
|
'Run Now': () => load('mywelcome.app.js'),
|
||||||
|
|
|
||||||
|
|
@ -33,4 +33,5 @@
|
||||||
0.26: Ensure that when redrawing, we always cancel any in-progress track draw
|
0.26: Ensure that when redrawing, we always cancel any in-progress track draw
|
||||||
0.27: Display message if no map is installed
|
0.27: Display message if no map is installed
|
||||||
0.28: Fix rounding errors
|
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
|
||||||
|
|
@ -38,17 +38,17 @@ if (m.map) {
|
||||||
m.lat = m.map.lat; // position of middle of screen
|
m.lat = m.map.lat; // position of middle of screen
|
||||||
m.lon = m.map.lon; // 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
|
// return number of tiles drawn
|
||||||
exports.draw = function() {
|
exports.draw = function() {
|
||||||
var cx = g.getWidth()/2;
|
|
||||||
var cy = g.getHeight()/2;
|
|
||||||
var p = Bangle.project({lat:m.lat,lon:m.lon});
|
var p = Bangle.project({lat:m.lat,lon:m.lon});
|
||||||
let count = 0;
|
let count = 0;
|
||||||
m.maps.forEach((map,idx) => {
|
m.maps.forEach((map,idx) => {
|
||||||
var d = map.scale/m.scale;
|
var d = map.scale/m.scale;
|
||||||
var ix = (p.x-map.center.x)/m.scale + (map.imgx*d/2) - cx;
|
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 iy = (map.center.y-p.y)/m.scale + (map.imgy*d/2) - CY;
|
||||||
var o = {};
|
var o = {};
|
||||||
var s = map.tilesize;
|
var s = map.tilesize;
|
||||||
if (d!=1) { // if the two are different, add scaling
|
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
|
/// Convert lat/lon to pixels on the screen
|
||||||
exports.latLonToXY = function(lat, lon) {
|
exports.latLonToXY = function(lat, lon) { "ram"
|
||||||
var p = Bangle.project({lat:m.lat,lon:m.lon});
|
var p = Bangle.project({lat:m.lat,lon:m.lon}),
|
||||||
var q = Bangle.project({lat:lat, lon:lon});
|
q = Bangle.project({lat:lat, lon:lon});
|
||||||
var cx = g.getWidth()/2;
|
|
||||||
var cy = g.getHeight()/2;
|
|
||||||
return {
|
return {
|
||||||
x : Math.round((q.x-p.x)/m.scale + cx),
|
x : Math.round((q.x-p.x)/m.scale + CX),
|
||||||
y : Math.round(cy - (q.y-p.y)/m.scale)
|
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});
|
var b = Bangle.project({lat:m.lat+1,lon:m.lon+1});
|
||||||
this.lon += x * m.scale / (a.x-b.x);
|
this.lon += x * m.scale / (a.x-b.x);
|
||||||
this.lat -= y * m.scale / (a.y-b.y);
|
this.lat -= y * m.scale / (a.y-b.y);
|
||||||
};
|
};
|
||||||
|
|
@ -45,3 +45,4 @@
|
||||||
0.36: When recording with 1 second periods, log time with one decimal.
|
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
|
0.37: 1 second periods + gps log => log when gps event is received, not with
|
||||||
setInterval.
|
setInterval.
|
||||||
|
0.38: Tweaks to speed up track rendering
|
||||||
|
|
@ -213,230 +213,230 @@ function viewTrack(filename, info) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
menu['< Back'] = () => { viewTracks(); };
|
menu['< Back'] = () => { viewTracks(); };
|
||||||
|
return E.showMenu(menu);
|
||||||
|
}
|
||||||
|
|
||||||
function plotTrack(info) { "ram"
|
function plotTrack(info) { "ram"
|
||||||
function distance(lat1,long1,lat2,long2) { "ram"
|
function distance(lat1,long1,lat2,long2) { "ram"
|
||||||
var x = (long1-long2) * Math.cos((lat1+lat2)*Math.PI/360);
|
var x = (long1-long2) * Math.cos((lat1+lat2)*Math.PI/360);
|
||||||
var y = lat2 - lat1;
|
var y = lat2 - lat1;
|
||||||
return Math.sqrt(x*x + y*y) * 6371000 * Math.PI / 180;
|
return Math.sqrt(x*x + y*y) * 6371000 * Math.PI / 180;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to convert lat/lon to XY
|
// Function to convert lat/lon to XY
|
||||||
var getMapXY;
|
var XY;
|
||||||
if (info.qOSTM) {
|
if (info.qOSTM) {
|
||||||
// scale map to view full track
|
// scale map to view full track
|
||||||
const max = Bangle.project({lat: info.maxLat, lon: info.maxLong});
|
const max = Bangle.project({lat: info.maxLat, lon: info.maxLong});
|
||||||
const min = Bangle.project({lat: info.minLat, lon: info.minLong});
|
const min = Bangle.project({lat: info.minLat, lon: info.minLong});
|
||||||
const scaleX = (max.x-min.x)/Bangle.appRect.w;
|
const scaleX = (max.x-min.x)/Bangle.appRect.w;
|
||||||
const scaleY = (max.y-min.y)/Bangle.appRect.h;
|
const scaleY = (max.y-min.y)/Bangle.appRect.h;
|
||||||
osm.scale = Math.ceil((scaleX > scaleY ? scaleX : scaleY)*1.1); // add 10% margin
|
osm.scale = Math.ceil((scaleX > scaleY ? scaleX : scaleY)*1.1); // add 10% margin
|
||||||
getMapXY = osm.latLonToXY.bind(osm);
|
XY = osm.latLonToXY.bind(osm);
|
||||||
} else {
|
} else {
|
||||||
getMapXY = function(lat, lon) { "ram"
|
XY = function(lat, lon) { "ram"
|
||||||
return {x:cx + Math.round((long - info.lon)*info.lfactor*info.scale),
|
return {x:cx + Math.round((long - info.lon)*info.lfactor*info.scale),
|
||||||
y:cy + Math.round((info.lat - lat)*info.scale)};
|
y:cy + Math.round((info.lat - lat)*info.scale)};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
E.showMenu(); // remove menu
|
E.showMenu(); // remove menu
|
||||||
E.showMessage(/*LANG*/"Drawing...",/*LANG*/"Track "+info.fn);
|
E.showMessage(/*LANG*/"Drawing...",/*LANG*/"Track "+info.fn);
|
||||||
g.flip(); // on buffered screens, draw a not saying we're busy
|
g.flip(); // on buffered screens, draw a not saying we're busy
|
||||||
g.clear(1);
|
g.clear(1);
|
||||||
var s = require("Storage");
|
var s = require("Storage");
|
||||||
var W = g.getWidth();
|
var G = g;
|
||||||
var H = g.getHeight();
|
var W = g.getWidth();
|
||||||
var cx = W/2;
|
var H = g.getHeight();
|
||||||
var cy = 24 + (H-24)/2;
|
var cx = W/2;
|
||||||
if (!info.qOSTM) {
|
var cy = 24 + (H-24)/2;
|
||||||
g.setColor("#f00").fillRect(9,80,11,120).fillPoly([9,60,19,80,0,80]);
|
if (!info.qOSTM) {
|
||||||
g.setColor(g.theme.fg).setFont("6x8").setFontAlign(0,0).drawString("N",10,50);
|
g.setColor("#f00").fillRect(9,80,11,120).fillPoly([9,60,19,80,0,80]);
|
||||||
} else {
|
g.setColor(g.theme.fg).setFont("6x8").setFontAlign(0,0).drawString("N",10,50);
|
||||||
osm.lat = info.lat;
|
} else {
|
||||||
osm.lon = info.lon;
|
osm.lat = info.lat;
|
||||||
osm.draw();
|
osm.lon = info.lon;
|
||||||
g.setColor("#000");
|
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 latIdx = info.fields.indexOf("Latitude");
|
||||||
var lonIdx = info.fields.indexOf("Longitude");
|
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
|
// skip until we find our first data
|
||||||
while(l!==undefined && c[latIdx]=="") {
|
while(l!==undefined && c[latIdx]=="") {
|
||||||
c = l.split(",");
|
c = l.split(",");
|
||||||
l = f.readLine(f);
|
l = f.readLine(f);
|
||||||
}
|
}
|
||||||
// now start plotting
|
// now iterate
|
||||||
var lat = +c[latIdx];
|
var p,lp = Bangle.project({lat:c[1],lon:c[2]});
|
||||||
var long = +c[lonIdx];
|
var t,dx,dy,d,lt = c[timeIdx];
|
||||||
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
|
|
||||||
while(l!==undefined) {
|
while(l!==undefined) {
|
||||||
c = l.split(",");l = f.readLine(f);
|
++nl;c=l.split(",");
|
||||||
if (c[latIdx]=="")continue;
|
l = f.readLine(f);
|
||||||
lat = +c[latIdx];
|
if (c[latIdx] == "") {
|
||||||
long = +c[lonIdx];
|
continue;
|
||||||
mp = getMapXY(lat, long);
|
}
|
||||||
g.lineTo(mp.x,mp.y);
|
t = c[timeIdx];
|
||||||
if (info.qOSTM) g.fillCircle(mp.x,mp.y,2); // make the track more visible
|
i = Math.round(80*(t - strt)/dur);
|
||||||
var d = distance(olat,olong,lat,long);
|
p = Bangle.project({lat:c[latIdx],lon:c[lonIdx]});
|
||||||
if (!isNaN(d)) dist+=d;
|
dx = p.x-lp.x;
|
||||||
olat = lat;
|
dy = p.y-lp.y;
|
||||||
olong = long;
|
d = Math.sqrt(dx*dx+dy*dy);
|
||||||
ox = mp.x;
|
if (t!=lt) {
|
||||||
oy = mp.y;
|
infn[i]+=d / (t-lt); // speed
|
||||||
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]++;
|
infc[i]++;
|
||||||
}
|
}
|
||||||
} else if (style=="Altitude") {
|
lp = p;
|
||||||
title = /*LANG*/"Altitude (m)";
|
lt = t;
|
||||||
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;i<infn.length;i++) {
|
|
||||||
if (infc[i]>0) 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<min) min=n;
|
|
||||||
}
|
}
|
||||||
// work out a nice grid value
|
} else throw new Error("Unknown type "+style);
|
||||||
var heightDiff = max-min;
|
var min=100000,max=-100000;
|
||||||
var grid = 1;
|
for (var i=0;i<infn.length;i++) {
|
||||||
while (heightDiff/grid > 8) {
|
if (infc[i]>0) infn[i]=factor*infn[i]/infc[i];
|
||||||
grid*=2;
|
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
|
var n = infn[i];
|
||||||
g.clear(1).setFont("6x8",1);
|
if (n>max) max=n;
|
||||||
var r = require("graph").drawLine(g, infn, {
|
if (n<min) min=n;
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
// work out a nice grid value
|
||||||
return E.showMenu(menu);
|
var heightDiff = max-min;
|
||||||
|
var grid = 1;
|
||||||
|
while (heightDiff/grid > 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();
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
"id": "recorder",
|
"id": "recorder",
|
||||||
"name": "Recorder",
|
"name": "Recorder",
|
||||||
"shortName": "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.",
|
"description": "Record GPS position, heart rate and more in the background, then download to your PC.",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"tags": "tool,outdoors,gps,widget,clkinfo",
|
"tags": "tool,outdoors,gps,widget,clkinfo",
|
||||||
|
|
|
||||||
|
|
@ -77,3 +77,4 @@ of 'Select Clock'
|
||||||
calibration was done.
|
calibration was done.
|
||||||
0.67: Rename 'Wake on BTN1/Touch' to 'Wake on Button/Tap' on Bangle.js 2
|
0.67: Rename 'Wake on BTN1/Touch' to 'Wake on Button/Tap' on Bangle.js 2
|
||||||
0.68: Fix syntax error
|
0.68: Fix syntax error
|
||||||
|
0.69: Add option to wake on double tap
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"id": "setting",
|
"id": "setting",
|
||||||
"name": "Settings",
|
"name": "Settings",
|
||||||
"version": "0.68",
|
"version": "0.69",
|
||||||
"description": "A menu for setting up Bangle.js",
|
"description": "A menu for setting up Bangle.js",
|
||||||
"icon": "settings.png",
|
"icon": "settings.png",
|
||||||
"tags": "tool,system",
|
"tags": "tool,system",
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ function updateOptions() {
|
||||||
var o = settings.options;
|
var o = settings.options;
|
||||||
// Check to make sure nobody disabled all wakeups and locked themselves out!
|
// Check to make sure nobody disabled all wakeups and locked themselves out!
|
||||||
if (BANGLEJS2) {
|
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;
|
o.wakeOnBTN1 = true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -451,48 +451,58 @@ function showLCDMenu() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (BANGLEJS2)
|
if (BANGLEJS2) {
|
||||||
Object.assign(lcdMenu, {
|
Object.assign(lcdMenu, {
|
||||||
/*LANG*/'Wake on Button': {
|
/*LANG*/'Wake on Button': {
|
||||||
value: settings.options.wakeOnBTN1,
|
value: !!settings.options.wakeOnBTN1,
|
||||||
onchange: () => {
|
onchange: () => {
|
||||||
settings.options.wakeOnBTN1 = !settings.options.wakeOnBTN1;
|
settings.options.wakeOnBTN1 = !settings.options.wakeOnBTN1;
|
||||||
updateOptions();
|
updateOptions();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
/*LANG*/'Wake on Tap': {
|
/*LANG*/'Wake on Tap': {
|
||||||
value: settings.options.wakeOnTouch,
|
value: !!settings.options.wakeOnTouch,
|
||||||
onchange: () => {
|
onchange: () => {
|
||||||
settings.options.wakeOnTouch = !settings.options.wakeOnTouch;
|
settings.options.wakeOnTouch = !settings.options.wakeOnTouch;
|
||||||
updateOptions();
|
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, {
|
Object.assign(lcdMenu, {
|
||||||
/*LANG*/'Wake on BTN1': {
|
/*LANG*/'Wake on BTN1': {
|
||||||
value: settings.options.wakeOnBTN1,
|
value: !!settings.options.wakeOnBTN1,
|
||||||
onchange: () => {
|
onchange: () => {
|
||||||
settings.options.wakeOnBTN1 = !settings.options.wakeOnBTN1;
|
settings.options.wakeOnBTN1 = !settings.options.wakeOnBTN1;
|
||||||
updateOptions();
|
updateOptions();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
/*LANG*/'Wake on BTN2': {
|
/*LANG*/'Wake on BTN2': {
|
||||||
value: settings.options.wakeOnBTN2,
|
value: !!settings.options.wakeOnBTN2,
|
||||||
onchange: () => {
|
onchange: () => {
|
||||||
settings.options.wakeOnBTN2 = !settings.options.wakeOnBTN2;
|
settings.options.wakeOnBTN2 = !settings.options.wakeOnBTN2;
|
||||||
updateOptions();
|
updateOptions();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
/*LANG*/'Wake on BTN3': {
|
/*LANG*/'Wake on BTN3': {
|
||||||
value: settings.options.wakeOnBTN3,
|
value: !!settings.options.wakeOnBTN3,
|
||||||
onchange: () => {
|
onchange: () => {
|
||||||
settings.options.wakeOnBTN3 = !settings.options.wakeOnBTN3;
|
settings.options.wakeOnBTN3 = !settings.options.wakeOnBTN3;
|
||||||
updateOptions();
|
updateOptions();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
/*LANG*/'Wake on Touch': {
|
/*LANG*/'Wake on Touch': {
|
||||||
value: settings.options.wakeOnTouch,
|
value: !!settings.options.wakeOnTouch,
|
||||||
onchange: () => {
|
onchange: () => {
|
||||||
settings.options.wakeOnTouch = !settings.options.wakeOnTouch;
|
settings.options.wakeOnTouch = !settings.options.wakeOnTouch;
|
||||||
updateOptions();
|
updateOptions();
|
||||||
|
|
@ -500,14 +510,14 @@ function showLCDMenu() {
|
||||||
}});
|
}});
|
||||||
Object.assign(lcdMenu, {
|
Object.assign(lcdMenu, {
|
||||||
/*LANG*/'Wake on FaceUp': {
|
/*LANG*/'Wake on FaceUp': {
|
||||||
value: settings.options.wakeOnFaceUp,
|
value: !!settings.options.wakeOnFaceUp,
|
||||||
onchange: () => {
|
onchange: () => {
|
||||||
settings.options.wakeOnFaceUp = !settings.options.wakeOnFaceUp;
|
settings.options.wakeOnFaceUp = !settings.options.wakeOnFaceUp;
|
||||||
updateOptions();
|
updateOptions();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
/*LANG*/'Wake on Twist': {
|
/*LANG*/'Wake on Twist': {
|
||||||
value: settings.options.wakeOnTwist,
|
value: !!settings.options.wakeOnTwist,
|
||||||
onchange: () => {
|
onchange: () => {
|
||||||
settings.options.wakeOnTwist = !settings.options.wakeOnTwist;
|
settings.options.wakeOnTwist = !settings.options.wakeOnTwist;
|
||||||
updateOptions();
|
updateOptions();
|
||||||
|
|
|
||||||
|
|
@ -130,7 +130,6 @@
|
||||||
},
|
},
|
||||||
'Date Suffix:': {
|
'Date Suffix:': {
|
||||||
value: appSettings.enableSuffix,
|
value: appSettings.enableSuffix,
|
||||||
format: v => v ? 'Yes' : 'No',
|
|
||||||
onchange: v => {
|
onchange: v => {
|
||||||
appSettings.enableSuffix = v;
|
appSettings.enableSuffix = v;
|
||||||
writeSettings();
|
writeSettings();
|
||||||
|
|
@ -138,7 +137,6 @@
|
||||||
},
|
},
|
||||||
'Lead Zero:': {
|
'Lead Zero:': {
|
||||||
value: appSettings.enableLeadingZero,
|
value: appSettings.enableLeadingZero,
|
||||||
format: v => v ? 'Yes' : 'No',
|
|
||||||
onchange: v => {
|
onchange: v => {
|
||||||
appSettings.enableLeadingZero = v;
|
appSettings.enableLeadingZero = v;
|
||||||
writeSettings();
|
writeSettings();
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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="))
|
||||||
|
|
@ -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<faceCount; ++face) {
|
||||||
|
Ab = Bb = Cb = false;
|
||||||
|
int color = *faceOffset++ & ~2;
|
||||||
|
color ^= (color >> 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();
|
||||||
|
After Width: | Height: | Size: 4.7 KiB |
|
|
@ -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}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
|
@ -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.
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
atob("MDDD/wAA//+nMRF8r3PznG1znfdmmccUUUUUMMQQQQQQQQUdFFc8/66rKKLBBBBBBBBBBB6zyy+mnFcUUUUUMMQQQQMQMddFFdk3266rLKLLBBBBBBBBK6y6x/7FddcUUUUUMMMQQQQMcdUdd9e66q6rLJqLBBBBBBBSpyqze65dcUccUUUUUMMMMMNdccdd9ce66pqqppqLBBBBBBOp6qu/6qpcccccccUUUMMOsOccEdFlcUW5q66ppprLJBBDDWyr64866rIcUcccUUUUUMMUXfEGkGlddUWpqJ6pppqLLhCjO26u4867LLIcdFlFl0UUUXEMVdEEGndcUUVpjKs30330303ra1Ng8327LLIUW0VlnF9FEXcEEQUOnHdcUUVppppA0033u77sxBBA036pqrIUUUUUWHHHFHFHUYQUdENcEcVpppppBL373swzBBLB3rK4irAUUUUUYYGHGHEMMYUUcMMEdcVp5pprBDBWc2hBBBBLbC7Ba6ocUUUUMUYGmEUYQMUUMUUMFddpppprK5A82rDDBDBKLLDDK64UUcefEQGlcMMMQMQUcUMMVFdppp/2pA36BBBBDJDKpppjDKsUcedHccYQQQYUUUUFccUUVcdpr+38rBBBBBBDJBDBJ3KJqK4UEVelcYUYUQUQUMYYcdcdcUVoprc6rK6hJLLBDBBBBCqqqKIEUWkUUenUUVdUMYUUQQQcUUWpqfrDK22rK4+rBDDJBC5BJpIUW8UUUddcfGnUFFEUMdccUUVqeBLKKbrLY82q6yuBCa66ppAW0UUUXcUUGnddddUUVcdFUUWeLDDLdrDI+7a66ypLJpyyrLC0UMUV8UUe8VccddUUUUdcUUOLBBCdrBDfC66J6rJJqKKyBBAUMMV0UMW0cccUcccUUUVUUMNBBCdqBBKKKprLJ1pqLDLDBBAMMeUUMMUUccUUW0UUUUMUMMRBD1qJBDDKJrLLfLLLBBDLDLAUHUUMMUUUUUUW0UUUUMUUUUNAqKJBBBDKLLLXLLDJBDBBDLAEUUUMQMUUUcVUUUUUMUMMMUQqLKBBBDLLK6rLDBBBBDDBBBAUUUMQMUUUfcUMMMQMMUUQQQSKLBBBBDLL2rBBBBBBBBBBBBAUUMMMMUUfcUMMMMQMMMMQQQTLBBBBBDL2rBBBBBBBhBBBBBA=")
|
||||||
|
|
@ -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; face<faceCount; ++face) {
|
||||||
|
Ab = Bb = Cb = false;
|
||||||
|
int color = *faceOffset++ + tint;
|
||||||
|
if (!color) color++;
|
||||||
|
|
||||||
|
unsigned char* indexA = vtxOffset + ((int)*faceOffset++) * 3;
|
||||||
|
unsigned char* indexB = vtxOffset + ((int)*faceOffset++) * 3;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
After Width: | Height: | Size: 5.8 KiB |
|
|
@ -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}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 93 KiB |
|
|
@ -5,7 +5,6 @@
|
||||||
'': { 'title': 'Welcome App' },
|
'': { 'title': 'Welcome App' },
|
||||||
'Run next boot': {
|
'Run next boot': {
|
||||||
value: !settings.welcomed,
|
value: !settings.welcomed,
|
||||||
format: v => v ? 'Yes' : 'No',
|
|
||||||
onchange: v => require('Storage').write('welcome.json', {welcomed: !v}),
|
onchange: v => require('Storage').write('welcome.json', {welcomed: !v}),
|
||||||
},
|
},
|
||||||
'Run Now': () => load('welcome.app.js'),
|
'Run Now': () => load('welcome.app.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});
|
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
|
// check for manual boolean formatter
|
||||||
const m = fileContents.match(/format: *\(\) *=>.*["'](yes|on)["']/i);
|
const m = fileContents.match(/format: *\(?\w*\)? *=>.*["'](yes|on)["']/i);
|
||||||
if (m) {
|
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});
|
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});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2
core
|
|
@ -1 +1 @@
|
||||||
Subproject commit 364b2c1b00de17ffbbee87fb1d91e79b513b9127
|
Subproject commit bd301be3324775a8f464328ba9e34f750d503a2b
|
||||||
|
|
@ -16,7 +16,7 @@ if (window.location.host=="banglejs.com") {
|
||||||
'This is not the official Bangle.js App Loader - you can try the <a href="https://banglejs.com/apps/">Official Version</a> here.';
|
'This is not the official Bangle.js App Loader - you can try the <a href="https://banglejs.com/apps/">Official Version</a> here.';
|
||||||
}
|
}
|
||||||
|
|
||||||
var RECOMMENDED_VERSION = "2v20";
|
var RECOMMENDED_VERSION = "2v21";
|
||||||
// could check http://www.espruino.com/json/BANGLEJS.json for this
|
// could check http://www.espruino.com/json/BANGLEJS.json for this
|
||||||
|
|
||||||
// We're only interested in Bangles
|
// We're only interested in Bangles
|
||||||
|
|
|
||||||