1
0
Fork 0

Merge remote-tracking branch 'upstream/master'

master
MaBecker 2019-11-26 17:19:42 +01:00
commit e473c6f4ee
200 changed files with 755 additions and 43 deletions

9
LICENSE Normal file
View File

@ -0,0 +1,9 @@
Copyright 2019 Gordon Williams, Pur3 Ltd
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

215
README.md
View File

@ -3,16 +3,16 @@ Bangle.js App Loader (and Apps)
Try it live at [banglejs.com/apps](https://banglejs.com/apps)
### How does it work?
## How does it work?
* A list of apps is in `apps.json`
* Each element references an app in `apps/` which is uploaded
* Each element references an app in `apps/<id>` which is uploaded
* When it starts, BangleAppLoader checks the JSON and compares
it with the files it sees in the watch's storage.
* To upload an app, BangleAppLoader checks the files that are
listed in `apps.json`, loads them, and sends them over Web Bluetooth.
### What filenames are used
## What filenames are used
Filenames in storage are limited to 8 characters. To
easily distinguish between file types, we use the following:
@ -22,15 +22,21 @@ easily distinguish between file types, we use the following:
* `-stuff` is JS code
* `=stuff` is JS code for stuff that is run at boot time - eg. handling settings or creating widgets on the clock screen
### Developing your own app
## Developing your own app
* Head over to [the Web IDE](https://www.espruino.com/ide/) and ensure `Save on Send` in settings set to the *default setting* of `To RAM`
* We'd recommend that you start off using code from 'Example Applications' (below) to get started...
* Load [`app.js`](apps/_example_app/app.js) or [`widget.js`](apps/_example_widget/widget.js) into the IDE and start developing.
* The `Upload` button will load your app to Bangle.js temporarily
## Adding your app to the menu
* Start writing your code in the IDE, with `Save on Send` in settings set to
the *default* of `To RAM`
* When you have your app as you want it, add it as a file in `apps/`, lets assume `apps/my-great-app.js`
* Come up with a unique 7 character name, we'll assume `7chname`
* Create `apps/my-great-app.png` as a 48px icon
* Use http://www.espruino.com/Image+Converter to create as 1 bit, 4 bit or 8 bit Web Palette "Image String" and save it as `apps/my-great-app-icon.js`
* Create an entry in `apps/my-great-app.json` as follows:
* Create a folder called `apps/<id>`, lets assume `apps/7chname`
* We'd recommend that you copy files from 'Example Applications' (below) as a base, or...
* `apps/7chname/app.png` should be a 48px icon
* Use http://www.espruino.com/Image+Converter to create `apps/7chname/app-icon.js`, using a 1 bit, 4 bit or 8 bit Web Palette "Image String"
* Create an entry in `apps/7chname/app.json` as follows:
```
{
@ -40,23 +46,117 @@ the *default* of `To RAM`
}
```
* Create an entry in `apps.json` as follows:
* Create an entry in `apps.json` as follows:
```
{ "id": "7chname",
"name": "My app's human readable name",
"icon": "my-great-app.png",
"icon": "app.png",
"description": "A detailed description of my great app",
"tags": "",
"storage": [
{"name":"+7chname","url":"my-great-app.json"},
{"name":"-7chname","url":"my-great-app.js"},
{"name":"*7chname","url":"my-great-app.js","evaluate":true}
{"name":"+7chname","url":"app.json"},
{"name":"-7chname","url":"app.js"},
{"name":"*7chname","url":"app-icon.js","evaluate":true}
],
},
```
### `apps.json` format
## Testing
### Online
This is the best way to test...
* Fork the https://github.com/espruino/BangleApps git repository
* Add your files
* Go to GitHub Settings and activate GitHub Pages
* Run your personal `Bangle App Loader` at https://\<your-github-username\>.github.io/BangleApps/index.html to load apps onto your device
* Your apps should be inside it - if there are problems, check your web browser's 'developer console' for errors
Be aware of the delay between commits and updates on github.io - it can take a few minutes (and a 'hard refresh' of your browser) for changes to take effect.
### Offline
You can add the following to the Espruino Web IDE:
```
// replace with your 7chname app name
var appname = "mygreat";
require("Storage").write('*'+appname,
// place app-icon.js contents here
);
//
require("Storage").write("+"+appname,{
"name":"My Great App","type":"",
"icon":"*"+appname,
"src":"-"+appname,
});
require("Storage").write("-"+appname,`
// place contents of app.js here
// be aware of double-quoting templated strings
`
```
When you upload code this way, your app will be uploaded to Bangle.js's menu
without you having to use the `Bangle App Loader`
## Example Applications
To make the process easier we've come up with some example applications that you can use as a base
when creating your own. Just come up with a unique 7 character name, copy `apps/_example_app`
or `apps/_example_widget` to `apps/7chname`, and add `apps/_example_X/add_to_apps.json` to
`apps.json`.
### App Example
The app example is available in [`apps/_example_app`](apps/_example_app)
Apps are listed in the Bangle.js menu, accessible from a clock app via the middle button.
* `add_to_apps.json` - insert into `apps.json`, describes the widget to bootloader and loader
* `app.png` - app icon - 48x48px
* `app-icon.js` - JS version of the icon (made with http://www.espruino.com/Image+Converter) for use in Bangle.js's menu
* `app.json` - short app name for Bangle.js menu and storage filenames
* `app.js` - app code
#### `app-icon.js`
The icon image and short description is used in the menu entry as selection posibility.
Use the Espruino [image converter](https://www.espruino.com/Image+Converter) and upload your `app.png` file.
Follow this steps to create a readable icon as image string.
1. upload a png file
2. set _X_ Use Compression
3. set _X_ Transparency (optional)
4. set Diffusion: _flat_
5. set Colours: _1 bit_, _4 bit_ or _8 bit Web Palette_
6. set Output as: _Image String_
Replace this line with the image converter output:
```
require("heatshrink").decompress(atob("mEwwJC/AH4A/AH4AgA=="));
```
Keep in mind to use this converter for creating images you like to draw with `g.drawImage()` with your app.
### Widget Example
The widget example is available in [`apps/_example_widget`](apps/_example_widget)
* `add_to_apps.json` - insert into `apps.json`, describes the widget to bootloader and loader
* `widget.json` - short widget name and storage names
* `widget.js` - widget code
## `apps.json` format
```
{ "id": "appid", // 7 character app id
@ -84,6 +184,87 @@ the *default* of `To RAM`
}
```
### Credits
* name, icon and description present the app in the app loader.
* tags is used for grouping apps in the library, separate multiple entries by comma. Known tags are `tool`, `system`, `clock`, `game`, `sound`, `gps`, `widget` or empty.
* storage is used to identify the app files and how to handle them
## Coding hints
- use `g.setFont(.., size)` to multiply the font size, eg ("6x8",3) : "18x24"
- use `g.drawString(text,x,y,true)` to draw with background color to overwrite existing text
- use `g.clearRect()` to clear parts of the screen, instead of using `g.clear()`
- use `g.fillPoly()` or `g.drawImage()` for complex graphic elements
- using `g.clear()` can cause screen flicker
- using `g.setLCDBrightness()` can save you power during long periods with lcd on
- chaining graphics methodes, eg `g.setColor(0xFD20).setFontAlign(0,0).setfont("6x8",3)`
### Graphic areas
The screen is parted in a widget and app area for lcd mode `direct`(default).
| areas | as rectangle or point |
| :-:| :-: |
| Widget | (0,0,239,23) |
| Apps | (0,24,239,239) |
| BTN1 | (230, 55) |
| BTN2 | (230, 140) |
| BTN3 | (230, 210) |
| BTN4 | (0,0,119, 239)|
| BTN5 | (120,0,239,239) |
- Use `g.setFontAlign(0, 0, 3)` to draw rotated string to BTN1-BTN3 with `g.drawString()`.
- For BTN4-5 the touch area is named
## Available colors
Yuu can use `g.setColor(r,g,b)` OR `g.setColor(16bitnumber)` - some common 16 bit colors are below:
| color-name | color-value|
| :-: | :-: |
| Black | 0x0000 |
| Navy | 0x000F |
| DarkGreen | 0x03E0 |
| DarkCyan | 0x03EF |
| Maroon | 0x7800 |
| Purple | 0x780F |
| Olive | 0x7BE0
| LightGray | 0xC618
| DarkGrey | 0x7BEF
| Blue | 0x001F
| Green | 0x07E0 |
| Cyan | 0x07FF |
| RED | 0xF800 |
| Magenta | 0xF81F |
| Yellow | 0xFFE0 |
| White | 0xFFFF |
| Orange | 0xFD20 |
| GreenYellow | 0xAFE5 |
| Pink | 0xF81F |
## API Reference
[Reference](http://www.espruino.com/Reference#software)
[Bangle Class](https://banglejs.com/reference#Bangle)
[Graphics Class](https://banglejs.com/reference#Graphics)
## 'Testing' folder
The [`testing`](testing) folder contains snippets of code that might be useful for your apps.
* `testing/colors.js` - 16 bit colors as name value pairs
* `testing/gpstrack.js` - code to store a GPS track in Bagle.js storage and output it back to the console
* `testing/map` - code for splitting an image into map tiles and then displaying them
## Credits
The majority of icons used for these apps are from [Icons8](https://icons8.com/) - we have a commercial license but icons are also free for Open Source projects.

View File

@ -10,7 +10,7 @@ var AppInfo = {
if (storageFile.content)
return Promise.resolve(storageFile);
else if (storageFile.url)
return fileGetter("apps/"+storageFile.url).then(content => {
return fileGetter(`apps/${app.id}/${storageFile.url}`).then(content => {
return {
name : storageFile.name,
content : content,

View File

@ -502,5 +502,30 @@
{"name": "-gpsinfo","url": "gps-info.js"},
{"name": "*gpsinfo","url": "gps-info-icon.js","evaluate": true}
]
},
{
"id": "promodo",
"name":"Promodoro",
"icon":"promodoro.png",
"description": "A simple promodoro timer.",
"tags": "promodoro,cooking,tools",
"type": "app",
"storage": [
{"name": "+promodo","url": "promodoro.json"},
{"name": "-promodo","url": "promodoro.js"},
{"name": "*promodo","url": "promodoro-icon.js","evaluate": true}
]
},
{ "id": "blobclk",
"name": "Large Digit Clock",
"icon": "clock-blob.png",
"description": "A clock with big digits",
"tags": "clock",
"type":"clock",
"storage": [
{"name":"+blobclk","url":"clock-blob.json"},
{"name":"-blobclk","url":"clock-blob.js"},
{"name":"*blobclk","url":"clock-blob-icon.js","evaluate":true}
]
}
]

View File

@ -0,0 +1,12 @@
// Create an entry in apps.json as follows:
{ "id": "7chname",
"name": "My app's human readable name",
"icon": "app.png",
"description": "A detailed description of my great app",
"tags": "",
"storage": [
{"name":"+7chname","url":"app.json"},
{"name":"-7chname","url":"app.js"},
{"name":"*7chname","url":"app-icon.js","evaluate":true}
],
}

View File

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

15
apps/_example_app/app.js Normal file
View File

@ -0,0 +1,15 @@
(() => {
// place your const, vars, functions or classes here
// special function to handle display switch on
Bangle.on('lcdPower', (on) => {
if (on) {
// call your app function here
}});
g.clear();
// call your app function here
})();

View File

@ -0,0 +1,5 @@
{
"name":"Short Name",
"icon":"*7chname",
"src":"-7chname"
}

BIN
apps/_example_app/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,11 @@
// Create an entry in apps.json as follows:
{ "id": "7chname",
"name": "My widget's human readable name",
"icon": "widget.png",
"description": "A detailed description of my great widget",
"tags": "",
"storage": [
{"name":"+7chname","url":"widget.json"},
{"name":"-7chname","url":"widget.js"},
],
}

View File

@ -0,0 +1,17 @@
(() => {
// add the width
var xpos = WIDGETPOS.tr-<the widget width>;
WIDGETPOS.tr-=<the widget width plus some extra pixel to keep distance to others>;
// draw your widget at xpos
function draw() {
// add your code
}
// add your widget
WIDGETS["mywidget"]={draw:draw};
})()

View File

@ -0,0 +1,4 @@
{
"name":"widgetname", "type":"widget",
"src":"-7chname"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 743 B

After

Width:  |  Height:  |  Size: 743 B

View File

@ -1,7 +1,7 @@
<html>
<head>
<head>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.0.3/dist/leaflet.css" />
<link rel="stylesheet" href="../css/spectre.min.css">
<link rel="stylesheet" href="../../css/spectre.min.css">
</head>
<style>
body {

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwhC/AH4AUmYAOCw0DC58wFyowHC/4X/C9mIxAGKC5IPBCIoXWnAXOB4ODAQQXTGQQXCJoyPMC4ggDC6IVBC6JKFC6JZDC6SODCgQXQFwalCSAoXJXAgXSdo4XOCIi/CL54XvABwX/C/4XtgYWPmAXFGCAWGAH4A/ABgA="))

108
apps/blobclk/clock-blob.js Normal file
View File

@ -0,0 +1,108 @@
(function(){
const buf = Graphics.createArrayBuffer(144,200,1,{msb:true});
const NUMBERS = [
[1,1,1,1,3,1,1,0,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1],//0
[0,1,1,1,3,0,0,1,1,1,0,0,1,1,1,0,0,1,1,1,0,0,1,1,1],//1
[1,1,1,1,3,0,0,1,1,1,2,1,1,1,4,1,1,1,0,0,1,1,1,1,1],//2
[1,1,1,1,3,0,0,1,1,1,1,1,1,1,1,0,0,1,1,1,1,1,1,1,1],//3
[1,1,0,1,1,1,1,0,1,1,1,1,1,1,1,5,1,1,1,1,0,0,1,1,1],//4
[1,1,1,1,1,1,1,1,0,0,5,1,1,1,3,0,0,1,1,1,1,1,1,1,1],//5
[1,1,1,1,1,1,1,1,0,0,1,1,1,1,3,1,1,0,1,1,1,1,1,1,1],//6
[1,1,1,1,3,0,0,1,1,1,0,2,1,1,1,0,1,1,1,0,0,1,1,1,0],//7
[1,1,1,1,3,1,1,0,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1],//8
[1,1,1,1,3,1,1,0,1,1,1,1,1,1,1,0,0,1,1,1,1,1,1,1,1] //9
];
let intervalRef = null;
let digits = [-1,-1,-1,-1,-1,-1];
function flip() {
g.setColor(1,1,1);
g.drawImage({width:buf.getWidth(),height:buf.getHeight(),buffer:buf.buffer},55,26);
try { if (drawWidgets) { drawWidgets();} } catch(err) {}
}
function drawPixel(ox,oy,x,y,r,p) {
let x1 = ox+x*(r*2+1);
let y1 = oy+y*(r*2+1);
let xmid = x1+r;
let ymid = y1+r;
let x2 = xmid+r;
let y2 = ymid+r;
if (p > 0) {
if (p > 1) {
buf.setColor(0,0,0);
buf.fillRect(x1,y1,x2,y2);
}
buf.setColor(1,1,1);
} else {
buf.setColor(0,0,0);
}
if (p < 2) {
buf.fillRect(x1,y1,x2,y2);
} else if (p === 2) {
buf.fillPoly([xmid,y1,x2,y1,x2,y2,x1,y2,x1,ymid]);
} else if (p === 3) {
buf.fillPoly([x1,y1,xmid,y1,x2,ymid,x2,y2,x1,y2]);
} else if (p === 4) {
buf.fillPoly([x1,y1,x2,y1,x2,ymid,xmid,y2,x1,y2]);
} else if (p === 5) {
buf.fillPoly([x1,y1,x2,y1,x2,y2,xmid,y2,x1,ymid]);
}
}
function redraw() {
let time = new Date();
let hours = time.getHours();
let mins = time.getMinutes();
let secs = time.getSeconds();
let newDigits = [Math.floor(hours/10),hours%10,Math.floor(mins/10),mins%10,Math.floor(secs/10),secs%10];
for (var p = 0;p<25;p++) {
var px = p%5;
var py = Math.floor(p/5);
if (digits[0] === -1 || NUMBERS[newDigits[0]][p] !== NUMBERS[digits[0]][p] ) {
drawPixel(0,20,px,py,6,NUMBERS[newDigits[0]][p]);
}
if (digits[1] === -1 || NUMBERS[newDigits[1]][p] !== NUMBERS[digits[1]][p] ) {
drawPixel(78,20,px,py,6,NUMBERS[newDigits[1]][p]);
}
if (digits[2] === -1 || NUMBERS[newDigits[2]][p] !== NUMBERS[digits[2]][p] ) {
drawPixel(0,92,px,py,6,NUMBERS[newDigits[2]][p]);
}
if (digits[3] === -1 || NUMBERS[newDigits[3]][p] !== NUMBERS[digits[3]][p] ) {
drawPixel(78,92,px,py,6,NUMBERS[newDigits[3]][p]);
}
if (digits[4] === -1 || NUMBERS[newDigits[4]][p] !== NUMBERS[digits[4]][p] ) {
drawPixel(69,164,px,py,3,NUMBERS[newDigits[4]][p]);
}
if (digits[5] === -1 || NUMBERS[newDigits[5]][p] !== NUMBERS[digits[5]][p] ) {
drawPixel(108,164,px,py,3,NUMBERS[newDigits[5]][p]);
}
}
digits = newDigits;
flip();
}
function clearTimers() {
if(intervalRef) {clearInterval(intervalRef);}
}
function startTimers() {
g.clear();
digits = [-1,-1,-1,-1,-1,-1];
intervalRef = setInterval(redraw,1000);
redraw();
}
startTimers();
Bangle.on('lcdPower',function(on) {
if (on) {
g.clear();
startTimers();
try { if (drawWidgets) { drawWidgets();} } catch(err) {}
} else {
clearTimers();
}
});
Bangle.on('faceUp',function(up){
if (up && !Bangle.isLCDOn()) {
clearTimers();
Bangle.setLCDPower(true);
}
});
})();

View File

@ -0,0 +1,7 @@
{
"name":"Large Clock",
"type":"clock",
"icon":"*blobclk",
"src":"-blobclk",
"sortorder":-10
}

BIN
apps/blobclk/clock-blob.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 817 B

View File

@ -101,12 +101,28 @@ if (startapp) {
function drawWidgets() {
for (var w of WIDGETS) w.draw();
}
var clockApps = require("Storage").list().filter(a=>a[0]=='+').map(app=>{
try { return require("Storage").readJSON(app); }
catch (e) {}
}).filter(app=>app.type=="clock").sort((a, b) => a.sortorder - b.sortorder);
if (clockApps && clockApps.length > 0) eval(require("Storage").read(clockApps[0].src));
var settings;
try {
settings = require("Storage").readJSON('@setting');
} catch (e) {
settings = {}
}
var clockApp = settings.clock;
if (clockApp) {
clockApp = require("Storage").read(clockApp)
}
if (!clockApp) {
var clockApps = require("Storage").list().filter(a=>a[0]=='+').map(app=>{
try { return require("Storage").readJSON(app); }
catch (e) {}
}).filter(app=>app.type=="clock").sort((a, b) => a.sortorder - b.sortorder);
if (clockApps && clockApps.length > 0) {
clockApp = require("Storage").read(clockApps[0].src);
}
}
if (clockApp) eval(clockApp);
else E.showMessage("No Clock Found");
delete clockApps;
require("Storage").list().filter(a=>a[0]=='=').forEach(widget=>eval(require("Storage").read(widget)));
setTimeout(drawWidgets,100);

View File

Before

Width:  |  Height:  |  Size: 741 B

After

Width:  |  Height:  |  Size: 741 B

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 303 B

After

Width:  |  Height:  |  Size: 303 B

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Before

Width:  |  Height:  |  Size: 617 B

After

Width:  |  Height:  |  Size: 617 B

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 506 B

After

Width:  |  Height:  |  Size: 506 B

View File

Before

Width:  |  Height:  |  Size: 506 B

After

Width:  |  Height:  |  Size: 506 B

View File

Before

Width:  |  Height:  |  Size: 632 B

After

Width:  |  Height:  |  Size: 632 B

Some files were not shown because too many files have changed in this diff Show More