Merge branch 'espruino:master' into master

master
nxdefiant 2024-02-10 09:38:24 +01:00 committed by GitHub
commit dc4b2dd741
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
89 changed files with 2515 additions and 333 deletions

View File

@ -1 +1 @@
0.01: New Widget!
0.01: New Clock Info!

View File

@ -0,0 +1 @@
0.01: New Clock!

View File

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

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwJC/AH4A/AH4AgA=="))

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

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

View File

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

View File

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

View File

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

View File

@ -1 +1,2 @@
1.00: initial release
1.01: added tap event to scroll METAR and toggle seconds display

View File

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

View File

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

View File

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

View File

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

1
apps/bthome/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: New App!

26
apps/bthome/README.md Normal file
View File

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

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

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEw4X/AAIHBy06nnnDiHwBRMDrgLJhtXBZM1qvABZHVqtwFxFVqowIhoLBGBE1q35GBHVrkDytyrAuGHIPVroLFFwODrklqoLFLoOCrALHLoIXILoVw+APBBYhdCsEAyphFFwITBgQDBMIgeBqtUgILCSQQuBrflBYW+SQYuBuENBYItB6owCXYUDBYIUBYYYuBh2wBYNQ9cFGAWlq0JsGUgNgy0J1WsEgMWhtwBYXXhWq1YLBkvD4HUgNwnk61Wq2ALBwEAkkBAYPq14kCktsgEMgZmBBIILDqoMBBQOWBIM61ALCrYLBh1WBYMKHgILBqxlBnILC2eqBYVVIAPlrWj1mg9QLDtkDyta1ns2AXEX4Va1c84YLEWYVa1XAhwLJ2B5BBZA6BBZOAC5UA5xHI1E8NYQAFh2g9hrCBY2vQYYAFgSPBF4QAFX4U6cgQLH9S/BAA2qcYYAG9WuPIILHOoKdBBY8D9WvgA"))

27
apps/bthome/app.js Normal file
View File

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

68
apps/bthome/boot.js Normal file
View File

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

17
apps/bthome/clkinfo.js Normal file
View File

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

BIN
apps/bthome/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

20
apps/bthome/metadata.json Normal file
View File

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

91
apps/bthome/settings.js Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -20,7 +20,6 @@
"< Back": () => back(),
'Front Tap:': {
value: (appSettings.enableTap === true),
format: v => v ? "On" : "Off",
onchange: v => {
appSettings.enableTap = v;
writeSettings();

View File

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

View File

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

1
apps/icons/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: New library

21
apps/icons/README.md Normal file
View File

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

BIN
apps/icons/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
apps/icons/gen/bike.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 510 B

BIN
apps/icons/gen/car.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 350 B

BIN
apps/icons/gen/close.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 B

BIN
apps/icons/gen/down.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 B

BIN
apps/icons/gen/fan.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 646 B

92
apps/icons/gen/generate.js Executable file
View File

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

BIN
apps/icons/gen/home.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 B

View File

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

BIN
apps/icons/gen/left.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 B

BIN
apps/icons/gen/light.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 452 B

BIN
apps/icons/gen/plug.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 B

BIN
apps/icons/gen/right.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 B

BIN
apps/icons/gen/rocket.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 675 B

BIN
apps/icons/gen/switch.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 B

BIN
apps/icons/gen/sync.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 445 B

BIN
apps/icons/gen/up.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 B

BIN
apps/icons/icons.img Normal file

Binary file not shown.

25
apps/icons/lib.js Normal file
View File

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

17
apps/icons/metadata.json Normal file
View File

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

View File

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

View File

@ -1 +1,2 @@
0.01: New App!
0.02: Add more control styles

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

9
apps/synthwave/README.md Normal file
View File

@ -0,0 +1,9 @@
# Synthwave Watch
Fly towards the sunset in a 3D jet, cruising to the sound of futuristic synthesizers*.
![](screenshot.png) ![](widgets.png) ![](theme.png)
Theme colors and widgets supported. Widgets only appear when the screen is locked.
* synthesizers not included

View File

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

760
apps/synthwave/app.js Normal file
View File

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

BIN
apps/synthwave/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

BIN
apps/synthwave/theme.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
apps/synthwave/widgets.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

9
apps/warpdrive/README.md Normal file
View File

@ -0,0 +1,9 @@
# WarpDrive
An animated watchface featuring 3D spaceships traveling just shy of ludicrous speed.
![](warpdrive.gif)
WE BREAK FOR NOBODY.
Theme colors and widgets supported. Widgets only appear when the screen is locked.

View File

@ -0,0 +1 @@
atob("MDDD/wAA//+nMRF8r3PznG1znfdmmccUUUUUMMQQQQQQQQUdFFc8/66rKKLBBBBBBBBBBB6zyy+mnFcUUUUUMMQQQQMQMddFFdk3266rLKLLBBBBBBBBK6y6x/7FddcUUUUUMMMQQQQMcdUdd9e66q6rLJqLBBBBBBBSpyqze65dcUccUUUUUMMMMMNdccdd9ce66pqqppqLBBBBBBOp6qu/6qpcccccccUUUMMOsOccEdFlcUW5q66ppprLJBBDDWyr64866rIcUcccUUUUUMMUXfEGkGlddUWpqJ6pppqLLhCjO26u4867LLIcdFlFl0UUUXEMVdEEGndcUUVpjKs30330303ra1Ng8327LLIUW0VlnF9FEXcEEQUOnHdcUUVppppA0033u77sxBBA036pqrIUUUUUWHHHFHFHUYQUdENcEcVpppppBL373swzBBLB3rK4irAUUUUUYYGHGHEMMYUUcMMEdcVp5pprBDBWc2hBBBBLbC7Ba6ocUUUUMUYGmEUYQMUUMUUMFddpppprK5A82rDDBDBKLLDDK64UUcefEQGlcMMMQMQUcUMMVFdppp/2pA36BBBBDJDKpppjDKsUcedHccYQQQYUUUUFccUUVcdpr+38rBBBBBBDJBDBJ3KJqK4UEVelcYUYUQUQUMYYcdcdcUVoprc6rK6hJLLBDBBBBCqqqKIEUWkUUenUUVdUMYUUQQQcUUWpqfrDK22rK4+rBDDJBC5BJpIUW8UUUddcfGnUFFEUMdccUUVqeBLKKbrLY82q6yuBCa66ppAW0UUUXcUUGnddddUUVcdFUUWeLDDLdrDI+7a66ypLJpyyrLC0UMUV8UUe8VccddUUUUdcUUOLBBCdrBDfC66J6rJJqKKyBBAUMMV0UMW0cccUcccUUUVUUMNBBCdqBBKKKprLJ1pqLDLDBBAMMeUUMMUUccUUW0UUUUMUMMRBD1qJBDDKJrLLfLLLBBDLDLAUHUUMMUUUUUUW0UUUUMUUUUNAqKJBBBDKLLLXLLDJBDBBDLAEUUUMQMUUUcVUUUUUMUMMMUQqLKBBBDLLK6rLDBBBBDDBBBAUUUMQMUUUfcUMMMQMMUUQQQSKLBBBBDLL2rBBBBBBBBBBBBAUUMMMMUUfcUMMMMQMMMMQQQTLBBBBBDL2rBBBBBBBhBBBBBA=")

702
apps/warpdrive/app.js Normal file
View File

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

BIN
apps/warpdrive/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

View File

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

View File

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

View File

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