mirror of https://github.com/espruino/BangleApps
commit
dca1ecf854
|
@ -26,4 +26,5 @@
|
|||
0.25: Added option to 'ignore' an app from the message
|
||||
0.26: Change handling of GPS status to depend on GPS events instead of connection events
|
||||
0.27: Issue newline before GB commands (solves issue with console.log and ignored commands)
|
||||
0.28: Navigation messages no longer launch the Maps view unless they're new
|
||||
0.28: Navigation messages no longer launch the Maps view unless they're new
|
||||
0.29: Support for http request xpath return format
|
|
@ -231,6 +231,7 @@
|
|||
//send the request
|
||||
var req = {t: "http", url:url, id:options.id};
|
||||
if (options.xpath) req.xpath = options.xpath;
|
||||
if (options.return) req.return = options.return; // for xpath
|
||||
if (options.method) req.method = options.method;
|
||||
if (options.body) req.body = options.body;
|
||||
if (options.headers) req.headers = options.headers;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"id": "android",
|
||||
"name": "Android Integration",
|
||||
"shortName": "Android",
|
||||
"version": "0.28",
|
||||
"version": "0.29",
|
||||
"description": "Display notifications/music/etc sent from the Gadgetbridge app on Android. This replaces the old 'Gadgetbridge' Bangle.js widget.",
|
||||
"icon": "app.png",
|
||||
"tags": "tool,system,messages,notifications,gadgetbridge",
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
0.01: Initial Creation
|
||||
0.02: Fixed some sleep bugs. Added a sleep mode toggle
|
||||
0.03: Reduce busy-loop and code
|
||||
0.04: Separate buzz-time and sleep-time
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "chimer",
|
||||
"name": "Chimer",
|
||||
"version": "0.03",
|
||||
"version": "0.04",
|
||||
"description": "A fork of Hour Chime that adds extra features such as: \n - Buzz or beep on every 60, 30 or 15 minutes. \n - Repeat Chime up to 3 times \n - Set hours to disable chime",
|
||||
"icon": "widget.png",
|
||||
"type": "widget",
|
||||
|
|
|
@ -20,15 +20,16 @@
|
|||
let count = settings.repeat;
|
||||
|
||||
const chime1 = () => {
|
||||
let p;
|
||||
if (settings.type === 1) {
|
||||
Bangle.buzz(100);
|
||||
p = Bangle.buzz(100);
|
||||
} else if (settings.type === 2) {
|
||||
Bangle.beep();
|
||||
p = Bangle.beep();
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
if (--count > 0)
|
||||
setTimeout(chime1, 150);
|
||||
p.then(() => setTimeout(chime1, 150));
|
||||
};
|
||||
|
||||
chime1();
|
||||
|
|
|
@ -3,4 +3,5 @@
|
|||
0.03: Reported image for battery now reflects charge level
|
||||
0.04: On 2v18+ firmware, we can now stop swipe events from being handled by other apps
|
||||
eg. when a clockinfo is selected, swipes won't affect swipe-down widgets
|
||||
0.05: Reported image for battery is now transparent (2v18+)
|
||||
0.05: Reported image for battery is now transparent (2v18+)
|
||||
0.06: When >1 clockinfo, swiping one back tries to ensure they don't display the same thing
|
|
@ -10,7 +10,12 @@ if (stepGoal == undefined) {
|
|||
stepGoal = d != undefined && d.settings != undefined ? d.settings.goal : 10000;
|
||||
}
|
||||
|
||||
// Load the settings, with defaults
|
||||
/// How many times has addInteractive been called?
|
||||
exports.loadCount = 0;
|
||||
/// A list of all the instances returned by addInteractive
|
||||
exports.clockInfos = [];
|
||||
|
||||
/// Load the settings, with defaults
|
||||
exports.loadSettings = function() {
|
||||
return Object.assign({
|
||||
hrmOn : 0, // 0(Always), 1(Tap)
|
||||
|
@ -22,6 +27,7 @@ exports.loadSettings = function() {
|
|||
);
|
||||
};
|
||||
|
||||
/// Load a list of ClockInfos - this does not cache and reloads each time
|
||||
exports.load = function() {
|
||||
var settings = exports.loadSettings();
|
||||
delete settings.apps; // keep just the basic settings in memory
|
||||
|
@ -63,7 +69,7 @@ exports.load = function() {
|
|||
} else img=atob("GBiBAAABgAADwAAHwAAPgACfAAHOAAPkBgHwDwP4Hwf8Pg/+fB//OD//kD//wD//4D//8D//4B//QB/+AD/8AH/4APnwAHAAACAAAA==");
|
||||
return {
|
||||
text : v + "%", v : v, min:0, max:100, img : img
|
||||
}
|
||||
};
|
||||
},
|
||||
show : function() { this.interval = setInterval(()=>this.emit('redraw'), 60000); Bangle.on("charging", batteryUpdateHandler); batteryUpdateHandler(); },
|
||||
hide : function() { clearInterval(this.interval); delete this.interval; Bangle.removeListener("charging", batteryUpdateHandler); },
|
||||
|
@ -73,7 +79,7 @@ exports.load = function() {
|
|||
get : () => { let v = Bangle.getHealthStatus("day").steps; return {
|
||||
text : v, v : v, min : 0, max : stepGoal,
|
||||
img : atob("GBiBAAcAAA+AAA/AAA/AAB/AAB/gAA/g4A/h8A/j8A/D8A/D+AfH+AAH8AHn8APj8APj8AHj4AHg4AADAAAHwAAHwAAHgAAHgAADAA==")
|
||||
}},
|
||||
};},
|
||||
show : function() { Bangle.on("step", stepUpdateHandler); stepUpdateHandler(); },
|
||||
hide : function() { Bangle.removeListener("step", stepUpdateHandler); },
|
||||
},
|
||||
|
@ -82,7 +88,7 @@ exports.load = function() {
|
|||
get : () => { return {
|
||||
text : (hrm||"--") + " bpm", v : hrm, min : 40, max : 200,
|
||||
img : atob("GBiBAAAAAAAAAAAAAAAAAAAAAADAAADAAAHAAAHjAAHjgAPngH9n/n82/gA+AAA8AAA8AAAcAAAYAAAYAAAAAAAAAAAAAAAAAAAAAA==")
|
||||
}},
|
||||
};},
|
||||
run : function() {
|
||||
Bangle.setHRMPower(1,"clkinfo");
|
||||
if (settings.hrmOn==1/*Tap*/) {
|
||||
|
@ -131,11 +137,11 @@ exports.load = function() {
|
|||
require("Storage").list(/clkinfo.js$/).forEach(fn => {
|
||||
try{
|
||||
var a = eval(require("Storage").read(fn))();
|
||||
var b = menu.find(x => x.name === a.name)
|
||||
var b = menu.find(x => x.name === a.name);
|
||||
if(b) b.items = b.items.concat(a.items);
|
||||
else menu = menu.concat(a);
|
||||
} catch(e){
|
||||
console.log("Could not load clock info "+E.toJS(fn))
|
||||
console.log("Could not load clock info "+E.toJS(fn));
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -204,11 +210,12 @@ exports.addInteractive = function(menu, options) {
|
|||
if ("function" == typeof options) options = {draw:options}; // backwards compatibility
|
||||
options.index = 0|exports.loadCount;
|
||||
exports.loadCount = options.index+1;
|
||||
exports.clockInfos[options.index] = options;
|
||||
options.focus = options.index==0 && options.x===undefined; // focus if we're the first one loaded and no position has been defined
|
||||
const appName = (options.app||"default")+":"+options.index;
|
||||
|
||||
// load the currently showing clock_infos
|
||||
let settings = exports.loadSettings()
|
||||
let settings = exports.loadSettings();
|
||||
if (settings.apps[appName]) {
|
||||
let a = settings.apps[appName].a|0;
|
||||
let b = settings.apps[appName].b|0;
|
||||
|
@ -259,6 +266,10 @@ exports.addInteractive = function(menu, options) {
|
|||
//can happen for dynamic ones (alarms, events)
|
||||
//in the worst case we come back to 0
|
||||
} while(menu[options.menuA].items.length==0);
|
||||
// When we change, ensure we don't display the same thing as another clockinfo if we can avoid it
|
||||
while ((options.menuB < menu[options.menuA].items.length) &&
|
||||
exports.clockInfos.some(m => (m!=options) && m.menuA==options.menuA && m.menuB==options.menuB))
|
||||
options.menuB++;
|
||||
}
|
||||
if (oldMenuItem) {
|
||||
menuHideItem(oldMenuItem);
|
||||
|
@ -319,6 +330,7 @@ exports.addInteractive = function(menu, options) {
|
|||
delete Bangle.CLKINFO_FOCUS;
|
||||
menuHideItem(menu[options.menuA].items[options.menuB]);
|
||||
exports.loadCount--;
|
||||
delete exports.clockInfos[options.index];
|
||||
};
|
||||
options.redraw = function() {
|
||||
drawItem(menu[options.menuA].items[options.menuB]);
|
||||
|
@ -339,8 +351,7 @@ exports.addInteractive = function(menu, options) {
|
|||
menuShowItem(menu[options.menuA].items[options.menuB]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
};
|
||||
delete settings; // don't keep settings in RAM - save space
|
||||
return options;
|
||||
};
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{ "id": "clock_info",
|
||||
"name": "Clock Info Module",
|
||||
"shortName": "Clock Info",
|
||||
"version":"0.05",
|
||||
"version":"0.06",
|
||||
"description": "A library used by clocks to provide extra information on the clock face (Altitude, BPM, etc)",
|
||||
"icon": "app.png",
|
||||
"type": "module",
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
/* Image filtering code that helps to transform the OSM tile
|
||||
into something that's usable on a 3bpp screen.
|
||||
|
||||
Stick this in a file so we can
|
||||
*/
|
||||
|
||||
|
||||
function imageFilterFor3BPP(srcData, dstData, options) {
|
||||
options = options || {};
|
||||
if (options.colLo === undefined)
|
||||
options.colLo = 140; // when adding contrast/saturation, this is the max saturaton we add
|
||||
if (options.colHi === undefined)
|
||||
options.colHi = 250;
|
||||
if (options.sharpen === undefined)
|
||||
options.sharpen = true;
|
||||
if (options.dither === undefined)
|
||||
options.dither = false;
|
||||
|
||||
const width = srcData.width;
|
||||
const height = srcData.height;
|
||||
var rgbaSrc = srcData.data;
|
||||
var rgbaDst = dstData.data;
|
||||
function getPixel(x,y) {
|
||||
if (x<0) x=0;
|
||||
if (y<0) y=0;
|
||||
if (x>=width) x=width-1;
|
||||
if (y>=height) y=height-1;
|
||||
var i = (x + y*width)*4;
|
||||
return [
|
||||
rgbaSrc[i+0], rgbaSrc[i+1], rgbaSrc[i+2]
|
||||
];
|
||||
}
|
||||
function dmul(a, mul) { return a.map(a => a.map(n=>n*mul)); }
|
||||
const KS = 5; // kernel size
|
||||
const KO = 2; // kernel offset
|
||||
const K = dmul([ // 5x5 sharpening kernel
|
||||
[ 1,4,6,4,1 ],
|
||||
[4,16,24,16,4],
|
||||
[6,24,-476,24,6],
|
||||
[4,16,24,16,4],
|
||||
[ 1,4,6,4,1 ],
|
||||
], -1/256);
|
||||
/*const KS = 7; // kernel size
|
||||
const KO = 3; // kernel offset
|
||||
const K = dmul([ // 7x7 sharpening (gaussian - 2x middle pixel)
|
||||
[ 0, 0, 1, 2, 1, 0, 0 ],
|
||||
[ 0, 3,13,22,13, 3, 0 ],
|
||||
[ 1,13,59,97,59,13, 1 ],
|
||||
[ 2,22,97,159-2006,97,22,2 ],
|
||||
[ 1,13,59,97,59,13, 1 ],
|
||||
[ 0, 3,13,22,13, 3, 0 ],
|
||||
[ 0, 0, 1, 2, 1, 0, 0 ],
|
||||
], -1/1003);*/
|
||||
const DITHERM = 3; // dither width -1 (dither must be power 2)
|
||||
const DITHER = dmul([ // dithering matrix
|
||||
[ 0,1,2,3 ],
|
||||
[ 1,2,3,0 ],
|
||||
[ 2,3,2,1 ],
|
||||
[ 3,2,1,0 ],
|
||||
], 256/4);
|
||||
|
||||
|
||||
var idx=0;
|
||||
for (var y=0;y<height;y++) {
|
||||
for (var x=0;x<width;x++) {
|
||||
var col;
|
||||
if (options.sharpen) {
|
||||
// Apply a sharpening filter
|
||||
var col = [0,0,0];
|
||||
for (var ky=0;ky<KS;ky++)
|
||||
for (var kx=0;kx<KS;kx++) {
|
||||
var c = getPixel(x+kx-KO, y+ky-KO);
|
||||
col[0] += c[0] * K[kx][ky];
|
||||
col[1] += c[1] * K[kx][ky];
|
||||
col[2] += c[2] * K[kx][ky];
|
||||
}
|
||||
for (var n=0;n<3;n++) {
|
||||
col[n] = Math.round(col[n]);
|
||||
if (col[n]<0) col[n]=0;
|
||||
if (col[n]>255) col[n]=255;
|
||||
}
|
||||
} else { // if not sharpening, just get pixel
|
||||
col = getPixel(x,y);
|
||||
}
|
||||
// increase saturation / contrast
|
||||
var min = Math.min(col[0], col[1], col[2]);
|
||||
var max = Math.max(col[0], col[1], col[2]);
|
||||
var d = max-min;
|
||||
if (min>options.colLo) min=options.colLo;
|
||||
if (max<options.colHi) max=options.colHi;
|
||||
d = max-min;
|
||||
for (var n=0;n<3;n++) {
|
||||
col[n] = (col[n]-min) * 256 / d;
|
||||
if (options.dither) { // && col[n]<192 is more pleasant
|
||||
if (col[n] > DITHER[x&DITHERM][y&DITHERM]) // dither
|
||||
col[n] = 255;
|
||||
else
|
||||
col[n] = 0;
|
||||
}
|
||||
rgbaDst[idx+n] = col[n];
|
||||
}
|
||||
rgbaDst[idx+3] = 255;
|
||||
idx+=4;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -69,7 +69,7 @@
|
|||
<script src="../../webtools/heatshrink.js"></script>
|
||||
<script src="../../webtools/imageconverter.js"></script>
|
||||
<script src="https://unpkg.com/leaflet-geosearch@3.6.0/dist/bundle.min.js"></script>
|
||||
|
||||
<script src="imagefilter.js"></script>
|
||||
<script>
|
||||
/*
|
||||
|
||||
|
@ -262,27 +262,10 @@ TODO:
|
|||
turn the saturation up to maximum, so when thresholded it
|
||||
works a lot better */
|
||||
var imageData = ctx.getImageData(0,0,width,height);
|
||||
var rgba = imageData.data;
|
||||
var l = width*height*4;
|
||||
for (var i=0;i<l;i+=4) {
|
||||
var min = Math.min(rgba[i+0],rgba[i+1],rgba[i+2]);
|
||||
var max = Math.max(rgba[i+0],rgba[i+1],rgba[i+2]);
|
||||
var d = max-min;
|
||||
if (max<120 || (d<8 && max<215)) { // black, or a darker grey
|
||||
rgba[i+0]=0;
|
||||
rgba[i+1]=0;
|
||||
rgba[i+2]=0;
|
||||
} else if (min>240 || d<8) { // white or grey
|
||||
rgba[i+0]=255;
|
||||
rgba[i+1]=255;
|
||||
rgba[i+2]=255;
|
||||
} else { // another colour - use max saturation
|
||||
rgba[i+0] = (rgba[i+0]-min) * 255 / d;
|
||||
rgba[i+1] = (rgba[i+1]-min) * 255 / d;
|
||||
rgba[i+2] = (rgba[i+2]-min) * 255 / d;
|
||||
}
|
||||
}
|
||||
ctx.putImageData(imageData,0,0);
|
||||
var dstData = ctx.createImageData(width, height);
|
||||
var filterOptions = {};
|
||||
imageFilterFor3BPP(imageData, dstData, filterOptions);
|
||||
ctx.putImageData(dstData,0,0);
|
||||
}
|
||||
console.log("Compression options", options);
|
||||
var w = Math.round(width / TILESIZE);
|
||||
|
@ -296,7 +279,7 @@ TODO:
|
|||
options.width = TILESIZE;
|
||||
options.height = TILESIZE;
|
||||
var imgstr = imageconverter.RGBAtoString(rgba, options);
|
||||
ctx.putImageData(imageData,x*TILESIZE, y*TILESIZE); // write preview
|
||||
//ctx.putImageData(imageData,x*TILESIZE, y*TILESIZE); // write preview
|
||||
/*var compress = 'require("heatshrink").decompress('
|
||||
if (!imgstr.startsWith(compress)) throw "Data in wrong format";
|
||||
imgstr = imgstr.slice(compress.length,-1);*/
|
||||
|
|
|
@ -0,0 +1,116 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>3 bit filter test</title>
|
||||
</head>
|
||||
<style>
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<p>This file tests imagefilter.js to allow different filtering options
|
||||
to be tested, so we can quickly see the easiest way of transforming OpenStreetMap tiles
|
||||
into 3 bits
|
||||
</p>
|
||||
<canvas id="maptiles" style="display:none"></canvas>
|
||||
|
||||
<button id="getmap">Test</button><br/>
|
||||
<input type="checkbox" id="finaldither" checked></input><span>Final dither (Bangle.js preview)</span><br/>
|
||||
<input type="checkbox" id="sharpen" checked></input><span>Sharpen</span>
|
||||
<input type="checkbox" id="dither"></input><span>Line Dither</span><br/>
|
||||
<input type="range" id="slider_lo" min="0" max="255" value="140">Lo threshold<br/>
|
||||
<input type="range" id="slider_hi" min="0" max="255" value="250">Hi threshold<br/>
|
||||
|
||||
<script src="../../../webtools/heatshrink.js"></script>
|
||||
<script src="../../../webtools/imageconverter.js"></script>
|
||||
<script src="../imagefilter.js"></script>
|
||||
<script>
|
||||
var TILESIZE = 96
|
||||
|
||||
// convert canvas into an actual tiled image file
|
||||
function tilesLoaded(ctx, width, height, mapImageFile) {
|
||||
var filterOptions = {
|
||||
colLo : 0|document.getElementById("slider_lo").value,
|
||||
colHi : 0|document.getElementById("slider_hi").value,
|
||||
sharpen : 0|document.getElementById("sharpen").checked,
|
||||
dither : 0|document.getElementById("dither").checked
|
||||
}
|
||||
console.log("filterOptions", filterOptions);
|
||||
var preview = document.getElementById("finaldither").checked;
|
||||
|
||||
|
||||
var options = {
|
||||
compression:false, output:"raw",
|
||||
mode:"3bit",
|
||||
diffusion:"bayer2"
|
||||
};
|
||||
/* If in 3 bit mode, go through all the data beforehand and
|
||||
turn the saturation up to maximum, so when thresholded it
|
||||
works a lot better */
|
||||
var imageData = ctx.getImageData(0,0,width,height);
|
||||
var dstData = ctx.createImageData(width, height);
|
||||
imageFilterFor3BPP(imageData, dstData, filterOptions);
|
||||
ctx.putImageData(dstData,0,0);
|
||||
|
||||
console.log("Compression options", options);
|
||||
var tiledImage;
|
||||
if (preview) {
|
||||
var w = Math.round(width / TILESIZE);
|
||||
var h = Math.round(height / TILESIZE);
|
||||
|
||||
for (var y=0;y<h;y++) {
|
||||
for (var x=0;x<w;x++) {
|
||||
var imageData = ctx.getImageData(x*TILESIZE, y*TILESIZE, TILESIZE, TILESIZE);
|
||||
var rgba = imageData.data;
|
||||
options.rgbaOut = rgba;
|
||||
options.width = TILESIZE;
|
||||
options.height = TILESIZE;
|
||||
var imgstr = imageconverter.RGBAtoString(rgba, options);
|
||||
ctx.putImageData(imageData,x*TILESIZE, y*TILESIZE); // write preview
|
||||
/*var compress = 'require("heatshrink").decompress('
|
||||
if (!imgstr.startsWith(compress)) throw "Data in wrong format";
|
||||
imgstr = imgstr.slice(compress.length,-1);*/
|
||||
if (tiledImage) tiledImage += imgstr.substr(3); // skip header
|
||||
else tiledImage = imgstr; // for first image, keep the header
|
||||
}
|
||||
}
|
||||
}
|
||||
return [{
|
||||
name:mapImageFile,
|
||||
content:tiledImage
|
||||
}];
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Render everything to a canvas...
|
||||
var canvas = document.getElementById("maptiles");
|
||||
canvas.style.display="";
|
||||
var ctx = canvas.getContext('2d');
|
||||
|
||||
|
||||
var img = new Image();
|
||||
img.crossOrigin = "Anonymous";
|
||||
img.onload = function(){
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
ctx.drawImage(img,0,0);
|
||||
tilesLoaded(ctx, canvas.width, canvas.height, "");
|
||||
};
|
||||
img.src = "osm-test.png"
|
||||
|
||||
function update() {
|
||||
ctx.drawImage(img,0,0);
|
||||
tilesLoaded(ctx, canvas.width, canvas.height, "");
|
||||
}
|
||||
|
||||
document.getElementById("getmap").addEventListener("click", update);
|
||||
document.getElementById("slider_lo").addEventListener("change",update);
|
||||
document.getElementById("slider_hi").addEventListener("change",update);
|
||||
document.getElementById("finaldither").addEventListener("click",update);
|
||||
document.getElementById("sharpen").addEventListener("click",update);
|
||||
document.getElementById("dither").addEventListener("click",update);
|
||||
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
Binary file not shown.
After Width: | Height: | Size: 1.3 MiB |
|
@ -30,7 +30,7 @@ g.clear();
|
|||
layout.render();
|
||||
```
|
||||
|
||||
`layoutObject` has:
|
||||
`layoutObject` (first argument) has:
|
||||
|
||||
- A `type` field of:
|
||||
- `undefined` - blank, can be used for padding
|
||||
|
@ -53,9 +53,11 @@ layout.render();
|
|||
- A `pad` integer field to set pixels padding
|
||||
- A `fillx` int to choose if the object should fill available space in x. 0=no, 1=yes, 2=2x more space
|
||||
- A `filly` int to choose if the object should fill available space in y. 0=no, 1=yes, 2=2x more space
|
||||
- `width` and `height` fields to optionally specify minimum size options is an object containing:
|
||||
- `width` and `height` fields to optionally specify minimum size
|
||||
|
||||
|
||||
`options` (second argument) is an object containing:
|
||||
|
||||
`options` has:
|
||||
|
||||
- `lazy` - a boolean specifying whether to enable automatic lazy rendering
|
||||
- `btns` - array of objects containing:
|
||||
|
|
Loading…
Reference in New Issue