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
|
||||
0.31: Implement API for activity fetching
|
||||
0.32: Added support for loyalty cards from gadgetbridge
|
||||
0.33: Fix alarms created in Gadgetbridge not repeating
|
||||
|
|
|
|||
|
|
@ -81,7 +81,12 @@
|
|||
for (var j = 0; j < event.d.length; j++) {
|
||||
// prevents all alarms from going off at once??
|
||||
var dow = event.d[j].rep;
|
||||
if (!dow) dow = 127; //if no DOW selected, set alarm to all DOW
|
||||
var rp = false;
|
||||
if (!dow) {
|
||||
dow = 127; //if no DOW selected, set alarm to all DOW
|
||||
} else {
|
||||
rp = true;
|
||||
}
|
||||
var last = (event.d[j].h * 3600000 + event.d[j].m * 60000 < currentTime) ? (new Date()).getDate() : 0;
|
||||
var a = require("sched").newDefaultAlarm();
|
||||
a.id = "gb"+j;
|
||||
|
|
@ -89,6 +94,7 @@
|
|||
a.on = event.d[j].on !== undefined ? event.d[j].on : true;
|
||||
a.t = event.d[j].h * 3600000 + event.d[j].m * 60000;
|
||||
a.dow = ((dow&63)<<1) | (dow>>6); // Gadgetbridge sends DOW in a different format
|
||||
a.rp = rp;
|
||||
a.last = last;
|
||||
alarms.push(a);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"id": "android",
|
||||
"name": "Android Integration",
|
||||
"shortName": "Android",
|
||||
"version": "0.32",
|
||||
"version": "0.33",
|
||||
"description": "Display notifications/music/etc sent from the Gadgetbridge app on Android. This replaces the old 'Gadgetbridge' Bangle.js widget.",
|
||||
"icon": "app.png",
|
||||
"tags": "tool,system,messages,notifications,gadgetbridge",
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
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)
|
||||
|
||||
Tap the screen in the top or bottom half to scroll the METAR text (in case not
|
||||
the whole report fits on the screen).
|
||||
the whole report fits on the screen). You can also tap the watch from the top
|
||||
or bottom to scroll, which works even with the screen locked.
|
||||
|
||||
The colour of the METAR text will change to orange if the report is more than
|
||||
1h old, and red if it's older than 1.5h.
|
||||
|
||||
To toggle the seconds display, double tap the watch from either the left or
|
||||
right. This only changes the display "temporarily" (ie. it doesn't change the
|
||||
default configured through the settings).
|
||||
|
||||
|
||||
## Settings
|
||||
|
||||
- **Show Seconds**: to conserve battery power, you can turn the seconds display off
|
||||
- **Show Seconds**: to conserve battery power, you can turn the seconds display off (as the default)
|
||||
- **Invert Scrolling**: swaps the METAR scrolling direction of the top and bottom taps
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -169,6 +169,7 @@ function drawSeconds() {
|
|||
let seconds = now.getSeconds().toString();
|
||||
if (seconds.length == 1) seconds = '0' + seconds;
|
||||
let y = Bangle.appRect.y + mainTimeHeight - 3;
|
||||
g.setBgColor(g.theme.bg);
|
||||
g.setFontAlign(-1, 1).setFont("Vector", secondaryFontHeight).setColor(COLOUR_GREY);
|
||||
g.drawString(seconds, horizontalCenter + 54, y, true);
|
||||
}
|
||||
|
|
@ -232,10 +233,10 @@ function draw() {
|
|||
// initialise
|
||||
g.clear(true);
|
||||
|
||||
// scroll METAR lines on taps
|
||||
Bangle.setUI("clockupdown", action => {
|
||||
// scroll METAR lines (either by touch or tap)
|
||||
function scrollAVWX(action) {
|
||||
switch (action) {
|
||||
case -1: // top tap
|
||||
case -1: // top touch/tap
|
||||
if (settings.invertScrolling) {
|
||||
if (METARscollLines > 0)
|
||||
METARscollLines--;
|
||||
|
|
@ -244,7 +245,7 @@ Bangle.setUI("clockupdown", action => {
|
|||
METARscollLines++;
|
||||
}
|
||||
break;
|
||||
case 1: // bottom tap
|
||||
case 1: // bottom touch/tap
|
||||
if (settings.invertScrolling) {
|
||||
if (METARscollLines < METARlinesCount - 4)
|
||||
METARscollLines++;
|
||||
|
|
@ -254,11 +255,41 @@ Bangle.setUI("clockupdown", action => {
|
|||
}
|
||||
break;
|
||||
default:
|
||||
// ignore
|
||||
// ignore other actions
|
||||
}
|
||||
drawAVWX();
|
||||
}
|
||||
|
||||
Bangle.on('tap', data => {
|
||||
switch (data.dir) {
|
||||
case 'top':
|
||||
scrollAVWX(-1);
|
||||
break;
|
||||
case 'bottom':
|
||||
scrollAVWX(1);
|
||||
break;
|
||||
case 'left':
|
||||
case 'right':
|
||||
// toggle seconds display on double taps left or right
|
||||
if (data.double) {
|
||||
if (settings.showSeconds) {
|
||||
clearInterval(secondsInterval);
|
||||
let y = Bangle.appRect.y + mainTimeHeight - 3;
|
||||
g.clearRect(horizontalCenter + 54, y - secondaryFontHeight, g.getWidth(), y);
|
||||
settings.showSeconds = false;
|
||||
} else {
|
||||
settings.showSeconds = true;
|
||||
syncSecondsUpdate();
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// ignore other taps
|
||||
}
|
||||
});
|
||||
|
||||
Bangle.setUI("clockupdown", scrollAVWX);
|
||||
|
||||
// load widgets
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
|
|
@ -276,8 +307,8 @@ updateAVWX();
|
|||
|
||||
|
||||
// TMP for debugging:
|
||||
//METAR = 'YAAA 011100Z 21014KT CAVOK 23/08 Q1018 RMK RF000/0000';
|
||||
//METAR = 'YAAA 150900Z 14012KT 9999 SCT045 BKN064 26/14 Q1012 RMK RF000/0000 DL-W/DL-NW';
|
||||
//METAR = 'YAAA 020030Z VRB CAVOK';
|
||||
//METAR = 'YAAA 011100Z 21014KT CAVOK 23/08 Q1018 RMK RF000/0000'; drawAVWX();
|
||||
//METAR = 'YAAA 150900Z 14012KT 9999 SCT045 BKN064 26/14 Q1012 RMK RF000/0000 DL-W/DL-NW'; drawAVWX();
|
||||
//METAR = 'YAAA 020030Z VRB CAVOK'; drawAVWX();
|
||||
//METARts = new Date(Date.now() - 61 * 60000); // 61 to trigger warning, 91 to trigger alert
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@
|
|||
"< Back" : () => back(),
|
||||
'Show Seconds': {
|
||||
value: !!settings.showSeconds, // !! converts undefined to false
|
||||
format: v => v ? "On" : "Off",
|
||||
onchange: v => {
|
||||
settings.showSeconds = v;
|
||||
writeSettings();
|
||||
|
|
@ -25,7 +24,6 @@
|
|||
},
|
||||
'Invert Scrolling': {
|
||||
value: !!settings.invertScrolling, // !! converts undefined to false
|
||||
format: v => v ? "On" : "Off",
|
||||
onchange: v => {
|
||||
settings.invertScrolling = v;
|
||||
writeSettings();
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"id": "aviatorclk",
|
||||
"name": "Aviator Clock",
|
||||
"shortName":"AV8R Clock",
|
||||
"version":"1.00",
|
||||
"version":"1.01",
|
||||
"description": "A clock for aviators, with local time and UTC - and the latest METAR for the nearest airport",
|
||||
"icon": "aviatorclk.png",
|
||||
"screenshots": [{ "url": "screenshot.png" }, { "url": "screenshot2.png" }],
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
"shortName":"BTHome T",
|
||||
"version":"0.02",
|
||||
"description": "Displays temperature and pressure, and advertises them over bluetooth using BTHome.io standard",
|
||||
"description": "Displays temperature and pressure, and advertises them over bluetooth for Home Assistant using BTHome.io standard",
|
||||
"icon": "app.png",
|
||||
"tags": "bthome,bluetooth,temperature",
|
||||
"supports" : ["BANGLEJS2"],
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
0.01: New App!
|
||||
0.02: Disable not existing BTN3 on Bangle.js 2, set maximum transmit power
|
||||
0.03: Now use BTN2 on Bangle.js 1, and on Bangle.js 2 use the middle button to return to the menu
|
||||
|
|
@ -14,7 +14,8 @@ with 4 options:
|
|||
with this address will be connected to directly. If not specified a menu
|
||||
showing available Espruino devices is popped up.
|
||||
* **RX** - If checked, the app will display any data received from the
|
||||
device being connected to. Use this if you want to print data - eg: `print(E.getBattery())`
|
||||
device being connected to (waiting 500ms after the last data before disconnecting).
|
||||
Use this if you want to print data - eg: `print(E.getBattery())`
|
||||
|
||||
When done, click 'Upload'. Your changes will be saved to local storage
|
||||
so they'll be remembered next time you upload from the same device.
|
||||
|
|
@ -25,4 +26,9 @@ Simply load the app and you'll see a menu with the menu items
|
|||
you defined. Select one and you'll be able to connect to the device
|
||||
and send the command.
|
||||
|
||||
If a command should wait for a response then
|
||||
The Bangle will connect to the device, send the command, and if:
|
||||
|
||||
* `RX` isn't set it will disconnect immediately and return to the menu
|
||||
* `RX` is set it will listen for a response and write it to the screen, before
|
||||
disconnecting after 500ms of inactivity. To return to the menu after this, press the button.
|
||||
|
||||
|
|
|
|||
|
|
@ -194,16 +194,14 @@ function sendCommandRX(device, text, callback) {
|
|||
function done() {
|
||||
Terminal.println("\\n============\\n Disconnected");
|
||||
device.disconnect();
|
||||
if (global.BTN3 !== undefined) {
|
||||
setTimeout(function() {
|
||||
setWatch(function() {
|
||||
if (callback) callback();
|
||||
resolve();
|
||||
}, BTN3);
|
||||
g.reset().setFont("6x8",2).setFontAlign(0,0,1);
|
||||
g.drawString("Back", g.getWidth()-10, g.getHeight()-50);
|
||||
}, 200);
|
||||
}
|
||||
setTimeout(function() {
|
||||
setWatch(function() {
|
||||
if (callback) callback();
|
||||
resolve();
|
||||
}, (process.env.HWVERSION==2) ? BTN1 : BTN2);
|
||||
g.reset().setFont("6x8",2).setFontAlign(0,0,1);
|
||||
g.drawString("Back", g.getWidth()-10, g.getHeight()/2);
|
||||
}, 200);
|
||||
}
|
||||
device.getPrimaryService("6e400001-b5a3-f393-e0a9-e50e24dcca9e").then(function(s) {
|
||||
service = s;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"id": "espruinoctrl",
|
||||
"name": "Espruino Control",
|
||||
"shortName": "Espruino Ctrl",
|
||||
"version": "0.02",
|
||||
"version": "0.03",
|
||||
"description": "Send commands to other Espruino devices via the Bluetooth UART interface. Customisable commands!",
|
||||
"icon": "app.png",
|
||||
"tags": "tool,bluetooth",
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<link rel="stylesheet" href="../../css/spectre.min.css">
|
||||
</head>
|
||||
<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>
|
||||
|
||||
<div id="fw-unknown">
|
||||
|
|
@ -32,7 +32,7 @@
|
|||
bit of code that runs when Bangle.js starts, and it is able to update the
|
||||
Bangle.js firmware. Normally you would update firmware via this Firmware
|
||||
Updater app, but if for some reason Bangle.js will not boot, you can
|
||||
<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>
|
||||
<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>
|
||||
|
|
@ -42,7 +42,7 @@
|
|||
<div id="advanced-div" style="display:none">
|
||||
<p><b>Advanced</b></p>
|
||||
<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
|
||||
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>
|
||||
|
|
@ -58,6 +58,15 @@
|
|||
|
||||
<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/espruinotools.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]);
|
||||
var version = `unknown (CRC ${crcs[1]})`;
|
||||
var ok = true;
|
||||
if (crcs[0] == 1787004733) { // check 6 page CRC - the 7th page isn't used in 2v20
|
||||
version = "2v20";
|
||||
} else { // for other versions all 7 pages are used, check those
|
||||
if (crcs[0] == 1787004733) version = "2v20"; // check 6 page CRCs - the 7th page isn't used in 2v20+
|
||||
else if (crcs[0] == 3816337552) version = "2v21";
|
||||
else { // for other versions all 7 pages are used, check those
|
||||
var crc = crcs[1];
|
||||
if (crc==1339551013) { version = "2v10.219"; ok = false; }
|
||||
if (crc==1207580954) { version = "2v10.236"; ok = false; }
|
||||
|
|
@ -436,13 +445,20 @@ function handleUpload() {
|
|||
|
||||
document.getElementById('fileLoader').addEventListener('change', handleFileSelect, false);
|
||||
document.getElementById("upload").addEventListener("click", handleUpload);
|
||||
document.getElementById("info-btn").addEventListener("click", function() {
|
||||
document.getElementById("info-btn").addEventListener("click", function(e) {
|
||||
document.getElementById("info-btn").style = "display:none";
|
||||
document.getElementById("info-div").style = "";
|
||||
e.preventDefault();
|
||||
});
|
||||
document.getElementById("advanced-btn").addEventListener("click", function() {
|
||||
document.getElementById("advanced-btn").addEventListener("click", function(e) {
|
||||
document.getElementById("advanced-btn").style = "display:none";
|
||||
document.getElementById("advanced-div").style = "";
|
||||
e.preventDefault();
|
||||
});
|
||||
document.getElementById("changelog-btn").addEventListener("click", function(e) {
|
||||
document.getElementById("changelog-btn").style = "display:none";
|
||||
document.getElementById("changelog-div").style = "";
|
||||
e.preventDefault();
|
||||
});
|
||||
setTimeout(checkForFileOnServer, 10);
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@
|
|||
"< Back": () => back(),
|
||||
'Front Tap:': {
|
||||
value: (appSettings.enableTap === true),
|
||||
format: v => v ? "On" : "Off",
|
||||
onchange: v => {
|
||||
appSettings.enableTap = v;
|
||||
writeSettings();
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"id": "ha",
|
||||
"name": "Home Assistant",
|
||||
"version": "0.10",
|
||||
"description": "Integrates your Bangle.js into Home Assistant.",
|
||||
"description": "Integrates your Bangle.js into Home Assistant using Android Integration/Gadgetbridge",
|
||||
"icon": "ha.png",
|
||||
"type": "app",
|
||||
"tags": "tool,clkinfo,bluetooth",
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"name": "Home Assistant Sensors",
|
||||
"shortName": "HA sensors",
|
||||
"version": "0.02",
|
||||
"description": "Send sensor values to Home Assistant using the Android Integration.",
|
||||
"description": "Send sensor values to Home Assistant using Android Integration/Gadgetbridge",
|
||||
"icon": "ha.png",
|
||||
"type": "bootloader",
|
||||
"tags": "tool,sensors",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
"icon": "app.png",
|
||||
"type":"textinput",
|
||||
"default": true,
|
||||
"tags": "keyboard",
|
||||
"supports" : ["BANGLEJS2"],
|
||||
"screenshots": [{"url":"screenshot.png"}],
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
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
|
||||
|
||||
Run the app, and ensure you're not connected to your watch via Bluetooth
|
||||
(a warning will pop up if so).
|
||||
Run the app, then choose the type of controls you want and ensure you're not connected
|
||||
to your watch via Bluetooth (a warning will pop up if so).
|
||||
|
||||
Linear mode controls A/B axes individually, and allows you to vary the speed of the
|
||||
motors based on the distance you drag from the centre. Other modes just use on/off
|
||||
buttons.
|
||||
|
||||
| Mode | up | down | left | right |
|
||||
|------------|------|------|------|-------|
|
||||
| **Linear** | +A | -A | -B | +B |
|
||||
| **Normal** | +A | -A | -B | +B |
|
||||
| **Tank** | -A+B | +A-B | +A+B | -A-B |
|
||||
| **Merged** | -A-B | +A+B | +A-B | -A+B |
|
||||
|
||||
In all cases pressing the C/D buttons will turn on C/D outputs
|
||||
|
||||
Now press the arrow keys on the screen to control the robot.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
var lego = require("mouldking");
|
||||
lego.start();
|
||||
E.on('kill', () => {
|
||||
// return to normal Bluetooth advertising
|
||||
NRF.setAdvertising({},{showName:true});
|
||||
|
|
@ -12,59 +11,133 @@ var controlState = "";
|
|||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
var R = Bangle.appRect;
|
||||
// we'll divide up into 3x3
|
||||
function getBoxCoords(x,y) {
|
||||
return {
|
||||
x : R.x + R.w*x/3,
|
||||
y : R.y + R.h*y/3
|
||||
};
|
||||
}
|
||||
|
||||
function draw() {
|
||||
g.reset().clearRect(R);
|
||||
var c, ninety = Math.PI/2;
|
||||
var colOn = "#f00", colOff = g.theme.fg;
|
||||
c = getBoxCoords(1.5, 0.5);
|
||||
g.setColor(controlState=="up"?colOn:colOff).drawImage(arrowIcon,c.x,c.y,{rotate:0});
|
||||
c = getBoxCoords(2.5, 1.5);
|
||||
g.setColor(controlState=="right"?colOn:colOff).drawImage(arrowIcon,c.x,c.y,{rotate:ninety});
|
||||
c = getBoxCoords(0.5, 1.5);
|
||||
g.setColor(controlState=="left"?colOn:colOff).drawImage(arrowIcon,c.x,c.y,{rotate:-ninety});
|
||||
c = getBoxCoords(1.5, 1.5);
|
||||
g.setColor(controlState=="down"?colOn:colOff).drawImage(arrowIcon,c.x,c.y,{rotate:ninety*2});
|
||||
if (NRF.getSecurityStatus().connected) {
|
||||
c = getBoxCoords(1.5, 2.5);
|
||||
g.setFontAlign(0,0).setFont("6x8").drawString("WARNING:\nBluetooth Connected\nYou must disconnect\nbefore LEGO will work",c.x,c.y);
|
||||
function startLegoButtons(controls) {
|
||||
// we'll divide up into 3x3
|
||||
function getBoxCoords(x,y) {
|
||||
return {
|
||||
x : R.x + R.w*x/3,
|
||||
y : R.y + R.h*y/3
|
||||
};
|
||||
}
|
||||
}
|
||||
draw();
|
||||
NRF.on('connect', draw);
|
||||
NRF.on('disconnect', draw);
|
||||
|
||||
function setControlState(s) {
|
||||
controlState = s;
|
||||
var c = {};
|
||||
var speed = 3;
|
||||
if (s=="up") c={a:-speed,b:-speed};
|
||||
if (s=="down") c={a:speed,b:speed};
|
||||
if (s=="left") c={a:speed,b:-speed};
|
||||
if (s=="right") c={a:-speed,b:speed};
|
||||
function draw() {
|
||||
g.reset().clearRect(R);
|
||||
var c, ninety = Math.PI/2;
|
||||
var colOn = "#f00", colOff = g.theme.fg;
|
||||
c = getBoxCoords(1.5, 0.5);
|
||||
g.setColor(controlState=="up"?colOn:colOff).drawImage(arrowIcon,c.x,c.y,{rotate:0});
|
||||
c = getBoxCoords(2.5, 1.5);
|
||||
g.setColor(controlState=="right"?colOn:colOff).drawImage(arrowIcon,c.x,c.y,{rotate:ninety});
|
||||
c = getBoxCoords(0.5, 1.5);
|
||||
g.setColor(controlState=="left"?colOn:colOff).drawImage(arrowIcon,c.x,c.y,{rotate:-ninety});
|
||||
c = getBoxCoords(1.5, 1.5);
|
||||
g.setColor(controlState=="down"?colOn:colOff).drawImage(arrowIcon,c.x,c.y,{rotate:ninety*2});
|
||||
if (NRF.getSecurityStatus().connected) {
|
||||
c = getBoxCoords(1.5, 2.5);
|
||||
g.setFontAlign(0,0).setFont("6x8").drawString("WARNING:\nBluetooth Connected\nYou must disconnect\nbefore LEGO will work",c.x,c.y);
|
||||
}
|
||||
g.setFont("6x8:3").setFontAlign(0,0);
|
||||
c = getBoxCoords(0.5, 0.5);
|
||||
g.setColor(controlState=="c"?colOn:colOff).drawString("C",c.x,c.y);
|
||||
c = getBoxCoords(2.5, 0.5);
|
||||
g.setColor(controlState=="d"?colOn:colOff).drawString("D",c.x,c.y);
|
||||
}
|
||||
|
||||
function setControlState(s) {
|
||||
controlState = s;
|
||||
var c = {};
|
||||
if (s in controls)
|
||||
c = controls[s];
|
||||
draw();
|
||||
lego.set(c);
|
||||
}
|
||||
|
||||
lego.start();
|
||||
Bangle.setUI({mode:"custom", drag : e => {
|
||||
var x = Math.floor(E.clip((e.x - R.x) * 3 / R.w,0,2.99));
|
||||
var y = Math.floor(E.clip((e.y - R.y) * 3 / R.h,0,2.99));
|
||||
if (!e.b) {
|
||||
setControlState("");
|
||||
return;
|
||||
}
|
||||
if (y==0) { // top row
|
||||
if (x==0) setControlState("c");
|
||||
if (x==1) setControlState("up");
|
||||
if (x==2) setControlState("d");
|
||||
} else if (y==1) {
|
||||
if (x==0) setControlState("left");
|
||||
if (x==1) setControlState("down");
|
||||
if (x==2) setControlState("right");
|
||||
}
|
||||
}});
|
||||
|
||||
draw();
|
||||
lego.set(c);
|
||||
NRF.on('connect', draw);
|
||||
NRF.on('disconnect', draw);
|
||||
}
|
||||
|
||||
Bangle.on('drag',e => {
|
||||
var x = Math.floor(E.clip((e.x - R.x) * 3 / R.w,0,2.99));
|
||||
var y = Math.floor(E.clip((e.y - R.y) * 3 / R.h,0,2.99));
|
||||
if (!e.b) {
|
||||
setControlState("");
|
||||
return;
|
||||
}
|
||||
if (y==0) { // top row
|
||||
if (x==1) setControlState("up");
|
||||
} else if (y==1) {
|
||||
if (x==0) setControlState("left");
|
||||
if (x==1) setControlState("down");
|
||||
if (x==2) setControlState("right");
|
||||
function startLegoLinear() {
|
||||
var mx = R.x+R.w/2;
|
||||
var my = R.y+R.h/2;
|
||||
var x=0,y=0;
|
||||
var scale = 10;
|
||||
|
||||
function draw() {
|
||||
g.reset().clearRect(R);
|
||||
for (var i=3;i<60;i+=10)
|
||||
g.drawCircle(mx,my,i);
|
||||
g.setColor("#F00");
|
||||
var px = E.clip(mx + x*scale, R.x+20, R.x2-20);
|
||||
var py = E.clip(my + y*scale, R.y+20, R.y2-20);
|
||||
g.fillCircle(px, py, 20);
|
||||
}
|
||||
|
||||
lego.start();
|
||||
Bangle.setUI({mode:"custom", drag : e => {
|
||||
x = Math.round((e.x - mx) / scale);
|
||||
y = Math.round((e.y - my) / scale);
|
||||
if (!e.b) {
|
||||
x=0; y=0;
|
||||
}
|
||||
lego.set({a:x, b:y});
|
||||
draw();
|
||||
}});
|
||||
|
||||
draw();
|
||||
NRF.on('connect', draw);
|
||||
NRF.on('disconnect', draw);
|
||||
}
|
||||
|
||||
// Mappings of button to output
|
||||
const CONTROLS = {
|
||||
normal : {
|
||||
up : {a: 7,b: 0},
|
||||
down : {a:-7,b: 0},
|
||||
left : {a: 0,b:-7},
|
||||
right: {a: 0,b: 7},
|
||||
c : {c:7},
|
||||
d : {d:7}
|
||||
}, tank : {
|
||||
up : {a:-7,b:7},
|
||||
down : {a: 7,b:-7},
|
||||
left : {a: 7,b:7},
|
||||
right: {a:-7,b:-7},
|
||||
c : {c:7},
|
||||
d : {d:7}
|
||||
}, merged : {
|
||||
up : {a: 7,b: 7},
|
||||
down : {a:-7,b:-7},
|
||||
left : {a: 7,b:-7},
|
||||
right: {a:-7,b: 7},
|
||||
c : {c:7},
|
||||
d : {d:7}
|
||||
}
|
||||
};
|
||||
|
||||
E.showMenu({ "" : {title:"LEGO Remote", back:()=>load()},
|
||||
"Linear" : () => startLegoLinear(),
|
||||
"Normal" : () => startLego(CONTROLS.normal),
|
||||
"Tank" : () => startLego(CONTROLS.tank),
|
||||
"Marged" : () => startLego(CONTROLS.merged),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{ "id": "legoremote",
|
||||
"name": "LEGO Remote control",
|
||||
"shortName":"LEGO Remote",
|
||||
"version":"0.01",
|
||||
"version":"0.02",
|
||||
"description": "Use your Bangle.js to control LEGO models. See the README for compatibility",
|
||||
"icon": "app.png",
|
||||
"tags": "toy,lego,bluetooth",
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
'': { 'title': 'Welcome App' },
|
||||
'Run next boot': {
|
||||
value: !settings.welcomed,
|
||||
format: v => v ? 'Yes' : 'No',
|
||||
onchange: v => require('Storage').write('mywelcome.json', {welcomed: !v}),
|
||||
},
|
||||
'Run Now': () => load('mywelcome.app.js'),
|
||||
|
|
|
|||
|
|
@ -33,4 +33,5 @@
|
|||
0.26: Ensure that when redrawing, we always cancel any in-progress track draw
|
||||
0.27: Display message if no map is installed
|
||||
0.28: Fix rounding errors
|
||||
0.29: move exit to bottom of menu
|
||||
0.29: Keep exit at bottom of menu
|
||||
Speed up latLonToXY for track rendering
|
||||
|
|
@ -38,17 +38,17 @@ if (m.map) {
|
|||
m.lat = m.map.lat; // position of middle of screen
|
||||
m.lon = m.map.lon; // position of middle of screen
|
||||
}
|
||||
var CX = g.getWidth()/2;
|
||||
var CY = g.getHeight()/2;
|
||||
|
||||
// return number of tiles drawn
|
||||
exports.draw = function() {
|
||||
var cx = g.getWidth()/2;
|
||||
var cy = g.getHeight()/2;
|
||||
var p = Bangle.project({lat:m.lat,lon:m.lon});
|
||||
let count = 0;
|
||||
m.maps.forEach((map,idx) => {
|
||||
var d = map.scale/m.scale;
|
||||
var ix = (p.x-map.center.x)/m.scale + (map.imgx*d/2) - cx;
|
||||
var iy = (map.center.y-p.y)/m.scale + (map.imgy*d/2) - cy;
|
||||
var ix = (p.x-map.center.x)/m.scale + (map.imgx*d/2) - CX;
|
||||
var iy = (map.center.y-p.y)/m.scale + (map.imgy*d/2) - CY;
|
||||
var o = {};
|
||||
var s = map.tilesize;
|
||||
if (d!=1) { // if the two are different, add scaling
|
||||
|
|
@ -85,14 +85,12 @@ exports.draw = function() {
|
|||
};
|
||||
|
||||
/// Convert lat/lon to pixels on the screen
|
||||
exports.latLonToXY = function(lat, lon) {
|
||||
var p = Bangle.project({lat:m.lat,lon:m.lon});
|
||||
var q = Bangle.project({lat:lat, lon:lon});
|
||||
var cx = g.getWidth()/2;
|
||||
var cy = g.getHeight()/2;
|
||||
exports.latLonToXY = function(lat, lon) { "ram"
|
||||
var p = Bangle.project({lat:m.lat,lon:m.lon}),
|
||||
q = Bangle.project({lat:lat, lon:lon});
|
||||
return {
|
||||
x : Math.round((q.x-p.x)/m.scale + cx),
|
||||
y : Math.round(cy - (q.y-p.y)/m.scale)
|
||||
x : Math.round((q.x-p.x)/m.scale + CX),
|
||||
y : Math.round(CY - (q.y-p.y)/m.scale)
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -102,4 +100,4 @@ exports.scroll = function(x,y) {
|
|||
var b = Bangle.project({lat:m.lat+1,lon:m.lon+1});
|
||||
this.lon += x * m.scale / (a.x-b.x);
|
||||
this.lat -= y * m.scale / (a.y-b.y);
|
||||
};
|
||||
};
|
||||
|
|
@ -45,3 +45,4 @@
|
|||
0.36: When recording with 1 second periods, log time with one decimal.
|
||||
0.37: 1 second periods + gps log => log when gps event is received, not with
|
||||
setInterval.
|
||||
0.38: Tweaks to speed up track rendering
|
||||
|
|
@ -213,230 +213,230 @@ function viewTrack(filename, info) {
|
|||
});
|
||||
};
|
||||
menu['< Back'] = () => { viewTracks(); };
|
||||
return E.showMenu(menu);
|
||||
}
|
||||
|
||||
function plotTrack(info) { "ram"
|
||||
function distance(lat1,long1,lat2,long2) { "ram"
|
||||
var x = (long1-long2) * Math.cos((lat1+lat2)*Math.PI/360);
|
||||
var y = lat2 - lat1;
|
||||
return Math.sqrt(x*x + y*y) * 6371000 * Math.PI / 180;
|
||||
}
|
||||
function plotTrack(info) { "ram"
|
||||
function distance(lat1,long1,lat2,long2) { "ram"
|
||||
var x = (long1-long2) * Math.cos((lat1+lat2)*Math.PI/360);
|
||||
var y = lat2 - lat1;
|
||||
return Math.sqrt(x*x + y*y) * 6371000 * Math.PI / 180;
|
||||
}
|
||||
|
||||
// Function to convert lat/lon to XY
|
||||
var getMapXY;
|
||||
if (info.qOSTM) {
|
||||
// scale map to view full track
|
||||
const max = Bangle.project({lat: info.maxLat, lon: info.maxLong});
|
||||
const min = Bangle.project({lat: info.minLat, lon: info.minLong});
|
||||
const scaleX = (max.x-min.x)/Bangle.appRect.w;
|
||||
const scaleY = (max.y-min.y)/Bangle.appRect.h;
|
||||
osm.scale = Math.ceil((scaleX > scaleY ? scaleX : scaleY)*1.1); // add 10% margin
|
||||
getMapXY = osm.latLonToXY.bind(osm);
|
||||
} else {
|
||||
getMapXY = function(lat, lon) { "ram"
|
||||
return {x:cx + Math.round((long - info.lon)*info.lfactor*info.scale),
|
||||
y:cy + Math.round((info.lat - lat)*info.scale)};
|
||||
};
|
||||
}
|
||||
// Function to convert lat/lon to XY
|
||||
var XY;
|
||||
if (info.qOSTM) {
|
||||
// scale map to view full track
|
||||
const max = Bangle.project({lat: info.maxLat, lon: info.maxLong});
|
||||
const min = Bangle.project({lat: info.minLat, lon: info.minLong});
|
||||
const scaleX = (max.x-min.x)/Bangle.appRect.w;
|
||||
const scaleY = (max.y-min.y)/Bangle.appRect.h;
|
||||
osm.scale = Math.ceil((scaleX > scaleY ? scaleX : scaleY)*1.1); // add 10% margin
|
||||
XY = osm.latLonToXY.bind(osm);
|
||||
} else {
|
||||
XY = function(lat, lon) { "ram"
|
||||
return {x:cx + Math.round((long - info.lon)*info.lfactor*info.scale),
|
||||
y:cy + Math.round((info.lat - lat)*info.scale)};
|
||||
};
|
||||
}
|
||||
|
||||
E.showMenu(); // remove menu
|
||||
E.showMessage(/*LANG*/"Drawing...",/*LANG*/"Track "+info.fn);
|
||||
g.flip(); // on buffered screens, draw a not saying we're busy
|
||||
g.clear(1);
|
||||
var s = require("Storage");
|
||||
var W = g.getWidth();
|
||||
var H = g.getHeight();
|
||||
var cx = W/2;
|
||||
var cy = 24 + (H-24)/2;
|
||||
if (!info.qOSTM) {
|
||||
g.setColor("#f00").fillRect(9,80,11,120).fillPoly([9,60,19,80,0,80]);
|
||||
g.setColor(g.theme.fg).setFont("6x8").setFontAlign(0,0).drawString("N",10,50);
|
||||
} else {
|
||||
osm.lat = info.lat;
|
||||
osm.lon = info.lon;
|
||||
osm.draw();
|
||||
g.setColor("#000");
|
||||
E.showMenu(); // remove menu
|
||||
E.showMessage(/*LANG*/"Drawing...",/*LANG*/"Track "+info.fn);
|
||||
g.flip(); // on buffered screens, draw a not saying we're busy
|
||||
g.clear(1);
|
||||
var s = require("Storage");
|
||||
var G = g;
|
||||
var W = g.getWidth();
|
||||
var H = g.getHeight();
|
||||
var cx = W/2;
|
||||
var cy = 24 + (H-24)/2;
|
||||
if (!info.qOSTM) {
|
||||
g.setColor("#f00").fillRect(9,80,11,120).fillPoly([9,60,19,80,0,80]);
|
||||
g.setColor(g.theme.fg).setFont("6x8").setFontAlign(0,0).drawString("N",10,50);
|
||||
} else {
|
||||
osm.lat = info.lat;
|
||||
osm.lon = info.lon;
|
||||
osm.draw();
|
||||
g.setColor("#000");
|
||||
}
|
||||
var latIdx = info.fields.indexOf("Latitude");
|
||||
var lonIdx = info.fields.indexOf("Longitude");
|
||||
g.drawString(asTime(info.duration),10,220);
|
||||
var f = require("Storage").open(info.filename,"r");
|
||||
if (f===undefined) return;
|
||||
var l = f.readLine(f);
|
||||
l = f.readLine(f); // skip headers
|
||||
var ox=0;
|
||||
var oy=0;
|
||||
var olat,olong,dist=0;
|
||||
var i=0, c = l.split(",");
|
||||
// skip until we find our first data
|
||||
while(l!==undefined && c[latIdx]=="") {
|
||||
c = l.split(",");
|
||||
l = f.readLine(f);
|
||||
}
|
||||
// now start plotting
|
||||
var lat = +c[latIdx];
|
||||
var long = +c[lonIdx];
|
||||
var mp = XY(lat, long);
|
||||
g.moveTo(mp.x,mp.y);
|
||||
g.setColor("#0f0");
|
||||
g.fillCircle(mp.x,mp.y,5);
|
||||
if (info.qOSTM) g.setColor("#f09");
|
||||
else g.setColor(g.theme.fg);
|
||||
l = f.readLine(f);
|
||||
g.flip(); // force update
|
||||
while(l!==undefined) {
|
||||
c = l.split(",");l = f.readLine(f);
|
||||
if (c[latIdx]=="")continue;
|
||||
lat = +c[latIdx];
|
||||
long = +c[lonIdx];
|
||||
mp = XY(lat, long);
|
||||
G.lineTo(mp.x,mp.y);
|
||||
if (info.qOSTM) G.fillCircle(mp.x,mp.y,2); // make the track more visible
|
||||
var d = distance(olat,olong,lat,long);
|
||||
if (!isNaN(d)) dist+=d;
|
||||
olat = lat;
|
||||
olong = long;
|
||||
ox = mp.x;
|
||||
oy = mp.y;
|
||||
if (++i > 100) { G.flip();i=0; }
|
||||
}
|
||||
g.setColor("#f00");
|
||||
g.fillCircle(ox,oy,5);
|
||||
if (info.qOSTM) g.setColor("#000");
|
||||
else g.setColor(g.theme.fg);
|
||||
g.drawString(require("locale").distance(dist,2),g.getWidth() / 2, g.getHeight() - 20);
|
||||
g.setFont("6x8",2);
|
||||
g.setFontAlign(0,0,3);
|
||||
var isBTN3 = "BTN3" in global;
|
||||
g.drawString(/*LANG*/"Back",g.getWidth() - 10, isBTN3 ? (g.getHeight() - 40) : (g.getHeight()/2));
|
||||
setWatch(function() {
|
||||
viewTrack(info.fn, info);
|
||||
}, isBTN3?BTN3:BTN1);
|
||||
Bangle.drawWidgets();
|
||||
g.flip();
|
||||
}
|
||||
|
||||
function plotGraph(info, style) { "ram"
|
||||
E.showMenu(); // remove menu
|
||||
E.showMessage(/*LANG*/"Calculating...",/*LANG*/"Track "+info.fn);
|
||||
var filename = info.filename;
|
||||
var infn = new Float32Array(80);
|
||||
var infc = new Uint16Array(80);
|
||||
var title;
|
||||
var lt = 0; // last time
|
||||
var tn = 0; // count for each time period
|
||||
var strt, dur = info.duration;
|
||||
var f = require("Storage").open(filename,"r");
|
||||
if (f===undefined) return;
|
||||
var l = f.readLine(f);
|
||||
l = f.readLine(f); // skip headers
|
||||
var nl = 0, c, i;
|
||||
var factor = 1; // multiplier used for values when graphing
|
||||
var timeIdx = info.fields.indexOf("Time");
|
||||
if (l!==undefined) {
|
||||
c = l.split(",");
|
||||
strt = c[timeIdx];
|
||||
}
|
||||
if (style=="Heartrate") {
|
||||
title = /*LANG*/"Heartrate (bpm)";
|
||||
var hrmIdx = info.fields.indexOf("Heartrate");
|
||||
while(l!==undefined) {
|
||||
++nl;c=l.split(",");l = f.readLine(f);
|
||||
if (c[hrmIdx]=="") continue;
|
||||
i = Math.round(80*(c[timeIdx] - strt)/dur);
|
||||
infn[i]+=+c[hrmIdx];
|
||||
infc[i]++;
|
||||
}
|
||||
} else if (style=="Altitude") {
|
||||
title = /*LANG*/"Altitude (m)";
|
||||
var altIdx = info.fields.indexOf("Barometer Altitude");
|
||||
if (altIdx<0) altIdx = info.fields.indexOf("Altitude");
|
||||
while(l!==undefined) {
|
||||
++nl;c=l.split(",");l = f.readLine(f);
|
||||
if (c[altIdx]=="") continue;
|
||||
i = Math.round(80*(c[timeIdx] - strt)/dur);
|
||||
infn[i]+=+c[altIdx];
|
||||
infc[i]++;
|
||||
}
|
||||
} else if (style=="Speed") {
|
||||
// use locate to work out units
|
||||
var localeStr = require("locale").speed(1,5); // get what 1kph equates to
|
||||
let units = localeStr.replace(/[0-9.]*/,"");
|
||||
factor = parseFloat(localeStr)*3.6; // m/sec to whatever out units are
|
||||
// title
|
||||
title = /*LANG*/"Speed"+` (${units})`;
|
||||
var latIdx = info.fields.indexOf("Latitude");
|
||||
var lonIdx = info.fields.indexOf("Longitude");
|
||||
g.drawString(asTime(info.duration),10,220);
|
||||
var f = require("Storage").open(info.filename,"r");
|
||||
if (f===undefined) return;
|
||||
var l = f.readLine(f);
|
||||
l = f.readLine(f); // skip headers
|
||||
var ox=0;
|
||||
var oy=0;
|
||||
var olat,olong,dist=0;
|
||||
var i=0, c = l.split(",");
|
||||
// skip until we find our first data
|
||||
while(l!==undefined && c[latIdx]=="") {
|
||||
c = l.split(",");
|
||||
l = f.readLine(f);
|
||||
}
|
||||
// now start plotting
|
||||
var lat = +c[latIdx];
|
||||
var long = +c[lonIdx];
|
||||
var mp = getMapXY(lat, long);
|
||||
g.moveTo(mp.x,mp.y);
|
||||
g.setColor("#0f0");
|
||||
g.fillCircle(mp.x,mp.y,5);
|
||||
if (info.qOSTM) g.setColor("#f09");
|
||||
else g.setColor(g.theme.fg);
|
||||
l = f.readLine(f);
|
||||
g.flip(); // force update
|
||||
// now iterate
|
||||
var p,lp = Bangle.project({lat:c[1],lon:c[2]});
|
||||
var t,dx,dy,d,lt = c[timeIdx];
|
||||
while(l!==undefined) {
|
||||
c = l.split(",");l = f.readLine(f);
|
||||
if (c[latIdx]=="")continue;
|
||||
lat = +c[latIdx];
|
||||
long = +c[lonIdx];
|
||||
mp = getMapXY(lat, long);
|
||||
g.lineTo(mp.x,mp.y);
|
||||
if (info.qOSTM) g.fillCircle(mp.x,mp.y,2); // make the track more visible
|
||||
var d = distance(olat,olong,lat,long);
|
||||
if (!isNaN(d)) dist+=d;
|
||||
olat = lat;
|
||||
olong = long;
|
||||
ox = mp.x;
|
||||
oy = mp.y;
|
||||
if (++i > 100) { g.flip();i=0; }
|
||||
}
|
||||
g.setColor("#f00");
|
||||
g.fillCircle(ox,oy,5);
|
||||
if (info.qOSTM) g.setColor("#000");
|
||||
else g.setColor(g.theme.fg);
|
||||
g.drawString(require("locale").distance(dist,2),g.getWidth() / 2, g.getHeight() - 20);
|
||||
g.setFont("6x8",2);
|
||||
g.setFontAlign(0,0,3);
|
||||
var isBTN3 = "BTN3" in global;
|
||||
g.drawString(/*LANG*/"Back",g.getWidth() - 10, isBTN3 ? (g.getHeight() - 40) : (g.getHeight()/2));
|
||||
setWatch(function() {
|
||||
viewTrack(info.fn, info);
|
||||
}, isBTN3?BTN3:BTN1);
|
||||
Bangle.drawWidgets();
|
||||
g.flip();
|
||||
}
|
||||
|
||||
function plotGraph(info, style) { "ram"
|
||||
E.showMenu(); // remove menu
|
||||
E.showMessage(/*LANG*/"Calculating...",/*LANG*/"Track "+info.fn);
|
||||
var filename = info.filename;
|
||||
var infn = new Float32Array(80);
|
||||
var infc = new Uint16Array(80);
|
||||
var title;
|
||||
var lt = 0; // last time
|
||||
var tn = 0; // count for each time period
|
||||
var strt, dur = info.duration;
|
||||
var f = require("Storage").open(filename,"r");
|
||||
if (f===undefined) return;
|
||||
var l = f.readLine(f);
|
||||
l = f.readLine(f); // skip headers
|
||||
var nl = 0, c, i;
|
||||
var factor = 1; // multiplier used for values when graphing
|
||||
var timeIdx = info.fields.indexOf("Time");
|
||||
if (l!==undefined) {
|
||||
c = l.split(",");
|
||||
strt = c[timeIdx];
|
||||
}
|
||||
if (style=="Heartrate") {
|
||||
title = /*LANG*/"Heartrate (bpm)";
|
||||
var hrmIdx = info.fields.indexOf("Heartrate");
|
||||
while(l!==undefined) {
|
||||
++nl;c=l.split(",");l = f.readLine(f);
|
||||
if (c[hrmIdx]=="") continue;
|
||||
i = Math.round(80*(c[timeIdx] - strt)/dur);
|
||||
infn[i]+=+c[hrmIdx];
|
||||
++nl;c=l.split(",");
|
||||
l = f.readLine(f);
|
||||
if (c[latIdx] == "") {
|
||||
continue;
|
||||
}
|
||||
t = c[timeIdx];
|
||||
i = Math.round(80*(t - strt)/dur);
|
||||
p = Bangle.project({lat:c[latIdx],lon:c[lonIdx]});
|
||||
dx = p.x-lp.x;
|
||||
dy = p.y-lp.y;
|
||||
d = Math.sqrt(dx*dx+dy*dy);
|
||||
if (t!=lt) {
|
||||
infn[i]+=d / (t-lt); // speed
|
||||
infc[i]++;
|
||||
}
|
||||
} else if (style=="Altitude") {
|
||||
title = /*LANG*/"Altitude (m)";
|
||||
var altIdx = info.fields.indexOf("Barometer Altitude");
|
||||
if (altIdx<0) altIdx = info.fields.indexOf("Altitude");
|
||||
while(l!==undefined) {
|
||||
++nl;c=l.split(",");l = f.readLine(f);
|
||||
if (c[altIdx]=="") continue;
|
||||
i = Math.round(80*(c[timeIdx] - strt)/dur);
|
||||
infn[i]+=+c[altIdx];
|
||||
infc[i]++;
|
||||
}
|
||||
} else if (style=="Speed") {
|
||||
// use locate to work out units
|
||||
var localeStr = require("locale").speed(1,5); // get what 1kph equates to
|
||||
let units = localeStr.replace(/[0-9.]*/,"");
|
||||
factor = parseFloat(localeStr)*3.6; // m/sec to whatever out units are
|
||||
// title
|
||||
title = /*LANG*/"Speed"+` (${units})`;
|
||||
var latIdx = info.fields.indexOf("Latitude");
|
||||
var lonIdx = info.fields.indexOf("Longitude");
|
||||
// skip until we find our first data
|
||||
while(l!==undefined && c[latIdx]=="") {
|
||||
c = l.split(",");
|
||||
l = f.readLine(f);
|
||||
}
|
||||
// now iterate
|
||||
var p,lp = Bangle.project({lat:c[1],lon:c[2]});
|
||||
var t,dx,dy,d,lt = c[timeIdx];
|
||||
while(l!==undefined) {
|
||||
++nl;c=l.split(",");
|
||||
l = f.readLine(f);
|
||||
if (c[latIdx] == "") {
|
||||
continue;
|
||||
}
|
||||
t = c[timeIdx];
|
||||
i = Math.round(80*(t - strt)/dur);
|
||||
p = Bangle.project({lat:c[latIdx],lon:c[lonIdx]});
|
||||
dx = p.x-lp.x;
|
||||
dy = p.y-lp.y;
|
||||
d = Math.sqrt(dx*dx+dy*dy);
|
||||
if (t!=lt) {
|
||||
infn[i]+=d / (t-lt); // speed
|
||||
infc[i]++;
|
||||
}
|
||||
lp = p;
|
||||
lt = t;
|
||||
}
|
||||
} else throw new Error("Unknown type "+style);
|
||||
var min=100000,max=-100000;
|
||||
for (var i=0;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;
|
||||
lp = p;
|
||||
lt = t;
|
||||
}
|
||||
// work out a nice grid value
|
||||
var heightDiff = max-min;
|
||||
var grid = 1;
|
||||
while (heightDiff/grid > 8) {
|
||||
grid*=2;
|
||||
} 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; }
|
||||
}
|
||||
// draw
|
||||
g.clear(1).setFont("6x8",1);
|
||||
var r = require("graph").drawLine(g, infn, {
|
||||
x:4,y:24,
|
||||
width: g.getWidth()-24,
|
||||
height: g.getHeight()-(24+8),
|
||||
axes : true,
|
||||
gridy : grid,
|
||||
gridx : infn.length / 3,
|
||||
title: title,
|
||||
miny: min,
|
||||
maxy: max,
|
||||
xlabel : x=>Math.round(x*dur/(60*infn.length))+/*LANG*/" min" // minutes
|
||||
});
|
||||
g.setFont("6x8",2);
|
||||
g.setFontAlign(0,0,3);
|
||||
var isBTN3 = "BTN3" in global;
|
||||
g.drawString(/*LANG*/"Back",g.getWidth() - 10, isBTN3 ? (g.getHeight() - 40) : (g.getHeight()/2));
|
||||
setWatch(function() {
|
||||
viewTrack(info.filename, info);
|
||||
}, isBTN3?BTN3:BTN1);
|
||||
g.flip();
|
||||
var n = infn[i];
|
||||
if (n>max) max=n;
|
||||
if (n<min) min=n;
|
||||
}
|
||||
|
||||
return E.showMenu(menu);
|
||||
// work out a nice grid value
|
||||
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",
|
||||
"name": "Recorder",
|
||||
"shortName": "Recorder",
|
||||
"version": "0.37",
|
||||
"version": "0.38",
|
||||
"description": "Record GPS position, heart rate and more in the background, then download to your PC.",
|
||||
"icon": "app.png",
|
||||
"tags": "tool,outdoors,gps,widget,clkinfo",
|
||||
|
|
|
|||
|
|
@ -77,3 +77,4 @@ of 'Select Clock'
|
|||
calibration was done.
|
||||
0.67: Rename 'Wake on BTN1/Touch' to 'Wake on Button/Tap' on Bangle.js 2
|
||||
0.68: Fix syntax error
|
||||
0.69: Add option to wake on double tap
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "setting",
|
||||
"name": "Settings",
|
||||
"version": "0.68",
|
||||
"version": "0.69",
|
||||
"description": "A menu for setting up Bangle.js",
|
||||
"icon": "settings.png",
|
||||
"tags": "tool,system",
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ function updateOptions() {
|
|||
var o = settings.options;
|
||||
// Check to make sure nobody disabled all wakeups and locked themselves out!
|
||||
if (BANGLEJS2) {
|
||||
if (!(o.wakeOnBTN1||o.wakeOnFaceUp||o.wakeOnTouch||o.wakeOnTwist)) {
|
||||
if (!(o.wakeOnBTN1||o.wakeOnFaceUp||o.wakeOnTouch||o.wakeOnDoubleTap||o.wakeOnTwist)) {
|
||||
o.wakeOnBTN1 = true;
|
||||
}
|
||||
} else {
|
||||
|
|
@ -451,48 +451,58 @@ function showLCDMenu() {
|
|||
}
|
||||
});
|
||||
|
||||
if (BANGLEJS2)
|
||||
if (BANGLEJS2) {
|
||||
Object.assign(lcdMenu, {
|
||||
/*LANG*/'Wake on Button': {
|
||||
value: settings.options.wakeOnBTN1,
|
||||
value: !!settings.options.wakeOnBTN1,
|
||||
onchange: () => {
|
||||
settings.options.wakeOnBTN1 = !settings.options.wakeOnBTN1;
|
||||
updateOptions();
|
||||
}
|
||||
},
|
||||
/*LANG*/'Wake on Tap': {
|
||||
value: settings.options.wakeOnTouch,
|
||||
value: !!settings.options.wakeOnTouch,
|
||||
onchange: () => {
|
||||
settings.options.wakeOnTouch = !settings.options.wakeOnTouch;
|
||||
updateOptions();
|
||||
}
|
||||
}
|
||||
});
|
||||
else
|
||||
if (process.env.VERSION.replace("v",0)>=2020)
|
||||
Object.assign(lcdMenu, {
|
||||
/*LANG*/'Wake on Double Tap': {
|
||||
value: !!settings.options.wakeOnDoubleTap,
|
||||
onchange: () => {
|
||||
settings.options.wakeOnDoubleTap = !settings.options.wakeOnDoubleTap;
|
||||
updateOptions();
|
||||
}
|
||||
}
|
||||
});
|
||||
} else
|
||||
Object.assign(lcdMenu, {
|
||||
/*LANG*/'Wake on BTN1': {
|
||||
value: settings.options.wakeOnBTN1,
|
||||
value: !!settings.options.wakeOnBTN1,
|
||||
onchange: () => {
|
||||
settings.options.wakeOnBTN1 = !settings.options.wakeOnBTN1;
|
||||
updateOptions();
|
||||
}
|
||||
},
|
||||
/*LANG*/'Wake on BTN2': {
|
||||
value: settings.options.wakeOnBTN2,
|
||||
value: !!settings.options.wakeOnBTN2,
|
||||
onchange: () => {
|
||||
settings.options.wakeOnBTN2 = !settings.options.wakeOnBTN2;
|
||||
updateOptions();
|
||||
}
|
||||
},
|
||||
/*LANG*/'Wake on BTN3': {
|
||||
value: settings.options.wakeOnBTN3,
|
||||
value: !!settings.options.wakeOnBTN3,
|
||||
onchange: () => {
|
||||
settings.options.wakeOnBTN3 = !settings.options.wakeOnBTN3;
|
||||
updateOptions();
|
||||
}
|
||||
},
|
||||
/*LANG*/'Wake on Touch': {
|
||||
value: settings.options.wakeOnTouch,
|
||||
value: !!settings.options.wakeOnTouch,
|
||||
onchange: () => {
|
||||
settings.options.wakeOnTouch = !settings.options.wakeOnTouch;
|
||||
updateOptions();
|
||||
|
|
@ -500,14 +510,14 @@ function showLCDMenu() {
|
|||
}});
|
||||
Object.assign(lcdMenu, {
|
||||
/*LANG*/'Wake on FaceUp': {
|
||||
value: settings.options.wakeOnFaceUp,
|
||||
value: !!settings.options.wakeOnFaceUp,
|
||||
onchange: () => {
|
||||
settings.options.wakeOnFaceUp = !settings.options.wakeOnFaceUp;
|
||||
updateOptions();
|
||||
}
|
||||
},
|
||||
/*LANG*/'Wake on Twist': {
|
||||
value: settings.options.wakeOnTwist,
|
||||
value: !!settings.options.wakeOnTwist,
|
||||
onchange: () => {
|
||||
settings.options.wakeOnTwist = !settings.options.wakeOnTwist;
|
||||
updateOptions();
|
||||
|
|
|
|||
|
|
@ -130,7 +130,6 @@
|
|||
},
|
||||
'Date Suffix:': {
|
||||
value: appSettings.enableSuffix,
|
||||
format: v => v ? 'Yes' : 'No',
|
||||
onchange: v => {
|
||||
appSettings.enableSuffix = v;
|
||||
writeSettings();
|
||||
|
|
@ -138,7 +137,6 @@
|
|||
},
|
||||
'Lead Zero:': {
|
||||
value: appSettings.enableLeadingZero,
|
||||
format: v => v ? 'Yes' : 'No',
|
||||
onchange: v => {
|
||||
appSettings.enableLeadingZero = v;
|
||||
writeSettings();
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
'Run next boot': {
|
||||
value: !settings.welcomed,
|
||||
format: v => v ? 'Yes' : 'No',
|
||||
onchange: v => require('Storage').write('welcome.json', {welcomed: !v}),
|
||||
},
|
||||
'Run Now': () => load('welcome.app.js'),
|
||||
|
|
|
|||
|
|
@ -263,7 +263,7 @@ apps.forEach((app,appIdx) => {
|
|||
WARN(`App ${app.id} has a setting file but no corresponding data entry (add \`"data":[{"name":"${app.id}.settings.json"}]\`)`, {file:appDirRelative+file.url});
|
||||
}
|
||||
// check for manual boolean formatter
|
||||
const m = fileContents.match(/format: *\(\) *=>.*["'](yes|on)["']/i);
|
||||
const m = fileContents.match(/format: *\(?\w*\)? *=>.*["'](yes|on)["']/i);
|
||||
if (m) {
|
||||
WARN(`Settings for ${app.id} has a boolean formatter - this is handled automatically, the line can be removed`, {file:appDirRelative+file.url, line: fileContents.substr(0, m.index).split("\n").length});
|
||||
}
|
||||
|
|
|
|||
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.';
|
||||
}
|
||||
|
||||
var RECOMMENDED_VERSION = "2v20";
|
||||
var RECOMMENDED_VERSION = "2v21";
|
||||
// could check http://www.espruino.com/json/BANGLEJS.json for this
|
||||
|
||||
// We're only interested in Bangles
|
||||
|
|
|
|||